Search › A-Z › I › Intellivision (jzIntv)
Intellivision (jzIntv)
jzIntv, by Joseph Zbiciak, is the reference emulator for the Mattel Intellivision and its 16-bit General Instrument CP-1610 CPU, STIC video chip and AY-3-8914 sound. Here its C source is rebuilt to WebAssembly, so it runs the console directly in the browser and plugs into the shared CP-1610 debugger, live registers, memory, single-step, breakpoints and watchpoints. It boots with jzIntv’s own free mini-EXEC and mini-GROM system ROMs and a demo cartridge, so no Mattel-copyrighted ROM is required.
Runs on: Web browser
Intellivision (jzIntv) Online Emulator
Play Intellivision (jzIntv) using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Intellivision (EXEC + demo) | Intellivision (jzIntv) | Intellivision | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
jzIntv is a Mattel Intellivision emulator written in C. For the web it is rebuilt from source to WebAssembly with Emscripten (SDL1 backend). You self-host just two files - jzintv.js (the Emscripten loader/glue) and jzintv.wasm (the compiled machine). jzIntv’s three free system ROMs and the demo cartridge are embedded inside the wasm through jzIntv’s own lzoe virtual filesystem (web_files.c), so there is no separate data file to ship.
var Module = {
canvas: document.getElementById("canvas"), // SDL renders the Intellivision here
arguments: [],
noInitialRun: true // we call main() ourselves after setup
};
JZINTV(Module).then(function(m){
m.callMain([]); // starts emscripten_set_main_loop; boots EXEC + demo
});
The main loop is ours. Native jzIntv runs a while(1); under Emscripten the loop body is handed to emscripten_set_main_loop and driven by requestAnimationFrame. Each frame it runs one Intellivision video frame (unless a debug-pause flag is set) and pumps SDL events. The exported control surface:
| Member | Kind | What it does |
|---|---|---|
Module._emudbg_pause(1|0) | export | Set / clear the paused flag the main loop checks once per frame. Our transport’s pause / resume. |
Module._emudbg_step_frame() | export | Run one whole video frame (CPU + STIC + PSG). Frame-step. |
Module._emudbg_step_insn() | export | Run exactly one CP-1610 instruction. Single-step. |
Module._emudbg_reg(i) / _emudbg_set_reg(i,v) | export | Read / write one register: 0-7 = R0-R7 (R6 = SP, R7 = PC). |
Module._emudbg_flags() / _emudbg_set_flags(f) | export | The six status bits packed: bit0 S, 1 C, 2 O, 3 Z, 4 I, 5 D. |
Module._emudbg_read(addr) | export | Side-effect-free 16-bit word of the CP-1610 space (RAM + ROM; 0 in I/O windows). |
Module._emudbg_key(sym, down) | export | Push an SDL keysym into jzIntv’s event queue; drives the on-screen keypad. |
Module._emudbg_reset() | export | Soft-reset the machine (re-runs the EXEC from 000). |
Debugger integration
This is the point of the Tier-4 build: how a COMPILED WebAssembly core gets the same debugger as the pure-JS emulators. jzIntv keeps its CP-1610 state inside the wasm heap, unreachable from JS. Rather than serialise it out every frame, we added one small file, src/emscripten/emudbg.c, whose functions - when CALLED by the debugger’s ~10 Hz refresh loop or the Step button - copy the state out on demand. There is no per-instruction or per-cycle hook; with the debugger closed the core’s hot path is untouched.
Where the state lives. The whole Intellivision is one global in jzintv_em.c: cfg_t intv. Its CPU is intv.cp1600, whose register file and status bits are plain struct members:
typedef struct cp1600_t {
uint16_t r[8]; // R0-R7 (R6 = SP, R7 = PC)
int S,C,O,Z,I,D; // Sign, Carry, Overflow, Zero, Int-enable, Double-byte
...
} cp1600_t;
extern cfg_t intv; // the live machine - a plain global
What emudbg.c reads. emudbg_reg(i) returns intv.cp1600.r[i]; emudbg_flags() packs the six S C O Z I D ints into one word (and emudbg_set_reg / emudbg_set_flags write them back). emudbg_read(addr) reads the 16-bit bus side-effect-free through jzIntv’s own periph_peek(), which routes to each device’s peek handler and never disturbs an I/O device, so auto-polling a memory view is safe.
| What the debugger needs | Where it comes from in the wasm core |
|---|---|
| R0-R7 (writable) | intv.cp1600.r[] via emudbg_reg / _set_reg. |
| S C O Z I D flags | intv.cp1600.S..D via emudbg_flags / _set_flags. |
| 16-bit CP-1610 bus / RAM / EXEC ROM | emudbg_read → periph_peek(intv.intv, …). |
| Disassembly | a NEW cp1610 decoder (/debugger/src/cpus/cp1610.js) pointed at emudbg_read. |
The CP-1610 decoder. No existing decoder matched, so a faithful CP-1610 disassembler was written from jzIntv’s own dis1600.c opcode dispatch. The CP-1610 is a word machine, but the debugger’s memory views hand a decoder bytes, so, as with this site’s PDP-8, each 16-bit word is exposed as two little-endian bytes and every instruction length is a multiple of two. Address operands are still shown as the true CP-1610 word value (e.g. 000 = the EXEC entry).
Pause / step. Pause and resume flip a single emudbg_paused flag that the patched main loop tests once per frame (never per instruction). Because the CP-1610 core is an interpreter with a real per-instruction entry point, single-instruction step is exact: emudbg_step_insn() ticks the CPU peripheral by one, advancing exactly one instruction, so the register and memory views change by one CP-1610 op. emudbg_step_frame() runs one whole video frame for coarse stepping.
Breakpoints and watchpoints are enforced, without violating the golden rule. jzIntv’s CP-1610 core already exposes a per-instruction hook (cp1600_instr_tick) and a step_count single-step mode. emudbg.c registers a tiny checker on that hook only while at least one breakpoint or watchpoint exists: the checker compares the program counter against the breakpoint list and each watched word against its previous value, and returns CYC_MAX to halt the instant PC reaches a breakpoint (before the instruction runs) or a watched word’s value changes. With no breakpoints or watchpoints set, the normal case, and always with the debugger closed, no hook is registered and step_count stays 0, so the core’s hot path is completely untouched. Execution breakpoints halt before the target instruction; write watchpoints halt on the instruction that changed the watched word.
Reaching another compiled core. The recipe is general: find the struct/globals holding the CPU registers and the guest-memory peek, add a handful of EMSCRIPTEN_KEEPALIVE functions that copy them out (plus a paused flag and a step that calls the core’s smallest advance), export them, and read them from JS each refresh. A core that keeps its registers in a plain struct, as jzIntv’s cp1600_t does, is fully debuggable from JS with a source change measured in dozens of lines.
Architecture
jzIntv is a full-system Mattel Intellivision: the 16-bit General Instrument CP-1610 CPU, the STIC (Standard Television Interface Chip) video, the AY-3-8914 (PSG) sound, the Intellivoice speech synthesiser, the ECS expansion, and the memory map (scratch + system RAM, GROM/GRAM graphics, EXEC ROM, cartridge). It runs real Intellivision software entirely client-side.
cp1600/*.c- the CP-1610 interpreter;cp1600_tis the register file (r[8], flagsS C O Z I D),cp1600_runexecutes it.stic/*.c- the STIC video chip: backgrounds (BACKTAB), the eight MOBs (sprites), and the colour-stack / foreground-background modes.ay8910/*.c- the AY-3-8914 programmable sound generator;ivoice/*.cthe SP0256 Intellivoice.mem/*.c,periph/*.c- the 16-bit address space and the decoupled peripheral bus (periph_tick,periph_peek).x11 / plat / gfx / snd / event (SDL)- the front end: the SDL video surface, sound, and the keyboard event map that binds PC keys to the hand controllers.- Embedded ROMs (via
lzoe):miniexec.bin(free EXEC replacement),minigrom.bin(free GROM),fake_ecs.bin, and a small demogame.bin.
The default machine boots jzIntv’s free system ROMs and a demo cartridge, so no Mattel-copyrighted EXEC/GROM is needed and the shared CP-1610 disassembler drives the disasm view correctly across RAM and the EXEC ROM.
What was patched to build this (the reproducible bits). Source: spatula-city.org/~im14u2c/intv, release 20200712. Two source changes only: (1) a new src/emscripten/emudbg.c with the sampling hooks above (added to OBJS in src/emscripten/subMakefile); (2) a small patch to src/jzintv_em.c - auto-start so the machine boots without a click (a real click still opens WebAudio), and honour emudbg_paused once per frame in the run loop. Build (Emscripten 6.0.3):
# from src/, output straight to .js (MODULARIZE needs no HTML template)
make -f Makefile.emscripten_sdl1 X=.js \
EXTRA="-sUSE_SDL=1" SDL1_LFLAGS="-sUSE_SDL=1" \
LFLAGS="-sMODULARIZE=1 -sEXPORT_NAME=JZINTV -sINVOKE_RUN=0 \
-sALLOW_MEMORY_GROWTH=1 \
-sEXPORTED_FUNCTIONS=_main,_emudbg_reg,_emudbg_set_reg,_emudbg_pc,\
_emudbg_flags,_emudbg_set_flags,_emudbg_read,_emudbg_pause,_emudbg_is_paused,\
_emudbg_step_insn,_emudbg_step_frame,_emudbg_reset,_emudbg_key,_emudbg_set_bp,\
_emudbg_clr_bp,_emudbg_set_wp,_emudbg_clr_wp,_malloc,_free \
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap,getValue,setValue,HEAPU8,HEAPU32,callMain"