GBakeBoy Emulator

Posted on Fri 11 May 2018 in yeah

Part 1. Build an emulator

Inspired by various videos of a computer playing game I wanted to see if I could create one for myself.
Keeping things simple the goal would be:

Goal: Create a system that learns to play super mario on the gameboy

gameboy

This would need a couple of things

  • A gameboy emulator (with an api).
  • A vague understanding of asm.
  • An understanding of deep reinforcement learning (RL).
  • Something to bridge the emulator and RL.

Step 1. Build a gameboy emulator

cpu

How hard can it be? super hard!

GBakeBoy code on github

There are plenty of resources on the internet and a lot of people have succesfully built one. I started over xmas. It would be in python because thats what I know and what the RL would be using.

Thankfully the gameboy is a fairly simple machine. It just fetches and executes single instructions on a single core. All of the hardware is memory mapped which also keeps it simple.

The initial goal was to get it to boot the bios. Once games start to load you have timings and interupts and screen syncs to think about. The bios sets up some hardware and initialises parts of the memory (also an impressive twist at the end to hide itself).

boot

I got a basic memory map setup, bios file loaded and watched as it fetched and executed its first instruction.
Yey! it's alive!

This is the main loop which takes the next instruction and executes it. Eat your heart out Von Neumann!

def execute(self):
    """
    Execute the next instruction
    """
    instr = self.get_next_instruction()
    logging.debug(hex(instr))
    command = self.instructions[instr]
    args = self.get_additional_instructions(command)
    logging.debug('Command: {} {} {}'.format(hex(instr), command['fn'].__name__, hex_array(args)))

    extra = self.execute_command(command, args)
    if extra is not "KEEP_PC":
        self.increment_pc(command['PC'])
    return command['cycles']

Opcodes Instructions are defined as properties of the cpu. We store it as a dictionary that maps the hex number to the instruction function. Each instruction has the function to call, how many clock cycles are executed and how many bytes to move the PC forward by. Some instructions contain extra parameters for the instruction, this is where the immediate_8 and immediate_16 parts come in (and hence the need to move the PC further for the next instruction)

self.instructions = {
    0x05: {  # 5
        'fn': self.DEC_B,
        'cycles': 4,
        'PC': 1
    },
    0x06: {  # 6
        'fn': self.LD_B_d8,
        'immediate_8': True,
        'cycles': 8,
        'PC': 2
    },

Once we have the parameters we then do the execution phase. This instruction loads an 8 bit byte into register B.

def LD_B_d8(self, args):
    """
    0x06
    LD B, d8
    """
    nn = args[0]
    self.set_B(nn)
    self.set_flags(False)

Then it was just a case of abstracting out the memory devices and adding more instructions to the interpreter. It stopped whenever it found a new instruction and I'd add a function in the code for it.

The stack handling was working ok and then I got stuck. The wrong thing was on the stack and I couldn't figure out why. I tried various things but couldn't get it working.

Sometime later I read that it could take up to a year to build a working emulator. It was at this point I put a pause on gbakeboy development and found an existing python based gameboy emulator. Though I hope to pick this up again at some point as I found it fairly interesting.

Links I found helpful:

  • https://github.com/Baekalfen/PyBoy/raw/master/PyBoy.pdf
  • https://github.com/Dooskington/GameLad/wiki
  • http://www.pastraiser.com/cpu/gameboy/gameboy_opcodes.html
  • emudev.slack.com

More TODO:

  • Main TODO - finish the bios boot.
  • Clean up the opcodes, there is a lot of reduncancy in there.
  • Add/fix the tests.

mario

Links