feat(core): add whole-machine save states with F5/F9 window slots
Machine::save_state and load_state encode the CPU, every RAM, and every device through Snapshot impls into a versioned, checksummed format. Rejected loads leave the machine unchanged. The ROM window saves to and loads from one state file with F5 and F9.
This commit is contained in:
parent
be9c676476
commit
fd73788735
55 changed files with 5157 additions and 20 deletions
|
|
@ -99,6 +99,10 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
|
||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
||||
| `crates/gba-core/src/snapshot.rs` | Save-state binary primitives (writer, reader, tags, FNV fingerprint) and `SnapshotError` |
|
||||
| `crates/gba-core/src/machine/snapshot.rs` | `Machine::save_state`/`load_state`: header, image and cartridge identity, checksum, verify-before-mutate |
|
||||
| `crates/gba-core/src/memory/snapshot.rs`, `crates/gba-core/src/io/snapshot.rs` | Memory bus and I/O register encoders; every other state type implements `Snapshot` beside its definition |
|
||||
| `crates/gba-core/tests/save_state.rs` | Restore-equals-uninterrupted determinism, in-flight Flash operation, and rejected-input tests |
|
||||
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, ordered CPU timing transactions, and timing breakdowns |
|
||||
| `crates/gba-core/src/timing/prefetch.rs` | Nominal eight-halfword opcode queue, partial transfers, capacity stops, and cancellation |
|
||||
| `crates/gba-core/src/timing/prefetch/tests.rs` | Wait-state matrices, partial words, cancellation phases, boundaries, configuration, and progress consistency |
|
||||
|
|
@ -175,6 +179,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/desktop/tests/save_cli.rs` | Original program/save/restart/load tests, diagnostic isolation, and native-window persistence |
|
||||
| `crates/desktop/src/cartridge/rtc_clock.rs` | Host-only UTC seed and monotonic elapsed-time adapter, with injected epoch/duration tests |
|
||||
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
||||
| `crates/desktop/src/cartridge/state_file.rs` | Atomic save-state file writes and bounded reads |
|
||||
| `crates/desktop/src/cartridge/window/tests.rs` | Original ROM pixels, keyboard mapping, STOP wake, HALT, frame budgets, and diagnostics |
|
||||
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, optional audio draining/muting, speed-scaled host pacing, and frame-limited exit |
|
||||
| `crates/desktop/examples/write_rom_demo.rs` | Non-overwriting export of the original input-test ROM |
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ Controls use the same focus correction and key mapping as the existing demos:
|
|||
| Backspace | Select |
|
||||
|
||||
Escape or the window close control ends execution. Enter does not reset the emulator.
|
||||
F5 writes and F9 loads a [save state](save-states.md) in the file chosen by `--state-file PATH` (default: the ROM path with a `.state` extension).
|
||||
Focus loss releases all buttons but does not pause execution.
|
||||
The host samples buttons before each execution slice, including while stopped.
|
||||
|
||||
|
|
|
|||
62
docs/hardware/save-states.md
Normal file
62
docs/hardware/save-states.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Save states
|
||||
|
||||
A save state freezes the emulated machine at a step boundary and restores it later.
|
||||
It is separate from the game's own save (Flash) and does not replace it: [Flash persistence](saves.md) still needs the game's SAVE command and a clean exit.
|
||||
|
||||
## Desktop use
|
||||
|
||||
Window mode binds two keys while the window has focus:
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
| F5 | Write the state file |
|
||||
| F9 | Load the state file |
|
||||
|
||||
The slot is `--state-file PATH`. Without it, the ROM path with its extension replaced by `.state` is used (`roms/pokemon-emerald.gba` becomes `roms/pokemon-emerald.state`).
|
||||
Only one slot exists. F5 replaces it, via a unique sibling temporary file that is synced and renamed, so an interrupted write keeps the previous state.
|
||||
State files are created with mode `0600`. The option is accepted only with `--window`.
|
||||
|
||||
A rejected load (missing file, wrong ROM, corrupt or older-format file) is reported in the terminal and the game keeps running unchanged.
|
||||
Loading discards queued host audio and shows the state's last captured frame immediately.
|
||||
The Flash image inside the state replaces the running one. It is written to `--save-file` only on a clean exit, and only if it differs from the last persisted bytes.
|
||||
The RTC resumes at the saved calendar value and then follows host elapsed time again.
|
||||
Quitting with Escape does not write a state. Press F5 first.
|
||||
|
||||
## What a state holds
|
||||
|
||||
`Machine::save_state` and `Machine::load_state` (`crates/gba-core/src/machine/snapshot.rs`) encode:
|
||||
|
||||
- CPU registers, banks, status, instruction buffer, and retained fetch history.
|
||||
- All RAMs, VRAM, palette, and OAM, plus the Flash image and its modified flag.
|
||||
- Every device: timers and the shared prescaler, DMA channels and latches, display position, interrupts, keypad, serial, audio (FIFOs, PSG channels, wave RAM), Flash and RTC/GPIO state machines.
|
||||
- Video latches: affine accumulators, mosaic and window counters, sprite row pipeline, and the double-buffered scanline capture.
|
||||
- Game Pak prefetch, IWRAM bus lanes, BIOS protected-read snapshot, the device cycle count, and the last step timing.
|
||||
|
||||
It does not hold the ROM or firmware bytes, the host's choice of cartridge hardware and save device, row-capture and audio-capture enablement, or host audio queues and drop counters.
|
||||
An enabled audio capture keeps running and resumes its sample phase.
|
||||
|
||||
## Format and checks
|
||||
|
||||
The layout is `GBAS`, a format version, ROM length and fingerprint, firmware presence and fingerprint, cartridge setup, a length-prefixed payload, and a trailing checksum.
|
||||
Fingerprints and the checksum are 64-bit FNV-1a: they catch mismatched images and accidental corruption, not tampering.
|
||||
The payload is positional with a four-byte tag per section, so a reader out of step fails at the next boundary.
|
||||
Integers are little-endian. Allocations are bounded by fixed memory sizes; no length field drives an unbounded allocation.
|
||||
|
||||
`load_state` verifies the magic, version, checksum, and ROM/firmware/cartridge identity before changing anything.
|
||||
These failures leave the machine untouched.
|
||||
A failure after that point returns `RestoreFailed`; it indicates an encoder defect, and the machine must be rebuilt.
|
||||
Any format change bumps the version. Older states are rejected, not migrated.
|
||||
|
||||
## Limits
|
||||
|
||||
- `save_state` fails with `MidStep` inside a step and `Unsupported` while the machine holds a deferred diagnostic error (a stored failed instruction fetch or failed captured frame). Saving at a later step boundary normally works.
|
||||
- States are for the same build line: any change to encoded hardware state needs a version bump and invalidates old files.
|
||||
- Checksum and fingerprints are not authentication. Do not load untrusted state files expecting more than memory safety.
|
||||
- A state restores hardware, not the host: window frame counters, audio device buffers, and the host RTC baseline are not part of it.
|
||||
|
||||
## Evidence
|
||||
|
||||
`crates/gba-core/tests/save_state.rs` runs original programs, snapshots mid-run, restores into a freshly built machine, and compares per-frame hashes of pixels, captured audio, registers, and the device clock, then compares the final state bytes.
|
||||
It also restores a Flash program operation in flight and checks that every rejected input leaves the machine unchanged.
|
||||
Each encoded type destructures its struct exhaustively, so a new field is a compile error until it is encoded or excluded with a reason.
|
||||
Per-type unit tests check round trips, rejection of every truncated prefix, and invalid discriminants.
|
||||
|
|
@ -130,7 +130,7 @@ The frontend persists changed bytes only on a clean exit:
|
|||
|
||||
An incomplete Flash command/operation refuses persistence and returns an error, even if no bytes changed yet.
|
||||
CPU, DMA, rendering, window, or reported audio errors do not persist the session image.
|
||||
There is no autosave, crash recovery, save-state support, or RTC persistence.
|
||||
There is no autosave, crash recovery, or RTC persistence. [Save states](save-states.md) are a separate, whole-machine feature and do not replace the game's own save.
|
||||
The emulator cannot detect a game's multi-command save transaction. Exiting between completed commands can still save incomplete game data.
|
||||
A crash or forced termination loses changes since startup and can leave a stale lock.
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Transactional Flash completion and modification tracking, with exact-size core image loading and inspection.
|
||||
- Optional desktop `--save-file` loading and clean-exit persistence, with exclusive locks, atomic installation, and unique previous-image backups.
|
||||
- Original ROM save/restart/load checks across processes, including native-window persistence on Darwin arm64.
|
||||
- Whole-machine [save states](hardware/save-states.md): `Machine::save_state`/`load_state` with image and cartridge identity checks, a checksum, and verify-before-mutate loading; ROM windows bind F5 (save) and F9 (load) to a `--state-file` slot.
|
||||
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
||||
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
|
||||
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
|
||||
|
|
@ -192,7 +193,7 @@ That run used no scripted input or pixel assertions.
|
|||
A [fixed-time input probe](research/emerald-wave.md) found a wave trigger with its gate disabled.
|
||||
Wave support now passes that point and reaches 1,320 frames with scheduled Start/A presses and releases.
|
||||
Output differs from a no-input control, but readable menus and gameplay remain unverified.
|
||||
Flash writing and host persistence now pass original save/restart/load tests. Emerald saving and reloading remain unverified.
|
||||
Flash writing and host persistence now pass original save/restart/load tests. Emerald saving and reloading remain unverified; save states offer a separate restore point.
|
||||
A user reached starter selection, then reported a BIOS invalid-argument trap.
|
||||
An [isolated reproduction](research/emerald-empty-lz.md) identified a zero-length LZ77 header rejected before the length check.
|
||||
The replacement BIOS now returns for that empty stream. Full starter-selection progress after this correction needs user retesting.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue