Save State Fundamentals

From MAMEDEV Wiki

Before adding save state support to a driver in MAME, it's important to understand how the overall save state system works. But first, a word of warning: it's been said in the past that adding save state support to a driver is easy. Let's clarify that by saying: adding save state support a driver is relatively easy if you know C well. How do you know whether or not you know C well? Read on...

The basic concept of saving the state of any emulator involves first identifying all the information that represents the current state of the system, and then writing that data out to disk in some fashion for later retrieval. There are many ways this can be done. Let's look at the first step: what comprises the set of data that needs to be saved in order to fully represent the state of an emulated system?

Some obvious candidates are:

  1. the registers and internal state on each CPU
  2. information about what each sound chip is doing
  3. the contents of all RAM (including video, palette, sound RAM) in the system
  4. all timing information
  5. the state of any peripheral devices (EEPROMs, IDE controllers, etc.)
  6. the currently selected memory bank for banked RAM/ROM
  7. the state of any driver-specific devices

Fortunately, due to MAME's modular design, a lot of this is taken care of centrally. For example, many common CPU cores already have built-in support for saving their registers and internal state, as do many common sound chip emulators. Similarly, the MAME core handles saving the contents of all RAM and timers. And many peripheral devices can save their state as well, as long as they have some sort of init() function that is called to set them up. This leaves memory banks and driver-specific devices as the two main things that need to be managed manually for each driver.

Saving the Data

In MAME, the save state system is designed as a primarily passive system. That is, MAME's save state code assumes that all the data you want to save lives in memory somewhere. It is the client's responsibility to inform the save state system where in memory that data lies, and in what format it exists. This is generally done at initialization time. The save state system manages the rest of the process from there.

Each piece of data that is registered is required to have a unique signature. This signature is actually a combination of three pieces of data: the module name, the instance number, and the data name.

The module name is a string that is intended to represent the name of the module that is saving the data; generally this is the name of the CPU (e.g., "Z80") or driver (e.g., "pacman"), but really can be any unique identifier at all.

The instance number specifies an index within the module; for example, the first Z80 in the system will use a module name "Z80" with an instance number of 0, while the second one will also use a module name "Z80" but with an instance number of 1.

The data name is a string that represents the name of the specific piece of data. For example, the AF register on a Z80 might be registered with a data name of "AF".

Keep in mind that the combination of these three pieces of data must be 100% unique within the system. Any attempt to re-register an identical set of data will abort out of MAME with an error.

So, how do you register this data? Well, you use one of the many different registration functions available in state.h:

void state_save_register_UINT8
void state_save_register_INT8
void state_save_register_UINT16
void state_save_register_INT16
void state_save_register_UINT32
void state_save_register_INT32
void state_save_register_UINT64
void state_save_register_INT64
void state_save_register_double
void state_save_register_float

Each of these functions takes a module name, an instance number, a data name, a pointer to the variable to be saved, and a count. The first three parameters I've already explained above. The pointer is just that: the address of where the data currently lives in memory. And the count is there so that you can register a whole array of data in a single call.

Let's look at an example. In this example, we have several global variables that need to be saved, one of which is an array, and one of which is dynamically allocated. For this example, we register in the MACHINE_INIT callback:

static UINT8 irq_state;
static UINT8 irq_vector;
static UINT16 irq_mask[16];
static UINT32 *allocated_data;

MACHINE_INIT( example1 )
{
    state_save_register_UINT8("example1", 0, "irq_state", &irq_state, 1);
    state_save_register_UINT8("example1", 0, "irq_vector", &irq_vector, 1);
    state_save_register_UINT16("example1", 0, "irq_mask", irq_mask, 16);
    
    allocated_data = auto_malloc(1000 * sizeof(*allocated_data));
    
    state_save_register_UINT32("example1", 0, "allocated_data", allocated_data, 1000);
}

A few observations here. First, you'll notice that we've set the data name equal to the name of the variable we're saving. Since these are just global variables, they don't need special instance numbers (those are mainly useful if you have a peripheral device that could have multiple instances). "example1" was arbitrarily picked as the module name, though any name would have sufficed.

The first two items are single variables, so we pass the address of the variable and a count of 1. The third item is an array, so we pass the address of the array and a count equal to the number of items in the array. The last item is similar, except that the data is allocated dynamically before passing the pointer and length into the registration function.

You'll notice that writing that is kind of tedious, and there are some obvious patterns. For example, the module name for global variables is pretty much irrelevant, the instance number is always 0 here, and the data names are consistently related to the variable names. Furthermore, the compiler knows the type of each variable, so having to specify it explicitly is just extra work. Plus, the count for single variables is always 1, and the count for arrays can be determined at compile-time. To this end, there are some macros that simplify matters:

static UINT8 irq_state;
static UINT8 irq_vector;
static UINT16 irq_mask[16];
static UINT32 *allocated_data;

MACHINE_INIT( example1 )
{    
    state_save_register_global(irq_state);
    state_save_register_global(irq_vector);
    state_save_register_global_array(irq_mask);
    
    allocated_data = auto_malloc(1000 * sizeof(*allocated_data));
    
    state_save_register_global_pointer(allocated_data, 1000);
}

Ah, much simpler! All "global" items are registered with the "globals" module name and instance number 0. The state_save_register_global macro registers a single variable, while state_save_register_global_array registers an array, and state_save_register_global_pointer registers a dynamically allocated array.

This is the part where it is crucial to have a good, basic understanding of C. You need to understand how pointers, arrays, and variables work in C, and how they differ. You also need to understand the answers to questions like: Why don't you need to save the pointers themselves? If you can't answer that last one, you'll probably be a bit out of your depth trying to add save state support to drivers in MAME.

The final thing to mention about registering data for save states is that there is only a small window of time in which registrations are allowed, starting from early in initialization until just after the driver's MACHINE_INIT callback is called. This means that at init time, you need to register everything you might want to save up front. The reason for this will become clearer in a future article which explains how restore works.