From 67fd9046ba656e6917853a3112eee25787afe746 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 22 Oct 2026 07:14:09 -0400 Subject: [PATCH 1/2] Baked carve field reads - ChunkBakeCache.fillColumns batches the missing requested columns of one chunk plane and publishes them - BakedDoubleStream and BakedStream expose fillColumns with the per-column fallback when memoization is not allowed - Carve profile field reads heights, regions, surface and cave biomes from the nine overlapped chunk planes - Fluid presence samples the fluid plane only for columns below the fluid height - Carving reuses one dimension carving resolver state per thread scratch - Bake recent-entry memo holds a 16-slot per-thread chunk table instead of one entry --- .../iris/generation/bake/ChunkBakeCache.java | 40 ++- .../iris/generation/bake/ChunkPlanes.java | 49 ++++ .../mantle/MantleCarvingComponent.java | 119 +++++++-- .../generation/stream/BakedDoubleStream.java | 15 ++ .../iris/generation/stream/BakedStream.java | 15 ++ .../generation/bake/ChunkBakeCacheTest.java | 151 +++++++++++ .../MantleCarvingComponentBakedFieldTest.java | 242 ++++++++++++++++++ 7 files changed, 597 insertions(+), 34 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvingComponentBakedFieldTest.java 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 502e27596..cb1c13dab 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 @@ -3,6 +3,7 @@ package art.arcane.iris.generation.bake; import art.arcane.iris.generation.cache.ConcurrentClockCache; import art.arcane.iris.generation.stream.ProvisionalSampling; +import java.util.Arrays; import java.util.Objects; public final class ChunkBakeCache { @@ -10,6 +11,7 @@ public final class ChunkBakeCache { static final int MAXIMUM_CAPACITY = 32_768; static final long BYTES_PER_ENTRY = 13L * 1_024L; static final int HEAP_SHARE = 32; + private static final int RECENT_SLOTS = 16; private static final ThreadLocal SPARSE = ThreadLocal.withInitial(() -> new int[1]); private final PlaneSource source; @@ -52,17 +54,16 @@ public final class ChunkBakeCache { public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) { long key = key(chunkX, chunkZ); + int slot = Recent.slot(chunkX, chunkZ); Recent last = recent.get(); int current = generation; - ChunkPlanes planes; - if (last.key == key && last.generation == current) { - planes = last.planes; - } else { + ChunkPlanes planes = last.get(key, slot, current); + if (planes == null) { planes = entries.get(key); if (planes == null) { return null; } - last.remember(key, planes, current); + last.remember(key, slot, planes, current); } return planes.planeIfPresent(plane); } @@ -75,6 +76,10 @@ public final class ChunkBakeCache { return remembered(chunkX, chunkZ).complete(plane, source); } + public void fillColumns(BakePlane plane, int chunkX, int chunkZ, long[] columns, Object target) { + remembered(chunkX, chunkZ).plane(plane).fillColumns(source, chunkX, chunkZ, columns, target); + } + public boolean memoAllowed() { return SPARSE.get()[0] == 0 && !source.mainThread(); } @@ -102,7 +107,7 @@ public final class ChunkBakeCache { private ChunkPlanes remembered(int chunkX, int chunkZ) { int current = generation; ChunkPlanes planes = planes(chunkX, chunkZ); - recent.get().remember(key(chunkX, chunkZ), planes, current); + recent.get().remember(key(chunkX, chunkZ), Recent.slot(chunkX, chunkZ), planes, current); return planes; } @@ -123,14 +128,25 @@ public final class ChunkBakeCache { } private static final class Recent { - private long key; + private final long[] keys = new long[RECENT_SLOTS]; + private final ChunkPlanes[] planes = new ChunkPlanes[RECENT_SLOTS]; private int generation = -1; - private ChunkPlanes planes; - private void remember(long key, ChunkPlanes planes, int generation) { - this.key = key; - this.planes = planes; - this.generation = generation; + private static int slot(int chunkX, int chunkZ) { + return ((chunkZ & 3) << 2) | (chunkX & 3); + } + + private ChunkPlanes get(long key, int slot, int generation) { + return this.generation == generation && keys[slot] == key ? planes[slot] : null; + } + + private void remember(long key, int slot, ChunkPlanes value, int generation) { + if (this.generation != generation) { + Arrays.fill(planes, null); + this.generation = generation; + } + keys[slot] = key; + planes[slot] = value; } } } 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 42427fcec..beaf09b8e 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 @@ -145,6 +145,55 @@ public final class ChunkPlanes { return values; } + void fillColumns(PlaneSource source, int chunkX, int chunkZ, long[] columns, Object target) { + long[] missing = new long[4]; + boolean any = false; + if (!complete) { + for (int word = 0; word < 4; word++) { + missing[word] = columns[word] & ~(long) READY.getAcquire(ready, word); + any |= missing[word] != 0L; + } + } + if (!any) { + copyColumns(columns, values, target); + return; + } + long revision = ProvisionalSampling.revision(); + source.fill(kind, chunkX, chunkZ, missing, target); + long[] known = new long[4]; + for (int word = 0; word < 4; word++) { + known[word] = columns[word] & ~missing[word]; + } + copyColumns(known, values, target); + if (revision == ProvisionalSampling.revision()) { + publish(missing, target); + } + } + + private void publish(long[] missing, Object computed) { + copyColumns(missing, computed, values); + for (int word = 0; word < 4; word++) { + if (missing[word] != 0L) { + READY.getAndBitwiseOrRelease(ready, word, missing[word]); + } + } + } + + private void copyColumns(long[] columns, Object from, Object to) { + for (int word = 0; word < 4; word++) { + long bits = columns[word]; + while (bits != 0L) { + int index = (word << 6) | Long.numberOfTrailingZeros(bits); + if (kind.doubles()) { + ((double[]) to)[index] = ((double[]) from)[index]; + } else { + ((Object[]) to)[index] = ((Object[]) from)[index]; + } + bits &= bits - 1L; + } + } + } + private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) { long[] missing = missing(); Object computed = kind.doubles() ? new double[256] : new Object[256]; diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java index a04d02033..3844dab06 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java @@ -34,6 +34,8 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.iris.generation.stream.BakedDoubleStream; +import art.arcane.iris.generation.stream.BakedStream; import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D; import art.arcane.iris.generation.simd.SimdSupport; import art.arcane.volmlib.util.documentation.ChunkCoordinates; @@ -64,6 +66,12 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; private static final int[] KERNEL_DZ = new int[KERNEL_SIZE]; private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE]; + private static final int NEIGHBOURS = 9; + private static final int[] NEIGHBOUR_DX = new int[NEIGHBOURS]; + private static final int[] NEIGHBOUR_DZ = new int[NEIGHBOURS]; + private static final long[][] NEIGHBOUR_COLUMNS = new long[NEIGHBOURS][4]; + private static final int[] FIELD_NEIGHBOUR = new int[FIELD_SIZE * FIELD_SIZE]; + private static final int[] FIELD_COLUMN = new int[FIELD_SIZE * FIELD_SIZE]; private static final ThreadLocal BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); private final Object profileCarverLock = new Object(); @@ -81,6 +89,22 @@ public class MantleCarvingComponent extends IrisMantleComponent { kernelIndex++; } } + for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) { + NEIGHBOUR_DX[neighbour] = (neighbour / 3) - 1; + NEIGHBOUR_DZ[neighbour] = (neighbour % 3) - 1; + } + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int offsetX = fieldX - BLEND_RADIUS; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + int offsetZ = fieldZ - BLEND_RADIUS; + int neighbour = ((Math.floorDiv(offsetX, CHUNK_SIZE) + 1) * 3) + Math.floorDiv(offsetZ, CHUNK_SIZE) + 1; + int column = ((offsetZ & (CHUNK_SIZE - 1)) << 4) | (offsetX & (CHUNK_SIZE - 1)); + int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ; + FIELD_NEIGHBOUR[fieldIndex] = neighbour; + FIELD_COLUMN[fieldIndex] = column; + NEIGHBOUR_COLUMNS[neighbour][column >>> 6] |= 1L << (column & 63); + } + } } public MantleCarvingComponent(EngineMantle engineMantle) { @@ -128,7 +152,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) { IrisComplex complex = context.getComplex(); - IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + IrisDimensionCarvingResolver.State resolverState = blendScratch.resolverState; int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights); PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); List weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); @@ -422,7 +446,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) { int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS; int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS; - prefillProfileFieldSamples(startX, startZ, complex, blendScratch); + prefillProfileFieldSamples(chunkX, chunkZ, startX, startZ, complex, blendScratch); for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { int worldX = startX + fieldX; @@ -519,17 +543,28 @@ public class MantleCarvingComponent extends IrisMantleComponent { return resolved; } - private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { - fillFieldHeights(complex.getNaturalHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights); - Arrays.fill(blendScratch.fieldFluidHeights, complex.getFluidHeight()); - fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights, - blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid); - fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions); - fillFieldObjects(complex.getNaturalTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes); - fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes); + private void prefillProfileFieldSamples(int chunkX, int chunkZ, int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { + fillFieldHeights(complex.getNaturalHeightStream(), chunkX, chunkZ, startX, startZ, blendScratch); + double fluidHeight = complex.getFluidHeight(); + Arrays.fill(blendScratch.fieldFluidHeights, fluidHeight); + fillFieldFluidPresence(complex.getFluidStream(), chunkX, chunkZ, startX, startZ, Math.round(fluidHeight), blendScratch); + fillFieldObjects(complex.getRegionStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldRegions, blendScratch.neighbourObjects); + fillFieldObjects(complex.getNaturalTrueBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldSurfaceBiomes, blendScratch.neighbourObjects); + fillFieldObjects(complex.getCaveBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldCaveBiomes, blendScratch.neighbourObjects); } - private void fillFieldObjects(ProceduralStream stream, int startX, int startZ, T[] target) { + @SuppressWarnings("unchecked") + private void fillFieldObjects(ProceduralStream stream, int chunkX, int chunkZ, int startX, int startZ, T[] target, Object[][] neighbourObjects) { + if (stream instanceof BakedStream baked) { + for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) { + baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour], + NEIGHBOUR_COLUMNS[neighbour], neighbourObjects[neighbour]); + } + for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) { + target[fieldIndex] = (T) neighbourObjects[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]]; + } + return; + } for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { int worldX = startX + fieldX; for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { @@ -538,7 +573,19 @@ public class MantleCarvingComponent extends IrisMantleComponent { } } - private void fillFieldHeights(ProceduralStream stream, int startX, int startZ, double[] target) { + private void fillFieldHeights(ProceduralStream stream, int chunkX, int chunkZ, int startX, int startZ, BlendScratch blendScratch) { + double[] target = blendScratch.fieldSurfaceHeights; + if (stream instanceof BakedDoubleStream baked) { + double[][] neighbourHeights = blendScratch.neighbourHeights; + for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) { + baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour], + NEIGHBOUR_COLUMNS[neighbour], neighbourHeights[neighbour]); + } + for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) { + target[fieldIndex] = neighbourHeights[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]]; + } + return; + } for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { int worldX = startX + fieldX; for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { @@ -547,20 +594,41 @@ public class MantleCarvingComponent extends IrisMantleComponent { } } - private void fillFieldFluidPresence( - IrisComplex complex, - int startX, - int startZ, - double[] surfaceHeights, - double[] fluidHeights, - boolean[] target - ) { + private void fillFieldFluidPresence(ProceduralStream stream, int chunkX, int chunkZ, int startX, int startZ, + long roundedFluidHeight, BlendScratch blendScratch) { + double[] surfaceHeights = blendScratch.fieldSurfaceHeights; + boolean[] target = blendScratch.fieldHasFluid; + if (stream instanceof BakedStream baked) { + long[][] submerged = blendScratch.neighbourSubmerged; + for (long[] columns : submerged) { + Arrays.fill(columns, 0L); + } + for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) { + if (Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight) { + int column = FIELD_COLUMN[fieldIndex]; + submerged[FIELD_NEIGHBOUR[fieldIndex]][column >>> 6] |= 1L << (column & 63); + } + } + Object[][] neighbourObjects = blendScratch.neighbourObjects; + for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) { + long[] columns = submerged[neighbour]; + if ((columns[0] | columns[1] | columns[2] | columns[3]) != 0L) { + baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour], + columns, neighbourObjects[neighbour]); + } + } + for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) { + target[fieldIndex] = Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight + && B.isFluid((NativeBlockState) neighbourObjects[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]]); + } + return; + } for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { int worldX = startX + fieldX; for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ; - target[fieldIndex] = B.isFluid(complex.getFluidStream().get(worldX, startZ + fieldZ)) - && Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]); + target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ)) + && Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight; } } } @@ -682,6 +750,10 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA]; + private final double[][] neighbourHeights = new double[NEIGHBOURS][CHUNK_AREA]; + private final Object[][] neighbourObjects = new Object[NEIGHBOURS][CHUNK_AREA]; + private final long[][] neighbourSubmerged = new long[NEIGHBOURS][4]; + private final IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); private int nextWeightBuffer; private boolean inUse; @@ -705,6 +777,9 @@ public class MantleCarvingComponent extends IrisMantleComponent { Arrays.fill(fieldRegions, null); Arrays.fill(fieldSurfaceBiomes, null); Arrays.fill(fieldCaveBiomes, null); + for (Object[] objects : neighbourObjects) { + Arrays.fill(objects, null); + } columnProfileWeights.clear(); profileOrder.clear(); nextWeightBuffer = 0; 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 5578bb160..913bf0d9b 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 @@ -81,6 +81,21 @@ public final class BakedDoubleStream extends BasicStream implements Chun } } + public void fillColumns(int chunkX, int chunkZ, long[] columns, double[] target) { + if (cache.memoAllowed()) { + cache.fillColumns(plane, chunkX, chunkZ, columns, target); + return; + } + int originX = chunkX << 4; + int originZ = chunkZ << 4; + for (int word = 0; word < 4; word++) { + for (long bits = columns[word]; bits != 0L; bits &= bits - 1L) { + int index = (word << 6) | Long.numberOfTrailingZeros(bits); + target[index] = getDouble(originX + (index & 15), originZ + (index >> 4)); + } + } + } + @Override public void fillChunkRaw(int worldX, int worldZ, Object[] target) { double[] values = new double[256]; 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 1a0594b73..9f0f922ed 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 @@ -54,6 +54,21 @@ public final class BakedStream extends BasicStream implements ChunkFillabl return scalar.fromDouble(d); } + public void fillColumns(int chunkX, int chunkZ, long[] columns, Object[] target) { + if (cache.memoAllowed()) { + cache.fillColumns(plane, chunkX, chunkZ, columns, target); + return; + } + int originX = chunkX << 4; + int originZ = chunkZ << 4; + for (int word = 0; word < 4; word++) { + for (long bits = columns[word]; bits != 0L; bits &= bits - 1L) { + int index = (word << 6) | Long.numberOfTrailingZeros(bits); + target[index] = get(originX + (index & 15), originZ + (index >> 4)); + } + } + } + @Override public void fillChunkRaw(int worldX, int worldZ, Object[] target) { int chunkX = worldX >> 4; diff --git a/core/src/test/java/art/arcane/iris/generation/bake/ChunkBakeCacheTest.java b/core/src/test/java/art/arcane/iris/generation/bake/ChunkBakeCacheTest.java index 83a785507..62ff49f14 100644 --- a/core/src/test/java/art/arcane/iris/generation/bake/ChunkBakeCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/bake/ChunkBakeCacheTest.java @@ -1,7 +1,9 @@ package art.arcane.iris.generation.bake; +import art.arcane.iris.generation.stream.ProvisionalSampling; import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; @@ -87,6 +89,155 @@ public class ChunkBakeCacheTest { assertEquals(64, cache.capacity()); } + @Test + public void rememberedPlanesAnswerOnlyForTheirOwnChunkAndOnlyUntilClear() { + ChunkBakeCache cache = new ChunkBakeCache(new ConstantSource(), 256); + int[][] chunks = {{0, 0}, {1, 0}, {0, 1}, {4, 0}, {0, 4}, {-4, -4}, {3, 3}, {7, 7}}; + for (int[] chunk : chunks) { + cache.complete(BakePlane.BASE_HEIGHT, chunk[0], chunk[1]); + } + for (int round = 0; round < 2; round++) { + for (int[] chunk : chunks) { + ChunkPlanes.Plane plane = cache.planeIfPresent(BakePlane.BASE_HEIGHT, chunk[0], chunk[1]); + assertEquals(chunk[0] * 1_000D + chunk[1] + 5D, plane.doubleAt(5), 0D); + } + } + assertNull(cache.planeIfPresent(BakePlane.BASE_HEIGHT, 8, 8)); + + cache.clear(); + + for (int[] chunk : chunks) { + assertNull(cache.planeIfPresent(BakePlane.BASE_HEIGHT, chunk[0], chunk[1])); + } + } + + @Test + public void fillColumnsComputesOnlyTheMissingRequestedColumnsAndPublishesThem() { + RecordingSource source = new RecordingSource(); + ChunkBakeCache cache = new ChunkBakeCache(source, 64); + long[] strip = columns(13, 0, 3, 16); + double[] target = new double[256]; + + cache.fillColumns(BakePlane.NATURAL_HEIGHT, 2, -1, strip, target); + + assertEquals(1, source.calls); + assertArrayEquals(strip, source.lastMissing); + ChunkPlanes.Plane plane = cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 2, -1); + for (int index = 0; index < 256; index++) { + boolean requested = (strip[index >>> 6] & (1L << (index & 63))) != 0L; + assertEquals(requested, plane.ready(index)); + if (requested) { + assertEquals(RecordingSource.value(2, -1, index), target[index], 0D); + } + } + + long[] corner = columns(13, 13, 3, 3); + long[] wider = columns(10, 13, 6, 3); + double[] second = new double[256]; + cache.fillColumns(BakePlane.NATURAL_HEIGHT, 2, -1, wider, second); + + assertEquals(2, source.calls); + long[] expected = new long[4]; + for (int word = 0; word < 4; word++) { + expected[word] = wider[word] & ~corner[word]; + } + assertArrayEquals(expected, source.lastMissing); + for (int index = 0; index < 256; index++) { + if ((wider[index >>> 6] & (1L << (index & 63))) != 0L) { + assertEquals(RecordingSource.value(2, -1, index), second[index], 0D); + } + } + + cache.fillColumns(BakePlane.NATURAL_HEIGHT, 2, -1, corner, new double[256]); + assertEquals(2, source.calls); + assertArrayEquals(columns(13, 0, 3, 16), strip); + assertArrayEquals(columns(10, 13, 6, 3), wider); + } + + @Test + public void fillColumnsReadsCompletedPlanesWithoutRecomputing() { + RecordingSource source = new RecordingSource(); + ChunkBakeCache cache = new ChunkBakeCache(source, 64); + cache.complete(BakePlane.REGION, -4, 7); + Object[] target = new Object[256]; + + cache.fillColumns(BakePlane.REGION, -4, 7, columns(0, 0, 16, 3), target); + + assertEquals(1, source.calls); + assertEquals(RecordingSource.label(-4, 7, 37), target[37]); + assertNull(target[3 * 16]); + } + + @Test + public void fillColumnsHandsBackProvisionalValuesWithoutPublishingThem() { + RecordingSource source = new RecordingSource(); + source.provisional = true; + ChunkBakeCache cache = new ChunkBakeCache(source, 64); + long[] block = columns(4, 4, 2, 2); + double[] target = new double[256]; + + cache.fillColumns(BakePlane.NATURAL_HEIGHT, 0, 0, block, target); + + int index = (4 << 4) | 4; + assertEquals(RecordingSource.value(0, 0, index), target[index], 0D); + assertTrue(cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 0, 0) == null + || !cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 0, 0).ready(index)); + + source.provisional = false; + cache.fillColumns(BakePlane.NATURAL_HEIGHT, 0, 0, block, new double[256]); + assertEquals(2, source.calls); + assertTrue(cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 0, 0).ready(index)); + } + + private static long[] columns(int x0, int z0, int width, int depth) { + long[] mask = new long[4]; + for (int x = x0; x < x0 + width; x++) { + for (int z = z0; z < z0 + depth; z++) { + int index = (z << 4) | x; + mask[index >>> 6] |= 1L << (index & 63); + } + } + return mask; + } + + private static final class RecordingSource implements PlaneSource { + private int calls; + private long[] lastMissing; + private boolean provisional; + + private static double value(int chunkX, int chunkZ, int index) { + return chunkX * 10_000D + chunkZ * 1_000D + index + 0.25D; + } + + private static String label(int chunkX, int chunkZ, int index) { + return chunkX + ":" + chunkZ + ":" + index; + } + + @Override + public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) { + calls++; + lastMissing = missing.clone(); + if (provisional) { + ProvisionalSampling.mark(); + } + for (int index = 0; index < 256; index++) { + if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) { + continue; + } + if (values instanceof double[] doubles) { + doubles[index] = value(chunkX, chunkZ, index); + } else { + ((Object[]) values)[index] = label(chunkX, chunkZ, index); + } + } + } + + @Override + public boolean mainThread() { + return false; + } + } + private static final class ConstantSource implements PlaneSource { @Override public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) { diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvingComponentBakedFieldTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvingComponentBakedFieldTest.java new file mode 100644 index 000000000..3a867f242 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvingComponentBakedFieldTest.java @@ -0,0 +1,242 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.PlaneSource; +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.cave.IrisCaveProfile; +import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.EngineMetrics; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.stream.BakedDoubleStream; +import art.arcane.iris.generation.stream.BakedStream; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.pack.value.IrisRange; +import art.arcane.iris.world.IrisWorld; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.stream.ProceduralStream; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MantleCarvingComponentBakedFieldTest { + private static final double FLUID_HEIGHT = 63D; + private static final int[][] ORDER = { + {1, 1}, {-2, 0}, {0, 0}, {2, -1}, {-1, -1}, {0, 1}, {1, -2}, {-2, 2}, {2, 2}, {-1, 1}, {0, -1}, {1, 0} + }; + + private final IrisCaveProfile dimensionProfile = new IrisCaveProfile().setEnabled(true); + private final IrisCaveProfile regionProfile = new IrisCaveProfile().setEnabled(true); + private final IrisCaveProfile surfaceProfile = new IrisCaveProfile().setEnabled(true); + private final IrisCaveProfile shallowCaveProfile = new IrisCaveProfile().setEnabled(true); + private final IrisCaveProfile deepCaveProfile = new IrisCaveProfile().setEnabled(true); + private final IrisRegion profiledRegion = new IrisRegion().setCaveProfile(regionProfile); + private final IrisRegion plainRegion = new IrisRegion(); + private final IrisBiome profiledSurface = new IrisBiome().setCaveProfile(surfaceProfile); + private final IrisBiome plainSurface = new IrisBiome(); + private final IrisBiome shallowCave = new IrisBiome().setCaveProfile(shallowCaveProfile); + private final IrisBiome deepCave = new IrisBiome().setCaveProfile(deepCaveProfile).setCaveMinDepthBelowSurface(60); + private final IrisBiome plainCave = new IrisBiome(); + private final NativeBlockState water = fluid(true); + private final NativeBlockState air = fluid(false); + + @Test + public void bakedFieldReadsMatchPerColumnStreamReads() throws Exception { + List plain = run(false, false); + List baked = run(true, false); + List warmed = run(true, true); + + assertTrue(plain.size() > ORDER.length); + assertEquals(plain, baked); + assertEquals(plain, warmed); + } + + @Test + public void apronHeightsAreComputedThroughThePlaneSourceAndNeverPerColumn() throws Exception { + AtomicInteger scalarHeights = new AtomicInteger(); + Harness harness = harness(true, scalarHeights); + for (int[] chunk : ORDER) { + harness.component.generateLayer(harness.writer, chunk[0], chunk[1], harness.context); + } + + assertEquals(0, scalarHeights.get()); + } + + private List run(boolean baked, boolean warm) throws Exception { + Harness harness = harness(baked, new AtomicInteger()); + if (warm) { + for (int x = -40; x < 56; x += 3) { + for (int z = -40; z < 56; z += 5) { + harness.complex.getNaturalHeightStream().getDouble(x, z); + harness.complex.getCaveBiomeStream().get(x, z); + harness.complex.getFluidStream().get(x, z); + } + } + } + for (int[] chunk : ORDER) { + harness.component.generateLayer(harness.writer, chunk[0], chunk[1], harness.context); + } + return harness.calls; + } + + @SuppressWarnings("unchecked") + private Harness harness(boolean baked, AtomicInteger scalarHeights) throws ReflectiveOperationException { + Engine engine = mock(Engine.class); + EngineMantle engineMantle = mock(EngineMantle.class); + when(engineMantle.getEngine()).thenReturn(engine); + MantleCarvingComponent component = new MantleCarvingComponent(engineMantle); + IrisDimension dimension = new IrisDimension(); + dimension.setCaveProfile(dimensionProfile); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(0).maxHeight(128).build()); + when(engine.getMetrics()).thenReturn(new EngineMetrics(16)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true); + when(complex.getFluidHeight()).thenReturn(FLUID_HEIGHT); + + ProceduralStream heights = mock(ProceduralStream.class); + when(heights.getDouble(anyDouble(), anyDouble())).thenAnswer(call -> { + scalarHeights.incrementAndGet(); + return height((int) (double) call.getArgument(0), (int) (double) call.getArgument(1)); + }); + ProceduralStream regions = objects(this::region); + ProceduralStream surfaces = objects(this::surface); + ProceduralStream caves = objects(this::cave); + ProceduralStream fluids = objects(this::fluidAt); + if (baked) { + ChunkBakeCache cache = new ChunkBakeCache(new FunctionSource(), 256); + when(complex.getNaturalHeightStream()).thenReturn(new BakedDoubleStream(cache, BakePlane.NATURAL_HEIGHT, heights)); + when(complex.getRegionStream()).thenReturn(new BakedStream<>(cache, BakePlane.REGION, regions)); + when(complex.getNaturalTrueBiomeStream()).thenReturn(new BakedStream<>(cache, BakePlane.NATURAL_TRUE_BIOME, surfaces)); + when(complex.getCaveBiomeStream()).thenReturn(new BakedStream<>(cache, BakePlane.CAVE_BIOME, caves)); + when(complex.getFluidStream()).thenReturn(new BakedStream<>(cache, BakePlane.FLUID, fluids)); + } else { + when(complex.getNaturalHeightStream()).thenReturn(heights); + when(complex.getRegionStream()).thenReturn(regions); + when(complex.getNaturalTrueBiomeStream()).thenReturn(surfaces); + when(complex.getCaveBiomeStream()).thenReturn(caves); + when(complex.getFluidStream()).thenReturn(fluids); + } + ChunkContext context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(complex); + MantleWriter writer = mock(MantleWriter.class); + Mantle storage = mock(Mantle.class); + when(storage.getWorldHeight()).thenReturn(128); + when(writer.getMantle()).thenReturn(storage); + + List calls = new ArrayList<>(); + Map carvers = new IdentityHashMap<>(); + IrisCaveProfile[] profiles = {dimensionProfile, regionProfile, surfaceProfile, shallowCaveProfile, deepCaveProfile}; + for (int index = 0; index < profiles.length; index++) { + int profileIndex = index; + IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class); + doAnswer(call -> { + IrisRange range = call.getArgument(6); + calls.add(call.getArgument(1) + "," + call.getArgument(2) + " profile=" + profileIndex + + " range=" + (range == null ? "-" : range.getMin() + ".." + range.getMax()) + + " weights=" + Arrays.toString((double[]) call.getArgument(3)) + + " heights=" + Arrays.toString((int[]) call.getArgument(7)) + + " boundaries=" + Arrays.toString((long[]) call.getArgument(8))); + return 0; + }).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(), + any(), any(), any(), any(), any()); + carvers.put(profiles[index], carver); + } + Field profileCarvers = MantleCarvingComponent.class.getDeclaredField("profileCarvers"); + profileCarvers.setAccessible(true); + profileCarvers.set(component, carvers); + return new Harness(component, complex, context, writer, calls); + } + + @SuppressWarnings("unchecked") + private static ProceduralStream objects(ColumnFunction function) { + ProceduralStream stream = mock(ProceduralStream.class); + when(stream.get(anyDouble(), anyDouble())).thenAnswer(call -> + function.at((int) (double) call.getArgument(0), (int) (double) call.getArgument(1))); + return stream; + } + + private static double height(int x, int z) { + return 64D + 9.5D * Math.sin(x * 0.37D) * Math.cos(z * 0.23D) + Math.floorMod(x * 31 + z * 17, 7) * 0.31D; + } + + private IrisRegion region(int x, int z) { + return Math.floorMod((x >> 3) + (z >> 3), 3) == 0 ? profiledRegion : plainRegion; + } + + private IrisBiome surface(int x, int z) { + return Math.floorMod(x * 7 + z * 3, 11) < 4 ? profiledSurface : plainSurface; + } + + private IrisBiome cave(int x, int z) { + return switch (Math.floorMod((x >> 2) ^ (z >> 2), 5)) { + case 0 -> shallowCave; + case 1, 2 -> deepCave; + case 3 -> plainCave; + default -> null; + }; + } + + private NativeBlockState fluidAt(int x, int z) { + return Math.floorMod(x + z, 5) == 0 ? air : water; + } + + private static NativeBlockState fluid(boolean fluid) { + NativeBlockState state = mock(NativeBlockState.class); + when(state.isFluid()).thenReturn(fluid); + return state; + } + + private interface ColumnFunction { + T at(int x, int z); + } + + private final class FunctionSource implements PlaneSource { + @Override + public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) { + for (int index = 0; index < 256; index++) { + if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) { + continue; + } + int x = (chunkX << 4) + (index & 15); + int z = (chunkZ << 4) + (index >> 4); + switch (plane) { + case NATURAL_HEIGHT -> ((double[]) values)[index] = height(x, z); + case REGION -> ((Object[]) values)[index] = region(x, z); + case NATURAL_TRUE_BIOME -> ((Object[]) values)[index] = surface(x, z); + case CAVE_BIOME -> ((Object[]) values)[index] = cave(x, z); + case FLUID -> ((Object[]) values)[index] = fluidAt(x, z); + default -> throw new IllegalStateException("Unexpected plane " + plane); + } + } + } + + @Override + public boolean mainThread() { + return false; + } + } + + private record Harness(MantleCarvingComponent component, IrisComplex complex, ChunkContext context, + MantleWriter writer, List calls) { + } +} From e38f956d66270023e30b39832dcc47de25381679 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 22 Oct 2026 07:19:57 -0400 Subject: [PATCH 2/2] Terminal carving roots - Roots without selectable children resolve to themselves before the per-column child cache - Selection plans build through one shared helper for root checks and child selection --- .../terrain/IrisDimensionCarvingResolver.java | 31 +++++++++++++------ ...risDimensionCarvingResolverParityTest.java | 26 ++++++++++++++++ 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolver.java b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolver.java index b977e8614..979f8042e 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolver.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolver.java @@ -120,7 +120,7 @@ public final class IrisDimensionCarvingResolver { } int remainingDepth = clampDepth(rootEntry.getChildRecursionDepth()); - if (remainingDepth <= 0) { + if (remainingDepth <= 0 || terminal(engine, data, rootEntry, rootBiome, resolvedState)) { return rootEntry; } @@ -131,13 +131,12 @@ public final class IrisDimensionCarvingResolver { return columns.entries[slot]; } - Map entryIndex = resolveEntryIndex(engine, resolvedState); IrisDimensionCarvingEntry current = rootEntry; int depth = remainingDepth; boolean cacheable = true; while (depth > 0) { cacheable &= fixedChildStyle(current); - IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState); + IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, resolvedState); if (selected == null || selected == current || selected.getWorldYRange() != null && !selected.getWorldYRange().contains(worldY)) { break; @@ -164,6 +163,24 @@ public final class IrisDimensionCarvingResolver { return current; } + private static boolean terminal(Engine engine, IrisData data, IrisDimensionCarvingEntry entry, IrisBiome biome, State state) { + KList children = entry.getChildren(); + if (children == null || children.isEmpty()) { + return true; + } + return selectionPlan(engine, data, entry, biome, state).size() <= 1L; + } + + private static IrisRaritySelection selectionPlan(Engine engine, IrisData data, IrisDimensionCarvingEntry parent, + IrisBiome parentBiome, State state) { + IrisRaritySelection selectionPlan = state.selectionPlans.get(parent); + if (selectionPlan == null) { + selectionPlan = buildSelectionPlan(data, parent, parentBiome, resolveEntryIndex(engine, state), state); + state.selectionPlans.put(parent, selectionPlan); + } + return selectionPlan; + } + private static boolean fixedChildStyle(IrisDimensionCarvingEntry entry) { for (IrisGeneratorStyle style = entry.getChildStyle(); style != null; style = style.getFracture()) { if (style.getExpression() != null) { @@ -202,7 +219,6 @@ public final class IrisDimensionCarvingResolver { IrisDimensionCarvingEntry parent, int worldX, int worldZ, - Map entryIndex, State state ) { KList children = parent.getChildren(); @@ -215,12 +231,7 @@ public final class IrisDimensionCarvingResolver { return parent; } - IrisRaritySelection selectionPlan = state.selectionPlans.get(parent); - if (selectionPlan == null) { - selectionPlan = buildSelectionPlan(data, parent, parentBiome, entryIndex, state); - state.selectionPlans.put(parent, selectionPlan); - } - + IrisRaritySelection selectionPlan = selectionPlan(engine, data, parent, parentBiome, state); if (selectionPlan.size() <= 1L) { return parent; } diff --git a/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java index 2dbd4a142..28bff4309 100644 --- a/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java @@ -166,6 +166,32 @@ public class IrisDimensionCarvingResolverParityTest { assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state)); } + @Test + public void rootsWithoutSelectableChildrenResolveToThemselvesWithoutSampling() { + for (List children : List.of(List.of(), List.of("missing-child"))) { + Fixture fixture = createFixture(); + IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low")); + parent.setChildRecursionDepth(3); + parent.setChildren(new KList<>(children)); + CNG generator = mock(CNG.class); + doReturn(0.5D).when(generator).noiseFast2D(anyDouble(), anyDouble()); + doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class)); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + + for (int worldY = -80; worldY < 140; worldY += 13) { + for (int x = -20; x < 20; x += 3) { + for (int z = -20; z < 20; z += 7) { + assertSame(legacyResolveFromRoot(fixture.engine, parent, x, worldY, z), + IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, worldY, z, state)); + assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, worldY, z, state)); + } + } + } + + verify(generator, never()).noiseFast2D(anyDouble(), anyDouble()); + } + } + @Test public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() { Fixture fixture = createFixture();