CHIP-8

Fish 'n Chips 'n Finishin' CHIP-8

Nov 04, 2018

I haven’t had fish ‘n chips in a while but that’s beside the point. Way back, I started a CHIP-8 emulator project in Java and then dropped it soon after I realized my lack of experience with low-level programming. However, since delving into NESASM, I decided this would be prime time to pick CHIP-8 back up again. I also chose to make it in C++ this time since, prior, I had nearly zero experience with low level programming languages (aside from 6502 assembly), and I also wanted to get some more experience with SDL.

Overview

There are many good guides out there detailing the CHIP-8 instruction set, but, roughly speaking, the CHIP-8 has 4096 bytes of memory, 16 general purpose VX registers, an I register for holding memory locations, two registers for delay and sound, and a call stack that stores address locations for subroutines.

Additionally, the emulator had to implement a program counter (PC) as well as arrays for holding graphics data and keyboard presses.

The emulator naturally has to read in a rom and then correctly execute the proper opcodes. This one uses SDL for displaying graphics and taking in input.

Process and Design

Much of the project was kickstarted thanks in large part to many guides detailing the process of building a CHIP-8 emulator. The CHIP-8 CPU itself simply initializes and indefinitely runs cycles. Each iteration gets and executes the opcode where PC is currently at and then moves PC 2 bytes forward. The reason for the 2 byte step is that the opcodes are 16 bits and thus require 2 bytes for each one.

In code, the opcodes were stiched together with bitwise operations and then sent through a chain of if statements that would determine the command, parse the arguments, and execute the proper functionality.

Afterwards, the process of building the emulator was relatively straight-forward as it was largely translating the functionality described online into code. Perhaps the most troublesome opcodes were the draw command and subroutine call.

Roughly, the draw command takes in a coordinate and height as arguments and, starting from the address in the I register, draws the bit representation of the data in memory as a row by XOR-ing the bits to the screen and then continues to move down in both memory and rows until it has drawn to the proper height. While decoding drawing the bits into the graphics array was enough of a pain as it was, the draw command also needs to take into account sprites that overflow off the edge of the screen. The standard way of dealing with overflow is to wrap-around both vertically and horizontally, but some emulators (and roms) don’t account for this. Moreover, the CHIP-8 also detects collision by setting VF if a draw command changes a pixel from 1 to 0. For the longest time, many roms were broken simply because the emulator was doing collision detection wrong as it had been both only detecting a collision on the last pixel drawn as well as detecting the wrong condition for collision since I had assumed I had already XOR-ed the two values.

Meanwhile, the subroutine call was a hassle, not because it was difficult per se, but rather due to my lack of experience with C++, the difficulty of tracing multiple subroutines, and even discovering that the subroutine call was buggy to begin with. In general, subroutines work by taking the current address location pointed to by PC, pushing it to the call stack, and jumping to another memory location. When a subroutine return opcode is called, the PC is set to the address at the top of the stack and the address is itself popped off. Early on, I would get memory exception errors for certain roms, not knowing where they came from. Eventually, on a whim, I tried to pop from an empty stack, and, lo and behold, there was that error. Turns out bugs in other parts of the emulator were messing up program flow and would sometimes call a subroutine return without having called a subroutine before. However, even within a subroutine, it seemed like returning would not jump back to the pushed address. This time, the problem was a conceptual one as I had been pushing the address locations into the stack as bytes and not shorts, resulting in the first byte of the address being cut off.

While these two were the biggest hurdles, some other run-ins included:

  • Problems with casting to unsigned char
  • Making SDL draw faster
  • Confusing indices with stored values in reg load and dump
  • Skipping instructions when jumping to address locations due to incrementing PC

In retrospect, the SDL drawing component was not as bad as I had originally anticipated (probably since I took most of it from online lol). However, as is the opinion of many I believe, sound in SDL is horrific. I had planned to use the sound tutorial in Lazy Foo but after finding out that I had to get more libraries linked properly, I promptly gave up and took code to run WAV files from elsewhere.

I had also considered implementing the SCHIP-48 instruction set from the outset but was not sure how easily it could be done. For one, documentation on SCHIP is more sparse than CHIP-8 which made it slightly more difficult. In the end, however, SCHIP-48 was only a hop, a skip, and some shady monkey-patching away, and I managed to get the few new opcodes written relatively painlessly while preserving compatibility with CHIP-8 roms.

After Thoughts

In reality, this project is not the best-written CHIP-8 emulator out there (especially considering that this is a common beginner’s project…). Most difficulties were due to being unfamiliar with SDL and C++ going in as well as an overall lack of understanding with Visual Studio. However, I am also quite proud of this project as I had long dreamed of making an emulator since my middle school days looking up incomplete NES emulator guides. I had given up on this project 2 years ago when I started it in Java, and I guess it means a lot now that I managed to pick it up and actually make it work.

Plus, I got to play a bunch of games while making it!

Emulator running PONG

Acknowledgements

This project was a hurdle for me and I got a lot of help out of these resources (also on project readme):