SearchA-ZV › v86

v86

2012 Open source · BSD-2-Clause Online

v86 emulates a complete 32-bit x86 PC in JavaScript/WebAssembly, booting MS-DOS, Windows, Linux, ReactOS and more from disk images entirely client-side. A striking demonstration of full-system emulation on the open web.

Visit the official site ↗

Runs on: Web browser

v86 Online Emulator

Play v86 using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
FreeDOSv86IBM PC/ATDOS (MS-DOS / DR-DOS)greyOpen ⛶
Windows 3.1v86IBM PC/ATWindows 3.xgreyOpen ⛶
Windows 3.0v86IBM PC/ATWindows 3.xgreyOpen ⛶
Windows 98v86IBM PC/ATWindows 9xgreyOpen ⛶
Windows 95v86IBM PC/ATWindows 9xgreyOpen ⛶
ReactOSv86IBM PC/ATReactOSgreyOpen ⛶
Buildroot Linuxv86IBM PC/ATLinuxopenOpen ⛶

Machines emulated

Operating systems

Chips

Notes

Embedding

v86 emulates a whole x86 PC — CPU, chipset, BIOS, VGA, PS/2 keyboard — with the CPU compiled to WebAssembly. You construct one V86 object, point it at self-hosted BIOS + a boot disk, and give it a screen container; it fetches and instantiates v86.wasm and boots. We self-host everything (no CDN): libv86.js, v86.wasm, SeaBIOS + VGABIOS, and a 720 KB FreeDOS floppy.

var emulator = new V86({
  wasm_path:        "/emulator/v86/src/v86.wasm",
  bios:             { url: "/emulator/v86/src/seabios.bin" },
  vga_bios:         { url: "/emulator/v86/src/vgabios.bin" },
  fda:              { url: "/emulator/v86/src/freedos722.img" },   // floppy A: -> A:\>
  memory_size:      32 * 1024 * 1024,
  vga_memory_size:  2  * 1024 * 1024,
  screen_container: screenContainer,                    // a <div> holding a text <div> + a <canvas>
  autostart:        true
});

The screen is two elements, not one. v86 renders VGA text mode into an inner <div> (real DOM text, white-space:pre) and graphics mode into a <canvas>, toggling their visibility. The debugger re-homes one screen element between the play view and its Screen window, so we hand it the whole screen_container — both children travel together and DOS text keeps rendering wherever the container lands.

MemberKindWhat it does
emulator.run() / emulator.stop()methodStart / stop the WASM CPU loop. Our transport's resume / pause.
emulator.restart()methodReboot the machine.
emulator.v86.cpu.main_loop()methodRun ONE time-slice of the CPU (the only step primitive a JIT-in-WASM core exposes). Our step / stepInsn.
emulator.keyboard_send_scancodes([..])methodInject Set-1 keyboard scancodes over the PS/2 bus — drives the on-screen keyboard.
emulator.v86.cpuobjectThe live CPU: typed-array views onto the WASM memory (registers + RAM) and a handful of bound wasm exports.

Debugger integration

This is the interesting part: how a WebAssembly CPU gets the same debugger as the pure-JS cores. v86 does not hide its state inside the WASM sandbox — it keeps the entire architectural state in one linear WASM memory and, at construction, wraps typed-array VIEWS around fixed byte offsets in that memory. Those views are ordinary JavaScript objects, so reading and writing them reads and writes the exact bytes the CPU executes on next cycle. No message passing, no serialization.

What the debugger needsWhere it lives in the WASM core
EAX ECX EDX EBX ESP EBP ESI EDIcpu.reg32 — an Int32Array(8) view; writable (poking an element writes WASM memory).
Instruction pointer (for disasm)cpu.instruction_pointer[0] — the linear EIP (CS base + IP); cpu.get_real_eip() gives the IP offset inside CS.
Segment selectors CS DS SS ES FS GScpu.sreg — a Uint16Array(8) view (order ES CS SS DS FS GS). Shown read-only: writing a selector alone would not recompute the cached segment base the JIT uses.
EFLAGS + condition flagscpu.get_eflags() — a bound wasm export that recombines cpu.flags with the lazily-evaluated flags_changed. Read-only for the same lazy-eval reason.
Physical RAMcpu.mem8 — a Uint8Array over guest physical memory; mem8[phys] is a side-effect-free byte read/write (it is the backing store, so it never touches an I/O device).

Reaching the views. The public V86 object hides the machine one level down: emulator.v86 is the machine and emulator.v86.cpu is the CPU carrying the views above. A few wasm functions are already bound onto that JS object (get_eflags, get_real_eip, get_seg_cs, main_loop), so we call them directly. The boot script grabs emulator.v86.cpu after construction and publishes it, so the plug-in never has to know about WASM at all.

