SearchA-ZS › Sega Mega-CD

Sega Mega-CD

1991 Open source · PicoDrive Online

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.

PicoDrive on GitHub ↗

Visit the official site ↗

Runs on: Web browser

Sega Mega-CD Online Emulator

Play Sega Mega-CD using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Mega-CD BIOS (no disc)Sega Mega-CDSega Mega-CDgreyOpen ⛶

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.

MemberKindWhat it does
_wasm_init()exportPicoInit + set the RGB output buffer.
_wasm_start_mcd(n)exportForce Mega-CD hardware, copy the BIOS in, cold-reset the CD machine (no disc).
_wasm_tick()exportRun exactly one video frame (PicoFrame) and convert it to RGBA.
_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)exportBIOS staging pointer; write the 6-button pad word.
_wasm_is_mcd()export1 once the Mega-CD hardware is active.
_wasm_dbg_* / _wasm_sub_*exportThe 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 i calls SekStepM68k() = 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) and pico/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.c builds 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