SW Development Roadmap - jamrobmartin/GameboyEmulator GitHub Wiki

This page documents the high level steps taken to create the Gameboy Emulator.

Basic Setup

  1. Create the base form. This is just a basic form with some components to make the form look like a gameboy. Commit 47bc341
  2. We added a Button Pressed event and wired up each of the buttons to fire this event. Commit f104c43
  3. We added a debug window, similar to a command prompt, so that we can view some debug information. Commit 5e45d26

Emulator Setup

  1. First we create the Emulator Singleton. The emulator class will contain the thread that simulates the gameboy.
  2. Next we add an Event handler for button presses. Right now it only handles the On/Off buttons.
  3. Based on the On/Off button press, we start/stop a thread that runs while the emulator is powered on. Commit 035c8f9
  4. We add all of the individual components that make up a Gameboy. All of these classes are created as singletons. This is for two reasons: 1) Because there should only be one instance of each item; 2) so that each class can directly access each other. Commit e89d29e

CPU Setup

  1. First we have to add several registers to the CPU class. These registers can be accessed either directly, or in pairs, so some bit manipulation is required.
  2. We will be performing a lot of Byte and Word operations, so we created custom classes for these. Commit 3616c0b
  3. The CPU continually loops through a cycle of fetching and executing Instructions. We set up the format of this loop.
  4. The Instruction Class was set up. Each time a CPU fetch occurs, it will create an instruction based on the fetched OpCode.
  5. Lastly, We will need Bus Reads and Bus Writes to access the data, so we set up the basics of this process. Commit d79e6c2

Cartridge Setup

  1. Before we can do anything else, we need ROM data to read from. We add a menu button to select a ROM.
  2. ROM data is read from a file and stored in the Data field of the ROM class.
  3. The CPU reads ROM data to get instruction OpCodes. We display some basic info on the instruction being executed. Commit 9234995

BLARGG Test Setup

  1. BLARGG Tests are special ROMS that are used to test an Emulator. They run a series of operations, and then write the results to the serial port.
  2. We enabled the basics of IO R/W so that we can store and retrieve serial data.
  3. We added the BLARGG test Update and Display functions to the main ThreadLoop. Commit 5914d29

Implementing Instructions

  1. First we create a simple table to help us visualize which Instructions we have implemented so far. Right now, nothing is implemented so the table is all red. Commit ee8eb95

[Note] For the rest of the Implementing Instructions section, you will see that we include a comment "// Cycle" every now and then. This will be used later when we implement Interrupts and Timer. Right now, those comments don't do anything, but it will make you life a lot easier if you include them now instead of adding them in later.

LD Instructions

  1. The first batch of instructions we will create are the LD instructions that load directly to/from registers. These are all in the OpCode range 0x40 - 0x7F. The only exception is 0x76, which is reserved for the HALT command. Commit a4a9d09

  2. The rest of the LD instructions are a little different. We create special cases for the rest of them. This includes the LDH instructions for 0xE0 and 0xF0, and the auto increment/decrement instructions for 0x22, 0x32, 0x2A, 0x3A. Commit 4e838d

  3. Now that we have a few instructions parsed, we will create the first Execute Instruction calls. The first two are ExecuteInstructionLD() and ExecuteInstructionLDH().

  4. In support of processing these instructions, there are a handful of helpers that are needed. For starters, several of the load instructions have parameters which require you to fetch additional Bytes. We create the FetchData() method to help with this. There are also some instructions where the source or the destination of the load is a memory location. We create the InterpretInstruction() method to help with this.

  5. Lastly, now that we have the ability to load to various memory locations, we need to wire up the Read/Write calls in Cartridge, PPU, and RAM to actually store data. In PPU and RAM, we just create several Byte[] fields to store data, for now. In Cartridge, We create Byte[] for ROM and RAM, but also have to map data from the cartridge file to these fields. Commit d6455b4

[Note] You will notice that trying to run any of the BLARGG test ROMS will yield strange results. This is because we have not implemented all of the instructions yet.

  1. There were several remaining LD instructions to implement. These remaining LD instructions are considered 16 bit LD instructions. This should take care of all of the LD instructions.
  2. We also did some reformatting of the Implemented Instructions Form. Commit c94477f

2-Byte Instructions

