Sega 32X
An in-browser Sega 32X emulator powered by a from-source WebAssembly build of PicoDrive (notaz / irixxxx). The 32X is a 1994 add-on that plugs into the Genesis / Mega Drive: it keeps the Motorola 68000 main CPU and adds two Hitachi SH-2 RISC processors. PicoDrive is one of the few emulators with strong 32X support. This build wraps PicoDrive's core in a small standalone Emscripten shim with an on-demand sampling API over the 68000 register and memory interface, so the shared debugger gets genuine live 68000 registers, side-effect-free memory, a real single-instruction step, and execution breakpoints plus memory watchpoints. It boots a homebrew 32X colour-bars ROM that turns the 32X adapter on, so the dual-SH-2 hardware genuinely engages.
Runs on: Web browser
Sega 32X Online Emulator
Play Sega 32X using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| 32X color bars (homebrew) | Sega 32X | Sega 32X | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The 32X core is PicoDrive (notaz / irixxxx) compiled to WebAssembly through Emscripten. PicoDrive is one of the few emulators with strong Sega 32X support. It emulates the Genesis/Mega Drive base machine and the 32X add-on's two Hitachi SH-2 processors. Rather than the libretro core (which needs the RetroArch frontend) or the SDL build, we wrote a tiny standalone shim, platform/wasm/wasm.c, that talks to PicoDrive's OWN C API and exposes it to JavaScript through a handful of flat functions. The module is built with -s MODULARIZE=1 so picodrive.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt picodrive.js / picodrive.wasm and the homebrew ROM.
// picodrive.js defines a global Module factory; instantiate it, then drive it.
var pico = await Module({ locateFile: f => SRC + f }); // finds picodrive.wasm
pico._wasm_init(); // PicoInit + set RGB output buffer
var ptr = pico._wasm_get_rom_buffer_ref(rom.length); // staging buffer for the cartridge
pico.HEAPU8.set(rom, ptr);
pico._wasm_start(rom.length); // byteswap + PicoCartInsert + reset
Rendering is manual. PicoDrive rasters into a 320×240 buffer; the shim converts each frame to RGBA in the WASM heap. Each frame we blit that buffer to an off-screen canvas and draw it scaled to the visible canvas:
var rgba = new Uint8ClampedArray(pico.HEAPU8.buffer, pico._wasm_get_frame_buffer_ref(), 320*240*4);
backImg.data.set(rgba); bctx.putImageData(backImg, 0, 0);
ctx.drawImage(back, 0,0, pico._wasm_video_width(), pico._wasm_video_height(), 0,0, canvas.width, canvas.height);
Input is a single 16-bit word in the MXYZ SACB RLDU layout of a Genesis 6-button pad. We build the mask from the pressed buttons and hand it to _wasm_set_pad() once per frame; the shim copies it into PicoIn.pad[0] before PicoFrame() runs the video frame.
| Member | Kind | What it does |
|---|---|---|
_wasm_init() / _wasm_start(n) | export | PicoInit + set the RGB output buffer; then byteswap the ROM, PicoCartInsert and reset. |
_wasm_tick() | export | Run exactly one video frame (PicoFrame) and convert it to RGBA. Our frame-step and the run loop's advance. |
_wasm_get_frame_buffer_ref() | export | Pointer to the 320×240 RGBA framebuffer in the heap. |
_wasm_get_rom_buffer_ref(n) / _wasm_set_pad(v) | export | ROM staging pointer; write the 6-button pad word. |
_wasm_is_32x() | export | 1 once the running ROM has switched the 32X adapter on (the two SH-2s are live). |
_wasm_dbg_* | export | The debug sampling API (registers, memory, step, reset), see below. |
_wasm_video_width/height() | export | Active picture size (320×240). |
Debugger integration
This is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines - real registers, real memory, a real single-instruction step, plus execution breakpoints and memory watchpoints. The stock PicoDrive frontends keep the whole 68000 inside the WASM sandbox, so we built it from source with a tiny sampling API over PicoDrive's public 68000 (FAME) interface.
1 · Exactly what was added (the only new source). A single new file, platform/wasm/wasm.c, is the whole frontend: it drives the core (PicoInit / PicoCartInsert / PicoFrame) and appends a block of EMSCRIPTEN_KEEPALIVE functions that read live state through PicoDrive's own macros over the global PicoCpuFM68k (an M68K_CONTEXT declared in pico/pico_int.h). They are sampling functions: the debugger calls them ~10×/second while its window is open, and once per Step. There is no per-instruction or per-cycle callback anywhere - the emulator's hot path (PicoFrame / fm68k_emulate) is byte-for-byte unchanged (the project's golden performance rule).
// platform/wasm/wasm.c - sampling only, no hot-path hook
u32 wasm_dbg_reg(int i){ // 0-7 = D0-D7, 8-15 = A0-A7
return i<8 ? PicoCpuFM68k.dreg[i].D : PicoCpuFM68k.areg[i-8].D; }
u32 wasm_dbg_pc(){ return SekPc & 0xffffff; } // fm68k_get_pc()
u32 wasm_dbg_sr(){ return PicoCpuFM68k.sr & 0xffff; }
u32 wasm_dbg_read(u32 a){ // side-effect-free 68000 bus read
if (a < Pico.romsize) return Pico.rom[a^1]; // MEM_BE2: ROM is byteswapped
if ((a & 0xe00000)==0xe00000) return PicoMem.ram[(a&0xffff)^1];
return 0; } // VDP / I-O pages -> 0
void wasm_dbg_step(){ SekStepM68k(); } // REAL one-instruction step
// + set_reg / set_pc / set_sr / usp / ssp / dbg_write / wasm_reset
The functions are exported with -sEXPORTED_FUNCTIONS and the heaps with -sEXPORTED_RUNTIME_METHODS=HEAPU8,HEAPU32,HEAPF32. That is the whole change: one additive C file, no edit to any core source.
2 · What is REAL here. Because FAME keeps the 68000 register file in a plain C struct that PicoDrive's macros expose, this build has:
- Real registers - D0-D7, A0-A7, PC, SR (with X N Z V C S flags) and USP/SSP, read and written live off the FAME register file.
- Real single-instruction step -
Step icallswasm_dbg_step()= PicoDrive'sSekStepM68k()=fm68k_emulate(&PicoCpuFM68k, 1, 0), which executes exactly one 68000 instruction; PC and the registers change by one instruction per click. - Side-effect-free memory - reads resolve the cartridge ROM and 68K work RAM directly (returning 0 for the VDP / 32X / I-O windows), so auto-polling the hex/disasm view never acknowledges an interrupt or clears a latch.
- Execution breakpoints & memory watchpoints - implemented host-side in the run loop (see below), so they cost nothing when the debugger is closed.
3 · Breakpoints and watchpoints without a hot-path hook. When no breakpoint or watchpoint is set, the loop runs a whole frame at native speed with _wasm_tick(). As soon as one is armed, the loop switches to stepping the 68000 one instruction at a time with wasm_dbg_step(), comparing the live PC against the breakpoint set and the watched addresses against their last sampled value, and pausing on a hit. This is a pure host-side check that only runs while a guard is armed. The C core is never modified, honouring the golden performance rule. (While single-stepping under an armed breakpoint the video/SH-2 do not advance in lockstep with the 68000, so the picture holds on its last frame until you resume.)
4 · The controllable loop. We own the frame loop so the transport can pause/step. While running, each animation frame writes the pad word, advances one frame with _wasm_tick(), and blits. Pause stops the loop; Resume restarts it; Step (frame) runs one _wasm_tick(); Step i (instruction) calls wasm_dbg_step(); Reset calls wasm_reset() = PicoReset().
Architecture
PicoDrive is an accuracy-and-speed Sega 8/16-bit + 32X + Sega-CD emulator. Our build is a thin standalone Emscripten port of its core. The whole machine lives in one WASM module built from C:
- FAME - the Motorola 68000 core (
cpu/fame/famec.c), the Genesis/32X main CPU. Its register contextPicoCpuFM68kand PicoDrive'sSekPc/SekDar/SekSrmacros are what this integration samples;SekStepM68k()is the run-one-instruction primitive we use for single-stepping. - Two Hitachi SH-2s - the heart of the 32X (
cpu/sh2MAME interpreter,pico/32x/sh2soc.c): a master and a slave RISC CPU running the 32X's polygon / framebuffer code. They come alive the moment the ROM writes the adapter-enable (ADEN) register. - CZ80 - the Genesis sound CPU (
cpu/cz80). - VDP + 32X VDP - the Genesis 315-5313 video chip plus the 32X's own framebuffer/priority overlay (
pico/32x/draw.c), composited into the 320×240 bitmap. - Memory map -
pico/memory.candpico/32x/memory.cbuild the 68000 bus: cartridge ROM at $000000, work RAM at $FF0000, the VDP / I-O / Z80 windows, and the 32X control/PWM/comms registers around $A15100. Writing ADEN there callsPico32xStartup(), which brings up the SH-2s.
How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc on PATH), plus the emscripten zlib port.
git clone --recurse-submodules https://github.com/irixxxx/picodrive.git
cd picodrive
# 1. add platform/wasm/wasm.c (core-API frontend + wasm_dbg_* hooks)
# 2. compile the core (pico/*, pico/32x/*, cpu/fame, cpu/cz80, cpu/sh2 MAME
# interpreter) with EMU_F68K + _USE_CZ80, NO DRC, NO ARM asm, and the shim:
emcc -O2 -I. -sUSE_ZLIB=1 -DEMU_F68K -D_USE_CZ80 -DNDEBUG -c <core .c files> platform/wasm/wasm.c
emcc -O2 *.o -o picodrive.js -sUSE_ZLIB=1 -sMODULARIZE=1 -sEXPORT_NAME=Module -sALLOW_MEMORY_GROWTH=1 -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8,HEAPU32,HEAPF32 -sEXPORTED_FUNCTIONS=_wasm_init,_wasm_start,_wasm_tick,_wasm_dbg_pc,... # -> picodrive.js + .wasm
The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. The FAME 68000 and the MAME SH-2 interpreter both run in plain C; there is no dynarec, so nothing needs writable-executable memory.