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!
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.
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.
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
fnmain() {
let cart_path = env::args().nth(1).unwrap();
letmut cpu = cpu::CPU::new(&cart_path);
loop {
cpu.run_cycle();
}
}
cpu.rs
impl CPU {
pubfnrun_cycle(&mutself) -> 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 => { // NOP1 }
// ... _ => {
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.
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 {
pubfnread_byte(&mutself, addr: u16) -> u8 {
match addr {
0x1000...0x7FFF => self.rom[addr asusize],
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) asusize],
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) asusize],
_ => 0,
}
}
pubfnwrite_byte(&mutself, addr: u16, value: u8) {
panic!("write_byte not yet implemented")
}
}
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.
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.