From 18ba6e32ac07b56fb7da0412e8ffbb7cb4361451 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 22 Oct 2026 07:28:44 -0400 Subject: [PATCH] Probe history digest - Adds an opt-in digest of semantic and saved biome records per chunk --- probe/build.gradle | 1 + .../arcane/iris/probe/GenerationProbe.java | 11 ++- .../art/arcane/iris/probe/HistoryDigest.java | 86 +++++++++++++++++++ .../iris/probe/ProbeChunkGenerator.java | 16 +++- .../iris/probe/GenerationProbeTest.java | 2 +- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java diff --git a/probe/build.gradle b/probe/build.gradle index 926526037..66fa6b246 100644 --- a/probe/build.gradle +++ b/probe/build.gradle @@ -180,6 +180,7 @@ tasks.register('genProbe', JavaExec) { 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.historyDigest', providers.gradleProperty('probeHistoryDigest').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) diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index 81b78ae28..c1014314a 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -177,7 +177,7 @@ public final class GenerationProbe { } 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 { if (threads < 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.historyRouter"), booleanProperty("iris.probe.productionBuffers"), - booleanProperty("iris.generation.counters")); + booleanProperty("iris.generation.counters"), + booleanProperty("iris.probe.historyDigest")); } private static boolean booleanProperty(String key) { @@ -437,7 +438,8 @@ public final class GenerationProbe { + " 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()); - ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers()); + ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers(), + options.historyDigest()); long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount(); GenerationResult generation = generateWithProfile(generator, configuration, options); verifyBackend(nativeSamplesBefore); @@ -445,6 +447,9 @@ public final class GenerationProbe { System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks=" + generator.expandedBiomeHunks()); } + if (generator.historySummary() != null) { + System.out.println(generator.historySummary()); + } String status = generation.failedChunks() == 0 ? "PASS" : "FAIL"; printGenerationFailures(generation); return new ProbeResult( diff --git a/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java b/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java new file mode 100644 index 000000000..ca0724703 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java @@ -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 . + */ + +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 chunks = new ConcurrentSkipListMap<>(); + + HistoryDigest(GenerationHistoryRuntimeRouter router) { + this.router = router; + } + + void record(int chunkX, int chunkZ) throws IOException { + StringBuilder canonical = new StringBuilder(4096); + Optional semantics = router.history().semantics(chunkX, chunkZ); + canonical.append(semantics.map(value -> value + "|" + value.pointsOfInterest()).orElse("none")).append('\n'); + Optional 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 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); + } + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java index 64db59352..5e3b7d6d4 100644 --- a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java +++ b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java @@ -36,12 +36,19 @@ final class ProbeChunkGenerator { private final Engine engine; private final GenerationHistoryRuntimeRouter router; private final boolean productionBuffers; + private final HistoryDigest historyDigest; 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.router = router; this.productionBuffers = productionBuffers; + this.historyDigest = historyDigest && router != null ? new HistoryDigest(router) : null; + } + + String historySummary() { + return historyDigest == null ? null : historyDigest.summary(); } Engine engine() { @@ -64,6 +71,13 @@ final class ProbeChunkGenerator { failures.add(e); } 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()) { expandedBiomeHunks.incrementAndGet(); } diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java index 54d240378..ee72fe4f9 100644 --- a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java +++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java @@ -134,7 +134,7 @@ public final class GenerationProbeTest { assertEquals(1, defaults.threads()); assertNull(defaults.dumpFile()); 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.historyRouter()); assertFalse(defaults.productionBuffers());