Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/planning/xhci-head-to-head.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# xHCI Head-to-Head: Linux Module vs Breenix Driver

## Status Summary

| | Linux Module | Breenix Driver |
|---|---|---|
| **Location** | `linux_xhci_module/breenix_xhci_probe.c` | `kernel/src/drivers/usb/xhci.rs` |
| **Platform** | linux-probe VM (Alpine ARM64) | breenix-dev VM (Breenix ARM64) |
| **M1-M10** | All PASS | All PASS |
| **M11 (Event Delivery)** | PASS — HID reports received via MSI | FAIL — CC=12, all endpoints Halted |
| **Proven 100%?** | Yes, with and without xhci_hcd priming | No |

## Linux Module: Proven Working

The Linux module has been validated in two configurations:

1. **With xhci_hcd priming** — stock Linux xhci_hcd loads first, our module replaces it
2. **Without xhci_hcd priming** — stock xhci_hcd unbound before our module loads

Both produce identical results: all 11 milestones pass, HID interrupt events arrive via MSI handler, keyboard/mouse reports are printed to dmesg.

## Instrumentation Parity Gaps

The Linux module dumps full device contexts (192-256 byte hex dumps) at every configuration step. The Breenix driver only logs completion codes. **This is the core diagnostic gap.**

### Critical Missing Dumps in Breenix

| Milestone | Linux Dumps | Breenix Dumps | Gap |
|---|---|---|---|
| M2 (Reset) | USBSTS + all registers (`ms_regs`) | USBSTS only | Missing register snapshot |
| M3 (Data Structures) | Ring/ERST + all registers | Ring/ERST only | Missing register snapshot |
| M4 (Running) | USBCMD/STS/IMAN + all registers | USBCMD/STS/IMAN only | Missing register snapshot |
| **M7 (Address Device)** | **Input ctx (192B) + cmd TRB + evt TRB + output ctx (192B)** | **CC only** | **MAJOR — no context visibility** |
| **M8 (Endpoint Config)** | **Input ctx (256B) + output ctx (256B) + BW dance contexts** | **CC only** | **MAJOR — no endpoint context visibility** |
| **M9 (HID Setup)** | **Output ctx after SET_CONFIG + after HID setup** | **CC only** | **MAJOR — no post-config state** |
| M10 (Interrupt Transfer) | TRB + EP state + registers | TRB + pre/post EP state + registers | None (Breenix more detailed) |
| M11 (Event Delivery) | Register snapshot + EP contexts + DCBAA | Same + pending event check | None |

### What This Means

We cannot currently do a byte-for-byte comparison of device context contents between the two platforms. The Linux module shows exactly what input context was sent and what output context the controller returned. Breenix only shows "CC=1" (success) — we can't see if the actual context data differs.

## What We Know Works Identically

- Controller discovery (BAR, capabilities, version)
- Controller reset (HCRST completes, CNR clears)
- Data structure setup (DCBAA, command ring, event ring, ERST)
- Controller start (RS=1, INTE=1, IMAN.IE=1)
- Port detection (CCS=1, PED=1, speed correct)
- Slot enablement (EnableSlot CC=1)
- Device addressing (AddressDevice CC=1)
- Endpoint configuration (ConfigureEndpoint CC=1, BW dance CC=1)
- HID class setup (SET_CONFIGURATION, SET_IDLE, SET_PROTOCOL all succeed)
- Interrupt TRB queueing (Normal TRBs enqueued, doorbells rung)

## What Fails

- **M11 only**: First interrupt transfer event returns CC=12 (Endpoint Not Enabled)
- All 4 interrupt endpoints transition immediately from Running to Halted
- Continuous polling confirms CC=12 never clears

## Eliminated Hypotheses (26 total)

1-18: Prior session hypotheses (xHCI logic, DMA, cache, register ordering, etc.)
19. Set TR Dequeue Pointer explicit command
20. USB device state (SET_CONFIGURATION(0) before ConfigureEndpoint)
21. xhci_hcd priming (Linux module works without it)
22. PCI configuration differences (identical Command register)
23. MSI configuration/ordering
24. Timing (10s, 60s delays)
25. phymemrange_enable alone (fires but no ep create)
26. EHCI companion init (CONFIGFLAG=1, RS=1, HCRST — no effect)

