Neo Geo
This is the SNK Neo Geo running in your browser: the GEOLITH core (a highly accurate emulator for the Neo Geo AES home console and MVS arcade board) compiled to WebAssembly with Emscripten and self-hosted, no CDN. It boots the Neo Geo system BIOS plus a cartridge in TerraOnion's single-file .NEO format, and plugs into the shared in-frame debugger for the Motorola 68000 main CPU.
Runs on: Web browser
Neo Geo Online Emulator
Play Neo Geo using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Metal Slug | Neo Geo | SNK Neo Geo | grey | Open ⛶ | |
| The King of Fighters '94 | Neo Geo | SNK Neo Geo | grey | Open ⛶ | |
| Samurai Shodown | Neo Geo | SNK Neo Geo | grey | Open ⛶ | |
| Fatal Fury | Neo Geo | SNK Neo Geo | grey | Open ⛶ | |
| Puzzle Bobble | Neo Geo | SNK Neo Geo | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The Neo Geo core is GEOLITH (Rupert Carmichael), a highly accurate emulator for the Neo Geo AES / MVS, compiled to WebAssembly with Emscripten. GEOLITH normally ships as a libretro core; rather than pull in the whole RetroArch frontend we wrote a tiny standalone Emscripten frontend, neo_wasm.c, that talks to GEOLITH's OWN C API (geo_init / geo_bios_load_mem / geo_neo_load / geo_exec) and exposes it to JavaScript through a handful of flat functions. The module is built with -s MODULARIZE=1 so neogeo.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt neogeo.js / neogeo.wasm, the MVS system BIOS (neogeo.zip), and each game in TerraOnion's single-file .NEO format.
// neogeo.js defines a global Module factory; instantiate it, stage BIOS + game, boot.
var neo = await Module({ locateFile: f => SRC + f }); // finds neogeo.wasm
var bp = neo._wasm_bios_buf(bios.length); neo.HEAPU8.set(bios, bp); // stage neogeo.zip
var gp = neo._wasm_neo_buf(game.length); neo.HEAPU8.set(game, gp); // stage the .NEO
neo._wasm_start(); // region+system, load BIOS+cart, reset
Rendering is manual. GEOLITH rasters into a 320×264 XRGB8888 line buffer; the frontend converts the visible 304×224 window 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(neo.HEAPU8.buffer, neo._wasm_fb_ptr(), 304*224*4);
backImg.data.set(rgba); bctx.putImageData(backImg, 0, 0);
ctx.drawImage(back, 0,0, 304,224, 0,0, canvas.width, canvas.height);
Input is the Neo Geo controller latch. Each of the two joystick ports is a byte (Up/Down/Left/Right + A/B/C/D, active low); Start/Select and the coin slots are separate status registers. We keep the pressed bits in JS and push them once per frame; the frontend's registered GEOLITH input callbacks return the active-low bytes the 68000 reads.
| Member | Kind | What it does |
|---|---|---|
_wasm_bios_buf(n) / _wasm_neo_buf(n) | export | Allocate + return a staging pointer for the BIOS zip and the .NEO cartridge. |
_wasm_start() | export | Set region US / system MVS, geo_init, load BIOS + cart, wire input, geo_reset. |
_wasm_tick() | export | Run exactly one video frame (geo_exec) and convert it to RGBA. Our frame-step and the run loop's advance. |
_wasm_fb_ptr() | export | Pointer to the 304×224 RGBA framebuffer in the heap. |
_wasm_set_pad(port,bits) | export | Write a joystick latch (bit0 Up … bit7 D, pressed=1). |
_wasm_set_sys(v) / _wasm_set_coin(v) | export | Start/Select bits; coin-slot bits (needed to credit an MVS game). |
_wasm_dbg_* | export | The debug sampling API (registers, memory, step, reset) — see below. |
_wasm_vw() / _wasm_vh() | export | Visible picture size (304×224). |
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. GEOLITH's 68000 is Musashi, whose register file is reachable through m68k_get_reg / m68k_set_reg, and whose m68k_execute(1) runs exactly one instruction. We expose these through a small sampling API from our standalone frontend.
1 · Exactly what was added (the only new source). A single new file, neo_wasm.c, is the whole frontend: it drives the core (geo_init / geo_bios_load_mem / geo_neo_load / geo_exec) and appends a block of EMSCRIPTEN_KEEPALIVE functions that read live state through Musashi's own API. 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 hot path (geo_exec / m68k_execute) is byte-for-byte unchanged (the golden performance rule). A second tiny file, cd_stubs.c, no-ops the Neo Geo CD symbols so the cartridge-only build skips the whole CD stack.
// neo_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(NULL, M68K_REG_D0+i)
: m68k_get_reg(NULL, M68K_REG_A0+(i-8)); }
u32 wasm_dbg_pc(){ return m68k_get_reg(NULL, M68K_REG_PC); }
u32 wasm_dbg_sr(){ return m68k_get_reg(NULL, M68K_REG_SR) & 0xffff; }
u32 wasm_dbg_read(u32 a){ // side-effect-free 68000 bus read
romdata_t *rd = geo_romdata_ptr();
if (a < 0x100000) return rd->p[a]; // P ROM
if (a < 0x110000) return ram[a & 0xffff]; // 68K work RAM
... } // BIOS / NVRAM windows, else 0
void wasm_dbg_step(){ geo_m68k_run(1); } // REAL one-instruction step = m68k_execute(1)
2 · What is REAL here. Because Musashi keeps the 68000 register file in a plain C context that m68k_get_reg exposes, 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 Musashi's register file.
- Real single-instruction step —
Step icallswasm_dbg_step()=geo_m68k_run(1)=m68k_execute(1), which executes exactly one 68000 instruction; PC and the registers change by one instruction per click. - Side-effect-free memory — reads resolve the cartridge P ROM, the 68K work RAM, the system BIOS and NVRAM directly (returning 0 for 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() (geo_exec). 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. (While single-stepping under an armed breakpoint the Z80/video 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 input latch, 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() = geo_reset(1).
Architecture
The Neo Geo (SNK, 1990) shipped as the MVS arcade board and the identical AES home console — the most powerful 2D hardware of its era, with a huge 24-bit sprite engine. GEOLITH emulates the whole machine at instruction-level granularity; our build is a thin standalone Emscripten port of its core. The whole machine lives in one WASM module built from C:
- Musashi 68000 — the Motorola 68000 main CPU (
src/m68k) at ~12 MHz. Its register context is what this integration samples viam68k_get_reg;m68k_execute(1)(wrapped by GEOLITH'sgeo_m68k_run) is the run-one-instruction primitive we use for single-stepping. - Zilog Z80 — the sound CPU (
src/z80), which drives the sound chip and answers the 68000 over a command/reply latch. - YM2610 (OPNB) — the FM + SSG + ADPCM-A/B sound chip, emulated by ymfm (Aaron Giles).
- LSPC — the Neo Geo line-sprite/video controller (
geo_lspc.c): up to 381 sprites of 16×(16..512), a fix layer, and a 4096-colour-from-65536 palette, composited into a 320×224 picture. - Memory map —
geo_m68k.cbuilds the 68000 bus: P ROM at $000000 (with a banked window at $200000), 64 KB work RAM at 00000, the palette / video / I-O registers, the system BIOS at $C00000, and NVRAM at $D00000. Cartridges load from TerraOnion's single-file.NEOcontainer, which packs the P / S / M1 / V / C ROMs unencrypted but unpatched, so GEOLITH still emulates the real protection/bankswitch chips.
How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc on PATH).
git clone https://github.com/libretro/geolith-libretro.git geolith
# 1. add neo_wasm.c (core-API frontend + wasm_dbg_* hooks) and cd_stubs.c
# 2. compile the core (src/geo*.c, src/m68k, src/z80, src/ymfm, deps/miniz,
# deps/speex) + the shim, MVS cartridge only (CD subsystem stubbed):
emcc -O2 -Igeolith/src -Igeolith/deps/miniz -c <core .c files> neo_wasm.c cd_stubs.c
emcc -O2 *.o -o neogeo.js -sMODULARIZE=1 -sEXPORT_NAME=Module -sALLOW_MEMORY_GROWTH=1 -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8,HEAPU32 -sEXPORTED_FUNCTIONS=_wasm_start,_wasm_tick,_wasm_dbg_pc,... # -> neogeo.js + .wasm
The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. Musashi and the Z80/ymfm cores all run in plain C; there is no dynarec, so nothing needs writable-executable memory.