Genesis
An in-browser Sega Genesis / Mega Drive emulator powered by a WebAssembly build of Genesis Plus GX (Eke-Eke), whose main CPU is the Motorola 68000 (Musashi core) with a Zilog Z80 for sound. This build was rebuilt from source with a small on-demand sampling API over the 68000 register and memory interface, so the shared debugger gets genuine live 68000 registers, side-effect-free memory, and a real single-instruction step. It boots a public-domain Mega Drive colour-bars demo to a live picture.
Genesis Plus GX on GitHub ↗ · wasm-genplus harness ↗
Runs on: Web browser
Genesis Online Emulator
Play Genesis using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Color bars demo | Genesis | Sega Genesis | open | Open ⛶ | |
| Color bars demo | Genesis | Sega Mega Drive | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The Genesis core is Genesis Plus GX (Eke-Eke) compiled to WebAssembly through Emscripten. Rather than the heavy libretro frontend we use the standalone wasm-genplus harness: a small C shim (src/main/c/wasm/wasm.c) exposes the core to JavaScript through a handful of flat functions, and the module is built with -s MODULARIZE=1 so genplus.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt genplus.js / genplus.wasm and the public-domain ROM.
// genplus.js defines a global Module factory; instantiate it, then drive it.
var gens = await Module({ locateFile: f => SRC + f }); // finds genplus.wasm
gens._init(); // malloc frame / audio / input buffers
var ptr = gens._get_rom_buffer_ref(rom.length); // where to write the cartridge
gens.HEAPU8.set(rom, ptr);
gens._start(); // load_rom + system_init + system_reset
Rendering is manual. The core has no SDL video; it rasters into a 640×480 RGBA buffer in the WASM heap. Each frame we blit that buffer to an off-screen canvas and draw the active viewport (_wasm_video_width() × _wasm_video_height(), typically 320×224) scaled to the visible canvas:
var vram = new Uint8ClampedArray(gens.HEAPU8.buffer, gens._get_frame_buffer_ref(), 640*480*4);
backImg.data.set(vram); bctx.putImageData(backImg, 0, 0);
ctx.drawImage(back, 0,0, gens._wasm_video_width(), gens._wasm_video_height(), 0,0, canvas.width, canvas.height);
Input is a 32-float GamePad-style buffer in the heap. We write the D-pad axes and the A/B/C/Start button slots, then call _wasm_input_update() once per frame (it copies the buffer into the core's input.pad[0]) before _tick() runs the video frame.
| Member | Kind | What it does |
|---|---|---|
gens._init() / _start() | export | Allocate buffers; then load the ROM and reset the machine. |
gens._tick() | export | Run exactly one video frame (system_frame_gen). Our frame-step and the run loop's advance. |
gens._get_frame_buffer_ref() | export | Pointer to the 640×480 RGBA framebuffer in the heap. |
gens._get_rom_buffer_ref(n) / _get_input_buffer_ref() | export | Pointers to the ROM staging buffer and the 32-float input buffer. |
gens._wasm_input_update() | export | Copy the input buffer into input.pad[0] (called each frame). |
gens._wasm_dbg_* | export | The debug sampling API added in the rebuild (see below): registers, memory, step, reset. |
gens._wasm_video_width/height() | export | Active picture size within the 640×480 bitmap. |
Debugger integration
This is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines - real registers, real memory, and a real single-instruction step. The stock wasm-genplus build keeps the entire 68000 inside the WASM sandbox and exports no register getters, so we rebuilt it from source with a tiny sampling API over Genesis Plus GX's public Musashi interface.
1 · Exactly what was changed (the only source edit). We appended a block of EMSCRIPTEN_KEEPALIVE functions to src/main/c/wasm/wasm.c. They read live state through the global m68k object (an m68ki_cpu_core declared in core/m68k/m68k.h) and its public helpers m68k_get_reg / m68k_set_reg / m68k_run. 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 (system_frame_gen / m68k_run's inner loop) is byte-for-byte unchanged (the project's golden performance rule).
// appended to src/main/c/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 ? m68k_get_reg(M68K_REG_D0+i) : m68k_get_reg(M68K_REG_A0+i-8); }
u32 wasm_dbg_pc(){ return m68k_get_reg(M68K_REG_PC); }
u32 wasm_dbg_sr(){ return m68k_get_reg(M68K_REG_SR); }
u32 wasm_dbg_read(u32 a){ // side-effect-free 68000 bus read
cpu_memory_map *m = &m68k.memory_map[(a>>16)&0xff];
return m->base ? m->base[(a&0xffff)^1] : 0; } // LSB_FIRST; I/O pages -> 0
void wasm_dbg_step(){ m68k_run(m68k.cycles + 1); } // REAL one-instruction step
// + set_reg / set_pc / set_sr / sp / usp / isp / dbg_write / wasm_reset / video_w|h
Then their export was arranged by adding EXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8,HEAPU32,HEAPF32 to the linker flags in CMakeLists.txt (so JS can read the heaps and call the KEEPALIVE hooks) and rebuilding. That is the whole change: one additive C block plus one build-flag line.
2 · What is REAL here. Because Musashi keeps the 68000 register file in a plain C struct and exposes m68k_get_reg/set_reg, 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 Musashi register file.
- Real single-instruction step -
Step icallswasm_dbg_step()=m68k_run(m68k.cycles + 1). Musashi's run loop executes instructions until the master-cycle target, so advancing the target by one runs exactly one 68000 instruction; PC and the registers change by one instruction per click. - Side-effect-free memory - reads resolve the 68000 page in
m68k.memory_map[]and read the RAM/ROM byte directly (returning 0 for the VDP / I/O windows whose page has nobasepointer), so auto-polling the hex/disasm view never acknowledges an interrupt or clears a latch.
3 · What is NOT provided. Execution breakpoints and write watchpoints are omitted. Musashi (unlike the Amiga's Moira core) has no built-in guard facility, and the only way to catch a PC host-side would be a per-instruction check in the run loop - which would slow the emulator even with the debugger closed and so violates the golden performance rule. Registers, memory, pause/resume, frame-step and real single-instruction step are all present; breakpoints are the one honest gap.
4 · The controllable loop. We own the frame loop so the transport can pause/step. While running, each animation frame writes the input buffer, calls _wasm_input_update(), advances one frame with _tick(), and blits. Pause stops the loop; Resume restarts it; Step (frame) runs one _tick(); Step i (instruction) calls wasm_dbg_step(); Reset calls wasm_reset() = system_reset().
Architecture
Genesis Plus GX is an accuracy-focused Sega 8/16-bit emulator; wasm-genplus is a thin Emscripten port of it. The whole machine lives in one WASM module built from C:
- Musashi - the Motorola 68000 core (
core/m68k, Karl Stenerud, adapted by Eke-Eke for shared cycle counting). Its register file andm68k_get_reg/set_regAPI are what this integration samples;m68k_run(cycles)is the run-to-cycle primitive we use for single-stepping. - Z80 - the sound CPU (
core/z80), driving the FM/PSG, cycle-shared with the 68000. - VDP - the Sega 315-5313 video chip (
core/vdp_ctrl,core/vdp_render): planes A/B, sprites and the colour RAM, rendered into the 640×480 bitmap. - YM2612 + SN76489 - FM synthesis and PSG (
core/sound); available but unused here (we run silent for headless verification). - Memory map -
mem68k.cbuildsm68k.memory_map[256]: cartridge ROM at $000000, work RAM at $FF0000, and the VDP / I-O / Z80 windows around $A00000-$C00000. RAM/ROM pages carry a directbasepointer (what our side-effect-free reader uses); I/O pages carry read/write function pointers instead.
How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc/emcmake/emmake on PATH), cmake, make.
git clone --recurse-submodules https://github.com/h1romas4/wasm-genplus.git
cd wasm-genplus
# 1. append the wasm_dbg_* sampling block to src/main/c/wasm/wasm.c
# 2. add -s EXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8,HEAPU32,HEAPF32 to the LD flags in CMakeLists.txt
mkdir build && cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5
emmake make -j4 # -> ../src/main/js/genplus.js + genplus.wasm
The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. The only source change is the additive wasm_dbg_* block plus the one export-flag line; nothing on the emulation hot path is touched.