Search › A-Z › S › Sega Mega-CD
Sega Mega-CD
An in-browser Sega Mega-CD (Sega CD) emulator powered by a from-source WebAssembly build of PicoDrive (notaz / irixxxx). The Mega-CD is a 1991 CD-ROM add-on that plugs under the Genesis / Mega Drive: it keeps the Motorola 68000 main CPU and adds a second 68000 sub-CPU on the CD board, a CD controller and a graphics ASIC. This build wraps PicoDrive's core in a small standalone Emscripten shim that boots the machine with only its Mega-CD BIOS and no disc, so it comes up on the animated BIOS CD-player screen. An on-demand sampling API over the main 68000 register and memory interface gives the shared debugger genuine live registers, side-effect-free memory, a real single-instruction step, and execution breakpoints plus memory watchpoints.
Runs on: Web browser
Sega Mega-CD Online Emulator
Play Sega Mega-CD using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Mega-CD BIOS (no disc) | Sega Mega-CD | Sega Mega-CD | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The core is PicoDrive (notaz / irixxxx) compiled to WebAssembly through Emscripten. PicoDrive emulates the Genesis / Mega Drive base machine and its add-ons, including the Mega-CD / Sega CD - a second Motorola 68000 sub-CPU, the CD controller and the graphics ASIC. Rather than the libretro core, 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 Mega-CD BIOS.
// 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(bios.length); // staging buffer for the BIOS
pico.HEAPU8.set(bios, ptr);
pico._wasm_start_mcd(bios.length); // AHW=MCD, PicoCreateMCD, boot the BIOS
Booting to the BIOS with no disc. A cartridge machine boots straight from its ROM; the Mega-CD instead runs a BIOS. _wasm_start_mcd() reproduces exactly what PicoDrive's own media loader does for a "MegaCD BIOS" file, minus the disc layer: it byteswaps the BIOS the way PicoCartLoad does, forces PicoIn.AHW = PAHW_MCD, copies the BIOS into Pico_mcd->bios with PicoCreateMCD, then calls PicoCartInsert with a zero-length cartridge so the CD memory map (PicoMemSetupCD) and cold reset (PicoPowerMCD) run. With no disc the BIOS shows its CD-player / animation screen - a guaranteed non-black picture and a clean 68000 debugger target.
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() | export | PicoInit + set the RGB output buffer. |
_wasm_start_mcd(n) | export | Force Mega-CD hardware, copy the BIOS in, cold-reset the CD machine (no disc). |
_wasm_tick() | export | Run exactly one video frame (PicoFrame) and convert it to RGBA. |
_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 | BIOS staging pointer; write the 6-button pad word. |
_wasm_is_mcd() | export | 1 once the Mega-CD hardware is active. |
_wasm_dbg_* / _wasm_sub_* | export | The debug sampling API (main-68000 registers, memory, step, reset) and the read-only CD sub-CPU sample - see below. |
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 · The primary CPU is the MAIN 68000. The Mega-CD has two 68000s - the console's main CPU and a second sub-CPU on the CD board. We expose the main one (PicoDrive's global PicoCpuFM68k, an M68K_CONTEXT) as the debug CPU. A block of EMSCRIPTEN_KEEPALIVE functions reads its live state through PicoDrive's own macros:
// 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; }
u32 wasm_dbg_read(u32 a){ // side-effect-free MAIN 68000 bus read
if (a < 0x20000) return Pico_mcd->bios[a^1]; // 128K BIOS at $000000 (byteswapped)
if ((a & 0xe00000)==0xe00000) return PicoMem.ram[(a&0xffff)^1];
return 0; } // VDP / gate-array / I-O -> 0
void wasm_dbg_step(){ SekStepM68k(); } // REAL one-instruction step
u32 wasm_sub_pc(){ return SekPcS68k; } // read-only sample of the CD sub-68000
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 - the emulator hot path is unchanged.
2 · A wasm-safety fix to PicoDrive's memory map. This surfaced getting the Mega-CD BIOS to boot. PicoDrive's 68000 bus is a page table whose entries pack a handler pointer as ptr>>1 and recover it as v<<1, relying on function pointers being even-aligned (always true on native CPUs). On WebAssembly a function pointer is a table INDEX that can be odd, so the round-trip silently dispatched some VDP handlers to the wrong function - and the Sega CD BIOS uses byte writes to the VDP data port to fire its start-up VRAM fills, so it hung on a DMA-busy wait forever. The fix (guarded to Emscripten only) stores handlers UNHALVED with the flag bit, so odd indices survive:
// pico/memory.h - recover a handler from a map entry
#ifdef __EMSCRIPTEN__
#define map_func(v) ((uptr)((v) & ~MAP_FLAG)) // full index, odd bit preserved
#else
#define map_func(v) ((uptr)((v) << 1)) // classic halved pointer
#endif
3 · 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, plus a read-only sub-PC sample of the CD sub-CPU.
- Real single-instruction step -
Step icallsSekStepM68k()=fm68k_emulate(&PicoCpuFM68k, 1, 0), executing exactly one 68000 instruction; PC and the registers change by one instruction per click. - Side-effect-free memory - reads resolve the BIOS ROM and 68K work RAM directly (returning 0 for the VDP / gate-array / 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, so they cost nothing when the debugger is closed.
4 · Breakpoints and watchpoints without a hot-path hook. When no guard 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 main 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.
Architecture
PicoDrive is an accuracy-and-speed Sega 8/16-bit + 32X + Mega-CD emulator. Our build is a thin standalone Emscripten port of its core. The Mega-CD machine lives in one WASM module built from C:
- FAME - the Motorola 68000 core (
cpu/fame/famec.c). The Mega-CD has TWO of them: the console's MAIN 68000 (PicoCpuFM68k, the CPU this debugger drives, running the console + the BIOS) and the CD-board SUB 68000 (PicoCpuFS68k, running the CD BIOS/kernel that paints the animated CD-player screen). - CD controller + graphics ASIC -
pico/cd/cdc.c/cdd.c(the CDC data controller and CDD drive) andpico/cd/gfx.c(the Mega-CD rotation/scaling ASIC that spins the CD logo). - VDP - the Genesis 315-5313 video chip (
pico/videoport.c/draw.c) composited into the 320×240 bitmap. The BIOS drives it directly to draw the start-up screen. - Memory map -
pico/cd/memory.cbuilds the two 68000 buses: the main CPU sees the 128K BIOS at $000000, work RAM at $FF0000, the VDP at $C00000 and the CD gate-array registers around $A12000; the sub-CPU sees program RAM, word RAM, PCM and the CDC. Writing the sub-CPU reset register brings the sub-68000 out of reset so it runs the CD BIOS.
The Genesis Z80 sound CPU is left disabled for this BIOS-only boot: it is not needed to reach the animation, sidesteps PicoDrive's CZ80 interpreter, and this build is single-threaded (no SharedArrayBuffer) so it hosts anywhere. There is no dynarec - the FAME 68000s and the MAME SH-2 interpreter all run in plain C.
How to build this exact artefact. Toolchain: Homebrew emscripten (emcc on PATH) + 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_start_mcd + wasm_dbg_* hooks)
# 2. apply the wasm map_func fix in pico/memory.h + pico/memory.c (xmap_set)
# 3. compile the core (pico/*, pico/cd/*, pico/32x/*, cpu/fame, cpu/sh2 MAME) + 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_mcd,_wasm_tick,_wasm_dbg_pc,... # -> picodrive.js + .wasm