Added probe parity oracle

- Dump every genProbe chunk as palette-indexed blocks and biomes with -PprobeDumpFile
- Add genDiff task comparing two dumps by coordinate with block, surface, biome and category parity
- Generate measured chunks on N threads in spiral order with -PprobeThreads and report wall-clock cps
- Mirror pregeneration with -PprobePregenProfile (pregen performance profile and hydrology prefetch scope)
- Print the measured window for JFR attribution and accept -PprobeJfrSettings
- Keep chunk hashes and the run signature byte-identical to the sequential probe
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 18:47:25 -04:00
1 parent c3359e0a16
commit e2430500cf
6 files changed
+1356 -82

No files matched your search

@@ -0,0 +1,310 @@
/*
* 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.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public final class ChunkDump {
static final long MAGIC = 0x4952495344554D50L;
static final int VERSION = 1;
static final String NULL_BLOCK_KEY = "minecraft:air";
static final String NULL_BIOME_KEY = "null";
private static final int MAXIMUM_PALETTE = 65_535;
private static final int STREAM_BUFFER = 1 << 20;
private ChunkDump() {
}
record Header(String dimensionKey, long seed, int minY, int height) {
}
record ChunkSnapshot(int chunkX, int chunkZ, int height,
String[] blockPalette, char[] blocks,
String[] biomePalette, char[] biomes) {
ChunkSnapshot {
int expected = 256 * height;
if (blocks.length != expected || biomes.length != expected) {
throw new IllegalArgumentException("Chunk " + chunkX + "," + chunkZ + " expected " + expected
+ " entries but held " + blocks.length + " blocks and " + biomes.length + " biomes.");
}
}
static int index(int localX, int localZ, int y, int height) {
return ((localX << 4) | localZ) * height + y;
}
long coordinateKey() {
return key(chunkX, chunkZ);
}
static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
}
}
static ChunkSnapshot capture(int chunkX, int chunkZ, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, int height) {
int size = 256 * height;
char[] blockIndices = new char[size];
char[] biomeIndices = new char[size];
PaletteBuilder blockPalette = new PaletteBuilder();
PaletteBuilder biomePalette = new PaletteBuilder();
int index = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
NativeBlockState state = blocks.get(x, y, z);
NativeBiome biome = biomes.get(x, y, z);
blockIndices[index] = blockPalette.indexOf(state, state == null ? NULL_BLOCK_KEY : null);
biomeIndices[index] = biomePalette.indexOf(biome, biome == null ? NULL_BIOME_KEY : null);
index++;
}
}
}
return new ChunkSnapshot(chunkX, chunkZ, height, blockPalette.keys(), blockIndices,
biomePalette.keys(), biomeIndices);
}
static final class Writer implements AutoCloseable {
private final DataOutputStream output;
private boolean closed;
Writer(Path file, Header header) throws IOException {
this.output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(
Files.newOutputStream(file, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE), STREAM_BUFFER), STREAM_BUFFER));
output.writeLong(MAGIC);
output.writeInt(VERSION);
output.writeUTF(header.dimensionKey());
output.writeLong(header.seed());
output.writeInt(header.minY());
output.writeInt(header.height());
}
void write(ChunkSnapshot snapshot) throws IOException {
output.writeByte(1);
output.writeInt(snapshot.chunkX());
output.writeInt(snapshot.chunkZ());
writePalette(snapshot.blockPalette(), snapshot.blocks());
writePalette(snapshot.biomePalette(), snapshot.biomes());
}
private void writePalette(String[] palette, char[] indices) throws IOException {
output.writeInt(palette.length);
for (String key : palette) {
output.writeUTF(key);
}
if (palette.length <= 256) {
byte[] encoded = new byte[indices.length];
for (int index = 0; index < indices.length; index++) {
encoded[index] = (byte) indices[index];
}
output.write(encoded);
return;
}
byte[] encoded = new byte[indices.length * 2];
for (int index = 0; index < indices.length; index++) {
char value = indices[index];
encoded[index * 2] = (byte) (value >>> 8);
encoded[index * 2 + 1] = (byte) value;
}
output.write(encoded);
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
try {
output.writeByte(0);
} finally {
output.close();
}
}
}
static final class Reader implements AutoCloseable {
private final DataInputStream input;
private final Header header;
private boolean finished;
Reader(Path file) throws IOException {
this.input = new DataInputStream(new BufferedInputStream(
new GZIPInputStream(Files.newInputStream(file), STREAM_BUFFER), STREAM_BUFFER));
long magic = input.readLong();
if (magic != MAGIC) {
input.close();
throw new IOException(file + " is not an Iris chunk dump.");
}
int version = input.readInt();
if (version != VERSION) {
input.close();
throw new IOException(file + " uses dump version " + version + ", expected " + VERSION + ".");
}
this.header = new Header(input.readUTF(), input.readLong(), input.readInt(), input.readInt());
if (header.height() < 1) {
input.close();
throw new IOException(file + " declares a non-positive height " + header.height() + ".");
}
}
Header header() {
return header;
}
ChunkSnapshot next() throws IOException {
if (finished) {
return null;
}
int marker;
try {
marker = input.readUnsignedByte();
} catch (EOFException truncated) {
throw new IOException("Chunk dump ended without its terminator.", truncated);
}
if (marker == 0) {
finished = true;
return null;
}
if (marker != 1) {
throw new IOException("Chunk dump holds an unknown record marker " + marker + ".");
}
int chunkX = input.readInt();
int chunkZ = input.readInt();
int size = 256 * header.height();
String[] blockPalette = readPalette();
char[] blocks = readIndices(blockPalette.length, size);
String[] biomePalette = readPalette();
char[] biomes = readIndices(biomePalette.length, size);
return new ChunkSnapshot(chunkX, chunkZ, header.height(), blockPalette, blocks, biomePalette, biomes);
}
private String[] readPalette() throws IOException {
int length = input.readInt();
if (length < 1 || length > MAXIMUM_PALETTE) {
throw new IOException("Chunk dump palette length " + length + " is out of range.");
}
String[] palette = new String[length];
for (int index = 0; index < length; index++) {
palette[index] = input.readUTF();
}
return palette;
}
private char[] readIndices(int paletteLength, int size) throws IOException {
char[] indices = new char[size];
if (paletteLength <= 256) {
byte[] encoded = new byte[size];
input.readFully(encoded);
for (int index = 0; index < size; index++) {
indices[index] = (char) (encoded[index] & 0xFF);
}
} else {
byte[] encoded = new byte[size * 2];
input.readFully(encoded);
for (int index = 0; index < size; index++) {
indices[index] = (char) (((encoded[index * 2] & 0xFF) << 8) | (encoded[index * 2 + 1] & 0xFF));
}
}
for (char value : indices) {
if (value >= paletteLength) {
throw new IOException("Chunk dump index " + (int) value + " exceeds palette length " + paletteLength + ".");
}
}
return indices;
}
@Override
public void close() throws IOException {
input.close();
}
}
private static final class PaletteBuilder {
private final IdentityHashMap<Object, Character> byIdentity = new IdentityHashMap<>();
private final Map<String, Character> byKey = new HashMap<>();
private final List<String> keys = new ArrayList<>();
private Object lastValue;
private char lastIndex;
private boolean hasLast;
char indexOf(Object value, String nullKey) {
if (hasLast && value == lastValue) {
return lastIndex;
}
Character cached = value == null ? null : byIdentity.get(value);
char resolved;
if (cached != null) {
resolved = cached;
} else {
String key = value == null ? nullKey : keyOf(value);
Character existing = byKey.get(key);
if (existing != null) {
resolved = existing;
} else {
if (keys.size() >= MAXIMUM_PALETTE) {
throw new IllegalStateException("Chunk palette exceeds " + MAXIMUM_PALETTE + " entries.");
}
resolved = (char) keys.size();
keys.add(key);
byKey.put(key, resolved);
}
if (value != null) {
byIdentity.put(value, resolved);
}
}
lastValue = value;
lastIndex = resolved;
hasLast = true;
return resolved;
}
String[] keys() {
return keys.toArray(new String[0]);
}
private static String keyOf(Object value) {
if (value instanceof NativeBlockState state) {
return state.key();
}
return ((NativeBiome) value).key();
}
}
}
@@ -0,0 +1,424 @@
/*
* 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 java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public final class GenerationDiff {
private static final String LOG_PREFIX = "[gendiff]";
private static final int DEFAULT_WORST_CHUNKS = 10;
private static final Set<String> AIR = Set.of("air", "cave_air", "void_air");
private static final Set<String> FLUID = Set.of("water", "lava", "bubble_column");
private static final Set<String> ORE_BLOCKS = Set.of("ancient_debris", "raw_iron_block", "raw_copper_block",
"raw_gold_block");
private static final Set<String> DECOR_EXACT = Set.of("snow", "fire", "grass", "short_grass", "tall_grass",
"farmland", "wheat", "turtle_egg", "lily_pad", "hay_block");
private static final List<String> DECOR_FRAGMENTS = List.of("log", "wood", "leaves", "hyphae", "stem",
"sapling", "propagule", "flower", "tulip", "orchid", "allium", "poppy", "dandelion", "daisy",
"cornflower", "lily", "bluet", "rose", "peony", "lilac", "petals", "eyeblossom", "leaf_litter",
"fern", "bush", "shrub", "vine", "mushroom", "kelp", "seagrass", "coral", "sea_pickle", "cactus",
"sugar_cane", "bamboo", "carpet", "dripleaf", "azalea", "roots", "sprouts", "fungus", "wart_block",
"shroomlight", "lichen", "spore_blossom", "berry", "pumpkin", "melon", "pointed_dripstone", "cobweb",
"torch", "lantern", "planks", "stairs", "slab", "fence", "wall", "door", "glass", "brick", "button",
"chest", "barrel", "bookshelf", "chain", "_bed", "banner", "sign", "campfire", "candle", "head",
"skull", "sculk_sensor", "sculk_shrieker", "sculk_catalyst", "pillar", "sea_lantern", "glowstone");
private GenerationDiff() {
}
enum Category {
AIR,
FLUID,
TERRAIN,
ORE,
DECOR
}
record ChunkScore(int chunkX, int chunkZ, long blockMismatches, long nonAirPositions, long nonAirMismatches,
long surfaceDeltaSum, int surfaceMaxDelta, long biomeMismatches) {
double nonAirIdentity() {
return nonAirPositions == 0L ? 100D : 100D * (nonAirPositions - nonAirMismatches) / nonAirPositions;
}
}
static final class Totals {
long chunksBaseline;
long chunksCandidate;
long duplicateBaseline;
long duplicateCandidate;
long commonChunks;
long onlyBaseline;
long onlyCandidate;
long positions;
long identical;
long nonAirPositions;
long nonAirIdentical;
long columns;
long surfaceIdentical;
long surfaceDeltaSum;
int surfaceMaxDelta;
long terrainSurfaceIdentical;
long terrainSurfaceDeltaSum;
int terrainSurfaceMaxDelta;
long biomePositions;
long biomeIdentical;
final long[][] matrix = new long[Category.values().length][Category.values().length];
final long[] baselineCategoryCounts = new long[Category.values().length];
final long[] candidateCategoryCounts = new long[Category.values().length];
final List<ChunkScore> scores = new ArrayList<>();
final Map<String, Integer> canonicalIds = new HashMap<>();
}
public static void main(String[] arguments) {
if (arguments.length < 2 || arguments.length > 3) {
System.out.println(LOG_PREFIX + " FAIL: expected <baselineDump> <candidateDump> [worstChunks]");
System.exit(2);
return;
}
try {
int worst = arguments.length == 3 ? Integer.parseInt(arguments[2]) : DEFAULT_WORST_CHUNKS;
Totals totals = compare(Path.of(arguments[0]), Path.of(arguments[1]));
print(totals, Math.max(0, worst));
System.exit(totals.commonChunks > 0 ? 0 : 1);
} catch (Throwable failure) {
System.out.println(LOG_PREFIX + " FAIL: " + failure.getMessage());
failure.printStackTrace(System.out);
System.exit(1);
}
}
static Totals compare(Path baselineFile, Path candidateFile) throws IOException {
Totals totals = new Totals();
Map<Long, ChunkDump.ChunkSnapshot> baseline = new HashMap<>();
ChunkDump.Header baselineHeader;
try (ChunkDump.Reader reader = new ChunkDump.Reader(baselineFile)) {
baselineHeader = reader.header();
ChunkDump.ChunkSnapshot snapshot;
while ((snapshot = reader.next()) != null) {
totals.chunksBaseline++;
if (baseline.putIfAbsent(snapshot.coordinateKey(), snapshot) != null) {
totals.duplicateBaseline++;
}
}
}
Map<Long, Boolean> seenCandidate = new HashMap<>();
try (ChunkDump.Reader reader = new ChunkDump.Reader(candidateFile)) {
ChunkDump.Header candidateHeader = reader.header();
printHeaders(baselineFile, baselineHeader, candidateFile, candidateHeader);
ChunkDump.ChunkSnapshot snapshot;
while ((snapshot = reader.next()) != null) {
totals.chunksCandidate++;
long key = snapshot.coordinateKey();
if (seenCandidate.putIfAbsent(key, Boolean.TRUE) != null) {
totals.duplicateCandidate++;
continue;
}
ChunkDump.ChunkSnapshot expected = baseline.remove(key);
if (expected == null) {
totals.onlyCandidate++;
continue;
}
totals.commonChunks++;
compareChunk(totals, expected, baselineHeader.minY(), snapshot, candidateHeader.minY());
}
}
totals.onlyBaseline = baseline.size();
return totals;
}
static Category classify(String key) {
int properties = key.indexOf('[');
String material = properties >= 0 ? key.substring(0, properties) : key;
int namespace = material.indexOf(':');
String path = namespace >= 0 ? material.substring(namespace + 1) : material;
if (AIR.contains(path)) {
return Category.AIR;
}
if (FLUID.contains(path)) {
return Category.FLUID;
}
if (path.endsWith("_ore") || ORE_BLOCKS.contains(path)) {
return Category.ORE;
}
if (DECOR_EXACT.contains(path)) {
return Category.DECOR;
}
for (String fragment : DECOR_FRAGMENTS) {
if (path.contains(fragment)) {
return Category.DECOR;
}
}
return Category.TERRAIN;
}
private static void compareChunk(Totals totals, ChunkDump.ChunkSnapshot baseline, int baselineMinY,
ChunkDump.ChunkSnapshot candidate, int candidateMinY) {
String[] baselinePalette = baseline.blockPalette();
String[] candidatePalette = candidate.blockPalette();
int[] baselineBlockIds = canonicalIds(totals, baselinePalette);
int[] candidateBlockIds = canonicalIds(totals, candidatePalette);
int[] baselineBiomeIds = canonicalIds(totals, baseline.biomePalette());
int[] candidateBiomeIds = canonicalIds(totals, candidate.biomePalette());
byte[] baselineCategories = categories(baselinePalette);
byte[] candidateCategories = categories(candidatePalette);
int air = Category.AIR.ordinal();
int fluid = Category.FLUID.ordinal();
int terrain = Category.TERRAIN.ordinal();
int ore = Category.ORE.ordinal();
int baselineHeight = baseline.height();
int candidateHeight = candidate.height();
int lowY = Math.min(baselineMinY, candidateMinY);
int highY = Math.max(baselineMinY + baselineHeight, candidateMinY + candidateHeight) - 1;
int biomeLowY = Math.max(baselineMinY, candidateMinY);
int biomeHighY = Math.min(baselineMinY + baselineHeight, candidateMinY + candidateHeight) - 1;
char[] baselineBlocks = baseline.blocks();
char[] candidateBlocks = candidate.blocks();
char[] baselineBiomes = baseline.biomes();
char[] candidateBiomes = candidate.biomes();
long blockMismatches = 0L;
long nonAirPositions = 0L;
long nonAirMismatches = 0L;
long surfaceDeltaSum = 0L;
int surfaceMaxDelta = 0;
long biomeMismatches = 0L;
for (int column = 0; column < 256; column++) {
int baselineBase = column * baselineHeight;
int candidateBase = column * candidateHeight;
int baselineSurface = lowY - 1;
int candidateSurface = lowY - 1;
int baselineTerrainSurface = lowY - 1;
int candidateTerrainSurface = lowY - 1;
for (int y = lowY; y <= highY; y++) {
int baselineY = y - baselineMinY;
int candidateY = y - candidateMinY;
boolean baselineInside = baselineY >= 0 && baselineY < baselineHeight;
boolean candidateInside = candidateY >= 0 && candidateY < candidateHeight;
int baselineIndex = baselineInside ? baselineBlocks[baselineBase + baselineY] : -1;
int candidateIndex = candidateInside ? candidateBlocks[candidateBase + candidateY] : -1;
int baselineCategory = baselineIndex < 0 ? air : baselineCategories[baselineIndex];
int candidateCategory = candidateIndex < 0 ? air : candidateCategories[candidateIndex];
boolean equal;
if (baselineIndex < 0 || candidateIndex < 0) {
equal = baselineCategory == air && candidateCategory == air;
} else {
equal = baselineBlockIds[baselineIndex] == candidateBlockIds[candidateIndex];
}
totals.positions++;
totals.baselineCategoryCounts[baselineCategory]++;
totals.candidateCategoryCounts[candidateCategory]++;
if (equal) {
totals.identical++;
} else {
blockMismatches++;
totals.matrix[baselineCategory][candidateCategory]++;
}
if (baselineCategory != air || candidateCategory != air) {
nonAirPositions++;
if (!equal) {
nonAirMismatches++;
}
}
if (baselineCategory != air && baselineCategory != fluid) {
baselineSurface = y;
}
if (candidateCategory != air && candidateCategory != fluid) {
candidateSurface = y;
}
if (baselineCategory == terrain || baselineCategory == ore) {
baselineTerrainSurface = y;
}
if (candidateCategory == terrain || candidateCategory == ore) {
candidateTerrainSurface = y;
}
}
int delta = Math.abs(baselineSurface - candidateSurface);
int terrainDelta = Math.abs(baselineTerrainSurface - candidateTerrainSurface);
totals.columns++;
if (delta == 0) {
totals.surfaceIdentical++;
}
if (terrainDelta == 0) {
totals.terrainSurfaceIdentical++;
}
totals.surfaceDeltaSum += delta;
totals.terrainSurfaceDeltaSum += terrainDelta;
totals.surfaceMaxDelta = Math.max(totals.surfaceMaxDelta, delta);
totals.terrainSurfaceMaxDelta = Math.max(totals.terrainSurfaceMaxDelta, terrainDelta);
surfaceDeltaSum += delta;
surfaceMaxDelta = Math.max(surfaceMaxDelta, delta);
for (int y = biomeLowY; y <= biomeHighY; y++) {
int baselineBiome = baselineBiomes[baselineBase + y - baselineMinY];
int candidateBiome = candidateBiomes[candidateBase + y - candidateMinY];
totals.biomePositions++;
if (baselineBiomeIds[baselineBiome] == candidateBiomeIds[candidateBiome]) {
totals.biomeIdentical++;
} else {
biomeMismatches++;
}
}
}
totals.nonAirPositions += nonAirPositions;
totals.nonAirIdentical += nonAirPositions - nonAirMismatches;
totals.scores.add(new ChunkScore(baseline.chunkX(), baseline.chunkZ(), blockMismatches, nonAirPositions,
nonAirMismatches, surfaceDeltaSum, surfaceMaxDelta, biomeMismatches));
}
private static int[] canonicalIds(Totals totals, String[] palette) {
int[] ids = new int[palette.length];
for (int index = 0; index < palette.length; index++) {
ids[index] = totals.canonicalIds.computeIfAbsent(canonicalKey(palette[index]),
key -> totals.canonicalIds.size());
}
return ids;
}
static String canonicalKey(String key) {
int properties = key.indexOf('[');
int namespace = key.indexOf(':');
if (namespace >= 0 && (properties < 0 || namespace < properties)) {
return key;
}
return "minecraft:" + key;
}
private static byte[] categories(String[] palette) {
byte[] categories = new byte[palette.length];
for (int index = 0; index < palette.length; index++) {
categories[index] = (byte) classify(palette[index]).ordinal();
}
return categories;
}
private static void printHeaders(Path baselineFile, ChunkDump.Header baseline, Path candidateFile,
ChunkDump.Header candidate) {
System.out.println(LOG_PREFIX + " baseline: " + baselineFile + " dimension=" + baseline.dimensionKey()
+ " seed=" + baseline.seed() + " minY=" + baseline.minY() + " height=" + baseline.height());
System.out.println(LOG_PREFIX + " candidate: " + candidateFile + " dimension=" + candidate.dimensionKey()
+ " seed=" + candidate.seed() + " minY=" + candidate.minY() + " height=" + candidate.height());
if (!baseline.dimensionKey().equals(candidate.dimensionKey()) || baseline.seed() != candidate.seed()) {
System.out.println(LOG_PREFIX + " WARNING: dumps differ in dimension or seed");
}
if (baseline.minY() != candidate.minY() || baseline.height() != candidate.height()) {
System.out.println(LOG_PREFIX + " WARNING: dumps differ in vertical range; blocks compare over the union"
+ " (outside a range counts as air) and biomes over the intersection");
}
}
private static void print(Totals totals, int worst) {
System.out.println(LOG_PREFIX + " chunks: baseline=" + totals.chunksBaseline + " candidate="
+ totals.chunksCandidate + " common=" + totals.commonChunks + " only_baseline=" + totals.onlyBaseline
+ " only_candidate=" + totals.onlyCandidate + " duplicate_baseline=" + totals.duplicateBaseline
+ " duplicate_candidate=" + totals.duplicateCandidate);
System.out.printf(Locale.ROOT, "%s blocks identical: %.4f%% (%d/%d)%n", LOG_PREFIX,
percent(totals.identical, totals.positions), totals.identical, totals.positions);
System.out.printf(Locale.ROOT, "%s blocks identical where either side is non-air: %.4f%% (%d/%d)%n",
LOG_PREFIX, percent(totals.nonAirIdentical, totals.nonAirPositions), totals.nonAirIdentical,
totals.nonAirPositions);
System.out.printf(Locale.ROOT, "%s surface (highest non-air non-fluid) identical columns: %.4f%% mean_abs_delta=%.4f max_abs_delta=%d%n",
LOG_PREFIX, percent(totals.surfaceIdentical, totals.columns),
mean(totals.surfaceDeltaSum, totals.columns), totals.surfaceMaxDelta);
System.out.printf(Locale.ROOT, "%s terrain surface (highest terrain or ore) identical columns: %.4f%% mean_abs_delta=%.4f max_abs_delta=%d%n",
LOG_PREFIX, percent(totals.terrainSurfaceIdentical, totals.columns),
mean(totals.terrainSurfaceDeltaSum, totals.columns), totals.terrainSurfaceMaxDelta);
System.out.printf(Locale.ROOT, "%s biomes identical: %.4f%% (%d/%d)%n", LOG_PREFIX,
percent(totals.biomeIdentical, totals.biomePositions), totals.biomeIdentical, totals.biomePositions);
Category[] categories = Category.values();
System.out.println(LOG_PREFIX + " category volumes (baseline -> candidate):");
for (Category category : categories) {
long before = totals.baselineCategoryCounts[category.ordinal()];
long after = totals.candidateCategoryCounts[category.ordinal()];
System.out.printf(Locale.ROOT, " %-8s %12d -> %12d (%+.3f%%)%n", category, before, after,
before == 0L ? 0D : 100D * (after - before) / before);
}
System.out.println(LOG_PREFIX + " mismatches by category (either side in category):");
for (Category category : categories) {
System.out.printf(Locale.ROOT, " %-8s %12d%n", category, categoryMismatches(totals, category));
}
System.out.println(LOG_PREFIX + " mismatch matrix (rows baseline category, columns candidate category):");
StringBuilder headerLine = new StringBuilder(String.format(Locale.ROOT, " %-8s", ""));
for (Category category : categories) {
headerLine.append(String.format(Locale.ROOT, " %12s", category));
}
System.out.println(headerLine);
for (Category row : categories) {
StringBuilder line = new StringBuilder(String.format(Locale.ROOT, " %-8s", row));
for (Category column : categories) {
line.append(String.format(Locale.ROOT, " %12d", totals.matrix[row.ordinal()][column.ordinal()]));
}
System.out.println(line);
}
List<ChunkScore> ranked = new ArrayList<>(totals.scores);
ranked.sort(Comparator.comparingLong(ChunkScore::nonAirMismatches)
.thenComparingLong(ChunkScore::surfaceDeltaSum)
.reversed());
int shown = Math.min(worst, ranked.size());
System.out.println(LOG_PREFIX + " worst " + shown + " chunks:");
for (int index = 0; index < shown; index++) {
ChunkScore score = ranked.get(index);
System.out.printf(Locale.ROOT,
" chunk=%d,%d nonair_mismatch=%d nonair_identity=%.3f%% block_mismatch=%d surface_mean_abs_delta=%.3f surface_max_abs_delta=%d biome_mismatch=%d%n",
score.chunkX(), score.chunkZ(), score.nonAirMismatches(), score.nonAirIdentity(),
score.blockMismatches(), score.surfaceDeltaSum() / 256D, score.surfaceMaxDelta(),
score.biomeMismatches());
}
System.out.printf(Locale.ROOT,
"IRIS_GENDIFF_RESULT version=1 common_chunks=%d only_baseline=%d only_candidate=%d block_identity_pct=%.4f nonair_identity_pct=%.4f surface_identity_pct=%.4f surface_mean_abs_delta=%.4f surface_max_abs_delta=%d terrain_surface_identity_pct=%.4f terrain_surface_mean_abs_delta=%.4f biome_identity_pct=%.4f mismatch_air=%d mismatch_fluid=%d mismatch_terrain=%d mismatch_ore=%d mismatch_decor=%d%n",
totals.commonChunks, totals.onlyBaseline, totals.onlyCandidate,
percent(totals.identical, totals.positions),
percent(totals.nonAirIdentical, totals.nonAirPositions),
percent(totals.surfaceIdentical, totals.columns),
mean(totals.surfaceDeltaSum, totals.columns), totals.surfaceMaxDelta,
percent(totals.terrainSurfaceIdentical, totals.columns),
mean(totals.terrainSurfaceDeltaSum, totals.columns),
percent(totals.biomeIdentical, totals.biomePositions),
categoryMismatches(totals, Category.AIR), categoryMismatches(totals, Category.FLUID),
categoryMismatches(totals, Category.TERRAIN), categoryMismatches(totals, Category.ORE),
categoryMismatches(totals, Category.DECOR));
}
private static long categoryMismatches(Totals totals, Category category) {
long count = 0L;
for (Category other : Category.values()) {
count += totals.matrix[category.ordinal()][other.ordinal()];
if (other != category) {
count += totals.matrix[other.ordinal()][category.ordinal()];
}
}
return count;
}
private static double percent(long part, long whole) {
return whole == 0L ? 100D : 100D * part / whole;
}
private static double mean(long sum, long count) {
return count == 0L ? 0D : sum / (double) count;
}
}
@@ -32,8 +32,13 @@ import art.arcane.iris.generation.runtime.EngineWorldManager;
import art.arcane.iris.generation.runtime.EngineWorldManagerProvider;
import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
@@ -42,6 +47,7 @@ import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -55,7 +61,11 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public final class GenerationProbe {
private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>());
@@ -158,9 +168,62 @@ public final class GenerationProbe {
}
}
record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile) {
ProbeOptions {
if (threads < 1) {
throw new IllegalArgumentException("Thread count must be at least 1.");
}
if (dumpFile != null && !dumpFile.isAbsolute()) {
throw new IllegalArgumentException("Dump file must be an absolute path.");
}
}
static ProbeOptions fromSystemProperties() {
String dump = System.getProperty("iris.probe.dumpFile", "").trim();
return new ProbeOptions(
Integer.parseInt(System.getProperty("iris.probe.threads", "1").trim()),
dump.isEmpty() ? null : Path.of(dump),
Boolean.parseBoolean(System.getProperty("iris.probe.pregenProfile", "false").trim()));
}
boolean parallel() {
return threads > 1;
}
}
record ChunkCoordinate(int x, int z) {
}
record ChunkOutcome(int index, ChunkCoordinate coordinate, long elapsedNanos,
Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes, List<Throwable> failures) {
}
record ThroughputSummary(int threads, boolean multicore, boolean pregenProfile, int burstParallelism,
int noiseCacheSize, int measuredChunks, long warmupWallNanos, long measuredWallNanos,
long latencySumNanos, TimingSummary latencies, int unattributedReports) {
String machineLine() {
double wallSeconds = measuredWallNanos / 1_000_000_000D;
double chunksPerSecond = wallSeconds == 0D ? 0D : measuredChunks / wallSeconds;
return String.format(Locale.ROOT,
"IRIS_GENPROBE_THROUGHPUT version=1 threads=%d multicore=%s pregen_profile=%s burst_parallelism=%d noise_cache_size=%d measured_chunks=%d warmup_wall_ms=%.3f measured_wall_ms=%.3f wall_cps=%.3f latency_sum_ms=%.3f latency_median_ms=%.3f latency_p95_ms=%.3f latency_max_ms=%.3f effective_concurrency=%.2f unattributed_reports=%d",
threads,
multicore,
pregenProfile,
burstParallelism,
noiseCacheSize,
measuredChunks,
nanosToMillis(warmupWallNanos),
nanosToMillis(measuredWallNanos),
chunksPerSecond,
nanosToMillis(latencySumNanos),
nanosToMillis(latencies.medianNanos()),
nanosToMillis(latencies.p95Nanos()),
nanosToMillis(latencies.maxNanos()),
measuredWallNanos == 0L ? 0D : latencySumNanos / (double) measuredWallNanos,
unattributedReports);
}
}
record TimingSummary(long medianNanos, long p95Nanos, long maxNanos, long totalNanos) {
static TimingSummary from(List<Long> samples) {
if (samples.isEmpty()) {
@@ -184,6 +247,10 @@ public final class GenerationProbe {
}
return new TimingSummary(median, sorted.get(p95Index), sorted.get(size - 1), total);
}
TimingSummary withTotal(long totalNanos) {
return new TimingSummary(medianNanos, p95Nanos, maxNanos, totalNanos);
}
}
record GenerationResult(int successfulChunks, int failedChunks, long firstChunkNanos,
@@ -220,8 +287,10 @@ public final class GenerationProbe {
public static void main(String[] args) {
ProbeConfiguration configuration;
ProbeOptions options;
try {
configuration = ProbeConfiguration.parse(args);
options = ProbeOptions.fromSystemProperties();
} catch (Throwable e) {
System.out.println("[genprobe] FAIL: " + e.getMessage());
e.printStackTrace(System.out);
@@ -231,7 +300,7 @@ public final class GenerationProbe {
int exitCode;
try {
ProbeResult result = run(configuration);
ProbeResult result = run(configuration, options);
System.out.println(result.machineLine());
exitCode = result.failedChunks() == 0 ? 0 : 1;
} catch (Throwable e) {
@@ -272,7 +341,7 @@ public final class GenerationProbe {
return coordinates;
}
private static ProbeResult run(ProbeConfiguration configuration) throws Throwable {
private static ProbeResult run(ProbeConfiguration configuration, ProbeOptions options) throws Throwable {
if (!configuration.packSource().isDirectory()) {
throw new IllegalArgumentException("Pack folder not found: " + configuration.packSource().getAbsolutePath());
}
@@ -296,6 +365,9 @@ public final class GenerationProbe {
System.out.println("[genprobe] center chunk: " + configuration.centerChunkX() + "," + configuration.centerChunkZ());
System.out.println("[genprobe] multicore: " + configuration.multicore());
System.out.println("[genprobe] studio: " + configuration.studio());
System.out.println("[genprobe] threads: " + options.threads());
System.out.println("[genprobe] pregen profile: " + options.pregenProfile());
System.out.println("[genprobe] dump file: " + (options.dumpFile() == null ? "none" : options.dumpFile()));
long engineStart = System.nanoTime();
data = IrisData.get(pack);
@@ -327,7 +399,7 @@ public final class GenerationProbe {
+ " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight()
+ " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos)));
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
GenerationResult generation = generate(engine, configuration);
GenerationResult generation = generateWithProfile(engine, configuration, options);
verifyBackend(nativeSamplesBefore);
String status = generation.failedChunks() == 0 ? "PASS" : "FAIL";
printGenerationFailures(generation);
@@ -401,77 +473,213 @@ public final class GenerationProbe {
System.out.println("[genprobe] offline pack validation: PASS");
}
private static GenerationResult generate(Engine engine, ProbeConfiguration configuration) {
private static GenerationResult generateWithProfile(Engine engine, ProbeConfiguration configuration,
ProbeOptions options) throws Exception {
if (!options.pregenProfile()) {
return generate(engine, configuration, options);
}
PregenPerformanceProfile.apply(engine);
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
try (HydrologyTileCache.PregenerationScope ignored = hydrology == null ? null
: hydrology.preparePregeneration(pregenerationArea(configuration))) {
return generate(engine, configuration, options);
} finally {
PregenPerformanceProfile.restore();
}
}
static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) {
List<ChunkCoordinate> coordinates = scheduleCoordinates(
configuration.warmupChunks() + configuration.measuredChunks(),
configuration.centerChunkX(), configuration.centerChunkZ());
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (ChunkCoordinate coordinate : coordinates) {
minimumX = Math.min(minimumX, coordinate.x());
minimumZ = Math.min(minimumZ, coordinate.z());
maximumX = Math.max(maximumX, coordinate.x());
maximumZ = Math.max(maximumZ, coordinate.z());
}
int centerBlockX = Math.toIntExact(Math.clamp(((long) configuration.centerChunkX() << 4) + 8L,
(long) minimumX << 4, (((long) maximumX + 1L) << 4) - 1L));
int centerBlockZ = Math.toIntExact(Math.clamp(((long) configuration.centerChunkZ() << 4) + 8L,
(long) minimumZ << 4, (((long) maximumZ + 1L) << 4) - 1L));
return new HydrologyTileCache.PregenerationArea(centerBlockX, centerBlockZ,
(long) minimumX << 4, (long) minimumZ << 4,
(((long) maximumX + 1L) << 4) - 1L, (((long) maximumZ + 1L) << 4) - 1L);
}
static List<Integer> spiralOrder(List<ChunkCoordinate> coordinates, int fromIndex, int centerChunkX,
int centerChunkZ) {
List<Integer> order = new ArrayList<>(coordinates.size() - fromIndex);
for (int index = fromIndex; index < coordinates.size(); index++) {
order.add(index);
}
order.sort(Comparator
.comparingInt((Integer index) -> ringOf(coordinates.get(index), centerChunkX, centerChunkZ))
.thenComparingDouble(index -> angleOf(coordinates.get(index), centerChunkX, centerChunkZ)));
return order;
}
private static int ringOf(ChunkCoordinate coordinate, int centerChunkX, int centerChunkZ) {
return Math.max(Math.abs(coordinate.x() - centerChunkX), Math.abs(coordinate.z() - centerChunkZ));
}
private static double angleOf(ChunkCoordinate coordinate, int centerChunkX, int centerChunkZ) {
return Math.atan2(coordinate.z() - centerChunkZ, coordinate.x() - centerChunkX);
}
private static GenerationResult generate(Engine engine, ProbeConfiguration configuration,
ProbeOptions options) throws Exception {
int totalChunks = configuration.warmupChunks() + configuration.measuredChunks();
List<ChunkCoordinate> coordinates = scheduleCoordinates(
totalChunks, configuration.centerChunkX(), configuration.centerChunkZ());
List<Long> measuredTimings = new ArrayList<>(configuration.measuredChunks());
MessageDigest signature = sha256();
int successfulChunks = 0;
int failedChunks = 0;
long firstChunkNanos = 0L;
Map<String, Integer> distinctFailures = new LinkedHashMap<>();
Map<String, String> firstFailureChunk = new LinkedHashMap<>();
int height = engine.getTarget().getHeight();
for (int index = 0; index < totalChunks; index++) {
ChunkCoordinate coordinate = coordinates.get(index);
drainReported();
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,
configuration.multicore());
} catch (Throwable e) {
failures.add(e);
}
long elapsed = System.nanoTime() - started;
failures.addAll(drainReported());
if (index == 0) {
firstChunkNanos = elapsed;
}
if (index >= configuration.warmupChunks()) {
measuredTimings.add(elapsed);
}
if (failures.isEmpty()) {
successfulChunks++;
updateSignature(signature, coordinate, blocks, biomes, height);
ChunkDump.Header header = new ChunkDump.Header(configuration.dimensionKey(), configuration.seed(),
engine.getMinHeight(), height);
try (OutcomeSink sink = new OutcomeSink(options.dumpFile(), header, configuration.measuredChunks())) {
if (options.parallel()) {
generateParallel(engine, configuration, options, coordinates, sink);
} else {
failedChunks++;
recordFailures(coordinate, failures, distinctFailures, firstFailureChunk);
generateSequential(engine, configuration, coordinates, sink);
}
return sink.result(configuration);
}
}
int completed = index + 1;
if (completed == configuration.warmupChunks()) {
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks");
} else if (completed > configuration.warmupChunks()
&& (completed == totalChunks || (completed - configuration.warmupChunks()) % 128 == 0)) {
System.out.println("[genprobe] measured progress: "
+ (completed - configuration.warmupChunks()) + "/" + configuration.measuredChunks());
private static void generateSequential(Engine engine, ProbeConfiguration configuration,
List<ChunkCoordinate> coordinates, OutcomeSink sink) throws IOException {
int totalChunks = coordinates.size();
long measuredStartEpochMillis = 0L;
for (int index = 0; index < totalChunks; index++) {
if (index == configuration.warmupChunks()) {
measuredStartEpochMillis = System.currentTimeMillis();
}
drainReported();
ChunkOutcome outcome = generateChunk(engine, index, coordinates.get(index), configuration.multicore());
outcome.failures().addAll(drainReported());
sink.accept(outcome, index >= configuration.warmupChunks());
printProgress(configuration, index + 1, totalChunks);
}
printWindow(measuredStartEpochMillis, System.currentTimeMillis());
}
private static void printWindow(long startEpochMillis, long endEpochMillis) {
System.out.println("IRIS_GENPROBE_WINDOW phase=measured start_epoch_ms=" + startEpochMillis
+ " end_epoch_ms=" + endEpochMillis);
}
private static void generateParallel(Engine engine, ProbeConfiguration configuration, ProbeOptions options,
List<ChunkCoordinate> coordinates, OutcomeSink sink) throws Exception {
ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()];
ExecutorService executor = Executors.newFixedThreadPool(options.threads());
long warmupWallNanos;
long measuredWallNanos;
List<Throwable> unattributed = new ArrayList<>();
try {
drainReported();
List<Integer> warmup = new ArrayList<>(configuration.warmupChunks());
for (int index = 0; index < configuration.warmupChunks(); index++) {
warmup.add(index);
}
long warmupStarted = System.nanoTime();
runPhase(executor, engine, configuration, coordinates, warmup, outcomes);
warmupWallNanos = System.nanoTime() - warmupStarted;
unattributed.addAll(drainReported());
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks on "
+ options.threads() + " threads");
List<Integer> measured = spiralOrder(coordinates, configuration.warmupChunks(),
configuration.centerChunkX(), configuration.centerChunkZ());
long measuredStartEpochMillis = System.currentTimeMillis();
long measuredStarted = System.nanoTime();
runPhase(executor, engine, configuration, coordinates, measured, outcomes);
measuredWallNanos = System.nanoTime() - measuredStarted;
printWindow(measuredStartEpochMillis, System.currentTimeMillis());
unattributed.addAll(drainReported());
System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/"
+ configuration.measuredChunks());
} finally {
executor.shutdown();
if (!executor.awaitTermination(30L, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
if (!distinctFailures.isEmpty()) {
System.out.println("[genprobe] DISTINCT ROOT CAUSES (" + distinctFailures.size() + "):");
for (Map.Entry<String, Integer> entry : distinctFailures.entrySet()) {
System.out.println(" x" + entry.getValue() + " (first at chunk "
+ firstFailureChunk.get(entry.getKey()) + ") " + entry.getKey());
for (int index = 0; index < outcomes.length; index++) {
ChunkOutcome outcome = outcomes[index];
outcomes[index] = null;
sink.accept(outcome, index >= configuration.warmupChunks());
}
sink.reportUnattributed(unattributed);
TimingSummary latencies = TimingSummary.from(sink.measuredTimings());
ThroughputSummary throughput = new ThroughputSummary(
options.threads(),
configuration.multicore(),
options.pregenProfile(),
MultiBurst.burst.parallelism(),
IrisSettings.get().getPerformance().getNoiseCacheSize(),
configuration.measuredChunks(),
warmupWallNanos,
measuredWallNanos,
latencies.totalNanos(),
latencies,
unattributed.size());
sink.measuredWallNanos(measuredWallNanos);
System.out.println(throughput.machineLine());
}
private static void runPhase(ExecutorService executor, Engine engine, ProbeConfiguration configuration,
List<ChunkCoordinate> coordinates, List<Integer> order,
ChunkOutcome[] outcomes) throws Exception {
List<Future<ChunkOutcome>> futures = new ArrayList<>(order.size());
for (int index : order) {
ChunkCoordinate coordinate = coordinates.get(index);
futures.add(executor.submit(() -> generateChunk(engine, index, coordinate, configuration.multicore())));
}
for (Future<ChunkOutcome> future : futures) {
ChunkOutcome outcome;
try {
outcome = future.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);
}
outcomes[outcome.index()] = outcome;
}
}
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) {
if (completed == configuration.warmupChunks()) {
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks");
} else if (completed > configuration.warmupChunks()
&& (completed == totalChunks || (completed - configuration.warmupChunks()) % 128 == 0)) {
System.out.println("[genprobe] measured progress: "
+ (completed - configuration.warmupChunks()) + "/" + configuration.measuredChunks());
}
return new GenerationResult(
successfulChunks,
failedChunks,
firstChunkNanos,
TimingSummary.from(measuredTimings),
HexFormat.of().formatHex(signature.digest()).substring(0, 16));
}
private static void recordFailures(ChunkCoordinate coordinate, List<Throwable> failures,
@@ -499,32 +707,42 @@ public final class GenerationProbe {
nanosToMillis(generation.measuredTimings().maxNanos())));
}
private static void updateSignature(MessageDigest digest, ChunkCoordinate coordinate,
Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes, int height) {
private static void updateSignature(MessageDigest digest, ChunkDump.ChunkSnapshot snapshot, int height) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
updateDigest(digest, coordinate.x() + "," + coordinate.z());
updateDigest(blockDigest, coordinate.x() + "," + coordinate.z());
updateDigest(biomeDigest, coordinate.x() + "," + coordinate.z());
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
NativeBlockState state = blocks.get(x, y, z);
NativeBiome biome = biomes.get(x, y, z);
String stateKey = state == null ? "minecraft:air" : state.key();
String biomeKey = biome == null ? "null" : biome.key();
updateDigest(digest, stateKey);
updateDigest(digest, biomeKey);
updateDigest(blockDigest, stateKey);
updateDigest(biomeDigest, biomeKey);
}
}
String coordinateLabel = snapshot.chunkX() + "," + snapshot.chunkZ();
updateDigest(digest, coordinateLabel);
updateDigest(blockDigest, coordinateLabel);
updateDigest(biomeDigest, coordinateLabel);
byte[][] blockKeys = terminatedKeys(snapshot.blockPalette());
byte[][] biomeKeys = terminatedKeys(snapshot.biomePalette());
char[] blocks = snapshot.blocks();
char[] biomes = snapshot.biomes();
int size = 256 * height;
for (int index = 0; index < size; index++) {
byte[] stateKey = blockKeys[blocks[index]];
byte[] biomeKey = biomeKeys[biomes[index]];
digest.update(stateKey);
digest.update(biomeKey);
blockDigest.update(stateKey);
biomeDigest.update(biomeKey);
}
System.out.println("GENPROBE_CHUNK_HASH chunk=" + coordinate.x() + "," + coordinate.z()
System.out.println("GENPROBE_CHUNK_HASH chunk=" + coordinateLabel
+ " blocks=" + HexFormat.of().formatHex(blockDigest.digest()).substring(0, 16)
+ " biomes=" + HexFormat.of().formatHex(biomeDigest.digest()).substring(0, 16));
}
private static byte[][] terminatedKeys(String[] palette) {
byte[][] encoded = new byte[palette.length][];
for (int index = 0; index < palette.length; index++) {
byte[] key = palette[index].getBytes(StandardCharsets.UTF_8);
byte[] terminated = new byte[key.length + 1];
System.arraycopy(key, 0, terminated, 0, key.length);
encoded[index] = terminated;
}
return encoded;
}
private static void updateDigest(MessageDigest digest, String value) {
digest.update(value.getBytes(StandardCharsets.UTF_8));
digest.update((byte) 0);
@@ -659,4 +877,88 @@ public final class GenerationProbe {
private static double nanosToMillis(long nanos) {
return nanos / 1_000_000D;
}
private static final class OutcomeSink implements AutoCloseable {
private final MessageDigest signature = sha256();
private final List<Long> measuredTimings;
private final Map<String, Integer> distinctFailures = new LinkedHashMap<>();
private final Map<String, String> firstFailureChunk = new LinkedHashMap<>();
private final ChunkDump.Writer dump;
private final int height;
private int successfulChunks;
private int failedChunks;
private int unattributedReports;
private long firstChunkNanos;
private long measuredWallNanos = -1L;
private OutcomeSink(Path dumpFile, ChunkDump.Header header, int measuredChunks) throws IOException {
this.measuredTimings = new ArrayList<>(measuredChunks);
this.height = header.height();
this.dump = dumpFile == null ? null : new ChunkDump.Writer(dumpFile, header);
}
void accept(ChunkOutcome outcome, boolean measured) throws IOException {
if (outcome.index() == 0) {
firstChunkNanos = outcome.elapsedNanos();
}
if (measured) {
measuredTimings.add(outcome.elapsedNanos());
}
if (!outcome.failures().isEmpty()) {
failedChunks++;
recordFailures(outcome.coordinate(), outcome.failures(), distinctFailures, firstFailureChunk);
return;
}
successfulChunks++;
ChunkDump.ChunkSnapshot snapshot = ChunkDump.capture(outcome.coordinate().x(), outcome.coordinate().z(),
outcome.blocks(), outcome.biomes(), height);
updateSignature(signature, snapshot, height);
if (dump != null) {
dump.write(snapshot);
}
}
void reportUnattributed(List<Throwable> reports) {
unattributedReports += reports.size();
if (!reports.isEmpty()) {
printDistinctCauses("parallel generation reported errors without a chunk owner", reports);
}
}
void measuredWallNanos(long nanos) {
measuredWallNanos = nanos;
}
List<Long> measuredTimings() {
return measuredTimings;
}
GenerationResult result(ProbeConfiguration configuration) {
if (!distinctFailures.isEmpty()) {
System.out.println("[genprobe] DISTINCT ROOT CAUSES (" + distinctFailures.size() + "):");
for (Map.Entry<String, Integer> entry : distinctFailures.entrySet()) {
System.out.println(" x" + entry.getValue() + " (first at chunk "
+ firstFailureChunk.get(entry.getKey()) + ") " + entry.getKey());
}
}
TimingSummary timings = TimingSummary.from(measuredTimings);
if (measuredWallNanos >= 0L) {
timings = timings.withTotal(measuredWallNanos);
}
int failures = failedChunks + (unattributedReports > 0 && failedChunks == 0 ? 1 : 0);
return new GenerationResult(
successfulChunks,
failures,
firstChunkNanos,
timings,
HexFormat.of().formatHex(signature.digest()).substring(0, 16));
}
@Override
public void close() throws IOException {
if (dump != null) {
dump.close();
}
}
}
}
@@ -0,0 +1,146 @@
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.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public final class GenerationDiffTest {
private static final int HEIGHT = 8;
private static final int MIN_Y = -4;
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void roundTripsWidePalettesAndChunkCoordinates() throws IOException {
String[] palette = new String[300];
for (int index = 0; index < palette.length; index++) {
palette[index] = "minecraft:block_" + index;
}
char[] blocks = new char[256 * HEIGHT];
for (int index = 0; index < blocks.length; index++) {
blocks[index] = (char) (index % palette.length);
}
ChunkDump.ChunkSnapshot wide = new ChunkDump.ChunkSnapshot(-3, 7, HEIGHT, palette, blocks,
new String[]{"minecraft:plains"}, new char[256 * HEIGHT]);
Path file = write("wide.bin", List.of(wide));
try (ChunkDump.Reader reader = new ChunkDump.Reader(file)) {
assertEquals(new ChunkDump.Header("overworld", 1337L, MIN_Y, HEIGHT), reader.header());
ChunkDump.ChunkSnapshot read = reader.next();
assertEquals(-3, read.chunkX());
assertEquals(7, read.chunkZ());
assertArrayEquals(palette, read.blockPalette());
assertArrayEquals(blocks, read.blocks());
assertArrayEquals(new String[]{"minecraft:plains"}, read.biomePalette());
assertNull(reader.next());
}
}
@Test
public void capturesUnsetHunkEntriesAsAirAndNullBiome() {
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, HEIGHT, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, HEIGHT, 16);
ChunkDump.ChunkSnapshot snapshot = ChunkDump.capture(1, 2, blocks, biomes, HEIGHT);
assertArrayEquals(new String[]{ChunkDump.NULL_BLOCK_KEY}, snapshot.blockPalette());
assertArrayEquals(new String[]{ChunkDump.NULL_BIOME_KEY}, snapshot.biomePalette());
assertEquals(256 * HEIGHT, snapshot.blocks().length);
}
@Test
public void reportsFullIdentityIndependentOfChunkOrderAndNamespaceSpelling() throws IOException {
Path baseline = write("a.bin", List.of(terrain(0, 0, 3, "minecraft:air"), terrain(1, 0, 5, "minecraft:air")));
Path candidate = write("b.bin", List.of(terrain(1, 0, 5, "air"), terrain(0, 0, 3, "minecraft:air")));
GenerationDiff.Totals totals = GenerationDiff.compare(baseline, candidate);
assertEquals(2L, totals.commonChunks);
assertEquals(0L, totals.onlyBaseline);
assertEquals(0L, totals.onlyCandidate);
assertEquals(totals.positions, totals.identical);
assertEquals(totals.nonAirPositions, totals.nonAirIdentical);
assertEquals(totals.columns, totals.surfaceIdentical);
assertEquals(totals.biomePositions, totals.biomeIdentical);
}
@Test
public void treatsBothAirSpellingsInOnePaletteAsTheSameBlock() throws IOException {
char[] blocks = new char[256 * HEIGHT];
Arrays.fill(blocks, 0, HEIGHT, (char) 1);
ChunkDump.ChunkSnapshot mixed = new ChunkDump.ChunkSnapshot(0, 0, HEIGHT,
new String[]{"minecraft:air", "air"}, blocks, new String[]{"minecraft:plains"}, new char[256 * HEIGHT]);
ChunkDump.ChunkSnapshot reordered = new ChunkDump.ChunkSnapshot(0, 0, HEIGHT,
new String[]{"air", "minecraft:air"}, blocks.clone(), new String[]{"plains"}, new char[256 * HEIGHT]);
GenerationDiff.Totals totals = GenerationDiff.compare(write("a.bin", List.of(mixed)),
write("b.bin", List.of(reordered)));
assertEquals(totals.positions, totals.identical);
assertEquals(totals.biomePositions, totals.biomeIdentical);
}
@Test
public void attributesSurfaceAndCategoryMismatchesAndUnmatchedChunks() throws IOException {
Path baseline = write("a.bin", List.of(terrain(0, 0, 5, "minecraft:air"), terrain(4, 4, 5, "minecraft:air")));
Path candidate = write("b.bin", List.of(terrain(0, 0, 3, "minecraft:air"), terrain(9, 9, 5, "minecraft:air")));
GenerationDiff.Totals totals = GenerationDiff.compare(baseline, candidate);
assertEquals(1L, totals.commonChunks);
assertEquals(1L, totals.onlyBaseline);
assertEquals(1L, totals.onlyCandidate);
assertEquals(256L * 2L, totals.matrix[GenerationDiff.Category.TERRAIN.ordinal()][GenerationDiff.Category.AIR.ordinal()]);
assertEquals(0L, totals.surfaceIdentical);
assertEquals(256L * 2L, totals.surfaceDeltaSum);
assertEquals(2, totals.surfaceMaxDelta);
assertEquals(256L * 5L, totals.nonAirPositions);
assertEquals(256L * 3L, totals.nonAirIdentical);
}
@Test
public void classifiesBlockFamilies() {
assertEquals(GenerationDiff.Category.AIR, GenerationDiff.classify("cave_air"));
assertEquals(GenerationDiff.Category.AIR, GenerationDiff.classify("minecraft:void_air"));
assertEquals(GenerationDiff.Category.FLUID, GenerationDiff.classify("minecraft:water[level=0]"));
assertEquals(GenerationDiff.Category.ORE, GenerationDiff.classify("minecraft:deepslate_diamond_ore"));
assertEquals(GenerationDiff.Category.DECOR, GenerationDiff.classify("minecraft:oak_leaves[distance=1,persistent=false]"));
assertEquals(GenerationDiff.Category.DECOR, GenerationDiff.classify("minecraft:snow[layers=1]"));
assertEquals(GenerationDiff.Category.TERRAIN, GenerationDiff.classify("minecraft:snow_block"));
assertEquals(GenerationDiff.Category.TERRAIN, GenerationDiff.classify("minecraft:grass_block[snowy=false]"));
assertEquals("minecraft:air", GenerationDiff.canonicalKey("air"));
assertEquals("custom:thing[a=b:c]", GenerationDiff.canonicalKey("custom:thing[a=b:c]"));
}
private ChunkDump.ChunkSnapshot terrain(int chunkX, int chunkZ, int solidHeight, String airKey) {
char[] blocks = new char[256 * HEIGHT];
for (int column = 0; column < 256; column++) {
Arrays.fill(blocks, column * HEIGHT, column * HEIGHT + solidHeight, (char) 1);
}
return new ChunkDump.ChunkSnapshot(chunkX, chunkZ, HEIGHT, new String[]{airKey, "minecraft:stone"}, blocks,
new String[]{"minecraft:plains"}, new char[256 * HEIGHT]);
}
private Path write(String name, List<ChunkDump.ChunkSnapshot> snapshots) throws IOException {
Path file = folder.getRoot().toPath().resolve(name);
try (ChunkDump.Writer writer = new ChunkDump.Writer(file, new ChunkDump.Header("overworld", 1337L, MIN_Y, HEIGHT))) {
for (ChunkDump.ChunkSnapshot snapshot : snapshots) {
writer.write(snapshot);
}
}
return file;
}
}
@@ -1,8 +1,10 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import org.junit.Test;
import java.io.File;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -76,6 +78,59 @@ public final class GenerationProbeTest {
assertTrue(coordinates.stream().allMatch(coordinate -> Math.abs(coordinate.z() + 2048) <= 18));
}
@Test
public void ordersMeasuredChunksOutwardFromTheCenterWithoutTouchingWarmup() {
List<GenerationProbe.ChunkCoordinate> coordinates = GenerationProbe.scheduleCoordinates(320, 2048, 2048);
List<Integer> order = GenerationProbe.spiralOrder(coordinates, 64, 2048, 2048);
assertEquals(256, order.size());
assertEquals(256, new HashSet<>(order).size());
assertTrue(order.stream().allMatch(index -> index >= 64));
int previousRing = -1;
for (int index : order) {
GenerationProbe.ChunkCoordinate coordinate = coordinates.get(index);
int ring = Math.max(Math.abs(coordinate.x() - 2048), Math.abs(coordinate.z() - 2048));
assertTrue(ring >= previousRing);
previousRing = ring;
}
}
@Test
public void pregenerationAreaCoversEveryScheduledChunk() {
GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
new File("/tmp/pack"), "overworld", 64, 256, 2048, 2048, true, false, 1337L);
HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration);
for (GenerationProbe.ChunkCoordinate coordinate : GenerationProbe.scheduleCoordinates(320, 2048, 2048)) {
assertTrue(((long) coordinate.x() << 4) >= area.minimumBlockX());
assertTrue((((long) coordinate.x() << 4) + 15L) <= area.maximumBlockX());
assertTrue(((long) coordinate.z() << 4) >= area.minimumBlockZ());
assertTrue((((long) coordinate.z() << 4) + 15L) <= area.maximumBlockZ());
}
}
@Test
public void readsThroughputAndDumpOptionsFromSystemProperties() {
System.setProperty("iris.probe.threads", "16");
System.setProperty("iris.probe.dumpFile", "/tmp/iris-dump.bin");
System.setProperty("iris.probe.pregenProfile", "true");
try {
GenerationProbe.ProbeOptions options = GenerationProbe.ProbeOptions.fromSystemProperties();
assertEquals(16, options.threads());
assertEquals(Path.of("/tmp/iris-dump.bin"), options.dumpFile());
assertTrue(options.pregenProfile());
assertTrue(options.parallel());
} finally {
System.clearProperty("iris.probe.threads");
System.clearProperty("iris.probe.dumpFile");
System.clearProperty("iris.probe.pregenProfile");
}
GenerationProbe.ProbeOptions defaults = GenerationProbe.ProbeOptions.fromSystemProperties();
assertEquals(1, defaults.threads());
assertNull(defaults.dumpFile());
assertThrows(IllegalArgumentException.class, () -> new GenerationProbe.ProbeOptions(0, null, false));
}
@Test
public void reportsMedianNearestRankP95MaximumAndTotal() {
GenerationProbe.TimingSummary odd = GenerationProbe.TimingSummary.from(List.of(50L, 10L, 30L, 20L, 40L));