Skip to content

Latest commit

 

History

History
234 lines (189 loc) · 13.5 KB

README.md

File metadata and controls

234 lines (189 loc) · 13.5 KB

Gameboy Emulator

This is my messy implementation of a gameboy emulator using C++. I've been working on this on and off (whenever I had time, but sometimes I was too busy to work on it). I generally tried to refactor after finishing a subset of emulator though.

Screenshots

Pictures

Note I took these pictures when I had a small bug in rendering (Red and Blue channels were swapped).

I used this colour scheme: https://lospec.com/palette-list/kirokaze-gameboy for the background. This is also the colour scheme for my emulator for both sprites + background.

For sprites, I used the colour scheme used here: https://binji.github.io/posts/pokegb/

Gifs

I used this colour scheme for Link's Awakening: https://lospec.com/palette-list/kirokaze-gameboy.

For Pokemon, I used this: https://binji.github.io/posts/pokegb/

Installation

These instructions are targeted towards Ubuntu 18.04. I don't know the installation process for other operating systems.

First, clone the repo wherever you want.

Install CMakeLists:

sudo apt-get install cmake

Install SDL2:

sudo apt-get install libsdl2-*

Install Ninja:

sudo apt-get install ninja-build

You can install GTest if you want to run that one test I wrote but probably not worth it. I've commented out the code that part that uses it (since I ended up not really using it)

Compile and build (assuming you are at the repo directory):

If using a newer CMake version:

cmake -S . -B build -G Ninja
cmake --build build

Otherwise:

mkdir build
cd build
cmake .. -G Ninja
ninja

Note: traditional Makefiles are fine too (just remove the -G Ninja part) but the compilation is slower.

The generate file will be called gb-emulator, and will be located in the build folder.

In the future, I plan on adding dependency management via Conan. Unfortunately I ran into issues with glibc on my machine and will do this later.

Using the Emulator

To run with a ROM for the first time:

./gb-emulator <path-to-gb-file>

To run with a ROM with a sav file:

./gb-emulator <path-to-gb-file> <path-to-sav-file>

You need to make sure you use the correct ROM and associated sav file. To see how to save files, see the controllers. Note: the emulator doesn't support other emulators sav files. The format I use is just a straight dump of a bunch of different variables in binary format. This lets you start right back where you ended.

Controls

Keyboard Input Gameboy Input
Up Up
Down Down
Left Left
Right Right
Z A
X B
Space Select
Enter Start
Window Exit Quit with overwrite save file
Escape Quit without overwrite save file

Configuring the Emulator

