From b2990104d23bd27bd30972cc8be1dbd504f76fda Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 30 Oct 2026 20:54:55 -0400 Subject: [PATCH] Sped up hydrology sampling - Bounds sampling answers a column from its region, land or sea type, or noise-picked biome when every biome it can still become shares the group bounds - Base terrain heights are kept in a shared per-column memo for hydrology batches, terrain3D anchors and scalar sampling - Hydrology height batches skip columns whose base height is already known - The per-batch base height map and its thread local are removed - The four inferred biome streams share one context region stream - BakedStream exposes a baked-plane lookup without falling back to the source - Tests cover region constants, the base height memo and bit-exact heights with and without region constants --- .../iris/generation/runtime/IrisComplex.java | 245 +++++++++++++----- .../iris/generation/runtime/RegionBounds.java | 120 +++++++++ .../generation/runtime/SharedBaseHeights.java | 76 ++++++ .../runtime/SharedBoundsBiomes.java | 12 +- .../iris/generation/stream/BakedStream.java | 7 + .../runtime/IrisComplexBakeParityTest.java | 20 ++ .../IrisComplexBoundsBiomeCacheTest.java | 8 +- .../generation/runtime/RegionBoundsTest.java | 136 ++++++++++ .../runtime/SharedBaseHeightsTest.java | 87 +++++++ .../runtime/SharedBoundsBiomesTest.java | 2 +- 10 files changed, 635 insertions(+), 78 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java create mode 100644 core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java create mode 100644 core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java create mode 100644 core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index 4bb51ddaa..acec89aa9 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -91,7 +91,6 @@ import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider; import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.interpolation.Interpolated; -import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; @@ -120,10 +119,11 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) -@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) +@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); + private static final long NEGATIVE_ZERO_BITS = Double.doubleToRawLongBits(-0D); private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; @@ -146,7 +146,6 @@ public class IrisComplex implements DataProvider { Comparator.nullsFirst(Comparator.naturalOrder()) ); private static final ThreadLocal BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new); - private static final ThreadLocal HYDROLOGY_BASE_HEIGHTS = new ThreadLocal<>(); private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = { InferredType.LAND, InferredType.CAVE, @@ -160,6 +159,7 @@ public class IrisComplex implements DataProvider { @Getter(AccessLevel.NONE) private final int biomeBoundsSamplingStep; private transient volatile SharedCornerBounds sharedCornerBounds; + private transient volatile SharedBaseHeights sharedBaseHeights; private transient volatile SharedBoundsBiomes sharedBoundsBiomes; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) @@ -259,6 +259,10 @@ public class IrisComplex implements DataProvider { private IrisBiome focusBiome; private IrisRegion focusRegion; private Map> generatorBounds; + @Getter(AccessLevel.NONE) + private final transient RegionBounds regionBounds; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream contextRegionStream; private Set generatorBiomes; @Setter(AccessLevel.NONE) private final transient HydrologyRuntime hydrologyRuntime; @@ -386,6 +390,9 @@ public class IrisComplex implements DataProvider { frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators(); } generatorBounds = buildGeneratorBounds(engine); + regionBounds = focusedBiomes == null && !imageMapRuntime.has(IrisImageMapApplication.BIOME) + ? RegionBounds.compile(preparedRegions, frozenInterpolators.length, new BoundsReach(), this::groupBounds) + : null; biomeBuffetGenerators = compileBuffetGeneratorGroups(); chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() && (biomeBuffet == null || biomeBuffet.cells().isEmpty()); @@ -417,20 +424,21 @@ public class IrisComplex implements DataProvider { : RegionSelection.select(new RegionSelection.Options( engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)); regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource); - caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + contextRegionStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)); + caveSource = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE)) .convertAware2D(ProceduralStream::get); caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource); inferredStreams.put(InferredType.CAVE, caveBiomeStream); - landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + landBiomeStream = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.LAND)) .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.LAND, landBiomeStream); - seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + seaBiomeStream = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.SEA)) .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SEA, seaBiomeStream); - shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + shoreBiomeStream = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE)) .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); @@ -812,17 +820,35 @@ public class IrisComplex implements DataProvider { } return; } + SharedBaseHeights memo = sharedBaseHeights(); TerrainBatchScratch scratch = BATCH_SCRATCH.get(); int mark = scratch.mark(); int indexMark = scratch.indexMark(); try { double[] batchXs = scratch.buffer(count); double[] batchZs = scratch.buffer(count); + double[] computed = scratch.buffer(count); + int[] slots = scratch.indices(count); + int missing = 0; for (int index = 0; index < count; index++) { - batchXs[index] = xs[from + index]; - batchZs[index] = zs[from + index]; + int x = xs[from + index]; + int z = zs[from + index]; + double known = memo.get(x, z); + if (Double.isNaN(known)) { + batchXs[missing] = x; + batchZs[missing] = z; + slots[missing++] = index; + } else { + heights[index] = known; + } + } + if (missing > 0) { + batchBaseHeights(scratch, batchXs, batchZs, missing, computed); + for (int index = 0; index < missing; index++) { + heights[slots[index]] = computed[index]; + memo.put((int) batchXs[index], (int) batchZs[index], computed[index]); + } } - batchBaseHeights(scratch, batchXs, batchZs, count, heights); } finally { scratch.reset(mark); scratch.resetIndices(indexMark); @@ -830,23 +856,9 @@ public class IrisComplex implements DataProvider { if (baseOnly || terrain3D == null && transitionDisplacement == null) { return; } - Long2DoubleOpenHashMap known = new Long2DoubleOpenHashMap(count); - known.defaultReturnValue(Double.NaN); for (int index = 0; index < count; index++) { - known.put(hydrologyColumnKey(xs[from + index], zs[from + index]), heights[index]); + heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]); } - HYDROLOGY_BASE_HEIGHTS.set(new HydrologyBaseHeights(this, known)); - try { - for (int index = 0; index < count; index++) { - heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]); - } - } finally { - HYDROLOGY_BASE_HEIGHTS.remove(); - } - } - - private static long hydrologyColumnKey(int x, int z) { - return ((long) x << 32) | (z & 0xFFFFFFFFL); } private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) { @@ -1082,15 +1094,21 @@ public class IrisComplex implements DataProvider { } private double sampleUnblendedNaturalTerrainHeight(double x, double z) { - HydrologyBaseHeights batched = HYDROLOGY_BASE_HEIGHTS.get(); - if (batched != null && batched.owner() == this && (int) x == x && (int) z == z) { - double known = batched.heights().get(hydrologyColumnKey((int) x, (int) z)); - if (!Double.isNaN(known)) { - return known; - } + if (!memoColumn(x) || !memoColumn(z)) { + return imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z)); } - double proceduralHeight = proceduralTerrainHeight.sample(x, z); - return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); + SharedBaseHeights memo = sharedBaseHeights(); + double known = memo.get((int) x, (int) z); + if (!Double.isNaN(known)) { + return known; + } + double height = imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z)); + memo.put((int) x, (int) z, height); + return height; + } + + private static boolean memoColumn(double coordinate) { + return (int) coordinate == coordinate && Double.doubleToRawLongBits(coordinate) != NEGATIVE_ZERO_BITS; } static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) { @@ -1885,7 +1903,7 @@ public class IrisComplex implements DataProvider { int grid = biomeBoundsSamplingStep; GridBoundsCache cache = gridBoundsCache.get(); if (grid <= 1) { - return sampleBoundsRaw(cache, engine, interpolator, generators, x, z); + return sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, x, z); } int xi = (int) Math.floor(x); @@ -1941,7 +1959,7 @@ public class IrisComplex implements DataProvider { if (GenerationCounter.ENABLED) { GenerationCounter.CORNER_STARCASTS.increment(); } - packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz); + packed = computePackedBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz); if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) { // A non-finite corner is a transient sampling fault, not a fact about the terrain: // hand it back uncached so the next sample recomputes it instead of pinning it. @@ -1972,6 +1990,20 @@ public class IrisComplex implements DataProvider { return shared; } + private SharedBaseHeights sharedBaseHeights() { + SharedBaseHeights shared = sharedBaseHeights; + if (shared == null) { + synchronized (this) { + shared = sharedBaseHeights; + if (shared == null) { + shared = new SharedBaseHeights(SharedBaseHeights.capacityFor(Runtime.getRuntime().maxMemory())); + sharedBaseHeights = shared; + } + } + } + return shared; + } + private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream source) { SharedBoundsBiomes shared = sharedBoundsBiomes; if (shared != null && shared.source() == source) { @@ -1987,8 +2019,9 @@ public class IrisComplex implements DataProvider { } } - private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, int gx, int gz) { - NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz); + private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, + IrisGenerator[] generators, int gx, int gz) { + NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, gx, gz); return (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL); } @@ -2006,10 +2039,11 @@ public class IrisComplex implements DataProvider { return a + ((b - a) * fz); } - private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) { + private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, + IrisGenerator[] generators, double x, double z) { IdentityHashMap cachedBounds = generatorBounds.get(interpolator); BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler; - sampler.bind(this, engine, generators, cachedBounds); + sampler.bind(this, engine, interpolatorIndex, generators, cachedBounds); try { return interpolator.interpolateBounds(x, z, sampler); @@ -2297,6 +2331,11 @@ public class IrisComplex implements DataProvider { return reached; } + private NoiseBounds groupBounds(int group, IrisBiome biome) { + GeneratorBounds cached = generatorBounds.get(frozenInterpolators[group]).get(biome); + return (cached == null ? computeGeneratorBounds(frozenGenerators[group], biome) : cached).noiseBounds; + } + static double averageGeneratorHeights( IrisGenerator[] generators, double low, @@ -2604,6 +2643,32 @@ public class IrisComplex implements DataProvider { } } + private final class BoundsReach implements RegionBounds.Reach { + @Override + public List picks(IrisRegion region, InferredType type) { + return loadInferredBiomes(biomeKeys(region, type), type); + } + + @Override + public Collection descendants(IrisBiome biome) { + Set reached = Collections.newSetFromMap(new IdentityHashMap<>()); + ArrayDeque pending = new ArrayDeque<>(); + pending.add(biome); + while (!pending.isEmpty()) { + IrisBiome next = pending.poll(); + if (!reached.add(next)) { + continue; + } + for (IrisBiome child : next.getRealChildren(IrisComplex.this)) { + if (child != null) { + pending.add(child); + } + } + } + return reached; + } + } + /** * Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator * bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind} @@ -2619,9 +2684,12 @@ public class IrisComplex implements DataProvider { private final IdentityHashMap localBounds = new IdentityHashMap<>(8); private IrisComplex complex; private Engine engine; + private int group; private IrisGenerator[] generators; private IdentityHashMap cachedBounds; private SharedBoundsBiomes sharedBiomes; + private BakedStream bakedBiomes; + private RegionBounds regional; private boolean integerCoordinates; private boolean inUse; @@ -2629,14 +2697,19 @@ public class IrisComplex implements DataProvider { return inUse; } - private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap cachedBounds) { + private void bind(IrisComplex complex, Engine engine, int group, IrisGenerator[] generators, + IdentityHashMap cachedBounds) { this.complex = complex; this.engine = engine; + this.group = group; this.generators = generators; this.cachedBounds = cachedBounds; this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream || complex.baseBiomeStream instanceof CachedStream2D; this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null; + this.bakedBiomes = complex.regionBounds != null && complex.baseBiomeStream instanceof BakedStream baked + ? baked : null; + this.regional = bakedBiomes == null ? null : complex.regionBounds; this.inUse = true; if (sampleSource.get() != complex.baseBiomeStream) { sampleCache.clear(); @@ -2654,6 +2727,8 @@ public class IrisComplex implements DataProvider { generators = null; cachedBounds = null; sharedBiomes = null; + bakedBiomes = null; + regional = null; inUse = false; } @@ -2662,25 +2737,23 @@ public class IrisComplex implements DataProvider { try { double sampleX = integerCoordinates ? (int) xx : xx; double sampleZ = integerCoordinates ? (int) zz : zz; - IrisBiome bx = sampleCache.get(sampleX, sampleZ); - if (bx == null) { - bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ); - if (bx == null) { - ChunkBakeCache.enterSparse(); - try { - bx = complex.baseBiomeStream.get(xx, zz); - } finally { - ChunkBakeCache.exitSparse(); - } - if (sharedBiomes != null) { - sharedBiomes.put((int) sampleX, (int) sampleZ, bx); - } + Object known = sampleCache.get(sampleX, sampleZ); + if (known == null && sharedBiomes != null) { + known = sharedBiomes.get((int) sampleX, (int) sampleZ); + if (known != null) { + sampleCache.put(sampleX, sampleZ, known); } - sampleCache.put(sampleX, sampleZ, bx); } - - GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); - return bounds.noiseBounds; + if (known instanceof IrisBiome biome) { + return bounds(biome); + } + ChunkBakeCache.enterSparse(); + try { + return regional == null ? bounds(remember(sampleX, sampleZ, complex.baseBiomeStream.get(xx, zz))) + : regionalBounds(known, sampleX, sampleZ); + } finally { + ChunkBakeCache.exitSparse(); + } } catch (Throwable e) { GenerationFailures.rethrowEngineFailure(e); long now = System.currentTimeMillis(); @@ -2696,6 +2769,49 @@ public class IrisComplex implements DataProvider { return ZERO_NOISE_BOUNDS; } + + private NoiseBounds regionalBounds(Object known, double x, double z) { + RegionBounds.Pick pick = known instanceof RegionBounds.Pick picked ? picked : null; + IrisRegion region = pick != null ? pick.region() : known instanceof IrisRegion cached ? cached : null; + if (region == null) { + IrisBiome baked = bakedBiomes.baked((int) x, (int) z); + if (baked != null) { + return bounds(remember(x, z, baked)); + } + region = remember(x, z, complex.contextRegionStream.get(x, z)); + } + RegionBounds.Constants constants = regional.of(region); + NoiseBounds constant = constants == null ? null : constants.any(group); + if (constant != null) { + return constant; + } + InferredType type = pick != null ? pick.type() : complex.bridgeStream.get(x, z); + constant = constants == null ? null : constants.typed(type, group); + if (constant != null) { + return constant; + } + IrisBiome picked = pick != null ? pick.biome() : complex.createInferredBiomeStream(region, type).get(x, z); + constant = regional.pick(picked, group); + if (constant != null) { + if (pick == null) { + remember(x, z, new RegionBounds.Pick(region, type, picked)); + } + return constant; + } + return bounds(remember(x, z, complex.implode(picked, x, z))); + } + + private NoiseBounds bounds(IrisBiome biome) { + return complex.resolveGeneratorBounds(engine, generators, biome, cachedBounds, localBounds).noiseBounds; + } + + private T remember(double x, double z, T sample) { + if (sharedBiomes != null) { + sharedBiomes.put((int) x, (int) z, sample); + } + sampleCache.put(x, z, sample); + return sample; + } } /** @@ -2705,13 +2821,13 @@ public class IrisComplex implements DataProvider { private static final class CoordinateBiomeCache { private final long[] xBits; private final long[] zBits; - private final IrisBiome[] values; + private final Object[] values; private final int mask; private CoordinateBiomeCache(int capacity) { xBits = new long[capacity]; zBits = new long[capacity]; - values = new IrisBiome[capacity]; + values = new Object[capacity]; mask = capacity - 1; } @@ -2719,20 +2835,20 @@ public class IrisComplex implements DataProvider { Arrays.fill(values, null); } - private IrisBiome get(double x, double z) { + private Object get(double x, double z) { long xb = Double.doubleToLongBits(x); long zb = Double.doubleToLongBits(z); int slot = mix(xb, zb) & mask; return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null; } - private void put(double x, double z, IrisBiome biome) { + private void put(double x, double z, Object sample) { long xb = Double.doubleToLongBits(x); long zb = Double.doubleToLongBits(z); int slot = mix(xb, zb) & mask; xBits[slot] = xb; zBits[slot] = zb; - values[slot] = biome; + values[slot] = sample; } private int mix(long xb, long zb) { @@ -2784,7 +2900,4 @@ public class IrisComplex implements DataProvider { transitionDisplacement.clear(); } } - - private record HydrologyBaseHeights(IrisComplex owner, Long2DoubleOpenHashMap heights) { - } } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java b/core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java new file mode 100644 index 000000000..4fff46022 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java @@ -0,0 +1,120 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.terrain.InferredType; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.volmlib.util.interpolation.NoiseBounds; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class RegionBounds { + private final Map regions; + private final Map picks; + + private RegionBounds(Map regions, Map picks) { + this.regions = regions; + this.picks = picks; + } + + static RegionBounds compile(Collection regions, int groups, Reach reach, GroupBounds bounds) { + IdentityHashMap compiled = new IdentityHashMap<>(Math.max(16, regions.size() * 2)); + IdentityHashMap picks = new IdentityHashMap<>(); + for (IrisRegion region : regions) { + if (compiled.containsKey(region)) { + continue; + } + NoiseBounds[] land = typeConstants(reach.picks(region, InferredType.LAND), groups, reach, bounds, picks); + NoiseBounds[] sea = typeConstants(reach.picks(region, InferredType.SEA), groups, reach, bounds, picks); + NoiseBounds[] any = new NoiseBounds[groups]; + for (int group = 0; group < groups; group++) { + any[group] = land[group] != null && sea[group] != null && sameBits(land[group], sea[group]) ? land[group] : null; + } + compiled.put(region, new Constants(any, land, sea)); + } + return new RegionBounds(compiled, picks); + } + + Constants of(IrisRegion region) { + return regions.get(region); + } + + NoiseBounds pick(IrisBiome picked, int group) { + NoiseBounds[] constants = picks.get(picked); + return constants == null ? null : constants[group]; + } + + private static NoiseBounds[] typeConstants(List picked, int groups, Reach reach, GroupBounds bounds, + Map picks) { + if (picked.isEmpty()) { + return new NoiseBounds[groups]; + } + Set reached = Collections.newSetFromMap(new IdentityHashMap<>()); + for (IrisBiome biome : picked) { + Collection descendants = reach.descendants(biome); + reached.addAll(descendants); + if (!picks.containsKey(biome)) { + picks.put(biome, constants(descendants, groups, bounds)); + } + } + return constants(reached, groups, bounds); + } + + private static NoiseBounds[] constants(Collection biomes, int groups, GroupBounds bounds) { + NoiseBounds[] constants = new NoiseBounds[groups]; + for (int group = 0; group < groups; group++) { + constants[group] = constant(biomes, group, bounds); + } + return constants; + } + + private static NoiseBounds constant(Collection biomes, int group, GroupBounds bounds) { + NoiseBounds first = null; + for (IrisBiome biome : biomes) { + NoiseBounds next = bounds.of(group, biome); + if (first == null) { + first = next; + } else if (!sameBits(first, next)) { + return null; + } + } + return first; + } + + private static boolean sameBits(NoiseBounds first, NoiseBounds second) { + return Double.doubleToRawLongBits(first.min()) == Double.doubleToRawLongBits(second.min()) + && Double.doubleToRawLongBits(first.max()) == Double.doubleToRawLongBits(second.max()); + } + + record Constants(NoiseBounds[] any, NoiseBounds[] land, NoiseBounds[] sea) { + NoiseBounds any(int group) { + return any[group]; + } + + NoiseBounds typed(InferredType type, int group) { + return switch (type) { + case LAND -> land[group]; + case SEA -> sea[group]; + case SHORE, CAVE -> null; + }; + } + } + + record Pick(IrisRegion region, InferredType type, IrisBiome biome) { + } + + interface Reach { + List picks(IrisRegion region, InferredType type); + + Collection descendants(IrisBiome biome); + } + + @FunctionalInterface + interface GroupBounds { + NoiseBounds of(int group, IrisBiome biome); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java new file mode 100644 index 000000000..9ee1db996 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java @@ -0,0 +1,76 @@ +package art.arcane.iris.generation.runtime; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; + +final class SharedBaseHeights { + static final int MINIMUM_CAPACITY = 1 << 16; + static final int MAXIMUM_CAPACITY = 1 << 21; + static final long BYTES_PER_ENTRY = 20L; + static final long HEAP_SHARE = 256L; + private static final VarHandle INTS = MethodHandles.arrayElementVarHandle(int[].class); + private static final VarHandle LONGS = MethodHandles.arrayElementVarHandle(long[].class); + + private final long[] keys; + private final long[] values; + private final int[] stamps; + private final int mask; + + SharedBaseHeights(int capacity) { + if (capacity < 2 || Integer.bitCount(capacity) != 1) { + throw new IllegalArgumentException("capacity must be a power of two of at least 2"); + } + this.keys = new long[capacity]; + this.values = new long[capacity]; + this.stamps = new int[capacity]; + this.mask = capacity - 1; + } + + static int capacityFor(long maxMemory) { + long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY; + long bounded = Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries)); + return Integer.highestOneBit((int) bounded); + } + + double get(int x, int z) { + long key = key(x, z); + int slot = slot(key); + int before = (int) INTS.getAcquire(stamps, slot); + if (before == 0 || (before & 1) != 0) { + return Double.NaN; + } + long storedKey = (long) LONGS.getAcquire(keys, slot); + long value = (long) LONGS.getAcquire(values, slot); + VarHandle.loadLoadFence(); + int after = (int) INTS.getAcquire(stamps, slot); + if (before != after || storedKey != key) { + return Double.NaN; + } + return Double.longBitsToDouble(value); + } + + void put(int x, int z, double height) { + if (!Double.isFinite(height)) { + return; + } + long key = key(x, z); + int slot = slot(key); + int stamp = (int) INTS.getAcquire(stamps, slot); + if ((stamp & 1) != 0 || !INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) { + return; + } + LONGS.setRelease(keys, slot, key); + LONGS.setRelease(values, slot, Double.doubleToRawLongBits(height)); + INTS.setRelease(stamps, slot, stamp + 2); + } + + private int slot(long key) { + long mixed = (key ^ (key >>> 30)) * 0xBF58476D1CE4E5B9L; + mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL; + return (int) ((mixed ^ (mixed >>> 31)) & mask); + } + + private static long key(int x, int z) { + return ((long) x << 32) | (z & 0xFFFFFFFFL); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java index 3c3fa5ee4..2ebcc725a 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java @@ -1,7 +1,5 @@ package art.arcane.iris.generation.runtime; -import art.arcane.iris.generation.biome.IrisBiome; - import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.Objects; @@ -49,7 +47,7 @@ final class SharedBoundsBiomes { return (int) ((mixed ^ (mixed >>> 31)) & mask); } - IrisBiome get(int x, int z) { + Object get(int x, int z) { long key = key(x, z); int slot = slot(x, z); int before = (int) INTS.getAcquire(stamps, slot); @@ -63,11 +61,11 @@ final class SharedBoundsBiomes { if (before != after || storedKey != key) { return null; } - return (IrisBiome) value; + return value; } - void put(int x, int z, IrisBiome biome) { - if (biome == null) { + void put(int x, int z, Object sample) { + if (sample == null) { return; } int slot = slot(x, z); @@ -76,7 +74,7 @@ final class SharedBoundsBiomes { return; } LONGS.setRelease(keys, slot, key(x, z)); - OBJECTS.setRelease(values, slot, biome); + OBJECTS.setRelease(values, slot, sample); INTS.setRelease(stamps, slot, stamp + 2); } diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java index 9f0f922ed..1b528c4c8 100644 --- a/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java @@ -39,6 +39,13 @@ public final class BakedStream extends BasicStream implements ChunkFillabl return value; } + @SuppressWarnings("unchecked") + public T baked(int blockX, int blockZ) { + int index = ((blockZ & 15) << 4) | (blockX & 15); + ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4); + return values != null && values.ready(index) ? (T) values.objectAt(index) : null; + } + @Override public T get(double x, double y, double z) { return scalar.get(x, y, z); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java index f455cd6a5..7f143089c 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java @@ -22,11 +22,13 @@ import org.junit.rules.TemporaryFolder; import org.mockito.MockedStatic; import org.mockito.Mockito; +import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.util.SplittableRandom; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.ArgumentMatchers.any; @@ -81,6 +83,24 @@ public class IrisComplexBakeParityTest { } } + @Test + public void regionConstantBoundsEqualFullBiomeBoundsBitForBit() throws Exception { + try (Statics ignored = new Statics(); Fixture shortcut = new Fixture(pack()); Fixture full = new Fixture(pack())) { + Field regionBounds = IrisComplex.class.getDeclaredField("regionBounds"); + regionBounds.setAccessible(true); + assertNotNull(regionBounds.get(shortcut.complex)); + regionBounds.set(full.complex, null); + SplittableRandom random = new SplittableRandom(0x5EEDL); + for (int column = 0; column < 2048; column++) { + int x = random.nextInt(-6400, 6400); + int z = random.nextInt(-6400, 6400); + assertEquals("column " + x + "," + z, + Double.doubleToRawLongBits(full.complex.getBaseTerrainHeightStream().getDouble(x, z)), + Double.doubleToRawLongBits(shortcut.complex.getBaseTerrainHeightStream().getDouble(x, z))); + } + } + } + @Test public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception { try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) { diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java index 401c1b533..6cf44f763 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java @@ -243,7 +243,7 @@ public class IrisComplexBoundsBiomeCacheTest { constructor.setAccessible(true); Object cache = constructor.newInstance(4096); Method get = type.getDeclaredMethod("get", double.class, double.class); - Method put = type.getDeclaredMethod("put", double.class, double.class, IrisBiome.class); + Method put = type.getDeclaredMethod("put", double.class, double.class, Object.class); get.setAccessible(true); put.setAccessible(true); IrisBiome first = new IrisBiome(); @@ -266,7 +266,7 @@ public class IrisComplexBoundsBiomeCacheTest { } Field values = type.getDeclaredField("values"); values.setAccessible(true); - assertEquals(4096, ((IrisBiome[]) values.get(cache)).length); + assertEquals(4096, ((Object[]) values.get(cache)).length); } private static ProceduralStream constant(IrisBiome biome) { @@ -295,7 +295,7 @@ public class IrisComplexBoundsBiomeCacheTest { constructor.setAccessible(true); cache = constructor.newInstance(); sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class, - IrisInterpolator.class, IrisGenerator[].class, double.class, double.class); + IrisInterpolator.class, int.class, IrisGenerator[].class, double.class, double.class); sample.setAccessible(true); boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds") .getDeclaredConstructor(double.class, double.class); @@ -313,7 +313,7 @@ public class IrisComplexBoundsBiomeCacheTest { private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z) throws ReflectiveOperationException { - return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, new IrisGenerator[0], x, z); + return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, 0, new IrisGenerator[0], x, z); } private Object newGridCache() throws ReflectiveOperationException { diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java new file mode 100644 index 000000000..f4168db0d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java @@ -0,0 +1,136 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.terrain.InferredType; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.volmlib.util.interpolation.NoiseBounds; +import org.junit.Test; + +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class RegionBoundsTest { + private static final NoiseBounds FLAT = new NoiseBounds(0D, 0D); + private static final NoiseBounds HILLS = new NoiseBounds(4D, 30D); + + @Test + public void groupsEveryReachableBiomeAgreesOnAreConstantForTheRegion() { + IrisBiome plains = new IrisBiome(); + IrisBiome forest = new IrisBiome(); + IrisBiome ocean = new IrisBiome(); + IrisRegion region = new IrisRegion(); + Map bounds = new IdentityHashMap<>(); + bounds.put(plains, new NoiseBounds[]{FLAT, HILLS}); + bounds.put(forest, new NoiseBounds[]{new NoiseBounds(0D, 0D), new NoiseBounds(2D, 30D)}); + bounds.put(ocean, new NoiseBounds[]{FLAT, HILLS}); + + RegionBounds compiled = RegionBounds.compile(List.of(region), 2, + new Fixed(Map.of(InferredType.LAND, List.of(plains, forest), InferredType.SEA, List.of(ocean)), Map.of()), + (group, biome) -> bounds.get(biome)[group]); + RegionBounds.Constants constants = compiled.of(region); + + assertBits(FLAT, constants.any(0)); + assertBits(FLAT, constants.typed(InferredType.LAND, 0)); + assertBits(FLAT, constants.typed(InferredType.SEA, 0)); + assertNull(constants.any(1)); + assertNull(constants.typed(InferredType.LAND, 1)); + assertSame(HILLS, constants.typed(InferredType.SEA, 1)); + assertSame(HILLS, compiled.pick(plains, 1)); + assertBits(new NoiseBounds(2D, 30D), compiled.pick(forest, 1)); + } + + @Test + public void picksAreConstantOnlyWhenEveryDescendantAgrees() { + IrisBiome steppe = new IrisBiome(); + IrisBiome meadow = new IrisBiome(); + IrisBiome mesa = new IrisBiome(); + IrisBiome canyon = new IrisBiome(); + IrisRegion region = new IrisRegion(); + Map bounds = new IdentityHashMap<>(); + bounds.put(steppe, FLAT); + bounds.put(meadow, new NoiseBounds(0D, 0D)); + bounds.put(mesa, FLAT); + bounds.put(canyon, HILLS); + + RegionBounds compiled = RegionBounds.compile(List.of(region), 1, + new Fixed(Map.of(InferredType.LAND, List.of(steppe, mesa), InferredType.SEA, List.of()), + descendants(steppe, List.of(steppe, meadow), mesa, List.of(mesa, canyon))), + (group, biome) -> bounds.get(biome)); + + assertNull(compiled.of(region).typed(InferredType.LAND, 0)); + assertSame(FLAT, compiled.pick(steppe, 0)); + assertNull(compiled.pick(mesa, 0)); + assertNull(compiled.pick(canyon, 0)); + } + + @Test + public void landAndSeaConstantsOnlyMergeWhenTheirBitsMatch() { + IrisBiome beach = new IrisBiome(); + IrisBiome reef = new IrisBiome(); + IrisRegion region = new IrisRegion(); + Map bounds = new IdentityHashMap<>(); + bounds.put(beach, new NoiseBounds(0D, 0D)); + bounds.put(reef, new NoiseBounds(-0D, 0D)); + + RegionBounds.Constants constants = RegionBounds.compile(List.of(region), 1, + new Fixed(Map.of(InferredType.LAND, List.of(beach), InferredType.SEA, List.of(reef)), Map.of()), + (group, biome) -> bounds.get(biome)).of(region); + + assertNull(constants.any(0)); + assertSame(bounds.get(beach), constants.typed(InferredType.LAND, 0)); + assertSame(bounds.get(reef), constants.typed(InferredType.SEA, 0)); + } + + @Test + public void emptyBiomeListsAndUnknownRegionsHaveNoConstant() { + IrisRegion region = new IrisRegion(); + IrisBiome only = new IrisBiome(); + RegionBounds compiled = RegionBounds.compile(List.of(region), 1, + new Fixed(Map.of(InferredType.LAND, List.of(only), InferredType.SEA, List.of()), Map.of()), + (group, biome) -> FLAT); + RegionBounds.Constants constants = compiled.of(region); + + assertSame(FLAT, constants.typed(InferredType.LAND, 0)); + assertNull(constants.typed(InferredType.SEA, 0)); + assertNull(constants.any(0)); + assertNull(constants.typed(InferredType.SHORE, 0)); + assertNull(constants.typed(InferredType.CAVE, 0)); + assertNull(compiled.of(new IrisRegion())); + assertNull(compiled.of(null)); + assertNull(compiled.pick(new IrisBiome(), 0)); + assertNull(compiled.pick(null, 0)); + } + + private static Map> descendants(IrisBiome first, List firstDescendants, + IrisBiome second, List secondDescendants) { + Map> descendants = new IdentityHashMap<>(); + descendants.put(first, firstDescendants); + descendants.put(second, secondDescendants); + return descendants; + } + + private static void assertBits(NoiseBounds expected, NoiseBounds actual) { + assertEquals(Double.doubleToRawLongBits(expected.min()), Double.doubleToRawLongBits(actual.min())); + assertEquals(Double.doubleToRawLongBits(expected.max()), Double.doubleToRawLongBits(actual.max())); + } + + private record Fixed(Map> picks, Map> descendants) + implements RegionBounds.Reach { + @Override + public List picks(IrisRegion region, InferredType type) { + return picks.get(type); + } + + @Override + public Collection descendants(IrisBiome biome) { + List known = descendants.get(biome); + return known == null ? List.of(biome) : known; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java new file mode 100644 index 000000000..d9914c18c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java @@ -0,0 +1,87 @@ +package art.arcane.iris.generation.runtime; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SharedBaseHeightsTest { + @Test + public void storedColumnsComeBackWithTheirExactBits() { + SharedBaseHeights cache = new SharedBaseHeights(1 << 10); + double height = Math.nextUp(71.25D); + + assertTrue(Double.isNaN(cache.get(12, -40))); + cache.put(12, -40, height); + + assertEquals(Double.doubleToRawLongBits(height), Double.doubleToRawLongBits(cache.get(12, -40))); + assertTrue(Double.isNaN(cache.get(-40, 12))); + } + + @Test + public void originMissesUntilItIsStored() { + SharedBaseHeights cache = new SharedBaseHeights(2); + + assertTrue(Double.isNaN(cache.get(0, 0))); + cache.put(0, 0, -0D); + assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(cache.get(0, 0))); + } + + @Test + public void nonFiniteHeightsAreNeverStored() { + SharedBaseHeights cache = new SharedBaseHeights(2); + cache.put(3, 4, 64D); + cache.put(3, 4, Double.NaN); + cache.put(3, 4, Double.POSITIVE_INFINITY); + + assertEquals(64D, cache.get(3, 4), 0D); + } + + @Test + public void fullCoordinateDomainKeepsDistinctColumns() { + int[] coordinates = {0, 1, -1, 1 << 21, -(1 << 21), 30_000_000, -30_000_000, + Integer.MIN_VALUE, Integer.MAX_VALUE}; + for (int x : coordinates) { + for (int z : coordinates) { + SharedBaseHeights cache = new SharedBaseHeights(2); + cache.put(x, z, 12D); + for (int otherX : coordinates) { + for (int otherZ : coordinates) { + if (x == otherX && z == otherZ) { + assertEquals(12D, cache.get(otherX, otherZ), 0D); + } else { + assertTrue(Double.isNaN(cache.get(otherX, otherZ))); + } + } + } + } + } + } + + @Test + public void aCollidingColumnReplacesTheOldOne() { + SharedBaseHeights cache = new SharedBaseHeights(2); + cache.put(5, 9, 40D); + cache.put(6, 9, 41D); + cache.put(7, 9, 42D); + + int stored = 0; + for (int x = 5; x <= 7; x++) { + double value = cache.get(x, 9); + if (!Double.isNaN(value)) { + assertEquals(35D + x, value, 0D); + stored++; + } + } + assertTrue(stored >= 1 && stored <= 2); + } + + @Test + public void capacityFollowsTheHeapWithinBounds() { + assertEquals(SharedBaseHeights.MINIMUM_CAPACITY, SharedBaseHeights.capacityFor(0L)); + assertEquals(SharedBaseHeights.MAXIMUM_CAPACITY, SharedBaseHeights.capacityFor(Long.MAX_VALUE)); + int capacity = SharedBaseHeights.capacityFor(6L << 30); + assertEquals(1, Integer.bitCount(capacity)); + assertTrue(capacity >= SharedBaseHeights.MINIMUM_CAPACITY && capacity <= SharedBaseHeights.MAXIMUM_CAPACITY); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java index b1f152f5c..3b9c21fb1 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java @@ -146,7 +146,7 @@ public class SharedBoundsBiomesTest { int xi = (int) ((state & Long.MAX_VALUE) % coordinates.length); int zi = (int) ((state >>> 10) % coordinates.length); cache.put(coordinates[xi], coordinates[zi], biomes[xi][zi]); - IrisBiome actual = cache.get(coordinates[xi], coordinates[zi]); + Object actual = cache.get(coordinates[xi], coordinates[zi]); if (actual != null) { hits.incrementAndGet(); if (actual != biomes[xi][zi]) {