How to save/restore the state of an emulated game

Several MAME developers and others have recently started writing code to save/restore the state of emulated games.  In this article I shall attempt to describe how this process works in HiVE and how the same concepts can be applied to other emulators (including MAME).

General Overview
While an emulator is running a game, it must orchestrate various processes occuring with various objects (both dynamic and static) which represent (emulate) the hardware of the original game.  Typically these objects will include the memory controlled by the microprocessor(s), the microprocessor register values, state machines representing various hardware such as sound or video and any user-settable options specific to the emulator.   The current 'state' of a game is contained in these objects and has the potential to be saved and loaded if these objects can be saved and restored in a reasonable fashion.   In the case of HiVE, these objects consist of the memory maps of the microprocessors (64K each for the 8-bit micros supported by HiVE), structures containing the register values, and a few assorted variables with state-machine info to emulate various bits of hardware.  Since each emulated game (which doesn't run on hardware common to other games) uses it's own set of variables and memory maps, then it logically follows that each different game will require a different procedure to save/restore its unique variables.  This is the stumbling block preventing MAME from supporting save/restore in all games - since each game driver is different (and often written by different authors), it will require custom code to save/restore its state.  In an emulator with 1400+ supported games, this becomes a daunting task for a single person to do and a difficult task to coordinate for a multitude of authors.

The Specifics
Shown below is my function to save/load the game state for Mappy and the other games which run on that platform (e.g. Super Pac-Man).  I have created 2 functions (not shown - these functions are left as an exercise for the reader :) ) which handle the dirtywork of loading and saving the game state.  The parameters passed are the game number to save (1-5), the CPU memory map (including memory usage flags) and 2 optional buffers containing anything else which needs to be saved.  For this particular hardware platform, the memory of CPU #2 does not need to be saved since all of its RAM is represented in CPU #1's memory map as shared areas.  The only info of CPU #2 which needs to be saved is the current CPU state (register values).  The load/save function looks at the flag map of the memory map and only saves sections containing RAM and custom functions since the ROM areas do not change.  It then compresses the RAM and areas containing custom functions by combining repeating sequences of zeros.  This form of compression may seem very simplistic, but it is able to compress the 3 64K memory maps of Galaga & its registers to a file size of about 5K.

As shown below, Mappy has just a few variables which need to be saved.  The sound chip info is also saved, but probably does not need to be.  When a game is restored, the screen is forced to repaint itself and the hiscore flag is set indicating that the emulator should not attempt to reload the hiscore values since they have already been loaded in the process of restoring the machine state.  I have left extra space in the "temp" buffer for future expansion.  This way, if there are other games which have similar hardware, but require other variables to be added, I can modify this function without invalidating the existing saved-game files.

Portability Issues
As mentioned by Brad Oliver, making the code portable across multiple platforms complicates things a bit.  Besides endian issues, there is also the problem of variations in compiler behavior with respect to structure packing/padding.  A convention of using either big or little endian and specific code to extract variables from structures becomes necessary to ensure compatibility across various platforms.

The save/load functions can be called at any moment during gameplay because they save/restore the entire machine state; it is not necessary to wait for the self-test to finish.  In fact, the save/load state can be used as a convenient way to avoid the load self-test of certain games.  The information I've provided in this article should be sufficient for any emulator developer to add this feature to his emulator.   If there is anything which needs clarification, write me a note and I will add it.

/****************************************************************************
*                                                                           *
* FUNCTION : MappyLoadSave(int, BOOL)                                       *
*                                                                           *
* PURPOSE : Load or save the current game state.                            *
*                                                                           *
****************************************************************************/

void MappyLoadSave(int iGame, BOOL bLoad)
{
char cTemp[256];

   if (bLoad)
      { /* Load a game state */
      EMULoadState(iGame, &mem_map1[MEM_ROMRAM], (char *)&regs1, sizeof(regs1), (char *)&cTemp, 256);
      cIRQEnable1 = cTemp[0]; /* Restore all game variables */
      cIRQEnable2 = cTemp[1];
      cCPU2Enable = cTemp[2];
      cMappyScroll = cTemp[3];
      memcpy(cVolume, &cTemp[4], 8);
      memcpy(iFreq, &cTemp[12], 32);
      memcpy(pWave, &cTemp[44], 32);
      memcpy(iPosition, &cTemp[76], 32);
      memcpy(&regs2, &cTemp[108], sizeof(regs2)); /* Restore CPU #2 regs */
      memset(cDirtyChar, 1, 2047); /* Force a repaint of the character buffer */
      lDirtyRect = -1; /* Force a total repaint of the display */
      bHiLoaded = TRUE; /* Don't try to reload hiscore table */
      }
   else /* Save a game state */
      {
      cTemp[0] = cIRQEnable1; /* Save all game variables */
      cTemp[1] = cIRQEnable2;
      cTemp[2] = cCPU2Enable;
      cTemp[3] = cMappyScroll;
      memcpy(&cTemp[4], cVolume, 8); /* Namco sound chip variables */
      memcpy(&cTemp[12], iFreq, 32);
      memcpy(&cTemp[44], pWave, 32);
      memcpy(&cTemp[76], iPosition, 32);
      memcpy(&cTemp[108], &regs2, sizeof(regs2)); /* CPU #2 registers */
      EMUSaveState(iGame, &mem_map1[MEM_ROMRAM], (char *)&regs1, sizeof(regs1), (char *)&cTemp, 256);
      }

} /* MappyLoadSave() */

Back