SearchA-ZN › Neo Geo

Neo Geo

1990 Open source · BSD-3-Clause Online

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.

Visit the source repository ↗

Visit the official site ↗

Runs on: Web browser

Neo Geo Online Emulator

Play Neo Geo using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Metal SlugNeo GeoSNK Neo GeogreyOpen ⛶
The King of Fighters '94Neo GeoSNK Neo GeogreyOpen ⛶
Samurai ShodownNeo GeoSNK Neo GeogreyOpen ⛶
Fatal FuryNeo GeoSNK Neo GeogreyOpen ⛶
Puzzle BobbleNeo GeoSNK Neo GeogreyOpen ⛶

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.

MemberKindWhat it does
_wasm_bios_buf(n) / _wasm_neo_buf(n)exportAllocate + return a staging pointer for the BIOS zip and the .NEO cartridge.
_wasm_start()exportSet region US / system MVS, geo_init, load BIOS + cart, wire input, geo_reset.
_wasm_tick()exportRun exactly one video frame (geo_exec) and convert it to RGBA. Our frame-step and the run loop's advance.
_wasm_fb_ptr()exportPointer to the 304×224 RGBA framebuffer in the heap.
_wasm_set_pad(port,bits)exportWrite a joystick latch (bit0 Up … bit7 D, pressed=1).
_wasm_set_sys(v) / _wasm_set_coin(v)exportStart/Select bits; coin-slot bits (needed to credit an MVS game).
_wasm_dbg_*exportThe debug sampling API (registers, memory, step, reset) — see below.
_wasm_vw() / _wasm_vh()exportVisible 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 stepStep i calls wasm_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 via m68k_get_reg; m68k_execute(1) (wrapped by GEOLITH's geo_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 mapgeo_m68k.c builds 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 .NEO container, 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.