SearchA-ZS › SimCoupe

SimCoupe

1999 Open source · GPL-2.0-or-later Online

SimCoupe, by Simon Owen, is a SAM Coupé emulator written in C++. This build runs it in the browser as WebAssembly and boots straight to SAM BASIC. It uses Kosarev's cycle-accurate Z80 core, and is wired to the emulators.org in-frame debugger so you can single-step the Z80, read and write registers and the paged 64K memory, and set execution breakpoints.

Visit the official site ↗

Runs on: Web browser

SimCoupe Online Emulator

Play SimCoupe using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
SAM Coupé BASICSimCoupesam-coupegreyOpen ⛶

Machine emulated

The SAM Coupé (Miles Gordon Technology / SAM Computers, 1989), a Z80B home computer, a would-be ZX Spectrum successor with a custom ASIC, four screen modes and a SAA1099 stereo sound chip.

Chips

Notes

Embedding

SimCoupe is a C++ SAM Coupe emulator. This is the SamCoupeWeb fork built to WebAssembly with Emscripten and SDL2. Vendor the three build artefacts and load the glue script; the emulator then runs its own emscripten_set_main_loop, boots to SAM BASIC with no media, and renders into a WebGL canvas with id="canvas".

Boot. Define Module before loading samcoupeweb.js: give it the canvas, the command-line arguments (which pick the SAM system ROM out of the preloaded virtual file-system), and a print hook to notice the Main::Init: Success! banner. When onRuntimeInitialized fires the export table is live and we publish window.EMU_BOOT:

var Module = {
  canvas: document.getElementById('canvas'),
  arguments: ['-usewebgl','1', '-rom','/Resource/samcoupe.rom', '-drive2','1', '-visiblearea','1'],
  locateFile: function(p){ return '/emulator/sam-coupe/src/' + p; },
  onRuntimeInitialized: function(){ /* publish EMU_BOOT (see below) */ }
};

Control. The debugger needs pause / step / register + memory access, all of which are thin JavaScript calls onto the export hooks compiled into the module (Module._emudbg_*). The run loop is owned by C, so pausing is a flag the loop honours once per frame — never a per-instruction callback:

Hook (C export)From JavaScriptWhat it does
emudbg_pause(on)Module._emudbg_pause(1)Set the emulator's g_fPaused flag; the emscripten main loop checks it once per frame and stops advancing.
emudbg_step()Module._emudbg_step()Run exactly one Z80 instruction (cpu.on_step() + events + interrupt), completing the video frame when the cycle counter wraps.
emudbg_step_frame()Module._emudbg_step_frame()Advance to the end of the current 50Hz video frame.
emudbg_pc() / emudbg_reg(i) / emudbg_set_reg(i,v)Module._emudbg_reg(3)Sample or poke one Z80 register by index (AF BC DE HL IX IY SP PC I R, the shadows, IM, IFF).
emudbg_read(a) / emudbg_read_block(a,ptr,n) / emudbg_write(a,v)Module._emudbg_read(a)Read or write one byte of the SAM's paged 64K address space, side-effect-free (straight through the paging pointers, no I/O).
emudbg_bp_set(a,on) / emudbg_bp_has(a) / emudbg_bp_clear()Module._emudbg_bp_set(a,1)Arm or clear a native execution breakpoint on a logical PC.
EMS_SamKey(samkey,down)Module._EMS_SamKey(56,1)Hold or release a SAM key by its matrix code, for the on-screen keyboard.

The physical keyboard already works through SDL2 whenever the canvas is focused, so typing is character-accurate. Everything is sampled on demand at ~10 Hz while the debugger window is open; with the window closed the emulator runs at full native speed, untouched.

Debugger integration

This is a Tier 4 integration: the CPU state lives inside a compiled wasm module, so SimCoupe was rebuilt from source with a small set of export hooks. The changes are confined to three files and add nothing to the per-instruction hot path.

