Merge L3-hydrology

This commit is contained in:
Brian Neumann-Fopiano
2026-09-28 12:20:03 -04:00
18 changed files with 1058 additions and 191 deletions
@@ -29,6 +29,7 @@ import java.util.function.LongSupplier;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.DimensionTerrainContext;
@@ -338,7 +339,12 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
return cached;
}
}
H resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
H resolved;
try {
resolved = ProvisionalSampling.memoizable(() -> resolveStructureBiome(engine, quartX, quartY, quartZ, sampler));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (cacheable) {
cache.put(runtimeIdentity, key, resolved);
}
@@ -388,8 +394,13 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
return cached;
}
}
H resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, null);
H resolved;
try {
resolved = ProvisionalSampling.memoizable(() -> resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, null));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (cacheable) {
cache.put(runtimeIdentity, key, resolved);
}
@@ -531,7 +542,12 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
if (cached != null) {
return cached;
}
H resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
H resolved;
try {
resolved = ProvisionalSampling.memoizable(() -> resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
cache.put(runtimeIdentity, key, resolved);
return resolved;
}
@@ -0,0 +1,64 @@
package art.arcane.iris.generation.hydrology;
/**
* The chunks a planned tile publishes at least one footprint column into. A chunk outside every
* relevant tile's occupancy composes to no hydrology at all, so it can be answered without the
* tiles being resident.
*/
final class HydrologyChunkOccupancy {
static final HydrologyChunkOccupancy EMPTY = new HydrologyChunkOccupancy(0, 0, 0, 0, new long[0]);
private final int minimumChunkX;
private final int minimumChunkZ;
private final int width;
private final int depth;
private final long[] bits;
private HydrologyChunkOccupancy(int minimumChunkX, int minimumChunkZ, int width, int depth, long[] bits) {
this.minimumChunkX = minimumChunkX;
this.minimumChunkZ = minimumChunkZ;
this.width = width;
this.depth = depth;
this.bits = bits;
}
static HydrologyChunkOccupancy of(RiverFootprint footprint) {
if (footprint == null) {
return EMPTY;
}
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (long packed : footprint.columns().keySet()) {
int chunkX = RiverFootprint.unpackX(packed) >> 4;
int chunkZ = RiverFootprint.unpackZ(packed) >> 4;
minimumX = Math.min(minimumX, chunkX);
minimumZ = Math.min(minimumZ, chunkZ);
maximumX = Math.max(maximumX, chunkX);
maximumZ = Math.max(maximumZ, chunkZ);
}
if (minimumX > maximumX) {
return EMPTY;
}
int width = maximumX - minimumX + 1;
int depth = maximumZ - minimumZ + 1;
long[] bits = new long[Math.toIntExact(Math.ceilDiv(Math.multiplyExact((long) width, depth), 64L))];
for (long packed : footprint.columns().keySet()) {
int index = ((RiverFootprint.unpackZ(packed) >> 4) - minimumZ) * width
+ (RiverFootprint.unpackX(packed) >> 4) - minimumX;
bits[index >>> 6] |= 1L << index;
}
return new HydrologyChunkOccupancy(minimumX, minimumZ, width, depth, bits);
}
boolean occupies(int chunkX, int chunkZ) {
int localX = chunkX - minimumChunkX;
int localZ = chunkZ - minimumChunkZ;
if (localX < 0 || localZ < 0 || localX >= width || localZ >= depth) {
return false;
}
int index = localZ * width + localX;
return (bits[index >>> 6] & 1L << index) != 0L;
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.hydrology;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
@@ -9,7 +10,7 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -36,6 +37,13 @@ public final class HydrologyTileCache implements AutoCloseable {
private static final int MAXIMUM_COMPOSED_CHUNKS = 256;
private static final int CHUNK_SIZE = 16;
private static final int CHUNK_COLUMN_COUNT = CHUNK_SIZE * CHUNK_SIZE;
private static final int LOCAL_CHUNK_SLOTS = 16;
private static final byte AROUND_UNKNOWN = 0;
private static final byte AROUND_EMPTY = 1;
private static final byte AROUND_PUBLISHED = 2;
private static final int MAXIMUM_PREGENERATION_TILES = 4096;
private static final int PREGENERATION_HALO_BLOCKS = 512;
private static final HydrologyColumnSample[] NO_COLUMNS = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
private static final Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
.weigher((SharedTileKey key, HydrologyTile tile) -> HydrologyCacheWeights.bounded(
@@ -48,17 +56,18 @@ public final class HydrologyTileCache implements AutoCloseable {
private final int publicationRadius;
private final int maximumEntries;
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
private final ConcurrentHashMap<HydrologyTileKey, HydrologyChunkOccupancy> occupancies;
private final Cache<Long, ChunkColumns> composedChunks;
private final Cache<HydrologyTileKey, List<HydrologyDiagnosticCandidate>> diagnostics;
private final AtomicLong cacheEpoch;
private final AtomicBoolean closed;
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
private final ThreadLocal<LocalChunks> localChunks;
private final Executor prefetchExecutor;
private final int prefetchSlots;
private final ConcurrentLinkedQueue<HydrologyTileKey> prefetchQueue;
private final Set<HydrologyTileKey> queuedPrefetches;
private final LinkedHashSet<ChunkCoordinate> queriedChunks;
private final Set<CacheLoadKey<HydrologyTileKey>> demandedPlans;
private final AtomicBoolean prefetchActive;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, CompletableFuture<HydrologyTile>> planning;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, PendingLoad<HydrologyTile>> loading;
private final ConcurrentHashMap<CacheLoadKey<Long>, PendingLoad<ChunkColumns>> composing;
@@ -72,6 +81,8 @@ public final class HydrologyTileCache implements AutoCloseable {
private volatile Runnable terrainPreparation;
private PregenerationScope pregenerationScope;
private int demandBatches;
private int activePrefetches;
private boolean pumping;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES, null, null, null);
@@ -112,6 +123,7 @@ public final class HydrologyTileCache implements AutoCloseable {
this.tileSize = planner.settings().routing().tileSize();
this.publicationRadius = planner.settings().publicationRadius();
this.prefetchExecutor = prefetchExecutor;
this.prefetchSlots = prefetchSlots(HydrologyPlanningAdmission.maximumRoots());
this.waitingForbidden = waitingForbidden;
this.sharedCacheScope = sharedCacheScope;
this.persistentStore = null;
@@ -128,7 +140,7 @@ public final class HydrologyTileCache implements AutoCloseable {
this.queuedPrefetches = ConcurrentHashMap.newKeySet();
this.queriedChunks = new LinkedHashSet<>();
this.demandedPlans = new HashSet<>();
this.prefetchActive = new AtomicBoolean();
this.occupancies = new ConcurrentHashMap<>();
HydrologyCacheBudget budget = HydrologyCacheBudget.runtime();
this.tiles = Caffeine.newBuilder()
.maximumWeight(budget.tileBytes())
@@ -147,10 +159,18 @@ public final class HydrologyTileCache implements AutoCloseable {
.build();
this.cacheEpoch = new AtomicLong();
this.closed = new AtomicBoolean();
this.localChunkColumns = new ThreadLocal<>();
this.localChunks = ThreadLocal.withInitial(LocalChunks::new);
this.neighbourPrefetchEnabled = true;
}
/**
* Speculative roots run beside each other but leave one root permit free, so a chunk that needs a
* tile nobody queued yet does not wait behind a full row of prefetches.
*/
static int prefetchSlots(int maximumRoots) {
return Math.max(1, maximumRoots - 1);
}
/**
* The tile, planned on the prefetch executor when there is one so that a caller that gets
* interrupted or cancelled (a map render, a command) never aborts a plan every other caller is
@@ -317,9 +337,14 @@ public final class HydrologyTileCache implements AutoCloseable {
try {
present = tiles.getIfPresent(key);
HydrologyTile tile = present == null ? planRequired(key, epoch) : present;
HydrologyChunkOccupancy occupancy = occupancies.get(key);
if (occupancy == null) {
occupancy = HydrologyChunkOccupancy.of(tile.footprint());
}
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
tiles.put(key, tile);
occupancies.putIfAbsent(key, occupancy);
}
}
owned.future().complete(tile);
@@ -355,9 +380,20 @@ public final class HydrologyTileCache implements AutoCloseable {
SHARED_TILES.invalidate(sharedKey);
}
}
if (attempt == 0L && store != null) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
planner.reuseResolvedTile(persisted);
if (sharedKey != null) {
SHARED_TILES.put(sharedKey, persisted);
}
IrisLogging.debug("Loaded persisted prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
return persisted;
}
}
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(
closed::get)) {
return planAdmitted(key, planningEpoch, sharedKey, store, attempt == 0L);
return planAdmitted(key, planningEpoch, sharedKey, store);
}
} catch (RuntimeException failure) {
if (failure instanceof CancellationException || interrupted(failure)) {
@@ -407,18 +443,9 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
/** Plans the tile under a root permit; a persisted plan is restored before any permit is taken. */
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
SharedTileKey sharedKey, PreparedHydrologyTileStore store,
boolean reusePrepared) {
if (store != null && reusePrepared) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
planner.reuseResolvedTile(persisted);
SHARED_TILES.put(sharedKey, persisted);
IrisLogging.debug("Loaded persisted prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
return persisted;
}
}
SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
prepareTerrain();
HydrologyTile planned = planner.plan(key);
synchronized (publicationLock) {
@@ -470,10 +497,12 @@ public final class HydrologyTileCache implements AutoCloseable {
}
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
LocalChunkColumns local = localChunkColumns.get();
if (local != null && local.epoch() == cacheEpoch.get()
&& local.columns().chunkX() == chunkX && local.columns().chunkZ() == chunkZ) {
return HydrologyColumnSnapshot.ready(local.columns().columnAt(blockX, blockZ).orElse(null));
ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ);
if (local != null) {
return HydrologyColumnSnapshot.ready(local.columnAt(blockX, blockZ).orElse(null));
}
if (knownEmpty(chunkX, chunkZ)) {
return HydrologyColumnSnapshot.ready(null);
}
ChunkColumns composed = composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ)));
if (composed != null) {
@@ -485,6 +514,7 @@ public final class HydrologyTileCache implements AutoCloseable {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestUnplanned(chunkX, chunkZ);
ProvisionalSampling.mark();
return HydrologyColumnSnapshot.unavailable();
}
snapshots.add(tile);
@@ -497,18 +527,56 @@ public final class HydrologyTileCache implements AutoCloseable {
}
/**
* Whether every tile the column's chunk composes from is already planned. A caller that must not
* wait (the server thread answering a height or biome query) uses this before sampling: when the
* answer is false the missing tiles are handed to the prefetch executor so a later query finds
* them, and nothing is planned on the caller.
* Whether the column's chunk can be answered without planning: every tile it composes from is
* already planned. A caller that must not wait (the server thread answering a height or biome
* query) uses this before sampling: when the answer is false the missing tiles are handed to the
* prefetch executor so a later query finds them, nothing is planned on the caller, and the caller
* is marked as answering provisionally.
*/
public boolean isPlanned(int blockX, int blockZ) {
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
if (composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ))) != null) {
if (knownEmpty(chunkX, chunkZ)
|| composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ))) != null) {
return true;
}
return !requestUnplanned(chunkX, chunkZ);
if (requestUnplanned(chunkX, chunkZ)) {
ProvisionalSampling.mark();
return false;
}
return true;
}
/**
* Whether any planned footprint column lies in the chunk or one of its eight neighbours, which
* decides whether the chunk can hold published hydrology at all. Tiles are planned like a sample
* would plan them, and each thread remembers its recent final answers.
*/
public boolean hasColumnsAround(int chunkX, int chunkZ) {
long epoch = cacheEpoch.get();
LocalChunks local = localChunks(epoch);
int slot = LocalChunks.slot(chunkX, chunkZ);
long key = RiverFootprint.pack(chunkX, chunkZ);
if (local.around[slot] != AROUND_UNKNOWN && local.aroundKeys[slot] == key) {
return local.around[slot] == AROUND_PUBLISHED;
}
boolean published = publishesAround(chunkX, chunkZ);
if (!waitsForbidden() && local.epoch == epoch) {
local.aroundKeys[slot] = key;
local.around[slot] = published ? AROUND_PUBLISHED : AROUND_EMPTY;
}
return published;
}
private boolean publishesAround(int chunkX, int chunkZ) {
for (int offsetZ = -1; offsetZ <= 1; offsetZ++) {
for (int offsetX = -1; offsetX <= 1; offsetX++) {
if (!chunkColumns((chunkX + offsetX) * CHUNK_SIZE, (chunkZ + offsetZ) * CHUNK_SIZE).empty()) {
return true;
}
}
}
return false;
}
/**
@@ -567,6 +635,7 @@ public final class HydrologyTileCache implements AutoCloseable {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
tiles.invalidateAll();
occupancies.clear();
composedChunks.invalidateAll();
diagnostics.invalidateAll();
composing.forEach((key, load) -> {
@@ -581,7 +650,7 @@ public final class HydrologyTileCache implements AutoCloseable {
demandedPlans.clear();
demandBatches = 0;
}
localChunkColumns.remove();
localChunks.remove();
for (CompletableFuture<ChunkColumns> future : cancelledQueries) {
future.completeExceptionally(new CancellationException("Hydrology query was invalidated."));
}
@@ -600,19 +669,27 @@ public final class HydrologyTileCache implements AutoCloseable {
neighbourPrefetchEnabled = enabled;
}
/**
* Queues every tile the area's chunks compose from, nearest the centre first and at most
* {@link #MAXIMUM_PREGENERATION_TILES} of them, so planning runs ahead of the generation front and
* each plan lands in the prepared-plan store. Speculation stays inside the area plus a halo that
* covers generation reaching past its edge.
*/
public PregenerationScope preparePregeneration(PregenerationArea area) {
Objects.requireNonNull(area, "area");
long reach = (long) PREGENERATION_HALO_BLOCKS + publicationRadius;
TileBounds bounds = new TileBounds(
tileCoordinate(Math.subtractExact(area.minimumBlockX(), reach), tileSize),
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), reach), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockX(), reach), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockZ(), reach), tileSize));
TileBounds required = new TileBounds(
tileCoordinate(Math.subtractExact(area.minimumBlockX(), publicationRadius), tileSize),
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), publicationRadius), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockX(), publicationRadius), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockZ(), publicationRadius), tileSize));
List<HydrologyTileKey> keys = prefetchAreaKeys(
Math.max(area.minimumBlockX(), (long) area.centerBlockX() - tileSize),
Math.max(area.minimumBlockZ(), (long) area.centerBlockZ() - tileSize),
Math.min(area.maximumBlockX(), (long) area.centerBlockX() + tileSize),
Math.min(area.maximumBlockZ(), (long) area.centerBlockZ() + tileSize),
area.centerBlockX(), area.centerBlockZ());
List<HydrologyTileKey> keys = nearestFirst(required, tileCoordinate(area.centerBlockX(), tileSize),
tileCoordinate(area.centerBlockZ(), tileSize), MAXIMUM_PREGENERATION_TILES);
PregenerationScope scope = new PregenerationScope(bounds);
try {
enqueuePrefetchArea(keys, scope);
@@ -713,19 +790,47 @@ public final class HydrologyTileCache implements AutoCloseable {
&& tile.tileSize() == tileSize;
}
/**
* The chunk's composed columns. A chunk no planned tile publishes into is answered from the
* occupancy index alone, without composition or the tiles being resident; the last chunks each
* thread resolved answer repeated lookups without touching any shared structure.
*/
private ChunkColumns chunkColumns(int blockX, int blockZ) {
requireOpen();
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
long epoch = cacheEpoch.get();
LocalChunkColumns local = localChunkColumns.get();
if (local != null && local.epoch() == epoch
&& local.columns().chunkX() == chunkX
&& local.columns().chunkZ() == chunkZ) {
return local.columns();
LocalChunks local = localChunks(epoch);
ChunkColumns cached = local.get(chunkX, chunkZ);
if (cached != null) {
return cached;
}
TileBounds relevant = chunkTiles(chunkX, chunkZ);
ChunkColumns columns;
if (knownEmpty(chunkX, chunkZ, relevant)) {
columns = local.put(ChunkColumns.empty(chunkX, chunkZ));
} else if (waitsForbidden()) {
columns = nonblockingChunkColumns(chunkX, chunkZ, epoch);
if (columns == null) {
ProvisionalSampling.mark();
columns = ChunkColumns.empty(chunkX, chunkZ);
} else {
local.put(columns);
}
} else {
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
columns = local.put(loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ));
}
if (!relevant.equals(local.prefetched)) {
local.prefetched = relevant;
prefetchNeighbours(relevant);
}
return columns;
}
/** The chunk's columns from resident tiles only, or null after handing its missing tiles to the planner. */
private ChunkColumns nonblockingChunkColumns(int chunkX, int chunkZ, long epoch) {
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
if (waitsForbidden()) {
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
@@ -736,7 +841,7 @@ public final class HydrologyTileCache implements AutoCloseable {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestChunk(chunkX, chunkZ);
return ChunkColumns.empty(chunkX, chunkZ);
return null;
}
snapshots.add(tile);
}
@@ -746,12 +851,46 @@ public final class HydrologyTileCache implements AutoCloseable {
composedChunks.put(packedChunk, columns);
}
}
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
}
ChunkColumns columns = loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
private LocalChunks localChunks(long epoch) {
LocalChunks local = localChunks.get();
if (local.epoch != epoch) {
Arrays.fill(local.slots, null);
Arrays.fill(local.around, AROUND_UNKNOWN);
local.prefetched = null;
local.epoch = epoch;
}
return local;
}
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
private TileBounds chunkTiles(int chunkX, int chunkZ) {
long minimumBlockX = (long) chunkX * CHUNK_SIZE;
long minimumBlockZ = (long) chunkZ * CHUNK_SIZE;
return new TileBounds(
tileCoordinate(minimumBlockX - publicationRadius, tileSize),
tileCoordinate(minimumBlockZ - publicationRadius, tileSize),
tileCoordinate(minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize),
tileCoordinate(minimumBlockZ + CHUNK_SIZE - 1L + publicationRadius, tileSize));
}
private boolean knownEmpty(int chunkX, int chunkZ) {
return knownEmpty(chunkX, chunkZ, chunkTiles(chunkX, chunkZ));
}
/** Whether every tile the chunk composes from is planned and none of them publishes into it. */
private boolean knownEmpty(int chunkX, int chunkZ, TileBounds relevant) {
for (int tileZ = relevant.minimumZ(); tileZ <= relevant.maximumZ(); tileZ++) {
for (int tileX = relevant.minimumX(); tileX <= relevant.maximumX(); tileX++) {
HydrologyChunkOccupancy occupancy = occupancies.get(new HydrologyTileKey(tileX, tileZ));
if (occupancy == null || occupancy.occupies(chunkX, chunkZ)) {
return false;
}
}
}
return true;
}
private ChunkColumns loadChunkColumns(CacheLoadKey<Long> loadKey, int chunkX, int chunkZ) {
@@ -801,19 +940,12 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
private ArrayList<HydrologyTileKey> relevantKeys(int chunkX, int chunkZ) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) minimumBlockZ + CHUNK_SIZE - 1L + publicationRadius, tileSize);
ArrayList<HydrologyTileKey> relevantKeys = new ArrayList<>(
Math.multiplyExact(maximumTileX - minimumTileX + 1, maximumTileZ - minimumTileZ + 1)
);
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
TileBounds relevant = chunkTiles(chunkX, chunkZ);
ArrayList<HydrologyTileKey> relevantKeys = new ArrayList<>(Math.multiplyExact(
relevant.maximumX() - relevant.minimumX() + 1, relevant.maximumZ() - relevant.minimumZ() + 1));
for (int tileZ = relevant.minimumZ(); tileZ <= relevant.maximumZ(); tileZ++) {
for (int tileX = relevant.minimumX(); tileX <= relevant.maximumX(); tileX++) {
relevantKeys.add(new HydrologyTileKey(tileX, tileZ));
}
}
@@ -821,11 +953,17 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
if (knownEmpty(chunkX, chunkZ)) {
return ChunkColumns.empty(chunkX, chunkZ);
}
ArrayList<HydrologyTileKey> relevantKeys = relevantKeys(chunkX, chunkZ);
return composeChunkColumns(chunkX, chunkZ, relevantKeys, tiles(relevantKeys));
}
private ChunkColumns composeChunkColumnsInline(int chunkX, int chunkZ, long epoch) {
if (knownEmpty(chunkX, chunkZ)) {
return ChunkColumns.empty(chunkX, chunkZ);
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
@@ -839,41 +977,43 @@ public final class HydrologyTileCache implements AutoCloseable {
List<HydrologyTile> relevantTiles) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
int minimumTileX = relevantKeys.getFirst().tileX();
int maximumTileX = relevantKeys.getLast().tileX();
int minimumTileZ = relevantKeys.getFirst().tileZ();
int maximumTileZ = relevantKeys.getLast().tileZ();
prefetchNeighbours(minimumTileX, maximumTileX, minimumTileZ, maximumTileZ);
if (knownEmpty(chunkX, chunkZ)) {
return ChunkColumns.empty(chunkX, chunkZ);
}
HydrologyColumnSample[] columns = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
boolean published = false;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int blockX = minimumBlockX + localX;
int blockZ = minimumBlockZ + localZ;
columns[localZ * CHUNK_SIZE + localX] = composeColumn(
HydrologyColumnSample column = composeColumn(
blockX,
blockZ,
relevantKeys,
relevantTiles
)
.orElse(null);
columns[localZ * CHUNK_SIZE + localX] = column;
published |= column != null;
}
}
return new ChunkColumns(chunkX, chunkZ, columns);
return published ? new ChunkColumns(chunkX, chunkZ, columns) : ChunkColumns.empty(chunkX, chunkZ);
}
/**
* Plans the ring of tiles around the ones a chunk just needed, on the prefetch executor, so a
* generation front walking outward finds its next tiles already planned instead of stalling on
* a cold plan. Tiles already cached or already being prefetched are skipped; a tile a chunk
* needs before its prefetch finishes simply joins that computation.
* Plans the ring of tiles around the ones a chunk needs, on the prefetch executor, so a generation
* front walking outward finds its next tiles already planned instead of stalling on a cold plan.
* Tiles already planned or already being prefetched are skipped; a tile a chunk needs before its
* prefetch finishes simply joins that computation.
*/
private void prefetchNeighbours(int minimumTileX, int maximumTileX, int minimumTileZ, int maximumTileZ) {
private void prefetchNeighbours(TileBounds relevant) {
if (prefetchExecutor == null || !neighbourPrefetchEnabled) {
return;
}
for (int tileZ = minimumTileZ - 1; tileZ <= maximumTileZ + 1; tileZ++) {
for (int tileX = minimumTileX - 1; tileX <= maximumTileX + 1; tileX++) {
if (tileX >= minimumTileX && tileX <= maximumTileX && tileZ >= minimumTileZ && tileZ <= maximumTileZ) {
for (int tileZ = relevant.minimumZ() - 1; tileZ <= relevant.maximumZ() + 1; tileZ++) {
for (int tileX = relevant.minimumX() - 1; tileX <= relevant.maximumX() + 1; tileX++) {
if (tileX >= relevant.minimumX() && tileX <= relevant.maximumX()
&& tileZ >= relevant.minimumZ() && tileZ <= relevant.maximumZ()) {
continue;
}
prefetch(new HydrologyTileKey(tileX, tileZ));
@@ -886,33 +1026,51 @@ public final class HydrologyTileCache implements AutoCloseable {
if (prefetchExecutor == null) {
return;
}
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
centreBlockX, centreBlockZ), null);
TileBounds area = new TileBounds(
tileCoordinate((long) minimumBlockX - publicationRadius, tileSize),
tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize),
tileCoordinate((long) maximumBlockX + publicationRadius, tileSize),
tileCoordinate((long) maximumBlockZ + publicationRadius, tileSize));
enqueuePrefetchArea(nearestFirst(area, tileCoordinate(centreBlockX, tileSize),
tileCoordinate(centreBlockZ, tileSize), Integer.MAX_VALUE), null);
}
private List<HydrologyTileKey> prefetchAreaKeys(long minimumBlockX, long minimumBlockZ, long maximumBlockX,
long maximumBlockZ, int centreBlockX, int centreBlockZ) {
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) maximumBlockX + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) maximumBlockZ + publicationRadius, tileSize);
int centreTileX = tileCoordinate(centreBlockX, tileSize);
int centreTileZ = tileCoordinate(centreBlockZ, tileSize);
/** Tiles of the bounds in rings around the centre tile, each ring in row order, up to the limit. */
static List<HydrologyTileKey> nearestFirst(TileBounds bounds, int centreX, int centreZ, int limit) {
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
keys.add(new HydrologyTileKey(tileX, tileZ));
long rings = Math.max(
Math.max((long) centreX - bounds.minimumX(), (long) bounds.maximumX() - centreX),
Math.max((long) centreZ - bounds.minimumZ(), (long) bounds.maximumZ() - centreZ));
for (long ring = 0L; ring <= rings && keys.size() < limit; ring++) {
long firstZ = Math.max(bounds.minimumZ(), centreZ - ring);
long lastZ = Math.min(bounds.maximumZ(), centreZ + ring);
for (long tileZ = firstZ; tileZ <= lastZ && keys.size() < limit; tileZ++) {
if (tileZ == centreZ - ring || tileZ == centreZ + ring) {
long firstX = Math.max(bounds.minimumX(), centreX - ring);
long lastX = Math.min(bounds.maximumX(), centreX + ring);
for (long tileX = firstX; tileX <= lastX && keys.size() < limit; tileX++) {
keys.add(new HydrologyTileKey((int) tileX, (int) tileZ));
}
continue;
}
long westX = centreX - ring;
long eastX = centreX + ring;
if (westX >= bounds.minimumX() && westX <= bounds.maximumX()) {
keys.add(new HydrologyTileKey((int) westX, (int) tileZ));
}
if (eastX >= bounds.minimumX() && eastX <= bounds.maximumX() && keys.size() < limit) {
keys.add(new HydrologyTileKey((int) eastX, (int) tileZ));
}
}
}
keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX() - centreTileX), Math.abs(key.tileZ() - centreTileZ)))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
return keys;
}
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, PregenerationScope scope) {
Set<HydrologyTileKey> area = scope != null ? new HashSet<>(keys) : Set.of();
if (persistentStore == null) {
tiles.cleanUp();
}
synchronized (prefetchQueue) {
if (closed.get()) {
if (scope != null) {
@@ -937,7 +1095,7 @@ public final class HydrologyTileCache implements AutoCloseable {
return;
}
for (HydrologyTileKey key : keys) {
if (!enqueuePrefetch(key) && speculativeEntryCount() >= maximumEntries) {
if (!enqueuePrefetch(key) && speculationFull()) {
break;
}
}
@@ -947,12 +1105,14 @@ public final class HydrologyTileCache implements AutoCloseable {
private boolean prefetch(HydrologyTileKey key) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| occupancies.containsKey(key)
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)) {
return false;
}
if (persistentStore == null) {
tiles.cleanUp();
}
synchronized (prefetchQueue) {
if (!enqueuePrefetch(key)) {
return false;
@@ -965,10 +1125,10 @@ public final class HydrologyTileCache implements AutoCloseable {
private boolean enqueuePrefetch(HydrologyTileKey key) {
if (closed.get()
|| pregenerationScope != null && !pregenerationScope.bounds.contains(key)
|| tiles.getIfPresent(key) != null
|| occupancies.containsKey(key)
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
|| speculationFull()) {
return false;
}
queuedPrefetches.add(key);
@@ -976,6 +1136,15 @@ public final class HydrologyTileCache implements AutoCloseable {
return true;
}
/**
* Without a prepared-plan store a speculative tile lives only in memory, so speculation stops at
* the tile budget; with one every plan is persisted and its occupancy kept, so it never has to
* stay resident.
*/
private boolean speculationFull() {
return persistentStore == null && speculativeEntryCount() >= maximumEntries;
}
private int speculativeEntryCount() {
long entries = tiles.estimatedSize();
long budget = HydrologyCacheBudget.runtime().tileBytes();
@@ -993,39 +1162,59 @@ public final class HydrologyTileCache implements AutoCloseable {
return Math.max(0L, budget - weight) / expectedWeight;
}
/**
* Starts queued work while nothing is demanded: a nonblocking chunk query on its own, otherwise
* up to {@link #prefetchSlots} speculative roots at once. One caller pumps at a time and loops, so
* work that completes on the calling thread resumes the loop instead of recursing into it.
*/
private void pumpPrefetch() {
synchronized (prefetchQueue) {
if (prefetchExecutor == null || closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()
|| !prefetchActive.compareAndSet(false, true)) {
if (pumping) {
return;
}
pumping = true;
}
scheduleNextPrefetch();
}
private void scheduleNextPrefetch() {
boolean idle = false;
try {
while (!idle) {
ChunkCoordinate chunk = null;
CacheLoadKey<HydrologyTileKey> loadKey = null;
ChunkCoordinate chunk;
long epoch;
synchronized (prefetchQueue) {
if (closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()) {
prefetchActive.set(false);
return;
}
epoch = cacheEpoch.get();
if (prefetchExecutor != null && !closed.get() && demandBatches == 0 && demandedPlans.isEmpty()) {
chunk = pollQuery();
if (chunk != null) {
demandBatches++;
} else {
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
}
} else if (activePrefetches < prefetchSlots) {
HydrologyTileKey key = pollPrefetch(epoch);
if (key != null) {
activePrefetches++;
loadKey = new CacheLoadKey<>(epoch, key);
}
}
}
if (chunk == null && loadKey == null) {
pumping = false;
idle = true;
}
}
if (chunk != null) {
startQuery(chunk, epoch);
} else if (loadKey != null) {
planAsync(loadKey).whenComplete((tile, failure) -> finishPrefetch());
}
}
} finally {
if (!idle) {
synchronized (prefetchQueue) {
pumping = false;
}
}
}
}
private void startQuery(ChunkCoordinate chunk, long epoch) {
composeAsync(chunk, epoch).whenComplete((columns, failure) -> {
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
@@ -1033,17 +1222,20 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
finishDemandBatch(epoch);
scheduleNextPrefetch();
});
} else {
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
}
private void finishPrefetch() {
synchronized (prefetchQueue) {
activePrefetches--;
}
pumpPrefetch();
}
private ChunkCoordinate pollQuery() {
while (!queriedChunks.isEmpty()) {
ChunkCoordinate chunk = queriedChunks.getFirst();
if (composedChunks.getIfPresent(chunk.packed()) == null) {
if (composedChunks.getIfPresent(chunk.packed()) == null && !knownEmpty(chunk.x(), chunk.z())) {
return chunk;
}
queriedChunks.removeFirst();
@@ -1105,7 +1297,8 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private HydrologyTileKey pollPrefetch() {
private HydrologyTileKey pollPrefetch(long epoch) {
if (persistentStore == null) {
tiles.cleanUp();
long weight = tiles.policy().eviction().orElseThrow().weightedSize().orElse(0L);
if (speculativeCapacity(tiles.estimatedSize(), HydrologyCacheBudget.runtime().tileBytes(), weight) == 0L) {
@@ -1113,13 +1306,15 @@ public final class HydrologyTileCache implements AutoCloseable {
queuedPrefetches.clear();
return null;
}
}
HydrologyTileKey key;
while ((key = prefetchQueue.poll()) != null) {
if (!queuedPrefetches.remove(key)) {
continue;
}
if ((pregenerationScope == null || pregenerationScope.bounds.contains(key))
&& tiles.getIfPresent(key) == null) {
&& !occupancies.containsKey(key)
&& !planning.containsKey(new CacheLoadKey<>(epoch, key))) {
return key;
}
}
@@ -1449,7 +1644,7 @@ public final class HydrologyTileCache implements AutoCloseable {
private record PendingTile(int index, CompletableFuture<HydrologyTile> future) {
}
private record TileBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
record TileBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
private boolean contains(HydrologyTileKey key) {
return key.tileX() >= minimumX && key.tileX() <= maximumX
&& key.tileZ() >= minimumZ && key.tileZ() <= maximumZ;
@@ -1458,6 +1653,9 @@ public final class HydrologyTileCache implements AutoCloseable {
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
private long estimatedBytes() {
if (empty()) {
return 64L;
}
long bytes = 64L + 8L * columns.length;
for (HydrologyColumnSample column : columns) {
if (column != null) {
@@ -1467,9 +1665,16 @@ public final class HydrologyTileCache implements AutoCloseable {
return bytes;
}
/** A chunk with no hydrology at all, the answer for a caller that may not wait for its plan. */
/**
* A chunk with no hydrology at all: one no planned tile publishes into, or the answer for a
* caller that may not wait for its plan.
*/
private static ChunkColumns empty(int chunkX, int chunkZ) {
return new ChunkColumns(chunkX, chunkZ, new HydrologyColumnSample[CHUNK_COLUMN_COUNT]);
return new ChunkColumns(chunkX, chunkZ, NO_COLUMNS);
}
private boolean empty() {
return columns == NO_COLUMNS;
}
private Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
@@ -1479,7 +1684,27 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
/** Direct-mapped over a 4 by 4 chunk neighbourhood, so a placement crossing chunk edges keeps its chunks. */
private static final class LocalChunks {
private final ChunkColumns[] slots = new ChunkColumns[LOCAL_CHUNK_SLOTS];
private final long[] aroundKeys = new long[LOCAL_CHUNK_SLOTS];
private final byte[] around = new byte[LOCAL_CHUNK_SLOTS];
private long epoch = Long.MIN_VALUE;
private TileBounds prefetched;
private ChunkColumns get(int chunkX, int chunkZ) {
ChunkColumns columns = slots[slot(chunkX, chunkZ)];
return columns != null && columns.chunkX() == chunkX && columns.chunkZ() == chunkZ ? columns : null;
}
private ChunkColumns put(ChunkColumns columns) {
slots[slot(columns.chunkX(), columns.chunkZ())] = columns;
return columns;
}
private static int slot(int chunkX, int chunkZ) {
return (chunkX & 3) | (chunkZ & 3) << 2;
}
}
private record ChunkCoordinate(int x, int z) {
@@ -195,6 +195,11 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
cache.prepareChunkColumns(blockX, blockZ);
}
/** Whether the chunk or one of its eight neighbours holds a planned footprint column. */
public boolean hasColumnsAround(int chunkX, int chunkZ) {
return cache.hasColumnsAround(chunkX, chunkZ);
}
public HydrologyRenderSample renderSample(double x, double z) {
int blockX = (int) StrictMath.floor(x);
int blockZ = (int) StrictMath.floor(z);
@@ -75,7 +75,8 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null || !complex.allowsMantleChunkWrite(chunkX, chunkZ)) {
if (runtime == null || !complex.allowsMantleChunkWrite(chunkX, chunkZ)
|| !runtime.hasColumnsAround(chunkX, chunkZ)) {
return;
}
int minimumX = chunkX << 4;
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.block.TileData;
@@ -32,6 +33,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final List<Mutation> mutations;
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
private boolean hydrologyGateKnown;
private int hydrologyGateChunkX;
private int hydrologyGateChunkZ;
private boolean hydrologyGateOpen;
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
this.writer = writer;
@@ -350,10 +355,24 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
private boolean hasProtectedHydrology(int x, int y, int z) {
if (!mayHoldHydrologyCells(x >> 4, z >> 4)) {
return false;
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
return hydrology != null && hydrology.protectsPlacement();
}
private boolean mayHoldHydrologyCells(int chunkX, int chunkZ) {
if (!hydrologyGateKnown || hydrologyGateChunkX != chunkX || hydrologyGateChunkZ != chunkZ) {
IrisComplex complex = writer.getEngine().getComplex();
hydrologyGateOpen = complex == null || complex.mayHoldHydrologyCells(chunkX, chunkZ);
hydrologyGateChunkX = chunkX;
hydrologyGateChunkZ = chunkZ;
hydrologyGateKnown = true;
}
return hydrologyGateOpen;
}
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
entries.sort((first, second) -> Boolean.compare(
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stream.GenerationStreams;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.configuration.IrisSettings;
@@ -991,7 +992,11 @@ public class IrisComplex implements DataProvider {
HydrologyColumnLayer layer = sample.primarySurfaceLayerOrNull();
if (hydrologyBanks3D != null && layer != null && layer.terrainOwned() && !layer.channel()) {
Optional<Terrain3DColumn> column = hydrologyBanks3D.columnIfReady(blockCoordinate(x), blockCoordinate(z));
return column.isPresent() ? column.get().topY() : naturalHeightStream.getDouble(x, z);
if (column.isPresent()) {
return column.get().topY();
}
ProvisionalSampling.mark();
return naturalHeightStream.getDouble(x, z);
}
return sample.terrainHeight();
}
@@ -1184,8 +1189,11 @@ public class IrisComplex implements DataProvider {
return false;
}
HydrologyColumnLayer layer = snapshot.column() == null ? null : snapshot.column().primarySurfaceLayerOrNull();
return layer == null || !layer.terrainOwned() || layer.channel()
|| hydrologyBanks3D.columnIfReady(x, z).isPresent();
if (layer == null || !layer.terrainOwned() || layer.channel() || hydrologyBanks3D.columnIfReady(x, z).isPresent()) {
return true;
}
ProvisionalSampling.mark();
return false;
}
public ProceduralStream<Double> getRawHeightStream() {
@@ -1218,6 +1226,17 @@ public class IrisComplex implements DataProvider {
|| !transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
}
/**
* Whether the mantle chunk can hold hydrology cave cells. The hydrology component publishes them
* only from footprint columns within one block of the chunk, so a chunk with none around it holds
* none. A transition world or a studio keeps cells an earlier plan wrote, so there every chunk may.
*/
public boolean mayHoldHydrologyCells(int chunkX, int chunkZ) {
return hydrologyRuntime == null || transitionGenerationPlan != null
|| terrainEngine == null || terrainEngine.isStudio()
|| hydrologyRuntime.hasColumnsAround(chunkX, chunkZ);
}
public boolean allowsNewDiscreteContentAt(int blockX, int blockZ) {
return transitionGenerationPlan == null
|| transitionGenerationPlan.allowsNewDiscreteContentAt(blockX, blockZ);
@@ -18,7 +18,7 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
super();
this.stream = stream;
this.engine = engine;
this.cache = new WorldCache2DDouble((x, z) -> stream.getDouble(x, z), size);
this.cache = new WorldCache2DDouble((x, z) -> ProvisionalSampling.memoizableDouble(stream, x, z), size);
IrisServices.get(PreservationRegistry.class).registerCache(this);
}
@@ -34,7 +34,7 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
@Override
public Double get(double x, double z) {
return cache.get((int) x, (int) z);
return getDouble(x, z);
}
@Override
@@ -44,7 +44,11 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
@Override
public double getDouble(double x, double z) {
try {
return cache.get((int) x, (int) z);
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.doubleValue();
}
}
@Override
@@ -75,13 +79,29 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
try {
cache.fillChunk(chunkX, chunkZ, target);
} catch (ProvisionalSampling.Unmemoizable provisional) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
}
@Override
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
try {
cache.fillChunk(chunkX, chunkZ, target);
} catch (ProvisionalSampling.Unmemoizable provisional) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
}
}
@@ -36,7 +36,8 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
super();
this.stream = stream;
this.engine = engine;
cache = WorldCache2D.<T>ofInts(stream::get, size, () -> new ChunkCache2D<>("iris"));
cache = WorldCache2D.<T>ofInts((x, z) -> ProvisionalSampling.memoizable(stream, x, z), size,
() -> new ChunkCache2D<>("iris"));
IrisServices.get(PreservationRegistry.class).registerCache(this);
}
@@ -52,7 +53,11 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
@Override
public T get(double x, double z) {
try {
return cache.get((int) x, (int) z);
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
}
@Override
@@ -87,6 +92,14 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
try {
cache.fillChunk(chunkX, chunkZ, target);
} catch (ProvisionalSampling.Unmemoizable provisional) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = get(originX + (index & 15), originZ + (index >> 4));
}
}
}
}
@@ -0,0 +1,110 @@
package art.arcane.iris.generation.stream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.function.Supplier;
/**
* Tracks answers a thread gave from inputs that are not final yet. The server thread never waits for
* a hydrology plan, so it answers from natural terrain until the plan lands; a value derived from such
* an answer is right for that caller only and must never be memoized, or generation would later read
* it and its output would depend on timing.
*/
public final class ProvisionalSampling {
private static final ThreadLocal<Tracker> TRACKERS = ThreadLocal.withInitial(Tracker::new);
private ProvisionalSampling() {
}
/** Records that the calling thread just answered from inputs that are not final. */
public static void mark() {
TRACKERS.get().provisional = true;
}
/**
* The stream's value for a memoizing cache, or {@link Unmemoizable} carrying it when sampling it
* read a provisional answer. The mark still reaches every enclosing resolver.
*/
public static <T> T memoizable(ProceduralStream<T> stream, int x, int z) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
T value;
boolean provisional;
try {
value = stream.get(x, z);
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
throw new Unmemoizable(value);
}
return value;
}
public static double memoizableDouble(ProceduralStream<Double> stream, int x, int z) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
double value;
boolean provisional;
try {
value = stream.getDouble(x, z);
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
throw new Unmemoizable(value);
}
return value;
}
public static <T> T memoizable(Supplier<T> resolver) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
T value;
boolean provisional;
try {
value = resolver.get();
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
throw new Unmemoizable(value);
}
return value;
}
private static final class Tracker {
private boolean provisional;
private boolean begin() {
boolean outer = provisional;
provisional = false;
return outer;
}
private boolean end(boolean outer) {
boolean inner = provisional;
provisional = outer || inner;
return inner;
}
}
/** Carries a provisional value past a memoizing cache so the cache stores nothing. */
public static final class Unmemoizable extends RuntimeException {
private final transient Object value;
private Unmemoizable(Object value) {
super(null, null, false, false);
this.value = value;
}
@SuppressWarnings("unchecked")
public <T> T value() {
return (T) value;
}
public double doubleValue() {
return (Double) value;
}
}
}
@@ -10,6 +10,7 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.DimensionTerrainContext;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
@@ -327,7 +328,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
}
}
H resolvedHolder = resolveStructureBiomeHolder(x, y, z);
H resolvedHolder;
try {
resolvedHolder = ProvisionalSampling.memoizable(() -> resolveStructureBiomeHolder(x, y, z));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (!cacheable) {
return resolvedHolder;
}
@@ -453,7 +459,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
if (surfaceHolder != null) {
return surfaceHolder;
}
H resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
H resolvedSurfaceHolder;
try {
resolvedSurfaceHolder = ProvisionalSampling.memoizable(() -> resolveSurfaceStructureBiomeHolder(x, z));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
H existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
@@ -624,7 +635,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
}
}
H resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
H resolvedHolder;
try {
resolvedHolder = ProvisionalSampling.memoizable(() -> resolveVisibleBiomeHolder(x, y, z, resolverState));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (!cacheable) {
return resolvedHolder;
}
@@ -69,7 +69,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final long MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
private static final long ADMISSION_WAIT_BOUND_MS = 500L;
private static final int HYDROLOGY_PREFETCH_HALO_BLOCKS = 512;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -283,10 +282,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
HydrologyTileCache.PregenerationArea area = new HydrologyTileCache.PregenerationArea(
centerBlockX, centerBlockZ,
(long) boundsMinRegionX * 512L - HYDROLOGY_PREFETCH_HALO_BLOCKS,
(long) boundsMinRegionZ * 512L - HYDROLOGY_PREFETCH_HALO_BLOCKS,
((long) boundsMaxRegionX + 1L) * 512L - 1L + HYDROLOGY_PREFETCH_HALO_BLOCKS,
((long) boundsMaxRegionZ + 1L) * 512L - 1L + HYDROLOGY_PREFETCH_HALO_BLOCKS);
(long) boundsMinRegionX * 512L,
(long) boundsMinRegionZ * 512L,
((long) boundsMaxRegionX + 1L) * 512L - 1L,
((long) boundsMaxRegionZ + 1L) * 512L - 1L);
HydrologyTileCache.PregenerationScope scope = engine.getComplex().getHydrologyRuntime().preparePregeneration(area);
hydrologyPrefetchScope = scope;
if (closing.get()) {
@@ -0,0 +1,52 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class HydrologyChunkOccupancyTest {
@Test
public void occupiesExactlyTheChunksHoldingFootprintColumnsAtSignedCoordinates() {
int[][] columns = {{0, 0}, {15, 15}, {-1, -1}, {-17, 40}, {1000, -2000}, {-4097, 4096}};
HydrologyChunkOccupancy occupancy = HydrologyChunkOccupancy.of(footprint(columns));
HashSet<Long> expected = new HashSet<>();
for (int[] column : columns) {
expected.add(RiverFootprint.pack(Math.floorDiv(column[0], 16), Math.floorDiv(column[1], 16)));
}
int occupied = 0;
for (int chunkZ = -300; chunkZ <= 300; chunkZ++) {
for (int chunkX = -300; chunkX <= 300; chunkX++) {
boolean holds = occupancy.occupies(chunkX, chunkZ);
assertEquals(expected.contains(RiverFootprint.pack(chunkX, chunkZ)), holds);
occupied += holds ? 1 : 0;
}
}
assertEquals(expected.size(), occupied);
assertFalse(occupancy.occupies(Integer.MAX_VALUE >> 4, Integer.MIN_VALUE >> 4));
}
@Test
public void footprintsWithoutColumnsOccupyNothing() {
assertSame(HydrologyChunkOccupancy.EMPTY, HydrologyChunkOccupancy.of(RiverFootprint.empty()));
assertSame(HydrologyChunkOccupancy.EMPTY, HydrologyChunkOccupancy.of(null));
assertFalse(HydrologyChunkOccupancy.EMPTY.occupies(0, 0));
assertTrue(HydrologyChunkOccupancy.of(footprint(new int[][]{{3, 4}})).occupies(0, 0));
}
private static RiverFootprint footprint(int[][] columns) {
Map<Long, HydrologyColumnSample> samples = new HashMap<>();
for (int[] column : columns) {
samples.put(RiverFootprint.pack(column[0], column[1]),
new HydrologyColumnSample(column[0], column[1], 80, 63, false, "parent", List.of()));
}
return new RiverFootprint(samples);
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
@@ -14,6 +15,7 @@ import java.lang.reflect.Field;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -399,6 +401,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(0, 0));
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(tile.columnAt(anyInt(), anyInt())).thenAnswer(invocation -> {
@@ -625,8 +628,10 @@ public class HydrologyTileCacheTest {
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0, -1536, -1536, 2047, 2047))) {
cache.prefetchArea(-4096, -4096, 4095, 4095, 0, 0);
assertEquals(prefetchRectangle(-2, 2, -2, 2), new HashSet<>(planned));
assertEquals(25, planned.size());
cache.prefetchArea(-4096, -4096, 4095, 4095, 0, 0);
assertEquals(prefetchRectangle(-3, 2, -3, 2), new HashSet<>(planned));
planned.clear();
HydrologyTileKey demanded = new HydrologyTileKey(8, 8);
assertSame(tile, cache.get(demanded));
@@ -682,27 +687,81 @@ public class HydrologyTileCacheTest {
}
@Test
public void largePregenBoundsKeepInitialWarmupLocal() {
public void largePregenBoundsPlanTheNearestTilesUpToTheLookaheadLimit() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
usePreparedStore(cache, mock(PreparedHydrologyTileStore.class));
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0,
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE))) {
assertEquals(prefetchRectangle(-2, 1, -2, 1), new HashSet<>(planned));
assertEquals(16, planned.size());
assertEquals(4096, planned.size());
assertEquals(4096, new HashSet<>(planned).size());
assertEquals(new HydrologyTileKey(0, 0), planned.getFirst());
assertEquals(prefetchRectangle(-1, 1, -1, 1), new HashSet<>(planned.subList(0, 9)));
assertTrue(new HashSet<>(planned).containsAll(prefetchRectangle(-31, 31, -31, 31)));
for (HydrologyTileKey key : planned) {
assertTrue(Math.max(Math.abs(key.tileX()), Math.abs(key.tileZ())) <= 32);
}
}
cache.close();
}
@Test
public void preparedStoreLetsPregenSpeculationRunPastTheTileBudget() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
RiverFootprint footprint = mock(RiverFootprint.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(tile.footprint()).thenReturn(footprint);
when(footprint.size()).thenReturn((int) (HydrologyCacheBudget.runtime().tileBytes() * 3L / 5L / 64L));
when(footprint.columns()).thenReturn(Map.of());
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
PreparedHydrologyTileStore store = mock(PreparedHydrologyTileStore.class);
usePreparedStore(cache, store);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(pregenArea(0, 0))) {
drainPrefetchTasks(queued);
assertEquals(prefetchRectangle(-2, 1, -2, 1), new HashSet<>(planned));
assertEquals(16, planned.size());
assertTrue(cache.size() < 16);
verify(store, times(16)).save(tile);
assertTrue(cache.isPlanned(40, 40));
}
cache.close();
}
private static void usePreparedStore(HydrologyTileCache cache, PreparedHydrologyTileStore store) throws Exception {
Field persistentStore = HydrologyTileCache.class.getDeclaredField("persistentStore");
persistentStore.setAccessible(true);
persistentStore.set(cache, store);
}
/** Gives a mocked tile one footprint column in each listed chunk, so those chunks compose from it. */
private static void publishesInto(HydrologyTile tile, long... chunks) {
HashMap<Long, HydrologyColumnSample> columns = new HashMap<>();
for (long chunk : chunks) {
columns.put(RiverFootprint.pack(RiverFootprint.unpackX(chunk) * 16, RiverFootprint.unpackZ(chunk) * 16),
mock(HydrologyColumnSample.class));
}
RiverFootprint footprint = mock(RiverFootprint.class);
when(footprint.columns()).thenReturn(columns);
when(tile.footprint()).thenReturn(footprint);
}
private static HydrologyTileCache.PregenerationArea pregenArea(int x, int z) {
return new HydrologyTileCache.PregenerationArea(x, z, (long) x - 1024, (long) z - 1024,
(long) x + 1024, (long) z + 1024);
@@ -868,6 +927,7 @@ public class HydrologyTileCacheTest {
ArrayList<Long> visitedColumns = new ArrayList<>();
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(2, 3));
doAnswer(invocation -> {
int blockX = invocation.getArgument(0);
int blockZ = invocation.getArgument(1);
@@ -1642,6 +1702,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(0, 0), RiverFootprint.pack(secondChunkX, 0));
CountDownLatch started = new CountDownLatch(2);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
@@ -1675,6 +1736,7 @@ public class HydrologyTileCacheTest {
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
publishesInto(tile, RiverFootprint.pack(0, 0));
ExecutorService executor = Executors.newFixedThreadPool(8);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
@@ -1779,6 +1841,8 @@ public class HydrologyTileCacheTest {
HydrologyTile newTile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(oldTile, newTile);
publishesInto(oldTile, RiverFootprint.pack(0, 0));
publishesInto(newTile, RiverFootprint.pack(0, 0));
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
@@ -1815,6 +1879,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(0, 0));
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
doAnswer(invocation -> cache.columnAt(0, 0)).when(tile).columnAt(anyInt(), anyInt());
@@ -2338,6 +2403,7 @@ public class HydrologyTileCacheTest {
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
publishesInto(tile, RiverFootprint.pack(2, 2));
List<Runnable> queued = new ArrayList<>();
AtomicBoolean forbidden = new AtomicBoolean(true);
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add, forbidden::get);
@@ -2399,6 +2465,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(2, 2));
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, null, () -> true);
cache.get(new HydrologyTileKey(0, 0));
AtomicBoolean evicted = new AtomicBoolean();
@@ -2417,4 +2484,120 @@ public class HydrologyTileCacheTest {
assertFalse(cache.columnSnapshot(40, 40).available());
}
@Test
public void occupancyAnswersEmptyChunksExactlyWhereCompositionFindsNoColumn() {
HydrologyPlannerSettings settings = featureSettings();
HydrologyPlanner planner = new HydrologyPlanner(811L, settings, this::featureTerrain);
HydrologyTileCache cache = new HydrologyTileCache(planner, 256);
int tileSize = settings.routing().tileSize();
int radius = settings.publicationRadius();
int emptyChunks = 0;
int publishedChunks = 0;
for (int chunkZ = -2; chunkZ <= 5; chunkZ++) {
for (int chunkX = -2; chunkX <= 5; chunkX++) {
boolean published = false;
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
int x = chunkX * 16 + localX;
int z = chunkZ * 16 + localZ;
boolean expected = false;
for (int tileZ = Math.floorDiv(z - radius, tileSize); tileZ <= Math.floorDiv(z + radius, tileSize); tileZ++) {
for (int tileX = Math.floorDiv(x - radius, tileSize); tileX <= Math.floorDiv(x + radius, tileSize); tileX++) {
expected |= cache.get(new HydrologyTileKey(tileX, tileZ)).columnAt(x, z).isPresent();
}
}
assertEquals(expected, cache.columnAt(x, z).isPresent());
published |= expected;
}
}
if (published) {
publishedChunks++;
} else {
emptyChunks++;
}
}
}
assertTrue(emptyChunks > 0);
assertTrue(publishedChunks > 0);
}
@Test
public void emptyChunksAnswerFromOccupancyAfterTheirTilesLeaveMemory() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
publishesInto(tile, RiverFootprint.pack(0, 0));
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
HydrologyTileKey origin = new HydrologyTileKey(0, 0);
assertTrue(cache.columnAt(552, 552).isEmpty());
verify(planner, times(1)).plan(origin);
verify(tile, never()).columnAt(anyInt(), anyInt());
Field tilesField = HydrologyTileCache.class.getDeclaredField("tiles");
tilesField.setAccessible(true);
((Cache<?, ?>) tilesField.get(cache)).invalidateAll();
assertEquals(0, cache.size());
assertTrue(cache.columnAt(568, 552).isEmpty());
assertTrue(cache.isPlanned(568, 552));
assertTrue(cache.columnSnapshot(568, 552).available());
verify(planner, times(1)).plan(origin);
verify(tile, never()).columnAt(anyInt(), anyInt());
cache.columnAt(8, 8);
verify(tile, times(4 * 256)).columnAt(anyInt(), anyInt());
}
@Test
public void speculativeRootsLeaveOneRootPermitForDemand() {
assertEquals(1, HydrologyTileCache.prefetchSlots(1));
assertEquals(1, HydrologyTileCache.prefetchSlots(2));
assertEquals(3, HydrologyTileCache.prefetchSlots(4));
assertEquals(7, HydrologyTileCache.prefetchSlots(8));
}
@Test
public void nearestFirstWalksRingsInRowOrderInsideTheBounds() {
HydrologyTileCache.TileBounds bounds = new HydrologyTileCache.TileBounds(-2, -1, 3, 2);
List<HydrologyTileKey> keys = HydrologyTileCache.nearestFirst(bounds, 0, 0, Integer.MAX_VALUE);
ArrayList<HydrologyTileKey> expected = new ArrayList<>(prefetchRectangle(-2, 3, -1, 2));
expected.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX()), Math.abs(key.tileZ())))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
assertEquals(expected, keys);
List<HydrologyTileKey> outside = HydrologyTileCache.nearestFirst(bounds, 9, -7, 5);
assertEquals(5, outside.size());
assertEquals(new HydrologyTileKey(3, -1), outside.getFirst());
for (HydrologyTileKey key : outside) {
assertTrue(key.tileX() >= -2 && key.tileX() <= 3 && key.tileZ() >= -1 && key.tileZ() <= 2);
}
}
@Test
public void neighbourhoodAnswersFollowPublishedColumnsAndResetWithTheEpoch() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile publishing = mock(HydrologyTile.class);
HydrologyTile empty = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
AtomicReference<HydrologyTile> current = new AtomicReference<>(publishing);
when(planner.plan(any(HydrologyTileKey.class))).thenAnswer(invocation -> current.get());
publishesInto(publishing, RiverFootprint.pack(0, 0));
when(publishing.columnAt(0, 0)).thenReturn(Optional.of(
new HydrologyColumnSample(0, 0, 90, 63, false, "parent", List.of())));
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
assertTrue(cache.hasColumnsAround(1, 1));
assertTrue(cache.hasColumnsAround(-1, 0));
assertFalse(cache.hasColumnsAround(2, 0));
assertTrue(cache.hasColumnsAround(1, 1));
current.set(empty);
cache.clear();
assertFalse(cache.hasColumnsAround(1, 1));
assertFalse(cache.hasColumnsAround(0, 0));
}
}
@@ -61,10 +61,12 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class MantleHydrologyComponentTest {
@@ -2007,4 +2009,23 @@ public class MantleHydrologyComponentTest {
openToSurface.add(position);
}
}
@Test
public void chunksWithoutNearbyFootprintColumnsPublishNothingWithoutReadingCavePlans() {
IrisComplex complex = mock(IrisComplex.class);
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
ChunkContext context = mock(ChunkContext.class);
MantleWriter writer = mock(MantleWriter.class);
when(context.getComplex()).thenReturn(complex);
when(complex.getHydrologyRuntime()).thenReturn(runtime);
when(complex.allowsMantleChunkWrite(3, -4)).thenReturn(true);
when(runtime.hasColumnsAround(3, -4)).thenReturn(false);
new MantleHydrologyComponent(mock(EngineMantle.class)).generateLayer(writer, 3, -4, context);
verify(runtime).hasColumnsAround(3, -4);
verify(runtime, never()).cavePlansIn(anyInt(), anyInt(), anyInt(), anyInt());
verify(complex, never()).sampleHydrologyColumn(anyDouble(), anyDouble());
verifyNoInteractions(writer);
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
@@ -74,6 +75,7 @@ public class MantleWriterPreObjectJournalTest {
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getComplex()).thenReturn(mock(IrisComplex.class));
when(engineMantle.getEngine()).thenReturn(mock(Engine.class));
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
matter = spy(new IrisMatter(16, 16, 16));
@@ -43,6 +43,7 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -657,4 +658,30 @@ public class ObjectDestinationTransactionTest {
private record Marker(String value) {
}
@Test
public void chunksThatCannotHoldHydrologyCellsSkipTheCellLookup() {
MantleWriter writer = writer();
IrisComplex complex = mock(IrisComplex.class);
when(writer.getEngine().getComplex()).thenReturn(complex);
when(complex.mayHoldHydrologyCells(1, 0)).thenReturn(true);
when(writer.getPrerequisiteDataIfPresent(anyInt(), anyInt(), anyInt(), any()))
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
NativeBlockState block = mock(NativeBlockState.class);
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
for (int x = 0; x < 16; x++) {
transaction.setData(x, 4, 0, block);
}
transaction.setData(16, 4, 0, block);
for (int x = 0; x < 16; x++) {
assertSame(block, transaction.get(x, 4, 0));
}
assertEquals(16, transaction.sourcePlanSince(0).mutationsFor(0, 0).size());
assertTrue(transaction.sourcePlanSince(0).mutationsFor(1, 0).isEmpty());
verify(complex, times(1)).mayHoldHydrologyCells(0, 0);
verify(writer, never()).getPrerequisiteDataIfPresent(0, 4, 0, HydrologyCaveCell.class);
verify(writer).getPrerequisiteDataIfPresent(16, 4, 0, HydrologyCaveCell.class);
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
public class ProvisionalSamplingTest {
@Test
public void provisionalValuesReachTheCallerWithoutBeingMemoized() {
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
AtomicBoolean planned = new AtomicBoolean();
AtomicInteger samples = new AtomicInteger();
ProceduralStream<Double> source = ProceduralStream.ofDouble((x, z) -> {
samples.incrementAndGet();
if (!planned.get()) {
ProvisionalSampling.mark();
return -1D;
}
return x + z;
});
CachedDoubleStream2D cached = new CachedDoubleStream2D("provisional", mock(Engine.class), source, 4);
assertEquals(-1D, cached.getDouble(3, 4), 0D);
assertEquals(-1D, cached.get(3D, 4D), 0D);
assertEquals(2, samples.get());
planned.set(true);
assertEquals(7D, cached.getDouble(3, 4), 0D);
assertEquals(7D, cached.getDouble(3, 4), 0D);
assertEquals(3, samples.get());
}
}
@Test
public void derivedCachesInheritTheMarkAndFillChunksWithoutStoringIt() {
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
AtomicBoolean planned = new AtomicBoolean();
AtomicInteger samples = new AtomicInteger();
ProceduralStream<Double> source = ProceduralStream.ofDouble((x, z) -> {
samples.incrementAndGet();
if (!planned.get() && x == 5 && z == 0) {
ProvisionalSampling.mark();
return -1D;
}
return 1D;
});
CachedDoubleStream2D base = new CachedDoubleStream2D("base", mock(Engine.class), source, 4);
CachedStream2D<String> derived = new CachedStream2D<>("derived", mock(Engine.class),
base.convert((Double value) -> value < 0D ? "natural" : "planned"), 4);
Object[] row = new Object[256];
derived.fillChunkRaw(0, 0, row);
assertEquals("natural", row[5]);
assertEquals("planned", row[6]);
assertEquals("natural", derived.get(5D, 0D));
planned.set(true);
int before = samples.get();
assertEquals("planned", derived.get(5D, 0D));
assertEquals(before + 1, samples.get());
assertEquals("planned", derived.get(6D, 0D));
assertEquals(before + 1, samples.get());
}
}
}