diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java new file mode 100644 index 000000000..b0064d1a2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java @@ -0,0 +1,84 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.cache.ConcurrentClockCache; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * Shares the adaptive cave planes' 8-block density lattice between neighbouring chunks. Every lattice column on a + * chunk edge is sampled by two chunks and every corner by four, with identical coordinates and carver noise, so a + * value computed once is reused bit for bit. Racing writers store the same value. + */ +final class CaveDensityGrid { + static final int LATTICE_SHIFT = 3; + private static final int SEGMENT_SHIFT = 5; + private static final int SEGMENT_SIZE = 1 << SEGMENT_SHIFT; + private static final long COORDINATE_MASK = 0xFFFFFFL; + private static final VarHandle VALUES = MethodHandles.arrayElementVarHandle(double[].class); + private static final AtomicInteger OWNERS = new AtomicInteger(); + + private final ConcurrentClockCache columns; + + CaveDensityGrid(int capacity) { + columns = new ConcurrentClockCache<>(capacity); + } + + static int nextOwner() { + return OWNERS.getAndIncrement(); + } + + Column column(Object owner, int ownerId, int height, int latticeX, int latticeZ) { + long key = (long) (ownerId & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK); + Column cached = columns.get(key); + if (cached == null) { + cached = columns.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height)); + } + return cached.owner == owner && cached.latticeX == latticeX && cached.latticeZ == latticeZ ? cached : null; + } + + void clear() { + columns.clear(); + } + + static final class Column { + private final Object owner; + private final int latticeX; + private final int latticeZ; + private final AtomicReferenceArray segments; + + private Column(Object owner, int latticeX, int latticeZ, int height) { + this.owner = owner; + this.latticeX = latticeX; + this.latticeZ = latticeZ; + segments = new AtomicReferenceArray<>(Math.ceilDiv(Math.max(1, height), SEGMENT_SIZE)); + } + + double get(int y) { + int segment = y >> SEGMENT_SHIFT; + if (segment >= segments.length()) { + return Double.NaN; + } + double[] values = segments.getAcquire(segment); + return values == null ? Double.NaN : (double) VALUES.getOpaque(values, y & (SEGMENT_SIZE - 1)); + } + + void set(int y, double value) { + int segment = y >> SEGMENT_SHIFT; + if (segment >= segments.length()) { + return; + } + double[] values = segments.getAcquire(segment); + if (values == null) { + double[] created = new double[SEGMENT_SIZE]; + Arrays.fill(created, Double.NaN); + double[] published = segments.compareAndExchange(segment, null, created); + values = published == null ? created : published; + } + VALUES.setOpaque(values, y & (SEGMENT_SIZE - 1), value); + } + } +} 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 d3059fdba..0f6858c6e 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 @@ -61,6 +61,7 @@ public class IrisCaveCarver3D { private static final int MINIMUM_PARALLEL_DENSITY_CELLS = 16_384; 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 final Engine engine; private final IrisData data; @@ -96,9 +97,16 @@ public class IrisCaveCarver3D { private final int densityMaximumY; private final ThreadLocal carving = ThreadLocal.withInitial(() -> false); private final ThreadLocal scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new); + private final CaveDensityGrid densityGrid; + private final int densityGridOwner = CaveDensityGrid.nextOwner(); public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { + this(engine, profile, new CaveDensityGrid(STANDALONE_DENSITY_GRID_COLUMNS)); + } + + IrisCaveCarver3D(Engine engine, IrisCaveProfile profile, CaveDensityGrid densityGrid) { this.engine = engine; + this.densityGrid = densityGrid; this.data = engine.getData(); this.profile = profile; this.carveAir = new MatterCavern(true, "", LIQUID_AIR); @@ -1542,7 +1550,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -1595,7 +1603,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -1643,7 +1651,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpOnly(scratch, x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -1696,14 +1704,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpModules( - scratch, - x, - y, - z0 + sampleLocalZ, - localModules, - activeModuleCount - ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -2463,6 +2464,33 @@ public class IrisCaveCarver3D { scratch.adaptiveGeometryAxisCells = axisCells; } + private double latticeDensity(CaveCarveScratch scratch, int x, int y, int z) { + int latticeMask = (1 << CaveDensityGrid.LATTICE_SHIFT) - 1; + CaveDensityGrid.Column column = ((x | z) & latticeMask) != 0 ? null : densityGrid.column(this, densityGridOwner, + densityMaximumY + 1, x >> CaveDensityGrid.LATTICE_SHIFT, z >> CaveDensityGrid.LATTICE_SHIFT); + 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 samplePlaneDensity(CaveCarveScratch scratch, int x, int y, int z) { + if (!hasWarp) { + return sampleDensityNoWarpNoModules(x, y, z); + } + int activeModuleCount = hasModules ? prepareActiveModules(scratch, y) : 0; + if (activeModuleCount == 0) { + return sampleDensityWarpOnly(scratch, x, y, z); + } + return sampleDensityWarpModules(scratch, x, y, z, scratch.activeModules, activeModuleCount); + } + private double sampleDensityNoWarpNoModules(int x, int y, int z) { double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java index 237ff2041..9c8dbf454 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java @@ -54,6 +54,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final double THRESHOLD_PENALTY = 0.24D; private static final int MAX_BLENDED_PROFILE_PASSES = 2; private static final int MAX_POOLED_WEIGHT_BUFFERS = 64; + /** Lattice columns kept for neighbouring chunks: about 3 KB each on land, roughly 50 MB when full. */ + private static final int DENSITY_GRID_COLUMNS = 16_384; private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1; private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH; private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; @@ -62,6 +64,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final ThreadLocal BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); private final Object profileCarverLock = new Object(); + private final CaveDensityGrid densityGrid = new CaveDensityGrid(DENSITY_GRID_COLUMNS); private volatile Map profileCarvers = new IdentityHashMap<>(); static { @@ -94,6 +97,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { // Carvers rebuild deterministically from the carve seed, so output is unchanged. synchronized (profileCarverLock) { profileCarvers = new IdentityHashMap<>(); + densityGrid.clear(); } } @@ -524,7 +528,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { return carver; } - IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile); + IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile, densityGrid); synchronized (profileCarverLock) { IrisCaveCarver3D published = profileCarvers.get(profile); if (published != null) { diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java index 21c277cb0..2bb001781 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java @@ -1038,6 +1038,40 @@ public class IrisCaveCarver3DNearParityTest { largestPlane < 192); } + @Test + public void sharedDensityLatticeReproducesIsolatedCarvesAcrossNeighbourChunks() throws Exception { + Engine engine = createEngine(96, 90); + for (boolean warp : new boolean[]{false, true}) { + for (boolean modules : new boolean[]{false, true}) { + AtomicLong sharedSamples = new AtomicLong(); + AtomicLong isolatedSamples = new AtomicLong(); + IrisCaveCarver3D shared = new IrisCaveCarver3D(engine, + createProfile(warp, modules).setAdaptiveSampling(true), new CaveDensityGrid(4096)); + baseDensityField.set(shared, new CountingCNG((CNG) baseDensityField.get(shared), sharedSamples)); + for (int chunkX = -1; chunkX <= 1; chunkX++) { + for (int chunkZ = -1; chunkZ <= 1; chunkZ++) { + WriterCapture sharedCapture = createWriterCapture(96); + int sharedCarved = shared.carve(sharedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, + null, filledHeights(90)); + IrisCaveCarver3D isolated = new IrisCaveCarver3D(engine, + createProfile(warp, modules).setAdaptiveSampling(true)); + baseDensityField.set(isolated, new CountingCNG((CNG) baseDensityField.get(isolated), isolatedSamples)); + WriterCapture isolatedCapture = createWriterCapture(96); + int isolatedCarved = isolated.carve(isolatedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, + null, filledHeights(90)); + assertTrue(isolatedCarved > 0); + assertEquals(isolatedCarved, sharedCarved); + assertEquals(isolatedCapture.carvedCells, sharedCapture.carvedCells); + assertEquals(isolatedCapture.carvedLiquids, sharedCapture.carvedLiquids); + } + } + assertTrue("warp=" + warp + " modules=" + modules + " shared=" + sharedSamples.get() + + " isolated=" + isolatedSamples.get(), + sharedSamples.get() < isolatedSamples.get() * 3 / 4); + } + } + } + private void assertExactParity(boolean warp, boolean modules, boolean adaptiveSampling) throws Exception { Engine engine = createEngine(96, 90); double[] columnWeights = fullWeights();