Implementing saving and loading of states in a C# GB Emulator


Implementing saving and loading of states in a C# GB Emulator

When I first started exploring emulators, I was hypnotized by the possibility of saving and loading states. I remember playing Super Mario World on my cousin’s Super Nintendo and leaving it turned on for days, just so we could get stuck on Chocolate Island forever. That’s why save states were such a very pleasant surprise, they worked like magic. But then recently, I wanted to work on a personal project that required this feature, so it was time to unravel the mystery.

Considerations

It’s worth noting that I’m not exactly experienced with the emulation of video games. In fact, I’ve only recently started learning about it, and haven’t yet dared to make my own emulator from scratch (and probably never will). So what I’m writing here shouldn’t be taken as absolute truth, but my hope is that it can serve as guidance.

I also won’t be covering the details of how the emulation works, as it is an article of its own. A huge one, probably. So if you are curious, I suggest this video from Wesley Cabus, who got me interested in messing with emulators.

Lastly, the segregation of responsibilities is not properly defined in this project. For example, you’ll notice that I added a bit of loading logic to the Form.cs class. Ideally, we would like to avoid logic in our layouts as much as possible.

What are save states?

Simply put, it’s a dump of the values of every relevant variable on the emulator. Whenever you save the current state, you are dumping the values of every memory address, the current CPU cycle, and any other relevant information that will help the emulator get back on track. This information will then be used when loading the state, assigning the values back to where they were. It’s pretty much like dragging a running train from one rail to another: if anything is missing, oh boy, things will get ugly.

Show me the code

As mentioned before, I have no plans of coding an emulator myself, so instead, I forked an existing one, and you can find the commit with all the relevant code here.

Saving the state

Let’s go through some relevant code, starting with the ProjectDMG.cs:

public void EXECUTE()
{           
    while (power_switch)
    {
        lock (saveLock)
        {
            // Run emulation
        }
    }
}

internal void GenerateSaveState(string fileName)
{
    lock (saveLock)
    {
        var dmgSaveState = new ProjectDMGSavedState()
        {
            cyclesThisUpdate = cyclesThisUpdate,
        };

        saveStateManager.GenerateSaveState(mmu, cpu, ppu, timer, dmgSaveState, fileName);
    }
}

The GenerateSaveState method gets a hold on the saveLock, so the emulation is paused to prevent the values from being updated while being saved. Then it generates its own saved state and calls the SaveStateManager to continue.

SaveStateManager:

internal class SaveStateManager
    {
        public SaveStateManager()
        {
            if (!Directory.Exists(GetDefaultPath()))
            {
                Directory.CreateDirectory(GetDefaultPath());
            }
        }

        internal void GenerateSaveState(MMU mmu, CPU cpu, PPU ppu,TIMER timer, ProjectDMGSavedState projectDMGSavedState, string fileName)
        {
            var filePath = GetPath(fileName);
            var mmuState = mmu.CreateSaveState();
            var cpuState = cpu.CreateSaveState();
            var ppuState = ppu.CreateSaveState();
            var gamePakState = mmu.CreateGamePakSaveState();
            var timerState = timer.CreateSaveState();
            var state = new SavedState()
            {
                CPUSavedState = cpuState,
                MMUSavedState = mmuState,
                PPUSavedState = ppuState,
                GamePakSavedState = gamePakState,
                TimerSavedState = timerState,
                ProjectDMGSavedState = projectDMGSavedState,
            };
            SaveStateSerializer.Serialize(filePath, state);
        }

        internal SavedState LoadSavedState(string fileName)
        {
            var filePath = GetPath(fileName);
            return SaveStateSerializer.Deserialize<SavedState>(filePath);
        }

        private string GetDefaultPath()
            => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SaveStates");

        private string GetPath(string fileName)
            => Path.Combine(GetDefaultPath(), fileName + ".st");
    }

The SaveStateManager is merely a coordinator. He orders every component to return their current state, combines that information, and serializes the file to disk. Let’s take a quick look at how one of these states looks.

[ProtoContract]
public class CPUSavedState
{
    [ProtoMember(1)]
    public byte A { get; set; }

    [ProtoMember(2)]
    public byte B { get; set; }

    [ProtoMember(3)]
    public byte C { get; set; }

