Probe history digest

- Adds an opt-in digest of semantic and saved biome records per chunk
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 11:05:58 -04:00
1 parent f7f9f5f651
commit 18ba6e32ac
5 files changed
+111 -5

No files matched your search

+1
View File
@@ -180,6 +180,7 @@ tasks.register('genProbe', JavaExec) {
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.hydrologyPreplan', providers.gradleProperty('probeHydrologyPreplan').orElse('false').get())
systemProperty('iris.probe.historyRouter', providers.gradleProperty('probeHistoryRouter').orElse('false').get()) systemProperty('iris.probe.historyRouter', providers.gradleProperty('probeHistoryRouter').orElse('false').get())
systemProperty('iris.probe.historyDigest', providers.gradleProperty('probeHistoryDigest').orElse('false').get())
systemProperty('iris.probe.productionBuffers', providers.gradleProperty('probeProductionBuffers').orElse('false').get()) systemProperty('iris.probe.productionBuffers', providers.gradleProperty('probeProductionBuffers').orElse('false').get())
String probeCounters = providers.gradleProperty('probeCounters').orElse('false').get() String probeCounters = providers.gradleProperty('probeCounters').orElse('false').get()
systemProperty('iris.generation.counters', probeCounters) systemProperty('iris.generation.counters', probeCounters)
@@ -177,7 +177,7 @@ public final class GenerationProbe {
} }
record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile, boolean hydrologyPreplan, record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile, boolean hydrologyPreplan,
boolean historyRouter, boolean productionBuffers, boolean counters) { boolean historyRouter, boolean productionBuffers, boolean counters, boolean historyDigest) {
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.");
@@ -196,7 +196,8 @@ public final class GenerationProbe {
booleanProperty("iris.probe.hydrologyPreplan"), booleanProperty("iris.probe.hydrologyPreplan"),
booleanProperty("iris.probe.historyRouter"), booleanProperty("iris.probe.historyRouter"),
booleanProperty("iris.probe.productionBuffers"), booleanProperty("iris.probe.productionBuffers"),
booleanProperty("iris.generation.counters")); booleanProperty("iris.generation.counters"),
booleanProperty("iris.probe.historyDigest"));
} }
private static boolean booleanProperty(String key) { private static boolean booleanProperty(String key) {
@@ -437,7 +438,8 @@ 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());
ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers()); ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers(),
options.historyDigest());
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount(); long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
GenerationResult generation = generateWithProfile(generator, configuration, options); GenerationResult generation = generateWithProfile(generator, configuration, options);
verifyBackend(nativeSamplesBefore); verifyBackend(nativeSamplesBefore);
@@ -445,6 +447,9 @@ public final class GenerationProbe {
System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks=" System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks="
+ generator.expandedBiomeHunks()); + generator.expandedBiomeHunks());
} }
if (generator.historySummary() != null) {
System.out.println(generator.historySummary());
}
String status = generation.failedChunks() == 0 ? "PASS" : "FAIL"; String status = generation.failedChunks() == 0 ? "PASS" : "FAIL";
printGenerationFailures(generation); printGenerationFailures(generation);
return new ProbeResult( return new ProbeResult(
@@ -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.world.history.ChunkGenerationSemantics;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedBiomeChunk;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentSkipListMap;
final class HistoryDigest {
private final GenerationHistoryRuntimeRouter router;
private final Map<Long, byte[]> chunks = new ConcurrentSkipListMap<>();
HistoryDigest(GenerationHistoryRuntimeRouter router) {
this.router = router;
}
void record(int chunkX, int chunkZ) throws IOException {
StringBuilder canonical = new StringBuilder(4096);
Optional<ChunkGenerationSemantics> semantics = router.history().semantics(chunkX, chunkZ);
canonical.append(semantics.map(value -> value + "|" + value.pointsOfInterest()).orElse("none")).append('\n');
Optional<SavedBiomeChunk> biomes = router.biomes().snapshot(chunkX, chunkZ);
if (biomes.isEmpty()) {
canonical.append("none");
} else {
SavedBiomeChunk chunk = biomes.get();
canonical.append(chunk.header());
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
canonical.append('\n').append(chunk.column(localX, localZ));
}
}
}
chunks.put(((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL), sha256(canonical.toString().getBytes(StandardCharsets.UTF_8)));
}
String summary() {
MessageDigest digest = newDigest();
ByteBuffer key = ByteBuffer.allocate(Long.BYTES);
for (Map.Entry<Long, byte[]> entry : chunks.entrySet()) {
key.clear();
key.putLong(entry.getKey());
digest.update(key.array());
digest.update(entry.getValue());
}
return "IRIS_GENPROBE_HISTORY version=1 chunks=" + chunks.size()
+ " digest=" + HexFormat.of().formatHex(digest.digest(), 0, 8);
}
private static byte[] sha256(byte[] bytes) {
return newDigest().digest(bytes);
}
private static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException missing) {
throw new IllegalStateException("SHA-256 is unavailable", missing);
}
}
}
@@ -36,12 +36,19 @@ final class ProbeChunkGenerator {
private final Engine engine; private final Engine engine;
private final GenerationHistoryRuntimeRouter router; private final GenerationHistoryRuntimeRouter router;
private final boolean productionBuffers; private final boolean productionBuffers;
private final HistoryDigest historyDigest;
private final AtomicInteger expandedBiomeHunks = new AtomicInteger(); private final AtomicInteger expandedBiomeHunks = new AtomicInteger();
ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) { ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers,
boolean historyDigest) {
this.engine = engine; this.engine = engine;
this.router = router; this.router = router;
this.productionBuffers = productionBuffers; this.productionBuffers = productionBuffers;
this.historyDigest = historyDigest && router != null ? new HistoryDigest(router) : null;
}
String historySummary() {
return historyDigest == null ? null : historyDigest.summary();
} }
Engine engine() { Engine engine() {
@@ -64,6 +71,13 @@ final class ProbeChunkGenerator {
failures.add(e); failures.add(e);
} }
long elapsed = System.nanoTime() - started; long elapsed = System.nanoTime() - started;
if (historyDigest != null && failures.isEmpty()) {
try {
historyDigest.record(coordinate.x(), coordinate.z());
} catch (Throwable e) {
failures.add(e);
}
}
if (biomes instanceof ProductionBiomeHunk production && production.expanded()) { if (biomes instanceof ProductionBiomeHunk production && production.expanded()) {
expandedBiomeHunks.incrementAndGet(); expandedBiomeHunks.incrementAndGet();
} }
@@ -134,7 +134,7 @@ public final class GenerationProbeTest {
assertEquals(1, defaults.threads()); assertEquals(1, defaults.threads());
assertNull(defaults.dumpFile()); assertNull(defaults.dumpFile());
assertThrows(IllegalArgumentException.class, assertThrows(IllegalArgumentException.class,
() -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false)); () -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false, false));
assertFalse(defaults.hydrologyPreplan()); assertFalse(defaults.hydrologyPreplan());
assertFalse(defaults.historyRouter()); assertFalse(defaults.historyRouter());
assertFalse(defaults.productionBuffers()); assertFalse(defaults.productionBuffers());