Writing an emulator is a good way to get more acquainted both with the more technical details of programming and the language you decide to write said emulator in. Zig is a young up-and-coming programming language that already makes a mighty fine alternative to various other systems-level programming languages such as C, Go, Rust, Etc. And so what better way to get acquainted with this new contender than to write an emulator?

Emulators are at their heart fairly basic things, but for more complex systems they can get rather, well, complex. For this reason we’ll start of this emulation series by emulating the CHIP-8 system using Zig and SDL2. CHIP-8 itself is a specification for a simple virtual machine for 2D games designed in the 1970s. It’s virtual because CHIP-8 was never implemented in hardware, only software (which I suppose makes this more of an interpreter instead of an emulator, but that’s neither here nor there).

CHIP-8 was designed so that different implementations of the virtual machine made on different hardware could all still run the same games, regardless of the different environments. That means that by the end of this guide you’ll be able to run games made all the way back in the 1970s, neat eh? All in all it’s rather primitive — especially by modern standards — but still a good place to get started for the topic of emulation.

This guide will have everything you’ll need to get a CHIP-8 emulator up and running, but keep in mind that it’s light on the details and explanations. The project shouldn’t take much longer than a day if you hack away at the whole thing.


Table of Contents


Requirements

  • Some programming experience
  • Basic computer science knowledge

Keep in mind that this guide is not a beginner’s programmer tutorial or an SDL2 tutorial. It is merely an introduction to the Zig programming language via implementing the CHIP-8 specification.

CHIP-8 Specifications

CHIP-8 has the following components:

  • Memory: 4 kilobytes of RAM
  • Display: 64 x 32 pixel monochrome display
  • Program Counter (PC): points to the current instruction in memory
  • Index (I): 16-bit index register used to point to locations in memory
  • Stack: A stack for 16-bit addresses, which is used to call functions and return from them
  • Delay Timer: An 8-bit timer which is decremented at a rate of 60 Hz until it reaches 0
  • Sound Timer: An 8-bit timer which functions like the delay timer, but which also gives off a beeping sound as long as it’s not 0
  • Registers: 16 8-bit general-purpose variable registers numbered 0 through F in hexadecimal, called V0 through VF

Note that the VF register is also called the flag register, and many instructions use it to signify various things, such as overflow in an addition operation.

Setup

If you haven’t already, go ahead and install Zig and create a new project. We’ll be using version 0.11.0 in this project.

To start all we need to do is link SDL2 to the project. Many languages make linking or wrapping C libraries an absolute pain but Zig makes it an absolute breeze, a joy even. It has built-in support for importing C header files (which I’ve found don’t even need to be modified at all unless you’re using obscure C features) and the build system itself uses Zig for logic, meaning we don’t have to fiddle with the likes of Make or CMake or Ninja or any other pain in the ass build system to get SDL2 up and running in our project.

Matter of fact if you’re on Linux you only need to do two things to get SDL2 linked to our project. First go to your build.zig file and add this snippet of code somewhere.

exe.linkSystemLibrary("SDL2");
exe.linkLibC();Code language: CSS (css)

And then create a new file in the project source called c.zig and insert this code:

// usingnamespace marks that everything in the cImport
// is brought into the namespace of the file and
// pub marks that everything brought in by our
// usingnamespace in public
pub usingnamespace @cImport({
  @cInclude("SDL2/SDL.h");
});Code language: JavaScript (javascript)

And that’s literally it. Well, you’ll also have to install your system-specific SDL2 dev packages, but besides that we’re done, SDL2 is linked to our project.

If you’re on Windows, uhhhhh, good luck figure it out yourself. ¯\_(ツ)_/¯

(There’s an SDL2 zig package on GitHub that should’ve made Windows setup easy, but I couldn’t get it working. It should still be pretty easy to set up on Windows though. All you should have to do is download the SDL2 dev DLLs and headers, add them somewhere in your project folder, and point Zig towards them. Poke around the Zig std docs to figure out how.)

Basic Display

Before we get started with the actual emulator we’re gonna need a display to draw things to. Obviously SDL is what we’ll be using for the display, as well as input and audio. To get started create a new source file called display.zig and insert this boilerplate code:


const std = @import("std");  // We'll be using this later
const c = @import("c.zig");

// pub marks the struct as public
pub const Display = struct {
  const Self = @This();
};Code language: JavaScript (javascript)

Zig is much like C in that everything is rather simple. There are no classes, there are no objects, no templates, no borrow checker or garbage collector, nothing of that nature. Pretty much everything is either a primitive, a function, or a struct, with some nice syntactic sugar that makes things easier to read and write.

Along the way we’ll see some code that looks somewhat object oriented, but keep in mind that’s just syntactic sugar. There is no inheritance or polymorphism (at least of the OOP kind) in Zig. This simplicity of things is what makes Zig so C-like and much less of a nightmare than either C++ or Rust.

Anyway, on to creating the actual display. Modify the display.zig file to look like this:

const std = @import("std");
const c = @import("c.zig");

pub const Display = struct {
  const Self = @This();

  window: *c.SDL_Window,
  open: bool,

  // These functions are that nice
  // syntactic sugar I was talking about
  pub fn create(
    title: [*]const u8,        // The string type in Zig
    width: i32,height: i32
  ) !Self {

  }

  pub fn free(self: *Self) void {

  }
};
Code language: PHP (php)

You can see that we added some fields for the SDL window and for keeping track of if the display is still open. Below that are some helper functions that will create and free our display struct.

