Execution Pipeline
Click any stage to see details.
Main Loop Walkthrough
JS schedules the next tick via
setTimeout / requestAnimationFrame.JS resumes from the browser event loop and calls into WASM.
Runs for a fixed number of cycles or until a timer fires. Tight inner loop in Rust-compiled WASM.
Reads EIP → translates virtual address → fetches bytes from WASM linear memory (guest RAM).
Looks up EIP (physical) in the JIT hash table. If a compiled function exists, jump-calls it directly.
JIT function runs natively; interpreter decodes prefix, opcode, ModRM, SIB, displacement, immediate bytes.
If
IN/OUT or MMIO access: WASM calls JS import (e.g. io_port_write8) → device handler runs → returns.run_hardware_timers() updates PIT, RTC, APIC timer — may raise IRQs.If IRQ pending and IF flag set: fetch IDT entry, push CS:EIP, load handler vector, dispatch.
If time budget spent: return to JS, schedule next tick. Otherwise loop back to step 3.
JIT Compilation Pipeline
When the WASM interpreter executes an x86 basic block frequently enough, the JIT compiler kicks in and translates that block to a native WebAssembly function. Future executions jump directly to this compiled function, bypassing the decode loop entirely.
Input: x86 Binary
FA 31 C0 8E D0…Output: WASM Function
WebAssembly.FunctionWebAssembly.TableCompilation Steps (jit.rs → codegen.rs)
codegen_finalize() callback compiles WASM bytes via WebAssembly.compile().jit_clear_func() removes the cached function.WASM ↔ JavaScript Bridge
The WASM module imports JavaScript functions for all device I/O. Every IN/OUT instruction or MMIO access crosses this boundary. The WASM runtime suspends, JavaScript runs, then WASM resumes with the result.
| Function | Dir | Called when |
|---|---|---|
io_port_read8/16/32(addr) | WASM→JS | Guest executes IN AL, dx etc. JS dispatches to device handler. |
io_port_write8/16/32(addr, val) | WASM→JS | Guest executes OUT dx, AL etc. JS dispatches to device handler. |
mmap_read8/32(addr) | WASM→JS | Guest accesses MMIO region (VGA, APIC, Virtio queues). |
mmap_write8/16/32/64/128(addr,…) | WASM→JS | Guest writes to MMIO region (VGA framebuffer, device registers). |
run_hardware_timers(acpi_timer, now) | WASM→JS | Called each main loop iteration to advance PIT, RTC, APIC timers. |
codegen_finalize(idx, start, flags, ptr, len) | WASM→JS | JIT asks JS to compile WASM bytes and install function in table. |
jit_clear_func(idx) | WASM→JS | Self-modifying code: invalidates cached JIT function for an address. |
cpu_exception_hook(n) | WASM→JS | CPU exception fired (e.g. #GP, #PF). JS can log or break. |
microtick() | WASM→JS | Returns performance.now() for timer calibration. |
device_raise_irq(irq) | JS→WASM | JS device signals interrupt to CPU (e.g. PS/2 raises IRQ1 after key press). |
device_lower_irq(irq) | JS→WASM | JS device de-asserts interrupt line. |
cpu.main_loop() | JS→WASM | JS calls WASM to execute instructions for one time slice. |
x86 Registers in WASM Memory
All registers live at fixed byte offsets in WASM linear memory. Both Rust and JavaScript can access them via typed array views.
EFLAGS are computed lazily. Rather than recalculating all 6 condition flags after every ALU operation, v86 stores the operands (last_op1, last_op2, last_result) and recomputes only the flags actually tested by the next branch or PUSHF.