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 index d7b4a4bfc..a175e83bb 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshot.java @@ -144,20 +144,6 @@ public final class CaveTerrainSnapshot { } } - 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); 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 adc54a791..60f5c42d1 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 @@ -136,12 +136,8 @@ 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; @@ -150,14 +146,6 @@ 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/runtime/Engine.java b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java index 568c92088..74dab18fb 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java @@ -342,12 +342,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, @BlockCoordinates default SubterrainCell getSubterrainCell(int x, int y, int z) { - if (getComplex() == null || getDimension() == null - || !getDimension().allowsSubterrainFeatures() - || getDimension().getSubterrainFeatures() == null || getDimension().getSubterrainFeatures().isEmpty()) { - return SubterrainCell.OUTSIDE; - } - SubterrainPlanner planner = getComplex().getSubterrainPlanner(); + IrisComplex complex = getComplex(); + SubterrainPlanner planner = complex == null ? null : SubterrainChunkCells.activePlanner(complex, getDimension()); return planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, y + getWorld().minHeight(), z); } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 709d50b7c..0956215c7 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -19,6 +19,7 @@ package art.arcane.iris.generation.runtime; import art.arcane.iris.generation.subterrain.SubterrainCell; +import art.arcane.iris.generation.subterrain.SubterrainPlanner; import art.arcane.iris.generation.runtime.EngineBackgroundTasks.BackgroundTaskDrain; import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly; import art.arcane.iris.pack.loading.IrisData; @@ -178,6 +179,7 @@ public class IrisEngine implements Engine { @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private volatile boolean generationHistoryRoutingRequired; + private volatile SubterrainGate subterrainGate; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) volatile EngineTarget publishedTarget; @@ -613,14 +615,55 @@ public class IrisEngine implements Engine { @BlockCoordinates @Override public SubterrainCell getSubterrainCell(int x, int y, int z) { - if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) { - Optional saved = generationHistoryRuntimeRouter.biomes() - .subterrainCell(x, y + getWorld().minHeight(), z); + EngineThreadState.Frame frame = threadState.current(); + GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter; + if ((frame == null || frame.binding == null) && router != null) { + Optional saved = router.biomes() + .subterrainCell(x, y + targetIn(frame).getWorld().minHeight(), z); if (saved.isPresent()) { return saved.get(); } } - return Engine.super.getSubterrainCell(x, y, z); + if (frame == null || frame.assembly == null && (frame.environment == null || frame.binding != null)) { + GenerationRuntime current = selectedGenerationRuntime(frame); + if (current == null) { + return SubterrainCell.OUTSIDE; + } + SubterrainPlanner planner = admittedSubterrainPlanner(current.complex(), current.dimension()); + return planner == null ? SubterrainCell.OUTSIDE + : planner.sample(x, y + current.target().getWorld().minHeight(), z); + } + SubterrainPlanner planner = subterrainPlannerIn(frame); + return planner == null ? SubterrainCell.OUTSIDE + : planner.sample(x, y + targetIn(frame).getWorld().minHeight(), z); + } + + @ChunkCoordinates + public SubterrainChunkCells subterrainChunkCells(int chunkX, int chunkZ) { + EngineThreadState.Frame frame = threadState.current(); + if ((frame == null || frame.binding == null) && generationHistoryRuntimeRouter != null) { + return SubterrainChunkCells.delegated(this); + } + SubterrainPlanner planner = subterrainPlannerIn(frame); + return SubterrainChunkCells.planned(this, planner, chunkX, chunkZ, + planner == null ? 0 : targetIn(frame).getWorld().minHeight()); + } + + private SubterrainPlanner subterrainPlannerIn(EngineThreadState.Frame frame) { + IrisComplex complex = complexIn(frame); + return complex == null ? null : admittedSubterrainPlanner(complex, dimensionIn(frame)); + } + + private SubterrainPlanner admittedSubterrainPlanner(IrisComplex complex, IrisDimension dimension) { + if (complex == null || dimension == null) { + return null; + } + SubterrainGate gate = subterrainGate; + if (gate == null || gate.dimension() != dimension) { + gate = new SubterrainGate(dimension, SubterrainChunkCells.admits(dimension)); + subterrainGate = gate; + } + return gate.admitted() ? complex.getSubterrainPlanner() : null; } @BlockCoordinates @@ -1036,6 +1079,10 @@ public class IrisEngine implements Engine { return threadState.binding() != null; } + public boolean hasGenerationHistoryRouting() { + return generationHistoryRuntimeRouter != null || generationHistoryRoutingRequired; + } + private boolean usesScopedNaturalTerrain() { return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext(); } @@ -1333,7 +1380,10 @@ public class IrisEngine implements Engine { @Override public IrisComplex getComplex() { - EngineThreadState.Frame frame = threadState.current(); + return complexIn(threadState.current()); + } + + private IrisComplex complexIn(EngineThreadState.Frame frame) { RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null && assembly.complex != null) { return assembly.complex; @@ -1344,7 +1394,10 @@ public class IrisEngine implements Engine { @Override public EngineTarget getTarget() { - EngineThreadState.Frame frame = threadState.current(); + return targetIn(threadState.current()); + } + + private EngineTarget targetIn(EngineThreadState.Frame frame) { RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.target; @@ -1372,7 +1425,10 @@ public class IrisEngine implements Engine { @Override public IrisDimension getDimension() { - EngineThreadState.Frame frame = threadState.current(); + return dimensionIn(threadState.current()); + } + + private IrisDimension dimensionIn(EngineThreadState.Frame frame) { if (frame != null) { BiomeEnvironmentBinding environment = frame.environment; if (environment != null && frame.binding == null) { @@ -1892,4 +1948,6 @@ public class IrisEngine implements Engine { } } + private record SubterrainGate(IrisDimension dimension, boolean admitted) { + } } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/SubterrainChunkCells.java b/core/src/main/java/art/arcane/iris/generation/runtime/SubterrainChunkCells.java new file mode 100644 index 000000000..371be9645 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/SubterrainChunkCells.java @@ -0,0 +1,67 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.subterrain.SubterrainCell; +import art.arcane.iris.generation.subterrain.SubterrainPlanner; +import art.arcane.iris.generation.terrain.IrisDimension; + +import java.util.Objects; + +public final class SubterrainChunkCells { + private final Engine engine; + private final boolean delegated; + private final SubterrainPlanner.ChunkSampler sampler; + private final int minimumY; + + private SubterrainChunkCells(Engine engine, boolean delegated, SubterrainPlanner.ChunkSampler sampler, int minimumY) { + this.engine = Objects.requireNonNull(engine); + this.delegated = delegated; + this.sampler = sampler; + this.minimumY = minimumY; + } + + public static SubterrainChunkCells of(Engine engine, int chunkX, int chunkZ) { + return engine instanceof IrisEngine irisEngine + ? irisEngine.subterrainChunkCells(chunkX, chunkZ) + : delegated(engine); + } + + public static SubterrainChunkCells delegated(Engine engine) { + return new SubterrainChunkCells(engine, true, null, 0); + } + + static SubterrainChunkCells planned(Engine engine, SubterrainPlanner planner, int chunkX, int chunkZ, int minimumY) { + return new SubterrainChunkCells(engine, false, planner == null ? null : planner.chunkSampler(chunkX, chunkZ), minimumY); + } + + static SubterrainPlanner activePlanner(IrisComplex complex, IrisDimension dimension) { + if (complex == null || dimension == null || !admits(dimension)) { + return null; + } + return complex.getSubterrainPlanner(); + } + + static boolean admits(IrisDimension dimension) { + return dimension.allowsSubterrainFeatures() + && dimension.getSubterrainFeatures() != null && !dimension.getSubterrainFeatures().isEmpty(); + } + + public SubterrainCell cell(int x, int y, int z) { + if (delegated) { + SubterrainCell cell = engine.getSubterrainCell(x, y, z); + return cell == null ? SubterrainCell.OUTSIDE : cell; + } + return sampler == null ? SubterrainCell.OUTSIDE : sampler.sample(x, y + minimumY, z); + } + + public IrisBiome biome(int x, int y, int z) { + if (delegated) { + return engine.getSubterrainBiome(x, y, z); + } + SubterrainCell cell = cell(x, y, z); + if (!cell.occupied() || cell.room().biome().isEmpty()) { + return null; + } + return engine.getSubterrainBiome(x, y, z); + } +} 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 a5df09420..dfdf2bfe9 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 @@ -20,9 +20,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.subterrain.SubterrainCell; -import art.arcane.iris.generation.subterrain.SubterrainPlan; import art.arcane.iris.generation.subterrain.SubterrainPlanner; -import java.util.List; import art.arcane.iris.generation.subterrain.SubterrainRasterizer; import art.arcane.iris.generation.subterrain.SubterrainSurfaceMaterials; import art.arcane.iris.generation.runtime.DimensionStackContext; @@ -31,6 +29,7 @@ import art.arcane.iris.generation.runtime.UpperDimensionContext; import art.arcane.iris.generation.terrain.Terrain3DColumn; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.SubterrainChunkCells; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.util.stream.ProceduralStream; @@ -39,6 +38,7 @@ 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; +import art.arcane.iris.generation.biome.IrisBiomePaletteLayer; import art.arcane.iris.generation.decoration.IrisDecorationPart; import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; @@ -101,7 +101,7 @@ public class IrisCarveModifier extends EngineAssignedModifier public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start(); Mantle mantle = getEngine().getMantle().getMantle(); - CaveInputs resolverState = new CaveInputs(getEngine()); + CaveInputs resolverState = CaveInputs.forTerrainChunk(getEngine(), x, z); IrisCarveScratch scratch = SCRATCH.get(); scratch.reset(); Long2ObjectOpenHashMap caveBiomeCache = scratch.caveBiomeCache; @@ -144,6 +144,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); + SubterrainPlanner subterrainPlanner = resolverState.complex.getSubterrainPlanner(); CarveResolutionContext resolutionContext = new CarveResolutionContext( output, context, @@ -156,44 +157,28 @@ public class IrisCarveModifier extends EngineAssignedModifier caveLavaHeight, chunkBlockX, chunkBlockZ, - getComplex().getSubterrainPlanner(), - getComplex().getSubterrainPlanner() == null ? List.of() : getComplex().getSubterrainPlanner().plansForBounds(chunkBlockX - 1, chunkBlockZ - 1, chunkBlockX + 16, chunkBlockZ + 16) + getEngine().getWorld().minHeight(), + resolverState.complex, + subterrainPlanner == null ? null : subterrainPlanner.chunkSampler(x, z) ); CarveResolver carveResolver = new CarveResolver(resolutionContext); terrain.forEachCell(carveResolver::apply); if (scratch.customCaveBiomePresent) { addInternalWallsFromMantle(terrain, walls, columnMasks); } else { - addInternalWallsFromMasks(walls, columnMasks); + collectInternalWalls(scratch.wallMasks, columnMasks); } 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 = terrain.hydrology(rx, yy, rz); - if (hydrology != null && hydrology.protectsPlacement() - || subterrainCell(rx + chunkBlockX, yy, rz + chunkBlockZ).owned()) { - return; - } - int worldX = rx + chunkBlockX; - int worldZ = rz + chunkBlockZ; - String customBiome = cavern.getCustomBiome(); - IrisBiome biome = customBiome.isEmpty() - ? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, resolverState) - : resolveCustomBiome(customBiomeCache, customBiome); - - if (biome != null) { - NativeBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, wallData); - int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); - - if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) { - output.setRaw(rx, yy, rz, data); - } - } - }); + WallPainter painter = new WallPainter(output, terrain, resolverState, caveBiomeCache, customBiomeCache, + surfaceHeights, getData(), chunkBlockX, chunkBlockZ); + if (!scratch.customCaveBiomePresent) { + paintInternalWalls(painter, scratch.wallMasks, boundaryMasks); + } + walls.forEach(painter); for (int columnIndex = 0; columnIndex < 256; columnIndex++) { processColumnFromMask( @@ -205,7 +190,8 @@ public class IrisCarveModifier extends EngineAssignedModifier z, resolverState, caveBiomeCache, - customBiomeCache + customBiomeCache, + scratch.layers ); } @@ -223,7 +209,8 @@ public class IrisCarveModifier extends EngineAssignedModifier z, resolverState, caveBiomeCache, - customBiomeCache + customBiomeCache, + scratch.layers ); } @@ -257,11 +244,6 @@ public class IrisCarveModifier extends EngineAssignedModifier } } - private SubterrainCell subterrainCell(int x, int y, int z) { - SubterrainCell cell = getEngine().getSubterrainCell(x, y, z); - return cell == null ? SubterrainCell.OUTSIDE : cell; - } - public void preserveSubterrain(int blockX, int blockZ, Hunk output) { SubterrainPlanner planner = getComplex().getSubterrainPlanner(); if (planner == null || planner.isEmpty()) { @@ -437,8 +419,9 @@ public class IrisCarveModifier extends EngineAssignedModifier int localX = x & 15; int localZ = z & 15; int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); - SubterrainCell feature = context.subterrainPlanner() == null ? SubterrainCell.OUTSIDE : context.subterrainPlanner().sample(context.subterrainPlans(), - context.chunkBlockX() + localX, y + getEngine().getWorld().minHeight(), context.chunkBlockZ() + localZ); + SubterrainCell feature = context.subterrainSampler() == null ? SubterrainCell.OUTSIDE + : context.subterrainSampler().sampleCandidates(context.chunkBlockX() + localX, y + context.minimumY(), + context.chunkBlockZ() + localZ); if (feature.owned() && (!feature.solid() || feature.room().reservedSolid())) { context.output().setRaw(localX, y, localZ, SubterrainRasterizer.state(feature)); if (feature.carve()) { @@ -492,7 +475,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (isFluidIntent(cavern)) { fluid = hydrology == null ? context.chunkContext().getFluid().get(localX, localZ) - : getComplex().resolveHydrologyFluid( + : context.complex().resolveHydrologyFluid( hydrology.fluidProfileKey(), context.chunkBlockX() + localX, context.chunkBlockZ() + localZ @@ -526,35 +509,87 @@ public class IrisCarveModifier extends EngineAssignedModifier int caveLavaHeight, int chunkBlockX, int chunkBlockZ, - SubterrainPlanner subterrainPlanner, - List subterrainPlans + int minimumY, + IrisComplex complex, + SubterrainPlanner.ChunkSampler subterrainSampler ) { } - private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) { + static void collectInternalWalls(CarveColumnMask[] wallMasks, CarveColumnMask[] columnMasks) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) { - CarveColumnMask columnMask = columnMasks[columnIndex]; - if (columnMask.isEmpty()) { - continue; - } - int rx = columnIndex >> 4; int rz = columnIndex & 15; - int yy = columnMask.nextSetBit(0); - while (yy >= 0) { - if (rz < 15 && !columnMasks[columnIndex + 1].contains(yy)) { - walls.put(rx, yy, rz + 1, BASIC_CAVERN); + wallMasks[columnIndex].collectWalls( + columnMasks[columnIndex], + rz > 0 ? columnMasks[columnIndex - 1] : null, + rz < 15 ? columnMasks[columnIndex + 1] : null, + rx > 0 ? columnMasks[columnIndex - 16] : null, + rx < 15 ? columnMasks[columnIndex + 16] : null + ); + } + } + + private static void paintInternalWalls(WallPainter painter, CarveColumnMask[] wallMasks, CarveColumnMask[] boundaryMasks) { + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + CarveColumnMask wallMask = wallMasks[columnIndex]; + if (wallMask.isEmpty()) { + continue; + } + int rx = columnIndex >> 4; + int rz = columnIndex & 15; + CarveColumnMask boundaryMask = boundaryMasks[columnIndex]; + for (int yy = wallMask.nextSetBit(0); yy >= 0; yy = wallMask.nextSetBit(yy + 1)) { + if (!boundaryMask.contains(yy)) { + painter.accept(rx, yy, rz, BASIC_CAVERN); } - if (rx < 15 && !columnMasks[columnIndex + 16].contains(yy)) { - walls.put(rx + 1, yy, rz, BASIC_CAVERN); - } - if (rz > 0 && !columnMasks[columnIndex - 1].contains(yy)) { - walls.put(rx, yy, rz - 1, BASIC_CAVERN); - } - if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) { - walls.put(rx - 1, yy, rz, BASIC_CAVERN); - } - yy = columnMask.nextSetBit(yy + 1); + } + } + } + + private final class WallPainter implements CarveWallBuffer.Consumer { + private final Hunk output; + private final CaveTerrainSnapshot terrain; + private final CaveInputs inputs; + private final Long2ObjectOpenHashMap caveBiomeCache; + private final Map customBiomeCache; + private final int[] surfaceHeights; + private final IrisData data; + private final int chunkBlockX; + private final int chunkBlockZ; + + private WallPainter(Hunk output, CaveTerrainSnapshot terrain, CaveInputs inputs, + Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache, + int[] surfaceHeights, IrisData data, int chunkBlockX, int chunkBlockZ) { + this.output = output; + this.terrain = terrain; + this.inputs = inputs; + this.caveBiomeCache = caveBiomeCache; + this.customBiomeCache = customBiomeCache; + this.surfaceHeights = surfaceHeights; + this.data = data; + this.chunkBlockX = chunkBlockX; + this.chunkBlockZ = chunkBlockZ; + } + + @Override + public void accept(int rx, int yy, int rz, MatterCavern cavern) { + HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz); + int worldX = rx + chunkBlockX; + int worldZ = rz + chunkBlockZ; + if (hydrology != null && hydrology.protectsPlacement() || inputs.cells.cell(worldX, yy, worldZ).owned() + || yy >= surfaceHeights[PowerOfTwoCoordinates.packLocal16(rx, rz)] || !B.isSolid(output.getRaw(rx, yy, rz))) { + return; + } + String customBiome = cavern.getCustomBiome(); + IrisBiome biome = customBiome.isEmpty() + ? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, inputs) + : resolveCustomBiome(customBiomeCache, customBiome); + if (biome == null) { + return; + } + NativeBlockState wall = biome.getWall().get(rng, worldX, yy, worldZ, data); + if (wall != null) { + output.setRaw(rx, yy, rz, wall); } } } @@ -621,10 +656,6 @@ public class IrisCarveModifier extends EngineAssignedModifier } int height = maxY + 1; - 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); @@ -633,16 +664,16 @@ public class IrisCarveModifier extends EngineAssignedModifier for (int offset = 0; offset < 16; offset++) { int index = yy * 16 + offset; if (west != null) { - tryAddBoundaryWall(ownWest[index], fromWest[index], walls, boundaryMasks, 0, yy, offset); + tryAddBoundaryWall(terrain.composedCavern(0, yy, offset), fromWest[index], walls, boundaryMasks, 0, yy, offset); } if (east != null) { - tryAddBoundaryWall(ownEast[index], fromEast[index], walls, boundaryMasks, 15, yy, offset); + tryAddBoundaryWall(terrain.composedCavern(15, yy, offset), fromEast[index], walls, boundaryMasks, 15, yy, offset); } if (north != null) { - tryAddBoundaryWall(ownNorth[index], fromNorth[index], walls, boundaryMasks, offset, yy, 0); + tryAddBoundaryWall(terrain.composedCavern(offset, yy, 0), fromNorth[index], walls, boundaryMasks, offset, yy, 0); } if (south != null) { - tryAddBoundaryWall(ownSouth[index], fromSouth[index], walls, boundaryMasks, offset, yy, 15); + tryAddBoundaryWall(terrain.composedCavern(offset, yy, 15), fromSouth[index], walls, boundaryMasks, offset, yy, 15); } } } @@ -682,7 +713,8 @@ public class IrisCarveModifier extends EngineAssignedModifier int chunkZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, - Map customBiomeCache + Map customBiomeCache, + CaveLayerMemo layers ) { if (columnMask == null || columnMask.isEmpty()) { return; @@ -697,33 +729,19 @@ public class IrisCarveModifier extends EngineAssignedModifier int rz = columnIndex & 15; int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX); int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ); - CaveZone zone = new CaveZone(); - zone.setFloor(firstHeight); - int buf = firstHeight - 1; - int y = firstHeight; - - while (y >= 0) { - if (y <= resolverState.height) { - if (y == buf + 1) { - buf = y; - zone.ceiling = buf; - } else { - if (zone.isValid(resolverState.height)) { - processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, - caveBiomeCache, customBiomeCache); - } - zone = new CaveZone(); - zone.setFloor(y); - buf = y; - } + int height = resolverState.height; + layers.reset(); + int floor = firstHeight; + while (floor >= 0 && floor <= height) { + int end = columnMask.nextClearBit(floor) - 1; + CaveZone zone = new CaveZone(); + zone.setFloor(floor); + zone.setCeiling(Math.min(end, height)); + if (zone.isValid(height)) { + processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, + caveBiomeCache, customBiomeCache, layers); } - - y = columnMask.nextSetBit(y + 1); - } - - if (zone.isValid(resolverState.height)) { - processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState, - caveBiomeCache, customBiomeCache); + floor = columnMask.nextSetBit(end + 1); } } @@ -737,7 +755,8 @@ public class IrisCarveModifier extends EngineAssignedModifier int chunkZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, - Map customBiomeCache + Map customBiomeCache, + CaveLayerMemo layers ) { int firstHeight = boundaryMask.nextSetBit(0); if (firstHeight < 0) { @@ -748,6 +767,7 @@ public class IrisCarveModifier extends EngineAssignedModifier int rz = columnIndex & 15; int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX); int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ); + layers.reset(); int zoneFloor = firstHeight; int zoneCeiling = firstHeight; int y = boundaryMask.nextSetBit(firstHeight + 1); @@ -757,7 +777,7 @@ public class IrisCarveModifier extends EngineAssignedModifier zoneCeiling = y; } else { paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, - resolverState, caveBiomeCache, customBiomeCache); + resolverState, caveBiomeCache, customBiomeCache, layers); zoneFloor = y; zoneCeiling = y; } @@ -765,7 +785,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, - resolverState, caveBiomeCache, customBiomeCache); + resolverState, caveBiomeCache, customBiomeCache, layers); } private void paintBoundaryZone( @@ -780,7 +800,8 @@ public class IrisCarveModifier extends EngineAssignedModifier int zoneCeiling, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, - Map customBiomeCache + Map customBiomeCache, + CaveLayerMemo layers ) { IrisBiome floorBiome = resolveCaveBoundaryBiome( walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ, @@ -794,9 +815,8 @@ public class IrisCarveModifier extends EngineAssignedModifier if (floorBiome != null) { 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); + IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(resolverState.dimension); + KList floorLayers = floorLayers(layers, floorBiome, worldX, worldZ, zoneFloor, resolverState); for (int i = 0; i < zoneFloor - 1; i++) { if (!floorLayers.hasIndex(i)) { break; @@ -833,8 +853,7 @@ public class IrisCarveModifier extends EngineAssignedModifier if (ceilingBiome != null) { int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); - KList ceilingLayers = ceilingBiome.generateCeilingLayers( - resolverState.dimension, worldX, worldZ, rng, 3, zoneCeiling, resolverState.data, resolverState.complex); + KList ceilingLayers = ceilingLayers(layers, ceilingBiome, worldX, worldZ, zoneCeiling, resolverState); for (int i = 0; i < ceilingLayers.size(); i++) { int ceilingY = zoneCeiling + i + 1; if (ceilingY >= worldMaxY) { @@ -879,7 +898,8 @@ public class IrisCarveModifier extends EngineAssignedModifier CaveZone zone, int rx, int rz, int xx, int zz, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, - Map customBiomeCache) { + Map customBiomeCache, + CaveLayerMemo layers) { int maxY = output.getHeight(); if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) { @@ -899,8 +919,8 @@ public class IrisCarveModifier extends EngineAssignedModifier if (floorBiome != null) { 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); + IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(resolverState.dimension); + KList floorBlocks = floorLayers(layers, floorBiome, xx, zz, zone.floor, resolverState); for (int i = 0; i < zone.floor - 1; i++) { if (!floorBlocks.hasIndex(i)) { break; @@ -933,7 +953,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } if (ceilingBiome != null) { - KList ceilingBlocks = ceilingBiome.generateCeilingLayers(resolverState.dimension, xx, zz, rng, 3, zone.ceiling, resolverState.data, resolverState.complex); + KList ceilingBlocks = ceilingLayers(layers, ceilingBiome, xx, zz, zone.ceiling, resolverState); for (int i = 0; i < ceilingBlocks.size(); i++) { int cy = zone.ceiling + i + 1; if (cy >= maxY) { @@ -961,6 +981,44 @@ public class IrisCarveModifier extends EngineAssignedModifier normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz); } + private KList floorLayers(CaveLayerMemo layers, IrisBiome biome, int worldX, int worldZ, int floorY, + CaveInputs inputs) { + if (!hasHeightFreeFloorLayers(biome)) { + return biome.generateLayers(inputs.dimension, worldX, worldZ, rng, 3, floorY, inputs.data, inputs.complex); + } + KList remembered = layers.floor(biome); + if (remembered != null) { + return remembered; + } + KList blocks = biome.generateLayers(inputs.dimension, worldX, worldZ, rng, 3, floorY, inputs.data, inputs.complex); + layers.rememberFloor(biome, blocks); + return blocks; + } + + private KList ceilingLayers(CaveLayerMemo layers, IrisBiome biome, int worldX, int worldZ, int ceilingY, + CaveInputs inputs) { + KList remembered = layers.ceiling(biome); + if (remembered != null) { + return remembered; + } + KList blocks = biome.generateCeilingLayers(inputs.dimension, worldX, worldZ, rng, 3, ceilingY, inputs.data, inputs.complex); + layers.rememberCeiling(biome, blocks); + return blocks; + } + + static boolean hasHeightFreeFloorLayers(IrisBiome biome) { + if (biome.isLockLayers()) { + return false; + } + KList layers = biome.getLayers(); + for (int index = 0; index < layers.size(); index++) { + if (!layers.get(index).getSlopeCondition().isDefault()) { + return false; + } + } + return true; + } + public void decorateNaturalCaves(int blockX, int blockZ, Hunk output, ChunkContext context) { Mantle mantle = getEngine().getMantle().getMantle(); CaveTerrainSnapshot terrain = context == null ? null : context.getCaveTerrain(); @@ -983,26 +1041,30 @@ public class IrisCarveModifier extends EngineAssignedModifier int depth = output.getDepth(); int height = output.getHeight(); ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; + SolidMemo solids = new SolidMemo(); 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); + boolean previousKnown = false; + boolean previousSolid = false; for (int y = 1; y < height; y++) { if (y > top) { break; } - NativeBlockState state = output.getRaw(localX, y, localZ); - if (B.isSolid(state)) { + boolean solid = solids.test(output.getRaw(localX, y, localZ)); + boolean decorated = false; + if (solid) { if (floor >= 0) { CaveZone zone = new CaveZone(); zone.setFloor(floor); zone.setCeiling(y - 1); SubterrainCell floorCell = zone.isValid(resolver.height) - ? subterrainCell(worldX, floor, worldZ) : null; + ? resolver.cells.cell(worldX, floor, worldZ) : null; if (floorCell != null && (floorCell.occupied() - || !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ, context))) { + || !isNaturalTerrainOpening(resolver.complex, worldX, floor, worldZ, context))) { if (caveFloorMarkers && markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) { mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR); } @@ -1012,15 +1074,19 @@ public class IrisCarveModifier extends EngineAssignedModifier worldX, worldZ, resolver, caveBiomes, customBiomes); decorateZone(output, zone, localX, localZ, worldX, worldZ, floorBiome, ceilingBiome, floorCell); + decorated = true; if (extent != null) { top = extent.highestStoredY(localX, localZ); } } floor = -1; } - } else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) { + } else if (floor < 0 && (previousKnown ? previousSolid + : solids.test(output.getRaw(localX, y - 1, localZ)))) { floor = y; } + previousSolid = solid; + previousKnown = !decorated; } } } @@ -1057,6 +1123,9 @@ public class IrisCarveModifier extends EngineAssignedModifier int worldX, int worldZ ) { + if (!terrain.hasHydrology()) { + return; + } 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++) { @@ -1090,7 +1159,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { - IrisBiome featureBiome = getEngine().getSubterrainBiome(worldX, y, worldZ); + IrisBiome featureBiome = resolverState.cells.biome(worldX, y, worldZ); if (featureBiome != null) { return featureBiome; } @@ -1127,8 +1196,8 @@ public class IrisCarveModifier extends EngineAssignedModifier } /** The dimension's underground river bed palette, or null when nothing paints. */ - private IrisRiverMaterialConfig undergroundBedMaterial() { - IrisHydrology hydrology = getDimension().getHydrology(); + private static IrisRiverMaterialConfig undergroundBedMaterial(IrisDimension dimension) { + IrisHydrology hydrology = dimension.getHydrology(); if (hydrology == null) { return null; } @@ -1256,6 +1325,9 @@ public class IrisCarveModifier extends EngineAssignedModifier } private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, CaveInputs resolverState) { + if (resolverState.captures(x, z)) { + return resolverState.resolve(x, y, z); + } long key = BlockPosition.toLong(x, y, z); IrisBiome cachedBiome = caveBiomeCache.get(key); if (cachedBiome != null) { @@ -1287,6 +1359,8 @@ public class IrisCarveModifier extends EngineAssignedModifier private final int height; private final IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); private final IrisDimensionCarvingResolver.Snapshot snapshot; + private final SubterrainChunkCells cells; + private final boolean subterrainFirst; private final int minimumY; private final DimensionStackContext stack; private final ProceduralStream surfaceBiomes; @@ -1301,6 +1375,10 @@ public class IrisCarveModifier extends EngineAssignedModifier this(engine, false, 0, 0); } + static CaveInputs forTerrainChunk(Engine engine, int chunkX, int chunkZ) { + return new CaveInputs(engine, false, chunkX, chunkZ); + } + /** * 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 @@ -1316,14 +1394,19 @@ public class IrisCarveModifier extends EngineAssignedModifier dimension = engine.getDimension(); data = engine.getData(); height = engine.getHeight(); + cells = SubterrainChunkCells.of(engine, chunkX, chunkZ); IrisContext context = IrisContext.get(); - boolean generating = engine instanceof IrisEngine irisEngine - && irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread() + IrisEngine irisEngine = engine instanceof IrisEngine candidate ? candidate : null; + boolean bound = irisEngine != null && irisEngine.hasGenerationRuntimeScope(); + boolean worker = irisEngine != null && !engine.getPlatformHooks().isMainThread(); + boolean generating = bound && worker && context != null && context.getChunkContext() != null && context.getChunkContext().getComplex() == complex; boolean natural = generating && context.getChunkContext().isNaturalTerrain(); - boolean scoped = natural || generating && contentChunk; - this.chunkLimited = !natural; + boolean unrouted = !bound && worker && !irisEngine.hasGenerationHistoryRouting(); + boolean scoped = natural || generating && contentChunk || unrouted; + this.subterrainFirst = unrouted; + this.chunkLimited = !natural && !unrouted; this.chunkX = chunkX; this.chunkZ = chunkZ; snapshot = scoped ? IrisDimensionCarvingResolver.snapshot(engine) : null; @@ -1335,10 +1418,20 @@ public class IrisCarveModifier extends EngineAssignedModifier columns = scoped ? new CaveColumn[1024] : null; } + boolean captures(int x, int z) { + return snapshot != null && (!chunkLimited || (x >> 4) == chunkX && (z >> 4) == chunkZ); + } + IrisBiome resolve(int x, int y, int z) { - if (snapshot == null || chunkLimited && ((x >> 4) != chunkX || (z >> 4) != chunkZ)) { + if (!captures(x, z)) { return engine.getCaveBiome(x, y, z, state); } + if (subterrainFirst) { + IrisBiome feature = cells.biome(x, y, z); + if (feature != null) { + return feature; + } + } IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z); if (configured != null) { return configured; @@ -1372,6 +1465,19 @@ public class IrisCarveModifier extends EngineAssignedModifier private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) { } + private static final class SolidMemo { + private NativeBlockState state; + private boolean solid; + + private boolean test(NativeBlockState candidate) { + if (candidate != state) { + state = candidate; + solid = B.isSolid(candidate); + } + return solid; + } + } + @Data public static class CaveZone { private int ceiling = -1; diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java index 6dfd76d6f..aa9ebbb9a 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java @@ -19,6 +19,8 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; @@ -30,10 +32,12 @@ import java.util.Map; final class IrisCarveScratch { final CarveColumnMask[] columnMasks = new CarveColumnMask[256]; final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256]; + final CarveColumnMask[] wallMasks = new CarveColumnMask[256]; final int[] surfaceHeights = new int[256]; final CarveWallBuffer walls = new CarveWallBuffer(512); final Map customBiomeCache = new HashMap<>(); final Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(4096); + final CaveLayerMemo layers = new CaveLayerMemo(); int[] upperSurfaceHeights; boolean customCaveBiomePresent; @@ -41,6 +45,7 @@ final class IrisCarveScratch { for (int index = 0; index < columnMasks.length; index++) { columnMasks[index] = new CarveColumnMask(); boundaryMasks[index] = new CarveColumnMask(); + wallMasks[index] = new CarveColumnMask(); } } @@ -55,6 +60,7 @@ final class IrisCarveScratch { for (int index = 0; index < columnMasks.length; index++) { columnMasks[index].clear(); boundaryMasks[index].clear(); + wallMasks[index].clear(); } walls.clear(); customBiomeCache.clear(); @@ -63,6 +69,52 @@ final class IrisCarveScratch { } } +final class CaveLayerMemo { + private static final int SLOTS = 4; + + private final Layers[] floor = new Layers[SLOTS]; + private final Layers[] ceiling = new Layers[SLOTS]; + private int floorCount; + private int ceilingCount; + + void reset() { + floorCount = 0; + ceilingCount = 0; + } + + KList floor(IrisBiome biome) { + return find(floor, floorCount, biome); + } + + KList ceiling(IrisBiome biome) { + return find(ceiling, ceilingCount, biome); + } + + void rememberFloor(IrisBiome biome, KList blocks) { + if (floorCount < SLOTS) { + floor[floorCount++] = new Layers(biome, blocks); + } + } + + void rememberCeiling(IrisBiome biome, KList blocks) { + if (ceilingCount < SLOTS) { + ceiling[ceilingCount++] = new Layers(biome, blocks); + } + } + + private static KList find(Layers[] entries, int count, IrisBiome biome) { + for (int index = 0; index < count; index++) { + if (entries[index].biome() == biome) { + return entries[index].blocks(); + } + } + return null; + } + + private record Layers(IrisBiome biome, KList blocks) { + } +} + final class CarveColumnMask { private long[] words = new long[8]; private int maxWord = -1; @@ -108,10 +160,59 @@ final class CarveColumnMask { } } + int nextClearBit(int fromBit) { + int startBit = Math.max(0, fromBit); + int wordIndex = startBit >> 6; + if (wordIndex > maxWord) { + return startBit; + } + + long word = ~words[wordIndex] & (-1L << (startBit & 63)); + while (true) { + if (word != 0L) { + return (wordIndex << 6) + Long.numberOfTrailingZeros(word); + } + + wordIndex++; + if (wordIndex > maxWord) { + return wordIndex << 6; + } + word = ~words[wordIndex]; + } + } + boolean isEmpty() { return maxWord < 0; } + void collectWalls(CarveColumnMask own, CarveColumnMask first, CarveColumnMask second, + CarveColumnMask third, CarveColumnMask fourth) { + int top = Math.max(Math.max(topWord(first), topWord(second)), Math.max(topWord(third), topWord(fourth))); + clear(); + if (top < 0) { + return; + } + if (top >= words.length) { + words = Arrays.copyOf(words, top + 1); + } + for (int index = 0; index <= top; index++) { + long walls = (word(first, index) | word(second, index) | word(third, index) | word(fourth, index)) + & ~word(own, index); + words[index] = walls; + if (walls != 0L) { + maxWord = index; + } + } + } + + private static int topWord(CarveColumnMask mask) { + return mask == null ? -1 : mask.maxWord; + } + + private static long word(CarveColumnMask mask, int index) { + return mask == null || index > mask.maxWord ? 0L : mask.words[index]; + } + boolean contains(int y) { if (y < 0) { return false; diff --git a/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java b/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java index 03548a3d8..3edb86168 100644 --- a/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java @@ -1,6 +1,7 @@ package art.arcane.iris.generation.subterrain; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -113,6 +114,10 @@ public final class SubterrainPlanner { return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), x, y, z); } + public ChunkSampler chunkSampler(int chunkX, int chunkZ) { + return new ChunkSampler(this, chunkX, chunkZ, isEmpty() ? List.of() : candidates(chunkX, chunkZ)); + } + public SubterrainCell sample(List candidates, int x, int y, int z) { SubterrainCell selected = raw(candidates, x, y, z); if (!selected.fluid()) { @@ -238,6 +243,79 @@ public final class SubterrainPlanner { } } + public static final class ChunkSampler { + private final SubterrainPlanner planner; + private final int chunkX; + private final int chunkZ; + private final List plans; + private final int[] columnMinY; + private final int[] columnMaxY; + + private ChunkSampler(SubterrainPlanner planner, int chunkX, int chunkZ, List plans) { + this.planner = planner; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.plans = plans; + if (plans.isEmpty()) { + columnMinY = null; + columnMaxY = null; + return; + } + columnMinY = new int[256]; + columnMaxY = new int[256]; + Arrays.fill(columnMinY, Integer.MAX_VALUE); + Arrays.fill(columnMaxY, Integer.MIN_VALUE); + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + for (int index = 0; index < plans.size(); index++) { + SubterrainBounds bounds = plans.get(index).bounds(); + int lowX = Math.max(bounds.minX(), baseX) - baseX; + int highX = Math.min(bounds.maxX(), baseX + 15) - baseX; + int lowZ = Math.max(bounds.minZ(), baseZ) - baseZ; + int highZ = Math.min(bounds.maxZ(), baseZ + 15) - baseZ; + for (int localX = lowX; localX <= highX; localX++) { + for (int localZ = lowZ; localZ <= highZ; localZ++) { + int column = (localX << 4) | localZ; + columnMinY[column] = Math.min(columnMinY[column], bounds.minY()); + columnMaxY[column] = Math.max(columnMaxY[column], bounds.maxY()); + } + } + } + } + + public boolean isEmpty() { + return plans.isEmpty(); + } + + public SubterrainCell sample(int x, int y, int z) { + if ((x >> 4) != chunkX || (z >> 4) != chunkZ) { + return planner.sample(x, y, z); + } + if (plans.isEmpty() || y < planner.minY || y >= planner.maxYExclusive) { + return SubterrainCell.OUTSIDE; + } + return sampleColumn(x, y, z); + } + + public SubterrainCell sampleCandidates(int x, int y, int z) { + if (plans.isEmpty()) { + return SubterrainCell.OUTSIDE; + } + if ((x >> 4) != chunkX || (z >> 4) != chunkZ) { + return planner.sample(plans, x, y, z); + } + return sampleColumn(x, y, z); + } + + private SubterrainCell sampleColumn(int x, int y, int z) { + int column = ((x & 15) << 4) | (z & 15); + if (y < columnMinY[column] || y > columnMaxY[column]) { + return SubterrainCell.OUTSIDE; + } + return planner.sample(plans, x, y, z); + } + } + private record CandidateTile(int chunkX, int chunkZ, List plans) { } 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 index 8712b3082..aacd00b00 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java @@ -57,10 +57,20 @@ public class CaveTerrainSnapshotTest { 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); + assertEquals(height * 16, expected.length); for (int index = 0; index < expected.length; index++) { - assertSame(face + " " + index, expected[index], actual[index]); + int offset = index & 15; + int x = switch (face) { + case WEST -> 0; + case EAST -> 15; + case NORTH, SOUTH -> offset; + }; + int z = switch (face) { + case NORTH -> 0; + case SOUTH -> 15; + case WEST, EAST -> offset; + }; + assertSame(face + " " + index, expected[index], snapshot.composedCavern(x, index >> 4, z)); } } } 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 5acfa3fb2..dbacdce86 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 @@ -9,6 +9,7 @@ import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EnginePlatformHooks; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.SubterrainChunkCells; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry; import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; @@ -148,6 +149,41 @@ public class IrisCarveModifierCaveInputsTest { } } + @Test + public void unroutedWorkerCapturesColumnsLikeTheEngineIncludingSubterrainBiomes() { + try (Fixture fixture = new Fixture()) { + when(fixture.engine.hasGenerationRuntimeScope()).thenReturn(false); + when(fixture.engine.hasGenerationHistoryRouting()).thenReturn(false); + doReturn(SubterrainChunkCells.delegated(fixture.engine)).when(fixture.engine).subterrainChunkCells(anyInt(), anyInt()); + IrisBiome feature = biome("feature", 0); + doReturn(feature).when(fixture.engine).getSubterrainBiome(-20, 33, 18); + doReturn(feature).when(fixture.scalar).getSubterrainBiome(-20, 33, 18); + IrisCarveModifier.CaveInputs inputs = IrisCarveModifier.CaveInputs.forContentChunk(fixture.engine, -2, 1); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + for (int x = -35; x <= -13; x++) { + for (int z = 13; z <= 35; z += 2) { + for (int y : new int[]{0, 30, 33, 34, 39, 40}) { + assertSame(x + "," + y + "," + z, fixture.scalar.getCaveBiome(x, y, z, state), inputs.resolve(x, y, z)); + } + } + } + assertSame(feature, inputs.resolve(-20, 33, 18)); + assertTrue(fixture.samples.get() > 0); + } + } + + @Test + public void routedOrBoundUnscopedEnginesKeepCoordinateDispatch() { + try (Fixture fixture = new Fixture()) { + when(fixture.engine.hasGenerationRuntimeScope()).thenReturn(false); + when(fixture.engine.hasGenerationHistoryRouting()).thenReturn(true); + IrisBiome historical = biome("historical", 0); + when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))).thenReturn(historical); + assertSame(historical, IrisCarveModifier.CaveInputs.forTerrainChunk(fixture.engine, -2, 1).resolve(-20, 20, 18)); + assertEquals(0, 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 d0eb081fb..e19f5e393 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 @@ -67,7 +67,8 @@ public class IrisCarveModifierInferenceIsolationTest { int.class, IrisCarveModifier.CaveInputs.class, Long2ObjectOpenHashMap.class, - Map.class + Map.class, + CaveLayerMemo.class ); paintBoundaryZone.setAccessible(true); paintBoundaryZone.invoke( @@ -83,7 +84,8 @@ public class IrisCarveModifierInferenceIsolationTest { 1, new IrisCarveModifier.CaveInputs(engine), new Long2ObjectOpenHashMap(), - customBiomes + customBiomes, + new CaveLayerMemo() ); IrisBiome fallback = new IrisBiome(); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierLayerMemoTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierLayerMemoTest.java new file mode 100644 index 000000000..11c52fe98 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierLayerMemoTest.java @@ -0,0 +1,95 @@ +package art.arcane.iris.generation.stage; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomePaletteLayer; +import art.arcane.iris.generation.biome.IrisSurfaceLayerFallback; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisSlopeClip; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.noise.CNG; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +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.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisCarveModifierLayerMemoTest { + private static final Map BLOCKS = new HashMap<>(); + + @Test + public void heightFreeFloorLayersIgnoreTheZoneFloor() { + IrisData data = mock(IrisData.class); + IrisComplex complex = mock(IrisComplex.class); + when(complex.hasTerrain3D()).thenReturn(true); + when(complex.terrainSurfaceSlope(anyInt(), anyInt(), anyInt())).thenAnswer(call -> (double) (int) call.getArgument(1)); + RNG rng = new RNG(31L); + IrisBiomePaletteLayer top = layer(data, 2, new IrisSlopeClip()); + IrisBiomePaletteLayer under = layer(data, 3, new IrisSlopeClip()); + IrisBiome biome = new IrisBiome().setLayers(new KList().qadd(top).qadd(under)); + IrisDimension dimension = new IrisDimension().setSurfaceLayerFallback(IrisSurfaceLayerFallback.TOP_LAYER); + + assertTrue(IrisCarveModifier.hasHeightFreeFloorLayers(biome)); + KList reference = biome.generateLayers(dimension, 14D, -9D, rng, 3, 0, data, complex); + for (int floor = -64; floor < 320; floor += 7) { + assertEquals(reference, biome.generateLayers(dimension, 14D, -9D, rng, 3, floor, data, complex)); + } + + biome.setLockLayers(true); + assertFalse(IrisCarveModifier.hasHeightFreeFloorLayers(biome)); + biome.setLockLayers(false); + biome.setLayers(new KList().qadd(top).qadd(layer(data, 1, new IrisSlopeClip(0D, 2D)))); + assertFalse(IrisCarveModifier.hasHeightFreeFloorLayers(biome)); + } + + @Test + public void memoAnswersOnlyTheBiomesOfTheCurrentColumn() { + CaveLayerMemo memo = new CaveLayerMemo(); + IrisBiome first = new IrisBiome(); + IrisBiome second = new IrisBiome(); + KList floor = new KList<>(); + KList ceiling = new KList<>(); + memo.rememberFloor(first, floor); + memo.rememberCeiling(second, ceiling); + + assertSame(floor, memo.floor(first)); + assertNull(memo.floor(second)); + assertSame(ceiling, memo.ceiling(second)); + assertNull(memo.ceiling(first)); + memo.reset(); + assertNull(memo.floor(first)); + assertNull(memo.ceiling(second)); + } + + private static IrisBiomePaletteLayer layer(IrisData data, int thickness, IrisSlopeClip slope) { + IrisBiomePaletteLayer layer = mock(IrisBiomePaletteLayer.class); + CNG height = mock(CNG.class); + when(layer.getZoom()).thenReturn(1D); + when(layer.getMinHeight()).thenReturn(thickness); + when(layer.getMaxHeight()).thenReturn(thickness); + when(layer.getSlopeCondition()).thenReturn(slope); + when(layer.getHeightGenerator(any(RNG.class), same(data))).thenReturn(height); + when(height.fit(anyInt(), anyInt(), anyDouble(), anyDouble())).thenReturn(thickness); + when(layer.get(any(RNG.class), anyInt(), anyDouble(), anyDouble(), anyDouble(), same(data))) + .thenAnswer(call -> block((int) call.getArgument(1))); + return layer; + } + + private static NativeBlockState block(int signature) { + return BLOCKS.computeIfAbsent(signature, key -> mock(NativeBlockState.class)); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierZoneParityTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierZoneParityTest.java index cb10fce0f..031067cab 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierZoneParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierZoneParityTest.java @@ -112,39 +112,15 @@ public class IrisCarveModifierZoneParityTest { private List bitsetZones(CarveColumnMask columnMask, int maxHeight) { List zones = new ArrayList<>(); - int firstHeight = columnMask.nextSetBit(0); - if (firstHeight < 0) { - return zones; - } - - int floor = firstHeight; - int ceiling = -1; - int buf = firstHeight - 1; - int y = firstHeight; - while (y >= 0) { - if (y >= 0 && y <= maxHeight) { - if (y == buf + 1) { - buf = y; - ceiling = buf; - } else if (isValidZone(floor, ceiling, maxHeight)) { - zones.add(zoneKey(floor, ceiling)); - floor = y; - ceiling = -1; - buf = y; - } else { - floor = y; - ceiling = -1; - buf = y; - } + int floor = columnMask.nextSetBit(0); + while (floor >= 0 && floor <= maxHeight) { + int end = columnMask.nextClearBit(floor) - 1; + int ceiling = Math.min(end, maxHeight); + if (isValidZone(floor, ceiling, maxHeight)) { + zones.add(zoneKey(floor, ceiling)); } - - y = columnMask.nextSetBit(y + 1); + floor = columnMask.nextSetBit(end + 1); } - - if (isValidZone(floor, ceiling, maxHeight)) { - zones.add(zoneKey(floor, ceiling)); - } - return zones; } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java index bfe93fb0c..9a6fa61b4 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java @@ -5,7 +5,10 @@ import art.arcane.volmlib.util.matter.MatterCavern; import org.junit.Test; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Random; +import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -65,6 +68,54 @@ public class IrisCarveScratchTest { assertEquals(0, wallCount[0]); } + @Test + public void maskWallsMatchTheNeighborScanOfCarvedCells() { + Random random = new Random(52_117L); + for (int round = 0; round < 40; round++) { + CarveColumnMask[] columns = new CarveColumnMask[256]; + CarveColumnMask[] walls = new CarveColumnMask[256]; + for (int index = 0; index < 256; index++) { + columns[index] = new CarveColumnMask(); + walls[index] = new CarveColumnMask(); + walls[index].add(random.nextInt(700)); + } + int cells = random.nextInt(4000); + int height = 64 + random.nextInt(700); + for (int cell = 0; cell < cells; cell++) { + columns[random.nextInt(256)].add(1 + random.nextInt(height - 1)); + } + Set expected = new HashSet<>(); + for (int index = 0; index < 256; index++) { + int rx = index >> 4; + int rz = index & 15; + for (int y = columns[index].nextSetBit(0); y >= 0; y = columns[index].nextSetBit(y + 1)) { + if (rz < 15 && !columns[index + 1].contains(y)) { + expected.add(key(rx, y, rz + 1)); + } + if (rx < 15 && !columns[index + 16].contains(y)) { + expected.add(key(rx + 1, y, rz)); + } + if (rz > 0 && !columns[index - 1].contains(y)) { + expected.add(key(rx, y, rz - 1)); + } + if (rx > 0 && !columns[index - 16].contains(y)) { + expected.add(key(rx - 1, y, rz)); + } + } + } + IrisCarveModifier.collectInternalWalls(walls, columns); + Set actual = new HashSet<>(); + for (int index = 0; index < 256; index++) { + for (int y = walls[index].nextSetBit(0); y >= 0; y = walls[index].nextSetBit(y + 1)) { + actual.add(key(index >> 4, y, index & 15)); + assertFalse(columns[index].contains(y)); + } + assertEquals(walls[index].nextSetBit(0) < 0, walls[index].isEmpty()); + } + assertEquals(expected, actual); + } + } + private static String key(int x, int y, int z) { return x + ":" + y + ":" + z; } 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 1144815d2..72f4e1a51 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 @@ -376,6 +376,7 @@ public class IrisNaturalCaveScanTest { chunk = mock(MantleChunk.class); doReturn(engine).when(modifier).getEngine(); doReturn(complex).when(modifier).getComplex(); + doReturn(complex).when(engine).getComplex(); doReturn(32).when(engine).getHeight(); doReturn(new SeedManager(1337L)).when(engine).getSeedManager(); doReturn(engineMantle).when(engine).getMantle(); diff --git a/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java index 6cb0f2535..5e6bf4e97 100644 --- a/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java @@ -638,6 +638,48 @@ public class SubterrainPlannerTest { assertTrue(original.sample(anchor.x(), anchor.y(), anchor.z()).occupied()); } + @Test + public void chunkSamplersMatchCoordinateSamplingInsideAndAroundTheirChunk() { + for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) { + int length = family == IrisSubterrainFamily.TECTONIC_FAULT ? 208 : 64; + SubterrainPlanner planner = planner(compactFeature(family).setLength(length).setProbability(0.6)); + SubterrainPlanner reference = planner(compactFeature(family).setLength(length).setProbability(0.6)); + SubterrainPlan plan = first(planner); + int centerChunkX = plan.centerX() >> 4; + int centerChunkZ = plan.centerZ() >> 4; + int occupied = 0; + int emptyChunks = 0; + for (int chunkX = centerChunkX - 3; chunkX <= centerChunkX + 3; chunkX += 2) { + for (int chunkZ = centerChunkZ - 3; chunkZ <= centerChunkZ + 3; chunkZ += 2) { + SubterrainPlanner.ChunkSampler sampler = planner.chunkSampler(chunkX, chunkZ); + List chunkPlans = reference.plansForBounds((chunkX << 4) - 1, (chunkZ << 4) - 1, + (chunkX << 4) + 16, (chunkZ << 4) + 16); + if (sampler.isEmpty()) { + emptyChunks++; + } + for (int x = (chunkX << 4) - 3; x < (chunkX << 4) + 19; x++) { + for (int z = (chunkZ << 4) - 3; z < (chunkZ << 4) + 19; z++) { + for (int y = -66; y <= 99; y++) { + SubterrainCell expected = reference.sample(x, y, z); + assertEquals(family + " " + x + "," + y + "," + z, expected, sampler.sample(x, y, z)); + assertEquals(family + " candidates " + x + "," + y + "," + z, + reference.sample(chunkPlans, x, y, z), sampler.sampleCandidates(x, y, z)); + if (expected.occupied()) { + occupied++; + } + } + } + } + } + } + assertTrue(family + " occupied", occupied > 100); + assertTrue(family + " empty chunks", emptyChunks < 16); + } + SubterrainPlanner empty = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(), 7123, -64, 97, (x, z) -> "test-region")); + assertTrue(empty.chunkSampler(3, -5).isEmpty()); + assertEquals(SubterrainCell.OUTSIDE, empty.chunkSampler(3, -5).sample(50, 10, -70)); + } + @Test(expected = IllegalArgumentException.class) public void duplicateIdentifiersAreRejected() { IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);