A simple Game Boy emulator in Rust, built as a practice project.
It was created as an attempt at building a working emulator based almost entirely on documentation on the console.
While writing the code, I tried to avoid reading code examples and sources of other emulators, as a soft-limitation on myself. I did resort to reading examples, however, on a few non-GB specific things, like some CPU instruction behaviors, especially with respect to the CPU flags.
Emulators with built-in debuggers were also used as reference.
The project makes use of very few external dependencies. The only used crates are the Rust SDL2 bindings
and the bitflags crate for convenience.
Late in the project, the crates Tao and Rust for Windows were added. See notes.
This emulator is far from fully featured and ignores, by design, some features and systems present in the Game Boy console and other emulator.
- The emulator is not fully cycle-accurate.
- However, CPU instructions are cycle-accurate (ignoring memory timings).
- PPU rendering should be cycle-accurate, as far as I can tell.
- Infrared and serial communication were ignored.
- Only MBC1 (Memory Bank Controller) is implemented at the moment.
- OAM corruption bug is ignored at the moment.
- Memory access blocks are ignored (such as during pixel drawing).
Most resources used were picked up in the excellent Awesome Game Boy Development list on GitHub.
Before starting the project, I read the "Writing NES Emulator in Rust" e-book by @bugzmanov, to get a general idea of how an emulator is structured.
- Pan Docs - Used as the main reference for the entire console.
- Pan Docs Rendering Internals - No longer included in the full docs, but clarifies a few things about rendering.
- The Ultimate Game Boy Talk by Michael Steil - Used mainly as another reference to understanding the Game Boy rendering.
- Coffee GB - Once most systems were working, used as an example for a rewrite of the rendering system (which turns out unnecessary, see notes).
- gb-opcodes - Table of CPU instructions, arranged by their hex representation.
- RGBDS opcodes reference - A more detailed reference of the opcodes.
- BGB - An emulator with a visual debugger.
- Emulicious - Another emulator with a very powerful built-in debugger.
- Blargg's test roms - Used to test the CPU implementation.
- dmg-acid2 - Used to test the image rendering.
-
Debugging the test roms proved a bit challenging when trying to find how and when my implementation was digressing from the expected behavior by simply using a debugger.
Using Emulicious' Trace Logger helped tremendously, by saving a log of Emulicious passing the tests correctly and comparing it to a matching log generated by my code using a simple text diffing tool.
The diff helped pinpoint the exact instructions that contained bugs in their implementations.
-
Spent a few days and multiple approaches trying to write the rendering code. The Pan Docs documentation was confusing, and the process difficult to debug.
After multiple nights of scratching my head, searching the internet, fiddling with off-by-one errors and reading the docs over and over, the emulator finally rendered the correct reference image of the dmg-acid2 test.
Ironically, any game ROMs tested still failed to show an image. As of writing these lines, I believe this is due to the audio system being unimplemented.
-
After debugging the emulator's run on a ROM of Tetris and comparing it to against traces from Emulicious, it turned out that the implementation of the joystick inputs was not properly implemented.
After fixing those issues, the emulator ran its first game roms with at least some success.
-
Eventually rewrote the rendering subsystem to something that's closer to how the device works, using the Rendering Internals page (linked above) of the Pan Docs.
Since some things were still unclear, I went to watch the rendering part in The Ultimate Game Boy Talk (linked above).
While it clarified even more bits about the topic, it seems more changes are required.
-
Programming the audio system was actually fairly straightforward, with a few interesting challenges, like trying to get the timing of the channels right, and figuring out how to translate from the variable sample rate of the Game Boy system to a constant sample rate of 48000Hz.
The audio still sounds harsh compared to that produced by Emulicious. My guess is that they either use something other than "pure" square waves, or have further processing, perhaps the high-pass filter mentioned in the docs has that affect.
-
At the time of writing this point, it seems to me that the last remaining major issue has something to do with the timings of the rendering system, which is still not fully clear from all the documentation I've read so far.
Considering finally taking a shortcut at this point and look into other source codes of emulators for this, but yet to be determined.
-
Frustrated with the repeating rendering issues, I looked into the rendering source code of Coffee GB, a Game Boy emulator written in Java which tries to emulate the rendering hardware, same as this emulator (as opposed to skipping the hardware emulation and rendering tiles and sprites directly to a buffer).
Funny enough, I ran into the same issues I had in my own code and ended up with the exact same result, only now with cleaner code.
At this point, I debugged a ROM of Super Mario Land and compared it to similar results from Emulicious.
This lead me to find bugs in the OAM DMA transfer functionality and a bug in the Interrupt Service Routine that allowed additional interrupts to run while one is already being handled.
With those issues fixed, there still remains a small visual glitch during rendering, which is to be expected in actual devices, but made slightly worse in the emulator due to another small bug which I will fix later. -
After deciding I want to have some more basic functionality for the emulator, I added Tao and the Rust for Windows crate for window management and using some bits from the Windows API, like opening a file selection dialog.
During this effort I rewrote bits of the main executable file (
main.rs
), to use the new crates and improve the game loop which runs the emulator, to keep it running at a pace closer to hardware timings.After doing so, the only remaining major issue that bothered me, was sound synchronization. Since the emulator's run was dictated by its window's event loop, this caused its audio to suffer from clock drift, which is a well known problem.
-
Finally, I decided to pick the method used by many emulators, which is to sync the emulator to the computer's audio system.
Doing this was both surprisingly easy and surprisingly effective.
Using a callback received from SDL's audio system to clock the emulator until it could fill a sample buffer of an arbitrary size (chosen by trial-and-error), resulted in perfectly synced audio with few-to-no stutters, and kept the emulator running at correct pace regardless of the buffer's size, again to my surprise.
At this point, there is still an issue of possible image tearing when drawing, but I consider it small enough to not warrant fixing right now.
I consider all of these optional and may or may not get around to doing them.
- Some code organization and cleanup.
- Refactors and rewrites for systems like the CPU, for cleaner code.
- Refactors for small bits, like giving some registers their own struct implementations.
- Improve audio system to sound a bit less harsh.
- Implement Game Boy Color?
This part would take quite a bit of work, so I don't know if and when I'll decide to do it.