mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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
This commit is contained in:
1 parent
e2430500cf
commit
7c4a5e1937
6 files changed
+998
-4
No files matched your search
@@ -66,6 +66,24 @@ final class CaveCarveScratch {
|
|||||||
final int[] tileLocalX = new int[4];
|
final int[] tileLocalX = new int[4];
|
||||||
final int[] tileLocalZ = new int[4];
|
final int[] tileLocalZ = new int[4];
|
||||||
final int[] tileTopY = 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];
|
CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0];
|
||||||
double[] activeModuleRemainingMin = new double[0];
|
double[] activeModuleRemainingMin = new double[0];
|
||||||
double[] activeModuleRemainingMax = new double[0];
|
double[] activeModuleRemainingMax = new double[0];
|
||||||
@@ -84,5 +102,6 @@ final class CaveCarveScratch {
|
|||||||
Arrays.fill(sectionMatter, null);
|
Arrays.fill(sectionMatter, null);
|
||||||
Arrays.fill(sectionSlices, null);
|
Arrays.fill(sectionSlices, null);
|
||||||
Arrays.fill(densityColumns, null);
|
Arrays.fill(densityColumns, null);
|
||||||
|
Arrays.fill(cellCornerColumns, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ import java.util.concurrent.ForkJoinPool;
|
|||||||
import java.util.concurrent.ForkJoinWorkerThread;
|
import java.util.concurrent.ForkJoinWorkerThread;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.LongAdder;
|
||||||
|
|
||||||
public class IrisCaveCarver3D {
|
public class IrisCaveCarver3D {
|
||||||
private static final byte LIQUID_AIR = 0;
|
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 MAXIMUM_DENSITY_WORKERS = 4;
|
||||||
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
|
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
|
||||||
private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024;
|
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 Engine engine;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
@@ -155,6 +161,10 @@ public class IrisCaveCarver3D {
|
|||||||
densityMaximumY = engine.getWorld().getHeight() - 1;
|
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) {
|
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
||||||
CaveCarveScratch scratch = scratchCache.get();
|
CaveCarveScratch scratch = scratchCache.get();
|
||||||
if (!scratch.fullWeightsInitialized) {
|
if (!scratch.fullWeightsInitialized) {
|
||||||
@@ -504,7 +514,9 @@ public class IrisCaveCarver3D {
|
|||||||
int minimumY = pass.minimumY() + index * rows;
|
int minimumY = pass.minimumY() + index * rows;
|
||||||
int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1);
|
int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1);
|
||||||
if (minimumY <= maximumY) {
|
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);
|
runDensityTasks(tasks, pool);
|
||||||
@@ -697,10 +709,25 @@ public class IrisCaveCarver3D {
|
|||||||
int[] columns, int[] columnTops, int columnCount, double[] thresholds,
|
int[] columns, int[] columnTops, int columnCount, double[] thresholds,
|
||||||
long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors,
|
long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors,
|
||||||
double surfaceBreakBoost, double[] verticalFade, CaveCarveScratch sourceScratch,
|
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 static final class DensityTask implements Runnable, ForkJoinPool.ManagedBlocker {
|
||||||
private final Runnable work;
|
private final Runnable work;
|
||||||
private final AtomicBoolean claimed = new AtomicBoolean();
|
private final AtomicBoolean claimed = new AtomicBoolean();
|
||||||
@@ -795,7 +822,7 @@ public class IrisCaveCarver3D {
|
|||||||
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
|
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
|
||||||
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
|
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
|
||||||
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
|
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++) {
|
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||||
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
||||||
@@ -944,12 +971,23 @@ public class IrisCaveCarver3D {
|
|||||||
adaptiveSampleStep,
|
adaptiveSampleStep,
|
||||||
effectiveAdaptiveSampleStep
|
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(
|
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
|
||||||
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
|
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
|
||||||
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
|
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
|
||||||
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, effectiveAdaptiveSampleStep, effectiveAdaptiveThresholdMargin,
|
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++) {
|
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||||
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
||||||
@@ -1059,6 +1097,432 @@ public class IrisCaveCarver3D {
|
|||||||
return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST);
|
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<Matter> 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<MatterCavern> 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<MatterCavern> 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(
|
private int carvePassLattice(
|
||||||
MantleChunk<Matter> chunk,
|
MantleChunk<Matter> chunk,
|
||||||
int x0,
|
int x0,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+370
@@ -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<IrisCaveFieldModule> 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<Long, Map<Integer, Matter>> chunks = new HashMap<>();
|
||||||
|
private final IdentityHashMap<MatterCavern, Integer> identities = new IdentityHashMap<>();
|
||||||
|
private final List<Long> 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<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
|
||||||
|
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||||
|
Map<Integer, Matter> 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<MatterCavern> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.mantle.IrisCaveCarver3D;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
import art.arcane.iris.pack.PackValidator;
|
import art.arcane.iris.pack.PackValidator;
|
||||||
@@ -302,6 +303,7 @@ public final class GenerationProbe {
|
|||||||
try {
|
try {
|
||||||
ProbeResult result = run(configuration, options);
|
ProbeResult result = run(configuration, options);
|
||||||
System.out.println(result.machineLine());
|
System.out.println(result.machineLine());
|
||||||
|
System.out.println(IrisCaveCarver3D.cellCounters().machineLine());
|
||||||
exitCode = result.failedChunks() == 0 ? 0 : 1;
|
exitCode = result.failedChunks() == 0 ? 0 : 1;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
System.out.println("[genprobe] FAIL: probe execution failed");
|
System.out.println("[genprobe] FAIL: probe execution failed");
|
||||||
|
|||||||
Reference in new issue
Block a user