diff --git a/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java index 8135a3c9c..62270dc8a 100644 --- a/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java @@ -8,6 +8,7 @@ import art.arcane.iris.generation.runtime.EngineMetrics; import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler; import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.world.history.FloatingBiomeOverlay; import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; @@ -23,6 +24,7 @@ public class ChunkContext { private boolean naturalTerrain = true; private Hunk terrainBiomes; private FloatingBiomeOverlay floatingBiomes; + private CaveTerrainSnapshot caveTerrain; private final int x; private final int z; private final IrisComplex complex; @@ -157,6 +159,14 @@ public class ChunkContext { return floatingBiomes; } + public CaveTerrainSnapshot getCaveTerrain() { + return caveTerrain; + } + + public void setCaveTerrain(CaveTerrainSnapshot caveTerrain) { + this.caveTerrain = caveTerrain; + } + public void setNaturalBiome(int localX, int y, int localZ, NativeBiome biome) { if (terrainBiomes != null) { terrainBiomes.setRaw(localX, y, localZ, biome); diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java new file mode 100644 index 000000000..a447c328c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java @@ -0,0 +1,210 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.world.storage.matter.PreObjectMatterCell; +import art.arcane.volmlib.util.hunk.bits.DataContainer; +import art.arcane.volmlib.util.hunk.storage.MappedHunk; +import art.arcane.volmlib.util.hunk.storage.PaletteOrHunk; +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.matter.MatterSlice; + +import java.util.Map; +import java.util.Objects; +import java.util.function.IntFunction; + +/** + * The terrain view of one mantle chunk's cavern and hydrology cells (journaled pre-object values win), + * copied once under the chunk monitor. Terrain components finish before any content write reaches a + * chunk and every mantle-writer write journals the value it replaces, so the view holds while the chunk + * generates; the raw writers that run after sCave (dimension stack clearing, continuation restore) drop + * it from the chunk context instead. + */ +public final class CaveTerrainSnapshot { + private static final int SECTION_VOLUME = 4096; + + private final int chunkX; + private final int chunkZ; + private final MatterCavern[][] caverns; + private final HydrologyCaveCell[][] hydrology; + private boolean hydrologyPresent; + + private CaveTerrainSnapshot(int chunkX, int chunkZ, int sections) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + caverns = new MatterCavern[sections][]; + hydrology = new HydrologyCaveCell[sections][]; + } + + public static CaveTerrainSnapshot capture(MantleChunk chunk, int chunkX, int chunkZ) { + Objects.requireNonNull(chunk, "Terrain mantle chunk"); + synchronized (chunk) { + int sections = chunk.sectionCount(); + CaveTerrainSnapshot snapshot = new CaveTerrainSnapshot(chunkX, chunkZ, sections); + for (int section = 0; section < sections; section++) { + if (!chunk.exists(section)) { + continue; + } + Matter matter = chunk.get(section); + if (matter != null) { + snapshot.captureSection(section, matter); + } + } + return snapshot; + } + } + + public boolean covers(int chunkX, int chunkZ) { + return this.chunkX == chunkX && this.chunkZ == chunkZ; + } + + public boolean hasHydrology() { + return hydrologyPresent; + } + + public MatterCavern cavern(int x, int y, int z) { + if (y < 0) { + return null; + } + int section = y >> 4; + if (section >= caverns.length) { + return null; + } + MatterCavern[] values = caverns[section]; + return values == null ? null : values[index(x, y, z)]; + } + + public HydrologyCaveCell hydrology(int x, int y, int z) { + if (!hydrologyPresent || y < 0) { + return null; + } + int section = y >> 4; + if (section >= hydrology.length) { + return null; + } + HydrologyCaveCell[] values = hydrology[section]; + return values == null ? null : values[index(x, y, z)]; + } + + public MatterCavern composedCavern(int x, int y, int z) { + HydrologyCaveCell cell = hydrology(x, y, z); + return cell != null ? cell.asCavern() : cavern(x, y, z); + } + + /** + * Visits every cell holding a cavern or a hydrology cell, section by section in slice position order. + */ + public void forEachCell(CellConsumer consumer) { + for (int section = 0; section < caverns.length; section++) { + MatterCavern[] sectionCaverns = caverns[section]; + HydrologyCaveCell[] sectionHydrology = hydrologyPresent ? hydrology[section] : null; + if (sectionCaverns == null && sectionHydrology == null) { + continue; + } + int baseY = section << 4; + for (int position = 0; position < SECTION_VOLUME; position++) { + MatterCavern cavern = sectionCaverns == null ? null : sectionCaverns[position]; + HydrologyCaveCell cell = sectionHydrology == null ? null : sectionHydrology[position]; + if (cavern != null || cell != null) { + consumer.accept(position & 15, baseY + ((position >> 4) & 15), position >> 8, cavern, cell); + } + } + } + } + + public MatterCavern[] composedFace(TerrainMatterView.Face face, int height) { + Objects.requireNonNull(face, "Terrain face"); + if (height < 0) { + throw new IllegalArgumentException("Terrain face height must be nonnegative"); + } + MatterCavern[] result = new MatterCavern[Math.multiplyExact(height, 16)]; + for (int y = 0; y < height; y++) { + for (int offset = 0; offset < 16; offset++) { + result[y * 16 + offset] = composedCavern(face.x(offset), y, face.z(offset)); + } + } + return result; + } + + private void captureSection(int section, Matter matter) { + caverns[section] = copy(matter.getSlice(MatterCavern.class), MatterCavern[]::new); + hydrology[section] = copy(matter.getSlice(HydrologyCaveCell.class), HydrologyCaveCell[]::new); + MatterSlice journal = matter.getSlice(PreObjectMatterCell.class); + if (journal instanceof MappedHunk mapped) { + @SuppressWarnings("unchecked") + Map entries = (Map) mapped.getData(); + for (Map.Entry entry : entries.entrySet()) { + restoreOriginals(section, entry.getKey(), entry.getValue()); + } + } else if (journal != null) { + for (int position = 0; position < SECTION_VOLUME; position++) { + PreObjectMatterCell cell = journal.get(position & 15, (position >> 4) & 15, position >> 8); + if (cell != null) { + restoreOriginals(section, position, cell); + } + } + } + if (hydrology[section] != null && containsAny(hydrology[section])) { + hydrologyPresent = true; + } else { + hydrology[section] = null; + } + } + + private void restoreOriginals(int section, int position, PreObjectMatterCell cell) { + if (cell.cavernCaptured() && (caverns[section] != null || cell.cavern() != null)) { + if (caverns[section] == null) { + caverns[section] = new MatterCavern[SECTION_VOLUME]; + } + caverns[section][position] = cell.cavern(); + } + if (cell.hydrologyCaptured() && (hydrology[section] != null || cell.hydrology() != null)) { + if (hydrology[section] == null) { + hydrology[section] = new HydrologyCaveCell[SECTION_VOLUME]; + } + hydrology[section][position] = cell.hydrology(); + } + } + + @SuppressWarnings("unchecked") + private static T[] copy(MatterSlice slice, IntFunction allocator) { + if (slice == null) { + return null; + } + T[] values = allocator.apply(SECTION_VOLUME); + if (slice instanceof PaletteOrHunk storage && storage.isPalette() + && slice.getWidth() == 16 && slice.getHeight() == 16 && slice.getDepth() == 16) { + return ((DataContainer) storage.palette()).copyPresent(values) == 0 ? null : values; + } + int width = Math.min(16, slice.getWidth()); + int height = Math.min(16, slice.getHeight()); + int depth = Math.min(16, slice.getDepth()); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + for (int z = 0; z < depth; z++) { + values[index(x, y, z)] = slice.get(x, y, z); + } + } + } + return values; + } + + private static boolean containsAny(Object[] values) { + for (Object value : values) { + if (value != null) { + return true; + } + } + return false; + } + + private static int index(int x, int y, int z) { + return ((z & 15) << 8) | ((y & 15) << 4) | (x & 15); + } + + @FunctionalInterface + public interface CellConsumer { + void accept(int x, int y, int z, MatterCavern cavern, HydrologyCaveCell hydrology); + } +} 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..f5009f63c 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,11 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { private final ThreadLocal activeComponentPriority = new ThreadLocal<>(); @Getter(AccessLevel.NONE) private final ThreadLocal objectPlacementCapture = new ThreadLocal<>(); + @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 +111,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 +139,41 @@ 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); + } + + /** + * 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 +876,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 && !access.ready[index]) { + access.ensure(chunk, cx, cz); + access.ready[index] = true; + } + return chunk; } @ChunkCoordinates @@ -1502,6 +1538,9 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { @Override public void close() { for (int index = 0; index < window.length(); index++) { + if (window.get(index) == null) { + continue; + } MantleChunk chunk = window.getAndSet(index, null); if (chunk != null) { chunk.release(); @@ -1509,6 +1548,45 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } } + private final class TerrainAccess { + private final MantleComponent[] components; + private final ChunkContext context; + private final IrisComplex complex; + private final boolean[] ready = new boolean[window.length()]; + + 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/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java b/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java index f463252b8..f77f9c009 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/TerrainMatterView.java @@ -134,8 +134,12 @@ public final class TerrainMatterView { SOUTH(false, 15); private final int[] positions = new int[256]; + private final boolean fixedX; + private final int coordinate; Face(boolean fixedX, int coordinate) { + this.fixedX = fixedX; + this.coordinate = coordinate; for (int y = 0; y < 16; y++) { for (int offset = 0; offset < 16; offset++) { int x = fixedX ? coordinate : offset; @@ -144,6 +148,14 @@ public final class TerrainMatterView { } } } + + int x(int offset) { + return fixedX ? coordinate : offset; + } + + int z(int offset) { + return fixedX ? offset : coordinate; + } } public static void iterate(MantleChunk chunk, Class type, diff --git a/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java index ea61380f6..d0fe437c9 100644 --- a/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java +++ b/core/src/main/java/art/arcane/iris/generation/mode/ModeOverworld.java @@ -109,7 +109,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { registerTerrainStage(sCave); registerTerrainStage(sPost); registerTerrainStage(sFloatingTerrainSolid); - registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k)); + registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k, c)); // Never burst these three: all of them write the same block hunk (and sDecorant reads // the surface sInsertMatter writes), so parallel order is scheduler-dependent. The // production path already runs them inline in this order; sequential registration diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java index 5c5c9bda4..768c68dea 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java @@ -218,6 +218,9 @@ public interface EngineMode extends Staged { IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx); ctx.beginContent(); List continuations = restoreObjectContinuations(x, z); + if (!continuations.isEmpty()) { + ctx.setCaveTerrain(null); + } for (EngineStage i : stages) { i.generate(x, z, blocks, biomes, multicore, ctx); } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index 64bf27b3e..7f58625b2 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -29,6 +29,7 @@ import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.iris.generation.runtime.EngineAssignedModifier; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.TerrainMatterView; import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.biome.IrisBiome; @@ -126,6 +127,10 @@ public class IrisCarveModifier extends EngineAssignedModifier MantleChunk mantleChunk = mantle.getChunk(x, z).use(); try { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + CaveTerrainSnapshot terrain = CaveTerrainSnapshot.capture(mantleChunk, x, z); + if (context != null) { + context.setCaveTerrain(terrain); + } int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); CarveResolutionContext resolutionContext = new CarveResolutionContext( @@ -142,32 +147,20 @@ public class IrisCarveModifier extends EngineAssignedModifier chunkBlockZ ); CarveResolver carveResolver = new CarveResolver(resolutionContext); - TerrainMatterView.iterate(mantleChunk, MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply( - xx, - yy, - zz, - cavern, - dataIfPresent(mantleChunk, xx, yy, zz, HydrologyCaveCell.class) - )); - TerrainMatterView.iterate(mantleChunk, HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> { - if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) { - carveResolver.apply(xx, yy, zz, null, hydrology); - } - }); + terrain.forEachCell(carveResolver::apply); if (scratch.customCaveBiomePresent) { - addInternalWallsFromMantle(mantleChunk, walls, columnMasks); + addInternalWallsFromMantle(terrain, walls, columnMasks); } else { addInternalWallsFromMasks(walls, columnMasks); } - addCrossChunkBoundaryWalls(mantle, mantleChunk, walls, boundaryMasks, x, z, surfaceHeights); + addCrossChunkBoundaryWalls(mantle, terrain, walls, boundaryMasks, x, z, surfaceHeights); getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); try { IrisData wallData = getData(); walls.forEach((rx, yy, rz, cavern) -> { - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, rx, yy, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz); if (hydrology != null && hydrology.protectsPlacement()) { return; } @@ -191,8 +184,7 @@ public class IrisCarveModifier extends EngineAssignedModifier for (int columnIndex = 0; columnIndex < 256; columnIndex++) { processColumnFromMask( output, - mantleChunk, - mantle, + terrain, columnMasks[columnIndex], columnIndex, x, @@ -209,7 +201,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } processBoundaryColumnFromMask( output, - mantleChunk, + terrain, boundaryMasks[columnIndex], walls, columnIndex, @@ -472,7 +464,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - private void addInternalWallsFromMantle(MantleChunk mc, CarveWallBuffer walls, CarveColumnMask[] columnMasks) { + private void addInternalWallsFromMantle(CaveTerrainSnapshot terrain, CarveWallBuffer walls, CarveColumnMask[] columnMasks) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) { CarveColumnMask columnMask = columnMasks[columnIndex]; if (columnMask.isEmpty()) { @@ -483,18 +475,18 @@ public class IrisCarveModifier extends EngineAssignedModifier int rz = columnIndex & 15; int yy = columnMask.nextSetBit(0); while (yy >= 0) { - MatterCavern cavern = composedCavernAt(mc, rx, yy, rz); + MatterCavern cavern = terrain.composedCavern(rx, yy, rz); if (cavern != null) { - if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) { + if (rz < 15 && terrain.composedCavern(rx, yy, rz + 1) == null) { walls.put(rx, yy, rz + 1, cavern); } - if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) { + if (rx < 15 && terrain.composedCavern(rx + 1, yy, rz) == null) { walls.put(rx + 1, yy, rz, cavern); } - if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) { + if (rz > 0 && terrain.composedCavern(rx, yy, rz - 1) == null) { walls.put(rx, yy, rz - 1, cavern); } - if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) { + if (rx > 0 && terrain.composedCavern(rx - 1, yy, rz) == null) { walls.put(rx - 1, yy, rz, cavern); } } @@ -505,7 +497,7 @@ public class IrisCarveModifier extends EngineAssignedModifier private void addCrossChunkBoundaryWalls( Mantle mantle, - MantleChunk mc, + CaveTerrainSnapshot terrain, CarveWallBuffer walls, CarveColumnMask[] boundaryMasks, int chunkX, @@ -534,10 +526,10 @@ public class IrisCarveModifier extends EngineAssignedModifier } int height = maxY + 1; - MatterCavern[] ownWest = west == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.WEST, height); - MatterCavern[] ownEast = east == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.EAST, height); - MatterCavern[] ownNorth = north == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.NORTH, height); - MatterCavern[] ownSouth = south == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.SOUTH, height); + MatterCavern[] ownWest = west == null ? null : terrain.composedFace(TerrainMatterView.Face.WEST, height); + MatterCavern[] ownEast = east == null ? null : terrain.composedFace(TerrainMatterView.Face.EAST, height); + MatterCavern[] ownNorth = north == null ? null : terrain.composedFace(TerrainMatterView.Face.NORTH, height); + MatterCavern[] ownSouth = south == null ? null : terrain.composedFace(TerrainMatterView.Face.SOUTH, height); MatterCavern[] fromWest = west == null ? null : TerrainMatterView.getComposedFace(west, TerrainMatterView.Face.EAST, height); MatterCavern[] fromEast = east == null ? null : TerrainMatterView.getComposedFace(east, TerrainMatterView.Face.WEST, height); MatterCavern[] fromNorth = north == null ? null : TerrainMatterView.getComposedFace(north, TerrainMatterView.Face.SOUTH, height); @@ -586,18 +578,9 @@ public class IrisCarveModifier extends EngineAssignedModifier return plate.get(chunkX & 31, chunkZ & 31); } - private MatterCavern composedCavernAt(MantleChunk mantleChunk, int x, int y, int z) { - return TerrainMatterView.getComposedCavern(mantleChunk, x, y, z); - } - - private static T dataIfPresent(MantleChunk mantleChunk, int x, int y, int z, Class type) { - return TerrainMatterView.get(mantleChunk, x, y, z, type); - } - private void processColumnFromMask( Hunk output, - MantleChunk mc, - Mantle mantle, + CaveTerrainSnapshot terrain, CarveColumnMask columnMask, int columnIndex, int chunkX, @@ -631,7 +614,7 @@ public class IrisCarveModifier extends EngineAssignedModifier zone.ceiling = buf; } else { if (zone.isValid(resolverState.height)) { - processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, + processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache); } zone = new CaveZone(); @@ -644,14 +627,14 @@ public class IrisCarveModifier extends EngineAssignedModifier } if (zone.isValid(resolverState.height)) { - processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, + processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache); } } private void processBoundaryColumnFromMask( Hunk output, - MantleChunk mantleChunk, + CaveTerrainSnapshot terrain, CarveColumnMask boundaryMask, CarveWallBuffer walls, int columnIndex, @@ -678,7 +661,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (y == zoneCeiling + 1) { zoneCeiling = y; } else { - paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, + paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); zoneFloor = y; zoneCeiling = y; @@ -686,13 +669,13 @@ public class IrisCarveModifier extends EngineAssignedModifier y = boundaryMask.nextSetBit(y + 1); } - paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, + paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); } private void paintBoundaryZone( Hunk output, - MantleChunk mantleChunk, + CaveTerrainSnapshot terrain, CarveWallBuffer walls, int rx, int rz, @@ -715,8 +698,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } if (floorBiome != null) { - HydrologyCaveCell floorHydrology = dataIfPresent( - mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class); + HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zoneFloor, rz); IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(); KList floorLayers = floorBiome.generateLayers( resolverState.dimension, worldX, worldZ, rng, 3, zoneFloor, resolverState.data, resolverState.complex); @@ -728,8 +710,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (floorY < 0) { break; } - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, rx, floorY, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, floorY, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -764,8 +745,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (ceilingY >= worldMaxY) { break; } - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, rx, ceilingY, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, ceilingY, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -800,7 +780,7 @@ public class IrisCarveModifier extends EngineAssignedModifier return (h & 15L) == 0L; } - private void processZone(Hunk output, MantleChunk mc, Mantle mantle, + private void processZone(Hunk output, CaveTerrainSnapshot terrain, CaveZone zone, int rx, int rz, int xx, int zz, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, @@ -815,16 +795,15 @@ public class IrisCarveModifier extends EngineAssignedModifier output.setRaw(rx, zone.ceiling, rz, AIR); } - IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); - IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); + IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); if (floorBiome == null && ceilingBiome == null) { - normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz); + normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz); return; } if (floorBiome != null) { - HydrologyCaveCell floorHydrology = dataIfPresent( - mc, rx, zone.floor, rz, HydrologyCaveCell.class); + HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zone.floor, rz); IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(); KList floorBlocks = floorBiome.generateLayers(resolverState.dimension, xx, zz, rng, 3, zone.floor, resolverState.data, resolverState.complex); for (int i = 0; i < zone.floor - 1; i++) { @@ -832,7 +811,7 @@ public class IrisCarveModifier extends EngineAssignedModifier break; } int y = zone.floor - i - 1; - HydrologyCaveCell hydrology = dataIfPresent(mc, rx, y, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, y, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -865,7 +844,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (cy >= maxY) { break; } - HydrologyCaveCell hydrology = dataIfPresent(mc, rx, cy, rz, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(rx, cy, rz); if (hydrology != null && hydrology.protectsPlacement() && hydrology.action() != HydrologyCaveAction.SEAL_GUARD) { @@ -884,64 +863,71 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz); + normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz); } - public void decorateNaturalCaves(int blockX, int blockZ, Hunk output) { + public void decorateNaturalCaves(int blockX, int blockZ, Hunk output, ChunkContext context) { Mantle mantle = getEngine().getMantle().getMantle(); - MantleChunk chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use(); - CaveInputs resolver = new CaveInputs(getEngine()); + CaveTerrainSnapshot terrain = context == null ? null : context.getCaveTerrain(); + if (context != null) { + context.setCaveTerrain(null); + } + if (terrain == null || !terrain.covers(blockX >> 4, blockZ >> 4)) { + MantleChunk chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use(); + try { + terrain = CaveTerrainSnapshot.capture(chunk, blockX >> 4, blockZ >> 4); + } finally { + chunk.release(); + } + } + CaveInputs resolver = CaveInputs.forContentChunk(getEngine(), blockX >> 4, blockZ >> 4); Long2ObjectOpenHashMap caveBiomes = new Long2ObjectOpenHashMap<>(256); Map customBiomes = new HashMap<>(); - try { - int width = output.getWidth(); - int depth = output.getDepth(); - int height = output.getHeight(); - ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; - for (int localX = 0; localX < width; localX++) { - for (int localZ = 0; localZ < depth; localZ++) { - int worldX = blockX + localX; - int worldZ = blockZ + localZ; - int floor = -1; - int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); - for (int y = 1; y < height; y++) { - if (y > top) { - break; - } - NativeBlockState state = output.getRaw(localX, y, localZ); - if (B.isSolid(state)) { - if (floor >= 0) { - CaveZone zone = new CaveZone(); - zone.setFloor(floor); - zone.setCeiling(y - 1); - if (zone.isValid(resolver.height) - && !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) { - if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) { - mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING); - } - if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) { - mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR); - } - IrisBiome floorBiome = resolveCaveBoundaryBiome(chunk, localX, floor, localZ, - worldX, worldZ, resolver, caveBiomes, customBiomes); - IrisBiome ceilingBiome = resolveCaveBoundaryBiome(chunk, localX, y - 1, localZ, - worldX, worldZ, resolver, caveBiomes, customBiomes); - decorateZone(output, zone, localX, localZ, worldX, worldZ, - floorBiome, ceilingBiome); - if (extent != null) { - top = extent.highestStoredY(localX, localZ); - } + int width = output.getWidth(); + int depth = output.getDepth(); + int height = output.getHeight(); + ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; + for (int localX = 0; localX < width; localX++) { + for (int localZ = 0; localZ < depth; localZ++) { + int worldX = blockX + localX; + int worldZ = blockZ + localZ; + int floor = -1; + int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); + for (int y = 1; y < height; y++) { + if (y > top) { + break; + } + NativeBlockState state = output.getRaw(localX, y, localZ); + if (B.isSolid(state)) { + if (floor >= 0) { + CaveZone zone = new CaveZone(); + zone.setFloor(floor); + zone.setCeiling(y - 1); + if (zone.isValid(resolver.height) + && !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) { + if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) { + mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING); + } + if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) { + mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR); + } + IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, localX, floor, localZ, + worldX, worldZ, resolver, caveBiomes, customBiomes); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, localX, y - 1, localZ, + worldX, worldZ, resolver, caveBiomes, customBiomes); + decorateZone(output, zone, localX, localZ, worldX, worldZ, + floorBiome, ceilingBiome); + if (extent != null) { + top = extent.highestStoredY(localX, localZ); } - floor = -1; } - } else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) { - floor = y; + floor = -1; } + } else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) { + floor = y; } } } - } finally { - chunk.release(); } } @@ -966,7 +952,7 @@ public class IrisCarveModifier extends EngineAssignedModifier private void normalizeCaveZoneWaterlogging( Hunk output, - MantleChunk mantleChunk, + CaveTerrainSnapshot terrain, CaveZone zone, int localX, int localZ, @@ -976,13 +962,11 @@ public class IrisCarveModifier extends EngineAssignedModifier int minimumY = Math.max(0, zone.floor - 1); int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1); for (int y = minimumY; y <= maximumY; y++) { - HydrologyCaveCell hydrology = dataIfPresent( - mantleChunk, localX, y, localZ, HydrologyCaveCell.class); + HydrologyCaveCell hydrology = terrain.hydrology(localX, y, localZ); if (hydrology == null) { continue; } - MatterCavern baseline = dataIfPresent( - mantleChunk, localX, y, localZ, MatterCavern.class); + MatterCavern baseline = terrain.cavern(localX, y, localZ); NativeBlockState current = output.getRaw(localX, y, localZ); NativeBlockState columnFluid = getComplex().resolveHydrologyFluid( hydrology.fluidProfileKey(), @@ -1001,8 +985,8 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - IrisBiome resolveCaveBoundaryBiome(MantleChunk mantleChunk, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { - MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z); + IrisBiome resolveCaveBoundaryBiome(CaveTerrainSnapshot terrain, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + MatterCavern cavern = terrain == null ? null : terrain.composedCavern(x, y, z); return resolveCaveBoundaryBiome( cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache); } @@ -1179,19 +1163,39 @@ public class IrisCarveModifier extends EngineAssignedModifier private final ProceduralStream caveBiomes; private final ProceduralStream heights; private final CaveColumn[] columns; + private final boolean chunkLimited; + private final int chunkX; + private final int chunkZ; CaveInputs(Engine engine) { + this(engine, false, 0, 0); + } + + /** + * Inputs for the content stage of one chunk. A coordinate inside that chunk resolves under the chunk's + * own generation route, exactly the binding the natural terrain stage samples with, so it takes the + * captured column path; any other coordinate keeps the per-coordinate history dispatch. + */ + static CaveInputs forContentChunk(Engine engine, int chunkX, int chunkZ) { + return new CaveInputs(engine, true, chunkX, chunkZ); + } + + private CaveInputs(Engine engine, boolean contentChunk, int chunkX, int chunkZ) { this.engine = engine; complex = engine.getComplex(); dimension = engine.getDimension(); data = engine.getData(); height = engine.getHeight(); IrisContext context = IrisContext.get(); - boolean scoped = engine instanceof IrisEngine irisEngine + boolean generating = engine instanceof IrisEngine irisEngine && irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread() && context != null && context.getChunkContext() != null - && context.getChunkContext().getComplex() == complex - && context.getChunkContext().isNaturalTerrain(); + && context.getChunkContext().getComplex() == complex; + boolean natural = generating && context.getChunkContext().isNaturalTerrain(); + boolean scoped = natural || generating && contentChunk; + this.chunkLimited = !natural; + this.chunkX = chunkX; + this.chunkZ = chunkZ; snapshot = scoped ? IrisDimensionCarvingResolver.snapshot(engine) : null; minimumY = scoped ? engine.getWorld().minHeight() : 0; stack = scoped ? engine.getDimensionStackContext() : null; @@ -1202,7 +1206,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } IrisBiome resolve(int x, int y, int z) { - if (snapshot == null) { + if (snapshot == null || chunkLimited && ((x >> 4) != chunkX || (z >> 4) != chunkZ)) { return engine.getCaveBiome(x, y, z, state); } IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z); diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java index 041a2af5d..a3813bac2 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java @@ -110,6 +110,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator> 4, z >> 4)) { return; } + context.setCaveTerrain(null); MantleChunk chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); try { synchronized (chunk) { diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java new file mode 100644 index 000000000..95c64d0c2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java @@ -0,0 +1,175 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; +import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.world.storage.matter.IrisMatterSupport; +import art.arcane.iris.world.storage.matter.PreObjectMatterCell; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CaveTerrainSnapshotTest { + private static final int SECTIONS = 5; + + @BeforeClass + public static void registerMatter() { + IrisMatterSupport.ensureRegistered(); + } + + @Test + public void snapshotMatchesTerrainViewForEveryCellFaceAndResolverInput() { + for (long seed = 1; seed <= 2; seed++) { + MantleChunk chunk = randomChunk(new Random(seed), seed % 2 == 0); + CaveTerrainSnapshot snapshot = CaveTerrainSnapshot.capture(chunk, 3, -4); + assertTrue(snapshot.covers(3, -4)); + assertFalse(snapshot.covers(3, 4)); + boolean anyHydrology = false; + for (int x = 0; x < 16; x++) { + for (int y = -2; y < SECTIONS * 16 + 3; y++) { + for (int z = 0; z < 16; z++) { + String cell = "seed=" + seed + " " + x + "," + y + "," + z; + assertSame(cell, TerrainMatterView.get(chunk, x, y, z, MatterCavern.class), snapshot.cavern(x, y, z)); + HydrologyCaveCell hydrology = TerrainMatterView.get(chunk, x, y, z, HydrologyCaveCell.class); + anyHydrology |= hydrology != null; + assertSame(cell, hydrology, snapshot.hydrology(x, y, z)); + assertSame(cell, TerrainMatterView.getComposedCavern(chunk, x, y, z), snapshot.composedCavern(x, y, z)); + } + } + } + assertEquals(anyHydrology, snapshot.hasHydrology()); + for (TerrainMatterView.Face face : TerrainMatterView.Face.values()) { + for (int height : new int[]{0, 1, 37, SECTIONS * 16, SECTIONS * 16 + 9}) { + MatterCavern[] expected = TerrainMatterView.getComposedFace(chunk, face, height); + MatterCavern[] actual = snapshot.composedFace(face, height); + assertEquals(expected.length, actual.length); + for (int index = 0; index < expected.length; index++) { + assertSame(face + " " + index, expected[index], actual[index]); + } + } + } + assertEquals(legacyResolverInputs(chunk), snapshotResolverInputs(snapshot)); + } + } + + @Test + public void chunksWithoutHydrologyAnswerNullWithoutCells() { + Matter[] sections = new Matter[SECTIONS]; + sections[1] = new IrisMatter(16, 16, 16); + MatterCavern cavern = new MatterCavern(true, "", (byte) 0); + sections[1].slice(MatterCavern.class).set(4, 5, 6, cavern); + sections[1].slice(PreObjectMatterCell.class).set(4, 6, 6, PreObjectMatterCell.hydrology(null)); + CaveTerrainSnapshot snapshot = CaveTerrainSnapshot.capture(chunk(sections), 0, 0); + assertFalse(snapshot.hasHydrology()); + assertNull(snapshot.hydrology(4, 22, 6)); + assertSame(cavern, snapshot.composedCavern(4, 21, 6)); + assertNull(snapshot.cavern(4, 22, 6)); + } + + private static Map legacyResolverInputs(MantleChunk chunk) { + Map inputs = new HashMap<>(); + for (int x = 0; x < 16; x++) { + for (int y = 0; y < SECTIONS * 16; y++) { + for (int z = 0; z < 16; z++) { + MatterCavern cavern = TerrainMatterView.get(chunk, x, y, z, MatterCavern.class); + HydrologyCaveCell hydrology = TerrainMatterView.get(chunk, x, y, z, HydrologyCaveCell.class); + if (cavern != null || hydrology != null) { + inputs.put(x + ":" + y + ":" + z, identity(cavern) + "/" + identity(hydrology)); + } + } + } + } + return inputs; + } + + private static Map snapshotResolverInputs(CaveTerrainSnapshot snapshot) { + Map inputs = new HashMap<>(); + snapshot.forEachCell((x, y, z, cavern, hydrology) -> assertNull(inputs.put(x + ":" + y + ":" + z, + identity(cavern) + "/" + identity(hydrology)))); + return inputs; + } + + private static String identity(Object value) { + return value == null ? "null" : Integer.toHexString(System.identityHashCode(value)); + } + + private static MantleChunk randomChunk(Random random, boolean manyCaverns) { + MatterCavern[] caverns = new MatterCavern[manyCaverns ? 40 : 4]; + for (int index = 0; index < caverns.length; index++) { + caverns[index] = new MatterCavern(index % 3 != 0, "biome-" + index, (byte) (index % 4)); + } + HydrologyCaveCell[] hydrology = new HydrologyCaveCell[HydrologyCaveAction.values().length]; + for (int index = 0; index < hydrology.length; index++) { + hydrology[index] = new HydrologyCaveCell(HydrologyCaveAction.values()[index], "river", "flooded-" + index); + } + Matter[] sections = new Matter[SECTIONS]; + for (int section = 0; section < SECTIONS; section++) { + if (section == 2) { + continue; + } + Matter matter = new IrisMatter(16, 16, 16); + sections[section] = matter; + int mode = section % 4; + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + boolean aligned = ((z << 8 | y << 4 | x) & 0x333) == 0; + int roll = random.nextInt(100); + if (mode == 1 ? aligned && roll < 50 : roll < 40) { + matter.slice(MatterCavern.class).set(x, y, z, caverns[random.nextInt(caverns.length)]); + } + if (mode != 3 && random.nextInt(100) < 6) { + matter.slice(HydrologyCaveCell.class).set(x, y, z, hydrology[random.nextInt(hydrology.length)]); + } + int journal = random.nextInt(100); + if (journal < 3) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, PreObjectMatterCell.cavern(null)); + } else if (journal < 6) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, + PreObjectMatterCell.cavern(caverns[random.nextInt(caverns.length)])); + } else if (journal < 8) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, PreObjectMatterCell.hydrology(null)); + } else if (journal < 10) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, + PreObjectMatterCell.block(null).captureHydrology(hydrology[random.nextInt(hydrology.length)])); + } else if (journal < 13) { + matter.slice(PreObjectMatterCell.class).set(x, y, z, PreObjectMatterCell.string("marker")); + } + } + } + } + } + return chunk(sections); + } + + @SuppressWarnings("unchecked") + private static MantleChunk chunk(Matter[] sections) { + MantleChunk chunk = mock(MantleChunk.class); + when(chunk.sectionCount()).thenReturn(sections.length); + when(chunk.exists(anyInt())).thenAnswer(call -> { + int section = call.getArgument(0); + return section >= 0 && section < sections.length && sections[section] != null; + }); + when(chunk.get(anyInt())).thenAnswer(call -> { + assertTrue(Thread.holdsLock(chunk)); + int section = call.getArgument(0); + return section >= 0 && section < sections.length ? sections[section] : null; + }); + return chunk; + } +} 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 diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java index 53d93e80c..04cfa19a2 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java @@ -2,7 +2,6 @@ package art.arcane.iris.generation.runtime; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.mantle.EngineMantle; -import art.arcane.iris.generation.mantle.TerrainMatterView; import art.arcane.iris.generation.stage.IrisCarveModifier; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiomePaletteLayer; @@ -15,12 +14,13 @@ import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.function.Consumer4; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.interpolation.Interpolated; +import art.arcane.iris.world.storage.matter.IrisMatterSupport; +import art.arcane.volmlib.util.matter.IrisMatter; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import org.junit.Test; @@ -42,7 +42,6 @@ import static org.mockito.ArgumentMatchers.any; 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.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doAnswer; @@ -100,19 +99,10 @@ public class IrisCarveWallPaletteScopeTest { } try (IrisEngine.GenerationRuntimeScope generation = fixture.engine.openGenerationRuntimeScope( new IrisEngine.GenerationRuntimeBinding(fixture.engine, selected)); - IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context); - MockedStatic terrain = mockStatic(TerrainMatterView.class)) { + IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context)) { if (mode == Mode.ASSEMBLY) { fixture.engine.threadState.setAssembly(fixture.assembly); } - terrain.when(() -> TerrainMatterView.iterate(eq(fixture.chunk), eq(MatterCavern.class), any())) - .thenAnswer(call -> { - Consumer4 consumer = call.getArgument(2); - for (int coordinate : new int[]{4, 8, 12}) { - consumer.accept(coordinate, 10, coordinate, new MatterCavern(true, "", (byte) 0)); - } - return null; - }); try { assertSame(expectedData, fixture.engine.getData()); assertEquals(mode == Mode.NATURAL, expectedComplex.isNaturalTerrainContext()); @@ -196,6 +186,14 @@ public class IrisCarveWallPaletteScopeTest { doReturn(mantle).when(engineMantle).getMantle(); doReturn(chunk).when(mantle).getChunk(-2, 3); doReturn(chunk).when(chunk).use(); + IrisMatterSupport.ensureRegistered(); + Matter section = new IrisMatter(16, 16, 16); + for (int coordinate : new int[]{4, 8, 12}) { + section.slice(MatterCavern.class).set(coordinate, 10, coordinate, new MatterCavern(true, "", (byte) 0)); + } + doReturn(1).when(chunk).sectionCount(); + doReturn(true).when(chunk).exists(0); + doReturn(section).when(chunk).get(0); active = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); detached = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); field(engine, IrisEngine.class, "lifecycleLock", new Object()); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java index 85c9eca15..f43fe5bf1 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.TerrainMatterView; import art.arcane.iris.world.IrisWorld; import art.arcane.iris.world.storage.matter.IrisMatterSupport; @@ -56,6 +57,7 @@ public class IrisCarveBoundaryFacesTest { return true; }); when(chunks[side].get(anyInt())).thenAnswer(call -> matter[selected][(int) call.getArgument(0)]); + when(chunks[side].sectionCount()).thenReturn(2); if (side == 0) { continue; } @@ -87,10 +89,10 @@ public class IrisCarveBoundaryFacesTest { int[] surfaces = new int[256]; Arrays.fill(surfaces, 30); Method method = IrisCarveModifier.class.getDeclaredMethod("addCrossChunkBoundaryWalls", - Mantle.class, MantleChunk.class, CarveWallBuffer.class, CarveColumnMask[].class, + Mantle.class, CaveTerrainSnapshot.class, CarveWallBuffer.class, CarveColumnMask[].class, int.class, int.class, int[].class); method.setAccessible(true); - method.invoke(modifier, mantle, chunks[0], walls, masks, 0, 0, surfaces); + method.invoke(modifier, mantle, CaveTerrainSnapshot.capture(chunks[0], 0, 0), walls, masks, 0, 0, surfaces); CarveWallBuffer expectedWalls = new CarveWallBuffer(64); for (int y = 1; y < 32; y++) { for (int offset = 0; offset < 16; offset++) { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java index 31cbad1d9..c32ea2a51 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java @@ -2,6 +2,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.ResourceLoader; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.biome.IrisBiome; @@ -12,9 +13,10 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.iris.world.storage.matter.IrisMatterSupport; +import art.arcane.volmlib.util.matter.IrisMatter; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; -import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.math.RNG; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.junit.Test; @@ -55,30 +57,27 @@ public class IrisCarveModifierBoundarySupportTest { rng.setAccessible(true); rng.set(modifier, new RNG(71L)); + IrisMatterSupport.ensureRegistered(); + Matter floorMatter = new IrisMatter(16, 16, 16); + Matter ceilingMatter = new IrisMatter(16, 16, 16); + floorMatter.slice(MatterCavern.class).set(1, 6, 2, new MatterCavern(true, "custom/floor", (byte) 0)); + ceilingMatter.slice(MatterCavern.class).set(1, 11, 2, new MatterCavern(true, "custom/other", (byte) 0)); MantleChunk mantleChunk = mock(MantleChunk.class); - Matter floorMatter = mock(Matter.class); - Matter ceilingMatter = mock(Matter.class); - MatterSlice floorSlice = mock(MatterSlice.class); - MatterSlice ceilingSlice = mock(MatterSlice.class); + doReturn(3).when(mantleChunk).sectionCount(); doReturn(true).when(mantleChunk).exists(0); doReturn(true).when(mantleChunk).exists(2); doReturn(floorMatter).when(mantleChunk).get(0); doReturn(ceilingMatter).when(mantleChunk).get(2); - doReturn(true).when(floorMatter).hasSlice(MatterCavern.class); - doReturn(true).when(ceilingMatter).hasSlice(MatterCavern.class); - doReturn(floorSlice).when(floorMatter).getSlice(MatterCavern.class); - doReturn(ceilingSlice).when(ceilingMatter).getSlice(MatterCavern.class); - doReturn(new MatterCavern(true, "custom/floor", (byte) 0)).when(floorSlice).get(1, 6, 2); - doReturn(null).when(ceilingSlice).get(1, 10, 2); + CaveTerrainSnapshot terrain = CaveTerrainSnapshot.capture(mantleChunk, 2, 2); Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(); Map customBiomeCache = new HashMap<>(); IrisCarveModifier.CaveInputs resolverState = new IrisCarveModifier.CaveInputs(engine); IrisBiome floor = modifier.resolveCaveBoundaryBiome( - mantleChunk, 1, 6, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); + terrain, 1, 6, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); IrisBiome ceiling = modifier.resolveCaveBoundaryBiome( - mantleChunk, 1, 42, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); + terrain, 1, 42, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); assertSame(customFloor, floor); assertSame(resolvedCeiling, ceiling); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveInputsTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveInputsTest.java index d07611c8e..5acfa3fb2 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveInputsTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveInputsTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger; 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.Mockito.CALLS_REAL_METHODS; @@ -122,6 +123,31 @@ public class IrisCarveModifierCaveInputsTest { } } + @Test + public void contentChunkCapturesOnlyItsOwnColumnsAndDispatchesTheRestThroughHistory() { + try (Fixture fixture = new Fixture()) { + when(fixture.context.isNaturalTerrain()).thenReturn(false); + IrisBiome historical = biome("historical", 0); + when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))).thenReturn(historical); + IrisCarveModifier.CaveInputs inputs = IrisCarveModifier.CaveInputs.forContentChunk(fixture.engine, -2, 1); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + for (int x = -32; x <= -17; x++) { + for (int z = 16; z <= 31; z += 5) { + for (int y : new int[]{0, 30, 33, 34, 39, 40}) { + assertSame(fixture.scalar.getCaveBiome(x, y, z, state), inputs.resolve(x, y, z)); + } + } + } + int captured = fixture.samples.get(); + assertTrue(captured > 0); + assertSame(historical, inputs.resolve(-33, 20, 16)); + assertSame(historical, inputs.resolve(-16, 20, 16)); + assertSame(historical, inputs.resolve(-17, 20, 15)); + assertSame(historical, inputs.resolve(-17, 20, 32)); + assertEquals(captured, fixture.samples.get()); + } + } + private static IrisBiome biome(String key, int depth) { IrisBiome biome = new IrisBiome(); biome.setLoadKey(key); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java index 433a55dd5..ca9f39398 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.terrain.InferredType; @@ -56,7 +57,7 @@ public class IrisCarveModifierInferenceIsolationTest { Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod( "paintBoundaryZone", Hunk.class, - MantleChunk.class, + CaveTerrainSnapshot.class, CarveWallBuffer.class, int.class, int.class, @@ -72,7 +73,7 @@ public class IrisCarveModifierInferenceIsolationTest { paintBoundaryZone.invoke( modifier, mock(Hunk.class), - mock(MantleChunk.class), + CaveTerrainSnapshot.capture(mock(MantleChunk.class), 0, 0), walls, 0, 0, diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java index 9b16e3bcd..59045f346 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java @@ -5,6 +5,8 @@ import art.arcane.iris.generation.decoration.IrisCeilingDecorator; import art.arcane.iris.generation.decoration.IrisSurfaceDecorator; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.SeedManager; +import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.EngineMantle; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.decoration.IrisDecorationPart; @@ -32,18 +34,22 @@ import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; 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.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.clearInvocations; 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.never; import static org.mockito.Mockito.verify; public class IrisNaturalCaveScanTest { @@ -70,6 +76,26 @@ public class IrisNaturalCaveScanTest { verifyScan(16, 16); } + @Test + public void decorationReusesOnlyTheCarveSnapshotOfItsOwnChunk() throws Exception { + Fixture fixture = new Fixture(); + ChunkContext context = mock(ChunkContext.class); + doReturn(CaveTerrainSnapshot.capture(fixture.chunk, -2, 3)).when(context).getCaveTerrain(); + clearInvocations(fixture.chunk); + fixture.modifier.decorateNaturalCaves(-32, 48, fixture.output(3, 2), context); + verify(fixture.chunk, never()).use(); + verify(context).setCaveTerrain(null); + assertFalse(fixture.calls.isEmpty()); + + Fixture other = new Fixture(); + ChunkContext stale = mock(ChunkContext.class); + doReturn(CaveTerrainSnapshot.capture(other.chunk, 5, 5)).when(stale).getCaveTerrain(); + clearInvocations(other.chunk); + other.modifier.decorateNaturalCaves(-32, 48, other.output(3, 2), stale); + verify(other.chunk).use(); + assertEquals(fixture.calls, other.calls); + } + @Test public void listenerFailureReleasesMantleChunk() throws Exception { Fixture fixture = new Fixture(); @@ -78,7 +104,7 @@ public class IrisNaturalCaveScanTest { throw failure; }); assertSame(failure, assertThrows(IllegalStateException.class, - () -> fixture.modifier.decorateNaturalCaves(-32, 48, output))); + () -> fixture.modifier.decorateNaturalCaves(-32, 48, output, null))); verify(fixture.chunk).release(); } @@ -92,7 +118,7 @@ public class IrisNaturalCaveScanTest { IllegalStateException failure = new IllegalStateException("height unavailable"); doAnswer(call -> { throw failure; }).when(output).getHeight(); assertSame(failure, assertThrows(IllegalStateException.class, - () -> fixture.modifier.decorateNaturalCaves(-32, 48, output))); + () -> fixture.modifier.decorateNaturalCaves(-32, 48, output, null))); verify(fixture.chunk).release(); } @@ -104,7 +130,7 @@ public class IrisNaturalCaveScanTest { Hunk fullOutput = full.sparseOutput(Hunk.newArrayHunk(16, 32, 16)); List fullWrites = new ArrayList<>(); full.modifier.decorateNaturalCaves(-32, 48, - fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null); Fixture bounded = new Fixture(true); ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class); @@ -114,7 +140,7 @@ public class IrisNaturalCaveScanTest { bounded.sparseOutput(holder); List boundedWrites = new ArrayList<>(); bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder, - (x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + (x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null); assertTrue("fixture must decorate a zone above the initial column top", full.calls.contains("floor:0:0:27:1")); @@ -141,7 +167,7 @@ public class IrisNaturalCaveScanTest { Hunk output = fixture.output(width, depth); List writes = new ArrayList<>(); fixture.modifier.decorateNaturalCaves(-32, 48, - output.listen((x, y, z, state) -> writes.add(x + ":" + y + ":" + z + ":" + state.key()))); + output.listen((x, y, z, state) -> writes.add(x + ":" + y + ":" + z + ":" + state.key())), null); List expectedCalls = new ArrayList<>(); List expectedMarkers = new ArrayList<>(); List expectedWrites = new ArrayList<>(); @@ -239,7 +265,7 @@ public class IrisNaturalCaveScanTest { IrisBiome biome = mock(IrisBiome.class); doReturn(new IrisDecorator[]{new IrisDecorator()}).when(biome).getDecoratorBucket(IrisDecorationPart.NONE); doReturn(new IrisDecorator[]{new IrisDecorator()}).when(biome).getDecoratorBucket(IrisDecorationPart.CEILING); - doReturn(biome).when(modifier).resolveCaveBoundaryBiome(eq(chunk), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), any(), any()); + doReturn(biome).when(modifier).resolveCaveBoundaryBiome(argThat((CaveTerrainSnapshot terrain) -> terrain != null && terrain.covers(-2, 3)), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), any(), any()); IrisDecorantActuator decorant = mock(IrisDecorantActuator.class); IrisSurfaceDecorator surface = mock(IrisSurfaceDecorator.class); IrisCeilingDecorator roof = mock(IrisCeilingDecorator.class);