You’ll notice an exclamation next to the return type of the create function. That signifies that the function is capable of returning an error, and that the caller of the function must handle it should it do so. We’ll see how that’s done later, for now let’s actually get these functions working

The entirety of the create function for now will look like this:

// Initialize SDL2
if(c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO) != 0) {
  return error.SDLInitializationFailed;
}

// Create SDL2 window
var window = c.SDL_CreateWindow(
  title,
  c.SDL_WINDOWPOS_UNDEFINED,c.SDL_WINDOWPOS_UNDEFINED,
  width,height,
  c.SDL_WINDOW_SHOWN,
) orelse {
  c.SDL_Quit();
  return error.SDLWindowCreationFailed;
};

// Return Self type struct
return Self {
  .window = window,
  .open = true,
};Code language: JavaScript (javascript)

Pretty basic, all we’re doing is initializing SDL and creating a window. But you can see a few interesting things here. First is how easy Zig makes it to use C libraries. When we imported the SDL header file the Zig compiler automatically wrapped all of the structs, functions, and macros for us, ready to use.

Second is how Zig handles null values. Unless specifically specified, pointers in Zig can’t have null values, and if you’re dealing with a function that can potentially return a null value but your variable isn’t marked as being nullable you’ll have to handle that somehow. We do so by adding an “orelse” statement after our function which will be automatically called in the case SDL_CreateWindow fails for some reason and returns null.

Third is the error handling in Zig. At a glance it seems similar to the traditional try/catch kind of error handling but in reality it’s more akin to Rust’s style of error handling with enums and whatnot. That’s all just hidden behind syntactic sugar and compiler trickery however.

Moving on to the free function. We initialized SDL and created a window, so all we have to do is destroy the window and cleanup SDL.

c.SDL_DestroyWindow(self.window);
c.SDL_Quit();Code language: CSS (css)

Having just a create and free function isn’t enough though, I mean we’re gonna actually want to use our window, which with SDL involves setting up a loop and checking for input. To do that we’ll add one more function.

pub fn input(self: *Self) void {
  var event: c.SDL_Event = undefined;
  while(c.SDL_PollEvent(&event) != 0) {
    switch(event.@"type") {
      c.SDL_QUIT => {
        self.open = false;
      },
      else => {},
    }
  }
}Code language: PHP (php)

The input function loops through and handles SDL’s queue of events. Currently the only event we’re handling is the SDL_QUIT event, but we’ll add a couple more later. But for now we have enough to get a display working so head on over to the main.zig file, clear it out, and add this code:

const std = @import("std");  // We'll be using this later
const Display = @import("display.zig").Display;

pub fn main() !void {
  var display = try Display.create("CHIP-8", 800,400);
  defer display.free();

  // Display loop
  while(display.open) {
    display.input();
  }
}Code language: JavaScript (javascript)

The code should be mostly self explanatory. You should notice however the “try” and “defer” keywords. The try keyword tries to call a function that’s capable of throwing an error and passes the error on to the next call stack if it happens. The defer keyword is another little fun feature of Zig that allows us to defer a function call to the end of a code block. It makes freeing resources in instances like these a bit neater. But anyway with this we should have a working display upon running the program.

Drawing to the Display

We have a display, but it’s not really useful if we can’t draw anything to it. Luckily for us SDL has a built-in rendering API, but we’re gonna have to set up some other stuff before getting to that first. What we’re gonna do is instead of drawing to the screen directly and having to fumble around with SDL’s stuff to get things working exactly right we’re gonna instead create an intermediary Bitmap struct which will be what our CHIP-8 emulator actually draws to and from and from which our SDL display just copies the pixel data from. Create a new source file called bitmap.zig and add this boilerplate:

const std = @import("std");

pub const Bitmap = struct {
  const Self = @This();

  allocator: std.mem.Allocator,
  width: u8,
  height: u8,
  pixels: []u1,

  // Create Bitmap
  pub fn create(allocator: std.mem.Allocator, width: u8,height: u8) !Self {

  }

  // Free Bitmap
  pub fn free(self: *Self) void {

  }

  // Clear Bitmap to specified value
  pub fn clear(self: *Self, value: u1) void {

  }

  // Set pixel value at x,y coordinate
  pub fn setPixel(self: *Self, x: u8,y: u8) bool {

  }

  // Get pixel value at x,y coordinate
  pub fn getPixel(self: *Self, x: u8,y: u8) u1 {

  }
};
Code language: PHP (php)

Two things stand out here, first is std.mem.Allocator. Allocating memory in Zig is interesting, in other languages such as C you can just call malloc and get some memory but in Zig you have to use intermediary allocators provided to you by the standard library. The reason for this is so that you can choose precisely how allocations are done in functions in the standard library. It can be a little annoying to have to move allocator structs around all the time but the flexibility it provides can be life-saving.

The other thing which stands out is the setPixel function. The pixel display in CHIP-8 is a bit wonky. There are no instructions to directly set a pixel value in CHIP-8, only instructions to XOR the value at a particular pixel. Why it was built like this I don’t know — probably has something to do with it being made in the 1970s — but it’s how it works so we’re rolling with it. The setPixel function will XOR the value in the pixel array at the specified coordinates, and return true if the value was set to 0 (if a pixel was erased essentially) and return false otherwise.

The entirety of the Bitmap struct’s methods will look like this:

