mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
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:
1 parent
c3359e0a16
commit
e2430500cf
6 files changed
+1356
-82
No files matched your search
+38
-1
@@ -173,11 +173,28 @@ tasks.register('genProbe', JavaExec) {
|
||||
if (providers.gradleProperty('probeNativeLibrary').isPresent()) {
|
||||
systemProperty('volmlib.noise.nativeBillowLibrary', providers.gradleProperty('probeNativeLibrary').get())
|
||||
}
|
||||
systemProperty('iris.probe.threads', providers.gradleProperty('probeThreads').orElse('1').get())
|
||||
systemProperty('iris.probe.pregenProfile', providers.gradleProperty('probePregenProfile').orElse('false').get())
|
||||
dependsOn(':core:compileJava')
|
||||
doFirst {
|
||||
if (!probePack.isPresent() || probePack.get().isBlank()) {
|
||||
throw new GradleException('genProbe requires -PprobePack=/absolute/path/to/pack')
|
||||
}
|
||||
Provider<String> probeDumpFile = providers.gradleProperty('probeDumpFile')
|
||||
if (probeDumpFile.isPresent()) {
|
||||
File dump = new File(probeDumpFile.get())
|
||||
if (!dump.isAbsolute() || dump.isDirectory()) {
|
||||
throw new GradleException('probeDumpFile must be an absolute file path')
|
||||
}
|
||||
java.nio.file.Path normalizedDump = dump.canonicalFile.toPath()
|
||||
if (normalizedDump.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
|
||||
throw new GradleException('probeDumpFile must point outside the repository')
|
||||
}
|
||||
if (normalizedDump.parent == null || !normalizedDump.parent.toFile().isDirectory()) {
|
||||
throw new GradleException('probeDumpFile parent directory must already exist')
|
||||
}
|
||||
systemProperty('iris.probe.dumpFile', normalizedDump.toString())
|
||||
}
|
||||
if (!probeDimension.isPresent() || probeDimension.get().isBlank()) {
|
||||
throw new GradleException('genProbe requires -PprobeDimension=<dimension-key>')
|
||||
}
|
||||
@@ -194,6 +211,22 @@ tasks.register('genProbe', JavaExec) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('genDiff', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Compares two genProbe chunk dumps and reports block, surface, biome and per-category parity.'
|
||||
mainClass = 'art.arcane.iris.probe.GenerationDiff'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
maxHeapSize = '4g'
|
||||
doFirst {
|
||||
Provider<String> baseline = providers.gradleProperty('diffBaseline')
|
||||
Provider<String> candidate = providers.gradleProperty('diffCandidate')
|
||||
if (!baseline.isPresent() || !candidate.isPresent()) {
|
||||
throw new GradleException('genDiff requires -PdiffBaseline=<dump> and -PdiffCandidate=<dump>')
|
||||
}
|
||||
args(baseline.get(), candidate.get(), providers.gradleProperty('diffWorstChunks').orElse('10').get())
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('hydrologyPackProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Scans explicitly bounded real-pack hydrology tiles across multiple engine seeds and enforces accepted feature/profile coverage.'
|
||||
@@ -353,9 +386,13 @@ tasks.register('transitionProbe', JavaExec) {
|
||||
if (parent == null || !parent.isDirectory()) {
|
||||
throw new GradleException('probeJfr parent directory must already exist')
|
||||
}
|
||||
String settings = providers.gradleProperty('probeJfrSettings').orElse('profile').get()
|
||||
if (settings.isBlank() || settings.contains(',')) {
|
||||
throw new GradleException('probeJfrSettings must name a JFR configuration or an absolute .jfc path')
|
||||
}
|
||||
task.jvmArgs(
|
||||
'-XX:FlightRecorderOptions=stackdepth=256',
|
||||
"-XX:StartFlightRecording=filename=${normalized},settings=profile,dumponexit=true"
|
||||
"-XX:StartFlightRecording=filename=${normalized},settings=${settings},dumponexit=true"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user