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 {
maxHeapSize = '1g'
jvmArgs('-XX:+ExitOnOutOfMemoryError')
systemProperty('iris.generation.counters', 'true')
}
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;
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 final PlaneSource source;
private final ConcurrentClockCache<ChunkPlanes> entries;
private final ThreadLocal<Recent> recent = ThreadLocal.withInitial(Recent::new);
private volatile ConcurrentClockCache<ChunkPlanes> entries;
private volatile int generation;
public ChunkBakeCache(PlaneSource source, int capacityChunks) {
this.source = Objects.requireNonNull(source, "Bake plane source");
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
}
@Override
public ColumnBake get(int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).columnBake(source);
}
@Override
public ColumnBake getIfPresent(int chunkX, int chunkZ) {
ChunkPlanes planes = planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.columnBake();
public static int capacityFor(long maxMemory) {
long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY;
return (int) Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries));
}
public int capacity() {
return entries.capacity();
}
public synchronized void ensureCapacity(int chunks) {
ConcurrentClockCache<ChunkPlanes> current = entries;
if (current.capacity() >= chunks) {
return;
}
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 synchronized void clear() {
generation++;
entries.forEach(ChunkPlanes::release);
entries.clear();
}
public long[] census() {
@@ -61,32 +50,19 @@ public final class ChunkBakeCache implements BakeCache {
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) {
long key = key(chunkX, chunkZ);
Recent last = recent.get();
ConcurrentClockCache<ChunkPlanes> current = entries;
int current = generation;
ChunkPlanes planes;
if (last.key == key && last.entries == current) {
if (last.key == key && last.generation == current) {
planes = last.planes;
} else {
planes = current.get(key);
planes = entries.get(key);
if (planes == null) {
return null;
}
last.key = key;
last.planes = planes;
last.entries = current;
last.remember(key, planes, current);
}
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);
}
private ChunkPlanes remembered(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries;
private ChunkPlanes planes(int chunkX, int chunkZ) {
long key = key(chunkX, chunkZ);
ChunkPlanes existing = current.get(key);
ChunkPlanes planes = existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
Recent last = recent.get();
last.key = key;
last.planes = planes;
last.entries = current;
ChunkPlanes existing = entries.get(key);
return existing != null ? existing : entries.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
}
private ChunkPlanes remembered(int chunkX, int chunkZ) {
int current = generation;
ChunkPlanes planes = planes(chunkX, chunkZ);
recent.get().remember(key(chunkX, chunkZ), planes, current);
return planes;
}
@@ -147,7 +124,13 @@ public final class ChunkBakeCache implements BakeCache {
private static final class Recent {
private long key;
private int generation = -1;
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 chunkZ;
private final AtomicReferenceArray<Plane> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
private volatile ColumnBake columnBake;
ChunkPlanes(int chunkX, int chunkZ) {
this.chunkX = chunkX;
@@ -70,21 +69,10 @@ public final class ChunkPlanes {
}
}
ColumnBake columnBake() {
return columnBake;
}
ColumnBake columnBake(PlaneSource source) {
ColumnBake bake = columnBake;
if (bake != null) {
return bake;
void release() {
for (int slot = 0; slot < planes.length(); slot++) {
planes.setRelease(slot, null);
}
long revision = ProvisionalSampling.revision();
bake = source.assemble(this);
if (revision == ProvisionalSampling.revision()) {
columnBake = bake;
}
return bake;
}
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;
import art.arcane.iris.generation.runtime.GenerationCounter;
import java.util.concurrent.atomic.LongAdder;
public final class GeneratorEvaluations {
@@ -12,15 +14,24 @@ public final class GeneratorEvaluations {
}
public static void bakedChunk(long evaluations) {
if (!GenerationCounter.ENABLED) {
return;
}
BAKED_CHUNKS.increment();
BAKED.add(evaluations);
}
public static void batched(long evaluations) {
if (!GenerationCounter.ENABLED) {
return;
}
BATCHED.add(evaluations);
}
public static void scalar(int evaluations) {
if (!GenerationCounter.ENABLED) {
return;
}
SCALAR.add(evaluations);
}
@@ -3,7 +3,5 @@ package art.arcane.iris.generation.bake;
public interface PlaneSource {
void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values);
ColumnBake assemble(ChunkPlanes planes);
boolean mainThread();
}
@@ -85,6 +85,16 @@ public final class TerrainProgram {
}
int mark = scratch.mark();
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 activeCount = 0;
for (int index = 0; index < count; index++) {
@@ -111,7 +121,6 @@ public final class TerrainProgram {
activeCount++;
}
if (activeCount == 0) {
scratch.resetIndices(indexMark);
return 0L;
}
double[] activeX = scratch.buffer(activeCount);
@@ -154,8 +163,6 @@ public final class TerrainProgram {
for (int index = 0; index < activeCount; index++) {
heights[activeIndex[index]] += sums[index] / members.length;
}
scratch.reset(mark);
scratch.resetIndices(indexMark);
return (long) activeCount * members.length;
}
}
@@ -144,11 +144,11 @@ public class MultiBurst extends MultiBurstSupport {
}
static int plannerParallelism() {
return plannerParallelism(MultiBurstSupport.performanceProcessors());
return plannerParallelism(MultiBurstSupport.performanceProcessors(), Runtime.getRuntime().availableProcessors());
}
static int plannerParallelism(int performanceProcessors) {
return Math.max(4, performanceProcessors);
static int plannerParallelism(int performanceProcessors, int availableProcessors) {
return Math.max(2, Math.min(Math.max(4, performanceProcessors), availableProcessors));
}
@Override
@@ -80,7 +80,6 @@ public final class HydrologyTileCache implements AutoCloseable {
private volatile SharedCacheScope sharedCacheScope;
private volatile PreparedHydrologyTileStore persistentStore;
private volatile boolean neighbourPrefetchEnabled;
private volatile Runnable terrainPreparation;
private PregenerationScope pregenerationScope;
private int demandBatches;
private int activePrefetches;
@@ -268,7 +267,6 @@ public final class HydrologyTileCache implements AutoCloseable {
private List<HydrologyDiagnosticCandidate> planDiagnostics(HydrologyTileKey key, long epoch) {
HydrologyTile tile = planInline(key, epoch);
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
prepareTerrain();
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. */
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
prepareTerrain();
HydrologyTile planned = planner.plan(key);
synchronized (publicationLock) {
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
@@ -777,17 +774,6 @@ public final class HydrologyTileCache implements AutoCloseable {
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) {
if (load.owner() == Thread.currentThread()) {
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,
context.waitingForbidden()
);
this.cache.setTerrainPreparation(context.terrainPreparation());
IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d",
settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism());
}
@@ -17,8 +17,7 @@ public record IrisHydrologyRuntimeContext(
IrisHydrologyNaturalHeightDescriber naturalHeightDescriber,
IrisHydrologyNaturalOceanClassifier naturalOceanClassifier,
HydrologyCaveVoxelViewFactory caveViewFactory,
BooleanSupplier waitingForbidden,
Runnable terrainPreparation
BooleanSupplier waitingForbidden
) {
/**
* {@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(caveViewFactory);
Objects.requireNonNull(waitingForbidden);
Objects.requireNonNull(terrainPreparation);
}
}
@@ -91,10 +91,15 @@ public final class GeneratorProgram {
return sample(scratch, x, z);
}
int mark = scratch.mark();
double[] cached = scratch.buffer(1);
boolean hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0);
double value = cached[0];
scratch.reset(mark);
boolean hit;
double value;
try {
double[] cached = scratch.buffer(1);
hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0);
value = cached[0];
} finally {
scratch.reset(mark);
}
if (hit) {
return value;
}
@@ -125,6 +130,15 @@ public final class GeneratorProgram {
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark();
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[] northEast = 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;
}
scratch.resetIndices(indexMark);
scratch.reset(mark);
}
private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) {
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
int indexMark = scratch.indexMark();
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);
double[] missingX = 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) {
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[] sampleZ = 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);
}
}
scratch.reset(mark);
}
}
@@ -52,6 +52,14 @@ public final class NoiseLayerProgram {
return;
}
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[] sampleZ = scratch.buffer(count);
if (children.length == 0) {
@@ -81,7 +89,6 @@ public final class NoiseLayerProgram {
for (int index = 0; index < count; index++) {
out[index] = post(out[index]);
}
scratch.reset(mark);
}
void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
@@ -91,6 +98,14 @@ public final class NoiseLayerProgram {
return;
}
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;
double[] sampleX = scratch.buffer(pairs);
double[] sampleZ = scratch.buffer(pairs);
@@ -122,7 +137,6 @@ public final class NoiseLayerProgram {
forward[index] = post(values[index]);
reverse[index] = post(values[count + index]);
}
scratch.reset(mark);
}
public double sample(TerrainBatchScratch scratch, double x, double z) {
@@ -133,14 +147,17 @@ public final class NoiseLayerProgram {
double sampleZ = z;
if (children.length > 0) {
int mark = scratch.mark();
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
sampleX += pair[0] - offset;
sampleZ += pair[1] - offset;
try {
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
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));
}
@@ -157,16 +174,19 @@ public final class NoiseLayerProgram {
double reverseZ = x;
if (children.length > 0) {
int mark = scratch.mark();
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
forwardX += pair[0] - offset;
forwardZ += pair[1] - offset;
reverseX += pair[1] - offset;
reverseZ += pair[0] - offset;
try {
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
forwardX += pair[0] - offset;
forwardZ += pair[1] - 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[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;
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.ChunkPlanes;
import art.arcane.iris.generation.bake.ColumnBake;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.bake.TerrainProgram;
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.BakedHeightStream;
import art.arcane.iris.generation.stream.BakedStream;
import art.arcane.iris.generation.stream.GenerationStreams;
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.ProvisionalSampling;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
@@ -132,16 +128,14 @@ import java.nio.file.Path;
import java.util.function.BiFunction;
@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"})
@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"})
@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", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
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. */
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. */
@@ -175,12 +169,6 @@ public class IrisComplex implements DataProvider {
@Getter(AccessLevel.NONE)
private final transient ChunkBakeCache bakeCache;
@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;
@Getter(AccessLevel.NONE)
private final transient Map<InferredType, ProceduralStream<IrisBiome>> inferredByType;
@@ -209,18 +197,6 @@ public class IrisComplex implements DataProvider {
private final TransitionGenerationPlan transitionGenerationPlan;
@Getter(AccessLevel.NONE)
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 double fluidHeight;
private IrisData data;
@@ -415,7 +391,8 @@ public class IrisComplex implements DataProvider {
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()
.select(engine.getDimension().getRockPalette().getBlockData(data));
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
@@ -508,8 +485,7 @@ public class IrisComplex implements DataProvider {
(x, z) -> describeNaturalHeight(engine, x, z),
this::sampleNaturalOcean,
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
() -> engine.getPlatformHooks().isMainThread(),
this::expandHydrologyNoiseCaches
() -> engine.getPlatformHooks().isMainThread()
));
hydrologyRuntime.setNeighbourPrefetchEnabled(!engine.isStudio());
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;
}
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) {
if (hydrologyRuntime != null) {
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
@@ -1110,11 +1011,9 @@ public class IrisComplex implements DataProvider {
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
private final boolean batch = supportsHeightBatch();
private volatile boolean bakeReserved;
@Override
public double sample(int blockX, int blockZ) {
reserveBake();
return naturalHeightStream.getDouble(blockX, blockZ);
}
@@ -1129,17 +1028,8 @@ public class IrisComplex implements DataProvider {
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
return;
}
reserveBake();
sampleNaturalBatch(coordinates, count, heights);
}
private void reserveBake() {
if (bakeReserved) {
return;
}
bakeReserved = true;
reserveHydrologyBake(HydrologyNoiseCacheBudget.SHARED);
}
}
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
public boolean mainThread() {
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() {
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) {
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
@@ -3021,16 +2829,6 @@ public class IrisComplex implements DataProvider {
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() {
if (terrainTransform != null) {
terrainTransform.close();
@@ -3044,8 +2842,6 @@ public class IrisComplex implements DataProvider {
if (hydrologyRuntime != null) {
hydrologyRuntime.close();
}
releaseHydrologyNoiseCaches();
releaseHydrologyBake();
bakeCache.clear();
if (transitionDisplacement != null) {
transitionDisplacement.clear();
@@ -146,6 +146,7 @@ public class IrisObject extends IrisRegistrant {
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
transient volatile ObjectPlacementBake.Layout placementLayout;
@Getter
@Setter
private transient TreeGrounding treeGrounding;
@@ -15,140 +15,73 @@ import java.util.concurrent.ConcurrentHashMap;
final class ObjectPlacementBake {
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
private final long blocksRevision;
private final long statesRevision;
private final TreeGrounding grounding;
private final int count;
private final Layout layout;
private final Key key;
private final int[] offsetX;
private final int[] offsetY;
private final int[] offsetZ;
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 ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count,
int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes,
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
private ObjectPlacementBake(Layout layout, Key key, int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY) {
this.layout = layout;
this.key = key;
this.blocksRevision = blocksRevision;
this.statesRevision = statesRevision;
this.grounding = grounding;
this.count = count;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.offsetZ = offsetZ;
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) {
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();
try {
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();
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++;
Layout layout = Layout.of(object);
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
ObjectPlacementBake cached = variants.get(key);
if (cached != null && cached.layout == layout) {
return cached;
}
if (palette.size() > Short.MAX_VALUE) {
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
+ Short.MAX_VALUE + " distinct block states");
ObjectPlacementBake built = bake(object, layout, key, spin, ceilingHang, translateOffset);
if (cached != null) {
variants.replace(key, cached, built);
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
variants.putIfAbsent(key, built);
}
short[] paletteIndexes = new short[index];
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);
return built;
} finally {
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,
IrisBlockVector translateOffset) {
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() {
return count;
return layout.count;
}
int offsetX(int index) {
@@ -190,22 +117,23 @@ final class ObjectPlacementBake {
}
int paletteIndex(int index) {
return paletteIndexes[index];
return layout.paletteIndexes[index];
}
NativeBlockState[] palette() {
return palette;
return layout.palette;
}
int[] tileBlocks() {
return tileBlocks;
return layout.tileBlocks;
}
TileData[] tileValues() {
return tileValues;
return layout.tileValues;
}
boolean canopy(int index) {
long[] canopy = layout.canopy;
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
}
@@ -214,6 +142,7 @@ final class ObjectPlacementBake {
if (resolved != null) {
return resolved;
}
int count = layout.count;
object.readLock.lock();
try {
resolved = new double[count * 3];
@@ -237,6 +166,112 @@ final class ObjectPlacementBake {
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,
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
long translateZ) {
@@ -18,15 +18,12 @@
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.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.parallel.MultiBurstSupport;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public final class PregenPerformanceProfile {
@@ -36,22 +33,11 @@ public final class PregenPerformanceProfile {
}
public static boolean apply() {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
if (!MultiBurst.burst.raiseParallelism(pregenBurstParallelism(MultiBurstSupport.performanceProcessors()))) {
return false;
}
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(MultiBurstSupport.performanceProcessors()))) {
PARALLELISM_HOLDERS.incrementAndGet();
changed = true;
}
return changed;
PARALLELISM_HOLDERS.incrementAndGet();
return true;
}
/**
@@ -75,13 +61,9 @@ public final class PregenPerformanceProfile {
}
public static void apply(Engine engine) {
if (engine != null) {
prepareNoiseCacheInPlace(engine);
}
apply();
if (engine != null) {
IrisLogging.info("Pregen profile applied: noiseCacheSize="
+ IrisSettings.get().getPerformance().getNoiseCacheSize());
IrisLogging.info("Pregen profile applied: burstParallelism=" + MultiBurst.burst.parallelism());
}
}
@@ -92,10 +74,4 @@ public final class PregenPerformanceProfile {
static int pregenBurstParallelism(int performanceProcessors) {
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
public void plannerPoolFollowsThePerformanceCores() {
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12));
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1));
assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32));
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12, 16));
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1, 8));
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
@@ -44,8 +44,6 @@ public class HydrologyDiagnosticCacheTest {
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
assertSame(tile, cache.get(key));
verify(planner, never()).diagnosticCandidates(tile);
AtomicInteger preparations = new AtomicInteger();
cache.setTerrainPreparation(preparations::incrementAndGet);
List<HydrologyDiagnosticCandidate> result = cache.diagnosticCandidates(key);
supplied.clear();
assertEquals(List.of(candidate), result);
@@ -53,7 +51,6 @@ public class HydrologyDiagnosticCacheTest {
assertSame(result, cache.diagnosticCandidates(key));
verify(planner, times(1)).plan(key);
verify(planner, times(1)).diagnosticCandidates(tile);
assertEquals(1, preparations.get());
}
}
@@ -99,7 +99,6 @@ public class HydrologyTileCacheTest {
new PreparedHydrologyTileStore(root, scope, 64).save(tile);
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
cache.enableSharedCache(scope, root);
cache.setTerrainPreparation(() -> { throw new AssertionError("Restoration must not sample terrain"); });
HydrologyTile restored = cache.get(key);
assertEquals(tile.key(), restored.key());
assertEquals(tile.footprint(), restored.footprint());
@@ -877,13 +876,6 @@ public class HydrologyTileCacheTest {
when(tile.tileSize()).thenReturn(64);
HydrologyTileCache first = new HydrologyTileCache(firstPlanner, 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, second.get(key));
@@ -891,7 +883,6 @@ public class HydrologyTileCacheTest {
verify(firstPlanner, times(1)).plan(key);
verify(secondPlanner, never()).plan(key);
verify(secondPlanner, times(1)).reuseResolvedTile(tile);
assertEquals(1, terrainPreparations.get());
}
@Test(timeout = 10000L)
@@ -159,7 +159,7 @@ public class IrisHydrologyPhysicalOceanTest {
shore.setLoadKey("shore");
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(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 {
@@ -229,7 +229,7 @@ public class IrisHydrologyPolicyCacheTest {
private IrisHydrologyRuntime runtime() {
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(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;
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.block.B;
import art.arcane.iris.generation.image.IrisImageMapApplication;
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.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
@@ -67,7 +67,8 @@ public class IrisComplexBakeParityTest {
for (int chunk = 0; chunk < 48; chunk++) {
int chunkX = 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++) {
int x = (chunkX << 4) + (index & 15);
int z = (chunkZ << 4) + (index >> 4);
@@ -88,7 +89,7 @@ public class IrisComplexBakeParityTest {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
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();
for (int index = 0; index < 256; index++) {
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
@@ -195,6 +196,10 @@ public class IrisComplexBakeParityTest {
doReturn(complex).when(engine).getComplex();
}
private ChunkFillableDoubleStream2D baseHeights() {
return (ChunkFillableDoubleStream2D) complex.getBaseTerrainHeightStream();
}
@Override
public void 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);
IrisHydrologyRuntimeContext context = new IrisHydrologyRuntimeContext(17L, 384, historical.dimension,
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");
runtimeContext.setAccessible(true);
runtimeContext.set(hydrology, context);
@@ -4,9 +4,16 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.testsupport.KeyedBlockState;
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.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ObjectPlacementBakeTest {
@Test
@@ -65,6 +72,57 @@ public class ObjectPlacementBakeTest {
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() {
IrisObject object = new IrisObject(5, 5, 5);
KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
@@ -65,6 +65,7 @@ public class ObjectPlacementGoldenTest {
private static final int SEEDS = 5;
private static final long GOLDEN = 0x408f9d4113015897L;
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
private static final long HIGH_VARIANT_GOLDEN = 0xf017622340ecac42L;
private static final List<String> PALETTE = List.of(
"minecraft:stone",
@@ -229,6 +230,34 @@ public class ObjectPlacementGoldenTest {
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) {
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
return mixed ^ (mixed >>> 29);
@@ -15,7 +15,6 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -46,13 +45,17 @@ public class PregenPerformanceProfileTest {
}
@Test
public void directEngineProfileResizesActualTerrainCachesWithoutRebuildingHistory() {
PregenPerformanceProfile.apply(engine);
public void directEngineProfileLeavesTerrainCachesAndHistoryUntouched() {
try {
PregenPerformanceProfile.apply(engine);
assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(complex).ensureTerrainNoiseCacheSize(4_096);
verify(complex, never()).getHeightStream();
verify(engine, never()).hotloadComplex();
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(engine, never()).getComplex();
verify(complex, never()).getHeightStream();
verify(engine, never()).hotloadComplex();
} finally {
PregenPerformanceProfile.restore();
}
}
@Test
@@ -60,42 +63,26 @@ public class PregenPerformanceProfileTest {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
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(engine, never()).hotloadComplex();
verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS);
verify(engine, never()).hotloadComplex();
} finally {
PregenPerformanceProfile.restore();
}
}
@Test
public void largerConfiguredCachesRemainLarger() {
IrisSettings.get().getPerformance().setNoiseCacheSize(8_192);
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));
public void enginesCreatedDuringOrAfterPregenerationKeepTheConfiguredNoiseCacheSize() {
try {
PregenPerformanceProfile.applyToGenerator(null);
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
} finally {
PregenPerformanceProfile.restore();
}
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