mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
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:
commit
a25d8ec367
33 files changed
+541
-1194
No files matched your search
@@ -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.");
|
||||||
|
|||||||
-1
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -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
|
||||||
|
|||||||
-3
@@ -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)
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-96
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-3
@@ -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();
|
||||||
|
|||||||
-398
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user