SearchA-ZO › Out Run

Out Run

1986 Arcade Online

Out Run is Sega's 1986 arcade driving game, running here in the browser from its authentic program ROMs. The board is unusual for its day: two Motorola 68000 CPUs (a main CPU for the game and a sub CPU for the road and object maths) plus a Z80 for sound, driving Sega's System-16B tilemap video, a custom road generator and a zoomed-sprite chip. The road, the Ferrari and the roadside sprites all render through the attract and demo; sound is silent.

There is no standalone JavaScript 68000 on the site, so this build vendors a standalone 68000 execution core — Karl Stenerud's Musashi (MIT) — compiled to WebAssembly on its own and wrapped in a hand-written Out Run board (the Sega 315-5195 memory mapper, the two 68000s driven through Musashi's context switch, and the System-16B tilemap and palette video). A small sampling API over Musashi's register and memory interface feeds the shared in-frame debugger, so you get genuine live 68000 registers, side-effect-free memory, single-instruction step, execution breakpoints and write watchpoints.

Out Run Online Emulator

Play Out Run using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Out RunOut RunOut RungreyOpen ⛶

Machines emulated

Chips

Notes

Embedding

Out Run's board is two Motorola 68000s (a main CPU and a sub CPU) plus a Z80 for sound. The site's other 68000 machines are whole WASM-bundled emulators, so there was no standalone JS 68000 to reuse. This build vendors a standalone 68000 execution core — Karl Stenerud's Musashi (MIT) — compiled to WebAssembly on its own, and wraps it in a small hand-written Out Run board (outrun.c): the Sega 315-5195 memory mapper, the two 68000s driven through Musashi's context switch, the System-16B tilemap + palette video, and the I/O. The board is a port of MAME's sega/segaorun.cpp and sega/segaic16.cpp. Everything is self-hosted (no CDN): outrun.js / outrun.wasm and the program, tile, road and sprite ROMs.

// outrun.js is a MODULARIZE factory; instantiate it, stage the ROMs, boot.
var m = await Module({ locateFile: f => SRC + f });   // finds outrun.wasm
m.HEAPU8.set(mainrom, m._wasm_main_rom());   // staged into the C ROM arrays
m.HEAPU8.set(subrom,  m._wasm_sub_rom());
m.HEAPU8.set(gfx,     m._wasm_gfx_rom());    // tiles
m.HEAPU8.set(road,    m._wasm_road_rom());   // road gfx
m.HEAPU8.set(spr,     m._wasm_sprite_rom()); // zoomed sprites (32-bit LE)
m._wasm_start();                              // reset both 68000s and run

The 4 program EPROMs are byte-interleaved the way the hardware wires them (even byte on the upper data lane, odd on the lower), exactly as MAME's ROM_LOAD16_BYTE does, before staging. Rendering is manual: the board rasters into a 320×224 RGBA framebuffer in the WASM heap, which we blit to an off-screen canvas each frame and draw scaled to the visible canvas.

var rgba = new Uint8ClampedArray(m.HEAPU8.buffer, m._wasm_fb_ptr(), 320*224*4);
img.data.set(rgba); bctx.putImageData(img, 0, 0);
ctx.drawImage(back, 0,0,320,224, 0,0, canvas.width, canvas.height);

Input is the real cabinet's controls. Steering, gas and brake are analogue, read through the board's ADC0804: we push a byte per channel (_wasm_set_adc) — steering centres at 0x80, the pedals run 0x000xff. Start, coin and the gear-shift toggle are digital bits on the SERVICE port (_wasm_set_dport).

Debugger integration

This is a WebAssembly build, yet it gets the same live debugger as the pure-JavaScript machines — real 68000 registers, real memory, a real single-instruction step, plus execution breakpoints and write watchpoints. The core 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 appended to the board.

1 · What was added. A block of EMSCRIPTEN_KEEPALIVE functions in outrun.c read and write live state through Musashi's own API. They are sampling functions: the debugger calls them a few times a second while its window is open, and once per Step. There is no per-instruction or per-cycle hook in the hot path (the frame runs at native speed). Because Out Run has two 68000s, the sampling functions always select the main CPU's saved Musashi context before reading, so the debugger inspects the main CPU.

