Merge L3-hydrology

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-28 12:20:03 -04:00
commit 9d3597703d
18 files changed
+1058 -191

No files matched your search

@@ -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,47 +790,109 @@ 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;
}
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
if (waitsForbidden()) {
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
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);
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestChunk(chunkX, chunkZ);
return ChunkColumns.empty(chunkX, chunkZ);
}
snapshots.add(tile);
}
ChunkColumns columns = composeChunkColumns(chunkX, chunkZ, keys, snapshots);
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
}
}
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return 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);
}
ChunkColumns columns = loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
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));
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestChunk(chunkX, chunkZ);
return null;
}
snapshots.add(tile);
}
ChunkColumns columns = composeChunkColumns(chunkX, chunkZ, keys, snapshots);
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, 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) {
long epoch = loadKey.epoch();
long packedChunk = loadKey.key();
@@ -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();
tiles.cleanUp();
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;
}
tiles.cleanUp();
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,57 +1162,80 @@ 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() {
CacheLoadKey<HydrologyTileKey> loadKey = null;
ChunkCoordinate chunk;
long epoch;
synchronized (prefetchQueue) {
if (closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()) {
prefetchActive.set(false);
return;
}
epoch = cacheEpoch.get();
chunk = pollQuery();
if (chunk != null) {
demandBatches++;
} else {
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
}
loadKey = new CacheLoadKey<>(epoch, key);
}
}
if (chunk != null) {
composeAsync(chunk, epoch).whenComplete((columns, failure) -> {
boolean idle = false;
try {
while (!idle) {
ChunkCoordinate chunk = null;
CacheLoadKey<HydrologyTileKey> loadKey = null;
long epoch;
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
queriedChunks.remove(chunk);
epoch = cacheEpoch.get();
if (prefetchExecutor != null && !closed.get() && demandBatches == 0 && demandedPlans.isEmpty()) {
chunk = pollQuery();
if (chunk != null) {
demandBatches++;
} 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;
}
}
finishDemandBatch(epoch);
scheduleNextPrefetch();
});
} else {
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
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()) {
queriedChunks.remove(chunk);
}
}
finishDemandBatch(epoch);
});
}
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,13 +1297,15 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private HydrologyTileKey pollPrefetch() {
tiles.cleanUp();
long weight = tiles.policy().eviction().orElseThrow().weightedSize().orElse(0L);
if (speculativeCapacity(tiles.estimatedSize(), HydrologyCacheBudget.runtime().tileBytes(), weight) == 0L) {
prefetchQueue.clear();
queuedPrefetches.clear();
return null;
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) {
prefetchQueue.clear();
queuedPrefetches.clear();
return null;
}
}
HydrologyTileKey key;
while ((key = prefetchQueue.poll()) != null) {
@@ -1119,7 +1313,8 @@ public final class HydrologyTileCache implements AutoCloseable {
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) {
return cache.get((int) x, (int) 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;
cache.fillChunk(chunkX, chunkZ, target);
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;
cache.fillChunk(chunkX, chunkZ, target);
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) {
return cache.get((int) x, (int) 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;
cache.fillChunk(chunkX, chunkZ, target);
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()) {