v0.2.0 "Persistence"
v0.2.0 "Persistence"
A versioned, deterministic core-wide save-state format — the prerequisite for rewind/run-ahead
(v0.3.0 "Continuum"), netplay rollback (v1.2.0), and TAS movie replay (v1.4.0), none of which
build until this lands (docs/adr/0006-save-state-format.md, now Accepted).
Oracle/golden suites: all held, no regressions. The full workspace test suite (including
--features test-roms) is green; the new round-trip determinism test additionally proves the
save-state format bit-identical (framebuffer + audio) across a no-coprocessor ROM, a Curated
Super FX ROM, and a BestEffort commercial coprocessor ROM.
Save-state wire format (rustysnes-savestate, new leaf crate):
- SaveWriter/SaveReader: an allocation-free tagged/length-prefixed section format, no serde/
reflection (keeps no_std targets byte-identical, keeps each component's format change local
and auditable). Nested sections write directly into the parent buffer with a patched length
placeholder rather than a throwaway Vec per section. - SaveStateError rejects truncated input, a wrong/unexpected section tag, a bad top-level magic,
and a newer format version than the build understands -- never a panic, never a silent
partial load.
Coprocessor boards (rustysnes-cart, T-52-002 -- every board round-trips its state):
- Obc1Board, Dsp1Board, NecDspVariantBoard (DSP-1/2/4, ST010 via the shared Upd77c25 engine),
Cx4Board (Hg51b), Sdd1Board (+ its Decompressor), SuperFxBoard (Gsu), Sa1Board, Spc7110Board
(+ its Decompressor and paired EpsonRtc). - Untrusted-input validation throughout: an out-of-range semantic value (a cursor, a state-
machine discriminant, a decompressor mode) is rejected with a typed error; a genuine N-bit
hardware register width (a pointer register, a bus-address-line value) is masked to that
width on load, matching the exact mask every normal-operation write already applies at its
own use site. Firmware/chip-ROM dumps are never written to a save-state (ADR 0003).
Core engines (T-52-003 -- Cpu/Ppu/Apu/Bus/System all round-trip their exact state):
- Cpu::save_state/load_state: the full 65C816 register file, WAI/STP latches, cycle counter;
register widths re-masked to the emulation/Status::X invariants on load. - Ppu::save_state/load_state: VRAM/CGRAM/OAM, the full register file (including the six-layer
window unit), write latches, the dot/scanline timeline, interrupt/frame-poll state, region,
and the framebuffer. - Apu::save_state/load_state (rustysnes-apu): Spc700 + Dsp (128-byte register mirror, all 8
voices, echo/noise/BRR/latch/clock sub-units, the queued output-sample FIFO) + ARAM + the
three timers + the in-flight instruction micro-op plan -- the SPC700 analogue of the GSU's
pending-checkpoint queue, needed because sub-instruction lockstep execution can leave an
instruction genuinely mid-drain at any save point. - System::save_state()/load_state() (rustysnes-core): the top-level versioned envelope -- a
4-byte magic (b"RSNS") + u16 format version wrapping Cpu, the whole Bus (Ppu/Apu/Dma/Clock/
MulDiv/WRAM, plus a loaded cart's coprocessor state + battery SRAM), the determinism seed, and
the SA-1 second CPU when present. A save-state's cart/SA-1 presence and SRAM length are
cross-checked against the target System's own installed state and rejected on mismatch --
restoring a cart-carrying save-state requires the caller to have already loaded the same ROM
first, the same "never embed a ROM/firmware byte" posture every coprocessor already follows.
Round-trip determinism test (T-52-004, crates/rustysnes-test-harness/tests/
save_state_determinism.rs): boot a ROM, run 30 frames, snapshot, restore onto a separate
freshly-booted System, run 30 more frames on both the original (continuing uninterrupted) and
the restored system, assert byte-identical framebuffer + audio between the two -- extends the
existing determinism-contract test pattern (docs/adr/0004) rather than inventing a new
verification method.
This release landed across PRs #4-#12, each independently reviewed by Gemini + Copilot and
adjudicated before merge.