Gauntlet
Gauntlet (Atari Games, 1985) is the arcade game that popularised four-player cooperative play: up to four heroes - Warrior, Valkyrie, Wizard and Elf - fight through top-down mazes swarming with ghosts, grunts and demons while a digitised voice narrates ("Warrior needs food, badly"). This build runs the original Atari "Gauntlet" board in the browser - a Motorola 68010 main CPU, a MOS 6502 sound CPU and the YM2151, POKEY and TMS5220 speech chips, compiled from C++ to WebAssembly - and boots straight into attract mode. It is wired to the emulators.org in-frame debugger so you can single-step the 68000, read and write its registers and memory, and set execution breakpoints and write watchpoints.
Runs on: Web browser
Gauntlet Online Emulator
Play Gauntlet using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Gauntlet | Gauntlet | Gauntlet | grey | Open ⛶ | |
| Gauntlet (2 Players) | Gauntlet | Gauntlet | grey | Open ⛶ | |
| Gauntlet II | Gauntlet | Gauntlet | grey | Open ⛶ |
Machine emulated
The Atari "Gauntlet" board (Atari Games, 1985) - a Motorola 68010 main CPU at ~7.16 MHz, a MOS 6502 sound CPU at ~1.79 MHz, and a trio of sound chips: a Yamaha YM2151 FM synthesiser, an Atari POKEY, and a TMS5220 speech synthesiser for the game's spoken warnings. Video is a scrolling tile playfield plus an alphanumeric layer and a motion-object (sprite) engine scanned as a 336×240 raster, and Atari's slapstic PAL scrambles the 68000 ROM addresses as copy protection. The same board hosts Gauntlet II (1986).
Chips
Notes
Embedding
The core is FinalBurn Neo (FBNeo) - a mature, accurate arcade emulator - compiled to WebAssembly with Emscripten and driving its atari/d_gauntlet driver. FBNeo normally ships as a libretro core or an SDL app; instead of pulling in either frontend we wrote a tiny standalone Emscripten frontend, gauntlet_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 gauntlet.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt gauntlet.js / gauntlet.wasm and each game as a standard FBNeo/MAME romset ZIP.
// gauntlet.js defines a global Module factory; instantiate it, stage game name + romset, boot.
var g = await Module({ locateFile: f => SRC + f }); // finds gauntlet.wasm
var np = g._wasm_game_buf(name.length); g.HEAPU8.set(name, np); // "gauntlet"
var rp = g._wasm_rom_buf(rom.length); g.HEAPU8.set(rom, rp); // stage the romset ZIP
g._wasm_start(); // select driver, load ROMs, BurnDrvInit, reset
Rendering is manual. FBNeo rasters into its own bitmap (pBurnDraw); the frontend converts the visible 336×240 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(g.HEAPU8.buffer, g._wasm_fb_ptr(), 336*240*4);
backImg.data.set(rgba); bctx.putImageData(backImg, 0, 0);
ctx.drawImage(back, 0,0, 336,240, 0,0, canvas.width, canvas.height);
Input is the Gauntlet player latch. Each of the four heroes has an 8-way stick plus a Fire and a Magic button, and there is a per-player Coin slot (Gauntlet has no Start button - inserting a coin drops a hero straight into the maze). 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.
| Member | Kind | What it does |
|---|---|---|
_wasm_game_buf(n) / _wasm_rom_buf(n) | export | Allocate + return a staging pointer for the driver short-name and the romset ZIP. |
_wasm_start() | export | Select the Gauntlet driver by name, install a zip-backed BurnExtLoadRom, BurnDrvInit, wire input, reset. |
_wasm_tick() | export | Run exactly one video frame (BurnDrvFrame) and convert it to RGBA. Our frame-step and the run loop's advance. |
_wasm_fb_ptr() | export | Pointer to the 336×240 RGBA framebuffer in the heap. |
_wasm_set_input(player,bits) | export | Write a player latch (bit0 Up, bit1 Down, bit2 Left, bit3 Right, bit4 Fire, bit5 Magic; pressed=1). |
_wasm_set_sys(v) | export | Coin bits (bit0 P1 … bit3 P4) - needed to credit the game. |
_wasm_dbg_* | export | The debug sampling API (registers, memory, step, reset) - see below. |
_wasm_vw() / _wasm_vh() | export | Visible picture size (336×240). |
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 keeps its register file in a plain C context reachable through m68k_get_reg / m68k_set_reg, and SekRun(0,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, gauntlet_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 FBNeo's own 68000 interface. 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) is byte-for-byte unchanged (the golden performance rule).
// gauntlet_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_read(u32 a){ // side-effect-free: pointer-mapped pages only
if (a <= 0x037fff || (a>=0x040000 && a<=0x07ffff) // 68K program ROM
|| (a>=0x800000 && a<=0x801fff)) // 68K work RAM
return SekReadByte(a);
return 0; } // slapstic / I-O windows read 0
void wasm_dbg_step(){ SekRun(0, 1); } // REAL one-instruction step
2 · What is REAL here. Because FBNeo 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 the 68000 register file.
- Real single-instruction step -
Step icallswasm_dbg_step()=SekRun(0,1), which executes exactly one 68000 instruction; PC and the registers change by one instruction per click. - Side-effect-free memory - reads resolve only the pointer-mapped program ROM and 8 KB work RAM at $800000 via
SekReadByte(returning 0 for the slapstic window at $038000 and all I-O), so auto-polling the hex/disasm view never trips the copy-protection bank switch 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 6502 sound CPU and 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
Atari Games' Gauntlet (1985) is the arcade game that popularised four-player cooperative play; up to four heroes - Warrior, Valkyrie, Wizard and Elf - fight through top-down mazes while a digitised voice narrates ("Warrior needs food, badly"). FBNeo emulates the whole board; our build is a thin standalone Emscripten port of its atari driver. The whole machine lives in one WASM module built from C/C++:
- Motorola 68010 - the main CPU at ~7.16 MHz (object-code compatible with the 68000, which is why the shared
m68000decoder disassembles it). Its register context is what this integration samples viam68k_get_reg;SekRun(0,1)is the run-one-instruction primitive used for single-stepping. - MOS 6502 - the sound CPU at ~1.79 MHz, which drives the audio chips and answers the 68000 over a command latch.
- YM2151 + POKEY + TMS5220 - the FM synthesiser, Atari's POKEY (tones + noise), and the TMS5220 speech chip that together make Gauntlet's music, effects and the famous spoken warnings.
- Atari custom video - a scrolling playfield tile layer, an alphanumeric layer, and a motion-object (sprite) engine composited into a 336×240 picture; the slapstic is Atari's address-scrambling copy-protection PAL mapped into the 68000 ROM space at $038000.
- Memory map - the 68000 bus places the program ROM at $000000-$07FFFF (with the slapstic window at $038000), 8 KB of work RAM at $800000, and the playfield / motion-object / palette RAM and I-O in the $900000 region. 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) and a Gauntlet-only driverlist.h
# 2. add gauntlet_wasm.cpp (burn-library frontend + wasm_dbg_* hooks) + burner_stubs.cpp
# 3. compile the burn core + atari/d_gauntlet + 68000 + 6502 + YM2151/POKEY/TMS5220
# + atariic/atarimo/slapstic/watchdog + the shim:
emcc -O2 <burn core + atari + cpu + snd .c/.cpp> gauntlet_wasm.cpp -o gauntlet.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,... # -> gauntlet.js + .wasm
The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. The 68000, 6502 and sound cores all run in plain C/C++; there is no dynarec, so nothing needs writable-executable memory.