SearchA-ZS › Street Fighter II

Street Fighter II

1991 Arcade Open source · Non-commercial (FBNeo) Online

Street Fighter II: The World Warrior (Capcom, 1991) is the arcade game that founded the modern fighting-game genre, running here on the Capcom CP System (CPS-1) board in your browser. The core is FinalBurn Neo compiled to WebAssembly with Emscripten and self-hosted, no CDN. A standalone frontend drives FBNeo's own burn-library API and boots a standard romset straight to attract mode, 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

Street Fighter II Online Emulator

Play Street Fighter II using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Street Fighter II: The World WarriorStreet Fighter IICPS-1greyOpen ⛶
Final FightStreet Fighter IICPS-1greyOpen ⛶
Ghouls'n GhostsStreet Fighter IICPS-1greyOpen ⛶

Machine emulated

Chips

Notes

Embedding

The CPS-1 core is FinalBurn Neo (FBNeo) — a mature, accurate arcade emulator whose Capcom CP System driver runs Street Fighter II and its siblings — compiled to WebAssembly with Emscripten. FBNeo normally ships as a libretro core or an SDL app; rather than pull in either frontend we wrote a tiny standalone Emscripten frontend, cps_wasm.cpp, that talks to FBNeo's OWN C++ burn-library API (BurnLibInit / BurnDrvSelect / BurnDrvInit / BurnDrvFrame) and exposes it to JavaScript through a handful of flat functions. The module is built with -s MODULARIZE=1 so cps1.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt cps1.js / cps1.wasm and each game as a standard FBNeo romset ZIP.

// cps1.js defines a global Module factory; instantiate it, stage game name + romset, boot.
var cps = await Module({ locateFile: f => SRC + f });   // finds cps1.wasm
var np = cps._wasm_game_buf(name.length); cps.HEAPU8.set(name, np); // "sf2"
var rp = cps._wasm_rom_buf(rom.length); cps.HEAPU8.set(rom, rp);   // stage the romset ZIP
cps._wasm_start();                                    // select driver, load ROMs, BurnDrvInit, reset

Rendering is manual. FBNeo rasters into its own bitmap (pBurnDraw); the frontend converts the visible 384×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(cps.HEAPU8.buffer, cps._wasm_fb_ptr(), 384*224*4);
backImg.data.set(rgba); bctx.putImageData(backImg, 0, 0);
ctx.drawImage(back, 0,0, 384,224, 0,0, canvas.width, canvas.height);

Input is the CPS-1 player latch. Each player has an 8-way stick plus three punches and three kicks; Start / Coin are separate service bits. We keep the pressed bits in JS and push them once per frame; the frontend maps them onto FBNeo's per-input pVal bytes (found by name through BurnDrvGetInputInfo) that the 68000 reads.

MemberKindWhat it does
_wasm_game_buf(n) / _wasm_rom_buf(n)exportAllocate + return a staging pointer for the driver short-name and the romset ZIP.
_wasm_start()exportSelect the CPS-1 driver by name, install a zip-backed BurnExtLoadRom, BurnDrvInit, wire input, reset.
_wasm_tick()exportRun exactly one video frame (BurnDrvFrame) and convert it to RGBA. Our frame-step and the run loop's advance.
_wasm_fb_ptr()exportPointer to the 384×224 RGBA framebuffer in the heap.
_wasm_set_input(player,bits)exportWrite a player latch (bit0 Up … bit9 Strong Kick, pressed=1).
_wasm_set_sys(v)exportStart / Coin / Service bits (needed to credit the game).
_wasm_dbg_*exportThe debug sampling API (registers, memory, step, reset) — see below.
_wasm_vw() / _wasm_vh()exportVisible picture size (384×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. FBNeo'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, cps_wasm.cpp, is the whole frontend: it drives the burn library (BurnLibInit / BurnDrvSelect / BurnDrvInit / BurnDrvFrame) 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 (BurnDrvFrame / m68k_execute) is byte-for-byte unchanged (the golden performance rule).

// cps_wasm.cpp — 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
  if (a < CpsRomLen) return CpsRom[a];        // 68K program ROM
  if ((a & 0xff0000) == 0xff0000) return CpsRam[a & 0xffff]; // 68K work RAM
  return 0; }                                // I/O windows read 0
void wasm_dbg_step(){ m68k_execute(1); }          // REAL one-instruction step

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() = 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 68K program ROM and the 64 KB work RAM at $FF0000 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() (BurnDrvFrame). 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().

Architecture

Capcom's CP System (CPS-1) debuted in 1988 and hosted a run of landmark arcade games; Street Fighter II: The World Warrior (1991) is the board's defining title and the game that started the fighting-game genre as we know it. FBNeo emulates the whole board; our build is a thin standalone Emscripten port of its capcom driver. The whole machine lives in one WASM module built from C/C++:

  • Musashi 68000 — the Motorola 68000 main CPU at 10 MHz. Its register context is what this integration samples via m68k_get_reg; m68k_execute(1) is the run-one-instruction primitive we use for single-stepping.
  • Zilog Z80 — the sound CPU at ~3.58 MHz, which drives the sound chips and answers the 68000 over a command latch.
  • YM2151 + OKI MSM6295 — the FM synthesiser and the ADPCM sample player that together make the CPS-1 soundtrack and voices.
  • CPS-A / CPS-B — Capcom's custom video chips: a scroll1 (8×8 text), scroll2 (16×16) and scroll3 (32×32) tile layer plus a 16×16 sprite/object layer, composited with a 4096-colour palette into a 384×224 picture.
  • Memory map — the 68000 bus places the program ROM at $000000, the CPS I/O and video registers in the $800000 region, and 64 KB of work RAM at $FF0000. Games load from a standard FBNeo/MAME romset ZIP that packs the program, graphics, and audio ROMs.

How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc on PATH).

git clone https://github.com/finalburnneo/FBNeo.git fbneo
# 1. generate m68kops.c (m68kmake), ctv.h (ctv_make), a capcom-only driverlist.h (gamelist.pl)
# 2. add cps_wasm.cpp (burn-library frontend + wasm_dbg_* hooks) + burner_stubs.cpp
# 3. compile the burn core + capcom driver + Musashi 68000 + Z80 + YM2151/MSM6295 + the shim:
emcc -O2 <burn core + capcom + cpu + snd .c/.cpp> cps_wasm.cpp -o cps1.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,...   # -> cps1.js + .wasm

The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. Musashi and the Z80/sound cores all run in plain C/C++; there is no dynarec, so nothing needs writable-executable memory.