SearchA-ZJ › js-dos

js-dos

2015 Open source · GPL-2.0 Online

js-dos is a WebAssembly build of DOSBox with a small JavaScript API, used to embed playable DOS games and utilities directly in a web page. It is the technology behind the Internet Archive’s in-browser MS-DOS collection.

Visit the official site ↗

Runs on: Web browser

js-dos Online Emulator

Play js-dos using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
DOS promptjs-dosIBM PC/ATDOS (MS-DOS / DR-DOS)openOpen ⛶

Machines emulated

Operating systems

Chips

Notes

Embedding

js-dos is DOSBox compiled to WebAssembly with Emscripten. It is one ordinary Emscripten Module: you give it a <canvas>, optional command-line arguments, and it fetches dosbox.js + dosbox.wasm and boots. We self-host everything (no CDN); there is no external ROM because DOSBox ships its own DOS kernel and BIOS, so the whole thing is GPL-2 and freely redistributable.

var Module = {
  canvas:    document.getElementById('canvas'),
  arguments: ['-c', 'mount c /c', '-c', 'c:'],   // land at C:\>
  preRun:    [ function(){ Module.FS.mkdir('/c'); } ],
  onRuntimeInitialized: boot                                // wasm ready -> publish EMU_BOOT
};
// <script src="/emulator/js-dos/src/dosbox.js"></script> instantiates it.
MemberKindWhat it does
Module._emudbg_pause(1|0)hookPause / resume by swapping DOSBox's frame loop (see the debugger note). Our transport's pause / resume.
Module._emudbg_step()hookQueue exactly one CPU instruction; the paused frame loop runs it. Our step / stepInsn.
Module._emudbg_reg(i) / _emudbg_set_reg(i,v)hookRead / write one x86 register by index (added to DOSBox — see below).
Module._emudbg_read(addr)hookOne side-effect-free guest byte. Drives the memory + disassembly views.
Module.canvaselementThe SDL2 render target. The debugger re-homes it between the play view and its Screen window.

Physical keys work through SDL2's own window key handling once the canvas has focus. The on-screen keyboard dispatches synthetic KeyboardEvents (keyed on event.code) that SDL2 receives the same way.

Debugger integration

This is the Tier-4 point: giving a COMPILED wasm core the same debugger as the pure-JS emulators by rebuilding it with hooks. em-dosbox keeps its whole CPU state inside the wasm sandbox — no views leak out — so we added ONE source file, src/emudbg.cpp, whose functions SAMPLE that state on demand. They are called ~10×/second by the debugger refresh and once per Step; nothing is added to DOSBox's per-instruction hot path (the golden performance rule).

What the debugger needsThe hook, and the DOSBox internal it reads
EAX ECX EDX EBX ESP EBP ESI EDI, EIPemudbg_reg(i) / emudbg_set_reg(i,v) read/write reg_eaxreg_edi, reg_eip — the cpu_regs struct in include/regs.h. Freely writable.
CS DS SS ES FS GS, EFLAGSemudbg_reg(i) reads SegValue(cs..gs) (the Segs struct) and reg_flags. Shown read-only.
Program counter for disasmemudbg_pc() returns the real-mode linear address (SegValue(cs)<<4)+reg_ip.
Physical RAMemudbg_read(addr) / emudbg_read_block(addr,len) call phys_readb() (from include/mem.h) — the raw MemBase backing store, so a read never touches an I/O device.

Pause / resume. DOSBox drives emulation through a swappable loop pointer (DOSBOX_SetLoop). emudbg_pause(1) installs our own Emudbg_Pause_Loop; emudbg_pause(0) restores Normal_Loop. The pause loop holds the CPU still and yields a frame to the browser, so while paused the whole page — and the debugger — stays fully responsive.

Single-step — a real one-instruction step. DOSBox's own debugger steps by running the core with a one-instruction budget: CPU_Cycles = 1; (*cpudecoder)();. emudbg_step() queues that, and the paused frame loop executes exactly one instruction, then stops again. So unlike v86 (whose JIT-in-wasm core only exposes a whole time-slice), Step here advances the CPU by a single x86 instruction and you watch EIP and the registers move one op at a time.