// Create Bitmap
pub fn create(allocator: std.mem.Allocator, width: u8,height: u8) !Self {
  // Allocate pixel array
  var pixels = try allocator.alloc(u1, @as(u16, width) * @as(u16, height));
  @memset(pixels, 0);

  return Self {
    // We save the allocator so we can free our allocated data with it later
    .allocator = allocator,
    .width = width,
    .height = height,
    .pixels = pixels,
  };
}

// Free Bitmap
pub fn free(self: *Self) void {
  // Free allocated data with allocator
  self.allocator.free(self.pixels);
}

// Clear Bitmap to specified value
pub fn clear(self: *Self, value: u1) void {
  @memset(self.pixels, value);
}

// Set pixel value at x,y coordinate
pub fn setPixel(self: *Self, x: u8,y: u8) bool {
  // Return if x or y is invalid
  if(x >= self.width or y >= self.height) return false;

  var index: u16 = @as(u16, x) + @as(u16, y) * @as(u16, self.width);
  self.pixels[index] ^= 1;
  return (self.pixels[index] == 0);
}

// Get pixel value at x,y coordinate
pub fn getPixel(self: *Self, x: u8,y: u8) u1 {
  // Return if x or y is invalid
  if(x >= self.width or y >= self.height) return 0;

  var index: u16 = @as(u16, x) + @as(u16, y) * @as(u16, self.width);
  return self.pixels[index];
}Code language: PHP (php)

The only real notable thing is all of the @as things everywhere. Zig’s typecasting is a bit wonky and complicated but for now just know that all it’s doing is typecasting the variables to the correct integer types for the multiplication of large numbers.

In the main.zig file add this code to the beginning of the main function:

// General purpose memory allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer {
  const leaked = gpa.deinit();
  if(leaked == .leak) {
    @panic("MEMORY LEAK");
  }
}

var bitmap = try Bitmap.create(allocator, 64,32);
defer bitmap.free();Code language: JavaScript (javascript)

One of the neat things about Zig’s allocators is that we can use them to automatically check for memory leaks in our program.

Oh, of course don’t forget to import the Bitmap struct at the beginning of the file

const Bitmap = @import("bitmap.zig").Bitmap;Code language: JavaScript (javascript)

So we have a bitmap of pixels, but we still need to actually draw it. Go back to the display.zig file and add these fields to the struct:

renderer: *c.SDL_Renderer,
framebuffer: *c.SDL_Texture,
framebuffer_width: u8,
framebuffer_height: u8,Code language: HTTP (http)

Modify the create function declaration:

pub fn create(
  title: [*]const u8,
  width: i32,height: i32,
  framebuffer_width: u8,framebuffer_height: u8 // New
) !Self {Code language: PHP (php)

Then in the create function after we create our window add this code:

// Create SDL2 renderer
var renderer = c.SDL_CreateRenderer(window, -1, c.SDL_RENDERER_ACCELERATED) orelse {
  c.SDL_DestroyWindow(window);
  c.SDL_Quit();
  return error.SDLRendererCreationFailed;
};

// Create display framebuffer
var framebuffer = c.SDL_CreateTexture(
  renderer,
  c.SDL_PIXELFORMAT_RGBA8888,
  c.SDL_TEXTUREACCESS_STREAMING,
  framebuffer_width,framebuffer_height
) orelse {
  c.SDL_DestroyRenderer(renderer);
  c.SDL_DestroyWindow(window);
  c.SDL_Quit();
  return error.SDLTextureNull;
};Code language: JavaScript (javascript)

And at the end at our return don’t forget to add the new fields to the created struct:

// Return Self type struct
return Self {
  .window = window,
  .renderer = renderer,
  .framebuffer = framebuffer,
  .framebuffer_width = framebuffer_width,
  .framebuffer_height = framebuffer_height,
  .open = true,
};Code language: JavaScript (javascript)

All we’ve done here is add an SDL renderer and texture to our struct. We’re going to copy the pixel data from our bitmap to our SDL texture and then render that texture to the display using the SDL renderer.

In display.zig create a new function called draw:

pub fn draw(self: *Self, bitmap: *Bitmap) void {
  if(bitmap.width != self.framebuffer_width) return;
  if(bitmap.height != self.framebuffer_height) return;

  // You can use whatever colors you want
  const clear_value = c.SDL_Color {
    .r = 0,
    .g = 0,
    .b = 0,
    .a = 255,
  };
  const color_value = c.SDL_Color {
    .r = 255,
    .g = 255,
    .b = 255,
    .a = 255,
  };

  var pixels: ?*anyopaque = null;
  var pitch: i32 = 0;

  // Lock framebuffer so we can write pixel data to it
  if(c.SDL_LockTexture(self.framebuffer, null, &pixels, &pitch) != 0) {
    c.SDL_Log("Failed to lock texture: %s\n", c.SDL_GetError());
    return;
  }

  // Cast pixels pointer so that we can use offsets
  var upixels: [*]u32 = @ptrCast(@alignCast(pixels.?));

  // Copy pixel loop
  var y:u8 = 0;
  while(y < self.framebuffer_height) : (y += 1) {
    var x:u8 = 0;
    while(x < self.framebuffer_width) : (x += 1) {
      var index: usize = @as(usize, y) * @divExact(@as(usize, @intCast(pitch)), @sizeOf(u32)) + @as(usize, x);
      var color = if(bitmap.getPixel(x,y) == 1) color_value else clear_value;

      // It would probably be better to
      // use SDL_MapRGBA here but it was
      // giving me errors for some reason
      // and I'm too lazy to figure it out
      var r: u32 = @as(u32, color.r) << 24;
      var g: u32 = @as(u32, color.g) << 16;
      var b: u32 = @as(u32, color.b) << 8;
      var a: u32 = @as(u32, color.a) << 0;

      upixels[index] = r | g | b | a;
    }
  }

  _ = c.SDL_UnlockTexture(self.framebuffer);

  _ = c.SDL_RenderClear(self.renderer);
  _ = c.SDL_RenderCopy(self.renderer, self.framebuffer, null,null);
  _ = c.SDL_RenderPresent(self.renderer);
}Code language: PHP (php)

The draw function is a lot longer and slightly more complicated than the others, but all it’s really doing is locking the SDL texture, getting a pointer to its pixel data, converting the data in our bitmap to the proper pixel data and setting it in the texture data, and then rendering the texture.

You might be wondering what those _ = bits are doing there. In Zig every value that an expression creates has to be used or at the very least acknowledged. All of those SDL functions return values but we don’t want/need to use them so we simply use the _ = to acknowledge that they exist.

With that done we’re now finally able to actually draw stuff to the screen. Head on over to the main.zig file and change it to look like this:

const std = @import("std");
const Display = @import("display.zig").Display;
const Bitmap = @import("bitmap.zig").Bitmap;    // New

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  const allocator = gpa.allocator();
  defer {
    const leaked = gpa.deinit();
    if(leaked == .leak) {
      @panic("MEMORY LEAK");
    }
  }

  // New
  var bitmap = try Bitmap.create(allocator, 64,32);
  defer bitmap.free();
  _ = bitmap.setPixel(5,5);

  var display = try Display.create("CHIP-8", 800,400, bitmap.width,bitmap.height);
  defer display.free();

  // Display loop
  while(display.open) {
    display.input();
    display.draw(&bitmap); // New
  }
}Code language: JavaScript (javascript)

