diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleComponent.java index 481deb029..735228278 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleComponent.java @@ -55,8 +55,12 @@ public interface MantleComponent extends Comparable { return getInputRadius(); } - default boolean isInputGenerationLazy() { - return false; + /** + * The part of {@code inputRadius} generated before this component runs. Content writers generate the + * terrain of any chunk read beyond it on first access, so this sizes the eager window, not what may be read. + */ + default int getEagerInputRadius(int inputRadius) { + return inputRadius; } default IrisData getData() { diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java index 90b0fd61a..0fda648ec 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java @@ -67,8 +67,8 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { } @Override - public boolean isInputGenerationLazy() { - return true; + public int getEagerInputRadius(int inputRadius) { + return 0; } @Override diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java index ca13b2de4..32f3478c2 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java @@ -128,6 +128,11 @@ public class MantleObjectComponent extends IrisMantleComponent { return calculateInputRadius(getRadius(), hasCollisionRules()); } + @Override + public int getEagerInputRadius(int inputRadius) { + return Math.min(inputRadius, calculateEagerInputRadius(getRadius(), hasCollisionRules())); + } + @Override public void hotload() { super.hotload(); @@ -272,6 +277,17 @@ public class MantleObjectComponent extends IrisMantleComponent { return radius > 0 ? Math.ceilDiv(radius, 16) : 0; } + /** + * Every replayed source chunk plus one chunk of placement footprint. Larger footprints read past it and + * get their terrain generated on first access. + */ + static int calculateEagerInputRadius(int radius, boolean sourceAnchoredCollisions) { + int normalizedRadius = Math.max(0, radius); + long sourceLegs = sourceAnchoredCollisions ? 2L : 1L; + long inputRadius = (sourceChunkRadius(normalizedRadius) * 16L * sourceLegs) + Math.min(normalizedRadius, 16) + 1L; + return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius; + } + static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) { int normalizedRadius = Math.max(0, radius); long sourceChunkRadius = sourceChunkRadius(normalizedRadius); diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java index 3561ccddb..e0f6f834f 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java @@ -35,10 +35,12 @@ import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.iris.pack.value.IrisPosition; import art.arcane.iris.generation.block.TileData; +import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.function.Function3; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.math.RNG; @@ -90,6 +92,15 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { private final ThreadLocal activeComponentPriority = new ThreadLocal<>(); @Getter(AccessLevel.NONE) private final ThreadLocal objectPlacementCapture = new ThreadLocal<>(); + @Getter(AccessLevel.NONE) + @EqualsAndHashCode.Exclude + @ToString.Exclude + private final boolean[] terrainReady; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + @EqualsAndHashCode.Exclude + @ToString.Exclude + private TerrainAccess terrainAccess; public MantleWriter(EngineMantle engineMantle, Mantle mantle, int x, int z, int radius, boolean multicore) { this(engineMantle, mantle, x, z, radius, radius * 2, multicore); @@ -104,17 +115,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { int accessRadius, boolean multicore ) { - this.engineMantle = engineMantle; - this.mantle = mantle; - this.radius = accessRadius; - this.x = x; - this.z = z; - IrisComplex complex = engineMantle.getComplex(); - this.transitionGenerationPlan = complex == null ? null : complex.getTransitionGenerationPlan(); - // Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the - // boxed per-block map lookup on the placement and carve hot paths. - this.windowSide = (this.radius * 2) + 1; - this.window = new AtomicReferenceArray<>(windowSide * windowSide); + this(engineMantle, mantle, x, z, accessRadius); final boolean foliaMaintenance = J.isFolia() && WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity()); @@ -142,6 +143,42 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } } + private MantleWriter(EngineMantle engineMantle, Mantle mantle, int x, int z, int accessRadius) { + this.engineMantle = engineMantle; + this.mantle = mantle; + this.radius = accessRadius; + this.x = x; + this.z = z; + IrisComplex complex = engineMantle.getComplex(); + this.transitionGenerationPlan = complex == null ? null : complex.getTransitionGenerationPlan(); + // Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the + // boxed per-block map lookup on the placement and carve hot paths. + this.windowSide = (this.radius * 2) + 1; + this.window = new AtomicReferenceArray<>(windowSide * windowSide); + this.terrainReady = new boolean[windowSide * windowSide]; + } + + /** + * A writer over one already loaded chunk, for generating that chunk's terrain components without a + * prefetch. + */ + static MantleWriter forChunk(EngineMantle engineMantle, Mantle mantle, MantleChunk chunk, + int chunkX, int chunkZ) { + MantleWriter writer = new MantleWriter(engineMantle, mantle, chunkX, chunkZ, 0); + writer.window.set(0, chunk.use()); + return writer; + } + + /** + * From now on every chunk this writer hands out has its terrain components generated first. Called once + * the terrain passes of the window are done and before any content pass reads or writes. + */ + void requireTerrainOnAccess(List terrainComponents, ChunkContext context, IrisComplex complex) { + terrainAccess = terrainComponents.isEmpty() + ? null + : new TerrainAccess(terrainComponents.toArray(new MantleComponent[0]), context, complex); + } + static int resolvePrefetchParallelism(boolean foliaMaintenance, boolean multicore, int availableProcessors) { if (foliaMaintenance) { return 1; @@ -844,20 +881,24 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { return null; } - while (true) { - MantleChunk chunk = window.get(index); - if (chunk != null) { - return chunk; - } - + MantleChunk chunk = window.get(index); + while (chunk == null) { // Losing this race must release our own use, never the winner's: the winner is already // writing through the chunk it published. MantleChunk acquired = mantle.getChunk(cx, cz).use(); if (window.compareAndSet(index, null, acquired)) { - return acquired; + chunk = acquired; + } else { + acquired.release(); + chunk = window.get(index); } - acquired.release(); } + TerrainAccess access = terrainAccess; + if (access != null && !terrainReady[index]) { + access.ensure(chunk, cx, cz); + terrainReady[index] = true; + } + return chunk; } @ChunkCoordinates @@ -1509,6 +1550,44 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } } + private final class TerrainAccess { + private final MantleComponent[] components; + private final ChunkContext context; + private final IrisComplex complex; + + private TerrainAccess(MantleComponent[] components, ChunkContext context, IrisComplex complex) { + this.components = components; + this.context = context; + this.complex = complex; + } + + private void ensure(MantleChunk chunk, int chunkX, int chunkZ) { + if (chunk.isFlagged(MantleFlag.PLANNED) || complex != null && !complex.allowsMantleChunkWrite(chunkX, chunkZ)) { + return; + } + for (MantleComponent component : components) { + if (chunk.isFlagged(component.getFlag()) || !hasPrerequisites(chunk, component)) { + continue; + } + chunk.raiseFlagSuspend(component.getFlag(), () -> { + try (MantleWriter terrainWriter = forChunk(engineMantle, mantle, chunk, chunkX, chunkZ)) { + terrainWriter.withComponentPriority(component.getPriority(), + () -> component.generateLayer(terrainWriter, chunkX, chunkZ, context)); + } + }); + } + } + + private static boolean hasPrerequisites(MantleChunk chunk, MantleComponent component) { + for (MantleFlag prerequisite : component.getPrerequisiteFlags()) { + if (!chunk.isFlagged(prerequisite)) { + return false; + } + } + return true; + } + } + public final class ObjectPlacementCapture implements AutoCloseable { private final ObjectContinuationBundle.PlacementKey key; private final Long2ObjectOpenHashMap fragments = new Long2ObjectOpenHashMap<>(); diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java index 4595f4afc..5b42014ed 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java @@ -90,7 +90,7 @@ public interface MatterGenerator { int prefetchRadius = passPlans[0].passChunkRadius(); LongOpenHashSet partialChunks = new LongOpenHashSet(); List requiredComponents = enabledComponents(); - List terrainFlags = terrainFlags(requiredComponents); + boolean terrainOnAccess = false; try (MantleWriter writer = new MantleWriter( getEngine().getMantle(), @@ -129,6 +129,10 @@ public interface MatterGenerator { if (enabledComponentCount == 0) { continue; } + if (!terrainOnAccess && includesContent(enabledComponents, enabledComponentCount)) { + writer.requireTerrainOnAccess(terrainComponents(requiredComponents), context, complex); + terrainOnAccess = true; + } // Every multicore generation claims its components through the in-flight map. // A caller outside the pool spreads its window across the pool; a pool thread @@ -185,9 +189,6 @@ public interface MatterGenerator { continue; } - if (phase == MatterGenerationPhase.CONTENT) { - requireTerrainPhase(chunk, passX, passZ, terrainFlags); - } int eligibleComponentCount = 0; for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { MantleComponent component = enabledComponents[componentIndex]; @@ -303,9 +304,8 @@ public interface MatterGenerator { : 0; int componentInputRadius = component.getInputRadius(x, z, invocationChunkRadius, context); passAccessInputRadius = Math.max(passAccessInputRadius, componentInputRadius); - if (!component.isInputGenerationLazy()) { - passGenerationInputRadius = Math.max(passGenerationInputRadius, componentInputRadius); - } + passGenerationInputRadius = Math.max(passGenerationInputRadius, + component.getEagerInputRadius(componentInputRadius)); } int accessInvocationRadius = accessDownstreamBlockRadius + passBlockRadius; int generationInvocationRadius = generationDownstreamBlockRadius + passBlockRadius; @@ -332,24 +332,23 @@ public interface MatterGenerator { return List.copyOf(components); } - private static List terrainFlags(List components) { - List flags = new ArrayList<>(); - for (MantleComponent component : components) { - if (component.getGenerationPhase() == MatterGenerationPhase.TERRAIN) { - flags.add(component.getFlag()); + private static boolean includesContent(MantleComponent[] components, int count) { + for (int index = 0; index < count; index++) { + if (components[index].getGenerationPhase() == MatterGenerationPhase.CONTENT) { + return true; } } - return List.copyOf(flags); + return false; } - private static void requireTerrainPhase(MantleChunk chunk, int chunkX, int chunkZ, - List flags) { - for (MantleFlag flag : flags) { - if (!chunk.isFlagged(flag)) { - throw new IllegalStateException("Content generation at " + chunkX + "," + chunkZ - + " requires completed terrain component " + flag.name()); + private static List terrainComponents(List components) { + List terrain = new ArrayList<>(); + for (MantleComponent component : components) { + if (component.getGenerationPhase() == MatterGenerationPhase.TERRAIN) { + terrain.add(component); } } + return terrain; } private static boolean hasCompletedComponents(MantleChunk chunk, List components) { diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java index 86d56bb57..bca1f7dd9 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java @@ -45,6 +45,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -188,6 +189,23 @@ public class MantleObjectComponentBoundaryRadiusTest { MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true)); } + @Test + public void eagerInputRadiusCoversEverySourceChunkPlusOneChunkOfFootprint() { + assertEquals(129, MantleObjectComponent.calculateEagerInputRadius(103, false)); + assertEquals(65, MantleObjectComponent.calculateEagerInputRadius(33, false)); + assertEquals(113, MantleObjectComponent.calculateEagerInputRadius(33, true)); + assertEquals(27, MantleObjectComponent.calculateEagerInputRadius(10, false)); + assertEquals(1, MantleObjectComponent.calculateEagerInputRadius(0, false)); + for (int radius = 0; radius < 300; radius++) { + for (boolean collisions : new boolean[]{false, true}) { + assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions) + <= MantleObjectComponent.calculateInputRadius(radius, collisions)); + } + } + assertEquals(Integer.MAX_VALUE, + MantleObjectComponent.calculateEagerInputRadius(Integer.MAX_VALUE, true)); + } + @Test public void proceduralTreeTransformsContributeToRadius() { IrisObjectPlacement placement = new IrisObjectPlacement() diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorCarvePassRadiusTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorCarvePassRadiusTest.java index 748e356d0..d327ab4a2 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorCarvePassRadiusTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorCarvePassRadiusTest.java @@ -309,8 +309,8 @@ public class MatterGeneratorCarvePassRadiusTest { } @Override - public boolean isInputGenerationLazy() { - return lazyInputGeneration; + public int getEagerInputRadius(int inputRadius) { + return lazyInputGeneration ? 0 : inputRadius; } @Override diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java index d1727ce4a..5f8f8339e 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java @@ -387,27 +387,94 @@ public class MatterGeneratorConcurrencyTest { } @Test - public void contentPhaseRequiresTerrainEvenWhenItsExplicitPrerequisiteIsMissing() { + public void contentPhaseGeneratesTerrainOfEveryChunkBeforeItIsReadOrWritten() { GeneratorFixture fixture = new GeneratorFixture(true); + List events = new ArrayList<>(); RecordingComponent terrain = new RecordingComponent(ReservedFlag.CARVED, 0, 0) { @Override public MatterGenerationPhase getGenerationPhase() { return MatterGenerationPhase.TERRAIN; } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + assertSame(fixture.mantle.getChunk(x, z), writer.acquireChunk(x, z)); + events.add("terrain " + x + "," + z); + } }; RecordingComponent content = new RecordingComponent(ReservedFlag.OBJECT, 1, 0) { @Override public MantleFlag[] getPrerequisiteFlags() { return new MantleFlag[]{ReservedFlag.CARVED}; } + + @Override + public int getInputRadius() { + return 48; + } + + @Override + public int getEagerInputRadius(int inputRadius) { + return 0; + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + events.add("content " + x + "," + z); + assertSame(fixture.mantle.getChunk(x + 3, z), writer.acquireChunk(x + 3, z)); + writer.acquireChunk(x + 3, z); + events.add("read " + (x + 3) + "," + z); + } }; TestMatterGenerator generator = fixture.generator(List.of( new MantlePass(List.of(terrain), 0, 0), new MantlePass(List.of(content), 0, 0))); - assertThrows(IllegalStateException.class, - () -> generator.generateContentMatter(0, 0, false, fixture.context)); - assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); + generator.generateTerrainMatter(0, 0, false, fixture.context); + assertEquals(List.of("terrain 0,0"), events); + generator.generateContentMatter(0, 0, false, fixture.context); + + assertEquals(List.of("terrain 0,0", "content 0,0", "terrain 3,0", "read 3,0"), events); + assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); + assertTrue(fixture.mantle.getChunk(3, 0).isFlagged(ReservedFlag.CARVED)); + assertFalse(fixture.mantle.getChunk(3, 0).isFlagged(ReservedFlag.OBJECT)); + } + + @Test + public void contentPhaseGeneratesMissingTerrainOfItsOwnChunk() { + GeneratorFixture fixture = new GeneratorFixture(true); + AtomicInteger terrainRuns = new AtomicInteger(); + RecordingComponent terrain = new RecordingComponent(ReservedFlag.CARVED, 0, 0) { + @Override + public MatterGenerationPhase getGenerationPhase() { + return MatterGenerationPhase.TERRAIN; + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + terrainRuns.incrementAndGet(); + } + }; + RecordingComponent content = new RecordingComponent(ReservedFlag.OBJECT, 1, 0) { + @Override + public MantleFlag[] getPrerequisiteFlags() { + return new MantleFlag[]{ReservedFlag.CARVED}; + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + assertEquals(1, terrainRuns.get()); + } + }; + TestMatterGenerator generator = fixture.generator(List.of( + new MantlePass(List.of(terrain), 0, 0), + new MantlePass(List.of(content), 0, 0))); + + generator.generateContentMatter(0, 0, false, fixture.context); + + assertEquals(1, terrainRuns.get()); + assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(ReservedFlag.OBJECT)); + assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); } @Test