CPU & JIT Engine

How v86 executes x86 instructions — from the main loop in JavaScript down to JIT-compiled WebAssembly functions.

Execution Pipeline

Click any stage to see details.

Timer Tick
JS schedules next run
🔁
WASM Main Loop
cpu.main_loop()
JIT Cache?
Is block compiled?
💨
Execute
JIT or interpreter
🔌
I/O Access?
port in/out or MMIO
🔔
Interrupts
PIC → IDT dispatch

Main Loop Walkthrough

next_tick(delay)
JS schedules the next tick via setTimeout / requestAnimationFrame.
do_tick() called
JS resumes from the browser event loop and calls into WASM.
cpu.main_loop() [WASM]
Runs for a fixed number of cycles or until a timer fires. Tight inner loop in Rust-compiled WASM.
Instruction fetch
Reads EIP → translates virtual address → fetches bytes from WASM linear memory (guest RAM).
JIT hit?
Looks up EIP (physical) in the JIT hash table. If a compiled function exists, jump-calls it directly.
Execute (JIT or interpret)
JIT function runs natively; interpreter decodes prefix, opcode, ModRM, SIB, displacement, immediate bytes.
I/O side-effect?
If IN/OUT or MMIO access: WASM calls JS import (e.g. io_port_write8) → device handler runs → returns.
Hardware timers
run_hardware_timers() updates PIT, RTC, APIC timer — may raise IRQs.
Interrupt check
If IRQ pending and IF flag set: fetch IDT entry, push CS:EIP, load handler vector, dispatch.
Loop or yield
If time budget spent: return to JS, schedule next tick. Otherwise loop back to step 3.

JIT Compilation Pipeline

How it works

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

• EIP = 0x7C00 (e.g. boot sector)
• Raw bytes: FA 31 C0 8E D0…
• ModRM, SIB, displacement
• REP/LOCK prefixes
• 16/32-bit mixed mode

Output: WASM Function

WebAssembly.Function
• Stored in WebAssembly.Table
• Direct register ↔ WASM local mapping
• Inlined flag computation
• No decode overhead

Compilation Steps (jit.rs → codegen.rs)

1. ENTRY DETECTION
Tracks basic block entry points. After N interpreter runs, marks block for JIT.
2. CFG ANALYSIS
control_flow.rs maps branches, calls, returns to identify block boundaries.
3. CODE GENERATION
jit_instructions.rs emits WASM bytecode for each x86 instruction.
4. FINALIZATION
JS codegen_finalize() callback compiles WASM bytes via WebAssembly.compile().
5. TABLE INSTALL
Function stored in WASM indirect table. EIP→table-index mapping cached.
6. INVALIDATION
If guest writes to a JIT-compiled page, jit_clear_func() removes the cached function.

WASM ↔ JavaScript Bridge

Import/Export boundary

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.

FunctionDirCalled when
io_port_read8/16/32(addr)WASM→JSGuest executes IN AL, dx etc. JS dispatches to device handler.
io_port_write8/16/32(addr, val)WASM→JSGuest executes OUT dx, AL etc. JS dispatches to device handler.
mmap_read8/32(addr)WASM→JSGuest accesses MMIO region (VGA, APIC, Virtio queues).
mmap_write8/16/32/64/128(addr,…)WASM→JSGuest writes to MMIO region (VGA framebuffer, device registers).
run_hardware_timers(acpi_timer, now)WASM→JSCalled each main loop iteration to advance PIT, RTC, APIC timers.
codegen_finalize(idx, start, flags, ptr, len)WASM→JSJIT asks JS to compile WASM bytes and install function in table.
jit_clear_func(idx)WASM→JSSelf-modifying code: invalidates cached JIT function for an address.
cpu_exception_hook(n)WASM→JSCPU exception fired (e.g. #GP, #PF). JS can log or break.
microtick()WASM→JSReturns performance.now() for timer calibration.
device_raise_irq(irq)JS→WASMJS device signals interrupt to CPU (e.g. PS/2 raises IRQ1 after key press).
device_lower_irq(irq)JS→WASMJS device de-asserts interrupt line.
cpu.main_loop()JS→WASMJS 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.

EAX
AX / AH / AL
mem[64] — accumulator
ECX
CX / CH / CL
mem[68] — counter
EDX
DX / DH / DL
mem[72] — data/I/O
EBX
BX / BH / BL
mem[76] — base
ESP
SP
mem[80] — stack ptr
EBP
BP
mem[84] — base ptr
ESI
SI
mem[88] — source idx
EDI
DI
mem[92] — dest idx
EIP
instruction ptr
mem[556]
EFLAGS
CF ZF SF OF PF AF
mem[120] — lazy eval
CS/DS/SS
segment selectors
mem[668..] — sreg[8]
CR0/CR4
control registers
protected mode, paging
XMM0–7
128-bit SSE regs
mem[832..] — 8 × 16B
ST(0)–ST(7)
FPU 80-bit stack
mem[1152..] — 8 × 10B
Lazy flags

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.