And with that you should have a single pixel displaying on the screen.

Loading ROMs

Alright so we’re almost ready to start work on the actual emulator itself, but we have two more things we have to do before that, the first up being setting up ROM loading. Plenty of CHIP-8 ROMs can be found free around the internet or on GitHub so don’t be afraid to look around. I’ll personally be using the blitz.rom ROM from this repository: https://github.com/badlogic/chip8/tree/master/roms.

To start create a new source file called device.zig and insert this boilerplate:

const std = @import("std");

const DEFAULT_FONT = [80]u8{
  0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
  0x20, 0x60, 0x20, 0x20, 0x70, // 1
  0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
  0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
  0x90, 0x90, 0xF0, 0x10, 0x10, // 4
  0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
  0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
  0xF0, 0x10, 0x20, 0x40, 0x40, // 7
  0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
  0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
  0xF0, 0x90, 0xF0, 0x90, 0x90, // A
  0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
  0xF0, 0x80, 0x80, 0x80, 0xF0, // C
  0xE0, 0x90, 0x90, 0x90, 0xE0, // D
  0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
  0xF0, 0x80, 0xF0, 0x80, 0x80  // F
};

pub const Device = struct {
  const Self = @This();

  allocator: std.mem.Allocator,
  memory: []u8, // RAM memory for emulator

  // Create the device
  pub fn create(allocator: std.mem.Allocator) !Self {

  }

  // Free the device
  pub fn free(self: *Self) void {

  }

  // Load raw ROM data into memory
  pub fn loadProgramIntoMemory(self: *Self, program: []u8) void {

  }

  // Load the ROM data from a file
  pub fn loadROM(self: *Self, path: []const u8) bool {

  }
};Code language: PHP (php)

This struct will serve as a sort of abstract “device” for our emulator, a device which contains the actual memory of the emulator and is capable of loading ROMs from the filesystem. Despite how important that may sound nothing particularly fancy will be happening here except for file loading.

You may be wondering about that big DEFAULT_FONT array at the top. CHIP-8 is expected to have a default font loaded into memory for games to use, so we’ve just added that default font at the top there and will load it into memory upon device creation.

// Create the device
pub fn create(allocator: std.mem.Allocator) !Self {
  var memory = try allocator.alloc(u8, 4096);
  @memcpy(memory[0x000..0x050], DEFAULT_FONT[0..80]);

  return Self {
    .allocator = allocator,
    .memory = memory,
  };
}

// Free the device
pub fn free(self: *Self) void {
  self.allocator.free(self.memory);
}

// Load raw ROM data into memory
pub fn loadProgramIntoMemory(self: *Self, program: []u8) void {
  @memcpy(self.memory[0x200..0x200 + program.len], program[0..program.len]);
}

// Load the ROM data from a file
pub fn loadROM(self: *Self, path: []const u8) bool {
  var file = std.fs.cwd().openFile(path, .{}) catch return false;
  defer file.close();

  var stat = file.stat() catch return false;
  if(stat.size > self.memory.len - 0x200) return false;

  var buffer = self.allocator.alloc(u8, stat.size) catch return false;
  defer self.allocator.free(buffer);

  file.reader().readNoEof(buffer) catch return false;

  self.loadProgramIntoMemory(buffer);

  return true;
}Code language: PHP (php)

