Split probe helpers

- Move chunk generation and buffer selection into ProbeChunkGenerator
- Move measured-window stage and counter lines into ProbeWindowMetrics
- Move hydrology pre-planning into ProbeHydrologyPreplan
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 19:45:12 -04:00
1 parent b6b01c88f5
commit 217ed04f45
5 files changed
+298 -202

No files matched your search

@@ -22,26 +22,20 @@ 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;
@@ -53,7 +47,6 @@ 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;
@@ -72,12 +65,12 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; 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<>());
@@ -278,29 +271,6 @@ 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,
@@ -329,51 +299,6 @@ 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;
@@ -508,7 +433,7 @@ public final class GenerationProbe {
+ " 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() System.out.println("IRIS_GENPROBE_MANTLE version=1 radius=" + engine.getMantle().getRadius()
+ " real_radius=" + engine.getMantle().getRealRadius()); + " real_radius=" + engine.getMantle().getRealRadius());
ProbeGenerator generator = new ProbeGenerator(engine, router, options.productionBuffers()); ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers());
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount(); long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
GenerationResult generation = generateWithProfile(generator, configuration, options); GenerationResult generation = generateWithProfile(generator, configuration, options);
verifyBackend(nativeSamplesBefore); verifyBackend(nativeSamplesBefore);
@@ -598,7 +523,7 @@ public final class GenerationProbe {
System.out.println("[genprobe] offline pack validation: PASS"); System.out.println("[genprobe] offline pack validation: PASS");
} }
private static GenerationResult generateWithProfile(ProbeGenerator generator, ProbeConfiguration configuration, private static GenerationResult generateWithProfile(ProbeChunkGenerator generator, ProbeConfiguration configuration,
ProbeOptions options) throws Exception { ProbeOptions options) throws Exception {
Engine engine = generator.engine(); Engine engine = generator.engine();
if (!options.pregenProfile()) { if (!options.pregenProfile()) {
@@ -621,48 +546,10 @@ public final class GenerationProbe {
if (!options.hydrologyPreplan()) { if (!options.hydrologyPreplan()) {
return; return;
} }
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime(); Optional<ProbeHydrologyPreplan> preplan = ProbeHydrologyPreplan.run(engine, configuration);
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()); printDistinctCauses("hydrology preplan reported errors", drainReported());
System.out.println(preplan.machineLine()); System.out.println(preplan.map(ProbeHydrologyPreplan::machineLine)
} .orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology"));
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) {
@@ -708,7 +595,7 @@ 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(ProbeGenerator generator, ProbeConfiguration configuration, private static GenerationResult generate(ProbeChunkGenerator generator, ProbeConfiguration configuration,
ProbeOptions options) throws Exception { ProbeOptions options) throws Exception {
Engine engine = generator.engine(); Engine engine = generator.engine();
int totalChunks = configuration.warmupChunks() + configuration.measuredChunks(); int totalChunks = configuration.warmupChunks() + configuration.measuredChunks();
@@ -727,16 +614,16 @@ public final class GenerationProbe {
} }
} }
private static void generateSequential(ProbeGenerator generator, ProbeConfiguration configuration, private static void generateSequential(ProbeChunkGenerator generator, ProbeConfiguration configuration,
ProbeOptions options, List<ChunkCoordinate> coordinates, ProbeOptions options, List<ChunkCoordinate> coordinates,
OutcomeSink sink) throws IOException { OutcomeSink sink) throws IOException {
int totalChunks = coordinates.size(); int totalChunks = coordinates.size();
long measuredStartEpochMillis = 0L; long measuredStartEpochMillis = 0L;
WindowMetrics measuredStart = null; ProbeWindowMetrics 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()); measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
} }
drainReported(); drainReported();
ChunkOutcome outcome = generator.generate(index, coordinates.get(index), configuration.multicore()); ChunkOutcome outcome = generator.generate(index, coordinates.get(index), configuration.multicore());
@@ -745,14 +632,15 @@ public final class GenerationProbe {
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()), printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
configuration.measuredChunks(), options); configuration.measuredChunks(), options);
} }
private static void printWindowMetrics(WindowMetrics start, WindowMetrics end, int chunks, ProbeOptions options) { private static void printWindowMetrics(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks,
System.out.println(stagesLine(start, end, chunks, options.threads())); ProbeOptions options) {
System.out.println(ProbeWindowMetrics.stagesLine(start, end, chunks, options.threads()));
if (options.counters()) { if (options.counters()) {
System.out.println(countersLine(start, end, chunks)); System.out.println(ProbeWindowMetrics.countersLine(start, end, chunks));
} }
} }
@@ -761,7 +649,7 @@ public final class GenerationProbe {
+ " end_epoch_ms=" + endEpochMillis); + " end_epoch_ms=" + endEpochMillis);
} }
private static void generateParallel(ProbeGenerator generator, ProbeConfiguration configuration, private static void generateParallel(ProbeChunkGenerator generator, ProbeConfiguration configuration,
ProbeOptions options, List<ChunkCoordinate> coordinates, ProbeOptions options, List<ChunkCoordinate> coordinates,
OutcomeSink sink) throws Exception { OutcomeSink sink) throws Exception {
ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()]; ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()];
@@ -784,13 +672,13 @@ 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()); ProbeWindowMetrics measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
long measuredStartEpochMillis = System.currentTimeMillis(); long measuredStartEpochMillis = System.currentTimeMillis();
long measuredStarted = System.nanoTime(); long measuredStarted = System.nanoTime();
runPhase(executor, generator, 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()), printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
configuration.measuredChunks(), options); 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() + "/"
@@ -825,7 +713,7 @@ public final class GenerationProbe {
System.out.println(throughput.machineLine()); System.out.println(throughput.machineLine());
} }
private static void runPhase(ExecutorService executor, ProbeGenerator generator, private static void runPhase(ExecutorService executor, ProbeChunkGenerator generator,
ProbeConfiguration configuration, List<ChunkCoordinate> coordinates, ProbeConfiguration configuration, List<ChunkCoordinate> coordinates,
List<Integer> order, 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());
@@ -1057,72 +945,6 @@ 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,98 @@
/*
* 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.context.IrisContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
final class ProbeChunkGenerator {
private final Engine engine;
private final GenerationHistoryRuntimeRouter router;
private final boolean productionBuffers;
private final AtomicInteger expandedBiomeHunks = new AtomicInteger();
ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) {
this.engine = engine;
this.router = router;
this.productionBuffers = productionBuffers;
}
Engine engine() {
return engine;
}
int expandedBiomeHunks() {
return expandedBiomeHunks.get();
}
GenerationProbe.ChunkOutcome generate(int index, GenerationProbe.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 GenerationProbe.ChunkOutcome(index, coordinate, elapsed, blocks, biomes, failures);
}
private Hunk<NativeBlockState> newBlocks(int height) {
return productionBuffers ? new ProductionBlockHunk(height) : Hunk.newArrayHunk(16, height, 16);
}
private Hunk<NativeBiome> newBiomes(int height) {
return productionBuffers ? new ProductionBiomeHunk(height, ProbeChunkGenerator::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");
}
}
@@ -0,0 +1,109 @@
/*
* 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.hydrology.HydrologyPlannerSettings;
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.runtime.Engine;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) {
private static final int PLANNING_THREADS = 4;
static Optional<ProbeHydrologyPreplan> run(Engine engine, GenerationProbe.ProbeConfiguration configuration)
throws Exception {
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
if (hydrology == null) {
return Optional.empty();
}
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 = tiles(configuration, tileSize, publicationRadius, reachChunks);
long started = System.nanoTime();
ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), PLANNING_THREADS));
try {
List<Future<?>> plans = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
plans.add(executor.submit(() -> hydrology.tile(key)));
}
for (Future<?> plan : plans) {
await(plan);
}
} finally {
executor.shutdown();
}
return Optional.of(new ProbeHydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks,
System.nanoTime() - started));
}
static List<HydrologyTileKey> tiles(GenerationProbe.ProbeConfiguration configuration, int tileSize,
int publicationRadius, int reachChunks) {
HydrologyTileCache.PregenerationArea area = GenerationProbe.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;
}
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, elapsedNanos / 1_000_000D);
}
private static void await(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);
}
}
}
@@ -0,0 +1,67 @@
/*
* 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.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.GenerationCounter;
import art.arcane.volmlib.util.cache.CacheContention;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
record ProbeWindowMetrics(Map<String, EngineMetrics.StageTotal> stages, Map<String, Long> counters) {
static ProbeWindowMetrics capture(Engine engine, boolean counters) {
return new ProbeWindowMetrics(engine.getMetrics().stageTotals(), counters ? counterValues() : Map.of());
}
static String stagesLine(ProbeWindowMetrics start, ProbeWindowMetrics 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(ProbeWindowMetrics start, ProbeWindowMetrics 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();
}
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;
}
}
@@ -171,8 +171,8 @@ public final class GenerationProbeTest {
after.put("total", new EngineMetrics.StageTotal(350.25D, 20L)); after.put("total", new EngineMetrics.StageTotal(350.25D, 20L));
after.put("context.prefill.height", new EngineMetrics.StageTotal(12.5D, 10L)); after.put("context.prefill.height", new EngineMetrics.StageTotal(12.5D, 10L));
String line = GenerationProbe.stagesLine(new GenerationProbe.WindowMetrics(before, Map.of()), String line = ProbeWindowMetrics.stagesLine(new ProbeWindowMetrics(before, Map.of()),
new GenerationProbe.WindowMetrics(after, Map.of()), 10, 1); new ProbeWindowMetrics(after, Map.of()), 10, 1);
assertEquals("IRIS_GENPROBE_STAGES version=1 chunks=10 threads=1 total=250.250 context.prefill.height=12.500", line); assertEquals("IRIS_GENPROBE_STAGES version=1 chunks=10 threads=1 total=250.250 context.prefill.height=12.500", line);
} }
@@ -186,8 +186,8 @@ public final class GenerationProbeTest {
after.put("generator_evaluations", 5_000L); after.put("generator_evaluations", 5_000L);
after.put("corner_starcasts", 330L); after.put("corner_starcasts", 330L);
String line = GenerationProbe.countersLine(new GenerationProbe.WindowMetrics(Map.of(), before), String line = ProbeWindowMetrics.countersLine(new ProbeWindowMetrics(Map.of(), before),
new GenerationProbe.WindowMetrics(Map.of(), after), 2); new ProbeWindowMetrics(Map.of(), after), 2);
assertEquals("IRIS_GENPROBE_COUNTERS version=1 chunks=2 generator_evaluations=4000" 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); + " generator_evaluations_per_chunk=2000.0 corner_starcasts=330 corner_starcasts_per_chunk=165.0", line);
@@ -200,7 +200,7 @@ public final class GenerationProbeTest {
int tileSize = 512; int tileSize = 512;
int publicationRadius = 96; int publicationRadius = 96;
int reachChunks = 13; int reachChunks = 13;
List<HydrologyTileKey> keys = GenerationProbe.preplanTiles(configuration, tileSize, publicationRadius, reachChunks); List<HydrologyTileKey> keys = ProbeHydrologyPreplan.tiles(configuration, tileSize, publicationRadius, reachChunks);
Set<HydrologyTileKey> planned = new HashSet<>(keys); Set<HydrologyTileKey> planned = new HashSet<>(keys);
assertEquals(keys.size(), planned.size()); assertEquals(keys.size(), planned.size());