SearchA-ZS › SUPER-CHIP / XO-CHIP

SUPER-CHIP / XO-CHIP

2026 Open source · MIT Online

A self-authored, browser-based interpreter for the modern CHIP-8 extensions: SUPER-CHIP (SCHIP) and XO-CHIP, plus the base CHIP-8 they build on. SUPER-CHIP adds a 128×64 hi-res display, hardware scrolling, 16×16 sprites and a big font; XO-CHIP adds up to four colours via two bit-planes, a 64 KB address space, ranged register save/load and sampled audio. It runs open Octojam homebrew from John Earnest's chip8Archive and plugs into the site's full instruction-level debugger.

Browse the chip8Archive ↗

Visit the official site ↗

Runs on: Web browser

SUPER-CHIP / XO-CHIP Online Emulator

Play SUPER-CHIP / XO-CHIP using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Sound Test (CHIP-8 buzzer)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶
Black Rainbow (SUPER-CHIP)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶
Into The GarlicScape (XO-CHIP)SUPER-CHIP / XO-CHIPXO-CHIPopenOpen ⛶
An Evening to Die For (XO-CHIP)SUPER-CHIP / XO-CHIPXO-CHIPopenOpen ⛶
Super NeatBoy (XO-CHIP)SUPER-CHIP / XO-CHIPXO-CHIPopenOpen ⛶
Octoma (XO-CHIP)SUPER-CHIP / XO-CHIPXO-CHIPopenOpen ⛶
Skyward (XO-CHIP)SUPER-CHIP / XO-CHIPXO-CHIPopenOpen ⛶
Truck Simul8or (XO-CHIP)SUPER-CHIP / XO-CHIPXO-CHIPopenOpen ⛶
Octopeg (SUPER-CHIP)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶
Eaty The Alien (SUPER-CHIP)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶
Super Octogon (SUPER-CHIP)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶
Rockto (SUPER-CHIP)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶
Sub-Terr8nia (SUPER-CHIP)SUPER-CHIP / XO-CHIPSUPER-CHIPopenOpen ⛶

Machines emulated

Chips

Notes

Embedding

The core is a single self-contained file, schip.js, exporting a global SCHIP. It implements the whole CHIP-8 family superset — base CHIP-8, SUPER-CHIP and XO-CHIP — selected per ROM by a profile string that only flips a handful of behavioural quirks.

Own the loop. SCHIP.boot() ships a self-driving requestAnimationFrame loop, but a debugger cannot pause that. So the embed calls SCHIP.create() for a bare vm (no loop, no ROM) and drives it itself:

var vm = SCHIP.create({ canvas: canvas, speed: 30, profile: 'xochip' });
fetch(romUrl).then(r => r.arrayBuffer()).then(ab => {
  vm.load(new Uint8Array(ab));   // resets + copies the ROM to $200
  (function loop(){ vm.tick(); requestAnimationFrame(loop); })();
});

vm.tick() runs vm.speed instructions then services the 60 Hz timers and repaints; vm.step() is one instruction. The embed publishes both, plus the vm itself, on window.EMU_BOOT so the shared debugger can reach in.

Debugger integration

The whole machine is plain JavaScript state, so nothing had to be added to the core to expose it — the debug plug-in (schip-debug.js) reads vm.V, vm.I, vm.pc, vm.mem, vm.gfx and friends directly and writes them back through the register set() hooks.

CapabilityTechnique
Single-stepvm.step() executes exactly one opcode (it advances PC and mutates state, but never touches the RAF clock).
RegistersV0-VF, I, PC, SP, DT, ST and the XO-CHIP plane selector are read live each refresh and written back into the typed arrays.
DisassemblyA new schip decoder (/debugger/src/cpus/schip.js) — a superset of chip8 adding the SCHIP scroll/font/RPL and XO-CHIP plane/audio/long-I opcodes.
BreakpointsWhen the PC-breakpoint set is non-empty the loop steps one instruction at a time and halts before executing a watched PC.
WatchpointsWrite watchpoints are host-side: while any address is watched the loop snapshots those bytes before each instruction and pauses the moment one changes — no write hook needed inside the core.

