From 2fef055e5c72658fc52308d2508d975387c1be26 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:43:39 -0400 Subject: [PATCH] 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]; + } + } +}