// outrun.c — sampling only, over the MAIN 68000's context
static void dbg_main(void){ m68k_set_context(CTX_MAIN); CUR = 0; }
u32  wasm_dbg_reg(int i){ dbg_main();                 // 0-7 = D0-D7, 8-15 = A0-A7
  return i<8 ? m68k_get_reg(0,M68K_REG_D0+i)
            : m68k_get_reg(0,M68K_REG_A0+(i-8)); }
u32  wasm_dbg_read(u32 a){ return main_read8(a); }   // side-effect-free bus read
void wasm_dbg_step(void){ m68k_set_context(CTX_MAIN); m68k_execute(1); m68k_get_context(CTX_MAIN); }

2 · What is REAL. D0-D7, A0-A7, PC, SR (with the X N Z V C S flags) and USP/SSP are read and written live off Musashi's register file. Step i runs exactly one 68000 instruction. Memory reads resolve the program ROM, work RAM, tile / text / palette / sprite RAM and the sub-CPU shared RAM directly (returning safe values for I/O windows), so auto-polling the hex / disasm view never kicks the watchdog or clears a latch. The disassembly uses the shared m68000 decoder.

3 · Breakpoints and watchpoints without a hot-path hook. When nothing is armed the loop runs a whole frame at native speed (_wasm_tick). As soon as a breakpoint or watchpoint is set, the loop switches to stepping the main 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 — a pure host-side check that costs nothing when the debugger is closed.

Architecture

Sega's Out Run (1986) is one of the most influential arcade racers. Its board is unusually rich for its day:

  • Two Motorola 68000s — a main CPU (game logic, the debugger target) and a sub CPU (road and object maths), sharing work RAM and the road buffers. Both run on Musashi; a single WASM module drives the two of them by swapping Musashi's CPU context (m68k_get_context / m68k_set_context) between the two buses each slice.
  • Sega 315-5195 memory mapper — Out Run has no fixed address map; the main 68000 programs this chip at boot to lay out ROM, work RAM, the tile / text / palette / sprite RAM, the I/O and the shared window to the sub CPU. The board presents Out Run's fixed decoded layout directly.
  • System-16B tilemap video — two scrolling 128×64 tile planes (built from 64×32 pages) plus a fixed 64×28 text layer, 8×8 three-plane tiles, through a 4096-entry palette (five bits per channel with a shade/hilight bit). This draws the clouds, hills and text.
  • Sega road generator — the custom road chip fills each scanline below the horizon from a road ROM: two road layers with per-line horizontal scroll, colour tables and a priority PAL for the stripes and shoulders, plus the solid sky fill above the horizon. This is the perspective road you drive on.
  • Zoomed-sprite chip — Out Run's sprite generator scales each sprite in both axes from a 1 MB sprite ROM (hardware zoom, flip, per-line pitch, four banks). This draws the Ferrari, the traffic and the roadside objects, mixed against the tilemap and road by a 4-level priority.
  • Zilog Z80 + sound — a Z80 running the authentic sound program drives a YM2151 (Yamaha OPM FM synth, the music) and a Sega 315-5218 SegaPCM 16-channel sampler (engine, voices and effects, from the PCM ROMs). This build emulates all three and mixes them to stereo — see the Sound section below.

How to build this artefact. Toolchain: Homebrew emscripten (emcc on PATH). Generate Musashi's opcode tables, then compile the core + the board:

# 1. generate Musashi's opcode handlers (m68kmake reads m68k_in.c)
cc -o m68kmake m68kmake.c && ./m68kmake
# 2. compile the standalone 68000 core + the Out Run board to wasm
emcc -O2 outrun.c m68kcpu.c m68kops.c m68kdasm.c softfloat/softfloat.c -o outrun.js      -sMODULARIZE=1 -sEXPORT_NAME=Module -sALLOW_MEMORY_GROWTH=1      -sEXPORTED_FUNCTIONS=_wasm_start,_wasm_tick,_wasm_dbg_pc,...   # -> outrun.js + .wasm

