pci: full driver-side library (MSI/MSI-X, power, FLR, extended caps); xhci goes interrupt-driven

library/device/pci is now the complete generic floor a leaf PCI driver
needs, instead of just what virtio-gpu used:

- pci-class: capability IDs, MSI/MSI-X/power-management/PCI-Express
  register layouts, extended-capability header decode (host-tested),
  per-bit command constants, remaining header offsets.
- pci.Function: header accessors, disableBusMaster + interrupt-disable
  helpers, findCapability, programMsi/disableMsi, MsiX vector-table
  struct, ensurePowerStateD0, functionLevelReset (BAR save/restore),
  extended-capability iterator.

Proven by the new pci-caps QEMU case: a pci-cap-test fixture claims an
extra e1000e (PM+MSI+PCIe+MSI-X, no danos driver) and readback-verifies
every surface, including the first driver-side use of msi_bind.

usb-xhci-bus converts from 8 ms event-ring polling to message-signalled
interrupts: plain MSI where offered (real Intel xHC), MSI-X entry 0
otherwise (qemu-xhci has no MSI capability), byte-identical polling as
fallback. The timer survives as a 250 ms port-reconcile/lost-edge tick —
real-hardware USB2 hub debounce still needs it. MSI setup runs BEFORE
controller bring-up: QEMU's xhci only registers the MSI-X vector as used
when IMAN.IE is written while MSI-X is already enabled; interrupts are
silently dropped otherwise (real hardware does not care about the order).

101/101 QEMU cases green; real-hardware smoke passed (mouse works,
boot 2026-07-23T174805Z, plain-MSI branch, vector 33).
This commit is contained in:
Daniel Samson
2026-07-26 18:31:27 +01:00
parent e376c9e908
commit 9e649178bf
8 changed files with 781 additions and 22 deletions
@@ -1563,6 +1563,15 @@ pub const Controller = struct {
return report;
}
/// Clear interrupter 0's pending bit (IMAN.IP). IP is write-1-to-clear, and the
/// read-back carries IE (plain read-write) through unchanged. In MSI mode the
/// driver clears IP **before** draining the ring: an event that lands after the
/// drain then takes IP 0→1 and fires a fresh edge, where clearing afterwards would
/// leave a race in which a new event finds IP already set and raises nothing.
pub fn acknowledgeInterrupt(self: *const Controller) void {
write32(self.interrupter(interrupter_management), read32(self.interrupter(interrupter_management)) | 1);
}
/// Drain any events currently on the event ring: interrupt reports into the
/// report queue, PORT STATUS CHANGES into the port-change queue (hot-plug —
/// these were silently dropped before M20). Non-blocking — called on the