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
@@ -23,86 +23,66 @@ const service = @import("service");
const time = @import("time");
const memory = @import("memory");
const logging = @import("logging");
const acpi_ids = @import("acpi-ids");
const pci_class = @import("pci-class");
const usb_ids = @import("usb-ids");
const device_manager_protocol = @import("device-manager-protocol");
const registry = @import("device-registry");
const fs = @import("file-system");
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller —
/// Serial Bus Controller / USB Controller / XHCI — named from pci-class.zig rather
/// than written as the bare 0x0C0330 (docs/coding-standards.md, "Named values").
const xhci_pci_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.serial_bus),
.subclass = @intFromEnum(pci_class.serial_bus.SubClass.usb),
.prog_if = @intFromEnum(pci_class.serial_bus.usb.ProgIf.xhci),
});
// --- the device registry ------------------------------------------------------
// Driver matching is data-driven and authoritative: /etc/devices.csv (parsed by
// the device-registry module) names, per bus, which driver binds a reported
// device, the most-specific match winning. There is no compiled-in fallback — a
// device no row matches goes unbound and is logged. This retired the hand-kept
// pciDriverForIdentity / hidDriverFor / usbDriverForIdentity switch tables
// (docs/device-manager.md: "matching stays code until the third bus").
/// The PCI class triple of a virtio-gpu — Display Controller / Other (0x80) / 0. The class
/// alone cannot tell it from any other display/other function, so the driver re-confirms
/// vendor 0x1AF4 / device 0x1050 from config space once spawned; this only gets it spawned.
const virtio_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.display),
.subclass = 0x80, // "Other" — no named SubClass member (PCI convention)
.prog_if = 0,
});
/// The CSV bytes, held for the life of the process because the parsed rules'
/// string fields (hid, driver) slice into this buffer.
var registry_source: [8192]u8 = undefined;
var registry_rules: [64]registry.Rule = undefined;
var registry_count: usize = 0;
const vga_compatible_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.display),
.subclass = @intFromEnum(pci_class.display.SubClass.vga_compatible),
.prog_if = 0,
});
/// The driver that serves a *reported* PCI function (M19.3: matching moved
/// from the boot snapshot to the bus reports), or null. A machine can carry
/// several identical controllers — one driver instance per reported device,
/// its registered id as argv[1].
fn pciDriverForIdentity(identity: u64) ?[]const u8 {
return switch (identity) {
xhci_pci_class => "/system/drivers/usb-xhci-bus",
vga_compatible_gpu_pci_class => "/system/drivers/display",
virtio_gpu_pci_class => "/system/drivers/virtio-gpu",
else => null,
/// Read and parse /etc/devices.csv once at boot. The file lives in the initial
/// ramdisk, which the kernel serves directly — no filesystem service need be up
/// (fat is spawned after the manager), so this is a plain fs.open + read.
fn loadRegistry() void {
var file = fs.open("/etc/devices.csv", .{}) orelse {
_ = logging.write("/system/services/device-manager: /etc/devices.csv missing — nothing will match\n");
return;
};
defer file.close();
var used: usize = 0;
while (used < registry_source.len) {
const n = file.read(registry_source[used..]) orelse break;
if (n == 0) break;
used += n;
}
const result = registry.parse(registry_source[0..used], &registry_rules);
registry_count = result.count;
if (result.malformed != 0) std.log.info("/etc/devices.csv: {d} malformed line(s) skipped", .{result.malformed});
if (result.truncated) _ = logging.write("/system/services/device-manager: /etc/devices.csv has more rules than the table holds\n");
std.log.info("/etc/devices.csv: {d} rule(s) loaded", .{registry_count});
}
/// The driver that serves a *reported* ACPI device by its `_HID` (M20.3:
/// ps2-bus now binds the PS/2 nodes the acpi service reports, not boot-snapshot
/// nodes the kernel used to build). ps2-bus is a singleton that finds both its
/// devices by hid once spawned, so keyboard and mouse map to the same name.
fn hidDriverFor(hid: []const u8) ?[]const u8 {
if (std.mem.eql(u8, hid, "PNP0303")) return "/system/drivers/ps2-bus"; // PS/2 keyboard
if (std.mem.eql(u8, hid, "PNP0F13")) return "/system/drivers/ps2-bus"; // PS/2 mouse
return null;
}
/// The driver that serves a *reported* USB interface by its (class, subclass,
/// protocol) triple — the third bus after PCI and ACPI (docs/device-manager.md:
/// matching stays code until the third bus). The xHCI bus driver reports each
/// interface with this packed triple as its identity; the matched class driver is
/// spawned with the interface's registered id as argv[1], which it presents to the
/// bus driver to open the device.
fn usbDriverForIdentity(identity: u64) ?[]const u8 {
const keyboard = comptime usb_ids.packTriple(
@intFromEnum(usb_ids.Class.hid),
@intFromEnum(usb_ids.hid.SubClass.boot),
@intFromEnum(usb_ids.hid.Protocol.keyboard),
);
const mouse = comptime usb_ids.packTriple(
@intFromEnum(usb_ids.Class.hid),
@intFromEnum(usb_ids.hid.SubClass.boot),
@intFromEnum(usb_ids.hid.Protocol.mouse),
);
const storage = comptime usb_ids.packTriple(
@intFromEnum(usb_ids.Class.mass_storage),
@intFromEnum(usb_ids.mass_storage.SubClass.scsi),
@intFromEnum(usb_ids.mass_storage.Protocol.bulk_only),
);
return switch (identity) {
keyboard => "/system/drivers/usb-hid-keyboard",
mouse => "/system/drivers/usb-hid-mouse",
storage => "/system/drivers/usb-storage",
else => null,
/// Build a registry Identity from a bus driver's report: the bus it named, the
/// class triple unpacked from `identity` (0xCCSSPP — the same packing for a PCI
/// class code and a USB class triple), the widened numeric ids, and the ACPI hid.
fn identityFromReport(report: device_manager_protocol.ChildAdded) registry.Identity {
const bus: registry.Bus = switch (report.bus) {
@intFromEnum(device_manager_protocol.BusKind.pci) => .pci,
@intFromEnum(device_manager_protocol.BusKind.usb) => .usb,
@intFromEnum(device_manager_protocol.BusKind.acpi) => .acpi,
else => .unknown,
};
const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len;
return .{
.bus = bus,
.base = @truncate(report.identity >> 16),
.subclass = @truncate(report.identity >> 8),
.prog_if = @truncate(report.identity),
.vendor = report.vendor,
.device = report.device,
.subsystem = report.subsystem,
.hid = report.hid[0..hid_len],
};
}
@@ -365,6 +345,10 @@ fn sweepDeadlines() void {
fn initialise(endpoint: ipc.Handle) bool {
manager_endpoint = endpoint;
// Load the authoritative driver-match registry before any bus driver can
// report a device to match against it.
loadRegistry();
// Enumerate into a heap buffer (too big for the one-page user stack).
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
_ = logging.write("/system/services/device-manager: out of memory\n");
@@ -459,24 +443,22 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
if (!addChild(report.parent, report.bus_address, report.identity, report.device_id, sender)) status = -1;
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
if (status == 0) publishEvent(message[0..device_manager_protocol.child_added_size]);
// Matching from reports (M19.3): a registered child whose identity
// names a driver gets one, once — re-reports after a bus restart
// dedupe on the registered id, exactly like the registrations do.
// Matching from reports (M19.3), now data-driven via the /etc/devices.csv
// registry: a registered child gets the most-specific driver its identity
// matches, once — re-reports after a bus restart dedupe on the registered
// id, exactly like the registrations do.
if (status == 0 and report.device_id != device_manager_protocol.no_device) {
if (pciDriverForIdentity(report.identity)) |child_driver| {
if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true);
}
// USB interface match: the reported identity is the packed class triple,
// and the class driver is spawned with the interface's registered id.
if (usbDriverForIdentity(report.identity)) |usb_driver| {
if (!driverForDevice(report.device_id)) addDriver(usb_driver, report.device_id, true);
}
// ACPI _HID match (M20.3): ps2-bus is a singleton that finds its own
// devices by hid, so spawn it once, without a device assignment.
const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len;
if (hid_len != 0) {
if (hidDriverFor(report.hid[0..hid_len])) |hid_driver| {
if (!alreadySupervised(hid_driver)) addDriver(hid_driver, device_manager_protocol.no_device, false);
const id = identityFromReport(report);
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
if (match.ambiguous)
std.log.info("/etc/devices.csv: multiple equally-specific rules match the device {s} reported; binding {s}", .{ driver.name(), match.driver });
if (id.bus == .acpi) {
// An hid-matched driver (ps2-bus) is a singleton that finds its
// own devices once spawned — spawn it once, no device assignment.
if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false);
} else {
// A per-device driver: one instance, the registered id as argv[1].
if (!driverForDevice(report.device_id)) addDriver(match.driver, report.device_id, true);
}
}
}