synvael/docs/adr/0009-baseline-relative-sparse-chunk-persistence.md
Serkyo cae434d227
Some checks are pending
CI / Rust Check & Lint (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Lua Lint & Format (push) Waiting to run
CI / LFS Pointer Guard (push) Waiting to run
docs(workspace): rewrite the subsystem notes and ADRs
2026-08-06 22:52:57 +02:00

28 lines
3.6 KiB
Markdown

# 0009. Baseline-relative sparse chunk persistence
- **Status:** Accepted
- **Date:** 2026-07-10
## Context
Because our worldgen is strictly seed-deterministic (see [ADR-0003](0003-seed-deterministic-worldgen.md)), any completely unmodified chunk is reproducible bit-for-bit from just `(seed, chunk_coord, worldgen_version)`. Our world is procedurally generated, entirely unbounded on the Y axis, and viewed at massive horizontal distances in both single-player and multiplayer. Because of this, the total set of chunks a session simply *visits* is effectively unbounded.
If we persisted the full voxel contents of every single visited chunk (the naive model), the save file size would scale directly with the volume *explored* rather than the volume *actually changed*. Since we use a half-scale voxel grid ([ADR-0002](0002-half-scale-voxel-grid.md)) where a unit volume holds roughly eight times the voxels of a standard 1-meter grid, that storage cost would compound aggressively. The overwhelming majority of visited chunks are never modified by the player, so storing them on disk just duplicates data that our generator can reproduce perfectly on demand.
The part of this decision that is hardest to reverse is the *on-disk representation* of a chunk. Both the `SYNC` record format and the `ChunkData` type are strictly shaped by it, and changing this representation later will require a heavy save-format migration.
## Decision
We only persist a chunk when its contents actually diverge from its deterministic baseline.
- **Representation:** Both on disk (in the `SYNC` record) and in memory (as `ChunkData`), a modified chunk is stored purely as a sparse `local_index → BlockId` edit map layered directly over the regenerated baseline, alongside the `worldgen_version` that the baseline is pinned to. If a chunk is totally unmodified, it stores absolutely no voxel data and is omitted from its region file entirely.
- **Load:** When we load a chunk, we resolve the baseline by regenerating it from the seed, and then we just apply the stored diff on top if a record exists. If there is no record (a miss), it means the chunk was never modified, so the freshly regenerated baseline *is* the chunk.
- **Version pinning:** Each persisted chunk explicitly records the `worldgen_version` its baseline was generated under. This guarantees that a future generator update won't silently shift the baseline beneath an already-modified chunk and corrupt the edits. A region file pins a `base_worldgen_version` globally and only stores per-chunk exceptions to save space.
## Consequences
- Save file size explicitly scales with the volume *modified*, not the volume explored. If a player walks across untouched terrain for miles, it writes absolutely nothing to disk.
- When you revisit an unmodified chunk, it inherently re-runs worldgen instead of reading anything back from disk. We mitigate this CPU cost heavily using an LRU cache of regenerated baselines. This cache is purely a performance layer and doesn't affect authority or determinism at all.
- Worldgen determinism is aggressively promoted from a mere worldgen-local property to a rock-solid, load-bearing invariant of the entire persistence layer. If the generator ever ceased to be reproducible, every unmodified chunk and every stored diff's baseline would be instantly corrupted. Because of this, determinism regressions are guarded aggressively by tests.
- A per-chunk `worldgen_version` stamp is mandatory metadata. The load and write-back paths must strictly honor it once we introduce more than one worldgen version. (Right now we assume a single version, and honoring it fully is tracked as follow-on work).