We still can't get a BLARGG Test to run correctly. This is because there are several instructions that need to read the next byte as a parameter, but we haven't implemented them yet. This is causing the parameter byte to be read as if it is its own instruction, which confuses the CPU. We will focus on implementing these instructions next so that we can finally have a successful BLARGG test run.

  1. First up are the Jump Instructions. These instruction cause the program counter to jump to a specific location. Several of these have conditional checks so the jump will only occur if the condition is met. Commit a669ae6

  2. Next up are the Jump Relative Instructions. These are similar to the Jump instructions, except the memory location you jump to is relative to the current PC value. Similar to JP Instructions, JR Instructions also have several conditionals. Commit c4fd430

  3. Now we implement CALL Instructions. Call instructions are basically Jump Instructions, but you store off the current Program Counter value to the Stack so that you may return to it later. Commit 81d6ac8

  4. After the series of commands that come after a Call Instruction are completed, the Program Counter returns to where the Call was made by using a RET Instruction. The RET Instruction simply pulls two bytes off of the Stack and loads them in to PC. Commit dae2108

  5. Next up we start to implement some of the Arithmetic/Logical Instructions. There are a series of these instructions that operate on two registers. The instructions we implement here operate on the A register, and use the next Byte as a parameter for that operation. Commit 9be6ca6

  6. There is a special instruction that operates on the SP. It takes the current SP position, adds a relative value from a parameter, and loads that back in to SP. Commit 19e6b8a

  7. The last batch is a big one. A CB Instruction (0xCB) performs bit operations on the various registers, or the memory stored at (HL). The operation to be performed is stored in the subsequent Byte that we read in as a parameter. The various bits in that byte tell us what the operation is, and which register to perform the operation on. Commit b5ae476

INC/DEC Instructions

If you run the LD R,R BLARGG test, and print out the not implemented instructions, you will see 0x1C getting called a lot. This instruction is an increment instruction that increases the value stored in the E register. We will determine which instructions to implement next as they get flagged as not implemented when trying to run a BLARGG Test. The goal for right now is to get to a point where a BLARGG Test can be completed. Commit 9338e9d

  1. We implemented INC and DEC Instructions. These Instructions take a value from a register, increment or decrement it by 1, and then save it back to the register. Depending on if the register is a single register or a register pair, the condition flags may or may not be set. Commit 7ed8055

NOP/STOP

  1. The NOP Instruction is simply a No Operation Instruction. The CPU reads the Byte, and then does nothing. The STOP command is supposed to put the Gameboy in a low power state. For right now, we will just treat it like a NOP as well. Commit 6b20ed9

  2. We were running in to some issues where the BLARGG Tests were not correctly writing to memory. We also had an issue where flags weren't being set correctly. We fixed those bugs and now can proceed with implementing more instructions. Commit 3fbced8

DI/EI

  1. We implement the Disable Interrupt and Enable Interrupt Instructions. Right now we don't actually implement the interrupts themselves so these instructions just update some Interrupt properties. Commit 9c98b6a

POP/PUSH

  1. We implement POP and PUSH Instructions next. POP and PUSH simply take the specified Register and either store or retrieve their value from the Stack. Commit 24f7a73

Arithmetic and Logical Instructions

First we implement the register based Arithmetic and Logical Instructions. These are all of the instructions 0x80-0xBF. These are almost identical to the parameter based Arithmetic and Logical Instructions we implemented in the 2-byte Instructions section. The main difference being that the parameter we used for these instructions is a value from another Register. Commit 9ae413c

Finally we implement the four remaining Arithmetic and Logical Instructions: DAA, CPL, SCF, and CCF. Commit 0dd4de4

[Note] At this point, running the BLARGG Tests will at least start to display some of the text from the serial port!

RLCA, RRCA, RLA, RRA

We implement a few remaining rotation/shift instructions. These are very similar to the CB Instructions we previously implemented, with a few variations specific to these four instructions. Commit 47a9fc7

ADD HL,Reg

We implemented the remaining four ADD Instructions. These instructions are similar to previous ADD Instructions, except the result of these ADD operations get stored in HL. Commit 0944747

RST

We implemented the RST Instructions. These Instructions are special cases of the CALL Instruction in which the destination address is prespecified. Commit cf8b985

HALT/RETI

We implement the final two instructions. RETI is simply a call to RET, but we enable Interrupts first. HALT is a function that will be utilized by interrupts, but the functionality hasnt been implemented yet. For now, when we run a CPU step, we check to see if the Halted flag has been set. If it is, we do nothing on that CPU step, but set the Halted flag back to False. Commit ef9e93c

BLARGG Tests

First we do a little clean up to make the BLARGG Output a little more readable. Commit 1a226b8