Nothing particularly interesting is happening in the code, except for the use of Zig’s slices. Slices in Zig are a special kind of pointer that reference a contiguous subset of elements in a sequence. In English and (mostly) in practice that just means that they’re pointers to data in arrays with additional useful information (such as length) added to them. They’re used all around in the standard library and are rather flexible. You can have a slice pointing to data stored in an array, you can have a slice pointing to data stored in another slice, and you can create slices on-the-fly by using that [start_index..end_index] syntax as shown above. Like I said they’re used all the time in the standard library so that’s why they’re littered throughout the code.

With just that little bit of code we already have ROM loading done and ready, so go back to the main.zig and and this snippet somewhere near the beginning of the main function:

var device = try Device.create(allocator);
defer device.free();

if(!device.loadROM("./roms/blitz.rom")) {
  std.debug.print("Failed to load CHIP-8 ROM\n", .{});
  return;
}Code language: PHP (php)

Not forgetting to import the Device struct at the beginning of the file:

const Device = @import("device.zig").Device;Code language: JavaScript (javascript)

Input

Alright, the last thing we have to do before getting to the actual emulator is to setup keyboard input. CHIP-8 has a simple 16 key input, with each key being valued 0x0…0xF. Luckily SDL makes input for us a breeze. We’ll use scancodes instead of keycodes so that anyone can use the emulator regardless of keyboard layout.

In display.zig add this new field:

keys: [16]bool,Code language: CSS (css)

And add it add the end of the create function:

return Self {
  .width = width,
  .height = height,
  .fwidth = fwidth,
  .fheight = fheight,
  .window = window,
  .renderer = renderer,
  .framebuffer = framebuffer,
  .open = true,
  .keys = std.mem.zeroes([16]bool), // New
};Code language: JavaScript (javascript)

Afterwards go to the input function and add these two new cases to the switch statement:

c.SDL_KEYDOWN => {
  switch(event.@"key".@"keysym".@"scancode") {
    c.SDL_SCANCODE_1 => { self.keys[0x1] = true; },
    c.SDL_SCANCODE_2 => { self.keys[0x2] = true; },
    c.SDL_SCANCODE_3 => { self.keys[0x3] = true; },
    c.SDL_SCANCODE_4 => { self.keys[0xC] = true; },
    c.SDL_SCANCODE_Q => { self.keys[0x4] = true; },
    c.SDL_SCANCODE_W => { self.keys[0x5] = true; },
    c.SDL_SCANCODE_E => { self.keys[0x6] = true; },
    c.SDL_SCANCODE_R => { self.keys[0xD] = true; },
    c.SDL_SCANCODE_A => { self.keys[0x7] = true; },
    c.SDL_SCANCODE_S => { self.keys[0x8] = true; },
    c.SDL_SCANCODE_D => { self.keys[0x9] = true; },
    c.SDL_SCANCODE_F => { self.keys[0xE] = true; },
    c.SDL_SCANCODE_Z => { self.keys[0xA] = true; },
    c.SDL_SCANCODE_X => { self.keys[0x0] = true; },
    c.SDL_SCANCODE_C => { self.keys[0xB] = true; },
    c.SDL_SCANCODE_V => { self.keys[0xF] = true; },
    else => {},
  }
},
c.SDL_KEYUP => {
  switch(event.@"key".@"keysym".@"scancode") {
    c.SDL_SCANCODE_1 => { self.keys[0x1] = false; },
    c.SDL_SCANCODE_2 => { self.keys[0x2] = false; },
    c.SDL_SCANCODE_3 => { self.keys[0x3] = false; },
    c.SDL_SCANCODE_4 => { self.keys[0xC] = false; },
    c.SDL_SCANCODE_Q => { self.keys[0x4] = false; },
    c.SDL_SCANCODE_W => { self.keys[0x5] = false; },
    c.SDL_SCANCODE_E => { self.keys[0x6] = false; },
    c.SDL_SCANCODE_R => { self.keys[0xD] = false; },
    c.SDL_SCANCODE_A => { self.keys[0x7] = false; },
    c.SDL_SCANCODE_S => { self.keys[0x8] = false; },
    c.SDL_SCANCODE_D => { self.keys[0x9] = false; },
    c.SDL_SCANCODE_F => { self.keys[0xE] = false; },
    c.SDL_SCANCODE_Z => { self.keys[0xA] = false; },
    c.SDL_SCANCODE_X => { self.keys[0x0] = false; },
    c.SDL_SCANCODE_C => { self.keys[0xB] = false; },
    c.SDL_SCANCODE_V => { self.keys[0xF] = false; },
    else => {},
  }
},Code language: PHP (php)

We’ll be using all of the left-most keys on the keyboard to represent the 16 key input.

Creating the Emulator

Finally, after all that setup we’re ready to make the actual honest-to-God emulator. For other types of emulators this would easily be the most time-consuming part, but luckily for us the CHIP-8 instruction set only has 35 opcodes.

Since the main focus of this section is the implementation of the instruction set, I’ll just give you all of the boilerplate and helper functions that we use from the get-go. Create a new file called cpu.zig and insert this code:

const std = @import("std");
const Bitmap = @import("bitmap.zig").Bitmap;
const Display = @import("display.zig").Display;