For configurations, for now, direct modification of the source code + recompilation is required. To change the colour schemes, src/PPU/PPU.cpp has 3 unordered maps for changing colours in an anonymous namespace at the top of the file. These can be modified from the emulator directly to whatever colour. I used 3 to provide further flexibility (it's why the sprites and background have different colours for example).

For modifying the save file name, this can be done in the src/CPU/CPU.cpp in the loadROM function.

As a future TODO, I might expose these parameters in the future for modification during runtime.

Games Tested

  • Dr Mario
  • Tetris
  • Super Mario Land
    • Top sometimes flashes
  • Legend of Zelda Link's Awakening
  • Super Mario Land 2
  • Pokemon Red
    • Has some graphics issues (e.g. random tiles flash on bootup)

I tested mostly just the beginning of these games few mins of these games so note that. I don't recommend using this emulator for playing games unless you're fine with potential bugs creeping up (at worst, losing your save data).

Future TODOs

If I decide to pursue this again, I may do the following:

  • Make CPU cycle accurate
  • Switch to PPU fifo
  • Refactoring of code to better integrate everything together
  • Add audio support
  • Expose colour palette + controllers, etc to allow for more customization
  • Loading configs from file maybe
  • Support renaming of save file, using multiple save files, not overwriting it, etc
  • Better GUI (e.g. buttons to load game, etc)
  • Add safety to the code (e.g. prevent not using gb files for rom). I have some safe practices but my code overall isn't really safe

Notes on implementation

Originally, the plan was to write this in modern C++. That clearly did not pan out. The gameboy instruction set is quite large so I ended up implementing it a pretty messy way. In hindsight, with better plan, there are lots of chunks that I could have reused (e.g. some opcodes do basically the same thing but slightly differently). I did do some code reuse (which is why my raw opcode count is much less than 512), in hindsight, I should have reused a lot more things.

Suggested CPU implementations:

  • Hashmap that maps opcode -> a struct consisting of instruction. This struct would contain a field the "type of access" (e.g. use a register as an address vs read from memory vs just use a register directly), as well as a generic implementation for the instruction itself. I'm in too deep to do this though.

I also realized that I rely on the MMU for handling interrupts, timers and PPU registers. This isn't necessarily a bad thing, since they are located on specific memory addresses, but in hindsight, I think it would have been cleaner to have separated out these functionalities into where they should go (e.g. have an interrupt manager, timer manager and put the relevant PPU functions into the PPU). I may refactor later to do this.

My emulator is clock accurate on the opcode level but isn't clock accurate in terms of memory accesses or interrupts. Doing it at the whole interrupt level might be something I explore if it's necessary (I have some of it implemented but it's unused because I didn't really need it and need to add some more things (like varying the number of cycles, etc), but doing it at the memory access / actual subopcode level is a bit inconvenient since I didn't realize this was necessary. In hindsight, I should have read the entire pandoc instead of just reading as I implemented.

Another thing I believe would have been cleaner is having a 'bus' where I connect the different components. For components that need direct access to particular parts, I would make a shared ptr and connect it directly. Additionally, I think that I might use completely public classes rather than classes with private and public members in the future. I somewhat badly planned parts of this due to it.

I also copied over a lot of code in multiple files / components (e.g. getBit, setBit, etc). In the future, I think having a utils or common file that would contain code that I see being used often is a good idea just to increase code reuse.

As I will mention later, my PPU implementation is inaccurate (because it relies on end of scanline rendering). This was simpler, and my implementation ended up being a combination of the tutorial by CodeSlinger and the PPU description by the Gameboy Emulator Development Guide. In the future, I might refactor the follow what the real gameboy actually does (e.g. Pixel FIFO) to increase accuracy of my emulator but I'm happy with the results despite the low accuracy.

For ROM banking, I implemented enough of MBC3 to play Pokemon and enough of MBC1 to play games. I felt this was sufficient. In the future, I might add more features (e.g. battery, latch clock, etc), but for now it's fine without. To save, I instead just dumped all the data into a .sav file and called it a day. This soln worked well (and provides the convenience of saving even in games without saving).

Note on Tests

Originally I had planned on unit testing every function I wrote via GTest. While this approach is fine, I realized that it was simpler to just rely on tools that were specifically created for testing gameboys (e.g. test ROMs). Due to this, the testing part of this project was mostly abandoned.

Testing ROMs Acknowledgements

For CPU instructions, I used blargg's tests. This can be found at: https://gbdev.gg8.se/wiki/articles/Test_ROMs or https://github.com/retrio/gb-test-roms

I currently pass the CPU instruction test and the instruction timing test.

I also mostly pass dmg-acid2 (see: https://github.com/mattcurrie/dmg-acid2). However, there are a few graphical errors (namely the smile extends slightly too much, ther are 2 white bars that are approx 1 pixel in height and go from the left side of the face's edge to the eye and from the right side of the mouth to the face's edge). I also have some dots under HELLO WORLD!. I plan on debugging this later on but it doesn't seem to affect playing games much. Also, note if you use multiple colours, the output resembles CGB running in DMG mode rather than the DMG mode (this behavior is correct, but you will see a similar graphical glitch to the below screenshot).

Debugging Acknowledgements

To get started on debugging my opcodes, I used BGB (https://bgb.bircd.org/) as a dissassembler on blargg's tests. This is a high accuracy emulator (see: https://gbdev.gg8.se/wiki/articles/Test_ROMs). I mainly used this to compare the output of the disassembler with my program (by printing out the status of my registers). This helped me find minor bugs that really messed up everyhing such as accidental infinite loops due to mixing up the order of registers in some instructions. Once I got past the requirements for blargg's tests initialization, I shifted to using it exclusively because it was a more scalable testing method.

References