Merge branch 'perf-dem' into perf-review

This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 21:29:13 -04:00
commit b10966af47
10 files changed
+635 -78

No files matched your search

@@ -91,7 +91,6 @@ import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider;
import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated; import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -120,10 +119,11 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@Data @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"}) @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", "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", "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 { public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); 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 AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
@@ -146,7 +146,6 @@ public class IrisComplex implements DataProvider {
Comparator.nullsFirst(Comparator.naturalOrder()) Comparator.nullsFirst(Comparator.naturalOrder())
); );
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new); private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
private static final ThreadLocal<HydrologyBaseHeights> HYDROLOGY_BASE_HEIGHTS = new ThreadLocal<>();
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = { private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
InferredType.LAND, InferredType.LAND,
InferredType.CAVE, InferredType.CAVE,
@@ -160,6 +159,7 @@ public class IrisComplex implements DataProvider {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final int biomeBoundsSamplingStep; private final int biomeBoundsSamplingStep;
private transient volatile SharedCornerBounds sharedCornerBounds; private transient volatile SharedCornerBounds sharedCornerBounds;
private transient volatile SharedBaseHeights sharedBaseHeights;
private transient volatile SharedBoundsBiomes sharedBoundsBiomes; private transient volatile SharedBoundsBiomes sharedBoundsBiomes;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@@ -259,6 +259,10 @@ public class IrisComplex implements DataProvider {
private IrisBiome focusBiome; private IrisBiome focusBiome;
private IrisRegion focusRegion; private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds; private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
@Getter(AccessLevel.NONE)
private final transient RegionBounds regionBounds;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisRegion> contextRegionStream;
private Set<IrisBiome> generatorBiomes; private Set<IrisBiome> generatorBiomes;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private final transient HydrologyRuntime hydrologyRuntime; private final transient HydrologyRuntime hydrologyRuntime;
@@ -386,6 +390,9 @@ public class IrisComplex implements DataProvider {
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators(); frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
} }
generatorBounds = buildGeneratorBounds(engine); generatorBounds = buildGeneratorBounds(engine);
regionBounds = focusedBiomes == null && !imageMapRuntime.has(IrisImageMapApplication.BIOME)
? RegionBounds.compile(preparedRegions, frozenInterpolators.length, new BoundsReach(), this::groupBounds)
: null;
biomeBuffetGenerators = compileBuffetGeneratorGroups(); biomeBuffetGenerators = compileBuffetGeneratorGroups();
chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty()); && (biomeBuffet == null || biomeBuffet.cells().isEmpty());
@@ -417,20 +424,21 @@ public class IrisComplex implements DataProvider {
: RegionSelection.select(new RegionSelection.Options( : RegionSelection.select(new RegionSelection.Options(
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)); engine.getDimension(), data, regionStyleStream, imageMapRuntime, null));
regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource); caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
inferredStreams.put(InferredType.CAVE, caveBiomeStream); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.LAND, landBiomeStream); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SEA, seaBiomeStream); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream); inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
@@ -812,17 +820,35 @@ public class IrisComplex implements DataProvider {
} }
return; return;
} }
SharedBaseHeights memo = sharedBaseHeights();
TerrainBatchScratch scratch = BATCH_SCRATCH.get(); TerrainBatchScratch scratch = BATCH_SCRATCH.get();
int mark = scratch.mark(); int mark = scratch.mark();
int indexMark = scratch.indexMark(); int indexMark = scratch.indexMark();
try { try {
double[] batchXs = scratch.buffer(count); double[] batchXs = scratch.buffer(count);
double[] batchZs = 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++) { for (int index = 0; index < count; index++) {
batchXs[index] = xs[from + index]; int x = xs[from + index];
batchZs[index] = zs[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 { } finally {
scratch.reset(mark); scratch.reset(mark);
scratch.resetIndices(indexMark); scratch.resetIndices(indexMark);
@@ -830,23 +856,9 @@ public class IrisComplex implements DataProvider {
if (baseOnly || terrain3D == null && transitionDisplacement == null) { if (baseOnly || terrain3D == null && transitionDisplacement == null) {
return; return;
} }
Long2DoubleOpenHashMap known = new Long2DoubleOpenHashMap(count);
known.defaultReturnValue(Double.NaN);
for (int index = 0; index < count; index++) { 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) { 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) { private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
HydrologyBaseHeights batched = HYDROLOGY_BASE_HEIGHTS.get(); if (!memoColumn(x) || !memoColumn(z)) {
if (batched != null && batched.owner() == this && (int) x == x && (int) z == z) { return imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z));
double known = batched.heights().get(hydrologyColumnKey((int) x, (int) z));
if (!Double.isNaN(known)) {
return known;
}
} }
double proceduralHeight = proceduralTerrainHeight.sample(x, z); SharedBaseHeights memo = sharedBaseHeights();
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); 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) { static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
@@ -1885,7 +1903,7 @@ public class IrisComplex implements DataProvider {
int grid = biomeBoundsSamplingStep; int grid = biomeBoundsSamplingStep;
GridBoundsCache cache = gridBoundsCache.get(); GridBoundsCache cache = gridBoundsCache.get();
if (grid <= 1) { 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); int xi = (int) Math.floor(x);
@@ -1941,7 +1959,7 @@ public class IrisComplex implements DataProvider {
if (GenerationCounter.ENABLED) { if (GenerationCounter.ENABLED) {
GenerationCounter.CORNER_STARCASTS.increment(); 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))) { if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
// A non-finite corner is a transient sampling fault, not a fact about the terrain: // 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. // 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; 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<IrisBiome> source) { private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream<IrisBiome> source) {
SharedBoundsBiomes shared = sharedBoundsBiomes; SharedBoundsBiomes shared = sharedBoundsBiomes;
if (shared != null && shared.source() == source) { 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) { private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex,
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz); 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); 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); 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<IrisBiome, GeneratorBounds> cachedBounds = generatorBounds.get(interpolator); IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds = generatorBounds.get(interpolator);
BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler; BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler;
sampler.bind(this, engine, generators, cachedBounds); sampler.bind(this, engine, interpolatorIndex, generators, cachedBounds);
try { try {
return interpolator.interpolateBounds(x, z, sampler); return interpolator.interpolateBounds(x, z, sampler);
@@ -2297,6 +2331,11 @@ public class IrisComplex implements DataProvider {
return reached; 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( static double averageGeneratorHeights(
IrisGenerator[] generators, IrisGenerator[] generators,
double low, double low,
@@ -2604,6 +2643,32 @@ public class IrisComplex implements DataProvider {
} }
} }
private final class BoundsReach implements RegionBounds.Reach {
@Override
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
return loadInferredBiomes(biomeKeys(region, type), type);
}
@Override
public Collection<IrisBiome> descendants(IrisBiome biome) {
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
ArrayDeque<IrisBiome> 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 * 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} * bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
@@ -2619,9 +2684,12 @@ public class IrisComplex implements DataProvider {
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8); private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
private IrisComplex complex; private IrisComplex complex;
private Engine engine; private Engine engine;
private int group;
private IrisGenerator[] generators; private IrisGenerator[] generators;
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds; private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
private SharedBoundsBiomes sharedBiomes; private SharedBoundsBiomes sharedBiomes;
private BakedStream<IrisBiome> bakedBiomes;
private RegionBounds regional;
private boolean integerCoordinates; private boolean integerCoordinates;
private boolean inUse; private boolean inUse;
@@ -2629,14 +2697,19 @@ public class IrisComplex implements DataProvider {
return inUse; return inUse;
} }
private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) { private void bind(IrisComplex complex, Engine engine, int group, IrisGenerator[] generators,
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) {
this.complex = complex; this.complex = complex;
this.engine = engine; this.engine = engine;
this.group = group;
this.generators = generators; this.generators = generators;
this.cachedBounds = cachedBounds; this.cachedBounds = cachedBounds;
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?> this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|| complex.baseBiomeStream instanceof CachedStream2D<?>; || complex.baseBiomeStream instanceof CachedStream2D<?>;
this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null; this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null;
this.bakedBiomes = complex.regionBounds != null && complex.baseBiomeStream instanceof BakedStream<IrisBiome> baked
? baked : null;
this.regional = bakedBiomes == null ? null : complex.regionBounds;
this.inUse = true; this.inUse = true;
if (sampleSource.get() != complex.baseBiomeStream) { if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear(); sampleCache.clear();
@@ -2654,6 +2727,8 @@ public class IrisComplex implements DataProvider {
generators = null; generators = null;
cachedBounds = null; cachedBounds = null;
sharedBiomes = null; sharedBiomes = null;
bakedBiomes = null;
regional = null;
inUse = false; inUse = false;
} }
@@ -2662,25 +2737,23 @@ public class IrisComplex implements DataProvider {
try { try {
double sampleX = integerCoordinates ? (int) xx : xx; double sampleX = integerCoordinates ? (int) xx : xx;
double sampleZ = integerCoordinates ? (int) zz : zz; double sampleZ = integerCoordinates ? (int) zz : zz;
IrisBiome bx = sampleCache.get(sampleX, sampleZ); Object known = sampleCache.get(sampleX, sampleZ);
if (bx == null) { if (known == null && sharedBiomes != null) {
bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ); known = sharedBiomes.get((int) sampleX, (int) sampleZ);
if (bx == null) { if (known != null) {
ChunkBakeCache.enterSparse(); sampleCache.put(sampleX, sampleZ, known);
try {
bx = complex.baseBiomeStream.get(xx, zz);
} finally {
ChunkBakeCache.exitSparse();
}
if (sharedBiomes != null) {
sharedBiomes.put((int) sampleX, (int) sampleZ, bx);
}
} }
sampleCache.put(sampleX, sampleZ, bx);
} }
if (known instanceof IrisBiome biome) {
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); return bounds(biome);
return bounds.noiseBounds; }
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) { } catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e); GenerationFailures.rethrowEngineFailure(e);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -2696,6 +2769,49 @@ public class IrisComplex implements DataProvider {
return ZERO_NOISE_BOUNDS; 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> 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 static final class CoordinateBiomeCache {
private final long[] xBits; private final long[] xBits;
private final long[] zBits; private final long[] zBits;
private final IrisBiome[] values; private final Object[] values;
private final int mask; private final int mask;
private CoordinateBiomeCache(int capacity) { private CoordinateBiomeCache(int capacity) {
xBits = new long[capacity]; xBits = new long[capacity];
zBits = new long[capacity]; zBits = new long[capacity];
values = new IrisBiome[capacity]; values = new Object[capacity];
mask = capacity - 1; mask = capacity - 1;
} }
@@ -2719,20 +2835,20 @@ public class IrisComplex implements DataProvider {
Arrays.fill(values, null); Arrays.fill(values, null);
} }
private IrisBiome get(double x, double z) { private Object get(double x, double z) {
long xb = Double.doubleToLongBits(x); long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z); long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask; int slot = mix(xb, zb) & mask;
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null; 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 xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z); long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask; int slot = mix(xb, zb) & mask;
xBits[slot] = xb; xBits[slot] = xb;
zBits[slot] = zb; zBits[slot] = zb;
values[slot] = biome; values[slot] = sample;
} }
private int mix(long xb, long zb) { private int mix(long xb, long zb) {
@@ -2784,7 +2900,4 @@ public class IrisComplex implements DataProvider {
transitionDisplacement.clear(); transitionDisplacement.clear();
} }
} }
private record HydrologyBaseHeights(IrisComplex owner, Long2DoubleOpenHashMap heights) {
}
} }
@@ -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<IrisRegion, Constants> regions;
private final Map<IrisBiome, NoiseBounds[]> picks;
private RegionBounds(Map<IrisRegion, Constants> regions, Map<IrisBiome, NoiseBounds[]> picks) {
this.regions = regions;
this.picks = picks;
}
static RegionBounds compile(Collection<IrisRegion> regions, int groups, Reach reach, GroupBounds bounds) {
IdentityHashMap<IrisRegion, Constants> compiled = new IdentityHashMap<>(Math.max(16, regions.size() * 2));
IdentityHashMap<IrisBiome, NoiseBounds[]> 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<IrisBiome> picked, int groups, Reach reach, GroupBounds bounds,
Map<IrisBiome, NoiseBounds[]> picks) {
if (picked.isEmpty()) {
return new NoiseBounds[groups];
}
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
for (IrisBiome biome : picked) {
Collection<IrisBiome> 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<IrisBiome> 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<IrisBiome> 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<IrisBiome> picks(IrisRegion region, InferredType type);
Collection<IrisBiome> descendants(IrisBiome biome);
}
@FunctionalInterface
interface GroupBounds {
NoiseBounds of(int group, IrisBiome biome);
}
}
@@ -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);
}
}
@@ -1,7 +1,5 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.util.Objects; import java.util.Objects;
@@ -49,7 +47,7 @@ final class SharedBoundsBiomes {
return (int) ((mixed ^ (mixed >>> 31)) & mask); return (int) ((mixed ^ (mixed >>> 31)) & mask);
} }
IrisBiome get(int x, int z) { Object get(int x, int z) {
long key = key(x, z); long key = key(x, z);
int slot = slot(x, z); int slot = slot(x, z);
int before = (int) INTS.getAcquire(stamps, slot); int before = (int) INTS.getAcquire(stamps, slot);
@@ -63,11 +61,11 @@ final class SharedBoundsBiomes {
if (before != after || storedKey != key) { if (before != after || storedKey != key) {
return null; return null;
} }
return (IrisBiome) value; return value;
} }
void put(int x, int z, IrisBiome biome) { void put(int x, int z, Object sample) {
if (biome == null) { if (sample == null) {
return; return;
} }
int slot = slot(x, z); int slot = slot(x, z);
@@ -76,7 +74,7 @@ final class SharedBoundsBiomes {
return; return;
} }
LONGS.setRelease(keys, slot, key(x, z)); LONGS.setRelease(keys, slot, key(x, z));
OBJECTS.setRelease(values, slot, biome); OBJECTS.setRelease(values, slot, sample);
INTS.setRelease(stamps, slot, stamp + 2); INTS.setRelease(stamps, slot, stamp + 2);
} }
@@ -39,6 +39,13 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
return value; 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 @Override
public T get(double x, double y, double z) { public T get(double x, double y, double z) {
return scalar.get(x, y, z); return scalar.get(x, y, z);
@@ -22,11 +22,13 @@ import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.lang.reflect.Field;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.SplittableRandom; import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any; 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 @Test
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception { public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) { try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
@@ -243,7 +243,7 @@ public class IrisComplexBoundsBiomeCacheTest {
constructor.setAccessible(true); constructor.setAccessible(true);
Object cache = constructor.newInstance(4096); Object cache = constructor.newInstance(4096);
Method get = type.getDeclaredMethod("get", double.class, double.class); 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); get.setAccessible(true);
put.setAccessible(true); put.setAccessible(true);
IrisBiome first = new IrisBiome(); IrisBiome first = new IrisBiome();
@@ -266,7 +266,7 @@ public class IrisComplexBoundsBiomeCacheTest {
} }
Field values = type.getDeclaredField("values"); Field values = type.getDeclaredField("values");
values.setAccessible(true); values.setAccessible(true);
assertEquals(4096, ((IrisBiome[]) values.get(cache)).length); assertEquals(4096, ((Object[]) values.get(cache)).length);
} }
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) { private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
@@ -295,7 +295,7 @@ public class IrisComplexBoundsBiomeCacheTest {
constructor.setAccessible(true); constructor.setAccessible(true);
cache = constructor.newInstance(); cache = constructor.newInstance();
sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class, 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); sample.setAccessible(true);
boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds") boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds")
.getDeclaredConstructor(double.class, double.class); .getDeclaredConstructor(double.class, double.class);
@@ -313,7 +313,7 @@ public class IrisComplexBoundsBiomeCacheTest {
private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z) private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z)
throws ReflectiveOperationException { 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 { private Object newGridCache() throws ReflectiveOperationException {
@@ -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<IrisBiome, NoiseBounds[]> 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<IrisBiome, NoiseBounds> 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<IrisBiome, NoiseBounds> 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<IrisBiome, List<IrisBiome>> descendants(IrisBiome first, List<IrisBiome> firstDescendants,
IrisBiome second, List<IrisBiome> secondDescendants) {
Map<IrisBiome, List<IrisBiome>> 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<InferredType, List<IrisBiome>> picks, Map<IrisBiome, List<IrisBiome>> descendants)
implements RegionBounds.Reach {
@Override
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
return picks.get(type);
}
@Override
public Collection<IrisBiome> descendants(IrisBiome biome) {
List<IrisBiome> known = descendants.get(biome);
return known == null ? List.of(biome) : known;
}
}
}
@@ -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);
}
}
@@ -146,7 +146,7 @@ public class SharedBoundsBiomesTest {
int xi = (int) ((state & Long.MAX_VALUE) % coordinates.length); int xi = (int) ((state & Long.MAX_VALUE) % coordinates.length);
int zi = (int) ((state >>> 10) % coordinates.length); int zi = (int) ((state >>> 10) % coordinates.length);
cache.put(coordinates[xi], coordinates[zi], biomes[xi][zi]); 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) { if (actual != null) {
hits.incrementAndGet(); hits.incrementAndGet();
if (actual != biomes[xi][zi]) { if (actual != biomes[xi][zi]) {