Merged review fixes

- Heap-sized terrain bake, bounded object rotation bakes, generation-counted bake thread locals, unused bake assembly removed
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 04:48:46 -04:00
commit a25d8ec367
33 files changed
+541 -1194

No files matched your search

+1
View File
@@ -178,6 +178,7 @@ dependencies {
tasks.named('test').configure { tasks.named('test').configure {
maxHeapSize = '1g' maxHeapSize = '1g'
jvmArgs('-XX:+ExitOnOutOfMemoryError') jvmArgs('-XX:+ExitOnOutOfMemoryError')
systemProperty('iris.generation.counters', 'true')
} }
java { java {
@@ -1,7 +0,0 @@
package art.arcane.iris.generation.bake;
public interface BakeCache {
ColumnBake get(int chunkX, int chunkZ);
ColumnBake getIfPresent(int chunkX, int chunkZ);
}
@@ -1,41 +0,0 @@
package art.arcane.iris.generation.bake;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Objects;
public final class BakeRegistry<T> {
private final Object lock = new Object();
private volatile IdentityHashMap<T, Integer> ids = new IdentityHashMap<>();
private volatile Object[] values = new Object[16];
private int size;
public short id(T value) {
Objects.requireNonNull(value, "Registered value");
Integer known = ids.get(value);
if (known != null) {
return known.shortValue();
}
synchronized (lock) {
Integer existing = ids.get(value);
if (existing != null) {
return existing.shortValue();
}
if (size == Short.MAX_VALUE) {
throw new IllegalStateException("Bake registry is full");
}
Object[] grown = size == values.length ? Arrays.copyOf(values, size * 2) : values;
grown[size] = value;
values = grown;
IdentityHashMap<T, Integer> next = new IdentityHashMap<>(ids);
next.put(value, size);
ids = next;
return (short) size++;
}
}
@SuppressWarnings("unchecked")
public T value(int id) {
return (T) values[id];
}
}
@@ -5,47 +5,36 @@ import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.util.Objects; import java.util.Objects;
public final class ChunkBakeCache implements BakeCache { public final class ChunkBakeCache {
public static final int MINIMUM_CAPACITY = 2_048;
static final int MAXIMUM_CAPACITY = 32_768;
static final long BYTES_PER_ENTRY = 13L * 1_024L;
static final int HEAP_SHARE = 32;
private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]); private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]);
private final PlaneSource source; private final PlaneSource source;
private final ConcurrentClockCache<ChunkPlanes> entries;
private final ThreadLocal<Recent> recent = ThreadLocal.withInitial(Recent::new); private final ThreadLocal<Recent> recent = ThreadLocal.withInitial(Recent::new);
private volatile ConcurrentClockCache<ChunkPlanes> entries; private volatile int generation;
public ChunkBakeCache(PlaneSource source, int capacityChunks) { public ChunkBakeCache(PlaneSource source, int capacityChunks) {
this.source = Objects.requireNonNull(source, "Bake plane source"); this.source = Objects.requireNonNull(source, "Bake plane source");
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks)); this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
} }
@Override public static int capacityFor(long maxMemory) {
public ColumnBake get(int chunkX, int chunkZ) { long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY;
return planes(chunkX, chunkZ).columnBake(source); return (int) Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries));
}
@Override
public ColumnBake getIfPresent(int chunkX, int chunkZ) {
ChunkPlanes planes = planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.columnBake();
} }
public int capacity() { public int capacity() {
return entries.capacity(); return entries.capacity();
} }
public synchronized void ensureCapacity(int chunks) { public synchronized void clear() {
ConcurrentClockCache<ChunkPlanes> current = entries; generation++;
if (current.capacity() >= chunks) { entries.forEach(ChunkPlanes::release);
return; entries.clear();
}
ConcurrentClockCache<ChunkPlanes> resized = new ConcurrentClockCache<>(chunks);
current.forEach(planes -> resized.putIfAbsent(key(planes.chunkX(), planes.chunkZ()), planes));
entries = resized;
}
public void clear() {
ConcurrentClockCache<ChunkPlanes> current = entries;
entries = new ConcurrentClockCache<>(current.capacity());
current.clear();
} }
public long[] census() { public long[] census() {
@@ -61,32 +50,19 @@ public final class ChunkBakeCache implements BakeCache {
return counts; return counts;
} }
public ChunkPlanes planes(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries;
long key = key(chunkX, chunkZ);
ChunkPlanes existing = current.get(key);
return existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
}
public ChunkPlanes planesIfPresent(int chunkX, int chunkZ) {
return entries.get(key(chunkX, chunkZ));
}
public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) { public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) {
long key = key(chunkX, chunkZ); long key = key(chunkX, chunkZ);
Recent last = recent.get(); Recent last = recent.get();
ConcurrentClockCache<ChunkPlanes> current = entries; int current = generation;
ChunkPlanes planes; ChunkPlanes planes;
if (last.key == key && last.entries == current) { if (last.key == key && last.generation == current) {
planes = last.planes; planes = last.planes;
} else { } else {
planes = current.get(key); planes = entries.get(key);
if (planes == null) { if (planes == null) {
return null; return null;
} }
last.key = key; last.remember(key, planes, current);
last.planes = planes;
last.entries = current;
} }
return planes.planeIfPresent(plane); return planes.planeIfPresent(plane);
} }
@@ -117,15 +93,16 @@ public final class ChunkBakeCache implements BakeCache {
remembered(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value); remembered(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value);
} }
private ChunkPlanes remembered(int chunkX, int chunkZ) { private ChunkPlanes planes(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries;
long key = key(chunkX, chunkZ); long key = key(chunkX, chunkZ);
ChunkPlanes existing = current.get(key); ChunkPlanes existing = entries.get(key);
ChunkPlanes planes = existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ)); return existing != null ? existing : entries.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
Recent last = recent.get(); }
last.key = key;
last.planes = planes; private ChunkPlanes remembered(int chunkX, int chunkZ) {
last.entries = current; int current = generation;
ChunkPlanes planes = planes(chunkX, chunkZ);
recent.get().remember(key(chunkX, chunkZ), planes, current);
return planes; return planes;
} }
@@ -147,7 +124,13 @@ public final class ChunkBakeCache implements BakeCache {
private static final class Recent { private static final class Recent {
private long key; private long key;
private int generation = -1;
private ChunkPlanes planes; private ChunkPlanes planes;
private ConcurrentClockCache<ChunkPlanes> entries;
private void remember(long key, ChunkPlanes planes, int generation) {
this.key = key;
this.planes = planes;
this.generation = generation;
}
} }
} }
@@ -13,7 +13,6 @@ public final class ChunkPlanes {
private final int chunkX; private final int chunkX;
private final int chunkZ; private final int chunkZ;
private final AtomicReferenceArray<Plane> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length); private final AtomicReferenceArray<Plane> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
private volatile ColumnBake columnBake;
ChunkPlanes(int chunkX, int chunkZ) { ChunkPlanes(int chunkX, int chunkZ) {
this.chunkX = chunkX; this.chunkX = chunkX;
@@ -70,21 +69,10 @@ public final class ChunkPlanes {
} }
} }
ColumnBake columnBake() { void release() {
return columnBake; for (int slot = 0; slot < planes.length(); slot++) {
} planes.setRelease(slot, null);
ColumnBake columnBake(PlaneSource source) {
ColumnBake bake = columnBake;
if (bake != null) {
return bake;
} }
long revision = ProvisionalSampling.revision();
bake = source.assemble(this);
if (revision == ProvisionalSampling.revision()) {
columnBake = bake;
}
return bake;
} }
public static final class Plane { public static final class Plane {
@@ -1,55 +0,0 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
public final class ColumnBake {
public static final int APRON_WIDTH = 18;
public final int chunkX;
public final int chunkZ;
public final short[] regionId;
public final short[] baseBiomeId;
public final short[] trueBiomeId;
public final short[] caveBiomeId;
public final double[] baseHeight;
public final double[] naturalHeight;
public final double[] height;
public final int[] roundedHeight;
public final float[] fluidSurface;
public final short[] rockState;
public final short[] fluidState;
public final Terrain3DColumn[] terrainColumns;
public final double[] apronHeight;
public ColumnBake(Planes planes) {
this.chunkX = planes.chunkX();
this.chunkZ = planes.chunkZ();
this.regionId = planes.regionId();
this.baseBiomeId = planes.baseBiomeId();
this.trueBiomeId = planes.trueBiomeId();
this.caveBiomeId = planes.caveBiomeId();
this.baseHeight = planes.baseHeight();
this.naturalHeight = planes.naturalHeight();
this.height = planes.height();
this.roundedHeight = planes.roundedHeight();
this.fluidSurface = planes.fluidSurface();
this.rockState = planes.rockState();
this.fluidState = planes.fluidState();
this.terrainColumns = planes.terrainColumns();
this.apronHeight = planes.apronHeight();
}
public static int index(int localX, int localZ) {
return (localZ << 4) | localX;
}
public static int apronIndex(int localX, int localZ) {
return (localZ + 1) * APRON_WIDTH + localX + 1;
}
public record Planes(int chunkX, int chunkZ, short[] regionId, short[] baseBiomeId, short[] trueBiomeId,
short[] caveBiomeId, double[] baseHeight, double[] naturalHeight, double[] height,
int[] roundedHeight, float[] fluidSurface, short[] rockState, short[] fluidState,
Terrain3DColumn[] terrainColumns, double[] apronHeight) {
}
}
@@ -1,5 +1,7 @@
package art.arcane.iris.generation.bake; package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.runtime.GenerationCounter;
import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.atomic.LongAdder;
public final class GeneratorEvaluations { public final class GeneratorEvaluations {
@@ -12,15 +14,24 @@ public final class GeneratorEvaluations {
} }
public static void bakedChunk(long evaluations) { public static void bakedChunk(long evaluations) {
if (!GenerationCounter.ENABLED) {
return;
}
BAKED_CHUNKS.increment(); BAKED_CHUNKS.increment();
BAKED.add(evaluations); BAKED.add(evaluations);
} }
public static void batched(long evaluations) { public static void batched(long evaluations) {
if (!GenerationCounter.ENABLED) {
return;
}
BATCHED.add(evaluations); BATCHED.add(evaluations);
} }
public static void scalar(int evaluations) { public static void scalar(int evaluations) {
if (!GenerationCounter.ENABLED) {
return;
}
SCALAR.add(evaluations); SCALAR.add(evaluations);
} }
@@ -3,7 +3,5 @@ package art.arcane.iris.generation.bake;
public interface PlaneSource { public interface PlaneSource {
void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values); void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values);
ColumnBake assemble(ChunkPlanes planes);
boolean mainThread(); boolean mainThread();
} }
@@ -85,6 +85,16 @@ public final class TerrainProgram {
} }
int mark = scratch.mark(); int mark = scratch.mark();
int indexMark = scratch.indexMark(); int indexMark = scratch.indexMark();
try {
return accumulate(members, group, scratch, xs, zs, count, lows, highs, heights);
} finally {
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
}
private long accumulate(IrisGenerator[] members, int group, TerrainBatchScratch scratch, double[] xs, double[] zs,
int count, double[] lows, double[] highs, double[] heights) {
int[] modes = scratch.indices(count); int[] modes = scratch.indices(count);
int activeCount = 0; int activeCount = 0;
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
@@ -111,7 +121,6 @@ public final class TerrainProgram {
activeCount++; activeCount++;
} }
if (activeCount == 0) { if (activeCount == 0) {
scratch.resetIndices(indexMark);
return 0L; return 0L;
} }
double[] activeX = scratch.buffer(activeCount); double[] activeX = scratch.buffer(activeCount);
@@ -154,8 +163,6 @@ public final class TerrainProgram {
for (int index = 0; index < activeCount; index++) { for (int index = 0; index < activeCount; index++) {
heights[activeIndex[index]] += sums[index] / members.length; heights[activeIndex[index]] += sums[index] / members.length;
} }
scratch.reset(mark);
scratch.resetIndices(indexMark);
return (long) activeCount * members.length; return (long) activeCount * members.length;
} }
} }
@@ -144,11 +144,11 @@ public class MultiBurst extends MultiBurstSupport {
} }
static int plannerParallelism() { static int plannerParallelism() {
return plannerParallelism(MultiBurstSupport.performanceProcessors()); return plannerParallelism(MultiBurstSupport.performanceProcessors(), Runtime.getRuntime().availableProcessors());
} }
static int plannerParallelism(int performanceProcessors) { static int plannerParallelism(int performanceProcessors, int availableProcessors) {
return Math.max(4, performanceProcessors); return Math.max(2, Math.min(Math.max(4, performanceProcessors), availableProcessors));
} }
@Override @Override
@@ -80,7 +80,6 @@ public final class HydrologyTileCache implements AutoCloseable {
private volatile SharedCacheScope sharedCacheScope; private volatile SharedCacheScope sharedCacheScope;
private volatile PreparedHydrologyTileStore persistentStore; private volatile PreparedHydrologyTileStore persistentStore;
private volatile boolean neighbourPrefetchEnabled; private volatile boolean neighbourPrefetchEnabled;
private volatile Runnable terrainPreparation;
private PregenerationScope pregenerationScope; private PregenerationScope pregenerationScope;
private int demandBatches; private int demandBatches;
private int activePrefetches; private int activePrefetches;
@@ -268,7 +267,6 @@ public final class HydrologyTileCache implements AutoCloseable {
private List<HydrologyDiagnosticCandidate> planDiagnostics(HydrologyTileKey key, long epoch) { private List<HydrologyDiagnosticCandidate> planDiagnostics(HydrologyTileKey key, long epoch) {
HydrologyTile tile = planInline(key, epoch); HydrologyTile tile = planInline(key, epoch);
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) { try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
prepareTerrain();
return List.copyOf(planner.diagnosticCandidates(tile)); return List.copyOf(planner.diagnosticCandidates(tile));
} }
} }
@@ -459,7 +457,6 @@ public final class HydrologyTileCache implements AutoCloseable {
/** Plans the tile under a root permit; a persisted plan is restored before any permit is taken. */ /** Plans the tile under a root permit; a persisted plan is restored before any permit is taken. */
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch, private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
SharedTileKey sharedKey, PreparedHydrologyTileStore store) { SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
prepareTerrain();
HydrologyTile planned = planner.plan(key); HydrologyTile planned = planner.plan(key);
synchronized (publicationLock) { synchronized (publicationLock) {
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) { if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
@@ -777,17 +774,6 @@ public final class HydrologyTileCache implements AutoCloseable {
clear(); clear();
} }
public void setTerrainPreparation(Runnable preparation) {
terrainPreparation = Objects.requireNonNull(preparation, "terrainPreparation");
}
private void prepareTerrain() {
Runnable preparation = terrainPreparation;
if (preparation != null) {
preparation.run();
}
}
private static void collectActiveLoad(List<CompletableFuture<?>> active, PendingLoad<?> load) { private static void collectActiveLoad(List<CompletableFuture<?>> active, PendingLoad<?> load) {
if (load.owner() == Thread.currentThread()) { if (load.owner() == Thread.currentThread()) {
throw new IllegalStateException("Hydrology tile cache cannot close from an active load."); throw new IllegalStateException("Hydrology tile cache cannot close from an active load.");
@@ -123,7 +123,6 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
IrisPlatforms.isBound() ? MultiBurst.hydrology : null, IrisPlatforms.isBound() ? MultiBurst.hydrology : null,
context.waitingForbidden() context.waitingForbidden()
); );
this.cache.setTerrainPreparation(context.terrainPreparation());
IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d", IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d",
settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism()); settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism());
} }
@@ -17,8 +17,7 @@ public record IrisHydrologyRuntimeContext(
IrisHydrologyNaturalHeightDescriber naturalHeightDescriber, IrisHydrologyNaturalHeightDescriber naturalHeightDescriber,
IrisHydrologyNaturalOceanClassifier naturalOceanClassifier, IrisHydrologyNaturalOceanClassifier naturalOceanClassifier,
HydrologyCaveVoxelViewFactory caveViewFactory, HydrologyCaveVoxelViewFactory caveViewFactory,
BooleanSupplier waitingForbidden, BooleanSupplier waitingForbidden
Runnable terrainPreparation
) { ) {
/** /**
* {@code waitingForbidden} is true on a thread that must never wait for a cold hydrology plan, * {@code waitingForbidden} is true on a thread that must never wait for a cold hydrology plan,
@@ -37,6 +36,5 @@ public record IrisHydrologyRuntimeContext(
Objects.requireNonNull(naturalOceanClassifier); Objects.requireNonNull(naturalOceanClassifier);
Objects.requireNonNull(caveViewFactory); Objects.requireNonNull(caveViewFactory);
Objects.requireNonNull(waitingForbidden); Objects.requireNonNull(waitingForbidden);
Objects.requireNonNull(terrainPreparation);
} }
} }
@@ -91,10 +91,15 @@ public final class GeneratorProgram {
return sample(scratch, x, z); return sample(scratch, x, z);
} }
int mark = scratch.mark(); int mark = scratch.mark();
double[] cached = scratch.buffer(1); boolean hit;
boolean hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0); double value;
double value = cached[0]; try {
scratch.reset(mark); double[] cached = scratch.buffer(1);
hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0);
value = cached[0];
} finally {
scratch.reset(mark);
}
if (hit) { if (hit) {
return value; return value;
} }
@@ -125,6 +130,15 @@ public final class GeneratorProgram {
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark(); int mark = scratch.mark();
int indexMark = scratch.indexMark(); int indexMark = scratch.indexMark();
try {
fillSurfaceHeights(scratch, xs, zs, count, out);
} finally {
scratch.resetIndices(indexMark);
scratch.reset(mark);
}
}
private void fillSurfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int[] northWest = scratch.indices(count); int[] northWest = scratch.indices(count);
int[] northEast = scratch.indices(count); int[] northEast = scratch.indices(count);
int[] southWest = scratch.indices(count); int[] southWest = scratch.indices(count);
@@ -209,14 +223,21 @@ public final class GeneratorProgram {
} }
out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail; out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail;
} }
scratch.resetIndices(indexMark);
scratch.reset(mark);
} }
private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) { private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) {
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes(); IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
int indexMark = scratch.indexMark(); int indexMark = scratch.indexMark();
int mark = scratch.mark(); int mark = scratch.mark();
try {
fillLattice(scratch, nodes, lattice, stripes);
} finally {
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
}
private void fillLattice(TerrainBatchScratch scratch, int nodes, double[] lattice, IrisGenerator.SurfaceCacheStripe[] stripes) {
int[] missing = scratch.indices(nodes); int[] missing = scratch.indices(nodes);
double[] missingX = scratch.buffer(nodes); double[] missingX = scratch.buffer(nodes);
double[] missingZ = scratch.buffer(nodes); double[] missingZ = scratch.buffer(nodes);
@@ -242,12 +263,18 @@ public final class GeneratorProgram {
} }
} }
} }
scratch.reset(mark);
scratch.resetIndices(indexMark);
} }
private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark(); int mark = scratch.mark();
try {
sampleBatch(scratch, xs, zs, count, out);
} finally {
scratch.reset(mark);
}
}
private void sampleBatch(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
double[] sampleX = scratch.buffer(count); double[] sampleX = scratch.buffer(count);
double[] sampleZ = scratch.buffer(count); double[] sampleZ = scratch.buffer(count);
double[] noise = scratch.buffer(count); double[] noise = scratch.buffer(count);
@@ -284,6 +311,5 @@ public final class GeneratorProgram {
out[index] = generator.cell(xs[index], zs[index], out[index], 0D); out[index] = generator.cell(xs[index], zs[index], out[index], 0D);
} }
} }
scratch.reset(mark);
} }
} }
@@ -52,6 +52,14 @@ public final class NoiseLayerProgram {
return; return;
} }
int mark = scratch.mark(); int mark = scratch.mark();
try {
fill(scratch, xs, zs, count, out);
} finally {
scratch.reset(mark);
}
}
private void fill(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
double[] sampleX = scratch.buffer(count); double[] sampleX = scratch.buffer(count);
double[] sampleZ = scratch.buffer(count); double[] sampleZ = scratch.buffer(count);
if (children.length == 0) { if (children.length == 0) {
@@ -81,7 +89,6 @@ public final class NoiseLayerProgram {
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
out[index] = post(out[index]); out[index] = post(out[index]);
} }
scratch.reset(mark);
} }
void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) { void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
@@ -91,6 +98,14 @@ public final class NoiseLayerProgram {
return; return;
} }
int mark = scratch.mark(); int mark = scratch.mark();
try {
fillPair(scratch, xs, zs, count, forward, reverse);
} finally {
scratch.reset(mark);
}
}
private void fillPair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
int pairs = count * 2; int pairs = count * 2;
double[] sampleX = scratch.buffer(pairs); double[] sampleX = scratch.buffer(pairs);
double[] sampleZ = scratch.buffer(pairs); double[] sampleZ = scratch.buffer(pairs);
@@ -122,7 +137,6 @@ public final class NoiseLayerProgram {
forward[index] = post(values[index]); forward[index] = post(values[index]);
reverse[index] = post(values[count + index]); reverse[index] = post(values[count + index]);
} }
scratch.reset(mark);
} }
public double sample(TerrainBatchScratch scratch, double x, double z) { public double sample(TerrainBatchScratch scratch, double x, double z) {
@@ -133,14 +147,17 @@ public final class NoiseLayerProgram {
double sampleZ = z; double sampleZ = z;
if (children.length > 0) { if (children.length > 0) {
int mark = scratch.mark(); int mark = scratch.mark();
double[] pair = scratch.buffer(2); try {
for (int child = 0; child < children.length; child++) { double[] pair = scratch.buffer(2);
children[child].samplePair(scratch, x, z, pair); for (int child = 0; child < children.length; child++) {
double offset = childOffsets[child]; children[child].samplePair(scratch, x, z, pair);
sampleX += pair[0] - offset; double offset = childOffsets[child];
sampleZ += pair[1] - offset; sampleX += pair[0] - offset;
sampleZ += pair[1] - offset;
}
} finally {
scratch.reset(mark);
} }
scratch.reset(mark);
} }
return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ)); return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ));
} }
@@ -157,16 +174,19 @@ public final class NoiseLayerProgram {
double reverseZ = x; double reverseZ = x;
if (children.length > 0) { if (children.length > 0) {
int mark = scratch.mark(); int mark = scratch.mark();
double[] pair = scratch.buffer(2); try {
for (int child = 0; child < children.length; child++) { double[] pair = scratch.buffer(2);
children[child].samplePair(scratch, x, z, pair); for (int child = 0; child < children.length; child++) {
double offset = childOffsets[child]; children[child].samplePair(scratch, x, z, pair);
forwardX += pair[0] - offset; double offset = childOffsets[child];
forwardZ += pair[1] - offset; forwardX += pair[0] - offset;
reverseX += pair[1] - offset; forwardZ += pair[1] - offset;
reverseZ += pair[0] - offset; reverseX += pair[1] - offset;
reverseZ += pair[0] - offset;
}
} finally {
scratch.reset(mark);
} }
scratch.reset(mark);
} }
out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ)); out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ));
out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ)); out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ));
@@ -1,61 +0,0 @@
package art.arcane.iris.generation.runtime;
final class HydrologyNoiseCacheBudget {
static final int MAXIMUM_CHUNKS = 32_768;
static final int BYTES_PER_CHUNK = 4_096;
static final HydrologyNoiseCacheBudget SHARED = new HydrologyNoiseCacheBudget(Runtime.getRuntime().maxMemory() / 8L);
private final long maximumBytes;
private long reservedBytes;
HydrologyNoiseCacheBudget(long maximumBytes) {
if (maximumBytes < 0L) {
throw new IllegalArgumentException("Cache budget must not be negative.");
}
this.maximumBytes = maximumBytes;
}
synchronized Reservation reserve(int cacheCount, int requestedAdditionalChunks) {
if (cacheCount <= 0 || requestedAdditionalChunks < 0 || requestedAdditionalChunks > MAXIMUM_CHUNKS) {
throw new IllegalArgumentException("Invalid hydrology cache reservation.");
}
long bytesPerIncrement = (long) BYTES_PER_CHUNK * cacheCount;
int additionalChunks = (int) Math.min(requestedAdditionalChunks, (maximumBytes - reservedBytes) / bytesPerIncrement);
long bytes = additionalChunks * bytesPerIncrement;
reservedBytes += bytes;
return new Reservation(this, additionalChunks, bytes);
}
synchronized long reservedBytes() {
return reservedBytes;
}
private synchronized void release(Reservation reservation) {
if (!reservation.released) {
reservedBytes -= reservation.bytes;
reservation.released = true;
}
}
static final class Reservation implements AutoCloseable {
private final HydrologyNoiseCacheBudget budget;
private final int additionalChunks;
private final long bytes;
private boolean released;
private Reservation(HydrologyNoiseCacheBudget budget, int additionalChunks, long bytes) {
this.budget = budget;
this.additionalChunks = additionalChunks;
this.bytes = bytes;
}
int additionalChunks() {
return additionalChunks;
}
@Override
public void close() {
budget.release(this);
}
}
}
@@ -19,21 +19,17 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.bake.BakePlane; import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.BakeRegistry;
import art.arcane.iris.generation.bake.ChunkBakeCache; import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes; import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.iris.generation.bake.ColumnBake;
import art.arcane.iris.generation.bake.GeneratorEvaluations; import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.bake.PlaneSource; import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.bake.TerrainProgram; import art.arcane.iris.generation.bake.TerrainProgram;
import art.arcane.iris.generation.noise.TerrainBatchScratch; import art.arcane.iris.generation.noise.TerrainBatchScratch;
import art.arcane.iris.generation.simd.SimdSupport;
import art.arcane.iris.generation.stream.BakedDoubleStream; import art.arcane.iris.generation.stream.BakedDoubleStream;
import art.arcane.iris.generation.stream.BakedHeightStream; import art.arcane.iris.generation.stream.BakedHeightStream;
import art.arcane.iris.generation.stream.BakedStream; import art.arcane.iris.generation.stream.BakedStream;
import art.arcane.iris.generation.stream.GenerationStreams; import art.arcane.iris.generation.stream.GenerationStreams;
import art.arcane.iris.generation.stream.RarityStream; import art.arcane.iris.generation.stream.RarityStream;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D; import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.ProvisionalSampling; import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.subterrain.SubterrainPlanner; import art.arcane.iris.generation.subterrain.SubterrainPlanner;
@@ -132,16 +128,14 @@ import java.nio.file.Path;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@Data @Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) @EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) @ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
public class IrisComplex implements DataProvider { public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
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 = 524_288; private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
private static final int BAKE_CHUNKS_PER_NOISE_CHUNK = 8;
private static final int HYDROLOGY_BAKE_BUDGET_SHARES = 2;
/** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */ /** One million corners: about 20 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. */
@@ -175,12 +169,6 @@ public class IrisComplex implements DataProvider {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final transient ChunkBakeCache bakeCache; private final transient ChunkBakeCache bakeCache;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final transient BakeRegistry<IrisBiome> biomeRegistry = new BakeRegistry<>();
@Getter(AccessLevel.NONE)
private final transient BakeRegistry<IrisRegion> regionRegistry = new BakeRegistry<>();
@Getter(AccessLevel.NONE)
private final transient BakeRegistry<NativeBlockState> blockRegistry = new BakeRegistry<>();
@Getter(AccessLevel.NONE)
private final transient long childStyleSeed; private final transient long childStyleSeed;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final transient Map<InferredType, ProceduralStream<IrisBiome>> inferredByType; private final transient Map<InferredType, ProceduralStream<IrisBiome>> inferredByType;
@@ -209,18 +197,6 @@ public class IrisComplex implements DataProvider {
private final TransitionGenerationPlan transitionGenerationPlan; private final TransitionGenerationPlan transitionGenerationPlan;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final transient boolean detached; private final transient boolean detached;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient List<NoiseCacheCapacity> hydrologyNoiseCaches = List.of();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient HydrologyNoiseCacheBudget.Reservation hydrologyNoiseCacheReservation;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient boolean hydrologyNoiseCachesClosed;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient HydrologyNoiseCacheBudget.Reservation hydrologyBakeReservation;
private RNG rng; private RNG rng;
private double fluidHeight; private double fluidHeight;
private IrisData data; private IrisData data;
@@ -415,7 +391,8 @@ public class IrisComplex implements DataProvider {
return value; return value;
}); });
bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * BAKE_CHUNKS_PER_NOISE_CHUNK); bakeCache = new ChunkBakeCache(new BakeSource(), detached ? ChunkBakeCache.MINIMUM_CAPACITY
: ChunkBakeCache.capacityFor(Runtime.getRuntime().maxMemory()));
rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
.select(engine.getDimension().getRockPalette().getBlockData(data)); .select(engine.getDimension().getRockPalette().getBlockData(data));
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource); rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
@@ -508,8 +485,7 @@ public class IrisComplex implements DataProvider {
(x, z) -> describeNaturalHeight(engine, x, z), (x, z) -> describeNaturalHeight(engine, x, z),
this::sampleNaturalOcean, this::sampleNaturalOcean,
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint), footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
() -> engine.getPlatformHooks().isMainThread(), () -> engine.getPlatformHooks().isMainThread()
this::expandHydrologyNoiseCaches
)); ));
hydrologyRuntime.setNeighbourPrefetchEnabled(!engine.isStudio()); hydrologyRuntime.setNeighbourPrefetchEnabled(!engine.isStudio());
IrisRiverBank3DConfig banks3D = configuredHydrology.getRivers().getGeometry().getBanks3D(); IrisRiverBank3DConfig banks3D = configuredHydrology.getRivers().getGeometry().getBanks3D();
@@ -576,81 +552,6 @@ public class IrisComplex implements DataProvider {
return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize; return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
} }
public void ensureTerrainNoiseCacheSize(int minimumChunks) {
if (minimumChunks <= 0) {
throw new IllegalArgumentException("Terrain noise cache size must be positive");
}
bakeCache.ensureCapacity(minimumChunks * BAKE_CHUNKS_PER_NOISE_CHUNK);
}
void expandHydrologyNoiseCaches() {
expandHydrologyNoiseCaches(HydrologyNoiseCacheBudget.SHARED);
}
synchronized void expandHydrologyNoiseCaches(HydrologyNoiseCacheBudget budget) {
if (detached || hydrologyRuntime == null || hydrologyNoiseCachesClosed || hydrologyNoiseCacheReservation != null) {
return;
}
IdentityHashMap<ProceduralStream<?>, Boolean> selected = new IdentityHashMap<>();
List<NoiseCacheCapacity> capacities = new ArrayList<>(5);
int additionalChunks = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS;
for (ProceduralStream<?> stream : new ProceduralStream<?>[]{
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, naturalHeightStream, regionStream, bridgeStream}) {
if (!(stream instanceof CachedStream2D<?> || stream instanceof CachedDoubleStream2D)
|| selected.put(stream, Boolean.TRUE) != null) {
continue;
}
int originalChunks = Math.toIntExact(((MeteredCache) stream).getMaxSize() / 256L);
if (originalChunks >= HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS) {
continue;
}
capacities.add(new NoiseCacheCapacity(stream, originalChunks));
additionalChunks = Math.min(additionalChunks, HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS - originalChunks);
}
if (capacities.isEmpty()) {
return;
}
hydrologyNoiseCaches = List.copyOf(capacities);
hydrologyNoiseCacheReservation = budget.reserve(capacities.size(), additionalChunks);
for (NoiseCacheCapacity capacity : hydrologyNoiseCaches) {
capacity.resize(capacity.originalChunks() + hydrologyNoiseCacheReservation.additionalChunks());
}
}
private synchronized void releaseHydrologyNoiseCaches() {
hydrologyNoiseCachesClosed = true;
if (hydrologyNoiseCacheReservation == null) {
return;
}
for (NoiseCacheCapacity capacity : hydrologyNoiseCaches) {
capacity.resize(capacity.originalChunks());
}
hydrologyNoiseCacheReservation.close();
hydrologyNoiseCacheReservation = null;
hydrologyNoiseCaches = List.of();
}
synchronized void reserveHydrologyBake(HydrologyNoiseCacheBudget budget) {
if (detached || hydrologyRuntime == null || hydrologyNoiseCachesClosed || hydrologyBakeReservation != null) {
return;
}
int capacity = bakeCache.capacity();
int requested = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS - capacity;
if (requested <= 0) {
return;
}
hydrologyBakeReservation = budget.reserve(HYDROLOGY_BAKE_BUDGET_SHARES, requested);
bakeCache.ensureCapacity(capacity + hydrologyBakeReservation.additionalChunks());
}
private synchronized void releaseHydrologyBake() {
if (hydrologyBakeReservation == null) {
return;
}
hydrologyBakeReservation.close();
hydrologyBakeReservation = null;
}
void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) { void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) {
if (hydrologyRuntime != null) { if (hydrologyRuntime != null) {
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot); hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
@@ -1110,11 +1011,9 @@ public class IrisComplex implements DataProvider {
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider { private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
private final boolean batch = supportsHeightBatch(); private final boolean batch = supportsHeightBatch();
private volatile boolean bakeReserved;
@Override @Override
public double sample(int blockX, int blockZ) { public double sample(int blockX, int blockZ) {
reserveBake();
return naturalHeightStream.getDouble(blockX, blockZ); return naturalHeightStream.getDouble(blockX, blockZ);
} }
@@ -1129,17 +1028,8 @@ public class IrisComplex implements DataProvider {
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights); IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
return; return;
} }
reserveBake();
sampleNaturalBatch(coordinates, count, heights); sampleNaturalBatch(coordinates, count, heights);
} }
private void reserveBake() {
if (bakeReserved) {
return;
}
bakeReserved = true;
reserveHydrologyBake(HydrologyNoiseCacheBudget.SHARED);
}
} }
private final class BakeSource implements PlaneSource { private final class BakeSource implements PlaneSource {
@@ -1159,11 +1049,6 @@ public class IrisComplex implements DataProvider {
} }
} }
@Override
public ColumnBake assemble(ChunkPlanes planes) {
return assembleColumnBake(planes.chunkX(), planes.chunkZ());
}
@Override @Override
public boolean mainThread() { public boolean mainThread() {
return terrainEngine.getPlatformHooks().isMainThread(); return terrainEngine.getPlatformHooks().isMainThread();
@@ -1207,87 +1092,10 @@ public class IrisComplex implements DataProvider {
} }
} }
public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) {
return bakeCache.complete(plane, chunkX, chunkZ);
}
public long[] bakeCensus() { public long[] bakeCensus() {
return bakeCache.census(); return bakeCache.census();
} }
public ColumnBake columnBake(int chunkX, int chunkZ) {
return bakeCache.get(chunkX, chunkZ);
}
public ColumnBake columnBakeIfPresent(int chunkX, int chunkZ) {
return bakeCache.getIfPresent(chunkX, chunkZ);
}
public IrisBiome biome(int id) {
return id < 0 ? null : biomeRegistry.value(id);
}
public short biomeId(IrisBiome biome) {
return biome == null ? -1 : biomeRegistry.id(biome);
}
public IrisRegion region(int id) {
return id < 0 ? null : regionRegistry.value(id);
}
public short regionId(IrisRegion region) {
return region == null ? -1 : regionRegistry.id(region);
}
public NativeBlockState blockState(int id) {
return id < 0 ? null : blockRegistry.value(id);
}
private ColumnBake assembleColumnBake(int chunkX, int chunkZ) {
double[] height = (double[]) bakeCache.complete(BakePlane.HEIGHT, chunkX, chunkZ);
int[] rounded = new int[256];
SimdSupport.kernels().roundToInt(height, rounded, 256);
int originX = chunkX << 4;
int originZ = chunkZ << 4;
float[] fluidSurface = new float[256];
Terrain3DColumn[] terrainColumns = new Terrain3DColumn[256];
for (int index = 0; index < 256; index++) {
int x = originX + (index & 15);
int z = originZ + (index >> 4);
fluidSurface[index] = (float) riverWaterSurfaceStream.getDouble(x, z);
terrainColumns[index] = terrainColumn(x, z);
}
double[] apron = new double[ColumnBake.APRON_WIDTH * ColumnBake.APRON_WIDTH];
for (int row = 0; row < ColumnBake.APRON_WIDTH; row++) {
for (int column = 0; column < ColumnBake.APRON_WIDTH; column++) {
apron[row * ColumnBake.APRON_WIDTH + column] = heightStream.getDouble(originX - 1 + column, originZ - 1 + row);
}
}
return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ,
ids((Object[]) bakeCache.complete(BakePlane.REGION, chunkX, chunkZ), regionRegistry),
ids((Object[]) bakeCache.complete(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry),
ids((Object[]) bakeCache.complete(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry),
ids((Object[]) bakeCache.complete(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry),
(double[]) bakeCache.complete(BakePlane.BASE_HEIGHT, chunkX, chunkZ),
(double[]) bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ),
height,
rounded,
fluidSurface,
ids((Object[]) bakeCache.complete(BakePlane.ROCK, chunkX, chunkZ), blockRegistry),
ids((Object[]) bakeCache.complete(BakePlane.FLUID, chunkX, chunkZ), blockRegistry),
terrainColumns,
apron));
}
@SuppressWarnings("unchecked")
private static <T> short[] ids(Object[] values, BakeRegistry<T> registry) {
short[] ids = new short[values.length];
for (int index = 0; index < values.length; index++) {
ids[index] = values[index] == null ? -1 : registry.id((T) values[index]);
}
return ids;
}
private double sampleUnblendedNaturalTerrainHeight(double x, double z) { private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
double proceduralHeight = proceduralTerrainHeight.sample(x, z); double proceduralHeight = proceduralTerrainHeight.sample(x, z);
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
@@ -3021,16 +2829,6 @@ public class IrisComplex implements DataProvider {
record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) { record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) {
} }
private record NoiseCacheCapacity(ProceduralStream<?> stream, int originalChunks) {
private void resize(int chunks) {
if (stream instanceof CachedDoubleStream2D doubles) {
doubles.setMaximumChunks(chunks);
} else if (stream instanceof CachedStream2D<?> objects) {
objects.setMaximumChunks(chunks);
}
}
}
public void close() { public void close() {
if (terrainTransform != null) { if (terrainTransform != null) {
terrainTransform.close(); terrainTransform.close();
@@ -3044,8 +2842,6 @@ public class IrisComplex implements DataProvider {
if (hydrologyRuntime != null) { if (hydrologyRuntime != null) {
hydrologyRuntime.close(); hydrologyRuntime.close();
} }
releaseHydrologyNoiseCaches();
releaseHydrologyBake();
bakeCache.clear(); bakeCache.clear();
if (transitionDisplacement != null) { if (transitionDisplacement != null) {
transitionDisplacement.clear(); transitionDisplacement.clear();
@@ -146,6 +146,7 @@ public class IrisObject extends IrisRegistrant {
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>(); transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>(); transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>(); transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
transient volatile ObjectPlacementBake.Layout placementLayout;
@Getter @Getter
@Setter @Setter
private transient TreeGrounding treeGrounding; private transient TreeGrounding treeGrounding;
@@ -15,140 +15,73 @@ import java.util.concurrent.ConcurrentHashMap;
final class ObjectPlacementBake { final class ObjectPlacementBake {
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32; private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
private final long blocksRevision; private final Layout layout;
private final long statesRevision; private final Key key;
private final TreeGrounding grounding;
private final int count;
private final int[] offsetX; private final int[] offsetX;
private final int[] offsetY; private final int[] offsetY;
private final int[] offsetZ; private final int[] offsetZ;
private final int[] layerY; private final int[] layerY;
private final short[] paletteIndexes;
private final NativeBlockState[] palette;
private final int[] tileBlocks;
private final TileData[] tileValues;
private final long[] canopy;
private final Key key;
private volatile double[] exact; private volatile double[] exact;
private ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count, private ObjectPlacementBake(Layout layout, Key key, int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY) {
int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes, this.layout = layout;
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
this.key = key; this.key = key;
this.blocksRevision = blocksRevision;
this.statesRevision = statesRevision;
this.grounding = grounding;
this.count = count;
this.offsetX = offsetX; this.offsetX = offsetX;
this.offsetY = offsetY; this.offsetY = offsetY;
this.offsetZ = offsetZ; this.offsetZ = offsetZ;
this.layerY = layerY; this.layerY = layerY;
this.paletteIndexes = paletteIndexes;
this.palette = palette;
this.tileBlocks = tileBlocks;
this.tileValues = tileValues;
this.canopy = canopy;
} }
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) { static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
Key key = Key.of(spin, ceilingHang, translateOffset); Key key = Key.of(spin, ceilingHang, translateOffset);
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
ObjectPlacementBake cached = variants.get(key);
if (cached != null && cached.matches(object)) {
return cached;
}
ObjectPlacementBake built = bake(object, key, spin, ceilingHang, translateOffset);
if (built.matches(object)) {
if (variants.size() >= MAXIMUM_VARIANTS_PER_OBJECT) {
variants.clear();
}
variants.put(key, built);
}
return built;
}
private static ObjectPlacementBake bake(IrisObject object, Key key, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
object.readLock.lock(); object.readLock.lock();
try { try {
VectorMap<NativeBlockState> blocks = object.blocks; Layout layout = Layout.of(object);
VectorMap<TileData> states = object.states; ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
long blocksRevision = blocks.modificationRevision(); ObjectPlacementBake cached = variants.get(key);
long statesRevision = states.modificationRevision(); if (cached != null && cached.layout == layout) {
TreeGrounding grounding = object.getTreeGrounding(); return cached;
boolean hasStates = !states.isEmpty();
IntArrayList x = new IntArrayList();
IntArrayList y = new IntArrayList();
IntArrayList z = new IntArrayList();
IntArrayList layer = new IntArrayList();
IntArrayList indexes = new IntArrayList();
IntArrayList tileBlocks = new IntArrayList();
List<TileData> tileValues = new ArrayList<>();
List<NativeBlockState> palette = new ArrayList<>();
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
IntArrayList canopyBlocks = new IntArrayList();
boolean layerDiffers = false;
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (cursor.next()) {
IrisBlockVector source = cursor.key();
NativeBlockState state = cursor.value();
if (state == null) {
state = IrisObject.States.air();
}
transform(source, rotated, spin, ceilingHang, translateOffset);
int roundedY = (int) Math.round(rotated.getY());
int floorY = rotated.getBlockY();
x.add((int) Math.round(rotated.getX()));
y.add(roundedY);
z.add((int) Math.round(rotated.getZ()));
layer.add(floorY);
layerDiffers |= floorY != roundedY;
Integer slot = paletteSlots.get(state);
if (slot == null) {
slot = palette.size();
palette.add(state);
paletteSlots.put(state, slot);
}
indexes.add(slot);
if (hasStates) {
TileData tile = states.get(source);
if (tile != null) {
tileBlocks.add(index);
tileValues.add(tile);
}
}
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
canopyBlocks.add(index);
}
index++;
} }
if (palette.size() > Short.MAX_VALUE) { ObjectPlacementBake built = bake(object, layout, key, spin, ceilingHang, translateOffset);
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than " if (cached != null) {
+ Short.MAX_VALUE + " distinct block states"); variants.replace(key, cached, built);
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
variants.putIfAbsent(key, built);
} }
short[] paletteIndexes = new short[index]; return built;
for (int block = 0; block < index; block++) {
paletteIndexes[block] = (short) indexes.getInt(block);
}
long[] canopy = null;
if (!canopyBlocks.isEmpty()) {
canopy = new long[(index + 63) >>> 6];
for (int block : canopyBlocks) {
canopy[block >>> 6] |= 1L << (block & 63);
}
}
return new ObjectPlacementBake(key, blocksRevision, statesRevision, grounding, index,
x.toIntArray(), y.toIntArray(), z.toIntArray(), layerDiffers ? layer.toIntArray() : null,
paletteIndexes, palette.toArray(new NativeBlockState[0]),
tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
} finally { } finally {
object.readLock.unlock(); object.readLock.unlock();
} }
} }
private static ObjectPlacementBake bake(IrisObject object, Layout layout, Key key, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
int count = layout.count;
int[] x = new int[count];
int[] y = new int[count];
int[] z = new int[count];
int[] layer = new int[count];
boolean layerDiffers = false;
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
int index = 0;
while (cursor.next() && index < count) {
transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
int roundedY = (int) Math.round(rotated.getY());
int floorY = rotated.getBlockY();
x[index] = (int) Math.round(rotated.getX());
y[index] = roundedY;
z[index] = (int) Math.round(rotated.getZ());
layer[index] = floorY;
layerDiffers |= floorY != roundedY;
index++;
}
if (index != count) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was built");
}
return new ObjectPlacementBake(layout, key, x, y, z, layerDiffers ? layer : null);
}
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang, private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) { IrisBlockVector translateOffset) {
target.setX(source.getX()); target.setX(source.getX());
@@ -163,14 +96,8 @@ final class ObjectPlacementBake {
} }
} }
boolean matches(IrisObject object) {
return object.blocks.modificationRevision() == blocksRevision
&& object.states.modificationRevision() == statesRevision
&& object.getTreeGrounding() == grounding;
}
int count() { int count() {
return count; return layout.count;
} }
int offsetX(int index) { int offsetX(int index) {
@@ -190,22 +117,23 @@ final class ObjectPlacementBake {
} }
int paletteIndex(int index) { int paletteIndex(int index) {
return paletteIndexes[index]; return layout.paletteIndexes[index];
} }
NativeBlockState[] palette() { NativeBlockState[] palette() {
return palette; return layout.palette;
} }
int[] tileBlocks() { int[] tileBlocks() {
return tileBlocks; return layout.tileBlocks;
} }
TileData[] tileValues() { TileData[] tileValues() {
return tileValues; return layout.tileValues;
} }
boolean canopy(int index) { boolean canopy(int index) {
long[] canopy = layout.canopy;
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L; return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
} }
@@ -214,6 +142,7 @@ final class ObjectPlacementBake {
if (resolved != null) { if (resolved != null) {
return resolved; return resolved;
} }
int count = layout.count;
object.readLock.lock(); object.readLock.lock();
try { try {
resolved = new double[count * 3]; resolved = new double[count * 3];
@@ -237,6 +166,112 @@ final class ObjectPlacementBake {
return resolved; return resolved;
} }
static final class Layout {
private final long blocksRevision;
private final long statesRevision;
private final TreeGrounding grounding;
private final int count;
private final short[] paletteIndexes;
private final NativeBlockState[] palette;
private final int[] tileBlocks;
private final TileData[] tileValues;
private final long[] canopy;
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
this.blocksRevision = blocksRevision;
this.statesRevision = statesRevision;
this.grounding = grounding;
this.count = count;
this.paletteIndexes = paletteIndexes;
this.palette = palette;
this.tileBlocks = tileBlocks;
this.tileValues = tileValues;
this.canopy = canopy;
}
private static Layout of(IrisObject object) {
Layout current = object.placementLayout;
if (current != null && current.matches(object)) {
return current;
}
Layout built = build(object);
if (!built.matches(object)) {
return built;
}
object.placementLayout = built;
if (current != null) {
object.placementBakes.clear();
}
return built;
}
private static Layout build(IrisObject object) {
VectorMap<NativeBlockState> blocks = object.blocks;
VectorMap<TileData> states = object.states;
long blocksRevision = blocks.modificationRevision();
long statesRevision = states.modificationRevision();
TreeGrounding grounding = object.getTreeGrounding();
boolean hasStates = !states.isEmpty();
int size = blocks.size();
short[] paletteIndexes = new short[size];
IntArrayList tileBlocks = new IntArrayList();
List<TileData> tileValues = new ArrayList<>();
List<NativeBlockState> palette = new ArrayList<>();
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
long[] canopy = null;
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (cursor.next()) {
IrisBlockVector source = cursor.key();
NativeBlockState state = cursor.value();
if (state == null) {
state = IrisObject.States.air();
}
Integer slot = paletteSlots.get(state);
if (slot == null) {
slot = palette.size();
if (slot >= Short.MAX_VALUE) {
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
+ Short.MAX_VALUE + " distinct block states");
}
palette.add(state);
paletteSlots.put(state, slot);
}
if (index == paletteIndexes.length) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement layout was built");
}
paletteIndexes[index] = (short) slot.intValue();
if (hasStates) {
TileData tile = states.get(source);
if (tile != null) {
tileBlocks.add(index);
tileValues.add(tile);
}
}
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
if (canopy == null) {
canopy = new long[(size + 63) >>> 6];
}
canopy[index >>> 6] |= 1L << (index & 63);
}
index++;
}
if (index != size) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement layout was built");
}
return new Layout(blocksRevision, statesRevision, grounding, index, paletteIndexes,
palette.toArray(new NativeBlockState[0]), tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
}
private boolean matches(IrisObject object) {
return object.blocks.modificationRevision() == blocksRevision
&& object.states.modificationRevision() == statesRevision
&& object.getTreeGrounding() == grounding;
}
}
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin, record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY, long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
long translateZ) { long translateZ) {
@@ -18,15 +18,12 @@
package art.arcane.iris.world.pregen; package art.arcane.iris.world.pregen;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.platform.generation.PlatformChunkGenerator; import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.parallel.MultiBurstSupport; import art.arcane.volmlib.util.parallel.MultiBurstSupport;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
public final class PregenPerformanceProfile { public final class PregenPerformanceProfile {
@@ -36,22 +33,11 @@ public final class PregenPerformanceProfile {
} }
public static boolean apply() { public static boolean apply() {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance(); if (!MultiBurst.burst.raiseParallelism(pregenBurstParallelism(MultiBurstSupport.performanceProcessors()))) {
int previousNoiseCacheSize = performance.getNoiseCacheSize(); return false;
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
} }
PARALLELISM_HOLDERS.incrementAndGet();
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(MultiBurstSupport.performanceProcessors()))) { return true;
PARALLELISM_HOLDERS.incrementAndGet();
changed = true;
}
return changed;
} }
/** /**
@@ -75,13 +61,9 @@ public final class PregenPerformanceProfile {
} }
public static void apply(Engine engine) { public static void apply(Engine engine) {
if (engine != null) {
prepareNoiseCacheInPlace(engine);
}
apply(); apply();
if (engine != null) { if (engine != null) {
IrisLogging.info("Pregen profile applied: noiseCacheSize=" IrisLogging.info("Pregen profile applied: burstParallelism=" + MultiBurst.burst.parallelism());
+ IrisSettings.get().getPerformance().getNoiseCacheSize());
} }
} }
@@ -92,10 +74,4 @@ public final class PregenPerformanceProfile {
static int pregenBurstParallelism(int performanceProcessors) { static int pregenBurstParallelism(int performanceProcessors) {
return Math.max(4, performanceProcessors * 2); return Math.max(4, performanceProcessors * 2);
} }
private static void prepareNoiseCacheInPlace(Engine engine) {
IrisComplex complex = Objects.requireNonNull(engine.getComplex(), "Pregeneration requires an active biome complex");
int configuredChunks = Math.max(IrisSettings.get().getPerformance().getNoiseCacheSize(), 4_096);
complex.ensureTerrainNoiseCacheSize(configuredChunks);
}
} }
@@ -0,0 +1,105 @@
package art.arcane.iris.generation.bake;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ChunkBakeCacheTest {
private static final long MEGABYTE = 1L << 20;
private static final long GIGABYTE = 1L << 30;
@Test
public void capacityChargesTheGeneratedChunkFootprintAgainstAHeapShare() {
assertEquals(5_041, ChunkBakeCache.capacityFor(2L * GIGABYTE));
assertEquals(10_082, ChunkBakeCache.capacityFor(4L * GIGABYTE));
assertEquals(15_123, ChunkBakeCache.capacityFor(6L * GIGABYTE));
for (long heap = 512L * MEGABYTE; heap <= 64L * GIGABYTE; heap += 256L * MEGABYTE) {
int capacity = ChunkBakeCache.capacityFor(heap);
if (capacity > ChunkBakeCache.MINIMUM_CAPACITY) {
assertTrue("heap " + heap, capacity * ChunkBakeCache.BYTES_PER_ENTRY <= heap / ChunkBakeCache.HEAP_SHARE);
}
}
}
@Test
public void capacityStaysBetweenTheFloorAndTheCeilingAndGrowsWithTheHeap() {
assertEquals(ChunkBakeCache.MINIMUM_CAPACITY, ChunkBakeCache.capacityFor(0L));
assertEquals(ChunkBakeCache.MINIMUM_CAPACITY, ChunkBakeCache.capacityFor(512L * MEGABYTE));
assertEquals(ChunkBakeCache.MAXIMUM_CAPACITY, ChunkBakeCache.capacityFor(64L * GIGABYTE));
assertEquals(ChunkBakeCache.MAXIMUM_CAPACITY, ChunkBakeCache.capacityFor(Long.MAX_VALUE));
int previous = 0;
for (long heap = 0L; heap <= 32L * GIGABYTE; heap += 128L * MEGABYTE) {
int capacity = ChunkBakeCache.capacityFor(heap);
assertTrue(capacity >= previous);
previous = capacity;
}
}
@Test
public void clearRetiresPlanesThatThreadsStillRemember() {
ChunkBakeCache cache = new ChunkBakeCache(new ConstantSource(), 64);
double[] before = (double[]) cache.complete(BakePlane.BASE_HEIGHT, 3, -5);
ChunkPlanes.Plane remembered = cache.planeIfPresent(BakePlane.BASE_HEIGHT, 3, -5);
assertNotNull(remembered);
assertSame(before, cache.complete(BakePlane.BASE_HEIGHT, 3, -5));
cache.clear();
assertNull(cache.planeIfPresent(BakePlane.BASE_HEIGHT, 3, -5));
double[] after = (double[]) cache.complete(BakePlane.BASE_HEIGHT, 3, -5);
assertNotSame(before, after);
assertEquals(before[17], after[17], 0D);
assertNotSame(remembered, cache.planeIfPresent(BakePlane.BASE_HEIGHT, 3, -5));
}
@Test
public void releasedEntriesHoldNoPlanes() {
ChunkPlanes planes = new ChunkPlanes(1, 1);
ConstantSource source = new ConstantSource();
for (BakePlane kind : BakePlane.PLANES) {
planes.complete(kind, source);
assertNotNull(planes.planeIfPresent(kind));
}
planes.release();
for (BakePlane kind : BakePlane.PLANES) {
assertNull(planes.planeIfPresent(kind));
}
}
@Test
public void clearLeavesAnEmptyCacheOfTheSameCapacity() {
ChunkBakeCache cache = new ChunkBakeCache(new ConstantSource(), 64);
for (int chunk = 0; chunk < 8; chunk++) {
cache.complete(BakePlane.HEIGHT, chunk, -chunk);
}
assertEquals(8L, cache.census()[BakePlane.PLANES.length]);
cache.clear();
assertEquals(0L, cache.census()[BakePlane.PLANES.length]);
assertEquals(64, cache.capacity());
}
private static final class ConstantSource implements PlaneSource {
@Override
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
if (values instanceof double[] doubles) {
for (int index = 0; index < 256; index++) {
doubles[index] = chunkX * 1_000D + chunkZ + index;
}
}
}
@Override
public boolean mainThread() {
return false;
}
}
}
@@ -63,9 +63,17 @@ public class HydrologyBurstTest {
@Test @Test
public void plannerPoolFollowsThePerformanceCores() { public void plannerPoolFollowsThePerformanceCores() {
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12)); assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12, 16));
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1)); assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1, 8));
assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32)); assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32, 32));
}
@Test
public void plannerPoolNeverOutgrowsASmallHost() {
assertEquals(2, MultiBurst.HydrologyBurst.plannerParallelism(1, 1));
assertEquals(2, MultiBurst.HydrologyBurst.plannerParallelism(2, 2));
assertEquals(3, MultiBurst.HydrologyBurst.plannerParallelism(3, 3));
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(2, 4));
} }
@Test @Test
@@ -44,8 +44,6 @@ public class HydrologyDiagnosticCacheTest {
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) { try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
assertSame(tile, cache.get(key)); assertSame(tile, cache.get(key));
verify(planner, never()).diagnosticCandidates(tile); verify(planner, never()).diagnosticCandidates(tile);
AtomicInteger preparations = new AtomicInteger();
cache.setTerrainPreparation(preparations::incrementAndGet);
List<HydrologyDiagnosticCandidate> result = cache.diagnosticCandidates(key); List<HydrologyDiagnosticCandidate> result = cache.diagnosticCandidates(key);
supplied.clear(); supplied.clear();
assertEquals(List.of(candidate), result); assertEquals(List.of(candidate), result);
@@ -53,7 +51,6 @@ public class HydrologyDiagnosticCacheTest {
assertSame(result, cache.diagnosticCandidates(key)); assertSame(result, cache.diagnosticCandidates(key));
verify(planner, times(1)).plan(key); verify(planner, times(1)).plan(key);
verify(planner, times(1)).diagnosticCandidates(tile); verify(planner, times(1)).diagnosticCandidates(tile);
assertEquals(1, preparations.get());
} }
} }
@@ -99,7 +99,6 @@ public class HydrologyTileCacheTest {
new PreparedHydrologyTileStore(root, scope, 64).save(tile); new PreparedHydrologyTileStore(root, scope, 64).save(tile);
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) { try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
cache.enableSharedCache(scope, root); cache.enableSharedCache(scope, root);
cache.setTerrainPreparation(() -> { throw new AssertionError("Restoration must not sample terrain"); });
HydrologyTile restored = cache.get(key); HydrologyTile restored = cache.get(key);
assertEquals(tile.key(), restored.key()); assertEquals(tile.key(), restored.key());
assertEquals(tile.footprint(), restored.footprint()); assertEquals(tile.footprint(), restored.footprint());
@@ -877,13 +876,6 @@ public class HydrologyTileCacheTest {
when(tile.tileSize()).thenReturn(64); when(tile.tileSize()).thenReturn(64);
HydrologyTileCache first = new HydrologyTileCache(firstPlanner, 4, null, null, scope); HydrologyTileCache first = new HydrologyTileCache(firstPlanner, 4, null, null, scope);
HydrologyTileCache second = new HydrologyTileCache(secondPlanner, 4, null, null, scope); HydrologyTileCache second = new HydrologyTileCache(secondPlanner, 4, null, null, scope);
AtomicInteger terrainPreparations = new AtomicInteger();
first.setTerrainPreparation(terrainPreparations::incrementAndGet);
second.setTerrainPreparation(() -> {
throw new AssertionError("Shared tile reuse must not prepare terrain caches");
});
assertEquals(0, terrainPreparations.get());
assertSame(tile, first.get(key)); assertSame(tile, first.get(key));
assertSame(tile, second.get(key)); assertSame(tile, second.get(key));
@@ -891,7 +883,6 @@ public class HydrologyTileCacheTest {
verify(firstPlanner, times(1)).plan(key); verify(firstPlanner, times(1)).plan(key);
verify(secondPlanner, never()).plan(key); verify(secondPlanner, never()).plan(key);
verify(secondPlanner, times(1)).reuseResolvedTile(tile); verify(secondPlanner, times(1)).reuseResolvedTile(tile);
assertEquals(1, terrainPreparations.get());
} }
@Test(timeout = 10000L) @Test(timeout = 10000L)
@@ -159,7 +159,7 @@ public class IrisHydrologyPhysicalOceanTest {
shore.setLoadKey("shore"); shore.setLoadKey("shore");
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data, return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(x, z, height) -> new IrisHydrologyNaturalSample(height, oceanIntent.isOcean(x, z), shore, region), (x, z, height) -> new IrisHydrologyNaturalSample(height, oceanIntent.isOcean(x, z), shore, region),
heights, (x, z) -> "coast", oceanIntent, footprint -> null, () -> false, () -> {})); heights, (x, z) -> "coast", oceanIntent, footprint -> null, () -> false));
} }
private static IrisHydrologyRoutingTerrainSampler sampler(IrisHydrologyRuntime runtime) throws Exception { private static IrisHydrologyRoutingTerrainSampler sampler(IrisHydrologyRuntime runtime) throws Exception {
@@ -229,7 +229,7 @@ public class IrisHydrologyPolicyCacheTest {
private IrisHydrologyRuntime runtime() { private IrisHydrologyRuntime runtime() {
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data, return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(x, z, height) -> new IrisHydrologyNaturalSample(height, false, biome.get(), region.get()), (x, z, height) -> new IrisHydrologyNaturalSample(height, false, biome.get(), region.get()),
(x, z) -> 180D, (x, z) -> "constant", (x, z) -> false, footprint -> null, () -> false, () -> {})); (x, z) -> 180D, (x, z) -> "constant", (x, z) -> false, footprint -> null, () -> false));
} }
} }
@@ -1,96 +0,0 @@
package art.arcane.iris.generation.runtime;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyNoiseCacheBudgetTest {
@Test
public void reservationsClampToTheRemainingSharedCapacity() {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(4_096L * 15L + 4_095L);
HydrologyNoiseCacheBudget.Reservation first = budget.reserve(4, 10);
HydrologyNoiseCacheBudget.Reservation second = budget.reserve(2, 10);
assertEquals(3, first.additionalChunks());
assertEquals(1, second.additionalChunks());
assertEquals(14L * 4_096L, budget.reservedBytes());
first.close();
first.close();
assertEquals(2L * 4_096L, budget.reservedBytes());
second.close();
assertEquals(0L, budget.reservedBytes());
}
@Test
public void emptyBudgetAndZeroDemandReserveNothing() {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(0L);
try (HydrologyNoiseCacheBudget.Reservation reservation = budget.reserve(5, 32_768)) {
assertEquals(0, reservation.additionalChunks());
}
HydrologyNoiseCacheBudget available = new HydrologyNoiseCacheBudget(1_000_000L);
try (HydrologyNoiseCacheBudget.Reservation reservation = available.reserve(5, 0)) {
assertEquals(0, reservation.additionalChunks());
}
assertEquals(0L, budget.reservedBytes());
assertEquals(0L, available.reservedBytes());
}
@Test
public void requestsCannotExceedTheCapacityLimit() {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
try (HydrologyNoiseCacheBudget.Reservation reservation = budget.reserve(5, 32_768)) {
assertEquals(32_768, reservation.additionalChunks());
assertEquals(5L * 32_768L * 4_096L, budget.reservedBytes());
}
assertThrows(IllegalArgumentException.class, () -> budget.reserve(0, 10));
assertThrows(IllegalArgumentException.class, () -> budget.reserve(5, -1));
assertThrows(IllegalArgumentException.class, () -> budget.reserve(5, 32_769));
assertThrows(IllegalArgumentException.class, () -> new HydrologyNoiseCacheBudget(-1L));
}
@Test(timeout = 10_000L)
public void concurrentOwnersNeverOversubscribeAndReleaseIndependently() throws Exception {
long maximumBytes = 4_096L * 503L;
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(maximumBytes);
CountDownLatch start = new CountDownLatch(1);
List<Future<HydrologyNoiseCacheBudget.Reservation>> pending = new ArrayList<>();
try (ExecutorService workers = Executors.newFixedThreadPool(16)) {
for (int index = 0; index < 32; index++) {
pending.add(workers.submit(() -> {
assertTrue(start.await(5L, TimeUnit.SECONDS));
return budget.reserve(5, 50);
}));
}
start.countDown();
List<HydrologyNoiseCacheBudget.Reservation> reservations = new ArrayList<>();
long expectedBytes = 0L;
for (Future<HydrologyNoiseCacheBudget.Reservation> future : pending) {
HydrologyNoiseCacheBudget.Reservation reservation = future.get(5L, TimeUnit.SECONDS);
reservations.add(reservation);
expectedBytes += reservation.additionalChunks() * 5L * 4_096L;
}
assertEquals(expectedBytes, budget.reservedBytes());
assertTrue(expectedBytes <= maximumBytes);
List<Future<?>> releases = new ArrayList<>();
for (HydrologyNoiseCacheBudget.Reservation reservation : reservations) {
releases.add(workers.submit(() -> {
reservation.close();
reservation.close();
}));
}
for (Future<?> release : releases) {
release.get(5L, TimeUnit.SECONDS);
}
assertEquals(0L, budget.reservedBytes());
}
}
}
@@ -1,11 +1,11 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.GeneratorEvaluations; import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.image.IrisImageMapApplication; import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.image.IrisImageMapRuntime; import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatform;
@@ -67,7 +67,8 @@ public class IrisComplexBakeParityTest {
for (int chunk = 0; chunk < 48; chunk++) { for (int chunk = 0; chunk < 48; chunk++) {
int chunkX = random.nextInt(-400, 400); int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400); int chunkZ = random.nextInt(-400, 400);
double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ); double[] baked = new double[256];
batched.baseHeights().fillChunkDoubles(chunkX << 4, chunkZ << 4, baked);
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
int x = (chunkX << 4) + (index & 15); int x = (chunkX << 4) + (index & 15);
int z = (chunkZ << 4) + (index >> 4); int z = (chunkZ << 4) + (index >> 4);
@@ -88,7 +89,7 @@ public class IrisComplexBakeParityTest {
int chunkX = random.nextInt(-400, 400); int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400); int chunkZ = random.nextInt(-400, 400);
GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot(); GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot();
batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ); batched.baseHeights().fillChunkDoubles(chunkX << 4, chunkZ << 4, new double[256]);
GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot(); GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot();
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4)); scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
@@ -195,6 +196,10 @@ public class IrisComplexBakeParityTest {
doReturn(complex).when(engine).getComplex(); doReturn(complex).when(engine).getComplex();
} }
private ChunkFillableDoubleStream2D baseHeights() {
return (ChunkFillableDoubleStream2D) complex.getBaseTerrainHeightStream();
}
@Override @Override
public void close() { public void close() {
complex.close(); complex.close();
@@ -1,398 +0,0 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyTile;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisComplexHydrologyNoiseCacheTest {
@Test
public void pregenGrowsTheBackingHeightCacheWithoutReplacingItsThreadAwareStream() throws Exception {
IrisSettings previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisSettings.get().getPerformance().setNoiseCacheSize(1_024);
try (Fixture fixture = new Fixture(1_024)) {
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
PregenPerformanceProfile.apply(fixture.engine);
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
assertSame(wrapper, fixture.complex.getRawHeightStream());
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
} finally {
PregenPerformanceProfile.restore();
IrisSettings.settings = previousSettings;
}
}
@Test
public void pregenPreservesActiveHydrologyGrants() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
long reservedBytes = budget.reservedBytes();
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(32_768);
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(reservedBytes, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void pregenPreservesLargerRuntimeCacheCapacities() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(65_536);
assertTrue(fixture.bakes.capacity() >= 65_536);
}
}
@Test
public void idleRuntimeReservesOnlyOnDemandAndRestoresAfterHydrologyDrains() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
HydrologyTileCache cache = fixture.planningCache(budget);
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
cache.get(new HydrologyTileKey(0, 0));
cache.get(new HydrologyTileKey(0, 0));
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
doAnswer(invocation -> {
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
cache.close();
return null;
}).when(fixture.hydrology).close();
fixture.complex.close();
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
doNothing().when(fixture.hydrology).close();
fixture.complex.close();
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test(timeout = 10_000L)
public void concurrentCacheExpansionsShareOneReservation() throws Exception {
try (Fixture fixture = new Fixture(1_024);
ExecutorService callers = Executors.newFixedThreadPool(2)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
CountDownLatch entered = new CountDownLatch(2);
CountDownLatch release = new CountDownLatch(1);
Runnable prepare = () -> {
entered.countDown();
await(release);
fixture.complex.expandHydrologyNoiseCaches(budget);
};
Future<?> first = callers.submit(prepare);
Future<?> second = callers.submit(prepare);
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertEquals(0L, budget.reservedBytes());
release.countDown();
first.get(5L, TimeUnit.SECONDS);
second.get(5L, TimeUnit.SECONDS);
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test(timeout = 10_000L)
public void closeDrainsAnAdmittedFirstDemandBeforeReleasingItsGrant() throws Exception {
try (Fixture fixture = new Fixture(1_024);
ExecutorService callers = Executors.newFixedThreadPool(2)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
HydrologyTileCache cache = fixture.planningCache(budget);
CountDownLatch preparing = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch closing = new CountDownLatch(1);
cache.setTerrainPreparation(() -> {
preparing.countDown();
await(release);
fixture.complex.expandHydrologyNoiseCaches(budget);
});
doAnswer(invocation -> {
closing.countDown();
cache.close();
return null;
}).when(fixture.hydrology).close();
Future<?> demand = callers.submit(() -> cache.get(new HydrologyTileKey(0, 0)));
assertTrue(preparing.await(5L, TimeUnit.SECONDS));
Future<?> close = callers.submit(fixture.complex::close);
assertTrue(closing.await(5L, TimeUnit.SECONDS));
try {
assertThrows(TimeoutException.class, () -> close.get(100L, TimeUnit.MILLISECONDS));
assertEquals(0L, budget.reservedBytes());
} finally {
release.countDown();
}
demand.get(5L, TimeUnit.SECONDS);
close.get(5L, TimeUnit.SECONDS);
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void volumetricNaturalHeightReservesItsDistinctFifthCache() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
CachedDoubleStream2D natural = fixture.heightCache(1_024);
fixture.complex.setUnblendedNaturalHeightStream(natural);
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(32_768);
assertEquals(32_768L * 256L, natural.getMaxSize());
assertEquals(5L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(1_024L * 256L, natural.getMaxSize());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void partialGrantPreservesTheConfiguredFloorForEveryCache() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(4L * 80L * 4_096L + 4_095L);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(1_104);
assertEquals(4L * 80L * 4_096L, budget.reservedBytes());
fixture.complex.close();
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void configuredCapacityAboveTargetRemainsUnchanged() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(65_536);
assertEquals(0L, budget.reservedBytes());
fixture.complex.close();
fixture.assertCapacities(65_536);
}
}
@Test
public void detachedAndNonHydrologyRuntimesDoNotReserve() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
setField(fixture.complex, "detached", true);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.complex.reserveHydrologyBake(budget);
fixture.assertCapacities(1_024);
assertEquals(1_024, fixture.bakes.capacity());
setField(fixture.complex, "detached", false);
fixture.complex.setHydrologyRuntime(null);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.complex.reserveHydrologyBake(budget);
fixture.assertCapacities(1_024);
assertEquals(1_024, fixture.bakes.capacity());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationGrowsTheBakeToTheHydrologyTarget() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.reserveHydrologyBake(budget);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(32_768, fixture.bakes.capacity());
assertEquals(2L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
assertEquals(32_768, fixture.bakes.capacity());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
fixture.complex.reserveHydrologyBake(budget);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationTakesOnlyTheGrantedShareOfTheBudget() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(2L * 80L * 4_096L + 4_095L);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(1_104, fixture.bakes.capacity());
assertEquals(2L * 80L * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationLeavesLargerBakesUnchanged() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(65_536, fixture.bakes.capacity());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void failedHydrologyDrainRetainsTheReservationUntilRetry() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
HydrologyTileCache cache = fixture.planningCache(budget);
cache.get(new HydrologyTileKey(0, 0));
doThrow(new IllegalStateException("Hydrology is still draining")).when(fixture.hydrology).close();
assertThrows(IllegalStateException.class, fixture.complex::close);
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
doAnswer(invocation -> {
cache.close();
return null;
}).when(fixture.hydrology).close();
fixture.complex.close();
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void mappedStreamRetainsOnlyTheFinalCacheAtTheExpandedCapacity() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
CachedStream2D<IrisBiome> inner = fixture.biomes;
CachedStream2D<IrisBiome> mapped = new CachedStream2D<>("mapped", fixture.engine, inner, 1_024);
fixture.complex.setBaseBiomeStream(mapped);
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
assertEquals(1_024L * 256L, inner.getMaxSize());
assertEquals(32_768L * 256L, mapped.getMaxSize());
fixture.complex.close();
assertEquals(1_024L * 256L, mapped.getMaxSize());
}
}
private static void setField(IrisComplex complex, String name, Object value) throws Exception {
Field field = IrisComplex.class.getDeclaredField(name);
field.setAccessible(true);
field.set(complex, value);
}
private static void await(CountDownLatch latch) {
try {
assertTrue(latch.await(5L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
}
private static final class Fixture implements AutoCloseable {
private final MockedStatic<IrisServices> services = mockStatic(IrisServices.class);
private final Engine engine = mock(Engine.class);
private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
private final IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
private final CachedStream2D<IrisBiome> biomes;
private final CachedStream2D<IrisRegion> regions;
private final CachedStream2D<InferredType> bridge;
private final CachedDoubleStream2D height;
private final ChunkBakeCache bakes;
private Fixture(int capacity) throws Exception {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
biomes = objectCache(capacity);
regions = objectCache(capacity);
bridge = objectCache(capacity);
height = heightCache(capacity);
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
complex.setNaturalHeightStream(height);
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
setField(complex, "bakeCache", bakes);
when(engine.getComplex()).thenReturn(complex);
complex.setBaseBiomeStream(biomes);
complex.setRegionStream(regions);
complex.setBridgeStream(bridge);
complex.setBaseTerrainHeightStream(height);
complex.setUnblendedNaturalHeightStream(height);
complex.setHydrologyRuntime(hydrology);
}
@Override
public void close() {
services.close();
}
private <T> CachedStream2D<T> objectCache(int capacity) {
@SuppressWarnings("unchecked")
ProceduralStream<T> source = mock(ProceduralStream.class);
return new CachedStream2D<>("cache", engine, source, capacity);
}
private CachedDoubleStream2D heightCache(int capacity) {
return new CachedDoubleStream2D("height", engine, ProceduralStream.ofDouble((x, z) -> x - z), capacity);
}
private HydrologyTileCache planningCache(HydrologyNoiseCacheBudget budget) {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(mock(HydrologyTile.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
cache.setTerrainPreparation(() -> complex.expandHydrologyNoiseCaches(budget));
doAnswer(invocation -> {
cache.close();
return null;
}).when(hydrology).close();
return cache;
}
private void assertCapacities(int chunks) {
assertEquals(chunks * 256L, biomes.getMaxSize());
assertEquals(chunks * 256L, regions.getMaxSize());
assertEquals(chunks * 256L, bridge.getMaxSize());
assertEquals(chunks * 256L, height.getMaxSize());
}
}
}
@@ -87,7 +87,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
when(historical.complex.getHydrologyRuntime()).thenReturn(hydrology); when(historical.complex.getHydrologyRuntime()).thenReturn(hydrology);
IrisHydrologyRuntimeContext context = new IrisHydrologyRuntimeContext(17L, 384, historical.dimension, IrisHydrologyRuntimeContext context = new IrisHydrologyRuntimeContext(17L, 384, historical.dimension,
historical.data, (x, z, height) -> null, (x, z) -> 80D, (x, z) -> "constant", historical.data, (x, z, height) -> null, (x, z) -> 80D, (x, z) -> "constant",
(x, z) -> false, footprint -> null, () -> false, () -> {}); (x, z) -> false, footprint -> null, () -> false);
Field runtimeContext = IrisHydrologyRuntime.class.getDeclaredField("context"); Field runtimeContext = IrisHydrologyRuntime.class.getDeclaredField("context");
runtimeContext.setAccessible(true); runtimeContext.setAccessible(true);
runtimeContext.set(hydrology, context); runtimeContext.set(hydrology, context);
@@ -4,9 +4,16 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.testsupport.KeyedBlockState; import art.arcane.iris.testsupport.KeyedBlockState;
import org.junit.Test; import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ObjectPlacementBakeTest { public class ObjectPlacementBakeTest {
@Test @Test
@@ -65,6 +72,57 @@ public class ObjectPlacementBakeTest {
assertSame(after, ObjectPlacementBake.of(object, spin, false, null)); assertSame(after, ObjectPlacementBake.of(object, spin, false, null));
} }
@Test
public void highVariantPlacementsKeepCachedVariantsAndNeverRebuildTheLayout() {
IrisObject object = object();
IrisObjectRotation pine = new IrisObjectRotation();
pine.setEnabled(true);
pine.setXAxis(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
pine.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 270D, 90D));
pine.setZAxis(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
IrisBlockVector translate = new IrisBlockVector(0D, -6D, 0D);
Random random = new Random(1444L);
ObjectPlacementBake first = ObjectPlacementBake.of(object, new SpinKernel(pine, 0, 0, 0), false, translate);
Map<ObjectPlacementBake.Key, ObjectPlacementBake> settled = null;
Set<ObjectPlacementBake.Key> distinct = new HashSet<>();
for (int placement = 0; placement < 4000; placement++) {
SpinKernel spin = new SpinKernel(pine, random.nextInt(360), random.nextInt(360), random.nextInt(360));
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, translate);
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
assertSame(first.palette(), bake.palette());
assertTrue(object.placementBakes.size() <= 32);
assertTransformed(object, spin, translate, bake);
if (placement == 2000) {
settled = new HashMap<>(object.placementBakes);
}
}
assertTrue(distinct.size() > 40);
assertEquals(32, settled.size());
assertEquals(settled.keySet(), object.placementBakes.keySet());
for (Map.Entry<ObjectPlacementBake.Key, ObjectPlacementBake> entry : settled.entrySet()) {
assertSame(entry.getValue(), object.placementBakes.get(entry.getKey()));
}
}
private static void assertTransformed(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
double[] exact = bake.exact(object, spin);
int index = 0;
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector expected = source.clone();
spin.rotate(expected);
expected.add(translate);
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
assertEquals(expected.getBlockY(), bake.layerY(index));
assertEquals(expected.getX(), exact[index * 3], 0D);
assertEquals(expected.getY(), exact[index * 3 + 1], 0D);
assertEquals(expected.getZ(), exact[index * 3 + 2], 0D);
index++;
}
assertEquals(bake.count(), index);
}
private static IrisObject object() { private static IrisObject object() {
IrisObject object = new IrisObject(5, 5, 5); IrisObject object = new IrisObject(5, 5, 5);
KeyedBlockState stone = new KeyedBlockState("minecraft:stone"); KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
@@ -65,6 +65,7 @@ public class ObjectPlacementGoldenTest {
private static final int SEEDS = 5; private static final int SEEDS = 5;
private static final long GOLDEN = 0x408f9d4113015897L; private static final long GOLDEN = 0x408f9d4113015897L;
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL; private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
private static final long HIGH_VARIANT_GOLDEN = 0xf017622340ecac42L;
private static final List<String> PALETTE = List.of( private static final List<String> PALETTE = List.of(
"minecraft:stone", "minecraft:stone",
@@ -229,6 +230,34 @@ public class ObjectPlacementGoldenTest {
assertEquals(Long.toHexString(MARKER_GOLDEN), Long.toHexString(combined)); assertEquals(Long.toHexString(MARKER_GOLDEN), Long.toHexString(combined));
} }
@Test
public void highVariantPlacementsMatchTheRecordedBlockStream() {
Random random = new Random(1444L);
IrisObjectRotation pine = rotation(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D),
new IrisAxisRotationClamp(true, false, 0D, 270D, 90D), new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
long combined = 29L;
int writes = 0;
for (int index = 0; index < 16; index++) {
IrisObject object = randomObject(random, index);
object.setLoader(objectData);
IrisObjectPlacement placement = randomPlacement(random).toPlacement("pine");
placement.setRotation(pine);
for (int seed = 0; seed < 150; seed++) {
RNG rng = new RNG(index * 104_729L + seed);
GoldenPlacer placer = new GoldenPlacer(engine, (seed & 1) == 0);
int y = seed % 3 == 0 ? 90 + (seed & 7) : -1;
int result = object.place(300 + seed, y, -400 + index * 7, placer, placement, rng, null, null, data);
long hash = mix(placer.hash, result);
hash = mix(hash, rng.nextLong());
combined = mix(combined, hash);
writes += placer.writes;
}
assertTrue(object.placementBakes.size() <= 32);
}
assertTrue(writes > 10_000);
assertEquals(Long.toHexString(HIGH_VARIANT_GOLDEN), Long.toHexString(combined));
}
private static long mix(long hash, long value) { private static long mix(long hash, long value) {
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L; long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
return mixed ^ (mixed >>> 29); return mixed ^ (mixed >>> 29);
@@ -15,7 +15,6 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@@ -46,13 +45,17 @@ public class PregenPerformanceProfileTest {
} }
@Test @Test
public void directEngineProfileResizesActualTerrainCachesWithoutRebuildingHistory() { public void directEngineProfileLeavesTerrainCachesAndHistoryUntouched() {
PregenPerformanceProfile.apply(engine); try {
PregenPerformanceProfile.apply(engine);
assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize()); assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(complex).ensureTerrainNoiseCacheSize(4_096); verify(engine, never()).getComplex();
verify(complex, never()).getHeightStream(); verify(complex, never()).getHeightStream();
verify(engine, never()).hotloadComplex(); verify(engine, never()).hotloadComplex();
} finally {
PregenPerformanceProfile.restore();
}
} }
@Test @Test
@@ -60,42 +63,26 @@ public class PregenPerformanceProfileTest {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class); PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
when(generator.getEngine()).thenReturn(engine); when(generator.getEngine()).thenReturn(engine);
PregenPerformanceProfile.applyToGenerator(generator); try {
PregenPerformanceProfile.applyToGenerator(generator);
verify(complex).ensureTerrainNoiseCacheSize(4_096); verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS);
verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS); verify(engine, never()).hotloadComplex();
verify(engine, never()).hotloadComplex(); } finally {
PregenPerformanceProfile.restore();
}
} }
@Test @Test
public void largerConfiguredCachesRemainLarger() { public void enginesCreatedDuringOrAfterPregenerationKeepTheConfiguredNoiseCacheSize() {
IrisSettings.get().getPerformance().setNoiseCacheSize(8_192); try {
PregenPerformanceProfile.applyToGenerator(null);
PregenPerformanceProfile.apply(engine);
assertEquals(8_192, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(complex).ensureTerrainNoiseCacheSize(8_192);
}
@Test
public void failedCachePreparationDoesNotChangeGlobalTuningOrAcquireAParallelismHold() {
int holders = PregenPerformanceProfile.parallelismHolders();
int parallelism = MultiBurst.burst.parallelism();
doThrow(new IllegalStateException("Runtime closed"))
.when(complex).ensureTerrainNoiseCacheSize(4_096);
assertThrows(IllegalStateException.class, () -> PregenPerformanceProfile.apply(engine));
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
} finally {
PregenPerformanceProfile.restore();
}
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize()); assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
assertEquals(parallelism, MultiBurst.burst.parallelism());
assertEquals(holders, PregenPerformanceProfile.parallelismHolders());
}
@Test
public void profileBeforeWorldCreationStillAppliesGlobalTuning() {
PregenPerformanceProfile.applyToGenerator(null);
assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize());
} }
@Test @Test