Breakpoints / watchpoints are surfaced but best-effort: normal execution runs inside DOSBox's own loop and DOSBox's native execution-breakpoint facility is a separate DEBUG build, so a host-side PC check would mean a per-instruction hook — which the golden rule forbids. They are honestly labelled coarse; true PC breakpoints would need DOSBox's C_DEBUG core wired to the hooks.

The honest headless caveat. DOSBox waits for keyboard input with a host-side idle loop that (by design, so its cooperative Asyncify threading stays valid) does not yield mid-instruction. At an idle DOS prompt that keeps the wasm thread busy. It is irrelevant in a real browser (compositing is off-thread) and it disappears the moment you PAUSE — which is exactly when the debugger is used — because the pause loop yields every frame.

Architecture

DOSBox is a full-system PC emulator: an x86 real-mode CPU (8086/286-class), plus PIC, PIT, DMA, VGA/CGA/EGA, SB16, and its own DOS kernel and BIOS. em-dosbox compiles the whole thing to WebAssembly with Emscripten Asyncify, so DOSBox's blocking, cooperative code runs unchanged in the browser.

  • dosbox.js — the Emscripten runtime + glue that instantiates the wasm, owns the canvas, and routes SDL2 input/audio.
  • dosbox.wasm — DOSBox itself: the CPU cores, the device models, the DOS/BIOS, and our emudbg.cpp hooks.

The default machine is a real-mode DOS PC, so the shared 16-bit x86 disassembler drives the disasm view correctly. DOS software that flips into 32-bit protected mode will disassemble approximately (the decoder is an 8086/16-bit model).

Sound

Pattern V-native. DOSBox already emulates the PC's sound hardware — the Sound Blaster (PCM/DMA digital audio), the AdLib / OPL2 FM synthesiser, and the PC speaker tone generator — and mixes them to one stereo stream in its own mixer. Because em-dosbox is an Emscripten SDL2 build, that mixed stream already flows through SDL2's own WebAudio pipeline, so we do NOT route anything through the site's shared EmuAudio sink; we drive the core's existing graph and only govern mute. Rerouting a working SDL2 output would risk double audio or phase corruption.

Where the samples come from. Emscripten's SDL2 lazily creates an AudioContext the first time DOSBox opens the audio device and installs a ScriptProcessorNode whose onaudioprocess pulls a fresh block of mixed samples out of the wasm heap on demand (via Module.SDL2.audio.currentOutputBuffer), wiring that node straight to audioContext.destination.

The handle. The context is Module.SDL2.audioContext and the source node is Module.SDL2.audio.scriptProcessorNode. Browsers block audio until a real user gesture, and SDL2 auto-resumes the context on any page gesture, so to guarantee the machine starts silent and only sounds when the viewer asks, we splice a GainNode between the script-processor and the destination and hold it at zero while muted:

// splice: scriptProcessorNode -> gain(0) -> destination, context suspended
var g = ctx.createGain();
g.gain.value = 0;
g.connect(ctx.destination);
node.disconnect();
node.connect(g);
ctx.suspend();

A ~300 ms guard re-asserts silence and (re)splices the gain the moment SDL first opens audio, so a stray gesture can never leak sound before the button is pressed.

The contract. window.EMU_BOOT.transport exposes isMuted() and setMute(m). It starts muted. setMute(false) — called from the real click on the Sound button — sets the gain to 1 and resume()s the context; setMute(true) sets the gain back to 0 and suspend()s it, which also stops the script-processor callbacks so nothing reaches the destination.

Boots to its natural state. The machine lands at a C:\> prompt and makes no sound on its own. Sound is fully supported and plays whenever the DOS software programs the PC's audio hardware. The page starts muted, so click the Sound button to hear it.

Headless caveat (honest). Full DOSBox cannot be exercised in this GPU-less, single-thread headless sandbox — SDL2's WebGL renderer has no GPU and DOSBox's Asyncify main loop never yields to the CDP task queue (see the debugger note and RUNNING.md). What is verified headless: the boot script parses and publishes the transport, and the mute contract is intact (!!(transport.setMute && transport.isMuted) is true and it starts muted).