pub const CPU = struct {
  const Self = @This();

  memory: *[]u8,
  bitmap: *Bitmap,
  display: *Display,
  pc: u16,
  i: u16,
  dtimer: u8,
  stimer: u8,
  v: [16]u8,
  stack: [16]u16,
  stack_idx: u8,
  paused: bool,
  paused_x: u8,
  speed: u8,

  pub fn create(
    memory: *[]u8,  // Pointer to device memory
    bitmap: *Bitmap,
    display: *Display
  ) Self {
    return Self {
      .memory = memory,
      .bitmap = bitmap,
      .display = display,
      .pc = 0x200, // ROMs are loaded in at 0x200 so this is where the PC will be
      .i = 0,
      .dtimer = 0,
      .stimer = 0,
      .v = std.mem.zeroes([16]u8), // Create zero-initialized array,
      .stack = std.mem.zeroes([16]u16),
      .stack_idx = 0,
      .paused = false,
      .paused_x = 0, // For storing key press after un-pausing
      .speed = 10,
    };
  }

  // Cycle through opcodes
  pub fn cycle(self: *Self) void {
    if(self.paused) {
      var i: u8 = 0;
      while(i < 16) : (i += 1) {
        if(self.display.keys[i]) {
          self.paused = false;
          self.v[self.paused_x] = i;
        }
      }
    }

    var i: u8 = 0;
    while(i < self.speed) : (i += 1) {
      // We aren't running instructions
      // if the emulator is paused
      if(!self.paused) {
        // CHIP-8 opcodes are two bytes long
        // .* is used to dereference pointers in Zig
        var opcode: u16 = (@as(u16, self.memory.*[self.pc]) << 8 | @as(u16, self.memory.*[self.pc+1]));
        self.executeInstruction(opcode);
      }
    }

    if(!self.paused) {
      self.updateTimers();
    }

    self.playSound();
  }



  // Update timers
  fn updateTimers(self: *Self) void {
    if(self.dtimer > 0) self.dtimer -= 1;
    if(self.stimer > 0) self.stimer -= 1;
  }

  fn playSound(self: *Self) void {
    if(self.stimer > 0) {
      // TODO
    } else {
      // TODO
    }
  }

  // Push address to stack
  fn stackPush(self: *Self, address: u16) void {
    if(self.stack_idx > 15) return;

    self.stack[self.stack_idx] = address;
    self.stack_idx += 1;
  }

  // Pop address from stack
  fn stackPop(self: *Self) u16 {
    if(self.stack_idx == 0) return 0;

    var value = self.stack[self.stack_idx-1];
    self.stack_idx -= 1;
    return value;
  }

  // Execute opcode
  fn executeInstruction(self: *Self, opcode: u16) void {
    self.pc += 2;

    var x = (opcode & 0x0F00) >> 8;
    var y = (opcode & 0x00F0) >> 4;

    // Big 'ol opcode switch
    switch(opcode & 0xF000) {
      0x0000 => {
        switch(opcode) {
          0x00E0 => {},
          0x00EE => {},
          else => {},
        }
      },
      0x1000 => {},
      0x2000 => {},
      0x3000 => {},
      0x4000 => {},
      0x5000 => {},
      0x6000 => {},
      0x7000 => {},
      0x8000 => {
        switch(opcode & 0xF) {
          0x0 => {},
          0x1 => {},
          0x2 => {},
          0x3 => {},
          0x4 => {},
          0x5 => {},
          0x6 => {},
          0x7 => {},
          0xE => {},
          else => {},
        }
      },
      0x9000 => {},
      0xA000 => {},
      0xB000 => {},
      0xC000 => {},
      0xD000 => {},
      0xE000 => {
        switch(opcode & 0xFF) {
          0x9E => {},
          0xA1 => {},
          else => {},
        }
      },
      0xF000 => {
        switch(opcode & 0xFF) {
          0x07 => {},
          0x0A => {},
          0x15 => {},
          0x18 => {},
          0x1E => {},
          0x29 => {},
          0x33 => {},
          0x55 => {},
          0x65 => {},
          else => {},
        }
      },
      else => {},
    }
  }
};Code language: PHP (php)

A big bit of boilerplate to be sure, but at this point everything should be self-explanatory.

0nnn – SYS Addr

This opcode can be ignored.

00E0 – CLS

Clears the screen.

0x00E0 => { self.bitmap.clear(0); },Code language: PHP (php)

00EE – RET

Pop the last address in the stack and store it in the program counter.

0x00EE => { self.pc = self.stackPop(); },Code language: PHP (php)

1nnn – JMP addr

Set the program counter to the value stored in nnn.

0x1000 => { self.pc = (opcode & 0xFFF); },Code language: PHP (php)

2nnn – CALL addr

Push the current program counter address to the stack and jump to the specified address.

0x2000 => { self.stackPush(self.pc); self.pc = (opcode & 0xFFF); },Code language: PHP (php)

3xkk – SE Vx, byte

Compare value in register x (Vx) to the value kk and skip the next instruction if equal.

0x3000 => { if(self.v[x] == (opcode & 0xFF)) self.pc += 2; },Code language: PHP (php)

4xkk – SNE Vx, byte

Compare value in register x (Vx) to the value kk and skip the next instruction if not equal.

0x4000 => { if(self.v[x] != (opcode & 0xFF)) self.pc += 2; },Code language: PHP (php)

5xy0 – SE Vx, Vy

Compare value in register x (Vx) to the value in register y (Vy) and skip the next instruction if equal.

0x5000 => { if(self.v[x] == self.v[y]) self.pc += 2; },Code language: PHP (php)

6xkk – LD Vx, byte

Set the value of Vx to kk.

0x6000 => { self.v[x] = @as(u8, @truncate(opcode & 0xFF)); },Code language: PHP (php)

“@truncate” allows us to type cast a value to a type with a smaller byte size in Zig.

7xkk – ADD Vx, byte

Add kk to Vx.

