Skip to content
RetrogamingDeep Dive Published Updated 6 min readViews unavailable

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

How emulators serialize CPU, memory, devices, clocks, and pending events—and why completeness, determinism, versioning, and validation matter.

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).

The list also needs scheduler deadlines, fractional clock remainders, DMA progress, controller shift registers, disc or tape position, coprocessor pipelines, random-number state, memory-mapper latches, pending host callbacks, and any cache whose contents can affect behavior. Purely derived caches may be rebuilt after load, but only if invalidation and reconstruction are deterministic.

For threaded emulators, all participating devices must reach a coherent checkpoint. Copying RAM while an audio or graphics worker is still mutating related state creates a snapshot that never existed. Implementations pause or synchronize workers, capture state, then resume without exposing a partial machine.

Serialization is a designed format, not necessarily a memory dump

An emulator can write each field into an explicit schema, use a tagged save system, or serialize an opaque core-defined byte stream. Dumping C or C++ structures directly is tempting but fragile: padding, pointer values, endianness, compiler layout, host word size, and transient references are not portable machine state. Pointers generally need to become identifiers or offsets and be reconstructed after loading.

A robust file wraps state with a format version, emulator and core identity, target system, content hash, firmware identity, options affecting determinism, byte order, payload size, and integrity check. Compression can reduce storage, but it comes after defining the canonical state. The container should reject impossible sizes and unsupported versions before allocating or changing the running machine.

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

A save state represents one emulator core’s model of a machine, not a platform-standard game save. Some projects design versioned, backward-compatible formats; others explicitly support states only within a narrow version range. A bug fix can add a missing latch, change event scheduling, or reinterpret a field, making an older snapshot incomplete even when its bytes parse correctly.

Cross-emulator compatibility is harder because two accurate emulators may use different internal decomposition and timing phases. A portable schema would need a shared definition of every software-visible and in-flight state, including undocumented behavior. It is possible for a specific platform, but it requires deliberate coordination and conversion—not simply matching CPU register names.

Never overwrite the only important in-game save on the assumption a state will remain loadable. Keep normal save RAM or memory-card files, export them where supported, and retain the emulator/core version that created long-lived states. Save states are excellent session checkpoints and research artifacts but weak substitutes for a game’s own persistent format.

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.

“Almost free” applies to architecture, not runtime cost or correctness. Rewind needs a bounded ring buffer and may store compressed full states or deltas. Run-ahead and rollback need state operations inside a frame deadline. TAS tools require deterministic movies, input timing, rerecord counts, and often emulator-version pinning. A slow or incomplete serializer makes every dependent feature unreliable.

Delta states can reduce memory by retaining only pages or fields changed since a base snapshot. They complicate random access and recovery because losing the base invalidates descendants. Periodic full checkpoints bound the dependency chain. Compression and deduplication should be measured against CPU latency, not selected only for smallest files.

Side effects and persistent storage need rules

Loading a state can move emulated save RAM backward while the frontend also maintains a newer battery-save file. Define whether state load restores persistent memory, when it is flushed, and how conflicts are presented. Automatic saving immediately after loading an old state can destroy later in-game progress.

External side effects cannot always be rolled back. Network packets, achievements, screenshots, host file writes, haptics, and audio already played exist outside the snapshot. Deterministic modes either suppress, log, or commit such effects only when the timeline becomes authoritative. Replaying a state must not resubmit purchases, analytics, or irreversible operations.

Disc-swapping and removable media add identity questions. A state may expect a specific disc, track position, and drive status. Loading it with different content should fail clearly or request the correct verified image rather than continuing into undefined behavior.

Testing a save system

Create states at difficult boundaries: mid-instruction where supported, during DMA, at scanline transitions, while audio envelopes run, during disc access, and around interrupts. Run forward for a fixed input sequence, hash authoritative state and output, reload the checkpoint, replay, and compare at multiple future points. Repeat across supported host architectures and emulator builds.

Long-horizon tests matter because a missing fractional timer can diverge minutes later. Fuzz or corrupt headers and payload lengths to verify the loader rejects malformed files safely. A state file is untrusted input when downloaded or shared; parsing bugs can become code-execution vulnerabilities, so bounds checks and minimal privileges are preservation concerns too.

For backward compatibility, keep fixtures from released versions and test every supported migration path. If a format cannot be upgraded, document that boundary and provide the exact old build or an offline conversion tool where licensing and platform support allow it.

Why this makes save states version-sensitive but incredibly powerful

Save states are powerful because the emulator can expose machine state the original game never chose to save. That power does not require a raw struct dump, but it does require a complete model, a deterministic restore point, and an explicit compatibility policy. Keep important states paired with the exact core build, content and firmware hashes, and relevant options; preserve ordinary game saves separately; and test before upgrading the only environment that can read them.

Related:

Sources:

Comments