v86 keeps browser APIs (keyboard events, canvas rendering, WebSocket) completely separate from device emulation logic. The BusConnector acts as a message broker — adapters send events in, devices respond and send data out. Neither side knows about the other's implementation.
Bus Architecture
Browser Adapters
Emulator Devices
Message Catalog
Sender: Keyboard adapter on keydown/keyup DOM events.
Receiver: PS/2 controller — queues byte in keyboard FIFO, raises IRQ1.
Data: A single integer — PC scan code set 2 byte. E0-prefix extended keys send two messages (0xE0, then the key code). Key release sends 0xF0 then the code.
bus.send("keyboard-code", 0x1C); // 'A' pressed
bus.send("keyboard-code", 0xF0); // release prefix
bus.send("keyboard-code", 0x1C); // 'A' released
Sender: Mouse adapter on mousemove (with Pointer Lock) or pointermove.
Receiver: PS/2 controller — constructs 3-byte mouse packet (flags, dx, dy), queues it, raises IRQ12.
Data: Array [dx, dy, dz] — relative pixel movement. Clamped to -255..255 per packet.
bus.send("mouse-delta", [dx, dy, dz]);
Sender: Mouse adapter on mousedown/mouseup events.
Receiver: PS/2 — updates button state bits in the next mouse packet's flag byte.
bus.send("mouse-click", [left_btn, middle_btn, right_btn]);
// e.g. left pressed: [1, 0, 0]
Sender: NE2000 or Virtio-net device — called internally when guest transmits a frame.
Receiver: Network adapter — forwards raw Ethernet frame to WebSocket relay server, fetch API, or local loop.
bus.send("network-send", new Uint8Array(ethernet_frame));
Sender: Serial adapter — when user types in the terminal UI.
Receiver: UART device — pushes byte into RX FIFO, raises IRQ if interrupts enabled.
bus.send("serial0-input", char_code); // e.g. 0x0D for Enter
Sender: VGA device — sent every timer tick when dirty scanlines exist.
Receiver: Screen adapter — blits pixel data to canvas using putImageData().
Data: Object with dirty region and pixel buffer reference.
{
dirty_x: 0, dirty_y: 0,
dirty_w: 640, dirty_h: 16, // changed rows
buffer: Uint8Array, // RGBA pixels
image_data: ImageData // pre-built ImageData object
}
Sender: VGA device — when guest changes display mode (text↔graphics, resolution change).
Receiver: Screen adapter — resizes canvas, switches rendering logic.
bus.send("screen-set-mode", is_graphical_mode);
// true = pixel buffer mode, false = text mode
Sender: PIT device — when guest sets/clears port 0x61 bit 0 (speaker gate) and bit 1 (PIT ch2 gate).
Receiver: Speaker adapter — starts or stops the Web Audio oscillator node.
Sender: PIT device — when channel 2 counter is reprogrammed (changes tone frequency).
Receiver: Speaker adapter — updates the oscillator frequency.
bus.send("pcspeaker-update", [pit_mode, frequency_hz]);
Sender: Network adapter — when a packet arrives from the relay server or fetch response.
Receiver: NE2000/Virtio-net device — copies frame into RX ring buffer, raises interrupt.
bus.send("network-receive", new Uint8Array(ethernet_frame));
Sender: UART device — when TX FIFO byte is ready to be transmitted.
Receiver: Serial adapter — appends character to terminal output buffer.
bus.send("serial0-output", char_code); // e.g. 0x41 = 'A'
Fired when cpu.init() completes — all devices initialized, BIOS loaded, CMOS filled. The emulator is ready to start but has not yet begun executing.
Application code (starter.js) listens for this to call emulator.run().
Fired when run() is called — the main loop begins ticking. Guest CPU is now executing instructions.
Fired when stop() is called or the guest issues an ACPI shutdown. Main loop halts; guest memory is preserved.
Fired by the WASM import cpu_event_halt() when the guest executes a HLT instruction. JS suspends ticking until an IRQ wakes the CPU.
Fired during async file loading (BIOS, disk images). Application can display a progress bar. download-error fires if a fetch fails.
bus.send("download-progress", {
file_name: "linux.iso",
loaded: 4194304,
total: 671088640
});
Bus Implementation
Bus.create() returns two linked BusConnector objects. Messages sent on one appear as received on the other — there's no global singleton. This lets the emulator and adapters evolve independently.