sigilOS RetroPie now has a formal savestate ABI across 7 emulator cores — the foundation for Mode-2 deterministic rollback netplay. core_savestate(dst) serializes the full machine state (CPU registers, RAM, PPU/graphics state, OAM) to a memory region; core_loadstate(src) restores it deterministically. Display buffers are excluded (regenerated by core_run_frame — no need to serialize the render output). core_savestate_size() returns the blob size for each core. Covered cores: NES (10,528 B), Game Boy (65,600 B), Odyssey 2, TI-99/4A, Dragon 32, and Acorn Electron (contiguous state blocks), PC-FX (65,672 B — two disjoint regions: CPU state + 64KB RAM). Deterministic round-trip verified. EL0 apps rebuilt with savestate ABI. (sigil-retropie f32ba57)
What savestates enable
Mode-2 SRDX netplay requires that either peer can roll the simulation back to a known-good state when input desync is detected. A "rollback" is: snapshot the current state → receive the remote peer's input for frame N → re-simulate from frame N → arrive at a corrected frame N+1. Without savestates, rollback is impossible. With savestates, any frame boundary is a potential rollback point.
This is the same technique used by GGPO and modern rollback netcode implementations — the innovation in sigilOS is that it operates at the kernel level, with the savestate ABI defined by the emulator core seam rather than by a user-space library. The capability model means only the session-authorized netplay peer can trigger a core_loadstate — a rogue EL0 process cannot force a state restore.
The ABI (f32ba57): Three functions, universal across all cores
dst. Writes CPU registers, system RAM, graphics/PPU state, OAM, sound registers, mapper state. Excludes display output buffers (regenerated from state by core_run_frame). Returns 0.src. After a core_loadstate, calling core_run_frame produces the same output as it would have after the original core_savestate. Deterministic by construction.core_savestate.Core-by-core savestate sizes
| Core | CPU | State contents | Blob size |
|---|---|---|---|
| NES | MOS 6502 | CPU regs + 2KB system RAM + PPU (VRAM, OAM, palette, scroll, nametables) | 10,528 B |
| Game Boy | Sharp LR35902 | 64KB address space (ROM bank mirrors + VRAM + WRAM + OAM + I/O + HRAM) + CPU regs (PC/SP/AF/BC/DE/HL + flags) | 65,600 B |
| Odyssey 2 | Intel 8048 | Contiguous memory block (scratchpad RAM + register file + I/O state) to display offset | ~1,200 B |
| TI-99/4A | TMS9900 | Contiguous block (scratchpad + VDP VRAM + CPU regs + VDP registers) to display offset | ~16,512 B |
| Dragon 32 | Motorola 6809E | Contiguous block (32KB RAM + CPU regs + MC6847 VDG state) to display offset | ~32,800 B |
| Acorn Electron | MOS 6502A | Contiguous block (32KB RAM + CPU regs + ULA state) to display offset | ~32,800 B |
| PC-FX | NEC V810 | Two disjoint regions: CPU state (registers + PC + PSW) + 2MB system RAM | 65,672 B |
The contiguous memory block to display offset pattern (Odyssey 2, TI-99/4A, Dragon 32, Electron): these cores store all state in a single flat memory region; the display buffer starts at a fixed offset that core_savestate stops before. Clean separation — state before the offset, render buffer after.
The PC-FX uses two disjoint regions because the V810's CPU state (general-purpose registers, program counter, program status word) is in a separate structure from the 2MB RAM. core_savestate serializes both regions sequentially into the output blob.
Deterministic round-trip
The savestate_test.sg test for each core runs the same sequence: core_run_frame → core_savestate → modify state → core_loadstate → core_run_frame → verify output matches pre-modification run. All 7 cores PASS.
Determinism here means pixel-exact: the frame output after a restore-and-run must be a byte-for-byte match with the frame that would have been produced had the modification never occurred. This is a stronger guarantee than "close enough" — rollback netplay fails if the restored simulation diverges even by one pixel in the internal framebuffer.
SRDX Mode-2 integration path
Savestates plug into the SRDX netplay pipeline at the frame boundary:
Frame N begins: pmtmr_frame_start() // timestamp
Input received: xbox_bt_snap() / gp_snap_p1() // input state snapshot
Simulation: core_run_frame()
State committed: core_savestate(rollback_slot[N % 8]) // circular rollback buffer
SRDX event — remote peer sends corrected input for frame M < N:
core_loadstate(rollback_slot[M % 8])
gp_rest()
re-run frames M+1 .. N
resume
The circular rollback buffer (8 slots) means any of the last 8 frames is a valid rollback target. At 60 fps, 8 frames = ~133 ms of rollback window — enough for typical WAN round-trip latencies. The slot index wraps via N % 8, so the oldest slot is always overwritten first.
The SRDX transport surface (syscalls 113–116, shipped in the same sprint) carries the corrected input event from the remote peer to the local kernel thread that drives this loop. The savestate ABI and the SRDX transport together are the two halves of Mode-2 rollback netplay.