JSSpeccy
JSSpeccy runs a 48K or 128K ZX Spectrum in the browser and is designed to be dropped into a web page, so a Spectrum game can be played inline. It powers the playable listings on several Spectrum archives.
Runs on: Web browser
JSSpeccy Online Emulator
Play JSSpeccy using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| ZX Spectrum 48K BASIC | JSSpeccy | ZX Spectrum 48K | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
JSSpeccy 2 is a set of plain-global JavaScript modules with no bundler. Vendor the core files and load them in dependency order, then drive the machine yourself rather than calling the JSSpeccy() wrapper - its internal requestAnimationFrame loop cannot be paused or stepped from outside, which the debugger needs.
Boot. The Z80 core is generated at load: call JSSpeccy.buildZ80() once to define JSSpeccy.Z80, then wire the chips together (the JSSpeccy.Spectrum constructor does the same thing internally) and run your own loop:
JSSpeccy.buildZ80({ traps: [], applyContention: true });
var model = JSSpeccy.Spectrum.MODEL_48K;
var memory = JSSpeccy.Memory({ model: model });
var display = JSSpeccy.Display({ viewport: vp, memory: memory, model: model, borderEnabled: true, settings: settings });
var ioBus = JSSpeccy.IOBus({ keyboard: kbd, display: display, memory: memory, sound: snd, contentionTable: model.contentionTable });
var z80 = JSSpeccy.Z80({ memory: memory, ioBus: ioBus, display: display });
z80.reset(); memory.reset();
(function frame(){ // one 50Hz frame = 69888 T-states
display.startFrame();
z80.requestInterrupt();
z80.runFrame(model.frameLength);
display.endFrame();
z80.setTstates(z80.getTstates() - model.frameLength);
requestAnimationFrame(frame);
})();
The machine is plain objects. Everything the debugger needs is a live handle. There is no wasm heap to reach into:
| Member | Kind | What it does |
|---|---|---|
z80.runFrame(t) | method | Run the CPU until tstates >= t and the current instruction (with any DD/FD/CB prefixes) has finished. The single-step primitive is runFrame(getTstates()+1). |
z80.getPC() / setPC(v) | method | Read / write the program counter. There are matching accessors for every pair: AF BC DE HL, the shadows AF' BC' DE' HL', IX IY SP, and I R. |
z80.getTstates() / setTstates(v) | method | The T-state counter within the current frame; used to bound runFrame. |
z80.reset() / requestInterrupt() | method | Reset the CPU, or raise the 50Hz maskable interrupt at frame start. |
memory.read(a) / write(a, v) | method | The banked 64K bus: ROM at $0000, RAM from $4000. Reads are side-effect-free, so they are safe for an auto-polling hex view. |
display.startFrame() / endFrame() | method | Raster the picture into the <canvas> around each frame's CPU run. |
Because the CPU, bus and RAM are ordinary JavaScript, the debugger single-steps with runFrame(getTstates()+1), reads and writes registers straight off the get*/set* accessors, and implements breakpoints and watchpoints as host-side checks around those calls, no changes to the emulator core.
Debugger integration
The plug-in (jsspeccy-debug.js) reads the live machine from window.EMU_BOOT and calls EmuKit.defineMachine with a transport, the Z80 register set and the memory chips. It reuses the shared z80 disassembler (/debugger/src/cpus/z80.js).
Single-step. JSSpeccy's core has no per-instruction entry point, but its frame loop condition is while (tstates < frameLength || opcodePrefix). Passing a tiny budget therefore runs exactly one whole instruction:
function stepInsn(){ z80.runFrame(z80.getTstates() + 1); }
Breakpoints are a Set of PC values; when any are set the loop runs one instruction at a time and compares getPC() before each. Watchpoints wrap memory.write and halt when a watched address is written. The interrupt is only requested at the true start of a frame, and a frame that is paused mid-way (on a breakpoint) resumes without re-triggering it, so timing stays faithful while stepping.
Architecture
JSSpeccy 2 is a readable, interpreted ZX Spectrum. Each part is a plain object hanging off the global JSSpeccy function:
Z80- the CPU interpreter, generated at run time:buildZ80()assembles the opcode handlers into one bigrunFramevia string templating (for speed), covering the base page and the CB / ED / DD / FD / DDCB / FDCB prefixes.Memory- the banked 64K bus. On the 48K,$0000–$3FFFis the system ROM and$4000–$FFFFis RAM (the screen bitmap + attributes live at$4000).Display- the ULA video: it clocks a virtual beam in step with the CPU and rasters border, bitmap and attribute colour into the canvas.IOBus- port decoding: keyboard reads on even ports, the border/beeper on port$FE, and (on the 128K) memory paging and the AY sound chip.roms['48.rom']- the 16K Sinclair system ROM, embedded as a byte array and installed as the bottom bank.
The emulator is faithful at the CPU and memory level, including contended-memory timing, and, crucially, every part is exposed as ordinary JavaScript, which is exactly what makes it a good debugging target.