zxplay_go — architecture overview

zxplay_go is a hardware-faithful emulator for the Sinclair 8-bit line, written in Go. One codebase runs the ZX80, ZX81, every classic Spectrum (48K, 128K, +2, +2A, +3), the Pentagon 128, the SAM Coupé, and a from-the-silicon-up ZX Spectrum Next. In this repository (zxcode) it is vendored under packages/emulator-core/zxplay_go with a WebAssembly port applied in-tree, and it powers the browser emulator behind @zxplay/emulator.

Diagram: diagrams/system-overview.drawio.

One core, three surfaces

The whole emulator is a single Go module. Three surfaces share it, selected by build tags and CLI flags:

The surface split is thin by design. main() is one line per build tag, and the emulator struct in cmd/zxplay_go/main.go is the same machine assembly everywhere. See frontends.md.

The core pillars

Five packages carry every machine:

On top of those sit the machine stacks: pkg/next/* (the Next's FPGA hardware, by far the largest area), pkg/peripherals plus the classic device packages, pkg/zx8x (ZX80/81), and pkg/sam (SAM Coupé).

The frame loop

Diagram: diagrams/frame-loop.drawio.

Everything advances in units of one 50 Hz video frame:

  1. The pacer fires. Desktop: a 20 ms wall-clock ticker. Browser: a requestAnimationFrame loop paced by the audio clock (produce frames until ~60 ms of samples are in flight). Headless: as fast as possible.
  2. cpu.ExecuteFrame(budget) runs instructions until the frame's T-state budget is spent (FrameTStates × SpeedMultiplier — on the Next the live NR$03/NR$05 geometry, sampled per frame, so a guest timing retune applies at the frame boundary like the FPGA's vsync latch). Each instruction passes through pre-fetch hooks (divMMC, IF1, Multiface, Beta, esxDOS), the M1 fetch, the opcode switch, memory contention, and port dispatch through the ULA. The frame interrupt is a narrow T-state pulse on the Next (VHDL-derived assert point and width) and a frame-start assert on the classics. Non-raster INT sources hook the same per-instruction sample point through cpu.ExtIntFunc — on the Next that is the CTC channels' pulse-mode interrupts (pkg/next/ctcblock.go), which batch-advance the channels on the CLK_28 reference timeline and assert the line for the FPGA's 32-CPU-cycle pulse width. With the Next's hardware-IM2 vectored mode on (NR$C0 bit 0), the IM2Block (pkg/next/im2block.go) wraps that hook: frame/line pulses are rerouted into the im2 daisy chain (cpu.RouteIntFunc), CTC ZC/TO pulses feed it raw, the chain asserts the line as a level held until serviced, IM 2 acceptance takes the hardware-generated vector from cpu.IntAckFunc, and the exact pair ED 4D (cpu.OnRETI) is the end-of-interrupt.
  3. End of frame: peripherals.Frame(), kbd.Tick(), boot bookkeeping.
  4. ULA.Render() builds the frame once: it first flushes the audio events recorded during the frame into the ring buffer (event-timed box-filter reconstruction), then paints border and screen from recorded mid-frame changes, then, on the Next, runs the per-scanline compositor with the Copper stepping ahead of each row.
  5. Output. The RGBA frame goes to the Fyne canvas or is copied out by zxFrame; audio is pulled by oto (desktop) or zxPullAudio into an AudioWorklet (browser).

Timing subtleties worth knowing early:

Fidelity philosophy

The project's stated invariant (ROADMAP.md): no hacks. Every hardware question has a designated oracle, and code comments cite the oracle inline:

The cold boot is the integration test: the Next boots real NextZXOS end-to-end through the FPGA bootrom → TBBLUE firmware → NextZXOS chain, with no captured-state replay. See diagrams/next-boot-chain.drawio and next-fpga.md.

Honest status (mirrors the README): the classic line is mature and stable. The Next boots faithfully and its hardware blocks are extensively tested, but arbitrary .NEX game compatibility is the youngest area.

Styles of emulation

Different hardware earns different styles. The catalogue is in emulation-patterns.md and per-chip detail in chips.md; the short version:

Where the Next fits

pkg/next/* is roughly half the emulator. Its shape:

All of it is documented in next-fpga.md.

This repository's context (zxcode)

Upstream zxplay_go is a desktop emulator. This repo vendors it and adds:

The maintenance gotchas for the browser boot path are documented in packages/emulator-core/README.md (seed-table and menu-index coupling to the SD distro version).