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).