0x7000 => { self.v[x] +%= @as(u8, @truncate(opcode & 0xFF)); },Code language: PHP (php)

“+%=” allows us to overflow and wrap around a value while doing addition in Zig.

8xy0 – LD Vx, Vy

Load the value in Vy into Vx.

0x0 => { self.v[x] = self.v[y]; },Code language: PHP (php)

8xy1 – OR Vx, Vy

Set Vx to the value of Vx | Vy and set the flag register (VF) to 0.

0x1 => { self.v[x] |= self.v[y]; self.v[0xF] = 0; },Code language: PHP (php)

8xy2 – AND Vx, Vy

Set Vx to the value of Vx & Vy and set VF to 0.

0x2 => { self.v[x] &= self.v[y]; self.v[0xF] = 0; },Code language: PHP (php)

8xy3 – XOR Vx, Vy

Set Vx to the value of Vx ^ Vy and set VF to 0.

0x3 => { self.v[x] ^= self.v[y]; self.v[0xF] = 0; },Code language: PHP (php)

8xy4 – ADD Vx, Vy

Set Vx to the value of Vx + Vy and set VF to 1 if overflow, 0 otherwise.

0x4 => {
  var sum: u32 = (@as(u32, self.v[x]) + @as(u32, self.v[y]));
  self.v[x] = @as(u8, @truncate(sum));

  self.v[0xF] = 0;
  if(sum > 0xFF)
    self.v[0xF] = 1;
},Code language: PHP (php)

This one’s a little more complex. Instead of just wrapping around during an overflow the CHIP-8 spec says instead that if an addition is greater than 8 bits the VF register is set to 1 and only the lowest 8 bits of the result are to be kept. Some quirkery with CHIP-8 also requires that the VF register is set at specific times during an instruction, which we’ll see more of in some other instructions.

8xy5 – SUB Vx, Vy

Set Vx to the value of Vx – Vy and set VF to 1 if Vx > Vy, 0 otherwise.

0x5 => {
  var vX = self.v[x];
  var vY = self.v[y];

  self.v[x] = vX -% vY;

  self.v[0xF] = 0;
  if(vX > vY)
    self.v[0xF] = 1;
},Code language: PHP (php)

I know the implementation of this may seem a bit wonky but we implement it this way to apply to the CHIP-8 specs. In most instructions the VF flag is usually set after a calculation takes place. This is because the VF flag itself could be used in a calculation and setting it beforehand would ruin said calculation, so we do it after.

8xy6 – SHR Vx {, Vy}

Set Vx to the value of Vy SHR 1. If the least significant bit of Vy is 1 set VF to 1, 0 otherwise.

0x6 => {
  var vY = self.v[y];

  self.v[x] = vY >> 1;

  self.v[0xF] = 0;
  if(vY & 0x01 != 0)
    self.v[0xF] = 1;
},Code language: PHP (php)

8xy7 – SUBN Vx, Vy

Set vX to the value of Vy – Vx and set VF to 1 if Vy > Vx, 0 otherwise.

0x7 => {
  var vX = self.v[x];
  var vY = self.v[y];

  self.v[x] = vY -% vX;

  self.v[0xF] = 0;
  if(vY > vX)
    self.v[0xF] = 1;
},Code language: PHP (php)

8xyE – SHL Vx {, Vy}

Set Vx to the value of Vy SHL 1. If the most significant bit of Vy is 1 set VF to 1, 0 otherwise.

0xE => {
  var vY = self.v[y];

  self.v[x] = vY << 1;

  self.v[0xF] = 0;
  if(vY & 0x80 != 0)
    self.v[0xF] = 1;
},Code language: PHP (php)

9xy0 – SNE Vx, Vy

Skip the next instruction if Vx != Vy.

0x9000 => { if(self.v[x] != self.v[y]) self.pc += 2; },Code language: PHP (php)

Annn – LD I, addr

Set the value of register I to nnn.

0xA000 => { self.i = (opcode & 0xFFF); },Code language: PHP (php)

Bnnn – JP V0, addr

Set the value of the program counter to nnn plus the value of V0.

0xB000 => { self.pc = (opcode & 0xFFF) + self.v[0]; },Code language: PHP (php)

Cxkk – RND Vx, byte

Generate a random number from 0 to 255, AND it with the value kk, and store the result in Vx.

0xC000 => {
  // Get the operating system
  // to generate a random seed
  var seed: u64 = 11111;
  std.os.getrandom(std.mem.asBytes(&seed)) catch {};

  // Generate a random number
  var rnd = std.rand.DefaultPrng.init(seed);
  var num = rnd.random().int(u8);

  // AND and store
  self.v[x] = num & (@as(u8, @truncate(opcode)) & 0xFF);
},Code language: PHP (php)

Dxyn – DRW Vx, Vy, nibble

Display an n-byte sprite starting at memory location I at pixel location (Vx, Vy), set VF = collision.

The interpreter reads n bytes from memory, starting at the address stored in I. These bytes are then displayed as sprites on the screen at coordinates (Vx, Vy). Sprites are XORed onto the existing screen. If this causes any pixels to be erased, VF is set to 1, 0 otherwise. If a sprite’s x or y is positioned outside of the screen, it wraps around to the opposite side of the screen. This behavior however does not apply to the coordinates of the individual pixels in the sprite.