    [ProtoMember(4)]
    public byte D { get; set; }

    [ProtoMember(5)]
    public byte E { get; set; }

    [ProtoMember(6)]
    public byte F { get; set; }

    [ProtoMember(7)]
    public byte H { get; set; }

    [ProtoMember(8)]
    public byte L { get; set; }

    [ProtoMember(9)]
    public ushort PC { get; set; }

    [ProtoMember(10)]
    public ushort SP { get; set; }

    [ProtoMember(11)]
    public bool IME { get; set; }

    [ProtoMember(12)]
    public bool IMEEnabler { get; set; }

    [ProtoMember(13)]
    public bool HALTED { get; set; }

    [ProtoMember(14)]
    public bool HALT_BUG { get; set; }

    [ProtoMember(15)]
    public int cycles { get; set; }
}

If you were to compare it with the CPU class, you would realize that the CPUSavedState class is merely holding all of the CPU data. Of course, we are not storing every variable, as some of them just point to each other. You might have noticed the ProtoContract and ProtoMember attributes, they come from the protobuf-net library, which was chosen due to being very efficient.

As mentioned before, I’m still new to the world of emulation, so it is possible that not all of the information I’ve decided to store is necessary. But hey, better safe than sorry, I guess.

All the other calls to the CreateSaveState methods are very similar in concept, so I’ll not be covering them here.

Loading the state

Now that we have a state, it’s time to load it. Let’s go to the Form.cs class.

private void LoadState(string fileName)
{
    dmg.POWER_OFF();
    var state = dmg.LoadSavedState(fileName);

    while (dmg.IsRunning)
        Thread.Sleep(100);

    dmg.POWER_ON(romPath, state);
}

Remember the issue with the overlapping of responsibilities I mentioned before? Yeah… anyway, let’s move on! The code begins by shutting down the current emulation and loading the desired save state file. Once we are sure the emulation is stopped, we restart it, while also providing the state class.

Let’s move onward to the ProjectDMG.cs class:

internal void POWER_ON(string cartName, SavedState state)
{
    mmu = new MMU(MemoryWatcherProvider.GetInstance(), state?.MMUSavedState);
    cpu = new CPU(mmu, state?.CPUSavedState);
    ppu = new PPU(window, state?.PPUSavedState);
    timer = new TIMER(state?.TimerSavedState);
    joypad = new JOYPAD();
    saveStateManager = new SaveStateManager();

    mmu.loadGamePak(cartName, state?.GamePakSavedState);

    if (state != null)
    {
        cyclesThisUpdate = state.ProjectDMGSavedState.cyclesThisUpdate;
    }

    Task t = Task.Factory.StartNew(EXECUTE, TaskCreationOptions.LongRunning);
}

Nothing too fancy here, we’re just creating the emulator components, and providing them with a state. Let’s check how the CPU.cs handles this state:

internal CPU(MMU mmu, CPUSavedState savedState)
{
    if (savedState != null)
    {
        SetValuesFromState(savedState);
    }
    else
    {
        AF = 0x01B0;
        BC = 0x0013;
        DE = 0x00D8;
        HL = 0x014d;
        SP = 0xFFFE;
        PC = 0x100;
    }

    this.mmu = mmu;
}

private void SetValuesFromState(CPUSavedState savedState)
{
    A = savedState.A;
    B = savedState.B;
    C = savedState.C;
    D = savedState.D;
    E = savedState.E;
    F = savedState.F;
    H = savedState.H;
    L = savedState.L;
    IME = savedState.IME;
    PC = savedState.PC;
    SP = savedState.SP;
    IMEEnabler = savedState.IMEEnabler;
    HALTED = savedState.HALTED;
    HALT_BUG = savedState.HALT_BUG;
    cycles = savedState.cycles;
}

As you can see, if it receives a state in its constructor, it will load the values on startup, otherwise it will boot up with the default ones.

Conclusion

And that’s it, we’ve figured out the magic! There is room for improvement here, such as finding out exactly which values we need to store, as well as organizing this a bit better. I also haven’t implemented loading and saving for other MBCs besides the MBC3, since it was the only one I was using.

I would also love to have unit tests for all of this code, as it’s pretty much critical for the software. I’d be terrified of removing/adding stuff to this once I created a couple of states.