SearchA-ZC › Capcom CPS-1 Classics

Capcom CPS-1 Classics

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

Four defining action games from Capcom's CP System (CPS-1) arcade board (1988), running in your browser: Ghouls'n Ghosts (the punishing run-and-jump platformer), Strider (the acrobatic ninja classic), Willow (the action-RPG of the film) and U.N. Squadron (the horizontal shoot-'em-up). The core is FinalBurn Neo compiled to WebAssembly with Emscripten and self-hosted, no CDN — the same CPS-1 build as our Street Fighter II page. 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

Capcom CPS-1 Classics Online Emulator

Play Capcom CPS-1 Classics using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Ghouls'n GhostsCapcom CPS-1 ClassicsCPS-1greyOpen ⛶
StriderCapcom CPS-1 ClassicsCPS-1greyOpen ⛶
WillowCapcom CPS-1 ClassicsCPS-1greyOpen ⛶
U.N. SquadronCapcom CPS-1 ClassicsCPS-1greyOpen ⛶

Machine emulated

Chips

Notes

Embedding

These are Capcom CP System (CPS-1) arcade games running on FinalBurn Neo (FBNeo) — a mature, accurate arcade emulator whose Capcom driver runs the whole CPS-1 library — 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. This emulator shares the exact same CPS-1 core as our sf2 (Street Fighter II) page; the games differ only in which romset ZIP is staged.

// 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); // "ghouls"
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. These action games use an 8-way stick plus up to three action buttons (jump / fire / weapon, per game), with Start / Coin as 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. The core's name matcher accepts both the fighting-game names and the generic P1 Button 1..6 names, so the one build drives Street Fighter II and these action titles alike.

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 … bit6 Button 3, 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. This page gathers four of the board's defining action titles: Ghouls'n Ghosts (1988, the punishing run-and-jump platformer and one of the first CPS-1 games), Strider (1989, the acrobatic ninja arcade classic), Willow (1989, the action-RPG take on the film) and U.N. Squadron / Area 88 (1989, the horizontal shoot-'em-up). 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.