From 1acda886b31728dba464ecd632b56bf233900a17 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 23:22:16 -0400 Subject: [PATCH] Memoized bake planes - Planes fill lazily per column with ready bits and batch only missing columns on full-chunk requests - Natural fills batch only the base heights Terrain3D still needs and reuse the sparse memo - Bounds lattice and hydrology planner reads stay out of the bake - Bake capacity is eight times the noise cache, covering the measured working set - Generator evaluation counters and a bake census print from the probe - Parity test compares batched chunk base heights and evaluation counts with the scalar path --- .../iris/generation/bake/BakePlane.java | 30 ++- .../iris/generation/bake/ChunkBakeCache.java | 138 ++++------ .../iris/generation/bake/ChunkPlanes.java | 148 +++++++++-- .../generation/bake/GeneratorEvaluations.java | 48 ++++ .../iris/generation/bake/PlaneSource.java | 4 +- .../iris/generation/bake/TerrainProgram.java | 18 +- .../iris/generation/runtime/IrisComplex.java | 240 ++++++++++++------ .../generation/stream/BakedDoubleStream.java | 33 +-- .../generation/stream/BakedHeightStream.java | 39 ++- .../iris/generation/stream/BakedStream.java | 34 +-- .../runtime/IrisComplexBakeParityTest.java | 204 +++++++++++++++ .../arcane/iris/probe/GenerationProbe.java | 21 +- 12 files changed, 666 insertions(+), 291 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java create mode 100644 core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java b/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java index 3d7f0cd33..a915f3222 100644 --- a/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java @@ -1,16 +1,26 @@ package art.arcane.iris.generation.bake; public enum BakePlane { - REGION, - BASE_BIOME, - BASE_HEIGHT, - NATURAL_HEIGHT, - NATURAL_TRUE_BIOME, - HEIGHT, - TRUE_BIOME, - ROCK, - FLUID, - CAVE_BIOME; + REGION(false), + BASE_BIOME(false), + BASE_HEIGHT(true), + NATURAL_HEIGHT(true), + NATURAL_TRUE_BIOME(false), + HEIGHT(true), + TRUE_BIOME(false), + ROCK(false), + FLUID(false), + CAVE_BIOME(false); static final BakePlane[] PLANES = values(); + + private final boolean doubles; + + BakePlane(boolean doubles) { + this.doubles = doubles; + } + + public boolean doubles() { + return doubles; + } } diff --git a/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java b/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java index 0dd59ca33..2344e03fe 100644 --- a/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java +++ b/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java @@ -1,29 +1,25 @@ package art.arcane.iris.generation.bake; import art.arcane.iris.generation.cache.ConcurrentClockCache; +import art.arcane.iris.generation.stream.ProvisionalSampling; import java.util.Objects; -import java.util.function.DoubleSupplier; import java.util.function.Supplier; public final class ChunkBakeCache implements BakeCache { - private static final ThreadLocal DEPTH = ThreadLocal.withInitial(() -> new int[2]); - private static final int BAKING = 0; - private static final int SPARSE = 1; + private static final ThreadLocal SPARSE = ThreadLocal.withInitial(() -> new int[1]); private final PlaneSource source; - private final PlaneSource tracked; private volatile ConcurrentClockCache entries; public ChunkBakeCache(PlaneSource source, int capacityChunks) { this.source = Objects.requireNonNull(source, "Bake plane source"); - this.tracked = new TrackedSource(source); this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks)); } @Override public ColumnBake get(int chunkX, int chunkZ) { - return planes(chunkX, chunkZ).columnBake(tracked); + return planes(chunkX, chunkZ).columnBake(source); } @Override @@ -50,6 +46,19 @@ public final class ChunkBakeCache implements BakeCache { entries.clear(); } + public long[] census() { + long[] counts = new long[BakePlane.PLANES.length + 1]; + entries.forEach(planes -> { + counts[BakePlane.PLANES.length]++; + for (BakePlane plane : BakePlane.PLANES) { + if (planes.planeIfPresent(plane) != null) { + counts[plane.ordinal()]++; + } + } + }); + return counts; + } + public ChunkPlanes planes(int chunkX, int chunkZ) { ConcurrentClockCache current = entries; long key = key(chunkX, chunkZ); @@ -61,38 +70,35 @@ public final class ChunkBakeCache implements BakeCache { return entries.get(key(chunkX, chunkZ)); } - public Object bake(BakePlane plane, int chunkX, int chunkZ) { - return planes(chunkX, chunkZ).plane(plane, tracked); - } - - public Object read(BakePlane plane, int chunkX, int chunkZ) { + public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) { ChunkPlanes planes = entries.get(key(chunkX, chunkZ)); - if (planes != null) { - Object published = planes.published(plane); - if (published != null) { - return published; - } - } - if (!denseReadsAllowed()) { - return null; - } - return (planes != null ? planes : planes(chunkX, chunkZ)).plane(plane, tracked); + return planes == null ? null : planes.planeIfPresent(plane); } - public boolean denseReadsAllowed() { - int[] depth = DEPTH.get(); - if (depth[SPARSE] > 0 || source.mainThread()) { - return false; + public ChunkPlanes.Plane plane(BakePlane plane, int chunkX, int chunkZ) { + return planes(chunkX, chunkZ).plane(plane); + } + + public Object complete(BakePlane plane, int chunkX, int chunkZ) { + return planes(chunkX, chunkZ).complete(plane, source); + } + + public boolean memoAllowed() { + return SPARSE.get()[0] == 0 && !source.mainThread(); + } + + public void storeDouble(BakePlane plane, int blockX, int blockZ, double value, long revision) { + if (revision != ProvisionalSampling.revision()) { + return; } - return depth[BAKING] > 0 || source.generationContext(); + planes(blockX >> 4, blockZ >> 4).plane(plane).storeDouble(((blockZ & 15) << 4) | (blockX & 15), value); } - public boolean bakingAllowed() { - return !inSparseScope() && !source.mainThread(); - } - - public static boolean inSparseScope() { - return DEPTH.get()[SPARSE] > 0; + public void storeObject(BakePlane plane, int blockX, int blockZ, Object value, long revision) { + if (revision != ProvisionalSampling.revision()) { + return; + } + planes(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value); } public static T sparse(Supplier action) { @@ -104,73 +110,19 @@ public final class ChunkBakeCache implements BakeCache { } } - public static double sparseDouble(DoubleSupplier action) { - enterSparse(); - try { - return action.getAsDouble(); - } finally { - exitSparse(); - } - } - - public static void sparse(Runnable action) { - enterSparse(); - try { - action.run(); - } finally { - exitSparse(); - } - } - public static void enterSparse() { - DEPTH.get()[SPARSE]++; + SPARSE.get()[0]++; } public static void exitSparse() { - DEPTH.get()[SPARSE]--; + SPARSE.get()[0]--; + } + + public static boolean inSparseScope() { + return SPARSE.get()[0] > 0; } public static long key(int chunkX, int chunkZ) { return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); } - - private static final class TrackedSource implements PlaneSource { - private final PlaneSource delegate; - - private TrackedSource(PlaneSource delegate) { - this.delegate = delegate; - } - - @Override - public Object compute(BakePlane plane, int chunkX, int chunkZ) { - int[] depth = DEPTH.get(); - depth[BAKING]++; - try { - return delegate.compute(plane, chunkX, chunkZ); - } finally { - depth[BAKING]--; - } - } - - @Override - public ColumnBake assemble(ChunkPlanes planes) { - int[] depth = DEPTH.get(); - depth[BAKING]++; - try { - return delegate.assemble(planes); - } finally { - depth[BAKING]--; - } - } - - @Override - public boolean mainThread() { - return delegate.mainThread(); - } - - @Override - public boolean generationContext() { - return delegate.generationContext(); - } - } } diff --git a/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java b/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java index bacc3b9d1..6a981f9c5 100644 --- a/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java +++ b/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java @@ -2,12 +2,17 @@ package art.arcane.iris.generation.bake; import art.arcane.iris.generation.stream.ProvisionalSampling; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; public final class ChunkPlanes { + private static final VarHandle READY = MethodHandles.arrayElementVarHandle(long[].class); + private final int chunkX; private final int chunkZ; - private final AtomicReferenceArray planes = new AtomicReferenceArray<>(BakePlane.PLANES.length); + private final AtomicReferenceArray planes = new AtomicReferenceArray<>(BakePlane.PLANES.length); private volatile ColumnBake columnBake; ChunkPlanes(int chunkX, int chunkZ) { @@ -23,40 +28,50 @@ public final class ChunkPlanes { return chunkZ; } - public Object published(BakePlane plane) { - Object state = planes.getAcquire(plane.ordinal()); - return state instanceof Pending ? null : state; + public Plane planeIfPresent(BakePlane kind) { + return planes.getAcquire(kind.ordinal()); } - Object plane(BakePlane plane, PlaneSource source) { - int slot = plane.ordinal(); + public Object published(BakePlane kind) { + Plane plane = planes.getAcquire(kind.ordinal()); + return plane != null && plane.complete ? plane.values : null; + } + + public Plane plane(BakePlane kind) { + int slot = kind.ordinal(); + Plane plane = planes.getAcquire(slot); + if (plane != null) { + return plane; + } + Plane created = new Plane(kind); + Plane witness = planes.compareAndExchangeRelease(slot, null, created); + return witness == null ? created : witness; + } + + Object complete(BakePlane kind, PlaneSource source) { + Plane plane = plane(kind); while (true) { - Object state = planes.getAcquire(slot); - if (state != null && !(state instanceof Pending)) { - return state; + if (plane.complete) { + return plane.values; } - if (state instanceof Pending pending) { - if (pending.owner == Thread.currentThread()) { - return source.compute(plane, chunkX, chunkZ); + Pending running = plane.filling.get(); + if (running != null) { + if (running.owner == Thread.currentThread()) { + return plane.unpublishedFill(source, chunkX, chunkZ); } - pending.await(); + running.await(); continue; } Pending pending = new Pending(Thread.currentThread()); - if (!planes.compareAndSet(slot, null, pending)) { + if (!plane.filling.compareAndSet(null, pending)) { continue; } - Object computed = null; - boolean publishable = false; try { - long revision = ProvisionalSampling.revision(); - computed = source.compute(plane, chunkX, chunkZ); - publishable = revision == ProvisionalSampling.revision(); + return plane.fill(source, chunkX, chunkZ); } finally { - planes.setRelease(slot, publishable ? computed : null); + plane.filling.set(null); pending.complete(); } - return computed; } } @@ -77,6 +92,97 @@ public final class ChunkPlanes { return bake; } + public static final class Plane { + private final BakePlane kind; + private final Object values; + private final long[] ready = new long[4]; + private final AtomicReference filling = new AtomicReference<>(); + private volatile boolean complete; + + private Plane(BakePlane kind) { + this.kind = kind; + this.values = kind.doubles() ? new double[256] : new Object[256]; + } + + public boolean ready(int index) { + return complete || ((long) READY.getAcquire(ready, index >>> 6) & (1L << (index & 63))) != 0L; + } + + public double doubleAt(int index) { + return ((double[]) values)[index]; + } + + public Object objectAt(int index) { + return ((Object[]) values)[index]; + } + + public void storeDouble(int index, double value) { + ((double[]) values)[index] = value; + markReady(index); + } + + public void storeObject(int index, Object value) { + ((Object[]) values)[index] = value; + markReady(index); + } + + private void markReady(int index) { + READY.getAndBitwiseOrRelease(ready, index >>> 6, 1L << (index & 63)); + } + + private long[] missing() { + long[] missing = new long[4]; + for (int word = 0; word < 4; word++) { + missing[word] = ~(long) READY.getAcquire(ready, word); + } + return missing; + } + + private Object fill(PlaneSource source, int chunkX, int chunkZ) { + long[] missing = missing(); + Object computed = kind.doubles() ? new double[256] : new Object[256]; + long revision = ProvisionalSampling.revision(); + source.fill(kind, chunkX, chunkZ, missing, computed); + if (revision != ProvisionalSampling.revision()) { + return merge(missing, computed); + } + for (int index = 0; index < 256; index++) { + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + if (kind.doubles()) { + ((double[]) values)[index] = ((double[]) computed)[index]; + } else { + ((Object[]) values)[index] = ((Object[]) computed)[index]; + } + } + } + for (int word = 0; word < 4; word++) { + READY.setRelease(ready, word, -1L); + } + complete = true; + return values; + } + + private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) { + long[] missing = missing(); + Object computed = kind.doubles() ? new double[256] : new Object[256]; + source.fill(kind, chunkX, chunkZ, missing, computed); + return merge(missing, computed); + } + + private Object merge(long[] missing, Object computed) { + for (int index = 0; index < 256; index++) { + if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) { + if (kind.doubles()) { + ((double[]) computed)[index] = ((double[]) values)[index]; + } else { + ((Object[]) computed)[index] = ((Object[]) values)[index]; + } + } + } + return computed; + } + } + private static final class Pending { private final Thread owner; private boolean done; diff --git a/core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java b/core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java new file mode 100644 index 000000000..7466bed71 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java @@ -0,0 +1,48 @@ +package art.arcane.iris.generation.bake; + +import java.util.concurrent.atomic.LongAdder; + +public final class GeneratorEvaluations { + private static final LongAdder BAKED_CHUNKS = new LongAdder(); + private static final LongAdder BAKED = new LongAdder(); + private static final LongAdder BATCHED = new LongAdder(); + private static final LongAdder SCALAR = new LongAdder(); + + private GeneratorEvaluations() { + } + + public static void bakedChunk(long evaluations) { + BAKED_CHUNKS.increment(); + BAKED.add(evaluations); + } + + public static void batched(long evaluations) { + BATCHED.add(evaluations); + } + + public static void scalar(int evaluations) { + SCALAR.add(evaluations); + } + + public static Snapshot snapshot() { + return new Snapshot(BAKED_CHUNKS.sum(), BAKED.sum(), BATCHED.sum(), SCALAR.sum()); + } + + public static void reset() { + BAKED_CHUNKS.reset(); + BAKED.reset(); + BATCHED.reset(); + SCALAR.reset(); + } + + public record Snapshot(long bakedChunks, long bakedEvaluations, long batchedEvaluations, long scalarEvaluations) { + public double bakedEvaluationsPerChunk() { + return bakedChunks == 0L ? 0D : (double) bakedEvaluations / bakedChunks; + } + + public Snapshot minus(Snapshot earlier) { + return new Snapshot(bakedChunks - earlier.bakedChunks, bakedEvaluations - earlier.bakedEvaluations, + batchedEvaluations - earlier.batchedEvaluations, scalarEvaluations - earlier.scalarEvaluations); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java b/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java index afb13bc36..5e98ffd08 100644 --- a/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java +++ b/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java @@ -1,11 +1,9 @@ package art.arcane.iris.generation.bake; public interface PlaneSource { - Object compute(BakePlane plane, int chunkX, int chunkZ); + void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values); ColumnBake assemble(ChunkPlanes planes); boolean mainThread(); - - boolean generationContext(); } diff --git a/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java index 8dc834e65..b631c660c 100644 --- a/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java +++ b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java @@ -6,7 +6,6 @@ import art.arcane.iris.generation.noise.TerrainBatchScratch; import art.arcane.volmlib.util.math.M; import java.util.Arrays; -import java.util.concurrent.atomic.LongAdder; public final class TerrainProgram { private static final int SKIPPED = 0; @@ -16,7 +15,6 @@ public final class TerrainProgram { private final IrisGenerator[][] generators; private final GeneratorProgram[][] programs; private final long heightSeed; - private final LongAdder evaluations = new LongAdder(); private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) { this.generators = generators; @@ -44,19 +42,11 @@ public final class TerrainProgram { return generators[group].length; } - public long evaluations() { - return evaluations.sum(); - } - - public void countScalarEvaluations(int count) { - evaluations.add(count); - } - - public void accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count, + public long accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] lows, double[] highs, double[] heights) { IrisGenerator[] members = generators[group]; if (members.length == 0 || count == 0) { - return; + return 0L; } int mark = scratch.mark(); int indexMark = scratch.indexMark(); @@ -87,7 +77,7 @@ public final class TerrainProgram { } if (activeCount == 0) { scratch.resetIndices(indexMark); - return; + return 0L; } double[] activeX = scratch.buffer(activeCount); double[] activeZ = scratch.buffer(activeCount); @@ -129,8 +119,8 @@ public final class TerrainProgram { for (int index = 0; index < activeCount; index++) { heights[activeIndex[index]] += sums[index] / members.length; } - evaluations.add((long) activeCount * members.length); scratch.reset(mark); scratch.resetIndices(indexMark); + return (long) activeCount * members.length; } } 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 52c6b8c4f..027e2c8e6 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 @@ -23,6 +23,7 @@ import art.arcane.iris.generation.bake.BakeRegistry; import art.arcane.iris.generation.bake.ChunkBakeCache; import art.arcane.iris.generation.bake.ChunkPlanes; import art.arcane.iris.generation.bake.ColumnBake; +import art.arcane.iris.generation.bake.GeneratorEvaluations; import art.arcane.iris.generation.bake.PlaneSource; import art.arcane.iris.generation.bake.SparseHeightMemo; import art.arcane.iris.generation.bake.TerrainProgram; @@ -406,7 +407,7 @@ public class IrisComplex implements DataProvider { return value; }); - bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize); + bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * 8); rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() .select(engine.getDimension().getRockPalette().getBlockData(data)); rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource); @@ -844,6 +845,7 @@ public class IrisComplex implements DataProvider { zs[index] = RiverFootprint.unpackZ(missing[index]); } TerrainProgram program = terrainProgram(); + long evaluations = 0L; for (int group = 0; group < frozenInterpolators.length; group++) { if (frozenGenerators[group].length == 0) { continue; @@ -854,8 +856,9 @@ public class IrisComplex implements DataProvider { lows[index] = bounds.min(); highs[index] = bounds.max(); } - program.accumulate(group, scratch, xs, zs, size, lows, highs, values); + evaluations += program.accumulate(group, scratch, xs, zs, size, lows, highs, values); } + GeneratorEvaluations.batched(evaluations); for (int index = 0; index < size; index++) { values[index] = finishBaseHeight(xs[index], zs[index], values[index]); } @@ -865,15 +868,16 @@ public class IrisComplex implements DataProvider { } } - private double[] bakeBaseHeights(int chunkX, int chunkZ) { - double[] values = new double[256]; + private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) { int originX = chunkX << 4; int originZ = chunkZ << 4; if (!supportsChunkHeightBatch()) { for (int index = 0; index < 256; index++) { - values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4)); + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4)); + } } - return values; + return; } TerrainBatchScratch scratch = BATCH_SCRATCH.get(); int mark = scratch.mark(); @@ -883,44 +887,55 @@ public class IrisComplex implements DataProvider { double[] zs = scratch.buffer(256); double[] lows = scratch.buffer(256); double[] highs = scratch.buffer(256); + double[] heights = scratch.buffer(256); + int[] columns = scratch.indices(256); + int count = 0; for (int index = 0; index < 256; index++) { - xs[index] = originX + (index & 15); - zs[index] = originZ + (index >> 4); + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + xs[count] = originX + (index & 15); + zs[count] = originZ + (index >> 4); + heights[count] = 0D; + columns[count++] = index; + } + } + if (count == 0) { + return; } TerrainProgram program = terrainProgram(); GridBoundsCache cache = gridBoundsCache.get(); int grid = biomeBoundsSamplingStep; boolean corners = ChunkCorners.supports(grid); + long evaluations = 0L; for (int group = 0; group < frozenInterpolators.length; group++) { if (frozenGenerators[group].length == 0) { continue; } if (corners) { - if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, lows, highs)) { + if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, count, lows, highs)) { continue; } } else { - for (int index = 0; index < 256; index++) { + for (int index = 0; index < count; index++) { NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group, frozenGenerators[group], xs[index], zs[index]); lows[index] = bounds.min(); highs[index] = bounds.max(); } } - program.accumulate(group, scratch, xs, zs, 256, lows, highs, values); + evaluations += program.accumulate(group, scratch, xs, zs, count, lows, highs, heights); } - for (int index = 0; index < 256; index++) { - values[index] = finishBaseHeight(xs[index], zs[index], values[index]); + for (int index = 0; index < count; index++) { + values[columns[index]] = finishBaseHeight(xs[index], zs[index], heights[index]); } + GeneratorEvaluations.bakedChunk(evaluations); } finally { scratch.reset(mark); scratch.resetIndices(indexMark); } - return values; } private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ, - double[] xs, double[] zs, double[] lows, double[] highs) { + double[] xs, double[] zs, int count, double[] lows, double[] highs) { int cells = 16 / grid; int stride = cells + 1; long[] packed = new long[stride * stride]; @@ -936,7 +951,7 @@ public class IrisComplex implements DataProvider { if (silent) { return false; } - for (int index = 0; index < 256; index++) { + for (int index = 0; index < count; index++) { double x = xs[index]; double z = zs[index]; int gx = ((int) Math.floor(x)) & ~(grid - 1); @@ -967,6 +982,62 @@ public class IrisComplex implements DataProvider { return true; } + private void basePlane(int chunkX, int chunkZ, long[] missing, double[] values) { + int originX = chunkX << 4; + int originZ = chunkZ << 4; + long[] compute = missing.clone(); + for (int index = 0; index < 256; index++) { + if ((compute[index >>> 6] & (1L << (index & 63))) == 0L) { + continue; + } + double memoized = sparseBaseHeights.get(originX + (index & 15), originZ + (index >> 4)); + if (!Double.isNaN(memoized)) { + values[index] = memoized; + compute[index >>> 6] &= ~(1L << (index & 63)); + } + } + bakeBaseHeights(chunkX, chunkZ, compute, values); + } + + private void prefetchBaseHeights(int chunkX, int chunkZ, long[] naturalMissing) { + ChunkPlanes.Plane base = bakeCache.plane(BakePlane.BASE_HEIGHT, chunkX, chunkZ); + int originX = chunkX << 4; + int originZ = chunkZ << 4; + long[] compute = new long[4]; + boolean any = false; + for (int index = 0; index < 256; index++) { + if ((naturalMissing[index >>> 6] & (1L << (index & 63))) == 0L || base.ready(index)) { + continue; + } + int x = originX + (index & 15); + int z = originZ + (index >> 4); + if (terrain3D != null && !terrain3D.requiresBaseHeight(x, z)) { + continue; + } + double memoized = sparseBaseHeights.get(x, z); + if (!Double.isNaN(memoized)) { + base.storeDouble(index, memoized); + continue; + } + compute[index >>> 6] |= 1L << (index & 63); + any = true; + } + if (!any) { + return; + } + double[] values = new double[256]; + long revision = ProvisionalSampling.revision(); + bakeBaseHeights(chunkX, chunkZ, compute, values); + if (revision != ProvisionalSampling.revision()) { + return; + } + for (int index = 0; index < 256; index++) { + if ((compute[index >>> 6] & (1L << (index & 63))) != 0L) { + base.storeDouble(index, values[index]); + } + } + } + private boolean supportsChunkHeightBatch() { return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() && (biomeBuffet == null || biomeBuffet.cells().isEmpty()); @@ -1012,9 +1083,9 @@ public class IrisComplex implements DataProvider { for (int index = 0; index < count; index++) { int x = RiverFootprint.unpackX(coordinates[index]); int z = RiverFootprint.unpackZ(coordinates[index]); - if (publishedPlane(BakePlane.NATURAL_HEIGHT, x, z) != null + if (bakedColumn(BakePlane.NATURAL_HEIGHT, x, z) || terrain3D != null && !terrain3D.requiresBaseHeight(x, z) - || publishedPlane(BakePlane.BASE_HEIGHT, x, z) != null + || bakedColumn(BakePlane.BASE_HEIGHT, x, z) || !Double.isNaN(sparseBaseHeights.get(x, z))) { continue; } @@ -1027,22 +1098,14 @@ public class IrisComplex implements DataProvider { sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]); } } - BakedDoubleStream naturalHeights = (BakedDoubleStream) naturalHeightStream; for (int index = 0; index < count; index++) { - heights[index] = naturalHeights.sparse(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index])); + heights[index] = naturalHeightStream.getDouble(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index])); } } - @SuppressWarnings("unchecked") - private IrisBiome sparseBaseBiome(double x, double z) { - return baseBiomeStream instanceof BakedStream baked - ? ((BakedStream) baked).sparse((int) x, (int) z) - : baseBiomeStream.get(x, z); - } - - private Object publishedPlane(BakePlane plane, int blockX, int blockZ) { - ChunkPlanes planes = bakeCache.planesIfPresent(blockX >> 4, blockZ >> 4); - return planes == null ? null : planes.published(plane); + private boolean bakedColumn(BakePlane plane, int blockX, int blockZ) { + ChunkPlanes.Plane values = bakeCache.planeIfPresent(plane, blockX >> 4, blockZ >> 4); + return values != null && values.ready(((blockZ & 15) << 4) | (blockX & 15)); } private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider { @@ -1050,7 +1113,12 @@ public class IrisComplex implements DataProvider { @Override public double sample(int blockX, int blockZ) { - return ((BakedDoubleStream) naturalHeightStream).sparse(blockX, blockZ); + ChunkBakeCache.enterSparse(); + try { + return naturalHeightStream.getDouble(blockX, blockZ); + } finally { + ChunkBakeCache.exitSparse(); + } } @Override @@ -1075,19 +1143,19 @@ public class IrisComplex implements DataProvider { private final class BakeSource implements PlaneSource { @Override - public Object compute(BakePlane plane, int chunkX, int chunkZ) { - return switch (plane) { - case REGION -> objectPlane(regionSource, chunkX, chunkZ); - case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ); - case BASE_HEIGHT -> bakeBaseHeights(chunkX, chunkZ); - case NATURAL_HEIGHT -> doublePlane(naturalHeightSource, chunkX, chunkZ); - case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ); - case HEIGHT -> heightPlane(chunkX, chunkZ); - case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ); - case ROCK -> objectPlane(rockSource, chunkX, chunkZ); - case FLUID -> objectPlane(fluidSource, chunkX, chunkZ); - case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ); - }; + public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) { + switch (plane) { + case REGION -> objectPlane(regionSource, chunkX, chunkZ, missing, (Object[]) values); + case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ, missing, (Object[]) values); + case BASE_HEIGHT -> basePlane(chunkX, chunkZ, missing, (double[]) values); + case NATURAL_HEIGHT -> naturalPlane(chunkX, chunkZ, missing, (double[]) values); + case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ, missing, (Object[]) values); + case HEIGHT -> heightPlane(chunkX, chunkZ, missing, (double[]) values); + case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ, missing, (Object[]) values); + case ROCK -> objectPlane(rockSource, chunkX, chunkZ, missing, (Object[]) values); + case FLUID -> objectPlane(fluidSource, chunkX, chunkZ, missing, (Object[]) values); + case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ, missing, (Object[]) values); + } } @Override @@ -1100,52 +1168,50 @@ public class IrisComplex implements DataProvider { return terrainEngine.getPlatformHooks().isMainThread(); } - @Override - public boolean generationContext() { - IrisContext context = IrisContext.get(); - if (context == null || context.getEngine() != terrainEngine) { - return false; - } - ChunkContext chunk = context.getChunkContext(); - return chunk != null && chunk.getComplex() == IrisComplex.this; - } - - private Object[] objectPlane(ProceduralStream source, int chunkX, int chunkZ) { - Object[] values = new Object[256]; + private void objectPlane(ProceduralStream source, int chunkX, int chunkZ, long[] missing, Object[] values) { int originX = chunkX << 4; int originZ = chunkZ << 4; for (int index = 0; index < 256; index++) { - values[index] = source.get(originX + (index & 15), originZ + (index >> 4)); + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + values[index] = source.get(originX + (index & 15), originZ + (index >> 4)); + } } - return values; } - private double[] heightPlane(int chunkX, int chunkZ) { - double[] values = new double[256]; + private void naturalPlane(int chunkX, int chunkZ, long[] missing, double[] values) { + prefetchBaseHeights(chunkX, chunkZ, missing); + int originX = chunkX << 4; + int originZ = chunkZ << 4; + for (int index = 0; index < 256; index++) { + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + values[index] = naturalHeightSource.getDouble(originX + (index & 15), originZ + (index >> 4)); + } + } + } + + private void heightPlane(int chunkX, int chunkZ, long[] missing, double[] values) { int originX = chunkX << 4; int originZ = chunkZ << 4; boolean main = terrainEngine.getPlatformHooks().isMainThread(); - for (int index = 0; index < 256; index++) { - int x = originX + (index & 15); - int z = originZ + (index >> 4); - values[index] = main ? nonblockingTerrainHeight(x, z) : resolveHydrologyTerrainHeight(x, z); + if (!main && !ChunkBakeCache.inSparseScope()) { + bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ); } - return values; - } - - private double[] doublePlane(ProceduralStream source, int chunkX, int chunkZ) { - double[] values = new double[256]; - int originX = chunkX << 4; - int originZ = chunkZ << 4; for (int index = 0; index < 256; index++) { - values[index] = source.getDouble(originX + (index & 15), originZ + (index >> 4)); + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + int x = originX + (index & 15); + int z = originZ + (index >> 4); + values[index] = main ? nonblockingTerrainHeight(x, z) : resolveHydrologyTerrainHeight(x, z); + } } - return values; } } public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) { - return bakeCache.bake(plane, chunkX, chunkZ); + return bakeCache.complete(plane, chunkX, chunkZ); + } + + public long[] bakeCensus() { + return bakeCache.census(); } public ColumnBake columnBake(int chunkX, int chunkZ) { @@ -1177,7 +1243,7 @@ public class IrisComplex implements DataProvider { } private ColumnBake assembleColumnBake(int chunkX, int chunkZ) { - double[] height = (double[]) bakeCache.bake(BakePlane.HEIGHT, chunkX, chunkZ); + double[] height = (double[]) bakeCache.complete(BakePlane.HEIGHT, chunkX, chunkZ); int[] rounded = new int[256]; SimdSupport.kernels().roundToInt(height, rounded, 256); int originX = chunkX << 4; @@ -1197,17 +1263,17 @@ public class IrisComplex implements DataProvider { } } return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ, - ids((Object[]) bakeCache.bake(BakePlane.REGION, chunkX, chunkZ), regionRegistry), - ids((Object[]) bakeCache.bake(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry), - ids((Object[]) bakeCache.bake(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry), - ids((Object[]) bakeCache.bake(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry), - (double[]) bakeCache.bake(BakePlane.BASE_HEIGHT, chunkX, chunkZ), - (double[]) bakeCache.bake(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ), + ids((Object[]) bakeCache.complete(BakePlane.REGION, chunkX, chunkZ), regionRegistry), + ids((Object[]) bakeCache.complete(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry), + ids((Object[]) bakeCache.complete(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry), + ids((Object[]) bakeCache.complete(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry), + (double[]) bakeCache.complete(BakePlane.BASE_HEIGHT, chunkX, chunkZ), + (double[]) bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ), height, rounded, fluidSurface, - ids((Object[]) bakeCache.bake(BakePlane.ROCK, chunkX, chunkZ), blockRegistry), - ids((Object[]) bakeCache.bake(BakePlane.FLUID, chunkX, chunkZ), blockRegistry), + ids((Object[]) bakeCache.complete(BakePlane.ROCK, chunkX, chunkZ), blockRegistry), + ids((Object[]) bakeCache.complete(BakePlane.FLUID, chunkX, chunkZ), blockRegistry), terrainColumns, apron)); } @@ -2496,6 +2562,7 @@ public class IrisComplex implements DataProvider { } return height / generators.length; } + GeneratorEvaluations.scalar(generators.length); for (IrisGenerator generator : generators) { double noise = generator.getHeight(x, z, seed); if (!Double.isFinite(noise)) { @@ -2834,7 +2901,12 @@ public class IrisComplex implements DataProvider { double sampleZ = integerCoordinates ? (int) zz : zz; IrisBiome bx = sampleCache.get(sampleX, sampleZ); if (bx == null) { - bx = complex.sparseBaseBiome(xx, zz); + ChunkBakeCache.enterSparse(); + try { + bx = complex.baseBiomeStream.get(xx, zz); + } finally { + ChunkBakeCache.exitSparse(); + } sampleCache.put(sampleX, sampleZ, bx); } diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java index 65ce01f5a..56b122025 100644 --- a/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java @@ -37,21 +37,18 @@ public final class BakedDoubleStream extends BasicStream implements Chun public double getDouble(double x, double z) { int blockX = (int) x; int blockZ = (int) z; - Object values = cache.read(plane, blockX >> 4, blockZ >> 4); - return values == null ? scalar.getDouble(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)]; - } - - public double sparse(int blockX, int blockZ) { - Object published = publishedPlane(blockX >> 4, blockZ >> 4); - if (published != null) { - return ((double[]) published)[((blockZ & 15) << 4) | (blockX & 15)]; + int index = ((blockZ & 15) << 4) | (blockX & 15); + ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4); + if (values != null && values.ready(index)) { + return values.doubleAt(index); } - ChunkBakeCache.enterSparse(); - try { + if (!cache.memoAllowed()) { return scalar.getDouble(blockX, blockZ); - } finally { - ChunkBakeCache.exitSparse(); } + long revision = ProvisionalSampling.revision(); + double value = scalar.getDouble(blockX, blockZ); + cache.storeDouble(plane, blockX, blockZ, value, revision); + return value; } @Override @@ -81,15 +78,14 @@ public final class BakedDoubleStream extends BasicStream implements Chun } int chunkX = worldX >> 4; int chunkZ = worldZ >> 4; - Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ); - if (values != null) { - System.arraycopy(values, 0, target, 0, 256); + if (cache.memoAllowed()) { + System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256); return; } int originX = chunkX << 4; int originZ = chunkZ << 4; for (int index = 0; index < 256; index++) { - target[index] = scalar.getDouble(originX + (index & 15), originZ + (index >> 4)); + target[index] = getDouble(originX + (index & 15), originZ + (index >> 4)); } } @@ -101,9 +97,4 @@ public final class BakedDoubleStream extends BasicStream implements Chun target[index] = values[index]; } } - - private Object publishedPlane(int chunkX, int chunkZ) { - ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ); - return planes == null ? null : planes.published(plane); - } } diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java index 9ae17fd53..70efa9736 100644 --- a/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java @@ -33,10 +33,7 @@ public final class BakedHeightStream extends BasicStream implements Chun if (mainThread.getAsBoolean()) { return nonblocking.noise(x, z); } - int blockX = (int) x; - int blockZ = (int) z; - Object values = cache.read(BakePlane.HEIGHT, blockX >> 4, blockZ >> 4); - return values == null ? resolved.noise(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)]; + return column((int) x, (int) z, !ChunkBakeCache.inSparseScope()); } @Override @@ -65,18 +62,15 @@ public final class BakedHeightStream extends BasicStream implements Chun throw new IllegalArgumentException("Expected a 16x16 target array."); } boolean main = mainThread.getAsBoolean(); - if (!main && (worldX & 15) == 0 && (worldZ & 15) == 0) { - Object values = ChunkBakeCache.inSparseScope() ? published(worldX >> 4, worldZ >> 4) - : cache.bake(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4); - if (values != null) { - System.arraycopy(values, 0, target, 0, 256); - return; - } + boolean memo = !main && !ChunkBakeCache.inSparseScope(); + if (memo && (worldX & 15) == 0 && (worldZ & 15) == 0) { + System.arraycopy(cache.complete(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4), 0, target, 0, 256); + return; } for (int index = 0; index < 256; index++) { int x = worldX + (index & 15); int z = worldZ + (index >> 4); - target[index] = main ? nonblocking.noise(x, z) : column(x, z); + target[index] = main ? nonblocking.noise(x, z) : column(x, z, memo); } } @@ -89,13 +83,18 @@ public final class BakedHeightStream extends BasicStream implements Chun } } - private double column(int x, int z) { - Object values = published(x >> 4, z >> 4); - return values == null ? resolved.noise(x, z) : ((double[]) values)[((z & 15) << 4) | (x & 15)]; - } - - private Object published(int chunkX, int chunkZ) { - ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ); - return planes == null ? null : planes.published(BakePlane.HEIGHT); + private double column(int x, int z, boolean memo) { + int index = ((z & 15) << 4) | (x & 15); + ChunkPlanes.Plane values = cache.planeIfPresent(BakePlane.HEIGHT, x >> 4, z >> 4); + if (values != null && values.ready(index)) { + return values.doubleAt(index); + } + if (!memo) { + return resolved.noise(x, z); + } + long revision = ProvisionalSampling.revision(); + double value = resolved.noise(x, z); + cache.storeDouble(BakePlane.HEIGHT, x, z, value, revision); + return value; } } 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 0bb0a0a63..805d857a4 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 @@ -33,22 +33,18 @@ public final class BakedStream extends BasicStream implements ChunkFillabl public T get(double x, double z) { int blockX = (int) x; int blockZ = (int) z; - Object values = cache.read(plane, blockX >> 4, blockZ >> 4); - return values == null ? scalar.get(blockX, blockZ) : (T) ((Object[]) values)[((blockZ & 15) << 4) | (blockX & 15)]; - } - - @SuppressWarnings("unchecked") - public T sparse(int blockX, int blockZ) { - Object published = publishedPlane(blockX >> 4, blockZ >> 4); - if (published != null) { - return (T) ((Object[]) published)[((blockZ & 15) << 4) | (blockX & 15)]; + int index = ((blockZ & 15) << 4) | (blockX & 15); + ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4); + if (values != null && values.ready(index)) { + return (T) values.objectAt(index); } - ChunkBakeCache.enterSparse(); - try { + if (!cache.memoAllowed()) { return scalar.get(blockX, blockZ); - } finally { - ChunkBakeCache.exitSparse(); } + long revision = ProvisionalSampling.revision(); + T value = scalar.get(blockX, blockZ); + cache.storeObject(plane, blockX, blockZ, value, revision); + return value; } @Override @@ -70,20 +66,14 @@ public final class BakedStream extends BasicStream implements ChunkFillabl public void fillChunkRaw(int worldX, int worldZ, Object[] target) { int chunkX = worldX >> 4; int chunkZ = worldZ >> 4; - Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ); - if (values != null) { - System.arraycopy(values, 0, target, 0, 256); + if (cache.memoAllowed()) { + System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256); return; } int originX = chunkX << 4; int originZ = chunkZ << 4; for (int index = 0; index < 256; index++) { - target[index] = scalar.get(originX + (index & 15), originZ + (index >> 4)); + target[index] = get(originX + (index & 15), originZ + (index >> 4)); } } - - private Object publishedPlane(int chunkX, int chunkZ) { - ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ); - return planes == null ? null : planes.published(plane); - } } 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 new file mode 100644 index 000000000..ec36ad135 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java @@ -0,0 +1,204 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.configuration.IrisSettings; +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.GeneratorEvaluations; +import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.image.IrisImageMapApplication; +import art.arcane.iris.generation.image.IrisImageMapRuntime; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.testsupport.PlatformLeakGuard; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.MockedStatic; + +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.assertTrue; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +public class IrisComplexBakeParityTest { + @ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private IrisSettings previousSettings; + + @Before + public void prepareRuntime() { + previousSettings = IrisSettings.settings; + IrisSettings.settings = new IrisSettings(); + IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class)); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.platformName()).thenReturn("bukkit"); + IrisPlatforms.bind(platform); + } + + @After + public void releaseRuntime() { + IrisPlatforms.unbind(); + IrisServices.remove(PreservationRegistry.class); + IrisSettings.settings = previousSettings; + } + + @Test + public void batchedChunkBaseHeightsEqualScalarHeightsBitForBit() throws Exception { + try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) { + SplittableRandom random = new SplittableRandom(0xBA5EL); + for (int chunk = 0; chunk < 48; chunk++) { + int chunkX = random.nextInt(-400, 400); + int chunkZ = random.nextInt(-400, 400); + double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ); + for (int index = 0; index < 256; index++) { + int x = (chunkX << 4) + (index & 15); + int z = (chunkZ << 4) + (index >> 4); + assertEquals("chunk " + chunkX + "," + chunkZ + " column " + index, + Double.doubleToRawLongBits(scalar.complex.getBaseTerrainHeightStream().getDouble(x, z)), + Double.doubleToRawLongBits(baked[index])); + } + } + } + } + + @Test + public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception { + try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) { + SplittableRandom random = new SplittableRandom(0xC0DEL); + long totalBatched = 0L; + for (int chunk = 0; chunk < 32; chunk++) { + int chunkX = random.nextInt(-400, 400); + int chunkZ = random.nextInt(-400, 400); + GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot(); + batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ); + GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot(); + for (int index = 0; index < 256; index++) { + scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4)); + } + GeneratorEvaluations.Snapshot afterScalar = GeneratorEvaluations.snapshot(); + long batchedEvaluations = afterBatch.minus(before).bakedEvaluations(); + assertEquals(0L, afterBatch.minus(before).scalarEvaluations()); + assertEquals("chunk " + chunkX + "," + chunkZ, batchedEvaluations, + afterScalar.minus(afterBatch).scalarEvaluations()); + totalBatched += batchedEvaluations; + } + assertTrue(totalBatched > 0L); + } + } + + private Path pack() throws Exception { + Path pack = temporaryFolder.newFolder().toPath(); + Files.createDirectories(pack.resolve("dimensions")); + Files.createDirectories(pack.resolve("regions")); + Files.createDirectories(pack.resolve("biomes")); + Files.createDirectories(pack.resolve("generators")); + Files.writeString(pack.resolve("dimensions/main.json"), """ + {"regions":["alpha","beta"],"landChance":0.55,"regionZoom":0.2,"biomeZoom":0.15, + "carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}} + """); + Files.writeString(pack.resolve("regions/alpha.json"), + "{\"landBiomes\":[\"peaks\",\"meadow\",\"dunes\"],\"seaBiomes\":[\"sea\"],\"rarity\":1}"); + Files.writeString(pack.resolve("regions/beta.json"), + "{\"landBiomes\":[\"meadow\",\"spires\"],\"seaBiomes\":[\"sea\"],\"rarity\":2}"); + generator(pack, "mountain", "BILINEAR_STARCAST_9", 52, 0.5D, """ + [{"style":{"style":"FRACTAL_SMOKE","zoom":1},"seed":3356}]""", ""); + generator(pack, "magnet", "BILINEAR_STARCAST_9", 52, 0.5D, """ + [{"style":{"style":"FRACTAL_SMOKE","zoom":0.6},"seed":911}]""", ""); + generator(pack, "plain", "BILINEAR_STARCAST_9", 12, 0.5D, """ + [{"style":{"style":"IRIS_DOUBLE","zoom":0.87,"fracture":{"style":"NOWHERE","zoom":0.195,"multiplier":18}},"seed":10056,"bezier":true}]""", ""); + generator(pack, "cliffs", "BILINEAR_STARCAST_9", 12, 0.5D, """ + [{"style":{"style":"GLOB","zoom":0.4,"exponent":0.7,"fracture":{"style":"NOWHERE","zoom":0.1,"multiplier":5}},"negative":true,"seed":10056}]""", + ",\"cliffHeightMax\":80,\"cliffHeightMin\":35,\"cliffHeightGenerator\":{\"seed\":2348,\"style\":{\"style\":\"CELLULAR_HEIGHT\"},\"zoom\":0.4}"); + generator(pack, "dunes", "BILINEAR_STARCAST_9", 23, 1D, """ + [{"style":{"style":"SIMPLEX","zoom":2},"seed":77,"fracture":[{"style":{"style":"PERLIN"},"seed":5,"opacity":2.5}]}]""", ""); + generator(pack, "spikes", "STARCAST_12", 42, 0D, """ + [{"style":{"style":"CELLULAR","zoom":0.5},"seed":4}]""", ""); + biome(pack, "peaks", "[{\"generator\":\"mountain\",\"min\":40,\"max\":120},{\"generator\":\"spikes\",\"min\":0,\"max\":20}]", + ",\"children\":[\"meadow\"],\"childShrinkFactor\":1.5"); + biome(pack, "meadow", "[{\"generator\":\"plain\",\"min\":2,\"max\":14},{\"generator\":\"cliffs\",\"min\":0,\"max\":9}]", ""); + biome(pack, "dunes", "[{\"generator\":\"dunes\",\"min\":3,\"max\":30},{\"generator\":\"magnet\",\"min\":-4,\"max\":6}]", ""); + biome(pack, "spires", "[{\"generator\":\"spikes\",\"min\":10,\"max\":60},{\"generator\":\"plain\",\"min\":1,\"max\":4}]", ""); + biome(pack, "sea", "[{\"generator\":\"mountain\",\"min\":-32,\"max\":-10}]", ""); + return pack; + } + + private static void generator(Path pack, String key, String function, double scale, double detail, + String composite, String extra) throws Exception { + Files.writeString(pack.resolve("generators/" + key + ".json"), "{\"surfaceDetail\":" + detail + + ",\"interpolator\":{\"function\":\"" + function + "\",\"horizontalScale\":" + scale + "}" + + ",\"seed\":" + Math.abs(key.hashCode()) + ",\"composite\":" + composite.strip() + extra + "}"); + } + + private static void biome(Path pack, String key, String generators, String extra) throws Exception { + Files.writeString(pack.resolve("biomes/" + key + ".json"), "{\"generators\":" + generators + extra + "}"); + } + + private static final class Statics implements AutoCloseable { + private final MockedStatic blocks = mockStatic(B.class); + private final MockedStatic imageRuntimes = mockStatic(IrisImageMapRuntime.class); + + private Statics() { + NativeBlockState block = mock(NativeBlockState.class); + when(block.isFluid()).thenReturn(true); + blocks.when(() -> B.getState(anyString())).thenReturn(block); + blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block); + IrisImageMapRuntime images = mock(IrisImageMapRuntime.class); + when(images.has(IrisImageMapApplication.BIOME)).thenReturn(false); + when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble())) + .thenAnswer(invocation -> invocation.getArgument(2)); + imageRuntimes.when(() -> IrisImageMapRuntime.compile(any(Engine.class))).thenReturn(images); + } + + @Override + public void close() { + imageRuntimes.close(); + blocks.close(); + } + } + + private static final class Fixture implements AutoCloseable { + private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + private final IrisData data; + private final IrisComplex complex; + + private Fixture(Path pack) { + data = IrisData.openRuntime(pack.toFile()); + IrisDimension dimension = data.getDimensionLoader().load("main"); + doReturn(data).when(engine).getData(); + doReturn(dimension).when(engine).getDimension(); + doReturn(384).when(engine).getHeight(); + doReturn(-64).when(engine).getMinHeight(); + doReturn(320).when(engine).getMaxHeight(); + when(hooks.isMainThread()).thenReturn(false); + doReturn(hooks).when(engine).getPlatformHooks(); + doReturn(new SeedManager(1337L)).when(engine).getSeedManager(); + complex = new IrisComplex(engine); + doReturn(complex).when(engine).getComplex(); + } + + @Override + public void close() { + complex.close(); + data.close(); + } + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index 671b2b408..d94371ffe 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -18,6 +18,8 @@ package art.arcane.iris.probe; +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.GeneratorEvaluations; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidator; @@ -553,9 +555,11 @@ public final class GenerationProbe { List coordinates, OutcomeSink sink) throws IOException { int totalChunks = coordinates.size(); long measuredStartEpochMillis = 0L; + GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot(); for (int index = 0; index < totalChunks; index++) { if (index == configuration.warmupChunks()) { measuredStartEpochMillis = System.currentTimeMillis(); + measuredStartEvaluations = GeneratorEvaluations.snapshot(); } drainReported(); ChunkOutcome outcome = generateChunk(engine, index, coordinates.get(index), configuration.multicore()); @@ -563,12 +567,22 @@ public final class GenerationProbe { sink.accept(outcome, index >= configuration.warmupChunks()); printProgress(configuration, index + 1, totalChunks); } - printWindow(measuredStartEpochMillis, System.currentTimeMillis()); + printWindow(engine, measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations); } - private static void printWindow(long startEpochMillis, long endEpochMillis) { + private static void printWindow(Engine engine, long startEpochMillis, long endEpochMillis, GeneratorEvaluations.Snapshot startEvaluations) { System.out.println("IRIS_GENPROBE_WINDOW phase=measured start_epoch_ms=" + startEpochMillis + " end_epoch_ms=" + endEpochMillis); + GeneratorEvaluations.Snapshot evaluations = GeneratorEvaluations.snapshot().minus(startEvaluations); + System.out.printf(Locale.ROOT, "IRIS_GENPROBE_GENERATORS baked_chunks=%d baked_evaluations=%d evaluations_per_baked_chunk=%.3f batched_evaluations=%d scalar_evaluations=%d%n", + evaluations.bakedChunks(), evaluations.bakedEvaluations(), evaluations.bakedEvaluationsPerChunk(), + evaluations.batchedEvaluations(), evaluations.scalarEvaluations()); + long[] census = engine.getComplex().bakeCensus(); + StringBuilder planes = new StringBuilder("IRIS_GENPROBE_BAKE entries=").append(census[BakePlane.values().length]); + for (BakePlane plane : BakePlane.values()) { + planes.append(' ').append(plane.name().toLowerCase(Locale.ROOT)).append('=').append(census[plane.ordinal()]); + } + System.out.println(planes); } private static void generateParallel(Engine engine, ProbeConfiguration configuration, ProbeOptions options, @@ -594,10 +608,11 @@ public final class GenerationProbe { List measured = spiralOrder(coordinates, configuration.warmupChunks(), configuration.centerChunkX(), configuration.centerChunkZ()); long measuredStartEpochMillis = System.currentTimeMillis(); + GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot(); long measuredStarted = System.nanoTime(); runPhase(executor, engine, configuration, coordinates, measured, outcomes); measuredWallNanos = System.nanoTime() - measuredStarted; - printWindow(measuredStartEpochMillis, System.currentTimeMillis()); + printWindow(engine, measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations); unattributed.addAll(drainReported()); System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/" + configuration.measuredChunks());