Documentation › Browser build
WebAssembly (browser) build
Phosphoric compiles to WebAssembly via Emscripten: the full emulator (6502 CPU,
memory, VIA, PSG, ULA, keyboard, cassette, disk) runs in a tab, rendered on a <canvas>,
audio through Web Audio, keyboard through the DOM — reusing the existing SDL2 path (Emscripten's SDL port).
Prerequisites
An active Emscripten SDK (emcc on the
PATH):
git clone https://github.com/emscripten-core/emsdk
cd emsdk && ./emsdk install latest && ./emsdk activate latest
source ./emsdk_env.sh
Build and run
make wasm
(cd web && python3 -m http.server 8000)
# open http://localhost:8000/phosphoric.html
make wasm produces, in web/: phosphoric.html (page + canvas),
phosphoric.js, phosphoric.wasm, and phosphoric.data (the
roms/ ROMs preloaded into the virtual filesystem). The page boots the Atmos
(-r /roms/basic11b.rom); click the screen for keyboard focus and audio.
Deployment: required assets + CSP
The page loads its logic (the Module definition, UI, keyboard, drag-and-drop) from
web/shell.js — an external file that must be deployed alongside
phosphoric.html/.js/.wasm/.data. shell.js is
versioned (source) and referenced by phosphoric.html via
<script src="shell.js">.
This externalisation makes the bundle compatible with a strict Content-Security-Policy.
A host serving the page under script-src 'self' 'wasm-unsafe-eval' would block an inline
<script> (script-src-elem) → Module never defined →
Module.canvas undefined → a fatal error at the WebGL createContext. By
externalising all the JS (and replacing the canvas's inline oncontextmenu attribute with a DOM
listener), phosphoric.html has no inline script/handler at all: it runs under a
strict CSP just as under a permissive one (GitHub Pages).
Minimum required CSP
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'
'self'— allowsshell.jsandphosphoric.js(both external).'wasm-unsafe-eval'— required:phosphoric.jscompiles the module viaWebAssembly.instantiateStreaming/instantiate, blocked underscript-src 'self'alone. It is the WASM sub-token (≠'unsafe-eval', far broader) — safe.
⚠️ Do not remove 'wasm-unsafe-eval': under a bare
script-src 'self', WebAssembly compilation is refused and the emulator does not start. After the
externalisation fix, neither 'unsafe-inline' nor hash/nonce are needed — only the WASM token is.
A script-src-elem block whose source is sandbox eval code (not
phosphoric.html) comes from a browser extension, not Phosphoric: the page injects
no inline script or eval. No effect on the emulator.
Interface (web page)
The page shows a vertical icon rail on the left (JOric-style) and the ORIC keyboard as an overlay:
- MODEL — toggle ORIC-1 / Atmos (badge
1/A, cold restart with the chosen ROM). - LOAD + drag-and-drop a
.tap/.dskonto the screen: the file is inserted and the machine reboots onto it. EJECT to remove it. - Deep links (URL parameters) —
?rom=oric1|atmospicks the machine and?media=<file>loads media on first load. The type is inferred from the extension:.tap→ cassette,.dsk→ floppy (the Microdisc controller is enabled at boot). The target file must be served as binary. - RESET — cold reboot keeping ROM and media.
- KEYS — show/hide the virtual keyboard.
- FULL — fullscreen (canvas centred, 240/224 ratio preserved).
- CRT — scanline + vignette filter (state remembered).
- SAVE / REST — save state to a
.ost(downloaded) / restore a.ostlive, without reboot. - TAPE / DISK LEDs — light up during a CLOAD (cassette) or a disk access (WD1793 BUSY).
- Faithful virtual ORIC keyboard, semi-transparent overlay: real layout (ESC, CTRL, FUNCT, 2× SHIFT, RETURN, DEL, SPACE, arrows) with sticky CTRL / FUNCT / SHIFT modifiers. On ORIC-1, the FUNCT key (Atmos-only) is absent.
CTRL+T and other chords: the browser reserves some shortcuts (CTRL+T = new tab) at the OS level. Use the virtual keyboard's CTRL key: it writes the ORIC matrix via a direct C call, so the browser does not intercept it.
Serve files over HTTP (not file://): the browser refuses to load a
.wasm from the local filesystem.
Technical details
- Main loop: in the browser, the C
whileloop yields to the event loop each frame, via Asyncify (-sASYNCIFY) +emscripten_sleep()in the frame limiter — which also paces at ~50 Hz. - Stack:
emulator_tis large (framebuffer + memory) and lives onmain()'s stack; the build forces-sSTACK_SIZE=8MB. - Networking: features that require native sockets/threads (TCP/PTY/COM serial, GDB stub, Cast server, TLS) link as no-ops in the browser — the machine core, video, audio, keyboard, cassette and disk all work.
Fidelity — verified
The WASM output is byte-identical to the native build for identical inputs: a headless Atmos boot compiled to WASM and run under Node.js produces the exact same PPM screenshot as the native binary (tested at 2M and 5M cycles). The core's cycle-accurate determinism is preserved across the WebAssembly compilation.
Browser rendering was also validated: the page loaded in headless Chromium shows the correct Atmos boot screen,
and keyboard input works (typing PRINT 6*7 + RETURN shows 42) — ROM boot → keyboard
injection → BASIC execution → rendering, entirely in the browser.
Reproduced from docs/wasm.md (authoritative, up-to-date version on GitHub).