<< Newer Article #151 Older >>

Save State Fundamentals, part 2

So, after reading part 1, you now know that in order to save the state of a driver, you need to register with the save state system all the bits of memory that need to be preserved. The next obvious questions is, how does it actually work?

As I mentioned before, all registration must occur in the window between early initialization and just after the driver’s MACHINE_INIT callback is called. The reason for this is simple: after that point, the game is actually up and running, and the user can request a save at any point thereafter.

Before a request to save the state can actually transpire, the state of the system must meet a few basic requirements. Usually these are met within a few milliseconds of the save state request, so it is usually not noticeable what is happening under the covers. To begin with, a save will only occur in between timeslices of CPU execution. That is, the state will only ever be saved at the start of a round-robin timeslice of the CPUs. This reduces the chances that any CPU will be in an odd state.

The second major requirement is that there can be no "anonymous" timers active in the system. What is an anonymous timer? It is any timer that is set using the timer_set call. Timers created this way are one-shot disposable timers and are intended primarily for things like timer_set(TIME_NOW)-style synchronization callbacks. Some games (including many I've written unfortunately) will cascade these anonymous timers by calling timer_set in the callback from a previous timer_set. This means that at any given instant, there is always at least one anonymous timer pending, and you will get an error after 1 second if the system can't get to a state where there are no pending anonymous timers. (The solution to this problem is to create a timer at init time using timer_alloc and then use timer_adjust on that timer to get the same behavior.)

Once these requirements are met, the actual save can occur.

To perform a save, the save state system first goes through the roster of data that was registered and computes a signature. This signature is a convolution of key data from each registration: the module name, the instance number, the data name, and the total size in bytes. The convolving function is simply a CRC, so the resulting signature is a 4-byte CRC that (mostly) uniquely describes the data that will be saved.

Once the signature is computed, it is written to the save state file, along with a simple header. After that, the save state system simply goes through every bit of data that was registered, and writes it to the save state file. Easy enough.

To restore the state of the system, it is important to realize that we can only perform a restore after all the parts of the system have registered their memory for saving. This means that we can only restore to a system that is fully up and running. If you initiate a restore from the command line, what actually happens is we set a flag to remember this request, and simply start running the game as usual. Before the first CPU timeslice, we check the flag, and if it is set, we begin the restore at that point.

To perform the actual restore, we first compute the signature of all the registered data, and compare it to the signature that was stored in the save state file. The two must match; if they don't, then something has changed in terms of what was registered between the time the game was originally saved and the current time. Usually this means that additional items were registered, or the names were changed, or something else is different. In this case, the game cannot be restored, and you will get an error popup in MAME.

If the signatures do match, then the save state system iterates through all of the registered data, finds that data in the save state file, and loads the memory. At that point, in theory, the game should just pick up where it left off.

Sadly, it's not always that easy.

Sometimes, the data as it is stored in memory is not formatted conveniently for saving as-is. In this case, what you need to do is convert the inconveniently-formatted data into something more readily saveable just before the save occurs. Fortunately, you can do this in MAME. This is what's known as a "pre-save" callback, and you can register them with the save state system at the same time you register all the data. There are three types of callbacks you can register: one that passes you back an int parameter (which you supply at registration time), one that passes you back a void * parameter, and one that passes you back nothing.

The way you would use it is like this. First, you would define up front the memory that will hold the saveable version of your data, and you would register that at init time with the save state system. At the same time, you would also register a pre-save callback. Then, when a save occurs, just before all the memory is read out, your callback is called. This gives you the opportunity to fill the previously-registered memory with converted data.

Given that, then, how do you un-do this conversion after a restore? Yep, you guessed it, there is also a "post-load" callback you can register. The same three types of callbacks are supported. These callbacks are called just after all the memory is read back from the save state file, and allow you to reconstruct your inconveniently-formatted data from what was saved.

One of the most common uses for a post-load callback is to update memory banks. At init time, you would register the global variable that specifies the current bank number, and you would also register a post-load callback. When that callback is called, you know that your global variable contains the bank number, but the memory system needs to be informed so that it is pointing to the right memory. So in your post-load callback, you call memory_set_bankptr to update your memory bank.

Of course, this is actually a bad example, because the memory system has an alternate way of handling banking now that is compatible with the save state system. But it is illustrative of the reason why you might want to have a post-load callback.

In the third and final part of this series, I will go through the steps necessary to add save state support to a driver by walking through the process on an existing MAME driver that doesn't yet have support. Stay tuned!