-
Notifications
You must be signed in to change notification settings - Fork 0
Emulators
25 emulators are copied under emulators/ with bundled local games/discs/cartridges, driving 28 sub-systems. Seven of the emulator dirs are the VICE-libretro family — PET, VIC-20, MAX, C64, C16, Plus/4, C128 — and an eighth EmulatorJS bundle is coleco/ (gearcoleco). All eight share one copy of the EmulatorJS framework via emulators/_shared-ejs/ (saves ~13 MB vs per-bundle copies; the 5 unique VICE cores + the gearcoleco core 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/jsdos/ |
MS-DOS | caiiiycuk/js-dos v7.0.0 |
js-dos.js + wdosbox.wasm
|
10 flat titles + DUKE submenu (3) + KEEN submenu (7) — 20 .jsdos bundles in bundles/
|
emulators/webmsx/ |
MSX / MSX2 / MSX2+ | ppeccin/WebMSX (mirror of webmsx.org) | Single 5 MB index.html (engine + UI + BIOSes inlined) |
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 + js/jsvecx.js (~31 MB total) |
~100 commercial+homebrew .bin organised by publisher/author folder under roms/
|
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: 5× .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 |
9× .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 |
8× .crt carts + MAX BASIC cart (games/). Sources: TOSEC + Internet Archive Slalom + bank-extracted from the Multimax EasyFlash compilation. |
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: ~9 MB.
- The user picks a number in a
GAMESmenu. -
N.batruns the named launcher, e.g.sonic1. -
sonic1.batis alink:entry pointing at the emulator's URL. - 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, jsdos, 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> — note: use index.html (the default /), never seamless.html. seamless is iframe-embed-only; standalone it loads the ROM but renders the canvas at zero size due to a sizer/init race |
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). |
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:
BBCboots to BBC BASIC>,MSX1/MSX2boots to MSX BASICOk,COCOboots to Color BASIC,800XLboots to AltirraOS-XLREADY(entry label:BASIC),SPECTRUM/ZX81/CPCeach 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 smallempty.prgfor 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>/Okstyle command-line systems.
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: 19 platforms / 190 pages complete — Apple I, Apple ][, Odyssey², ColecoVision, Intellivision, NES, Atari 2600, Atari 7800, Vectrex, Atari 800XL, Atari 400, BBC Micro, Acorn Electron, BBC Master, ZX Spectrum, ZX81, Amstrad CPC, Commodore PET, Commodore VIC-20. Console tier ✅ complete; Atari 8-bit family ✅ complete; Acorn family ✅ complete; Sinclair family ✅ complete; Amstrad CPC ✅; Commodore PET + VIC-20 ✅. 9 sub-systems still pending (MAX, C64, C16, Plus/4, C128, MSX1, MSX2, CoCo, DOS). 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.
Same recipe each time:
-
Copy the emulator's deployed bundle into
emulators/<name>/(mirror withwgetif it's hosted, or build from source if you must). Drop the upstreamnode_modules, the inner.git, any.gitignorethat excludes deploy artefacts, and any upstreamREADME.md/ dev tooling (.claude/, source-map.mapfiles, etc.). -
Strip telemetry and any other external runtime fetches from the source's
index.htmland 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. PatchedcheckForUpdates()to a no-opreturn;. - Hardcoded NetPlay / multiplayer relay servers (Javatari had
SERVER_ADDRESS: "webmsx.azurewebsites.net/"for keep-alive + WebSocket). Set the address to""so any guardedaddress && …short-circuits and the wss:// URL becomes invalid. - CSS web-font
@importrules pointing atfonts.googleapis.comor 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.
- Inline analytics (
-
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. -
Write
play.htmlif 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.
- reads
-
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. -
Wire
fs.js: each game's.batgets alink:field pointing at the URL pattern above. Per-systemGAMESmenu follows the gold-standard 45-char box format (see Customising the Filesystem#menu-box-format) with 10 titles + a PROMPT/DESKTOP entry.
Some platforms have multiple machines under the same vendor — e.g. ATARI has 2600 / 7800 as siblings under CONSOLE and 400 / 800XL / ST 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.
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.)
Same hierarchical idea applied to a single emulator: a single menu entry can promote into a submenu of related titles. DOS uses this for DUKE (Duke Nukem 1/2/3D) and KEEN (Commander Keen 1-6 + Keen Dreams) — each is a [GAMES] entry on the parent DOS menu that routes into its own subdir. The launcher recipe is unchanged from the flat case (each game still resolves to ?game=<key> in the same bundles/ directory); only the navigation layer adds a level.
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.
The seamless launcher does load ROMs correctly but renders the canvas at zero size in a standalone tab — the sizer races with init and ends up shrinking the canvas to nothing. Always link index.html?rom=… (the bundle's full UI) instead.
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 preRun → createO2EM(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.
./serve.sh # or: python3 -m http.server 8765After editing fs.js or any emulator wrapper, hard-refresh the prompt page (Ctrl+Shift+R). Browsers aggressively cache fs.js between visits.