Implement a significantly more optimized biome lookup for surface rules

This commit is contained in:
embeddedt 2026-03-14 19:44:42 -04:00
parent 1289897004
commit 22915a91a1
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
3 changed files with 316 additions and 0 deletions

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import net.minecraft.world.level.biome.BiomeManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(BiomeManager.class)
public interface BiomeManagerAccessor {
@Accessor("biomeZoomSeed")
long mfix$getZoomSeed();
@Accessor("noiseBiomeSource")
BiomeManager.NoiseBiomeSource mfix$getBiomeSource();
}

View File

@ -0,0 +1,40 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.NoiseChunk;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.SurfaceRules;
import net.minecraft.world.level.levelgen.SurfaceSystem;
import net.minecraft.world.level.levelgen.WorldGenerationContext;
import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Function;
@Mixin(SurfaceSystem.class)
public class SurfaceSystemMixin {
private static final ThreadLocal<ChunkBiomeLookup> MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new);
@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)
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) {
var lookup = MFIX_LOOKUP_CACHE.get();
BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager;
lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager);
return lookup;
}
@Inject(method = "buildSurface", at = @At("RETURN"))
private void disposeLookup(RandomState randomState, BiomeManager biomeManager, Registry<Biome> biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) {
MFIX_LOOKUP_CACHE.get().dispose();
}
}

View File

