5 KiB
Save format
How modified chunks are framed, stored, and read back from disk. This note describes the implementation.
The pure, in-memory framing (the SYNR region index and SYNC chunk records) lives in crates/shared/src/save/. The filesystem side — reading a region file, mutating its chunks, and flushing it back crash-safely — lives in crates/server/src/save/. The runtime load path that turns a ChunkPos into a resident chunk lives in crates/server/src/world_server.rs and is driven by the streaming reconcile loop documented in chunk_streaming.md.
What is persisted
Only chunks that diverge from their deterministic worldgen baseline are stored; the rationale is recorded in ADR-0009. A modified chunk is a ChunkData: the chunk position, the worldgen_version its baseline is pinned to, and a sparse local_index → BlockId edit map. An unmodified chunk stores no voxel data and is absent from its region file.
On-disk layout
Voxel storage is partitioned into region files, each covering a 32×32 grid of chunk columns in the XZ plane (the grid is 2D; Y is not partitioned). A chunk's region is (cx.div_euclid(32), cz.div_euclid(32)) — div_euclid, not truncating division, so negative columns floor toward negative infinity rather than toward zero. All multi-byte integers are little-endian.
A region file is a SYNR index followed by the SYNC records the index points at.
SYNRregion index (region.rs): magic tag, framing version, and three side tables — a header table (ChunkPos → offset+length+flagsfor every resident record), a free list (reclaimable spans left by removed or shrunken records), and a stamp table (per-chunkworldgen_versionexceptions; the region pins abase_worldgen_versionand stores only chunks that differ from it). The header and stamp tables areBTreeMaps so their serialization order is deterministic.SYNCchunk record (record.rs): a fixed header (magic, chunk-format version, flags,last_modifiedtimestamp in unix-ms, and the compressed and uncompressed payload lengths) followed by a zstd-compressed, postcard-serializedChunkData. The header is never compressed, so a repair tool can read framing without decompressing. Compression is zstd level 3, favouring speed.
Durability layer
RegionFile reads a region file into memory, mutates its chunks (write_chunk, remove_chunk), and flushes it back. The flush strategy is a whole-file atomic rewrite: the complete file image is serialized, written to a .tmp sibling, fsynced, renamed over the target, and the containing directory is fsynced. This is a simpler alternative to an incremental append-plus-header-rewrite scheme; the deviation is noted at the serialize site and tracked for revision as follow-on work. The on-disk format is unchanged, so the switch requires no migration (the free list and absolute record offsets already support it).
Concurrency: the save actor
Region files are owned by a single dedicated thread, the save actor (region_actor.rs). It holds the map of open RegionFiles and is their sole owner, so no region file needs a lock of its own. Worker threads never touch a region file directly; they hold cloned senders on the actor's request channel and communicate by message. This is the message-passing-over-shared-state concurrency stance from AGENTS.md applied to persistence: one queue thread serializes all region I/O, keeping it off both the simulation tick and the worker pool. A region file is opened on first access and its contents are served from memory thereafter.
Load pipeline
A load of ChunkPos runs on the worker pool (off the tick thread), in world_server.rs load_chunk:
- The worker asks the save actor for the stored record at the position.
- Hit (
Some(ChunkData)): the baseline is regenerated (via the LRU baseline cache) and the stored diff is materialized over it. - Miss (
None): the chunk was never modified, so the regenerated baseline is the chunk. - Save-layer error: streaming must not wedge, so the chunk falls back to a fresh baseline and the error is logged.
The regenerated baseline currently uses the current worldgen version rather than the record's stored worldgen_version; while a single version exists these coincide. Honouring the stored version on both load and write-back is follow-on work.