The first Sega Dreamcast emulator that runs in a browser, powered by the first SH4 to WebAssembly JIT recompiler. Native performance.
This repository contains a working WebAssembly build of flyinghead/flycast as a libretro core, plus the custom dynamic recompiler that makes it fast. Download the core, load it in an EmulatorJS-style frontend with your own legally dumped BIOS and game images, and Dreamcast games run in a browser tab. Heavy 3D titles hold a locked 60fps with clean audio.
Nobody had done this before. The upstream maintainer explicitly declined WASM support. EmulatorJS doesn't list Dreamcast as a supported system. The libretro buildbot produces no Flycast WASM core. The received wisdom was that a fast Dreamcast in the browser wasn't possible, because WebAssembly forbids the self-modifying code that every dynarec depends on. Turns out it is possible. This is it.
- The port (February 2026): upstream flycast compiled to WebAssembly with Emscripten and CMake, running as a libretro core with WebGL2 rendering. On the interpreter it booted and played at a couple of FPS. Proof of life, not a product.
- The JIT (March through August 2026): a from-scratch SH4 to WASM dynamic
recompiler. SH4 machine code is decoded to Flycast's SHIL IR, compiled to
WebAssembly bytecode in the browser at runtime, instantiated via
WebAssembly.compile, and dispatched throughcall_indirectfrom a C dispatch loop. No JavaScript in the hot path. 62 of 70 SHIL ops emit native wasm. Registers are cached in wasm locals. Hot code gets fused into multi-block modules. - The hard parts: self-modifying-code detection via page generations (eliminated 98% of runtime hashing), inline fast paths for guest memory access (import crossings cut from roughly 500K per frame to 9K, which was the single biggest wall), a guest-time-debt frame pacer, compile storm management, and an AudioWorklet ring with tempo-based rate control.
- The validation methodology: every subsystem was certified against the unmodified reference interpreter through differential test harnesses. Hundreds of thousands of block-level comparisons at zero divergence, instead of playing games and hoping. If you ever want to build a correct JIT solo, this methodology is probably the most reusable thing here.
- The result: from about 2 FPS to a locked 60fps at native resolution with clean audio in the heaviest titles tested.
Beyond the core and the JIT, this repository ships a complete, reproducible ecosystem so anyone can run Dreamcast games in a browser without compiling:
- Local web server (
server.js, pure Node.js, zero dependencies):- Automatic ROM detection — scans
frontend/roms/and parses.cue/.gdifiles to know their tracks; supports.chd,.zip(with a.gdi- tracks),
.cueand.gdi. The menu shows every game with no setup.
- tracks),
- Automatic BIOS detection — reports which BIOS files are present and
which are missing (
dc_boot.bin,dc_flash.bin, optionaldc_nvmem.bin), so the launcher can warn the user before starting. - Core cache-busting —
/api/romsreturns acoreVersionhash of the core.dataso browsers never serve a stale core after an update. - Cross-origin isolation headers (COOP/COEP) required by the WASM core (SharedArrayBuffer) — a plain static server will not work.
- Cross-platform — runs on Windows (no WSL needed), Windows+WSL and Linux; uses only Node.js APIs and resolves paths per OS automatically.
- Automatic ROM detection — scans
- Launcher page (
test-flycast.html): game menu, BIOS and track injection into the core's file system, and a live FPS + resolution overlay. - Reproducible build recipe:
build-ejs-ra.sh(EmulatorJS RetroArch objects with the exact flags),build-prod.sh(bitcode objects for the-fltolink),build-release.sh(core → 7z data → end-user zip pipeline) anddocs/BUILDING.md(the rationale behind every flag). - End-user release packaging: a self-contained zip ready for non-developers — see the next section.
Más allá del core y el JIT, este repositorio incluye un ecosistema completo y reproducible para que cualquiera pueda jugar a Dreamcast en el navegador sin compilar:
- Servidor web local (
server.js, Node.js puro, cero dependencias):- Detección automática de ROMs — escanea
frontend/roms/y analiza los ficheros.cue/.gdipara conocer sus pistas; soporta.chd,.zip(con.gdi+ pistas),.cuey.gdi. El menú muestra todos los juegos sin configuración. - Detección automática de BIOS — indica qué BIOS hay presentes y cuáles
faltan (
dc_boot.bin,dc_flash.bin, opcionaldc_nvmem.bin), para que el lanzador avise al usuario antes de empezar. - Cache-busting del core —
/api/romsdevuelve un hashcoreVersiondel.datadel core para que el navegador nunca sirva un core obsoleto tras una actualización. - Cabeceras de aislamiento de origen cruzado (COOP/COEP) que el core WASM necesita (SharedArrayBuffer) — un servidor estático simple no sirve.
- Multiplataforma — funciona en Windows (sin WSL), Windows+WSL y Linux; usa solo APIs de Node.js y resuelve rutas según el sistema operativo.
- Detección automática de ROMs — escanea
- Página de lanzamiento (
test-flycast.html): menú de juegos, inyección de BIOS y pistas en el sistema de ficheros del core, y un overlay en vivo de FPS + resolución. - Receta de compilación reproducible:
build-ejs-ra.sh(objetos de EmulatorJS RetroArch con las flags exactas),build-prod.sh(objetos bitcode para el enlazado con-flto),build-release.sh(tubería core → datos 7z → zip final) ydocs/BUILDING.md(la razón de cada flag). - Empaquetado de release para usuarios finales: un zip autocontenido listo para no desarrolladores — ver la siguiente sección.
SH4 machine code -> Flycast decoder -> SHIL IR -> wasm bytecode emitter
-> WebAssembly.compile -> function table -> call_indirect dispatch
(C dispatch loop, no JS in the hot path)
The full story lives in TECHNICAL_WRITEUP.md: architecture, every performance wall in the order it fell, the measured numbers, and the differential validation methodology. If you're here to learn how to build one of these, start there.
The release ships the built core (flycast_libretro.js plus .wasm) and its
configuration. It runs in any EmulatorJS-style libretro web frontend:
- Serve the core files alongside your frontend. The server must send
cross-origin isolation headers (
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corp). - Register the core with your frontend. See
config/core.jsonfor the metadata andconfig/dreamcast-core-options.jsonfor the tuned core options. - Supply your own legally dumped Dreamcast BIOS (
dc_boot.bin,dc_flash.bin) and game images (CHD, CDI, GDI, or CUE). No BIOS or game content is included or hosted here, and none ever will be. This project is the emulator, nothing else.
The file to upload as a GitHub Release is flycast-wasm-release.zip,
generated with bash build-release.sh. It is a self-contained, ready-to-play
package for non-developers: no compilation, no configuration. It contains
only what is needed to run the emulator:
server.js+package.json— run withnpm start(ornode server.js)frontend/bios/andfrontend/roms/— empty folders with instructions; this is where users drop their own legal BIOS and game filesfrontend/data/— the EmulatorJS frontend (loader, emulator, the two coresflycast-wasm.data+flycast-legacy-wasm.data, compression and localization)test-flycast.html— the launcher pageREADME.md,CREDITS.md,LICENSE(bilingual: English first, Spanish at the end)
To play in three steps:
- Unzip
flycast-wasm-release.zipanywhere (Windows, macOS or Linux). - Copy your BIOS (
dc_boot.bin,dc_flash.bin, optionaldc_nvmem.bin) intofrontend/bios/and your games (.chdrecommended, or.zip,.cue,.gdiwith their tracks) intofrontend/roms/. - Run
npm startand openhttp://localhost:3333/in your browser.
The server does the rest automatically: it detects the BIOS present and the
missing files, lists every game it finds (parsing .cue/.gdi tracks so
multi-file games work), cache-busts the core on every update, sends the
cross-origin isolation headers the WASM core requires, and shows a live
FPS + resolution overlay while you play. It runs on Windows (no WSL
needed), Windows+WSL and Linux — change the port with PORT=8421 npm start
if 3333 is busy.
El fichero que debe subirse como release de GitHub es
flycast-wasm-release.zip, generado con bash build-release.sh. Es un
paquete autocontenido y listo para jugar, pensado para no desarrolladores:
sin compilar, sin configurar. Contiene solo lo necesario para ejecutar el
emulador:
server.js+package.json— se ejecuta connpm start(onode server.js)frontend/bios/yfrontend/roms/— carpetas vacías con instrucciones; aquí es donde el usuario coloca su propia BIOS y sus juegos (copias legales)frontend/data/— el frontend EmulatorJS (loader, emulador, los dos coresflycast-wasm.data+flycast-legacy-wasm.data, compresión y localización)test-flycast.html— la página de lanzamientoREADME.md,CREDITS.md,LICENSE(bilingües: inglés primero, español al final)
Para jugar en tres pasos:
- Descomprime
flycast-wasm-release.zipen cualquier equipo (Windows, macOS o Linux). - Copia tu BIOS (
dc_boot.bin,dc_flash.bin, opcionaldc_nvmem.bin) enfrontend/bios/y tus juegos (.chdrecomendado, o.zip,.cue,.gdicon sus pistas) enfrontend/roms/. - Ejecuta
npm starty abrehttp://localhost:3333/en tu navegador.
El servidor hace el resto automáticamente: detecta la BIOS presente y los
ficheros que faltan, lista todos los juegos que encuentra (analizando las
pistas de .cue/.gdi para que los juegos multi-fichero funcionen),
actualiza el core con cache-busting en cada cambio, envía las cabeceras de
aislamiento de origen cruzado que el core WASM requiere, y muestra un
overlay en vivo de FPS + resolución mientras juegas. Funciona en Windows
(sin necesidad de WSL), Windows+WSL y Linux — cambia el puerto con
PORT=8421 npm start si el 3333 está ocupado.
Requires Linux or WSL2 with Emscripten SDK 3.1.74 or newer.
The full recipe with the rationale for every flag is in docs/BUILDING.md. The short version:
git clone https://github.com/flyinghead/flycast.git source
cd source && git checkout 2c48c01
git submodule update --init --recursive
# NOTE: shell/libretro/audiostream.cpp has CRLF line endings in the upstream
# repo. Convert it to LF or the patch will fail to apply:
sed -i 's/\r$//' shell/libretro/audiostream.cpp
# Apply the port + JIT patches (the canonical record of every modification)
git apply ../patches/wasm-jit-phase1-modified.patch
# Add the JIT sources
cp ../patches/rec_wasm.cpp ../patches/wasm_emit.h \
../patches/wasm_module_builder.h ../patches/fly_instrument.h core/rec-wasm/The core links against RetroArch's emscripten objects. This step is not
optional and the flags matter — the wrong ones cause retro_* symbol
collisions and libretro_dummy_* abort stubs.
bash build-ejs-ra.sh # clones EmulatorJS/RetroArch @ v1.22.2 and builds itKey flags (see build-ejs-ra.sh for the full rationale):
Makefile.emulatorjs(notMakefile.emscripten): definesEMULATORJS=1, which compilesemulatorjs_input.o(providesejs_set_keyboard_enabled) and the EmulatorJS frontend objects.HAVE_OPENGLES3=1: generatesglsym_es3.o(excluded from the link by build-prod.sh; without it you getglsym_es2.o, whoserglgen_symbol_mapcollides with flycast's).- No
HAVE_STATIC_DUMMY(default 0): withHAVE_STATIC_DUMMY=1,cores/dynamic_dummy.cemitsretro_*wrappers that collide with flycast'sshell/libretro/libretro.cpp. Excludingdynamic_dummy.oleavesabort("missing function: libretro_dummy_*")stubs and games die withAborted(missing function: libretro_dummy_retro_get_system_info).
The final LD step of that build fails with libretro_emscripten.a: No such file — that is expected and harmless; only obj-emscripten/*.o are needed.
LTO note:
build-prod.shlinks with-flto, which requires all input objects to be LLVM bitcode (magicBC), not final wasm. ExportEMCC_CFLAGS="-flto"before runningbuild-ejs-ra.sh(andbuild-prod.sh) so every object is compiled as bitcode. Without it, the EJS frontend symbols inretroarch.o/runloop.oare not linkable and the link fails withundefined exported symbol: _cmd_take_screenshot. Seedocs/BUILDING.mdfor the full explanation and verified results.
export EMCC_CFLAGS="-flto" # bitcode objects; required by the -flto link
bash build-ejs-ra.sh # EJS_RA objects (bitcode)
bash build-prod.sh # production core (diagnostics stripped)build-prod.sh does CMake + make for flycast, strips the conflicting objects,
links against the EJS_RA objects and packages the result into out/.
bash build-release.sh # core + release/ tree + flycast-wasm-release.zipbuild-release.sh runs build-prod.sh, packs the 7z data archive, refreshes
release/frontend/data/cores/ and zips release/ into
flycast-wasm-release.zip — a self-contained package (server + frontend +
cores, no build files) that non-developers can unzip and run with
npm start or node server.js. See docs/BUILDING.md §6.
These steps are exactly what the build scripts document, and the flow was
verified from a fresh clone. The JIT source files live in patches/ verbatim:
rec_wasm.cpp (dynarec, dispatch, SMC, test harnesses), wasm_emit.h (SHIL to
wasm op emitters), wasm_module_builder.h (wasm binary encoder), and
fly_instrument.h (instrumentation used by dev builds).
I built this to prove it could be done, and it's done: the JIT is certified against the reference interpreter, performance is native-class, and the whole approach is documented well enough to reproduce. I have a job and other projects, so development here will be slower than the sprint that built it, but the project is not parked. Next on the roadmap: WinCE compatibility.
If you want to contribute, two documents are your map:
- docs/WINCE_MMU_ROADMAP.md covers the one big missing feature and the next thing I plan to build. Windows CE titles (Sega Rally 2 and friends) need full SH4 MMU support, which the JIT currently doesn't have. The document is a complete phased implementation plan with root cause analysis: what breaks, why, the go/no-go experiment to run first, and the fast-path design already proven by flycast's native backends. I'll be chipping away at it. If you want to help, or beat me to it, the plan is right there.
- TECHNICAL_WRITEUP.md, the "Future work" section,
lists everything planned but not done, with the research already written up:
- Region compilation: whole-hot-page wasm modules with internal dispatch. Built and certified in-tree, dark behind flags. Needs a rollout soak, and the multi-region variant needs a churn breaker first.
- IndexedDB per-title bytecode cache so second sessions boot pre-warmed.
Caching compiled
Moduleobjects is impossible (browsers removed that), but caching the generated bytes is straightforward. - FPU register caching. Float registers still round-trip through context memory on every op. Measure first, then build.
- WASM branch hints: already emitted on every guard slow path, dormant until Chrome's V8 enables consumption by default. Firefox ships it today.
- WebGPU renderer: the path to per-pixel order-independent translucency, the last rendering-accuracy gap class.
- A handful of known cosmetic issues, documented in the write-up.
Pull requests welcome. Anything that comes with differential-harness evidence gets reviewed first.
- Flycast by flyinghead, the upstream emulator this is built on. All emulation correctness ultimately descends from that codebase and its reference interpreter.
- WebAssembly port and SH4 to WASM JIT by Nick Somers.
- EmulatorJS, the frontend ecosystem this core targets.
- Contributor: LokiCode404 — documented
and verified the exact build recipe (EJS_RA flags, CRLF patch fix, bitcode
objects for the
-fltolink), and contributed the local server and launcher packaging. Contribuidor: documentó y verificó la receta exacta de compilación (flags del EJS_RA, arreglo CRLF del patch, objetos bitcode para el enlazado con-flto), y contribuyó con el servidor local y el empaquetado de lanzamiento.
GPLv2, inherited from Flycast. Fork freely. Keep it open.


