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 ff514cea7..c0642db39 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 @@ -40,9 +40,14 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; + +import java.util.ArrayList; +import java.util.List; public class IrisDepositModifier extends EngineAssignedModifier { private final RNG rng; @@ -62,37 +67,42 @@ public class IrisDepositModifier extends EngineAssignedModifier terrain, int x, int z, boolean multicore, ChunkContext context) { IrisRegion region = context.getRegion().get(7, 7); IrisBiome biome = context.getBiome().get(7, 7); - BurstExecutor burst = burst().burst(multicore); + List generators = new ArrayList<>( + getDimension().getDeposits().size() + region.getDeposits().size() + biome.getDeposits().size()); + generators.addAll(getDimension().getDeposits()); + generators.addAll(region.getDeposits()); + generators.addAll(biome.getDeposits()); + if (generators.isEmpty()) { + return; + } + BurstExecutor burst = burst().burst(multicore); long seed = x * 341873128712L + z * 132897987541L; - long mask = 0; + PreparedDeposit[] prepared = new PreparedDeposit[generators.size()]; MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use(); try { - for (IrisDepositGenerator k : getDimension().getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(scopedDepositTask( - () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); + try { + for (int i = 0; i < generators.size(); i++) { + int index = i; + IrisDepositGenerator generator = generators.get(i); + RNG generatorRng = rng.nextParallelRNG(seed * (i + 1L)); + burst.queue(scopedDepositTask( + () -> 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(); } - - for (IrisDepositGenerator k : region.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(scopedDepositTask( - () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); - } - - for (IrisDepositGenerator k : biome.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(scopedDepositTask( - () -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); + 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); + } + } } } finally { - // 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(); - } + chunk.release(); } } @@ -115,17 +125,27 @@ public class IrisDepositModifier extends EngineAssignedModifier data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { - IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State(); - if (k.getSpawnChance() < rng.d()) - return; + place(prepare(k, rng, 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()); + } boolean oreDeposit = k.isOre(getData()); + boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter(); + IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null; - for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) { - if (k.getPerClumpSpawnChance() < rng.d()) + int attempts = rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); + List clumps = new ArrayList<>(Math.max(0, attempts)); + for (int l = 0; l < attempts; l++) { + RNG clumpRng = rng.nextParallelRNG(l + 1L); + if (k.getPerClumpSpawnChance() < clumpRng.d()) { continue; + } - IrisObject clump = k.getClump(getEngine(), rng, getData()); + IrisObject clump = k.getClump(getEngine(), clumpRng, getData()); int dim = clump.getW(); int min = dim / 2; @@ -136,8 +156,8 @@ public class IrisDepositModifier extends EngineAssignedModifier height - 2) + if (clippedHeight && y > height - 2) { continue; + } int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y)); IrisBiome surfaceBiome = context.getBiome().get(x, z); - IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter() + IrisBiome depositBiome = needsCaveBiome ? getEngine().getCaveBiome( (cx << 4) + x, biomeY, (cz << 4) + z, carvingState) : null; @@ -172,13 +193,13 @@ public class IrisDepositModifier extends EngineAssignedModifier data, + int cx, int cz, HeightMap he, ChunkContext context) { + if (deposit.clumps().isEmpty()) { + return; + } + IrisDepositGenerator k = deposit.generator(); + boolean oreDeposit = deposit.ore(); + IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State(); + IrisDimension dimension = getDimension(); + for (PreparedClump prepared : deposit.clumps()) { + IrisObject clump = prepared.object(); + int x = prepared.x(); + int y = prepared.y(); + int z = prepared.z(); + RNG rng = prepared.rng(); + + VectorMap.Cursor cursor = clump.getBlocks().cursor(); + while (cursor.next()) { + IrisBlockVector j = cursor.key(); int nx = j.getBlockX() + x; int ny = j.getBlockY() + y; int nz = j.getBlockZ() + z; @@ -236,7 +278,7 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps) { + } + + private record PreparedClump(IrisObject object, int x, int y, int z, RNG rng) { + } } diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java index 97a057836..1dd7cb627 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContextTest.java @@ -3,6 +3,9 @@ package art.arcane.iris.engine.modifier; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; +import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; @@ -16,6 +19,7 @@ import art.arcane.volmlib.util.collection.KList; 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.math.RNG; import org.junit.Test; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -33,14 +37,12 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -90,6 +92,26 @@ public class IrisDepositModifierContextTest { } } + @Test + public void failedClumpChanceDoesNotResampleTheConfiguredAttemptCount() { + Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + IrisDepositGenerator generator = mock(IrisDepositGenerator.class); + when(generator.getSpawnChance()).thenReturn(1D); + when(generator.getMinPerChunk()).thenReturn(0); + when(generator.getMaxPerChunk()).thenReturn(4); + when(generator.getPerClumpSpawnChance()).thenReturn(0D); + RNG rng = mock(RNG.class); + when(rng.d()).thenReturn(0.5D); + when(rng.i(0, 5)).thenReturn(4, 0); + when(rng.nextParallelRNG(anyLong())).thenReturn(rng); + + new IrisDepositModifier(engine).generate(generator, null, null, rng, 0, 0, false, null); + + verify(generator, times(4)).getPerClumpSpawnChance(); + verify(rng, times(1)).i(0, 5); + verify(generator, never()).getClump(any(), any(), any()); + } + private static final class Fixture implements AutoCloseable { private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final Engine engine; @@ -138,16 +160,27 @@ public class IrisDepositModifierContextTest { return null; }).when(scope).close(); } - modifier = spy(new IrisDepositModifier(engine)); - doAnswer(invocation -> { - assertNotSame(caller, Thread.currentThread()); - assertEquals(scoped, workerRuntime.get()); - assertSame(engine, IrisContext.require().getEngine()); - assertSame(context, IrisContext.require().getChunkContext()); - assertEquals(71L, IrisContext.require().getGenerationSessionId()); - completed.incrementAndGet(); - return null; - }).when(modifier).generate(any(), same(chunk), any(), any(), eq(2), eq(-3), eq(false), same(context)); + when(engine.getHeight()).thenReturn(16); + IrisObject clump = new IrisObject(1, 1, 1); + for (IrisDepositGenerator generator : new IrisDepositGenerator[]{dimensionDeposit, regionDeposit, biomeDeposit}) { + when(generator.getSpawnChance()).thenReturn(1D); + when(generator.getMinPerChunk()).thenReturn(1); + when(generator.getMaxPerChunk()).thenReturn(1); + when(generator.getPerClumpSpawnChance()).thenReturn(1D); + when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT); + when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM); + when(generator.matchesBiome(any(), any())).thenReturn(true); + doAnswer(invocation -> { + assertNotSame(caller, Thread.currentThread()); + assertEquals(scoped, workerRuntime.get()); + assertSame(engine, IrisContext.require().getEngine()); + assertSame(context, IrisContext.require().getChunkContext()); + assertEquals(71L, IrisContext.require().getGenerationSessionId()); + completed.incrementAndGet(); + return clump; + }).when(generator).getClump(any(), any(), any()); + } + modifier = new IrisDepositModifier(engine); doAnswer(invocation -> { assertEquals(3, completed.get()); assertEquals(scoped ? 3 : 0, closedScopes.get()); 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 new file mode 100644 index 000000000..3289fa5a0 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierOrderingTest.java @@ -0,0 +1,187 @@ +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; +import art.arcane.iris.engine.object.IrisObject; +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.math.IrisBlockVector; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; +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 org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +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.mock; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class IrisDepositModifierOrderingTest { + @Before + public void bindPlatform() { + IrisPlatforms.unbind(); + 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); + } + + @After + public void unbindPlatform() { + IrisPlatforms.unbind(); + } + + @Test + public void reverseWorkerCompletionKeepsDimensionRegionBiomePlacementOrder() throws Exception { + try (Fixture fixture = new Fixture(true)) { + fixture.generate(); + assertEquals(List.of("biome", "region", "dimension"), fixture.completed); + fixture.assertBiomeWins(); + } + } + + @Test + public void inlinePreparationUsesTheSameOverlappingHostRules() throws Exception { + try (Fixture fixture = new Fixture(false)) { + fixture.generate(); + assertEquals(List.of("dimension", "region", "biome"), fixture.completed); + fixture.assertBiomeWins(); + } + } + + private static PlatformBlockState state(String material) { + PlatformBlockState state = mock(PlatformBlockState.class); + when(state.key()).thenReturn("minecraft:" + material); + when(state.materialKey()).thenReturn("minecraft:" + material); + return state; + } + + private static final class Fixture implements AutoCloseable { + private final ExecutorService executor = Executors.newFixedThreadPool(3); + private final List completed = 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); + private final ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); + private final PlatformBlockState stone = state("stone"); + private final PlatformBlockState granite = state("granite"); + private final PlatformBlockState diorite = state("diorite"); + private final PlatformBlockState diamond = state("diamond_ore"); + private final Hunk output = Hunk.newArrayHunk(16, 16, 16); + private final MantleChunk chunk; + private final boolean parallel; + + @SuppressWarnings("unchecked") + private Fixture(boolean parallel) { + this.parallel = parallel; + when(engine.getHeight()).thenReturn(16); + when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null); + when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>()); + when(context.getRegion().get(anyInt(), anyInt()).getDepositVariants()).thenReturn(new KList<>()); + when(context.getGenerationSessionId()).thenReturn(11L); + chunk = mock(MantleChunk.class); + Mantle mantle = engine.getMantle().getMantle(); + doReturn(chunk).when(mantle).getChunk(0, 0); + when(chunk.use()).thenReturn(chunk); + MultiBurst pool = mock(MultiBurst.class); + BurstExecutor burst = new BurstExecutor(executor, 3); + burst.setMulticore(parallel); + when(engine.burst()).thenReturn(pool); + when(pool.burst(parallel)).thenReturn(burst); + IrisDepositGenerator dimensionDeposit = generator("dimension", stone, granite); + IrisDepositGenerator regionDeposit = generator("region", granite, diorite); + IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond); + when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit)); + when(context.getRegion().get(7, 7).getDeposits()).thenReturn(new KList<>(regionDeposit)); + when(context.getBiome().get(7, 7).getDeposits()).thenReturn(new KList<>(biomeDeposit)); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + output.set(x, 4, z, stone); + } + } + } + + private IrisDepositGenerator generator(String name, PlatformBlockState requiredHost, PlatformBlockState replacement) { + 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.getMinHeight()).thenReturn(4); + when(generator.getMaxHeight()).thenReturn(4); + when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT); + when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM); + when(generator.matchesBiome(any(), any())).thenReturn(true); + when(generator.canReplace(requiredHost)).thenReturn(true); + IrisObject clump = new IrisObject(33, 1, 33); + for (int x = -16; x <= 16; x++) { + for (int z = -16; z <= 16; z++) { + clump.getBlocks().put(new IrisBlockVector(x, 0, z), replacement); + } + } + when(generator.getClump(any(), any(), any())).thenAnswer(invocation -> { + if (parallel && name.equals("dimension")) { + assertTrue(regionReady.await(10, TimeUnit.SECONDS)); + } else if (parallel && name.equals("region")) { + assertTrue(biomeReady.await(10, TimeUnit.SECONDS)); + } + completed.add(name); + if (name.equals("biome")) { + biomeReady.countDown(); + } else if (name.equals("region")) { + regionReady.countDown(); + } + return clump; + }); + return generator; + } + + private void generate() { + new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context); + verify(chunk).release(); + } + + private void assertBiomeWins() { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + assertSame("Deposit collision at " + x + ",4," + z, diamond, output.get(x, 4, z)); + } + } + } + + @Override + public void close() throws Exception { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +}