Articles posted January 2006 Older >>

Save State Fundamentals, part 1

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 me 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 onward, and I'll explain.

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. Many common sound chip emulators do likewise. 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.

Now let's look at how the data is saved.

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 I tend to set the data name equal to the name of the variable I'm 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). I arbitrarily picked "example1" as my module name, though any name could 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 I will mention about registering data for save states is that there is only a small window of time—starting from early in initialization until just after the driver's MACHINE_INIT callback is called—during which registrations are allowed. 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.

Spoooon!

At long last, it looks like the original animated version of The Tick is going to be released on DVD, hopefully sometime this year. Now I can finally ditch the crappy VHS transfers I've been hanging onto all these years!

A Nice Surprise

One of my personal "most wanted" games showed up a couple of days ago on my doorstep:
crgo0002.png
I realize it's not too exciting, but it has some interesting technical differences from the original. There is a small PCB attached in a fairly hacky fashion to the main board with lots of hand-soldered wires. Originally I figured this was some kind of protection scheme, but it turns out that they added a sample player. The original Crowns Golf played some samples through the AY-8910 chip, believe it or not. I don't know if MAME's 8910 emulation is quite good enough, but you can hear something sample-like.

The new board seems to offload the work to this small PCB. It writes out a starting sample address and just assumes that the hardware plays it. There are 3 TTL chips that have the part numbers scratched off on the board. One is 18 pins, one is 20 pins, and one is 8 pins. I'm suspecting the 18-pin chip might be an MSM5205. The next step is to try running the ROM data through an ADPCM decoder and see if something reasonable comes out. Some tracing of the PCB with a logic probe should also help piece together what's going on.

Suffice to say, this is kind of a nice break from looking at late 90's 3D games running at 5fps. I've also been changing several sound cores over to outputting at their natural rates, and letting the streams system do the sample rate conversion with full oversampling. This simplifies several of the sound cores, which is nice.

Resource Management in MAME

If you're writing a driver, or adding save state support to a driver, it's important to understand how resources are managed in MAME.

In the old days, resources in MAME weren't tracked at all: if you did a malloc(), you had to do a free(); if you did a timer_alloc() you had to do a timer_free(), etc. Except that you probably didn't bother in a lot of cases because the standard command-line version of MAME only runs a single game at a time, and so it didn't really hurt to leave a bunch of extra resources allocated when you quit. Of course, those other ports like MacMAME — which allow you to stop one game and start another without quitting — would find that they would eventually crash due to running out of memory or some other resource thanks to all the leftovers.

Enforcing good resource management in the core is pretty straightforward: it is a relatively small amount of code, and doesn't change very quickly. However, enforcing good resource management across the hundreds of drivers is pretty much a nightmare. Drivers are hooked into the main system by providing callbacks. Below is a list of the common ones:

DRIVER_INIT
VIDEO_START/VIDEO_STOP
MACHINE_INIT/MACHINE_STOP

Most drivers allocate resources in either MACHINE_INIT or VIDEO_START, and in theory are supposed to provide a MACHINE_STOP or VIDEO_STOP callback to free those resources. In practice, this was used inconsistently, and it's also kind of a pain. Furthermore, some systems would allocate memory in DRIVER_INIT, but there was nowhere to properly free the memory. I suppose we could have added a DRIVER_STOP callback, but ultimately a different approach was taken.

If you think about it logically, 99% of the time, you would want MACHINE_STOP to free up all the resources allocated by MACHINE_INIT. Similarly, you would want VIDEO_STOP to free up all the resources allocated by VIDEO_START. So what if, instead of reqiring each driver to write code to release the resources, the core simply kept track of which resources were allocated when and automatically released them at the appropriate time? Turns out this works pretty well.

If you look at the ordering of when the callbacks are called, it looks like this (excuse the pseudo-code):

init
{
   DRIVER_INIT
   VIDEO_START
   reset:
   {
      MACHINE_INIT
      run-the-game-until-exit-or-reset
      MACHINE_STOP
   }
   if we-exited-due-to-reset then loop back to reset:
   VIDEO_STOP
}
exit

You'll notice that I have a couple of sets of curly braces embedded in the above pseudo-code. This gives you a hint as to how the automatic resource tracking works — it's very much like local variables in C/C++. As you leave each scope, all the tracked resources that were allocated within that scope go away automatically. When you hit the closing curly brace between MACHINE_STOP and VIDEO_STOP, the core will release all resources that were allocated since the corresponding open curly brace, which includes anything that was allocated at MACHINE_INIT time, as well as any tracked resources the core allocated at that time. Similarly, when you hit the second closing curly brace, all tracked resources allocated by DRIVER_INIT and VIDEO_START will be released.

Note that I said tracked resources. Only certain resources are tracked, and some of them need to be explicit. Timers, for example, are always tracked. Anytime you allocate a timer in MACHINE_INIT, it will be released when you exit the inner scope. In fact, timers are tracked in such a way that there is no explicit timer_free() call anymore; you have to rely on resource tracking to get rid of them.

The most obvious resource is not automatically tracked, and that is memory. The reason it is not automatically tracked is that there are some legitimate reasons for doing a malloc() and having it survive the boundaries of a scope. You can manually have MAME track memory by using auto_malloc() instead of malloc(). auto_malloc() works identically to malloc() except that any memory allocated will automatically be freed when you exit your current scope. You must be a little careful here because if you do an auto_malloc() in MACHINE_INIT and store that pointer in a global variable, the memory will be freed when the game is reset and MACHINE_INIT will be called again. Your global variable will still have that same pointer value, but the memory will be gone. The right approach is to never look at the old value of a global pointer like that, and simply always auto_malloc() in your MACHINE_INIT callback.

Another common resource that needs to be manually tracked is bitmaps. There are auto_bitmap_alloc() functions that create bitmaps which are automatically reclaimed when leaving the current scope.

The last major "resource" that is tracked in this fashion is a recent addition: saved state registrations. Prior to recent changes in the system, you could only register data to be saved in the outer scope (DRIVER_INIT/VIDEO_START) and no later. This was because MACHINE_INIT may be called multiple times if you hit F3 and reset the game, and the saved state system cannot handle duplicate reigstrations (I know, it doesn't seem that hard, but there were some major complications). In order to manage this, when you exit a scope, all saved state data that you registered within the scope will be forgotten, meaning you will need to re-register it again. Since the most likely case is that you registered it in your MACHINE_INIT callback, and since after a reset you will get another call to MACHINE_INIT, this doesn't really mean you need to do anything special. Simply perform your registrations there like nothing happened and it will all work out fine.

In future articles, I'll be talking about the saved state system in more detail, so if this sounds confusing, maybe it will make more sense soon.

Finally Some Banshee Progress

Took a decent amount of hacking, but I'm finally into the attract mode in NBA Showtime. The game crashes out pretty quickly and there are other problems, but at least now I can verify that the CHD dump is good (and it is).