From 75353d43c7da00beddf20cd7dac7133017e49404 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:36:35 -0400 Subject: [PATCH 1/2] Bounded sea surface decoration - Sea surface decorators respect engine and output ceilings in every placement mode - Clipped sea stacks retain their top palette - Tests cover short outputs negative bases and engine ceilings - Multi-palette tests verify shared random sources match fresh per-block sources --- .../decorator/IrisSeaSurfaceDecorator.java | 30 +++----- .../decorator/IrisSeaDecoratorBoundsTest.java | 37 +++++++++- .../object/tree/TreeBlockResolverTest.java | 70 +++++++++++++++++++ 3 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java index 0f79a82c3..03f2e8e0e 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java @@ -39,6 +39,10 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { @Override public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { + int maxBaseY = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()) - 1); + if (height < 0 || height >= maxBaseY) { + return; + } RNG rng = getRNG(realX, realZ); IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); @@ -48,8 +52,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { if (!decorator.isStacking()) { int targetY = height + 1; - if (height >= 0 && targetY < getEngine().getHeight() - && DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); if (block != null) { data.set(x, targetY, z, block); @@ -58,30 +61,15 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { return; } - int stack = DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height); - - if (stack == 1) { - int targetY = height + 1; - if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { - return; - } - - PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()); - if (block != null) { - data.set(x, targetY, z, block); - } - return; - } - - int engineHeight = getEngine().getHeight(); + int stack = Math.min(maxBaseY - height, + DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height)); for (int i = 0; i < stack; i++) { int h = height + i; int targetY = h + 1; - if (h >= max || targetY >= engineHeight - || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + if (!DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { break; } - double threshold = ((double) i) / (stack - 1); + double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1); PlatformBlockState block = threshold >= decorator.getTopThreshold() ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java index 19312a4c7..064489641 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisSeaDecoratorBoundsTest.java @@ -77,16 +77,51 @@ public class IrisSeaDecoratorBoundsTest { assertNull(fixture.output.get(0, 4, 0)); } + @Test + public void surfaceStackUsesTheShorterOutputHeight() { + Fixture fixture = new Fixture(8, true); + when(fixture.engine.getHeight()).thenReturn(64); + fixture.stacking(200, 30); + + fixture.decorate(6, 64); + + assertSame(fixture.top, fixture.output.get(0, 7, 0)); + } + + @Test + public void surfaceSingleRejectsOutputCeiling() { + Fixture fixture = new Fixture(8, true); + when(fixture.engine.getHeight()).thenReturn(64); + + fixture.decorate(7, 64); + + assertNull(fixture.output.get(0, 7, 0)); + } + + @Test + public void surfaceSingleStackRejectsNegativeBaseAndEngineCeiling() { + Fixture fixture = new Fixture(16, true); + when(fixture.engine.getHeight()).thenReturn(8); + fixture.stacking(1, 30); + when(fixture.decorator.isScaleStack()).thenReturn(false); + + fixture.decorate(-1, 16); + fixture.decorate(7, 16); + + assertNull(fixture.output.get(0, 0, 0)); + assertNull(fixture.output.get(0, 8, 0)); + } + private static final class Fixture { private final IrisDecorator decorator = mock(IrisDecorator.class); private final IrisBiome biome = mock(IrisBiome.class); private final PlatformBlockState body = mock(PlatformBlockState.class); private final PlatformBlockState top = mock(PlatformBlockState.class); + private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); private final Hunk output; private final IrisEngineDecorator placement; private Fixture(int height, boolean surface) { - Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); when(engine.getHeight()).thenReturn(height); output = Hunk.newArrayHunk(1, height, 1); IrisDecorationPart part = surface ? IrisDecorationPart.SEA_SURFACE : IrisDecorationPart.SEA_FLOOR; diff --git a/core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java b/core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java new file mode 100644 index 000000000..52fdd22b7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/tree/TreeBlockResolverTest.java @@ -0,0 +1,70 @@ +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisBlockData; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TreeBlockResolverTest { + @Test + public void sharedPaletteRandomSourceMatchesFreshPerVoxelSources() { + IrisProceduralTree tree = new IrisProceduralTree(); + tree.setSeed(84261L); + tree.setTrunkPalette(palette("oak_log", "spruce_log")); + tree.setLeavesPalette(palette("oak_leaves", "birch_leaves")); + tree.setSecondaryTrunkPalette(palette("birch_log", "jungle_log")); + tree.setSecondaryLeavesPalette(palette("spruce_leaves", "jungle_leaves")); + RNG shared = new RNG(tree.getSeed()); + List freshBlocks = new ArrayList<>(); + List sharedBlocks = new ArrayList<>(); + Set materials = new HashSet<>(); + TreeBlockCanvas.Role[] roles = { + TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Role.LEAF, + TreeBlockCanvas.Role.SECONDARY_TRUNK, TreeBlockCanvas.Role.SECONDARY_LEAF + }; + for (int x = -8; x <= 8; x++) { + for (int y = 0; y < 24; y++) { + TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(x, y, x - y); + TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell( + roles[Math.floorMod(x + y, roles.length)], TreeBlockCanvas.Axis.NONE, false, -1, null); + PlatformBlockState expected = TreeBlockResolver.resolve(tree, null, cell, position, new RNG(tree.getSeed())); + PlatformBlockState actual = TreeBlockResolver.resolve(tree, null, cell, position, shared); + freshBlocks.add(position + ":" + expected.key()); + sharedBlocks.add(position + ":" + actual.key()); + materials.add(actual.key()); + } + } + + assertEquals(freshBlocks, sharedBlocks); + assertEquals(new RNG(tree.getSeed()).nextLong(), shared.nextLong()); + assertTrue(materials.size() > roles.length); + } + + private static IrisMaterialPalette palette(String first, String second) { + IrisBlockData firstBlock = block(first); + IrisBlockData secondBlock = block(second); + return new IrisMaterialPalette().setPalette(new KList<>(firstBlock, secondBlock)); + } + + private static IrisBlockData block(String material) { + PlatformBlockState state = mock(PlatformBlockState.class); + when(state.key()).thenReturn("minecraft:" + material); + IrisBlockData block = mock(IrisBlockData.class); + when(block.getWeight()).thenReturn(1); + when(block.getBlockData(null)).thenReturn(state); + return block; + } +} From 420148fcc74545ab597a0ebcf24aaaafefcffdbf Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:46:25 -0400 Subject: [PATCH 2/2] Bounded deposit preparation - Deposit preparation retains at most 32 clumps between ordered placement passes - Large deposit counts retain parallel preparation and stable per-clump seeds - Overlap tests cover every attempt across preparation windows - Deposit tests isolate platform block conversion from cached registry fixtures --- .../engine/modifier/IrisDepositModifier.java | 85 +++++++++++++------ .../IrisDepositModifierOrderingTest.java | 54 ++++++++++-- 2 files changed, 103 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java index c0642db39..6dbd40b16 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java @@ -50,6 +50,9 @@ import java.util.ArrayList; import java.util.List; public class IrisDepositModifier extends EngineAssignedModifier { + private static final int CLUMPS_PER_BATCH = 8; + private static final int PREPARATION_BATCH_COUNT = 4; + private final RNG rng; public IrisDepositModifier(Engine engine) { @@ -78,31 +81,47 @@ public class IrisDepositModifier extends EngineAssignedModifier prepared[index] = prepare(generator, generatorRng, x, z, null, context), context)); - } - } finally { - // complete() must run before release() even when queueing throws — already - // submitted burst tasks must never write into a released chunk. - burst.complete(); - } - try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) { - for (PreparedDeposit deposit : prepared) { - if (deposit != null) { - place(deposit, chunk, terrain, x, z, null, context); + () -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), context)); + if (pending == prepared.length) { + completeBatches(burst, prepared, chunk, terrain, x, z, context); + pending = 0; } } } + completeBatches(burst, prepared, chunk, terrain, x, z, context); } finally { - chunk.release(); + // complete() must run before release() even when queueing throws — already + // submitted burst tasks must never write into a released chunk. + try { + burst.complete(); + } finally { + chunk.release(); + } + } + } + + private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk, + Hunk terrain, int x, int z, ChunkContext context) { + burst.complete(); + try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) { + for (int i = 0; i < prepared.length; i++) { + PreparedDeposit deposit = prepared[i]; + prepared[i] = null; + if (deposit != null) { + place(deposit, chunk, terrain, x, z, null, context); + } + } } } @@ -125,21 +144,30 @@ public class IrisDepositModifier extends EngineAssignedModifier data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { - place(prepare(k, rng, cx, cz, he, context), chunk, data, cx, cz, he, context); + DepositPlan plan = plan(k, rng); + for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) { + int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH); + place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context); + } } - private PreparedDeposit prepare(IrisDepositGenerator k, RNG rng, int cx, int cz, HeightMap he, ChunkContext context) { - if (k.getSpawnChance() < rng.d()) { - return new PreparedDeposit(k, false, List.of()); + private DepositPlan plan(IrisDepositGenerator generator, RNG rng) { + if (generator.getSpawnChance() < rng.d()) { + return new DepositPlan(generator, rng, false, 0); } + boolean ore = generator.isOre(getData()); + int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1); + return new DepositPlan(generator, rng, ore, attempts); + } - boolean oreDeposit = k.isOre(getData()); + private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) { + IrisDepositGenerator k = plan.generator(); + RNG rng = plan.rng(); + boolean oreDeposit = plan.ore(); boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter(); IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null; - - int attempts = rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); - List clumps = new ArrayList<>(Math.max(0, attempts)); - for (int l = 0; l < attempts; l++) { + List clumps = new ArrayList<>(limit - first); + for (int l = first; l < limit; l++) { RNG clumpRng = rng.nextParallelRNG(l + 1L); if (k.getPerClumpSpawnChance() < clumpRng.d()) { continue; @@ -464,6 +492,9 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps) { } diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java index 3289fa5a0..59f022235 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java @@ -9,6 +9,7 @@ import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.common.parallel.BurstExecutor; import art.arcane.iris.util.common.parallel.MultiBurst; @@ -18,9 +19,11 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.math.RNG; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; import java.util.ArrayList; import java.util.Collections; @@ -37,7 +40,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -49,7 +54,6 @@ public class IrisDepositModifierOrderingTest { PlatformRegistries registries = mock(PlatformRegistries.class); PlatformBlockState stone = state("stone"); when(registries.block(anyString())).thenReturn(stone); - when(registries.deepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1)); IrisPlatform platform = mock(IrisPlatform.class); when(platform.registries()).thenReturn(registries); IrisPlatforms.bind(platform); @@ -62,7 +66,7 @@ public class IrisDepositModifierOrderingTest { @Test public void reverseWorkerCompletionKeepsDimensionRegionBiomePlacementOrder() throws Exception { - try (Fixture fixture = new Fixture(true)) { + try (Fixture fixture = new Fixture(true, 1)) { fixture.generate(); assertEquals(List.of("biome", "region", "dimension"), fixture.completed); fixture.assertBiomeWins(); @@ -71,13 +75,35 @@ public class IrisDepositModifierOrderingTest { @Test public void inlinePreparationUsesTheSameOverlappingHostRules() throws Exception { - try (Fixture fixture = new Fixture(false)) { + try (Fixture fixture = new Fixture(false, 1)) { fixture.generate(); assertEquals(List.of("dimension", "region", "biome"), fixture.completed); fixture.assertBiomeWins(); } } + @Test + public void manyAttemptBatchesKeepEveryClumpSeedAndOrderedHostReplacement() throws Exception { + List expectedSamples = new ArrayList<>(); + RNG generatorRng = new RNG(0L).nextParallelRNG(0L); + for (String name : List.of("dimension", "region", "biome")) { + for (int i = 0; i < 25; i++) { + expectedSamples.add(name + ":" + generatorRng.nextParallelRNG(i + 1L).getSeed()); + } + } + Collections.sort(expectedSamples); + + for (boolean parallel : new boolean[]{false, true}) { + try (Fixture fixture = new Fixture(parallel, 25)) { + fixture.generate(); + Collections.sort(fixture.samples); + assertEquals(expectedSamples, fixture.samples); + assertEquals(75, fixture.completed.size()); + fixture.assertBiomeWins(); + } + } + } + private static PlatformBlockState state(String material) { PlatformBlockState state = mock(PlatformBlockState.class); when(state.key()).thenReturn("minecraft:" + material); @@ -88,6 +114,7 @@ public class IrisDepositModifierOrderingTest { private static final class Fixture implements AutoCloseable { private final ExecutorService executor = Executors.newFixedThreadPool(3); private final List completed = Collections.synchronizedList(new ArrayList<>()); + private final List samples = Collections.synchronizedList(new ArrayList<>()); private final CountDownLatch biomeReady = new CountDownLatch(1); private final CountDownLatch regionReady = new CountDownLatch(1); private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); @@ -99,10 +126,12 @@ public class IrisDepositModifierOrderingTest { private final Hunk output = Hunk.newArrayHunk(16, 16, 16); private final MantleChunk chunk; private final boolean parallel; + private final int attempts; @SuppressWarnings("unchecked") - private Fixture(boolean parallel) { + private Fixture(boolean parallel, int attempts) { this.parallel = parallel; + this.attempts = attempts; when(engine.getHeight()).thenReturn(16); when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null); when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>()); @@ -134,8 +163,8 @@ public class IrisDepositModifierOrderingTest { IrisDepositGenerator generator = mock(IrisDepositGenerator.class); when(generator.getSpawnChance()).thenReturn(1D); when(generator.getPerClumpSpawnChance()).thenReturn(1D); - when(generator.getMinPerChunk()).thenReturn(1); - when(generator.getMaxPerChunk()).thenReturn(1); + when(generator.getMinPerChunk()).thenReturn(attempts); + when(generator.getMaxPerChunk()).thenReturn(attempts); when(generator.getMinHeight()).thenReturn(4); when(generator.getMaxHeight()).thenReturn(4); when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT); @@ -149,11 +178,15 @@ public class IrisDepositModifierOrderingTest { } } when(generator.getClump(any(), any(), any())).thenAnswer(invocation -> { - if (parallel && name.equals("dimension")) { + assertTrue("Placement must release the first clump window before more are prepared", + samples.size() < 32 || output.get(0, 4, 0) != stone); + if (parallel && attempts == 1 && name.equals("dimension")) { assertTrue(regionReady.await(10, TimeUnit.SECONDS)); - } else if (parallel && name.equals("region")) { + } else if (parallel && attempts == 1 && name.equals("region")) { assertTrue(biomeReady.await(10, TimeUnit.SECONDS)); } + RNG rng = invocation.getArgument(1); + samples.add(name + ":" + rng.getSeed()); completed.add(name); if (name.equals("biome")) { biomeReady.countDown(); @@ -166,7 +199,10 @@ public class IrisDepositModifierOrderingTest { } private void generate() { - new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context); + try (MockedStatic blocks = mockStatic(B.class, CALLS_REAL_METHODS)) { + blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1)); + new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context); + } verify(chunk).release(); }