Implement a significantly more optimized biome lookup for surface rules
This commit is contained in:
parent
1289897004
commit
22915a91a1
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user