mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-30 05:50:48 +00:00
d
This commit is contained in:
@@ -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`).
|
||||
|
||||
Reference in New Issue
Block a user