v86
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.
Runs on: Web browser
v86 Online Emulator
Play v86 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| FreeDOS | v86 | IBM PC/AT | DOS (MS-DOS / DR-DOS) | grey | Open ⛶ |
| Windows 3.1 | v86 | IBM PC/AT | Windows 3.x | grey | Open ⛶ |
| Windows 3.0 | v86 | IBM PC/AT | Windows 3.x | grey | Open ⛶ |
| Windows 98 | v86 | IBM PC/AT | Windows 9x | grey | Open ⛶ |
| Windows 95 | v86 | IBM PC/AT | Windows 9x | grey | Open ⛶ |
| ReactOS | v86 | IBM PC/AT | ReactOS | grey | Open ⛶ |
| Buildroot Linux | v86 | IBM PC/AT | Linux | open | Open ⛶ |
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.
| Member | Kind | What it does |
|---|---|---|
emulator.run() / emulator.stop() | method | Start / stop the WASM CPU loop. Our transport's resume / pause. |
emulator.restart() | method | Reboot the machine. |
emulator.v86.cpu.main_loop() | method | Run ONE time-slice of the CPU (the only step primitive a JIT-in-WASM core exposes). Our step / stepInsn. |
emulator.keyboard_send_scancodes([..]) | method | Inject Set-1 keyboard scancodes over the PS/2 bus — drives the on-screen keyboard. |
emulator.v86.cpu | object | The 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 needs | Where it lives in the WASM core |
|---|---|
| EAX ECX EDX EBX ESP EBP ESI EDI | cpu.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 GS | cpu.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 flags | cpu.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 RAM | cpu.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: theV86faç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.zstRAM snapshot (straight to the desktop), and a Buildroot Linux live CD. Each config in the embed'sconfigs[]carries a smallv86spec — 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).