Game Boy emulator
Back in 2018 I stumbled upon a blog post by Imran Nazar describing how to create a Game Boy emulator that runs in the browser. While reading about Imran's implementation, I was coming up with alternate implementation strategies. I was also looking for a fun project to teach myself TypeScript. I was so eager that I began writing code within the same week.
In the rest of this article I'll describe what I intend to build (it's not yet finished), which portion I have built, then some reflections.
What I intend to build
There are loads of resources online that describe what an emulator is and how the Game Boy works. Instead of rehashing that info, I'll summarize the intended user experience (UX) and high level design.
UX
I started this project at a time in my life when I was thoroughly exploring the capabilities of the browser and much of the tooling surrounding SPA development, so naturally I wanted provide the full emulation experience in the browser: loading a ROM image (ROM), video, audio, controller, save states, fast forward, rewind, networking, and more.
Design
After reading a few documents about how the Game Boy works and several more about how to build an emulator, I felt confident enough to start breaking down the problem of "emulation" into more manageable chunks.
SPA
With my UX goals in mind, it was clear that to start I'd need to get a SPA running in the browser. Here's an illustration of the generic process:
graph TB %% Entities User subgraph Client Browser end subgraph Server WebServer[Web Server] Distributables end Developer subgraph Development Machine SourceCode[Source Code] end %% Relationships User -- opens website in --> Browser Browser -- requests website from --> WebServer WebServer -- loads website from --> Distributables Developer -- develops --> SourceCode SourceCode -- compiles to --> Distributables
As with many SPAs, the interesting stuff happens inside the browser, so let's go one layer deeper.
Browser / JavaScript
My SPA would request a ROM from the user, then load an emulator and an acompanying user interface (UI). The UI would act as an input/output (I/O) bridge between the emulator and browser.
graph TB %% Entities User subgraph Disk ROM end subgraph Browser WebPage[Web Page] CartData[Cartridge Data] Emulator UI[User Interface] end %% Relationships User -- navigates to --> WebPage User -- selects --> ROM ROM -- reads into --> CartData WebPage -- creates --> Emulator WebPage -- creates --> UI CartData -- passes into --> Emulator UI -- ticks --> Emulator UI -- renders data from --> Emulator User -- views & interacts with --> UI
Emulator
With this design I could encapsulate all Game Boy hardware emulation inside the emulator class. The primary components of the emulator include the central processing unit (CPU), the memory map, the input handler, and the picture processing unit (PPU). Here's a diagram illustrating the relationships between all of these components:
graph TB %% Entities CartData[Cartridge Data] UI[User Interface] subgraph Emulator InputHandler[Input Handler] MemoryMap[Memory Map] CPU PPU end %% Relationships CPU -- reads from & writes to --> MemoryMap MemoryMap -- maps from --> InputHandler MemoryMap -- maps from --> CartData InputHandler -- reads controller state from --> UI PPU -- reads from --> MemoryMap PPU -- renders to --> UI
At the time of writing my understanding of the Game Boy internals is still quite limited, so I imagine more components will fit into this diagram in the future; for example an audio controller, an interrupt controller, and a sprite mapper.
CPU
The CPU's primary job would be to execute instructions from the instruction set architecture (ISA), which often involves either reading from/writing to memory or manipulating CPU registers.
Memory Map
The memory map's primary job would be to take requests to read or write data at a variety of addresses and map those operations to either raw byte/word storage via typed arrays or a virtual address range represented by another emulator component. For example:
- Writing to video RAM would redirect to a sprite mapper, which would be used by the PPU and UI to draw the bitmap.
- Reading from the controller I/O registers would redirect to the input handler to ask for the controller's state.
- Reading from the screen state registers would redirect to the PPU to ask for the screen's state.
I chose to use typed arrays because they accurately represent the binary data that a real Game Boy would have stored in a region of raw memory.
Here's a not-so-comprehensive illustration of the memory map:
graph TB %% Entities CPU CartData[Cartridge Data] PPU SpriteMapper[Sprite Mapper] subgraph Memory Map ROMBanks[ROM Banks] VRAM[Video RAM] WRAM[Work RAM] WRAMArr[Typed Array] end %% Relationships CPU -- reads from & writes to --> ROMBanks ROMBanks -- redirect to --> CartData CPU -- writes to --> VRAM VRAM -- redirects to --> SpriteMapper PPU -- renders sprites from --> SpriteMapper CPU -- reads from & writes to --> WRAM WRAM -- redirects to --> WRAMArr
Input Handler
The input handler would act as a bridge between the UI and the memory map. The UI would get input events from the browser to update the state of the input handler. Then the CPU would request input state from the memory map, which would redirect its requests to the input handler.
graph TB %% Entities CPU subgraph Memory Map IORegisters[I/O Registers] end InputHandler[Input Handler] Browser UI[User Interface] %% Relationships Browser -- dispatches input events to --> UI UI -- updates input state of --> InputHandler CPU -- reads from --> IORegisters IORegisters -- redirect to --> InputHandler
UI
Early development of the debugger required a lot of debugging, so I intended to start with an ugly UI that primarily provides quick access to debugging information and controls.
I'm most comfortable with React and I enjoy the simplicity it provides, so all UI state and rendering will be handled inside React. However, the emulator was designed to operate independent of any rendering frameworks (outside of the canvas API for rendering the game's scanlines to the screen), so I decided to have the UI manage a ticker that manages the scheduling of emulator ticks. Then the UI can pause, step, and resume the emulator at the click of a button.
graph TB %% Entities Emulator subgraph UI RootCmp[Root Component] CanvasPanel[Canvas Panel] MemoryPanel[Memory Panel] CPUPanel[CPU Panel] ActionsPanel[Actions Panel] Ticker end %% Relationships RootCmp -- renders --> CanvasPanel RootCmp -- renders --> MemoryPanel RootCmp -- renders --> CPUPanel RootCmp -- renders --> ActionsPanel RootCmp -- creates --> Ticker Ticker -- ticks --> Emulator Emulator -- renders to --> CanvasPanel MemoryPanel -- reads memory from --> Emulator CPUPanel -- reads registers from --> Emulator ActionsPanel -- steps & resumes --> Ticker Ticker -- reads breakpoints from --> MemoryPanel
What I have built
My emulator is maybe better described as a "Game Boy CPU Emulator" due to its lack of features, though my goal to build a playable emulator remains so I'm still going to call it a Game Boy emulator. I mention this because my emulator can load a ROM, accurately execute CPU instructions from the ROM, and implements enough of the memory map to support some simpler games.
Once a ROM is loaded, my UI renders multiple panels:
-
A canvas.
The PPU will use this canvas for final video output; currently unused.
-
Raw memory view.
- Allows viewing of hex values at every address in memory.
- Highlights the point in memory referenced by the program counter register (PC) in the CPU.
- Allows breakpoints to be set at any address.
-
CPU register view.
-
Debug controls.
- Step: moves the CPU forward one instruction by executing the instruction at the current PC address.
- Resume: moves the CPU forward in real-time indefinitely, or until a breakpoint is reached.
Reflection
I'm writing about this project nearly seven years after starting this project; a lot of time to reflect on a project.
Challenges
This project started as a good way to learn TypeScript. The early days were spent reading the TypeScript docs and struggling with the language. To this day, my favorite way to learn about a language, library, or framework is to read the official docs or source code (assuming it's well documented and has well-made source code). Although I've never looked at the TypeScript source code, the official docs are very well formed and could hardly be more useful.
On top of struggling with the language, the problem I was solving, Game Boy emulation, was new and complex. There's a ton of great documentation to aid in Game Boy emulation scattered around the internet; most of the noteworthy resources can be found in the Awesome Game Boy Development GitHub repository. Being new to emulation and Game Boy hardware, I spent most of my reading time in the Pan Docs, it's like THE unofficial Game Boy development reference book.
I don't recommend taking on two or more difficult new endeavors in tandem, but it seemed to have worked out well enough because it didn't take long for me to learn a lot about TypeScript, Game Boy hardware, emulation, typed arrays, complex React state management, and so much more.
Future plans
Rendering graphics from memory would be a huge milestone, so that's my next big goal. This goal can be broken into smaller goals, for example creating another UI debug panel for inspecting raw graphical tiles, sprites, and backgrounds. Creating those things will help me build rendering tools that will be useful for ultimately rendering scanlines on the main "screen canvas" panel.
One challenge that I continue to face is the fact that the memory viewer is hard to read. With a little exposure, it's easy enough to read the instructions in assembly form, but as hex values I have to look up the hex in an opcode table (until I've committed the 501-instruction table to memory). I thought about adding the ability to disassemble the instruction in the memory view where the PC register points, but it would require a refactor of the CPU class' instruction mapper. I feel like the CPU instructions have been tested thoroughly enough that I'm much less interested in knowing which instructions are being executed, so this has taken a low priority.
Other than implementing the standard emulation features (save states, fast forward, rewind, networking, etc.) and more missing Game Boy features (e.g. audio, ROM banks), I haven't thought far beyond these two changes. I'd rather try to play some games and see what kinds of problems demand solving.
Personal growth
Before starting this project I was frustrated by the fact that I would start many side projects and finish none of them. Going into this project I was committed to "finishing" it. Seven years later and I still wouldn't consider it complete, but I'm happy that I made significant progress and still intend to work on it. I still regularly start side projects, but I'll often try to have more modest goals such as "learn about X" or "complete project with significantly reduced scope". It's hard to say whether I've learned or produced more than I would have otherwise, but at least my mind is no longer cluttered with unfinished projects.
Lessons learned
- The Game Boy is an awesome little machine.
- It's hard to go back to untyped JavaScript.
- Understanding how things work is still one of my favorite pastimes.
- The web is capable of creating quite rich user experiences.
Conclusion
Building a Game Boy emulator was (and hopefully continues to be) a great project. Perhaps when learning another new language I'll build another emulator. Feel free to play with my emulator or fork it (link at the top).