Search › A-Z › S › Sinclair QL
Sinclair QL
The Sinclair QL runs here in your browser. It is sQLux - the actively-developed SDL2 successor to uQLx, compiled to WebAssembly with Emscripten, booting SuperBASIC on the open Minerva QDOS-replacement ROM. The QL's processor is the Motorola 68008: the 68000 core with an 8-bit external bus, so it runs the full 68000 instruction set.
This build was rebuilt from source with a small debug sampling API over uQLx's plain-C register file, so the shared in-browser debugger gets genuine live 68000 registers (D0-D7, A0-A7, PC, SR, USP/SSP), side-effect-free memory, a real single-instruction step, and pause/resume, the same debugger the JavaScript machines have. Type on the physical keyboard, or use the on-screen QL keyboard on mobile.
Runs on: any modern web browser
Sinclair QL Online Emulator
Play Sinclair QL using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Sinclair QL (Minerva ROM) | Sinclair QL | Sinclair QL | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
sQLux is a C emulator built around SDL2; we compile it to one Emscripten module - sqlux.js loads sqlux.wasm, and a preloaded sqlux.data carries the Minerva ROM and a small sqlux.ini. Its main() parses the ini, allocates the QL address space, loads the ROM and installs an Emscripten main loop; we self-host all three files (no CDN).
// Module must exist before sqlux.js runs; SDL2 draws into Module.canvas.
window.Module = {
canvas: document.getElementById('canvas'),
locateFile: function(f){ return SRC + f; }, // find sqlux.wasm / sqlux.data
onRuntimeInitialized: function(){ publishBoot(); } // emudbg_* are ready
};
We own no JavaScript run loop. sQLux's own emscripten_set_main_loop(emu_loop, -1, 1) drives emulation from main(). In our single-threaded rebuild each emu_loop tick runs one QL video frame of 68000 execution (unless the debugger paused it), pumps input, and blits the QL screen to the canvas. JavaScript only reads state and toggles a pause flag through the exported functions:
| Member | Kind | What it does |
|---|---|---|
Module._emudbg_reg(i) / _emudbg_set_reg(i,v) | export | Read / write D0-D7 (i=0..7) and A0-A7 (i=8..15) from the reg[16] global. |
Module._emudbg_pc() / _emudbg_set_pc(a) | export | The live program counter (the pc pointer as a 24-bit guest address). |
Module._emudbg_sr() / _emudbg_set_sr(v) | export | The 68000 status register, assembled from / written to the flag bytes. |
Module._emudbg_read_block(a,ptr,len) | export | Copy a block of the 24-bit QL address space into a heap buffer, side-effect-free (raw backing store, no QDOS I/O trap). |
Module._emudbg_step() | export | Run exactly one 68000 instruction (ExecuteChunk(1)). The debugger's single-step. |
Module._emudbg_frame_tick(n) | export | Run ~one 50 Hz frame of execution. The run loop's advance and the frame-step. |
Module._emudbg_pause(on) / _emudbg_is_paused() | export | Set / read the pause flag that emu_loop honours (checked once per frame, never per instruction). |
Module._emudbg_key(sym,pressed) | export | Feed an SDL keycode straight to the QL keyboard matrix (drives the on-screen keyboard). |
Debugger integration
sQLux is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines - registers, memory, and a real single-instruction step. A stock Emscripten build keeps all CPU state inside the WASM sandbox, so we rebuilt it from source with a small sampling API. sQLux is an ideal target for this because it inherits uQLx's design where the entire 68000 register file is plain C globals, directly readable once exported.
1 · Exactly what was changed (the only source edits). Two additive edits plus the build glue:
// emudbg.c - sampling only, no hot-path hook. Reads uQLx's CPU globals:
// w32 reg[16]; w32 usp,ssp; uw16 *pc; w32 *memBase;
// Cond trace,supervisor,xflag,negative,zero,overflow,carry; char iMask;
EMSCRIPTEN_KEEPALIVE uint32_t emudbg_reg(int i){ return reg[i & 15]; } // D0-7 / A0-7
EMSCRIPTEN_KEEPALIVE uint32_t emudbg_pc(){ return ((char*)pc - (char*)memBase) & 0xffffff; }
EMSCRIPTEN_KEEPALIVE uint32_t emudbg_sr(){ /* assemble T,S,I2-0,X,N,Z,V,C from the flag bytes */ }
EMSCRIPTEN_KEEPALIVE void emudbg_read_block(uint32_t a, uint8_t *o, int n){ /* raw memBase copy */ }
EMSCRIPTEN_KEEPALIVE void emudbg_step(){ ExecuteChunk(1); } // REAL one-instruction step
EMSCRIPTEN_KEEPALIVE void emudbg_key(int sym, int pressed){ QLSDProcessKey(&ks, pressed); }
// + set_reg / set_pc / set_sr / usp / ssp / read / write / pause / is_paused / frame_tick
The step primitive is uQLx's own ExecuteChunk(n) (in iexl_general.c), which fetches and dispatches exactly n 68000 instructions with correct interrupt and trace handling - so ExecuteChunk(1) is a genuine single-instruction step, and the PC and registers change by one instruction per click. Memory reads copy straight out of memBase (the malloc'd QL RAM/ROM), so auto-polling the hex/disasm view never fires a QDOS I/O trap.
2 · The second edit, single-threaded, so it hosts anywhere. The stock sQLux WASM target runs the CPU in an SDL_CreateThread(QLRun) worker paced by a 50 Hz metronome thread, which needs SharedArrayBuffer and cross-origin-isolation headers. We dropped -pthread and rewrote emu_loop() to drive the machine itself: each browser animation frame it calls emudbg_frame_tick() (raise the QL frame interrupt with dosignal(), then ExecuteChunk(~60000)), drains SDL input events, and repaints. Pause simply skips that call. This is what makes pause/step exact and lets the page load with no special server headers.
3 · What is REAL here.
- Real registers - D0-D7, A0-A7, PC, SR (with T S, the I2-I0 mask, and X N Z V C flags), plus USP/SSP, read and written live off the uQLx register file.
- Real single-instruction step -
Step icallsExecuteChunk(1), advancing the 68000 by exactly one instruction. - Side-effect-free memory - reads use the raw
memBasebacking store, so the hex and disassembly views never disturb the running machine. - Frame-step + pause/resume - a single pause flag checked once per frame; the coarse step runs one 50 Hz frame.
What is approximate. There are no execution breakpoints or watchpoints: uQLx's dispatch loop has no cheap native guard facility, and adding a per-instruction PC check would violate the project's golden performance rule. The debugger surfaces registers + memory + real single-step + frame-step + pause instead. Frames are paced at the browser's ~60 Hz rather than the QL's exact 50 Hz, and the default run is lightly turbo (~60000 instructions/frame) so it boots quickly.
Architecture
The Sinclair QL (1984) is built on the Motorola 68008 - the 68000 core with an 8-bit external bus, so the full 68000 instruction set and the shared m68000 disassembler apply. sQLux emulates the whole machine in C:
- 68008 CPU - uQLx's table-driven interpreter (
iexl_general.c,instructions_ao.c,instructions_pz.c,xcodes.c). The register file is the globalreg[16](D0-D7 then A0-A7);ExecuteChunk(n)runs n instructions. - Memory -
memBaseis the base of the 24-bit QL address space: the 48K QDOS/Minerva ROM at $000000, the 32K screen at $020000, then RAM.memaccess.chandles the bus. - ZX8301/8302 (“ULA”) - the display and the keyboard/serial/microdrive controller, emulated in
QL_hardware.c/QL_screen.cand blitted to an SDL2 texture (src/SDL2screen.c). - Boot ROM - Minerva, an open, freely-redistributable reimplementation of the QL's QDOS operating system ROM. With Minerva and no microdrive/disk, the QL reaches its F1/F2 startup screen and then the SuperBASIC interpreter, a live, non-black QL picture.
How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc / emcmake / emmake on PATH), cmake, make.
git clone --recurse-submodules https://github.com/SinclairQL/sQLux.git
cd sQLux
# 1. add emudbg.c (the ~16 EMSCRIPTEN_KEEPALIVE sampling funcs) at the repo root
# 2. CMakeLists.txt (Emscripten branch): drop -pthread/-sUSE_PTHREADS + the IDBFS
# win-drive preloads; add emudbg.c to target_sources; add the emudbg_* names
# to -sEXPORTED_FUNCTIONS with ccall,cwrap,HEAPU8 in EXPORTED_RUNTIME_METHODS
# 3. src/SDL2main.c emu_loop(): single-threaded driver, no SDL_CreateThread,
# call emudbg_frame_tick()/QLSDLProcessEvents()/QLSDLForceRefresh() each tick
# 4. cp roms/Minerva_1.98a1.bin roms/MIN198.rom (SYSROM in sqlux_wasm.ini)
emcmake cmake -B wasm -DCMAKE_BUILD_TYPE=Release .
cd wasm && make -j4 # -> sqlux.js + sqlux.wasm + sqlux.data (vendored here)
The build is single-thread (no SharedArrayBuffer), so it runs from any static host. The only source changes are the additive emudbg.c and the single-threaded emu_loop; nothing on the emulation hot path is touched.