From 20e10999dd82eb5b1d2a12daeac4bdd38f4799b8 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:43:38 -0400 Subject: [PATCH 1/4] Lazy semantic region summaries - claims cache the next region shard without rebuilding its key summary - summaries are computed when a query or compaction reads them --- .../history/GenerationSemanticIndex.java | 6 +++++- .../history/GenerationSemanticIndexTest.java | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java index b99888127..016ba9054 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java @@ -1075,7 +1075,11 @@ public final class GenerationSemanticIndex { private void cacheRegion(long regionKey, RegionShard region) { regions.put(regionKey, region); - cacheSummary(regionKey, region.summary()); + if (region.summary == null) { + summaries.remove(regionKey); + } else { + cacheSummary(regionKey, region.summary); + } while (regions.size() > MAXIMUM_CACHED_REGIONS) { Iterator> entries = regions.entrySet().iterator(); entries.next(); diff --git a/core/src/test/java/art/arcane/iris/world/history/GenerationSemanticIndexTest.java b/core/src/test/java/art/arcane/iris/world/history/GenerationSemanticIndexTest.java index 08770aa91..27f92bd5e 100644 --- a/core/src/test/java/art/arcane/iris/world/history/GenerationSemanticIndexTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/GenerationSemanticIndexTest.java @@ -206,6 +206,27 @@ public final class GenerationSemanticIndexTest { new ChunkGenerationSemantics.BlockPosition(-520, 80, -8), 4)).isEmpty()); } + @Test + public void claimsDeferRegionSummariesUntilAQueryReadsThem() throws Exception { + Path root = temporaryFolder.newFolder("lazy-summaries").toPath(); + GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root); + + index.claimAndPersist(ChunkGenerationSemantics.builder(-33, -1, 1L).addObject("iris:first").seal().build()); + assertEquals(0, index.cachedSummaryCount()); + assertEquals("iris:first", requiredMatch(index, GenerationSemanticIndex.SemanticKind.OBJECT, "iris:first").key()); + assertEquals(1, index.cachedSummaryCount()); + + index.claimAndPersist(ChunkGenerationSemantics.builder(-34, -1, 1L).addObject("iris:second").seal().build()); + assertEquals(0, index.cachedSummaryCount()); + assertEquals("iris:second", requiredMatch(index, GenerationSemanticIndex.SemanticKind.OBJECT, "iris:second").key()); + assertEquals("iris:first", requiredMatch(index, GenerationSemanticIndex.SemanticKind.OBJECT, "iris:first").key()); + assertEquals(2, sealedClaims(index, 1L).size()); + + GenerationSemanticIndex reopened = GenerationSemanticIndex.load(root); + assertEquals("iris:second", requiredMatch(reopened, GenerationSemanticIndex.SemanticKind.OBJECT, "iris:second").key()); + assertEquals(index.recordsSnapshot(), reopened.recordsSnapshot()); + } + @Test public void conflictingActivationsAndFactsAfterSealAreRejectedWithoutDiskChanges() throws Exception { Path dimensionRoot = temporaryFolder.newFolder("sealed").toPath(); From 2fef055e5c72658fc52308d2508d975387c1be26 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:43:39 -0400 Subject: [PATCH 2/4] Stream terrain boundary capture - boundary columns build runs directly instead of voxel lists, tree sets and hash maps - palette sorted once per column with a direct comparator, same canonical order - geometry source reuses the last encoded voxel - equivalence test against the previous encoding --- .../stage/IrisTransitionGeometryActuator.java | 14 +- .../world/history/BoundaryColumnGeometry.java | 130 +++++++--- .../iris/world/history/SavedTerrainChunk.java | 31 ++- .../SavedTerrainCaptureEquivalenceTest.java | 234 ++++++++++++++++++ 4 files changed, 363 insertions(+), 46 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java index 777c18fec..ef7f5143f 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java @@ -95,6 +95,9 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen private final int minimumY; private final ChunkContext context; private final Map voxels = new IdentityHashMap<>(); + private NativeBlockState air; + private NativeBlockState lastState; + private BoundaryColumnGeometry.Voxel lastVoxel; private GeometrySource(Hunk blocks, Hunk biomes, int minimumY, ChunkContext context) { this.blocks = blocks; @@ -107,9 +110,16 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) { NativeBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ); if (state == null) { - state = IrisPlatforms.get().registries().air(); + if (air == null) { + air = IrisPlatforms.get().registries().air(); + } + state = air; } - return voxels.computeIfAbsent(state, GeometrySource::encode); + if (state != lastState) { + lastVoxel = voxels.computeIfAbsent(state, GeometrySource::encode); + lastState = state; + } + return lastVoxel; } @Override diff --git a/core/src/main/java/art/arcane/iris/world/history/BoundaryColumnGeometry.java b/core/src/main/java/art/arcane/iris/world/history/BoundaryColumnGeometry.java index bce08f4d5..4cd56bad3 100644 --- a/core/src/main/java/art/arcane/iris/world/history/BoundaryColumnGeometry.java +++ b/core/src/main/java/art/arcane/iris/world/history/BoundaryColumnGeometry.java @@ -9,14 +9,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.TreeSet; public final class BoundaryColumnGeometry implements NativeBlockColumn { public static final int MAXIMUM_HEIGHT = 65_536; - private static final Comparator VOXEL_ORDER = Comparator.comparing(Voxel::stateKey) - .thenComparing(Voxel::phase) - .thenComparing(Voxel::fluidStateKey) - .thenComparing(Voxel::protectedContent); + private static final Comparator VOXEL_ORDER = BoundaryColumnGeometry::compareVoxels; private static final Voxel AIR = new Voxel("minecraft:air", Phase.AIR, "", false); private static final BoundaryColumnGeometry EMPTY = new BoundaryColumnGeometry(0, List.of(), new int[0], new short[0]); @@ -42,36 +38,24 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn { if (voxels.size() > MAXIMUM_HEIGHT) { throw new IllegalArgumentException("Geometry column exceeds maximum height"); } - TreeSet used = new TreeSet<>(VOXEL_ORDER); - Voxel previous = null; + Runs runs = new Runs(voxels.size()); for (Voxel voxel : voxels) { - if (previous == null || !previous.equals(voxel)) { - used.add(voxel); - previous = voxel; - } + runs.add(voxel); } - List palette = List.copyOf(used); - Map indices = new HashMap<>(palette.size()); - for (int index = 0; index < palette.size(); index++) { - if (index > Short.MAX_VALUE) { - throw new IllegalArgumentException("Geometry palette exceeds compact index capacity"); - } - indices.put(palette.get(index), (short) index); + return runs.build(minimumY); + } + + private static int compareVoxels(Voxel first, Voxel second) { + int order = first.stateKey().compareTo(second.stateKey()); + if (order != 0) { + return order; } - int[] ends = new int[voxels.size()]; - short[] values = new short[voxels.size()]; - int runCount = 0; - previous = null; - for (int offset = 0; offset < voxels.size(); offset++) { - Voxel voxel = voxels.get(offset); - if (previous == null || !previous.equals(voxel)) { - values[runCount++] = indices.get(voxel); - previous = voxel; - } - ends[runCount - 1] = offset + 1; + order = first.phase().compareTo(second.phase()); + if (order != 0) { + return order; } - return new BoundaryColumnGeometry(minimumY, palette, - Arrays.copyOf(ends, runCount), Arrays.copyOf(values, runCount)); + order = first.fluidStateKey().compareTo(second.fluidStateKey()); + return order != 0 ? order : Boolean.compare(first.protectedContent(), second.protectedContent()); } public int minimumY() { @@ -256,6 +240,90 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn { } } + static final class Runs { + private static final int LINEAR_PALETTE_LIMIT = 32; + + private final Voxel[] voxels; + private final int[] ends; + private int count; + private int length; + + Runs(int capacity) { + voxels = new Voxel[capacity]; + ends = new int[capacity]; + } + + void clear() { + count = 0; + length = 0; + } + + void add(Voxel voxel) { + Objects.requireNonNull(voxel, "Geometry voxel"); + if (count == 0 || !sameVoxel(voxels[count - 1], voxel)) { + voxels[count++] = voxel; + } + ends[count - 1] = ++length; + } + + BoundaryColumnGeometry build(int minimumY) { + Voxel[] distinct = new Voxel[count]; + int[] slots = new int[count]; + Map lookup = null; + int distinctCount = 0; + for (int run = 0; run < count; run++) { + Voxel voxel = voxels[run]; + int slot = lookup == null ? linearIndex(distinct, distinctCount, voxel) : lookup.getOrDefault(voxel, -1); + if (slot < 0) { + slot = distinctCount; + distinct[distinctCount++] = voxel; + if (lookup != null) { + lookup.put(voxel, slot); + } else if (distinctCount > LINEAR_PALETTE_LIMIT) { + lookup = new HashMap<>(); + for (int index = 0; index < distinctCount; index++) { + lookup.put(distinct[index], index); + } + } + } + slots[run] = slot; + } + if (distinctCount > Short.MAX_VALUE + 1) { + throw new IllegalArgumentException("Geometry palette exceeds compact index capacity"); + } + Voxel[] palette = Arrays.copyOf(distinct, distinctCount); + Arrays.sort(palette, VOXEL_ORDER); + short[] slotIndices = new short[distinctCount]; + if (lookup == null) { + for (int slot = 0; slot < distinctCount; slot++) { + slotIndices[slot] = (short) linearIndex(palette, distinctCount, distinct[slot]); + } + } else { + for (int index = 0; index < distinctCount; index++) { + slotIndices[lookup.get(palette[index])] = (short) index; + } + } + short[] values = new short[count]; + for (int run = 0; run < count; run++) { + values[run] = slotIndices[slots[run]]; + } + return new BoundaryColumnGeometry(minimumY, Arrays.asList(palette), Arrays.copyOf(ends, count), values); + } + + private static int linearIndex(Voxel[] values, int size, Voxel voxel) { + for (int index = 0; index < size; index++) { + if (sameVoxel(values[index], voxel)) { + return index; + } + } + return -1; + } + + private static boolean sameVoxel(Voxel first, Voxel second) { + return first == second || first.equals(second); + } + } + public enum Phase { AIR, SOLID, diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java index 81bdcc83e..c04c36f0d 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedTerrainChunk.java @@ -4,9 +4,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.OptionalInt; @@ -65,14 +63,15 @@ public final class SavedTerrainChunk { } Math.addExact(minimumY, height); List columns = new ArrayList<>(256); + BoundaryColumnGeometry.Runs runs = new BoundaryColumnGeometry.Runs(height); for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { if (boundaryOnly && x != 0 && x != 15 && z != 0 && z != 15) { columns.add(null); continue; } - List voxels = new ArrayList<>(height); - Map biomePalette = new LinkedHashMap<>(); + runs.clear(); + List biomePalette = new ArrayList<>(4); short[] biomeIndices = new short[height / 4]; int surface = 0; int floor = 0; @@ -80,7 +79,7 @@ public final class SavedTerrainChunk { for (int offset = 0; offset < height; offset++) { int y = minimumY + offset; BoundaryColumnGeometry.Voxel voxel = Objects.requireNonNull(source.voxel(x, y, z), "voxel"); - voxels.add(voxel); + runs.add(voxel); if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) { surface = offset; } @@ -92,15 +91,10 @@ public final class SavedTerrainChunk { } if (offset % 4 == 0) { String biome = Objects.requireNonNull(source.biome(x, y, z), "physical biome"); - Short index = biomePalette.get(biome); - if (index == null) { - index = (short) biomePalette.size(); - biomePalette.put(biome, index); - } - biomeIndices[offset / 4] = index; + biomeIndices[offset / 4] = paletteIndex(biomePalette, biome); } } - BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(minimumY, voxels); + BoundaryColumnGeometry geometry = runs.build(minimumY); OptionalInt groundSurface = source.groundSurface(x, z); if (groundSurface.isPresent()) { floor = geometry.surfaceOffsetNear(groundSurface.getAsInt()); @@ -115,13 +109,24 @@ public final class SavedTerrainChunk { fluid > floor ? OptionalInt.of(fluid) : OptionalInt.empty(), OptionalInt.empty()), new TerrainBoundarySignature.Samples( new TerrainBoundarySignature.VerticalLayout(minimumY, 4, biomeIndices.length), - new TerrainBoundarySignature.BiomeEncoding(List.copyOf(biomePalette.keySet()), biomeIndices)), + new TerrainBoundarySignature.BiomeEncoding(biomePalette, biomeIndices)), geometry)); } } return new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, columns); } + private static short paletteIndex(List palette, String biome) { + for (int index = 0; index < palette.size(); index++) { + String candidate = palette.get(index); + if (candidate == biome || candidate.equals(biome)) { + return (short) index; + } + } + palette.add(biome); + return (short) (palette.size() - 1); + } + public SavedTerrainChunk boundaryOnly() { if (!hasColumn(1, 1)) { return this; diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java new file mode 100644 index 000000000..0bdd9c171 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/world/history/SavedTerrainCaptureEquivalenceTest.java @@ -0,0 +1,234 @@ +package art.arcane.iris.world.history; + +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.Random; +import java.util.TreeSet; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class SavedTerrainCaptureEquivalenceTest { + private static final int MINIMUM_Y = -64; + private static final int HEIGHT = 384; + private static final Comparator VOXEL_ORDER = Comparator + .comparing(BoundaryColumnGeometry.Voxel::stateKey) + .thenComparing(BoundaryColumnGeometry.Voxel::phase) + .thenComparing(BoundaryColumnGeometry.Voxel::fluidStateKey) + .thenComparing(BoundaryColumnGeometry.Voxel::protectedContent); + private static final List STATES = List.of( + voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false), + voxel("minecraft:deepslate", BoundaryColumnGeometry.Phase.SOLID, "", false), + voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false), + voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false), + voxel("minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false), + voxel("minecraft:lava[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:lava[level=0]", false), + voxel("minecraft:oak_log[axis=y]", BoundaryColumnGeometry.Phase.SOLID, "", true), + voxel("minecraft:kelp", BoundaryColumnGeometry.Phase.SOLID, "minecraft:water[level=0]", true), + voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", true)); + private static final List BIOMES = List.of("minecraft:plains", "minecraft:river", + "minecraft:lush_caves", "minecraft:deep_dark", "minecraft:dripstone_caves"); + + @Test + public void boundaryAndFullCapturesMatchTheReferenceEncoding() throws Exception { + Random random = new Random(20260928L); + for (int sample = 0; sample < 48; sample++) { + RandomSource source = new RandomSource(random, sample); + for (boolean boundaryOnly : new boolean[]{true, false}) { + SavedTerrainChunk expected = referenceCapture(-3, 7, source, boundaryOnly); + SavedTerrainChunk actual = boundaryOnly + ? SavedTerrainChunk.captureBoundary(-3, 7, MINIMUM_Y, HEIGHT, "minecraft:noise", source) + : SavedTerrainChunk.capture(-3, 7, MINIMUM_Y, HEIGHT, "minecraft:noise", source); + assertSameTerrain(expected, actual); + assertArrayEquals(NativeTerrainReceipt.encode(expected, 1L, "epoch"), + NativeTerrainReceipt.encode(actual, 1L, "epoch")); + } + } + } + + @Test + public void geometryMatchesTheReferenceOnWidePalettes() { + Random random = new Random(7L); + for (int sample = 0; sample < 32; sample++) { + ArrayList voxels = new ArrayList<>(HEIGHT); + int distinct = 1 + random.nextInt(sample < 16 ? 12 : 200); + while (voxels.size() < HEIGHT) { + int state = random.nextInt(distinct); + BoundaryColumnGeometry.Voxel voxel = voxel("minecraft:block_" + state, + BoundaryColumnGeometry.Phase.values()[state % 2], "", state % 3 == 0); + int end = Math.min(HEIGHT, voxels.size() + 1 + random.nextInt(4)); + while (voxels.size() < end) { + voxels.add(voxel); + } + } + assertEquals(referenceGeometry(MINIMUM_Y, voxels), BoundaryColumnGeometry.fromVoxels(MINIMUM_Y, voxels)); + } + } + + private static void assertSameTerrain(SavedTerrainChunk expected, SavedTerrainChunk actual) { + assertEquals(expected.chunkX(), actual.chunkX()); + assertEquals(expected.chunkZ(), actual.chunkZ()); + assertEquals(expected.nativeStatus(), actual.nativeStatus()); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + assertEquals(expected.hasColumn(x, z), actual.hasColumn(x, z)); + if (!expected.hasColumn(x, z)) { + continue; + } + TerrainBoundarySignature left = expected.column(-48 + x, 112 + z); + TerrainBoundarySignature right = actual.column(-48 + x, 112 + z); + assertEquals(left.column(), right.column()); + assertEquals(left.samples(), right.samples()); + assertEquals(left.geometry(), right.geometry()); + } + } + } + + private static SavedTerrainChunk referenceCapture(int chunkX, int chunkZ, SavedTerrainChunk.VoxelSource source, + boolean boundaryOnly) throws IOException { + List columns = new ArrayList<>(256); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (boundaryOnly && x != 0 && x != 15 && z != 0 && z != 15) { + columns.add(null); + continue; + } + List voxels = new ArrayList<>(HEIGHT); + Map biomePalette = new LinkedHashMap<>(); + short[] biomeIndices = new short[HEIGHT / 4]; + int surface = 0; + int floor = 0; + int fluid = -1; + for (int offset = 0; offset < HEIGHT; offset++) { + int y = MINIMUM_Y + offset; + BoundaryColumnGeometry.Voxel voxel = Objects.requireNonNull(source.voxel(x, y, z), "voxel"); + voxels.add(voxel); + if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) { + surface = offset; + } + if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) { + floor = offset; + } + if (!voxel.fluidStateKey().isEmpty()) { + fluid = offset; + } + if (offset % 4 == 0) { + String biome = Objects.requireNonNull(source.biome(x, y, z), "physical biome"); + Short index = biomePalette.get(biome); + if (index == null) { + index = (short) biomePalette.size(); + biomePalette.put(biome, index); + } + biomeIndices[offset / 4] = index; + } + } + BoundaryColumnGeometry geometry = referenceGeometry(MINIMUM_Y, voxels); + OptionalInt groundSurface = source.groundSurface(x, z); + if (groundSurface.isPresent()) { + floor = geometry.surfaceOffsetNear(groundSurface.getAsInt()); + surface = Math.max(floor, fluid); + } + columns.add(new TerrainBoundarySignature( + new TerrainBoundarySignature.Column(chunkX * 16 + x, chunkZ * 16 + z, surface, floor, + fluid > floor ? OptionalInt.of(fluid) : OptionalInt.empty(), OptionalInt.empty()), + new TerrainBoundarySignature.Samples( + new TerrainBoundarySignature.VerticalLayout(MINIMUM_Y, 4, biomeIndices.length), + new TerrainBoundarySignature.BiomeEncoding(List.copyOf(biomePalette.keySet()), biomeIndices)), + geometry)); + } + } + return new SavedTerrainChunk(chunkX, chunkZ, "minecraft:noise", columns); + } + + private static BoundaryColumnGeometry referenceGeometry(int minimumY, List voxels) { + TreeSet used = new TreeSet<>(VOXEL_ORDER); + BoundaryColumnGeometry.Voxel previous = null; + for (BoundaryColumnGeometry.Voxel voxel : voxels) { + if (previous == null || !previous.equals(voxel)) { + used.add(voxel); + previous = voxel; + } + } + List palette = List.copyOf(used); + Map indices = new HashMap<>(palette.size()); + for (int index = 0; index < palette.size(); index++) { + indices.put(palette.get(index), (short) index); + } + int[] ends = new int[voxels.size()]; + short[] values = new short[voxels.size()]; + int runCount = 0; + previous = null; + for (int offset = 0; offset < voxels.size(); offset++) { + BoundaryColumnGeometry.Voxel voxel = voxels.get(offset); + if (previous == null || !previous.equals(voxel)) { + values[runCount++] = indices.get(voxel); + previous = voxel; + } + ends[runCount - 1] = offset + 1; + } + return new BoundaryColumnGeometry(minimumY, palette, Arrays.copyOf(ends, runCount), Arrays.copyOf(values, runCount)); + } + + private static BoundaryColumnGeometry.Voxel voxel(String state, BoundaryColumnGeometry.Phase phase, String fluid, + boolean protectedContent) { + return new BoundaryColumnGeometry.Voxel(state, phase, fluid, protectedContent); + } + + private static final class RandomSource implements SavedTerrainChunk.VoxelSource { + private final BoundaryColumnGeometry.Voxel[][] voxels = new BoundaryColumnGeometry.Voxel[256][HEIGHT]; + private final String[][] biomes = new String[256][HEIGHT]; + private final int[] ground = new int[256]; + + private RandomSource(Random random, int sample) { + for (int column = 0; column < 256; column++) { + int offset = 0; + while (offset < HEIGHT) { + BoundaryColumnGeometry.Voxel state = STATES.get(random.nextInt(STATES.size())); + BoundaryColumnGeometry.Voxel copy = random.nextBoolean() ? state + : new BoundaryColumnGeometry.Voxel(new String(state.stateKey()), state.phase(), + new String(state.fluidStateKey()), state.protectedContent()); + int end = Math.min(HEIGHT, offset + 1 + random.nextInt(sample % 3 == 0 ? 3 : 64)); + while (offset < end) { + voxels[column][offset++] = copy; + } + } + offset = 0; + while (offset < HEIGHT) { + String biome = BIOMES.get(random.nextInt(BIOMES.size())); + String copy = random.nextBoolean() ? biome : new String(biome); + int end = Math.min(HEIGHT, offset + 4 + random.nextInt(96)); + while (offset < end) { + biomes[column][offset++] = copy; + } + } + ground[column] = sample % 4 == 0 ? -1 : random.nextInt(HEIGHT); + } + } + + @Override + public OptionalInt groundSurface(int localX, int localZ) { + int value = ground[localX * 16 + localZ]; + return value < 0 ? OptionalInt.empty() : OptionalInt.of(value); + } + + @Override + public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) { + return voxels[localX * 16 + localZ][worldY - MINIMUM_Y]; + } + + @Override + public String biome(int localX, int worldY, int localZ) { + return biomes[localX * 16 + localZ][worldY - MINIMUM_Y]; + } + } +} From c00dfbddcee285ac0d2af440470bcb2bd2de945c Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:43:39 -0400 Subject: [PATCH 3/4] Cheaper cave biome resolution - carving root entries cached per world Y in arrays instead of a boxed-key map - semantic cave capture skips re-adding the key it added last --- .../terrain/IrisDimensionCarvingResolver.java | 44 +++++++++++++++---- .../history/GenerationSemanticCapture.java | 12 ++++- ...risDimensionCarvingResolverParityTest.java | 7 ++- 3 files changed, 48 insertions(+), 15 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 9607089eb..0ef34aff0 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 @@ -14,7 +14,7 @@ import art.arcane.volmlib.util.noise.CNG; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -81,8 +81,9 @@ public final class IrisDimensionCarvingResolver { } private static IrisDimensionCarvingEntry resolveBoundRootEntry(Engine engine, int worldY, IrisData data, State resolvedState) { - if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) { - return resolvedState.rootEntriesByWorldY.get(worldY); + int slot = resolvedState.rootSlot(worldY); + if (slot >= 0) { + return resolvedState.roots[slot]; } return selectBoundRootEntry(engine.getDimension(), worldY, data, resolvedState); @@ -91,7 +92,7 @@ public final class IrisDimensionCarvingResolver { private static IrisDimensionCarvingEntry selectBoundRootEntry(IrisDimension dimension, int worldY, IrisData data, State resolvedState) { List entries = dimension.getCarving(); if (entries == null || entries.isEmpty()) { - resolvedState.rootEntriesByWorldY.put(worldY, null); + resolvedState.rememberRoot(worldY, null); return null; } @@ -104,7 +105,7 @@ public final class IrisDimensionCarvingResolver { resolved = entry; } - resolvedState.rootEntriesByWorldY.put(worldY, resolved); + resolvedState.rememberRoot(worldY, resolved); return resolved; } @@ -324,8 +325,9 @@ public final class IrisDimensionCarvingResolver { } public IrisBiome resolveBiome(int worldX, int worldY, int worldZ) { - IrisDimensionCarvingEntry root = state.rootEntriesByWorldY.containsKey(worldY) - ? state.rootEntriesByWorldY.get(worldY) + int slot = state.rootSlot(worldY); + IrisDimensionCarvingEntry root = slot >= 0 + ? state.roots[slot] : selectBoundRootEntry(dimension, worldY, data, state); if (root == null) { return null; @@ -336,7 +338,11 @@ public final class IrisDimensionCarvingResolver { } public static final class State { - private final Map rootEntriesByWorldY = new HashMap<>(); + private static final int ROOT_SLOTS = 512; + + private int[] rootWorldYs; + private IrisDimensionCarvingEntry[] roots; + private boolean[] rootsKnown; private final Map> selectionPlans = new IdentityHashMap<>(); private final Map biomeCache = new IdentityHashMap<>(); private WeakReference engineIdentity; @@ -357,7 +363,10 @@ public final class IrisDimensionCarvingResolver { engineIdentity = new WeakReference<>(engine); dimensionIdentity = new WeakReference<>(dimension); dataIdentity = new WeakReference<>(data); - rootEntriesByWorldY.clear(); + if (rootsKnown != null) { + Arrays.fill(rootsKnown, false); + Arrays.fill(roots, null); + } selectionPlans.clear(); biomeCache.clear(); entryIndex = null; @@ -366,6 +375,23 @@ public final class IrisDimensionCarvingResolver { return data; } + private int rootSlot(int worldY) { + int slot = worldY & (ROOT_SLOTS - 1); + return rootsKnown != null && rootsKnown[slot] && rootWorldYs[slot] == worldY ? slot : -1; + } + + private void rememberRoot(int worldY, IrisDimensionCarvingEntry root) { + if (rootsKnown == null) { + rootWorldYs = new int[ROOT_SLOTS]; + roots = new IrisDimensionCarvingEntry[ROOT_SLOTS]; + rootsKnown = new boolean[ROOT_SLOTS]; + } + int slot = worldY & (ROOT_SLOTS - 1); + rootWorldYs[slot] = worldY; + roots[slot] = root; + rootsKnown[slot] = true; + } + private static boolean references(WeakReference identity, Object value) { return identity != null && identity.get() == value; } diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java index edd4de352..8e8c8cb9a 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java @@ -227,12 +227,12 @@ public final class GenerationSemanticCapture { } resolvedCaves.set(position); if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) { - semantics.addCaveBiome(explicitBiomeKey); + caveBiomes.add(semantics, explicitBiomeKey); return; } IrisBiome caveBiome = caveBiomes.resolve(blockX, y, blockZ); if (caveBiome != null && caveBiome.getLoadKey() != null) { - semantics.addCaveBiome(caveBiome.getLoadKey()); + caveBiomes.add(semantics, caveBiome.getLoadKey()); } } @@ -280,6 +280,7 @@ public final class GenerationSemanticCapture { private final IrisDimensionCarvingResolver.State fallbackState = new IrisDimensionCarvingResolver.State(); private IrisDimensionCarvingResolver.Snapshot snapshot; private int minimumY; + private String lastKey; private CaveBiomes(Engine engine, boolean scoped) { this.engine = engine; @@ -290,6 +291,13 @@ public final class GenerationSemanticCapture { : null; } + private void add(ChunkGenerationSemantics.Builder semantics, String key) { + if (key != lastKey) { + semantics.addCaveBiome(key); + lastKey = key; + } + } + private IrisBiome resolve(int x, int y, int z) { if (!scoped) { return engine.getCaveBiome(x, y, z); 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 3ee98ec8b..504be0ec7 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 @@ -494,12 +494,11 @@ public class IrisDimensionCarvingResolverParityTest { assertSame(fixture.entry(), IrisDimensionCarvingResolver.resolveRootEntry( fixture.engine(), 80, state)); - Field rootEntriesField = IrisDimensionCarvingResolver.State.class - .getDeclaredField("rootEntriesByWorldY"); - rootEntriesField.setAccessible(true); + Field rootsField = IrisDimensionCarvingResolver.State.class.getDeclaredField("roots"); + rootsField.setAccessible(true); System.gc(); - assertSame(fixture.entry(), ((Map) rootEntriesField.get(state)).get(80)); + assertSame(fixture.entry(), ((IrisDimensionCarvingEntry[]) rootsField.get(state))[80]); } private RetainedFixture createRetainedFixture() { From adbcba3efc4b6ea87dc7123ccecf82ee47f65187 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:43:39 -0400 Subject: [PATCH 4/4] Batch history biome reads - saved biome capture resolves each quart column through one engine call - column-invariant height, region, surface and cave fallback resolved once - cavern reads use one pinned mantle chunk per column - saved biome cells interned per capture - engine getters borrow the bound route runtime for other chunks instead of opening a stage --- .../iris/generation/runtime/Engine.java | 132 ++++++++++--- .../iris/generation/runtime/IrisEngine.java | 57 ++++-- .../GenerationHistoryRuntimeRouter.java | 19 ++ .../iris/world/history/SavedBiomeCapture.java | 66 +++++-- .../runtime/EngineBiomeColumnTest.java | 180 ++++++++++++++++++ .../IrisEngineGenerationRuntimeScopeTest.java | 16 +- ...rationHistoryRuntimeRouterRoutingTest.java | 46 +++++ .../world/history/SavedBiomeCaptureTest.java | 20 ++ 8 files changed, 471 insertions(+), 65 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java index 23846e0ae..6816d3739 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java @@ -59,6 +59,7 @@ import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.function.Function2; import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.math.M; @@ -341,25 +342,101 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, @BlockCoordinates default IrisBiome getCaveOrMantleBiome(int x, int y, int z) { + IrisBiome mantleBiome = mantleCaveBiome(x, y, z); + return mantleBiome != null ? mantleBiome : getCaveBiome(x, y, z); + } + + private IrisBiome mantleCaveBiome(int x, int y, int z) { + IrisBiome flooded = floodedCaveBiome(x, y, z); + return flooded != null ? flooded : customCaveBiome(getMantle().getMantle().get(x, y, z, MatterCavern.class)); + } + + private IrisBiome floodedCaveBiome(int x, int y, int z) { HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent( getMantle().getMantle(), x, y, z); if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) { - IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey()); - if (biome != null) { - return biome; + return getData().getBiomeLoader().load(hydrology.floodedBiomeKey()); + } + return null; + } + + private IrisBiome customCaveBiome(MatterCavern cavern) { + if (cavern != null && cavern.getCustomBiome() != null && !cavern.getCustomBiome().isEmpty()) { + return getData().getBiomeLoader().load(cavern.getCustomBiome()); + } + return null; + } + + private MantleChunk useCavernChunk(int x, int z) { + Mantle mantle = getMantle().getMantle(); + int chunkX = x >> 4; + int chunkZ = z >> 4; + return mantle.hasTectonicPlate(chunkX >> 5, chunkZ >> 5) ? mantle.getChunk(chunkX, chunkZ).use() : null; + } + + /** + * Fills {@code biomes[i]} with {@link #getBiomeOrMantle(int, int, int)} and {@code regions[i]} with + * {@link #getRegion(int, int, int)} at y = i * step of one column, resolving what does not vary along it once. + */ + @BlockCoordinates + default void getBiomeOrMantleColumn(int x, int z, int step, IrisBiome[] biomes, IrisRegion[] regions) { + if (getDimensionStackContext() != null) { + for (int index = 0; index < biomes.length; index++) { + biomes[index] = getBiomeOrMantle(x, index * step, z); + regions[index] = getRegion(x, index * step, z); + } + return; + } + IrisComplex complex = getComplex(); + int caveTop = getHeight(x, z) - 2; + IrisRegion region = getRegion(x, 0, z); + IrisBiome surface = null; + boolean cavernChunkResolved = false; + MantleChunk cavernChunk = null; + boolean caveColumnResolved = false; + IrisBiome caveBase = null; + IrisBiome caveSurface = null; + int caveSurfaceY = 0; + try { + for (int index = 0; index < biomes.length; index++) { + int y = index * step; + regions[index] = region; + if (y > caveTop || complex.isTerrain3DSurface(x, y, z)) { + if (surface == null) { + surface = getSurfaceBiome(x, z); + } + biomes[index] = surface; + continue; + } + IrisBiome biome = floodedCaveBiome(x, y, z); + if (biome == null) { + if (!cavernChunkResolved) { + cavernChunk = useCavernChunk(x, z); + cavernChunkResolved = true; + } + biome = cavernChunk == null || y < 0 || y >= getMantle().getMantle().getWorldHeight() + ? null : customCaveBiome(cavernChunk.get(x & 15, y, z & 15, MatterCavern.class)); + } + if (biome == null) { + biome = resolveConfiguredCaveBiome(x, y, z, null); + } + if (biome == null) { + if (!caveColumnResolved) { + boolean naturalFallback = answersFromNaturalTerrain(x, z); + caveSurface = caveDepthSurfaceBiome(x, z, naturalFallback); + caveSurfaceY = caveDepthSurfaceY(x, z, naturalFallback); + caveBase = getCaveBiome(x, z); + caveColumnResolved = true; + } + biome = depthCaveBiome(caveBase, y, caveSurface, caveSurfaceY); + } + biomes[index] = biome; + } + } finally { + if (cavernChunk != null) { + cavernChunk.release(); } } - MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class); - - if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) { - IrisBiome biome = getData().getBiomeLoader().load(m.getCustomBiome()); - - if (biome != null) { - return biome; - } - } - - return getCaveBiome(x, y, z); } @ChunkCoordinates @@ -386,16 +463,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, return configuredBiome; } boolean naturalFallback = answersFromNaturalTerrain(x, z); - DimensionStackContext dimensionStackContext = getDimensionStackContext(); - IrisBiome surfaceBiome = naturalFallback || dimensionStackContext == null - ? naturalFallback - ? getComplex().naturalSurfaceBiome(x, z) - : getSurfaceBiome(x, z) - : getComplex().getTrueBiomeStream().get(x, z); - int surfaceY = naturalFallback + IrisBiome surfaceBiome = caveDepthSurfaceBiome(x, z, naturalFallback); + int surfaceY = caveDepthSurfaceY(x, z, naturalFallback); + return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY); + } + + private IrisBiome caveDepthSurfaceBiome(int x, int z, boolean naturalFallback) { + if (naturalFallback) { + return getComplex().naturalSurfaceBiome(x, z); + } + return getDimensionStackContext() == null ? getSurfaceBiome(x, z) : getComplex().getTrueBiomeStream().get(x, z); + } + + private int caveDepthSurfaceY(int x, int z, boolean naturalFallback) { + return naturalFallback ? getComplex().naturalTrueHeight(x, z) : getComplex().getHeightStream().get(x, z).intValue(); - return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY); } @BlockCoordinates @@ -431,7 +514,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, IrisBiome surfaceBiome, int surfaceY ) { - IrisBiome caveBiome = getCaveBiome(x, z); + return depthCaveBiome(getCaveBiome(x, z), y, surfaceBiome, surfaceY); + } + + private static IrisBiome depthCaveBiome(IrisBiome caveBiome, int y, IrisBiome surfaceBiome, int surfaceY) { if (caveBiome == null) { return surfaceBiome; } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 70cdd6b7c..37d89946f 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -519,7 +519,7 @@ public class IrisEngine implements Engine { return drawBiomeEnvironment(x, z, saved.get()); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "draw a pregeneration preview")) { + openGenerationHistoryReadScope(x, z, "draw a pregeneration preview")) { return Engine.super.draw(x, z); } }); @@ -535,7 +535,7 @@ public class IrisEngine implements Engine { return saved.get(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome environment")) { + openGenerationHistoryReadScope(x, z, "resolve a biome environment")) { return Engine.super.getBiomeEnvironment(x, y, z); } } @@ -547,7 +547,7 @@ public class IrisEngine implements Engine { return saved.get(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a mantle biome environment")) { + openGenerationHistoryReadScope(x, z, "resolve a mantle biome environment")) { return Engine.super.getBiomeOrMantleEnvironment(x, y, z); } } @@ -559,7 +559,7 @@ public class IrisEngine implements Engine { return saved.get(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome environment")) { + openGenerationHistoryReadScope(x, z, "resolve a surface biome environment")) { return Engine.super.getSurfaceBiomeEnvironment(x, z); } } @@ -595,7 +595,7 @@ public class IrisEngine implements Engine { return saved.get().biome(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome")) { + openGenerationHistoryReadScope(x, z, "resolve a biome")) { return Engine.super.getBiome(x, y, z); } } @@ -608,11 +608,27 @@ public class IrisEngine implements Engine { return saved.get().biome(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome or mantle biome")) { + openGenerationHistoryReadScope(x, z, "resolve a biome or mantle biome")) { return Engine.super.getBiomeOrMantle(x, y, z); } } + @BlockCoordinates + @Override + public void getBiomeOrMantleColumn(int x, int z, int step, IrisBiome[] biomes, IrisRegion[] regions) { + if (usesSavedBiomeEnvironment()) { + for (int index = 0; index < biomes.length; index++) { + biomes[index] = getBiomeOrMantle(x, index * step, z); + regions[index] = getRegion(x, index * step, z); + } + return; + } + try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = + openGenerationHistoryReadScope(x, z, "resolve a biome or mantle biome column")) { + Engine.super.getBiomeOrMantleColumn(x, z, step, biomes, regions); + } + } + @BlockCoordinates @Override public IrisRegion getRegion(int x, int z) { @@ -621,7 +637,7 @@ public class IrisEngine implements Engine { return saved.get().region(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a region")) { + openGenerationHistoryReadScope(x, z, "resolve a region")) { return Engine.super.getRegion(x, z); } } @@ -634,7 +650,7 @@ public class IrisEngine implements Engine { return saved.get().region(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical region")) { + openGenerationHistoryReadScope(x, z, "resolve a vertical region")) { return Engine.super.getRegion(x, y, z); } } @@ -647,7 +663,7 @@ public class IrisEngine implements Engine { return saved.get().biome(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave or mantle biome")) { + openGenerationHistoryReadScope(x, z, "resolve a cave or mantle biome")) { return Engine.super.getCaveOrMantleBiome(x, y, z); } } @@ -665,7 +681,7 @@ public class IrisEngine implements Engine { return Engine.super.getCaveBiome(x, z); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) { + openGenerationHistoryReadScope(x, z, "resolve a cave biome")) { return Engine.super.getCaveBiome(x, z); } } @@ -692,7 +708,7 @@ public class IrisEngine implements Engine { return Engine.super.getCaveBiome(x, y, z, state); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) { + openGenerationHistoryReadScope(x, z, "resolve a vertical cave biome")) { return Engine.super.getCaveBiome(x, y, z, state); } } @@ -708,7 +724,7 @@ public class IrisEngine implements Engine { return Engine.super.getSurfaceBiome(x, z); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) { + openGenerationHistoryReadScope(x, z, "resolve a surface biome")) { return Engine.super.getSurfaceBiome(x, z); } } @@ -717,7 +733,7 @@ public class IrisEngine implements Engine { @Override public double getSlope(int x, int z) { try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve terrain slope")) { + openGenerationHistoryReadScope(x, z, "resolve terrain slope")) { return Engine.super.getSlope(x, z); } } @@ -732,7 +748,7 @@ public class IrisEngine implements Engine { @Override public int getHeight(int x, int z, boolean ignoreFluid) { try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve terrain height")) { + openGenerationHistoryReadScope(x, z, "resolve terrain height")) { return Engine.super.getHeight(x, z, ignoreFluid); } } @@ -745,7 +761,7 @@ public class IrisEngine implements Engine { return recorded.get().objectKeys(); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) { + openGenerationHistoryReadScope(x << 4, z << 4, "resolve chunk objects")) { return getMantle().getObjectComponent().guess(x, z); } } @@ -758,7 +774,7 @@ public class IrisEngine implements Engine { return new HashSet<>(recorded.get().pointsOfInterest()); } try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked( + openGenerationHistoryReadScope( chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) { Set pois = new HashSet<>(); getMantle().getMantle().iterateChunk( @@ -854,7 +870,7 @@ public class IrisEngine implements Engine { int queryMinimumZ = Math.max(minimumZ, chunkMinimumZ); int queryMaximumZ = Math.min(maximumZ, chunkMinimumZ + 15); try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = - openGenerationHistoryCoordinateScopeUnchecked( + openGenerationHistoryReadScope( chunkMinimumX, chunkMinimumZ, "resolve native structure volumes")) { @@ -981,13 +997,16 @@ public class IrisEngine implements Engine { return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext(); } - private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked( + private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryReadScope( int blockX, int blockZ, String operation ) { try { - return openGenerationHistoryCoordinateScope(blockX, blockZ); + GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter; + return router == null + ? openGenerationHistoryCoordinateScope(blockX, blockZ) + : router.openReadScope(blockX, blockZ); } catch (IOException failure) { throw new IllegalStateException("Unable to " + operation + " through Iris generation history at " + blockX + "," + blockZ + ".", failure); diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java b/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java index 919d219c7..04760d61c 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java @@ -391,6 +391,25 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { return new CoordinateScope(blockX, blockZ, stage.route, stage); } + /** + * A coordinate scope for engine reads. Inside another chunk's route whose runtime the thread is bound to, it borrows + * that runtime instead of opening a stage: every generation stage binds the active activation, which cannot change + * while the route holds its transition participation. + */ + public CoordinateScope openReadScope(int blockX, int blockZ) throws IOException { + RuntimeRoute current = scopedRoute.get(); + if (current != null) { + if (current.chunkX() == Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE) + && current.chunkZ() == Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE)) { + return new CoordinateScope(blockX, blockZ, current, null); + } + if (current.binding == engine.captureGenerationRuntimeBinding()) { + return new CoordinateScope(blockX, blockZ, null, null); + } + } + return openCoordinateScope(blockX, blockZ); + } + public void recordNaturalTerrain(SavedTerrainChunk terrain) { SavedTerrainChunk captured = Objects.requireNonNull(terrain, "natural terrain"); RuntimeRoute route = scopedRoute.get(); diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java index 4e5b65270..90d8d63db 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java @@ -6,6 +6,7 @@ import art.arcane.iris.generation.terrain.IrisRegion; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +31,8 @@ public final class SavedBiomeCapture { TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan(); Map> historical = new HashMap<>(); Map> vertical = new HashMap<>(16); + Cells cells = new Cells(activationId); + Column column = new Column(engine, minimumY, height); for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int blockX = startX + localX; @@ -37,7 +40,7 @@ public final class SavedBiomeCapture { IrisRegion region = engine.getRegion(blockX, blockZ); IrisBiome surface = engine.getSurfaceBiome(blockX, blockZ); int surfaceY = minimumY + engine.getHeight(blockX, blockZ); - SavedBiomeChunk.Cell surfaceCell = cell(activationId, surface, region); + SavedBiomeChunk.Cell surfaceCell = cells.cell(surface, region); if (floating != null) { FloatingBiomeOverlay.Identity floatingSurface = floating.surfaceAt(localX, localZ); if (floatingSurface != null) { @@ -53,33 +56,33 @@ public final class SavedBiomeCapture { int quartKey = quartX * 16 + quartZ; List spans = vertical.get(quartKey); if (spans == null) { - spans = captureVertical(new Column(engine, activationId, startX + quartX, startZ + quartZ), transition, saved, historical, floating); + spans = captureVertical(column, startX + quartX, startZ + quartZ, cells, transition, saved, historical, floating); vertical.put(quartKey, spans); } - SavedBiomeChunk.Cell caveBase = cell(activationId, engine.getCaveBiome(blockX, blockZ), region); + SavedBiomeChunk.Cell caveBase = cells.cell(engine.getCaveBiome(blockX, blockZ), region); result.column(localX, localZ, new SavedBiomeChunk.Column(surfaceCell, caveBase, spans)); } } return result.build(); } - private static List captureVertical(Column column, TransitionGenerationPlan transition, - SavedBiomeRuntime saved, Map> historical, FloatingBiomeOverlay floating) throws IOException { - Engine engine = column.engine(); - int minimumY = engine.getMinHeight(); - int maximumY = minimumY + engine.getHeight(); + private static List captureVertical(Column column, int blockX, int blockZ, Cells cells, + TransitionGenerationPlan transition, SavedBiomeRuntime saved, + Map> historical, FloatingBiomeOverlay floating) throws IOException { + column.engine().getBiomeOrMantleColumn(blockX, blockZ, 4, column.biomes(), column.regions()); + int minimumY = column.minimumY(); + int maximumY = minimumY + column.height(); List spans = new ArrayList<>(); SavedBiomeChunk.Cell previous = null; int startY = minimumY; - for (int worldY = minimumY; worldY < maximumY; worldY += 4) { - IrisBiome biome = engine.getBiomeOrMantle(column.blockX(), worldY - minimumY, column.blockZ()); - IrisRegion region = engine.getRegion(column.blockX(), worldY - minimumY, column.blockZ()); - SavedBiomeChunk.Cell current = cell(column.activationId(), biome, region); + for (int index = 0; index < column.biomes().length; index++) { + int worldY = minimumY + index * 4; + SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]); if (floating != null) { - current = overlay(current, floating.volumeAt(column.blockX() & 15, worldY - minimumY, column.blockZ() & 15)); + current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15)); } if (transition != null) { - current = historicalCell(new Sample(column.blockX(), worldY, column.blockZ(), false), current, transition, saved, historical); + current = historicalCell(new Sample(blockX, worldY, blockZ, false), current, transition, saved, historical); } if (previous != null && !previous.equals(current)) { spans.add(new SavedBiomeChunk.Span(startY, worldY, previous)); @@ -132,7 +135,40 @@ public final class SavedBiomeCapture { Objects.requireNonNull(region, "region").getLoadKey()); } - private record Column(Engine engine, long activationId, int blockX, int blockZ) { + private record Column(Engine engine, int minimumY, int height, IrisBiome[] biomes, IrisRegion[] regions) { + private Column(Engine engine, int minimumY, int height) { + this(engine, minimumY, height, new IrisBiome[(height + 3) / 4], new IrisRegion[(height + 3) / 4]); + } + } + + private static final class Cells { + private final long activationId; + private IrisBiome[] biomes = new IrisBiome[8]; + private IrisRegion[] regions = new IrisRegion[8]; + private SavedBiomeChunk.Cell[] cells = new SavedBiomeChunk.Cell[8]; + private int size; + + private Cells(long activationId) { + this.activationId = activationId; + } + + private SavedBiomeChunk.Cell cell(IrisBiome biome, IrisRegion region) { + for (int index = 0; index < size; index++) { + if (biomes[index] == biome && regions[index] == region) { + return cells[index]; + } + } + SavedBiomeChunk.Cell created = SavedBiomeCapture.cell(activationId, biome, region); + if (size == cells.length) { + biomes = Arrays.copyOf(biomes, size * 2); + regions = Arrays.copyOf(regions, size * 2); + cells = Arrays.copyOf(cells, size * 2); + } + biomes[size] = biome; + regions[size] = region; + cells[size++] = created; + return created; + } } private record Sample(int x, int y, int z, boolean surface) { diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java new file mode 100644 index 000000000..65f612c61 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java @@ -0,0 +1,180 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.mantle.EngineMantle; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.loading.ResourceLoader; +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.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.stream.ProceduralStream; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +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.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doAnswer; +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 EngineBiomeColumnTest { + private static final int SAMPLES = 96; + + @BeforeClass + public static void initializeMantleBlockState() throws Exception { + NativeBlockState air = mock(NativeBlockState.class); + try (MockedStatic blocks = mockStatic(B.class)) { + blocks.when(() -> B.getState("AIR")).thenReturn(air); + Class.forName(EngineMantle.class.getName()); + } + } + + @Test + public void columnMatchesPointLookupsAcrossCavesSurfacesAndMantleBiomes() { + Random random = new Random(1337L); + for (int trial = 0; trial < 48; trial++) { + Engine engine = randomEngine(random, trial); + for (int column = 0; column < 24; column++) { + int x = random.nextInt(4096) - 2048; + int z = random.nextInt(4096) - 2048; + IrisBiome[] biomes = new IrisBiome[SAMPLES]; + IrisRegion[] regions = new IrisRegion[SAMPLES]; + engine.getBiomeOrMantleColumn(x, z, 4, biomes, regions); + for (int index = 0; index < SAMPLES; index++) { + assertSame(engine.getBiomeOrMantle(x, index * 4, z), biomes[index]); + assertSame(engine.getRegion(x, index * 4, z), regions[index]); + } + } + } + } + + @SuppressWarnings("unchecked") + private static Engine randomEngine(Random random, int trial) { + long salt = random.nextLong(); + IrisBiome[] surfaces = {biome("iris:plains", 0), biome("iris:desert", 0), biome("iris:forest", 0)}; + IrisBiome[] caves = {biome("iris:lush_caves", 0), biome("iris:deep_dark", 24), new IrisBiome(), null}; + IrisRegion[] regions = {region("iris:temperate"), region("iris:arid")}; + IrisBiome custom = biome("iris:custom_cave", 0); + IrisBiome configured = biome("iris:configured_cave", 0); + + ProceduralStream surfaceStream = mock(ProceduralStream.class); + when(surfaceStream.get(anyDouble(), anyDouble())).thenAnswer(call -> + surfaces[pick(salt, call.getArgument(0), call.getArgument(1), 1, surfaces.length)]); + ProceduralStream caveStream = mock(ProceduralStream.class); + when(caveStream.get(anyDouble(), anyDouble())).thenAnswer(call -> + caves[pick(salt, call.getArgument(0), call.getArgument(1), 2, caves.length)]); + ProceduralStream regionStream = mock(ProceduralStream.class); + when(regionStream.get(anyDouble(), anyDouble())).thenAnswer(call -> + regions[pick(salt, call.getArgument(0), call.getArgument(1), 3, regions.length)]); + ProceduralStream heightStream = mock(ProceduralStream.class); + when(heightStream.get(anyDouble(), anyDouble())).thenAnswer(call -> + (double) (40 + pick(salt, call.getArgument(0), call.getArgument(1), 4, 300))); + + IrisComplex complex = mock(IrisComplex.class); + when(complex.getTrueBiomeStream()).thenReturn(surfaceStream); + when(complex.getCaveBiomeStream()).thenReturn(caveStream); + when(complex.getRegionStream()).thenReturn(regionStream); + when(complex.getHeightStream()).thenReturn(heightStream); + when(complex.isTerrain3DSurface(anyInt(), anyInt(), anyInt())).thenAnswer(call -> + pick(salt, (int) call.getArgument(0) * 31 + (int) call.getArgument(1), (int) call.getArgument(2), 5, 9) == 0); + + Mantle mantle = mock(Mantle.class); + when(mantle.getWorldHeight()).thenReturn(384); + when(mantle.getLoadedRegions()).thenReturn(new KMap<>()); + when(mantle.hasTectonicPlate(anyInt(), anyInt())).thenReturn(true); + when(mantle.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(call -> + cavern(salt, call.getArgument(0), call.getArgument(1), call.getArgument(2))); + when(mantle.getChunk(anyInt(), anyInt())).thenAnswer(call -> { + int chunkX = call.getArgument(0); + int chunkZ = call.getArgument(1); + MantleChunk chunk = mock(MantleChunk.class); + when(chunk.use()).thenReturn(chunk); + when(chunk.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(read -> cavern(salt, + chunkX * 16 + (int) read.getArgument(0), read.getArgument(1), chunkZ * 16 + (int) read.getArgument(2))); + return chunk; + }); + EngineMantle engineMantle = mock(EngineMantle.class); + when(engineMantle.getMantle()).thenReturn(mantle); + doAnswer(call -> 20 + pick(salt, (int) call.getArgument(0), (int) call.getArgument(1), 7, 330)) + .when(engineMantle).getHighest(anyInt(), anyInt(), any(IrisData.class), anyBoolean()); + + IrisData data = mock(IrisData.class); + ResourceLoader loader = mock(ResourceLoader.class); + when(data.getBiomeLoader()).thenReturn(loader); + when(loader.load(anyString())).thenReturn(null); + when(loader.load("iris:custom_cave")).thenReturn(custom); + when(loader.load("iris:configured_cave")).thenReturn(configured); + + IrisDimension dimension = new IrisDimension(); + if (trial % 2 == 1) { + IrisDimensionCarvingEntry root = new IrisDimensionCarvingEntry(); + root.setBiome("iris:configured_cave"); + int bottom = -64 + random.nextInt(120); + root.setWorldYRange(new IrisRange(bottom, bottom + 8 + random.nextInt(80))); + root.setChildRecursionDepth(0); + dimension.setCarving(new KList<>(List.of(root))); + } + + EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); + Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + doReturn(complex).when(engine).getComplex(); + doReturn(engineMantle).when(engine).getMantle(); + doReturn(hooks).when(engine).getPlatformHooks(); + doReturn(data).when(engine).getData(); + doReturn(dimension).when(engine).getDimension(); + doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld(); + return engine; + } + + private static MatterCavern cavern(long salt, int x, int y, int z) { + return switch (pick(salt, x * 17 + y, z, 6, 12)) { + case 0 -> new MatterCavern(true, "iris:custom_cave", (byte) 0); + case 1 -> new MatterCavern(true, "iris:missing_cave", (byte) 1); + case 2 -> new MatterCavern(true, "", (byte) 0); + default -> null; + }; + } + + private static int pick(long salt, double x, double z, int channel, int bound) { + long mixed = salt ^ ((long) x * 0x9E3779B97F4A7C15L) ^ ((long) z * 0xC2B2AE3D27D4EB4FL) ^ channel * 0x165667B19E3779F9L; + mixed ^= mixed >>> 33; + mixed *= 0xFF51AFD7ED558CCDL; + mixed ^= mixed >>> 33; + return (int) Math.floorMod(mixed, (long) bound); + } + + private static IrisBiome biome(String key, int caveMinDepth) { + IrisBiome biome = new IrisBiome(); + biome.setLoadKey(key); + biome.setCaveMinDepthBelowSurface(caveMinDepth); + return biome; + } + + private static IrisRegion region(String key) { + IrisRegion region = new IrisRegion(); + region.setLoadKey(key); + return region; + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java index 011b55946..f7281edf0 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java @@ -204,10 +204,10 @@ public class IrisEngineGenerationRuntimeScopeTest { assertSame(cave, engine.getCaveBiome(19, 20, -3, new IrisDimensionCarvingResolver.State())); assertSame(surface, engine.getSurfaceBiome(19, -3)); assertSame(selected.complex, engine.getComplex()); - verify(router, never()).openCoordinateScope(anyInt(), anyInt()); + verify(router, never()).openReadScope(anyInt(), anyInt()); when(context.isNaturalTerrain()).thenReturn(false); assertSame(cave, engine.getCaveBiome(19, -3)); - verify(router, times(1)).openCoordinateScope(19, -3); + verify(router, times(1)).openReadScope(19, -3); } assertSame(active.complex, engine.getComplex()); assertNull(IrisContext.get()); @@ -242,7 +242,7 @@ public class IrisEngineGenerationRuntimeScopeTest { } } engine.getCaveBiome(19, -3); - verify(router, times(3)).openCoordinateScope(19, -3); + verify(router, times(3)).openReadScope(19, -3); } @Test @@ -274,7 +274,7 @@ public class IrisEngineGenerationRuntimeScopeTest { when(caves.get(19D, -3D)).thenReturn(cave); when(surfaces.get(19D, -3D)).thenReturn(surface); when(regions.get(19D, -3D)).thenReturn(region); - when(router.openCoordinateScope(19, -3)).thenAnswer(invocation -> { + when(router.openReadScope(19, -3)).thenAnswer(invocation -> { assertSame(active.complex, engine.getComplex()); IrisEngine.GenerationRuntimeScope runtimeScope = engine.openGenerationRuntimeScope(binding); GenerationHistoryRuntimeRouter.CoordinateScope coordinateScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class); @@ -292,7 +292,7 @@ public class IrisEngineGenerationRuntimeScopeTest { assertSame(region, engine.getRegion(19, 20, -3)); assertSame(active.complex, engine.getComplex()); } - verify(router, times(4)).openCoordinateScope(19, -3); + verify(router, times(4)).openReadScope(19, -3); verifyNoInteractions(saved); assertNull(IrisContext.get()); } @@ -318,7 +318,7 @@ public class IrisEngineGenerationRuntimeScopeTest { } verify(saved).resolveCaveBase(19, -3); verify(saved, times(2)).resolve(19, -64, -3, true); - verify(router, never()).openCoordinateScope(anyInt(), anyInt()); + verify(router, never()).openReadScope(anyInt(), anyInt()); assertNull(IrisContext.get()); } @@ -839,7 +839,7 @@ public class IrisEngineGenerationRuntimeScopeTest { assertSame(environment.biome(), engine.getSurfaceBiome(19, -3)); assertSame(environment.region(), engine.getRegion(19, -3)); assertSame(environment.region(), engine.getRegion(19, 20, -3)); - verify(router, never()).openCoordinateScope(anyInt(), anyInt()); + verify(router, never()).openReadScope(anyInt(), anyInt()); } @Test @@ -855,7 +855,7 @@ public class IrisEngineGenerationRuntimeScopeTest { assertSame(failure, assertThrows(SavedBiomeUnavailableException.class, () -> engine.getBiome(19, 20, -3))); - verify(router, never()).openCoordinateScope(anyInt(), anyInt()); + verify(router, never()).openReadScope(anyInt(), anyInt()); } @Test diff --git a/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouterRoutingTest.java b/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouterRoutingTest.java index b68468f74..4086a25ee 100644 --- a/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouterRoutingTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouterRoutingTest.java @@ -16,6 +16,7 @@ import org.mockito.MockedStatic; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -350,6 +351,11 @@ public final class GenerationHistoryRuntimeRouterRoutingTest extends GenerationH when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(biome); when(engine.getRegion(anyInt(), anyInt())).thenReturn(region); when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(region); + doAnswer(invocation -> { + Arrays.fill((IrisBiome[]) invocation.getArgument(3), biome); + Arrays.fill((IrisRegion[]) invocation.getArgument(4), region); + return null; + }).when(engine).getBiomeOrMantleColumn(anyInt(), anyInt(), anyInt(), any(), any()); EngineMantle engineMantle = mock(EngineMantle.class); @SuppressWarnings("unchecked") Mantle mantle = mock(Mantle.class); @@ -429,4 +435,44 @@ public final class GenerationHistoryRuntimeRouterRoutingTest extends GenerationH verify(engine, times(2)).openGenerationRuntimeScope(first); router.close(); } + + @Test + public void readScopeBorrowsTheBoundRouteRuntimeForOtherChunks() throws Exception { + Path world = temporaryFolder.newFolder("router-read-world").toPath(); + Path pack = createPack("router-read-pack", "alpha"); + GenerationHistory history = createHistory(world, pack); + IrisEngine engine = mock(IrisEngine.class); + FakeRuntimeFactory runtimes = new FakeRuntimeFactory(); + IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation()); + when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first); + AtomicReference scoped = installScopeTracking(engine); + when(engine.captureGenerationRuntimeBinding()).thenAnswer(invocation -> scoped.get()); + GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, + history, + (ignored, blockX, blockZ) -> signature(blockX, blockZ), + runtimes + ); + + try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(-1, -2); + GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope ignored = route.openRuntimeScope()) { + try (GenerationHistoryRuntimeRouter.CoordinateScope same = router.openReadScope(-1, -17)) { + assertSame(route.activation(), same.activation()); + } + try (GenerationHistoryRuntimeRouter.CoordinateScope neighbour = router.openReadScope(16, 0)) { + assertEquals(1, neighbour.chunkX()); + assertEquals(0, neighbour.chunkZ()); + assertFalse(neighbour.claimGeneratedSemantics()); + assertSame(first, scoped.get()); + } + verify(engine, times(1)).openGenerationRuntimeScope(first); + + scoped.set(runtimes.binding(history, history.activeActivation())); + try (GenerationHistoryRuntimeRouter.CoordinateScope rebound = router.openReadScope(16, 0)) { + assertSame(route.activation(), rebound.activation()); + } + verify(engine, times(2)).openGenerationRuntimeScope(first); + } + router.close(); + } } diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java index eaf64d98f..ea6dbe2f2 100644 --- a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java @@ -11,9 +11,11 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; 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.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; @@ -92,6 +94,7 @@ public class SavedBiomeCaptureTest { when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(host); when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(host); when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(host); + columnsFromPointLookups(engine); FloatingBiomeOverlay floating = new FloatingBiomeOverlay(16); FloatingBiomeOverlay.Identity childIdentity = new FloatingBiomeOverlay.Identity(child.getLoadKey(), region.getLoadKey()); floating.record(0, 8, 0, childIdentity); @@ -135,6 +138,7 @@ public class SavedBiomeCaptureTest { when(engine.getSurfaceBiome(anyInt(), anyInt())).thenAnswer(call -> (int) call.getArgument(0) == -15 ? right : left); when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(cave); when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call -> (int) call.getArgument(1) < 8 ? lower : upper); + columnsFromPointLookups(engine); SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, historical, null); @@ -167,9 +171,25 @@ public class SavedBiomeCaptureTest { doCallRealMethod().when(engine).getBiomeOrMantle(anyInt(), anyInt(), anyInt()); when(engine.getCaveOrMantleBiome(anyInt(), anyInt(), anyInt())) .thenAnswer(call -> engine.getCaveBiome(call.getArgument(0), call.getArgument(2))); + columnsFromPointLookups(engine); return engine; } + private static void columnsFromPointLookups(Engine engine) { + doAnswer(call -> { + int x = call.getArgument(0); + int z = call.getArgument(1); + int step = call.getArgument(2); + IrisBiome[] biomes = call.getArgument(3); + IrisRegion[] regions = call.getArgument(4); + for (int index = 0; index < biomes.length; index++) { + biomes[index] = engine.getBiomeOrMantle(x, index * step, z); + regions[index] = engine.getRegion(x, index * step, z); + } + return null; + }).when(engine).getBiomeOrMantleColumn(anyInt(), anyInt(), anyInt(), any(), any()); + } + private static GenerationHistory.GenerationStage stage() { GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); when(stage.chunkX()).thenReturn(-1);