mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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:
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)) {
|
||||||
|
|||||||
+47
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user