1. Base/CPU.cpp — the hooks. An extern "C" block reads the live global cpu (a z80::z80_cpu<sam_cpu> from Kosarev's z80 library) and SAM's paging pointers. Registers come straight off the core's accessors; memory is read through AddrReadPtr() so it is side-effect-free:

extern "C" {
  EMSCRIPTEN_KEEPALIVE uint32_t emudbg_reg(int i){ switch(i){ case 3: return cpu.get_hl(); /* ...AF BC DE HL IX IY SP PC I R, shadows, IM, IFF... */ } }
  EMSCRIPTEN_KEEPALIVE int emudbg_read(uint32_t a){ return *AddrReadPtr(a & 0xffff); }
  EMSCRIPTEN_KEEPALIVE void emudbg_step(){ cpu.on_step(); /* + CheckEvents + maskable interrupt + frame-end */ }
  EMSCRIPTEN_KEEPALIVE void emudbg_pause(int on){ g_fPaused = on; }
}

Execution breakpoints are native and cheap. A std::set<uint16_t> of PC values is checked inside CPU::ExecuteChunk() at instruction boundaries — but only when the set is non-empty, mirroring SimCoupe's own Breakpoint::breakpoints.empty() guard, so an idle debugger costs nothing. On a hit it sets g_fPaused and returns; a if (g_fPaused) return; at the top of ExecuteChunk() makes the pause take effect immediately. Watchpoints are not wired: Memory::Write() is inlined on the hot path, and instrumenting it would violate the "no per-cycle hook" rule, so the plug-in reports watchpoints as unavailable.

2. Base/Keyboard.cpp — the on-screen keyboard. SimCoupe rebuilds its key matrix from the physical keyboard every frame in Keyboard::Update(). A 9-byte override array is merged in at the end of that function, and EMS_SamKey(samkey, down) holds or releases a key by its eSamKey matrix code — so on-screen and physical keys coexist.

3. Build glue. The new exports are added to -sEXPORTED_FUNCTIONS and getValue,setValue,HEAPU8 to -sEXPORTED_RUNTIME_METHODS. The step primitive is one Z80 instruction (SimCoupe exposes cpu.on_step()), so single-instruction stepping is exact, not coarse.

The plug-in (sam-coupe-debug.js) reads these hooks through window.EMU_BOOT and calls EmuKit.defineMachine with the Z80 register set, the paged 64K bus and an on-screen SAM keyboard. It reuses the shared z80 disassembler (/debugger/src/cpus/z80.js).

Architecture

The SAM Coupe (MGT / SAM Computers, 1989) is a Z80B machine clocked at 6 MHz — a would-be ZX Spectrum successor, largely Spectrum-screen compatible but with a custom ASIC ("ASIC"/gate-array) driving a richer display and paged memory.

  • CPU — Zilog Z80B at 6 MHz. SimCoupe emulates it with Kosarev's cycle-accurate z80 C++ core, wrapped as sam_cpu with SAM-specific memory-contention and I/O callbacks.
  • Memory — up to 512K paged into the Z80's 64K address space in four 16K sections through the LMPR/HMPR/VMPR paging registers; the 32K system ROM lives at the bottom on boot. The debugger's memory view reads the live paged bus, so it follows whatever is currently banked in.
  • Display — the ASIC offers four screen modes, from the Spectrum-compatible Mode 1 to the 256×192 16-colour Mode 4, from a 128-colour palette.
  • Sound — a Philips SAA1099 stereo sound chip (six channels), emulated via SAASound.
  • Storage — twin 3.5″ floppy drives; this build boots to BASIC with no disk.

Because the whole machine is compiled C++, the debugger reaches it through the export hooks rather than live JavaScript objects — but the resulting register, memory and step access is the same as the pure-JavaScript machines on the site.

Sound

The SAM's Philips SAA1099 already makes real sound: SimCoupe's SAASound feeds Emscripten's SDL2 audio, which runs its own WebAudio graph. So this is a native-pipeline integration — the sound is not rerouted through the site's shared EmuAudio sink, and /debugger/src/audio.js is not loaded. The context handle SDL2 creates is Module.SDL2.audioContext, and the core pushes samples through Module.SDL2.audio.scriptProcessorNode, which SDL2 wires straight to audioContext.destination.

The page must start silent until the visitor asks for sound, but SDL2 auto-resumes its context on any user gesture anywhere on the page. To keep control we splice a GainNode between the core's scriptProcessorNode and the destination and hold it at 0 while muted; a ~300 ms guard re-asserts silence (and re-splices the gain once SDL2 lazily opens audio) so a stray click never leaks sound:

function audioWire(){
  var ctx = Module.SDL2.audioContext;
  if (!_gain){ _gain = ctx.createGain(); _gain.gain.value = _muted ? 0 : 1; _gain.connect(ctx.destination); }
  var node = Module.SDL2.audio.scriptProcessorNode;   // SDL2's output node
  if (!_rerouted && node){ node.disconnect(); node.connect(_gain); _rerouted = true; }
}
function setMute(m){
  _muted = !!m; audioWire();
  _gain.gain.value = _muted ? 0 : 1;
  _muted ? Module.SDL2.audioContext.suspend() : Module.SDL2.audioContext.resume();
}

The standard mute contract lives on window.EMU_BOOT.transport as isMuted() and setMute(m); it starts muted, so the SAA1099 is inaudible until the shell's Sound button is clicked. That real click both satisfies the browser's audio-gesture rule and calls setMute(false), which sets the gain to 1 and resume()s the context; muting again drops the gain to 0 and suspend()s it.