Chip 8 Emulator

In 2017, I started working on a Chip 8 emulator. After a couple of weeks, I shelved it, and last year I picked it up again to finish it. Chip 8 is a specification for an 8 bit console. It was first implemented on the COSMAC VIP, and it's been ported to a huge variety of platforms ever since. No actual Chip 8 console exists; it has always been implemented in software. Chip 8 is considered the "Hello, World" of emulators; most people start off on emulators by implementing a Chip 8. I implemented mine in Rust because I wanted to try using it more. I think this is the kind of project where Rust really shines; low-level and multithreaded (no async!).

Resources

There are a wide variety of resources available to help with the implementation. However, not all agree on the exact semantics of instructions, and some are incorrect:

Chip 8 Quirks

As Chip 8 got ported to various architectures over time, implementations started diverging a little. Some of them implemented opcodes differently, and they have different results than what the original specification had. These differences are called quirks. Games that were developed for one implementation may not work correctly if another implementation has different quirks. For example, here's a quirk that I ran into that caused bugs in Space Invaders. The opcodes for bitwise shift-left and shift-right are 8xyE, and 8xy6, respectively. The xy in the middle are register numbers. In some implementations, y is ignored and the shift is performed on the value in register x. However, there's a quirk where register x is set to the value in register y, and then the shift is performed. There are a few other quirks that I implemented in my emulator (mostly ones that weren't too difficult). Here's a full list of Chip 8 quirks.

Timers

I had a bit of trouble implementing the timers and getting it to work with the rest of the system. Specifically, with this instruction:

Fx0A - LD Vx, K
Wait for a key press, store the value of the key in Vx.

All execution stops until a key is pressed, then the value of that key is stored in Vx.

Initially, I thought all "execution stops" means that even the timers stop. However, the timers need to continue counting down. If they don't, it results in some very strange bugs.

Timer Implementation

Some people, when confronted with a problem, think, “I know, I’ll use threads,” and then two they hav erpoblesms.

-Ned Batchelder

To implement the timers (input and audio/delay), I decided to keep them on their own dedicated threads and use channels to send ticks to the main thread. This worked surprisingly well. Rust makes this extremely easy to setup:

    thread::spawn(move || loop {
        thread::sleep(timer_sleep);
        timer_tx.send(TimerOperation::Decrement(1)).unwrap();
    });

Then in the emulator loop to decrement the timer:

        // If there's a timer message, update the timers
        while let Ok(timer_operation) = self.timer_rx.try_recv() {
            match timer_operation {
                TimerOperation::Decrement(val) => {
                    self.sound_timer = self.sound_timer.saturating_sub(val);
                    self.delay_timer = self.delay_timer.saturating_sub(val);
                }
            }
        }

This allowed me to detach the frequency of the timers from the emulator frequency, and adjust both of them individually. It may have been possible to solve this using math and sleep() but this was a much cleaner solution for me. My initial idea was to use locks, but then I thought of the quote above, and decided to try channels. Turns out, channels are great! The timers worked as intended right away.

I did the same for input scanning, but on another thread.

Testing

I tried to test every single part of the emulator that I could. I highly recommend doing this, especially for opcodes. Having a good test suite enabled me to come back to this project 7 years after I started it. I also tried to make it modular with traits for every major component just in case I would need to mock them (using mockito), but I ended up not needing that. However, I think it was still worth doing that because it decoupled the components from one another.

Debugging

Debugging the emulator is pretty tedious. In order to make it easier, I added an option to print each opcode as it's being executed, and to dump the graphics (a bitmap) after each draw opcode. These two options - coupled with a debugger - allowed me to debug all of the bugs that I ran into. I didn't add single-stepping to the emulator but that would probably be the next step if I wanted to make it more friendly for Chip 8 developers. It would probably help debug the emulator as well but the other two were the lowest hanging fruit while I was investigating problems.