mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +00:00
CORRECTIONS
- this is the corrections from the afoemrnioned commits
This commit is contained in:
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) {
|
||||
|
||||
+25
-15
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -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);
|
||||
|
||||
+78
-85
@@ -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);
|
||||
|
||||
+5
-4
@@ -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}
|
||||
};
|
||||
|
||||
+4
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -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) {
|
||||
|
||||
+19
-1
@@ -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);
|
||||
|
||||
+4
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user