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.
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.
git clone https://github.com/ParcivalLTD/chip8.git
cd chip8Either implementation takes a ROM path as its argument, or picks from the bundled ROMs
interactively when given none. Esc quits.
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.ch8Requires 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.ch8If 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.
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.
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++.
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.
dotnet run --project tests/csharpBecause 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.
- No SUPER-CHIP or XO-CHIP extensions: no 128×64 mode, no scrolling, no
DXY016-row sprites. - The
8XY6and8XYEshifts operate onVxand ignoreVy, 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.
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.
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.
MIT — see LICENSE.
