From 037156ab7dd9e2e48c76967f54f7bff9d18d4e87 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 21:52:07 -0400 Subject: [PATCH 01/16] Added chunk terrain bake - Compiled generator programs evaluate layer trees in batches through NoiseProgram - TerrainProgram accumulates interpolator group means over chunk columns - ChunkBakeCache holds compute-once per-chunk planes with provisional results never published - Height, biome, region, rock, fluid and cave streams read the bake as thin facades - Sparse hydrology and bounds reads stay scalar with a direct-mapped base height memo - ColumnBake assembles id planes, heights, fluid surface, columns and the apron on demand --- .../iris/generation/bake/BakeCache.java | 7 + .../iris/generation/bake/BakePlane.java | 16 + .../iris/generation/bake/BakeRegistry.java | 41 ++ .../iris/generation/bake/ChunkBakeCache.java | 176 +++++ .../iris/generation/bake/ChunkPlanes.java | 107 +++ .../iris/generation/bake/ColumnBake.java | 55 ++ .../iris/generation/bake/PlaneSource.java | 11 + .../generation/bake/SparseHeightMemo.java | 41 ++ .../iris/generation/bake/TerrainProgram.java | 136 ++++ .../generation/noise/GeneratorProgram.java | 201 ++++++ .../iris/generation/noise/IrisGenerator.java | 119 ++-- .../generation/noise/IrisNoiseGenerator.java | 46 +- .../generation/noise/NoiseLayerProgram.java | 137 ++++ .../generation/noise/TerrainBatchScratch.java | 115 +++ .../iris/generation/runtime/IrisComplex.java | 658 ++++++++++++------ .../generation/stream/BakedDoubleStream.java | 109 +++ .../generation/stream/BakedHeightStream.java | 101 +++ .../iris/generation/stream/BakedStream.java | 89 +++ .../noise/GeneratorProgramParityTest.java | 140 ++++ .../noise/IrisGeneratorBatchTest.java | 28 +- .../runtime/IrisComplexHeightStreamTest.java | 11 +- .../IrisComplexHydrologyNoiseCacheTest.java | 19 +- 22 files changed, 2032 insertions(+), 331 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/SparseHeightMemo.java create mode 100644 core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java create mode 100644 core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java create mode 100644 core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java create mode 100644 core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java create mode 100644 core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java create mode 100644 core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java create mode 100644 core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java create mode 100644 core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java b/core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java new file mode 100644 index 000000000..5054c537c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java @@ -0,0 +1,7 @@ +package art.arcane.iris.generation.bake; + +public interface BakeCache { + ColumnBake get(int chunkX, int chunkZ); + + ColumnBake getIfPresent(int chunkX, int chunkZ); +} 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 new file mode 100644 index 000000000..3d7f0cd33 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java @@ -0,0 +1,16 @@ +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; + + static final BakePlane[] PLANES = values(); +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java b/core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java new file mode 100644 index 000000000..205982885 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java @@ -0,0 +1,41 @@ +package art.arcane.iris.generation.bake; + +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Objects; + +public final class BakeRegistry { + private final Object lock = new Object(); + private volatile IdentityHashMap ids = new IdentityHashMap<>(); + private volatile Object[] values = new Object[16]; + private int size; + + public short id(T value) { + Objects.requireNonNull(value, "Registered value"); + Integer known = ids.get(value); + if (known != null) { + return known.shortValue(); + } + synchronized (lock) { + Integer existing = ids.get(value); + if (existing != null) { + return existing.shortValue(); + } + if (size == Short.MAX_VALUE) { + throw new IllegalStateException("Bake registry is full"); + } + Object[] grown = size == values.length ? Arrays.copyOf(values, size * 2) : values; + grown[size] = value; + values = grown; + IdentityHashMap next = new IdentityHashMap<>(ids); + next.put(value, size); + ids = next; + return (short) size++; + } + } + + @SuppressWarnings("unchecked") + public T value(int id) { + return (T) values[id]; + } +} 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 new file mode 100644 index 000000000..0dd59ca33 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java @@ -0,0 +1,176 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.cache.ConcurrentClockCache; + +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 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); + } + + @Override + public ColumnBake getIfPresent(int chunkX, int chunkZ) { + ChunkPlanes planes = planesIfPresent(chunkX, chunkZ); + return planes == null ? null : planes.columnBake(); + } + + public int capacity() { + return entries.capacity(); + } + + public synchronized void ensureCapacity(int chunks) { + ConcurrentClockCache current = entries; + if (current.capacity() >= chunks) { + return; + } + ConcurrentClockCache resized = new ConcurrentClockCache<>(chunks); + current.forEach(planes -> resized.putIfAbsent(key(planes.chunkX(), planes.chunkZ()), planes)); + entries = resized; + } + + public void clear() { + entries.clear(); + } + + public ChunkPlanes planes(int chunkX, int chunkZ) { + ConcurrentClockCache current = entries; + long key = key(chunkX, chunkZ); + ChunkPlanes existing = current.get(key); + return existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ)); + } + + public ChunkPlanes planesIfPresent(int chunkX, int chunkZ) { + 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) { + 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); + } + + public boolean denseReadsAllowed() { + int[] depth = DEPTH.get(); + if (depth[SPARSE] > 0 || source.mainThread()) { + return false; + } + return depth[BAKING] > 0 || source.generationContext(); + } + + public boolean bakingAllowed() { + return !inSparseScope() && !source.mainThread(); + } + + public static boolean inSparseScope() { + return DEPTH.get()[SPARSE] > 0; + } + + public static T sparse(Supplier action) { + enterSparse(); + try { + return action.get(); + } finally { + exitSparse(); + } + } + + 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]++; + } + + public static void exitSparse() { + DEPTH.get()[SPARSE]--; + } + + 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 new file mode 100644 index 000000000..bacc3b9d1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java @@ -0,0 +1,107 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.stream.ProvisionalSampling; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +public final class ChunkPlanes { + private final int chunkX; + private final int chunkZ; + private final AtomicReferenceArray planes = new AtomicReferenceArray<>(BakePlane.PLANES.length); + private volatile ColumnBake columnBake; + + ChunkPlanes(int chunkX, int chunkZ) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + public int chunkX() { + return chunkX; + } + + public int chunkZ() { + return chunkZ; + } + + public Object published(BakePlane plane) { + Object state = planes.getAcquire(plane.ordinal()); + return state instanceof Pending ? null : state; + } + + Object plane(BakePlane plane, PlaneSource source) { + int slot = plane.ordinal(); + while (true) { + Object state = planes.getAcquire(slot); + if (state != null && !(state instanceof Pending)) { + return state; + } + if (state instanceof Pending pending) { + if (pending.owner == Thread.currentThread()) { + return source.compute(plane, chunkX, chunkZ); + } + pending.await(); + continue; + } + Pending pending = new Pending(Thread.currentThread()); + if (!planes.compareAndSet(slot, null, pending)) { + continue; + } + Object computed = null; + boolean publishable = false; + try { + long revision = ProvisionalSampling.revision(); + computed = source.compute(plane, chunkX, chunkZ); + publishable = revision == ProvisionalSampling.revision(); + } finally { + planes.setRelease(slot, publishable ? computed : null); + pending.complete(); + } + return computed; + } + } + + ColumnBake columnBake() { + return columnBake; + } + + ColumnBake columnBake(PlaneSource source) { + ColumnBake bake = columnBake; + if (bake != null) { + return bake; + } + long revision = ProvisionalSampling.revision(); + bake = source.assemble(this); + if (revision == ProvisionalSampling.revision()) { + columnBake = bake; + } + return bake; + } + + private static final class Pending { + private final Thread owner; + private boolean done; + + private Pending(Thread owner) { + this.owner = owner; + } + + private synchronized void complete() { + done = true; + notifyAll(); + } + + private synchronized void await() { + boolean interrupted = false; + while (!done) { + try { + wait(); + } catch (InterruptedException e) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java b/core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java new file mode 100644 index 000000000..749263925 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java @@ -0,0 +1,55 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.terrain.Terrain3DColumn; + +public final class ColumnBake { + public static final int APRON_WIDTH = 18; + + public final int chunkX; + public final int chunkZ; + public final short[] regionId; + public final short[] baseBiomeId; + public final short[] trueBiomeId; + public final short[] caveBiomeId; + public final double[] baseHeight; + public final double[] naturalHeight; + public final double[] height; + public final int[] roundedHeight; + public final float[] fluidSurface; + public final short[] rockState; + public final short[] fluidState; + public final Terrain3DColumn[] terrainColumns; + public final double[] apronHeight; + + public ColumnBake(Planes planes) { + this.chunkX = planes.chunkX(); + this.chunkZ = planes.chunkZ(); + this.regionId = planes.regionId(); + this.baseBiomeId = planes.baseBiomeId(); + this.trueBiomeId = planes.trueBiomeId(); + this.caveBiomeId = planes.caveBiomeId(); + this.baseHeight = planes.baseHeight(); + this.naturalHeight = planes.naturalHeight(); + this.height = planes.height(); + this.roundedHeight = planes.roundedHeight(); + this.fluidSurface = planes.fluidSurface(); + this.rockState = planes.rockState(); + this.fluidState = planes.fluidState(); + this.terrainColumns = planes.terrainColumns(); + this.apronHeight = planes.apronHeight(); + } + + public static int index(int localX, int localZ) { + return (localZ << 4) | localX; + } + + public static int apronIndex(int localX, int localZ) { + return (localZ + 1) * APRON_WIDTH + localX + 1; + } + + public record Planes(int chunkX, int chunkZ, short[] regionId, short[] baseBiomeId, short[] trueBiomeId, + short[] caveBiomeId, double[] baseHeight, double[] naturalHeight, double[] height, + int[] roundedHeight, float[] fluidSurface, short[] rockState, short[] fluidState, + Terrain3DColumn[] terrainColumns, double[] apronHeight) { + } +} 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 new file mode 100644 index 000000000..afb13bc36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java @@ -0,0 +1,11 @@ +package art.arcane.iris.generation.bake; + +public interface PlaneSource { + Object compute(BakePlane plane, int chunkX, int chunkZ); + + ColumnBake assemble(ChunkPlanes planes); + + boolean mainThread(); + + boolean generationContext(); +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/SparseHeightMemo.java b/core/src/main/java/art/arcane/iris/generation/bake/SparseHeightMemo.java new file mode 100644 index 000000000..d11d33be4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/SparseHeightMemo.java @@ -0,0 +1,41 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.volmlib.util.cache.CacheKey; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +public final class SparseHeightMemo { + private final AtomicReferenceArray nodes; + private final int mask; + + public SparseHeightMemo(int capacity) { + if (capacity <= 0 || Integer.bitCount(capacity) != 1) { + throw new IllegalArgumentException("Sparse height memo capacity must be a power of two"); + } + nodes = new AtomicReferenceArray<>(capacity); + mask = capacity - 1; + } + + public double get(int x, int z) { + long key = ((long) x << 32) ^ (z & 0xffffffffL); + Node node = nodes.getAcquire((int) CacheKey.mix(key) & mask); + return node != null && node.key == key ? node.height : Double.NaN; + } + + public void put(int x, int z, double height) { + if (!Double.isFinite(height)) { + return; + } + long key = ((long) x << 32) ^ (z & 0xffffffffL); + nodes.setRelease((int) CacheKey.mix(key) & mask, new Node(key, height)); + } + + public void clear() { + for (int index = 0; index < nodes.length(); index++) { + nodes.setRelease(index, null); + } + } + + private record Node(long key, double height) { + } +} 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 new file mode 100644 index 000000000..8dc834e65 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java @@ -0,0 +1,136 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.noise.GeneratorProgram; +import art.arcane.iris.generation.noise.IrisGenerator; +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; + private static final int CONSTANT = 1; + private static final int ACTIVE = 2; + + 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; + this.programs = programs; + this.heightSeed = heightSeed; + } + + public static TerrainProgram compile(IrisGenerator[][] generators, long heightSeed) { + GeneratorProgram[][] programs = new GeneratorProgram[generators.length][]; + for (int group = 0; group < generators.length; group++) { + programs[group] = new GeneratorProgram[generators[group].length]; + for (int index = 0; index < generators[group].length; index++) { + IrisGenerator generator = generators[group][index]; + programs[group][index] = generator.supportsBatch() ? generator.compileProgram(heightSeed) : null; + } + } + return new TerrainProgram(generators, programs, heightSeed); + } + + public int groups() { + return generators.length; + } + + public int groupSize(int group) { + 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, + double[] lows, double[] highs, double[] heights) { + IrisGenerator[] members = generators[group]; + if (members.length == 0 || count == 0) { + return; + } + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + int[] modes = scratch.indices(count); + int activeCount = 0; + for (int index = 0; index < count; index++) { + double low = lows[index]; + double high = highs[index]; + if (low == 0D && high == 0D) { + modes[index] = SKIPPED; + continue; + } + if (!Double.isFinite(low) || !Double.isFinite(high)) { + throw new IllegalStateException("Terrain generator bounds were not finite at " + xs[index] + "," + zs[index] + + " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ")."); + } + if (low == high) { + modes[index] = CONSTANT; + double sum = 0D; + for (int member = 0; member < members.length; member++) { + sum += low; + } + heights[index] += sum / members.length; + continue; + } + modes[index] = ACTIVE; + activeCount++; + } + if (activeCount == 0) { + scratch.resetIndices(indexMark); + return; + } + double[] activeX = scratch.buffer(activeCount); + double[] activeZ = scratch.buffer(activeCount); + double[] activeLow = scratch.buffer(activeCount); + double[] activeHigh = scratch.buffer(activeCount); + int[] activeIndex = scratch.indices(activeCount); + int cursor = 0; + for (int index = 0; index < count; index++) { + if (modes[index] == ACTIVE) { + activeX[cursor] = xs[index]; + activeZ[cursor] = zs[index]; + activeLow[cursor] = lows[index]; + activeHigh[cursor] = highs[index]; + activeIndex[cursor++] = index; + } + } + double[] sums = scratch.buffer(activeCount); + double[] noise = scratch.buffer(activeCount); + Arrays.fill(sums, 0, activeCount, 0D); + GeneratorProgram[] compiled = programs[group]; + for (int member = 0; member < members.length; member++) { + GeneratorProgram program = compiled[member]; + if (program != null) { + program.heights(scratch, activeX, activeZ, activeCount, noise); + } else { + for (int index = 0; index < activeCount; index++) { + noise[index] = members[member].getHeight(activeX[index], activeZ[index], heightSeed); + } + } + for (int index = 0; index < activeCount; index++) { + if (!Double.isFinite(noise[index])) { + throw new IllegalStateException("Terrain generator noise was not finite at " + activeX[index] + "," + activeZ[index] + + " (generator=" + members[member].getLoadKey() + ", noise=" + noise[index] + + ", bounds=" + activeLow[index] + ".." + activeHigh[index] + ")."); + } + sums[index] += M.lerp(activeLow[index], activeHigh[index], noise[index]); + } + } + 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); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java b/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java new file mode 100644 index 000000000..bd3c76c72 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java @@ -0,0 +1,201 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.interpolation.IrisInterpolation; + +import java.util.Arrays; + +public final class GeneratorProgram { + private static final int SURFACE_GRID = 6; + + private final IrisGenerator generator; + private final long superSeed; + private final NoiseLayerProgram[] layers; + private final double totalOpacity; + private final NoiseLayerProgram cliffs; + private final boolean multiplicitive; + private final double opacity; + private final double offsetX; + private final double offsetZ; + private final double zoom; + private final double surfaceDetail; + private final double cliffHeightMin; + private final double cliffHeightMax; + private final boolean cellCracks; + + GeneratorProgram(IrisGenerator generator, long superSeed, NoiseLayerProgram[] layers, double totalOpacity, NoiseLayerProgram cliffs) { + this.generator = generator; + this.superSeed = superSeed; + this.layers = layers; + this.totalOpacity = totalOpacity; + this.cliffs = cliffs; + this.multiplicitive = generator.isMultiplicitive(); + this.opacity = generator.getOpacity(); + this.offsetX = generator.getOffsetX(); + this.offsetZ = generator.getOffsetZ(); + this.zoom = generator.getZoom(); + this.surfaceDetail = generator.getSurfaceDetail(); + this.cliffHeightMin = generator.getCliffHeightMin(); + this.cliffHeightMax = generator.getCliffHeightMax(); + this.cellCracks = generator.getCellFractureHeight() != 0; + } + + public IrisGenerator generator() { + return generator; + } + + public void heights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + if (count == 0) { + return; + } + if (layers.length == 0) { + Arrays.fill(out, 0, count, 0D); + return; + } + if (surfaceDetail == 1D) { + sample(scratch, xs, zs, count, out); + return; + } + surfaceHeights(scratch, xs, zs, count, out); + } + + private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + int[] northWest = scratch.indices(count); + int[] northEast = scratch.indices(count); + int[] southWest = scratch.indices(count); + int[] southEast = scratch.indices(count); + double[] fractionX = scratch.buffer(count); + double[] fractionZ = scratch.buffer(count); + scratch.clearLattice(count * 4); + int detailed = 0; + for (int index = 0; index < count; index++) { + double lowerX = Math.floor(xs[index] / SURFACE_GRID) * SURFACE_GRID; + double lowerZ = Math.floor(zs[index] / SURFACE_GRID) * SURFACE_GRID; + double dx = (xs[index] - lowerX) / SURFACE_GRID; + double dz = (zs[index] - lowerZ) / SURFACE_GRID; + fractionX[index] = dx; + fractionZ[index] = dz; + northWest[index] = scratch.lattice(lowerX, lowerZ); + northEast[index] = dx == 0D ? -1 : scratch.lattice(lowerX + SURFACE_GRID, lowerZ); + southWest[index] = dz == 0D ? -1 : scratch.lattice(lowerX, lowerZ + SURFACE_GRID); + southEast[index] = dx == 0D || dz == 0D ? -1 : scratch.lattice(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID); + if (dx != 0D || dz != 0D) { + detailed++; + } + } + int nodes = scratch.latticeCount(); + double[] lattice = scratch.buffer(nodes); + resolveLattice(scratch, nodes, lattice); + double[] detail = null; + int[] detailIndex = null; + if (surfaceDetail != 0D && detailed > 0) { + double[] detailX = scratch.buffer(detailed); + double[] detailZ = scratch.buffer(detailed); + detail = scratch.buffer(detailed); + detailIndex = scratch.indices(count); + int cursor = 0; + for (int index = 0; index < count; index++) { + if (fractionX[index] != 0D || fractionZ[index] != 0D) { + detailX[cursor] = xs[index]; + detailZ[cursor] = zs[index]; + detailIndex[index] = cursor++; + } + } + sample(scratch, detailX, detailZ, detailed, detail); + } + for (int index = 0; index < count; index++) { + double dx = fractionX[index]; + double dz = fractionZ[index]; + double nw = lattice[northWest[index]]; + if (dx == 0D && dz == 0D) { + out[index] = nw; + continue; + } + double north = dx == 0D ? nw : IrisInterpolation.lerp(nw, lattice[northEast[index]], dx); + double smooth = north; + if (dz != 0D) { + double sw = lattice[southWest[index]]; + double south = dx == 0D ? sw : IrisInterpolation.lerp(sw, lattice[southEast[index]], dx); + smooth = IrisInterpolation.lerp(north, south, dz); + } + out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail; + } + scratch.resetIndices(indexMark); + scratch.reset(mark); + } + + private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) { + IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes(); + int indexMark = scratch.indexMark(); + int mark = scratch.mark(); + int[] missing = scratch.indices(nodes); + double[] missingX = scratch.buffer(nodes); + double[] missingZ = scratch.buffer(nodes); + int missingCount = 0; + for (int node = 0; node < nodes; node++) { + double x = scratch.latticeX(node); + double z = scratch.latticeZ(node); + if (x == (int) x && z == (int) z && IrisGenerator.cachedSurface(stripes, x, z, superSeed, lattice, node)) { + continue; + } + missing[missingCount] = node; + missingX[missingCount] = x; + missingZ[missingCount++] = z; + } + if (missingCount > 0) { + double[] sampled = scratch.buffer(missingCount); + sample(scratch, missingX, missingZ, missingCount, sampled); + for (int index = 0; index < missingCount; index++) { + int node = missing[index]; + lattice[node] = sampled[index]; + if (missingX[index] == (int) missingX[index] && missingZ[index] == (int) missingZ[index]) { + IrisGenerator.storeSurface(stripes, missingX[index], missingZ[index], superSeed, sampled[index]); + } + } + } + scratch.reset(mark); + scratch.resetIndices(indexMark); + } + + private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + int mark = scratch.mark(); + double[] sampleX = scratch.buffer(count); + double[] sampleZ = scratch.buffer(count); + double[] noise = scratch.buffer(count); + for (int index = 0; index < count; index++) { + sampleX[index] = (xs[index] + offsetX) / zoom; + sampleZ[index] = (zs[index] + offsetZ) / zoom; + } + Arrays.fill(out, 0, count, multiplicitive ? 1D : 0D); + for (NoiseLayerProgram layer : layers) { + layer.evaluate(scratch, sampleX, sampleZ, count, noise); + if (multiplicitive) { + for (int index = 0; index < count; index++) { + out[index] *= noise[index]; + } + } else { + for (int index = 0; index < count; index++) { + out[index] += noise[index]; + } + } + } + for (int index = 0; index < count; index++) { + double value = multiplicitive ? out[index] * opacity : (out[index] / totalOpacity) * opacity; + out[index] = Double.isNaN(value) ? 0D : value; + } + if (cliffs != null) { + cliffs.evaluate(scratch, sampleX, sampleZ, count, noise); + for (int index = 0; index < count; index++) { + double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, noise[index]); + out[index] = (Math.round((out[index] * 255D) / height) * height) / 255D; + } + } + if (cellCracks) { + for (int index = 0; index < count; index++) { + out[index] = generator.cell(xs[index], zs[index], out[index], 0D); + } + } + scratch.reset(mark); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java b/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java index 6ff6032fb..dd5ebd912 100644 --- a/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java @@ -41,10 +41,7 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; -import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.function.DoubleBinaryOperator; @Accessors(chain = true) @NoArgsConstructor @@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant { if (surfaceDetail == 1D) { return sampleHeight(rx, rz, superSeed, data, engine); } - return surfaceHeight(rx, rz, superSeed, data, engine, null); + return surfaceHeight(rx, rz, superSeed, data, engine); } public boolean supportsBatch() { @@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant { return !hasCliffs() || cliffHeightGenerator.supportsBatch(); } - public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) { - Objects.checkFromIndexSize(0, count, xs.length); - Objects.checkFromIndexSize(0, count, zs.length); - Objects.checkFromIndexSize(0, count, target.length); - if (count > 4096) { - throw new IllegalArgumentException("Generator batch exceeds 4096 samples"); + public GeneratorProgram compileProgram(long superSeed) { + if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) { + throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1"); } - if (count < 8 || !supportsBatch()) { - for (int index = 0; index < count; index++) { - target[index] = getHeight(xs[index], zs[index], superSeed); - } - return; - } - if (composite.isEmpty()) { - Arrays.fill(target, 0, count, 0D); - return; + if (!supportsBatch()) { + throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program"); } IrisData data = getLoader(); Engine engine = data == null ? null : data.getEngine(); - DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine); - for (int index = 0; index < count; index++) { - target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index]) - : surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared); - } - } - - private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) { int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ); - DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()]; + NoiseLayerProgram[] layers = new NoiseLayerProgram[composite.size()]; double totalOpacity = 0D; for (int index = 0; index < layers.length; index++) { IrisNoiseGenerator layer = composite.get(index); - layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine); + layers[index] = layer.compileProgram(getSeed() + superSeed + hc, data, engine); if (!multiplicitive) { totalOpacity += layer.getOpacity(); } } - double sumOpacity = totalOpacity; double cliffSeed = (double) (superSeed + 294596 + hc) - 34857; - DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch( - (long) (getSeed() + cliffSeed + hc), data, engine) : null; - return (rx, rz) -> { - double sampleX = (rx + offsetX) / zoom; - double sampleZ = (rz + offsetZ) / zoom; - double h = multiplicitive ? 1D : 0D; - for (DoubleBinaryOperator layer : layers) { - double sampled = layer.applyAsDouble(sampleX, sampleZ); - h = multiplicitive ? h * sampled : h + sampled; - } - double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity; - if (Double.isNaN(value)) { - value = 0D; - } - if (cliffs != null) { - double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, - cliffs.applyAsDouble(sampleX, sampleZ)); - value = (Math.round((value * 255D) / height) * height) / 255D; - } - return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value; - }; + NoiseLayerProgram cliffs = hasCliffs() + ? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine) + : null; + return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs); } - private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine, - DoubleBinaryOperator prepared) { + SurfaceCacheStripe[] surfaceStripes() { + IrisData data = getLoader(); + return surfaceCache(data, data == null ? null : data.getEngine()).stripes(); + } + + static boolean cachedSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double[] target, int index) { + long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); + long mixed = surfaceMix(key); + SurfaceSample cached = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)] + .get(key, superSeed, (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1)); + if (cached == null) { + return false; + } + target[index] = cached.height(); + return true; + } + + static void storeSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double height) { + long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); + long mixed = surfaceMix(key); + stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)] + .put((int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1), new SurfaceSample(key, superSeed, height)); + } + + private static long surfaceMix(long key) { + long mixed = key ^ (key >>> 33); + mixed *= 0xff51afd7ed558ccdL; + mixed ^= mixed >>> 33; + return mixed; + } + + private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) { SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes(); double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID; double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID; double dx = (rx - lowerX) / SURFACE_GRID; double dz = (rz - lowerZ) / SURFACE_GRID; - double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared); + double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine); if (dx == 0D && dz == 0D) { return northWest; } double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest, - cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx); + cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx); double smooth = north; if (dz != 0D) { - double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared); + double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine); double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest, - cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx); + cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx); smooth = IrisInterpolation.lerp(north, south, dz); } return surfaceDetail == 0D ? smooth - : smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail; + : smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail; } private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) { @@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant { return v; } - private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) { + private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) { if (x != (int) x || z != (int) z) { - return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z); + return sampleHeight(x, z, superSeed, data, engine); } long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); - long mixed = key ^ (key >>> 33); - mixed *= 0xff51afd7ed558ccdL; - mixed ^= mixed >>> 33; + long mixed = surfaceMix(key); SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]; int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1); SurfaceSample cached = stripe.get(key, superSeed, slot); if (cached != null) { return cached.height(); } - double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z); + double height = sampleHeight(x, z, superSeed, data, engine); stripe.put(slot, new SurfaceSample(key, superSeed, height)); return height; } @@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant { private record SurfaceSample(long key, long seed, double height) { } - private static final class SurfaceCacheStripe { + static final class SurfaceCacheStripe { private final AtomicReferenceArray entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE); private SurfaceSample get(long key, long seed, int slot) { diff --git a/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java b/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java index 2793c4c23..1ea234e8c 100644 --- a/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java @@ -39,6 +39,7 @@ import art.arcane.volmlib.util.noise.NoiseGenerator; import art.arcane.volmlib.util.noise.OffsetNoiseGenerator; import art.arcane.volmlib.util.noise.PerlinNoise; import art.arcane.volmlib.util.noise.SimplexNoise; +import art.arcane.volmlib.util.noise.program.NoiseProgram; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; @@ -47,8 +48,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.DoubleBinaryOperator; @Snippet("generator") @Accessors(chain = true) @@ -198,44 +199,29 @@ public class IrisNoiseGenerator { return true; } - DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) { + NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) { if (!enabled) { - double value = offsetY; - return (x, z) -> value; + return NoiseLayerProgram.disabled(this); } - if (hasDeepFracture()) { - FractureSamples samples = new FractureSamples(); - if (samples.prepare(this, superSeed, data, engine)) { - return samples::sample; + int enabledChildren = 0; + for (IrisNoiseGenerator child : fracture) { + if (child.isEnabled()) { + enabledChildren++; } } - DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()]; - double[] offsets = new double[fracture.size()]; + NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren]; + double[] offsets = new double[enabledChildren]; int salt = 33; - for (int index = 0; index < children.length; index++) { - IrisNoiseGenerator child = fracture.get(index); + int compiled = 0; + for (IrisNoiseGenerator child : fracture) { if (child.isEnabled()) { - children[index] = child.prepareBatch(superSeed + seed + salt, data, engine); - offsets[index] = child.getOpacity() / 2D; + children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine); + offsets[compiled++] = child.getOpacity() / 2D; } salt += 819; } - CNG generator = getGenerator(superSeed, data, engine); - return (x, z) -> samplePrepared(generator, children, offsets, x, z); - } - - private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets, - double xv, double zv) { - double x = xv; - double z = zv; - for (int index = 0; index < children.length; index++) { - DoubleBinaryOperator child = children[index]; - if (child != null) { - x += child.applyAsDouble(xv, zv) - offsets[index]; - z += child.applyAsDouble(zv, xv) - offsets[index]; - } - } - return sample(generator, x, z); + CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator"); + return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets); } public KList getAllComposites() { diff --git a/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java b/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java new file mode 100644 index 000000000..06c0571a4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java @@ -0,0 +1,137 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.noise.program.NoiseProgram; + +import java.util.Arrays; + +public final class NoiseLayerProgram { + private final boolean enabled; + private final double offsetY; + private final NoiseProgram program; + private final double zoom; + private final double opacity; + private final double offsetX; + private final double offsetZ; + private final double exponent; + private final boolean negative; + private final boolean parametric; + private final boolean bezier; + private final boolean sinCentered; + private final NoiseLayerProgram[] children; + private final double[] childOffsets; + + private NoiseLayerProgram(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) { + this.enabled = program != null; + this.offsetY = layer.getOffsetY(); + this.program = program; + this.zoom = layer.getZoom(); + this.opacity = layer.getOpacity(); + this.offsetX = layer.getOffsetX(); + this.offsetZ = layer.getOffsetZ(); + this.exponent = layer.getExponent(); + this.negative = layer.isNegative(); + this.parametric = layer.isParametric(); + this.bezier = layer.isBezier(); + this.sinCentered = layer.isSinCentered(); + this.children = children; + this.childOffsets = childOffsets; + } + + static NoiseLayerProgram disabled(IrisNoiseGenerator layer) { + return new NoiseLayerProgram(layer, null, new NoiseLayerProgram[0], new double[0]); + } + + static NoiseLayerProgram of(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) { + return new NoiseLayerProgram(layer, program, children, childOffsets); + } + + public void evaluate(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + if (!enabled) { + Arrays.fill(out, 0, count, offsetY); + return; + } + int mark = scratch.mark(); + double[] sampleX = scratch.buffer(count); + double[] sampleZ = scratch.buffer(count); + if (children.length == 0) { + for (int index = 0; index < count; index++) { + sampleX[index] = (xs[index] / zoom) + offsetX; + sampleZ[index] = (zs[index] / zoom) + offsetZ; + } + } else { + System.arraycopy(xs, 0, sampleX, 0, count); + System.arraycopy(zs, 0, sampleZ, 0, count); + double[] forward = scratch.buffer(count); + double[] reverse = scratch.buffer(count); + for (int child = 0; child < children.length; child++) { + children[child].evaluatePair(scratch, xs, zs, count, forward, reverse); + double offset = childOffsets[child]; + for (int index = 0; index < count; index++) { + sampleX[index] += forward[index] - offset; + sampleZ[index] += reverse[index] - offset; + } + } + for (int index = 0; index < count; index++) { + sampleX[index] = (sampleX[index] / zoom) + offsetX; + sampleZ[index] = (sampleZ[index] / zoom) + offsetZ; + } + } + program.fill2D(scratch.noise(), sampleX, sampleZ, count, out); + for (int index = 0; index < count; index++) { + out[index] = post(out[index]); + } + scratch.reset(mark); + } + + void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) { + if (!enabled) { + Arrays.fill(forward, 0, count, offsetY); + Arrays.fill(reverse, 0, count, offsetY); + return; + } + int mark = scratch.mark(); + int pairs = count * 2; + double[] sampleX = scratch.buffer(pairs); + double[] sampleZ = scratch.buffer(pairs); + System.arraycopy(xs, 0, sampleX, 0, count); + System.arraycopy(zs, 0, sampleZ, 0, count); + System.arraycopy(zs, 0, sampleX, count, count); + System.arraycopy(xs, 0, sampleZ, count, count); + if (children.length > 0) { + double[] childForward = scratch.buffer(count); + double[] childReverse = scratch.buffer(count); + for (int child = 0; child < children.length; child++) { + children[child].evaluatePair(scratch, xs, zs, count, childForward, childReverse); + double offset = childOffsets[child]; + for (int index = 0; index < count; index++) { + sampleX[index] += childForward[index] - offset; + sampleZ[index] += childReverse[index] - offset; + sampleX[count + index] += childReverse[index] - offset; + sampleZ[count + index] += childForward[index] - offset; + } + } + } + for (int index = 0; index < pairs; index++) { + sampleX[index] = (sampleX[index] / zoom) + offsetX; + sampleZ[index] = (sampleZ[index] / zoom) + offsetZ; + } + double[] values = scratch.buffer(pairs); + program.fill2D(scratch.noise(), sampleX, sampleZ, pairs, values); + for (int index = 0; index < count; index++) { + forward[index] = post(values[index]); + reverse[index] = post(values[count + index]); + } + scratch.reset(mark); + } + + private double post(double raw) { + double n = raw * opacity; + n = negative ? (-n + opacity) : n; + n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY; + n = parametric ? IrisInterpolation.parametric(n, 2D) : n; + n = bezier ? IrisInterpolation.bezier(n) : n; + n = sinCentered ? IrisInterpolation.sinCenter(n) : n; + return n; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java b/core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java new file mode 100644 index 000000000..9cdaaa6eb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java @@ -0,0 +1,115 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.noise.program.NoiseScratch; + +import java.util.Arrays; + +public final class TerrainBatchScratch { + private static final int MINIMUM_BUFFER = 256; + + private final NoiseScratch noise = new NoiseScratch(); + private double[][] buffers = new double[32][]; + private int[][] indexBuffers = new int[8][]; + private int top; + private int indexTop; + private long[] latticeX = new long[64]; + private long[] latticeZ = new long[64]; + private int[] latticeSlots = new int[64]; + private int latticeCapacity = 64; + private int latticeCount; + + public NoiseScratch noise() { + return noise; + } + + public int mark() { + return top; + } + + public int indexMark() { + return indexTop; + } + + public void reset(int mark) { + top = mark; + } + + public void resetIndices(int mark) { + indexTop = mark; + } + + public double[] buffer(int length) { + if (top == buffers.length) { + buffers = Arrays.copyOf(buffers, top * 2); + } + double[] buffer = buffers[top]; + if (buffer == null || buffer.length < length) { + buffer = new double[Math.max(length, MINIMUM_BUFFER)]; + buffers[top] = buffer; + } + top++; + return buffer; + } + + public int[] indices(int length) { + if (indexTop == indexBuffers.length) { + indexBuffers = Arrays.copyOf(indexBuffers, indexTop * 2); + } + int[] buffer = indexBuffers[indexTop]; + if (buffer == null || buffer.length < length) { + buffer = new int[Math.max(length, MINIMUM_BUFFER)]; + indexBuffers[indexTop] = buffer; + } + indexTop++; + return buffer; + } + + void clearLattice(int maximumEntries) { + int capacity = Integer.highestOneBit(Math.max(16, maximumEntries * 2) - 1) << 1; + if (latticeSlots.length < capacity) { + latticeSlots = new int[capacity]; + } else { + Arrays.fill(latticeSlots, 0, capacity, 0); + } + latticeCapacity = capacity; + latticeCount = 0; + } + + int lattice(double x, double z) { + long xBits = Double.doubleToRawLongBits(x); + long zBits = Double.doubleToRawLongBits(z); + long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31); + mixed ^= mixed >>> 29; + int mask = latticeCapacity - 1; + int slot = (int) mixed & mask; + while (true) { + int stored = latticeSlots[slot]; + if (stored == 0) { + if (latticeCount == latticeX.length) { + latticeX = Arrays.copyOf(latticeX, latticeCount * 2); + latticeZ = Arrays.copyOf(latticeZ, latticeCount * 2); + } + latticeX[latticeCount] = xBits; + latticeZ[latticeCount] = zBits; + latticeSlots[slot] = ++latticeCount; + return latticeCount - 1; + } + if (latticeX[stored - 1] == xBits && latticeZ[stored - 1] == zBits) { + return stored - 1; + } + slot = (slot + 1) & mask; + } + } + + int latticeCount() { + return latticeCount; + } + + double latticeX(int index) { + return Double.longBitsToDouble(latticeX[index]); + } + + double latticeZ(int index) { + return Double.longBitsToDouble(latticeZ[index]); + } +} 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 0b8255c20..52c6b8c4f 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 @@ -18,6 +18,19 @@ package art.arcane.iris.generation.runtime; +import art.arcane.iris.generation.bake.BakePlane; +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.PlaneSource; +import art.arcane.iris.generation.bake.SparseHeightMemo; +import art.arcane.iris.generation.bake.TerrainProgram; +import art.arcane.iris.generation.noise.TerrainBatchScratch; +import art.arcane.iris.generation.simd.SimdSupport; +import art.arcane.iris.generation.stream.BakedDoubleStream; +import art.arcane.iris.generation.stream.BakedHeightStream; +import art.arcane.iris.generation.stream.BakedStream; import art.arcane.iris.generation.stream.GenerationStreams; import art.arcane.iris.generation.stream.CachedDoubleStream2D; import art.arcane.iris.generation.stream.CachedStream2D; @@ -118,14 +131,15 @@ import java.nio.file.Path; import java.util.function.BiFunction; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) -@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) +@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288; + private static final int SPARSE_BASE_HEIGHT_CAPACITY = 1 << 17; /** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */ private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20; /** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */ @@ -141,6 +155,7 @@ public class IrisComplex implements DataProvider { IrisGenerator::getLoadKey, Comparator.nullsFirst(Comparator.naturalOrder()) ); + private static final ThreadLocal BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new); private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = { InferredType.LAND, InferredType.CAVE, @@ -153,6 +168,35 @@ public class IrisComplex implements DataProvider { private final int biomeBoundsSamplingStep; private transient volatile SharedCornerBounds sharedCornerBounds; @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient volatile TerrainProgram terrainProgram; + @Getter(AccessLevel.NONE) + private final transient ChunkBakeCache bakeCache; + @Getter(AccessLevel.NONE) + private final transient SparseHeightMemo sparseBaseHeights = new SparseHeightMemo(SPARSE_BASE_HEIGHT_CAPACITY); + @Getter(AccessLevel.NONE) + private final transient BakeRegistry biomeRegistry = new BakeRegistry<>(); + @Getter(AccessLevel.NONE) + private final transient BakeRegistry regionRegistry = new BakeRegistry<>(); + @Getter(AccessLevel.NONE) + private final transient BakeRegistry blockRegistry = new BakeRegistry<>(); + @Getter(AccessLevel.NONE) + private final transient ProceduralStream regionSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream baseBiomeSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream caveSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream rockSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream fluidSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream naturalHeightSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream naturalTrueBiomeSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream trueBiomeSource; + @Getter(AccessLevel.NONE) private final transient IrisInterpolator[] frozenInterpolators; @Getter(AccessLevel.NONE) private final transient IrisGenerator[][] frozenGenerators; @@ -213,9 +257,6 @@ public class IrisComplex implements DataProvider { private ProceduralStream unblendedNaturalHeightStream; private final TransitionDisplacementField transitionDisplacement; private ProceduralStream heightStream; - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - private CachedDoubleStream2D cachedHeightStream; private ProceduralStream roundedHeighteightStream; private ProceduralStream maxHeightStream; private ProceduralStream overlayStream; @@ -365,54 +406,58 @@ public class IrisComplex implements DataProvider { return value; }); - rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() + bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize); + rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() .select(engine.getDimension().getRockPalette().getBlockData(data)); - fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() + rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource); + fluidSource = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() .select(engine.getDimension().getFluidPalette().getBlockData(data)); + fluidStream = new BakedStream<>(bakeCache, BakePlane.FLUID, fluidSource); hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology()); regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream() .zoom(engine.getDimension().getRegionZoom()); regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE); - regionStream = focusedRegions != null ? focusedRegions - : GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options( - engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)), - "regionStream", engine, cacheSize); - caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + regionSource = focusedRegions != null ? focusedRegions + : RegionSelection.select(new RegionSelection.Options( + engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)); + regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource); + caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE)) - .convertAware2D(ProceduralStream::get), "caveBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); + caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource); inferredStreams.put(InferredType.CAVE, caveBiomeStream); - landBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.LAND)) - .convertAware2D(ProceduralStream::get), "landBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.LAND, landBiomeStream); - seaBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.SEA)) - .convertAware2D(ProceduralStream::get), "seaBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SEA, seaBiomeStream); - shoreBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE)) - .convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) : - GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream() - .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize); + ContinentGenerator.create(engine.getDimension(), rng, getData()).stream() + .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND); ProceduralStream proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes : - GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) - .convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize); - baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME) - ? proceduralBaseBiomeStream : GenerationStreams.cache2D(ProceduralStream.of((x, z) -> { + bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) + .convertAware2D(this::implode); + baseBiomeSource = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME) + ? proceduralBaseBiomeStream : ProceduralStream.of((x, z) -> { IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped; }, (x, y, z) -> { IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped; - }, proceduralBaseBiomeStream), "imageMappedBaseBiomeStream", engine, cacheSize); + }, proceduralBaseBiomeStream); + baseBiomeStream = focusedBiomes != null ? focusedBiomes : new BakedStream<>(bakeCache, BakePlane.BASE_BIOME, baseBiomeSource); proceduralTerrainHeight = new ProceduralTerrainHeightSampler( (x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()), engine.getDimension().getTerrainSamplingStep()); - baseTerrainHeightStream = GenerationStreams.cache2DDouble( - ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight), - "baseTerrainHeightStream", engine, cacheSize); + baseTerrainHeightStream = new BakedDoubleStream(bakeCache, BakePlane.BASE_HEIGHT, + ProceduralStream.ofDouble(this::sparseBaseTerrainHeight)); boolean terrain3DEnabled = false; for (IrisBiome biome : generatorBiomes) { if (biome.getTerrain3D() != null) { @@ -427,18 +472,20 @@ public class IrisComplex implements DataProvider { new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(), fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE)); unblendedNaturalHeightStream = terrain3DEnabled - ? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize) + ? ProceduralStream.ofDouble(this::sampleTerrain3DHeight) : baseTerrainHeightStream; transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField( transitionGenerationPlan, new TransitionDisplacementField.Sources( (x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight())); - naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream - : GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height( - blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))), - "naturalHeightStream", engine, cacheSize); - naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) - : GenerationStreams.cache2D(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome, - Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize); + naturalHeightSource = transitionDisplacement == null + ? unblendedNaturalHeightStream + : ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height( + blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))); + naturalHeightStream = new BakedDoubleStream(bakeCache, BakePlane.NATURAL_HEIGHT, naturalHeightSource); + naturalTrueBiomeSource = focusedBiomes != null ? focusedBiomes + : ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome, + Interpolated.of(value -> 0D, value -> null)); + naturalTrueBiomeStream = new BakedStream<>(bakeCache, BakePlane.NATURAL_TRUE_BIOME, naturalTrueBiomeSource); IrisHydrology configuredHydrology = engine.getDimension().getHydrology(); if (hydrologyActive(configuredHydrology)) { hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext( @@ -446,9 +493,9 @@ public class IrisComplex implements DataProvider { engine.getHeight(), engine.getDimension(), data, - (x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight), + (x, z, naturalHeight) -> ChunkBakeCache.sparse(() -> sampleHydrologyNatural(engine, x, z, naturalHeight)), new HydrologyHeightProvider(), - (x, z) -> describeNaturalHeight(engine, x, z), + (x, z) -> ChunkBakeCache.sparse(() -> describeNaturalHeight(engine, x, z)), this::sampleNaturalOcean, footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint), () -> engine.getPlatformHooks().isMainThread(), @@ -467,8 +514,8 @@ public class IrisComplex implements DataProvider { Math.max(4096, cacheSize), data, banks3D)); } } - cachedHeightStream = new TerrainHeightStream(engine, cacheSize); - heightStream = cachedHeightStream; + heightStream = new BakedHeightStream(bakeCache, () -> terrainEngine.getPlatformHooks().isMainThread(), + this::nonblockingTerrainHeight, this::resolveHydrologyTerrainHeight); roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z)) .round(); slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z)) @@ -476,8 +523,9 @@ public class IrisComplex implements DataProvider { naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize); // Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses. shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream; - trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream - .convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize); + trueBiomeSource = focusedBiomes != null ? focusedBiomes + : heightStream.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)); + trueBiomeStream = new BakedStream<>(bakeCache, BakePlane.TRUE_BIOME, trueBiomeSource); riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize); riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize); riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize); @@ -518,17 +566,11 @@ public class IrisComplex implements DataProvider { return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize; } - public synchronized void ensureTerrainNoiseCacheSize(int minimumChunks) { + public void ensureTerrainNoiseCacheSize(int minimumChunks) { if (minimumChunks <= 0) { throw new IllegalArgumentException("Terrain noise cache size must be positive"); } - CachedDoubleStream2D naturalCache = (CachedDoubleStream2D) naturalHeightStream; - if (naturalCache.getMaxSize() < minimumChunks * 256L) { - naturalCache.setMaximumChunks(minimumChunks); - } - if (cachedHeightStream.getMaxSize() < minimumChunks * 256L) { - cachedHeightStream.setMaximumChunks(minimumChunks); - } + bakeCache.ensureCapacity(minimumChunks); } void expandHydrologyNoiseCaches() { @@ -787,96 +829,220 @@ public class IrisComplex implements DataProvider { return true; } - private void sampleBaseHeightBatch(long[] coordinates, int count, double[] target) { - ((CachedDoubleStream2D) baseTerrainHeightStream).getBatch(coordinates, count, target, this::resolveBaseHeightBatch); + private void resolveBaseHeightBatch(long[] missing, int size, double[] values) { + TerrainBatchScratch scratch = BATCH_SCRATCH.get(); + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + try { + double[] xs = scratch.buffer(size); + double[] zs = scratch.buffer(size); + double[] lows = scratch.buffer(size); + double[] highs = scratch.buffer(size); + Arrays.fill(values, 0, size, 0D); + for (int index = 0; index < size; index++) { + xs[index] = RiverFootprint.unpackX(missing[index]); + zs[index] = RiverFootprint.unpackZ(missing[index]); + } + TerrainProgram program = terrainProgram(); + for (int group = 0; group < frozenInterpolators.length; group++) { + if (frozenGenerators[group].length == 0) { + continue; + } + for (int index = 0; index < size; 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, size, lows, highs, values); + } + for (int index = 0; index < size; index++) { + values[index] = finishBaseHeight(xs[index], zs[index], values[index]); + } + } finally { + scratch.reset(mark); + scratch.resetIndices(indexMark); + } } - private void resolveBaseHeightBatch(long[] missing, int size, double[] values) { - double[] xs = new double[size]; - double[] zs = new double[size]; - double[] lows = new double[size]; - double[] highs = new double[size]; - double[] activeXs = new double[size]; - double[] activeZs = new double[size]; - double[] noise = new double[size]; - double[] groupHeights = new double[size]; - int[] active = new int[size]; - for (int index = 0; index < size; index++) { - xs[index] = RiverFootprint.unpackX(missing[index]); - zs[index] = RiverFootprint.unpackZ(missing[index]); + private double[] bakeBaseHeights(int chunkX, int chunkZ) { + double[] values = new double[256]; + 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)); + } + return values; } - long seed = terrainEngine.getSeedManager().getHeight() + 239945; - for (int group = 0; group < frozenInterpolators.length; group++) { - IrisGenerator[] generators = frozenGenerators[group]; - if (generators.length == 0) { + TerrainBatchScratch scratch = BATCH_SCRATCH.get(); + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + try { + double[] xs = scratch.buffer(256); + double[] zs = scratch.buffer(256); + double[] lows = scratch.buffer(256); + double[] highs = scratch.buffer(256); + for (int index = 0; index < 256; index++) { + xs[index] = originX + (index & 15); + zs[index] = originZ + (index >> 4); + } + TerrainProgram program = terrainProgram(); + GridBoundsCache cache = gridBoundsCache.get(); + int grid = biomeBoundsSamplingStep; + boolean corners = ChunkCorners.supports(grid); + 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)) { + continue; + } + } else { + for (int index = 0; index < 256; 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); + } + for (int index = 0; index < 256; index++) { + values[index] = finishBaseHeight(xs[index], zs[index], values[index]); + } + } 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) { + int cells = 16 / grid; + int stride = cells + 1; + long[] packed = new long[stride * stride]; + boolean silent = true; + for (int row = 0; row < stride; row++) { + for (int column = 0; column < stride; column++) { + long corner = cornerBounds(cache, terrainEngine, frozenInterpolators[group], group, + frozenGenerators[group], originX + column * grid, originZ + row * grid); + packed[row * stride + column] = corner; + silent &= silentBounds(corner); + } + } + if (silent) { + return false; + } + for (int index = 0; index < 256; index++) { + double x = xs[index]; + double z = zs[index]; + int gx = ((int) Math.floor(x)) & ~(grid - 1); + int gz = ((int) Math.floor(z)) & ~(grid - 1); + double fx = (x - gx) / grid; + double fz = (z - gz) / grid; + int cornerIndex = ((gz - originZ) / grid) * stride + (gx - originX) / grid; + long b00 = packed[cornerIndex]; + if (fx == 0D && fz == 0D) { + lows[index] = boundsLow(b00); + highs[index] = boundsHigh(b00); + } else if (fz == 0D) { + long b10 = packed[cornerIndex + 1]; + lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz); + highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz); + } else if (fx == 0D) { + long b01 = packed[cornerIndex + stride]; + lows[index] = biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz); + highs[index] = biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz); + } else { + long b10 = packed[cornerIndex + 1]; + long b01 = packed[cornerIndex + stride]; + long b11 = packed[cornerIndex + stride + 1]; + lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz); + highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz); + } + } + return true; + } + + private boolean supportsChunkHeightBatch() { + return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() + && (biomeBuffet == null || biomeBuffet.cells().isEmpty()); + } + + private double finishBaseHeight(double x, double z, double interpolated) { + double height = Math.max(Math.min(interpolated + fluidHeight + overlayStream.get(x, z), terrainEngine.getHeight()), 0); + return imageMapRuntime.sampleTerrainHeight(x, z, height); + } + + private TerrainProgram terrainProgram() { + TerrainProgram program = terrainProgram; + if (program == null) { + synchronized (this) { + program = terrainProgram; + if (program == null) { + program = TerrainProgram.compile(frozenGenerators, terrainEngine.getSeedManager().getHeight() + 239945); + terrainProgram = program; + } + } + } + return program; + } + + private double sparseBaseTerrainHeight(double x, double z) { + int blockX = (int) x; + int blockZ = (int) z; + if (x != blockX || z != blockZ) { + return sampleUnblendedNaturalTerrainHeight(x, z); + } + double memoized = sparseBaseHeights.get(blockX, blockZ); + if (!Double.isNaN(memoized)) { + return memoized; + } + double height = sampleUnblendedNaturalTerrainHeight(x, z); + sparseBaseHeights.put(blockX, blockZ, height); + return height; + } + + private void sampleNaturalBatch(long[] coordinates, int count, double[] heights) { + long[] required = new long[count]; + int requiredCount = 0; + 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 + || terrain3D != null && !terrain3D.requiresBaseHeight(x, z) + || publishedPlane(BakePlane.BASE_HEIGHT, x, z) != null + || !Double.isNaN(sparseBaseHeights.get(x, z))) { continue; } - int activeCount = 0; - Arrays.fill(groupHeights, 0D); - for (int index = 0; index < size; index++) { - NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group, - generators, xs[index], zs[index]); - lows[index] = bounds.min(); - highs[index] = bounds.max(); - if (!Double.isFinite(lows[index]) || !Double.isFinite(highs[index])) { - averageGeneratorHeights(generators, lows[index], highs[index], xs[index], zs[index], seed); - } - if (lows[index] != highs[index]) { - active[activeCount] = index; - activeXs[activeCount] = xs[index]; - activeZs[activeCount++] = zs[index]; - } else { - for (int generator = 0; generator < generators.length; generator++) { - groupHeights[index] += lows[index]; - } - } - } - for (IrisGenerator generator : generators) { - if (activeCount == 0) { - break; - } - generator.getHeights(activeXs, activeZs, seed, noise, activeCount); - for (int sample = 0; sample < activeCount; sample++) { - int index = active[sample]; - if (!Double.isFinite(noise[sample])) { - throw new IllegalStateException("Terrain generator noise was not finite at " + xs[index] + "," + zs[index] - + " (generator=" + generator.getLoadKey() + ", noise=" + noise[sample] - + ", bounds=" + lows[index] + ".." + highs[index] + ")."); - } - groupHeights[index] += M.lerp(lows[index], highs[index], noise[sample]); - } - } - for (int index = 0; index < size; index++) { - values[index] += groupHeights[index] / generators.length; + required[requiredCount++] = coordinates[index]; + } + if (requiredCount > 0) { + double[] baseHeights = new double[requiredCount]; + resolveBaseHeightBatch(required, requiredCount, baseHeights); + for (int index = 0; index < requiredCount; index++) { + sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]); } } - for (int index = 0; index < size; index++) { - double height = Math.max(Math.min(values[index] + fluidHeight + overlayStream.get(xs[index], zs[index]), - terrainEngine.getHeight()), 0); - values[index] = imageMapRuntime.sampleTerrainHeight(xs[index], zs[index], height); + BakedDoubleStream naturalHeights = (BakedDoubleStream) naturalHeightStream; + for (int index = 0; index < count; index++) { + heights[index] = naturalHeights.sparse(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index])); } } - private void sampleUnblendedHeightBatch(long[] coordinates, int count, double[] target) { - if (terrain3D == null) { - sampleBaseHeightBatch(coordinates, count, target); - return; - } - ((CachedDoubleStream2D) unblendedNaturalHeightStream).getBatch(coordinates, count, target, (missing, size, values) -> { - long[] required = new long[size]; - int requiredCount = 0; - for (int index = 0; index < size; index++) { - if (terrain3D.requiresBaseHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]))) { - required[requiredCount++] = missing[index]; - } - } - if (requiredCount > 0) { - sampleBaseHeightBatch(required, requiredCount, new double[requiredCount]); - } - for (int index = 0; index < size; index++) { - values[index] = sampleTerrain3DHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[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 final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider { @@ -884,7 +1050,7 @@ public class IrisComplex implements DataProvider { @Override public double sample(int blockX, int blockZ) { - return naturalHeightStream.getDouble(blockX, blockZ); + return ((BakedDoubleStream) naturalHeightStream).sparse(blockX, blockZ); } @Override @@ -896,20 +1062,165 @@ public class IrisComplex implements DataProvider { public void sampleBatch(long[] coordinates, int count, double[] heights) { if (!batch) { IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights); - } else if (transitionDisplacement == null) { - sampleUnblendedHeightBatch(coordinates, count, heights); - } else { - ((CachedDoubleStream2D) naturalHeightStream).getBatch(coordinates, count, heights, (missing, size, values) -> { - sampleUnblendedHeightBatch(missing, size, values); - for (int index = 0; index < size; index++) { - values[index] = transitionDisplacement.height(RiverFootprint.unpackX(missing[index]), - RiverFootprint.unpackZ(missing[index]), values[index]); - } - }); + return; + } + ChunkBakeCache.enterSparse(); + try { + sampleNaturalBatch(coordinates, count, heights); + } finally { + ChunkBakeCache.exitSparse(); } } } + 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); + }; + } + + @Override + public ColumnBake assemble(ChunkPlanes planes) { + return assembleColumnBake(planes.chunkX(), planes.chunkZ()); + } + + @Override + public boolean mainThread() { + 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]; + 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)); + } + return values; + } + + private double[] heightPlane(int chunkX, int chunkZ) { + double[] values = new double[256]; + 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); + } + 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)); + } + return values; + } + } + + public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) { + return bakeCache.bake(plane, chunkX, chunkZ); + } + + public ColumnBake columnBake(int chunkX, int chunkZ) { + return bakeCache.get(chunkX, chunkZ); + } + + public ColumnBake columnBakeIfPresent(int chunkX, int chunkZ) { + return bakeCache.getIfPresent(chunkX, chunkZ); + } + + public IrisBiome biome(int id) { + return id < 0 ? null : biomeRegistry.value(id); + } + + public short biomeId(IrisBiome biome) { + return biome == null ? -1 : biomeRegistry.id(biome); + } + + public IrisRegion region(int id) { + return id < 0 ? null : regionRegistry.value(id); + } + + public short regionId(IrisRegion region) { + return region == null ? -1 : regionRegistry.id(region); + } + + public NativeBlockState blockState(int id) { + return id < 0 ? null : blockRegistry.value(id); + } + + private ColumnBake assembleColumnBake(int chunkX, int chunkZ) { + double[] height = (double[]) bakeCache.bake(BakePlane.HEIGHT, chunkX, chunkZ); + int[] rounded = new int[256]; + SimdSupport.kernels().roundToInt(height, rounded, 256); + int originX = chunkX << 4; + int originZ = chunkZ << 4; + float[] fluidSurface = new float[256]; + Terrain3DColumn[] terrainColumns = new Terrain3DColumn[256]; + for (int index = 0; index < 256; index++) { + int x = originX + (index & 15); + int z = originZ + (index >> 4); + fluidSurface[index] = (float) riverWaterSurfaceStream.getDouble(x, z); + terrainColumns[index] = terrainColumn(x, z); + } + double[] apron = new double[ColumnBake.APRON_WIDTH * ColumnBake.APRON_WIDTH]; + for (int row = 0; row < ColumnBake.APRON_WIDTH; row++) { + for (int column = 0; column < ColumnBake.APRON_WIDTH; column++) { + apron[row * ColumnBake.APRON_WIDTH + column] = heightStream.getDouble(originX - 1 + column, originZ - 1 + row); + } + } + 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), + height, + rounded, + fluidSurface, + ids((Object[]) bakeCache.bake(BakePlane.ROCK, chunkX, chunkZ), blockRegistry), + ids((Object[]) bakeCache.bake(BakePlane.FLUID, chunkX, chunkZ), blockRegistry), + terrainColumns, + apron)); + } + + @SuppressWarnings("unchecked") + private static short[] ids(Object[] values, BakeRegistry registry) { + short[] ids = new short[values.length]; + for (int index = 0; index < values.length; index++) { + ids[index] = values[index] == null ? -1 : registry.id((T) values[index]); + } + return ids; + } + private double sampleUnblendedNaturalTerrainHeight(double x, double z) { double proceduralHeight = proceduralTerrainHeight.sample(x, z); return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); @@ -2495,7 +2806,8 @@ public class IrisComplex implements DataProvider { this.engine = engine; this.generators = generators; this.cachedBounds = cachedBounds; - this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D; + this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream + || complex.baseBiomeStream instanceof CachedStream2D; this.inUse = true; if (sampleSource.get() != complex.baseBiomeStream) { sampleCache.clear(); @@ -2522,7 +2834,7 @@ public class IrisComplex implements DataProvider { double sampleZ = integerCoordinates ? (int) zz : zz; IrisBiome bx = sampleCache.get(sampleX, sampleZ); if (bx == null) { - bx = complex.baseBiomeStream.get(xx, zz); + bx = complex.sparseBaseBiome(xx, zz); sampleCache.put(sampleX, sampleZ, bx); } @@ -2592,62 +2904,6 @@ public class IrisComplex implements DataProvider { } } - private final class TerrainHeightStream extends CachedDoubleStream2D { - private TerrainHeightStream(Engine engine, int cacheSize) { - super("heightStream", engine, ProceduralStream.ofDouble(IrisComplex.this::resolveHydrologyTerrainHeight), cacheSize); - } - - @Override - public double getDouble(double x, double z) { - return terrainEngine.getPlatformHooks().isMainThread() - ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z); - } - - @Override - public Double get(double x, double y, double z) { - return getDouble(x, z); - } - - @Override - public double getDouble(double x, double y, double z) { - return getDouble(x, z); - } - - @Override - public void fillChunkDoubles(int worldX, int worldZ, double[] target) { - boolean mainThread = terrainEngine.getPlatformHooks().isMainThread(); - if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) { - super.fillChunkDoubles(worldX, worldZ, target); - return; - } - if (target == null || target.length != 256) { - throw new IllegalArgumentException("Expected a 16x16 target array."); - } - for (int index = 0; index < 256; index++) { - int x = worldX + (index & 15); - int z = worldZ + (index >> 4); - target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z); - } - } - - @Override - public void fillChunkRaw(int worldX, int worldZ, Object[] target) { - boolean mainThread = terrainEngine.getPlatformHooks().isMainThread(); - if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) { - super.fillChunkRaw(worldX, worldZ, target); - return; - } - if (target == null || target.length != 256) { - throw new IllegalArgumentException("Expected a 16x16 target array."); - } - for (int index = 0; index < 256; index++) { - int x = worldX + (index & 15); - int z = worldZ + (index >> 4); - target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z); - } - } - } - static final class ChildSelectionPlan { private final IrisRaritySelection selection; @@ -2698,6 +2954,8 @@ public class IrisComplex implements DataProvider { hydrologyRuntime.close(); } releaseHydrologyNoiseCaches(); + bakeCache.clear(); + sparseBaseHeights.clear(); if (transitionDisplacement != null) { transitionDisplacement.clear(); } 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 new file mode 100644 index 000000000..65ce01f5a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java @@ -0,0 +1,109 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.volmlib.util.stream.BasicStream; +import art.arcane.volmlib.util.stream.ProceduralStream; + +import java.util.Objects; + +public final class BakedDoubleStream extends BasicStream implements ChunkFillableStream2D, ChunkFillableDoubleStream2D { + private final ChunkBakeCache cache; + private final BakePlane plane; + private final ProceduralStream scalar; + + public BakedDoubleStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream scalar) { + super(); + this.cache = Objects.requireNonNull(cache, "Bake cache"); + this.plane = Objects.requireNonNull(plane, "Bake plane"); + this.scalar = Objects.requireNonNull(scalar, "Scalar source"); + } + + public BakePlane plane() { + return plane; + } + + public ProceduralStream scalar() { + return scalar; + } + + @Override + public Double get(double x, double z) { + return getDouble(x, z); + } + + @Override + 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)]; + } + ChunkBakeCache.enterSparse(); + try { + return scalar.getDouble(blockX, blockZ); + } finally { + ChunkBakeCache.exitSparse(); + } + } + + @Override + public Double get(double x, double y, double z) { + return scalar.get(x, y, z); + } + + @Override + public double getDouble(double x, double y, double z) { + return scalar.getDouble(x, y, z); + } + + @Override + public double toDouble(Double t) { + return t; + } + + @Override + public Double fromDouble(double d) { + return d; + } + + @Override + public void fillChunkDoubles(int worldX, int worldZ, double[] target) { + if (target == null || target.length != 256) { + throw new IllegalArgumentException("Expected a 16x16 target array."); + } + 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); + 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)); + } + } + + @Override + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + double[] values = new double[256]; + fillChunkDoubles(worldX, worldZ, values); + for (int index = 0; index < 256; index++) { + 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 new file mode 100644 index 000000000..9ae17fd53 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java @@ -0,0 +1,101 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.volmlib.util.stream.BasicStream; + +import java.util.Objects; +import java.util.function.BooleanSupplier; + +public final class BakedHeightStream extends BasicStream implements ChunkFillableStream2D, ChunkFillableDoubleStream2D { + private final ChunkBakeCache cache; + private final BooleanSupplier mainThread; + private final NoiseProvider nonblocking; + private final NoiseProvider resolved; + + public BakedHeightStream(ChunkBakeCache cache, BooleanSupplier mainThread, NoiseProvider nonblocking, NoiseProvider resolved) { + super(); + this.cache = Objects.requireNonNull(cache, "Bake cache"); + this.mainThread = Objects.requireNonNull(mainThread, "Main thread check"); + this.nonblocking = Objects.requireNonNull(nonblocking, "Nonblocking height"); + this.resolved = Objects.requireNonNull(resolved, "Resolved height"); + } + + @Override + public Double get(double x, double z) { + return getDouble(x, z); + } + + @Override + public double getDouble(double x, double z) { + 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)]; + } + + @Override + public Double get(double x, double y, double z) { + return getDouble(x, z); + } + + @Override + public double getDouble(double x, double y, double z) { + return getDouble(x, z); + } + + @Override + public double toDouble(Double t) { + return t; + } + + @Override + public Double fromDouble(double d) { + return d; + } + + @Override + public void fillChunkDoubles(int worldX, int worldZ, double[] target) { + if (target == null || target.length != 256) { + 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; + } + } + 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); + } + } + + @Override + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + double[] values = new double[256]; + fillChunkDoubles(worldX, worldZ, values); + for (int index = 0; index < 256; index++) { + target[index] = values[index]; + } + } + + 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); + } +} 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 new file mode 100644 index 000000000..0bb0a0a63 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java @@ -0,0 +1,89 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.volmlib.util.stream.BasicStream; +import art.arcane.volmlib.util.stream.ProceduralStream; + +import java.util.Objects; + +public final class BakedStream extends BasicStream implements ChunkFillableStream2D { + private final ChunkBakeCache cache; + private final BakePlane plane; + private final ProceduralStream scalar; + + public BakedStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream scalar) { + super(); + this.cache = Objects.requireNonNull(cache, "Bake cache"); + this.plane = Objects.requireNonNull(plane, "Bake plane"); + this.scalar = Objects.requireNonNull(scalar, "Scalar source"); + } + + public BakePlane plane() { + return plane; + } + + public ProceduralStream scalar() { + return scalar; + } + + @Override + @SuppressWarnings("unchecked") + 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)]; + } + ChunkBakeCache.enterSparse(); + try { + return scalar.get(blockX, blockZ); + } finally { + ChunkBakeCache.exitSparse(); + } + } + + @Override + public T get(double x, double y, double z) { + return scalar.get(x, y, z); + } + + @Override + public double toDouble(T t) { + return scalar.toDouble(t); + } + + @Override + public T fromDouble(double d) { + return scalar.fromDouble(d); + } + + @Override + 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); + 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)); + } + } + + 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/noise/GeneratorProgramParityTest.java b/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java new file mode 100644 index 000000000..bb2df6c93 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java @@ -0,0 +1,140 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; + +public class GeneratorProgramParityTest { + private static final NoiseStyle[] STYLES = { + NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS, NoiseStyle.IRIS_HALF, NoiseStyle.FRACTAL_SMOKE, + NoiseStyle.FRACTAL_WATER, NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.NOWHERE, + NoiseStyle.CELLULAR_IRIS_DOUBLE, NoiseStyle.SIMPLEX_VASCULAR, NoiseStyle.FLAT, NoiseStyle.STATIC, + NoiseStyle.GLOB_IRIS_THICK, NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.PERLIN_IRIS + }; + + @Test + public void programHeightsMatchScalarHeightsBitForBit() { + SplittableRandom random = new SplittableRandom(0x5EED1E55L); + TerrainBatchScratch scratch = new TerrainBatchScratch(); + for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) { + long shapeSeed = random.nextLong(); + IrisGenerator scalar = generator(new SplittableRandom(shapeSeed)); + IrisGenerator batch = generator(new SplittableRandom(shapeSeed)); + long superSeed = random.nextLong(); + GeneratorProgram program = batch.compileProgram(superSeed); + int count = 1 + random.nextInt(600); + double[] xs = new double[count]; + double[] zs = new double[count]; + double[] out = new double[count]; + fillCoordinates(random, xs, zs, count); + program.heights(scratch, xs, zs, count, out); + for (int index = 0; index < count; index++) { + assertEquals("generator " + generatorIndex + " point " + xs[index] + "," + zs[index], + Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)), + Double.doubleToRawLongBits(out[index])); + } + } + } + + @Test + public void chunkGridsMatchScalarHeightsBitForBit() { + SplittableRandom random = new SplittableRandom(0xC0FFEEL); + TerrainBatchScratch scratch = new TerrainBatchScratch(); + double[] xs = new double[256]; + double[] zs = new double[256]; + double[] out = new double[256]; + for (int generatorIndex = 0; generatorIndex < 60; generatorIndex++) { + long shapeSeed = random.nextLong(); + IrisGenerator scalar = generator(new SplittableRandom(shapeSeed)); + IrisGenerator batch = generator(new SplittableRandom(shapeSeed)); + long superSeed = random.nextLong(); + GeneratorProgram program = batch.compileProgram(superSeed); + for (int chunk = 0; chunk < 4; chunk++) { + int originX = random.nextInt(-200_000, 200_000) << 4; + int originZ = random.nextInt(-200_000, 200_000) << 4; + for (int index = 0; index < 256; index++) { + xs[index] = originX + (index & 15); + zs[index] = originZ + (index >> 4); + } + program.heights(scratch, xs, zs, 256, out); + for (int index = 0; index < 256; index++) { + assertEquals(Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)), + Double.doubleToRawLongBits(out[index])); + } + } + } + } + + private static void fillCoordinates(SplittableRandom random, double[] xs, double[] zs, int count) { + for (int index = 0; index < count; index++) { + xs[index] = coordinate(random); + zs[index] = random.nextInt(5) == 0 ? xs[index] : coordinate(random); + } + } + + private static double coordinate(SplittableRandom random) { + return switch (random.nextInt(6)) { + case 0 -> random.nextInt(-4096, 4096); + case 1 -> random.nextInt(-30_000_000, 30_000_000); + case 2 -> random.nextInt(-512, 512) + random.nextDouble(); + case 3 -> random.nextInt(-64, 64) * 6; + case 4 -> random.nextDouble(-30_000_000D, 30_000_000D); + default -> random.nextInt(-16, 16) * 0.25D; + }; + } + + private static IrisGenerator generator(SplittableRandom random) { + int layers = 1 + random.nextInt(3); + KList composite = new KList<>(); + for (int layer = 0; layer < layers; layer++) { + composite.add(layer(random, 0)); + } + double[] details = {0D, 0.25D, 0.5D, 1D}; + IrisGenerator generator = new IrisGenerator().setComposite(composite) + .setSurfaceDetail(details[random.nextInt(details.length)]) + .setMultiplicitive(random.nextInt(4) == 0) + .setSeed(random.nextLong(-5000L, 5000L)) + .setZoom(0.2D + random.nextDouble() * 3D) + .setOffsetX(random.nextInt(-40, 40) * 0.25D) + .setOffsetZ(random.nextInt(-40, 40) * 0.25D) + .setOpacity(0.1D + random.nextDouble()); + if (random.nextInt(3) == 0) { + generator.setCliffHeightMin(random.nextDouble() * 4D).setCliffHeightMax(4D + random.nextDouble() * 12D) + .setCliffHeightGenerator(layer(random, 2)); + } + if (random.nextInt(5) == 0) { + generator.setCellFractureHeight(random.nextDouble()).setCellFractureZoom(0.5D + random.nextDouble()); + } + return generator; + } + + private static IrisNoiseGenerator layer(SplittableRandom random, int depth) { + IrisNoiseGenerator layer = new IrisNoiseGenerator() + .setStyle(STYLES[random.nextInt(STYLES.length)].style()) + .setSeed(random.nextLong(-100_000L, 100_000L)) + .setZoom(0.3D + random.nextDouble() * 4D) + .setOpacity(0.05D + random.nextDouble()) + .setOffsetX(random.nextInt(-20, 20) * 0.5D) + .setOffsetZ(random.nextInt(-20, 20) * 0.5D) + .setOffsetY(random.nextInt(4) == 0 ? random.nextDouble() * 0.1D : 0D) + .setNegative(random.nextInt(4) == 0) + .setExponent(random.nextInt(4) == 0 ? 0.5D + random.nextDouble() : 1D) + .setParametric(random.nextInt(6) == 0) + .setBezier(random.nextInt(6) == 0) + .setSinCentered(random.nextInt(8) == 0) + .setOctaves(random.nextInt(5) == 0 ? 2 : 1) + .setEnabled(depth == 0 || random.nextInt(8) != 0); + if (depth < 4 && random.nextInt(3) != 0) { + int children = random.nextInt(4) == 0 ? 2 + random.nextInt(2) : 1; + KList fracture = new KList<>(); + for (int child = 0; child < children; child++) { + fracture.add(layer(random, depth + 1)); + } + layer.setFracture(fracture); + } + return layer; + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java b/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java index 7d984d9da..144f8da56 100644 --- a/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java +++ b/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java @@ -3,9 +3,6 @@ package art.arcane.iris.generation.noise; import art.arcane.volmlib.util.collection.KList; import org.junit.Test; -import java.util.ArrayList; -import java.util.List; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest { assertTrue(batch.supportsBatch()); double[] sampled = new double[XS.length]; for (long seed : seeds) { - batch.getHeights(XS, ZS, seed, sampled, sampled.length); + batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, sampled.length, sampled); for (int index = 0; index < sampled.length; index++) { assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index, Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)), @@ -54,7 +51,7 @@ public class IrisGeneratorBatchTest { IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D); double[] values = new double[XS.length]; for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) { - batch.getHeights(XS, ZS, seed, values, XS.length); + batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, XS.length, values); for (int index = 0; index < XS.length; index++) { assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)), Double.doubleToRawLongBits(values[index])); @@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest { } @Test - public void resolvesLayerContextOncePerBatch() { + public void resolvesLayerContextOncePerCompile() { IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style())); IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer)); double[] sampled = new double[XS.length]; - generator.getHeights(XS, ZS, 99L, sampled, XS.length); + GeneratorProgram program = generator.compileProgram(99L); verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull()); clearInvocations(layer); + program.heights(new TerrainBatchScratch(), XS, ZS, XS.length, sampled); + verify(layer, times(0)).getGenerator(anyLong(), isNull(), isNull()); for (int index = 0; index < XS.length; index++) { generator.getHeight(XS[index], ZS[index], 99L); } @@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest { } @Test - public void customGeneratorFallbackKeepsCoordinateOrder() { - List sampled = new ArrayList<>(); + public void customGeneratorsCannotCompile() { IrisGenerator generator = new IrisGenerator() { @Override public double getHeight(double x, double z, long seed) { - sampled.add(x); - return sampled.size(); + return x; } }; assertFalse(generator.supportsBatch()); - double[] values = new double[XS.length]; - generator.getHeights(XS, ZS, 0L, values, values.length); - for (int index = 0; index < values.length; index++) { - assertEquals(Double.valueOf(XS[index]), sampled.get(index)); - assertEquals(index + 1D, values[index], 0D); - } - assertThrows(IllegalArgumentException.class, - () -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097)); + assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L)); } @Test diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java index 1e0dbbac0..8f0829a1c 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java @@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest { try (Fixture fixture = new Fixture(pack())) { IrisBiome mapped = new IrisBiome(); when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped); - MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream(); - MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream(); - MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream(); assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32)); assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5)); assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32)); assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5)); - assertEquals(0L, bridge.getSize()); - assertEquals(0L, regions.getSize()); - assertEquals(0L, heights.getSize()); assertNull(fixture.complex.getSharedCornerBounds()); when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null); @@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest { int[] rounded = new int[256]; clearInvocations(fixture.hooks); cache.fillRounded(rounded); - verify(fixture.hooks, times(1)).isMainThread(); + verify(fixture.hooks, atMost(2)).isMainThread(); assertEquals(256, samples.get()); for (int index = 0; index < 256; index++) { int x = index & 15; @@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest { } clearInvocations(fixture.hooks); cache.fill(); - verify(fixture.hooks, times(1)).isMainThread(); + verify(fixture.hooks, atMost(2)).isMainThread(); assertEquals(256, samples.get()); } } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java index be0ae4f35..39576e976 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java @@ -2,6 +2,9 @@ package art.arcane.iris.generation.runtime; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.world.pregen.PregenPerformanceProfile; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.PlaneSource; +import art.arcane.iris.generation.bake.SparseHeightMemo; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.hydrology.HydrologyPlanner; import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings; @@ -48,17 +51,14 @@ public class IrisComplexHydrologyNoiseCacheTest { try (Fixture fixture = new Fixture(1_024)) { ProceduralStream wrapper = fixture.complex.getRawHeightStream(); assertEquals(13D, fixture.height.getDouble(18, 5), 0D); - assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D); PregenPerformanceProfile.apply(fixture.engine); - assertEquals(4_096L * 256L, fixture.height.getMaxSize()); - assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize()); + assertTrue(fixture.bakes.capacity() >= 4_096); assertEquals(1_024L * 256L, fixture.biomes.getMaxSize()); assertSame(wrapper, fixture.complex.getRawHeightStream()); assertEquals(47D, wrapper.getDouble(18, 5), 0D); assertEquals(13D, fixture.height.getDouble(18, 5), 0D); - assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D); } finally { PregenPerformanceProfile.restore(); IrisSettings.settings = previousSettings; @@ -75,7 +75,7 @@ public class IrisComplexHydrologyNoiseCacheTest { fixture.complex.ensureTerrainNoiseCacheSize(4_096); fixture.assertCapacities(32_768); - assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize()); + assertTrue(fixture.bakes.capacity() >= 4_096); assertEquals(reservedBytes, budget.reservedBytes()); fixture.complex.close(); assertEquals(0L, budget.reservedBytes()); @@ -88,7 +88,7 @@ public class IrisComplexHydrologyNoiseCacheTest { fixture.complex.ensureTerrainNoiseCacheSize(4_096); fixture.assertCapacities(65_536); - assertEquals(65_536L * 256L, fixture.rawHeight.getMaxSize()); + assertTrue(fixture.bakes.capacity() >= 65_536); } } @@ -297,7 +297,7 @@ public class IrisComplexHydrologyNoiseCacheTest { private final CachedStream2D regions; private final CachedStream2D bridge; private final CachedDoubleStream2D height; - private final CachedDoubleStream2D rawHeight; + private final ChunkBakeCache bakes; private Fixture(int capacity) throws Exception { services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class)); @@ -305,10 +305,11 @@ public class IrisComplexHydrologyNoiseCacheTest { regions = objectCache(capacity); bridge = objectCache(capacity); height = heightCache(capacity); - rawHeight = heightCache(capacity); + bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity); complex.setNaturalHeightStream(height); complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D)); - setField(complex, "cachedHeightStream", rawHeight); + setField(complex, "bakeCache", bakes); + setField(complex, "sparseBaseHeights", new SparseHeightMemo(16)); when(engine.getComplex()).thenReturn(complex); complex.setBaseBiomeStream(biomes); complex.setRegionStream(regions); From 1acda886b31728dba464ecd632b56bf233900a17 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 23:22:16 -0400 Subject: [PATCH 02/16] 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()); From 7408d02bbe5153388c554b69ef1ffb96f9a988af Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 23:27:01 -0400 Subject: [PATCH 03/16] Unboxed biome selection - Rarity selection streams pick from precomputed cumulative tables - Implode and surface biome fixes take primitive coordinates - Child selection reuses one derived child seed instead of a new RNG per level - Base biome selection reads bridge and inferred streams directly --- .../iris/generation/biome/IrisBiome.java | 9 ++- .../iris/generation/runtime/IrisComplex.java | 56 +++++++++------- .../iris/generation/stream/RarityStream.java | 64 +++++++++++++++++++ .../generation/terrain/RegionSelection.java | 3 +- .../runtime/IrisComplexSelfSelectionTest.java | 4 +- ...isCarveModifierInferenceIsolationTest.java | 6 +- 6 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java index e70c2ef91..b1e0cd05e 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java @@ -515,7 +515,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity { } public CNG getChildrenGenerator(RNG random, int sig, double scale) { - long seed = deriveChildStyleSeed(random, sig).getSeed(); + return getChildrenGenerator(deriveChildStyleSeed(random, sig).getSeed(), scale); + } + + public CNG getChildrenGenerator(long seed, double scale) { IrisData data = getLoader(); Engine engine = data == null ? null : data.getEngine(); CachedChildGenerator recent = recentChildGenerator; @@ -533,6 +536,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity { return random.nextParallelRNG(signature * CHILDREN_SEED_SALT); } + public static long childStyleSeed(RNG random, int signature) { + return deriveChildStyleSeed(random, signature).getSeed(); + } + public KList generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex); } 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 027e2c8e6..4c65412e2 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 @@ -33,6 +33,7 @@ import art.arcane.iris.generation.stream.BakedDoubleStream; import art.arcane.iris.generation.stream.BakedHeightStream; import art.arcane.iris.generation.stream.BakedStream; import art.arcane.iris.generation.stream.GenerationStreams; +import art.arcane.iris.generation.stream.RarityStream; import art.arcane.iris.generation.stream.CachedDoubleStream2D; import art.arcane.iris.generation.stream.CachedStream2D; import art.arcane.iris.generation.stream.ProvisionalSampling; @@ -132,8 +133,8 @@ import java.nio.file.Path; import java.util.function.BiFunction; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) -@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) +@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); @@ -182,6 +183,10 @@ public class IrisComplex implements DataProvider { @Getter(AccessLevel.NONE) private final transient BakeRegistry blockRegistry = new BakeRegistry<>(); @Getter(AccessLevel.NONE) + private final transient long childStyleSeed; + @Getter(AccessLevel.NONE) + private final transient Map> inferredByType; + @Getter(AccessLevel.NONE) private final transient ProceduralStream regionSource; @Getter(AccessLevel.NONE) private final transient ProceduralStream baseBiomeSource; @@ -321,6 +326,7 @@ public class IrisComplex implements DataProvider { IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE); UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes()); this.rng = new RNG(engine.getSeedManager().getComplex()); + childStyleSeed = IrisBiome.childStyleSeed(rng, 123); this.data = engine.getData(); imageMapRuntime = IrisImageMapRuntime.compile(engine); double height = engine.getMaxHeight(); @@ -334,7 +340,8 @@ public class IrisComplex implements DataProvider { boolean buffetActive = biomeBuffet != null && !biomeBuffet.cells().isEmpty(); focusBiome = buffetActive ? null : compatUsable(engine.getFocus()); focusRegion = buffetActive ? null : compatUsable(engine.getFocusRegion()); - Map> inferredStreams = new HashMap<>(); + Map> inferredStreams = new EnumMap<>(InferredType.class); + inferredByType = inferredStreams; KList preparedRegions = new KList<>(); if (focusBiome != null) { @@ -442,9 +449,10 @@ public class IrisComplex implements DataProvider { bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) : ContinentGenerator.create(engine.getDimension(), rng, getData()).stream() .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND); - ProceduralStream proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes : - bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) - .convertAware2D(this::implode); + ProceduralStream proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes + : ProceduralStream.of(this::proceduralBaseBiome, + (x, y, z) -> implode(inferredByType.get(bridgeStream.get(x, y, z)).get(x, z), x, z), + Interpolated.of(value -> 0D, value -> null)); baseBiomeSource = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME) ? proceduralBaseBiomeStream : ProceduralStream.of((x, z) -> { IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); @@ -1103,6 +1111,10 @@ public class IrisComplex implements DataProvider { } } + private IrisBiome proceduralBaseBiome(double x, double z) { + return implode(inferredByType.get(bridgeStream.get(x, z)).get(x, z), x, z); + } + 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)); @@ -1367,29 +1379,29 @@ public class IrisComplex implements DataProvider { IrisBiome emptyBiome ) { return switch (inferredType) { - case CAVE -> engine.getDimension().getCaveBiomeStyle() + case CAVE -> new RarityStream<>(engine.getDimension().getCaveBiomeStyle() .create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) - .zoom(region.getCaveBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE)) + .zoom(region.getCaveBiomeZoom()), + loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE)) .onNull(emptyBiome); - case LAND -> engine.getDimension().getLandBiomeStyle() + case LAND -> new RarityStream<>(engine.getDimension().getLandBiomeStyle() .create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) .zoom(engine.getDimension().getLandZoom()) - .zoom(region.getLandBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND)); - case SEA -> engine.getDimension().getSeaBiomeStyle() + .zoom(region.getLandBiomeZoom()), + loadInferredBiomes(region.getLandBiomes(), InferredType.LAND)); + case SEA -> new RarityStream<>(engine.getDimension().getSeaBiomeStyle() .create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) .zoom(engine.getDimension().getSeaZoom()) - .zoom(region.getSeaBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA)); - case SHORE -> engine.getDimension().getShoreBiomeStyle() + .zoom(region.getSeaBiomeZoom()), + loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA)); + case SHORE -> new RarityStream<>(engine.getDimension().getShoreBiomeStyle() .create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) - .zoom(region.getShoreBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE)); + .zoom(region.getShoreBiomeZoom()), + loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE)); }; } @@ -2020,7 +2032,7 @@ public class IrisComplex implements DataProvider { return biomeKey == null ? sample.parentBiomeKey() : biomeKey; } - private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) { + private IrisBiome fixBiomeType(double height, IrisBiome biome, IrisRegion region, double x, double z, double fluidHeight) { IrisBiome resolved = resolveSurfaceBiome( height, biome, @@ -2633,7 +2645,7 @@ public class IrisComplex implements DataProvider { return computed; } - private IrisBiome implode(IrisBiome b, Double x, Double z) { + private IrisBiome implode(IrisBiome b, double x, double z) { if (b.getChildren().isEmpty()) { return b; } @@ -2641,7 +2653,7 @@ public class IrisComplex implements DataProvider { return implode(b, x, z, 3); } - private IrisBiome implode(IrisBiome b, Double x, Double z, int max) { + private IrisBiome implode(IrisBiome b, double x, double z, int max) { if (max < 0) { return b; } @@ -2650,7 +2662,7 @@ public class IrisComplex implements DataProvider { return b; } - CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor()); + CNG childCell = b.getChildrenGenerator(childStyleSeed, b.getChildShrinkFactor()); ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b); IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType()); if (biome == b && hasFixedNoise(b.getChildStyle())) { diff --git a/core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java b/core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java new file mode 100644 index 000000000..2d85e06da --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java @@ -0,0 +1,64 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.volmlib.util.math.Rarity; +import art.arcane.volmlib.util.stream.BasicStream; +import art.arcane.volmlib.util.stream.ProceduralStream; + +import java.util.List; +import java.util.Objects; + +public final class RarityStream extends BasicStream { + private final ProceduralStream noise; + private final Object[] options; + private final double[] cumulative; + + public RarityStream(ProceduralStream noise, List options) { + super(); + this.noise = Objects.requireNonNull(noise, "Rarity noise"); + this.options = options.toArray(); + this.cumulative = new double[this.options.length]; + double buffer = 0; + for (int index = 0; index < this.options.length; index++) { + buffer += 1d / Rarity.get(this.options[index]); + cumulative[index] = buffer; + } + } + + @Override + public T get(double x, double z) { + return pick(noise.getDouble(x, z)); + } + + @Override + public T get(double x, double y, double z) { + return pick(noise.getDouble(x, y, z)); + } + + @Override + public double toDouble(T t) { + return 0; + } + + @Override + public T fromDouble(double d) { + return null; + } + + @SuppressWarnings("unchecked") + public T pick(double noiseValue) { + int count = options.length; + if (count == 0) { + return null; + } + if (count == 1) { + return (T) options[0]; + } + double threshold = cumulative[count - 1] * noiseValue; + for (int index = 0; index < count; index++) { + if (cumulative[index] >= threshold) { + return (T) options[index]; + } + } + return (T) options[count - 1]; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java b/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java index 99a1d2678..9c6234f9c 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java @@ -3,6 +3,7 @@ package art.arcane.iris.generation.terrain; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.image.IrisImageMapRuntime; import art.arcane.iris.generation.image.IrisImageMapApplication; +import art.arcane.iris.generation.stream.RarityStream; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.studio.generation.BiomeBuffetLayout; import art.arcane.volmlib.util.collection.KList; @@ -24,7 +25,7 @@ public final class RegionSelection { regions.add(region); } } - ProceduralStream selected = options.style().selectRarity(regions); + ProceduralStream selected = new RarityStream<>(options.style(), regions); if (!options.imageMaps().has(IrisImageMapApplication.REGION)) { return selected; } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java index 4d68fd070..b0e7aedd6 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java @@ -83,7 +83,7 @@ public class IrisComplexSelfSelectionTest { Field field = IrisComplex.class.getDeclaredField("childSelectionPlans"); field.setAccessible(true); field.set(complex, plans); - implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, Double.class, Double.class, int.class); + implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, double.class, double.class, int.class); implode.setAccessible(true); } @@ -106,7 +106,7 @@ public class IrisComplexSelfSelectionTest { } @Override - public CNG getChildrenGenerator(RNG rng, int signature, double scale) { + public CNG getChildrenGenerator(long seed, double scale) { return noise; } } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java index ca9f39398..d0eb081fb 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java @@ -97,11 +97,11 @@ public class IrisCarveModifierInferenceIsolationTest { doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble()); Method fixBiomeType = IrisComplex.class.getDeclaredMethod( "fixBiomeType", - Double.class, + double.class, IrisBiome.class, IrisRegion.class, - Double.class, - Double.class, + double.class, + double.class, double.class ); fixBiomeType.setAccessible(true); From ab69be59c7e4a7095149a5b829ea3268f4a89809 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 23:29:29 -0400 Subject: [PATCH 04/16] Rarity and capacity checks - Rarity stream picks are property-tested against the VolmLib rarity selection - Pregen capacity growth scales the bake by the same factor as engine start --- .../iris/generation/runtime/IrisComplex.java | 5 +- .../generation/stream/RarityStreamTest.java | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index 4c65412e2..db6dface3 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 @@ -142,6 +142,7 @@ public class IrisComplex implements DataProvider { private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288; private static final int SPARSE_BASE_HEIGHT_CAPACITY = 1 << 17; + private static final int BAKE_CHUNKS_PER_NOISE_CHUNK = 8; /** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */ private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20; /** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */ @@ -414,7 +415,7 @@ public class IrisComplex implements DataProvider { return value; }); - bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * 8); + bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * BAKE_CHUNKS_PER_NOISE_CHUNK); rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() .select(engine.getDimension().getRockPalette().getBlockData(data)); rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource); @@ -579,7 +580,7 @@ public class IrisComplex implements DataProvider { if (minimumChunks <= 0) { throw new IllegalArgumentException("Terrain noise cache size must be positive"); } - bakeCache.ensureCapacity(minimumChunks); + bakeCache.ensureCapacity(minimumChunks * BAKE_CHUNKS_PER_NOISE_CHUNK); } void expandHydrologyNoiseCaches() { diff --git a/core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java b/core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java new file mode 100644 index 000000000..c3dee77dd --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java @@ -0,0 +1,52 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.volmlib.util.math.Rarity; +import art.arcane.volmlib.util.stream.ProceduralStream; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.SplittableRandom; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class RarityStreamTest { + @Test + public void picksMatchRarityStreamSelectionForEveryNoiseValue() { + SplittableRandom random = new SplittableRandom(4242L); + for (int trial = 0; trial < 200; trial++) { + List