0xD000 => {
  var width: u16 = 8; // ALL sprite are 8 wide
  var height = (opcode & 0xF);

  // One of the few instructions
  // where it's fine to set this first
  self.v[0xF] = 0;

  var row: u8 = 0;
  while(row < height) : (row += 1) {
    var sprite = self.memory.*[self.i + row];

    var col: u8 = 0;
    while(col < width) : (col += 1) {
      // Wrap the x and y around
      // the screen if outside
      // the bounds
      var px = self.v[x] % self.bitmap.width;
      var py = self.v[y] % self.bitmap.height;

      // We don't wrap pixels that
      // are outside of the bounds
      if(px + col >= self.bitmap.width) continue;
      if(py + row >= self.bitmap.height) continue;

      // If the bit (sprite) is not 0
      // render/erase the pixel
      if((sprite & 0x80) > 0) {
        // If setPixel returns true
        // a pixel was erased, so set
        // VF to 1
        if(self.bitmap.setPixel(px + col,py + row)) {
          self.v[0xF] = 1;
        }
      }

      // Shift the sprite left 1 and
      // move the next col/bit of the
      // sprite into the first position
      sprite <<= 1;
    }
  }
},Code language: PHP (php)

Ex9E – SKP Vx

Skip the next instruction if the key with the value of Vx is pressed.

0x9E => { if(self.display.keys[self.v[x]]) self.pc += 2; },Code language: PHP (php)

ExA1 – SKNP Vx

Skip the next instruction if the key with the value of Vx is not pressed.


0xA1 => { if(!self.display.keys[self.v[x]]) self.pc += 2; },Code language: PHP (php)

Fx07 – LD Vx, DT

Set Vx to the value stored in the delay timer.

0x07 => { self.v[x] = self.dtimer; },Code language: PHP (php)

Fx0A – LD Vx, K

Pause the emulator until a key is pressed.

0x0A => { self.paused = true; self.paused_x = @as(u8, @truncate(x)); },Code language: PHP (php)

Astute readers may have observed some of these values being referenced earlier in the code. The functionality for this instruction was already setup in the boilerplate, so we don’t have to worry about adding anything more.

Fx15 – LD DT, Vx

Set the delay timer to the value stored in Vx.

0x15 => { self.dtimer = self.v[x]; },Code language: PHP (php)

Fx18 – LD ST, Vx

Set the sound timer to the value stored in Vx.

0x18 => { self.stimer = self.v[x]; },Code language: PHP (php)

Fx1E – ADD I, Vx

Add Vx to I.

0x1E => { self.i += self.v[x]; },Code language: PHP (php)

Fx29 – LD F, Vx – ADD I, Vx

Set I to the location of the sprite stored in Vx.

0x29 => { self.i = @as(u16, @intCast(self.v[x])) * 5; },Code language: PHP (php)

We multiply by 5 since every sprite is 5 bytes long.

Fx33 – LD B, Vx

Grab the hundreds, tens, and ones digits in the value stored in Vx and store them in I+0, I+1, and I+2, respectively.

0x33 => {
  self.memory.*[self.i+0] = (self.v[x] / 100) % 10;
  self.memory.*[self.i+1] = (self.v[x] / 10) % 10;
  self.memory.*[self.i+2] = self.v[x] % 10;
},Code language: PHP (php)

Fx55 – LD [I], Vx

Loop through the registers V0 through VF and store their values in memory starting at I and increment I.

0x55 => {
  var ri: u16 = 0;
  while(ri <= x) : (ri += 1) {
    self.memory.*[self.i + ri] = self.v[ri];
  }
  self.i += ri;
},Code language: PHP (php)

Fx65 – LD Vx, [I]

Read values from memory starting at I and store them in registers V0 through VF, incrementing I.

0x65 => {
  var ri: u16 = 0;
  while(ri <= x) : (ri += 1) {
    self.v[ri] = self.memory.*[self.i + ri];
  }
  self.i += ri;
},Code language: PHP (php)

Phew, and just like that we’ve implemented all of the CHIP-8 instructions. But we’re still not done quite yet! There’s one last little wrinkle to iron out: timing.

Timing

CHIP-8 expects to run at a rate of 60 Hz a second. Modern computers run, uhhhh, a helluva lot faster than that, so we’re gonna have to artificially limit how many times we cycle the CPU. We can easily do this by using Zig’s built-in time methods.

For the last time, head on over to main.zig. Remove this line:

_ = bitmap.setPixel(5,5);

And then rewrite the display loop to look like this:

// Display loop
const fps: f32 = 60.0;
const fpsInterval = 1000.0 / fps;
var previousTime = std.time.milliTimestamp();
var currentTime = std.time.milliTimestamp();

while(display.open) {
  display.input();

  currentTime = std.time.milliTimestamp();
  if(@as(f32, @floatFromInt(currentTime - previousTime)) > fpsInterval) {
    previousTime = currentTime;

    cpu.cycle();

    display.draw(&bitmap);
  }
}Code language: JavaScript (javascript)

And with that, our CHIP-8 emulator is done. I hope you enjoyed this little introduction to the Zig programming language. I’ll potentially make more stuff like this such as a NES or Gameboy emulator in the future. Or I might make a DOOM renderer. ¯\_(ツ)_/¯

Regardless, you can find more details about the more technical aspects of CHIP-8 in the resources down below. You can also find the full source code for the project at https://github.com/HandleHHandle/CHIP8Tutorial.

Till next time.

[Psssst, you may have noticed that we failed to implement the playSound function. Because I’m lazy I’ve decided to leave that as an exercise for the reader ;)]