Andrew Huynh
Thoughts 
and ramblings.
Email me!
Follow me on Twitter!
Check me out on LinkedIn.

Rust & Wasm CHIP-8 Emulator

2018 November 11 - San Francisco/Lisbon | 1835 words

#rustlang #emudev

What started as an attempt to demonstrate how interrupts and grayscale rendering works on the TI series graphing calculators turned into a full-blown attempt at writing an emulator that would be runnable in a modern browser using a combination of Rust πŸ¦€ and WebAssembly πŸ•Έ. The idea came to me while walking through the Rust + WebAssembly tutorial, where I realized that many of the same abstractions could apply to an emulated system.

I should note before you get any further that this is not a full guide to writing an emulator in Rust. I just wanted to document some interesting implementation details during my implementation, but this should (hopefully) act as a short β€œGetting Started” guide to point you in the right direction of your own emulator or if you just wanted to see what a Rust + WebAssembly project looks like.

Let's Play

I've embedded the emulator below so that you can try it out before reading any further. The keyboard for the CHIP-8 is represented by the 4x4 grid below.

Normal Keyboard      CHIP8 Keyboard
| 1 | 2 | 3 | 4 | -> | 1 | 2 | 3 | C |
| Q | W | E | R | -> | 4 | 5 | 6 | D |
| A | S | D | F | -> | 7 | 8 | 9 | E |
| Z | X | C | V | -> | A | 0 | B | F |

Just select one of the ROMs from the dropdown menu, hit the play button and it'll start the emulator.

CHIP-8: Hello Emulator

To dip my toes into emulator development, I decided to start small. CHIP-8 is often considered the "Hello World" equivalent of emulator projects due to its simplicity, with only 35 instructions, simple keyboard input, and simple sound management.

The CHIP-8 was never actually a real chip/system and was developed to be more of a virtual machine that could be run on different microcomputers at the time. It nevertheless captures all the abstractions required for other emulator projects and offers a friendly dip into the emulator development world. Having existing emulators to compare with was also tremendously helpful to understand certain subtleties of the implementation.

Additionally, there are also tons of great resources that fully describe the instruction set and even walk you through an implementation:

Setup

If you've followed the Rust-Wasm Book tutorial already, the following commands will look familiar. I started the project by generating the project folder with everything necessary to get started:

# Generates the rust project folder
> cargo generate --git https://github.com/rustwasm/wasm-pack-template emu-project
# Go into the project directory to initialize the javascript project
> cd emu-project
> npm init wasm-app www

Keeping with the statically typed language theme, I wanted to use TypeScript for as much of the browser code as possible, which required a couple tweaks to the generated project:

As for the generated javascript files output by wasm-pack, I couldn't quite figure out how to also convert bootstrap.js and index.js files to TypeScript so these are kept the same and will import the rest of the application.

What do we need to emulate?

Outside of the instructions, we'll also need to emulate essential parts of the CHIP-8 system that are required to run a program.

Most if not all of these can easily be represented by arrays of the the aforementioned types, all of which can be unsigned integers.

Execution Loop

At a high level, for successful emulation of the CHIP-8 system we want to correctly imitate every cycle of the CPU, keep track of any changes to the display/memory/registers, and finally handle any keyboard input.

If we had a function that would handle each tick of the CPU, it would look something very similar to the following:

1fn tick(&mut self) {
2 // 1. Find the opcode pointed to by the `PC` register.
3 let opcode = self.fetch()
4 // 2. Fetch and decode the opcode.
5 // 3. Execute the opcode & update the `PC`.
6 self.execute(opcode);
7}

This simple execution loop will form the basis for the rest of emulation code. Most, if not all, of our logic will happen inside the execute function, where each opcode will be decoded and applied to the registers/memory/display.

What happens during fetch()?

At a very high level, fetch returns a 16-bit instruction pointed to by the PC register and increments the PC. The only β€œgotcha” here would how instructions are stored in memory, most-significant byte first. Taking this into account, the code to grab the opcode in the correct byte order would like the following:

1let opcode = u16::from(mem[pc]) << 8 | u16::from(mem[pc + 1]);

What happens during execute()?

Like mentioned before, execute() is where the majority of the logic for the emulator will reside. In the case of my implementation, I break the opcodes into different prefixes (0x1000, 0x2000, etc) and implemented the logic for each corresponding prefix. For example, lets take the simple 0x00E0 opcode that is used to completely clear the screen:

1// First, we grab the first bytes of the opcode as a prefix
2let prefix = opcode & 0xF000;
3// Grab the lower two bytes.
4let lower = (opcode & 0x00FF) as u8;
5match prefix {
6 // Match and handle 0x0xxx opcodes
7 0x0000 => {
8 match lower {
9 // Clear display.
10 0xE0 => {
11 for idx in 0..DISPLAY_SIZE {
12 self.display[idx] = 0;
13 }
14 }
15 }
16 },
17 // handle 0x1xxx opcodes
18 0x1000 => {},
19 // etc.
20 ...,
21 _ => log!("Unknown opcode {:#X}", opcode)
22}

The opcode prefixes can be grabbed using a little bit-slinging magic that you see at the top of the code example: opcode & 0xF000. This bitwise AND operation will only return the very first byte of the opcode. This made it easy to write tests for each opcode, since instructions in the same prefix tend to be related to each other.

Rendering the Display

While most of the opcodes implemented in execute() tend to be only a handful of lines, the most complex of the opcodes is used to draw sprites to the screen. This single instruction handles reading a sprite stored in memory, xor-ing it to display memory. Additionally, a flag is also set if any pixels are erased, a feature is that is often used in programs for collision detection.

The 64x32 pixel display has a coordinate system that starts at the top left and extends downwards:


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚(0,0)         (63,0)β”‚
β”‚                    β”‚
β”‚(0,3)        (63,31)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Sprites that cross horizontal/vertical boundaries are wrapped to the other side. For example, if we have a sprite that starts at (63,0), the pixel at (63,0) will be set on and then wrapped around to (0,0), (1,0), etc.

Handling Input

The CHIP-8 system uses a hex keypad, which is a little odd but can be mapped easily to any 4x4 set of keys on modern day keyboards. All that needs to be tracked is whether key is pressed and whether the key was the last pressed key. This is accomplished by adding an event listener for the keyup and keydown events, where the key press is sent to the emulator for handling.

Here is an example of the handleKeyPress code for both the typescript and rust side. The typescript side needs a key map thats not shown to map the pressed key code to the corresponding emulated key, discarding all other key presses.

public handleKeyPress(ev: KeyboardEvent) {
    if (ev.keyCode in KEY_MAP) {
        this.engine.key_press(KEY_MAP[ev.keyCode]);
    }
}

And for the rust side, we keep track of the currently pressed key as well as setting the key to pressed until we receive a release event for that key.

1pub fn key_press(&mut self, key: Key) {
2 self.current_key = Some(key);
3 self.keys[key as usize] = true;
4}

Up Next

And that's pretty much it! I could've gone into far much depth into things like the opcodes or perhaps the display rendering, but I leave that up as an exercise for the reader. All the code for this emulator will be publicly available along with the research and ROMs used during its development.

Note that at the time of this writing there are still currently some bugs with the more complex ROMs and potentially some speed improvements that could be done. I plan on fixing up the last remaining bugs w/ the emulator and implementing the basic sound handler.

The next emulator added to the mix will probably be a Z80 or NES emulator, a much larger emulation implementation but should be able to reuse many of the components created for this project.

You can follow progress on my emulator(s) development here.