SearchA-ZG › Genesis

Genesis

2007 Open source · Genesis Plus GX Online

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 ↗

Visit the official site ↗

Runs on: Web browser

Genesis Online Emulator

Play Genesis using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Color bars demoGenesisSega GenesisopenOpen ⛶
Color bars demoGenesisSega Mega DriveopenOpen ⛶

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.

MemberKindWhat it does
gens._init() / _start()exportAllocate buffers; then load the ROM and reset the machine.
gens._tick()exportRun exactly one video frame (system_frame_gen). Our frame-step and the run loop's advance.
gens._get_frame_buffer_ref()exportPointer to the 640×480 RGBA framebuffer in the heap.
gens._get_rom_buffer_ref(n) / _get_input_buffer_ref()exportPointers to the ROM staging buffer and the 32-float input buffer.
gens._wasm_input_update()exportCopy the input buffer into input.pad[0] (called each frame).
gens._wasm_dbg_*exportThe debug sampling API added in the rebuild (see below): registers, memory, step, reset.
gens._wasm_video_width/height()exportActive 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 i calls wasm_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 no base pointer), 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 and m68k_get_reg/set_reg API 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.c builds m68k.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 direct base pointer (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.