## Next Steps: Close the Instrumentation Gap

### Priority 1: Add Context Dumps to Breenix

Add `ms_dump` equivalent for device contexts at M7, M8, M9:

- **M7**: Dump input context before AddressDevice, output context after
- **M8**: Dump input context before ConfigureEndpoint, output context after, plus BW dance contexts
- **M9**: Dump output context after SET_CONFIGURATION and after HID setup

### Priority 2: Extract Matching Data from Linux Module

Run the Linux module on linux-probe and capture the full dmesg output with all context dumps. This becomes the **reference dataset**.

### Priority 3: Byte-for-Byte Comparison

Compare every dumped context field between Linux and Breenix:
- Slot Context: Route String, Speed, Context Entries, Max Exit Latency
- Endpoint 0 Context: EP Type, Max Packet Size, TR Dequeue, Interval, etc.
- Interrupt EP Contexts: EP Type, Max Packet Size, Interval, Mult, MaxPStreams, etc.

Any difference is a candidate root cause for CC=12.

### Priority 4: Stop Using serial_println for Debug

All experimental debug output has been added via `serial_println!`. This violates the project's tracing policy. The Parallels workaround code should use the lock-free tracing subsystem instead.

## Parallels Host Log Observations

The Parallels host log shows the hypervisor has an internal "ep create" mechanism. On linux-probe, `ep create` events fire ~330ms after xHCI init. On breenix-dev, they never fire. This is a symptom, not a root cause — the hypervisor creates endpoints when the xHCI controller state is correct, and doesn't when it's not.

Rather than reverse-engineering Parallels' internal signaling, the right approach is to make Breenix's xHCI state **byte-identical** to Linux's. The context dumps will show us where they differ.
66 changes: 57 additions & 9 deletions kernel/src/arch_impl/aarch64/boot.S
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,19 @@
.equ BLOCK_FLAGS_DEVICE, (DESC_BLOCK | DESC_AF | DESC_SH_NONE | DESC_AP_KERNEL | DESC_ATTR_DEVICE | DESC_PXN | DESC_UXN)
.equ BLOCK_FLAGS_NORMAL, (DESC_BLOCK | DESC_AF | DESC_SH_INNER | DESC_AP_KERNEL | DESC_ATTR_NORMAL)

// Non-Cacheable block flags for DMA buffers (MAIR index 2 = 0x44)
.equ DESC_ATTR_NC, (2 << 2)
.equ BLOCK_FLAGS_NC, (DESC_BLOCK | DESC_AF | DESC_SH_INNER | DESC_AP_KERNEL | DESC_ATTR_NC)

// DMA NC region: 2MB block at physical 0x5000_0000
// L2 index = (0x50000000 - 0x40000000) / 0x200000 = 128
.equ NC_L2_INDEX, 128

// MAIR attributes
.equ MAIR_ATTR_DEVICE, 0x00
.equ MAIR_ATTR_NORMAL, 0xFF
.equ MAIR_EL1_VALUE, (MAIR_ATTR_DEVICE | (MAIR_ATTR_NORMAL << 8))
.equ MAIR_ATTR_NC, 0x44
.equ MAIR_EL1_VALUE, (MAIR_ATTR_DEVICE | (MAIR_ATTR_NORMAL << 8) | (MAIR_ATTR_NC << 16))

// TCR configuration (4KB granule, 48-bit VA, inner-shareable, WBWA)
.equ TCR_T0SZ, 16
Expand Down Expand Up @@ -262,10 +271,14 @@ setup_mmu:
bl zero_table
ldr x0, =ttbr0_l1
bl zero_table
ldr x0, =ttbr0_l2_ram
bl zero_table
ldr x0, =ttbr1_l0
bl zero_table
ldr x0, =ttbr1_l1
bl zero_table
ldr x0, =ttbr1_l2_ram
bl zero_table

