@wasm-gaming/fbneo-wasm
v0.1.2
Published
FBNeo (FinalBurn Neo arcade emulator) compiled to WebAssembly via Emscripten, with a JS SDK conforming to the wasm-gaming engine contract.
Downloads
45
Readme
@wasm-gaming/fbneo-wasm
FinalBurn Neo (FBNeo) — the multi-system arcade emulator (Capcom CPS-1/2/3, SNK Neo Geo, Sega System 16, Cave, PGM, and many more) — compiled to WebAssembly via Emscripten and packaged as a wasm-gaming engine SDK.
This subproject follows the same engine-package approach used by jgenesis-wasm, blastem-wasm, rsdkv3, and rsdkv4:
- typed
manifest - typed
options load(config)engine SDK surface- Makefile-driven build (
build-sdk,build-wasm,preview)
It conforms to the @wasm-gaming/engine-specs
contract (EngineSDK = { manifest, load }).
Contract surface
import { manifest, load } from '@wasm-gaming/fbneo-wasm';
const engine = await load({
canvas, // per engine-specs EngineConfig
assets: {
rom: romZipBytes, // arcade ROM set (zip)
bios: neogeoZipBytes, // optional: e.g. neogeo.zip for Neo Geo sets
},
options: { driver: 'mslug', renderFilter: 'pixelated' },
onEvent: (e) => console.log(e),
});
engine.start();FBNeo identifies a game by the ROM zip filename (without extension). Pass the
game short name explicitly via options.driver (e.g. "mslug", "sf2",
"kof98"), or let it be inferred from options.romFileName / the picked file's
name. (The bytes are always written to MEMFS as <driver>.zip, so the source
file itself never needs renaming — only its name has to resolve to a driver.)
Identifying a ROM set from its contents
When the filename is unreliable (a browser-mangled mslug (1).zip) but the ROM
files inside must keep their canonical names, recover the driver from the zip's
contents with the opt-in @wasm-gaming/fbneo-wasm/romset helper. It matches
the CRC-32s stored in the zip's central directory against a dataset of every
FBNeo ROM set — the same identity FBNeo uses internally — so it is unaffected by
re-compression, entry reordering, or split/merged packaging.
import { RomsetIndex, resolveDriver } from '@wasm-gaming/fbneo-wasm/romset';
// Load the dataset for the system(s) you support (per-system JSON, loaded on demand).
const neogeo = await fetch('/romsets/neogeo.json').then((r) => r.json());
const index = new RomsetIndex([neogeo]);
const match = resolveDriver(romZipBytes, index); // { driver: 'mslug', coverage: 1, ... } | null
const engine = await load({
canvas,
assets: { rom: romZipBytes },
options: { driver: match?.driver }, // fall back to your own logic when null
});The dataset ships as data/romsets/<system>.json (also served at /romsets/…
from the built dist/); data/romsets/index.json lists the systems and game
counts. Regenerate it from a pinned FBNeo checkout with make romsets.
Each ROM entry carries its internal name (n) and CRC-32 (c). neogeo.json
additionally carries each chip's size (s) and hardware region (r) — the chip
layout — which is what lets the neo helper below rebuild a ROM set from a
flash-cart image.
Because the match is by CRC-32 — and FBNeo itself looks ROM chips up by CRC before falling back to their names — a set is usually accepted as downloaded: extra files, subdirectories, and non-canonical chip filenames inside the zip do not matter. Only the zip's name has to resolve to a driver, which is exactly what this helper supplies.
Neo Geo .neo files (NeoSD / Terraonion)
FBNeo reads .zip and .7z ROM sets only. A .neo — the single-file format for
NeoSD/Terraonion flash carts, and how most Neo Geo homebrew is distributed — is a
4 KiB header followed by each ROM region as one contiguous blob (with the sprite
region byte-interleaved as the cartridge wires it), so it cannot be handed to the
engine as-is. The opt-in @wasm-gaming/fbneo-wasm/neo helper rebuilds the chip
split FBNeo expects, in the browser and with no dependencies:
import { isNeoFile, neoToRomset } from '@wasm-gaming/fbneo-wasm/neo';
const neogeo = await fetch('/romsets/neogeo.json').then((r) => r.json());
if (isNeoFile(bytes)) {
const { driver, zip } = neoToRomset(bytes, neogeo); // throws if it cannot be rebuilt
await load({ canvas, assets: { rom: zip }, options: { driver } });
}It identifies the driver by matching the .neo's region sizes against the chip
layouts in data/romsets/neogeo.json, rebuilds the individual chip dumps
(de-interleaving sprites, undoing the HARDWARE_SNK_SWAPP program-ROM swap), and
CRC-32 verifies every chip before returning — so it throws loudly rather than
handing the engine a broken set. The zip it returns is stored, not deflated:
FBNeo decompresses each chip immediately, so compressing would only be undone.
The same conversion is available offline, which is handy for preparing a set once rather than on every page load:
node scripts/neo2romset.mjs mygame.neo --out ./roms # needs `make build-lib`Sets whose program ROM lives inside the SMA custom chip cannot be reproduced by a straight carve; those fail CRC verification and are reported rather than written.
Options
| Option | Default | Description |
|--------|---------|-------------|
| romFileName | game.zip | Filename used when writing ROM bytes to MEMFS. |
| driver | (inferred) | FBNeo ROM set short name to boot. |
| renderFilter | pixelated | pixelated (crisp) or smooth (linear). |
| audioSampleRate | 44100 | Target audio sample rate in Hz. |
| vsync | true | Enable vertical sync. |
| integerScale | false | Integer scaling only. |
| windowScale | 2 | Internal render-resolution multiplier. |
Build
make build # Full build: WASM (Docker/Emscripten) + TypeScript SDK
make build-sdk # TypeScript only (SDK + manifest + demo shell)
make build-wasm # FBNeo WASM only (via Docker)
make preview # Serve dist/ with COOP/COEP headersWebAssembly threads/SharedArrayBuffer require the COOP/COEP headers that
make preview sets — serve the built dist/ with those headers in production too.
WASM artifacts
| File | Description |
|------|-------------|
| fbneo.js | Emscripten-generated module loader (createFbneoModule). |
| fbneo.wasm | Compiled FBNeo runtime. |
FBNeo uses Emscripten SDL2. The JS loader expects a <canvas id="canvas"> in the
DOM (the SDK sets this id automatically). ROM zips are written to the in-memory
MEMFS under /roms/ before the emulator boots, and FBNeo is launched with the
driver short name.
See CORE.md for the mapping between upstream FBNeo capabilities and the options currently exposed by this wrapper.