Memory is exposed side-effect-free as five chips: the 64 KB RAM (hex/disasm/bits), the call stack, the 128×64 framebuffer, the RPL user flags and the XO-CHIP audio pattern buffer.

Architecture

CHIP-8 is a 1977 byte-code virtual machine for the COSMAC VIP. This core runs it and its two popular extensions from one interpreter:

  • CHIP-8 — 64×32 monochrome, 4 KB RAM, sixteen 8-bit registers V0-VF, a 12-bit index I, two 60 Hz timers and a 16-key hex keypad.
  • SUPER-CHIP — the HP-48 calculator extension: a 128×64 hi-res mode, hardware scrolling (00CN/00FB/00FC), 16×16 sprites (DXY0), a 10-byte "big" font (FX30) and RPL user flags (FX75/FX85).
  • XO-CHIP — the modern extension: up to four colours via two independent bit-planes (FX01 plane select), a 64 KB address space with a 16-bit index register (F000 long load), ranged register save/load (5XY2/5XY3), vertical scroll (00DN) and a 128-bit sampled audio buffer (F002, FX3A pitch).

The three profiles differ only in quirks: SUPER-CHIP shifts in place, does not advance I on FX55/FX65 and uses BXNN; CHIP-8 and XO-CHIP shift from Vy, advance I and use BNNN. Sprites clip at the screen edge on SCHIP/XO-CHIP and wrap on CHIP-8. The ROMs here are open Octojam homebrew from John Earnest's chip8Archive.

Sound

The whole CHIP-8 family has a single sound source: a buzzer that plays while the 8-bit sound-timer register ST is non-zero. ST is loaded by FX18 and counts down at 60 Hz; the tone is on exactly while ST > 0. That is all the "sound hardware" there is — one gated fixed-pitch voice.

This is a Pattern V-stub integration: the core owns the audio and pushes samples to the shared sink. Rather than run its own AudioContext, schip.js synthesises the buzzer in JavaScript and hands one video frame of samples at a time to window.EmuAudio (/debugger/src/audio.js), which owns the single page AudioContext and the Sound/Mute gesture gating shared by all the authored emulators.

The core's _beep(on) method is called once per 60 Hz frame with on = (ST > 0). It generates a 440 Hz square wave — the conventional CHIP-8 buzzer pitch — at the sink's real rate:

var count = Math.round(EmuAudio.sampleRate / 60);   // stereo frames this tick
var buf = new Int16Array(count * 2);            // interleaved L,R,L,R…
var inc = 440 / EmuAudio.sampleRate;              // cycles advanced per sample
for (var i = 0; i < count; i++) {
  var s = phase < 0.5 ? amp : -amp;             // 50% duty square, mono
  buf[i*2] = s; buf[i*2+1] = s;                  // duplicated to both channels
  phase += inc; if (phase >= 1) phase -= 1;
}
EmuAudio.push(buf);

Because the wave is generated at EmuAudio.sampleRate (not a fixed 44.1 kHz), the pitch is correct on any device with no resampling, and pushing exactly round(sampleRate/60) stereo pairs each frame keeps the sink's ring buffer fed. A phase accumulator carried across frames keeps the square continuous while ST holds, and is reset to zero on a silent frame so each new beep starts cleanly. When ST is 0 a frame of silence is pushed, so timing never drifts.

The mute contract lives on window.EMU_BOOT.transport: isMuted() and setMute(m) delegate straight to EmuAudio. It starts muted — browsers block audio before a user gesture — and the shell's Sound/Mute button resumes the context and unmutes from a real click.

Default and mute. The default boot is a real homebrew game (Super NeatBoy). Sound is fully supported and plays when a ROM drives ST; the page starts muted, so click Sound to hear it. A self-authored CHIP-8 Sound Test config is also available if you want a continuous tone: its ROM is baked inline (no fetch), draws a glyph, then loops ST = 30 / DT = 60 for a steady buzzer.

Caveat. XO-CHIP also has a richer 128-bit sampled-audio channel (F002 pattern buffer, FX3A pitch) that is still gated by ST; this integration renders every ST-gated beep as the classic fixed 440 Hz square rather than replaying that sample buffer, so XO-CHIP music is heard as a rhythmic buzzer rather than its authored waveform.