@ -0,0 +1,262 @@
package org.embeddedt.modernfix.world.gen;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.util.LinearCongruentialGenerator;
import net.minecraft.util.Mth;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import java.util.Arrays;
import java.util.function.Function;
/**
* Drop-in replacement for {@code biomeManager::getBiome} in SurfaceSystem.buildSurface.
*
* <p>Pre-computes the Voronoi bias (fiddle) values and quart-resolution biome data for an
* entire chunk, then uses two optimizations:
* <ul>
* <li><b>Uniform check:</b> If all 8 Voronoi candidate cells for a given quart position
* hold the same biome, the Voronoi computation is skipped entirely.</li>
* <li><b>Pre-computed bias:</b> When the Voronoi is needed, the 48 LCG operations per block
* are replaced by array lookups of pre-computed fiddle values.</li>
* </ul>
*/
public class ChunkBiomeLookup implements Function<BlockPos, Holder<Biome>> {
@SuppressWarnings("unchecked")
private Holder<Biome>[] biomes = new Holder[0];
private double[] biasX = new double[0], biasY = new double[0], biasZ = new double[0];
private boolean[] uniform = new boolean[0];
private int qMinX, qMinY, qMinZ;
private int sizeX, sizeY, sizeZ;
private BiomeManager fallbackManager;
/**
* Pre-compute biome and bias data for the given chunk. Must be called before any
* {@link #getBiome} calls for positions within this chunk.
*
* @param source the underlying quart-resolution biome source (e.g. the chunk)
* @param biomeZoomSeed the obfuscated biome zoom seed from BiomeManager
*/
@SuppressWarnings("unchecked")
public void prepare(BiomeManager.NoiseBiomeSource source, long biomeZoomSeed, ChunkAccess chunk, BiomeManager fallback) {
int chunkMinX = chunk.getPos().getMinBlockX();
int chunkMinZ = chunk.getPos().getMinBlockZ();
int minBuildHeight = chunk.getMinBuildHeight();
int maxBuildHeight = minBuildHeight + chunk.getHeight(); // exclusive
// BiomeManager.getBiome subtracts a 2-block offset before converting to quart coords,
// then considers quart and quart+1 as the 8 Voronoi candidates.
int biomeOffset = 2;
int minBlockX = chunkMinX - biomeOffset;
int maxBlockX = chunkMinX + 15 - biomeOffset;
int minBlockZ = chunkMinZ - biomeOffset;
int maxBlockZ = chunkMinZ + 15 - biomeOffset;
int minBlockY = minBuildHeight - biomeOffset;
int maxBlockY = maxBuildHeight - 1 - biomeOffset;
// Quart range: fromBlock(min) to fromBlock(max) + 1 (for the +1 Voronoi candidate)
this.qMinX = QuartPos.fromBlock(minBlockX);
int qMaxX = QuartPos.fromBlock(maxBlockX) + 1;
this.qMinZ = QuartPos.fromBlock(minBlockZ);
int qMaxZ = QuartPos.fromBlock(maxBlockZ) + 1;
this.qMinY = QuartPos.fromBlock(minBlockY);
int qMaxY = QuartPos.fromBlock(maxBlockY) + 1;
this.sizeX = qMaxX - qMinX + 1; // always 6 for 16-wide chunks
this.sizeY = qMaxY - qMinY + 1;
this.sizeZ = qMaxZ - qMinZ + 1;
int totalCells = sizeX * sizeY * sizeZ;
// Reuse arrays across chunks if large enough
if (biomes.length < totalCells) {
biomes = new Holder[totalCells];
biasX = new double[totalCells];
biasY = new double[totalCells];
biasZ = new double[totalCells];
uniform = new boolean[totalCells];
}
// Fetch quart-resolution biomes
boolean isSingleBiome = !fetchBiomes(source);
if (isSingleBiome) {
// All cells hold the same biome, so no need to do expensive computations.
Arrays.fill(uniform, 0, totalCells, true);
} else {
this.computeUniformity();
this.computeBiases(biomeZoomSeed);
}
this.fallbackManager = fallback;
}
public void dispose() {
// Make sure we do not retain strong references to the biome holders
Arrays.fill(biomes, null);
}
private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) {
var biomes = this.biomes;
Holder<Biome> firstSeen = null;
boolean seenMultiple = false;
for (int rx = 0; rx < sizeX; rx++) {
int wx = qMinX + rx;
for (int rz = 0; rz < sizeZ; rz++) {
int wz = qMinZ + rz;
for (int ry = 0; ry < sizeY; ry++) {
int wy = qMinY + ry;
var biome = source.getNoiseBiome(wx, wy, wz);
biomes[index(rx, ry, rz)] = biome;
if (biome != firstSeen) {
if (firstSeen == null) {
firstSeen = biome;
} else {
seenMultiple = true;
}
}
}
}
}
return seenMultiple;
}
private void computeUniformity() {
// For each quart position, check if all 8 Voronoi candidates hold the same biome.
// If so, the Voronoi result is guaranteed to be that biome regardless of fractional
// position, so we can skip the distance computation entirely.
var uniform = this.uniform;
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
for (int rx = 0; rx < sizeX - 1; rx++) {
for (int rz = 0; rz < sizeZ - 1; rz++) {
for (int ry = 0; ry < sizeY - 1; ry++) {
uniform[index(rx, ry, rz)] = isUniform(rx, ry, rz);
}
}
}
}
private void computeBiases(long biomeZoomSeed) {
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
int qMinX = this.qMinX, qMinY = this.qMinY, qMinZ = this.qMinZ;
// Pre-compute bias (fiddle) values for the Voronoi distance computation.
for (int rx = 0; rx < sizeX; rx++) {
int wx = qMinX + rx;
for (int rz = 0; rz < sizeZ; rz++) {
int wz = qMinZ + rz;
for (int ry = 0; ry < sizeY; ry++) {
computeBias(index(rx, ry, rz), biomeZoomSeed, wx, qMinY + ry, wz);
}
}
}
}
private void computeBias(int idx, long seed, int x, int y, int z) {
// Reproduces the LCG chain from BiomeManager.getFiddledDistance exactly
long s = LinearCongruentialGenerator.next(seed, x);
s = LinearCongruentialGenerator.next(s, y);
s = LinearCongruentialGenerator.next(s, z);
s = LinearCongruentialGenerator.next(s, x);
s = LinearCongruentialGenerator.next(s, y);
s = LinearCongruentialGenerator.next(s, z);
biasX[idx] = getFiddle(s);
s = LinearCongruentialGenerator.next(s, seed);
biasY[idx] = getFiddle(s);
s = LinearCongruentialGenerator.next(s, seed);
biasZ[idx] = getFiddle(s);
}
private static double getFiddle(long seed) {
double d = (double) Math.floorMod(seed >> 24, 1024) / 1024.0D;
return (d - 0.5D) * 0.9D;
}
private boolean isUniform(int rx, int ry, int rz) {
var biomes = this.biomes;
Holder<Biome> ref = biomes[index(rx, ry, rz)];
for (int dx = 0; dx <= 1; dx++) {
for (int dy = 0; dy <= 1; dy++) {
for (int dz = 0; dz <= 1; dz++) {
if (biomes[index(rx + dx, ry + dy, rz + dz)] != ref) {
return false;
}
}
}
}
return true;
}
private int index(int rx, int ry, int rz) {
return (rx * sizeY + ry) * sizeZ + rz;
}
@Override
public Holder<Biome> apply(BlockPos pos) {
return getBiome(pos);
}
public Holder<Biome> getBiome(BlockPos pos) {
int i = pos.getX() - 2;
int j = pos.getY() - 2;
int k = pos.getZ() - 2;
int rx = QuartPos.fromBlock(i) - qMinX;
int ry = QuartPos.fromBlock(j) - qMinY;
int rz = QuartPos.fromBlock(k) - qMinZ;
if (rx < 0 || rx >= sizeX - 1 || ry < 0 || ry >= sizeY - 1 || rz < 0 || rz >= sizeZ - 1) {
return fallbackManager.getBiome(pos);
}
int baseIdx = index(rx, ry, rz);
if (uniform[baseIdx]) {
return biomes[baseIdx];
}
return getBiomeWithVoronoi(i, j, k, rx, ry, rz);
}
private Holder<Biome> getBiomeWithVoronoi(int i, int j, int k, int rx, int ry, int rz) {
var biasX = this.biasX;
var biasY = this.biasY;
var biasZ = this.biasZ;
double d0 = (double) QuartPos.quartLocal(i) / 4.0D;
double d1 = (double) QuartPos.quartLocal(j) / 4.0D;
double d2 = (double) QuartPos.quartLocal(k) / 4.0D;
int closestIdx = 0;
double closestDist = Double.POSITIVE_INFINITY;
for (int c = 0; c < 8; c++) {
boolean fx = (c & 4) == 0;
boolean fy = (c & 2) == 0;
boolean fz = (c & 1) == 0;
int idx = index(
rx + (fx ? 0 : 1),
ry + (fy ? 0 : 1),
rz + (fz ? 0 : 1)
);
double dx = (fx ? d0 : d0 - 1.0D) + biasX[idx];
double dy = (fy ? d1 : d1 - 1.0D) + biasY[idx];
double dz = (fz ? d2 : d2 - 1.0D) + biasZ[idx];
double dist = Mth.square(dx) + Mth.square(dy) + Mth.square(dz);
if (dist < closestDist) {
closestDist = dist;
closestIdx = idx;
}
}
return biomes[closestIdx];
}
}