mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Merge master
# Conflicts: # core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java
This commit is contained in:
commit
73af3efe12
34 files changed
+688
-63
No files matched your search
@@ -1089,16 +1089,18 @@ public class IrisComplex implements DataProvider {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnLayer original = sample.layers().get(index);
|
||||
int head = (int) Math.round(transitionDisplacement.fluidHeight(sample.x(), sample.z(), layer.fluidHeadY()));
|
||||
int displacedHead = (int) Math.round(
|
||||
transitionDisplacement.fluidHeight(sample.x(), sample.z(), original.fluidHeadY()));
|
||||
boolean channel = original.channel() && layer.bedY() <= displacedHead;
|
||||
int head = channel ? displacedHead : layer.bedY();
|
||||
if ((tapered.naturalHeight() < seaLevel || tapered.naturalHeight() == seaLevel && head > seaLevel)
|
||||
&& (original.terrainOwned() || original.fluidOwned() || original.grading() || original.shore())) {
|
||||
continue;
|
||||
}
|
||||
boolean dryChannel = original.channel() && layer.bedY() > head;
|
||||
layers.add(new HydrologyColumnLayer(layer.feature(), layer.bedY(),
|
||||
head, head, original.channel() && !dryChannel, layer.shore(), layer.grading(), original.connectedFluid() && !dryChannel,
|
||||
original.fallingFluid() && !dryChannel, original.receivingPool() && !dryChannel,
|
||||
layer.terrainOwned(), original.fluidOwned() && !dryChannel, layer.oceanApron(),
|
||||
head, head, channel, layer.shore(), layer.grading(), original.connectedFluid() && channel,
|
||||
original.fallingFluid() && channel, original.receivingPool() && channel,
|
||||
layer.terrainOwned(), original.fluidOwned() && channel, layer.oceanApron(),
|
||||
layer.profileKey(), layer.surfaceBiomeKey(), layer.mouthBiomeKey(), layer.shoreBiomeKey(),
|
||||
layer.bankBiomeKey(), layer.floodedCaveBiomeKey()));
|
||||
}
|
||||
@@ -1140,20 +1142,21 @@ public class IrisComplex implements DataProvider {
|
||||
return layer;
|
||||
}
|
||||
int bed = GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight);
|
||||
boolean dryChannel = layer.channel() && bed > layer.fluidHeadY();
|
||||
boolean channel = layer.channel() && bed <= layer.fluidHeadY();
|
||||
int head = channel ? layer.fluidHeadY() : bed;
|
||||
return new HydrologyColumnLayer(
|
||||
layer.feature(),
|
||||
bed,
|
||||
layer.fluidHeadY(),
|
||||
layer.ceilingY(),
|
||||
layer.channel() && !dryChannel,
|
||||
head,
|
||||
head,
|
||||
channel,
|
||||
layer.shore(),
|
||||
layer.grading(),
|
||||
layer.connectedFluid() && !dryChannel,
|
||||
layer.fallingFluid() && !dryChannel,
|
||||
layer.receivingPool() && !dryChannel,
|
||||
layer.connectedFluid() && channel,
|
||||
layer.fallingFluid() && channel,
|
||||
layer.receivingPool() && channel,
|
||||
layer.terrainOwned(),
|
||||
layer.fluidOwned() && !dryChannel,
|
||||
layer.fluidOwned() && channel,
|
||||
layer.oceanApron(),
|
||||
layer.profileKey(),
|
||||
layer.surfaceBiomeKey(),
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.concurrent.Semaphore;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.world.IrisWorlds;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
@@ -1290,6 +1291,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
@Override
|
||||
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
|
||||
try {
|
||||
generateNoiseChunk(world, random, x, z, d);
|
||||
} catch (Throwable e) {
|
||||
PregeneratorJob.reportChunkGenerationFailure(this.world.identity(), x, z, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void generateNoiseChunk(WorldInfo world, Random random, int x, int z, ChunkGenerator.ChunkData d) {
|
||||
if (closing) {
|
||||
throw new IllegalStateException("Iris chunk generation was rejected while the generator is closing.");
|
||||
}
|
||||
|
||||
@@ -254,6 +254,14 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
public static void reportChunkGenerationFailure(String worldIdentity, int chunkX, int chunkZ, Throwable failure) {
|
||||
PregeneratorJob inst = instance.get();
|
||||
if (inst == null || worldIdentity == null || !worldIdentity.equals(inst.worldIdentity())) {
|
||||
return;
|
||||
}
|
||||
inst.pregenerator.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||
}
|
||||
|
||||
public static boolean pauseResume() {
|
||||
PregeneratorJob inst = instance.get();
|
||||
return inst != null && inst.togglePause();
|
||||
|
||||
@@ -85,6 +85,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
|
||||
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||
method.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||
method.onPregenStart(centerBlockX, centerBlockZ);
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.world.pregen;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorkerPool;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import art.arcane.iris.world.IrisPaperLikeBackendMode;
|
||||
import art.arcane.iris.world.IrisRuntimeSchedulerMode;
|
||||
@@ -55,6 +56,7 @@ import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
// THREAD_COUNT records the pool's original size while boosted; BOOST_HOLDERS counts live
|
||||
@@ -64,7 +66,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
|
||||
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
|
||||
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
||||
private static final long CLOSE_DRAIN_WARNING_SECONDS = 60L;
|
||||
private static final long CLOSE_DRAIN_WARNING_SECONDS = 20L;
|
||||
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
|
||||
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
|
||||
private static final long MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
|
||||
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
|
||||
@@ -113,6 +116,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private final AtomicLong failed = new AtomicLong();
|
||||
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
||||
private final AtomicBoolean closing = new AtomicBoolean();
|
||||
private final PregenInFlightRequests inFlightRequests = new PregenInFlightRequests();
|
||||
private final AtomicReference<GenerationFailure> generationFailure = new AtomicReference<>();
|
||||
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
||||
private volatile Engine metricsEngine;
|
||||
private volatile Mantle cachedMantle;
|
||||
@@ -515,10 +520,20 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
|
||||
private void completeChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) {
|
||||
PregenListener owner = inFlightRequests.settle(PregenInFlightRequests.key(x, z));
|
||||
if (owner == null) {
|
||||
return;
|
||||
}
|
||||
settleChunk(x, z, owner, chunk, throwable, true);
|
||||
}
|
||||
|
||||
private void settleChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable, boolean reportFailure) {
|
||||
boolean success = false;
|
||||
try {
|
||||
if (throwable != null) {
|
||||
onChunkFutureFailure(x, z, throwable);
|
||||
if (reportFailure) {
|
||||
onChunkFutureFailure(x, z, throwable);
|
||||
}
|
||||
onChunkFailedToLoad(x, z);
|
||||
listener.onChunkFailed(x, z);
|
||||
} else if (chunk == null) {
|
||||
@@ -718,11 +733,57 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
+ " stalledForMs=" + stalledFor;
|
||||
}
|
||||
|
||||
private void markSubmitted() {
|
||||
private void markSubmitted(int x, int z, PregenListener listener) {
|
||||
inFlightRequests.add(PregenInFlightRequests.key(x, z), listener);
|
||||
submitted.incrementAndGet();
|
||||
inFlight.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||
GenerationFailure recorded = new GenerationFailure(chunkX, chunkZ, failure);
|
||||
if (generationFailure.compareAndSet(null, recorded)) {
|
||||
IrisLogging.error("Async pregen aborting: Iris failed to generate chunk " + chunkX + "," + chunkZ
|
||||
+ " and the server's chunk system will not complete the " + inFlightRequests.size()
|
||||
+ " outstanding chunk request(s). " + metricsSnapshot());
|
||||
}
|
||||
failOutstandingRequests(failure);
|
||||
}
|
||||
|
||||
private void failOutstandingRequests(Throwable failure) {
|
||||
Map<Long, PregenListener> outstanding = inFlightRequests.drain();
|
||||
for (Map.Entry<Long, PregenListener> entry : outstanding.entrySet()) {
|
||||
long key = entry.getKey();
|
||||
settleChunk(PregenInFlightRequests.chunkX(key), PregenInFlightRequests.chunkZ(key), entry.getValue(),
|
||||
null, failure, false);
|
||||
}
|
||||
admission.wake();
|
||||
backpressure.signalProgress();
|
||||
}
|
||||
|
||||
private void throwIfGenerationFailed() {
|
||||
GenerationFailure failure = generationFailure.get();
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Pregen aborted: Iris failed to generate chunk " + failure.chunkX() + ","
|
||||
+ failure.chunkZ() + " in world " + world.getName()
|
||||
+ " and the server's chunk system stopped completing requests.", failure.cause());
|
||||
}
|
||||
}
|
||||
|
||||
private void abandonOutstandingRequests(PregenAdmissionGate.Drain drain) {
|
||||
Map<Long, PregenListener> abandoned = inFlightRequests.drain();
|
||||
IrisLogging.error("Async pregen abandoned " + abandoned.size() + " chunk request(s) that did not complete within "
|
||||
+ CLOSE_DRAIN_TIMEOUT_SECONDS + "s of closing (" + drain.outstanding()
|
||||
+ " permit(s) still held). They are not cached and regenerate on the next run. " + metricsSnapshot());
|
||||
IllegalStateException cause = new IllegalStateException(
|
||||
"Async chunk request never completed before the pregen closed in world " + world.getName() + ".");
|
||||
for (Map.Entry<Long, PregenListener> entry : abandoned.entrySet()) {
|
||||
long key = entry.getKey();
|
||||
settleChunk(PregenInFlightRequests.chunkX(key), PregenInFlightRequests.chunkZ(key), entry.getValue(),
|
||||
null, cause, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void markFinished(boolean success) {
|
||||
if (success) {
|
||||
completed.incrementAndGet();
|
||||
@@ -840,11 +901,17 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
|
||||
boolean interrupted = Thread.interrupted();
|
||||
try {
|
||||
interrupted |= admission.awaitDrain(
|
||||
PregenAdmissionGate.Drain drain = admission.awaitDrain(
|
||||
CLOSE_DRAIN_WARNING_SECONDS,
|
||||
TimeUnit.SECONDS,
|
||||
CLOSE_DRAIN_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS,
|
||||
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
|
||||
);
|
||||
interrupted |= drain.interrupted();
|
||||
if (!drain.drained()) {
|
||||
abandonOutstandingRequests(drain);
|
||||
}
|
||||
syncGenerationHistory();
|
||||
|
||||
mantleCleanup.close(MANTLE_CLEANUP_DRAIN_SECONDS, TimeUnit.SECONDS);
|
||||
@@ -889,7 +956,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
return closing.get() || Thread.currentThread().isInterrupted();
|
||||
return closing.get() || generationFailure.get() != null || Thread.currentThread().isInterrupted();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -917,6 +984,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
throwIfGenerationFailed();
|
||||
listener.onChunkGenerating(x, z);
|
||||
if (MantleHeapPressure.overHighWater()) {
|
||||
reclaimMemory();
|
||||
@@ -947,10 +1015,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
recordPermitWait(waited.permitMs());
|
||||
}
|
||||
|
||||
throwIfGenerationFailed();
|
||||
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
|
||||
retainAround(x, z);
|
||||
markSubmitted();
|
||||
markSubmitted(x, z, listener);
|
||||
executor.generate(x, z, listener);
|
||||
GenerationFailure failure = generationFailure.get();
|
||||
if (failure != null) {
|
||||
failOutstandingRequests(failure.cause());
|
||||
}
|
||||
}
|
||||
|
||||
private void reclaimHeapPressure() {
|
||||
@@ -1187,6 +1260,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
private record GenerationFailure(int chunkX, int chunkZ, Throwable cause) {
|
||||
}
|
||||
|
||||
private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) {
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,11 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
||||
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||
method.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||
method.onPregenStart(centerBlockX, centerBlockZ);
|
||||
|
||||
@@ -78,6 +78,11 @@ public class HybridPregenMethod implements PregeneratorMethod {
|
||||
inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||
inWorld.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||
inWorld.onPregenStart(centerBlockX, centerBlockZ);
|
||||
|
||||
@@ -215,6 +215,10 @@ public class IrisPregenerator {
|
||||
signalState();
|
||||
}
|
||||
|
||||
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||
generator.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
boolean completed = false;
|
||||
|
||||
@@ -111,14 +111,20 @@ public final class PregenAdmissionGate {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean awaitDrain(long warningInterval, TimeUnit timeUnit, Runnable onWait) {
|
||||
public Drain awaitDrain(long warningInterval, TimeUnit warningUnit, long deadline, TimeUnit deadlineUnit, Runnable onWait) {
|
||||
long warningNanos = warningUnit.toNanos(warningInterval);
|
||||
long deadlineAt = System.nanoTime() + deadlineUnit.toNanos(deadline);
|
||||
boolean interrupted = false;
|
||||
lock.lock();
|
||||
try {
|
||||
while (free < permits) {
|
||||
long remaining = deadlineAt - System.nanoTime();
|
||||
if (remaining <= 0L) {
|
||||
return new Drain(false, interrupted, permits - free);
|
||||
}
|
||||
boolean signalled = false;
|
||||
try {
|
||||
signalled = changed.await(warningInterval, timeUnit);
|
||||
signalled = changed.awaitNanos(Math.min(warningNanos, remaining)) > 0L;
|
||||
} catch (InterruptedException e) {
|
||||
interrupted = true;
|
||||
continue;
|
||||
@@ -127,10 +133,12 @@ public final class PregenAdmissionGate {
|
||||
onWait.run();
|
||||
}
|
||||
}
|
||||
return new Drain(true, interrupted, 0);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
return interrupted;
|
||||
public record Drain(boolean drained, boolean interrupted, int outstanding) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.world.pregen;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class PregenInFlightRequests {
|
||||
private final ConcurrentHashMap<Long, PregenListener> requests = new ConcurrentHashMap<>();
|
||||
|
||||
public static long key(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
public static int chunkX(long key) {
|
||||
return (int) (key >> 32);
|
||||
}
|
||||
|
||||
public static int chunkZ(long key) {
|
||||
return (int) key;
|
||||
}
|
||||
|
||||
public boolean add(long key, PregenListener listener) {
|
||||
return requests.putIfAbsent(key, Objects.requireNonNull(listener, "listener")) == null;
|
||||
}
|
||||
|
||||
public PregenListener settle(long key) {
|
||||
return requests.remove(key);
|
||||
}
|
||||
|
||||
public Map<Long, PregenListener> drain() {
|
||||
HashMap<Long, PregenListener> drained = new HashMap<>();
|
||||
Iterator<Map.Entry<Long, PregenListener>> iterator = requests.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<Long, PregenListener> entry = iterator.next();
|
||||
if (requests.remove(entry.getKey(), entry.getValue())) {
|
||||
drained.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return drained;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return requests.size();
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,9 @@ public interface PregeneratorMethod {
|
||||
default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
|
||||
}
|
||||
|
||||
default void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||
}
|
||||
|
||||
default void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user