Rustyboy - Part 1: Welcome to Emulation Station

By Dave Allie
Published Jul 2, 20184 min read
I've seen a fair spread of emulators over the last 10 or so years and played around with a fair few of them as well (if you weren't playing Pokemon on the VisualBoyAdvance while pretending to do school work then, I'm sorry to say, your childhood was lacking). It wasn't until I stumbled across an old reddit post that I seriously considered building an emulator myself.
In the reddit post redditor yupferris was live streaming building an N64 emulator in Rust. Ferris walked through some of the tools he used, how he started the project, and most importantly, he showed off how much documentation existed for these older systems. There were docs on how sound worked, how the CPU ticked, how graphics were rendered and just about anything you could want to know about the internal working of the console. That was it I was hooked, time to build an emulator!
How hard can it be?

Picking a console and a language

The first step in building an emulator is deciding what to emulate. Some people suggest writing your first emulator for the CHIP-8 system, but on closer inspection, it looked almost a little too easy. Emulating the original Game Boy seemed complex enough to keep me scratching my head, but not so hard that I'd lose interest when I hit a roadblock. Plus, Game Boy had Pokemon. Who can say no to Pokemon?
Emulating a Game Boy definitely isn't a new thing, just a quick Google will return hundreds of results of Game Boy emulators written in every language imaginable, but that didn't bother me, it showed me that it was possible and there was absolutely no excuse as to why this emulator couldn't be finished.
Take a gander at those bezels. From a time when screen-to-body ratios weren't as important.
The language choice was easy. I'd been messing around in Rust on a few very small projects, but this seemed like the perfect opportunity to stretch my legs in Rust and start leveraging some of the more advanced features in a slightly larger codebase. And thus, my lil' Rustyboy was born.

Getting started

CPU

Unsurprisingly, Tetris for the Game Boy is a very simple ROM that doesn't utilise many of the advanced features of the Game Boy (aside from randomisation). Running Tetris gave me a tangible, and measurable goal. I started out by coding a simple CPU loop that ran completely unthrottled and crashed whenever it came across an instruction it didn't recognise. And oh boy, it crashed a lot.
main.rs
fn main() {
    let cart_path = env::args().nth(1).unwrap();
    let mut cpu = cpu::CPU::new(&cart_path);

    loop {
        cpu.run_cycle();
    }
}
cpu.rs
impl CPU {
    pub fn run_cycle(&mut self) -> u8 {
        let read_regs = self.reg;
        let code = self.get_byte();

        println!("instr: 0x{:X} -- opcode: 0x{:X}", read_regs.pc, code);

        match code {
            0x00 => { // NOP
                1
            }
            // ...
            _ => {
                panic!("unknown op code 0x{:X}", code);
            }
        }
    }
}
Every time it crashed, I dug through the Game Boy CPU Manual (hosted by Marc Rawer), found the relevant opcode, and wrote out the CPU instruction. There were some easy ones like NOP (literally, "no operation") which were one-liners, but then there were also ridiculous ones, like DAA, which was implemented 100 different way depending on where you looked. It was slow but steady, each time I added an operation the emulator made it a few steps further.
One of the bigger mistakes I made here was attempting to emulate a Z80 CPU instead of the Game Boy CPU. I found a few articles saying that they were quite similar, and others that the Game Boy used the off-the-shelf Z80. So when I came across http://clrhome.org/table/ - which clearly laid out all the different operations and their opcodes, I threw Marc's CPU manual out the window. Instead of scrolling through pages and pages of a PDF, it was all there in front of me. By the time I realised that the Game Boy did not, in fact, use a Z80, the damage was done. Half of the instructions I had written had correct timings and were referenced by the correct opcode, but the other half was a mess of incorrectly timed and referenced instructions that occasionally moved the program counter to wrong parts of the ROM. It took me a lot longer than I'd like to admit to find and fix those instructions.

MMU

Some instructions required reading from, or writing to, particular addresses in memory. Addressing the memory is done using a 16-bit value, meaning there only 65,536 addressable location in memory, and only half of that is for accessing data on the ROM. This creates a theoretical upper limit for ROM sizes of 32kB. However, looking at something like the Pokemon Red ROM, it's exactly 1MB, so how the heck is all that data accessible? This Game Boy memory map might give it away, but I'll be exploring it more in a future post.
In the beginning, like any good programmer, I chose to ignore the harder parts, and let future me deal with it. Instead of worry about 1MB ROMs, or writing out the interfaces for all the physical parts of the Game Boy, I wrote a very simple MMU that stubs almost every (non-ROM) read and hopes for the best. There was no way that Rustyboy could ever boot Pokemon in this state, but for a ROM as simple as Tetris, it didn't matter.
mmu.rs
impl MMU {
    pub fn read_byte(&mut self, addr: u16) -> u8 {
        match addr {
            0x1000...0x7FFF => self.rom[addr as usize],
            0x8000...0x9FFF => panic!("MMU ERROR: Load from GPU not implemented"),
            0xA000...0xBFFF => panic!("MMU ERROR: Load from cart RAM not implemented"),
            0xC000...0xFDFF => self.wram[(addr & 0x1FFF) as usize],
            0xFE00...0xFE9F => panic!("MMU ERROR: Load graphics sprite information not implemented"),
            0xFF00...0xFF7F => panic!("MMU ERROR: Memory mapped I/O not implemented"),
            0xFF80...0xFFFF => self.rom[(addr & 0x7F) as usize],
            _ => 0,
        }
    }

    pub fn write_byte(&mut self, addr: u16, value: u8) {
        panic!("write_byte not yet implemented")
    }
}

Kind of working v1

Commenting out all the panics, and attempting to run Rustyboy with the Tetris ROM, produced something like the following:
❯ cargo run -- roms/Tetris.gb
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/rustyboy roms/Tetris.gb`
ROM loaded from roms/Tetris.gb
instr: 0x100 -- opcode: 0x0
instr: 0x101 -- opcode: 0xC3
instr: 0x150 -- opcode: 0xC3
instr: 0x28B -- opcode: 0xAF
instr: 0x28C -- opcode: 0x21
instr: 0x28F -- opcode: 0xE
instr: 0x291 -- opcode: 0x6
instr: 0x293 -- opcode: 0x32
instr: 0x294 -- opcode: 0x5
instr: 0x295 -- opcode: 0x20
...
Each line contains the program counter (current address to lookup), and the operation found at that address to be executed.
There was no screen, no throttling and absolutely no pausing. Rustyboy would scream along at 100% CPU usage and try to process as many instructions as it could until it crashed or ended up in a loop waiting for graphics to render. It wasn't much, but I was over the moon.

Next Up

This post is part of a series, in the next post I will talk about finding and fixing those broken instructions using a debugger that I wrote into Rustyboy. You can find the full list of published parts here.
You can also find the full repo at https://github.com/daveallie/rustyboy.