This commit is contained in:
Brian Neumann-Fopiano
2026-08-12 13:52:16 -04:00
parent 12b97b7994
commit 365205ad0a
82 changed files with 7978 additions and 5625 deletions
+126 -85
View File
@@ -1,60 +1,124 @@
# 32 - Determinism & Goldenhash
GoldenHash is the cross-platform determinism gate: it generates chunks into in-memory buffers (no world block writes), hashes blocks and biomes, and either captures a baseline file or verifies against one. Identical pack bytes, Iris seed, radius, center, and height range must produce the same combined hash on Bukkit-family and every mod loader. Operator smoke sequences that use this gate are in `31 - Operator Runbooks & Smoke Tests.md`.
GoldenHash is the cross-platform determinism gate. It regenerates a square of chunks into memory buffers, hashes the blocks and biomes, and either writes that fingerprint to a baseline file or compares against one. The same pack bytes, Iris seed, radius, center, and height range must produce the same combined hash on Bukkit-family servers and on every mod loader. The operator runbooks that use this gate are in `31 - Operator Runbooks.md`.
## Tutorial: capture and compare a baseline
## What determinism means here
Prerequisites: a disposable Iris world, fixed pack bytes, seed `1337`, center chunk `0,0`, and a small radius such as `8` for the first run. Record the Iris artifact, Minecraft version, pack hash, dimension height range, and JVM before capture.
Iris generation is a pure function of the pack plus a seed. Feed the same pack and the same seed to the engine and every block and biome must come back identical — same chunk, same platform, next week, other machine. Nothing in that chain is allowed to depend on wall-clock time, hash iteration order, thread scheduling, or which chunks happened to be generated first.
1. On Bukkit, run the same `AUTO` command twice. The first run captures when the file is absent; the second must verify it:
That property is what makes an Iris world portable. A player's base survives a server restart, a Bukkit world can be reproduced on Fabric, and a pack author can hand someone a seed and get the same terrain back. When determinism breaks, it usually breaks quietly: a handful of chunks differ at a biome boundary or inside a cave, nobody notices until a world is regenerated months later and the terrain no longer lines up with the buildings on it.
GoldenHash exists because eyeballing terrain cannot catch that. Two screenshots can look identical while thousands of blocks differ. A hash catches a single changed block state.
## What GoldenHash actually does
1. Optionally deletes the world's mantle so nothing cached from an earlier run can leak into the comparison.
2. Walks a center-out spiral of chunks and calls `engine.generate` for each one into a scratch buffer. The world's region files are never touched.
3. SHA-256s every block state key in the chunk, and separately SHA-256s biome samples taken on a 4-block grid.
4. Sorts the per-chunk lines by chunk coordinate and SHA-256s the whole ordered body into one `#combined` value. Sorting is why thread count does not change the result.
5. Writes that as a baseline, or compares it to an existing one and reports MATCH or MISMATCH.
Because the scan regenerates rather than reading the world, it also proves the engine can reproduce what it already wrote — not just that the world file on disk is stable.
## Capture a baseline
Prerequisites: a disposable Iris world, frozen pack bytes, a known seed (`1337` is the convention across these docs), center chunk `0,0`, and a small radius such as `8` for a first run. Write down the Iris artifact version, Minecraft version, pack hash, dimension height range, and JVM before you start — a baseline without that provenance is not usable later.
1. On Bukkit, run the command twice. The Bukkit command is always `AUTO`: the first run captures because no file exists, the second verifies against it.
```text
/iris developer goldenhash world=<world> radius=8 threads=1 center-x=0 center-z=0 reset-mantle=true deep=false
/iris developer goldenhash world=<world> radius=8 threads=1 center-x=0 center-z=0 reset-mantle=true deep=false
```
2. On Fabric, Forge, or NeoForge, use explicit modes:
Expected: the first run prints a captured line with the chunk count, a 12-character short hash, and the absolute path of the `.hashes` file. The second prints MATCH with the same short hash.
If the first run prints nothing but an error about the world, the target is not a loaded Iris world with a live engine. Check `/iris worlds`.
2. On Fabric, Forge, or NeoForge, ask for the modes explicitly:
```text
/iris goldenhash 8 1 capture
/iris goldenhash 8 1 verify
```
3. Retain the generated `.hashes` file with the exact artifact and pack hash. Copy that baseline into the platform-specific golden directory on another loader, keep the dimension key, seed, radius, center, and height range identical, then run `verify` or Bukkit `AUTO`.
4. Confirm every platform reports `MATCH` and the same `#combined` value. Visual similarity is not parity evidence.
5. In a disposable copy, change one pack or engine input, rerun verify, and confirm the expected mismatch produces `.new` and a diagnosis file.
6. Restore the original input and confirm `MATCH` again before treating the baseline as a release artifact.
Expected: `capture` writes the file, `verify` reports MATCH. Progress lines appear per chunk while the total is 64 chunks or fewer, then every 32nd chunk above that.
GoldenHash resets mantle according to the platform rules below. Never use a production world merely because block writes are buffered.
3. Keep the `.hashes` file next to the exact artifact build and pack hash it came from. It is only meaningful with that provenance.
### Recovery
## Compare a second platform
| Result | Correction |
1. Copy the `.hashes` file into the other platform's golden directory (see the table below). Do not rename it — the filename encodes the dimension key, seed, center, and radius the comparison depends on.
2. Create a disposable world there from the same pack bytes and the same seed, with the same dimension height range.
3. Run `verify` (modded) or the same `AUTO` command (Bukkit).
4. Expected: MATCH, and the same `#combined` value on every platform. Screenshots that look alike prove nothing.
Expect one warning on every cross-platform comparison: the `#mc` metadata line differs because Bukkit records `Bukkit.getBukkitVersion()` (for example `26.2-R0.1-SNAPSHOT`) while mod loaders record the plain Minecraft version (`26.2`). That mismatch is a warning only and does not stop the comparison. A seed or dimension mismatch is a hard failure and does stop it.
## Prove the gate can fail
A gate nobody has seen fail is not a gate. Once, in a disposable copy:
1. Change one pack input or one engine input.
2. Re-run verify. Expected: MISMATCH, a `.new` file next to the golden file, and a `.diag-c<x>x<z>.txt` for the first differing chunk.
3. Restore the original input and confirm MATCH returns before you treat the baseline as a release artifact.
## Reading a result
| Result | Meaning |
|--------|---------|
| Captured | No baseline existed; one was written. The path is printed. |
| MATCH | Every chunk line is byte-identical to the baseline. The short combined hash is shown. |
| MISMATCH | At least one chunk differs. Up to 10 chunk keys are listed, then a count of the rest. `.new` and a diagnosis file are written. |
| Aborted | Some chunk failed to generate. Nothing is written; fix the generation failure first. |
| Wrong world | The baseline's `#seed` or `#dim` does not match the live engine. No comparison is attempted. |
The diagnosis file regenerates the first mismatched chunk twice back to back, then a third time after deleting mantle chunks around it, and labels what it found:
- **Repeat-generation STABLE** — two consecutive generations agree. The divergence depends on order or accumulated state, not on the generate call itself.
- **Repeat-generation UNSTABLE** — the same chunk differs between two consecutive calls with nothing in between. That is pure nondeterminism and is always an engine defect.
- **Mantle-reset** — compares the scan result against a generation with the surrounding mantle deleted. A difference here points at mantle carryover rather than the terrain math.
The file ends with a full non-air block dump of the first generation, which is what you diff against the other platform's dump.
## When a check fails
| Symptom | What to do |
|---|---|
| No baseline on Bukkit | Confirm the target is a loaded Iris world; the first `AUTO` run should capture. |
| `VERIFY` says the file is missing | Check the golden directory and exact filename inputs: dimension key, seed, center, and radius. |
| Seed or dimension mismatch | Recreate the disposable world with the recorded input; do not rename metadata to force a comparison. |
| Multi-thread mismatch only | Re-run both sides with `threads=1`; treat continued order dependence as an engine defect. |
| Stable mismatch | Compare pack bytes, height range, Iris artifact, Minecraft version warning, and mantle-reset choice, then inspect `.new` and the first `.diag-*` file. |
| Unstable repeat generation | Preserve the diagnosis artifacts and stop the release comparison; consecutive generation is nondeterministic. |
| Bukkit run says the target is not an Iris world | The world is not loaded or has no engine. Load it and retry; `AUTO` cannot capture from a dead engine. |
| `verify` says the baseline file is missing | The filename is built from dimension key, seed, center, and radius. One of those differs from the capture. Compare the name in the golden directory against your command. |
| Wrong world (seed or dimension) | Recreate the disposable world with the recorded seed and pack. Never hand-edit the metadata lines to force a comparison. |
| Mismatch only with multiple threads | Re-run both sides with `threads=1`. If serial matches and parallel does not, that is an engine defect, not a tuning problem — the result is sorted before hashing, so thread count must not matter. |
| Stable mismatch on both sides | Compare pack bytes, dimension height range, Iris artifact, and whether both runs reset mantle. Then read `.new` and the first `.diag-*` file. |
| Repeat-generation UNSTABLE | Stop the release comparison. Preserve the diagnosis artifacts; consecutive generation is nondeterministic and no baseline is trustworthy until that is fixed. |
## What it measures
## Rules that keep comparisons honest
- **Blocks:** every local column `x,z ∈ [0,15]` and every `y` from engine/world min height (inclusive) to max height (exclusive). Each block state key (for example `minecraft:stone`) is fed into a per-chunk SHA-256 digest.
- **Biomes:** same height span sampled on a 4-block step in x, y, and z (`BIOME_STEP = 4`). Null biome samples hash as `minecraft:plains` (constant `GoldenHashEngine.FALLBACK_BIOME_KEY`).
- **Combined hash:** SHA-256 over the ordered per-chunk lines for the full spiral; stored as `#combined=<hex>` in the golden file.
- **World disk:** buffers only — the Minecraft region files are not written by the scan. **Mantle** (Iris multi-chunk structure/carving cache) **is reset** when `reset-mantle` is true (Bukkit default) or always on modded.
1. **Same pack bytes everywhere.** Same pack key, same files. For a release baseline, freeze the default overworld download to a commit or tag.
2. **Same Iris seed.** Create with `seed=1337` (Bukkit) or the positional `1337` (modded), as in `02 - Getting Started.md`.
3. **Same radius and center.** Modded center is fixed at chunk `0,0`, so use `center-x=0 center-z=0` on Bukkit.
4. **Same height range.** The dimension's min and max Y define the hash window; changing height changes the hash by definition.
5. **Use `threads=1` when hunting order dependence.** Multi-threaded scans must still match the serial result.
6. **Reset mantle on both sides** when you are comparing regeneration purity. Bukkit defaults to `reset-mantle=true`; modded always resets.
7. **Do not confuse pregen with hashing.** GoldenHash regenerates into buffers and ignores whatever pregen wrote. Pregen is still worth running as a stress gate before or after (`07 - Pregeneration.md`).
8. **Disposable worlds only.** Buffers mean no block writes, but `reset-mantle` deletes the world's mantle files, and the diagnosis path deletes mantle chunks around the failing chunk.
Use disposable test worlds. Mantle reset deletes mantle files under the engine mantle data folder so regeneration starts from a clean mantle state.
## Reference
## File location and name
### What is hashed
- **Blocks:** every local column `x,z` in `0..15` and every `y` from the engine or world minimum height (inclusive) to the maximum (exclusive). Each block state key (for example `minecraft:stone`) is fed into a per-chunk SHA-256 digest.
- **Biomes:** the same height span sampled every 4 blocks in x, y, and z (`BIOME_STEP = 4`). A null biome sample hashes as `minecraft:plains` (`GoldenHashEngine.FALLBACK_BIOME_KEY`).
- **Combined:** SHA-256 over the per-chunk lines sorted by packed chunk key, stored as `#combined=<hex>`.
- **Not touched:** Minecraft region files. The scan generates into buffers only.
- **Deleted when `reset-mantle` is true:** every file directly inside the engine's mantle data folder, after a `saveAll`. This is the whole world's mantle, not only the scanned square, despite what the in-game help text implies.
### Golden file location and name
| Platform | Golden directory |
|----------|------------------|
| Bukkit-family plugin | Iris data folder `golden/` (for example `plugins/Iris/golden/`) |
| Fabric / Forge / NeoForge | Loader config dir `irisworldgen/golden/` |
Filename pattern:
| Fabric / Forge / NeoForge | `<configDir>/irisworldgen/golden/` |
```
<dimensionLoadKey>-s<seed>-c<centerChunkX>x<centerChunkZ>-r<radius>.hashes
@@ -62,87 +126,64 @@ Filename pattern:
Example: `overworld-s1337-c0x0-r22.hashes`.
Format header: `iris-goldenhash v1`. Metadata lines include `#world`, `#dim`, `#seed`, `#mc`, `#minY`/`maxY`, `#center`, `#radius`. Body lines are `chunkX chunkZ <blockSha256> <biomeSha256>`.
The seed in that name is `World.getSeed()` on Bukkit and the Iris engine seed (`engine.getSeedManager().getSeed()`) on mod loaders. For a world created through `/iris create` with an explicit seed they are the same value.
On mismatch, the engine also writes:
File layout:
- `<file>.new` — current scan body + combined hash
- `<file>.diag-c{x}x{z}.txt` — first mismatched chunk diagnosis (repeat-generation stability, mantle-reset comparison, non-air dump)
- Optional deep dumps when `deep=true` (Bukkit only): non-air blockstate listings under a `.deep` / `.deep-verify` sibling directory
```
#iris-goldenhash v1
#world=<world name>
#dim=<dimension load key>
#seed=<seed>
#mc=<platform Minecraft version string>
#minY=<min> maxY=<max>
#center=<cx>,<cz>
#radius=<chunks>
<chunkX> <chunkZ> <blockSha256> <biomeSha256>
...
#combined=<sha256>
```
## Modes
On mismatch the engine also writes:
Shared engine modes: `AUTO`, `CAPTURE`, `VERIFY`.
- `<file>.new` — the current body plus its combined hash
- `<file>.diag-c<x>x<z>.txt` — diagnosis of the first mismatched chunk
- `<file>.deep/` or `<file>.deep-verify/` — per-chunk non-air block dumps when `deep=true` (Bukkit only); the `-verify` suffix is used when a golden file already exists
### Modes
| Mode | Behavior |
|------|----------|
| `CAPTURE` | Always write a new golden file |
| `VERIFY` | Fail if no golden file; compare and report MATCH/MISMATCH |
| `AUTO` | Capture when the file is missing; verify when it exists |
| `VERIFY` | Fail if no golden file exists; otherwise compare and report MATCH or MISMATCH |
| `AUTO` | Capture when the file is missing, verify when it exists |
Bukkit command always uses `AUTO`. Modded commands expose `capture`, `verify`, and default `AUTO`.
The Bukkit command is hard-wired to `AUTO`. Modded exposes `capture` and `verify` literals and falls back to `AUTO`.
Verify rejects seed or dimension mismatches as hard failures. Minecraft version metadata mismatch is a **warning** only (hash comparison still runs).
### Commands
## Commands
### Bukkit
Bukkit:
```
/iris developer goldenhash world=<world> radius=<chunks> threads=<n> center-x=<cx> center-z=<cz> reset-mantle=<bool> deep=<bool>
```
Alias: `gold`. Defaults: `radius=8`, `threads=8`, `center-x=0`, `center-z=0`, `reset-mantle=true`, `deep=false`. Radius must be 0. Target must be a loaded Iris world with a live engine.
Alias `gold`. Defaults: `radius=8`, `threads=8`, `center-x=0`, `center-z=0`, `reset-mantle=true`, `deep=false`. Radius must be at least 0. The target must be a loaded Iris world with a live engine. Metadata comes from `World.getSeed()`, `Bukkit.getBukkitVersion()`, and the world's min and max height. Nothing stops a second Bukkit scan from starting while one is running; do not start one.
- Seed recorded and used for filename: `World.getSeed()`.
- MC version string: `Bukkit.getBukkitVersion()`.
- Generation path: `engine.generate` into a `TerrainChunk` buffer.
### Modded (Fabric / Forge / NeoForge)
Modded (Fabric / Forge / NeoForge):
```
/iris goldenhash [radius] [threads] [capture|verify]
```
Alias: `gold`. Defaults: radius `8`, threads `8`, mode `AUTO`. Radius range on the command tree: `0..256`. Threads: `1..64`. Center is always chunk `0,0`. Mantle is always reset. Deep dump is not exposed.
Alias `gold`. Defaults: radius `8`, threads `8`, mode `AUTO`. The command tree accepts radius `0..256` and threads `1..64`. Center is always chunk `0,0`, mantle is always reset, and deep dumps are not exposed. A second scan is refused while one is running.
- Seed: Iris engine seed (`engine.getSeedManager().getSeed()`), not a vanilla level-seed quirk.
- MC version: loader-reported Minecraft version string.
- Generation path: `engine.generate` into modded block/biome buffers.
- Only one modded GoldenHash scan may run at a time.
While any scan is active, Iris suspends engine mantle maintenance (trimming and plate unloading) process-wide so maintenance cannot perturb the comparison.
Only one shared-engine scan may be active process-wide (`GoldenHashEngine.isActive()`).
### Offline generation probe (not a GoldenHash file)
## Determinism rules operators must follow
`./gradlew :probe:genProbe -PprobePack=…` builds an offline engine for dimension key `overworld` at seed `1337`, validates the pack, generates a chunk spiral into buffers, and prints per-chunk hashes to stdout. It never reads or writes `iris-goldenhash v1` files, so it cannot be compared against a baseline. Use it as a headless regression signal; use in-game GoldenHash for cross-platform gates. Probe details: `31 - Operator Runbooks.md`.
1. **Same pack bytes** on every platform (same pack key, same files; freeze default overworld downloads to a commit/tag for release baselines).
2. **Same Iris seed** (create with `seed=1337` / positional `1337` as in `02 - Getting Started.md`).
3. **Same radius and center** (modded center is fixed at 0,0 — use `center-x=0 center-z=0` on Bukkit for parity).
4. **Same height range** (dimension min/max Y). Height is part of the hash window.
5. **Prefer `threads=1` for order-dependence checks.** Multi-thread scans must still match the serial result when the engine is deterministic; a multi-thread-only mismatch is a bug.
6. **Reset mantle** between capture and verify when comparing regeneration purity (`reset-mantle=true` / modded always).
7. **Do not treat world pregen order as the hash source** — GoldenHash re-generates into buffers. Pregen is still useful as a stress gate before or after hashing (`07 - Pregeneration.md`, `31 - Operator Runbooks & Smoke Tests.md`).
### Release gate
## Interpreting results
| Result | Meaning |
|--------|---------|
| Captured | New baseline written; path printed |
| MATCH | All chunk lines equal; combined hash short form shown |
| MISMATCH | One or more chunks differ; up to 10 chunk keys listed; `.new` written; diagnosis on first mismatch |
| Aborted | Not all chunks generated successfully; no golden write |
| Wrong world | Golden `#seed` / `#dim` does not match current engine |
Diagnosis labels:
- **Repeat-generation STABLE** — two back-to-back generations of the same chunk agree; divergence is order/state-dependent relative to the golden or mantle, not pure non-determinism per call.
- **Repeat-generation UNSTABLE** — same chunk differs between two consecutive generations without intervening work.
- **Mantle-reset** section compares the first generation to a generation after deleting mantle chunks in the structure radius.
## Offline generation probe (not GoldenHash files)
`./gradlew :probe:genProbe -PprobePack=…` builds an offline engine (dimension key `overworld`, seed `1337`) and prints per-chunk hashes to stdout. It validates the pack and exercises generation without a server; it does **not** read or write `iris-goldenhash v1` files. Use in-game GoldenHash for cross-platform golden files. Probe overview: `31 - Operator Runbooks & Smoke Tests.md`.
## Release gate expectation
Release procedure requires GoldenHash **VERIFY** to pass on all four platforms with the **same** combined hash for the shared pack and seed (`86 - Maintainer - Release Checklist.md`). An unexplained deterministic output change is a release blocker (`87 - Maintainer - Release Readiness.md`).
Release requires GoldenHash `VERIFY` to pass on all four platforms with the same combined hash for the shared pack and seed (`86 - Maintainer - Release Checklist.md`). An unexplained change in that hash blocks the release (`87 - Maintainer - Release Readiness.md`).