MAME Driver Save States: Register Runtime Data and Rebuild Derived State
Implement MAME driver save states with fixed and dynamic registration, safe postload reconstruction, support boundaries, and repeatable restoration tests.
MAME save-state support is a property of the configured machine, not just a driver saving a handful of visible registers. A driver can register its own state correctly and still depend on a device that does not support restoration. This guide is for MAME 0.289 driver and device authors: it focuses on what to register, when to register it, and how to rebuild derived state after the serialized values return. It is not a promise that every MAME system can produce valid save states.
Check the machine-level support boundary first
Before debugging a driver, confirm whether MAME considers the selected machine supported for save states. The user documentation warns that many drivers do not support the feature and that MAME may warn when it cannot establish that a state is valid. In the 0.289 source, the save manager closes registration after startup, checks for duplicate entries, then marks support false if any device type in the configured machine carries the SAVE_UNSUPPORTED flag. Registering more fields in the parent driver does not override that device-level boundary.
Treat the warning as useful evidence, not something to silence by forcing a test. If one child device cannot restore its state, a successful file write does not prove the whole machine will resume correctly. Record the machine name, MAME build, and warning alongside a reproducer. The scope here is to make a supported device’s own state complete and repeatable.
Identify the values that affect the next emulated event
Start from a restore question: after loading at this exact point, what values must be present for the next CPU read, interrupt, DMA step, scanline, or device response to match? Typical driver-owned values include a bank selection, a latched command, a pending interrupt bit, a transfer index, or writable device RAM. Register a timer phase only when it is driver-owned state not already represented by a registered MAME emu_timer; the scheduler already saves ordinary emulated timers. Include each authoritative value if changing it can change future emulated behavior, even if it is not currently visible on screen.
Separate authoritative state from values that can be reconstructed. A cache of decoded graphics or a pointer into a memory allocation is usually not the machine’s state; it is a host-side convenience. Conversely, a latch that chooses the next bank is authoritative even if its current effect is not immediately visible. Do not serialize host addresses, callbacks, mutexes, or references as if they were console hardware.
Do not register NVRAM persistence through save-state callbacks; MAME handles it through a separate subsystem. See MAME NVRAM Preservation: Separating Persistent Cabinet State from Save States for that device and file lifecycle.
Register fixed state during startup
MAME’s device_t documentation names device_start() as the correct place for save-state registration. For driver-owned state, use the driver’s startup hook: in the pinned 0.289 base NES path, nes_state::machine_start() allocates CIRAM and then registers m_last_frame_flip and the counted CIRAM buffer before returning. Some specialized MACHINE_START_MEMBER variants have their own startup registration code, so follow the actual configured machine’s hook rather than assuming every NES variant uses one identical path. The configured machine’s start() completes before registration is closed; after that, the save manager rejects attempts to add fields. Register the same fields once at startup; do not register them in device_reset() or each time a guest register is written.
For fixed scalar members and arrays, use save_item(NAME(member)). Give every registered field a stable, unique name within its device context. Duplicate registrations are detected when MAME closes the registry. For example, a device with a latched mode and interrupt request can use the same pattern as other upstream devices:
void example_device::device_start()
{
save_item(NAME(m_mode));
save_item(NAME(m_irq_pending));
save_item(NAME(m_transfer_position));
}
This is a schematic device fragment: the members must already exist, be initialized through the device’s normal lifecycle, and represent values that affect subsequent emulation. If a fixed array is part of the device’s state, register the array rather than only its first element. Do not register read-only ROM bytes merely because a device can access them; the image and machine configuration provide those bytes at startup.
Give dynamic buffers an explicit size
For fixed-size values and arrays, save_item registers the extent described by the object. MAME 0.289 also provides a save_item(std::vector<T>&) overload for approved element types; it registers the vector’s current size and obtains the data address from its first element. The cited overload has no explicit empty-vector check, so non-empty storage is an implementation precondition for this usage, not a checked error; keep the vector’s size and storage stable afterward. The implementation explicitly rejects using save_item on a raw pointer without a count. For dynamically allocated arrays represented by raw pointers or supported std::unique_ptr<T[]> storage, save_pointer takes an element count; provide non-empty backing storage for registrations that use the implementation’s first-element address. It registers the pointed-to data range; it does not make a process address into a portable saved value.
An upstream Apple DAFB device demonstrates the pattern: it allocates VRAM in device_start(), registers its scalar and fixed-array state with save_item, then registers the allocated m_vram payload with save_pointer(NAME(m_vram), m_vram_size). The count must describe the live allocation. Keep that storage alive and at a stable size for the registered lifetime; if a design can resize or replace a buffer, define a fixed maximum or a deliberate state representation instead of leaving registration pointed at stale storage.
MAME’s memory subsystem automatically saves memory shares. If RAM is represented by a memory share, use the framework’s memory object rather than separately registering the same bytes. The memory guide describes regions as storage for read-only data such as ROMs or fixed-decryption results; their contents are not saved, and the emulated system should not write to them. Do not register those image bytes as mutable runtime RAM or rely on post-load restoration to preserve changes to them. Knowing which subsystem owns a byte range avoids duplicate state and prevents image data from being mistaken for mutable RAM.
Rebuild derived state after the data is restored
The save manager restores registered entries before it dispatches post-load callbacks. Use a callback or a device’s device_post_load() implementation to repair state that depends on restored values: invalidate a cached decode, reselect a derived view, rebuild an internal pointer, or recompute screen parameters. The fixup should be repeatable and should not emulate a new guest bus write or generate an extra interrupt. Loading a state is restoration, not a new hardware event.
MAME’s DAFB implementation shows the mechanics: it allocates VRAM, registers display fields and VRAM, then registers recalc_mode() as a post-load callback. That function recomputes resolution and totals from restored timing values and reconfigures the screen. Scope this example carefully: DAFB also registers m_stride, and recalc_mode() divides it by clockdiv when convolution is enabled. It is therefore not a derived-state-only template; a new callback should avoid mutating serialized inputs unless that transformation is explicitly part of the device’s restoration contract. The general lesson is to rebuild host-side configuration after restoration, not to copy this specific function without checking its effects.
Do not assume a pointer remains valid just because its numeric address happens to be unchanged during one test. If the pointer is derived from restored bank, view, or allocation state, regenerate it from the owning MAME object after load. If an interface mix-in owns its own derived data, use its documented post-load hook instead of duplicating its repair in a parent driver.
Test the state as a repeatable event boundary
MAME’s debugger provides statesave name and stateload name; the documented quick-save keys are an alternative for interactive testing. Use a machine where the warning does not say save states are unsupported. Choose a point just before a state transition that exercises the fields you registered, such as a timer edge, bank switch, or mid-transfer buffer update.
- Start from a known machine configuration and content set. Record the MAME version, machine short name, command line, and any configuration that affects the run.
- Reach the chosen event boundary and issue
statesave before-event. - Run a fixed input sequence through the event. Record relevant device registers, expected interrupt or transfer order, and visible output.
- Issue
stateload before-event, replay the same input sequence, and compare the same observations. - Repeat the load several times and include a second checkpoint on the other side of the transition. Check reset separately; a cold or soft reset is not equivalent to loading a saved state.
If the second run diverges, first ask whether the missing value is authoritative state or derived state that was not rebuilt. Then check whether a core-owned component has declared save support, whether a buffer’s registered count still matches its allocation, and whether the callback ran after its inputs were restored. Avoid “fixing” the result by serializing every cache: that can preserve stale data rather than restore the device’s actual behavior.
The save manager’s signature is a CRC over each sorted entry name, m_typesize, m_typecount, and m_blockcount (plus a zero constant). It does not hash m_stride, the pointer, or the saved data itself. Changing the covered registry fields can make older files fail validation; this signature is not an automatic migration system. Since load/save traversal still uses m_stride, a stride or related layout change can leave the signature unchanged while changing how bytes are interpreted. Signature equality therefore does not prove layout compatibility: treat field renames, size, stride, layout, and new registrations as compatibility changes, retain tests for the versions you intend to support, and do not promise that a state from one MAME build will load in another unless you have verified that path.
The practical acceptance criterion is deterministic continuation for the supported machine configuration, not merely that MAME created a .sta file. Preserve ordinary NVRAM independently, keep the state test reproducible, and report unsupported devices or untested version boundaries honestly.
Related:
- MAME Timers and Scanlines: Synchronizing Arcade Events in Emulated Time
- MAME NVRAM Preservation: Separating Persistent Cabinet State from Save States
Sources:
- MAME Documentation 0.289: Default keyboard controls and save-state warning
- MAME Documentation 0.289: General debugger commands (
statesave,stateload) - MAME Documentation 0.289: Memory shares and regions
- MAME Documentation 0.289: NVRAM and save-state directories and lifecycles
- MAME 0.289 source: device lifecycle and save-state API, pinned release commit
- MAME 0.289 source: save-item and save-pointer registration, pinned release commit
- MAME 0.289 source: emulation timer state registration, pinned release commit
- MAME 0.289 source: registration closure, state signature, and post-load dispatch, pinned release commit
- MAME 0.289 source: memory-share save registration and allocation, pinned release commit
- MAME 0.289 source: memory-share allocation routing, pinned release commit
- MAME 0.289 source: machine startup completes before save-state registration closes, pinned release commit
- MAME 0.289 source: Apple DAFB VRAM registration and post-load mode rebuild, pinned release commit
- MAME 0.289 source: base NES startup allocation and save-state registration, pinned release commit
- MAME 0.289 source: specialized FDS startup registration, pinned release commit