At this point, we have attempted to implement all of the instructions. Most of the BLARGG Tests should be able to run and pass. Right now, these are the results I show when I run each of the tests:

Test # Current Output
01 "01-special Passed "
02 "02-interrupts EI Failed #002 "
03 "03-op sp,hl Passed Failed #000 "
04 "04-op r,imm FE DE Failed "
05 "05-op rp Passed Failed #000 "
06 "06-ld r,r Passed Failed #000 "
07 "07-jr,jp,call,ret,rst Passed Failed #000 "
08 "08-misc instrs Passed Failed #000 "
09 "09-op r,r B? B@ BA BB BC BD BF ?7 ?8 ?9 ?: ?; ?< ?> @? @@ @A @B @C @D @F Failed "
10 "10-bit ops Passed Failed #000 "
11 "11-op a,(hl) BE @E CB 7= CB 7E CB 8= CB 8E CB 9= CB 9E CB := CB :E Failed "

As we can see, less than ideal results... Time for some debugging!

  1. We had some copy paste errors, and some missing Int conversions in the SBC and CP Instructions Commit 6fe52fa

  2. We Implemented a Test All feature that will automatically run each of the tests in a given folder. This should help speed up testing if we want to run everything at once. Commit 7a779bf

  3. We had a bug with the ADD Instruction. The if statements only asked if we were performing ADD using Addressing Mode Register_Register. It was not asking if we were performing this on a 2-byte register or not. This was causing the case where we were trying to add the value of the memory location stored in HL to be treated as trying to add the value of HL. Commit e50128c

  4. Lastly, we had a bug in the CB Instructions where we were writing directly to HL instead of the memory location stored in HL. Commit 9bcc4f5

Now when we run the BLARGG Tests, we get the following results:

Test # Current Output
01 "01-special Passed "
02 "02-interrupts EI Failed #2 "
03 "03-op sp,hl Passed "
04 "04-op r,imm Passed "
05 "05-op rp Passed "
06 "06-ld r,r Passed "
07 "07-jr,jp,call,ret,rst Passed "
08 "08-misc instrs Passed "
09 "09-op r,r Passed "
10 "10-bit ops Passed "
11 "11-op a,(hl) Passed "

The only test that failed is test 02-interrupts, but that is okay because we haven't implemented interrupts yet!

Interrupts

  1. First thing we have to do is to create the Interrupt Enable and Interrupt Flag bytes.
  2. Then, we implement the HandleInterrupts method. This method checks to see if an interrupt is both enabled and set, and if it is, it pushes the current PC value to the stack and loads the address of an interrupt handler to the PC.
  3. Lastly, we make sure that Bus.Read and Bus.Write calls correctly utilize the new Interrupt Bytes. Commit 8fe5785

Now when we run the 02-Interrupts test, it tells us that Timer isnt working. This is because we haven't implemented Timer yet!

  1. First, we add a few fields to the Timer class. These represent the IO Bytes 0xFF04 - 0xFF07. We also make sure we update the R/W chain from Bus to IO to Timer to read and write to these new fields.

  2. Next we implement the Timer.Tick method. This method increases the value of DIV, and then requests the Timer interrupt if certain conditions have been met.

  3. Nothing currently calls Timer.Tick so we add a method to the Emulator Class called DoCycles(). This method emulates the CPU cycles that a real CPU would go through. Each cycle contains four ticks.

  4. And lastly, we need to call DoCycles(). Remember all of those "// Cycle" comments we put in when implementing the instructions? You can now do a Find-Replace All on this comment! Just replace "// Cycle" with "Emulator.Instance.DoCycles(1)" Commit 66a50ff

Now when we run the BLARGG Tests, we get the following results:

Test # Current Output
01 "01-special Passed "
02 "02-interrupts Passed "
03 "03-op sp,hl Passed "
04 "04-op r,imm Passed "
05 "05-op rp Passed "
06 "06-ld r,r Passed "
07 "07-jr,jp,call,ret,rst Passed "
08 "08-misc instrs Passed "
09 "09-op r,r Passed "
10 "10-bit ops Passed "
11 "11-op a,(hl) Passed "

Everything Passed!!!!!! Now that all of the instructions are implemented, we can move on to getting some things rendered!

Graphics

  1. The first thing we are going to do is create a debug window that lets us view the raw tile data. There are a lot of technical factors here. Refer to the comments in the source code, and the PanDocks Tile Data for more information. Commit 821cc54