Single-threaded (no SharedArrayBuffer), so it hosts anywhere. Musashi has no dynarec, so nothing needs writable-executable memory.

Sound

Out Run's sound board is a whole second computer, and this build runs it for real: a Zilog Z80 executes the authentic sound program (epr-10187.88), driving a Yamaha YM2151 (OPM FM synth — the music) and a Sega 315-5218 “SegaPCM” 16-channel sampler (engine, tyre and voice samples, read from the six PCM ROMs). All three are compiled into outrun.wasm alongside the 68000 board: the Z80 is superzazu's core (MIT), the YM2151 is the Jarek Burczynski / MAME core, and the SegaPCM is ported from MAME. This is an authored core emulating the real chips driven by the real ROMs — not a synthesized stand-in and not a recording.

The sound-command path (cycle-accurate). The main 68000 sends commands to the sound board through the 315-5195 mapper's sound register. The 68000 talks to that chip as 16-bit words on data lines D0–D7, so word-register 3 (the sound latch) carries its data on the odd byte — byte offset 0x07 of the register window. Each command is delivered to the Z80 at the exact time the main CPU wrote it: the Z80 is interleaved with the two 68000s through the frame, and on a sound write the board catches the Z80 (and the YM2151 / SegaPCM) up to that write's frame-cycle — computed from the main CPU's own cycle counter (m68k_cycles_run()) — before it latches the byte and pulses the Z80 /NMI. The Z80's handler reads the latch on port 0x40, which clears the “port B full” flag (the read acknowledge). This replaces an earlier revision that queued commands and drained them one per audio chunk, so a command could arrive milliseconds late; now commands land where they belong — during the attract the three per frame come in on the raster-interrupt scanlines, at ¼/½/¾ of the frame.

// outrun.c — main-68000 write to the 315-5195 sound register (word 3 = byte 0x07)
case 0x07:
  TO_SOUND = data;
  // run the Z80 + chips up to this write's exact frame-cycle, then deliver
  { long now = MAIN_CYC_BASE + m68k_cycles_run();
    snd_advance((int)((long long)(SND_RATE/60) * now / MAIN_CYC_FRAME)); }
  snd_latch(data);   // latch byte, raise port-B-full, pulse Z80 /NMI now
  break;

The acknowledge handshake. The 315-5195 comms port is modelled both ways, exactly as the chip works: a main write raises port B full and the Z80's latch read clears it, and the return latch (from_sound) is wired so a Z80 write to its port 0x40 lands in the register the main reads back at word 3. Out Run's sound program is one-way — it only ever reads the command latch and never drives the return latch — so that acknowledge byte idles at its reset value here just as it does on the real board; nothing in the game gates on it.

The FM tempo. The sound driver clocks its music off the YM2151's Timer A, polling the chip's status flag. The YM2151 core has two timer models; this build uses the internal per-sample timer (it advances inside the sample generator and sets the overflow flag), so the flag actually ticks and the sequencer runs. With the external timer model selected instead, nothing schedules the timer and the driver's tempo loop spins forever — that was the second thing blocking sound.

Rate, mixing and the sink. Both chips are initialised to EmuAudio.sampleRate (the shared AudioContext rate), so no resampling is needed: across a video frame the board runs the Z80 for one frame of its 4 MHz clock — interleaved with the 68000s and advanced at every sound write and every scanline — and produces exactly round(sampleRate/60) stereo sample-pairs of YM2151 + SegaPCM, mixed (with fixed gains) and clamped to Int16. embed.js reads that buffer each frame and calls EmuAudio.push(). Audio starts muted (browsers block audio before a gesture); the shell's Sound button calls transport.setMute(false), which resumes the context and unmutes. Demo Sounds is on by default, so the attract mode plays its music without a coin.

Honest residual. The one remaining by-ear choice is the mix balance: the YM2151 and SegaPCM are summed with fixed gains tuned by listening, not measured against a real cabinet. The command timing itself is no longer approximate — it is driven off the main CPU's cycle counter — and the acknowledge handshake is modelled in full (Out Run just never uses the return direction).