From 7c4a5e193749f13871b4c7d2342dfab72ddf47e9 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 19:26:52 -0400 Subject: [PATCH 1/2] Added cave cell kernel - Decide whole 8x8 adaptive cells as solid or open from lattice corner bounds with an ulp guard - Evaluate only mixed cells per column with the original bilinear arithmetic - Share the row kernel between serial and parallel adaptive classification - Count solid, open and mixed cells and exact rows, printed by the generation probe - Pin carve write sequences for eight carver configurations with golden hashes --- .../generation/mantle/CaveCarveScratch.java | 19 + .../generation/mantle/CaveCellKernel.java | 33 ++ .../generation/mantle/IrisCaveCarver3D.java | 472 +++++++++++++++++- .../generation/mantle/CaveCellKernelTest.java | 106 ++++ .../IrisCaveCarver3DCellKernelTest.java | 370 ++++++++++++++ .../arcane/iris/probe/GenerationProbe.java | 2 + 6 files changed, 998 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java create mode 100644 core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java create mode 100644 core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java index dd456abf5..6f6bf8c07 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java @@ -66,6 +66,24 @@ final class CaveCarveScratch { final int[] tileLocalX = new int[4]; final int[] tileLocalZ = new int[4]; final int[] tileTopY = new int[4]; + final long[] rowCarveBits = new long[4]; + final long[] cellPassBits = new long[4]; + final double[] rowLatticeDensity = new double[9]; + final CaveDensityGrid.Column[] cellCornerColumns = new CaveDensityGrid.Column[9]; + final double[] cellThresholdMin = new double[4]; + final double[] cellThresholdMax = new double[4]; + final double[] cellThresholdMaxBoost = new double[4]; + final int[] cellBreakFloorMin = new int[4]; + final int[] cellCeilingFadeStart = new int[4]; + final int[] cellTopY = new int[4]; + final boolean[] cellProtected = new boolean[4]; + final int[] columnProtectStartY = new int[256]; + final int[] columnProtectEndY = new int[256]; + int[] rowActiveCount = new int[0]; + long solidCells; + long openCells; + long mixedCells; + long exactRows; CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0]; double[] activeModuleRemainingMin = new double[0]; double[] activeModuleRemainingMax = new double[0]; @@ -84,5 +102,6 @@ final class CaveCarveScratch { Arrays.fill(sectionMatter, null); Arrays.fill(sectionSlices, null); Arrays.fill(densityColumns, null); + Arrays.fill(cellCornerColumns, null); } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java new file mode 100644 index 000000000..6eb42c753 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java @@ -0,0 +1,33 @@ +package art.arcane.iris.generation.mantle; + +final class CaveCellKernel { + static final int NONE = 0; + static final int ALL = 1; + static final int MIXED = 2; + private static final double GUARD_ULPS = 32D; + private static final double MAXIMUM_MAGNITUDE = 0x1p500; + + private CaveCellKernel() { + } + + static int decide(double d00, double d01, double d10, double d11, double thresholdLow, double thresholdHigh) { + double low = Math.min(Math.min(d00, d01), Math.min(d10, d11)); + double high = Math.max(Math.max(d00, d01), Math.max(d10, d11)); + double magnitude = Math.max(Math.abs(low), Math.abs(high)); + if (!(magnitude < MAXIMUM_MAGNITUDE)) { + return MIXED; + } + double guard = Math.ulp(magnitude) * GUARD_ULPS; + if (high + guard <= thresholdLow) { + return ALL; + } + if (low - guard > thresholdHigh) { + return NONE; + } + return MIXED; + } + + static double threshold(double localThreshold, double normalizationFactor, double inverseNormalization) { + return (localThreshold * normalizationFactor) * inverseNormalization; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java index 8bce7fdc5..a1b51914f 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java @@ -43,6 +43,7 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.LongAdder; public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; @@ -62,6 +63,11 @@ public class IrisCaveCarver3D { private static final int MAXIMUM_DENSITY_WORKERS = 4; private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17}; private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024; + private static final int[] CELL_CORNER_MASKS = {0b000011011, 0b000110110, 0b011011000, 0b110110000}; + private static final LongAdder SOLID_CELLS = new LongAdder(); + private static final LongAdder OPEN_CELLS = new LongAdder(); + private static final LongAdder MIXED_CELLS = new LongAdder(); + private static final LongAdder EXACT_ROWS = new LongAdder(); private final Engine engine; private final IrisData data; @@ -155,6 +161,10 @@ public class IrisCaveCarver3D { densityMaximumY = engine.getWorld().getHeight() - 1; } + public static CellCounters cellCounters() { + return new CellCounters(SOLID_CELLS.sum(), OPEN_CELLS.sum(), MIXED_CELLS.sum(), EXACT_ROWS.sum()); + } + public int carve(MantleWriter writer, int chunkX, int chunkZ) { CaveCarveScratch scratch = scratchCache.get(); if (!scratch.fullWeightsInitialized) { @@ -504,7 +514,9 @@ public class IrisCaveCarver3D { int minimumY = pass.minimumY() + index * rows; int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1); if (minimumY <= maximumY) { - tasks.add(() -> classifyRows(pass, minimumY, maximumY, classified)); + tasks.add(pass.cellKernel() + ? () -> classifyCellRows(pass, minimumY, maximumY, classified) + : () -> classifyRows(pass, minimumY, maximumY, classified)); } } runDensityTasks(tasks, pool); @@ -697,10 +709,25 @@ public class IrisCaveCarver3D { int[] columns, int[] columnTops, int columnCount, double[] thresholds, long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors, double surfaceBreakBoost, double[] verticalFade, CaveCarveScratch sourceScratch, - int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY + int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY, boolean cellKernel ) { } + public record CellCounters(long solidCells, long openCells, long mixedCells, long exactRows) { + public long decided() { + return solidCells + openCells; + } + + public long evaluated() { + return mixedCells; + } + + public String machineLine() { + return "IRIS_GENPROBE_CARVE_CELLS solid=" + solidCells + " open=" + openCells + " mixed=" + mixedCells + + " exact_rows=" + exactRows; + } + } + private static final class DensityTask implements Runnable, ForkJoinPool.ManagedBlocker { private final Runnable work; private final AtomicBoolean claimed = new AtomicBoolean(); @@ -795,7 +822,7 @@ public class IrisCaveCarver3D { byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass( x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount, passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY, - surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY)); + surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY, false)); for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex)); @@ -944,12 +971,23 @@ public class IrisCaveCarver3D { adaptiveSampleStep, effectiveAdaptiveSampleStep ); + boolean cellKernel = !skipExistingCarved && usesCellKernel(effectiveAdaptiveSampleStep); + if (cellKernel) { + prepareCellPlan(scratch, x0, z0, minY, passMaxY, activeColumnIndices, activeColumnCount, + surfaceFluidBoundaries, surfaceBreakThresholdBoost); + } byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass( x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount, passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY, surfaceBreakThresholdBoost, verticalEdgeFade, scratch, effectiveAdaptiveSampleStep, effectiveAdaptiveThresholdMargin, - fluidMaxY, matterByY)); + fluidMaxY, matterByY, cellKernel)); + + if (cellKernel) { + return emitCellPass(chunk, scratch, classified, x0, z0, minY, passMaxY, minSection, maxSection, + activeColumnIndices, activeColumnTopY, activeColumnCount, surfaceFluidBoundaries, + surfaceBreakThresholdBoost, verticalEdgeFade, fluidMaxY, matterByY, fluidSupportPlan); + } for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex)); @@ -1059,6 +1097,432 @@ public class IrisCaveCarver3D { return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST); } + private boolean usesCellKernel(int effectiveAdaptiveSampleStep) { + return effectiveAdaptiveSampleStep == ADAPTIVE_DEEP_SAMPLE_STEP + && verticalDensityStep == 1 + && parallelDensity + && (hasWarp || !hasModules); + } + + private void prepareCellPlan( + CaveCarveScratch plan, + int x0, + int z0, + int minY, + int passMaxY, + int[] activeColumnIndices, + int activeColumnCount, + long[] surfaceFluidBoundaries, + double surfaceBreakThresholdBoost + ) { + int rows = passMaxY - minY + 1; + if (plan.rowActiveCount.length < rows + 1) { + plan.rowActiveCount = new int[rows + 1]; + } + int[] rowActiveCount = plan.rowActiveCount; + Arrays.fill(rowActiveCount, 0, rows + 1, 0); + Arrays.fill(plan.cellPassBits, 0L); + Arrays.fill(plan.cellThresholdMin, Double.POSITIVE_INFINITY); + Arrays.fill(plan.cellThresholdMax, Double.NEGATIVE_INFINITY); + Arrays.fill(plan.cellThresholdMaxBoost, Double.NEGATIVE_INFINITY); + Arrays.fill(plan.cellBreakFloorMin, Integer.MAX_VALUE); + Arrays.fill(plan.cellCeilingFadeStart, Integer.MAX_VALUE); + Arrays.fill(plan.cellTopY, Integer.MIN_VALUE); + Arrays.fill(plan.cellProtected, false); + prepareAdaptiveGeometry(plan, ADAPTIVE_DEEP_SAMPLE_STEP, 2, 3); + double[] passThreshold = plan.passThreshold; + for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) { + int column = activeColumnIndices[activeIndex]; + int cell = ((column >> 7) << 1) | ((column >> 3) & 1); + int top = plan.columnMaxY[column]; + double threshold = passThreshold[column]; + plan.cellPassBits[column >> 6] |= 1L << column; + plan.cellThresholdMin[cell] = Math.min(plan.cellThresholdMin[cell], threshold); + plan.cellThresholdMax[cell] = Math.max(plan.cellThresholdMax[cell], threshold); + if (plan.surfaceBreakColumn[column]) { + plan.cellThresholdMaxBoost[cell] = Math.max(plan.cellThresholdMaxBoost[cell], threshold + surfaceBreakThresholdBoost); + plan.cellBreakFloorMin[cell] = Math.min(plan.cellBreakFloorMin[cell], plan.surfaceBreakFloorY[column]); + } else { + plan.cellThresholdMaxBoost[cell] = Math.max(plan.cellThresholdMaxBoost[cell], threshold); + } + if (plan.surfaceCeilingColumn[column]) { + plan.cellCeilingFadeStart[cell] = Math.min(plan.cellCeilingFadeStart[cell], top - SURFACE_CEILING_FADE_DEPTH + 1); + } + plan.cellTopY[cell] = Math.max(plan.cellTopY[cell], top); + int protectStart = SurfaceFluidBoundaryPlan.NO_BOUNDARY; + int protectEnd = Integer.MIN_VALUE; + if (surfaceFluidBoundaries != null) { + long boundary = surfaceFluidBoundaries[column]; + protectStart = SurfaceFluidBoundaryPlan.startY(boundary); + protectEnd = SurfaceFluidBoundaryPlan.endY(boundary); + } + plan.columnProtectStartY[column] = protectStart; + plan.columnProtectEndY[column] = protectEnd; + rowActiveCount[0]++; + rowActiveCount[top - minY + 1]--; + int protectedFrom = Math.max(protectStart, minY); + int protectedTo = Math.min(protectEnd, top); + if (protectedFrom <= protectedTo) { + rowActiveCount[protectedFrom - minY]--; + rowActiveCount[protectedTo - minY + 1]++; + plan.cellProtected[cell] = true; + } + } + for (int row = 1; row < rows; row++) { + rowActiveCount[row] += rowActiveCount[row - 1]; + } + int latticeX = x0 >> CaveDensityGrid.LATTICE_SHIFT; + int latticeZ = z0 >> CaveDensityGrid.LATTICE_SHIFT; + int usedCorners = 0; + for (int cell = 0; cell < 4; cell++) { + if (plan.cellTopY[cell] >= minY) { + usedCorners |= CELL_CORNER_MASKS[cell]; + } + } + for (int corner = 0; corner < 9; corner++) { + plan.cellCornerColumns[corner] = (usedCorners & (1 << corner)) == 0 ? null + : densityGrid.column(this, densityGridOwner, densityMaximumY + 1, + latticeX + (corner / 3), latticeZ + (corner % 3)); + } + } + + private int emitCellPass( + MantleChunk chunk, + CaveCarveScratch scratch, + byte[] classified, + int x0, + int z0, + int minY, + int passMaxY, + int minSection, + int maxSection, + int[] activeColumnIndices, + int[] activeColumnTopY, + int activeColumnCount, + long[] surfaceFluidBoundaries, + double surfaceBreakThresholdBoost, + double[] verticalEdgeFade, + int[] fluidMaxY, + MatterCavern[] matterByY, + CaveFluidSupportPlan fluidSupportPlan + ) { + int carved = 0; + long[] rowBits = scratch.rowCarveBits; + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex)); + int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15); + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex); + for (int y = sectionMinY; y <= sectionMaxY; y++) { + int fadeIndex = y - minY; + if (scratch.rowActiveCount[fadeIndex] == 0) { + continue; + } + MatterCavern verticalMatter = matterByY[fadeIndex]; + if (classified != null) { + carved += emitClassifiedRow(cavernSlice, classified, fadeIndex, y, verticalMatter, fluidSupportPlan); + continue; + } + classifyAdaptiveRow(scratch, scratch, x0, z0, y, minY, activeColumnIndices, activeColumnTopY, + activeColumnCount, surfaceFluidBoundaries, surfaceBreakThresholdBoost, verticalEdgeFade, rowBits); + for (int word = 0; word < 4; word++) { + long bits = rowBits[word]; + while (bits != 0L) { + int column = (word << 6) | Long.numberOfTrailingZeros(bits); + bits &= bits - 1L; + int localX = PowerOfTwoCoordinates.unpackLocal16X(column); + int localZ = column & 15; + MatterCavern matter = verticalMatter; + if (verticalMatter != carveLava && y <= fluidMaxY[column]) { + matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ, column, fluidMaxY, + cellThreshold(scratch, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade)); + } + writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); + carved++; + } + } + } + } + flushCellCounters(scratch); + return carved; + } + + private int emitClassifiedRow(MatterSlice cavernSlice, byte[] classified, int fadeIndex, int y, + MatterCavern verticalMatter, CaveFluidSupportPlan fluidSupportPlan) { + int carved = 0; + int offset = fadeIndex * 256; + for (int column = 0; column < 256; column++) { + byte flags = classified[offset + column]; + if ((flags & DENSITY_CARVE) == 0) { + continue; + } + MatterCavern matter = (flags & DENSITY_FLUID) != 0 ? carveFluid : verticalMatter; + writeCavern(cavernSlice, PowerOfTwoCoordinates.unpackLocal16X(column), y, column & 15, matter, fluidSupportPlan); + carved++; + } + return carved; + } + + private void classifyCellRows(DensityPass pass, int minimumY, int maximumY, byte[] classified) { + CaveCarveScratch scratch = new CaveCarveScratch(); + CaveCarveScratch plan = pass.sourceScratch(); + long[] rowBits = scratch.rowCarveBits; + for (int y = minimumY; y <= maximumY; y++) { + int fadeIndex = y - pass.minimumY(); + if (plan.rowActiveCount[fadeIndex] == 0) { + continue; + } + classifyAdaptiveRow(scratch, plan, pass.x(), pass.z(), y, pass.minimumY(), pass.columns(), pass.columnTops(), + pass.columnCount(), pass.fluidBoundaries(), pass.surfaceBreakBoost(), pass.verticalFade(), rowBits); + MatterCavern verticalMatter = pass.matterByY()[fadeIndex]; + int offset = fadeIndex * 256; + for (int word = 0; word < 4; word++) { + long bits = rowBits[word]; + while (bits != 0L) { + int column = (word << 6) | Long.numberOfTrailingZeros(bits); + bits &= bits - 1L; + MatterCavern matter = verticalMatter; + if (verticalMatter != carveLava && y <= pass.fluidTops()[column]) { + matter = resolveMatter(scratch, verticalMatter, pass.x() + PowerOfTwoCoordinates.unpackLocal16X(column), + y, pass.z() + (column & 15), column, pass.fluidTops(), + cellThreshold(plan, column, y, pass.minimumY(), pass.surfaceBreakBoost(), pass.verticalFade())); + } + classified[offset + column] = matter == carveFluid ? DENSITY_CARVE | DENSITY_FLUID : DENSITY_CARVE; + } + } + } + flushCellCounters(scratch); + } + + private void classifyAdaptiveRow( + CaveCarveScratch scratch, + CaveCarveScratch plan, + int x0, + int z0, + int y, + int minY, + int[] activeColumnIndices, + int[] activeColumnTopY, + int activeColumnCount, + long[] surfaceFluidBoundaries, + double surfaceBreakThresholdBoost, + double[] verticalEdgeFade, + long[] rowBits + ) { + Arrays.fill(rowBits, 0L); + if (plan.rowActiveCount[y - minY] >= ADAPTIVE_MIN_PLANE_COLUMNS) { + classifyCellRow(scratch, plan, x0, z0, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, rowBits); + return; + } + int[] planeColumnIndices = scratch.planeColumnIndices; + double[] planeThresholdLimit = scratch.planeThresholdLimit; + boolean[] planeCarve = scratch.planeCarve; + int planeCount = 0; + for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) { + if (activeColumnTopY[activeIndex] < y) { + continue; + } + int columnIndex = activeColumnIndices[activeIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) { + continue; + } + planeColumnIndices[planeCount] = columnIndex; + planeThresholdLimit[planeCount] = cellLocalThreshold(plan, columnIndex, y, minY, surfaceBreakThresholdBoost, + verticalEdgeFade) * normalizationFactor; + planeCount++; + } + classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + if (planeCarve[planeIndex]) { + int columnIndex = planeColumnIndices[planeIndex]; + rowBits[columnIndex >> 6] |= 1L << columnIndex; + } + } + scratch.exactRows++; + } + + private void classifyCellRow( + CaveCarveScratch scratch, + CaveCarveScratch plan, + int x0, + int z0, + int y, + int minY, + double surfaceBreakThresholdBoost, + double[] verticalEdgeFade, + long[] rowBits + ) { + int activeCells = 0; + int corners = 0; + for (int cell = 0; cell < 4; cell++) { + if (isCellActive(plan, cell, y)) { + activeCells |= 1 << cell; + corners |= CELL_CORNER_MASKS[cell]; + } + } + if (activeCells == 0) { + return; + } + double[] density = scratch.rowLatticeDensity; + for (int corner = 0; corner < 9; corner++) { + if ((corners & (1 << corner)) != 0) { + density[corner] = cornerDensity(scratch, plan.cellCornerColumns[corner], + x0 + (corner / 3) * ADAPTIVE_DEEP_SAMPLE_STEP, y, z0 + (corner % 3) * ADAPTIVE_DEEP_SAMPLE_STEP); + } + } + int fadeIndex = y - minY; + double fade = verticalEdgeFade[fadeIndex]; + double closure = plan.surfaceClosureThreshold[fadeIndex]; + for (int cell = 0; cell < 4; cell++) { + if ((activeCells & (1 << cell)) == 0) { + continue; + } + int corner = ((cell >> 1) * 3) + (cell & 1); + double d00 = density[corner]; + double d01 = density[corner + 1]; + double d10 = density[corner + 3]; + double d11 = density[corner + 4]; + double low = plan.cellThresholdMin[cell] - fade; + if (y >= plan.cellCeilingFadeStart[cell]) { + low = Math.min(low, closure); + } + double high = (y >= plan.cellBreakFloorMin[cell] ? plan.cellThresholdMaxBoost[cell] : plan.cellThresholdMax[cell]) - fade; + int decision = CaveCellKernel.decide(d00, d01, d10, d11, + CaveCellKernel.threshold(low, normalizationFactor, inverseNormalization), + CaveCellKernel.threshold(high, normalizationFactor, inverseNormalization)); + if (decision == CaveCellKernel.NONE) { + scratch.solidCells++; + } else if (decision == CaveCellKernel.ALL) { + scratch.openCells++; + markActiveCellColumns(plan, cell, y, rowBits); + } else { + scratch.mixedCells++; + classifyMixedCell(plan, cell, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, d00, d01, d10, d11, rowBits); + } + } + } + + private void classifyMixedCell( + CaveCarveScratch plan, + int cell, + int y, + int minY, + double surfaceBreakThresholdBoost, + double[] verticalEdgeFade, + double d00, + double d01, + double d10, + double d11, + long[] rowBits + ) { + double[] adaptiveTx = plan.adaptiveTx; + double[] adaptiveTz = plan.adaptiveTz; + int firstLocalX = (cell >> 1) << 3; + int firstLocalZ = (cell & 1) << 3; + for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) { + int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ); + for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) { + if (!isCellColumnActive(plan, column, y)) { + continue; + } + double tx = adaptiveTx[column]; + double tz = adaptiveTz[column]; + double dx0 = d00 + ((d10 - d00) * tx); + double dx1 = d01 + ((d11 - d01) * tx); + double predictedDensity = dx0 + ((dx1 - dx0) * tz); + if (predictedDensity <= cellThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade)) { + rowBits[column >> 6] |= 1L << column; + } + } + } + } + + private static void markActiveCellColumns(CaveCarveScratch plan, int cell, int y, long[] rowBits) { + int firstLocalX = (cell >> 1) << 3; + int firstLocalZ = (cell & 1) << 3; + for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) { + int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ); + for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) { + if (isCellColumnActive(plan, column, y)) { + rowBits[column >> 6] |= 1L << column; + } + } + } + } + + private static boolean isCellActive(CaveCarveScratch plan, int cell, int y) { + if (y > plan.cellTopY[cell]) { + return false; + } + if (!plan.cellProtected[cell]) { + return true; + } + int firstLocalX = (cell >> 1) << 3; + int firstLocalZ = (cell & 1) << 3; + for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) { + int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ); + for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) { + if (isCellColumnActive(plan, column, y)) { + return true; + } + } + } + return false; + } + + private static boolean isCellColumnActive(CaveCarveScratch plan, int column, int y) { + return (plan.cellPassBits[column >> 6] & (1L << column)) != 0L + && plan.columnMaxY[column] >= y + && (y < plan.columnProtectStartY[column] || y > plan.columnProtectEndY[column]); + } + + private double cornerDensity(CaveCarveScratch scratch, CaveDensityGrid.Column column, int x, int y, int z) { + if (column == null) { + return samplePlaneDensity(scratch, x, y, z); + } + double cached = column.get(y); + if (!Double.isNaN(cached)) { + return cached; + } + double density = samplePlaneDensity(scratch, x, y, z); + column.set(y, density); + return density; + } + + private double cellThreshold(CaveCarveScratch plan, int column, int y, int minY, double surfaceBreakThresholdBoost, + double[] verticalEdgeFade) { + return CaveCellKernel.threshold(cellLocalThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade), + normalizationFactor, inverseNormalization); + } + + private double cellLocalThreshold(CaveCarveScratch plan, int column, int y, int minY, double surfaceBreakThresholdBoost, + double[] verticalEdgeFade) { + double localThreshold = plan.passThreshold[column]; + if (plan.surfaceBreakColumn[column] && y >= plan.surfaceBreakFloorY[column]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[y - minY]; + return applySurfaceCeilingFade(plan, localThreshold, column, y, minY); + } + + private static void flushCellCounters(CaveCarveScratch scratch) { + if (scratch.solidCells != 0L) { + SOLID_CELLS.add(scratch.solidCells); + scratch.solidCells = 0L; + } + if (scratch.openCells != 0L) { + OPEN_CELLS.add(scratch.openCells); + scratch.openCells = 0L; + } + if (scratch.mixedCells != 0L) { + MIXED_CELLS.add(scratch.mixedCells); + scratch.mixedCells = 0L; + } + if (scratch.exactRows != 0L) { + EXACT_ROWS.add(scratch.exactRows); + scratch.exactRows = 0L; + } + } + private int carvePassLattice( MantleChunk chunk, int x0, diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java new file mode 100644 index 000000000..4df05e6af --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java @@ -0,0 +1,106 @@ +package art.arcane.iris.generation.mantle; + +import org.junit.Test; + +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class CaveCellKernelTest { + private static final int TRIALS = 200_000; + + @Test + public void decidedCellsAgreeWithEveryInterpolatedColumn() { + SplittableRandom random = new SplittableRandom(91_337L); + long decided = 0L; + for (int trial = 0; trial < TRIALS; trial++) { + double scale = Math.scalb(1D, random.nextInt(-30, 4)); + double base = (random.nextDouble() * 2D - 1D) * scale; + double spread = random.nextInt(4) == 0 ? 0D : random.nextDouble() * scale * Math.scalb(1D, -random.nextInt(0, 40)); + double d00 = base + spread * (random.nextDouble() * 2D - 1D); + double d01 = random.nextInt(8) == 0 ? d00 : base + spread * (random.nextDouble() * 2D - 1D); + double d10 = base + spread * (random.nextDouble() * 2D - 1D); + double d11 = random.nextInt(8) == 0 ? d10 : base + spread * (random.nextDouble() * 2D - 1D); + double low = Math.min(Math.min(d00, d01), Math.min(d10, d11)); + double high = Math.max(Math.max(d00, d01), Math.max(d10, d11)); + int mode = random.nextInt(6); + double[] thresholds = new double[64]; + for (int column = 0; column < 64; column++) { + thresholds[column] = pickThreshold(random, low, high, mode); + } + double thresholdLow = Double.POSITIVE_INFINITY; + double thresholdHigh = Double.NEGATIVE_INFINITY; + for (double threshold : thresholds) { + thresholdLow = Math.min(thresholdLow, threshold); + thresholdHigh = Math.max(thresholdHigh, threshold); + } + int decision = CaveCellKernel.decide(d00, d01, d10, d11, thresholdLow, thresholdHigh); + if (decision == CaveCellKernel.MIXED) { + continue; + } + decided++; + for (int column = 0; column < 64; column++) { + double tx = (column >> 3) / 8D; + double tz = (column & 7) / 8D; + boolean carved = bilinear(d00, d01, d10, d11, tx, tz) <= thresholds[column]; + assertEquals("trial " + trial + " column " + column, decision == CaveCellKernel.ALL, carved); + } + } + assertTrue("decided " + decided, decided > TRIALS / 4); + } + + @Test + public void thresholdsAtTheCornerExtremesStayMixed() { + double d00 = 0.1D; + double d01 = -0.2D; + double d10 = 0.3D; + double d11 = 0.05D; + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D, 0.3D)); + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, Math.nextUp(0.3D), 0.4D)); + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, -0.3D, Math.nextDown(-0.2D))); + assertEquals(CaveCellKernel.ALL, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D + 1.0E-12D, 0.5D)); + assertEquals(CaveCellKernel.NONE, CaveCellKernel.decide(d00, d01, d10, d11, -0.9D, -0.2D - 1.0E-12D)); + } + + @Test + public void nonFiniteCornersAreNeverDecided() { + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(Double.NaN, 0D, 0D, 0D, 5D, 5D)); + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, Double.POSITIVE_INFINITY, 0D, 0D, -5D, -5D)); + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, Double.NEGATIVE_INFINITY, 0D, + Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, 0D, 0D, Double.NaN, 1D)); + } + + @Test + public void monotoneThresholdMappingPreservesBounds() { + SplittableRandom random = new SplittableRandom(7_771L); + for (int trial = 0; trial < TRIALS; trial++) { + double normalization = 0.25D + random.nextDouble() * 3D; + double inverse = 1D / normalization; + double a = (random.nextDouble() * 2D - 1D) * Math.scalb(1D, random.nextInt(-20, 3)); + double b = random.nextInt(4) == 0 ? Math.nextUp(a) : a + random.nextDouble() * Math.scalb(1D, random.nextInt(-50, 0)); + assertTrue(CaveCellKernel.threshold(a, normalization, inverse) <= CaveCellKernel.threshold(b, normalization, inverse)); + assertEquals(Double.doubleToRawLongBits((a * normalization) * inverse), + Double.doubleToRawLongBits(CaveCellKernel.threshold(a, normalization, inverse))); + } + } + + private static double pickThreshold(SplittableRandom random, double low, double high, int mode) { + double width = Math.max(high - low, Math.ulp(Math.max(Math.abs(low), Math.abs(high)))); + return switch (mode) { + case 0 -> high + Math.ulp(high) * random.nextInt(0, 64); + case 1 -> low - Math.ulp(low) * random.nextInt(0, 64); + case 2 -> high + width * random.nextDouble() * 0.5D; + case 3 -> low - width * random.nextDouble() * 0.5D; + case 4 -> high + width * (1D + random.nextDouble() * 4D); + default -> low - width * (1D + random.nextDouble() * 4D); + }; + } + + private static double bilinear(double d00, double d01, double d10, double d11, double tx, double tz) { + double dx0 = d00 + ((d10 - d00) * tx); + double dx1 = d01 + ((d11 - d01) * tx); + return dx0 + ((dx1 - dx0) * tz); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java new file mode 100644 index 000000000..9dfd04fd4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java @@ -0,0 +1,370 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.cave.IrisCaveFieldModule; +import art.arcane.iris.generation.cave.IrisCaveProfile; +import art.arcane.iris.generation.noise.IrisGeneratorStyle; +import art.arcane.iris.generation.noise.IrisStyledRange; +import art.arcane.iris.generation.noise.NoiseStyle; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.EngineMetrics; +import art.arcane.iris.generation.runtime.SeedManager; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.value.IrisRange; +import art.arcane.iris.world.IrisWorld; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; +import art.arcane.volmlib.util.matter.slices.CavernMatter; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +public class IrisCaveCarver3DCellKernelTest { + private static final long CARVE_SEED = -54_777_863_784_492_918L; + private static final int WORLD_HEIGHT = 160; + private static final int CHUNK_SPAN = 4; + + @Test + public void warpModulesProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.WARP_MODULES, 0xdf617c77d3c3cc5eL, 67798); + } + + @Test + public void warpOnlyTerrainProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.WARP_TERRAIN, 0x7e0b63cab5bb3904L, 67036); + } + + @Test + public void styledThresholdBlendProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.STYLED_BLEND, 0x25792d49143774faL, 30387); + } + + @Test + public void fluidBoundaryProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.FLUID_BOUNDARY, 0xb102145856119afeL, 17584); + } + + @Test + public void noWarpModulesProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.NO_WARP_MODULES, 0x5a83dc20c89c6398L, 47534); + } + + @Test + public void verticalStepProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.VERTICAL_STEP, 0xac0413031ed65b61L, 48011); + } + + @Test + public void latticeProfileMatchesGoldenWriteSequence() throws Exception { + assertScenario(Scenario.LATTICE, 0xc7ffab155e6bdef9L, 72714); + } + + @Test + public void floorFluidProfilesSharingChunksMatchGoldenWriteSequence() throws Exception { + assertScenario(Scenario.FLOOR_FLUID_LAYERED, 0xb4c0fb23e071617dL, 68747); + } + + @Test + public void cellKernelDecidesCellsWithoutColumnEvaluation() throws Exception { + IrisCaveCarver3D.CellCounters before = IrisCaveCarver3D.cellCounters(); + run(Scenario.WARP_MODULES, false); + IrisCaveCarver3D.CellCounters after = IrisCaveCarver3D.cellCounters(); + long decided = after.decided() - before.decided(); + long evaluated = after.evaluated() - before.evaluated(); + assertTrue("decided=" + decided + " evaluated=" + evaluated, decided > 0L && evaluated > 0L); + } + + private void assertScenario(Scenario scenario, long expectedHash, int expectedWrites) throws Exception { + Result serial = run(scenario, false); + Result parallel = run(scenario, true); + assertTrue(serial.carved > 0); + assertEquals(serial.hash, parallel.hash); + assertEquals(serial.writes, parallel.writes); + assertEquals(serial.carved, parallel.carved); + assertEquals(expectedHash, serial.hash); + assertEquals(expectedWrites, serial.writes); + } + + private Result run(Scenario scenario, boolean parallel) throws Exception { + if (!parallel) { + return carveArea(scenario); + } + ForkJoinPool pool = new ForkJoinPool(4); + try { + return pool.submit(() -> carveArea(scenario)).get(120, TimeUnit.SECONDS); + } finally { + pool.shutdownNow(); + } + } + + private Result carveArea(Scenario scenario) { + Engine engine = createEngine(scenario); + CaveDensityGrid grid = new CaveDensityGrid(4096); + IrisCaveProfile[] profiles = scenario.profiles(); + IrisCaveCarver3D[] carvers = new IrisCaveCarver3D[profiles.length]; + for (int index = 0; index < profiles.length; index++) { + carvers[index] = new IrisCaveCarver3D(engine, profiles[index], grid); + } + Capture capture = new Capture(); + int carved = 0; + for (int chunkX = -2; chunkX < CHUNK_SPAN - 2; chunkX++) { + for (int chunkZ = -1; chunkZ < CHUNK_SPAN - 1; chunkZ++) { + MantleWriter writer = capture.writer(chunkX, chunkZ); + CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan(); + for (int index = 0; index < carvers.length; index++) { + carved += carvers[index].carve(writer, chunkX, chunkZ, scenario.weights(index, chunkX, chunkZ), + 0.08D, 0.24D, scenario.worldYRange(), scenario.surfaceHeights(chunkX, chunkZ), + scenario.boundaries(chunkX, chunkZ), null, supportPlan); + } + capture.resolving = true; + supportPlan.resolve(writer.acquireChunk(chunkX, chunkZ)); + capture.finishResolve(); + } + } + return new Result(capture.hash, capture.writes, carved); + } + + private Engine createEngine(Scenario scenario) { + Engine engine = mock(Engine.class, withSettings().stubOnly()); + IrisData data = mock(IrisData.class, withSettings().stubOnly()); + IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly()); + SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly()); + doReturn(data).when(engine).getData(); + doReturn(dimension).when(engine).getDimension(); + doReturn(seedManager).when(engine).getSeedManager(); + doReturn(CARVE_SEED).when(seedManager).getCarve(); + doReturn(new EngineMetrics(16)).when(engine).getMetrics(); + doReturn(IrisWorld.builder().minHeight(-32).maxHeight(WORLD_HEIGHT - 32).build()).when(engine).getWorld(); + doReturn(WORLD_HEIGHT).when(engine).getHeight(); + doReturn(110).when(engine).getHeight(anyInt(), anyInt()); + doReturn(24).when(dimension).getCaveLavaHeight(); + doReturn(70).when(dimension).getFluidHeight(); + doReturn(scenario == Scenario.VERTICAL_STEP ? 4 : 1).when(dimension).getCaveDensitySamplingStep(); + return engine; + } + + private static IrisCaveProfile baseProfile(boolean warp, boolean modules) { + IrisCaveProfile profile = new IrisCaveProfile(); + profile.setEnabled(true); + profile.setVerticalRange(new IrisRange(4D, 150D)); + profile.setVerticalEdgeFade(14); + profile.setVerticalEdgeFadeStrength(0.21D); + profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.21D)); + profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.43D)); + profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.37D)); + profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.140625D)); + profile.setBaseWeight(1D); + profile.setDetailWeight(0.42D); + profile.setWarpStrength(warp ? 3.7D : 0D); + profile.setDensityThreshold(new IrisStyledRange(-0.18D, -0.18D, new IrisGeneratorStyle(NoiseStyle.FLAT))); + profile.setThresholdBias(0D); + profile.setSampleStep(1); + profile.setSurfaceClearance(5); + profile.setAllowSurfaceBreak(true); + profile.setSurfaceBreakNoiseThreshold(0.3D); + profile.setSurfaceBreakDepth(14); + profile.setSurfaceBreakThresholdBoost(0.21D); + profile.setAllowFluid(false); + profile.setAllowLava(true); + KList caveModules = new KList<>(); + if (modules) { + caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.33D), + 0.27D, 0.05D, new IrisRange(0D, 90D), false)); + caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.51D), + 0.19D, -0.07D, new IrisRange(40D, 150D), true)); + } + profile.setModules(caveModules); + return profile; + } + + private enum Scenario { + WARP_MODULES, + WARP_TERRAIN, + STYLED_BLEND, + FLUID_BOUNDARY, + NO_WARP_MODULES, + VERTICAL_STEP, + LATTICE, + FLOOR_FLUID_LAYERED; + + IrisCaveProfile[] profiles() { + if (this == FLOOR_FLUID_LAYERED) { + return new IrisCaveProfile[]{ + baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(4), + baseProfile(true, false).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(2) + .setAllowLava(false).setDensityThreshold(new IrisStyledRange(-0.24D, -0.08D, + new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.45D))) + }; + } + return new IrisCaveProfile[]{profile()}; + } + + IrisCaveProfile profile() { + return switch (this) { + case WARP_MODULES, VERTICAL_STEP -> baseProfile(true, true); + case WARP_TERRAIN -> baseProfile(true, false); + case STYLED_BLEND -> baseProfile(false, false).setDensityThreshold(new IrisStyledRange(-0.32D, -0.04D, + new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.6D))); + case FLUID_BOUNDARY -> baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(false) + .setFluidMinDepthBelowSurface(6).setAllowLava(false); + case NO_WARP_MODULES -> baseProfile(false, true); + case LATTICE -> baseProfile(true, true).setSampleStep(3); + case FLOOR_FLUID_LAYERED -> baseProfile(true, true); + }; + } + + double[] weights(int layer, int chunkX, int chunkZ) { + double[] weights = new double[256]; + for (int index = 0; index < 256; index++) { + int worldX = (chunkX << 4) + (index >> 4); + int worldZ = (chunkZ << 4) + (index & 15); + double blend = 0.5D + 0.6D * Math.sin(worldX * 0.11D) * Math.cos(worldZ * 0.07D); + weights[index] = switch (this) { + case STYLED_BLEND, FLUID_BOUNDARY -> blend; + case FLOOR_FLUID_LAYERED -> layer == 0 ? blend : 1D - blend; + default -> 1D; + }; + } + return weights; + } + + int[] surfaceHeights(int chunkX, int chunkZ) { + int[] heights = new int[256]; + for (int index = 0; index < 256; index++) { + int worldX = (chunkX << 4) + (index >> 4); + int worldZ = (chunkZ << 4) + (index & 15); + heights[index] = this == WARP_MODULES || this == LATTICE || this == FLOOR_FLUID_LAYERED + ? 118 + : 96 + (int) Math.round(22D * Math.sin(worldX * 0.09D) + 15D * Math.cos(worldZ * 0.13D)); + } + return heights; + } + + IrisRange worldYRange() { + return this == FLUID_BOUNDARY || this == FLOOR_FLUID_LAYERED ? new IrisRange(-20D, 100D) : null; + } + + long[] boundaries(int chunkX, int chunkZ) { + long[] boundaries = new long[256]; + Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE)); + if (this != FLUID_BOUNDARY && this != WARP_TERRAIN) { + return boundaries; + } + for (int index = 0; index < 256; index++) { + int worldX = (chunkX << 4) + (index >> 4); + if (Math.floorMod(worldX + chunkZ, 5) == 0) { + boundaries[index] = SurfaceFluidBoundaryPlan.boundary(60 + Math.floorMod(index, 7), 82); + } + } + return boundaries; + } + } + + private record Result(long hash, int writes, int carved) { + } + + private static final class Capture { + private final Map> chunks = new HashMap<>(); + private final IdentityHashMap identities = new IdentityHashMap<>(); + private final List resolved = new ArrayList<>(); + private long hash = 0xcbf29ce484222325L; + private int writes; + private boolean resolving; + + private MantleWriter writer(int chunkX, int chunkZ) { + MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly()); + @SuppressWarnings("unchecked") + Mantle mantle = mock(Mantle.class, withSettings().stubOnly()); + @SuppressWarnings("unchecked") + MantleChunk chunk = mock(MantleChunk.class, withSettings().stubOnly()); + long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); + Map sections = chunks.computeIfAbsent(key, ignored -> new HashMap<>()); + doReturn(mantle).when(writer).getMantle(); + doReturn(WORLD_HEIGHT).when(mantle).getWorldHeight(); + doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt()); + doAnswer(invocation -> sections.get(invocation.getArgument(0, Integer.class))).when(chunk).get(anyInt()); + doAnswer(invocation -> sections.computeIfAbsent(invocation.getArgument(0, Integer.class), + sectionIndex -> createSection(chunkX, chunkZ, sectionIndex))).when(chunk).getOrCreate(anyInt()); + return writer; + } + + private Matter createSection(int chunkX, int chunkZ, int sectionIndex) { + mix(0x5EC7L); + mix(chunkX); + mix(chunkZ); + mix(sectionIndex); + Matter matter = mock(Matter.class, withSettings().stubOnly()); + MatterSlice slice = new RecordingCavernMatter(this, sectionIndex); + doReturn(slice).when(matter).slice(MatterCavern.class); + doReturn(slice).when(matter).getSlice(MatterCavern.class); + return matter; + } + + private void record(int sectionIndex, int x, int y, int z, MatterCavern value) { + writes++; + if (resolving) { + resolved.add(((long) sectionIndex << 20) | ((long) x << 16) | ((long) y << 12) | ((long) z << 8) | value.getLiquid()); + return; + } + mix(sectionIndex); + mix((x << 8) | (y << 4) | z); + mix(value == null ? -1 : value.getLiquid()); + Integer identity = identities.get(value); + if (identity == null) { + identity = identities.size(); + identities.put(value, identity); + } + mix(identity); + } + + private void finishResolve() { + resolving = false; + resolved.sort(Long::compare); + for (long entry : resolved) { + mix(entry); + } + resolved.clear(); + } + + private void mix(long value) { + hash ^= value; + hash *= 0x100000001b3L; + } + } + + private static final class RecordingCavernMatter extends CavernMatter { + private final Capture capture; + private final int sectionIndex; + + private RecordingCavernMatter(Capture capture, int sectionIndex) { + super(16, 16, 16); + this.capture = capture; + this.sectionIndex = sectionIndex; + } + + @Override + public void set(int x, int y, int z, MatterCavern value) { + super.set(x, y, z, value); + capture.record(sectionIndex, x, y, z, value); + } + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index 671b2b408..162e1063b 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -18,6 +18,7 @@ package art.arcane.iris.probe; +import art.arcane.iris.generation.mantle.IrisCaveCarver3D; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidator; @@ -302,6 +303,7 @@ public final class GenerationProbe { try { ProbeResult result = run(configuration, options); System.out.println(result.machineLine()); + System.out.println(IrisCaveCarver3D.cellCounters().machineLine()); exitCode = result.failedChunks() == 0 ? 0 : 1; } catch (Throwable e) { System.out.println("[genprobe] FAIL: probe execution failed"); From 65542be8d6e2c3582f4d15e76743ec207edeb3c0 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 20:16:21 -0400 Subject: [PATCH 2/2] Tightened mixed cave cells - Classify mixed cells without fade, boost or protection through a branch-light column loop - Hoist the per-row bilinear terms shared by every column of a cell row - Mark open cells from the pass bitset when every pass column is active --- .../generation/mantle/CaveCarveScratch.java | 1 + .../generation/mantle/IrisCaveCarver3D.java | 63 ++++++++++++++++--- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java index 6f6bf8c07..aa3c3f690 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java @@ -76,6 +76,7 @@ final class CaveCarveScratch { final int[] cellBreakFloorMin = new int[4]; final int[] cellCeilingFadeStart = new int[4]; final int[] cellTopY = new int[4]; + final int[] cellMinTopY = new int[4]; final boolean[] cellProtected = new boolean[4]; final int[] columnProtectStartY = new int[256]; final int[] columnProtectEndY = new int[256]; diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java index a1b51914f..9c5551908 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java @@ -64,6 +64,7 @@ public class IrisCaveCarver3D { private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17}; private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024; private static final int[] CELL_CORNER_MASKS = {0b000011011, 0b000110110, 0b011011000, 0b110110000}; + private static final long[] CELL_Z_MASKS = {0x00FF00FF00FF00FFL, 0xFF00FF00FF00FF00L}; private static final LongAdder SOLID_CELLS = new LongAdder(); private static final LongAdder OPEN_CELLS = new LongAdder(); private static final LongAdder MIXED_CELLS = new LongAdder(); @@ -1128,6 +1129,7 @@ public class IrisCaveCarver3D { Arrays.fill(plan.cellBreakFloorMin, Integer.MAX_VALUE); Arrays.fill(plan.cellCeilingFadeStart, Integer.MAX_VALUE); Arrays.fill(plan.cellTopY, Integer.MIN_VALUE); + Arrays.fill(plan.cellMinTopY, Integer.MAX_VALUE); Arrays.fill(plan.cellProtected, false); prepareAdaptiveGeometry(plan, ADAPTIVE_DEEP_SAMPLE_STEP, 2, 3); double[] passThreshold = plan.passThreshold; @@ -1149,6 +1151,7 @@ public class IrisCaveCarver3D { plan.cellCeilingFadeStart[cell] = Math.min(plan.cellCeilingFadeStart[cell], top - SURFACE_CEILING_FADE_DEPTH + 1); } plan.cellTopY[cell] = Math.max(plan.cellTopY[cell], top); + plan.cellMinTopY[cell] = Math.min(plan.cellMinTopY[cell], top); int protectStart = SurfaceFluidBoundaryPlan.NO_BOUNDARY; int protectEnd = Integer.MIN_VALUE; if (surfaceFluidBoundaries != null) { @@ -1389,14 +1392,26 @@ public class IrisCaveCarver3D { int decision = CaveCellKernel.decide(d00, d01, d10, d11, CaveCellKernel.threshold(low, normalizationFactor, inverseNormalization), CaveCellKernel.threshold(high, normalizationFactor, inverseNormalization)); + boolean uniform = !plan.cellProtected[cell] && y <= plan.cellMinTopY[cell]; if (decision == CaveCellKernel.NONE) { scratch.solidCells++; } else if (decision == CaveCellKernel.ALL) { scratch.openCells++; - markActiveCellColumns(plan, cell, y, rowBits); + if (uniform) { + int word = (cell >> 1) << 1; + long mask = CELL_Z_MASKS[cell & 1]; + rowBits[word] |= plan.cellPassBits[word] & mask; + rowBits[word + 1] |= plan.cellPassBits[word + 1] & mask; + } else { + markActiveCellColumns(plan, cell, y, rowBits); + } } else { scratch.mixedCells++; - classifyMixedCell(plan, cell, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, d00, d01, d10, d11, rowBits); + if (uniform && y < plan.cellCeilingFadeStart[cell] && y < plan.cellBreakFloorMin[cell]) { + classifyPlainMixedCell(plan, cell, fade, d00, d01, d10, d11, rowBits); + } else { + classifyMixedCell(plan, cell, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, d00, d01, d10, d11, rowBits); + } } } } @@ -1420,15 +1435,15 @@ public class IrisCaveCarver3D { int firstLocalZ = (cell & 1) << 3; for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) { int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ); + double tx = adaptiveTx[rowColumn]; + double dx0 = d00 + ((d10 - d00) * tx); + double dx1 = d01 + ((d11 - d01) * tx); + double slope = dx1 - dx0; for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) { if (!isCellColumnActive(plan, column, y)) { continue; } - double tx = adaptiveTx[column]; - double tz = adaptiveTz[column]; - double dx0 = d00 + ((d10 - d00) * tx); - double dx1 = d01 + ((d11 - d01) * tx); - double predictedDensity = dx0 + ((dx1 - dx0) * tz); + double predictedDensity = dx0 + (slope * adaptiveTz[column]); if (predictedDensity <= cellThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade)) { rowBits[column >> 6] |= 1L << column; } @@ -1436,6 +1451,40 @@ public class IrisCaveCarver3D { } } + private void classifyPlainMixedCell( + CaveCarveScratch plan, + int cell, + double fade, + double d00, + double d01, + double d10, + double d11, + long[] rowBits + ) { + double[] passThreshold = plan.passThreshold; + double[] adaptiveTx = plan.adaptiveTx; + double[] adaptiveTz = plan.adaptiveTz; + int firstLocalX = (cell >> 1) << 3; + int firstLocalZ = (cell & 1) << 3; + for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) { + int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ); + double tx = adaptiveTx[rowColumn]; + double dx0 = d00 + ((d10 - d00) * tx); + double dx1 = d01 + ((d11 - d01) * tx); + double slope = dx1 - dx0; + long bits = 0L; + for (int offset = 0; offset < ADAPTIVE_DEEP_SAMPLE_STEP; offset++) { + int column = rowColumn + offset; + double predictedDensity = dx0 + (slope * adaptiveTz[column]); + if (predictedDensity <= CaveCellKernel.threshold(passThreshold[column] - fade, normalizationFactor, + inverseNormalization)) { + bits |= 1L << offset; + } + } + rowBits[rowColumn >> 6] |= bits << (rowColumn & 63); + } + } + private static void markActiveCellColumns(CaveCarveScratch plan, int cell, int y, long[] rowBits) { int firstLocalX = (cell >> 1) << 3; int firstLocalZ = (cell & 1) << 3;