Skip to content
daniel@cosenza:~/blog
RetrogamingDeep Dive June 8, 2026 3 min read

How Save States Work: Serializing an Entire Virtual Machine to Disk

A save state isn't a save file — it's a snapshot of literally everything, taken mid-execution. That distinction is why it's so powerful, and why it's so fragile across versions.

A traditional in-game save is data the game itself chose to write — your position, inventory, story progress — in a format its own developers designed. A save state is something else entirely: a snapshot of the entire emulated machine, taken at an arbitrary instant, capturing everything needed to resume execution from exactly that point as if no time had passed at all — mid-jump, mid-animation, one instruction into a function call, anywhere.

What actually has to be captured

Reconstructing a running machine exactly means serializing every piece of state that could affect future execution:

Save state contents (typical):
  - CPU registers (program counter, accumulator, flags, stack pointer)
  - Full RAM contents
  - PPU/video chip state (scroll position, palette, sprite tables,
    current scanline/dot being rendered)
  - APU/audio chip state (channel frequencies, volumes, envelope timers)
  - Cartridge mapper state (bank switching registers, if the cartridge
    hardware supports switching between ROM banks)
  - Any battery-backed or mapper-internal RAM
  - Pending/in-flight interrupt state

Miss any one of these and reloading the state won’t actually resume correctly — the emulator description “just serialize the RAM” undersells the problem badly; the RAM is often the easy part, and the video/audio chip’s mid-scanline internal state is usually the hardest, precisely because it’s tightly coupled to exact cycle timing (see cycle-accurate emulation).

Why a save state from one emulator often won’t load in another

A save state is fundamentally a dump of one specific emulator core’s internal data structures — not a documented, stable file format. If a core’s internals change between versions (a bug fix that adds a previously-missing register, a refactor that reorders a struct), old save states can fail to load or, worse, load into a subtly wrong internal layout. This is the direct trade-off for how save states work at all: capturing genuinely everything, with no restriction to a game-defined save format, means there’s no independent, stable schema to serialize against — the format is whatever the emulator’s own internals happen to look like at that version.

Save states as the basis for other features

Once an emulator can serialize and restore its entire state cheaply, several other features become almost free:

  • Rewind — periodically capture a save state in the background (every second, say), and “rewinding” is just reloading the most recent one before the point you want to undo to, then optionally re-simulating forward a few frames for a smoother effect.
  • Tool-assisted speedruns (TAS) — a TAS is built by repeatedly: try an input, save state, see the result, reload the state, try a different input — refining frame-by-frame until the input sequence is provably optimal, something only practical because reloading state is instant and exact.
  • Rollback netcode — as covered in rollback netcode, online multiplayer rollback is, at its core, “save state before simulating a guessed frame, and reload it if the guess turns out wrong” — applied dozens of times per second.

Why this makes save states version-sensitive but incredibly powerful

The same property that makes save states fragile across emulator versions — capturing raw internal state rather than a stable, documented format — is exactly what makes them powerful enough to build rewind, TAS tooling, and rollback netcode on top of. A documented, stable, cross-version save format would need to anticipate every piece of state any future core version might need, which isn’t realistic; an internal-state dump doesn’t need to anticipate anything, because it just captures whatever already exists. The practical implication for anyone using save states day to day is simple: keep them paired with the specific emulator core version that created them, and don’t expect one to survive a major core update untouched.