Now if we run the Tetris ROM, or the DR Mario ROM, you will see that pixels are being generated in the Debug Window.

  1. Next we implement the LCD Hardware Registers. These fields store data that is used during the graphics rendering process. Commit ee474b8

State Machine

The PPU continually switches between 4 states. One of the states pulls in data from the Object Attribute Memory. Another state draws pixels. The other two states implement a waiting mechanism at the end of each line, and at the end of each frame, used to keep everything in sync.

  1. For right now, we just implement a shell of the state machine. The state machine increments a counter and switches between each of the modes, but none of the modes do anything currently. Commit 559a47c

  2. There are several interrupts that can occur throughout the various states. We implement those now. Commit 9332fdc

[Note] If you run the Dr Mario ROM now, you will see that some of the sprites appear to move up and down!

Pixel Fetcher/ Pixel FIFO

Mode 3 of the PPU State Machine is where we actually draw the pixels to the screen. The PPU utilizes a Pixel Fetcher, and a Pixel FIFO Queue to accomplish this. The PPU ultimate will merge together pixels from the Background, Window, and Objects layers. For right now, lets just get the background drawing.

  1. First we need to add some fields to our PPU. First we add a VideoBuffer. This is where the pixel data that gets written to the screen gets stored and read from. The PPU will write pixel data to this buffer, and the UI(LCD) will read from this buffer. Next we add some fields that will be used by the Pixel Pipeline. Lastly we add some color palettes and a method to update them. We also add some properties that will pull data out the 0xFF40 - LCDC byte. These will also be used by the Pixel Pipeline.

  2. Next we create a class for the Pixel FIFO. This is essentially just a FIFO Queue that holds Color data(Pixel Data). We instantiate one object of this class called BackgroundPixelFIFO.

  3. Next we create the Pixel Fetcher methods. These methods handle all 5 states of the Pixel Fetcher: Tile, Data0, Data1, Sleep, and Push. The Tile and Data states just pull specific Bytes out of Memory based on the various fields we implemented earlier. The push state is what actually pushes pixels into the Pixel FIFO, which are eventually written to the VideoBuffer.

  4. Lastly, we wire up the rendering in the main GameboyForm. We create a timer that goes off every .25 seconds and causes the main PictureBox to redraw itself. When the redraw happens, the form basically iterates over every pixel data stored in the VideoBuffer and renders it in the appropriate location. Commit 33eb0b6

We now have basic Gameboy rendering! If you run the Dr Mario ROM, you will see that it loads the main menu and then proceeds to a demo type screen where there are actual moving pixels and such.

DMA

The Gameboy utilizes a special process called Direct Memory Access to efficiently write certain data to the OAM. This process is more efficient than writing to the OAM Directly. This will become more important later when we start rendering Sprites to the screen.

  1. We implement the DMA process in the PPU. This process is triggered by a Bus Write to 0xFF46. Commit 5bceda9

More Tests

Before proceeding anything further. It would be prudent of us to do some additional testing on the code we have already written. If there are bugs in the existing code, it will be easier to find and fix them now than to try to track them down later.

Interestingly, we must have incidentally broken something because running the BLARGG Test 02 now yields a "Halt Failed #5" Result.

  1. We begin by adding in a few more ROMS, adding in Header Information, and tweaking the DebugTileViewer. Commit 969c9e7

Instruction Timing

First we want to confirm Instruction Timing. We had previously checked Instruction Functionality, but we never checked to make sure we were applying the correct number of CPU Cycles per instruction.

  1. The first thing we do is reorganize how to handle Cycles. First, rather than doing the Cycles in line as they appear, we queue them up until an Instruction is executed, and then process them all at once.

  2. The next thing we do is standardize our language so we know if we are talking about Machine Cycles or Time Cycles. This means that all cycles are referring to M Cycles until we get to the individual DoCycles() methods. Commit 5bc37f7

  3. Next we add an Init Method to RAM so that we can correctly power off and power on without having to reopen the program every time. Commit eb538c4

  4. Now we added in the ability to keep track of Instruction Cycle Counts. Everytime that an instruction is executed, the cycle count associated with that instruction is updated in a set of three arrays.

Running each of the BLARGG Tests should run every possible instruction at least once. Running these and comparing the displayed cycle counts revealed a few inconsistencies.

  1. We implemented a few bug fixes to make sure the correct cycle counts were being used. Commit 830b07a

Now we try to run the instr_timing test, but we get a "Timer doesn't work properly" error.

Timer