Skip to content

Commit 22915a9

Browse files
committed
Implement a significantly more optimized biome lookup for surface rules
1 parent 1289897 commit 22915a9

3 files changed

Lines changed: 316 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
2+
3+
import net.minecraft.world.level.biome.BiomeManager;
4+
import org.spongepowered.asm.mixin.Mixin;
5+
import org.spongepowered.asm.mixin.gen.Accessor;
6+
7+
@Mixin(BiomeManager.class)
8+
public interface BiomeManagerAccessor {
9+
@Accessor("biomeZoomSeed")
10+
long mfix$getZoomSeed();
11+
12+
@Accessor("noiseBiomeSource")
13+
BiomeManager.NoiseBiomeSource mfix$getBiomeSource();
14+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
2+
3+
import com.llamalad7.mixinextras.sugar.Local;
4+
import net.minecraft.core.BlockPos;
5+
import net.minecraft.core.Holder;
6+
import net.minecraft.core.Registry;
7+
import net.minecraft.world.level.biome.Biome;
8+
import net.minecraft.world.level.biome.BiomeManager;
9+
import net.minecraft.world.level.chunk.ChunkAccess;
10+
import net.minecraft.world.level.levelgen.NoiseChunk;
11+
import net.minecraft.world.level.levelgen.RandomState;
12+
import net.minecraft.world.level.levelgen.SurfaceRules;
13+
import net.minecraft.world.level.levelgen.SurfaceSystem;
14+
import net.minecraft.world.level.levelgen.WorldGenerationContext;
15+
import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup;
16+
import org.spongepowered.asm.mixin.Mixin;
17+
import org.spongepowered.asm.mixin.injection.At;
18+
import org.spongepowered.asm.mixin.injection.Inject;
19+
import org.spongepowered.asm.mixin.injection.ModifyArg;
20+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
21+
22+
import java.util.function.Function;
23+
24+
@Mixin(SurfaceSystem.class)
25+
public class SurfaceSystemMixin {
26+
private static final ThreadLocal<ChunkBiomeLookup> MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new);
27+
28+
@ModifyArg(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;<init>(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"), index = 4)
29+
private Function<BlockPos, Holder<Biome>> useFasterLookup(Function<BlockPos, Holder<Biome>> biomeGetter, @Local(ordinal = 0, argsOnly = true) BiomeManager manager, @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk) {
30+
var lookup = MFIX_LOOKUP_CACHE.get();
31+
BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager;
32+
lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager);
33+
return lookup;
34+
}
35+
36+
@Inject(method = "buildSurface", at = @At("RETURN"))
37+
private void disposeLookup(RandomState randomState, BiomeManager biomeManager, Registry<Biome> biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) {
38+
MFIX_LOOKUP_CACHE.get().dispose();
39+
}
40+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package org.embeddedt.modernfix.world.gen;
2+
3+
import net.minecraft.core.BlockPos;
4+
import net.minecraft.core.Holder;
5+
import net.minecraft.core.QuartPos;
6+
import net.minecraft.util.LinearCongruentialGenerator;
7+
import net.minecraft.util.Mth;
8+
import net.minecraft.world.level.biome.Biome;
9+
import net.minecraft.world.level.biome.BiomeManager;
10+
import net.minecraft.world.level.chunk.ChunkAccess;
11+
12+
import java.util.Arrays;
13+
import java.util.function.Function;
14+
15+
/**
16+
* Drop-in replacement for {@code biomeManager::getBiome} in SurfaceSystem.buildSurface.
17+
*
18+
* <p>Pre-computes the Voronoi bias (fiddle) values and quart-resolution biome data for an
19+
* entire chunk, then uses two optimizations:
20+
* <ul>
21+
* <li><b>Uniform check:</b> If all 8 Voronoi candidate cells for a given quart position
22+
* hold the same biome, the Voronoi computation is skipped entirely.</li>
23+
* <li><b>Pre-computed bias:</b> When the Voronoi is needed, the 48 LCG operations per block
24+
* are replaced by array lookups of pre-computed fiddle values.</li>
25+
* </ul>
26+
*/
27+
public class ChunkBiomeLookup implements Function<BlockPos, Holder<Biome>> {
28+
@SuppressWarnings("unchecked")
29+
private Holder<Biome>[] biomes = new Holder[0];
30+
private double[] biasX = new double[0], biasY = new double[0], biasZ = new double[0];
31+
private boolean[] uniform = new boolean[0];
32+
33+
private int qMinX, qMinY, qMinZ;
34+
private int sizeX, sizeY, sizeZ;
35+
36+
private BiomeManager fallbackManager;
37+
38+
/**
39+
* Pre-compute biome and bias data for the given chunk. Must be called before any
40+
* {@link #getBiome} calls for positions within this chunk.
41+
*
42+
* @param source the underlying quart-resolution biome source (e.g. the chunk)
43+
* @param biomeZoomSeed the obfuscated biome zoom seed from BiomeManager
44+
*/
45+
@SuppressWarnings("unchecked")
46+
public void prepare(BiomeManager.NoiseBiomeSource source, long biomeZoomSeed, ChunkAccess chunk, BiomeManager fallback) {
47+
int chunkMinX = chunk.getPos().getMinBlockX();
48+
int chunkMinZ = chunk.getPos().getMinBlockZ();
49+
int minBuildHeight = chunk.getMinBuildHeight();
50+
int maxBuildHeight = minBuildHeight + chunk.getHeight(); // exclusive
51+
52+
// BiomeManager.getBiome subtracts a 2-block offset before converting to quart coords,
53+
// then considers quart and quart+1 as the 8 Voronoi candidates.
54+
int biomeOffset = 2;
55+
int minBlockX = chunkMinX - biomeOffset;
56+
int maxBlockX = chunkMinX + 15 - biomeOffset;
57+
int minBlockZ = chunkMinZ - biomeOffset;
58+
int maxBlockZ = chunkMinZ + 15 - biomeOffset;
59+
int minBlockY = minBuildHeight - biomeOffset;
60+
int maxBlockY = maxBuildHeight - 1 - biomeOffset;
61+
62+
// Quart range: fromBlock(min) to fromBlock(max) + 1 (for the +1 Voronoi candidate)
63+
this.qMinX = QuartPos.fromBlock(minBlockX);
64+
int qMaxX = QuartPos.fromBlock(maxBlockX) + 1;
65+
this.qMinZ = QuartPos.fromBlock(minBlockZ);
66+
int qMaxZ = QuartPos.fromBlock(maxBlockZ) + 1;
67+
this.qMinY = QuartPos.fromBlock(minBlockY);
68+
int qMaxY = QuartPos.fromBlock(maxBlockY) + 1;
69+
70+
this.sizeX = qMaxX - qMinX + 1; // always 6 for 16-wide chunks
71+
this.sizeY = qMaxY - qMinY + 1;
72+
this.sizeZ = qMaxZ - qMinZ + 1;
73+
74+
int totalCells = sizeX * sizeY * sizeZ;
75+
76+
// Reuse arrays across chunks if large enough
77+
if (biomes.length < totalCells) {
78+
biomes = new Holder[totalCells];
79+
biasX = new double[totalCells];
80+
biasY = new double[totalCells];
81+
biasZ = new double[totalCells];
82+
uniform = new boolean[totalCells];
83+
}
84+
85+
// Fetch quart-resolution biomes
86+
boolean isSingleBiome = !fetchBiomes(source);
87+
88+
if (isSingleBiome) {
89+
// All cells hold the same biome, so no need to do expensive computations.
90+
Arrays.fill(uniform, 0, totalCells, true);
91+
} else {
92+
this.computeUniformity();
93+
this.computeBiases(biomeZoomSeed);
94+
}
95+
96+
this.fallbackManager = fallback;
97+
}
98+
99+
public void dispose() {
100+
// Make sure we do not retain strong references to the biome holders
101+
Arrays.fill(biomes, null);
102+
}
103+
104+
private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) {
105+
var biomes = this.biomes;
106+
Holder<Biome> firstSeen = null;
107+
boolean seenMultiple = false;
108+
for (int rx = 0; rx < sizeX; rx++) {
109+
int wx = qMinX + rx;
110+
for (int rz = 0; rz < sizeZ; rz++) {
111+
int wz = qMinZ + rz;
112+
for (int ry = 0; ry < sizeY; ry++) {
113+
int wy = qMinY + ry;
114+
var biome = source.getNoiseBiome(wx, wy, wz);
115+
biomes[index(rx, ry, rz)] = biome;
116+
if (biome != firstSeen) {
117+
if (firstSeen == null) {
118+
firstSeen = biome;
119+
} else {
120+
seenMultiple = true;
121+
}
122+
}
123+
}
124+
}
125+
}
126+
return seenMultiple;
127+
}
128+
129+
private void computeUniformity() {
130+
// For each quart position, check if all 8 Voronoi candidates hold the same biome.
131+
// If so, the Voronoi result is guaranteed to be that biome regardless of fractional
132+
// position, so we can skip the distance computation entirely.
133+
var uniform = this.uniform;
134+
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
135+
136+
for (int rx = 0; rx < sizeX - 1; rx++) {
137+
for (int rz = 0; rz < sizeZ - 1; rz++) {
138+
for (int ry = 0; ry < sizeY - 1; ry++) {
139+
uniform[index(rx, ry, rz)] = isUniform(rx, ry, rz);
140+
}
141+
}
142+
}
143+
}
144+
145+
private void computeBiases(long biomeZoomSeed) {
146+
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
147+
int qMinX = this.qMinX, qMinY = this.qMinY, qMinZ = this.qMinZ;
148+
149+
// Pre-compute bias (fiddle) values for the Voronoi distance computation.
150+
for (int rx = 0; rx < sizeX; rx++) {
151+
int wx = qMinX + rx;
152+
for (int rz = 0; rz < sizeZ; rz++) {
153+
int wz = qMinZ + rz;
154+
for (int ry = 0; ry < sizeY; ry++) {
155+
computeBias(index(rx, ry, rz), biomeZoomSeed, wx, qMinY + ry, wz);
156+
}
157+
}
158+
}
159+
}
160+
161+
private void computeBias(int idx, long seed, int x, int y, int z) {
162+
// Reproduces the LCG chain from BiomeManager.getFiddledDistance exactly
163+
long s = LinearCongruentialGenerator.next(seed, x);
164+
s = LinearCongruentialGenerator.next(s, y);
165+
s = LinearCongruentialGenerator.next(s, z);
166+
s = LinearCongruentialGenerator.next(s, x);
167+
s = LinearCongruentialGenerator.next(s, y);
168+
s = LinearCongruentialGenerator.next(s, z);
169+
biasX[idx] = getFiddle(s);
170+
s = LinearCongruentialGenerator.next(s, seed);
171+
biasY[idx] = getFiddle(s);
172+
s = LinearCongruentialGenerator.next(s, seed);
173+
biasZ[idx] = getFiddle(s);
174+
}
175+
176+
private static double getFiddle(long seed) {
177+
double d = (double) Math.floorMod(seed >> 24, 1024) / 1024.0D;
178+
return (d - 0.5D) * 0.9D;
179+
}
180+
181+
private boolean isUniform(int rx, int ry, int rz) {
182+
var biomes = this.biomes;
183+
Holder<Biome> ref = biomes[index(rx, ry, rz)];
184+
for (int dx = 0; dx <= 1; dx++) {
185+
for (int dy = 0; dy <= 1; dy++) {
186+
for (int dz = 0; dz <= 1; dz++) {
187+
if (biomes[index(rx + dx, ry + dy, rz + dz)] != ref) {
188+
return false;
189+
}
190+
}
191+
}
192+
}
193+
return true;
194+
}
195+
196+
private int index(int rx, int ry, int rz) {
197+
return (rx * sizeY + ry) * sizeZ + rz;
198+
}
199+
200+
@Override
201+
public Holder<Biome> apply(BlockPos pos) {
202+
return getBiome(pos);
203+
}
204+
205+
public Holder<Biome> getBiome(BlockPos pos) {
206+
int i = pos.getX() - 2;
207+
int j = pos.getY() - 2;
208+
int k = pos.getZ() - 2;
209+
210+
int rx = QuartPos.fromBlock(i) - qMinX;
211+
int ry = QuartPos.fromBlock(j) - qMinY;
212+
int rz = QuartPos.fromBlock(k) - qMinZ;
213+
214+
if (rx < 0 || rx >= sizeX - 1 || ry < 0 || ry >= sizeY - 1 || rz < 0 || rz >= sizeZ - 1) {
215+
return fallbackManager.getBiome(pos);
216+
}
217+
218+
int baseIdx = index(rx, ry, rz);
219+
if (uniform[baseIdx]) {
220+
return biomes[baseIdx];
221+
}
222+
223+
return getBiomeWithVoronoi(i, j, k, rx, ry, rz);
224+
}
225+
226+
private Holder<Biome> getBiomeWithVoronoi(int i, int j, int k, int rx, int ry, int rz) {
227+
var biasX = this.biasX;
228+
var biasY = this.biasY;
229+
var biasZ = this.biasZ;
230+
231+
double d0 = (double) QuartPos.quartLocal(i) / 4.0D;
232+
double d1 = (double) QuartPos.quartLocal(j) / 4.0D;
233+
double d2 = (double) QuartPos.quartLocal(k) / 4.0D;
234+
235+
int closestIdx = 0;
236+
double closestDist = Double.POSITIVE_INFINITY;
237+
238+
for (int c = 0; c < 8; c++) {
239+
boolean fx = (c & 4) == 0;
240+
boolean fy = (c & 2) == 0;
241+
boolean fz = (c & 1) == 0;
242+
243+
int idx = index(
244+
rx + (fx ? 0 : 1),
245+
ry + (fy ? 0 : 1),
246+
rz + (fz ? 0 : 1)
247+
);
248+
249+
double dx = (fx ? d0 : d0 - 1.0D) + biasX[idx];
250+
double dy = (fy ? d1 : d1 - 1.0D) + biasY[idx];
251+
double dz = (fz ? d2 : d2 - 1.0D) + biasZ[idx];
252+
double dist = Mth.square(dx) + Mth.square(dy) + Mth.square(dz);
253+
254+
if (dist < closestDist) {
255+
closestDist = dist;
256+
closestIdx = idx;
257+
}
258+
}
259+
260+
return biomes[closestIdx];
261+
}
262+
}

0 commit comments

Comments
 (0)