CORRECTIONS

- this is the corrections from the afoemrnioned commits
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-04 00:02:43 -04:00
1 parent a771fa6856
commit 366033a535
46 files changed
+2425 -256

No files matched your search

@@ -43,6 +43,7 @@ import art.arcane.iris.util.common.scheduling.J;
import java.awt.Color;
import java.awt.EventQueue;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
@@ -69,6 +70,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
private final MemoryMonitor monitor;
private final PregenTask task;
private final AtomicBoolean saving;
private final AtomicBoolean stopRequested;
private final List<Consumer<Double>> onProgress = new CopyOnWriteArrayList<>();
private final List<Runnable> whenDone = new CopyOnWriteArrayList<>();
private final IrisPregenerator pregenerator;
@@ -91,10 +93,14 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
private volatile long lastElapsed = 0L;
private volatile String lastMethod = IrisLanguage.plain(DesktopUiMessages.PREGEN_METHOD_PENDING);
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
public PregeneratorJob(Configuration configuration) {
PregenTask task = configuration.task();
PregeneratorMethod method = configuration.method();
Engine engine = configuration.engine();
this.engine = engine;
monitor = new MemoryMonitor(50);
saving = new AtomicBoolean(false);
stopRequested = new AtomicBoolean(false);
info = new String[]{IrisLanguage.plain(DesktopUiMessages.PREGEN_INITIALIZING)};
this.task = task;
this.pregenerator = new IrisPregenerator(task, method, this);
@@ -122,11 +128,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
}
}
worker = new Thread(() -> {
J.sleep(1000);
computeBounds();
this.pregenerator.start();
}, "Iris Pregenerator");
worker = new Thread(() -> runWorker(configuration.preparation()), "Iris Pregenerator");
worker.setPriority(Thread.MIN_PRIORITY);
worker.setDaemon(true);
worker.setUncaughtExceptionHandler((thread, ex) -> IrisLogging.reportError(ex));
@@ -155,6 +157,26 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
}
}
private void runWorker(Runnable preparation) {
try {
J.sleep(1000);
if (stopRequested.get()) {
onClose();
return;
}
preparation.run();
if (stopRequested.get()) {
onClose();
return;
}
computeBounds();
pregenerator.start();
} catch (Throwable failure) {
IrisLogging.reportError("Pregen startup failed.", failure);
onClose();
}
}
private void computeBounds() {
task.iterateAllChunks((xx, zz) -> {
min.setX(Math.min(xx, min.getX()));
@@ -178,10 +200,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return false;
}
J.a(() -> {
inst.pregenerator.close();
inst.worker.interrupt();
});
inst.requestStop();
return true;
}
@@ -204,8 +223,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
}
private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs) {
inst.pregenerator.close();
inst.worker.interrupt();
inst.requestStop();
try {
inst.worker.join(Math.max(1L, timeoutMs));
} catch (InterruptedException e) {
@@ -357,12 +375,16 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
}
public void stop() {
J.a(() -> {
pregenerator.close();
worker.interrupt();
close();
instance.compareAndSet(this, null);
});
requestStop();
close();
}
private void requestStop() {
if (!stopRequested.compareAndSet(false, true)) {
return;
}
pregenerator.close();
worker.interrupt();
}
public void close() {
@@ -583,4 +605,17 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
public String[] progress() {
return info;
}
public record Configuration(
PregenTask task,
PregeneratorMethod method,
Engine engine,
Runnable preparation
) {
public Configuration {
Objects.requireNonNull(task, "Pregen task");
Objects.requireNonNull(method, "Pregen method");
Objects.requireNonNull(preparation, "Pregen preparation");
}
}
}
@@ -214,6 +214,7 @@ public class IrisPregenerator {
checkRegions();
int[] regionBounds = task.regionBounds();
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
generator.onPregenStart(task.getCenter().getX(), task.getCenter().getZ());
task.iterateRegions((x, z) -> visitRegion(x, z, true));
task.iterateRegions((x, z) -> visitRegion(x, z, false));
completed = true;
@@ -288,10 +289,17 @@ public class IrisPregenerator {
}
private void shutdown() {
Thread.interrupted();
shutdownStep("saving", listener::onSaving);
shutdownStep("generator", generator::close);
Thread.interrupted();
shutdownStep("ticker", ticker::interrupt);
shutdownStep("listener", listener::onClose);
shutdownStep("mantle", () -> {
Mantle mantle = getMantle();
if (mantle != null) {
reclaimTectonicPlates(mantle);
}
});
shutdownStep("protocol", () -> {
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer != null) {
@@ -299,12 +307,7 @@ public class IrisPregenerator {
protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total);
}
});
shutdownStep("mantle", () -> {
Mantle mantle = getMantle();
if (mantle != null) {
reclaimTectonicPlates(mantle);
}
});
shutdownStep("listener", listener::onClose);
}
private void shutdownStep(String step, Runnable action) {
@@ -18,16 +18,17 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -68,7 +69,7 @@ public final class PregenPerformanceProfile {
public static void apply(Engine engine) {
apply();
if (requiresNoiseCacheRefresh(engine)) {
if (!prepareNoiseCacheInPlace(engine)) {
engine.hotloadComplex();
logApplied();
}
@@ -77,35 +78,44 @@ public final class PregenPerformanceProfile {
public static void applyToGenerator(PlatformChunkGenerator generator) {
apply();
Engine engine = generator == null ? null : generator.getEngine();
if (requiresNoiseCacheRefresh(engine)) {
if (!prepareNoiseCacheInPlace(engine)) {
generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join();
logApplied();
}
}
/** Two workers per core: enough to keep cores busy while a share of the pool sits in waits. */
static int pregenBurstParallelism(int availableProcessors) {
return Math.max(4, availableProcessors * 2);
}
private static boolean requiresNoiseCacheRefresh(Engine engine) {
private static boolean prepareNoiseCacheInPlace(Engine engine) {
if (engine == null) {
return false;
return true;
}
IrisComplex complex = engine.getComplex();
if (complex == null) {
return true;
}
ProceduralStream<Double> heightStream = complex.getHeightStream();
if (!(heightStream instanceof MeteredCache cache)) {
return true;
return false;
}
ProceduralStream<IrisBiome> caveBiomeStream = complex.getCaveBiomeStream();
if (!(caveBiomeStream instanceof CachedStream2D<?> cachedStream) || !cachedStream.usesFastCache()) {
return false;
}
int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize();
if (!resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks)) {
return false;
}
return resizeNoiseCache(complex.getHeightStream(), configuredChunks);
}
private static boolean resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
if (stream instanceof CachedDoubleStream2D cachedStream) {
cachedStream.setMaximumChunks(configuredChunks);
return true;
}
long configuredMaxSize = (long) IrisSettings.get().getPerformance().getNoiseCacheSize() * 256L;
return cache.getMaxSize() != configuredMaxSize;
if (!(stream instanceof MeteredCache cache)) {
return false;
}
return cache.getMaxSize() == (long) configuredChunks * 256L;
}
private static void logApplied() {
@@ -128,6 +128,10 @@ public class PregenTask {
private static int[] computeOrder(long key) {
int pullX = (int) (key >> 32);
int pullZ = (int) key;
if (pullX >= 0 && pullX <= 31 && pullZ >= 0 && pullZ <= 31) {
return packOrder(contiguousOrder(pullX, pullZ));
}
Position2 pull = new Position2(pullX, pullZ);
KList<Position2> p = new KList<>();
new Spiraler(33, 33, (x, z) -> {
@@ -140,7 +144,10 @@ public class PregenTask {
p.add(new Position2(xx, zz));
}).drain();
p.sort(Comparator.comparing((i) -> i.distance(pull)));
KList<Position2> ordered = latticeOrder(p);
return packOrder(latticeOrder(p));
}
private static int[] packOrder(KList<Position2> ordered) {
int[] packed = new int[ordered.size()];
for (int index = 0; index < ordered.size(); index++) {
Position2 position = ordered.get(index);
@@ -150,6 +157,16 @@ public class PregenTask {
return packed;
}
private static KList<Position2> contiguousOrder(int pullX, int pullZ) {
KList<Position2> ordered = new KList<>();
new Spiraler(65, 65, (x, z) -> {
if (x >= 0 && x <= 31 && z >= 0 && z <= 31) {
ordered.add(new Position2(x, z));
}
}).setOffset(pullX, pullZ).drain();
return ordered;
}
/**
* Regroups a region's chunk order into {@link #CHUNK_LATTICE_STRIDE}-spaced lattices, keeping
* the pull order inside each lattice. Chunks generated back to back then sit a stride apart, so
@@ -186,9 +203,11 @@ public class PregenTask {
public void iterateChunks(int rX, int rZ, Spiraled s) {
Bound bound = bounds.chunk();
int pullX = PowerOfTwoCoordinates.blockToChunkFloor(center.getX()) - PowerOfTwoCoordinates.regionToChunk(rX);
int pullZ = PowerOfTwoCoordinates.blockToChunkFloor(center.getZ()) - PowerOfTwoCoordinates.regionToChunk(rZ);
iterateRegion(rX, rZ, ((x, z) -> {
if (bound.check(x, z)) s.on(x, z);
}));
}), pullX, pullZ);
}
public int[] regionBounds() {
@@ -83,6 +83,9 @@ public interface PregeneratorMethod {
default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
}
default void onPregenStart(int centerBlockX, int centerBlockZ) {
}
default void onRegionSubmitted(int regionX, int regionZ) {
}
@@ -87,6 +87,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onPregenStart(int centerBlockX, int centerBlockZ) {
method.onPregenStart(centerBlockX, centerBlockZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
method.onRegionSubmitted(regionX, regionZ);
@@ -88,6 +88,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final boolean urgent;
private final ConcurrentHashMap<Long, AtomicInteger> regionPending;
private final ConcurrentHashMap<Long, Queue<Chunk>> regionChunks;
private final Queue<CompletableFuture<Void>> pendingEvictions;
private final KSet<Long> drainedRegions;
private final KSet<Long> evictedRegions;
private volatile int evictionWindowRegions;
@@ -166,6 +167,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.urgent = false;
this.regionPending = new ConcurrentHashMap<>();
this.regionChunks = new ConcurrentHashMap<>();
this.pendingEvictions = new ConcurrentLinkedQueue<>();
this.drainedRegions = new KSet<>();
this.evictedRegions = new KSet<>();
this.evictionWindowRegions = -1;
@@ -173,7 +175,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.boundsMinRegionZ = Integer.MIN_VALUE;
this.boundsMaxRegionX = Integer.MAX_VALUE;
this.boundsMaxRegionZ = Integer.MAX_VALUE;
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
this.adaptiveInFlightLimit = new AtomicInteger(computeInitialInFlightLimit(
this.threads,
this.effectiveWorkerThreads));
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
int pregenWorldHeight = world.getMaxHeight() - world.getMinHeight();
this.backpressure = new PregenMantleBackpressure(
@@ -280,30 +284,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
boundsMinRegionZ = minRegionZ;
boundsMaxRegionX = maxRegionX;
boundsMaxRegionZ = maxRegionZ;
prefetchHydrology(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
/**
* Plans the hydrology tiles of the whole area up front, nearest the centre first, so the spiral
* finds its tiles ready instead of stalling on each cold plan.
*/
private void prefetchHydrology(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
@Override
public void onPregenStart(int centerBlockX, int centerBlockZ) {
Engine engine = resolveMetricsEngine();
if (engine == null || engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
return;
}
int minimumBlockX = minRegionX << 9;
int minimumBlockZ = minRegionZ << 9;
int maximumBlockX = ((maxRegionX + 1) << 9) - 1;
int maximumBlockZ = ((maxRegionZ + 1) << 9) - 1;
engine.getComplex().getHydrologyRuntime().prefetchArea(
minimumBlockX,
minimumBlockZ,
maximumBlockX,
maximumBlockZ,
(minimumBlockX + maximumBlockX) / 2,
(minimumBlockZ + maximumBlockZ) / 2
);
engine.getComplex().getHydrologyRuntime().preparePregeneration(centerBlockX, centerBlockZ);
}
private boolean inBounds(int rx, int rz) {
@@ -367,47 +356,22 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return true;
}
private void evictRegion(long c) {
private CompletableFuture<Void> evictRegion(long c) {
if (!evictedRegions.add(c)) {
return;
return CompletableFuture.completedFuture(null);
}
regionPending.remove(c);
Queue<Chunk> chunks = regionChunks.remove(c);
if (chunks == null || chunks.isEmpty()) {
return;
return CompletableFuture.completedFuture(null);
}
try {
if (foliaRuntime) {
Chunk anchor = null;
for (Chunk chunk : chunks) {
if (chunk == null) {
continue;
}
if (anchor == null) {
anchor = chunk;
}
int cx = chunk.getX();
int cz = chunk.getZ();
if (!J.runRegion(world, cx, cz, () -> unloadChunkSafely(cx, cz))) {
unloadChunkSafely(cx, cz);
}
}
if (anchor != null) {
int ax = anchor.getX();
int az = anchor.getZ();
if (!J.runRegion(world, ax, az, () -> INMS.get().flushChunkIO(world))) {
INMS.get().flushChunkIO(world);
}
}
return;
}
J.s(() -> {
CompletableFuture<Void> eviction;
if (foliaRuntime) {
eviction = evictFoliaRegion(chunks);
} else {
eviction = J.sfut(() -> {
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
@@ -416,9 +380,49 @@ public class AsyncPregenMethod implements PregeneratorMethod {
INMS.get().flushChunkIO(world);
});
} catch (Throwable e) {
IrisLogging.reportError(e);
}
return trackEviction(eviction);
}
private CompletableFuture<Void> evictFoliaRegion(Queue<Chunk> chunks) {
List<CompletableFuture<Void>> unloads = new ArrayList<>(chunks.size());
Chunk anchor = null;
for (Chunk chunk : chunks) {
if (chunk == null) {
continue;
}
if (anchor == null) {
anchor = chunk;
}
int chunkX = chunk.getX();
int chunkZ = chunk.getZ();
unloads.add(J.runRegionFuture(world, chunkX, chunkZ, () -> unloadChunkSafely(chunkX, chunkZ)));
}
CompletableFuture<Void> unloaded = CompletableFuture.allOf(unloads.toArray(CompletableFuture[]::new));
if (anchor == null) {
return unloaded;
}
int anchorX = anchor.getX();
int anchorZ = anchor.getZ();
return unloaded.thenCompose(ignored -> J.runRegionFuture(
world,
anchorX,
anchorZ,
() -> INMS.get().flushChunkIO(world)));
}
private CompletableFuture<Void> trackEviction(CompletableFuture<Void> eviction) {
pendingEvictions.add(eviction);
eviction.whenComplete((ignored, failure) -> {
pendingEvictions.remove(eviction);
if (failure != null) {
IrisLogging.reportError(failure);
}
});
return eviction;
}
private void unloadChunkSafely(int cx, int cz) {
@@ -438,36 +442,18 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private void flushAllRemainingChunks() {
List<Long> keys = new ArrayList<>(regionChunks.keySet());
if (foliaRuntime) {
for (Long rk : keys) {
evictRegion(rk);
}
return;
List<CompletableFuture<Void>> evictions = new ArrayList<>(keys.size() + pendingEvictions.size());
for (Long regionKey : keys) {
evictions.add(evictRegion(regionKey));
}
evictions.addAll(pendingEvictions);
CompletableFuture<Void> flush = J.sfut(() -> {
for (Long rk : keys) {
if (!evictedRegions.add(rk)) {
continue;
}
regionPending.remove(rk);
Queue<Chunk> chunks = regionChunks.remove(rk);
if (chunks == null) {
continue;
}
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
}
world.save();
INMS.get().flushChunkIO(world);
});
CompletableFuture<Void> evictionsComplete = CompletableFuture.allOf(
evictions.toArray(CompletableFuture[]::new));
CompletableFuture<Void> settledEvictions = evictionsComplete.handle((ignored, failure) -> null);
CompletableFuture<Void> flush = foliaRuntime
? settledEvictions
: settledEvictions.thenCompose(ignored -> J.sfut(() -> INMS.get().flushChunkIO(world)));
try {
flush.get(FLUSH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -585,9 +571,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return;
}
int deficit = threads - current;
int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1;
int next = Math.min(threads, current + step);
int next = nextAdaptiveInFlightLimit(current, threads);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("increase", next);
notifyPermitWaiters();
@@ -628,6 +612,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return strictSerial ? 1 : Math.max(1, recommendedCap);
}
static int computeInitialInFlightLimit(int concurrencyCap, int workerThreads) {
int initial = Math.max(4, Math.max(1, workerThreads));
return Math.min(Math.max(1, concurrencyCap), initial);
}
static int nextAdaptiveInFlightLimit(int current, int concurrencyCap) {
return Math.min(Math.max(1, concurrencyCap), Math.max(1, current) + 1);
}
static int resolvePaperLikeConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) {
int provisionedWorkerThreads = Math.max(1, configuredWorldGenThreads);
if (detectedWorkerPoolThreads > 0) {
@@ -175,6 +175,11 @@ public class CachedPregenMethod implements PregeneratorMethod {
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onPregenStart(int centerBlockX, int centerBlockZ) {
method.onPregenStart(centerBlockX, centerBlockZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
method.onRegionSubmitted(regionX, regionZ);
@@ -80,6 +80,11 @@ public class HybridPregenMethod implements PregeneratorMethod {
inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onPregenStart(int centerBlockX, int centerBlockZ) {
inWorld.onPregenStart(centerBlockX, centerBlockZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
inWorld.onRegionSubmitted(regionX, regionZ);
@@ -19,6 +19,7 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
@@ -123,7 +124,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
futures.clear();
}
private void unloadAndSaveAllChunks(boolean saveWorld) {
private void unloadAndSaveAllChunks(boolean flushChunkIo) {
if (J.isFolia()) {
lastUse.clear();
return;
@@ -138,12 +139,12 @@ public class MedievalPregenMethod implements PregeneratorMethod {
for (Chunk i : new ArrayList<>(lastUse.keySet())) {
Long lastUseTime = lastUse.get(i);
if (lastUseTime != null && M.ms() - lastUseTime >= 10) {
i.unload();
i.unload(true);
lastUse.remove(i);
}
}
if (saveWorld) {
world.save();
if (flushChunkIo) {
INMS.get().flushChunkIO(world);
}
});
@@ -311,13 +311,19 @@ public class IrisToolbelt {
if (PregeneratorJob.getInstance() != null) {
throw new IllegalStateException("An Iris pregeneration job is already running; stop it first with /iris pregen stop.");
}
applyPregenPerformanceProfile(engine);
boolean useCachedWrapper = false;
if (cached && engine != null) {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
}
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task) : method, engine);
PregeneratorMethod activeMethod = useCachedWrapper
? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task)
: method;
return new PregeneratorJob(new PregeneratorJob.Configuration(
task,
activeMethod,
engine,
() -> applyPregenPerformanceProfile(engine)));
}
private static PregenCache resolvePregenCache(IrisWorld world) {
@@ -78,6 +78,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.nio.file.Path;
import java.util.function.BiFunction;
@Data
@@ -87,6 +88,7 @@ public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
private static final int HEIGHT_BOUNDS_GRID = 4;
@@ -170,7 +172,7 @@ public class IrisComplex implements DataProvider {
}
public IrisComplex(Engine engine, boolean simple) {
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
this.rng = new RNG(engine.getSeedManager().getComplex());
@@ -320,6 +322,7 @@ public class IrisComplex implements DataProvider {
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
() -> engine.getPlatformHooks().isMainThread()
));
hydrologyRuntime.setNeighbourPrefetchEnabled(!engine.isStudio());
}
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
.cache2DDouble("heightStream", engine, cacheSize);
@@ -373,6 +376,16 @@ public class IrisComplex implements DataProvider {
//@done
}
static int noiseCacheSize(Engine engine, int configuredSize) {
return engine.isStudio() ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
}
void enableStudioHydrologyCache(String runtimeIdentity, Path persistentRoot) {
if (hydrologyRuntime != null) {
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
}
}
static InferredType resolveNaturalInferredType(
ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome,
@@ -958,7 +971,7 @@ public class IrisComplex implements DataProvider {
// catches the same corner reached by another thread or by hydrology planning earlier.
SharedCornerBounds shared = sharedCornerBounds();
long sharedKey = SharedCornerBounds.key(gx, gz, interpolatorIndex);
long packed = shared.contains(sharedKey) ? shared.get(sharedKey) : Long.MIN_VALUE;
long packed = shared.get(sharedKey);
if (packed == Long.MIN_VALUE) {
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
@@ -40,6 +40,7 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEngineData;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.spi.IrisLogging;
@@ -70,6 +71,7 @@ import lombok.Getter;
import lombok.Setter;
import java.io.File;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
@@ -224,6 +226,7 @@ public class IrisEngine implements Engine {
getData().registerEngine(this);
_t0 = M.ms();
long phaseStartedAt = System.nanoTime();
String studioCacheIdentity = currentStudioCacheIdentity();
getData().loadPrefetch(this);
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
@@ -236,6 +239,7 @@ public class IrisEngine implements Engine {
_t0 = M.ms();
phaseStartedAt = System.nanoTime();
EngineRuntime initialRuntime = runtimeBuilder.buildRuntime();
enableStableStudioCache(initialRuntime, studioCacheIdentity);
runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
logPackCompatSummary();
@@ -318,6 +322,47 @@ public class IrisEngine implements Engine {
Boolean.toString(skipped));
}
private String currentStudioCacheIdentity() {
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
return "";
}
try {
String contentFingerprint = PackValidationCache.contentFingerprint(
IrisPlatforms.get().packsFolderNoCreate());
String contextFingerprint = PackValidationCache.contextFingerprint();
if (contentFingerprint.isBlank() || contextFingerprint.isBlank()) {
return "";
}
return contentFingerprint + contextFingerprint;
} catch (RuntimeException failure) {
IrisLogging.warn("Studio runtime cache fingerprint failed: " + failure.getMessage());
return "";
}
}
private void enableStableStudioCache(EngineRuntime runtime, String initialIdentity) {
if (initialIdentity.isBlank()) {
return;
}
String finalIdentity = currentStudioCacheIdentity();
if (!initialIdentity.equals(finalIdentity)) {
IrisLogging.warn("Studio packs changed during runtime compilation; shared generation caches are disabled.");
return;
}
runtime.complex().enableStudioHydrologyCache(initialIdentity, studioHydrologyCacheRoot());
IrisLogging.debug("Enabled shared Studio hydrology cache identity="
+ initialIdentity.substring(0, Math.min(12, initialIdentity.length())));
}
private Path studioHydrologyCacheRoot() {
Path packsRoot = IrisPlatforms.get().packsFolderNoCreate().toPath().toAbsolutePath().normalize();
Path packRoot = packsRoot.resolve(getDimension().getLoadKey()).normalize();
if (!packRoot.startsWith(packsRoot)) {
throw new IllegalStateException("Studio dimension key resolves outside the Iris packs folder.");
}
return packRoot.resolve(".iris").resolve("studio-hydrology");
}
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
@@ -19,8 +19,8 @@ import java.util.Objects;
final class HydrologyFootprintCompiler {
private static final int FEATURE_ROLE_COUNT = 7;
private static final int COURSE_FOOTPRINT_CACHE_SIZE = 8;
private static final int VALIDATION_RASTER_CACHE_SIZE = 8;
private static final int COURSE_FOOTPRINT_CACHE_SIZE = 32;
private static final int VALIDATION_RASTER_CACHE_SIZE = 32;
private static final int[][] HORIZONTAL_NEIGHBORS = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
@@ -2,4 +2,8 @@ package art.arcane.iris.engine.hydrology;
public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainSampler {
HydrologyTerrainSample sampleBasis(int blockX, int blockZ);
default HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
return sampleBasis(blockX, blockZ);
}
}
@@ -320,9 +320,33 @@ public final class HydrologyPlanner {
resolvedOwners.invalidateAll();
}
void reuseResolvedTile(HydrologyTile tile) {
Objects.requireNonNull(tile, "tile");
if (tile.worldSeed() != worldSeed
|| tile.settingsFingerprint() != settings.fingerprint()
|| tile.tileSize() != settings.routing().tileSize()) {
throw new IllegalArgumentException("Hydrology tile does not match this planner.");
}
HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter.Result(
tile.nodes(),
tile.edges(),
tile.outlets(),
tile.courses(),
tile.cavePlans()
);
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(
tile.key(),
result,
tile.diagnosticCandidates(),
null
);
resolvedOwners.put(tile.key(), new CrossTileResolvedOwner(draft, List.of()));
}
private HydrologyOwnerDraft compileOwnerDraft(
HydrologyTileKey key,
CrossTileDraftAdmission crossTileAdmission
CrossTileDraftAdmission crossTileAdmission,
boolean retainFootprintCompiler
) {
PlanningSamples previous = planningSamples.get();
if (previous == null) {
@@ -333,7 +357,11 @@ public final class HydrologyPlanner {
draftProfiles.set(profile);
long started = System.nanoTime();
try {
HydrologyOwnerDraft draft = compileScopedOwnerDraft(key, crossTileAdmission);
HydrologyOwnerDraft draft = compileScopedOwnerDraft(
key,
crossTileAdmission,
retainFootprintCompiler
);
profile.log(key, ownerColorRank(key), System.nanoTime() - started);
return draft;
} finally {
@@ -350,7 +378,8 @@ public final class HydrologyPlanner {
private HydrologyOwnerDraft compileScopedOwnerDraft(
HydrologyTileKey key,
CrossTileDraftAdmission crossTileAdmission
CrossTileDraftAdmission crossTileAdmission,
boolean retainFootprintCompiler
) {
Objects.requireNonNull(key, "key");
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
@@ -560,7 +589,12 @@ public final class HydrologyPlanner {
}
HydrologyCaveCourseFilter.Result result = publication.result();
List<HydrologyDiagnosticCandidate> uniqueDiagnostics = uniqueDiagnostics(diagnostics);
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(key, result, uniqueDiagnostics);
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(
key,
result,
uniqueDiagnostics,
retainFootprintCompiler ? footprintCompiler : null
);
validateOwnerDraftReach(draft);
return draft;
}
@@ -576,14 +610,17 @@ public final class HydrologyPlanner {
rejection.winnerSourceId()
));
}
HydrologyFootprintCompiler footprintCompiler = new HydrologyFootprintCompiler(
settings,
new HydrologyFootprintCompiler.Sampling(
sampler,
geometrySampler,
naturalSampler
)
);
HydrologyFootprintCompiler footprintCompiler = resolution.draft().footprintCompiler();
if (footprintCompiler == null) {
footprintCompiler = new HydrologyFootprintCompiler(
settings,
new HydrologyFootprintCompiler.Sampling(
sampler,
geometrySampler,
naturalSampler
)
);
}
MaterializedHydrology materialized = materializeFinalHydrology(
result,
diagnostics,
@@ -743,11 +780,14 @@ public final class HydrologyPlanner {
try {
int ownerRank = ownerColorRank(key);
ColorRankedDraftAdmission admission = new ColorRankedDraftAdmission(key, ownerRank, context);
HydrologyOwnerDraft draft = compileOwnerDraft(key, admission);
validateOwnerDraftReach(draft);
HydrologyOwnerDraft draft = compileOwnerDraft(
key,
admission,
key.equals(context.root())
);
CrossTileResolvedOwner resolved = new CrossTileResolvedOwner(draft, admission.observedRejections());
context.remember(key, resolved);
resolvedOwners.put(key, resolved);
resolvedOwners.put(key, resolved.withoutFootprintCompiler());
owned.complete(resolved);
return resolved;
} catch (Throwable failure) {
@@ -3955,7 +3995,7 @@ public final class HydrologyPlanner {
int lowest = center.naturalHeight();
for (double distance = reach / 2D; distance <= reach; distance += reach / 2D) {
for (double direction = -1D; direction <= 1D; direction += 2D) {
HydrologyTerrainSample side = sampleLandBasis(
HydrologyTerrainSample side = sampleLandBasisWithoutSlope(
(int) StrictMath.round(point.x() + normalX * distance * direction),
(int) StrictMath.round(point.z() + normalZ * distance * direction));
if (side != null) {
@@ -4597,8 +4637,8 @@ public final class HydrologyPlanner {
private boolean traversableTerrainTransition(HydrologyPoint start, HydrologyPoint end) {
List<HydrologyPoint> crossing = rasterLine(start, end);
HydrologyTerrainSample startTerrain = sampleLandBasis(start.x(), start.z());
HydrologyTerrainSample endTerrain = sampleLandBasis(end.x(), end.z());
HydrologyTerrainSample startTerrain = sampleLandBasisWithoutSlope(start.x(), start.z());
HydrologyTerrainSample endTerrain = sampleLandBasisWithoutSlope(end.x(), end.z());
if (startTerrain == null || endTerrain == null) {
return false;
}
@@ -4606,7 +4646,7 @@ public final class HydrologyPlanner {
int threshold = settings.hydraulics().waterfallMinimumDrop();
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
HydrologyPoint point = crossing.get(pointIndex);
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null
|| !terrain.transitAllowed()
|| boundaryHeight - terrain.naturalHeight() >= threshold) {
@@ -4679,7 +4719,7 @@ public final class HydrologyPlanner {
List<HydrologyPoint> crossing = rasterLine(start, end);
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
HydrologyPoint point = crossing.get(pointIndex);
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || terrain.ocean() || !terrain.transitAllowed()) {
return false;
}
@@ -6531,7 +6571,7 @@ public final class HydrologyPlanner {
HydrologyPoint bankPoint = surfaceBankPoint(point, tangent, signedDistance);
int bankX = bankPoint.x();
int bankZ = bankPoint.z();
return sampleLandBasis(bankX, bankZ);
return sampleLandBasisWithoutSlope(bankX, bankZ);
}
private HydrologyPoint surfaceBankPoint(HydrologyPoint point, Direction tangent, int signedDistance) {
@@ -7721,6 +7761,33 @@ public final class HydrologyPlanner {
return terrain == null || terrain.ocean() ? null : terrain;
}
private HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
if (naturalSampler == null) {
return sampleBasis(blockX, blockZ);
}
PlanningSamples samples = planningSamples.get();
if (samples == null) {
return naturalSampler.sampleBasisWithoutSlope(blockX, blockZ);
}
long packed = RiverFootprint.pack(blockX, blockZ);
HydrologyTerrainSample fullSample = samples.basis.get(packed);
if (fullSample != null || samples.basis.containsKey(packed)) {
return fullSample;
}
HydrologyTerrainSample cached = samples.basisWithoutSlope.get(packed);
if (cached != null || samples.basisWithoutSlope.containsKey(packed)) {
return cached;
}
HydrologyTerrainSample sampled = naturalSampler.sampleBasisWithoutSlope(blockX, blockZ);
samples.basisWithoutSlope.put(packed, sampled);
return sampled;
}
private HydrologyTerrainSample sampleLandBasisWithoutSlope(int blockX, int blockZ) {
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(blockX, blockZ);
return terrain == null || terrain.ocean() ? null : terrain;
}
private HydrologyTerrainSample sampleDetailed(int blockX, int blockZ) {
PlanningSamples samples = planningSamples.get();
if (samples == null) {
@@ -7790,6 +7857,7 @@ public final class HydrologyPlanner {
private static final class PlanningSamples {
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basis;
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basisWithoutSlope;
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> detailed;
private final Long2ObjectOpenHashMap<HydrologyRoutingTerrainSampler.NaturalClassification> classifications;
private final HashMap<HydrologyGeometrySampler.Request, Integer> geometry;
@@ -7802,6 +7870,7 @@ public final class HydrologyPlanner {
private PlanningSamples() {
this.basis = new Long2ObjectOpenHashMap<>();
this.basisWithoutSlope = new Long2ObjectOpenHashMap<>();
this.detailed = new Long2ObjectOpenHashMap<>();
this.classifications = new Long2ObjectOpenHashMap<>();
this.geometry = new HashMap<>();
@@ -8295,8 +8364,15 @@ public final class HydrologyPlanner {
private record HydrologyOwnerDraft(
HydrologyTileKey key,
HydrologyCaveCourseFilter.Result result,
List<HydrologyDiagnosticCandidate> diagnostics
List<HydrologyDiagnosticCandidate> diagnostics,
HydrologyFootprintCompiler footprintCompiler
) {
private HydrologyOwnerDraft withoutFootprintCompiler() {
if (footprintCompiler == null) {
return this;
}
return new HydrologyOwnerDraft(key, result, diagnostics, null);
}
}
private record MaterializedHydrology(
@@ -8528,7 +8604,8 @@ public final class HydrologyPlanner {
throw new IllegalStateException("Cross-tile owner dependency exceeded its color-ranked geometry bound.");
}
CrossTileResolvedOwner existing = resolved.putIfAbsent(key, owner);
if (existing != null && !existing.equals(owner)) {
if (existing != null
&& !existing.withoutFootprintCompiler().equals(owner.withoutFootprintCompiler())) {
throw new IllegalStateException("Cross-tile owner resolution produced inconsistent cached results.");
}
if (resolved.size() > maximumOwners) {
@@ -8555,6 +8632,10 @@ public final class HydrologyPlanner {
private int ownerCount() {
return resolved.size();
}
private HydrologyTileKey root() {
return root;
}
}
private record CrossTileResolvedOwner(
@@ -8565,6 +8646,13 @@ public final class HydrologyPlanner {
Objects.requireNonNull(draft, "draft");
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
}
private CrossTileResolvedOwner withoutFootprintCompiler() {
HydrologyOwnerDraft uncachedDraft = draft.withoutFootprintCompiler();
return uncachedDraft == draft
? this
: new CrossTileResolvedOwner(uncachedDraft, observedRejections);
}
}
private record CrossTileRejectionKey(long courseId, long winnerCourseId) {
@@ -1,49 +1,70 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
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.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier;
public final class HydrologyTileCache implements AutoCloseable {
private static final int DEFAULT_MAXIMUM_ENTRIES = 64;
private static final int MAXIMUM_SHARED_ENTRIES = 8;
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 Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
.maximumSize(MAXIMUM_SHARED_ENTRIES)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
private final HydrologyPlanner planner;
private final int maximumEntries;
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
private final Cache<Long, ChunkColumns> composedChunks;
private final AtomicLong cacheEpoch;
private final AtomicBoolean closed;
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
private final Executor prefetchExecutor;
private final ConcurrentLinkedQueue<HydrologyTileKey> prefetchQueue;
private final Set<HydrologyTileKey> queuedPrefetches;
private final AtomicBoolean prefetchActive;
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<HydrologyTile>> planning;
private final BooleanSupplier waitingForbidden;
private volatile SharedCacheScope sharedCacheScope;
private volatile StudioHydrologyTileStore persistentStore;
private volatile boolean neighbourPrefetchEnabled;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES);
this(planner, DEFAULT_MAXIMUM_ENTRIES, null, null, null);
}
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries) {
this(planner, maximumEntries, null);
this(planner, maximumEntries, null, null, null);
}
public HydrologyTileCache(HydrologyPlanner planner, int maximumEntries, Executor prefetchExecutor) {
this(planner, maximumEntries, prefetchExecutor, null);
this(planner, maximumEntries, prefetchExecutor, null, null);
}
/**
@@ -58,15 +79,30 @@ public final class HydrologyTileCache implements AutoCloseable {
int maximumEntries,
Executor prefetchExecutor,
BooleanSupplier waitingForbidden
) {
this(planner, maximumEntries, prefetchExecutor, waitingForbidden, null);
}
public HydrologyTileCache(
HydrologyPlanner planner,
int maximumEntries,
Executor prefetchExecutor,
BooleanSupplier waitingForbidden,
SharedCacheScope sharedCacheScope
) {
this.planner = Objects.requireNonNull(planner, "planner");
this.prefetchExecutor = prefetchExecutor;
this.waitingForbidden = waitingForbidden;
this.sharedCacheScope = sharedCacheScope;
this.persistentStore = null;
this.planning = new ConcurrentHashMap<>();
if (maximumEntries < 1) {
throw new IllegalArgumentException("maximumEntries must be positive.");
}
this.maximumEntries = maximumEntries;
this.prefetchQueue = new ConcurrentLinkedQueue<>();
this.queuedPrefetches = ConcurrentHashMap.newKeySet();
this.prefetchActive = new AtomicBoolean();
this.tiles = Caffeine.newBuilder()
.maximumSize(maximumEntries)
.build();
@@ -74,7 +110,9 @@ public final class HydrologyTileCache implements AutoCloseable {
.maximumSize(MAXIMUM_COMPOSED_CHUNKS)
.build();
this.cacheEpoch = new AtomicLong();
this.closed = new AtomicBoolean();
this.localChunkColumns = new ThreadLocal<>();
this.neighbourPrefetchEnabled = true;
}
/**
@@ -87,17 +125,16 @@ public final class HydrologyTileCache implements AutoCloseable {
if (prefetchExecutor == null || plansInline()) {
return planInline(key);
}
return awaitTile(planAsync(key));
return awaitTile(planDemandAsync(key));
}
/**
* A pool worker plans on its own thread. Parking the workers of one pool on tasks of another is how
* two saturated pools deadlock each other (generation workers waiting for planning that waits for
* generation workers), and a ForkJoin worker keeps the planner's nested fan-out alive by helping
* with its own forks instead of waiting for them.
*/
private static boolean plansInline() {
return Thread.currentThread() instanceof ForkJoinWorkerThread;
private boolean plansInline() {
if (prefetchExecutor instanceof MultiBurst burst) {
return burst.ownsCurrentThread();
}
return prefetchExecutor instanceof ForkJoinPool pool
&& Thread.currentThread() instanceof ForkJoinWorkerThread worker
&& worker.getPool() == pool;
}
private HydrologyTile planInline(HydrologyTileKey key) {
@@ -116,8 +153,45 @@ public final class HydrologyTileCache implements AutoCloseable {
* cached so the next request plans it again.
*/
private HydrologyTile planOrEmpty(HydrologyTileKey key) {
long planningEpoch = cacheEpoch.get();
SharedTileKey sharedKey = sharedCacheScope == null ? null : new SharedTileKey(sharedCacheScope, key);
HydrologyTile shared = sharedKey == null ? null : SHARED_TILES.getIfPresent(sharedKey);
if (shared != null && validSharedTile(shared, sharedKey)) {
planner.reuseResolvedTile(shared);
IrisLogging.debug("Reused shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
return shared;
}
if (shared != null) {
SHARED_TILES.invalidate(sharedKey);
}
StudioHydrologyTileStore store = persistentStore;
if (store != null && persistentStudioKey(key)) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
SHARED_TILES.put(sharedKey, persisted);
planner.reuseResolvedTile(persisted);
IrisLogging.debug("Loaded persisted Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
return persisted;
}
}
try {
return planner.plan(key);
HydrologyTile planned = planner.plan(key);
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
if (existing == null) {
IrisLogging.debug("Published shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
}
}
if (store != null && persistentStudioKey(key) && !closed.get() && planningEpoch == cacheEpoch.get()) {
try {
store.save(planned);
IrisLogging.debug("Persisted Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
} catch (IOException failure) {
IrisLogging.reportError("Failed to persist Studio hydrology tile "
+ key.tileX() + "," + key.tileZ() + ".", failure);
}
}
return planned;
} catch (RuntimeException failure) {
if (interrupted(failure)) {
throw failure;
@@ -205,6 +279,10 @@ public final class HydrologyTileCache implements AutoCloseable {
public void clear() {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
synchronized (prefetchQueue) {
prefetchQueue.clear();
queuedPrefetches.clear();
}
tiles.invalidateAll();
composedChunks.invalidateAll();
localChunkColumns.remove();
@@ -219,11 +297,56 @@ public final class HydrologyTileCache implements AutoCloseable {
return maximumEntries;
}
public void setNeighbourPrefetchEnabled(boolean enabled) {
neighbourPrefetchEnabled = enabled;
}
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
neighbourPrefetchEnabled = true;
int tileSize = planner.settings().routing().tileSize();
prefetchArea(
Math.subtractExact(centerBlockX, tileSize),
Math.subtractExact(centerBlockZ, tileSize),
Math.addExact(centerBlockX, tileSize),
Math.addExact(centerBlockZ, tileSize),
centerBlockX,
centerBlockZ
);
}
public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) {
if (closed.get()) {
throw new IllegalStateException("Hydrology tile cache is closed.");
}
sharedCacheScope = Objects.requireNonNull(scope, "scope");
persistentStore = persistentRoot == null
? null
: new StudioHydrologyTileStore(persistentRoot, scope, planner.settings().routing().tileSize());
}
private boolean persistentStudioKey(HydrologyTileKey key) {
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTile = Math.subtractExact(tileCoordinate(-(long) publicationRadius, tileSize), 1);
int maximumTile = Math.addExact(tileCoordinate(CHUNK_SIZE - 1L + publicationRadius, tileSize), 1);
return key.tileX() >= minimumTile && key.tileX() <= maximumTile
&& key.tileZ() >= minimumTile && key.tileZ() <= maximumTile;
}
@Override
public void close() {
closed.set(true);
clear();
}
private boolean validSharedTile(HydrologyTile tile, SharedTileKey sharedKey) {
SharedCacheScope scope = sharedKey.scope();
return tile.key().equals(sharedKey.tileKey())
&& tile.worldSeed() == scope.worldSeed()
&& tile.settingsFingerprint() == scope.settingsFingerprint()
&& tile.tileSize() == planner.settings().routing().tileSize();
}
private ChunkColumns chunkColumns(int blockX, int blockZ) {
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
@@ -306,7 +429,7 @@ public final class HydrologyTileCache implements AutoCloseable {
* needs before its prefetch finishes simply joins that computation.
*/
private void prefetchNeighbours(int minimumTileX, int maximumTileX, int minimumTileZ, int maximumTileZ) {
if (prefetchExecutor == null) {
if (prefetchExecutor == null || !neighbourPrefetchEnabled) {
return;
}
for (int tileZ = minimumTileZ - 1; tileZ <= maximumTileZ + 1; tileZ++) {
@@ -319,11 +442,7 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
/**
* Plans every tile that touches the block area, nearest to the centre first, on the prefetch
* executor. A pregeneration calls this once up front so its spiral never waits on a cold plan
* once the first tiles are in; nothing happens without a prefetch executor.
*/
/** Plans tiles that touch the block area in nearest-first order without flooding the planner. */
public void prefetchArea(int minimumBlockX, int minimumBlockZ, int maximumBlockX, int maximumBlockZ, int centreBlockX, int centreBlockZ) {
if (prefetchExecutor == null) {
return;
@@ -342,16 +461,70 @@ public final class HydrologyTileCache implements AutoCloseable {
keys.add(new HydrologyTileKey(tileX, tileZ));
}
}
keys.sort(java.util.Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX() - centreTileX), Math.abs(key.tileZ() - centreTileZ)))
keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX() - centreTileX), Math.abs(key.tileZ() - centreTileZ)))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
for (HydrologyTileKey key : keys) {
prefetch(key);
if (!prefetch(key) && speculativeEntryCount() >= maximumEntries) {
break;
}
}
}
private void prefetch(HydrologyTileKey key) {
planAsync(key);
private boolean prefetch(HydrologyTileKey key) {
synchronized (prefetchQueue) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(key)
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
return false;
}
queuedPrefetches.add(key);
prefetchQueue.add(key);
}
pumpPrefetch();
return true;
}
private int speculativeEntryCount() {
tiles.cleanUp();
return Math.toIntExact(Math.min(
maximumEntries,
tiles.estimatedSize() + planning.size() + queuedPrefetches.size()
));
}
private void pumpPrefetch() {
if (prefetchExecutor == null || closed.get() || !prefetchActive.compareAndSet(false, true)) {
return;
}
scheduleNextPrefetch();
}
private void scheduleNextPrefetch() {
HydrologyTileKey key = pollPrefetch();
if (key == null || closed.get()) {
prefetchActive.set(false);
if (!prefetchQueue.isEmpty()) {
pumpPrefetch();
}
return;
}
planAsync(key).whenComplete((tile, failure) -> scheduleNextPrefetch());
}
private HydrologyTileKey pollPrefetch() {
HydrologyTileKey key;
while ((key = prefetchQueue.poll()) != null) {
if (!queuedPrefetches.remove(key)) {
continue;
}
if (tiles.getIfPresent(key) == null) {
return key;
}
}
return null;
}
/**
@@ -375,7 +548,7 @@ public final class HydrologyTileCache implements AutoCloseable {
int end = Math.min(loaded.length, start + batch);
futures.clear();
for (int index = start; index < end; index++) {
futures.add(planAsync(keys.get(index)));
futures.add(planDemandAsync(keys.get(index)));
}
for (int index = start; index < end; index++) {
loaded[index] = awaitTile(futures.get(index - start));
@@ -416,6 +589,11 @@ public final class HydrologyTileCache implements AutoCloseable {
return future;
}
private CompletableFuture<HydrologyTile> planDemandAsync(HydrologyTileKey key) {
queuedPrefetches.remove(key);
return planAsync(key);
}
private static HydrologyTile awaitTile(CompletableFuture<HydrologyTile> future) {
try {
return future.join();
@@ -518,4 +696,27 @@ public final class HydrologyTileCache implements AutoCloseable {
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
}
public record SharedCacheScope(
String runtimeIdentity,
long worldSeed,
int worldHeight,
String dimensionKey,
long settingsFingerprint
) {
public SharedCacheScope {
if (runtimeIdentity == null || runtimeIdentity.isBlank()) {
throw new IllegalArgumentException("Hydrology shared cache runtime identity is required.");
}
if (worldHeight < 3) {
throw new IllegalArgumentException("Hydrology shared cache world height must be at least three.");
}
if (dimensionKey == null || dimensionKey.isBlank()) {
throw new IllegalArgumentException("Hydrology shared cache dimension key is required.");
}
}
}
private record SharedTileKey(SharedCacheScope scope, HydrologyTileKey tileKey) {
}
}
@@ -0,0 +1,375 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveRejection;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveSource;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
final class StudioHydrologyTileStore {
private static final int SCHEMA_VERSION = 1;
private static final long MAXIMUM_COMPRESSED_BYTES = 128L * 1024L * 1024L;
private static final long MAXIMUM_DECOMPRESSED_BYTES = 512L * 1024L * 1024L;
private static final Gson GSON = new GsonBuilder()
.disableHtmlEscaping()
.serializeSpecialFloatingPointValues()
.create();
private final Path directory;
private final HydrologyTileCache.SharedCacheScope scope;
private final int expectedTileSize;
StudioHydrologyTileStore(Path root, HydrologyTileCache.SharedCacheScope scope, int expectedTileSize) {
this.scope = Objects.requireNonNull(scope, "scope");
if (expectedTileSize < 1) {
throw new IllegalArgumentException("Hydrology tile size must be positive.");
}
this.expectedTileSize = expectedTileSize;
this.directory = Objects.requireNonNull(root, "root")
.toAbsolutePath()
.normalize()
.resolve(scopeFingerprint(scope));
}
Optional<HydrologyTile> load(HydrologyTileKey key) {
Path file = file(key);
try {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(file)
|| Files.size(file) > MAXIMUM_COMPRESSED_BYTES) {
return Optional.empty();
}
} catch (IOException failure) {
return Optional.empty();
}
try (InputStream raw = Files.newInputStream(file);
InputStream buffered = new BufferedInputStream(raw);
InputStream compressed = new GZIPInputStream(buffered);
InputStream bounded = new LimitedInputStream(compressed, MAXIMUM_DECOMPRESSED_BYTES);
Reader reader = new InputStreamReader(bounded, StandardCharsets.UTF_8)) {
PersistedTile persisted = GSON.fromJson(reader, PersistedTile.class);
if (persisted == null
|| persisted.schemaVersion() != SCHEMA_VERSION
|| !key.equals(persisted.key())
|| persisted.worldSeed() != scope.worldSeed()
|| persisted.settingsFingerprint() != scope.settingsFingerprint()) {
return Optional.empty();
}
HydrologyTile tile = persisted.toTile();
return valid(tile, key) ? Optional.of(tile) : Optional.empty();
} catch (IOException | RuntimeException failure) {
return Optional.empty();
}
}
void save(HydrologyTile tile) throws IOException {
if (!valid(tile, tile.key())) {
throw new IOException("Refused to persist a hydrology tile outside the active Studio cache scope.");
}
Files.createDirectories(directory);
Path target = file(tile.key());
Path staged = Files.createTempFile(directory, ".hydrology-tile-", ".tmp");
try {
try (OutputStream raw = Files.newOutputStream(staged);
OutputStream buffered = new BufferedOutputStream(raw);
OutputStream compressed = new GZIPOutputStream(buffered);
Writer writer = new OutputStreamWriter(compressed, StandardCharsets.UTF_8)) {
GSON.toJson(PersistedTile.from(tile), writer);
}
if (Files.size(staged) > MAXIMUM_COMPRESSED_BYTES) {
throw new IOException("Studio hydrology tile cache entry exceeds the size limit.");
}
try {
Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(staged, target, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(staged);
}
}
Path file(HydrologyTileKey key) {
return directory.resolve("tile-" + key.tileX() + "-" + key.tileZ() + ".json.gz");
}
private boolean valid(HydrologyTile tile, HydrologyTileKey key) {
return tile != null
&& key.equals(tile.key())
&& tile.worldSeed() == scope.worldSeed()
&& tile.settingsFingerprint() == scope.settingsFingerprint()
&& tile.tileSize() == expectedTileSize;
}
private static String scopeFingerprint(HydrologyTileCache.SharedCacheScope scope) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
update(digest, scope.runtimeIdentity());
update(digest, Long.toString(scope.worldSeed()));
update(digest, Integer.toString(scope.worldHeight()));
update(digest, scope.dimensionKey());
update(digest, Long.toString(scope.settingsFingerprint()));
return HexFormat.of().formatHex(digest.digest());
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable.", exception);
}
}
private static void update(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
digest.update((byte) (bytes.length >>> 24));
digest.update((byte) (bytes.length >>> 16));
digest.update((byte) (bytes.length >>> 8));
digest.update((byte) bytes.length);
digest.update(bytes);
}
private record PersistedTile(
int schemaVersion,
HydrologyTileKey key,
long worldSeed,
long settingsFingerprint,
int tileSize,
List<DrainageNode> nodes,
List<DrainageEdge> edges,
List<RiverOutlet> outlets,
List<PersistedCourse> courses,
List<PersistedCavePlan> cavePlans,
List<HydrologyDiagnosticCandidate> diagnosticCandidates,
List<HydrologyColumnSample> columns
) {
private static PersistedTile from(HydrologyTile tile) {
ArrayList<PersistedCourse> courses = new ArrayList<>(tile.courses().size());
for (RiverCourse course : tile.courses()) {
courses.add(PersistedCourse.from(course));
}
ArrayList<PersistedCavePlan> cavePlans = new ArrayList<>(tile.cavePlans().size());
for (HydrologyCavePlan cavePlan : tile.cavePlans()) {
cavePlans.add(PersistedCavePlan.from(cavePlan));
}
return new PersistedTile(
SCHEMA_VERSION,
tile.key(),
tile.worldSeed(),
tile.settingsFingerprint(),
tile.tileSize(),
tile.nodes(),
tile.edges(),
tile.outlets(),
courses,
cavePlans,
tile.diagnosticCandidates(),
new ArrayList<>(tile.footprint().columns().values())
);
}
private HydrologyTile toTile() {
requireCollection(nodes, "nodes");
requireCollection(edges, "edges");
requireCollection(outlets, "outlets");
requireCollection(courses, "courses");
requireCollection(cavePlans, "cavePlans");
requireCollection(diagnosticCandidates, "diagnosticCandidates");
requireCollection(columns, "columns");
ArrayList<RiverCourse> restoredCourses = new ArrayList<>(courses.size());
for (PersistedCourse course : courses) {
restoredCourses.add(Objects.requireNonNull(course, "course").toCourse());
}
ArrayList<HydrologyCavePlan> restoredCavePlans = new ArrayList<>(cavePlans.size());
for (PersistedCavePlan cavePlan : cavePlans) {
restoredCavePlans.add(Objects.requireNonNull(cavePlan, "cavePlan").toPlan());
}
HashMap<Long, HydrologyColumnSample> restoredColumns = HashMap.newHashMap(columns.size());
for (HydrologyColumnSample column : columns) {
HydrologyColumnSample required = Objects.requireNonNull(column, "column");
HydrologyColumnSample existing = restoredColumns.put(
RiverFootprint.pack(required.x(), required.z()), required);
if (existing != null) {
throw new IllegalArgumentException("Duplicate hydrology footprint column.");
}
}
return new HydrologyTile(
key,
worldSeed,
settingsFingerprint,
tileSize,
nodes,
edges,
outlets,
restoredCourses,
restoredCavePlans,
diagnosticCandidates,
new RiverFootprint(restoredColumns)
);
}
private static void requireCollection(List<?> values, String name) {
Objects.requireNonNull(values, name);
}
}
private record PersistedCourse(
long id,
RiverCourseType type,
Long sourceNodeId,
Long outletId,
String profileKey,
int discharge,
List<DrainageEdge> drainageEdges,
List<HydraulicSegment> segments
) {
private static PersistedCourse from(RiverCourse course) {
return new PersistedCourse(
course.id(),
course.type(),
course.sourceNodeId().isPresent() ? course.sourceNodeId().getAsLong() : null,
course.outletId().isPresent() ? course.outletId().getAsLong() : null,
course.profileKey(),
course.discharge(),
course.drainageEdges(),
course.segments()
);
}
private RiverCourse toCourse() {
return new RiverCourse(
id,
type,
sourceNodeId == null ? OptionalLong.empty() : OptionalLong.of(sourceNodeId),
outletId == null ? OptionalLong.empty() : OptionalLong.of(outletId),
profileKey,
discharge,
drainageEdges,
segments
);
}
}
private record PersistedCavePlan(
HydrologyCaveSource source,
HydrologyCaveRejection rejection,
List<PersistedCaveCell> cells,
Long arbitrationWinnerSourceId
) {
private static PersistedCavePlan from(HydrologyCavePlan plan) {
ArrayList<PersistedCaveCell> cells = new ArrayList<>(plan.baselinePreconditions().size());
plan.forEachPrecondition((CavePosition position, CaveVoxelPrecondition precondition) -> cells.add(
new PersistedCaveCell(position, plan.actions().get(position), precondition)));
cells.sort(Comparator.comparingInt((PersistedCaveCell cell) -> cell.position().x())
.thenComparingInt(cell -> cell.position().z())
.thenComparingInt(cell -> cell.position().y()));
return new PersistedCavePlan(
plan.source(),
plan.rejection(),
cells,
plan.arbitrationWinnerSourceId().isPresent()
? plan.arbitrationWinnerSourceId().getAsLong()
: null
);
}
private HydrologyCavePlan toPlan() {
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
for (PersistedCaveCell cell : Objects.requireNonNull(cells, "cells")) {
PersistedCaveCell required = Objects.requireNonNull(cell, "cell");
CaveVoxelPrecondition existing = preconditions.put(required.position(), required.precondition());
if (existing != null) {
throw new IllegalArgumentException("Duplicate hydrology cave cell.");
}
if (required.action() != null) {
actions.put(required.position(), required.action());
}
}
return new HydrologyCavePlan(
source,
rejection,
actions,
preconditions,
arbitrationWinnerSourceId == null
? OptionalLong.empty()
: OptionalLong.of(arbitrationWinnerSourceId)
);
}
}
private record PersistedCaveCell(
CavePosition position,
HydrologyCaveAction action,
CaveVoxelPrecondition precondition
) {
private PersistedCaveCell {
Objects.requireNonNull(position, "position");
Objects.requireNonNull(precondition, "precondition");
}
}
private static final class LimitedInputStream extends FilterInputStream {
private final long limit;
private long count;
private LimitedInputStream(InputStream input, long limit) {
super(input);
this.limit = limit;
}
@Override
public int read() throws IOException {
int value = super.read();
if (value >= 0) {
advance(1L);
}
return value;
}
@Override
public int read(byte[] bytes, int offset, int length) throws IOException {
int read = super.read(bytes, offset, length);
if (read > 0) {
advance(read);
}
return read;
}
private void advance(long amount) throws IOException {
count += amount;
if (count > limit) {
throw new IOException("Studio hydrology tile cache entry exceeds the decompressed size limit.");
}
}
}
}
@@ -111,6 +111,11 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
return basis.terrain().withSlope(localSlope(blockX, blockZ, basis.naturalHeight()));
}
@Override
public HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
return basis(blockX, blockZ).terrain();
}
TerrainBasis basis(int blockX, int blockZ) {
long packed = pack(blockX, blockZ);
synchronized (lock) {
@@ -69,6 +69,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntConsumer;
@@ -168,11 +169,28 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
return cache.columnAt(blockX, blockZ);
}
/** Plans every tile touching the block area ahead of time, nearest the centre first. */
/** Plans a bounded window of tiles touching the block area ahead of time, nearest the centre first. */
public void prefetchArea(int minimumBlockX, int minimumBlockZ, int maximumBlockX, int maximumBlockZ, int centreBlockX, int centreBlockZ) {
cache.prefetchArea(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ, centreBlockX, centreBlockZ);
}
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
cache.preparePregeneration(centerBlockX, centerBlockZ);
}
public void setNeighbourPrefetchEnabled(boolean enabled) {
cache.setNeighbourPrefetchEnabled(enabled);
}
public void enableSharedCache(String runtimeIdentity, Path persistentRoot) {
cache.enableSharedCache(new HydrologyTileCache.SharedCacheScope(
runtimeIdentity,
context.seed(),
context.worldHeight(),
context.dimension().getLoadKey(),
settings.fingerprint()), persistentRoot);
}
public void prepareChunkColumns(int blockX, int blockZ) {
cache.prepareChunkColumns(blockX, blockZ);
}
@@ -447,6 +447,7 @@ public class IrisCaveCarver3D {
int[] activeColumnIndices = scratch.activeColumnIndices;
int[] activeColumnTopY = scratch.activeColumnTopY;
int activeColumnCount = 0;
int activeMaxY = Integer.MIN_VALUE;
for (int index = 0; index < 256; index++) {
double columnWeight = clampedWeights[index];
@@ -458,6 +459,7 @@ public class IrisCaveCarver3D {
passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
activeColumnIndices[activeColumnCount] = index;
activeColumnTopY[activeColumnCount] = columnMaxY[index];
activeMaxY = Math.max(activeMaxY, columnMaxY[index]);
activeColumnCount++;
}
@@ -469,11 +471,12 @@ public class IrisCaveCarver3D {
double[] planeThresholdLimit = scratch.planeThresholdLimit;
boolean[] planeCarve = scratch.planeCarve;
int minSection = PowerOfTwoCoordinates.floorDivPow2(minY, 4);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(maxY, 4);
int passMaxY = Math.min(maxY, activeMaxY);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(passMaxY, 4);
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(maxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex);
for (int y = sectionMinY; y <= sectionMaxY; y++) {
@@ -579,6 +582,7 @@ public class IrisCaveCarver3D {
int[] activeColumnIndices = scratch.activeColumnIndices;
int[] activeColumnTopY = scratch.activeColumnTopY;
int activeColumnCount = 0;
int activeMaxY = Integer.MIN_VALUE;
for (int index = 0; index < 256; index++) {
double columnWeight = clampedWeights[index];
@@ -590,6 +594,7 @@ public class IrisCaveCarver3D {
passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
activeColumnIndices[activeColumnCount] = index;
activeColumnTopY[activeColumnCount] = columnMaxY[index];
activeMaxY = Math.max(activeMaxY, columnMaxY[index]);
activeColumnCount++;
}
@@ -601,7 +606,8 @@ public class IrisCaveCarver3D {
double[] planeThresholdLimit = scratch.planeThresholdLimit;
boolean[] planeCarve = scratch.planeCarve;
int minSection = PowerOfTwoCoordinates.floorDivPow2(minY, 4);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(maxY, 4);
int passMaxY = Math.min(maxY, activeMaxY);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(passMaxY, 4);
int effectiveAdaptiveSampleStep = Math.max(adaptiveSampleStep, ADAPTIVE_DEEP_SAMPLE_STEP);
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
adaptiveThresholdMargin,
@@ -611,7 +617,7 @@ public class IrisCaveCarver3D {
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(maxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex);
for (int y = sectionMinY; y <= sectionMaxY; y++) {
@@ -1459,6 +1465,41 @@ public class IrisCaveCarver3D {
}
}
private void classifyDeepAdaptivePlaneFromSamples(
CaveCarveScratch scratch,
int[] planeColumnIndices,
double[] planeThresholdLimit,
int planeCount,
boolean[] planeCarve,
int adaptiveSampleStep,
double[] adaptivePlaneDensity,
int axisCells,
int axisSamples
) {
prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisSamples);
int[] adaptiveCellZ = scratch.adaptiveCellZ;
int[] adaptiveRow0 = scratch.adaptiveRow0;
int[] adaptiveRow1 = scratch.adaptiveRow1;
double[] adaptiveTx = scratch.adaptiveTx;
double[] adaptiveTz = scratch.adaptiveTz;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
int columnIndex = planeColumnIndices[planeIndex];
int cellZ = adaptiveCellZ[columnIndex];
int row0 = adaptiveRow0[columnIndex];
int row1 = adaptiveRow1[columnIndex];
double tx = adaptiveTx[columnIndex];
double tz = adaptiveTz[columnIndex];
double d00 = adaptivePlaneDensity[row0 + cellZ];
double d01 = adaptivePlaneDensity[row0 + cellZ + 1];
double d10 = adaptivePlaneDensity[row1 + cellZ];
double d11 = adaptivePlaneDensity[row1 + cellZ + 1];
double dx0 = d00 + ((d10 - d00) * tx);
double dx1 = d01 + ((d11 - d01) * tx);
double predictedDensity = dx0 + ((dx1 - dx0) * tz);
planeCarve[planeIndex] = predictedDensity <= planeThresholdLimit[planeIndex] * inverseNormalization;
}
}
private void classifyAdaptivePlaneColumnsWarpOnly(
CaveCarveScratch scratch,
int x0,
@@ -1615,6 +1656,21 @@ public class IrisCaveCarver3D {
double[] remainingMin,
double[] remainingMax
) {
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
classifyDeepAdaptivePlaneFromSamples(
scratch,
planeColumnIndices,
planeThresholdLimit,
planeCount,
planeCarve,
adaptiveSampleStep,
adaptivePlaneDensity,
axisCells,
axisSamples
);
return;
}
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
prepareAdaptivePlaneColumns(
@@ -2235,17 +2291,29 @@ public class IrisCaveCarver3D {
}
int support = 0;
int misses = 0;
if (isDensitySolid(scratch, x + 1, y, z, threshold)) {
support++;
} else {
misses++;
}
if (isDensitySolid(scratch, x - 1, y, z, threshold)) {
support++;
} else if (++misses >= 2) {
return false;
}
if (isDensitySolid(scratch, x, y, z + 1, threshold)) {
support++;
} else if (++misses >= 2) {
return false;
}
if (isDensitySolid(scratch, x, y, z - 1, threshold)) {
support++;
if (support >= 4) {
return true;
}
} else if (++misses >= 2) {
return false;
}
if (isDensitySolid(scratch, x, aboveY, z, threshold)) {
support++;
@@ -620,6 +620,9 @@ public class IrisDimension extends IrisRegistrant {
if (biome == null) {
continue;
}
if (biome.isCompatExcluded()) {
continue;
}
String loadKey = biome.getLoadKey();
if (loadKey == null || loadKey.isBlank() || biomes.containsKey(loadKey)) {
@@ -240,23 +240,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return new Location(world, 0.5D, y, 0.5D);
}
private int hydrologyTileSize() {
Engine activeEngine = engine;
if (activeEngine == null || activeEngine.getComplex() == null || activeEngine.getComplex().getHydrologyRuntime() == null) {
return 0;
}
return activeEngine.getComplex().getHydrologyRuntime().settings().routing().tileSize();
}
/**
* Starts planning the hydrology tiles around the initial spawn as soon as the generator is injected
* into a world whose spawn chunk does not exist yet. The first teleport there makes the chunk system
* generate chunks a few hundred blocks around the spawn, and each of those chunks' mantle windows
* reaches further still, so the area planned ahead is half a tile in every direction: every tile a
* cold entry can touch starts planning at once instead of one generation thread at a time finding
* it cold. A world that already generated its spawn (every existing world at boot, a hotloaded live
* world) skips it.
*/
/** Starts nearest-first hydrology planning around a new world's initial spawn. */
private void prefetchSpawnHydrology(Engine engine, World world) {
if (engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
return;
@@ -278,6 +262,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
);
}
private int hydrologyTileSize() {
Engine activeEngine = engine;
if (activeEngine == null || activeEngine.getComplex() == null || activeEngine.getComplex().getHydrologyRuntime() == null) {
return 0;
}
return activeEngine.getComplex().getHydrologyRuntime().settings().routing().tileSize();
}
private void updateSpawnLocation(World world) {
try {
Location initialSpawn = getInitialSpawnLocation(world);
@@ -62,6 +62,10 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
return cache.getMaxSize();
}
public void setMaximumChunks(int maximumChunks) {
cache.setMaximumChunks(maximumChunks);
}
@Override
public boolean isClosed() {
return engine.isClosed();