device-manager: data-driven driver matching via /etc/devices.csv

Replace the three hardcoded switch tables (pciDriverForIdentity, hidDriverFor,
usbDriverForIdentity) with an authoritative, human-readable device registry the
manager reads at boot. Matching is most-specific-wins across
base/subclass/prog_if/vendor/device/subsystem/hid, so a precise vendor:device
rule and a generic class rule coexist; an unmatched device is logged, never
guessed. This resolves docs' "matching stays code until the third bus".

- ABI: child_added and DeviceDescriptor gain vendor/device/subsystem; child_added
  gains a bus discriminator (BusKind) so PCI and USB class triples match against
  the right namespace.
- pci-bus reads vendor/device (config 0x00) and subsystem (0x2C, type-0) and
  reports them.
- library/device/registry: freestanding CSV parser + matchDriver() with
  specificity scoring; 5 unit tests wired into `zig build test`.
- etc/devices.csv bundled into the initrd; the kernel serves /etc directly, so
  the manager reads it before any filesystem service is up (fat starts later).
- virtio-gpu: drop the now-redundant post-spawn 1AF4:1050 re-confirm, since the
  registry binds this driver by exact identity.
- Remove the orphaned system/drivers/display driver (unreferenced by build or
  registry).
- docs: new devices-csv.md; device-manager.md "matching stays code" resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd
This commit is contained in:
2026-07-26 16:43:13 +01:00
co-authored by Claude Opus 4.8
parent bf0c3fd3e0
commit 203528c8a7
14 changed files with 645 additions and 245 deletions
@@ -91,6 +91,9 @@ mechanism), replacing first-come-first-served `device_claim` with policy. Identi
`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the
xHCI match already uses); USB children carry the (class, subclass, protocol) triple
from usb-ids.zig — each bus's native language, decoded by the shared ids modules.
(Since the registry landed, `child_added` also carries a `bus` discriminator and
the numeric `vendor`/`device`/`subsystem` ids the finer match levels need —
see [/etc/devices.csv](devices-csv.md).)
## Supervision and restart
@@ -169,9 +172,15 @@ published exit events, signals + `process`). On top of those:
- **Manager death**: drivers survive the manager; the restarted manager re-learns
the world (above). Checkpointing driver state with the manager is deferred until
something demonstrates the need.
- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` were
honest at two bus types; the third was expected to trigger the manifest (a driver
declares what it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
(Since then: the third bus — USB — arrived and is matched in code too. Today's
matchers are `pciDriverForIdentity`, `hidDriverFor`, and `usbDriverForIdentity`;
the manifest waits until code matching actually hurts.)
- **Matching is a registry, not code (resolved 2026-07-26).** `driverFor`/
`pciDriverFor` were honest at two bus types; the third (USB) was matched in code
too, and then the switch tables started to hurt — they keyed PCI matches on the
class triple alone, so a virtio-gpu could only be matched as a generic display
function and the driver had to re-confirm its `1AF4:1050` identity from config
space after being spawned. The manifest the earlier note anticipated landed as a
human-readable registry: **[/etc/devices.csv](devices-csv.md)**, parsed by the
pure `device-registry` module and read by the manager at boot. A row binds a
driver to a device by any of base / subclass / prog-IF / vendor / device /
subsystem / `_HID`, most-specific match winning; it is authoritative (no
compiled-in fallback — an unmatched device is logged, never guessed).
`pciDriverForIdentity`, `hidDriverFor`, and `usbDriverForIdentity` are gone.
@@ -0,0 +1,102 @@
# /etc/devices.csv — the device registry
**Status: built (2026-07-26).** The device manager reads `/etc/devices.csv` at
boot and binds every device a bus driver reports to the driver the registry
names. It replaces the three hand-written `switch` tables that used to live in
the manager (`pciDriverForIdentity`, `hidDriverFor`, `usbDriverForIdentity`) —
the "manifest" [device-manager.md](device-manager.md) anticipated once code
matching started to hurt. The parser and matcher are the pure, unit-tested
`device-registry` module (`library/device/registry/device-registry.zig`).
## Why a registry
The switch tables keyed PCI matches on the 24-bit class/subclass/prog-IF triple
alone. That is too coarse: a virtio-gpu is just "display / other" by class, so it
could only be *class-matched* and the driver had to re-confirm its real
`1AF4:1050` identity from config space **after** the manager had already spawned
it. The registry lets a rule bind on the full identity — down to vendor, device,
and subsystem — so the manager makes the precise decision itself, and the driver
comes up already knowing it is the right one.
It is also **data, not code**: teaching the system new hardware is a line in a
file, not an edit-and-recompile of the manager. And it is **greppable** — one
place to read "what binds what," the same idea as Linux's `modules.alias`.
## The file
One rule per line, nine comma-separated fields; `#` starts a comment (whole-line
or trailing); blank lines are ignored. Whitespace around a field is trimmed, so
columns may be padded for readability.
```
# bus base class prog_if vendor device subsystem hid driver
pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus
pci, 03, 00, 00, *, *, *, *, /system/drivers/display
pci, 03, 80, *, 1AF4, 1050, *, *, /system/drivers/virtio-gpu
usb, 03, 01, 01, *, *, *, *, /system/drivers/usb-hid-keyboard
acpi, *, *, *, *, *, *, PNP0303, /system/drivers/ps2-bus
```
| Field | Meaning | Notes |
|---|---|---|
| `bus` | `pci` \| `usb` \| `acpi` | which bus reported the device; picks the namespace for the id columns |
| `base` | PCI base class / USB class | hex |
| `class` | PCI subclass / USB subclass | hex |
| `prog_if` | PCI prog-IF / USB protocol | hex |
| `vendor` | PCI vendor / USB idVendor | hex |
| `device` | PCI device / USB idProduct | hex |
| `subsystem` | PCI subsystem, `(ssvid<<16)\|ssid` | hex; blank for usb/acpi |
| `hid` | ACPI `_HID` (e.g. `PNP0303`) | blank for pci/usb |
| `driver` | full ramdisk path to spawn | e.g. `/system/drivers/virtio-gpu` |
`*` or an empty field is a **wildcard** — it matches anything and adds nothing to
a rule's specificity.
## Levels of detection: most-specific-wins
Several rows may match one device. The manager picks the **most specific** — the
one that pins the finest-grained fields. Specificity weights double from the
coarsest level so each outweighs all coarser levels combined:
```
base(1) < class(2) < prog_if(4) < vendor(8) < subsystem(16) < device(32) ≈ hid(32)
```
So the generic `pci, 03, 00, 00, …/display` rule and the precise
`pci, 03, 80, *, 1AF4, 1050, …/virtio-gpu` rule coexist: the virtio card
(vendor 1AF4, device 1050) takes the specific rule; a plain VGA adapter still
falls to the generic one. Two rules that match a device with the *same*
specificity are a registry authoring error — the manager logs it loudly and binds
the first, so the shadowed rule is visible rather than silently dropped.
## Authoritative — no code fallback
There is no compiled-in default table behind the registry. A device that no row
matches goes **unbound** and is logged; the manager never guesses. A missing or
empty `/etc/devices.csv` therefore means nothing matches — which is loud at boot,
not a silent half-working system.
## How the manager reads it
`/etc/devices.csv` is bundled into the initial ramdisk (`build.zig`'s `bundled`
list). The kernel serves the initrd's `/etc` tree directly — the `fat` service is
spawned *after* the device manager and is irrelevant to `/etc` — so the manager
reads the file with a plain `fs.open("/etc/devices.csv")` + `read`, with no
filesystem service running and no boot-ordering dependency. It parses the bytes
once in `initialise`, before any bus driver can report a device to match.
## Feeding the matcher: the widened report
Finer-grained matching needs identity the old ABI threw away. Two things carry it
now: `child_added` (and `DeviceDescriptor`) grew `vendor` / `device` /
`subsystem` fields, filled by the PCI bus driver from config space (offsets
0x00 and 0x2C); and each bus driver states its `bus` in the report (a `BusKind`),
so the manager reads a PCI class triple and a USB class triple — the same 24 bits
in different namespaces — against the right `bus` column.
## Adding a driver
1. Build the driver binary and bundle it at `/system/drivers/<name>` (build.zig).
2. Add a row to `etc/devices.csv` naming the identity it binds and its full path.
No device-manager change is required — the registry is the seam.