This commit is contained in:
Brian Neumann-Fopiano
2026-07-09 18:34:16 -04:00
parent 0a7a531646
commit 6111b5670a
65 changed files with 998 additions and 382 deletions
@@ -95,7 +95,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private ResourceLoader<IrisObject> objectLoader;
private ResourceLoader<IrisMatterObject> matterLoader;
private ResourceLoader<IrisImage> imageLoader;
private ResourceLoader<IrisMatterObject> matterObjectLoader;
private ResourceLoader<IrisStructure> structureLoader;
private ResourceLoader<IrisJigsawPool> jigsawPoolLoader;
private ResourceLoader<IrisJigsawPiece> jigsawPieceLoader;
@@ -337,7 +336,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
this.expressionLoader = registerLoader(IrisExpression.class);
this.objectLoader = registerLoader(IrisObject.class);
this.imageLoader = registerLoader(IrisImage.class);
this.matterObjectLoader = registerLoader(IrisMatterObject.class);
this.matterLoader = registerLoader(IrisMatterObject.class);
this.structureLoader = registerLoader(IrisStructure.class);
this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class);
this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class);
@@ -53,7 +53,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
protected IrisMatterObject loadFile(File j, String name) {
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
IrisMatterObject t = IrisMatterObject.from(j);
IrisMatterObject t = IrisMatterObject.from(j, manager);
t.setLoadKey(name);
t.setLoader(manager);
t.setLoadFile(j);
@@ -204,22 +204,22 @@ public class IrisPregenerator {
ticker.start();
checkRegions();
PrecisionStopwatch p = PrecisionStopwatch.start();
boolean completed = false;
try {
int[] regionBounds = task.regionBounds();
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
task.iterateRegions((x, z) -> visitRegion(x, z, true));
task.iterateRegions((x, z) -> visitRegion(x, z, false));
long failedCount = failed.get();
if (failedCount > 0) {
IrisLogging.warn("Pregen finished with " + Form.f(failedCount) + " failed chunk(s); failures are not cached, rerun to fill them");
}
IrisLogging.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
completed = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Pregen aborted after " + Form.duration((long) p.getMilliseconds()) + " due to " + e.getClass().getSimpleName() + ": " + e.getMessage());
} finally {
shutdown();
}
if (completed) {
logSuccessfulCompletion(p);
}
if (benchmarking == null) {
IrisLogging.info(C.IRIS + "Pregen stopped.");
} else {
@@ -227,6 +227,17 @@ public class IrisPregenerator {
}
}
private void logSuccessfulCompletion(PrecisionStopwatch stopwatch) {
long failedCount = failed.get();
if (failedCount > 0) {
IrisLogging.warn("Pregen finished with " + Form.f(failedCount) + " failed chunk(s); failures are not cached, rerun to fill them");
}
IrisLogging.info("Pregen finished: generated=" + Form.f(generated.get())
+ " total=" + Form.f(totalChunks.get())
+ " failed=" + Form.f(failedCount)
+ " duration=" + Form.duration((long) stopwatch.getMilliseconds()));
}
private void checkRegions() {
task.iterateRegions(this::checkRegion);
}
@@ -55,6 +55,10 @@ public class PregenTask {
private final Bounds bounds = new Bounds();
protected PregenTask(boolean gui, Position2 center, int radiusX, int radiusZ) {
if (radiusX <= 0 || radiusZ <= 0) {
throw new IllegalArgumentException("Pregen radii must be greater than zero blocks.");
}
this.gui = gui;
this.center = new ProxiedPos(center);
this.radiusX = radiusX;
@@ -35,6 +35,18 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
}
}
private AsyncOrMedievalPregenMethod(PregeneratorMethod method) {
this.method = method;
}
public static AsyncOrMedievalPregenMethod strictSerial(World world) {
if (!PaperLib.isPaper()) {
throw new UnsupportedOperationException("Strict serial pregeneration requires Paper or a Paper-compatible server.");
}
return new AsyncOrMedievalPregenMethod(AsyncPregenMethod.strictSerial(world));
}
@Override
public void init() {
method.init();
@@ -103,6 +103,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final PregenMantleBackpressure backpressure;
public AsyncPregenMethod(World world, int unusedThreads) {
this(world, false);
}
private AsyncPregenMethod(World world, boolean strictSerial) {
if (!PaperLib.isPaper()) {
throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!");
}
@@ -138,7 +142,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
int configuredThreads = foliaRuntime
? computeFoliaRecommendedCap(workerThreadsForCap)
: computePaperLikeRecommendedCap(workerThreadsForCap);
this.threads = Math.max(1, configuredThreads);
this.threads = selectConcurrencyCap(configuredThreads, strictSerial);
this.workerPoolThreads = detectedWorkerPoolThreads;
this.runtimeCpuThreads = detectedCpuThreads;
this.effectiveWorkerThreads = workerThreadsForCap;
@@ -168,6 +172,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this::metricsSnapshot);
}
public static AsyncPregenMethod strictSerial(World world) {
return new AsyncPregenMethod(world, true);
}
private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) {
IrisPaperLikeBackendMode configuredMode = pregen.getPaperLikeBackendMode();
if (configuredMode != IrisPaperLikeBackendMode.AUTO) {
@@ -540,6 +548,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return recommendedCap;
}
static int selectConcurrencyCap(int recommendedCap, boolean strictSerial) {
return strictSerial ? 1 : Math.max(1, recommendedCap);
}
static int resolvePaperLikeConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) {
int provisionedWorkerThreads = Math.max(1, configuredWorldGenThreads);
if (detectedWorkerPoolThreads > 0) {
@@ -28,8 +28,16 @@ public class HybridPregenMethod implements PregeneratorMethod {
private final World world;
public HybridPregenMethod(World world, int threads) {
this(world, new AsyncOrMedievalPregenMethod(world, threads));
}
private HybridPregenMethod(World world, PregeneratorMethod inWorld) {
this.world = world;
inWorld = new AsyncOrMedievalPregenMethod(world, threads);
this.inWorld = inWorld;
}
public static HybridPregenMethod strictSerial(World world) {
return new HybridPregenMethod(world, AsyncOrMedievalPregenMethod.strictSerial(world));
}
@Override
@@ -46,6 +46,8 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class GoldenHashEngine {
public static final String FALLBACK_BIOME_KEY = "minecraft:plains";
public enum Mode {
AUTO,
CAPTURE,
@@ -53,7 +53,7 @@ public final class GoldenHashScanner {
GoldenHashEngine.Mode.AUTO,
resetMantle,
deep,
"null");
GoldenHashEngine.FALLBACK_BIOME_KEY);
this.hashEngine = new GoldenHashEngine(engine, request, IrisPlatforms.get().dataFolder("golden"), this::snapshot, feedback(), progress());
}
@@ -22,7 +22,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.data.KCache;
import art.arcane.volmlib.util.format.Form;
import art.arcane.iris.util.common.plugin.IrisService;
@@ -52,7 +51,7 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
}
public void printCaches() {
var c = getCaches();
List<MeteredCache> c = getCaches();
long s = 0;
long m = 0;
double p = 0;
@@ -68,7 +67,6 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
}
public void dereference() {
IrisContext.dereference();
IrisData.dereference();
threads.removeIf((i) -> !i.isAlive());
services.removeIf(ExecutorService::isShutdown);
@@ -122,7 +120,7 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
public void updateCaches() {
caches.removeIf(ref -> {
var c = ref.get();
MeteredCache c = ref.get();
return c == null || c.isClosed();
});
}
@@ -9,7 +9,6 @@ import java.util.ArrayList;
import java.util.List;
public final class IrisSplashComposer {
public static final String RELEASE_TAG = "RC.1.1.6";
private static final String SPLASH_PADDING = " ".repeat(4);
private IrisSplashComposer() {
@@ -20,7 +19,7 @@ public final class IrisSplashComposer {
String prefix = style.linePrefix();
return new String[]{
"",
prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + " " + RELEASE_TAG + "]"),
prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + "]"),
prefix + style.label(" Version: ") + style.value(version),
prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"),
prefix + style.label(" Server: ") + style.value(serverLine),
@@ -264,6 +264,10 @@ public class IrisToolbelt {
PregenPerformanceProfile.apply(engine);
}
public static boolean supportsStrictSerialPregeneration() {
return PaperLib.isPaper();
}
/**
* Start a pregenerator task. If the supplied generator is headless, headless mode is used,
* otherwise Hybrid mode is used.
@@ -273,8 +277,16 @@ public class IrisToolbelt {
* @return the pregenerator job (already started)
*/
public static PregeneratorJob pregenerate(PregenTask task, PlatformChunkGenerator gen) {
return pregenerate(task, new HybridPregenMethod(gen.getEngine().getWorld().realWorld(),
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), gen.getEngine());
World world = gen.getEngine().getWorld().realWorld();
int threads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism());
PregeneratorMethod method = new HybridPregenMethod(world, threads);
return pregenerate(task, method, gen.getEngine());
}
public static PregeneratorJob pregenerateSerial(PregenTask task, PlatformChunkGenerator gen) {
World world = gen.getEngine().getWorld().realWorld();
PregeneratorMethod method = HybridPregenMethod.strictSerial(world);
return pregenerate(task, method, gen.getEngine());
}
/**
@@ -290,7 +302,18 @@ public class IrisToolbelt {
return pregenerate(task, access(world));
}
return pregenerate(task, new HybridPregenMethod(world, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), null);
int threads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism());
PregeneratorMethod method = new HybridPregenMethod(world, threads);
return pregenerate(task, method, null);
}
public static PregeneratorJob pregenerateSerial(PregenTask task, World world) {
if (isIrisWorld(world)) {
return pregenerateSerial(task, access(world));
}
PregeneratorMethod method = HybridPregenMethod.strictSerial(world);
return pregenerate(task, method, null);
}
/**
@@ -35,7 +35,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
@@ -164,7 +163,7 @@ public class IrisComplex implements DataProvider {
.cache2D("regionStream", engine, cacheSize).waste("Region Stream");
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
String.valueOf(i * 38445).hashCode() * 3245556666L)).waste("Region ID Stream");
caveBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -173,7 +172,7 @@ public class IrisComplex implements DataProvider {
.onNull(emptyBiome)
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
landBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -183,7 +182,7 @@ public class IrisComplex implements DataProvider {
).convertAware2D(ProceduralStream::get)
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
inferredStreams.put(InferredType.LAND, landBiomeStream);
seaBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -193,7 +192,7 @@ public class IrisComplex implements DataProvider {
).convertAware2D(ProceduralStream::get)
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
inferredStreams.put(InferredType.SEA, seaBiomeStream);
shoreBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -216,37 +215,37 @@ public class IrisComplex implements DataProvider {
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize).waste("Height Stream");
roundedHeighteightStream = heightStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getHeight().getDouble(x, z))
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round().waste("Rounded Height Stream");
slopeStream = heightStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getHeight().getDouble(x, z))
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3).cache2DDouble("slopeStream", engine, cacheSize).waste("Slope Stream");
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
.convertAware2D((h, x, z) ->
fixBiomeType(h, baseBiomeStream.get(x, z),
regionStream.contextInjecting((c, xx, zz) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
.cache2D("trueBiomeStream", engine, cacheSize).waste("True Biome Stream");
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize).waste("True Biome Derivative Stream");
heightFluidStream = heightStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getHeight().getDouble(x, z))
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize).waste("Height Fluid Stream");
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height).waste("Max Height Stream");
terrainSurfaceDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize).waste("Surface Decoration Stream");
terrainCeilingDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
terrainCeilingDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCeilingDecoration", engine, cacheSize).waste("Ceiling Decoration Stream");
terrainCaveSurfaceDecoration = caveBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getCave().get(x, z))
terrainCaveSurfaceDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainCaveSurfaceDecoration", engine, cacheSize).waste("Cave Surface Stream");
terrainCaveCeilingDecoration = caveBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getCave().get(x, z))
terrainCaveCeilingDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCaveCeilingDecoration", engine, cacheSize).waste("Cave Ceiling Stream");
shoreSurfaceDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
shoreSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SHORE_LINE)).cache2D("shoreSurfaceDecoration", engine, cacheSize).waste("Shore Surface Stream");
seaSurfaceDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
seaSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_SURFACE)).cache2D("seaSurfaceDecoration", engine, cacheSize).waste("Sea Surface Stream");
seaFloorDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
seaFloorDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_FLOOR)).cache2D("seaFloorDecoration", engine, cacheSize).waste("Sea Floor Stream");
baseBiomeIDStream = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
baseBiomeIDStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, x, z) -> {
UUID d = regionIDStream.get(x, z);
return new UUID(b.getLoadKey().hashCode() * 818223L,
@@ -81,8 +81,6 @@ import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
@@ -94,8 +92,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Data
@EqualsAndHashCode(exclude = "context")
@ToString(exclude = "context")
public class IrisEngine implements Engine {
private final AtomicInteger bud;
private final AtomicInteger buds;
@@ -104,7 +100,6 @@ public class IrisEngine implements Engine {
private final AtomicDouble perSecond;
private final AtomicLong lastGPS;
private final EngineTarget target;
private final IrisContext context;
private final EngineMantle mantle;
private final ChronoLatch perSecondLatch;
private final ChronoLatch perSecondBudLatch;
@@ -156,7 +151,6 @@ public class IrisEngine implements Engine {
long _t0 = M.ms();
mantle = new IrisEngineMantle(this);
IrisLogging.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms");
context = new IrisContext(this);
cleaning = new AtomicBoolean(false);
modeFallbackLogged = new AtomicBoolean(false);
prefetchSaveStarted = new AtomicBoolean(false);
@@ -167,7 +161,6 @@ public class IrisEngine implements Engine {
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
IrisLogging.debug("[IrisEngine timing] dump+clearLists+reload=" + (M.ms() - _t0) + "ms");
}
context.touch();
getData().setEngine(this);
_t0 = M.ms();
getData().loadPrefetch(this);
@@ -717,9 +710,8 @@ public class IrisEngine implements Engine {
throw new GenerationSessionException("Generation session is closed for world \"" + getWorld().name() + "\".", true);
}
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate")) {
context.touch();
context.setGenerationSessionId(lease.sessionId());
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk();
PrecisionStopwatch p = PrecisionStopwatch.start();
Hunk<PlatformBlockState> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
@@ -733,7 +725,7 @@ public class IrisEngine implements Engine {
}
} else {
EngineMode activeMode = ensureMode();
activeMode.generate(x, z, blocks, vbiomes, multicore);
activeMode.generate(x, z, blocks, vbiomes, multicore, lease.sessionId());
}
boolean skipRealFlag = J.isFolia() && getWorld().hasRealWorld() && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(getWorld().realWorld());
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.core.nms.container.Pair;
@@ -34,6 +35,7 @@ import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.matter.IrisMatterContext;
import art.arcane.iris.util.project.matter.IrisMatterSupport;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -182,7 +184,7 @@ public class IrisEngineMantle implements EngineMantle {
IrisMatterSupport.ensureRegistered();
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
int worldHeight = engine.getTarget().getHeight();
MantleDataAdapter<Matter> adapter = createRuntimeDataAdapter();
MantleDataAdapter<Matter> adapter = createRuntimeDataAdapter(engine.getData());
MantleHooks hooks = createRuntimeHooks();
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
createRegionIO(dataFolder, worldHeight, adapter, hooks);
@@ -198,15 +200,15 @@ public class IrisEngineMantle implements EngineMantle {
);
}
public static MantleDataAdapter<Matter> createRuntimeDataAdapter() {
return createDataAdapter();
public static MantleDataAdapter<Matter> createRuntimeDataAdapter(IrisData data) {
return createDataAdapter(data);
}
public static MantleHooks createRuntimeHooks() {
return createHooks();
}
private static MantleDataAdapter<Matter> createDataAdapter() {
private static MantleDataAdapter<Matter> createDataAdapter(IrisData data) {
return new MantleDataAdapter<>() {
@Override
public Matter createSection() {
@@ -215,7 +217,9 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException {
return Matter.readDin(din);
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) {
return Matter.readDin(din);
}
}
@Override
@@ -51,7 +51,6 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -111,8 +110,6 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
return isClosed();
}
IrisContext getContext();
double getMaxBiomeObjectDensity();
double getMaxBiomeDecoratorDensity();
@@ -214,7 +211,6 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
Color ibc = biome.getColor(this, RenderType.BIOME);
Color rc = irc != null ? irc : Color.GREEN.darker();
Color bc = ibc != null ? ibc : biome.isAquatic() ? Color.BLUE : Color.YELLOW;
Color f = IrisColor.blend(rc, bc, bc, Color.getHSBColor(0, 0, (float) heightFactor));
return IrisColor.blend(rc, bc, bc, Color.getHSBColor(0, 0, (float) heightFactor));
}
@@ -51,7 +51,11 @@ public interface EngineMode extends Staged {
e.setMulticore(multicore);
for (EngineStage i : stages) {
e.queue(() -> i.generate(x, z, blocks, biomes, multicore, ctx));
e.queue(() -> {
try (IrisContext.Scope stageScope = IrisContext.open(getEngine(), ctx.getGenerationSessionId(), ctx)) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
});
}
e.complete();
@@ -71,19 +75,19 @@ public interface EngineMode extends Staged {
}
@BlockCoordinates
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore) {
IrisContext context = IrisContext.getOr(getEngine());
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
boolean cacheContext = true;
if (J.isFolia() && getEngine().getWorld().hasRealWorld() && shouldDisableContextCacheForMaintenance()) {
cacheContext = false;
}
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
ChunkContext ctx = new ChunkContext(x, z, getComplex(), context.getGenerationSessionId(), cacheContext, prefillPlan, getEngine().getMetrics());
context.setChunkContext(ctx);
ChunkContext ctx = new ChunkContext(x, z, getComplex(), generationSessionId, cacheContext, prefillPlan, getEngine().getMetrics());
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
}
}
@@ -80,8 +80,10 @@ public interface Locator<T> {
return (e, c) -> {
AtomicBoolean found = new AtomicBoolean(false);
try (GenerationSessionLease lease = e.acquireGenerationLease("locator_generate_matter")) {
IrisContext.getOr(e).setGenerationSessionId(lease.sessionId());
e.generateMatter(c.getX(), c.getZ(), true, new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null));
ChunkContext chunkContext = new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null);
try (IrisContext.Scope locatorScope = IrisContext.open(e, lease.sessionId(), chunkContext)) {
e.generateMatter(c.getX(), c.getZ(), true, chunkContext);
}
} catch (GenerationSessionException sessionException) {
throw new IllegalStateException(sessionException);
}
@@ -31,7 +31,7 @@ public class HeightmapObjectPlacer implements IObjectPlacer {
private final IrisObjectPlacement config;
private final IObjectPlacer oplacer;
public HeightmapObjectPlacer(Engine engine, RNG rng, int x, int yv, int z, IrisObjectPlacement config, IObjectPlacer oplacer) {
public HeightmapObjectPlacer(RNG rng, int x, int yv, int z, IrisObjectPlacement config, IObjectPlacer oplacer) {
s = rng.nextLong() + yv + z - x;
this.config = config;
this.oplacer = oplacer;
@@ -94,6 +94,6 @@ public class HeightmapObjectPlacer implements IObjectPlacer {
@Override
public Engine getEngine() {
return null;
return oplacer.getEngine();
}
}
@@ -33,7 +33,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class IrisPerfectionModifier extends EngineAssignedModifier<PlatformBlockState> {
private static final class States {
@@ -88,17 +87,14 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<PlatformBlock
hideOres(output, multicore);
}
AtomicBoolean changed = new AtomicBoolean(true);
int passes = 0;
AtomicInteger changes = new AtomicInteger();
List<Integer> surfaces = new ArrayList<>();
List<Integer> ceilings = new ArrayList<>();
BurstExecutor burst = burst().burst(multicore);
while (changed.get()) {
passes++;
changed.set(false);
for (int i = 0; i < 16; i++) {
int finalI = i;
burst.queue(() -> {
List<Integer> surfaces = new ArrayList<>();
List<Integer> ceilings = new ArrayList<>();
for (int j = 0; j < 16; j++) {
surfaces.clear();
ceilings.clear();
@@ -148,11 +144,9 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<PlatformBlock
if (remove) {
changed.set(true);
changes.getAndIncrement();
output.set(finalI, k, j, States.AIR);
if (remove2) {
changes.getAndIncrement();
output.set(finalI, k - 1, j, States.AIR);
}
}
@@ -161,6 +155,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<PlatformBlock
}
});
}
burst.complete();
}
getEngine().getMetrics().getPerfection().put(p.getMilliseconds());
@@ -30,8 +30,6 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.spi.PlatformBlockState;
import java.util.concurrent.atomic.AtomicInteger;
public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState> {
private static final class States {
private static final PlatformBlockState AIR = B.getState("AIR");
@@ -48,14 +46,12 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
@Override
public void onModify(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
PrecisionStopwatch p = PrecisionStopwatch.start();
AtomicInteger i = new AtomicInteger();
AtomicInteger j = new AtomicInteger();
Hunk<PlatformBlockState> sync = output.synchronize();
for (i.set(0); i.get() < output.getWidth(); i.getAndIncrement()) {
for (j.set(0); j.get() < output.getDepth(); j.getAndIncrement()) {
int ii = i.get();
int jj = j.get();
post(ii, jj, sync, ii + x, jj + z, context);
int width = output.getWidth();
int depth = output.getDepth();
for (int i = 0; i < width; i++) {
for (int j = 0; j < depth; j++) {
post(i, j, sync, i + x, j + z, context);
}
}
@@ -29,7 +29,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.volmlib.util.format.Form;
@@ -676,7 +675,7 @@ public class IrisObject extends IrisRegistrant {
}
public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, PlatformBlockState> listener, CarveResult c, IrisData rdata) {
IObjectPlacer placer = (config.getHeightmap() != null) ? new HeightmapObjectPlacer(oplacer.getEngine() == null ? IrisContext.get().getEngine() : oplacer.getEngine(), rng, x, yv, z, config, oplacer) : oplacer;
IObjectPlacer placer = config.getHeightmap() != null ? new HeightmapObjectPlacer(rng, x, yv, z, config, oplacer) : oplacer;
if (rdata != null) {
// Slope condition
@@ -1,7 +1,9 @@
package art.arcane.iris.engine.object.matter;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.util.project.matter.IrisMatterContext;
import art.arcane.iris.util.project.matter.IrisMatterSupport;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.matter.IrisMatter;
@@ -35,9 +37,11 @@ public class IrisMatterObject extends IrisRegistrant {
return new IrisMatterObject(IrisMatterSupport.from(object));
}
public static IrisMatterObject from(File j) throws IOException, ClassNotFoundException {
public static IrisMatterObject from(File j, IrisData data) throws IOException {
IrisMatterSupport.ensureRegistered();
return new IrisMatterObject(Matter.read(j));
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) {
return new IrisMatterObject(Matter.read(j));
}
}
private static Matter createMatter(int w, int h, int d) {
@@ -381,12 +381,20 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
public void withExclusiveControl(Runnable r) {
J.a(() -> {
boolean acquired = false;
try {
loadLock.acquire(LOAD_LOCKS);
acquired = true;
r.run();
loadLock.release(LOAD_LOCKS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
if (acquired) {
loadLock.release(LOAD_LOCKS);
}
}
});
}
@@ -394,14 +402,21 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
public CompletableFuture<Void> withExclusiveControlFuture(Runnable r) {
CompletableFuture<Void> future = new CompletableFuture<>();
J.a(() -> {
boolean acquired = false;
try {
loadLock.acquire(LOAD_LOCKS);
acquired = true;
r.run();
future.complete(null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
future.completeExceptionally(e);
} catch (Throwable e) {
future.completeExceptionally(e);
} finally {
loadLock.release(LOAD_LOCKS);
if (acquired) {
loadLock.release(LOAD_LOCKS);
}
}
});
return future;
@@ -121,6 +121,10 @@ public class ChunkContext {
return complex;
}
public long getGenerationSessionId() {
return generationSessionId;
}
public ChunkedDoubleDataCache getHeight() {
return height;
}
@@ -20,70 +20,55 @@ package art.arcane.iris.util.project.context;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import lombok.Data;
@Data
public class IrisContext {
private static final KMap<Thread, IrisContext> context = new KMap<>();
private static final ChronoLatch cl = new ChronoLatch(60000);
import java.util.Objects;
public final class IrisContext {
private static final ThreadLocal<IrisContext> CONTEXT = new ThreadLocal<>();
private final Engine engine;
private ChunkContext chunkContext;
private long generationSessionId;
private final ChunkContext chunkContext;
private final long generationSessionId;
public IrisContext(Engine engine) {
this.engine = engine;
}
public static IrisContext getOr(Engine engine) {
IrisContext c = get();
if (c == null) {
c = new IrisContext(engine);
touch(c);
}
return c;
private IrisContext(Engine engine, long generationSessionId, ChunkContext chunkContext) {
this.engine = Objects.requireNonNull(engine);
this.generationSessionId = generationSessionId;
this.chunkContext = chunkContext;
}
public static IrisContext get() {
return context.get(Thread.currentThread());
return CONTEXT.get();
}
public static void touch(IrisContext c) {
context.put(Thread.currentThread(), c);
if (!cl.couldFlip()) return;
synchronized (cl) {
if (cl.flip()) {
dereference();
}
public static IrisContext require() {
IrisContext current = get();
if (current == null) {
throw new IllegalStateException("No Iris execution context is bound to thread " + Thread.currentThread().getName() + ".");
}
return current;
}
public static synchronized void dereference() {
var it = context.entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
var thread = entry.getKey();
var context = entry.getValue();
if (thread == null || context == null) {
it.remove();
continue;
}
if (!thread.isAlive() || context.engine.isClosed()) {
IrisLogging.debug("Dereferenced Context<Engine> " + thread.getName() + " " + thread.threadId());
it.remove();
}
}
public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) {
Thread thread = Thread.currentThread();
IrisContext previous = CONTEXT.get();
IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext);
CONTEXT.set(installed);
return new Scope(thread, previous, installed);
}
public void touch() {
IrisContext.touch(this);
public Engine getEngine() {
return engine;
}
public ChunkContext getChunkContext() {
return chunkContext;
}
public long getGenerationSessionId() {
return generationSessionId;
}
public IrisData getData() {
@@ -95,9 +80,9 @@ public class IrisContext {
}
public KMap<String, Object> asContext() {
var hash32 = engine.getHash32().getNow(null);
var dimension = engine.getDimension();
var mantle = engine.getMantle();
Long hash32 = engine.getHash32().getNow(null);
IrisDimension dimension = engine.getDimension();
EngineMantle mantle = engine.getMantle();
return new KMap<String, Object>()
.qput("studio", engine.isStudio())
.qput("closed", engine.isClosed())
@@ -111,4 +96,37 @@ public class IrisContext {
.qput("loaded", mantle.getLoadedRegionCount())
.qput("queued", mantle.getUnloadRegionCount()));
}
public static final class Scope implements AutoCloseable {
private final Thread owner;
private final IrisContext previous;
private final IrisContext installed;
private boolean closed;
private Scope(Thread owner, IrisContext previous, IrisContext installed) {
this.owner = owner;
this.previous = previous;
this.installed = installed;
}
@Override
public void close() {
if (closed) {
return;
}
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris execution context scope closed from a different thread.");
}
IrisContext current = CONTEXT.get();
if (current != installed) {
throw new IllegalStateException("Iris execution context scopes must close in LIFO order.");
}
if (previous == null) {
CONTEXT.remove();
} else {
CONTEXT.set(previous);
}
closed = true;
}
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.util.project.matter;
import art.arcane.iris.core.loader.IrisData;
import java.util.Objects;
public final class IrisMatterContext {
private static final ThreadLocal<IrisData> DATA = new ThreadLocal<>();
private IrisMatterContext() {
}
public static Scope open(IrisData data) {
Thread owner = Thread.currentThread();
IrisData previous = DATA.get();
IrisData installed = Objects.requireNonNull(data);
DATA.set(installed);
return new Scope(owner, previous, installed);
}
public static IrisData require() {
IrisData data = DATA.get();
if (data == null) {
throw new IllegalStateException("No Iris matter data context is bound to thread " + Thread.currentThread().getName() + ".");
}
return data;
}
public static final class Scope implements AutoCloseable {
private final Thread owner;
private final IrisData previous;
private final IrisData installed;
private boolean closed;
private Scope(Thread owner, IrisData previous, IrisData installed) {
this.owner = owner;
this.previous = previous;
this.installed = installed;
}
@Override
public void close() {
if (closed) {
return;
}
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris matter data scope closed from a different thread.");
}
if (DATA.get() != installed) {
throw new IllegalStateException("Iris matter data scopes must close in LIFO order.");
}
if (previous == null) {
DATA.remove();
} else {
DATA.set(previous);
}
closed = true;
}
}
}
@@ -18,8 +18,10 @@
package art.arcane.iris.util.project.matter.slices;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.util.project.matter.IrisMatterContext;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.slices.RawMatter;
@@ -44,7 +46,12 @@ public class RegistryMatter<T extends IrisRegistrant> extends RawMatter<T> {
@Override
public T readNode(DataInputStream din) throws IOException {
IrisContext context = IrisContext.get();
return (T) context.getData().getLoaders().get(getType()).load(din.readUTF());
String key = din.readUTF();
IrisData data = IrisMatterContext.require();
ResourceLoader<? extends IrisRegistrant> loader = data.getLoaders().get(getType());
if (loader == null) {
throw new IOException("No Iris registry loader is available for matter type " + getType().getName() + " while reading key " + key + ".");
}
return (T) loader.load(key);
}
}
@@ -142,6 +142,10 @@ public interface ProceduralStream<T> extends ProceduralLayer, Interpolated<T> {
return new ContextInjectingStream<>(this, contextAccessor);
}
default ProceduralStream<T> contextInjecting(Engine engine, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
return new ContextInjectingStream<>(this, engine, contextAccessor);
}
default ProceduralStream<T> add(ProceduralStream<Double> a) {
return add2D((x, z) -> a.get(x, z));
}
@@ -2,14 +2,21 @@ package art.arcane.iris.util.project.stream.utility;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.function.Function3;
import art.arcane.iris.util.project.stream.BasicStream;
import art.arcane.iris.util.project.stream.ProceduralStream;
public class ContextInjectingStream<T> extends BasicStream<T> {
private final Engine engine;
private final Function3<ChunkContext, Integer, Integer, T> contextAccessor;
public ContextInjectingStream(ProceduralStream<T> stream, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
this(stream, null, contextAccessor);
}
public ContextInjectingStream(ProceduralStream<T> stream, Engine engine, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
super(stream);
this.engine = engine;
this.contextAccessor = contextAccessor;
}
@@ -19,9 +26,16 @@ public class ContextInjectingStream<T> extends BasicStream<T> {
if (context != null) {
ChunkContext chunkContext = context.getChunkContext();
int blockX = (int) x;
int blockZ = (int) z;
if (chunkContext != null && (int) x >> 4 == chunkContext.getX() >> 4 && (int) z >> 4 == chunkContext.getZ() >> 4) {
T t = contextAccessor.apply(chunkContext, (int) x & 15, (int) z & 15);
if (chunkContext != null
&& (engine == null || context.getEngine() == engine)
&& context.getGenerationSessionId() == chunkContext.getGenerationSessionId()
&& (engine == null || context.getGenerationSessionId() == engine.getGenerationSessionId())
&& blockX >> 4 == chunkContext.getX() >> 4
&& blockZ >> 4 == chunkContext.getZ() >> 4) {
T t = contextAccessor.apply(chunkContext, blockX & 15, blockZ & 15);
if (t != null) {
return t;
@@ -5,10 +5,14 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisPregeneratorInitTest {
@Test
@@ -35,6 +39,44 @@ public class IrisPregeneratorInitTest {
}
}
@Test
public void completionSummaryWaitsForAsyncCloseDrain() {
String output = runCompletionOnClose(false);
assertTrue(output.contains("Pregen finished: generated=9 total=9 failed=0"));
}
@Test
public void completionSummaryIncludesFailureDrainedOnClose() {
String output = runCompletionOnClose(true);
assertTrue(output.contains("Pregen finished: generated=8 total=9 failed=1"));
}
private String runCompletionOnClose(boolean failLast) {
IrisSettings previousSettings = IrisSettings.settings;
PrintStream previousOut = System.out;
IrisSettings.settings = new IrisSettings();
ByteArrayOutputStream output = new ByteArrayOutputStream();
System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8));
CompletionOnCloseMethod method = new CompletionOnCloseMethod(failLast);
PregenTask task = PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(1)
.radiusZ(1)
.build();
try {
IrisPregenerator pregenerator = new IrisPregenerator(task, method, new NoOpPregenListener());
pregenerator.start();
return output.toString(StandardCharsets.UTF_8);
} finally {
System.setOut(previousOut);
IrisSettings.settings = previousSettings;
}
}
private static final class TrackingPregeneratorMethod implements PregeneratorMethod {
private final AtomicInteger initCalls = new AtomicInteger();
private final AtomicInteger saveCalls = new AtomicInteger();
@@ -77,6 +119,60 @@ public class IrisPregeneratorInitTest {
}
}
private static final class CompletionOnCloseMethod implements PregeneratorMethod {
private final boolean failLast;
private int pending;
private PregenListener listener;
private CompletionOnCloseMethod(boolean failLast) {
this.failLast = failLast;
}
@Override
public void init() {
}
@Override
public void close() {
for (int index = 0; index < pending; index++) {
if (failLast && index == pending - 1) {
listener.onChunkFailed(0, 0);
} else {
listener.onChunkGenerated(0, 0);
}
}
}
@Override
public void save() {
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public String getMethod(int x, int z) {
return "test";
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
pending++;
this.listener = listener;
}
@Override
public Mantle getMantle() {
return null;
}
}
private static final class NoOpPregenListener implements PregenListener {
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
@@ -8,6 +8,7 @@ import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class PregenTaskInterleavedTraversalTest {
@Test
@@ -38,6 +39,35 @@ public class PregenTaskInterleavedTraversalTest {
assertEquals(asSet(baseline), asSet(firstInterleaved));
}
@Test
public void blockRadius352CoversExactly2025Chunks() {
PregenTask task = PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(352)
.radiusZ(352)
.build();
KList<Long> chunks = new KList<>();
task.iterateAllChunks((x, z) -> chunks.add(asKey(x, z)));
assertEquals(2025, chunks.size());
assertEquals(2025, asSet(chunks).size());
}
@Test
public void nonpositiveRadiusIsRejected() {
assertThrows(IllegalArgumentException.class, () -> PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(0)
.radiusZ(352)
.build());
assertThrows(IllegalArgumentException.class, () -> PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(352)
.radiusZ(-1)
.build());
}
private Set<Long> asSet(KList<Long> values) {
Set<Long> set = new HashSet<>();
for (Long value : values) {
@@ -35,4 +35,11 @@ public class AsyncPregenMethodConcurrencyCapTest {
assertEquals(32, AsyncPregenMethod.resolveFoliaConcurrencyWorkerThreads(4, 16, 32));
assertEquals(16, AsyncPregenMethod.resolveFoliaConcurrencyWorkerThreads(-1, 16, 12));
}
@Test
public void strictSerialOverridesRecommendedConcurrencyCap() {
assertEquals(1, AsyncPregenMethod.selectConcurrencyCap(128, true));
assertEquals(128, AsyncPregenMethod.selectConcurrencyCap(128, false));
assertEquals(1, AsyncPregenMethod.selectConcurrencyCap(0, false));
}
}
@@ -33,6 +33,7 @@ public class GoldenHashEngineTest {
private PlatformBlockState stone;
private PlatformBlockState dirt;
private PlatformBiome plains;
private PlatformBiome forest;
@Before
public void setUp() throws Exception {
@@ -49,6 +50,8 @@ public class GoldenHashEngineTest {
when(dirt.key()).thenReturn("minecraft:dirt");
plains = mock(PlatformBiome.class);
when(plains.key()).thenReturn("minecraft:plains");
forest = mock(PlatformBiome.class);
when(forest.key()).thenReturn("minecraft:forest");
}
@After
@@ -80,6 +83,26 @@ public class GoldenHashEngineTest {
assertEquals("#combined=" + referenceCombined(expectedBody), lines.get(9));
}
@Test
public void nullBiomeUsesCanonicalPlainsFallback() throws Exception {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine hashEngine = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1, null), feedback, progress());
assertTrue(hashEngine.run());
List<String> lines = Files.readAllLines(hashEngine.getGoldenFile().toPath(), StandardCharsets.UTF_8);
assertEquals(referenceLine(0, 0, 3, 2, 1), lines.get(8));
}
@Test
public void nonNullBiomeKeyRemainsUnchanged() throws Exception {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine hashEngine = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1, forest), feedback, progress());
assertTrue(hashEngine.run());
List<String> lines = Files.readAllLines(hashEngine.getGoldenFile().toPath(), StandardCharsets.UTF_8);
assertEquals(referenceLine(0, 0, 3, 2, 1, "minecraft:forest"), lines.get(8));
}
@Test
public void verifyMatchesGoldenCapture() {
RecordingFeedback captureFeedback = new RecordingFeedback();
@@ -130,7 +153,7 @@ public class GoldenHashEngineTest {
}
private GoldenHashEngine.Request request(GoldenHashEngine.Mode mode) {
return new GoldenHashEngine.Request("testworld", 1234L, "test-1.0", MIN_Y, MAX_Y, 0, 0, 0, 1, mode, false, false, "null");
return new GoldenHashEngine.Request("testworld", 1234L, "test-1.0", MIN_Y, MAX_Y, 0, 0, 0, 1, mode, false, false, GoldenHashEngine.FALLBACK_BIOME_KEY);
}
private GoldenHashEngine.Progress progress() {
@@ -139,6 +162,10 @@ public class GoldenHashEngineTest {
}
private GoldenHashEngine.ChunkSource source(PlatformBlockState special, int sx, int sy, int sz) {
return source(special, sx, sy, sz, plains);
}
private GoldenHashEngine.ChunkSource source(PlatformBlockState special, int sx, int sy, int sz, PlatformBiome biome) {
return (int chunkX, int chunkZ) -> new GoldenHashEngine.ChunkSnapshot() {
@Override
public int minY() {
@@ -157,12 +184,16 @@ public class GoldenHashEngineTest {
@Override
public PlatformBiome biome(int x, int y, int z) {
return plains;
return biome;
}
};
}
private String referenceLine(int chunkX, int chunkZ, int sx, int sy, int sz) throws Exception {
return referenceLine(chunkX, chunkZ, sx, sy, sz, "minecraft:plains");
}
private String referenceLine(int chunkX, int chunkZ, int sx, int sy, int sz, String biomeKey) throws Exception {
MessageDigest blockDigest = MessageDigest.getInstance("SHA-256");
MessageDigest biomeDigest = MessageDigest.getInstance("SHA-256");
for (int x = 0; x < 16; x++) {
@@ -176,7 +207,7 @@ public class GoldenHashEngineTest {
for (int x = 0; x < 16; x += 4) {
for (int z = 0; z < 16; z += 4) {
for (int y = MIN_Y; y < MAX_Y; y += 4) {
biomeDigest.update("minecraft:plains\n".getBytes(StandardCharsets.UTF_8));
biomeDigest.update((biomeKey + "\n").getBytes(StandardCharsets.UTF_8));
}
}
}
@@ -0,0 +1,17 @@
package art.arcane.iris.core.splash;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class IrisSplashComposerTest {
@Test
public void composeInfoUsesCurrentVersionWithoutStaleReleaseTag() {
String[] info = IrisSplashComposer.composeInfo("4.0.0-26.2", "Paper 26.2", IrisSplashComposer.InfoStyle.PLAIN);
assertEquals(" Iris, Dimension Engine [4.0]", info[1]);
assertEquals(" Version: 4.0.0-26.2", info[2]);
assertFalse(String.join("\n", info).contains("RC.1.1.6"));
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.engine.framework.placer;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HeightmapObjectPlacerTest {
@Test
public void engineOwnershipDelegatesWithoutAmbientContext() {
Engine engine = mock(Engine.class);
IObjectPlacer ownedDelegate = mock(IObjectPlacer.class);
when(ownedDelegate.getEngine()).thenReturn(engine);
HeightmapObjectPlacer owned = new HeightmapObjectPlacer(mock(RNG.class), 1, 2, 3, new IrisObjectPlacement(), ownedDelegate);
assertSame(engine, owned.getEngine());
IObjectPlacer unownedDelegate = mock(IObjectPlacer.class);
HeightmapObjectPlacer unowned = new HeightmapObjectPlacer(mock(RNG.class), 1, 2, 3, new IrisObjectPlacement(), unownedDelegate);
assertNull(unowned.getEngine());
}
}
@@ -0,0 +1,153 @@
package art.arcane.iris.util.project.context;
import art.arcane.iris.engine.framework.Engine;
import org.junit.Test;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisContextIsolationTest {
@Test
public void scopedContextsIsolateStatePerThread() throws Exception {
Engine engine = mock(Engine.class);
ChunkContext leftChunk = mock(ChunkContext.class);
ChunkContext rightChunk = mock(ChunkContext.class);
CyclicBarrier writesComplete = new CyclicBarrier(2);
AtomicReference<IrisContext> leftBound = new AtomicReference<>();
AtomicReference<IrisContext> rightBound = new AtomicReference<>();
AtomicReference<ChunkContext> leftObserved = new AtomicReference<>();
AtomicReference<ChunkContext> rightObserved = new AtomicReference<>();
AtomicReference<IrisContext> leftAfterClose = new AtomicReference<>();
AtomicReference<IrisContext> rightAfterClose = new AtomicReference<>();
AtomicReference<Throwable> failure = new AtomicReference<>();
Thread leftThread = new Thread(() -> exerciseContext(engine, 17L, leftChunk, writesComplete, leftBound, leftObserved, leftAfterClose, failure), "iris-context-left");
Thread rightThread = new Thread(() -> exerciseContext(engine, 17L, rightChunk, writesComplete, rightBound, rightObserved, rightAfterClose, failure), "iris-context-right");
leftThread.start();
rightThread.start();
leftThread.join(5000L);
rightThread.join(5000L);
assertFalse("Left context worker did not finish.", leftThread.isAlive());
assertFalse("Right context worker did not finish.", rightThread.isAlive());
assertNull(failure.get());
assertNotSame(leftBound.get(), rightBound.get());
assertSame(leftChunk, leftObserved.get());
assertSame(rightChunk, rightObserved.get());
assertNull(leftAfterClose.get());
assertNull(rightAfterClose.get());
}
@Test
public void scopeRebindsWorkerWhenEngineChanges() throws Exception {
Engine firstEngine = mock(Engine.class);
Engine secondEngine = mock(Engine.class);
AtomicReference<IrisContext> first = new AtomicReference<>();
AtomicReference<IrisContext> second = new AtomicReference<>();
Thread worker = new Thread(() -> {
try (IrisContext.Scope firstScope = IrisContext.open(firstEngine, 1L, null)) {
first.set(IrisContext.require());
}
try (IrisContext.Scope secondScope = IrisContext.open(secondEngine, 2L, null)) {
second.set(IrisContext.require());
}
}, "iris-context-reused-worker");
worker.start();
worker.join(5000L);
assertFalse("Reused context worker did not finish.", worker.isAlive());
assertSame(firstEngine, first.get().getEngine());
assertSame(secondEngine, second.get().getEngine());
assertNotSame(first.get(), second.get());
}
@Test
public void nestedScopeRestoresOuterContext() {
Engine outerEngine = mock(Engine.class);
Engine innerEngine = mock(Engine.class);
ChunkContext outerChunk = mock(ChunkContext.class);
ChunkContext innerChunk = mock(ChunkContext.class);
try (IrisContext.Scope outerScope = IrisContext.open(outerEngine, 11L, outerChunk)) {
IrisContext outer = IrisContext.require();
try (IrisContext.Scope innerScope = IrisContext.open(innerEngine, 12L, innerChunk)) {
IrisContext inner = IrisContext.require();
assertSame(innerEngine, inner.getEngine());
assertSame(innerChunk, inner.getChunkContext());
}
assertSame(outer, IrisContext.require());
}
assertNull(IrisContext.get());
}
@Test
public void nestedScopeRestoresOuterContextAfterEngineCloses() {
Engine engine = mock(Engine.class);
when(engine.isClosed()).thenReturn(false);
try (IrisContext.Scope outerScope = IrisContext.open(engine, 11L, null)) {
IrisContext outer = IrisContext.require();
when(engine.isClosed()).thenReturn(true);
assertTrue(engine.isClosed());
try (IrisContext.Scope innerScope = IrisContext.open(engine, 11L, null)) {
assertNotSame(outer, IrisContext.require());
}
assertSame(outer, IrisContext.require());
}
assertNull(IrisContext.get());
}
@Test
public void closingScopesOutOfOrderFails() {
Engine outerEngine = mock(Engine.class);
Engine innerEngine = mock(Engine.class);
IrisContext.Scope outerScope = IrisContext.open(outerEngine, 21L, null);
IrisContext.Scope innerScope = IrisContext.open(innerEngine, 22L, null);
assertThrows(IllegalStateException.class, outerScope::close);
innerScope.close();
outerScope.close();
assertNull(IrisContext.get());
}
private void exerciseContext(
Engine engine,
long generationSessionId,
ChunkContext chunk,
CyclicBarrier writesComplete,
AtomicReference<IrisContext> bound,
AtomicReference<ChunkContext> observed,
AtomicReference<IrisContext> afterClose,
AtomicReference<Throwable> failure
) {
try {
try (IrisContext.Scope scope = IrisContext.open(engine, generationSessionId, chunk)) {
bound.set(IrisContext.require());
await(writesComplete);
observed.set(IrisContext.require().getChunkContext());
}
afterClose.set(IrisContext.get());
} catch (Throwable e) {
failure.compareAndSet(null, e);
}
}
private void await(CyclicBarrier barrier) throws BrokenBarrierException, InterruptedException, TimeoutException {
barrier.await(5L, TimeUnit.SECONDS);
}
}
@@ -0,0 +1,37 @@
package art.arcane.iris.util.project.matter;
import art.arcane.iris.core.loader.IrisData;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
public class IrisMatterContextTest {
@Test
public void nestedScopesRestoreAndClearData() {
IrisData outerData = mock(IrisData.class);
IrisData innerData = mock(IrisData.class);
assertThrows(IllegalStateException.class, IrisMatterContext::require);
try (IrisMatterContext.Scope outerScope = IrisMatterContext.open(outerData)) {
assertSame(outerData, IrisMatterContext.require());
try (IrisMatterContext.Scope innerScope = IrisMatterContext.open(innerData)) {
assertSame(innerData, IrisMatterContext.require());
}
assertSame(outerData, IrisMatterContext.require());
}
assertThrows(IllegalStateException.class, IrisMatterContext::require);
}
@Test
public void closingScopesOutOfOrderFails() {
IrisMatterContext.Scope outerScope = IrisMatterContext.open(mock(IrisData.class));
IrisMatterContext.Scope innerScope = IrisMatterContext.open(mock(IrisData.class));
assertThrows(IllegalStateException.class, outerScope::close);
innerScope.close();
outerScope.close();
assertThrows(IllegalStateException.class, IrisMatterContext::require);
}
}
@@ -0,0 +1,62 @@
package art.arcane.iris.util.project.matter;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.util.project.matter.slices.SpawnerMatter;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class RegistryMatterContextTest {
@Test
public void registrySliceResolvesThroughExplicitDataOnIoWorker() throws Exception {
IrisSpawner firstResolved = new IrisSpawner();
IrisSpawner secondResolved = new IrisSpawner();
IrisData firstData = dataReturning(firstResolved);
IrisData secondData = dataReturning(secondResolved);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<IrisSpawner> firstRead = executor.submit(() -> readSpawner(firstData));
Future<IrisSpawner> secondRead = executor.submit(() -> readSpawner(secondData));
assertSame(firstResolved, firstRead.get());
assertSame(secondResolved, secondRead.get());
} finally {
executor.shutdownNow();
}
}
private IrisSpawner readSpawner(IrisData data) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
output.writeUTF("test-spawner");
}
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data);
DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
return new SpawnerMatter().readNode(input);
}
}
@SuppressWarnings("unchecked")
private IrisData dataReturning(IrisSpawner resolved) {
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
when(loader.load("test-spawner")).thenReturn(resolved);
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
loaders.put(IrisSpawner.class, loader);
IrisData data = mock(IrisData.class);
when(data.getLoaders()).thenReturn(loaders);
return data;
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris.util.project.stream.utility;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ContextInjectingStreamTest {
@Test
public void matchingOwnerSessionAndChunkUsesContextCache() {
Engine engine = mock(Engine.class);
when(engine.getGenerationSessionId()).thenReturn(41L);
ChunkContext chunkContext = chunkContext(32, 48, 41L);
ProceduralStream<String> source = sourceStream();
ContextInjectingStream<String> stream = new ContextInjectingStream<>(source, engine,
(context, x, z) -> "cache-" + x + "-" + z);
try (IrisContext.Scope scope = IrisContext.open(engine, 41L, chunkContext)) {
assertEquals("cache-3-3", stream.get(35D, 51D));
}
}
@Test
public void mismatchedOwnerSessionChunkOrCoordinatesFallsBackToSource() {
Engine engine = mock(Engine.class);
Engine otherEngine = mock(Engine.class);
when(engine.getGenerationSessionId()).thenReturn(41L);
when(otherEngine.getGenerationSessionId()).thenReturn(41L);
ProceduralStream<String> source = sourceStream();
ContextInjectingStream<String> stream = new ContextInjectingStream<>(source, engine,
(context, x, z) -> "cache");
try (IrisContext.Scope scope = IrisContext.open(otherEngine, 41L, chunkContext(32, 48, 41L))) {
assertEquals("source", stream.get(35D, 51D));
}
try (IrisContext.Scope scope = IrisContext.open(engine, 40L, chunkContext(32, 48, 40L))) {
assertEquals("source", stream.get(35D, 51D));
}
try (IrisContext.Scope scope = IrisContext.open(engine, 41L, chunkContext(32, 48, 41L))) {
assertEquals("source", stream.get(64D, 80D));
}
try (IrisContext.Scope scope = IrisContext.open(engine, 41L, null)) {
assertEquals("source", stream.get(35D, 51D));
}
}
private ChunkContext chunkContext(int x, int z, long generationSessionId) {
ChunkContext context = mock(ChunkContext.class);
when(context.getX()).thenReturn(x);
when(context.getZ()).thenReturn(z);
when(context.getGenerationSessionId()).thenReturn(generationSessionId);
return context;
}
@SuppressWarnings("unchecked")
private ProceduralStream<String> sourceStream() {
ProceduralStream<String> source = mock(ProceduralStream.class);
when(source.get(anyDouble(), anyDouble())).thenReturn("source");
return source;
}
}