SearchA-ZS › Sega 32X

Sega 32X

1994 Open source · PicoDrive Online

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.

PicoDrive on GitHub ↗

Visit the official site ↗

Runs on: Web browser

Sega 32X Online Emulator

Play Sega 32X using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
32X color bars (homebrew)Sega 32XSega 32XgreyOpen ⛶

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.

MemberKindWhat it does
_wasm_init() / _wasm_start(n)exportPicoInit + set the RGB output buffer; then byteswap the ROM, PicoCartInsert and reset.
_wasm_tick()exportRun exactly one video frame (PicoFrame) and convert it to RGBA. Our frame-step and the run loop's advance.
_wasm_get_frame_buffer_ref()exportPointer to the 320×240 RGBA framebuffer in the heap.
_wasm_get_rom_buffer_ref(n) / _wasm_set_pad(v)exportROM staging pointer; write the 6-button pad word.
_wasm_is_32x()export1 once the running ROM has switched the 32X adapter on (the two SH-2s are live).
_wasm_dbg_*exportThe debug sampling API (registers, memory, step, reset), see below.
_wasm_video_width/height()exportActive 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 i calls wasm_dbg_step() = PicoDrive's SekStepM68k() = 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 context PicoCpuFM68k and PicoDrive's SekPc/SekDar/SekSr macros 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/sh2 MAME 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.c and pico/32x/memory.c build 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 calls Pico32xStartup(), 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.