// TTBR0 L0[0] -> L1
ldr x0, =ttbr0_l0
Expand All @@ -280,12 +293,16 @@ setup_mmu:
orr x1, x1, x2
str x1, [x0, #0]

// TTBR0 L1[1] = normal (0x4000_0000 - 0x7FFF_FFFF)
ldr x1, =0x40000000
ldr x2, =BLOCK_FLAGS_NORMAL
orr x1, x1, x2
// TTBR0 L1[1] -> L2 table (0x4000_0000 - 0x7FFF_FFFF)
// Uses L2 to carve out a 2MB NC block for DMA at 0x5000_0000
ldr x1, =ttbr0_l2_ram
orr x1, x1, #DESC_TABLE
str x1, [x0, #8]

// Fill TTBR0 L2: 512 x 2MB blocks, index NC_L2_INDEX = NC, rest = Normal
ldr x0, =ttbr0_l2_ram
bl fill_l2_ram

// TTBR1 L0[0] -> L1
ldr x0, =ttbr1_l0
ldr x1, =ttbr1_l1
Expand All @@ -299,13 +316,18 @@ setup_mmu:
orr x1, x1, x2
str x1, [x0, #0]

// TTBR1 L1[1] = normal (high-half direct map)
ldr x1, =0x40000000
ldr x2, =BLOCK_FLAGS_NORMAL
orr x1, x1, x2
// TTBR1 L1[1] -> L2 table (high-half direct map 0x4000_0000 - 0x7FFF_FFFF)
// Same NC carveout for .dma section at 0xFFFF_0000_5000_0000
ldr x1, =ttbr1_l2_ram
orr x1, x1, #DESC_TABLE
str x1, [x0, #8]

// Fill TTBR1 L2: same layout as TTBR0
ldr x0, =ttbr1_l2_ram
bl fill_l2_ram

// TTBR1 L1[2] = normal (high-half direct map)
ldr x0, =ttbr1_l1
ldr x1, =0x80000000
ldr x2, =BLOCK_FLAGS_NORMAL
orr x1, x1, x2
Expand Down Expand Up @@ -355,6 +377,26 @@ zero_table_loop:
b.ne zero_table_loop
ret

// Fill L2 table at x0 with 512 x 2MB block entries for 0x4000_0000 - 0x7FFF_FFFF.
// Entry NC_L2_INDEX (index 128 = physical 0x5000_0000) gets BLOCK_FLAGS_NC.
// All other entries get BLOCK_FLAGS_NORMAL.
fill_l2_ram:
mov x1, #0 // i = 0
ldr x3, =0x40000000 // base physical address
ldr x4, =BLOCK_FLAGS_NORMAL
ldr x5, =BLOCK_FLAGS_NC
fill_l2_loop:
lsl x6, x1, #21 // offset = i * 2MB (0x200000)
add x6, x6, x3 // phys = 0x40000000 + offset
cmp x1, #NC_L2_INDEX
csel x7, x5, x4, eq // flags = NC if i==128, else Normal
orr x6, x6, x7 // entry = phys | flags
str x6, [x0, x1, lsl #3] // L2[i] = entry
add x1, x1, #1
cmp x1, #512
b.lt fill_l2_loop
ret

.section .text

/*
Expand Down Expand Up @@ -844,12 +886,18 @@ ttbr0_l0:
.global ttbr0_l1
ttbr0_l1:
.skip 4096
.global ttbr0_l2_ram
ttbr0_l2_ram:
.skip 4096
.global ttbr1_l0
ttbr1_l0:
.skip 4096
.global ttbr1_l1
ttbr1_l1:
.skip 4096
.global ttbr1_l2_ram
ttbr1_l2_ram:
.skip 4096

.balign 64
.global CPU_RELEASE_ADDR
Expand Down
11 changes: 11 additions & 0 deletions kernel/src/arch_impl/aarch64/linker.ld
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ SECTIONS
__stack_top = .;
}

/* DMA buffers - placed in Non-Cacheable region at physical 0x50000000.
* The loader maps this 2MB block with MAIR index 2 (NC) so the xHC
* controller and CPU see consistent data without cache maintenance.
* Zeroed at runtime by kernel init, not by loader (NOLOAD section). */
. = KERNEL_VIRT_BASE + 0x50000000;
.dma (NOLOAD) : AT(0x50000000) {
__dma_start = .;
*(.dma .dma.*)
__dma_end = .;
}

/DISCARD/ : {
*(.comment)
*(.note*)
Expand Down
3 changes: 2 additions & 1 deletion kernel/src/arch_impl/aarch64/mmu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ static mut L2_TABLE_RAM: PageTable = PageTable::new();

const MAIR_ATTR_DEVICE: u64 = 0x00;
const MAIR_ATTR_NORMAL: u64 = 0xFF;
const MAIR_EL1_VALUE: u64 = MAIR_ATTR_DEVICE | (MAIR_ATTR_NORMAL << 8);
const MAIR_ATTR_NC: u64 = 0x44;
const MAIR_EL1_VALUE: u64 = MAIR_ATTR_DEVICE | (MAIR_ATTR_NORMAL << 8) | (MAIR_ATTR_NC << 16);

const TCR_T0SZ: u64 = 16;
const TCR_T1SZ: u64 = 16 << 16;
Expand Down
42 changes: 5 additions & 37 deletions kernel/src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,50 +92,18 @@ pub fn init() -> usize {
let device_count = pci::enumerate();
serial_println!("[drivers] Found {} PCI devices", device_count);

// Log all PCI devices for debugging
if let Some(devices) = pci::get_devices() {
for dev in &devices {
serial_println!(
"[drivers] PCI {:02x}:{:02x}.{} [{:04x}:{:04x}] class={:?}/0x{:02x}",
dev.bus, dev.device, dev.function,
dev.vendor_id, dev.device_id,
dev.class, dev.subclass,
);
}
}

// Enumerate VirtIO PCI devices with modern transport
let virtio_devices = virtio::pci_transport::enumerate_virtio_pci_devices();
for dev in &virtio_devices {
serial_println!(
"[drivers] VirtIO PCI device: {} (type={})",
virtio::pci_transport::device_type_name(dev.device_id()),
dev.device_id()
);
}
serial_println!("[drivers] Found {} VirtIO PCI devices", virtio_devices.len());

// Initialize VirtIO GPU PCI driver.
// Even when a GOP framebuffer is available (Parallels), we try the VirtIO
// GPU PCI driver first — it supports arbitrary resolutions via
// CREATE_RESOURCE_2D, giving us control beyond the fixed GOP mode.
// If GPU PCI init fails, the GOP framebuffer is used as a fallback.
match virtio::gpu_pci::init() {
Ok(()) => {
serial_println!("[drivers] VirtIO GPU (PCI) initialized");
}
Err(e) => {
serial_println!("[drivers] VirtIO GPU (PCI) init failed: {}", e);
}
Ok(()) => serial_println!("[drivers] VirtIO GPU (PCI) initialized"),
Err(e) => serial_println!("[drivers] VirtIO GPU (PCI) init failed: {}", e),
}

// EHCI USB 2.0 host controller SKIPPED — investigating whether EHCI
// init causes a second xHC reset in the Parallels hypervisor, which
// would destroy interrupt endpoint configurations and cause CC=12.
// Intel 82801FB EHCI: vendor 0x8086, device 0x265c
if pci::find_device(0x8086, 0x265c).is_some() {
serial_println!("[drivers] EHCI USB 2.0 controller found but SKIPPED (CC=12 investigation)");
}
// EHCI USB 2.0 controller — initialization is handled inside xhci::init()
// as a prerequisite for Parallels USB device routing (companion controller model).
// Intel 82801FB EHCI: vendor 0x8086, device 0x265c at PCI 00:02.0

// Initialize XHCI USB host controller (keyboard + mouse)
// NEC uPD720200: vendor 0x1033, device 0x0194
Expand Down
Loading
Loading