Skip to content
ParcivalLTDPublic

About

CHIP-8 interpreter in C# and C++ with SDL2, 60 Hz timers, XOR sprite collision

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

11 Commits

Folders and files

Repository files navigation

CHIP-8 Emulator

CI

A CHIP-8 interpreter rendering through SDL2, in two implementations — the original C# one and a C++ port of it. Both run the classic homebrew library; Tetris, Pong, Breakout and Space Invaders are included.

Tetris running in the emulator

CHIP-8 is an interpreted virtual machine from 1977, designed so that games could be written once and run on any 1970s microcomputer that shipped an interpreter for it. It has 4 KB of memory, sixteen 8-bit registers, a 64×32 monochrome display and a 16-key hex keypad — small enough to implement completely, which makes it the standard first emulator project.

Running it

git clone https://github.com/ParcivalLTD/chip8.git
cd chip8

Either implementation takes a ROM path as its argument, or picks from the bundled ROMs interactively when given none. Esc quits.

C#

Requires the .NET 10 SDK and the native SDL2 library:

Platform SDL2
Windows, Linux x64, macOS Intel Bundled with the NuGet package — nothing to install
macOS Apple Silicon brew install sdl2
Linux ARM64 Your distro's SDL2 package
dotnet run --project chip-8 -- roms/tetris.ch8

C++

Requires CMake 3.16+ and a compiler with C++20 support (MSVC 2022, GCC 10+, Clang 10+).

cmake -B chip-8-cpp/build -S chip-8-cpp
cmake --build chip-8-cpp/build
./chip-8-cpp/build/chip8 roms/tetris.ch8

If SDL2 is installed — apt install libsdl2-dev, brew install sdl2, vcpkg — the build picks it up. If it is not, CMake downloads and builds a pinned SDL2 release as part of the configure step; pass -DCHIP8_FETCH_SDL2=OFF to require an installed copy instead. The ROMs are staged next to the executable at build time, so the interactive picker finds them wherever the binary ends up.

Controls

The original hardware had a 4×4 hex keypad. This maps it directly onto the number row and the letters A–F, so keys 1–9, 0 and A–F are hex keys 0x1–0x9, 0x0 and 0xA–0xF.

Most games use only a handful. Tetris is 4 5 6 to move and rotate; Pong is 1/4 for the left paddle and C/D for the right; Space Invaders is 4 5 6.

What's implemented

34 of the 35 documented instructions. The omission is 0NNN, which called a subroutine in RCA 1802 machine code on the host computer — it has no meaning outside original hardware and no ROM in circulation uses it. Everything else is present, including the full 8XY_ arithmetic and logic group, both EX__ key-state skips and all nine FX__ instructions.

Memory layout

Range Contents
0x000–0x04F Hex font, sixteen 4×5 glyphs at five bytes each
0x050–0x1FF Unused — on real hardware, the interpreter itself lived here
0x200–0xFFF Program, loaded from the .ch8 file

Timing. The delay and sound timers decrement at 60 Hz, paced by a monotonic clock — Stopwatch in C#, std::chrono::steady_clock in C++ — rather than wall-clock time. The CPU runs 11 instructions per frame, roughly 660 Hz — CHIP-8 never specified a clock rate, so this is tuned to feel right on the bundled ROMs and is a single constant in the host file if you want it faster.

Display. DXYN XORs an 8-pixel-wide sprite onto the framebuffer and sets VF when a lit pixel is switched off, which is how games detect collisions. Sprite origins wrap around the display; the sprite body then clips at the right and bottom edges rather than wrapping, matching the behaviour the common test ROMs expect.

The framebuffer is a flat array of 32-bit pixels handed straight to SDL_UpdateTexture each frame — pinned with GCHandle in C#, passed by pointer in C++. One streaming texture is allocated for the whole run: no per-frame surface, and in C# no copy between managed and native memory.

Audio. A sine tone generated in an SDL_AudioCallback while the sound timer is non-zero. Because the callback runs on SDL's audio thread, it only reads emulator state; the sound timer is decremented on the emulation thread and is marked volatile in C#, std::atomic in C++.

Project layout

chip-8/
  CPU.cs          interpreter — memory, registers, stack, instruction dispatch
  Program.cs      SDL host — window, render loop, input, audio, ROM selection
  roms/           bundled .ch8 files, shared by both implementations

chip-8-cpp/
  src/cpu.hpp     the same interpreter, as a class
  src/cpu.cpp
  src/main.cpp    the same SDL host
  CMakeLists.txt  build, SDL2 lookup, ROM staging

tests/
  cpp/            traces the C++ interpreter
  csharp/         traces the C# one, and drives the comparison

Around 750 lines of C#, 830 of C++. Neither interpreter depends on SDL, and both can be driven from a test harness: LoadProgram, then Step and TickTimers in C#; load_program, then step and tick_timers in C++.

The two are a line-for-line port of each other, down to the order the 8XY_ instructions write VF in — which matters when X is F. The C++ side differs only where the language does: the interpreter's faults are chip8::Error and chip8::UnsupportedOpcode rather than .NET exception types, the keyboard bitmask is set through set_key_down/set_key_up instead of being a public field, SDL handles are owned by unique_ptr, and the audio callback writes into SDL's buffer directly rather than through a staging array. CXNN's generator is written out longhand in both rather than taken from System.Random or <random>, so that a seed produces the same sequence either side.

Testing

dotnet run --project tests/csharp

Because one implementation is a port of the other, the useful question is whether they still agree. This runs both over the same programs — hand-written ones pinning individual instruction semantics, generated ones, and the bundled ROMs — and compares registers, timers, stack, keypad and framebuffer after every single instruction. Around 333,000 instructions across 66 scenarios in five seconds, and it needs no SDL2.

Every scenario has to match exactly; there is no tolerance anywhere. That is possible because both interpreters run the same specified random generator rather than their platform's, so a given seed produces the same CXNN sequence in each — which also makes the seeded constructor mean what it always claimed to, something System.Random could not deliver across .NET versions. tests/README.md has the details.

CI runs this on Linux, Windows and macOS, and builds both emulators on each — GCC, MSVC and Clang, with SDL2 from the system on Linux and macOS and from the CMake download fallback on Windows. On Linux it also starts both emulators against a virtual display to check the SDL hosts still come up, and holds the two to the same exit code and error message on bad input.

Known limitations

  • No SUPER-CHIP or XO-CHIP extensions: no 128×64 mode, no scrolling, no DXY0 16-row sprites.
  • The 8XY6 and 8XYE shifts operate on Vx and ignore Vy, following CHIP-48 and later interpreters rather than the original COSMAC VIP. This is what nearly all circulating ROMs assume, but it means a handful of very early programs behave incorrectly.
  • No save states, no debugger, no configurable keypad.

Background

Written as the practical component of a pre-university research thesis, Emulation of Video Games (2024), which was awarded a Hans Riegel Foundation Award. Originally targeted .NET Framework 4.7.2 and Windows only; ported to .NET 10 and made cross-platform in 2026, and to C++ later the same year.

ROMs

The included .ch8 files are CHIP-8 homebrew from the public collections that have circulated since the 1990s. They are distributed here for convenience; see the CHIP-8 archive for provenance and authorship.

License

MIT — see LICENSE.

About

CHIP-8 interpreter in C# and C++ with SDL2, 60 Hz timers, XOR sprite collision

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages