An interpreter for the CHIP-8 programming language. Written in C.
  • C 97.4%
  • Makefile 2.6%
Find a file
Luke McDonald 976016e587
Merge pull request #8 from Luke1245/benchmark
Create headless benchmark component
2026-09-17 14:29:31 +01:00
docs/assets Add test images 2026-09-10 23:15:05 +01:00
src Add headless benchmark harness 2026-09-17 14:23:37 +01:00
.gitignore Makefile creation and sdl3 inclusion 2026-08-13 18:42:43 +01:00
bench.ch8 Add basic benchmark ROM 2026-09-17 14:25:15 +01:00
LICENSE Add MIT License to the project 2026-09-07 22:04:11 +01:00
Makefile Add headless benchmark harness 2026-09-17 14:23:37 +01:00
README.md Insert benchmark results into README 2026-09-17 14:28:46 +01:00

CHIP-8 Interpreter

A CHIP-8 Interpreter written in C23 with SDL3 for graphics, audio, and input.

CHIP-8 is an interpreted programming language from the 1970s. Initially used for the COSMAC VIP and Telmac 1800. CHIP-8 is intended to use less memory than other languages at the time, while still being a helpful abstraction up from machine code. This project implements its Opcodes, memory map, keyboard and display from the original spec. This allows it to run ROMs written for it in the 70s and 80s.

Status: Complete. All functionality has been implemented.

A gif of the classic Brick video game running on the CHIP-8 interpreter

Features

  • Full implementation of CHIP-8 instruction set, with 35-opcodes
  • 64 \times 32 original pixel display with scale factors and custom colours
  • 60 Hz delay and sound timers, independent of CPU execution speed
  • Hexadecimal keypad with mappings to scancode EN-US QWERTY layout
  • Switch between extension behaviour through CLI

Setup

Dependencies

  • C23-capable compiler (built with Apple clang version 21.0.0)
  • SDL3 (built against SDL 3.4.12)
  • Make

Build

While packages for SDL3 do seem to exist. I had to build from source

git clone https://github.com/Luke1245/chip-8-interpreter/
cd chip-8-interpreter
make

Run

./build/release/main <rom_path> [-s scale factor] [-e extension]

Usage

The original CHIP-8 keypad is a 4x4 hexadecimal grid. In this interpreter it has been mapped onto a EN-US QWERTy keyboard with scancodes as follows:

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

The following keys are also in-use:

  • SPACE: Pauses the interpreter
  • ESCAPE: Quits the interpreter
  • O: Decreases volume
  • P: Increases volume

The following flags can be passed as arguments on the command line

Flag Usage
-s The scale factor used in upscaling the original 64 by 32 pixel display. Default is 20
-e The interpreter extension. This chooses the behaviour for ambiguous opcodes. Possible values of: CHIP8, SUPERCHIP, XOCHIP. Default is CHIP8

Architecture

The interpreter is split into files that handle separate important data for the program to work.

File Purpose
main.c Pull all the components together and execute the main loop of the interpreter
config.c Initialise the config and handle command line arguments
platform.c Handle all components involving SDL3, i.e. audio, rendering, and input
chip8.c Main logic of the interpreter. Memory map initialisation, timers, and all opcode handling

The main loop runs a fixed number of instructions per frame. For original CHIP-8, this is eight instructions. For other extensions, this is configurable via the clock rate in the config struct. Delay and sound timers are decremented at 60hz.

Design notes

Instruction Decoding

Because the CHIP-8 interpreter has relatively few opcodes compared to other emulator / interpreter projects, a large switch statement with nesting is an acceptable design decision. However, for future projects, I would like to attempt to implement more efficient techniques such as a function pointer table.

Timing

As different instructions require different amounts of time to execute, keeping consistent timing for the interpreter is important for a smooth experience. To solve this, we use functions from SDL that allow us to calculate the time elapsed per frame, and set a delay to next frame execution dependent on this elapsed time. If the elapsed time is less than the delay for (roughly, SDL does not take floats for delay values) 60hz (which is 16.67ms), then we delay for 16.67 - elapsed time. Otherwise, we delay for 0 seconds, as the elapsed time has already delayed the next frame enough.

Behavioural differences

The original CHIP-8 specification is not the only one that exists for the language. There are others such as SUPER-CHIP and XO-CHIP, which have different behaviours for certain opcodes. This means not all ROMs are compatible with every CHIP-8 interpreter. For this interpreter, I have implemented the various different behaviours of SUPER-CHIP and XO-CHIP, which can be configured through the CLI options.

Instruction(s) / Behaviour CHIP-8 SUPER-CHIP XO-CHIP
0x8XY1, 0x8XY2, 0x8XY3 / Bitwise AND, OR, XOR The flags register is reset to 0 The flags register is untouched The flags register is untouched
0xFX55, 0xFX65 / Register dump and load The index register is incremented The index register is untouched The index register is incremented
Display wait Speed is limited to max 60 sprites per second Speed is not limited Speed is not limited
0x8XY6, 0x8XYE / Bitwise left and right shift Set value of vX to vY before shift Only operate on the vX register Set value of vX to vY before shift
BNNN / BXNN / Jump with offset Jump to address NNN + v0 Jump to address XNN + vX Jump to address NNN + v0

Testing

The CHIP-8 test suite by Timendus is the most comprehensive set of tests for the CHIP-8 interpreter available. I used this to test all key functionality of my interpreter. It was especially useful in the development of the "quirks" section. Below are images of my interpreter passing the flags test and the quirks test (under the CHIP-8 original specification option). These are key tests for the logic functionalities of the interpreter.

An image of the interpreter running the flags test, passing them all

An image of the interpreter running the quriks test, passing them all

The benchmark harness in bench.c was also used with a custom ROM (bench.ch8) that tests a mix of ALU, register, index, and branching instructions without I/O.

It achieves 369M instructions/second, over a median of 3 runs, on an Apple M3 at -O2.

References

License

Released under the MIT License. See LICENSE for details.