mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Merge L3-hydrology
This commit is contained in:
@@ -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) {
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+19
@@ -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()) {
|
||||
|
||||
+52
@@ -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);
|
||||
}
|
||||
}
|
||||
+188
-5
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -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
@@ -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));
|
||||
|
||||
+27
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user