<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://edmondsnathan.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://edmondsnathan.github.io/" rel="alternate" type="text/html" /><updated>2025-11-09T03:22:28+00:00</updated><id>https://edmondsnathan.github.io/feed.xml</id><title type="html">Nathan’s Tech Blog</title><subtitle>Follow me on my journey through the world of computers!</subtitle><entry><title type="html">GameCrab Dev Log 0: Emulating the Game Boy’s CPU</title><link href="https://edmondsnathan.github.io/2025/11/08/Emulating-the-Game-Boy-CPU.html" rel="alternate" type="text/html" title="GameCrab Dev Log 0: Emulating the Game Boy’s CPU" /><published>2025-11-08T00:00:00+00:00</published><updated>2025-11-08T00:00:00+00:00</updated><id>https://edmondsnathan.github.io/2025/11/08/Emulating-the-Game-Boy-CPU</id><content type="html" xml:base="https://edmondsnathan.github.io/2025/11/08/Emulating-the-Game-Boy-CPU.html"><![CDATA[<h1 id="overview">Overview</h1>
<p>GameCrab is an in-development emulator for the Nintendo Game Boy written in Rust. The emulator is open source and dedicated to the public domain under the CC0 license.
This post covers how I went about emulating the Game Boy’s CPU. I’ll discuss how the CPU works and show the code I wrote to emulate it along the way.</p>
<h1 id="components-of-the-console">Components of the Console</h1>
<h2 id="the-cpu">The CPU</h2>
<p>The CPU object contains two fields, a register object which holds all of the different CPU registers, and a flag which toggles whether interrupts are enabled.</p>
<pre><code class="language-Rust">pub(crate) fn new() -&gt; CPU {
    CPU {
        registers: Registers::new(),
        enable_interrupts: false,
    }
}
</code></pre>
<h3 id="registers">Registers</h3>
<p>The register object simply holds all of the registers which compose the CPU.</p>
<pre><code class="language-Rust">pub(crate) struct Registers {
    pub a: u8,
    pub b: u8,
    pub c: u8,
    pub d: u8,
    pub e: u8,
    pub f: u8,
    pub h: u8,
    pub l: u8,
    pub sp: u16,
    pub pc: u16,
    pub bus: u16,
    pub x: u8,
    pub y: u8,
}
</code></pre>
<h2 id="the-clock">The Clock</h2>
<p>The Game Boy’s CPU has a clock which ticks at a rate of approximately 4 MHz. The clock can be broken down into M cycles and T cycles.
T cycles represent a single tick of the clock, and the CPU is able to perform one command each tick. The RAM takes 4 T cycles to access, so each M cycle is comprised of 4 T cycles.</p>
<h2 id="instructions-and-commands">Instructions and Commands</h2>
<p>Within the emulator, the terms instruction and command represent two separate yet related concepts:</p>
<h3 id="instructions">Instructions</h3>
<p>Instructions refer to the opcodes for the CPU. A byte in RAM is fetched and decoded to determine the instruction to execute.</p>
<pre><code class="language-Rust">// Each variant of instruction enum represents a category of instructions, and each variant contains another enum which disambiguates the category into a specific instruction
pub enum Instruction {
    CB,
    Control(ControlOps),
    Load16(Ld16),
    Push(PushPop),
    Pop(PushPop),
    Load8(Ld8, Ld8),
    Arithmetic16(A16Ops),
    Arithmetic8(A8Ops),
    JumpRelative(JR),
    Jump(JP),
    Restart(u8),
    Return(Ret),
    Call(Calls),
    BitOp(BitOps),
}
</code></pre>
<pre><code class="language-Rust">// For example, The Control variant of the Instruction enum takes a ControlOps enum as its field, each representing a different control instruction
// The hex numbers represent the opcodes byte for each instruction
pub enum ControlOps {
    NOP,  //0x00
    STOP, //0x10
    HALT, //0x76
    DI,   //0xF3
    EI,   //0xFB
    DAA,  //0x27
    SCF,  //0x37
    CPL,  //0x2F
    CCF,  //0x3F
}
</code></pre>
<h3 id="commands">Commands</h3>
<p>Commands are the steps performed to execute an instruction. Each command takes one clock cycle to execute, and multiple commands can be queued in order to execute an instruction. Commands come in two forms, read and update. An enum with two possible states is used to represent the commands.</p>
<h4 id="read">Read</h4>
<p>The first state is Read, which covers reading a byte between the RAM and registers. It takes two arguments, a source and destination. Each can either reference a CPU register or an address in RAM. As the names suggest, the value of the source is read and then written to the destination.</p>
<h4 id="update">Update</h4>
<p>The second state is Update, which handles updating the internal state of the console. This can come in many forms, such as performing arithmetic between CPU registers or copying a 16-bit value between register pairs. The update command takes a pointer to a function with a mutable reference to the console as an argument. This often comes in the form of a lambda expression, allowing functions with different signatures to be passed as a command.</p>
<pre><code class="language-Rust">// The Command enum has two variants, one for read and one for update
// Read has fields for the source and destination,
// and Update takes a function with a mutable reference to the console as an argument
pub(crate) enum Command {
    Read(Source, Destination),
    Update(fn(&amp;mut Console)),
}
</code></pre>
<h2 id="fetch-decode-execute-loop">Fetch-Decode-Execute Loop</h2>
<p>The CPU operates using a fetch-decode-execute loop. It begins by fetching a byte from RAM, decoding it into an instruction for the CPU, and finally executing the instruction.</p>
<h3 id="fetch">Fetch</h3>
<p>When it is time for the CPU to run the next instruction, it starts by fetching a byte in RAM at the address specified by the Program Counter(PC). This byte is then passed to the decoder to determine what instruction should be executed.</p>
<pre><code class="language-Rust">// The program counter points to an address in memory and the corresponding byte is fetched
// self is referencing the console
self.ram.fetch(self.cpu.get_register_16(&amp;Register16::Pc))
</code></pre>
<h3 id="decode">Decode</h3>
<p>The byte that was fetched is then used to determine which of the 500 CPU instructions to run. Because a single byte can only represent 256 values, the instruction 0xCB is treated as a prefix, meaning that the following byte in memory will represent a different set of 256 possible instructions.</p>
<pre><code class="language-Rust">// The decoder has two functions which are very similar.
// There is decode and decode_cb. Which function is run depends on the value of the CB flag, which is set to true when the previous command was 0xCB
// The fetched byte from RAM is passed into the function, and the decoder matches the low and high nibble of the byte in order to return the corresponding instruction
// decode_cb works in the exact same way, just with different instructions mapped to each byte
pub fn decode(byte: u8) -&gt; Result&lt;Instruction, String&gt; {
    let high_nibble = byte &amp; 0xF0;
    let low_nibble = byte &amp; 0x0F;

    match high_nibble {
        0x0 =&gt; match low_nibble {
            0x0 =&gt; Ok(Control(NOP)),
            0x1 =&gt; Ok(Load16(BCU16)),
            0x2 =&gt; Ok(Load8(Ld8::BC, Ld8::A)),
            0x3 =&gt; Ok(Arithmetic16(A16Ops::Inc(A16Args::BC))),
            0x4 =&gt; Ok(Arithmetic8(A8Ops::Inc(A8Args::B))),
            0x5 =&gt; Ok(Arithmetic8(A8Ops::Dec(A8Args::B))),
            0x6 =&gt; Ok(Load8(Ld8::B, Ld8::U8)),
            0x7 =&gt; Ok(BitOp(RLCA)),
            0x8 =&gt; Ok(Load16(U16SP)),
            0x9 =&gt; Ok(Arithmetic16(A16Ops::Add(A16Args::BC))),
            0xA =&gt; Ok(Load8(Ld8::A, Ld8::BC)),
            0xB =&gt; Ok(Arithmetic16(A16Ops::Dec(A16Args::BC))),
            0xC =&gt; Ok(Arithmetic8(A8Ops::Inc(A8Args::C))),
            0xD =&gt; Ok(Arithmetic8(A8Ops::Dec(A8Args::C))),
            0xE =&gt; Ok(Load8(Ld8::C, Ld8::U8)),
            0xF =&gt; Ok(BitOp(RRCA)),
            _ =&gt; Err(log_error(byte)),
        },
    // The function continues on like this for all 256 values
</code></pre>
<h3 id="execute">Execute</h3>
<p>The execute step is where things start to get interesting. As discussed earlier, instructions for the Game Boy are composed of a series of commands executed over multiple clock cycles. The commands are pushed onto the execution queue and executed over the following clock cycles.</p>
<pre><code class="language-Rust">// This function handles all 3 steps of the fetch, decode, execute loop
// self is referencing the console
pub(crate) fn fetch_decode_execute(&amp;mut self) {
    // The 0xCB instruction sets the cb_flag to true.
    // The value of the cb_flag determines which decoder should be used when reading the byte
    let decoder = match self.cb_flag {
        true =&gt; decode_cb,
        false =&gt; decode,
    };

    // The decode methods return a Result&lt;instruction, String&gt; because certain bytes do not map to any instruction.
    // The fetch method returns the byte at the addres represented by the program counter, and it is passed into the appropriate decoder to return an instruction
    let instruction = match decoder(self.ram.fetch(self.cpu.get_register_16(&amp;Register16::Pc))) {
        Ok(value) =&gt; value,
        Err(error) =&gt; panic!("{error}"),
    };

    // After the instruction has been decoded, we can set the cb_flag back to false so that we are ready for the next instruction
    self.cb_flag = false;

    // Every instruction for the Game Boy increments the program counter on the second tick, so we go ahead and schedule that here rather than duplicate it for every instruction
    self.push_command(1, Command::Update(Self::command_increment_pc));

    // Finally, we run the execute method, which takes an instruction and pushes all of the commands that compose that instruction onto the execution queue
    self.execute(instruction);
}
</code></pre>

<pre><code class="language-Rust">// This method takes an instruction as input and translates them to the corresponding method to execute that instructions
// The emulator is still early in devlopment, which is why there are so many todo macros :)
// self is referencing the console
pub fn execute(&amp;mut self, instruction: Instruction) {
    // Every instruction method returns an Option&lt;u64&gt;
    // This Option parameter tells the console how many ticks to wait before fetching the next instruction
    if let Some(next_instruction_offset) = match instruction {
        CB =&gt; self.instruction_cb(),
        Control(control_op) =&gt; self.instruction_control(control_op),
        Load16(ld16) =&gt; self.instruction_load16(ld16),
        Push(push_pop) =&gt; todo!("push not implemented"),
        Pop(push_pop) =&gt; todo!("pop not implemented"),
        Load8(to, from) =&gt; self.instruction_load8(to, from),
        Arithmetic16(a16_ops) =&gt; todo!("arith16 not implemented"),
        Arithmetic8(a8_ops) =&gt; todo!("arith8 not implemented"),
        JumpRelative(jr) =&gt; todo!("jumpRelative not implemented"),
        Jump(jp) =&gt; todo!("jump not implemented"),
        Restart(arg) =&gt; todo!("restart not implemented"),
        Return(ret) =&gt; todo!("return not implemented"),
        Call(calls) =&gt; todo!("call not implemented"),
        BitOp(bit_ops) =&gt; todo!("bitOp not implemented"),
    } {
        // This is where the next instruction gets queued, using the return value of the instruction methods
        self.queue_next_instruction(next_instruction_offset);
    }
}
</code></pre>
<h2 id="execution-queue">Execution Queue</h2>
<p>The execution queue is a hash map where the key is the tick count and the value is a queue of commands. The CPU only ever queues one command at each tick count, but this approach allows other components such as the PPU(Pixel Processing Unit) to also schedule commands each tick.
Each tick, the execution queue is queried to see if any commands are scheduled for that tick. If so, those commands are executed.</p>
<pre><code class="language-Rust">// The Hashmap takes a u64 as the key, which represents the tick counter
// The VecDeque is used as a queue, and each element of the queue is a Command object
// Each tick, the execution queue is passed the current tick count, and it pops off the queue of commands if one exists
pub(crate) struct ExecutionQueue {
    map: HashMap&lt;u64, VecDeque&lt;Command&gt;&gt;,
}
</code></pre>
<h3 id="queueing-commands">Queueing Commands</h3>
<p>Once the instruction to execute has been determined, we must queue all of the commands needed to execute the instruction.</p>
<pre><code class="language-Rust">// The method most instructions use is the push_command method
// This method takes a tick offset and the command to queue as arguments
// Tick offset is how many ticks in the future the command will be queued relative to the current tick count
// self is referencing the console
pub(super) fn push_command(&amp;mut self, tick_offset: u64, command: Command) {
    // The method adds the tick offset to the current tick count and pushes the command onto the queue using the push_command_absolute method
    self.execution_queue
        .push_command_absolute(self.tick_counter + tick_offset, command);
}

// This method is responsible for actually queueing the commands
// The command is scheduled at the actual tick count rather than a relative count
// self is referencing the execution queue
pub(crate) fn push_command_absolute(&amp;mut self, tick: u64, command: Command) {
    // First we query if a queue already exists at the specified tick
    match self.map.get_mut(&amp;tick) {
        // If so, we push the command to the back of the queue
        Some(cmds) =&gt; {
            cmds.push_back(command);
        }
        // If not, we instantiate a new queue at the specified tick
        None =&gt; {
            self.map.insert(tick, VecDeque::from([command]));
        }
    }
}
</code></pre>
<h1 id="the-console-object">The Console Object</h1>
<p>Now that we understand the elements of the console, it’s time to create the Console object.</p>
<pre><code class="language-Rust">// The Console is a composition of all the components discussed previously
// This allows the console to have methods which allow communication between components
pub struct Console {
    pub(crate) cpu: CPU,
    pub(crate) rom: ROM,
    pub(crate) ram: RAM,
    pub(crate) tick_counter: u64,
    pub(crate) execution_queue: ExecutionQueue,
    pub(crate) cb_flag: bool,
}
</code></pre>
<pre><code class="language-Rust">// When the console is instantiated, it calls the instantiation method for each of its components
// This allows components to start off in their default states
pub fn new() -&gt; Console {
    Console {
        cpu: CPU::new(),
        rom: ROM::new(),
        ram: RAM::new(),
        tick_counter: 0,
        execution_queue: ExecutionQueue::new(),
        cb_flag: false,
    }
}
</code></pre>
<pre><code class="language-Rust">// For example, the register object, contained inside of the CPU, has default starting values that need to be set for each register
pub fn new() -&gt; Registers {
    Registers {
        a: 0x01,
        b: 0xFF,
        c: 0x13,
        d: 0x00,
        e: 0xC1,
        f: 0x00,
        h: 0x84,
        l: 0x03,
        sp: 0xFFFE,
        pc: 0x0100,
        x: 0x0,
        y: 0x0,
        bus: 0x0100,
    }
}
</code></pre>
<h1 id="putting-it-all-together">Putting It All Together</h1>
<p>Now that everything has been explained, we can go step-by-step through an example to show how everything works in action. We will be fetching, decoding, and executing the instruction 0x01. This instruction is Load BC U16, which takes the two bytes of RAM following the instruction and reads the values into the BC register pair.</p>
<h2 id="unit-test">Unit Test</h2>
<p>We will start by looking at the unit test for this instruction, which starts by instantiating a new console object and populating the RAM with the data necessary to run the instruction. After that, it simulates the clock by repeatedly running the tick command. Finally we test to make sure the values stored in the registers are what we expect.</p>
<pre><code class="language-Rust">// This function handles the creation of the console for the unit tests
fn init(memory_map: Vec&lt;(u8, u16)&gt;) -&gt; Console {
    // first we instantiate a new Console object
    let mut console = Console::new();

    // The memory_map variable represents a vector of tuples where the u8 value is the byte to store in RAM, and the u16 is the address to store that byte
    for memory in memory_map {
        console.ram.set(memory.0, memory.1);
    }

    // Finally we return the console to the calling function
    console
}

#[test]
fn bc_u16() {
    // (0x01, 0x100) is the bc_u16 instruction stored at address 0x100
    // We start at address 0x100 because that is the initial value of the program counter
    // The next two tuples represent the following bytes of memory, and the values are what we will read from RAM into the register
    // The Game Boy is little endian, so the first byte following the instruction is the value to read into register C(50), and the second register reads into register B(45)
    let mut console = init(vec![(0x01, 0x100), (50, 0x101), (45, 0x102)]);

    // The Load BC U16 instruction requires 12 clock cycles to execute, so we will run the tick method 12 times 
    for n in 0..12 {
        console.tick();
    }

    // Now we test to ensure that the correct values have been read into each register
    assert_eq!(console.cpu.get_register(&amp;Register8::B), 45);
    assert_eq!(console.cpu.get_register(&amp;Register8::C), 50);
}
</code></pre>
<h2 id="the-tick-method">The Tick Method</h2>
<p>After initializing the console with the test data in RAM, the next step is to run the tick method.</p>
<pre><code class="language-Rust">// self is referencing the console
pub fn tick(&amp;mut self) {
    // The first time the tick method is run, it calls the fetch_decode_execute instruction in order to queue the first instruction
    if self.tick_counter == 0 {
        self.fetch_decode_execute();
    }

    // Now query the execution queue at the current tick and pop off the queue if one exists
    let map_option = self.execution_queue.pop(&amp;self.tick_counter);
    // If a command queue exists, we iterate through each command and execute it
    if let Some(queue) = map_option {
        for command in queue {
            command.execute_command(self);
        }
    }

    // Finally, we increment the tick counter so that we already for the next time the tick method is called
    self.tick_counter += 1;
}
</code></pre>
<h2 id="queueing-commands-1">Queueing Commands</h2>
<p>In order to execute a command, we must first queue it. Let’s take a look at examples for both Read and Update commands.</p>
<h3 id="read-commands">Read Commands</h3>
<p>To handle read commands, we call the read method which reads the value from the source and writes it to the destination.</p>
<pre><code class="language-Rust">fn read(console: &amp;mut Console, source: &amp;Source, destination: &amp;Destination) {
    // First we determine if the source is a Register or address in RAM and store the value into the value variable
    let value = match source {
        Source::Register(register) =&gt; console.cpu.get_register(register),
        Source::RamFromRegister(register16) =&gt; {
            console.ram.fetch(console.cpu.get_register_16(register16))
        }
    };

    // Now we write the value variable into the appropriate register or RAM address
    match destination {
        Destination::Register(register) =&gt; console.cpu.set_register(value, register),
        Destination::RamFromRegister(register16) =&gt; console
            .ram
            .set(value, console.cpu.get_register_16(register16)),
    }
}
</code></pre>
<p>Let’s take a look at an example of a read command used in the Load BC U16 instruction:</p>
<pre><code class="language-Rust">// In this example, we are scheduling a read command 5 ticks from the current ticks
// The source of the read is the byte of RAM at the address specified by the Bus register
// The destination is the register "high," which refers to the higher register in the register pair BC, which is the C register
// self is referencing the console
self.push_command(
    5,
    Read(
        Source::RamFromRegister(Register16::Bus),
        Destination::Register(high),
    ),
);
</code></pre>
<h3 id="update-commands">Update commands</h3>
<p>Now let’s take a look at two different update commands, one which passes a method directly, and one that uses a lambda expression. Remember that the Update command expects a function whose signature contains one argument, a mutable reference to the console object.</p>
<pre><code class="language-Rust">// Since command_increment_pc has a signature matching that which Update commands expect, we are able to directly pass it as a reference
// self is referencing the console
self.push_command(4, Update(Self::command_increment_pc));
</code></pre>
<pre><code class="language-Rust">// command_increment_pc's signature
pub(crate) fn command_increment_pc(&amp;mut self)
</code></pre>

<pre><code class="language-Rust">// Here, we want to call the method set_register_16, which takes 3 arguments, a mutable reference to the CPU, a 16 bit value, and a Register16, which is a register pair explained earlier
// Because this method does not match the signature expected by an Update command, we instead pass a lambda expression whose signature matches that expected by the Update command. Inside of that lambda expression, we are able to use the console argument passed into it to call the set_register_16 method with all of the arguments it requires.
// self is referencing the console
self.push_command(
    3,
    Update(|console: &amp;mut Console| {
        console.cpu.set_register_16(
            console.cpu.get_register_16(&amp;Register16::Pc),
            &amp;Register16::Bus,
        )
    }),
);
</code></pre>
<pre><code class="language-Rust">// The signature for set_register_16
// self is referencing the CPU object
pub(crate) fn set_register_16(&amp;mut self, value: u16, register: &amp;Register16) {
</code></pre>
<h2 id="executing-commands">Executing Commands</h2>
<p>Now it’s time to execute a command. As seen earlier, the tick method calls the execute_command method for each command queued at the current tick.</p>
<pre><code class="language-Rust">// self is referencing the command object
pub(crate) fn execute_command(&amp;self, console: &amp;mut Console) {
    // First we determine what variant of command we are working with, Read or Update
    match self {
        // If the Command is of type Read, we call the read method below, passing the source and destination
        Command::Read(source, destination) =&gt; Self::read(console, source, destination),
        // If we have an Update command, we call the function it points to and pass the console as a mutable reference
        Command::Update(func) =&gt; func(console),
    }
}
</code></pre>
<h2 id="the-load-bc-u16-instruction">The Load BC U16 Instruction</h2>
<p>Finally, we can take a look and undertand how the Load BC U16 instruction works. It’s a long method, but most of it is just repeatedly queueing commands.</p>
<pre><code class="language-Rust">// This method is used for multiple instructions which read 2 bytes from RAM into a register pair
// For this example, the register variable represents register pair BC
// self is referencing the console
fn u16_to_register(&amp;mut self, register: Register16) -&gt; Option&lt;u64&gt; {
    // This function takes a register pair and returns the 8-bit registers composing it
    // the low variable is register B, while high is register C
    let (low, high) = register16_to_register8(register);

    // We begin by pushing an Update command 3 ticks in the future
    // This command copies the value of the program counter register pair into the Bus register
    self.push_command(
        3,
        Update(|console: &amp;mut Console| {
            console.cpu.set_register_16(
                console.cpu.get_register_16(&amp;Register16::Pc),
                &amp;Register16::Bus,
            )
        }),
    );

    // Next we push a command to increment the program counter by 1
    self.push_command(4, Update(Self::command_increment_pc));

    // Now we use a Read command to read the RAM at the address specified by the Bus register into the high register(C)
    // The Game Boy is little endian, which is why the first byte is the high register and the second byte is the low register
    self.push_command(
        5,
        Read(
            Source::RamFromRegister(Register16::Bus),
            Destination::Register(high),
        ),
    );

    // Now we repeat the same commands, except read into the low register(B)
    // Once again, we start by loading the program counter's value into the Bus
    self.push_command(
        6,
        Update(|console: &amp;mut Console| {
            console.cpu.set_register_16(
                console.cpu.get_register_16(&amp;Register16::Pc),
                &amp;Register16::Bus,
            )
        }),
    );

    // We again increment the program counter by 1
    self.push_command(7, Update(Self::command_increment_pc));

    // Finally we read the Read the RAM address specified by the Bus into the low register(B)
    self.push_command(
        8,
        Read(
            Source::RamFromRegister(Register16::Bus),
            Destination::Register(low),
        ),
    );

    // The last step for all instructions is to return an integer specifying how many ticks the instruction takes to run
    // This allows us to schedule the next fetch, decode, and execute cycle, in this case, 12 ticks from the current tick
    Some(12)
}
</code></pre>
<h1 id="conclusion">Conclusion</h1>
<p>As is pretty obvious from the length of this article, a lot goes into executing an instruction in the emulator. While this approach takes a lot of setup to get running, I think it provides a pretty elegant way of implementing each instruction; you simply have to queue the commands composing the instruction using the <code>console.push_command</code> method. All of the complexity that comes with executing an instruction doesn’t have to be considered when you are working on the implementation. That’s a good thing too, considering the Game Boy has 500 CPU instructions!</p>
<h2 id="whats-next">What’s next</h2>
<p>Compared to my previous emulation project, the CHIP-8, The Game Boy is a whole different beast. I have only partially implemented the CPU, and already the project is more than 3x the lines of code of the CHIP-8 emulator. As for what’s next to do, I have to finish implementing all the remaining CPU instructions, which is much easier said than done. Thankfully, I have finished laying the groundwork and can focus solely on the instruction implementations now. I plan to post more updates as I progress with the emulator and come across new interesting challenges.</p>]]></content><author><name></name></author><category term="Other" /><summary type="html"><![CDATA[Creating a cycle-accurate Game Boy emulator in Rust]]></summary></entry><entry><title type="html">What I learned Building a CHIP-8 Interpreter</title><link href="https://edmondsnathan.github.io/2025/08/16/What-I-Learned-CHIP-8-Interpreter.html" rel="alternate" type="text/html" title="What I learned Building a CHIP-8 Interpreter" /><published>2025-08-16T00:00:00+00:00</published><updated>2025-08-16T00:00:00+00:00</updated><id>https://edmondsnathan.github.io/2025/08/16/What-I-Learned-CHIP-8-Interpreter</id><content type="html" xml:base="https://edmondsnathan.github.io/2025/08/16/What-I-Learned-CHIP-8-Interpreter.html"><![CDATA[<h1 id="overview">Overview</h1>
<p>This is a post detailing my experience writing an interpreter for the CHIP-8 language using C# with the MonoGame framework. The project can be found <a href="https://github.com/EdmondsNathan/CHIP8Interpreter" target="_blank">here</a> and is dedicated to the public domain under the CC0 license.</p>

<p><img src="/images/25-08/16/CHIP8-Splash.png" alt="CHIP-8 Splash Screen" /></p>

<h1 id="what-is-chip-8">What is CHIP-8</h1>
<p>CHIP-8 is a programming language from the 1970s which was used to make simple games for systems like the COSMAC VIP. It is an 8-bit system with 4 kilobytes of RAM and a 64x32 single-color display. As these specs suggest, it is a very minimal system, perfect for learning the basics of building an interpreter or emulator.</p>
<h1 id="what-i-made">What I Made</h1>
<h2 id="tools">Tools</h2>
<p>To create my CHIP-8 interpreter, I decided to use C# and .NET along with the MonoGame framework. This is the language I am most comfortable in, so that allowed me to focus my learning on the interpreter, rather than the programming language.</p>
<h2 id="overview-of-the-program">Overview of the Program</h2>
<p>I split my codebase into 3 major classes:</p>
<ul>
  <li>The Chip8 class holds the specifications. Things like the RAM, stack, registers, and program counter are stored in this class. When it is constructed, a font that programs may use is loaded into the RAM. A ROM may also be loaded into RAM if one is passed into the constructor.</li>
  <li>The Interpreter class handles the fetching, decoding, and executing of instructions. When it is constructed, the class takes a Chip8 object, as well as some optional parameters like the compatibility mode, clock speed, and keypad layout. Each cycle, the Fetch method fetches the next instruction, which is decoded and passed on to the Execute method.</li>
  <li>The final class is the Game class, which is where the MonoGame-specific code is located. This class is responsible for actually showing the display on your screen, handling keyboard input, and playing sounds. 
When the program is started, 3 threads are created and started. One is for the MonoGame logic, one for the interpreter, and one for handling the two timers of the CHIP-8.
    <h2 id="chip-8-specifications">CHIP-8 Specifications</h2>
    <p>The CHIP-8 is composed of several components:</p>
  </li>
  <li>The RAM is 4 kilobytes, separated into 4096 addressable bytes. The first 512 bytes of RAM were traditionally reserved, so programs tend to start at the 512th byte. In my program, this space is used to store font sprites.</li>
  <li>The display is 64x32 pixels, where each pixel is either on or off. This is represented in my program as an array of 32 64-bit integers. Each bit represents a single pixel on the screen.</li>
  <li>The program counter is a 16-bit integer which points to the instruction the interpreter is to execute next. Because the RAM is only 4096 bytes, only the first 12 bits are actually used, as that is enough to go from 0 to 4095.</li>
  <li>The index register is also a 16-bit integer which points to a memory address in RAM. This can be used by programs to read from and write to the RAM. Again, only 12 bits are actually needed for this.</li>
  <li>The stack is an array of 16 16-bit integers. This is used when programs run subroutines. When a subroutine is started, the current value of the program counter is pushed to the stack. When it is time to return from a subroutine, the most recent value on the stack is popped and the program counter is set to this value. Being a stack rather than just a single integer allows subroutines to be nested and properly return to the correct place in memory.</li>
  <li>There is both a delay and sound timer. These both function similarly. When the value of either timer is greater than 0, the timer is decremented at a rate of 1 every 60th of a second. The delay timer does nothing on its own, but programs can set or retrieve the current value of the timer in order to do whatever is needed. The sound timer functions by simply playing a tone whenever the value is not 0. So if you set the sound timer to 30, for example, a tone would be played for half of a second.</li>
  <li>There are 16 one-byte variable registers that programs can use to store and retrieve values. These registers can be used to do a number of things depending on the instruction, but the last register (0xF) is typically used as a flag for different instructions to use. For example, the instruction 0x8XY6 bit shifts variable register X by one bit to the right. The flag register is set to the value of the bit that is shifted out.</li>
  <li>Finally, my program has an input register which is used for, you guessed it, input. The CHIP-8 has a keypad of 16 keys, so this register contains a 16-bit integer where each bit represents the state of a key, 1 if it is pressed, and 0 if not.
    <pre><code class="language-csharp">public class Chip8
{
  public Byte[] RAM = new Byte[4096];
  public UInt64[] Display = new UInt64[32];
  public UInt16 ProgramCounter = 512;
  public UInt16 IndexRegister;
  public Stack&lt;UInt16&gt; SubStack = new(16);
  public Byte DelayTimer;
  public Byte SoundTimer;
  public Byte[] VariableRegisters = new Byte[16];
  public UInt16 InputRegister;
</code></pre>
    <h2 id="the-interpreter">The Interpreter</h2>
    <p>The interpreter is responsible for running the program stored in the RAM of the CHIP-8. Every clock cycle, the next instruction in RAM is fetched, decoded, and executed.</p>
    <h3 id="fetch-decode-execute-loop">Fetch, Decode, Execute Loop</h3>
    <p>The fetch, decode, execute loop is the heart of the interpreter. This is what transforms the RAM from a collection of bytes to an actual program. This loop is run every cycle at a rate determined by the clock speed of the interpreter.</p>
    <h4 id="fetch">Fetch</h4>
    <p>When fetching instructions, the interpreter looks at the RAM address stored in the CHIP-8’s program counter. The next two bytes are grabbed and stored as an instruction before incrementing the program counter by 2 so that it is ready to fetch the next instruction. The instruction is stored as a struct which contains information the decoder and executor will need to run the instruction.</p>
  </li>
</ul>

<pre><code class="language-csharp">public Instruction Fetch()
{
	var currentInstruction = Peek();

	_chip8.ProgramCounter += 2;

	return currentInstruction;
}

public Instruction Peek()
{
	return new((UInt16)((_chip8.RAM[_chip8.ProgramCounter] &lt;&lt; 8) + (_chip8.RAM[_chip8.ProgramCounter + 1])));
}
</code></pre>
<h4 id="decode">Decode</h4>
<p>After the instruction is fetched, it is passed on to the Execute method, which handles both decoding and executing. The instruction is broken down into 4 nibbles, each of which can have a different meaning depending on the instruction that is run. The high nibble is always used for specifying an instruction. The second nibble is referred to as X, the third as Y, and the fourth as N. The last two nibbles are referred to as NN, and the last 3 are NNN.</p>
<pre><code class="language-csharp">private void DecodeInstruction(UInt16 fullInstruction)
{
	FullInstruction = fullInstruction;
	HighNibble = (Byte)(fullInstruction &gt;&gt; 12);
	X = (Byte)((fullInstruction &gt;&gt; 8) &amp; 0x000F);
	Y = (Byte)((fullInstruction &gt;&gt; 4) &amp; 0x000F);
	N = (Byte)(fullInstruction &gt;&gt; 0 &amp; 0x000F);
	NN = (Byte)(fullInstruction &amp; 0x00FF);
	NNN = (UInt16)(fullInstruction &amp; 0x0FFF);
}
</code></pre>
<h4 id="execute">Execute</h4>
<p>Now that the instruction has been decoded, it is time to execute it. To select the correct instruction, I used a switch statement which switches based on the value of the high nibble. Within each case, there may be another nested switch to further decode the instruction. For example, if the high nibble is 0, that could refer to either instruction 0x00E0 or 0x00EE, which represent the instructions to clear the display and return from a subroutine, respectively. Depending on the instruction, some of the nibbles may represent arguments for the instruction. Take instruction 0x6XNN for example. This instruction is responsible for storing a value into one of the 16 variable registers. The high nibble, 6, is used purely to decode the instruction. The second nibble, X, is used as an argument to decide which register will have its value changed. The last two nibbles, NN, are used as an argument to tell what value will be put into register X. Let’s say that the instruction is 0x6AFF. This tells us that we will be storing the value 0xFF (255) into the register 0xA (10).</p>
<pre><code class="language-csharp">switch (instruction.HighNibble)
{
	case 0x0:
		switch (instruction.FullInstruction)
		{
			case 0x00E0:    //00E0 Clear Screen
				for (int row = 0; row &lt; _chip8.Display.Length; row++)
				{
					_chip8.Display[row] = 0;
				}
				break;
			case 0x00EE:    //00EE Return from subroutine, Set PC to sub stack and pop
				_chip8.ProgramCounter = _chip8.SubStack.Pop();
				break;
			default:
				Debug.WriteLine("Instruction not found");
				break;
		}
		break;
	case 0x1:   //1NNN Jump Program Counter to NNN
		_chip8.ProgramCounter = (UInt16)(instruction.NNN);
		break;
</code></pre>

<pre><code class="language-csharp">	case 0x6:   //6XNN Set register X to NN
		_chip8.VariableRegisters[instruction.X] = instruction.NN;
		break;
</code></pre>
<h2 id="monogame">MonoGame</h2>
<p>This program is also my first experience using the MonoGame framework. Nothing fancy is being done here, I am simply using MonoGame to handle the display, input, and sound.</p>
<h4 id="display">Display</h4>
<p>In order to draw the display, I get the display buffer from the CHIP-8 and iterate through each bit. The corresponding pixel (technically 10x10 pixels) is turned on or off depending on if the corresponding bit is 1 or 0.</p>
<pre><code class="language-csharp">private void DrawDisplay()
{
	for (int y = 0; y &lt; Chip8.DisplayHeight; y++)
	{
		UInt64 row = _chip8.Display[y];

		for (int x = 0; x &lt; Chip8.DisplayWidth; x++)
		{
			if (((row &gt;&gt; Chip8.DisplayWidth - x) &amp; 1) == 1)
			{
				_spriteBatch.Draw(_pixel, new Rectangle(x * 10, y * 10, 10, 10), Color.Red);
			}
		}
	}
}
</code></pre>
<h4 id="input">Input</h4>
<p>To capture input, the program simply iterates through each key and writes the state of each key directly to the input register. There are two common keypad layouts that the CHIP-8 used, and these can be selected between when constructing the interpreter.</p>
<pre><code class="language-csharp">private void UpdateInput(KeyboardState keyboardState)
{
	_keyboardState = Keyboard.GetState();
	UInt16 keyboardByte = 0;

	for (int i = 0; i &lt; 16; i++)
	{
		int result = _keyboardState.IsKeyDown(_keypad[i]) ? 1 : 0;
		keyboardByte += (UInt16)(result &lt;&lt; _keyLayout[i]);
	}

	_chip8.InputRegister = keyboardByte;
}
</code></pre>
<h4 id="sound">Sound</h4>
<p>For sound, the program looks at the sound timer and starts playback of a looping sound file if the timer is greater than 0, otherwise it will stop playback.</p>
<pre><code class="language-csharp">private void PlaySound()
{
	if (_chip8.SoundTimer &gt; 0)
	{
		_soundEffectInstance.Play();
	}
	else
	{
		_soundEffectInstance.Stop();
	}
}
</code></pre>
<h2 id="running-the-program">Running the Program</h2>
<p>Now that everything is built, it’s time to actually run the program! To start, an instance of the CHIP-8 and interpreter are created. Then, 3 threads are created and run in parallel.</p>
<ul>
  <li>Thread 1 starts up and runs the MonoGame aspect of the project.
    <pre><code class="language-csharp">private static void StartGame()
{
  using var game = new CHIP8Interpreter.Game1(_interpreter, _chip8);
  game.Run();
}
</code></pre>
  </li>
  <li>Thread 2 runs the interpreter. This thread will run a loop which will fetch, decode, and execute one instruction. After running, it will sleep for the appropriate amount of time based on the clock speed of the interpreter.
    <pre><code class="language-csharp">private static void StartChip8(Thread gameThread)
{
  Stopwatch stopwatch = new();
  TimeSpan timeSpan = new();

  while (gameThread.IsAlive)
  {
      Stopwatch deltaTime = new Stopwatch();
      deltaTime.Restart();
      stopwatch.Restart();
      _interpreter.Execute(_interpreter.Fetch());

      stopwatch.Stop();
      timeSpan = stopwatch.Elapsed;

      Thread.Sleep(TimeSpan.FromMilliseconds(1000f / (_interpreter.ClockSpeedHz - timeSpan.Milliseconds)));
      deltaTime.Stop();
  }
}
</code></pre>
  </li>
  <li>Thread 3 is responsible for counting down the timers. Since the timers count down 60 times per second, this thread simply loops at that rate and decrements the timers whenever they are greater than 0.
    <pre><code class="language-csharp">private static void StartTimers(Thread gameThread)
{
  while (gameThread.IsAlive)
  {
      if (_chip8.DelayTimer &gt; 0)
      {
          _chip8.DelayTimer--;
      }

      if (_chip8.SoundTimer &gt; 0)
      {
          _chip8.SoundTimer--;
      }

      Thread.Sleep(TimeSpan.FromMilliseconds(1000f / 60f));
  }
}
</code></pre>
    <p>If the game window is closed, thread 1 will exit, but the second and third thread will continue to run indefinitely. To solve this, rather than using a <code>while(true)</code> loop, the second and third thread are passed a reference to the initial thread, and they are looped using <code>while(gameThread.IsAlive)</code>, allowing the threads to exit as soon as thread 1 (gameThread) exits.</p>
    <pre><code class="language-csharp">static void Main(string[] args)
{
  Thread gameThread = new Thread(() =&gt; StartGame());
  gameThread.Start();

  Thread chip8Thread = new Thread(() =&gt; StartChip8(gameThread));
  chip8Thread.Start();

  Thread timerThread = new Thread(() =&gt; StartTimers(gameThread));
  timerThread.Start();
}
</code></pre>
  </li>
</ul>

<h1 id="what-i-learned">What I Learned</h1>
<p>Creating the CHIP-8 interpreter taught me a lot. My previous experience with C# and .NET has been mainly focused in Unity 3D, so this allowed me to work outside of my comfort zone and learn more about C#. I got to work with threads at a basic level, which is something I had never really used before. Granted, the usage of threads was at a pretty basic level, as I didn’t have to worry about things like race conditions, but you have to start somewhere! I also got to use many of the bitwise operations that C# provides, something I also never had to use in the past. I used these all over the project. The decoder made heavy use of bit shifts and bit masking in order to split the full instruction into the different nibbles that were needed to run the instructions. The draw instruction 0xDXYN also made heavy use of both bit shifts and bit masking.</p>
<pre><code class="language-csharp">case 0xD:   //DXYN Draw N pixels tall sprite from Index Register I's memory location at vX and vY
	Byte x = (Byte)(_chip8.VariableRegisters[instruction.X] % Chip8.DisplayWidth);
	Byte y = (Byte)(_chip8.VariableRegisters[instruction.Y] % Chip8.DisplayHeight);
	_chip8.VariableRegisters[0xF] = 0;

	for (int i = 0; i &lt; instruction.N; i++)
	{
		if (y + i &gt;= Chip8.DisplayHeight - 1)       //clip sprite vertically
		{
			break;
		}

		Byte row = _chip8.RAM[_chip8.IndexRegister + i];

		for (int e = 0; e &lt; 8; e++)
		{
			if (x + e &gt; Chip8.DisplayWidth - 1)    //clip sprite horizontally
			{
				break;
			}
			if (((row &gt;&gt; (7 - e)) &amp; 1) == 0)    //is sprite pixel 0?
			{
				continue;
			}
			//sprite pixel is 1
			if (((_chip8.Display[y + i] &gt;&gt; (Chip8.DisplayWidth - (x + e))) &amp; 0x1) == 0)    //is screen pixel 0?
			{
				_chip8.Display[y + i] += (UInt64)(1) &lt;&lt; (Chip8.DisplayWidth - x - e);
			}
			else
			{
				_chip8.Display[y + i] -= (UInt64)(1) &lt;&lt; (Chip8.DisplayWidth - x - e);
				_chip8.VariableRegisters[0xF] = 1;
			}
		}
	}
	break;
</code></pre>
<p>This project gave me a lot of insights into the inner workings of a computer system from a very low level. The interpreter functions like a basic CPU, taking binary data that is not human readable and transforming it into working programs like Pong or Lunar Lander. I had a basic understanding of how computers work under the hood, but this project allowed me to actually apply these concepts, exposing and filling many gaps in my knowledge along the way.
To get into specifics, some parts I found particularly interesting were the subroutine stack, the decoder, and the executor.</p>
<h2 id="subroutine-stack">Subroutine Stack</h2>
<p>The subroutine stack was an interesting look into a practical application of data structure as well as program flow control. Stacks function by allowing you to either push new data onto the top of the stack or pop data off of the top. The CHIP-8 uses the stack by pushing the location of the program counter to the stack before jumping to a new subroutine. Then, when it is time to return, the stack is popped and the program counter is set to that location. This allows the program to have nested subroutines and be able to return from each subroutine to the correct location. It’s an elegant solution to what is a pretty complex problem.</p>
<pre><code class="language-csharp">case 0x2:   //2NNN Subroutine, push PC to sub stack, jump PC to NNN
	_chip8.SubStack.Push(_chip8.ProgramCounter);
	_chip8.ProgramCounter = instruction.NNN;
	break;
</code></pre>

<pre><code class="language-csharp">case 0x00EE:    //00EE Return from subroutine, Set PC to sub stack and pop
	_chip8.ProgramCounter = _chip8.SubStack.Pop();
	break;
</code></pre>
<h2 id="decoder">Decoder</h2>
<p>I found the decoder to be an interesting look into how computers take the seemingly meaningless binary data and convert it into specific instructions. It is really cool to see 16 bits of data decoded into 34 different instructions along with many of the instructions being able to pass along arguments within those bits.</p>
<h2 id="executor">Executor</h2>
<p>The executor gave me a great insight into the functioning of computers. It’s truly amazing how programs like Pong, Space Invaders, and Lunar Lander are able to be created using only 34 basic instructions. It’s a great example of how relatively simple rules can give rise to very complex results. Every instruction comes down to just reading and/or modifying data in one way or another. The most complex instruction was 0xDXYN, which is responsible for drawing sprites to the screen. But when you get down to the implementation, even that is just reading and modifying data.</p>

<p><img src="/images/25-08/16/CHIP8-Pong.png" alt="CHIP-8 Pong" /></p>

<h1 id="challenges-i-overcame">Challenges I Overcame</h1>
<p>This project presented a lot of interesting challenges to work through. By far the most complex part was the execution step of the interpreter. A lot of the instructions had very specific quirks in the way that they work that had to be properly implemented. Some of the more complex instructions like 0xDXYN (draw sprites to the screen) took a lot of trial and error to get working right. Thankfully, the <a href="https://github.com/Timendus/chip8-test-suite?tab=readme-ov-file" target="_blank">test ROMs from Tim Franssen</a> as well as the excellent <a href="https://tobiasvl.github.io/blog/write-a-chip-8-emulator/" target="_blank">guide from Tobias Langhoff</a> proved invaluable in getting this project up and running.</p>
<h1 id="conclusion">Conclusion</h1>
<p>The CHIP-8 Interpreter was a fun project that taught me a lot about the inner workings of computers. The system is just complex enough to be challenging and teach a lot of important concepts, without being so complex as to get bogged down in the implementation. You can easily have some of the basic test roms running within a day or two, which is a great motivator to complete the project. I highly recommend this project to anyone interested in learning more about the inner workings of computers.</p>]]></content><author><name></name></author><category term="Other" /><summary type="html"><![CDATA[My journey building a CHIP-8 interpreter using C# .NET and MonoGame]]></summary></entry></feed>