Play / pause / step — and the honest limit. Pause is emulator.stop(), resume is emulator.run(), and we track an isPaused flag the shell's Play button and the debugger both read. The hard part is single-instruction step. A JIT-in-WASM core has no clean one-instruction primitive: the only export that advances the CPU is main_loop(), which runs a whole time-slice (many instructions, until a HLT or the slice's time budget). We inspected every published build — release, fallback (interpreter) and debug — and NONE exports a per-instruction cycle_internal / do_many_cycles. So our step is a burst step: while stopped we call cpu.main_loop() once, which advances EIP and the registers by one slice. That is enough to watch the registers and memory change live, but it is not one instruction, and we say so. (A true single step would need the x86 Trap Flag routed to a host trap — v86 does not surface that — or a patched debug WASM that exports the interpreter's inner cycle.)

Breakpoints / watchpoints are surfaced (the gutter and lists work) but best-effort: because normal execution happens inside v86's own WASM loop and a burst runs many instructions, a host-side PC check can only fire at burst boundaries, not before each instruction. They are honestly labelled as coarse rather than pretended exact.

What to copy for another WASM core (js-dos, np2kai, px68k, vAmigaWeb, infinite-mac, MartyPC). The whole technique is: (1) find the object that holds typed-array VIEWS onto the core's linear memory — if the core exposes registers/RAM as views, you are done, just read/write them each refresh; (2) map the debugger's register set and memory read/write straight onto those views; (3) wire pause/resume to whatever stop/run the core has; (4) for step, use the smallest "advance" the core exports (ideally one instruction; a time-slice if not) and be explicit about the granularity. A core that keeps state in views is fully debuggable from JS; a core that keeps state private inside WASM locals would need a patched build that copies its state out — that is the dividing line for the rest of the Tier-4 rollout.

Architecture

v86 is a full-system PC emulator. The x86 CPU is compiled to WebAssembly (with a self-hosted JIT that translates hot basic blocks to more WASM at run time); the surrounding machine — PIC, PIT, PS/2 controller, VGA, floppy/IDE, RTC — is JavaScript talking to the CPU over a bus. It boots real software from disk images entirely client-side.

  • libv86.js — the JavaScript machine: the V86 façade, the device models, the screen/keyboard adapters, and the loader that instantiates the WASM.
  • v86.wasm — the x86 CPU core + JIT, holding all CPU state in one linear memory (and the zstd decoder that inflates the boot-state snapshots).
  • seabios.bin / vgabios.bin — the self-hosted system + video BIOS (SeaBIOS / VGABIOS, LGPL, freely redistributable).
  • A menu of self-hosted OS disks, one per config: a FreeDOS floppy, Windows 3.0 / 3.1 / 95 hard disks (cold-boot to the desktop), Windows 98 and ReactOS hard disks that resume from a pre-booted .bin.zst RAM snapshot (straight to the desktop), and a Buildroot Linux live CD. Each config in the embed's configs[] carries a small v86 spec — memory size, which drive (fda / hda / cdrom), and how the image loads.

Loading a big disk without a big download. The small images (Win 3.x, DOS, Linux) load whole. The large ones (Win 95/98 ~300-450 MB, ReactOS ~700 MB) are split into fixed-size part files (async + use_parts): v86 fetches only the blocks the guest actually reads, each as a plain whole-file GET, so it self-hosts on any static server with no HTTP Range support. Paired with a state snapshot, only the desktop's working set ever downloads.

DOS and Linux boot in real mode / text mode, so the shared 16-bit x86 disassembler drives the disasm view exactly. The Windows and ReactOS guests run in 32-bit protected mode, where the same register / memory / step machinery is fully live (EAX..EDI, EIP, segments, EFLAGS, physical RAM) and the disasm view is approximate (the decoder is an 8086/16-bit model).

Sound

v86 already models real PC sound hardware — a Sound Blaster 16 (its DSP/DAC plus an OPL FM block and the SB mixer) and the PC speaker (PIT channel 2 gating a square wave) — and it drives them through its own WebAudio pipeline. So this is a native-audio integration: we do not route samples through the site's shared audio sink. We reuse v86's pipeline as-is and only add a master mute in front of the speakers.

The pipeline. When the machine is built, v86 constructs a SpeakerAdapter that owns one AudioContext and a small graph: the SB16 DAC (an AudioWorklet, or a ScriptProcessor fallback) and the PC-speaker OscillatorNode feed a mixer whose treble/bass/gain stages meet at a ChannelMerger, and that merger connects to the context's destination. The adapter also suspends the context when the CPU stops and resumes it when the CPU runs.

The handle. The context is emulator.speaker_adapter.audio_context and the mixer's final node is emulator.speaker_adapter.mixer.node_merger. On emulator-ready the boot script splices one GainNode into that last hop — it disconnects the merger from the destination and reconnects node_merger -> masterGain -> destination — so a single gain controls everything the guest can make audible.

Mute contract. The transport exposes the standard pair, both starting muted:

var muted = true;                              // browsers block audio before a gesture
transport.isMuted = function(){ return muted; };
transport.setMute = function(m){
  muted = !!m;
  if(muted){ master.gain.value = 0; actx.suspend(); }   // silence + park the context
  else    { master.gain.value = 1; actx.resume(); }   // unmuted from the button's real click
};

The master gain is the reliable mute: v86 re-resumes its context whenever the CPU runs (including when you un-pause in the debugger), so holding masterGain.gain = 0 guarantees silence no matter what the adapter does with the context. The suspend() is a belt-and-braces extra for the muted state; unmuting sets gain to 1 and calls resume() from the real click.

Sound plays when the software makes it. Each machine boots to its natural state and stays silent until the guest programs the sound hardware. Sound is fully supported through the pipeline above. The page starts muted, so click the Sound button to hear it.