mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-18 22:44:09 +00:00
Share cave density lattice
- Adaptive plane lattice samples cached per carver across neighbour chunks - Grid cleared with carvers on hotload
This commit is contained in:
@@ -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<Column> 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<double[]> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean> carving = ThreadLocal.withInitial(() -> false);
|
||||
private final ThreadLocal<CaveCarveScratch> 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;
|
||||
|
||||
@@ -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<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
|
||||
|
||||
private final Object profileCarverLock = new Object();
|
||||
private final CaveDensityGrid densityGrid = new CaveDensityGrid(DENSITY_GRID_COLUMNS);
|
||||
private volatile Map<IrisCaveProfile, IrisCaveCarver3D> 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) {
|
||||
|
||||
+34
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user