Skip to content

Emulators

Retro Jack edited this page Jun 8, 2026 · 71 revisions

Emulators

24 emulators are copied under emulators/ with bundled local games/discs/cartridges, driving 27 sub-systems. Seven of the emulator dirs are the VICE-libretro family — PET, VIC-20, MAX, C64, C16, Plus/4, C128 — and two more EmulatorJS bundles are coleco/ (gearcoleco) and jsnes/ (FCEUmm; the dir name predates a migration from bfirsh/jsnes to EmulatorJS+FCEUmm). All nine share one copy of the EmulatorJS framework via emulators/_shared-ejs/ (saves ~25 MB vs per-bundle copies; the 5 unique VICE cores + the gearcoleco + FCEUmm cores live alongside the framework there too). Two bundles stand alone with custom WASM frontends because EmulatorJS doesn't ship — or can't host — their cores: Intellivision (intellivision/, jzIntv) and Odyssey² (odyssey2/, libretro-o2em statically linked into a small SDL2/emscripten frontend; vanilla libretro cores fail in EJS because GameManager wraps EJS-specific extension functions that don't exist in upstream cores — see the gotcha below). Once python3 -m http.server is running (or ./serve.sh), they need no network — every byte is on the same origin.

Folder System Source Runtime ROMs / discs
emulators/apple1/ Apple I scullin/apple1js (MIT) apple1.js — pure-JS 6502 + Woz Monitor emulator 10× cassette tapes (Integer BASIC + 6502 assembly) in tapes/
emulators/apple2/ Apple ][+ whscullin/apple2js (MIT) — webpack 5 production build dist/main2.bundle.js + dynamic chunks (~2.3 MB) 10× .json disk images in json/disks/ (converted from .dsk via dsk2json)
emulators/jsbeeb/ BBC Micro + BBC Master 128 mattgodbolt/jsbeeb v1.12.0 Vite-built dist/ BBC: 10× .ssd. Master: 10× Master-enhanced .ssd/.dsd (same dist/discs/ dir, launched with ?model=Master). Elite Master uses .dsd (double-sided) for *DRIVE 2 access
emulators/electron/ Acorn Electron dmcoles/elkjs ElkJS pure-JS emulator + jQuery (jquery-1.9.1.js + jquery-ui) 10× .uef snapshot files — games load instantly from snapshots
emulators/jsnes/ NES EmulatorJS + FCEUmm libretro core (GPL-2.0) Shared EJS framework (_shared-ejs/) + fceumm-legacy-wasm.data (~1.05 MB) 10× .nes in roms/
emulators/javatari/ Atari 2600 ppeccin/javatari.js v5.0.4 javatari.js 10× .a26 in roms/
emulators/js7800/ Atari 7800 raz0red/JS7800 v0.0.9 js/js7800.min.js + js/bupboop.wasm (YM-2151 sound chip, ~590 KB) 10× .a78 in roms/
emulators/webmsx/ MSX / MSX2 (engine also supports MSX2+, not exposed) ppeccin/WebMSX (mirror of webmsx.org) Single 5 MB index.html (engine + UI + BIOSes inlined — includes MSX1 + MSX2 + MSX2+ system BIOSes) 10× MSX1 zips in games/msx1/ + 10× MSX2 zips in games/msx2/
emulators/jsvecx/ GCE Vectrex (Smith Engineering, 1982) DrSnuggles/jsvecx (raz0red's JS port of Valavan Manohararajah's VecX) index.html only (single-file minified bundle; the entire engine is inlined). Total bundle ~22 MB (was ~31 MB before the 07/06/2026 prune of upstream alternate entry points) ~500 ROM archive — 24 commercial in roms/Commercial/ + ~470 homebrew/demo subdirs; menu exposes 11 curated entries
emulators/atari800/ Atari 400 + Atari 800XL (same emulator, different boot machine) atari800/atari800 v5.2.0 (GPL-2.0+) — built from source to WASM via emscripten atari800.js (186 KB) + atari800.wasm (1.7 MB), AltirraOS-XL 3.41 + AltirraOS-800 (OS-B) + AltirraBASIC baked in (no copyrighted Atari OS ROM needed) 800XL: 9× .atr + 1× .rom cart (disk-era titles). 400: 6× .rom carts + 5× .atr (cart-era titles, shares starraid.rom with 800XL).
emulators/jsspeccy/ Sinclair Spectrum Matt Westcott / gasman's JSSpeccy 3 v3.2 (GPL-3.0) WASM Z80 core in Web Worker (jsspeccy-core.wasm 139 KB + jsspeccy-worker.js) + jsspeccy.js shell 10× .z80 snapshots in games/, 5 ROMs + 5 tape-loader SZX in jsspeccy/
emulators/jtyone/ Sinclair ZX81 Simon Holdsworth's JtyOne (port of Mike Wynne's EightyOne) Pure-JS Z80 core zx81_emu.js (141 KB) + roms/zx81.rom (8 KB) 10× .p.hex tapes in tapes/ (raw bytes hex-encoded — JtyOne's load format)
emulators/xroar/ Tandy Color Computer Ciaran Anscomb's XRoar v1.10 (GPL-3.0+) xroar.js (180 KB) + xroar.wasm (1.2 MB) + 9 system ROMs 10× .ccc cartridges in roms/
emulators/pet/ Commodore PET (4032 default, 3032 most games, per-game override for PETSCII Robots → 4032) EmulatorJS + VICE xpet libretro core (GPL-2.0). Mirrored from cdn.emulatorjs.org/stable/. play.html sets EJS globals incl. vice_pet_model + reads optional per-game model field from games.json .prg classics + PETSCII Robots .d64 (games/)
emulators/vic20/ Commodore VIC-20 EmulatorJS + VICE xvic libretro core (GPL-2.0) play.html sets EJS_core='vice_xvic' + vice_vic20_memory_expansions='all' for full 35K expansion 10× cart .prg (games/)
emulators/max/ Commodore MAX Machine (1982 Japan-only console) EmulatorJS + VICE x64 libretro core in Ultimax mode (vice_c64_model='ultimax') play.html sets EJS globals + Ultimax model 10× .crt carts + MAX BASIC cart (games/). Sources: TOSEC + Internet Archive Slalom + bank-extracted from the MultiMax EasyFlash compilation (Mole Attack, Money Wars added 07/06/2026).
emulators/c64/ Commodore 64 EmulatorJS + VICE x64 libretro core (GPL-2.0). Mirrored from cdn.emulatorjs.org/stable/. play.html sets EJS globals (EJS_pathtodata, EJS_core='vice_x64', EJS_gameUrl) and injects ejs/data/loader.js 10× .d64 disk images (games/) — VICE auto-mounts and runs
emulators/c16/ Commodore 16 EmulatorJS + VICE xplus4 libretro core in C16 mode (vice_plus4_model='c16pal') play.html shares xplus4 core w/ Plus/4, switches into 16K C16 model 10× .prg first-in-series C16 16K titles (games/)
emulators/plus4/ Commodore Plus/4 EmulatorJS + VICE xplus4 libretro core (GPL-2.0) play.html sets EJS_core='vice_xplus4' 10× .prg titles (games/)
emulators/c128/ Commodore 128 EmulatorJS + VICE x128 in native mode (vice_c128_go64='disabled') play.html sets EJS_core='vice_x128' + per-game video field toggles VICII (40-col) vs VDC (80-col); CSS scaleY(2.084) on the canvas + overflow:hidden on #game works around VICE x128's 856×288 framebuffer letterboxing in 4:3 5 commercial C128-distinctive builds + 5 homebrew 80-col titles (games/)
emulators/coleco/ ColecoVision (Coleco, August 1982) EmulatorJS + gearcoleco libretro core (GPL-3.0, Drhelius). Mirrored from cdn.emulatorjs.org/stable/. play.html sets EJS_core='coleco' + EJS_biosUrl='colecovision.rom' (bundle-local, ©1982 Coleco, MD5 2c66f5911e5b42b8ebe113403548eee7) 10× .col cart images in games/ — 1982/83 launch-era classics, Donkey Kong is the pack-in title
emulators/amstradcpc/ Amstrad CPC (1984, 6128 / 128 K / Locomotive BASIC 1.1) floooh/chips-test "tiny8bit" CPC WASM (MIT, Andre Weissflog). Mirrored from floooh.github.io/tiny8bit/. Custom play.html rewrites the URL's ?game=KEY into ?file=<dsk>&input=<boot-cmd> so floooh's sokol_args reads them at WASM init. No BIOS needed; tiny8bit auto-mounts .dsk to drive A:. No ?game= → plain Locomotive BASIC. 10× .dsk disk images in games/ — first-in-series CPC classics from TOSEC
emulators/intellivision/ Mattel Intellivision (1979) jzIntv (Joe Zbiciak) compiled to WebAssembly. WASM bundle mirrored from mholzinger/intellivision-overlay-editor. Custom play.html — fetches exec.bin + grom.bin + selected ROM, hands them to Module.launchJzintv() which writes them into the WASM FS and runs jzintv. Not an EmulatorJS-framework bundle (no coreintv core exists in EJS). 10× .int cart images in games/ — Astrosmash, BurgerTime, TRON, Snafu, B-17 Bomber, Night Stalker, Adventure (AD&D), Atlantis, Lock 'N' Chase, Star Strike
emulators/odyssey2/ Magnavox Odyssey² (1978) / Philips Videopac G7000 libretro/libretro-o2em (GPL-2.0+, original by Daniel Boris / Andre de la Rocha) built locally from source via emsdk + a small custom SDL2/emscripten frontend (frontend.c). o2em.js (172 KB) + o2em.wasm (840 KB) + o2rom.bin (1 KB, ©1978 Magnavox/Philips). play.html fetches BIOS + selected ROM, writes both into MEMFS via Module.preRun, then calls createO2EM(Module). Modularized build (EXPORT_NAME=createO2EM) so play.html drives the runtime lifecycle. Reproducible recipe: emulators/odyssey2/build.sh. 10× .bin cart images in games/ — Cosmic Conflict! (1979), Alien Invaders - Plus! (1980), K.C. Munchkin! / Quest for the Rings! / UFO! (1981), Atlantis / Demon Attack (Imagic) / Pick Axe Pete! / Smithereens! (1982), Killer Bees! (1983)

Shared-EJS note: all 7 VICE bundles + coleco/ + jsnes/ share one copy of the EmulatorJS framework at emulators/_shared-ejs/, along with all 5 unique VICE cores, the gearcoleco core, and the FCEUmm NES core. Each <bundle>/ keeps only play.html, controls.html, games.json, and games/* (plus colecovision.rom for coleco). Total shared dir: ~11 MB (8 MB cores + ~3 MB framework, including the localization JSON set).

How a game launches

  1. The user picks a number in a GAMES menu.
  2. N.bat runs the named launcher, e.g. sonic1.
  3. sonic1.bat is a link: entry pointing at the emulator's URL.
  4. The terminal opens that URL in a new tab via window.open(link, "_blank").

URL patterns vary by emulator:

Emulator URL pattern
jsnes, javatari, js7800, jsspeccy, jtyone, xroar, atari800 ../emulators/<name>/play.html?game=<key> — wrapper reads ?game=<key>, looks up {title, rom} in games.json, fetches the ROM, starts the emulator. For atari800, games.json entries can include a cartType field (e.g. Star Raiders cartType:1 = Standard 8KB) — the wrapper passes -cart-type N -cart /game.rom for headerless cart dumps and /game.atr for disk images. atari800 also accepts a ?machine= URL param to pick the boot machine: 400/800 (→ -atari / OS-B / AltirraOS-800), xl/800xl (→ -xl / AltirraOS-XL, the default), xe/130xe (→ -xe), xegs (→ -xegs). One atari800.wasm build, two visible sub-systems (HOMECOMP\ATARI\400 and HOMECOMP\ATARI\800XL), separated only by the URL param each menu's .bat links pass.
pet, vic20, max, c64, c16, plus4, c128 ../emulators/<name>/play.html?game=<key> — same wrapper pattern as above. games.json entries support optional per-game model (PET 3032 default → 4032 for PETSCII Robots) and video (C128 VICII default → VDC for 80-col titles) overrides. All seven VICE bundles ship the same input config: keyboardInput: 'enabled' + altKeyboardInput: 'enabled' (typed keys → VICE keyboard callback) + vice_joyport_type: '1' for the six with a joystick port (PET has no joystick). Numpad 8/4/6/2 drive joystick directions; 0 or 5 is fire; everything else types. Esc is browser-captured so RUN/STOP is remapped to Scroll Lock (+ Pause/Break + a top-left soft button) via emulators/_shared/genx-vice-softkeys.js.
electron ../emulators/electron/play.html?game=<key> — ElkJS loads <key>_snap.uef after a 3-second boot delay (OS ROM init). Games map in an inline games object in play.html. Sound activates on first keypress (browser autoplay policy).
apple2 ../emulators/apple2/play.html?game=<key> — inline script rewrites ?game= to apple2js's ?disk= form. No ?game= drops to Applesoft BASIC ] prompt.
jsbeeb (BBC) ../emulators/jsbeeb/dist/?disc1=<publisher>/<title>.ssd&autoboot?autoboot triggers *EXEC !BOOT; drop it to land at the BASIC > prompt
jsbeeb (Master) ../emulators/jsbeeb/dist/?model=Master&disc1=<publisher>/<title>.ssd&autoboot — same as BBC but ?model=Master boots the Master 128 (65C12, MOS 3.20). Elite Master uses .dsd (double-sided disc) because its !BOOT does *DRIVE 2 (side 1 of drive 0). No ?disc1= lands at BBC BASIC > prompt
webmsx ../emulators/webmsx/?ROM=games/<sub>/<name>.zip[&M=MSX1] — WebMSX auto-detects the cartridge mapper; MSX1 menu adds &M=MSX1 for authentic 8KB BIOS; SD Snatcher uses ?ANY=… because its zip contains 4 .DSK images, not a cart ROM
jsvecx ../emulators/jsvecx/?rom=<dir>/<title> — only index.html is shipped. The upstream's alternate launchers (seamless.html, rtm.html, vectrex.html, etc.) were pruned 07/06/2026 as orphans; if you import a fresh upstream copy, only re-add index.html.
amstradcpc ../emulators/amstradcpc/play.html?game=<key> — wrapper looks up {title, rom, input} in games.json and rewrites the URL to ?file=<rom>&input=<boot-cmd> before tiny8bit's WASM init. floooh's sokol_args reads ?file= to mount a .dsk and ?input= to type a run"<file> boot command. No ?game= lands at plain Locomotive BASIC.
intellivision ../emulators/intellivision/play.html?game=<key> — wrapper fetches exec.bin + grom.bin + the chosen .int, writes them into the WASM FS, calls Module.launchJzintv(). Custom desk.kbd keyboard map preloaded into jzintv.data provides numpad-as-keypad + arrow-keys-on-disc mappings the stock minimal map omits.
odyssey2 ../emulators/odyssey2/play.html?game=<key> — wrapper uses the shared genxLoadGame() helper to resolve ?game=<key> against games.json, fetches o2rom.bin (BIOS) and the chosen .bin in parallel, writes both into MEMFS via Module.preRun, then calls createO2EM(Module). Modularized emcc build hands the Module instance into the preRun callback as its first argument (not this).

DESKTOP vs PROMPT entries

Each emulator's GAMES menu has a "boot a clean machine" entry as item N+1:

  • PROMPT / BASIC for command-line systems — labels match what the user actually sees: BBC boots to BBC BASIC >, MSX1/MSX2 boots to MSX BASIC Ok, COCO boots to Color BASIC, 800XL boots to AltirraOS-XL READY (entry label: BASIC), SPECTRUM/ZX81/CPC each boot to their ROM BASIC (ZX BASIC / ZX81 BASIC / Locomotive BASIC) when the emulator is launched without a ?game= query string. All eight VICE-family + 800XL bundles route through a small empty.prg for the BASIC entry; Spectrum/ZX81/CPC pass no query at all (their emulators already drop to ROM BASIC with no software inserted). Convention: PROMPT for > / Ok style command-line systems.

Controls reference pages

Every emulator dir has a controls.html documenting the keyboard / joystick mapping for that system. Same dark-amber DOS-flavoured style across all pages; the page-level CSS lives in emulators/_shared/genx-controls.css (one shared file, all pages link to it).

Per-game instruction pages are replacing the generic controls.html with researched gameplay instructions at gamedocs/<platform>/<key>.html. When a game has a gamedocs page, the controls link points there instead of controls.html. Progress: 26 platforms / 261 pages complete — Apple I, Apple ][, Odyssey², ColecoVision, Intellivision, NES, Atari 2600, Atari 7800, Vectrex, Atari 800XL, Atari 400 (10/10 inc. Choplifter), BBC Micro, Acorn Electron, BBC Master, ZX Spectrum, ZX81, Amstrad CPC, Commodore PET, Commodore VIC-20, Commodore MAX Machine (10/10 inc. Mole Attack + Money Wars), Commodore 64, Commodore 16, Commodore Plus/4, Commodore 128, MSX1, MSX2. Console tier ✅ complete; Atari 8-bit family ✅ complete; Acorn family ✅ complete; Sinclair family ✅ complete; Amstrad CPC ✅; Commodore family ✅ complete (PET + VIC-20 + MAX + C64 + C16 + Plus/4 + C128); MSX family ✅ complete (MSX1 + MSX2). 1 sub-system still pending: CoCo. The genx-controls-link.js script reads ?game=, ?tape=, or ?rom= from the URL and routes to the matching gamedocs page, falling back to controls.html when no game param is present (prompt/BASIC entries).

Every emulator's main entry HTML carries a small bottom-right controls link (fixed position, semi-transparent, Wikipedia-style external-link SVG icon). The link sits in the entry HTML itself (play.html for the wrapped emulators, the upstream index.html for the URL-driven ones — jsbeeb, webmsx, jsvecx) — not in the virtual filesystem. The DOS prompt's dir output shows only .bat launchers and menu scripts, never raw .html files. The link's styles live in emulators/_shared/genx-controls-link.css.

When adding a new emulator, write its controls.html alongside the other site assets, have it <link> the shared genx-controls.css, and inject the same <a class="gx-controls-link"> snippet + the shared genx-controls-link.css stylesheet into its entry HTML. Don't add a .bat for it in fs.js.

Adding a new emulator

Same recipe each time:

  1. Copy the emulator's deployed bundle into emulators/<name>/ (mirror with wget if it's hosted, or build from source if you must). Drop the upstream node_modules, the inner .git, any .gitignore that excludes deploy artefacts, and any upstream README.md / dev tooling (.claude/, source-map .map files, etc.).
  2. Strip telemetry and any other external runtime fetches from the source's index.html and bundled JS/CSS before deploying. Patterns to check for (each one has bitten us at least once):
    • Inline analytics (gtag(, umami, etc.) — jsbeeb and webmsx both shipped Google Analytics; removed both.
    • Framework-version checks (fetch("https://cdn.<framework>.org/.../version.json")) — EmulatorJS in c64/vic20 ran this on every bootstrap. Patched checkForUpdates() to a no-op return;.
    • Hardcoded NetPlay / multiplayer relay servers (Javatari had SERVER_ADDRESS: "webmsx.azurewebsites.net/" for keep-alive + WebSocket). Set the address to "" so any guarded address && … short-circuits and the wss:// URL becomes invalid.
    • CSS web-font @import rules pointing at fonts.googleapis.com or similar — jsbeeb's Vite-built CSS had a Lato import. Strip the directive; fall through to the existing system-font fallback chain rather than bundle the font locally unless the visual difference matters.
  3. Add the standard <noscript> overlay to every new HTML entry point — copy the block verbatim from any existing emulator page; it's identical across the project.
  4. Write play.html if the emulator needs a key-based launcher. Skip this if the emulator's own page already accepts URL params (BBC, WebMSX, JSVecX patterns). For wrappers:
    • reads ?game=<key>,
    • fetches games.json,
    • looks up {title, rom},
    • boots the emulator with that ROM.
  5. Bundle the ROMs under emulators/<name>/<roms-dir>/. archive.org and most preservation sites don't set CORS for direct browser fetch, so wrappers can't pull at runtime — every byte must be on the same origin.
  6. Wire fs.js: each game's .bat gets a link: field pointing at the URL pattern above. Per-system GAMES menu follows the gold-standard 45-char box format (see Customising the Filesystem#menu-box-format) with 10 titles + a PROMPT/DESKTOP entry.

Multi-model platforms

Some platforms have multiple machines under the same vendor — e.g. ATARI has 2600 / 7800 as siblings under CONSOLE and 400 / 800XL as siblings under HOMECOMP, MSX has MSX1 and MSX2, SINCLAIR has SPECTRUM and ZX81. The hierarchical pattern: a parent menu with 1.<MODEL1> / 2.<MODEL2> rows, each pointing into its own GAMES dir. See prompt/javascript/fs.js for the exact tree shape. Single-platform manufacturers (NES, INTV, CVISION, ODYSSEY2, VECTREX, COCO) are listed directly under their CONSOLE or HOMECOMP parent — no manufacturer middle level. Note that 400 + 800XL share one emulator build (emulators/atari800/) — the menus differ only in which ?machine= URL param their launchers pass.

Placeholder sub-systems

None at present — every CONSOLE and HOMECOMP entry has a local emulator. (Odyssey² shipped on 05/06/2026; see the recipe below for the libretro-o2em + custom SDL2 frontend build.)

Gotchas

jsbeeb's empty-schema disc URLs don't unzip. ?disc1=path/file.zip falls through to the default: branch in loadDiscImage() (main.js), which calls fdc.load("discs/" + path) and hands raw bytes to discFor(). Only the sth: / http: / https: schemas unzip. Workaround: extract the inner .ssd from each upstream .zip and serve that raw — URL becomes ?disc1=<path>.ssd&autoboot.

Copied .gitignore files silently exclude deploy artefacts. emulators/jsbeeb/.gitignore had /dist (upstream's build-artefact rule); dist/discs/.gitignore had *.ssd (anti-piracy guard). Both meant the deployed jsbeeb on GitHub was actually broken for several commits — only worked locally where the files existed on disk. When copying an upstream tree, audit every .gitignore it ships and delete them.

Cross-origin ROM fetch is consistently blocked. Tested for archi.medes.live's files-archi.medes.live (no CORS), file-hunter's file-hunter.com/MSX/ (no CORS), archive.org direct downloads (varies — some allow, some 401). Always mirror locally; don't rely on runtime CORS fetches. WebMSX's local-domain fallback (SOFTWARE_BASE_DEV='software/', ROM_BASE_DEV='emu/roms/') and Arculator's equivalent are both lifesavers — neither needed a source patch when off-domain.

EmulatorJS's EJS_pathtodata ../ prepend. data/loader.js prepends "../" to any EJS_pathtodata that doesn't start with / and isn't an absolute URL, then dynamically imports src/*.js relative to that. From a page in a sub-directory, a value like "ejs/data/" ends up resolving one level above the current dir and the loader silently fails. Workaround: set EJS_pathtodata to an absolute path derived from the document URL, e.g. location.pathname.replace(/[^/]*$/, '') + 'ejs/data/'. (Was previously listed as a reason JSNES was preferred for NES; for C64 the fix is required and the integration works fine.)

archive.org collection access varies. nintendo-entertainment-system-nes-roms-europeusa allows direct file download via https://archive.org/download/<item>/<path>. Some other items return 401/403 (auth-gated). Many items intermittently return tiny HTML 500 pages instead of the file — fall back to https://archive.org/cors/<item>/<path> for those. Probe with curl -I before scripting bulk downloads.

WebMSX bundles everything inline. The deployed index.html is ~5 MB with the engine, UI, and commercial MSX/MSX2/MSX2+ BIOSes all baked in as JS. Mirror is just that one file plus the manifest, cache.manifest, and two icons — no separate .wasm to chase. Strip the inline Google Analytics block.

MSX2 disk titles need ?ANY= not ?ROM=. SD Snatcher's zip contains 4 .DSK images (3 game disks + USERDISK), not a cart ROM. WebMSX's ?ROM= parameter assumes cartridges; ?ANY= (alias for AUTODETECT_URL) inspects the contents and routes multi-disk zips to the Drive Stack so the user can swap mid-game.

JSVecX seamless.html only works inside an iframe. Historical note: the seamless launcher loads ROMs correctly but renders the canvas at zero size in a standalone tab (sizer/init race). For that reason and because we never linked it from the menu, it was pruned 07/06/2026 along with the other upstream alternate entry points (rtm.html, vectrex.html, overlay_*.html). Only index.html ships now. Recorded here so future re-imports of upstream remember to drop them again.

JS7800's bupboop.wasm dependency is invisible from the script tags. The upstream js/ directory ships bupboop.wasm (~590 KB YM-2151 sound chip emulator) referenced by bupboop.js but never listed in index.html's <script> tags. Without it: RuntimeError: abort(both async and sync fetching of the wasm failed) on every cart load. Probe the upstream js/ dir for sibling assets, not just the script tags. Also: roms/romlist-homebrew.json 404 is harmless — site.min.js looks for the upstream demo's homebrew cartlist; we don't ship one.

XRoar bundles the WASM but not the system ROMs. Despite "free open-source replacement ROMs" claims, the WASM does NOT ship them — slots come up (unpopulated) CRC32 INVALID until the BASIC ROM files are mirrored alongside. Also requires a Module.locateFile shim to strip leading slashes (WASM requests system ROMs via absolute paths like /d64_1.rom). And: upstream defines 15 ui_* callback functions the WASM invokes for UI updates — without them, you get Uncaught ReferenceError: ui_menu_clear is not defined and the emulator silently halts. Stub all 15 as no-ops if you're not rendering the upstream UI panels.

JtyOne loads tapes from custom .hex format, not .p. JtyOne's loader expects raw bytes encoded as a continuous ASCII hex string. Build with xxd -p file.p | tr -d '\n' > file.p.hex. The original .p files aren't fetched at runtime — only the .hex companions. Also: FileToLoad opens system-ROM URLs with a hardcoded leading slash ("/" + this.fileName); patch zx81_emu.js to drop the leading slash so paths resolve relative to the emulator dir. And: the error path references statusLabel which is undefined in upstream — masks all the actual loading errors until the absolute-path bug is fixed.

JSSpeccy 3 always shows a play-button overlay despite autoStart:true. Web Audio can't start without a user gesture (browser autoplay policy). The "play" overlay is unavoidable across emulators that use audio; not a bug.

Microvision attempted and dropped. The bundled JS Microvision emulators all only emulate ONE of the two CPUs the system shipped with: Berrry-app does Intel 8021, Paul Robson's MVEM does TMS1100. Picking one strands the other half of the library. Until a JS emulator covers both CPUs, Microvision sits out. Vectrex sits directly under CONSOLE for now; if a Microvision emulator surfaces later, a Smith Engineering parent menu can be re-introduced.

atari800: no pre-built WASM port exists, so we built our own. Three off-the-shelf options were ruled out before building from source: jsA8E (AnimaInCorpore/A8E, live at jsa8e.anides.de) ships no LICENSE file in its GitHub repo — all rights reserved by default, can't legally redistribute. EmulatorJS ships a5200 but no atari800 core. The official RetroArch emscripten nightly (buildbot.libretro.com/nightly/emscripten/RetroArch.7z, 760 MB) does NOT include atari800 in its ~90-core web build. So we build atari800/atari800 from source. Configure with the SDL2 software renderer (--without-opengl — emscripten's eglCreateContext fails and the canvas stays black), disable host-only features (--disable-netsio --disable-riodevice --disable-rserial --disable-rnetwork — they need clock_gettime / TTY_DEV_NAME), and bake in AltirraOS via --enable-altirra_bios (Avery Lee's freely-redistributable open-source OS, falls back automatically when no real ROM is found). Link with -sUSE_SDL=2 -sASYNCIFY -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=64MB — ASYNCIFY is mandatory because atari800's main loop in sdl/main.c is a for(;;) that would freeze the browser without it; ASYNCIFY suspends/resumes at SDL_Delay. Output: 1.7 MB WASM + 186 KB JS. Custom play.html wrapper preloads the ROM into the in-WASM virtual FS via Module.FS.createPreloadedFile and passes the path as atari800's first positional arg (autoboots disks) or -cart + -cart-type N (for raw .rom cart dumps).

Odyssey²: vanilla libretro cores are NOT drop-in for EmulatorJS. First attempt was to mirror the prebuilt o2em_libretro.{wasm,js} from the libretro emscripten nightly into EJS's cores/ dir, register "o2em":["o2em"] in src/consts.js, and rebrand the factory variable from libretro_o2em to EJS_Runtime. The core loaded, but EJS_GameManager immediately crashed with this.Module.cwrap is not a function — newer emcc doesn't export cwrap unless asked. Polyfilling cwrap only pushed the error to the next line: GameManager wraps a dozen EJS-specific extension functions (system_restart, simulate_input, toggleMainLoop, cmd_take_screenshot, ejs_set_variable, get_core_options, …) that don't exist in vanilla libretro cores — they're added by EJS's own forks (github.com/EmulatorJS/Gearcoleco, EmulatorJS/FreeIntv, etc.). The drop-in path is structurally dead unless EJS upstream adopts the core. We pivoted to a custom build: libretro-o2em compiled with platform=emscripten STATIC_LINKING=1 produces o2em_libretro_emscripten.bc (actually an ar archive, not LLVM bitcode despite the name — rename to .a). Link with a ~200-LOC SDL2/emscripten frontend (emulators/odyssey2/frontend.c) implementing the standard libretro callbacks: RGB565 → ARGB8888 video conversion, 16-bit stereo audio queued through SDL_QueueAudio, RETRO_DEVICE_KEYBOARD polled from SDL key state. Don't allocate the framebuffer on the stack — emscripten's default stack is 64 KB, a 384×269×4-byte ARGB buffer overflows it. Make it static. STATIC_LINKING=1 skips libretro-common helpers (filestream, file_path, encoding_crc32 etc.) on the assumption the frontend provides them — compile and link those .c files explicitly from libretro-o2em/libretro-common/. Modularize with EXPORT_NAME=createO2EM so play.html can drive the lifecycle (fetch BIOS + ROM → write to MEMFS via preRuncreateO2EM(Module)). In modularized builds, preRun callbacks receive the Module instance as their first argument; this is not reliably the Module — use function (mod) { mod.FS.writeFile(...) }. Reproducible recipe: emulators/odyssey2/build.sh.

Star Raiders is a cartridge masquerading as a disk on archive.org. The a8b_Star_Raiders_1979_Atari_US_k_file item ships an 8.7 KB .atr that wraps the 8 KB cart binary in an empty-disk container — boots straight into a 6502 CIM at $A184. Use a8b_cart_Star_Raiders_1979_Atari_US/Star_Raiders_1979_Atari_US.rom (8 KB raw cart) with cartType:1 (Standard 8KB) in games.json so the wrapper passes -cart-type 1 -cart /game.rom. Pattern applies to any Atari 8-bit cart-only title — check the a8b_cart_* items first.

Testing changes

./serve.sh        # or: python3 -m http.server 8765

After editing fs.js or any emulator wrapper, hard-refresh the prompt page (Ctrl+Shift+R). Browsers aggressively cache fs.js between visits.

Clone this wiki locally