Probe fidelity options

- Print measured-window stage totals from EngineMetrics as IRIS_GENPROBE_STAGES
- Time each context prefill task into its prefill timer
- Add gated generation counters for generator evaluations, corner starcasts and hydrology lookups
- Add -PprobeHydrologyPreplan to plan the probe area's hydrology tiles before warmup
- Add -PprobeHistoryRouter to generate through a generation-history router and registry contract
- Add -PprobeProductionBuffers to generate into production-equivalent block and biome hunks
- Print the mantle radius as IRIS_GENPROBE_MANTLE
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 19:20:17 -04:00
1 parent e2430500cf
commit b6b01c88f5
15 files changed
+1020 -113

No files matched your search

@@ -97,22 +97,23 @@ public class ChunkContext {
} }
List<Runnable> fillTasks = new ArrayList<>(6); List<Runnable> fillTasks = new ArrayList<>(6);
if (resolvedPlan.height) { if (resolvedPlan.height) {
fillTasks.add(() -> height.fillRounded(roundedHeight)); fillTasks.add(new PrefillFillTask(() -> height.fillRounded(roundedHeight),
capturePrefillMetric ? metrics.getContextPrefillHeight() : null));
} }
if (resolvedPlan.biome) { if (resolvedPlan.biome) {
fillTasks.add(new PrefillFillTask(biome)); fillTasks.add(new PrefillFillTask(biome::fill, capturePrefillMetric ? metrics.getContextPrefillBiome() : null));
} }
if (resolvedPlan.rock) { if (resolvedPlan.rock) {
fillTasks.add(new PrefillFillTask(rock)); fillTasks.add(new PrefillFillTask(rock::fill, capturePrefillMetric ? metrics.getContextPrefillRock() : null));
} }
if (resolvedPlan.fluid) { if (resolvedPlan.fluid) {
fillTasks.add(new PrefillFillTask(fluid)); fillTasks.add(new PrefillFillTask(fluid::fill, capturePrefillMetric ? metrics.getContextPrefillFluid() : null));
} }
if (resolvedPlan.region) { if (resolvedPlan.region) {
fillTasks.add(new PrefillFillTask(region)); fillTasks.add(new PrefillFillTask(region::fill, capturePrefillMetric ? metrics.getContextPrefillRegion() : null));
} }
if (resolvedPlan.cave) { if (resolvedPlan.cave) {
fillTasks.add(new PrefillFillTask(cave)); fillTasks.add(new PrefillFillTask(cave::fill, capturePrefillMetric ? metrics.getContextPrefillCave() : null));
} }
if (!shouldPrefillAsync(resolvedPlan, fillTasks.size())) { if (!shouldPrefillAsync(resolvedPlan, fillTasks.size())) {
@@ -297,15 +298,23 @@ public class ChunkContext {
} }
private static final class PrefillFillTask implements Runnable { private static final class PrefillFillTask implements Runnable {
private final ChunkedDataCache<?> dataCache; private final Runnable fill;
private final EngineMetrics.StageTimer timer;
private PrefillFillTask(ChunkedDataCache<?> dataCache) { private PrefillFillTask(Runnable fill, EngineMetrics.StageTimer timer) {
this.dataCache = dataCache; this.fill = fill;
this.timer = timer;
} }
@Override @Override
public void run() { public void run() {
dataCache.fill(); if (timer == null) {
fill.run();
return;
}
long startedNanos = System.nanoTime();
fill.run();
timer.put((System.nanoTime() - startedNanos) / 1_000_000D);
} }
} }
} }
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.hydrology; package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.runtime.GenerationClosedException; import art.arcane.iris.generation.runtime.GenerationClosedException;
import art.arcane.iris.generation.runtime.GenerationCounter;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.stream.ProvisionalSampling; import art.arcane.iris.generation.stream.ProvisionalSampling;
@@ -499,6 +500,9 @@ public final class HydrologyTileCache implements AutoCloseable {
} }
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) { public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
if (GenerationCounter.ENABLED) {
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
}
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ); return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
} }
@@ -507,6 +511,9 @@ public final class HydrologyTileCache implements AutoCloseable {
if (!waitsForbidden()) { if (!waitsForbidden()) {
return HydrologyColumnSnapshot.ready(columnAt(blockX, blockZ).orElse(null)); return HydrologyColumnSnapshot.ready(columnAt(blockX, blockZ).orElse(null));
} }
if (GenerationCounter.ENABLED) {
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
}
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE); int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE); int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ); ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ);
@@ -24,63 +24,65 @@ import lombok.Data;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
@Data @Data
public class EngineMetrics { public class EngineMetrics {
private final AtomicRollingSequence total; private final StageTimer total;
private final AtomicRollingSequence updates; private final StageTimer updates;
private final AtomicRollingSequence terrain; private final StageTimer terrain;
private final AtomicRollingSequence biome; private final StageTimer biome;
private final AtomicRollingSequence parallax; private final StageTimer parallax;
private final AtomicRollingSequence parallaxInsert; private final StageTimer parallaxInsert;
private final AtomicRollingSequence post; private final StageTimer post;
private final AtomicRollingSequence perfection; private final StageTimer perfection;
private final AtomicRollingSequence api; private final StageTimer api;
private final AtomicRollingSequence decoration; private final StageTimer decoration;
private final AtomicRollingSequence cave; private final StageTimer cave;
private final AtomicRollingSequence ravine; private final StageTimer ravine;
private final AtomicRollingSequence deposit; private final StageTimer deposit;
private final AtomicRollingSequence carveResolve; private final StageTimer carveResolve;
private final AtomicRollingSequence carveApply; private final StageTimer carveApply;
private final AtomicRollingSequence noiseHeightInterpolate; private final StageTimer noiseHeightInterpolate;
private final AtomicRollingSequence noiseHeightGenerator; private final StageTimer noiseHeightGenerator;
private final AtomicRollingSequence contextPrefill; private final StageTimer contextPrefill;
private final AtomicRollingSequence contextPrefillHeight; private final StageTimer contextPrefillHeight;
private final AtomicRollingSequence contextPrefillBiome; private final StageTimer contextPrefillBiome;
private final AtomicRollingSequence contextPrefillRock; private final StageTimer contextPrefillRock;
private final AtomicRollingSequence contextPrefillFluid; private final StageTimer contextPrefillFluid;
private final AtomicRollingSequence contextPrefillRegion; private final StageTimer contextPrefillRegion;
private final AtomicRollingSequence contextPrefillCave; private final StageTimer contextPrefillCave;
private final AtomicRollingSequence pregenWaitPermit; private final StageTimer pregenWaitPermit;
private final AtomicRollingSequence pregenWaitAdaptive; private final StageTimer pregenWaitAdaptive;
public EngineMetrics(int mem) { public EngineMetrics(int mem) {
this.total = new AtomicRollingSequence(mem); this.total = new StageTimer(mem);
this.terrain = new AtomicRollingSequence(mem); this.terrain = new StageTimer(mem);
this.api = new AtomicRollingSequence(mem); this.api = new StageTimer(mem);
this.biome = new AtomicRollingSequence(mem); this.biome = new StageTimer(mem);
this.perfection = new AtomicRollingSequence(mem); this.perfection = new StageTimer(mem);
this.parallax = new AtomicRollingSequence(mem); this.parallax = new StageTimer(mem);
this.parallaxInsert = new AtomicRollingSequence(mem); this.parallaxInsert = new StageTimer(mem);
this.post = new AtomicRollingSequence(mem); this.post = new StageTimer(mem);
this.decoration = new AtomicRollingSequence(mem); this.decoration = new StageTimer(mem);
this.updates = new AtomicRollingSequence(mem); this.updates = new StageTimer(mem);
this.cave = new AtomicRollingSequence(mem); this.cave = new StageTimer(mem);
this.ravine = new AtomicRollingSequence(mem); this.ravine = new StageTimer(mem);
this.deposit = new AtomicRollingSequence(mem); this.deposit = new StageTimer(mem);
this.carveResolve = new AtomicRollingSequence(mem); this.carveResolve = new StageTimer(mem);
this.carveApply = new AtomicRollingSequence(mem); this.carveApply = new StageTimer(mem);
this.noiseHeightInterpolate = new AtomicRollingSequence(mem); this.noiseHeightInterpolate = new StageTimer(mem);
this.noiseHeightGenerator = new AtomicRollingSequence(mem); this.noiseHeightGenerator = new StageTimer(mem);
this.contextPrefill = new AtomicRollingSequence(mem); this.contextPrefill = new StageTimer(mem);
this.contextPrefillHeight = new AtomicRollingSequence(mem); this.contextPrefillHeight = new StageTimer(mem);
this.contextPrefillBiome = new AtomicRollingSequence(mem); this.contextPrefillBiome = new StageTimer(mem);
this.contextPrefillRock = new AtomicRollingSequence(mem); this.contextPrefillRock = new StageTimer(mem);
this.contextPrefillFluid = new AtomicRollingSequence(mem); this.contextPrefillFluid = new StageTimer(mem);
this.contextPrefillRegion = new AtomicRollingSequence(mem); this.contextPrefillRegion = new StageTimer(mem);
this.contextPrefillCave = new AtomicRollingSequence(mem); this.contextPrefillCave = new StageTimer(mem);
this.pregenWaitPermit = new AtomicRollingSequence(mem); this.pregenWaitPermit = new StageTimer(mem);
this.pregenWaitAdaptive = new AtomicRollingSequence(mem); this.pregenWaitAdaptive = new StageTimer(mem);
} }
public KMap<String, Double> pull() { public KMap<String, Double> pull() {
@@ -134,4 +136,61 @@ public class EngineMetrics {
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D); averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
return Map.copyOf(averages); return Map.copyOf(averages);
} }
public Map<String, StageTotal> stageTotals() {
Map<String, StageTotal> totals = new LinkedHashMap<>();
totals.put("total", total.snapshot());
totals.put("terrain", terrain.snapshot());
totals.put("biome", biome.snapshot());
totals.put("parallax", parallax.snapshot());
totals.put("parallax.insert", parallaxInsert.snapshot());
totals.put("post", post.snapshot());
totals.put("perfection", perfection.snapshot());
totals.put("decoration", decoration.snapshot());
totals.put("api", api.snapshot());
totals.put("updates", updates.snapshot());
totals.put("cave", cave.snapshot());
totals.put("ravine", ravine.snapshot());
totals.put("deposit", deposit.snapshot());
totals.put("carve.resolve", carveResolve.snapshot());
totals.put("carve.apply", carveApply.snapshot());
totals.put("noise.height.interpolate", noiseHeightInterpolate.snapshot());
totals.put("noise.height.generator", noiseHeightGenerator.snapshot());
totals.put("context.prefill", contextPrefill.snapshot());
totals.put("context.prefill.height", contextPrefillHeight.snapshot());
totals.put("context.prefill.biome", contextPrefillBiome.snapshot());
totals.put("context.prefill.rock", contextPrefillRock.snapshot());
totals.put("context.prefill.fluid", contextPrefillFluid.snapshot());
totals.put("context.prefill.region", contextPrefillRegion.snapshot());
totals.put("context.prefill.cave", contextPrefillCave.snapshot());
totals.put("pregen.wait.permit", pregenWaitPermit.snapshot());
totals.put("pregen.wait.adaptive", pregenWaitAdaptive.snapshot());
return totals;
}
public static final class StageTimer extends AtomicRollingSequence {
private final DoubleAdder totalMillis = new DoubleAdder();
private final LongAdder samples = new LongAdder();
public StageTimer(int size) {
super(size);
}
@Override
public void put(double millis) {
super.put(millis);
totalMillis.add(millis);
samples.increment();
}
public StageTotal snapshot() {
return new StageTotal(totalMillis.sum(), samples.sum());
}
}
public record StageTotal(double millis, long samples) {
public StageTotal since(StageTotal earlier) {
return new StageTotal(millis - earlier.millis(), samples - earlier.samples());
}
}
} }
@@ -0,0 +1,34 @@
package art.arcane.iris.generation.runtime;
import java.util.concurrent.atomic.LongAdder;
public enum GenerationCounter {
GENERATOR_EVALUATIONS("generator_evaluations"),
CORNER_STARCASTS("corner_starcasts"),
HYDROLOGY_LOOKUPS("hydrology_lookups");
public static final boolean ENABLED = Boolean.getBoolean("iris.generation.counters");
private final String key;
private final LongAdder count = new LongAdder();
GenerationCounter(String key) {
this.key = key;
}
public String key() {
return key;
}
public void add(long amount) {
count.add(amount);
}
public void increment() {
count.increment();
}
public long sum() {
return count.sum();
}
}
@@ -867,6 +867,9 @@ public class IrisComplex implements DataProvider {
break; break;
} }
generator.getHeights(activeXs, activeZs, seed, noise, activeCount); generator.getHeights(activeXs, activeZs, seed, noise, activeCount);
if (GenerationCounter.ENABLED) {
GenerationCounter.GENERATOR_EVALUATIONS.add(activeCount);
}
for (int sample = 0; sample < activeCount; sample++) { for (int sample = 0; sample < activeCount; sample++) {
int index = active[sample]; int index = active[sample];
if (!Double.isFinite(noise[sample])) { if (!Double.isFinite(noise[sample])) {
@@ -1838,6 +1841,9 @@ public class IrisComplex implements DataProvider {
long sharedKey = SharedCornerBounds.key(gx, gz); long sharedKey = SharedCornerBounds.key(gx, gz);
long packed = shared.get(sharedKey, interpolatorIndex); long packed = shared.get(sharedKey, interpolatorIndex);
if (packed == Long.MIN_VALUE) { if (packed == Long.MIN_VALUE) {
if (GenerationCounter.ENABLED) {
GenerationCounter.CORNER_STARCASTS.increment();
}
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz); packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) { if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
// A non-finite corner is a transient sampling fault, not a fact about the terrain: // A non-finite corner is a transient sampling fault, not a fact about the terrain:
@@ -2080,6 +2086,9 @@ public class IrisComplex implements DataProvider {
} }
return height / generators.length; return height / generators.length;
} }
if (GenerationCounter.ENABLED) {
GenerationCounter.GENERATOR_EVALUATIONS.add(generators.length);
}
for (IrisGenerator generator : generators) { for (IrisGenerator generator : generators) {
double noise = generator.getHeight(x, z, seed); double noise = generator.getHeight(x, z, seed);
if (!Double.isFinite(noise)) { if (!Double.isFinite(noise)) {
@@ -0,0 +1,47 @@
package art.arcane.iris.generation.runtime;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
public final class EngineMetricsStageTotalsTest {
@Test
public void stageTotalsAccumulatePastTheRollingWindow() {
EngineMetrics metrics = new EngineMetrics(2);
metrics.getTerrain().put(1.5D);
metrics.getTerrain().put(2.5D);
metrics.getTerrain().put(4D);
EngineMetrics.StageTotal terrain = metrics.stageTotals().get("terrain");
assertEquals(8D, terrain.millis(), 0D);
assertEquals(3L, terrain.samples());
assertEquals(3.25D, metrics.getTerrain().getAverage(), 0D);
}
@Test
public void stageTotalsSinceAnEarlierSnapshotCoverOnlyTheLaterPuts() {
EngineMetrics metrics = new EngineMetrics(4);
metrics.getCarveResolve().put(3D);
EngineMetrics.StageTotal before = metrics.stageTotals().get("carve.resolve");
metrics.getCarveResolve().put(5D);
metrics.getCarveResolve().put(7D);
EngineMetrics.StageTotal window = metrics.stageTotals().get("carve.resolve").since(before);
assertEquals(12D, window.millis(), 0D);
assertEquals(2L, window.samples());
}
@Test
public void stageTotalsNameEveryPulledTimer() {
EngineMetrics metrics = new EngineMetrics(1);
Map<String, EngineMetrics.StageTotal> totals = metrics.stageTotals();
assertEquals(Set.copyOf(metrics.pull().keySet()), Set.copyOf(totals.keySet()));
assertEquals(metrics.pull().size(), totals.size());
}
}
+6
View File
@@ -175,6 +175,12 @@ tasks.register('genProbe', JavaExec) {
} }
systemProperty('iris.probe.threads', providers.gradleProperty('probeThreads').orElse('1').get()) systemProperty('iris.probe.threads', providers.gradleProperty('probeThreads').orElse('1').get())
systemProperty('iris.probe.pregenProfile', providers.gradleProperty('probePregenProfile').orElse('false').get()) systemProperty('iris.probe.pregenProfile', providers.gradleProperty('probePregenProfile').orElse('false').get())
systemProperty('iris.probe.hydrologyPreplan', providers.gradleProperty('probeHydrologyPreplan').orElse('false').get())
systemProperty('iris.probe.historyRouter', providers.gradleProperty('probeHistoryRouter').orElse('false').get())
systemProperty('iris.probe.productionBuffers', providers.gradleProperty('probeProductionBuffers').orElse('false').get())
String probeCounters = providers.gradleProperty('probeCounters').orElse('false').get()
systemProperty('iris.generation.counters', probeCounters)
systemProperty('volmlib.cache.countContention', probeCounters)
dependsOn(':core:compileJava') dependsOn(':core:compileJava')
doFirst { doFirst {
if (!probePack.isPresent() || probePack.get().isBlank()) { if (!probePack.isPresent() || probePack.get().isBlank()) {
@@ -20,6 +20,7 @@ package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@@ -84,11 +85,12 @@ public final class ChunkDump {
char[] biomeIndices = new char[size]; char[] biomeIndices = new char[size];
PaletteBuilder blockPalette = new PaletteBuilder(); PaletteBuilder blockPalette = new PaletteBuilder();
PaletteBuilder biomePalette = new PaletteBuilder(); PaletteBuilder biomePalette = new PaletteBuilder();
NativeBlockVolume stored = blocks instanceof NativeBlockVolume volume ? volume : null;
int index = 0; int index = 0;
for (int x = 0; x < 16; x++) { for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
NativeBlockState state = blocks.get(x, y, z); NativeBlockState state = stored == null ? blocks.get(x, y, z) : stored.getStoredRaw(x, y, z);
NativeBiome biome = biomes.get(x, y, z); NativeBiome biome = biomes.get(x, y, z);
blockIndices[index] = blockPalette.indexOf(state, state == null ? NULL_BLOCK_KEY : null); blockIndices[index] = blockPalette.indexOf(state, state == null ? NULL_BLOCK_KEY : null);
biomeIndices[index] = biomePalette.indexOf(biome, biome == null ? NULL_BIOME_KEY : null); biomeIndices[index] = biomePalette.indexOf(biome, biome == null ? NULL_BIOME_KEY : null);
@@ -18,31 +18,42 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator; import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineEffects; import art.arcane.iris.generation.runtime.EngineEffects;
import art.arcane.iris.generation.runtime.EngineEffectsProvider; import art.arcane.iris.generation.runtime.EngineEffectsProvider;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.EnginePlatformHooks; import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.EngineTarget; import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.runtime.EngineWorldManager; import art.arcane.iris.generation.runtime.EngineWorldManager;
import art.arcane.iris.generation.runtime.EngineWorldManagerProvider; import art.arcane.iris.generation.runtime.EngineWorldManagerProvider;
import art.arcane.iris.generation.runtime.GenerationCounter;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.runtime.MeteredCache; import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry; import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyTileCache; import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.IrisWorld; import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.IrisBoundarySignatureSampler;
import art.arcane.iris.world.pregen.PregenPerformanceProfile; import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.cache.CacheContention;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise; import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise;
@@ -66,6 +77,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public final class GenerationProbe { public final class GenerationProbe {
private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>()); private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>());
@@ -168,7 +180,8 @@ public final class GenerationProbe {
} }
} }
record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile) { record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile, boolean hydrologyPreplan,
boolean historyRouter, boolean productionBuffers, boolean counters) {
ProbeOptions { ProbeOptions {
if (threads < 1) { if (threads < 1) {
throw new IllegalArgumentException("Thread count must be at least 1."); throw new IllegalArgumentException("Thread count must be at least 1.");
@@ -183,7 +196,15 @@ public final class GenerationProbe {
return new ProbeOptions( return new ProbeOptions(
Integer.parseInt(System.getProperty("iris.probe.threads", "1").trim()), Integer.parseInt(System.getProperty("iris.probe.threads", "1").trim()),
dump.isEmpty() ? null : Path.of(dump), dump.isEmpty() ? null : Path.of(dump),
Boolean.parseBoolean(System.getProperty("iris.probe.pregenProfile", "false").trim())); booleanProperty("iris.probe.pregenProfile"),
booleanProperty("iris.probe.hydrologyPreplan"),
booleanProperty("iris.probe.historyRouter"),
booleanProperty("iris.probe.productionBuffers"),
booleanProperty("iris.generation.counters"));
}
private static boolean booleanProperty(String key) {
return Boolean.parseBoolean(System.getProperty(key, "false").trim());
} }
boolean parallel() { boolean parallel() {
@@ -257,6 +278,29 @@ public final class GenerationProbe {
TimingSummary measuredTimings, String signature) { TimingSummary measuredTimings, String signature) {
} }
record WindowMetrics(Map<String, EngineMetrics.StageTotal> stages, Map<String, Long> counters) {
static WindowMetrics capture(Engine engine, boolean counters) {
return new WindowMetrics(engine.getMetrics().stageTotals(), counters ? counterValues() : Map.of());
}
private static Map<String, Long> counterValues() {
Map<String, Long> values = new LinkedHashMap<>();
for (GenerationCounter counter : GenerationCounter.values()) {
values.put(counter.key(), counter.sum());
}
values.put("cache_duplicate_computations", CacheContention.duplicateComputations());
return values;
}
}
record HydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) {
String machineLine() {
return String.format(Locale.ROOT,
"IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=%d tile_size=%d publication_radius=%d reach_chunks=%d planned_ms=%.3f",
tiles, tileSize, publicationRadius, reachChunks, nanosToMillis(elapsedNanos));
}
}
record ProbeResult(String status, String dimensionKey, int warmupChunks, int measuredChunks, boolean multicore, record ProbeResult(String status, String dimensionKey, int warmupChunks, int measuredChunks, boolean multicore,
boolean studio, boolean studio,
int successfulChunks, int failedChunks, long engineReadyNanos, long firstChunkNanos, int successfulChunks, int failedChunks, long engineReadyNanos, long firstChunkNanos,
@@ -285,6 +329,51 @@ public final class GenerationProbe {
} }
} }
static String stagesLine(WindowMetrics start, WindowMetrics end, int chunks, int threads) {
StringBuilder line = new StringBuilder(512);
line.append("IRIS_GENPROBE_STAGES version=1 chunks=").append(chunks).append(" threads=").append(threads);
for (Map.Entry<String, EngineMetrics.StageTotal> entry : end.stages().entrySet()) {
EngineMetrics.StageTotal earlier = start.stages().get(entry.getKey());
EngineMetrics.StageTotal window = earlier == null ? entry.getValue() : entry.getValue().since(earlier);
line.append(' ').append(entry.getKey()).append('=')
.append(String.format(Locale.ROOT, "%.3f", window.millis()));
}
return line.toString();
}
static String countersLine(WindowMetrics start, WindowMetrics end, int chunks) {
StringBuilder line = new StringBuilder(256);
line.append("IRIS_GENPROBE_COUNTERS version=1 chunks=").append(chunks);
for (Map.Entry<String, Long> entry : end.counters().entrySet()) {
long window = entry.getValue() - start.counters().getOrDefault(entry.getKey(), 0L);
line.append(' ').append(entry.getKey()).append('=').append(window)
.append(' ').append(entry.getKey()).append("_per_chunk=")
.append(String.format(Locale.ROOT, "%.1f", chunks == 0 ? 0D : window / (double) chunks));
}
return line.toString();
}
static List<HydrologyTileKey> preplanTiles(ProbeConfiguration configuration, int tileSize, int publicationRadius,
int reachChunks) {
HydrologyTileCache.PregenerationArea area = pregenerationArea(configuration);
long reach = (long) reachChunks * 16L + publicationRadius;
int minimumX = Math.toIntExact(Math.floorDiv(area.minimumBlockX() - reach, tileSize)) - 1;
int minimumZ = Math.toIntExact(Math.floorDiv(area.minimumBlockZ() - reach, tileSize)) - 1;
int maximumX = Math.toIntExact(Math.floorDiv(area.maximumBlockX() + reach, tileSize)) + 1;
int maximumZ = Math.toIntExact(Math.floorDiv(area.maximumBlockZ() + reach, tileSize)) + 1;
int centerX = Math.floorDiv(area.centerBlockX(), tileSize);
int centerZ = Math.floorDiv(area.centerBlockZ(), tileSize);
List<HydrologyTileKey> keys = new ArrayList<>((maximumX - minimumX + 1) * (maximumZ - minimumZ + 1));
for (int tileZ = minimumZ; tileZ <= maximumZ; tileZ++) {
for (int tileX = minimumX; tileX <= maximumX; tileX++) {
keys.add(new HydrologyTileKey(tileX, tileZ));
}
}
keys.sort(Comparator.comparingInt((HydrologyTileKey key) ->
Math.max(Math.abs(key.tileX() - centerX), Math.abs(key.tileZ() - centerZ))));
return keys;
}
public static void main(String[] args) { public static void main(String[] args) {
ProbeConfiguration configuration; ProbeConfiguration configuration;
ProbeOptions options; ProbeOptions options;
@@ -354,7 +443,7 @@ public final class GenerationProbe {
Throwable executionFailure = null; Throwable executionFailure = null;
try { try {
File pack = clonePack(configuration.packSource(), workRoot); File pack = clonePack(configuration.packSource(), workRoot);
configureProbeRuntime(workRoot); configureProbeRuntime(workRoot, options.historyRouter());
validatePack(pack); validatePack(pack);
System.out.println("[genprobe] pack: " + configuration.packSource().getAbsolutePath()); System.out.println("[genprobe] pack: " + configuration.packSource().getAbsolutePath());
@@ -367,29 +456,48 @@ public final class GenerationProbe {
System.out.println("[genprobe] studio: " + configuration.studio()); System.out.println("[genprobe] studio: " + configuration.studio());
System.out.println("[genprobe] threads: " + options.threads()); System.out.println("[genprobe] threads: " + options.threads());
System.out.println("[genprobe] pregen profile: " + options.pregenProfile()); System.out.println("[genprobe] pregen profile: " + options.pregenProfile());
System.out.println("[genprobe] hydrology preplan: " + options.hydrologyPreplan());
System.out.println("[genprobe] history router: " + options.historyRouter());
System.out.println("[genprobe] production buffers: " + options.productionBuffers());
System.out.println("[genprobe] counters: " + options.counters());
System.out.println("[genprobe] dump file: " + (options.dumpFile() == null ? "none" : options.dumpFile())); System.out.println("[genprobe] dump file: " + (options.dumpFile() == null ? "none" : options.dumpFile()));
long engineStart = System.nanoTime(); long engineStart = System.nanoTime();
File worldFolder = new File(workRoot, "world");
data = IrisData.get(pack); data = IrisData.get(pack);
IrisDimension dimension = data.getDimensionLoader().load(configuration.dimensionKey()); IrisDimension dimension = loadDimension(data, configuration.dimensionKey());
if (dimension == null) { GenerationHistory history = null;
throw new IllegalStateException("Dimension '" + configuration.dimensionKey() if (options.historyRouter()) {
+ "' did not load from " + pack.getAbsolutePath()); history = HeadlessGenerationHistorySession.create(new RealPackProbeSupport.HistoryRequest(
worldFolder.toPath(), data, dimension, configuration.seed()), DataVersion.getLatest().get());
data.close();
data = IrisData.openRuntime(history.activePackRoot().toFile());
data.bindGenerationRegistryContract(history.activeEpoch().registryContract());
dimension = loadDimension(data, configuration.dimensionKey());
} }
IrisWorld world = IrisWorld.builder() IrisWorld world = IrisWorld.builder()
.platformIdentity("iris:probe") .platformIdentity("iris:probe")
.name("probe") .name("probe")
.seed(configuration.seed()) .seed(configuration.seed())
.worldFolder(new File(workRoot, "world")) .worldFolder(worldFolder)
.minHeight(dimension.getMinHeight()) .minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight()) .maxHeight(dimension.getMaxHeight())
.build(); .build();
EngineTarget target = new EngineTarget(world, dimension, data); EngineTarget target = new EngineTarget(world, dimension, data);
engine = new IrisEngine( IrisEngine.InitializationMode mode = configuration.studio()
target, ? IrisEngine.InitializationMode.STUDIO
configuration.studio() : IrisEngine.InitializationMode.RUNTIME;
? IrisEngine.InitializationMode.STUDIO GenerationHistoryRuntimeRouter router = null;
: IrisEngine.InitializationMode.RUNTIME); if (history == null) {
engine = new IrisEngine(target, mode);
} else {
IrisEngine historyEngine = new IrisEngine(target, mode,
history.paths().activationMantleRoot(history.activeActivation().activationId()),
history.activeEpoch().kernelVersion(), null);
engine = historyEngine;
router = GenerationHistoryRuntimeRouter.attach(historyEngine, history, IrisBoundarySignatureSampler.INSTANCE);
router.preloadActiveRuntimes();
}
long engineReadyNanos = System.nanoTime() - engineStart; long engineReadyNanos = System.nanoTime() - engineStart;
List<Throwable> initNoise = settleAndDrain(); List<Throwable> initNoise = settleAndDrain();
@@ -398,9 +506,16 @@ public final class GenerationProbe {
+ " seed=" + engine.getSeedManager().getSeed() + " seed=" + engine.getSeedManager().getSeed()
+ " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight() + " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight()
+ " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos))); + " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos)));
System.out.println("IRIS_GENPROBE_MANTLE version=1 radius=" + engine.getMantle().getRadius()
+ " real_radius=" + engine.getMantle().getRealRadius());
ProbeGenerator generator = new ProbeGenerator(engine, router, options.productionBuffers());
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount(); long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
GenerationResult generation = generateWithProfile(engine, configuration, options); GenerationResult generation = generateWithProfile(generator, configuration, options);
verifyBackend(nativeSamplesBefore); verifyBackend(nativeSamplesBefore);
if (options.productionBuffers()) {
System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks="
+ generator.expandedBiomeHunks());
}
String status = generation.failedChunks() == 0 ? "PASS" : "FAIL"; String status = generation.failedChunks() == 0 ? "PASS" : "FAIL";
printGenerationFailures(generation); printGenerationFailures(generation);
return new ProbeResult( return new ProbeResult(
@@ -445,7 +560,16 @@ public final class GenerationProbe {
} }
} }
private static void configureProbeRuntime(File workRoot) { private static IrisDimension loadDimension(IrisData data, String dimensionKey) {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Dimension '" + dimensionKey + "' did not load from "
+ data.getDataFolder().getAbsolutePath());
}
return dimension;
}
private static void configureProbeRuntime(File workRoot, boolean generationRegistry) {
REPORTED.clear(); REPORTED.clear();
StubPlatform.bindGenerationStateHandlers(); StubPlatform.bindGenerationStateHandlers();
StubPlatform.verbose(false); StubPlatform.verbose(false);
@@ -456,7 +580,8 @@ public final class GenerationProbe {
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new); IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new);
IrisServices.register(EnginePlatformHooks.class, new InertPlatformHooks()); IrisServices.register(EnginePlatformHooks.class, new InertPlatformHooks());
IrisPlatforms.unbind(); IrisPlatforms.unbind();
IrisPlatforms.bind(new StubPlatform(new File(workRoot, "platform-data"))); IrisPlatforms.bind(new StubPlatform(new File(workRoot, "platform-data"),
generationRegistry ? ProbeGenerationRegistry.INSTANCE : null));
} }
private static void validatePack(File pack) { private static void validatePack(File pack) {
@@ -473,21 +598,73 @@ public final class GenerationProbe {
System.out.println("[genprobe] offline pack validation: PASS"); System.out.println("[genprobe] offline pack validation: PASS");
} }
private static GenerationResult generateWithProfile(Engine engine, ProbeConfiguration configuration, private static GenerationResult generateWithProfile(ProbeGenerator generator, ProbeConfiguration configuration,
ProbeOptions options) throws Exception { ProbeOptions options) throws Exception {
Engine engine = generator.engine();
if (!options.pregenProfile()) { if (!options.pregenProfile()) {
return generate(engine, configuration, options); preplanHydrology(engine, configuration, options);
return generate(generator, configuration, options);
} }
PregenPerformanceProfile.apply(engine); PregenPerformanceProfile.apply(engine);
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime(); IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
try (HydrologyTileCache.PregenerationScope ignored = hydrology == null ? null try (HydrologyTileCache.PregenerationScope ignored = hydrology == null ? null
: hydrology.preparePregeneration(pregenerationArea(configuration))) { : hydrology.preparePregeneration(pregenerationArea(configuration))) {
return generate(engine, configuration, options); preplanHydrology(engine, configuration, options);
return generate(generator, configuration, options);
} finally { } finally {
PregenPerformanceProfile.restore(); PregenPerformanceProfile.restore();
} }
} }
private static void preplanHydrology(Engine engine, ProbeConfiguration configuration, ProbeOptions options)
throws Exception {
if (!options.hydrologyPreplan()) {
return;
}
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
if (hydrology == null) {
System.out.println("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology");
return;
}
HydrologyPlannerSettings settings = hydrology.settings();
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int reachChunks = Math.max(engine.getMantle().getRadius(), engine.getMantle().getRealRadius());
List<HydrologyTileKey> keys = preplanTiles(configuration, tileSize, publicationRadius, reachChunks);
long started = System.nanoTime();
ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), 4));
try {
List<Future<?>> plans = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
plans.add(executor.submit(() -> hydrology.tile(key)));
}
for (Future<?> plan : plans) {
awaitPlan(plan);
}
} finally {
executor.shutdown();
}
HydrologyPreplan preplan = new HydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks,
System.nanoTime() - started);
printDistinctCauses("hydrology preplan reported errors", drainReported());
System.out.println(preplan.machineLine());
}
private static void awaitPlan(Future<?> plan) throws Exception {
try {
plan.get();
} catch (ExecutionException failure) {
Throwable cause = failure.getCause();
if (cause instanceof Error error) {
throw error;
}
if (cause instanceof Exception exception) {
throw exception;
}
throw new IllegalStateException(cause);
}
}
static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) { static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) {
List<ChunkCoordinate> coordinates = scheduleCoordinates( List<ChunkCoordinate> coordinates = scheduleCoordinates(
configuration.warmupChunks() + configuration.measuredChunks(), configuration.warmupChunks() + configuration.measuredChunks(),
@@ -531,8 +708,9 @@ public final class GenerationProbe {
return Math.atan2(coordinate.z() - centerChunkZ, coordinate.x() - centerChunkX); return Math.atan2(coordinate.z() - centerChunkZ, coordinate.x() - centerChunkX);
} }
private static GenerationResult generate(Engine engine, ProbeConfiguration configuration, private static GenerationResult generate(ProbeGenerator generator, ProbeConfiguration configuration,
ProbeOptions options) throws Exception { ProbeOptions options) throws Exception {
Engine engine = generator.engine();
int totalChunks = configuration.warmupChunks() + configuration.measuredChunks(); int totalChunks = configuration.warmupChunks() + configuration.measuredChunks();
List<ChunkCoordinate> coordinates = scheduleCoordinates( List<ChunkCoordinate> coordinates = scheduleCoordinates(
totalChunks, configuration.centerChunkX(), configuration.centerChunkZ()); totalChunks, configuration.centerChunkX(), configuration.centerChunkZ());
@@ -541,29 +719,41 @@ public final class GenerationProbe {
engine.getMinHeight(), height); engine.getMinHeight(), height);
try (OutcomeSink sink = new OutcomeSink(options.dumpFile(), header, configuration.measuredChunks())) { try (OutcomeSink sink = new OutcomeSink(options.dumpFile(), header, configuration.measuredChunks())) {
if (options.parallel()) { if (options.parallel()) {
generateParallel(engine, configuration, options, coordinates, sink); generateParallel(generator, configuration, options, coordinates, sink);
} else { } else {
generateSequential(engine, configuration, coordinates, sink); generateSequential(generator, configuration, options, coordinates, sink);
} }
return sink.result(configuration); return sink.result(configuration);
} }
} }
private static void generateSequential(Engine engine, ProbeConfiguration configuration, private static void generateSequential(ProbeGenerator generator, ProbeConfiguration configuration,
List<ChunkCoordinate> coordinates, OutcomeSink sink) throws IOException { ProbeOptions options, List<ChunkCoordinate> coordinates,
OutcomeSink sink) throws IOException {
int totalChunks = coordinates.size(); int totalChunks = coordinates.size();
long measuredStartEpochMillis = 0L; long measuredStartEpochMillis = 0L;
WindowMetrics measuredStart = null;
for (int index = 0; index < totalChunks; index++) { for (int index = 0; index < totalChunks; index++) {
if (index == configuration.warmupChunks()) { if (index == configuration.warmupChunks()) {
measuredStartEpochMillis = System.currentTimeMillis(); measuredStartEpochMillis = System.currentTimeMillis();
measuredStart = WindowMetrics.capture(generator.engine(), options.counters());
} }
drainReported(); drainReported();
ChunkOutcome outcome = generateChunk(engine, index, coordinates.get(index), configuration.multicore()); ChunkOutcome outcome = generator.generate(index, coordinates.get(index), configuration.multicore());
outcome.failures().addAll(drainReported()); outcome.failures().addAll(drainReported());
sink.accept(outcome, index >= configuration.warmupChunks()); sink.accept(outcome, index >= configuration.warmupChunks());
printProgress(configuration, index + 1, totalChunks); printProgress(configuration, index + 1, totalChunks);
} }
printWindow(measuredStartEpochMillis, System.currentTimeMillis()); printWindow(measuredStartEpochMillis, System.currentTimeMillis());
printWindowMetrics(measuredStart, WindowMetrics.capture(generator.engine(), options.counters()),
configuration.measuredChunks(), options);
}
private static void printWindowMetrics(WindowMetrics start, WindowMetrics end, int chunks, ProbeOptions options) {
System.out.println(stagesLine(start, end, chunks, options.threads()));
if (options.counters()) {
System.out.println(countersLine(start, end, chunks));
}
} }
private static void printWindow(long startEpochMillis, long endEpochMillis) { private static void printWindow(long startEpochMillis, long endEpochMillis) {
@@ -571,8 +761,9 @@ public final class GenerationProbe {
+ " end_epoch_ms=" + endEpochMillis); + " end_epoch_ms=" + endEpochMillis);
} }
private static void generateParallel(Engine engine, ProbeConfiguration configuration, ProbeOptions options, private static void generateParallel(ProbeGenerator generator, ProbeConfiguration configuration,
List<ChunkCoordinate> coordinates, OutcomeSink sink) throws Exception { ProbeOptions options, List<ChunkCoordinate> coordinates,
OutcomeSink sink) throws Exception {
ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()]; ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()];
ExecutorService executor = Executors.newFixedThreadPool(options.threads()); ExecutorService executor = Executors.newFixedThreadPool(options.threads());
long warmupWallNanos; long warmupWallNanos;
@@ -585,7 +776,7 @@ public final class GenerationProbe {
warmup.add(index); warmup.add(index);
} }
long warmupStarted = System.nanoTime(); long warmupStarted = System.nanoTime();
runPhase(executor, engine, configuration, coordinates, warmup, outcomes); runPhase(executor, generator, configuration, coordinates, warmup, outcomes);
warmupWallNanos = System.nanoTime() - warmupStarted; warmupWallNanos = System.nanoTime() - warmupStarted;
unattributed.addAll(drainReported()); unattributed.addAll(drainReported());
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks on " System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks on "
@@ -593,11 +784,14 @@ public final class GenerationProbe {
List<Integer> measured = spiralOrder(coordinates, configuration.warmupChunks(), List<Integer> measured = spiralOrder(coordinates, configuration.warmupChunks(),
configuration.centerChunkX(), configuration.centerChunkZ()); configuration.centerChunkX(), configuration.centerChunkZ());
WindowMetrics measuredStart = WindowMetrics.capture(generator.engine(), options.counters());
long measuredStartEpochMillis = System.currentTimeMillis(); long measuredStartEpochMillis = System.currentTimeMillis();
long measuredStarted = System.nanoTime(); long measuredStarted = System.nanoTime();
runPhase(executor, engine, configuration, coordinates, measured, outcomes); runPhase(executor, generator, configuration, coordinates, measured, outcomes);
measuredWallNanos = System.nanoTime() - measuredStarted; measuredWallNanos = System.nanoTime() - measuredStarted;
printWindow(measuredStartEpochMillis, System.currentTimeMillis()); printWindow(measuredStartEpochMillis, System.currentTimeMillis());
printWindowMetrics(measuredStart, WindowMetrics.capture(generator.engine(), options.counters()),
configuration.measuredChunks(), options);
unattributed.addAll(drainReported()); unattributed.addAll(drainReported());
System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/" System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/"
+ configuration.measuredChunks()); + configuration.measuredChunks());
@@ -631,13 +825,13 @@ public final class GenerationProbe {
System.out.println(throughput.machineLine()); System.out.println(throughput.machineLine());
} }
private static void runPhase(ExecutorService executor, Engine engine, ProbeConfiguration configuration, private static void runPhase(ExecutorService executor, ProbeGenerator generator,
List<ChunkCoordinate> coordinates, List<Integer> order, ProbeConfiguration configuration, List<ChunkCoordinate> coordinates,
ChunkOutcome[] outcomes) throws Exception { List<Integer> order, ChunkOutcome[] outcomes) throws Exception {
List<Future<ChunkOutcome>> futures = new ArrayList<>(order.size()); List<Future<ChunkOutcome>> futures = new ArrayList<>(order.size());
for (int index : order) { for (int index : order) {
ChunkCoordinate coordinate = coordinates.get(index); ChunkCoordinate coordinate = coordinates.get(index);
futures.add(executor.submit(() -> generateChunk(engine, index, coordinate, configuration.multicore()))); futures.add(executor.submit(() -> generator.generate(index, coordinate, configuration.multicore())));
} }
for (Future<ChunkOutcome> future : futures) { for (Future<ChunkOutcome> future : futures) {
ChunkOutcome outcome; ChunkOutcome outcome;
@@ -657,21 +851,6 @@ public final class GenerationProbe {
} }
} }
private static ChunkOutcome generateChunk(Engine engine, int index, ChunkCoordinate coordinate,
boolean multicore) {
int height = engine.getTarget().getHeight();
List<Throwable> failures = new ArrayList<>();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, height, 16);
long started = System.nanoTime();
try {
engine.generate(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore);
} catch (Throwable e) {
failures.add(e);
}
return new ChunkOutcome(index, coordinate, System.nanoTime() - started, blocks, biomes, failures);
}
private static void printProgress(ProbeConfiguration configuration, int completed, int totalChunks) { private static void printProgress(ProbeConfiguration configuration, int completed, int totalChunks) {
if (completed == configuration.warmupChunks()) { if (completed == configuration.warmupChunks()) {
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks"); System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks");
@@ -878,6 +1057,72 @@ public final class GenerationProbe {
return nanos / 1_000_000D; return nanos / 1_000_000D;
} }
static final class ProbeGenerator {
private final Engine engine;
private final GenerationHistoryRuntimeRouter router;
private final boolean productionBuffers;
private final AtomicInteger expandedBiomeHunks = new AtomicInteger();
ProbeGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) {
this.engine = engine;
this.router = router;
this.productionBuffers = productionBuffers;
}
Engine engine() {
return engine;
}
int expandedBiomeHunks() {
return expandedBiomeHunks.get();
}
ChunkOutcome generate(int index, ChunkCoordinate coordinate, boolean multicore) {
int height = engine.getTarget().getHeight();
List<Throwable> failures = new ArrayList<>();
Hunk<NativeBlockState> blocks = newBlocks(height);
Hunk<NativeBiome> biomes = newBiomes(height);
long started = System.nanoTime();
try {
generateInto(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore);
} catch (Throwable e) {
failures.add(e);
}
long elapsed = System.nanoTime() - started;
if (biomes instanceof ProductionBiomeHunk production && production.expanded()) {
expandedBiomeHunks.incrementAndGet();
}
return new ChunkOutcome(index, coordinate, elapsed, blocks, biomes, failures);
}
Hunk<NativeBlockState> newBlocks(int height) {
return productionBuffers ? new ProductionBlockHunk(height) : Hunk.newArrayHunk(16, height, 16);
}
Hunk<NativeBiome> newBiomes(int height) {
return productionBuffers ? new ProductionBiomeHunk(height, ProbeGenerator::plainsBiome)
: Hunk.newArrayHunk(16, height, 16);
}
private void generateInto(int blockX, int blockZ, Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes,
boolean multicore) throws Exception {
if (router == null) {
engine.generate(blockX, blockZ, blocks, biomes, multicore);
return;
}
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = router.openCoordinateScope(blockX, blockZ);
GenerationSessionLease lease = engine.acquireGenerationLease("probe_terrain_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(blockX, blockZ, blocks, biomes, multicore);
historyScope.claimGeneratedSemantics();
}
}
private static NativeBiome plainsBiome() {
return IrisPlatforms.get().registries().biome("minecraft:plains");
}
}
private static final class OutcomeSink implements AutoCloseable { private static final class OutcomeSink implements AutoCloseable {
private final MessageDigest signature = sha256(); private final MessageDigest signature = sha256();
private final List<Long> measuredTimings; private final List<Long> measuredTimings;
@@ -0,0 +1,51 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.probe;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import java.util.List;
import java.util.Locale;
final class ProbeGenerationRegistry implements PlatformGenerationRegistry {
static final ProbeGenerationRegistry INSTANCE = new ProbeGenerationRegistry();
private ProbeGenerationRegistry() {
}
@Override
public String runtimeIdentity() {
return "iris-probe";
}
@Override
public List<String> legacyCustomBiomeResourceKeys(String packName, String dimensionKey, String customBiomeId) {
return List.of(dimensionKey.toLowerCase(Locale.ROOT) + ":" + customBiomeId.toLowerCase(Locale.ROOT));
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + dimensionTypeKey.toLowerCase(Locale.ROOT);
}
@Override
public Definition definition(String registryKey, String resourceKey) {
return Definition.resourceIdentity(registryKey, resourceKey);
}
}
@@ -0,0 +1,112 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.util.hunk.HunkMutationSupport;
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
import java.util.function.Supplier;
final class ProductionBiomeHunk extends StorageHunk<NativeBiome> {
private static final int CHUNK_SIZE = 16;
private final NativeBiome[] columns = new NativeBiome[CHUNK_SIZE * CHUNK_SIZE];
private final Supplier<NativeBiome> fallback;
private NativeBiome[] cells;
private volatile NativeBiome defaultBiome;
ProductionBiomeHunk(int height, Supplier<NativeBiome> fallback) {
super(CHUNK_SIZE, height, CHUNK_SIZE);
this.fallback = fallback;
}
@Override
public void set(int x1, int y1, int z1, int x2, int y2, int z2, NativeBiome biome) {
if (x1 == x2 && z1 == z2 && y1 == 0 && y2 == getHeight() - 1) {
fillColumn(x1, z1, biome);
return;
}
HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, biome);
}
@Override
public void setRaw(int x, int y, int z, NativeBiome biome) {
NativeBiome[] storage = cells == null ? expand() : cells;
storage[cellIndex(x, y, z)] = biome;
}
@Override
public NativeBiome getRaw(int x, int y, int z) {
NativeBiome[] storage = cells;
NativeBiome biome = storage == null ? columns[columnIndex(x, z)] : storage[cellIndex(x, y, z)];
if (biome != null) {
return biome;
}
NativeBiome resolved = defaultBiome;
if (resolved == null) {
resolved = fallback.get();
defaultBiome = resolved;
}
return resolved;
}
boolean expanded() {
return cells != null;
}
private void fillColumn(int x, int z, NativeBiome biome) {
NativeBiome[] storage = cells;
if (storage == null) {
columns[columnIndex(x, z)] = biome;
return;
}
int stride = CHUNK_SIZE * CHUNK_SIZE;
int index = columnIndex(x, z);
for (int y = 0; y < getHeight(); y++) {
storage[index] = biome;
index += stride;
}
}
private NativeBiome[] expand() {
NativeBiome[] storage = new NativeBiome[CHUNK_SIZE * getHeight() * CHUNK_SIZE];
int stride = CHUNK_SIZE * CHUNK_SIZE;
for (int column = 0; column < stride; column++) {
NativeBiome biome = columns[column];
if (biome == null) {
continue;
}
for (int index = column; index < storage.length; index += stride) {
storage[index] = biome;
}
}
cells = storage;
return storage;
}
private static int columnIndex(int x, int z) {
return ((z & (CHUNK_SIZE - 1)) * CHUNK_SIZE) + (x & (CHUNK_SIZE - 1));
}
private int cellIndex(int x, int y, int z) {
int clampedY = Math.max(0, Math.min(getHeight() - 1, y));
return (clampedY * CHUNK_SIZE * CHUNK_SIZE) + columnIndex(x, z);
}
}
@@ -0,0 +1,86 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.probe;
import art.arcane.iris.generation.block.BoundBlockState;
import art.arcane.iris.generation.chunk.ColumnExtent;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
final class ProductionBlockHunk extends StorageHunk<NativeBlockState> implements NativeBlockVolume, ColumnExtent {
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
private final NativeBlockState[] data;
private final int[] columnTops;
private final int zStride;
ProductionBlockHunk(int height) {
super(16, height, 16);
this.zStride = 16 * height;
this.data = new NativeBlockState[zStride * 16];
this.columnTops = new int[256];
Arrays.fill(columnTops, -1);
}
@Override
public void setRaw(int x, int y, int z, NativeBlockState t) {
if (t == null) {
return;
}
data[index(x, y, z)] = t;
int column = (z << 4) | x;
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
while (y > top) {
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
if (witness == top) {
break;
}
top = witness;
}
}
@Override
public NativeBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= getHeight()) {
return AIR.get();
}
NativeBlockState state = data[index(x, y, z)];
return state != null ? state : AIR.get();
}
@Override
public NativeBlockState getStoredRaw(int x, int y, int z) {
return data[index(x, y, z)];
}
@Override
public int highestStoredY(int x, int z) {
return (int) COLUMN_TOPS.getOpaque(columnTops, (z << 4) | x);
}
private int index(int x, int y, int z) {
return (z * zStride) + (y << 4) + x;
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel; import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.iris.spi.PlatformBiomeWriter; import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty; import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.entity.NativeEntityType; import art.arcane.volmlib.nativelib.entity.NativeEntityType;
@@ -78,7 +79,7 @@ public final class StubPlatform implements IrisPlatform {
return StubBlockState.of(key); return StubBlockState.of(key);
} }
private final StubRegistries registries = new StubRegistries(); private final StubRegistries registries;
private final StubScheduler scheduler = new StubScheduler(); private final StubScheduler scheduler = new StubScheduler();
private final StubStructureHooks structureHooks = new StubStructureHooks(); private final StubStructureHooks structureHooks = new StubStructureHooks();
private final StubBiomeWriter biomeWriter = new StubBiomeWriter(); private final StubBiomeWriter biomeWriter = new StubBiomeWriter();
@@ -89,7 +90,12 @@ public final class StubPlatform implements IrisPlatform {
} }
public StubPlatform(File dataFolder) { public StubPlatform(File dataFolder) {
this(dataFolder, null);
}
public StubPlatform(File dataFolder, PlatformGenerationRegistry generationRegistry) {
this.dataFolder = dataFolder; this.dataFolder = dataFolder;
this.registries = new StubRegistries(generationRegistry);
} }
private static final class StubBlockState implements NativeBlockState { private static final class StubBlockState implements NativeBlockState {
@@ -620,6 +626,20 @@ public final class StubPlatform implements IrisPlatform {
} }
private static final class StubRegistries implements PlatformRegistries { private static final class StubRegistries implements PlatformRegistries {
private final PlatformGenerationRegistry generationRegistry;
private StubRegistries(PlatformGenerationRegistry generationRegistry) {
this.generationRegistry = generationRegistry;
}
@Override
public PlatformGenerationRegistry generationRegistry() {
if (generationRegistry == null) {
return PlatformRegistries.super.generationRegistry();
}
return generationRegistry;
}
@Override @Override
public NativeBlockState decodeBlockState(String key) { public NativeBlockState decodeBlockState(String key) {
return blockOrNull(key); return blockOrNull(key);
@@ -1,15 +1,20 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyTileCache; import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.runtime.EngineMetrics;
import org.junit.Test; import org.junit.Test;
import java.io.File; import java.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
@@ -128,7 +133,96 @@ public final class GenerationProbeTest {
GenerationProbe.ProbeOptions defaults = GenerationProbe.ProbeOptions.fromSystemProperties(); GenerationProbe.ProbeOptions defaults = GenerationProbe.ProbeOptions.fromSystemProperties();
assertEquals(1, defaults.threads()); assertEquals(1, defaults.threads());
assertNull(defaults.dumpFile()); assertNull(defaults.dumpFile());
assertThrows(IllegalArgumentException.class, () -> new GenerationProbe.ProbeOptions(0, null, false)); assertThrows(IllegalArgumentException.class,
() -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false));
assertFalse(defaults.hydrologyPreplan());
assertFalse(defaults.historyRouter());
assertFalse(defaults.productionBuffers());
assertFalse(defaults.counters());
}
@Test
public void readsHarnessFidelityOptionsFromSystemProperties() {
System.setProperty("iris.probe.hydrologyPreplan", "true");
System.setProperty("iris.probe.historyRouter", "true");
System.setProperty("iris.probe.productionBuffers", "true");
System.setProperty("iris.generation.counters", "true");
try {
GenerationProbe.ProbeOptions options = GenerationProbe.ProbeOptions.fromSystemProperties();
assertTrue(options.hydrologyPreplan());
assertTrue(options.historyRouter());
assertTrue(options.productionBuffers());
assertTrue(options.counters());
assertFalse(options.parallel());
} finally {
System.clearProperty("iris.probe.hydrologyPreplan");
System.clearProperty("iris.probe.historyRouter");
System.clearProperty("iris.probe.productionBuffers");
System.clearProperty("iris.generation.counters");
}
}
@Test
public void stageLineReportsOnlyTheMeasuredWindow() {
Map<String, EngineMetrics.StageTotal> before = new LinkedHashMap<>();
before.put("total", new EngineMetrics.StageTotal(100D, 10L));
before.put("context.prefill.height", new EngineMetrics.StageTotal(0D, 0L));
Map<String, EngineMetrics.StageTotal> after = new LinkedHashMap<>();
after.put("total", new EngineMetrics.StageTotal(350.25D, 20L));
after.put("context.prefill.height", new EngineMetrics.StageTotal(12.5D, 10L));
String line = GenerationProbe.stagesLine(new GenerationProbe.WindowMetrics(before, Map.of()),
new GenerationProbe.WindowMetrics(after, Map.of()), 10, 1);
assertEquals("IRIS_GENPROBE_STAGES version=1 chunks=10 threads=1 total=250.250 context.prefill.height=12.500", line);
}
@Test
public void counterLineReportsWindowTotalsAndPerChunkRates() {
Map<String, Long> before = new LinkedHashMap<>();
before.put("generator_evaluations", 1_000L);
before.put("corner_starcasts", 0L);
Map<String, Long> after = new LinkedHashMap<>();
after.put("generator_evaluations", 5_000L);
after.put("corner_starcasts", 330L);
String line = GenerationProbe.countersLine(new GenerationProbe.WindowMetrics(Map.of(), before),
new GenerationProbe.WindowMetrics(Map.of(), after), 2);
assertEquals("IRIS_GENPROBE_COUNTERS version=1 chunks=2 generator_evaluations=4000"
+ " generator_evaluations_per_chunk=2000.0 corner_starcasts=330 corner_starcasts_per_chunk=165.0", line);
}
@Test
public void hydrologyPreplanCoversEveryTileAScheduledChunkComposesFromPlusTheNeighbourRing() {
GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
new File("/tmp/pack"), "overworld", 256, 1024, 2048, 2048, false, false, 1337L);
int tileSize = 512;
int publicationRadius = 96;
int reachChunks = 13;
List<HydrologyTileKey> keys = GenerationProbe.preplanTiles(configuration, tileSize, publicationRadius, reachChunks);
Set<HydrologyTileKey> planned = new HashSet<>(keys);
assertEquals(keys.size(), planned.size());
for (GenerationProbe.ChunkCoordinate chunk : GenerationProbe.scheduleCoordinates(1280, 2048, 2048)) {
for (int offsetX = -reachChunks; offsetX <= reachChunks; offsetX++) {
for (int offsetZ = -reachChunks; offsetZ <= reachChunks; offsetZ++) {
long minimumX = (long) (chunk.x() + offsetX) * 16L;
long minimumZ = (long) (chunk.z() + offsetZ) * 16L;
int firstX = Math.floorDiv(Math.toIntExact(minimumX - publicationRadius), tileSize) - 1;
int lastX = Math.floorDiv(Math.toIntExact(minimumX + 15L + publicationRadius), tileSize) + 1;
int firstZ = Math.floorDiv(Math.toIntExact(minimumZ - publicationRadius), tileSize) - 1;
int lastZ = Math.floorDiv(Math.toIntExact(minimumZ + 15L + publicationRadius), tileSize) + 1;
for (int tileX = firstX; tileX <= lastX; tileX++) {
for (int tileZ = firstZ; tileZ <= lastZ; tileZ++) {
assertTrue(planned.contains(new HydrologyTileKey(tileX, tileZ)));
}
}
}
}
}
HydrologyTileKey first = keys.getFirst();
assertEquals(new HydrologyTileKey(Math.floorDiv(2048 * 16 + 8, tileSize), Math.floorDiv(2048 * 16 + 8, tileSize)), first);
} }
@Test @Test
@@ -0,0 +1,126 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public final class ProductionHunkTest {
private static final int HEIGHT = 24;
@Test
public void blockHunkIgnoresNullWritesAndTracksTheHighestStoredCellPerColumn() {
ProductionBlockHunk blocks = new ProductionBlockHunk(HEIGHT);
NativeBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
NativeBlockState dirt = StubPlatform.blockStateForTest("minecraft:dirt");
blocks.setRaw(3, 5, 7, stone);
blocks.setRaw(3, 9, 7, dirt);
blocks.setRaw(3, 2, 7, stone);
blocks.setRaw(3, 9, 7, null);
assertSame(dirt, blocks.getStoredRaw(3, 9, 7));
assertSame(stone, blocks.getStoredRaw(3, 5, 7));
assertNull(blocks.getStoredRaw(3, 6, 7));
assertEquals(9, blocks.highestStoredY(3, 7));
assertEquals(-1, blocks.highestStoredY(7, 3));
}
@Test
public void dumpCaptureOfTheBlockHunkMatchesAnArrayHunkHoldingTheSameWrites() {
ProductionBlockHunk production = new ProductionBlockHunk(HEIGHT);
Hunk<NativeBlockState> array = Hunk.newArrayHunk(16, HEIGHT, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, HEIGHT, 16);
NativeBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
NativeBlockState water = StubPlatform.blockStateForTest("minecraft:water");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int top = (x * 7 + z * 3) % HEIGHT;
for (int y = 0; y <= top; y++) {
NativeBlockState state = y == top ? water : stone;
production.setRaw(x, y, z, state);
array.setRaw(x, y, z, state);
}
}
}
ChunkDump.ChunkSnapshot fromProduction = ChunkDump.capture(4, -9, production, biomes, HEIGHT);
ChunkDump.ChunkSnapshot fromArray = ChunkDump.capture(4, -9, array, biomes, HEIGHT);
assertArrayEquals(fromArray.blockPalette(), fromProduction.blockPalette());
assertArrayEquals(fromArray.blocks(), fromProduction.blocks());
assertTrue(List.of(fromProduction.blockPalette()).contains(ChunkDump.NULL_BLOCK_KEY));
}
@Test
public void biomeHunkKeepsWholeColumnWritesPerColumnAndReadsLikeAnArrayHunk() {
ProductionBiomeHunk production = new ProductionBiomeHunk(HEIGHT, () -> biome("minecraft:plains"));
Hunk<NativeBiome> array = Hunk.newArrayHunk(16, HEIGHT, 16);
NativeBiome forest = biome("minecraft:forest");
NativeBiome desert = biome("minecraft:desert");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
NativeBiome column = ((x + z) & 1) == 0 ? forest : desert;
production.set(x, 0, z, x, HEIGHT - 1, z, column);
array.set(x, 0, z, x, HEIGHT - 1, z, column);
}
}
assertFalse(production.expanded());
assertSameCells(array, production);
production.setRaw(2, 4, 9, desert);
array.setRaw(2, 4, 9, desert);
production.set(5, 0, 5, 5, HEIGHT - 1, 5, desert);
array.set(5, 0, 5, 5, HEIGHT - 1, 5, desert);
assertTrue(production.expanded());
assertSameCells(array, production);
}
@Test
public void biomeHunkReadsTheFallbackOnlyForUnwrittenCells() {
NativeBiome plains = biome("minecraft:plains");
ProductionBiomeHunk production = new ProductionBiomeHunk(HEIGHT, () -> plains);
NativeBiome forest = biome("minecraft:forest");
production.set(1, 0, 1, 1, HEIGHT - 1, 1, forest);
assertSame(forest, production.getRaw(1, HEIGHT - 1, 1));
assertSame(plains, production.getRaw(0, 0, 0));
}
private static void assertSameCells(Hunk<NativeBiome> expected, Hunk<NativeBiome> actual) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < HEIGHT; y++) {
assertSame(expected.get(x, y, z), actual.get(x, y, z));
}
}
}
}
private static NativeBiome biome(String key) {
return new TestBiome(key);
}
private record TestBiome(String key) implements NativeBiome {
@Override
public String namespace() {
return key.substring(0, key.indexOf(':'));
}
@Override
public Object nativeHandle() {
return key;
}
}
}