This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-11 10:35:59 -04:00
1 parent d480abad50
commit 2145e90b1c
46 files changed
+1766 -167

No files matched your search

@@ -18,6 +18,7 @@
package art.arcane.iris.configuration;
import art.arcane.iris.world.pregen.MantleHeapPressure;
import art.arcane.iris.world.IrisPaperLikeBackendMode;
import art.arcane.iris.world.IrisRuntimeSchedulerMode;
@@ -275,7 +276,7 @@ public class IrisSettings {
}
public int getEffectiveResidentTectonicPlates(int worldHeight) {
return effectiveResidentTectonicPlates(worldHeight, Runtime.getRuntime().maxMemory());
return effectiveResidentTectonicPlates(worldHeight, MantleHeapPressure.memoryBudgetBytes());
}
int effectiveResidentTectonicPlates(int worldHeight, long maximumHeapBytes) {
@@ -398,6 +399,7 @@ public class IrisSettings {
@Data
public static class IrisSettingsStudio {
public boolean openVSCode = true;
public boolean blendChanges = true;
public boolean disableTimeAndWeather = true;
public boolean entitySpawning = true;
public boolean autoStartDefaultStudio = false;
@@ -11,6 +11,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.util.hunk.Hunk;
@@ -25,6 +26,7 @@ public class ChunkContext {
private Hunk<NativeBiome> terrainBiomes;
private FloatingBiomeOverlay floatingBiomes;
private CaveTerrainSnapshot caveTerrain;
private TransitionTerrainColumn[] transitionTerrain;
private final int x;
private final int z;
private final IrisComplex complex;
@@ -167,6 +169,17 @@ public class ChunkContext {
this.caveTerrain = caveTerrain;
}
public void setTransitionTerrain(int localX, int localZ, Terrain3DColumn solids, int fluidHeight) {
if (transitionTerrain == null) {
transitionTerrain = new TransitionTerrainColumn[256];
}
transitionTerrain[(localZ << 4) | localX] = new TransitionTerrainColumn(solids, fluidHeight);
}
public TransitionTerrainColumn getTransitionTerrain(int localX, int localZ) {
return transitionTerrain == null ? null : transitionTerrain[(localZ << 4) | localX];
}
public void setNaturalBiome(int localX, int y, int localZ, NativeBiome biome) {
if (terrainBiomes != null) {
terrainBiomes.setRaw(localX, y, localZ, biome);
@@ -280,6 +293,9 @@ public class ChunkContext {
}
}
public record TransitionTerrainColumn(Terrain3DColumn solids, int fluidHeight) {
}
private static final class PrefillFillTask implements Runnable {
private final ChunkedDataCache<?> dataCache;
@@ -20,6 +20,8 @@ package art.arcane.iris.generation.decoration;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.util.hunk.Hunk;
@@ -60,7 +62,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
@BlockCoordinates
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<NativeBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
int fluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ);
int fluidHeight = surfaceFluidHeight(realX, realZ);
if (inferredType == InferredType.SHORE && height < fluidHeight) {
return;
}
@@ -100,6 +102,21 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
return height < fluidHeight && inferredType != InferredType.CAVE;
}
int surfaceFluidHeight(int worldX, int worldZ) {
IrisContext current = IrisContext.get();
ChunkContext chunk = current == null ? null : current.getChunkContext();
if (chunk != null && current.getEngine() == getEngine()
&& current.getGenerationSessionId() == getEngine().getGenerationSessionId()
&& current.getGenerationSessionId() == chunk.getGenerationSessionId()
&& worldX >> 4 == chunk.getX() >> 4 && worldZ >> 4 == chunk.getZ() >> 4) {
ChunkContext.TransitionTerrainColumn transition = chunk.getTransitionTerrain(worldX & 15, worldZ & 15);
if (transition != null) {
return transition.fluidHeight();
}
}
return getEngine().getMantle().getFluidHeight(worldX, worldZ);
}
static boolean skipsFluid(InferredType inferredType) {
return inferredType == InferredType.CAVE;
}
@@ -35,6 +35,8 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.generation.context.IrisContext;
import java.util.concurrent.TimeUnit;
import static art.arcane.iris.generation.runtime.EngineShutdownSequence.runCleanup;
/**
@@ -224,12 +226,14 @@ final class EngineHotloader {
StudioGenerationUpdate update = null;
boolean backgroundAdmissionClosed = false;
try {
long startedAt = System.nanoTime();
engine.requireRunning("hotload the Studio pack");
GenerationHistory history = router.history();
update = StudioGenerationUpdate.prepare(engine, history);
if (update == null) {
return;
}
long preparedAt = System.nanoTime();
engine.awaitNativeStructureBootstrap("Studio generation cutover");
engine.backgroundTasks.closeBackgroundTaskAdmission();
backgroundAdmissionClosed = true;
@@ -243,15 +247,27 @@ final class EngineHotloader {
engine.lifecycleState = LifecycleState.HOTLOADING;
}
engine.sealForTransition("Studio generation cutover", false);
long drainedAt = System.nanoTime();
IrisBoundarySignatureSampler.checkpoint(engine);
long checkpointedAt = System.nanoTime();
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState != LifecycleState.HOTLOADING || engine.runtime != previous) {
throw new IllegalStateException("Studio engine changed while its generation cutover was draining.");
}
engine.prepareRuntimeHotload();
IrisSettings settings = IrisSettings.get();
int transitionWidth = settings.getStudio().isBlendChanges()
? settings.getGenerator().getGenerationTransitionWidthBlocks() : 0;
history.stageUpdate(update.source(), update.fingerprint(), update.dimensionContract(),
update.registryContract(), IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
cutover.promotePending();
update.registryContract(), transitionWidth);
}
long stagedAt = System.nanoTime();
cutover.promotePending();
long promotedAt = System.nanoTime();
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState != LifecycleState.HOTLOADING) {
throw new IllegalStateException("Studio engine changed while its generation cutover was publishing.");
}
IrisStructureLocator.invalidate(engine);
StructureReachability.invalidate(engine);
engine.runtimeBuilder.refreshStudioRuntimeServices();
@@ -259,19 +275,27 @@ final class EngineHotloader {
engine.getEngineData().getStatistics().hotloaded();
engine.getPlatformHooks().applyWorldBoundary(engine);
}
long completedAt = System.nanoTime();
IrisLogging.debug("Studio update timing: world=%s total=%dms prepare=%dms drain=%dms checkpoint=%dms stage=%dms activation=%dms refresh=%dms",
engine.getWorld().name(), elapsedMillis(startedAt, completedAt),
elapsedMillis(startedAt, preparedAt), elapsedMillis(preparedAt, drainedAt),
elapsedMillis(drainedAt, checkpointedAt), elapsedMillis(checkpointedAt, stagedAt),
elapsedMillis(stagedAt, promotedAt), elapsedMillis(promotedAt, completedAt));
broadcastStudioHotload(false, "");
} catch (Throwable failure) {
if (previous != null) {
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState == LifecycleState.HOTLOADING
|| engine.lifecycleState == LifecycleState.RUNNING) {
boolean unchanged = false;
boolean activeGenerationUnchanged = false;
try {
unchanged = router.history().manifest() == previousManifest;
GenerationManifest currentManifest = router.history().manifest();
activeGenerationUnchanged = currentManifest.activeActivation().activationId()
== previousManifest.activeActivation().activationId();
} catch (Throwable historyFailure) {
failure.addSuppressed(historyFailure);
}
if (unchanged) {
if (activeGenerationUnchanged) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
} else {
engine.lifecycleState = LifecycleState.FAILED;
@@ -305,6 +329,10 @@ final class EngineHotloader {
}
}
private static long elapsedMillis(long startedAt, long completedAt) {
return TimeUnit.NANOSECONDS.toMillis(completedAt - startedAt);
}
private void requireMutableGenerationRuntime(String operation) {
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
throw new IllegalStateException("Cannot " + operation
@@ -995,7 +995,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
zone.setCeiling(y - 1);
if (zone.isValid(resolver.height)
&& (subterrainCell(worldX, floor, worldZ).occupied()
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ))) {
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ, context))) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
@@ -1112,6 +1112,17 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
}
private boolean isNaturalTerrainOpening(IrisComplex complex, int x, int y, int z, ChunkContext context) {
ChunkContext.TransitionTerrainColumn transition = context == null ? null
: context.getTransitionTerrain(x & 15, z & 15);
if (transition == null) {
return isNaturalTerrainOpening(complex, x, y, z);
}
Terrain3DColumn natural = complex.terrainColumn(x, z, complex.sampleHydrologyColumn(x, z));
return natural != null && IrisDecorantActuator.isTransitionTerrainOpening(natural, transition, y)
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
}
/** The dimension's underground river bed palette, or null when nothing paints. */
private IrisRiverMaterialConfig undergroundBedMaterial() {
IrisHydrology hydrology = getDimension().getHydrology();
@@ -174,8 +174,9 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
if (isFallingWaterfallThroat(hydrology)) {
continue;
}
int surfaceFluidHeight = (int) Math.round(
getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
ChunkContext.TransitionTerrainColumn transitionTerrain = context.getTransitionTerrain(i, j);
int surfaceFluidHeight = transitionTerrain == null ? (int) Math.round(
getComplex().getRiverWaterSurfaceStream().get(realX, realZ)) : transitionTerrain.fluidHeight();
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
continue;
@@ -231,7 +232,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
Terrain3DColumn terrainColumn = getComplex().terrainColumn(realX, realZ, hydrology);
if (terrainColumn != null) {
decorateTerrainLedges(output, terrainColumn, biome, i, j, realX, realZ);
decorateTerrainLedges(output, terrainColumn, transitionTerrain, biome, i, j, realX, realZ);
}
@@ -263,10 +264,15 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
}
private void decorateTerrainLedges(Hunk<NativeBlockState> output, Terrain3DColumn column,
ChunkContext.TransitionTerrainColumn transition,
IrisBiome biome, int localX, int localZ, int worldX, int worldZ) {
for (int span = 0; span + 1 < column.spanCount(); span++) {
int floorY = column.floor(span);
int ceilingY = column.ceiling(span + 1);
Terrain3DColumn ledges = transition == null ? column : transition.solids();
for (int span = 0; span + 1 < ledges.spanCount(); span++) {
int floorY = ledges.floor(span);
int ceilingY = ledges.ceiling(span + 1);
if (transition != null && resolveTerrainLedge(column, span, transition) == null) {
continue;
}
int headroom = ceilingY - floorY - 1;
if (headroom < 1 || !PREDICATE_SOLID.test(output.getRaw(localX, floorY, localZ))) {
continue;
@@ -280,6 +286,37 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
}
}
static TerrainLedge resolveTerrainLedge(Terrain3DColumn natural, int span,
ChunkContext.TransitionTerrainColumn transition) {
if (transition == null) {
return new TerrainLedge(natural.floor(span), natural.ceiling(span + 1));
}
Terrain3DColumn blended = transition.solids();
int blendedFloor = blended.floor(span);
int blendedCeiling = blended.ceiling(span + 1);
for (int candidate = 0; candidate + 1 < natural.spanCount(); candidate++) {
if (Math.max(natural.floor(candidate), blendedFloor) + 1
< Math.min(natural.ceiling(candidate + 1), blendedCeiling)) {
return new TerrainLedge(blendedFloor, blendedCeiling);
}
}
return null;
}
static boolean isTransitionTerrainOpening(Terrain3DColumn natural,
ChunkContext.TransitionTerrainColumn transition, int y) {
Terrain3DColumn blended = transition.solids();
for (int span = 0; span + 1 < blended.spanCount(); span++) {
if (y > blended.floor(span) && y < blended.ceiling(span + 1)) {
return resolveTerrainLedge(natural, span, transition) != null;
}
}
return false;
}
record TerrainLedge(int floorY, int ceilingY) {
}
enum ShorelineDecorationMode {
NONE,
LEGACY,
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineStage;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.BoundaryGeometryInfluence;
import art.arcane.iris.world.history.SavedTerrainChunk;
@@ -85,6 +86,17 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
blocks.setRaw(localX, offset, localZ, replacement);
}
context.setTerrainHeight(localX, localZ, floor);
int fluidHeight = floor;
while (fluidHeight + 1 < voxels.size()
&& voxels.get(fluidHeight + 1).phase() == BoundaryColumnGeometry.Phase.FLUID) {
fluidHeight++;
}
if (fluidHeight == floor) {
fluidHeight = -1;
}
context.setTransitionTerrain(localX, localZ, Terrain3DColumn.fromOccupancy(
floor, voxels.size(), offset -> voxels.get(offset).phase() == BoundaryColumnGeometry.Phase.SOLID),
fluidHeight);
}
}
}
@@ -149,6 +149,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final AtomicReference<CompletableFuture<Void>> watcherHotload = new AtomicReference<>();
private volatile Engine engine;
private volatile Looper hotloader;
private volatile boolean studioUpdatesPaused;
private volatile StudioMode lastMode;
private volatile UUID lastJigsawStudioRequestId;
private volatile boolean jigsawStudioActive;
@@ -978,6 +979,18 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return future;
}
@Override
public CompletableFuture<Void> quiesceStudioUpdatesAsync() {
studioUpdatesPaused = true;
cancelWatcherHotload();
Looper activeHotloader = hotloader;
hotloader = null;
if (activeHotloader != null) {
activeHotloader.interrupt();
}
return withExclusiveControlFuture(() -> { });
}
@Override
public void quiesceForServerShutdown() {
cancelWatcherHotload();
@@ -1071,26 +1084,33 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public void hotload() {
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
return;
}
withExclusiveControl(() -> getEngine().hotload());
withExclusiveControl(() -> {
if (!studioUpdatesPaused && !closing) {
getEngine().hotload();
}
});
}
private void hotloadFromWatcher() {
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
return;
}
CompletableFuture<Void> pending = new CompletableFuture<>();
long queuedAt = System.nanoTime();
watcherHotload.set(pending);
try {
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)
|| Thread.currentThread().isInterrupted()) {
pending.cancel(true);
} else {
J.a(() -> completeExclusiveControlFuture(loadLock, () -> {
if (!initialEntryPending && shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
if (!initialEntryPending && !studioUpdatesPaused && shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
IrisLogging.debug("Studio update admission: world=%s wait=%dms",
getTarget().getWorld().name(), TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - queuedAt));
getEngine().hotload();
}
}, pending));
@@ -66,6 +66,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
}
}
CompletableFuture<Void> quiesceStudioUpdatesAsync();
default void quiesceForServerShutdown() {
}
@@ -483,6 +483,7 @@ public final class StudioOpenCoordinator {
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
}
},
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.quiesceStudioUpdatesAsync(),
() -> unloadWorldFamily(worldName, world).thenRun(() -> unloadConfirmed.set(true)),
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(),
() -> deleteFolder
@@ -516,6 +517,7 @@ public final class StudioOpenCoordinator {
static CompletableFuture<Void> sequenceStudioClose(
Supplier<CompletableFuture<Void>> evacuate,
Runnable enterMaintenance,
Supplier<CompletableFuture<Void>> quiesceUpdates,
Supplier<CompletableFuture<Void>> unload,
Supplier<CompletableFuture<Void>> closeGenerator,
Supplier<CompletableFuture<Void>> deleteFolders
@@ -523,8 +525,9 @@ public final class StudioOpenCoordinator {
return invokePhase(evacuate)
.thenCompose(ignored -> {
enterMaintenance.run();
return invokePhase(unload);
return invokePhase(quiesceUpdates);
})
.thenCompose(ignored -> invokePhase(unload))
.thenCompose(ignored -> invokePhase(closeGenerator))
.thenCompose(ignored -> invokePhase(deleteFolders));
}
@@ -20,12 +20,10 @@ package art.arcane.iris.studio.workspace;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.studio.object.StudioOpenCoordinator;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import lombok.Data;
import org.bukkit.World;
@@ -136,8 +134,6 @@ public class IrisProject {
);
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
future.whenComplete((result, throwable) -> {
World maintenanceWorld = null;
boolean maintenanceActive = false;
try {
if (throwable != null) {
failed.set(true);
@@ -148,17 +144,7 @@ public class IrisProject {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED, MessageArgument.untrusted("error", String.valueOf(error.getMessage()))));
return;
}
maintenanceWorld = result.world();
if (maintenanceWorld != null) {
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
maintenanceActive = true;
}
} finally {
if (maintenanceActive && maintenanceWorld != null) {
World worldToRelease = maintenanceWorld;
J.a(() -> IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open"), 300);
}
complete.set(true);
}
});
@@ -365,13 +365,12 @@ public class IrisCreator {
worldKey,
IrisWorldStorage.levelRoot().getName()
);
BukkitWorldConfiguration.register(
bukkitRegistered = BukkitWorldConfiguration.register(
BUKKIT_YML,
registrationName,
dimension,
seed
);
bukkitRegistered = true;
) == BukkitWorldConfiguration.Registration.CREATED;
World createdWorld = world;
CompletableFuture<Void> multiverseRegistration = J.sfut(
() -> IrisServices.get(MultiverseCoreLink.class)
@@ -22,8 +22,8 @@ public record GenerationTransition(
if (algorithmVersion < 1) {
throw new IllegalArgumentException("Transition algorithm version must be positive.");
}
if (widthBlocks < MINIMUM_WIDTH_BLOCKS || widthBlocks > MAXIMUM_WIDTH_BLOCKS) {
throw new IllegalArgumentException("Transition width must be between "
if (widthBlocks != 0 && (widthBlocks < MINIMUM_WIDTH_BLOCKS || widthBlocks > MAXIMUM_WIDTH_BLOCKS)) {
throw new IllegalArgumentException("Transition width must be zero or between "
+ MINIMUM_WIDTH_BLOCKS + " and " + MAXIMUM_WIDTH_BLOCKS + " blocks.");
}
if ((boundaryIdentity == null) != (terrainSignatureIdentity == null)) {
@@ -4,7 +4,11 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -47,43 +51,69 @@ public final class IrisBoundarySignatureSampler implements GenerationBoundarySig
private static final class LiveCapture implements TerrainBoundarySignatureStore.SignatureSampler {
private final IrisEngine engine;
private final LinkedHashMap<Long, SavedTerrainChunk> chunks = new LinkedHashMap<>(32, 0.75F, true);
private final LinkedHashMap<Long, CompletableFuture<SavedTerrainChunk>> chunks = new LinkedHashMap<>(32, 0.75F, true);
private LiveCapture(IrisEngine engine) {
this.engine = engine;
}
@Override
public void prepareChunks(List<GenerationBoundary.ChunkCoordinate> coordinates) {
if (coordinates.size() > MAXIMUM_CACHED_CHUNKS) {
throw new IllegalArgumentException("Terrain capture batches cannot exceed 32 chunks.");
}
try {
for (GenerationBoundary.ChunkCoordinate coordinate : coordinates) {
captureChunk(coordinate.chunkX(), coordinate.chunkZ());
}
} catch (RuntimeException | Error failure) {
cancelPendingCaptures();
throw failure;
}
}
@Override
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
SavedTerrainChunk terrain = chunks.get(key);
if (terrain == null) {
terrain = captureChunk(chunkX, chunkZ);
chunks.put(key, terrain);
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
Map.Entry<Long, SavedTerrainChunk> oldest = chunks.firstEntry();
chunks.remove(oldest.getKey());
}
}
return terrain.column(blockX, blockZ);
}
private SavedTerrainChunk captureChunk(int chunkX, int chunkZ) throws IOException {
try {
return engine.getPlatformHooks().captureSavedTerrainChunk(engine, chunkX, chunkZ)
.get(30, TimeUnit.SECONDS);
return captureChunk(chunkX, chunkZ).get(30, TimeUnit.SECONDS).column(blockX, blockZ);
} catch (InterruptedException failure) {
cancelPendingCaptures();
Thread.currentThread().interrupt();
throw new IOException("Interrupted while capturing terrain at " + chunkX + "," + chunkZ, failure);
} catch (ExecutionException | TimeoutException failure) {
} catch (ExecutionException | TimeoutException | CancellationException failure) {
cancelPendingCaptures();
throw new IOException("Unable to capture terrain at " + chunkX + "," + chunkZ, failure);
}
}
private CompletableFuture<SavedTerrainChunk> captureChunk(int chunkX, int chunkZ) {
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
CompletableFuture<SavedTerrainChunk> terrain = chunks.get(key);
if (terrain != null) {
return terrain;
}
terrain = Objects.requireNonNull(engine.getPlatformHooks().captureSavedTerrainChunk(engine, chunkX, chunkZ),
"Terrain capture future");
chunks.put(key, terrain);
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
Map.Entry<Long, CompletableFuture<SavedTerrainChunk>> oldest = chunks.firstEntry();
chunks.remove(oldest.getKey());
oldest.getValue().cancel(false);
}
return terrain;
}
private void cancelPendingCaptures() {
for (CompletableFuture<SavedTerrainChunk> capture : chunks.values()) {
capture.cancel(false);
}
}
@Override
public void close() throws IOException {
cancelPendingCaptures();
try {
checkpoint(engine);
} finally {
@@ -66,6 +66,7 @@ public final class TerrainBoundarySignatureStore {
private static final int SHARD_FIXED_BODY_BYTES = 12;
private static final int CHECKSUM_BYTES = Integer.BYTES;
private static final int CELL_SIZE = 32;
private static final int PREPARED_CELL_WINDOW = 8;
private static final int SUPER_CELL_SIZE = 32;
private static final int MAXIMUM_CACHED_SHARDS = 64;
private static final int MAX_CELL_ENTRIES = CELL_SIZE * CELL_SIZE;
@@ -148,24 +149,34 @@ public final class TerrainBoundarySignatureStore {
LongArrays.quickSort(cellKeys, TerrainBoundarySignatureStore::compareCoordinates);
Long2ObjectOpenHashMap<CellReference> cells = new Long2ObjectOpenHashMap<>(cellKeys.length);
int count = 0;
for (long cellKey : cellKeys) {
BitSet mask = columns.remove(cellKey);
List<TerrainBoundarySignature> signatures = new ArrayList<>(mask.cardinality());
for (int index = mask.nextSetBit(0); index >= 0; index = mask.nextSetBit(index + 1)) {
int blockX = Math.addExact(Math.multiplyExact(cellX(cellKey), CELL_SIZE), index / CELL_SIZE);
int blockZ = Math.addExact(Math.multiplyExact(cellZ(cellKey), CELL_SIZE), index % CELL_SIZE);
TerrainBoundarySignature signature = Objects.requireNonNull(
sampler.sample(blockX, blockZ), "boundary signature"
);
if (signature.blockX() != blockX || signature.blockZ() != blockZ) {
throw new IOException("Boundary signature coordinates changed while sampling "
+ blockX + "," + blockZ + ".");
for (int offset = 0; offset < cellKeys.length; offset += PREPARED_CELL_WINDOW) {
int end = Math.min(cellKeys.length, offset + PREPARED_CELL_WINDOW);
List<GenerationBoundary.ChunkCoordinate> chunks = new ArrayList<>((end - offset) * 4);
for (int index = offset; index < end; index++) {
long cellKey = cellKeys[index];
addSampledChunks(cellKey, columns.get(cellKey), chunks);
}
sampler.prepareChunks(List.copyOf(chunks));
for (int cellIndex = offset; cellIndex < end; cellIndex++) {
long cellKey = cellKeys[cellIndex];
BitSet mask = columns.remove(cellKey);
List<TerrainBoundarySignature> signatures = new ArrayList<>(mask.cardinality());
for (int index = mask.nextSetBit(0); index >= 0; index = mask.nextSetBit(index + 1)) {
int blockX = Math.addExact(Math.multiplyExact(cellX(cellKey), CELL_SIZE), index / CELL_SIZE);
int blockZ = Math.addExact(Math.multiplyExact(cellZ(cellKey), CELL_SIZE), index % CELL_SIZE);
TerrainBoundarySignature signature = Objects.requireNonNull(
sampler.sample(blockX, blockZ), "boundary signature"
);
if (signature.blockX() != blockX || signature.blockZ() != blockZ) {
throw new IOException("Boundary signature coordinates changed while sampling "
+ blockX + "," + blockZ + ".");
}
signatures.add(canonicalizeSignature(signature));
}
signatures.add(canonicalizeSignature(signature));
String hash = CellShard.publish(directory, signatures, cellX(cellKey), cellZ(cellKey));
cells.put(cellKey, new CellReference(hash, signatures.size()));
count = Math.addExact(count, signatures.size());
}
String hash = CellShard.publish(directory, signatures, cellX(cellKey), cellZ(cellKey));
cells.put(cellKey, new CellReference(hash, signatures.size()));
count = Math.addExact(count, signatures.size());
}
byte[] identity = computeIdentity(activationId, count, cellKeys, cells);
return publishCatalog(activationId, new PublishedCatalog(
@@ -174,6 +185,27 @@ public final class TerrainBoundarySignatureStore {
}
}
private static void addSampledChunks(long cellKey, BitSet columns,
List<GenerationBoundary.ChunkCoordinate> chunks) {
int minimumX = Math.multiplyExact(cellX(cellKey), CELL_SIZE);
int minimumZ = Math.multiplyExact(cellZ(cellKey), CELL_SIZE);
int chunksPerSide = CELL_SIZE / GenerationBoundary.CHUNK_SIZE;
int prepared = 0;
for (int index = columns.nextSetBit(0); index >= 0; index = columns.nextSetBit(index + 1)) {
int localX = index / CELL_SIZE;
int localZ = index % CELL_SIZE;
int chunkIndex = localX / GenerationBoundary.CHUNK_SIZE * chunksPerSide
+ localZ / GenerationBoundary.CHUNK_SIZE;
int chunkBit = 1 << chunkIndex;
if ((prepared & chunkBit) == 0) {
prepared |= chunkBit;
chunks.add(new GenerationBoundary.ChunkCoordinate(
Math.floorDiv(Math.addExact(minimumX, localX), GenerationBoundary.CHUNK_SIZE),
Math.floorDiv(Math.addExact(minimumZ, localZ), GenerationBoundary.CHUNK_SIZE)));
}
}
}
private Snapshot publishCatalog(long activationId, PublishedCatalog catalog) throws IOException {
forceDirectory(directory);
Path target = snapshotPath(activationId);
@@ -678,6 +710,9 @@ public final class TerrainBoundarySignatureStore {
public interface SignatureSampler extends AutoCloseable {
TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException;
default void prepareChunks(List<GenerationBoundary.ChunkCoordinate> chunks) throws IOException {
}
@Override
default void close() throws IOException {
}
@@ -32,11 +32,11 @@ public final class TransitionGenerationPlan {
throw new IllegalArgumentException("Unsupported generation transition algorithm version "
+ specification.algorithmVersion());
}
terrainSampler = new TransitionBoundarySampler(
terrainSampler = specification.widthBlocks() == 0 ? null : new TransitionBoundarySampler(
specification.widthBlocks(),
terrainSignatures
);
terrainBand = new TransitionBandIndex(boundary, terrainSampler);
terrainBand = terrainSampler == null ? null : new TransitionBandIndex(boundary, terrainSampler);
newTerrain = TerrainSample.newTerrain(specification.widthBlocks());
}
@@ -97,11 +97,11 @@ public final class TransitionGenerationPlan {
}
public boolean hasTransitionAtChunk(int chunkX, int chunkZ) {
return terrainBand.contains(chunkX, chunkZ);
return terrainBand != null && terrainBand.contains(chunkX, chunkZ);
}
public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
if (!terrainBand.contains(blockX >> 4, blockZ >> 4)) {
if (!hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return BoundaryGeometryInfluence.none();
}
return terrainSampler.geometryAt(blockX, blockZ);
@@ -111,7 +111,7 @@ public final class TransitionGenerationPlan {
if (boundary.isHistoricalBlock(blockX, blockZ)) {
return TerrainSample.historicalTerrain();
}
return terrainBand.contains(blockX >> 4, blockZ >> 4) ? terrainSampler.sample(blockX, blockZ) : newTerrain;
return hasTransitionAtChunk(blockX >> 4, blockZ >> 4) ? terrainSampler.sample(blockX, blockZ) : newTerrain;
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
@@ -137,15 +137,15 @@ public final class TransitionGenerationPlan {
}
long terrainCatalogProbeCount() {
return terrainSampler.catalogProbeCount();
return terrainSampler == null ? 0L : terrainSampler.catalogProbeCount();
}
long terrainShardLoadCount() {
return terrainSampler.shardLoadCount();
return terrainSampler == null ? 0L : terrainSampler.shardLoadCount();
}
long terrainCandidateBuildCount() {
return terrainSampler.candidateBuildCount();
return terrainSampler == null ? 0L : terrainSampler.candidateBuildCount();
}
public record Specification(
@@ -171,8 +171,8 @@ public final class TransitionGenerationPlan {
if (oldEpochId.equals(newEpochId)) {
throw new IllegalArgumentException("Transition epoch IDs must differ");
}
if (widthBlocks <= 0) {
throw new IllegalArgumentException("Transition width must be positive");
if (widthBlocks < 0) {
throw new IllegalArgumentException("Transition width cannot be negative");
}
if (algorithmVersion < 1) {
throw new IllegalArgumentException("Transition algorithm version must be positive");
@@ -144,7 +144,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.chunkAccessMode = chunkAsyncMethodSelection.mode();
int detectedWorkerPoolThreads = resolveWorkerPoolThreads();
int detectedCpuThreads = Math.max(1, Runtime.getRuntime().availableProcessors());
this.pregenWorkerThreads = pregenWorkerThreadTarget(detectedCpuThreads, Runtime.getRuntime().maxMemory(),
this.pregenWorkerThreads = pregenWorkerThreadTarget(detectedCpuThreads, MantleHeapPressure.memoryBudgetBytes(),
!foliaRuntime && !strictSerial && detectedWorkerPoolThreads > 0);
int workerThreadsForCap = foliaRuntime
? resolveFoliaConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, pregenWorkerThreads)
@@ -166,7 +166,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
int configuredThreads = foliaRuntime
? computeFoliaRecommendedCap(workerThreadsForCap)
: computePaperLikeRecommendedCap(workerThreadsForCap);
configuredThreads = heapLimitedConcurrency(configuredThreads, Runtime.getRuntime().maxMemory());
configuredThreads = heapLimitedConcurrency(configuredThreads, MantleHeapPressure.memoryBudgetBytes());
this.threads = selectConcurrencyCap(configuredThreads, strictSerial);
this.workerPoolThreads = detectedWorkerPoolThreads;
this.runtimeCpuThreads = detectedCpuThreads;
@@ -304,7 +304,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
binding.releaseChunk(world, chunkX, chunkZ);
}
};
return new PregenChunkRetention(tickets, radius, retentionCapacity(Runtime.getRuntime().maxMemory()),
return new PregenChunkRetention(tickets, radius, retentionCapacity(MantleHeapPressure.memoryBudgetBytes()),
minRegionX, minRegionZ, maxRegionX, maxRegionZ);
} catch (Throwable e) {
PregenDiagnostics.probeFailed("pregen chunk retention for world " + world.getName(), e);
@@ -996,7 +996,14 @@ public class AsyncPregenMethod implements PregeneratorMethod {
PregenAdmissionGate.Wait waited;
try {
waited = admission.admit(() -> inFlight.get() >= adaptiveInFlightLimit.get(), this::isCancelled);
while (true) {
waited = admission.admit(() -> inFlight.get() >= adaptiveInFlightLimit.get(), this::isCancelled);
if (waited == null || !MantleHeapPressure.overHighWater()) {
break;
}
admission.release();
backpressure.awaitHeapHeadroom(MantleHeapPressure::overHighWater, this::reclaimHeapPressure);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
@@ -445,7 +445,7 @@ public class IrisPregenerator {
try {
generator.reclaimMemory();
} catch (Throwable e) {
IrisLogging.reportError("Pregen could not reclaim completed server chunks under heap pressure.", e);
IrisLogging.reportError("Pregen could not reclaim completed server chunks under memory pressure.", e);
}
try {
@@ -19,6 +19,7 @@
package art.arcane.iris.world.pregen;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.diagnostics.ContainerMemory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
@@ -31,6 +32,8 @@ import java.util.function.LongSupplier;
public final class MantleHeapPressure {
private static final double HIGH_WATER = 0.92D;
private static final double LOW_WATER = 0.82D;
private static final double CONTAINER_HIGH_WATER = 0.90D;
private static final double CONTAINER_LOW_WATER = 0.80D;
private static final long MAXIMUM_HYSTERESIS_MS = 60_000L;
private static final ObjectName DIAGNOSTIC_COMMAND = diagnosticCommandName();
private static final PanicGcReclaimer PANIC_RECLAIMER = new PanicGcReclaimer(
@@ -42,7 +45,7 @@ public final class MantleHeapPressure {
// An escalation step in the reclaim policy, rate limited by that policy rather than by
// anything the operator did. What is actionable is the wait budget being exceeded.
(double fraction) -> IrisLogging.info(
"Iris heap remained at %.1f%% after normal panic reclaim; invoking the current JVM's diagnostic full GC to keep generation live.",
"Iris memory pressure remained at %.1f%% of its safety budget after normal panic reclaim; invoking the current JVM's diagnostic full GC to keep generation live.",
fraction * 100.0D),
(String context, Throwable failure) -> IrisLogging.reportError(context, failure)));
private static final HeapPressureGate PRESSURE_GATE = new HeapPressureGate(
@@ -51,11 +54,47 @@ public final class MantleHeapPressure {
MAXIMUM_HYSTERESIS_MS,
System::currentTimeMillis,
PANIC_RECLAIMER::resetEpisode);
private static final HeapPressureGate CONTAINER_PRESSURE_GATE = new HeapPressureGate(
CONTAINER_HIGH_WATER,
CONTAINER_LOW_WATER,
Long.MAX_VALUE,
System::currentTimeMillis,
PANIC_RECLAIMER::resetEpisode);
private MantleHeapPressure() {
}
public static double usedFraction() {
return pressureFraction(heapUsedFraction(), ContainerMemory.snapshot().pressureFraction());
}
public static long memoryBudgetBytes() {
return memoryBudgetBytes(Runtime.getRuntime().maxMemory(), ContainerMemory.snapshot().limitBytes());
}
static long memoryBudgetBytes(long heapBytes, long containerBytes) {
return Math.min(heapBytes, containerBytes - containerBytes / 4L);
}
static double pressureFraction(double heapFraction, double containerFraction) {
if (containerFraction >= CONTAINER_HIGH_WATER) {
return Math.max(heapFraction, HIGH_WATER + (containerFraction - CONTAINER_HIGH_WATER));
}
double containerPressure = containerFraction <= 0D ? 0D
: LOW_WATER + (containerFraction - CONTAINER_LOW_WATER)
* (HIGH_WATER - LOW_WATER) / (CONTAINER_HIGH_WATER - CONTAINER_LOW_WATER);
return Math.max(heapFraction, containerPressure);
}
public static String describe() {
ContainerMemory.Snapshot container = ContainerMemory.snapshot();
String heap = Math.round(heapUsedFraction() * 100D) + "% heap";
return container.supported()
? heap + ", " + Math.round(container.pressureFraction() * 100D) + "% container working memory"
: heap;
}
private static double heapUsedFraction() {
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
long max = heap.getMax();
if (max <= 0L) {
@@ -71,7 +110,9 @@ public final class MantleHeapPressure {
}
public static boolean overHighWater() {
return PRESSURE_GATE.update(usedFraction());
boolean heapPressure = PRESSURE_GATE.update(heapUsedFraction());
boolean containerPressure = CONTAINER_PRESSURE_GATE.update(ContainerMemory.snapshot().pressureFraction());
return heapPressure || containerPressure;
}
public static double reclaimUrgency(double fraction) {
@@ -86,7 +127,7 @@ public final class MantleHeapPressure {
public static void requestPanicReclaim() {
double fraction = usedFraction();
PANIC_RECLAIMER.request(fraction, PRESSURE_GATE.update(fraction));
PANIC_RECLAIMER.request(fraction, overHighWater());
}
static void invokeHotSpotDiagnosticGc(MBeanServer server) throws Exception {
@@ -215,16 +215,16 @@ public final class PregenMantleBackpressure {
if (elapsed >= nextWarningElapsed) {
nextWarningElapsed = elapsed + warningInterval;
lastLog = logNow;
IrisLogging.warn("Pregen heap pressure is still blocking new chunk submissions after " + elapsed + "ms at "
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; waiting for headroom. "
IrisLogging.warn("Pregen memory pressure is still blocking new chunk submissions after " + elapsed + "ms at "
+ MantleHeapPressure.describe() + "; waiting for headroom. "
+ diagnostics.get());
onBudgetTimeout.run();
}
if (logNow - lastLog >= LOG_INTERVAL_MS) {
lastLog = logNow;
IrisLogging.info("Pregen heap pressure: pausing generation at "
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; evicting tectonic plates and waiting for headroom"
IrisLogging.info("Pregen memory pressure: pausing generation at "
+ MantleHeapPressure.describe() + "; evicting tectonic plates and waiting for headroom"
+ (mantle != null ? " (" + mantle.getLoadedRegionCount() + " plates resident)" : "") + ".");
}
@@ -289,6 +289,9 @@ public class J {
runnable.run();
return true;
}
if (!canSchedule()) {
return false;
}
if (runRegionImmediate(world, chunkX, chunkZ, runnable)) {
return true;
@@ -383,6 +386,9 @@ public class J {
if (delayTicks <= 0) {
return runRegion(world, chunkX, chunkZ, runnable);
}
if (!canSchedule()) {
return false;
}
if (runRegionDelayed(world, chunkX, chunkZ, runnable, delayTicks)) {
return true;