SearchA-ZJ › JSSpeccy

JSSpeccy

2010 Open source · GPL-3.0 Online

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.

Visit the official site ↗

Runs on: Web browser

JSSpeccy Online Emulator

Play JSSpeccy using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
ZX Spectrum 48K BASICJSSpeccyZX Spectrum 48KgreyOpen ⛶

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:

MemberKindWhat it does
z80.runFrame(t)methodRun 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)methodRead / 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)methodThe T-state counter within the current frame; used to bound runFrame.
z80.reset() / requestInterrupt()methodReset the CPU, or raise the 50Hz maskable interrupt at frame start.
memory.read(a) / write(a, v)methodThe 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()methodRaster 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 big runFrame via 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–$3FFF is the system ROM and $4000–$FFFF is 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.