mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 22:40:13 +00:00
Merge L4-terrain
This commit is contained in:
commit
cd61b75ff6
14 files changed
+778
-111
No files matched your search
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package art.arcane.iris.generation.cache;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.cache.CacheKey;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bounded, lock-free, lossy long-keyed cache for memoized values. Keys hash into 4-way sets and a full set
|
||||||
|
* evicts with CLOCK (second chance), so reads never write shared state except to set a reference bit once.
|
||||||
|
* Concurrent inserts of the same key may both compute; the first published value wins.
|
||||||
|
*/
|
||||||
|
public final class ConcurrentClockCache<V> {
|
||||||
|
private static final int WAYS = 4;
|
||||||
|
|
||||||
|
private final AtomicReferenceArray<Entry<V>> slots;
|
||||||
|
private final long sets;
|
||||||
|
|
||||||
|
public ConcurrentClockCache(int capacity) {
|
||||||
|
if (capacity <= 0) {
|
||||||
|
throw new IllegalArgumentException("Cache capacity must be positive");
|
||||||
|
}
|
||||||
|
int setCount = Math.ceilDiv(capacity, WAYS);
|
||||||
|
sets = setCount;
|
||||||
|
slots = new AtomicReferenceArray<>(Math.multiplyExact(setCount, WAYS));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int capacity() {
|
||||||
|
return slots.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public V get(long key) {
|
||||||
|
long hash = CacheKey.mix(key);
|
||||||
|
int base = base(hash);
|
||||||
|
for (int way = 0; way < WAYS; way++) {
|
||||||
|
Entry<V> entry = slots.getAcquire(base + way);
|
||||||
|
if (entry != null && entry.key == key) {
|
||||||
|
entry.touch();
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public V putIfAbsent(long key, V value) {
|
||||||
|
Objects.requireNonNull(value, "Cached value");
|
||||||
|
long hash = CacheKey.mix(key);
|
||||||
|
int base = base(hash);
|
||||||
|
int hand = (int) (hash >>> 60) & (WAYS - 1);
|
||||||
|
while (true) {
|
||||||
|
int empty = -1;
|
||||||
|
for (int way = 0; way < WAYS; way++) {
|
||||||
|
Entry<V> entry = slots.getAcquire(base + way);
|
||||||
|
if (entry == null) {
|
||||||
|
if (empty < 0) {
|
||||||
|
empty = way;
|
||||||
|
}
|
||||||
|
} else if (entry.key == key) {
|
||||||
|
entry.touch();
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry<V> created = new Entry<>(key, value);
|
||||||
|
if (empty >= 0) {
|
||||||
|
if (slots.compareAndSet(base + empty, null, created)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int victim = -1;
|
||||||
|
Entry<V> victimEntry = null;
|
||||||
|
for (int step = 0; step < WAYS * 2 && victim < 0; step++) {
|
||||||
|
int way = (hand + step) & (WAYS - 1);
|
||||||
|
Entry<V> entry = slots.getAcquire(base + way);
|
||||||
|
if (entry == null || !entry.referenced) {
|
||||||
|
victim = way;
|
||||||
|
victimEntry = entry;
|
||||||
|
} else {
|
||||||
|
entry.referenced = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (victim < 0) {
|
||||||
|
victim = hand;
|
||||||
|
victimEntry = slots.getAcquire(base + victim);
|
||||||
|
}
|
||||||
|
if (slots.compareAndSet(base + victim, victimEntry, created)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forEach(Consumer<? super V> action) {
|
||||||
|
for (int index = 0; index < slots.length(); index++) {
|
||||||
|
Entry<V> entry = slots.getAcquire(index);
|
||||||
|
if (entry != null) {
|
||||||
|
action.accept(entry.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (int index = 0; index < slots.length(); index++) {
|
||||||
|
slots.setRelease(index, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int base(long hash) {
|
||||||
|
return (int) (((hash & 0xffffffffL) * sets) >>> 32) * WAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Entry<V> {
|
||||||
|
private final long key;
|
||||||
|
private final V value;
|
||||||
|
private boolean referenced;
|
||||||
|
|
||||||
|
private Entry(long key, V value) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void touch() {
|
||||||
|
if (!referenced) {
|
||||||
|
referenced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,8 @@ import lombok.experimental.Accessors;
|
|||||||
@Description("Creates ore & other block deposits underground")
|
@Description("Creates ore & other block deposits underground")
|
||||||
@Data
|
@Data
|
||||||
public class IrisDepositGenerator {
|
public class IrisDepositGenerator {
|
||||||
|
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
|
||||||
|
private static final ThreadLocal<FootprintCells> FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new);
|
||||||
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
|
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
|
||||||
private static final long FNV_PRIME = 0x100000001b3L;
|
private static final long FNV_PRIME = 0x100000001b3L;
|
||||||
|
|
||||||
@@ -267,6 +269,42 @@ public class IrisDepositGenerator {
|
|||||||
return new IrisObject(1, 1, 1);
|
return new IrisObject(1, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KSet<BlockPosition> cells = new KSet<>();
|
||||||
|
vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z)));
|
||||||
|
return objectFromCells(cells, rng, rdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances {@code rng} exactly as {@link #getClump(Engine, RNG, IrisData)} would and reports the clump's width
|
||||||
|
* and lowest block offset without building it, so a caller can reject the clump first and replay the build
|
||||||
|
* only for clumps it keeps. Returns null without drawing when the shape has no footprint path.
|
||||||
|
*/
|
||||||
|
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
|
||||||
|
if (shape != IrisDepositShape.VANILLA_ELLIPSOID || minSize > FOOTPRINT_MAXIMUM_SIZE
|
||||||
|
|| maxSize > FOOTPRINT_MAXIMUM_SIZE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int paletteSize = getBlockData(rdata).size();
|
||||||
|
if (paletteSize == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int size = rng.i(minSize, maxSize + 1);
|
||||||
|
if (size <= 0) {
|
||||||
|
return ClumpFootprint.EMPTY;
|
||||||
|
}
|
||||||
|
FootprintCells cells = FOOTPRINT_CELLS.get();
|
||||||
|
cells.reset();
|
||||||
|
vanillaEllipsoidCells(rng, size, cells);
|
||||||
|
if (cells.count == 0) {
|
||||||
|
return ClumpFootprint.EMPTY;
|
||||||
|
}
|
||||||
|
for (int cell = 0; cell < cells.count; cell++) {
|
||||||
|
rng.i(0, paletteSize);
|
||||||
|
}
|
||||||
|
return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) {
|
||||||
float angle = rng.nextFloat() * (float) Math.PI;
|
float angle = rng.nextFloat() * (float) Math.PI;
|
||||||
float reach = size / 8F;
|
float reach = size / 8F;
|
||||||
double startX = Math.sin(angle) * reach;
|
double startX = Math.sin(angle) * reach;
|
||||||
@@ -309,7 +347,6 @@ public class IrisDepositGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KSet<BlockPosition> cells = new KSet<>();
|
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
double radius = nodes[i * 4 + 3];
|
double radius = nodes[i * 4 + 3];
|
||||||
if (radius < 0D) {
|
if (radius < 0D) {
|
||||||
@@ -338,14 +375,12 @@ public class IrisDepositGenerator {
|
|||||||
for (int z = minZ; z <= maxZ; z++) {
|
for (int z = minZ; z <= maxZ; z++) {
|
||||||
double nz = (z + 0.5D - centerZ) / radius;
|
double nz = (z + 0.5D - centerZ) / radius;
|
||||||
if (nx * nx + ny * ny + nz * nz < 1D) {
|
if (nx * nx + ny * ny + nz * nz < 1D) {
|
||||||
cells.add(new BlockPosition(x, y, z));
|
sink.add(x, y, z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectFromCells(cells, rng, rdata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
|
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
|
||||||
@@ -564,4 +599,52 @@ public class IrisDepositGenerator {
|
|||||||
|
|
||||||
record ClumpCacheKey(long depositSeed, int minSize, int maxSize) {
|
record ClumpCacheKey(long depositSeed, int minSize, int maxSize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface CellSink {
|
||||||
|
void add(int x, int y, int z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ClumpFootprint(int width, int minY, boolean empty) {
|
||||||
|
static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FootprintCells implements CellSink {
|
||||||
|
private static final int OFFSET = 32;
|
||||||
|
private static final int AXIS = 64;
|
||||||
|
private final long[] occupied = new long[AXIS * AXIS * AXIS / 64];
|
||||||
|
private final int[] touched = new int[AXIS * AXIS * AXIS / 64];
|
||||||
|
private int touchedCount;
|
||||||
|
private int count;
|
||||||
|
private int extentX;
|
||||||
|
private int minY;
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
for (int index = 0; index < touchedCount; index++) {
|
||||||
|
occupied[touched[index]] = 0L;
|
||||||
|
}
|
||||||
|
touchedCount = 0;
|
||||||
|
count = 0;
|
||||||
|
extentX = 0;
|
||||||
|
minY = Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(int x, int y, int z) {
|
||||||
|
int index = ((x + OFFSET) * AXIS + (y + OFFSET)) * AXIS + (z + OFFSET);
|
||||||
|
int word = index >>> 6;
|
||||||
|
long bit = 1L << (index & 63);
|
||||||
|
long current = occupied[word];
|
||||||
|
if ((current & bit) != 0L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (current == 0L) {
|
||||||
|
touched[touchedCount++] = word;
|
||||||
|
}
|
||||||
|
occupied[word] = current | bit;
|
||||||
|
count++;
|
||||||
|
extentX = Math.max(extentX, Math.abs(x));
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,10 @@ public class IrisOreGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
CNG chance = chanceCache.aquire(() -> chanceStyle.create(rng, data));
|
CNG chance = chanceCache.getIfPresent();
|
||||||
|
if (chance == null) {
|
||||||
|
chance = chanceCache.aquire(() -> chanceStyle.create(rng, data));
|
||||||
|
}
|
||||||
|
|
||||||
if (chance.noise(x, y, z) > threshold) {
|
if (chance.noise(x, y, z) > threshold) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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 MINIMUM_PARALLEL_DENSITY_CELLS = 16_384;
|
||||||
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 final Engine engine;
|
private final Engine engine;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
@@ -96,9 +97,16 @@ public class IrisCaveCarver3D {
|
|||||||
private final int densityMaximumY;
|
private final int densityMaximumY;
|
||||||
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
|
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
|
||||||
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
|
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) {
|
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.engine = engine;
|
||||||
|
this.densityGrid = densityGrid;
|
||||||
this.data = engine.getData();
|
this.data = engine.getData();
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.carveAir = new MatterCavern(true, "", LIQUID_AIR);
|
this.carveAir = new MatterCavern(true, "", LIQUID_AIR);
|
||||||
@@ -1542,7 +1550,7 @@ public class IrisCaveCarver3D {
|
|||||||
int rowOffset = sampleXIndex * axisSamples;
|
int rowOffset = sampleXIndex * axisSamples;
|
||||||
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
||||||
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
|
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;
|
int rowOffset = sampleXIndex * axisSamples;
|
||||||
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
||||||
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
|
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;
|
int rowOffset = sampleXIndex * axisSamples;
|
||||||
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
||||||
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
|
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;
|
int rowOffset = sampleXIndex * axisSamples;
|
||||||
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
|
||||||
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
|
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
|
||||||
adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpModules(
|
adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ);
|
||||||
scratch,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
z0 + sampleLocalZ,
|
|
||||||
localModules,
|
|
||||||
activeModuleCount
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2463,6 +2464,33 @@ public class IrisCaveCarver3D {
|
|||||||
scratch.adaptiveGeometryAxisCells = axisCells;
|
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) {
|
private double sampleDensityNoWarpNoModules(int x, int y, int z) {
|
||||||
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
|
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
|
||||||
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
|
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 double THRESHOLD_PENALTY = 0.24D;
|
||||||
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
|
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
|
||||||
private static final int MAX_POOLED_WEIGHT_BUFFERS = 64;
|
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_WIDTH = (BLEND_RADIUS * 2) + 1;
|
||||||
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
|
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
|
||||||
private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
|
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 static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
|
||||||
|
|
||||||
private final Object profileCarverLock = new Object();
|
private final Object profileCarverLock = new Object();
|
||||||
|
private final CaveDensityGrid densityGrid = new CaveDensityGrid(DENSITY_GRID_COLUMNS);
|
||||||
private volatile Map<IrisCaveProfile, IrisCaveCarver3D> profileCarvers = new IdentityHashMap<>();
|
private volatile Map<IrisCaveProfile, IrisCaveCarver3D> profileCarvers = new IdentityHashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -94,6 +97,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
// Carvers rebuild deterministically from the carve seed, so output is unchanged.
|
// Carvers rebuild deterministically from the carve seed, so output is unchanged.
|
||||||
synchronized (profileCarverLock) {
|
synchronized (profileCarverLock) {
|
||||||
profileCarvers = new IdentityHashMap<>();
|
profileCarvers = new IdentityHashMap<>();
|
||||||
|
densityGrid.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,7 +528,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
return carver;
|
return carver;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile);
|
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile, densityGrid);
|
||||||
synchronized (profileCarverLock) {
|
synchronized (profileCarverLock) {
|
||||||
IrisCaveCarver3D published = profileCarvers.get(profile);
|
IrisCaveCarver3D published = profileCarvers.get(profile);
|
||||||
if (published != null) {
|
if (published != null) {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||||
private static final int TERRAIN_COLUMN_CACHE_SIZE = 65_536;
|
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
||||||
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||||
@@ -386,10 +386,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
||||||
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
||||||
engine.getDimension().getTerrainSamplingStep());
|
engine.getDimension().getTerrainSamplingStep());
|
||||||
baseTerrainHeightStream = GenerationStreams.cache2DDouble(ProceduralStream.of(
|
baseTerrainHeightStream = GenerationStreams.cache2DDouble(
|
||||||
this::sampleUnblendedNaturalTerrainHeight,
|
ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight),
|
||||||
Interpolated.DOUBLE
|
"baseTerrainHeightStream", engine, cacheSize);
|
||||||
), "baseTerrainHeightStream", engine, cacheSize);
|
|
||||||
boolean terrain3DEnabled = false;
|
boolean terrain3DEnabled = false;
|
||||||
for (IrisBiome biome : generatorBiomes) {
|
for (IrisBiome biome : generatorBiomes) {
|
||||||
if (biome.getTerrain3D() != null) {
|
if (biome.getTerrain3D() != null) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.runtime.IrisEngine;
|
|||||||
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
|
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
|
||||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
|
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
|
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositPlacementScope;
|
import art.arcane.iris.generation.decoration.IrisDepositPlacementScope;
|
||||||
@@ -45,15 +46,20 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.iris.generation.block.B;
|
import art.arcane.iris.generation.block.B;
|
||||||
import art.arcane.iris.generation.block.VectorMap;
|
import art.arcane.iris.generation.block.VectorMap;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.loading.IrisRegistrant;
|
||||||
|
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState> {
|
public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||||
private static final int CLUMPS_PER_BATCH = 8;
|
private static final int CLUMPS_PER_BATCH = 8;
|
||||||
private static final int PREPARATION_BATCH_COUNT = 4;
|
private static final int PREPARATION_BATCH_COUNT = 4;
|
||||||
|
|
||||||
private final RNG rng;
|
private final RNG rng;
|
||||||
|
private volatile DepositVariantScan variantScan;
|
||||||
|
|
||||||
public IrisDepositModifier(Engine engine) {
|
public IrisDepositModifier(Engine engine) {
|
||||||
super(engine, "Deposit");
|
super(engine, "Deposit");
|
||||||
@@ -91,7 +97,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
Throwable callerFailure = null;
|
Throwable callerFailure = null;
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < generators.size(); i++) {
|
for (int i = 0; i < generators.size(); i++) {
|
||||||
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
|
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)), context);
|
||||||
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
||||||
int firstAttempt = first;
|
int firstAttempt = first;
|
||||||
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
||||||
@@ -163,20 +169,48 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
|
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
|
||||||
DepositPlan plan = plan(k, rng);
|
DepositPlan plan = plan(k, rng, context);
|
||||||
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
||||||
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
||||||
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
|
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DepositPlan plan(IrisDepositGenerator generator, RNG rng) {
|
private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) {
|
||||||
if (generator.getSpawnChance() < rng.d()) {
|
if (generator.getSpawnChance() < rng.d()) {
|
||||||
return new DepositPlan(generator, rng.getSeed(), false, 0);
|
return new DepositPlan(generator, rng.getSeed(), false, 0);
|
||||||
}
|
}
|
||||||
boolean ore = generator.isOre(getData());
|
boolean ore = generator.isOre(getData());
|
||||||
int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1);
|
int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1);
|
||||||
return new DepositPlan(generator, rng.getSeed(), ore, attempts);
|
return new DepositPlan(generator, rng.getSeed(), ore,
|
||||||
|
attempts == 0 || surfaceBiomeMayMatch(generator, context) ? attempts : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A surface-scoped biome filter reads the surface biome of the clump's own column, so when no column of the
|
||||||
|
* chunk passes it every clump is rejected at that check. Each clump draws from its own seeded RNG, so skipping
|
||||||
|
* them all up front changes nothing else.
|
||||||
|
*/
|
||||||
|
private static boolean surfaceBiomeMayMatch(IrisDepositGenerator generator, ChunkContext context) {
|
||||||
|
if (generator.getBiomeScope() != IrisDepositBiomeScope.SURFACE
|
||||||
|
|| (generator.getIncludedBiomes() == null || generator.getIncludedBiomes().isEmpty())
|
||||||
|
&& (generator.getExcludedBiomes() == null || generator.getExcludedBiomes().isEmpty())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
IrisBiome checked = null;
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
IrisBiome surface = context.getBiome().get(x, z);
|
||||||
|
if (surface == checked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (generator.matchesBiome(surface, null)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
checked = surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
|
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
|
||||||
@@ -185,15 +219,18 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
|
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
|
||||||
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
|
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
|
||||||
List<PreparedClump> clumps = new ArrayList<>(limit - first);
|
List<PreparedClump> clumps = new ArrayList<>(limit - first);
|
||||||
|
int terrainLimit = Integer.MIN_VALUE;
|
||||||
for (int l = first; l < limit; l++) {
|
for (int l = first; l < limit; l++) {
|
||||||
RNG clumpRng = new RNG(clumpSeed(plan.seed(), l));
|
long clumpSeed = clumpSeed(plan.seed(), l);
|
||||||
|
RNG clumpRng = new RNG(clumpSeed);
|
||||||
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
|
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisObject clump = k.getClump(getEngine(), clumpRng, getData());
|
IrisDepositGenerator.ClumpFootprint footprint = k.sampleClumpFootprint(clumpRng, getData());
|
||||||
|
IrisObject clump = footprint == null ? k.getClump(getEngine(), clumpRng, getData()) : null;
|
||||||
|
|
||||||
int dim = clump.getW();
|
int dim = footprint == null ? clump.getW() : footprint.width();
|
||||||
int min = dim / 2;
|
int min = dim / 2;
|
||||||
int max = (int) (16D - dim / 2D);
|
int max = (int) (16D - dim / 2D);
|
||||||
|
|
||||||
@@ -257,6 +294,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clump == null) {
|
||||||
|
if (k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN && !footprint.empty()) {
|
||||||
|
if (terrainLimit == Integer.MIN_VALUE) {
|
||||||
|
terrainLimit = chunkTerrainLimit(cx, cz, he, context, k.getSurfaceClearance());
|
||||||
|
}
|
||||||
|
if (y + footprint.minY() > terrainLimit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RNG replay = new RNG(clumpSeed);
|
||||||
|
replay.d();
|
||||||
|
clump = k.getClump(getEngine(), replay, getData());
|
||||||
|
}
|
||||||
|
|
||||||
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
|
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
|
||||||
}
|
}
|
||||||
return new PreparedDeposit(k, oreDeposit, clumps);
|
return new PreparedDeposit(k, oreDeposit, clumps);
|
||||||
@@ -271,6 +322,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
boolean oreDeposit = deposit.ore();
|
boolean oreDeposit = deposit.ore();
|
||||||
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
|
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
|
||||||
IrisDimension dimension = getDimension();
|
IrisDimension dimension = getDimension();
|
||||||
|
boolean variants = depositVariantsPossible();
|
||||||
for (PreparedClump prepared : deposit.clumps()) {
|
for (PreparedClump prepared : deposit.clumps()) {
|
||||||
IrisObject clump = prepared.object();
|
IrisObject clump = prepared.object();
|
||||||
int x = prepared.x();
|
int x = prepared.x();
|
||||||
@@ -325,8 +377,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
|
|
||||||
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
|
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
|
||||||
NativeBlockState ore = cursor.value();
|
NativeBlockState ore = cursor.value();
|
||||||
NativeBlockState remapped = resolveDepositVariant(
|
NativeBlockState remapped = variants ? resolveDepositVariant(
|
||||||
cx, cz, nx, ny, nz, ore, dimension, context, carvingState);
|
cx, cz, nx, ny, nz, ore, dimension, context, carvingState) : null;
|
||||||
NativeBlockState finalBlock = remapped != null
|
NativeBlockState finalBlock = remapped != null
|
||||||
? remapped
|
? remapped
|
||||||
: B.toDeepSlateOre(current, ore);
|
: B.toDeepSlateOre(current, ore);
|
||||||
@@ -344,6 +396,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
: context.getRoundedHeight(localX, localZ);
|
: context.getRoundedHeight(localX, localZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The highest block a TERRAIN-scoped deposit may place anywhere in the chunk; a clump whose lowest block sits
|
||||||
|
* above it would have every block rejected by {@link #placementSurfaceAllows}.
|
||||||
|
*/
|
||||||
|
private int chunkTerrainLimit(int cx, int cz, HeightMap he, ChunkContext context, int surfaceClearance) {
|
||||||
|
int highest = Integer.MIN_VALUE;
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
highest = Math.max(highest, getDepositTerrainSurface(cx, cz, x, z, he, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highest - Math.max(0, surfaceClearance);
|
||||||
|
}
|
||||||
|
|
||||||
static int depositSurfaceLimit(int surfaceY) {
|
static int depositSurfaceLimit(int surfaceY) {
|
||||||
return depositSurfaceLimit(surfaceY, 7);
|
return depositSurfaceLimit(surfaceY, 7);
|
||||||
}
|
}
|
||||||
@@ -460,6 +526,51 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
return Math.max(minimum, Math.min(center, maximum));
|
return Math.max(minimum, Math.min(center, maximum));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The per-block variant lookup resolves a cave biome for every placed ore, but it can only ever match when
|
||||||
|
* some biome, region or the dimension of the loaded pack declares variants. Anything the scan cannot
|
||||||
|
* enumerate (stacked dimensions, unreadable loaders) keeps the lookup.
|
||||||
|
*/
|
||||||
|
private boolean depositVariantsPossible() {
|
||||||
|
IrisData data = getData();
|
||||||
|
DepositVariantScan scan = variantScan;
|
||||||
|
if (scan == null || scan.data() != data) {
|
||||||
|
scan = new DepositVariantScan(data, scanDepositVariants(data));
|
||||||
|
variantScan = scan;
|
||||||
|
}
|
||||||
|
return scan.possible();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean scanDepositVariants(IrisData data) {
|
||||||
|
IrisDimension dimension = getDimension();
|
||||||
|
if (data == null || dimension == null || getEngine().getDimensionStackContext() != null
|
||||||
|
|| dimension.getDepositVariants() == null || !dimension.getDepositVariants().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Boolean regions = declaresVariants(data.getRegionLoader(), IrisRegion::getDepositVariants);
|
||||||
|
Boolean biomes = declaresVariants(data.getBiomeLoader(), IrisBiome::getDepositVariants);
|
||||||
|
return regions == null || regions || biomes == null || biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends IrisRegistrant> Boolean declaresVariants(ResourceLoader<T> loader,
|
||||||
|
Function<T, ? extends List<IrisDepositVariant>> variants) {
|
||||||
|
String[] keys = loader == null ? null : loader.getPossibleKeys();
|
||||||
|
if (keys == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String key : keys) {
|
||||||
|
T loaded = loader.load(key);
|
||||||
|
if (loaded == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<IrisDepositVariant> declared = variants.apply(loaded);
|
||||||
|
if (declared == null || !declared.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private NativeBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, NativeBlockState ore, IrisDimension dimension, ChunkContext context, IrisDimensionCarvingResolver.State carvingState) {
|
private NativeBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, NativeBlockState ore, IrisDimension dimension, ChunkContext context, IrisDimensionCarvingResolver.State carvingState) {
|
||||||
int worldX = (cx << 4) + nx;
|
int worldX = (cx << 4) + nx;
|
||||||
int worldZ = (cz << 4) + nz;
|
int worldZ = (cz << 4) + nz;
|
||||||
@@ -548,6 +659,9 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
|||||||
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
|
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record DepositVariantScan(IrisData data, boolean possible) {
|
||||||
|
}
|
||||||
|
|
||||||
private record PreparedDeposit(IrisDepositGenerator generator, boolean ore, List<PreparedClump> clumps) {
|
private record PreparedDeposit(IrisDepositGenerator generator, boolean ore, List<PreparedClump> clumps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package art.arcane.iris.generation.terrain;
|
package art.arcane.iris.generation.terrain;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.cache.ConcurrentClockCache;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||||
import art.arcane.volmlib.util.noise.CNG;
|
import art.arcane.volmlib.util.noise.CNG;
|
||||||
import art.arcane.volmlib.util.cache.CacheKey;
|
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
@@ -16,9 +15,10 @@ import java.util.concurrent.atomic.AtomicReferenceArray;
|
|||||||
|
|
||||||
public final class Terrain3DRuntime {
|
public final class Terrain3DRuntime {
|
||||||
private static final int STEP = 4;
|
private static final int STEP = 4;
|
||||||
private static final int CACHE_STRIPES = 16;
|
private static final int MINIMUM_COLUMNS = 16;
|
||||||
|
private static final int ANCHOR_BLOCK_SIZE = 16;
|
||||||
private static final int MAXIMUM_ANCHORS = 32_768;
|
private static final int MAXIMUM_ANCHORS = 32_768;
|
||||||
private static final long ANCHOR_CACHE_BYTES = 64L * 1024 * 1024;
|
private static final long ANCHOR_CACHE_BYTES = 128L * 1024 * 1024;
|
||||||
private static final long DENSITY_SALT = 0x536E4A11C924B3D7L;
|
private static final long DENSITY_SALT = 0x536E4A11C924B3D7L;
|
||||||
private static final long CRACK_SALT = 0x7839A16DC4052EFBL;
|
private static final long CRACK_SALT = 0x7839A16DC4052EFBL;
|
||||||
private static final NodeSample EMPTY_SAMPLE = new NodeSample(0D, 0D, 0D);
|
private static final NodeSample EMPTY_SAMPLE = new NodeSample(0D, 0D, 0D);
|
||||||
@@ -28,10 +28,11 @@ public final class Terrain3DRuntime {
|
|||||||
private final Sources sources;
|
private final Sources sources;
|
||||||
private final Options options;
|
private final Options options;
|
||||||
private final NoiseFactory noiseFactory;
|
private final NoiseFactory noiseFactory;
|
||||||
private final BoundedCache<Terrain3DFragmentFilter.DensityColumn> columns;
|
private final BlockCache<Terrain3DFragmentFilter.DensityColumn> columns;
|
||||||
private final BoundedCache<Anchor> anchors;
|
private final BlockCache<Anchor> anchors;
|
||||||
private final Terrain3DFragmentFilter fragments;
|
private final Terrain3DFragmentFilter fragments;
|
||||||
private final Map<IrisTerrain3D, CompiledProfile> profiles = new IdentityHashMap<>();
|
private final Object profileLock = new Object();
|
||||||
|
private volatile Map<IrisTerrain3D, CompiledProfile> profiles = new IdentityHashMap<>();
|
||||||
private final ThreadLocal<ColumnMemo> localColumn = ThreadLocal.withInitial(ColumnMemo::new);
|
private final ThreadLocal<ColumnMemo> localColumn = ThreadLocal.withInitial(ColumnMemo::new);
|
||||||
private volatile Object cacheGeneration = new Object();
|
private volatile Object cacheGeneration = new Object();
|
||||||
|
|
||||||
@@ -46,8 +47,8 @@ public final class Terrain3DRuntime {
|
|||||||
this.sources = Objects.requireNonNull(sources, "Terrain sources");
|
this.sources = Objects.requireNonNull(sources, "Terrain sources");
|
||||||
this.options = Objects.requireNonNull(options, "Terrain options");
|
this.options = Objects.requireNonNull(options, "Terrain options");
|
||||||
this.noiseFactory = Objects.requireNonNull(noiseFactory, "Terrain noise factory");
|
this.noiseFactory = Objects.requireNonNull(noiseFactory, "Terrain noise factory");
|
||||||
columns = new BoundedCache<>(options.maximumColumns());
|
columns = new BlockCache<>(options.maximumColumns(), 0);
|
||||||
anchors = new BoundedCache<>(anchorCacheCapacity(options.height(), options.maximumColumns()));
|
anchors = new BlockCache<>(anchorCacheCapacity(options.height(), options.maximumColumns()), 2);
|
||||||
fragments = new Terrain3DFragmentFilter(this::rawColumn);
|
fragments = new Terrain3DFragmentFilter(this::rawColumn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ public final class Terrain3DRuntime {
|
|||||||
long bytesPerAnchor = 160L + 48L * (Math.ceilDiv((long) height, STEP) + 1);
|
long bytesPerAnchor = 160L + 48L * (Math.ceilDiv((long) height, STEP) + 1);
|
||||||
long maximumEntries = Math.min(MAXIMUM_ANCHORS,
|
long maximumEntries = Math.min(MAXIMUM_ANCHORS,
|
||||||
Math.min((long) maximumColumns * 8, ANCHOR_CACHE_BYTES / bytesPerAnchor));
|
Math.min((long) maximumColumns * 8, ANCHOR_CACHE_BYTES / bytesPerAnchor));
|
||||||
return Math.max(CACHE_STRIPES, (int) (maximumEntries / CACHE_STRIPES) * CACHE_STRIPES);
|
return Math.max(ANCHOR_BLOCK_SIZE, (int) (maximumEntries / ANCHOR_BLOCK_SIZE) * ANCHOR_BLOCK_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public double height(int x, int z) {
|
public double height(int x, int z) {
|
||||||
@@ -94,8 +95,8 @@ public final class Terrain3DRuntime {
|
|||||||
columns.clear();
|
columns.clear();
|
||||||
anchors.clear();
|
anchors.clear();
|
||||||
fragments.clear();
|
fragments.clear();
|
||||||
synchronized (profiles) {
|
synchronized (profileLock) {
|
||||||
profiles.clear();
|
profiles = new IdentityHashMap<>();
|
||||||
}
|
}
|
||||||
cacheGeneration = new Object();
|
cacheGeneration = new Object();
|
||||||
localColumn.remove();
|
localColumn.remove();
|
||||||
@@ -106,9 +107,8 @@ public final class Terrain3DRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Terrain3DFragmentFilter.DensityColumn rawColumn(int x, int z) {
|
private Terrain3DFragmentFilter.DensityColumn rawColumn(int x, int z) {
|
||||||
long key = pack(x, z);
|
Terrain3DFragmentFilter.DensityColumn cached = columns.get(x, z);
|
||||||
Terrain3DFragmentFilter.DensityColumn cached = columns.get(key);
|
return cached == null ? columns.putIfAbsent(x, z,
|
||||||
return cached == null ? columns.putIfAbsent(key,
|
|
||||||
new Terrain3DFragmentFilter.DensityColumn(createColumn(x, z))) : cached;
|
new Terrain3DFragmentFilter.DensityColumn(createColumn(x, z))) : cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,9 +196,8 @@ public final class Terrain3DRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Anchor anchor(int x, int z) {
|
private Anchor anchor(int x, int z) {
|
||||||
long key = pack(x, z);
|
Anchor cached = anchors.get(x, z);
|
||||||
Anchor cached = anchors.get(key);
|
return cached == null ? anchors.putIfAbsent(x, z, createAnchor(x, z)) : cached;
|
||||||
return cached == null ? anchors.putIfAbsent(key, createAnchor(x, z)) : cached;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Anchor createAnchor(int x, int z) {
|
private Anchor createAnchor(int x, int z) {
|
||||||
@@ -227,19 +226,19 @@ public final class Terrain3DRuntime {
|
|||||||
if (config == null || !config.isEnabled()) {
|
if (config == null || !config.isEnabled()) {
|
||||||
return DISABLED;
|
return DISABLED;
|
||||||
}
|
}
|
||||||
synchronized (profiles) {
|
|
||||||
CompiledProfile cached = profiles.get(config);
|
CompiledProfile cached = profiles.get(config);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
CompiledProfile compiled = compile(config);
|
CompiledProfile compiled = compile(config);
|
||||||
synchronized (profiles) {
|
synchronized (profileLock) {
|
||||||
CompiledProfile existing = profiles.get(config);
|
CompiledProfile existing = profiles.get(config);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
profiles.put(config, compiled);
|
Map<IrisTerrain3D, CompiledProfile> updated = new IdentityHashMap<>(profiles);
|
||||||
|
updated.put(config, compiled);
|
||||||
|
profiles = updated;
|
||||||
return compiled;
|
return compiled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,7 +313,7 @@ public final class Terrain3DRuntime {
|
|||||||
boolean enabled, int maximumColumns) {
|
boolean enabled, int maximumColumns) {
|
||||||
public Options {
|
public Options {
|
||||||
if (height < 2 || height > 4096 || !Double.isFinite(fluidHeight)
|
if (height < 2 || height > 4096 || !Double.isFinite(fluidHeight)
|
||||||
|| maximumColumns < CACHE_STRIPES) {
|
|| maximumColumns < MINIMUM_COLUMNS) {
|
||||||
throw new IllegalArgumentException("Invalid volumetric terrain runtime options");
|
throw new IllegalArgumentException("Invalid volumetric terrain runtime options");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,74 +407,61 @@ public final class Terrain3DRuntime {
|
|||||||
double fluidFade, NoiseSource density, NoiseSource cracks) {
|
double fluidFade, NoiseSource density, NoiseSource cracks) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class BoundedCache<T> {
|
/**
|
||||||
private final CacheStripe<T>[] stripes;
|
* Groups the 16x16 block area of one chunk into a single cache entry, so neighbouring lookups share one
|
||||||
|
* lock-free set probe and a working set of whole chunks is evicted together.
|
||||||
|
*/
|
||||||
|
private static final class BlockCache<T> {
|
||||||
|
private final ConcurrentClockCache<AtomicReferenceArray<T>> blocks;
|
||||||
|
private final int cellShift;
|
||||||
|
private final int axisMask;
|
||||||
|
private final int axisBits;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
private BlockCache(int maximumEntries, int cellShift) {
|
||||||
private BoundedCache(int maximumSize) {
|
this.cellShift = cellShift;
|
||||||
stripes = (CacheStripe<T>[]) new CacheStripe<?>[CACHE_STRIPES];
|
axisBits = 4 - cellShift;
|
||||||
for (int index = 0; index < stripes.length; index++) {
|
axisMask = (1 << axisBits) - 1;
|
||||||
stripes[index] = new CacheStripe<>(Math.ceilDiv(maximumSize, CACHE_STRIPES));
|
int blockSize = 1 << (axisBits * 2);
|
||||||
}
|
blocks = new ConcurrentClockCache<>(Math.ceilDiv(maximumEntries, blockSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
private T get(long key) {
|
private T get(int x, int z) {
|
||||||
return stripe(key).get(key);
|
AtomicReferenceArray<T> block = blocks.get(blockKey(x, z));
|
||||||
|
return block == null ? null : block.getAcquire(slot(x, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
private T putIfAbsent(long key, T value) {
|
private T putIfAbsent(int x, int z, T value) {
|
||||||
return stripe(key).putIfAbsent(key, value);
|
long key = blockKey(x, z);
|
||||||
|
AtomicReferenceArray<T> block = blocks.get(key);
|
||||||
|
if (block == null) {
|
||||||
|
block = blocks.putIfAbsent(key, new AtomicReferenceArray<>(1 << (axisBits * 2)));
|
||||||
|
}
|
||||||
|
T existing = block.compareAndExchange(slot(x, z), null, value);
|
||||||
|
return existing == null ? value : existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clear() {
|
private void clear() {
|
||||||
for (CacheStripe<T> stripe : stripes) {
|
blocks.clear();
|
||||||
stripe.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int size() {
|
private int size() {
|
||||||
int size = 0;
|
int[] size = new int[1];
|
||||||
for (CacheStripe<T> stripe : stripes) {
|
blocks.forEach(block -> {
|
||||||
size += stripe.size();
|
for (int index = 0; index < block.length(); index++) {
|
||||||
|
if (block.getAcquire(index) != null) {
|
||||||
|
size[0]++;
|
||||||
}
|
}
|
||||||
return size;
|
}
|
||||||
|
});
|
||||||
|
return size[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private CacheStripe<T> stripe(long key) {
|
private static long blockKey(int x, int z) {
|
||||||
return stripes[(int) CacheKey.mix(key) & (CACHE_STRIPES - 1)];
|
return pack(x >> 4, z >> 4);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CacheStripe<T> {
|
private int slot(int x, int z) {
|
||||||
private final int maximumSize;
|
return ((x >> cellShift) & axisMask) << axisBits | ((z >> cellShift) & axisMask);
|
||||||
private final Long2ObjectLinkedOpenHashMap<T> entries = new Long2ObjectLinkedOpenHashMap<>(16);
|
|
||||||
|
|
||||||
private CacheStripe(int maximumSize) {
|
|
||||||
this.maximumSize = maximumSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized T get(long key) {
|
|
||||||
return entries.getAndMoveToLast(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized T putIfAbsent(long key, T value) {
|
|
||||||
T existing = entries.getAndMoveToLast(key);
|
|
||||||
if (existing != null) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
entries.putAndMoveToLast(key, value);
|
|
||||||
if (entries.size() > maximumSize) {
|
|
||||||
entries.removeFirst();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void clear() {
|
|
||||||
entries.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized int size() {
|
|
||||||
return entries.size();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package art.arcane.iris.generation.cache;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ConcurrentClockCacheTest {
|
||||||
|
@Test
|
||||||
|
public void firstPublishedValueWins() {
|
||||||
|
ConcurrentClockCache<Object> cache = new ConcurrentClockCache<>(16);
|
||||||
|
Object first = new Object();
|
||||||
|
assertNull(cache.get(7L));
|
||||||
|
assertSame(first, cache.putIfAbsent(7L, first));
|
||||||
|
assertSame(first, cache.putIfAbsent(7L, new Object()));
|
||||||
|
assertSame(first, cache.get(7L));
|
||||||
|
cache.clear();
|
||||||
|
assertNull(cache.get(7L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void capacityBoundsRetainedEntries() {
|
||||||
|
ConcurrentClockCache<Long> cache = new ConcurrentClockCache<>(64);
|
||||||
|
for (long key = 0; key < 10_000; key++) {
|
||||||
|
assertEquals(Long.valueOf(key), cache.putIfAbsent(key, key));
|
||||||
|
}
|
||||||
|
AtomicInteger retained = new AtomicInteger();
|
||||||
|
cache.forEach(value -> {
|
||||||
|
retained.incrementAndGet();
|
||||||
|
assertEquals(value, cache.get(value));
|
||||||
|
});
|
||||||
|
assertEquals(cache.capacity(), retained.get());
|
||||||
|
assertTrue(cache.capacity() >= 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void referencedEntriesSurviveAStreamOfOneTimeKeys() {
|
||||||
|
ConcurrentClockCache<Long> cache = new ConcurrentClockCache<>(4);
|
||||||
|
cache.putIfAbsent(-1L, -1L);
|
||||||
|
for (long key = 0; key < 1_000; key++) {
|
||||||
|
assertEquals(Long.valueOf(-1L), cache.get(-1L));
|
||||||
|
cache.putIfAbsent(key, key);
|
||||||
|
}
|
||||||
|
assertEquals(Long.valueOf(-1L), cache.get(-1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentWritersAgreeOnOneValuePerKey() throws Exception {
|
||||||
|
ConcurrentClockCache<Object> cache = new ConcurrentClockCache<>(1 << 12);
|
||||||
|
List<Future<Object[]>> results = new ArrayList<>();
|
||||||
|
try (ExecutorService executor = Executors.newFixedThreadPool(8)) {
|
||||||
|
for (int thread = 0; thread < 8; thread++) {
|
||||||
|
results.add(executor.submit(() -> {
|
||||||
|
Object[] seen = new Object[256];
|
||||||
|
for (int key = 0; key < seen.length; key++) {
|
||||||
|
seen[key] = cache.putIfAbsent(key, new Object());
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Object[] reference = results.getFirst().get(10, TimeUnit.SECONDS);
|
||||||
|
for (Future<Object[]> result : results) {
|
||||||
|
Object[] seen = result.get(10, TimeUnit.SECONDS);
|
||||||
|
for (int key = 0; key < seen.length; key++) {
|
||||||
|
Object cached = cache.get(key);
|
||||||
|
if (cached != null) {
|
||||||
|
assertSame(cached, seen[key]);
|
||||||
|
assertSame(reference[key], seen[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.decoration;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.block.IrisBlockData;
|
import art.arcane.iris.generation.block.IrisBlockData;
|
||||||
|
import art.arcane.iris.generation.block.VectorMap;
|
||||||
|
import art.arcane.iris.structure.object.IrisObject;
|
||||||
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.volmlib.util.documentation.Description;
|
import art.arcane.volmlib.util.documentation.Description;
|
||||||
@@ -224,6 +226,38 @@ public class IrisDepositTuningTest {
|
|||||||
assertNotEquals(17L * samples, largeBlocks);
|
assertNotEquals(17L * samples, largeBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ellipsoidFootprintConsumesTheBuildDrawsAndMatchesItsGeometry() {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
IrisDepositGenerator generator = generatorWithState(data, true);
|
||||||
|
generator.setShape(IrisDepositShape.VANILLA_ELLIPSOID);
|
||||||
|
for (int size : new int[]{0, 1, 3, 4, 9, 17, 25, 64}) {
|
||||||
|
generator.setMinSize(size).setMaxSize(size);
|
||||||
|
for (long seed = 0; seed < 300; seed++) {
|
||||||
|
RNG built = new RNG(seed);
|
||||||
|
IrisObject clump = generator.getClump(null, built, data);
|
||||||
|
RNG sampled = new RNG(seed);
|
||||||
|
IrisDepositGenerator.ClumpFootprint footprint = generator.sampleClumpFootprint(sampled, data);
|
||||||
|
assertNotNull(footprint);
|
||||||
|
assertEquals(clump.getW(), footprint.width());
|
||||||
|
assertEquals(clump.getBlocks().size() == 0, footprint.empty());
|
||||||
|
if (!footprint.empty()) {
|
||||||
|
int minimumY = Integer.MAX_VALUE;
|
||||||
|
VectorMap<NativeBlockState>.Cursor cursor = clump.getBlocks().cursor();
|
||||||
|
while (cursor.next()) {
|
||||||
|
minimumY = Math.min(minimumY, cursor.key().getBlockY());
|
||||||
|
}
|
||||||
|
assertEquals(minimumY, footprint.minY());
|
||||||
|
}
|
||||||
|
assertEquals(built.nextLong(), sampled.nextLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generator.setMinSize(65).setMaxSize(65);
|
||||||
|
assertNull(generator.sampleClumpFootprint(new RNG(1L), data));
|
||||||
|
generator.setShape(IrisDepositShape.VANILLA_SCATTERED).setMinSize(4).setMaxSize(4);
|
||||||
|
assertNull(generator.sampleClumpFootprint(new RNG(1L), data));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void vanillaScatteredSizeIsAnUpperCandidateBound() {
|
public void vanillaScatteredSizeIsAnUpperCandidateBound() {
|
||||||
IrisData data = mock(IrisData.class);
|
IrisData data = mock(IrisData.class);
|
||||||
|
|||||||
+34
@@ -1038,6 +1038,40 @@ public class IrisCaveCarver3DNearParityTest {
|
|||||||
largestPlane < 192);
|
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 {
|
private void assertExactParity(boolean warp, boolean modules, boolean adaptiveSampling) throws Exception {
|
||||||
Engine engine = createEngine(96, 90);
|
Engine engine = createEngine(96, 90);
|
||||||
double[] columnWeights = fullWeights();
|
double[] columnWeights = fullWeights();
|
||||||
|
|||||||
+87
-2
@@ -5,12 +5,14 @@ import art.arcane.iris.testsupport.KeyedBlockState;
|
|||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
|
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
|
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositShape;
|
import art.arcane.iris.generation.decoration.IrisDepositShape;
|
||||||
import art.arcane.iris.generation.decoration.IrisDepositVariant;
|
import art.arcane.iris.generation.decoration.IrisDepositVariant;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.structure.object.IrisObject;
|
||||||
import art.arcane.iris.spi.IrisPlatform;
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
@@ -25,17 +27,21 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
|||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
@@ -84,8 +90,38 @@ public class IrisDepositModifierParityTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@Test
|
||||||
|
public void variantFreePacksSkipPerBlockCaveBiomeLookupsWithIdenticalOutput() {
|
||||||
|
for (long seed : new long[]{101L, -73L, 90181L}) {
|
||||||
|
AtomicInteger scannedLookups = new AtomicInteger();
|
||||||
|
AtomicInteger unscannedLookups = new AtomicInteger();
|
||||||
|
long[] scanned = generateConfigured(seed, false, false, true, scannedLookups);
|
||||||
|
long[] unscanned = generateConfigured(seed, false, false, false, unscannedLookups);
|
||||||
|
assertArrayEquals("seed=" + seed, unscanned, scanned);
|
||||||
|
assertTrue(scannedLookups.get() < unscannedLookups.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unmatchableSurfaceBiomeFiltersSkipClumpsWithoutChangingOutput() {
|
||||||
|
long[] expected = {-4609692219192155345L, 784L, 2118L, 53L, 67L, 11L};
|
||||||
|
AtomicInteger clumps = new AtomicInteger();
|
||||||
|
assertArrayEquals(expected, generateConfigured(101L, false, true, false, new AtomicInteger(), clumps));
|
||||||
|
assertEquals(0, clumps.get());
|
||||||
|
}
|
||||||
|
|
||||||
private long[] generateConfigured(long seed, boolean multicore) {
|
private long[] generateConfigured(long seed, boolean multicore) {
|
||||||
|
return generateConfigured(seed, multicore, true, false, new AtomicInteger());
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
|
||||||
|
AtomicInteger caveBiomeLookups) {
|
||||||
|
return generateConfigured(seed, multicore, caveVariant, enumerablePack, caveBiomeLookups, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
|
||||||
|
AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps) {
|
||||||
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||||
ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS);
|
ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS);
|
||||||
IrisDimension dimension = new IrisDimension();
|
IrisDimension dimension = new IrisDimension();
|
||||||
@@ -99,7 +135,9 @@ public class IrisDepositModifierParityTest {
|
|||||||
denied.setLoadKey("denied");
|
denied.setLoadKey("denied");
|
||||||
IrisDepositVariant variant = new IrisDepositVariant().setMinHeight(-64).setMaxHeight(64);
|
IrisDepositVariant variant = new IrisDepositVariant().setMinHeight(-64).setMaxHeight(64);
|
||||||
variant.getRemap().put("minecraft:gold_ore", "minecraft:redstone_ore");
|
variant.getRemap().put("minecraft:gold_ore", "minecraft:redstone_ore");
|
||||||
|
if (caveVariant) {
|
||||||
cave.getDepositVariants().add(variant);
|
cave.getDepositVariants().add(variant);
|
||||||
|
}
|
||||||
dimension.getDeposits().add(generator("granite", IrisDepositShape.IRIS, 25, 70,
|
dimension.getDeposits().add(generator("granite", IrisDepositShape.IRIS, 25, 70,
|
||||||
IrisDepositHeightDistribution.CLIPPED_UNIFORM, "stone", "deepslate"));
|
IrisDepositHeightDistribution.CLIPPED_UNIFORM, "stone", "deepslate"));
|
||||||
region.getDeposits().add(generator("iron_ore", IrisDepositShape.VANILLA_ELLIPSOID, 20, 110,
|
region.getDeposits().add(generator("iron_ore", IrisDepositShape.VANILLA_ELLIPSOID, 20, 110,
|
||||||
@@ -114,11 +152,37 @@ public class IrisDepositModifierParityTest {
|
|||||||
gold.setExcludedBiomes(new KList<>("allowed"));
|
gold.setExcludedBiomes(new KList<>("allowed"));
|
||||||
gold.setDiscardChanceOnAirExposure(0.3D);
|
gold.setDiscardChanceOnAirExposure(0.3D);
|
||||||
surface.getDeposits().add(gold);
|
surface.getDeposits().add(gold);
|
||||||
|
if (unmatchableClumps != null) {
|
||||||
|
IrisDepositGenerator unmatchable = new CountingPaletteGenerator(state("emerald_ore"), unmatchableClumps)
|
||||||
|
.setShape(IrisDepositShape.VANILLA_ELLIPSOID).setMinSize(9).setMaxSize(9)
|
||||||
|
.setMinPerChunk(90).setMaxPerChunk(90).setMinHeight(-4).setMaxHeight(44)
|
||||||
|
.setHeightDistribution(IrisDepositHeightDistribution.UNIFORM).setSurfaceClearance(0)
|
||||||
|
.setReplaceableBlocks(new KList<>("stone"))
|
||||||
|
.setBiomeScope(IrisDepositBiomeScope.SURFACE)
|
||||||
|
.setIncludedBiomes(new KList<>("elsewhere"));
|
||||||
|
surface.getDeposits().add(unmatchable);
|
||||||
|
}
|
||||||
when(engine.getHeight()).thenReturn(48);
|
when(engine.getHeight()).thenReturn(48);
|
||||||
when(engine.getMinHeight()).thenReturn(-64);
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
when(engine.getSeedManager().getDeposit()).thenReturn(seed);
|
when(engine.getSeedManager().getDeposit()).thenReturn(seed);
|
||||||
when(engine.getDimension()).thenReturn(dimension);
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
if (enumerablePack) {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
ResourceLoader<IrisBiome> biomes = mock(ResourceLoader.class);
|
||||||
|
ResourceLoader<IrisRegion> regions = mock(ResourceLoader.class);
|
||||||
|
when(data.getBiomeLoader()).thenReturn(biomes);
|
||||||
|
when(data.getRegionLoader()).thenReturn(regions);
|
||||||
|
when(biomes.getPossibleKeys()).thenReturn(new String[]{"allowed", "denied", "surface"});
|
||||||
|
when(biomes.load("allowed")).thenReturn(cave);
|
||||||
|
when(biomes.load("denied")).thenReturn(denied);
|
||||||
|
when(biomes.load("surface")).thenReturn(surface);
|
||||||
|
when(regions.getPossibleKeys()).thenReturn(new String[]{"region"});
|
||||||
|
when(regions.load("region")).thenReturn(region);
|
||||||
|
when(engine.getData()).thenReturn(data);
|
||||||
|
when(engine.getDimensionStackContext()).thenReturn(null);
|
||||||
|
}
|
||||||
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenAnswer(invocation -> {
|
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenAnswer(invocation -> {
|
||||||
|
caveBiomeLookups.incrementAndGet();
|
||||||
int x = invocation.getArgument(0);
|
int x = invocation.getArgument(0);
|
||||||
int y = invocation.getArgument(1);
|
int y = invocation.getArgument(1);
|
||||||
int z = invocation.getArgument(2);
|
int z = invocation.getArgument(2);
|
||||||
@@ -366,7 +430,7 @@ public class IrisDepositModifierParityTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class PaletteGenerator extends IrisDepositGenerator {
|
private static class PaletteGenerator extends IrisDepositGenerator {
|
||||||
private final KList<NativeBlockState> blocks;
|
private final KList<NativeBlockState> blocks;
|
||||||
|
|
||||||
private PaletteGenerator(NativeBlockState block) {
|
private PaletteGenerator(NativeBlockState block) {
|
||||||
@@ -378,4 +442,25 @@ public class IrisDepositModifierParityTest {
|
|||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class CountingPaletteGenerator extends PaletteGenerator {
|
||||||
|
private final AtomicInteger clumps;
|
||||||
|
|
||||||
|
private CountingPaletteGenerator(NativeBlockState block, AtomicInteger clumps) {
|
||||||
|
super(block);
|
||||||
|
this.clumps = clumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IrisObject getClump(Engine engine, RNG rng, IrisData data) {
|
||||||
|
clumps.incrementAndGet();
|
||||||
|
return super.getClump(engine, rng, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData data) {
|
||||||
|
clumps.incrementAndGet();
|
||||||
|
return super.sampleClumpFootprint(rng, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -302,7 +302,7 @@ public class Terrain3DRuntimeTest {
|
|||||||
assertEquals(0, capacity % 16);
|
assertEquals(0, capacity % 16);
|
||||||
assertTrue(capacity >= 16);
|
assertTrue(capacity >= 16);
|
||||||
assertTrue(capacity <= 32_768);
|
assertTrue(capacity <= 32_768);
|
||||||
assertTrue(capacity * fullHeightStorage <= 64L * 1024 * 1024);
|
assertTrue(capacity * fullHeightStorage <= 128L * 1024 * 1024);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,9 +354,10 @@ public class Terrain3DRuntimeTest {
|
|||||||
for (int x = -65_536; x < 65_536; x++) {
|
for (int x = -65_536; x < 65_536; x++) {
|
||||||
assertEquals(96, runtime.column(x, -1).topY());
|
assertEquals(96, runtime.column(x, -1).topY());
|
||||||
}
|
}
|
||||||
assertEquals(65_536, runtime.cachedColumnCount());
|
assertTrue(runtime.cachedColumnCount() > 0);
|
||||||
|
assertTrue(runtime.cachedColumnCount() <= 65_536);
|
||||||
assertEquals(96, runtime.column(-65_536, -1).topY());
|
assertEquals(96, runtime.column(-65_536, -1).topY());
|
||||||
assertEquals(65_536, runtime.cachedColumnCount());
|
assertTrue(runtime.cachedColumnCount() <= 65_536);
|
||||||
runtime.clear();
|
runtime.clear();
|
||||||
assertEquals(0, runtime.cachedColumnCount());
|
assertEquals(0, runtime.cachedColumnCount());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user