Search › A-Z › S › SUPER-CHIP / XO-CHIP
SUPER-CHIP / XO-CHIP
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.
Runs on: Web browser
SUPER-CHIP / XO-CHIP Online Emulator
Play SUPER-CHIP / XO-CHIP using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Sound Test (CHIP-8 buzzer) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ | |
| Black Rainbow (SUPER-CHIP) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ | |
| Into The GarlicScape (XO-CHIP) | SUPER-CHIP / XO-CHIP | XO-CHIP | open | Open ⛶ | |
| An Evening to Die For (XO-CHIP) | SUPER-CHIP / XO-CHIP | XO-CHIP | open | Open ⛶ | |
| Super NeatBoy (XO-CHIP) | SUPER-CHIP / XO-CHIP | XO-CHIP | open | Open ⛶ | |
| Octoma (XO-CHIP) | SUPER-CHIP / XO-CHIP | XO-CHIP | open | Open ⛶ | |
| Skyward (XO-CHIP) | SUPER-CHIP / XO-CHIP | XO-CHIP | open | Open ⛶ | |
| Truck Simul8or (XO-CHIP) | SUPER-CHIP / XO-CHIP | XO-CHIP | open | Open ⛶ | |
| Octopeg (SUPER-CHIP) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ | |
| Eaty The Alien (SUPER-CHIP) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ | |
| Super Octogon (SUPER-CHIP) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ | |
| Rockto (SUPER-CHIP) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ | |
| Sub-Terr8nia (SUPER-CHIP) | SUPER-CHIP / XO-CHIP | SUPER-CHIP | open | Open ⛶ |
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.
| Capability | Technique |
|---|---|
| Single-step | vm.step() executes exactly one opcode (it advances PC and mutates state, but never touches the RAF clock). |
| Registers | V0-VF, I, PC, SP, DT, ST and the XO-CHIP plane selector are read live each refresh and written back into the typed arrays. |
| Disassembly | A 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. |
| Breakpoints | When the PC-breakpoint set is non-empty the loop steps one instruction at a time and halts before executing a watched PC. |
| Watchpoints | Write 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 (
FX01plane select), a 64 KB address space with a 16-bit index register (F000long load), ranged register save/load (5XY2/5XY3), vertical scroll (00DN) and a 128-bit sampled audio buffer (F002,FX3Apitch).
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.