From 203528c8a7fe39320ba4910db8b125894c7d8ee6 Mon Sep 17 00:00:00 2001 From: Daniel Samson Date: Sun, 26 Jul 2026 16:43:13 +0100 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd --- build.zig | 21 +- .../device-manager.md | 21 +- docs/device-driver-development/devices-csv.md | 102 +++++ etc/devices.csv | 33 ++ library/device/model/device-abi.zig | 9 + library/device/registry/device-registry.zig | 348 ++++++++++++++++++ .../device-manager-protocol.zig | 29 +- system/drivers/display/display.zig | 59 --- .../intel-integrated/intel-integrated.zig | 69 ---- system/drivers/pci-bus/pci-bus.zig | 15 + system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 1 + system/drivers/virtio-gpu/virtio-gpu.zig | 19 +- system/services/acpi/acpi.zig | 2 +- .../device-manager/device-manager.zig | 162 ++++---- 14 files changed, 645 insertions(+), 245 deletions(-) create mode 100644 docs/device-driver-development/devices-csv.md create mode 100644 etc/devices.csv create mode 100644 library/device/registry/device-registry.zig delete mode 100644 system/drivers/display/display.zig delete mode 100644 system/drivers/display/intel-integrated/intel-integrated.zig diff --git a/build.zig b/build.zig index a6671ef..37afc8c 100644 --- a/build.zig +++ b/build.zig @@ -284,6 +284,13 @@ pub fn build(b: *std.Build) void { const pci_class_module = b.addModule("pci-class", .{ .root_source_file = b.path("library/device/pci/pci-class.zig"), }); + // The device registry: parse /etc/devices.csv into match rules and bind a + // reported device to a driver — the data-driven, authoritative replacement for + // the manager's hand-written switch tables. Pure logic (no hardware, no + // syscalls), so it unit-tests with plain `zig test`; the manager imports it. + const device_registry_module = b.addModule("device-registry", .{ + .root_source_file = b.path("library/device/registry/device-registry.zig"), + }); // ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device // nodes. Also shared reference data. // The AML interpreter, a build module so the ring-3 acpi service can run the @@ -729,12 +736,10 @@ pub fn build(b: *std.Build) void { if (discovery == .acpi) programModule(discovery_exe).addImport("device-manager-protocol", device_manager_protocol_module); if (discovery == .acpi) programModule(discovery_exe).addImport("power-protocol", power_protocol_module); const device_manager_exe = addUserBinary(b, kernel_target, &default_imports, "device-manager", "system/services/device-manager/device-manager.zig"); - // Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330. - programModule(device_manager_exe).addImport("pci-class", pci_class_module); programModule(device_manager_exe).addImport("device-manager-protocol", device_manager_protocol_module); - // The manager matches reported USB interfaces by their (class,subclass,protocol) - // triple (usbDriverForIdentity), built from the named usb-ids codes. - programModule(device_manager_exe).addImport("usb-ids", usb_ids_module); + // Driver matching is data-driven: the manager parses /etc/devices.csv into this + // module's rules and binds each reported device by most-specific match. + programModule(device_manager_exe).addImport("device-registry", device_registry_module); // The input service and its exercisers: the fan-out server, a hardware-free synthetic // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. const input_exe = addUserBinary(b, kernel_target, &default_imports, "input", "system/services/input/input.zig"); @@ -764,6 +769,11 @@ pub fn build(b: *std.Build) void { .{ .path = "system/services/input", .binary = input_exe.getEmittedBin() }, .{ .path = "system/services/discovery", .binary = discovery_exe.getEmittedBin() }, .{ .path = "system/services/logger", .binary = logger_exe.getEmittedBin() }, + // A data file, not a binary: the device registry the manager reads at boot. + // Packing it under /etc makes the kernel auto-mount /etc as a read-only + // initrd tree (system/kernel/vfs.zig setInitialRamdisk), so the manager can + // fs.open("/etc/devices.csv") with no filesystem service running. + .{ .path = "etc/devices.csv", .binary = b.path("etc/devices.csv") }, .{ .path = "system/drivers/ps2-bus", .binary = ps2_bus_exe.getEmittedBin() }, .{ .path = "system/drivers/ps2-keyboard", .binary = ps2_keyboard_exe.getEmittedBin() }, .{ .path = "system/drivers/ps2-mouse", .binary = ps2_mouse_exe.getEmittedBin() }, @@ -1078,6 +1088,7 @@ pub fn build(b: *std.Build) void { "library/device/acpi/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21) "library/device/usb/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings "library/device/usb/usb-ids.zig", // class/subclass/protocol code assignments + "library/device/registry/device-registry.zig", // /etc/devices.csv parse + most-specific driver match "library/device/mmio/mmio.zig", // barriers assemble + registers round-trip "system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine "system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly diff --git a/docs/device-driver-development/device-manager.md b/docs/device-driver-development/device-manager.md index ff52d21..5ddc587 100644 --- a/docs/device-driver-development/device-manager.md +++ b/docs/device-driver-development/device-manager.md @@ -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. diff --git a/docs/device-driver-development/devices-csv.md b/docs/device-driver-development/devices-csv.md new file mode 100644 index 0000000..bceede4 --- /dev/null +++ b/docs/device-driver-development/devices-csv.md @@ -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/` (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. diff --git a/etc/devices.csv b/etc/devices.csv new file mode 100644 index 0000000..44afff1 --- /dev/null +++ b/etc/devices.csv @@ -0,0 +1,33 @@ +# /etc/devices.csv — the device→driver registry. +# +# The device manager reads this at boot and binds each device a bus driver +# reports to the driver named here. It is AUTHORITATIVE: a device that no row +# matches goes unbound (logged), never guessed. Edit this file to teach the +# system new hardware — no recompile of the device manager required. +# +# 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 which bus reported the device: pci | usb | acpi +# base PCI base class / USB class (hex) +# class PCI subclass / USB subclass (hex) +# prog_if PCI prog-IF / USB protocol (hex) +# vendor PCI vendor id / USB idVendor (hex) +# device PCI device id / USB idProduct (hex) +# subsystem PCI subsystem, packed (ssvid<<16)|ssid (hex) +# hid ACPI _HID string (e.g. PNP0303); blank for pci/usb +# driver full ramdisk path of the driver to spawn +# +# '*' or an empty field is a wildcard. When several rows match one device the +# MOST SPECIFIC wins (pinning vendor/device/hid beats pinning only a class), so +# a generic class rule and a precise vendor:device rule can coexist. +# +# bus base class prog_if vendor device subsystem hid driver +pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus +pci, 03, 80, *, 1AF4, 1050, *, *, /system/drivers/virtio-gpu +usb, 03, 01, 01, *, *, *, *, /system/drivers/usb-hid-keyboard +usb, 03, 01, 02, *, *, *, *, /system/drivers/usb-hid-mouse +usb, 08, 06, 50, *, *, *, *, /system/drivers/usb-storage +acpi, *, *, *, *, *, *, PNP0303, /system/drivers/ps2-bus +acpi, *, *, *, *, *, *, PNP0F13, /system/drivers/ps2-bus diff --git a/library/device/model/device-abi.zig b/library/device/model/device-abi.zig index 63b8213..355da1f 100644 --- a/library/device/model/device-abi.zig +++ b/library/device/model/device-abi.zig @@ -125,6 +125,15 @@ pub const DeviceDescriptor = extern struct { // `pci_device` is (an xHCI controller, an AHCI controller) — decode the triple into // names with the pci-class module. pci_class: u64, + // Numeric identity beyond the class triple, mirrored in the bus report's + // ChildAdded so /etc/devices.csv can bind on it: `vendor`/`device` are the PCI + // vendor/device (or USB idVendor/idProduct), `subsystem` is the PCI subsystem id + // packed `(subsystem_vendor << 16) | subsystem_device`. Zero where the bus has no + // such concept. Defaulted so existing descriptor literals keep compiling and lay + // out identically until they choose to set them. + vendor: u16 = 0, + device: u16 = 0, + subsystem: u32 = 0, hid_len: u64, resource_count: u64, hid: [8]u8, diff --git a/library/device/registry/device-registry.zig b/library/device/registry/device-registry.zig new file mode 100644 index 0000000..30ecb27 --- /dev/null +++ b/library/device/registry/device-registry.zig @@ -0,0 +1,348 @@ +//! The device registry: parse `/etc/devices.csv` into match rules and bind a +//! reported device to a driver. This is the data-driven replacement for the +//! device manager's three hand-written `switch` tables (`pciDriverForIdentity`, +//! `hidDriverFor`, `usbDriverForIdentity`); the registry is now **authoritative** +//! — a device that no row matches goes unbound (logged), never guessed. +//! +//! Pure logic: no hardware access, no syscalls, no allocator. `parse` fills a +//! caller-provided `[]Rule` whose string fields (`hid`, `driver`) are slices +//! *into the CSV source*, so the source buffer must outlive the rules (the +//! manager holds it in a static buffer for the life of the process — zero-copy). +//! That keeps this module freestanding and unit-testable with plain `zig test`. +//! +//! The file format (docs/device-driver-development/device-manager.md, and the +//! `/etc/devices.csv` header itself): one rule per line, nine comma-separated +//! fields, `#` starts a comment (whole-line or trailing), blank lines ignored. +//! +//! bus, base, class, prog_if, vendor, device, subsystem, hid, driver +//! +//! `bus` is `pci`/`usb`/`acpi`; the numeric fields are hex (with or without a +//! `0x` prefix); `*` or an empty field is a wildcard (matches anything). For PCI +//! the class triple is base/subclass/prog-IF; for USB it is class/subclass/ +//! protocol with vendor/device the idVendor/idProduct; ACPI matches on `hid` +//! (e.g. "PNP0303") with the triple left blank. `driver` is a full ramdisk path. + +const std = @import("std"); + +/// Which bus a rule or a reported device belongs to. `unknown` is what an +/// unrecognised `bus` token parses to — such a rule never matches (its bus +/// equals no real device's), so a typo fails safe rather than binding wrongly. +pub const Bus = enum { + pci, + usb, + acpi, + unknown, + + pub fn fromToken(token: []const u8) Bus { + if (std.mem.eql(u8, token, "pci")) return .pci; + if (std.mem.eql(u8, token, "usb")) return .usb; + if (std.mem.eql(u8, token, "acpi")) return .acpi; + return .unknown; + } +}; + +/// A reported device's full identity, as the manager assembles it from a +/// `child_added`: the bus-native class triple plus the numeric ids the widened +/// ABI now carries, or the ACPI `_HID` string. Fields a given bus does not have +/// are zero / empty (a PCI function has no `hid`; an ACPI device has no vendor). +pub const Identity = struct { + bus: Bus, + base: u8 = 0, + subclass: u8 = 0, + prog_if: u8 = 0, + vendor: u16 = 0, + device: u16 = 0, + subsystem: u32 = 0, + hid: []const u8 = "", +}; + +/// One parsed registry row. A `null` field is a wildcard — it matches any value +/// and contributes nothing to specificity. String fields point into the CSV +/// source that was parsed (see the module doc). +pub const Rule = struct { + bus: Bus, + base: ?u8 = null, + subclass: ?u8 = null, + prog_if: ?u8 = null, + vendor: ?u16 = null, + device: ?u16 = null, + subsystem: ?u32 = null, + hid: ?[]const u8 = null, + driver: []const u8, +}; + +/// Specificity weights: how much each pinned field counts toward "most specific +/// wins". Doubling from the coarsest (`base`) so that each level outweighs *all* +/// coarser levels combined (1+2+4+8+16 = 31 < 32) — a rule that pins `device` +/// always beats any rule that does not, no matter how many coarse fields the +/// latter pins. `hid` and `device` share the top tier (the user's "hid and +/// device weigh heaviest"); they never co-occur, since `hid` is ACPI-only and +/// `device` is a PCI/USB numeric id. +const weight_base: u32 = 1; +const weight_subclass: u32 = 2; +const weight_prog_if: u32 = 4; +const weight_vendor: u32 = 8; +const weight_subsystem: u32 = 16; +const weight_device: u32 = 32; +const weight_hid: u32 = 32; + +/// The outcome of `matchDriver`: the winning rule's driver path, its specificity, +/// and whether another rule tied it at that specificity. `ambiguous` is a +/// registry authoring error (two equally-specific rules claiming one device); the +/// manager logs it loudly and binds the first, so a shadowed rule is visible +/// rather than silently dropped. +pub const Match = struct { + driver: []const u8, + specificity: u32, + ambiguous: bool, +}; + +/// Whether `rule` matches `id`: same bus, and every pinned (non-wildcard) field +/// equal. `hid` compares as a string; the rest as integers. +fn matches(rule: Rule, id: Identity) bool { + if (rule.bus != id.bus) return false; + if (rule.base) |b| if (b != id.base) return false; + if (rule.subclass) |s| if (s != id.subclass) return false; + if (rule.prog_if) |p| if (p != id.prog_if) return false; + if (rule.vendor) |v| if (v != id.vendor) return false; + if (rule.device) |d| if (d != id.device) return false; + if (rule.subsystem) |s| if (s != id.subsystem) return false; + if (rule.hid) |h| if (!std.mem.eql(u8, h, id.hid)) return false; + return true; +} + +/// The specificity score of a rule — the sum of the weights of its pinned fields. +fn specificity(rule: Rule) u32 { + var score: u32 = 0; + if (rule.base != null) score += weight_base; + if (rule.subclass != null) score += weight_subclass; + if (rule.prog_if != null) score += weight_prog_if; + if (rule.vendor != null) score += weight_vendor; + if (rule.device != null) score += weight_device; + if (rule.subsystem != null) score += weight_subsystem; + if (rule.hid != null) score += weight_hid; + return score; +} + +/// Bind a reported device to a driver: of every rule that matches `id`, return +/// the most specific. `null` when nothing matches (the device goes unbound — +/// the authoritative registry does not guess). On an exact specificity tie the +/// first such rule in file order wins and `ambiguous` is set. +pub fn matchDriver(rules: []const Rule, id: Identity) ?Match { + var best: ?Match = null; + for (rules) |rule| { + if (!matches(rule, id)) continue; + const score = specificity(rule); + if (best) |current| { + if (score > current.specificity) { + best = .{ .driver = rule.driver, .specificity = score, .ambiguous = false }; + } else if (score == current.specificity) { + // Two equally-specific rules claim this device — keep the first, + // flag the ambiguity for the manager to log. + best.?.ambiguous = true; + } + } else { + best = .{ .driver = rule.driver, .specificity = score, .ambiguous = false }; + } + } + return best; +} + +// --- parsing ----------------------------------------------------------------- + +/// What one CSV line parsed to. `malformed` is a non-comment, non-blank line the +/// parser could not read (wrong field count, unparsable number, empty driver) — +/// the manager counts these and logs, so a broken registry is loud, not silent. +const Line = union(enum) { + rule: Rule, + ignorable, // blank or comment + malformed, +}; + +/// The result of `parse`: how many rules landed in the caller's buffer, and how +/// many non-ignorable lines were malformed (for the manager to log). `truncated` +/// is set if there were more valid rules than the buffer could hold. +pub const ParseResult = struct { + count: usize, + malformed: usize, + truncated: bool, +}; + +/// Strip a trailing `#` comment and surrounding whitespace from one raw line. +fn stripComment(raw: []const u8) []const u8 { + const body = if (std.mem.indexOfScalar(u8, raw, '#')) |hash| raw[0..hash] else raw; + return std.mem.trim(u8, body, " \t\r\n"); +} + +/// Parse one hex field into `T`, honouring `*`/empty as a wildcard (`null`) and +/// an optional `0x` prefix. Returns an error only for a genuinely unparsable +/// non-wildcard token, so the caller can mark the whole line malformed. +fn parseHexField(comptime T: type, field: []const u8) !?T { + const token = std.mem.trim(u8, field, " \t"); + if (token.len == 0 or std.mem.eql(u8, token, "*")) return null; + const digits = if (std.mem.startsWith(u8, token, "0x") or std.mem.startsWith(u8, token, "0X")) + token[2..] + else + token; + return try std.fmt.parseInt(T, digits, 16); +} + +/// Parse a wildcard-or-string field (the `hid` column): `*`/empty → wildcard. +fn parseStringField(field: []const u8) ?[]const u8 { + const token = std.mem.trim(u8, field, " \t"); + if (token.len == 0 or std.mem.eql(u8, token, "*")) return null; + return token; +} + +/// Classify and (if a rule) parse one line. Split out from `parse` so it can be +/// unit-tested directly. `line` is the raw line including no newline. +fn parseLine(line: []const u8) Line { + const body = stripComment(line); + if (body.len == 0) return .ignorable; + + // Nine comma-separated fields: bus, base, class, prog_if, vendor, device, + // subsystem, hid, driver. + var fields: [9][]const u8 = undefined; + var count: usize = 0; + var it = std.mem.splitScalar(u8, body, ','); + while (it.next()) |field| { + if (count >= fields.len) return .malformed; // too many columns + fields[count] = field; + count += 1; + } + if (count != fields.len) return .malformed; // too few columns + + const bus = Bus.fromToken(std.mem.trim(u8, fields[0], " \t")); + if (bus == .unknown) return .malformed; + + const driver = std.mem.trim(u8, fields[8], " \t"); + if (driver.len == 0) return .malformed; + + return .{ .rule = .{ + .bus = bus, + .base = parseHexField(u8, fields[1]) catch return .malformed, + .subclass = parseHexField(u8, fields[2]) catch return .malformed, + .prog_if = parseHexField(u8, fields[3]) catch return .malformed, + .vendor = parseHexField(u16, fields[4]) catch return .malformed, + .device = parseHexField(u16, fields[5]) catch return .malformed, + .subsystem = parseHexField(u32, fields[6]) catch return .malformed, + .hid = parseStringField(fields[7]), + .driver = driver, + } }; +} + +/// Parse a whole `/etc/devices.csv` into `out_rules`. The string fields of the +/// returned rules point into `source`, which must outlive them. +pub fn parse(source: []const u8, out_rules: []Rule) ParseResult { + var result: ParseResult = .{ .count = 0, .malformed = 0, .truncated = false }; + var lines = std.mem.splitScalar(u8, source, '\n'); + while (lines.next()) |line| { + switch (parseLine(line)) { + .ignorable => {}, + .malformed => result.malformed += 1, + .rule => |rule| { + if (result.count >= out_rules.len) { + result.truncated = true; + continue; + } + out_rules[result.count] = rule; + result.count += 1; + }, + } + } + return result; +} + +// --- tests ------------------------------------------------------------------- + +const testing = std.testing; + +// The worked example from the design: a specific virtio-gpu rule (pins vendor + +// device) and a generic display rule (class only) both match the virtio card; +// the specific one must win. And a plain VGA adapter still falls to the generic +// rule. This is the whole point of widening the ABI to carry vendor/device. +test "virtio device rule beats the generic display rule" { + const csv = + \\# bus, base, class, prog_if, vendor, device, subsystem, hid, driver + \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display + \\pci, 03, 80, *, 1AF4, 1050, *, *, /system/drivers/virtio-gpu + ; + var rules: [8]Rule = undefined; + const parsed = parse(csv, &rules); + try testing.expectEqual(@as(usize, 2), parsed.count); + try testing.expectEqual(@as(usize, 0), parsed.malformed); + + // The virtio-gpu function: display / other, vendor 1AF4 device 1050. + const virtio = matchDriver(rules[0..parsed.count], .{ + .bus = .pci, .base = 0x03, .subclass = 0x80, .prog_if = 0x00, + .vendor = 0x1AF4, .device = 0x1050, + }).?; + try testing.expect(!virtio.ambiguous); + try testing.expectEqualStrings("/system/drivers/virtio-gpu", virtio.driver); + + // A plain VGA adapter (display / VGA) still binds the generic display driver. + const vga = matchDriver(rules[0..parsed.count], .{ + .bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00, + .vendor = 0x1234, .device = 0x1111, + }).?; + try testing.expectEqualStrings("/system/drivers/display", vga.driver); +} + +test "no matching row leaves the device unbound" { + const csv = "pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus\n"; + var rules: [8]Rule = undefined; + const parsed = parse(csv, &rules); + try testing.expectEqual(@as(usize, 1), parsed.count); + + // An AHCI controller (mass storage / SATA / AHCI) has no row — unbound. + const unmatched = matchDriver(rules[0..parsed.count], .{ + .bus = .pci, .base = 0x01, .subclass = 0x06, .prog_if = 0x01, + }); + try testing.expect(unmatched == null); +} + +test "acpi rows match on hid" { + const csv = + \\acpi, *, *, *, *, *, *, PNP0303, /system/drivers/ps2-bus + \\acpi, *, *, *, *, *, *, PNP0F13, /system/drivers/ps2-bus + ; + var rules: [8]Rule = undefined; + const parsed = parse(csv, &rules); + try testing.expectEqual(@as(usize, 2), parsed.count); + + const keyboard = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0303" }).?; + try testing.expectEqualStrings("/system/drivers/ps2-bus", keyboard.driver); + const nothing = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0A03" }); + try testing.expect(nothing == null); +} + +test "equally specific rules flag ambiguity" { + const csv = + \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-a + \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-b + ; + var rules: [8]Rule = undefined; + const parsed = parse(csv, &rules); + const hit = matchDriver(rules[0..parsed.count], .{ + .bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00, + }).?; + try testing.expect(hit.ambiguous); + try testing.expectEqualStrings("/system/drivers/display-a", hit.driver); // first wins +} + +test "comments, blanks, and malformed lines" { + const csv = + \\# a header comment + \\ + \\pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus # trailing comment + \\pci, ZZ, 03, 30, *, *, *, *, /system/drivers/broken + \\pci, 03, 00, 00, *, *, *, *, + \\bogus-bus, *, *, *, *, *, *, *, /system/drivers/x + ; + var rules: [8]Rule = undefined; + const parsed = parse(csv, &rules); + try testing.expectEqual(@as(usize, 1), parsed.count); // only the xhci row is valid + try testing.expectEqual(@as(usize, 3), parsed.malformed); // bad hex, empty driver, bad bus + try testing.expectEqualStrings("/system/drivers/usb-xhci-bus", rules[0].driver); + try testing.expect(rules[0].hid == null); // trailing comment stripped, hid still wildcard +} diff --git a/library/protocol/device-manager/device-manager-protocol.zig b/library/protocol/device-manager/device-manager-protocol.zig index d557ec7..76900fc 100644 --- a/library/protocol/device-manager/device-manager-protocol.zig +++ b/library/protocol/device-manager/device-manager-protocol.zig @@ -11,6 +11,19 @@ /// startup instead of quiet corruption later. pub const version: u16 = 1; +/// Which bus a `child_added` came from — stated by the reporting bus driver so +/// the manager's /etc/devices.csv matcher knows how to read the report's identity +/// (a PCI class triple vs a USB class triple are the same 24 bits but different +/// namespaces) and which `bus` column a rule must name to bind it. `unknown` is +/// the zero default, so an un-upgraded reporter fails to match rather than +/// binding to the wrong bus's rule. +pub const BusKind = enum(u8) { + unknown = 0, + pci = 1, + usb = 2, + acpi = 3, +}; + /// What kind of driver is talking (docs/driver-model.md's shapes). pub const Role = enum(u8) { /// Owns a controller and reports the devices behind it (`child_added`). @@ -66,7 +79,9 @@ pub const reply_size = @sizeOf(HelloReply); /// restarted instance rediscovers and re-reports. pub const ChildAdded = extern struct { operation: u8 = @intFromEnum(Operation.child_added), - reserved0: u8 = 0, + /// A `BusKind` value: which bus reported this child, so the manager reads the + /// identity in the right namespace and matches against the right `bus` column. + bus: u8 = @intFromEnum(BusKind.unknown), reserved1: u16 = 0, reserved2: u32 = 0, /// The reporting driver's own device (the controller) — the child's parent. @@ -80,6 +95,18 @@ pub const ChildAdded = extern struct { /// manager hands a matched driver as its argv assignment — or `no_device` /// for an unregistered leaf (a USB port before the descriptor track). device_id: u64 = no_device, + /// The vendor id (PCI vendor / USB idVendor), or 0 when the bus has no such + /// concept (ACPI). Carried so the manager's /etc/devices.csv matcher can bind + /// on vendor — a level the bus-native `identity` (a class triple) cannot express. + vendor: u16 = 0, + /// The device id (PCI device / USB idProduct), or 0. The most specific numeric + /// level: this is what lets one virtio-gpu (1AF4:1050) be told from any other + /// virtio display function without the driver re-confirming after it is spawned. + device: u16 = 0, + /// The PCI subsystem id, packed `(subsystem_vendor << 16) | subsystem_device` + /// (so it reads vendor-first, matching the CSV's `ssvid:ssid`), or 0 when the + /// device has no subsystem id (a bridge, or a non-PCI bus). + subsystem: u32 = 0, /// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices /// discovered by firmware string rather than a numeric bus identity. Empty /// (all zero) otherwise. Widens for FDT `compatible` strings later. diff --git a/system/drivers/display/display.zig b/system/drivers/display/display.zig deleted file mode 100644 index 8c50f11..0000000 --- a/system/drivers/display/display.zig +++ /dev/null @@ -1,59 +0,0 @@ -//! /system/drivers/display - the generic display engine driver. -//! This driver is a non official driver for GPU vendors like Intel, NVIDIA, AMD. It provides basic -//! display engine features to the display engine protocol used by the display server, compositor -//! and graphical user interface libraries like Zooeee. -//! -//! This driver is acts like BUS driver, in that it detects the GPU, its capabilities and loads -//! sub-drivers for each device detected. Similar The device manager -//! finds display adaptor e.g. over the PCI/ACPI, and passes the buck on to this driver to handle. -//! -//! The display driver provides the low level part of identifying the device and launching the -//! generic device driver for a GPU vendor. -//! -//! It takes over the framebuffer feature that was setup during system boot. -const std = @import("std"); -const device = @import("driver"); -const ipc = @import("ipc"); -const process = @import("process"); -const service = @import("service"); -const device_manager = @import("driver"); -const logging = @import("logging"); -const mmio = @import("mmio"); -const display_protocol = @import("display-protocol"); -const scanout_protocol = @import("scanout-protocol"); -var device_id: u64 = 0; - -fn initialise(endpoint: ipc.Handle) bool { - _ = endpoint; - // Hello the device manager (role: device — we claim one GPU's PCI function - // and serve its display engine; we report no children). Best-effort: without a - // manager the driver still runs standalone; when present, the manager marks us - // up before the hello deadline and restarts us if we die. - _ = device_manager.hello(.device, device_id); - return true; -} - -fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize { - _ = sender; - _ = capability; - _ = reply; - - if (message.len < scanout_protocol.request_size) return 0; - return 0; -} - -pub fn main(init: process.Init) void { - const argument = init.arguments.get(1) orelse { - _ = logging.write("display: missing device id (argv[1])\n"); - return; - }; - device_id = std.fmt.parseInt(u64, argument, 10) catch { - std.log.info("malformed device id '{s}'", .{argument}); - return; - }; - service.run(256, .{ - .service = .scanout, - .init = initialise, - .on_message = onMessage, - }); -} diff --git a/system/drivers/display/intel-integrated/intel-integrated.zig b/system/drivers/display/intel-integrated/intel-integrated.zig deleted file mode 100644 index 7c78f8d..0000000 --- a/system/drivers/display/intel-integrated/intel-integrated.zig +++ /dev/null @@ -1,69 +0,0 @@ -//! /system/drivers/display/intel-integrated - the intel 985 family display engine driver. -const std = @import("std"); -const device = @import("driver"); -const ipc = @import("ipc"); -const process = @import("process"); -const service = @import("service"); -const logging = @import("logging"); -const mmio = @import("mmio"); -const display_protocol = @import("display-protocol"); -const scanout_protocol = @import("scanout-protocol"); -const device_manager_protocol = @import("device-manager-protocol"); -var device_id: u64 = 0; - - -fn initialise(endpoint: ipc.Handle) bool { - _ = endpoint; - return true; -} - -fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize { - _ = sender; - _ = capability; - _ = reply; - - if (message.len < scanout_protocol.request_size) return 0; - const request = std.mem.bytesToValue(scanout_protocol.Request, message[0..scanout_protocol.request_size]); - switch (request.operation) { - _ => return 0, - // TODO: - // @intFromEnum(sp.Operation.present) => return scanoutStatus(reply, presentFull()), - // @intFromEnum(sp.Operation.get_modes) => { - // var response = sp.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined }; - // for (0..sp.max_modes) |i| { - // response.modes[i] = if (i < offered_modes.len) - // .{ .width = offered_modes[i].width, .height = offered_modes[i].height } - // else - // .{ .width = 0, .height = 0 }; - // } - // @memcpy(reply[0..sp.modes_reply_size], std.mem.asBytes(&response)); - // return sp.modes_reply_size; - // }, - // @intFromEnum(sp.Operation.set_mode) => { - // const w = request.width; - // const h = request.height; - // if (w == 0 or h == 0 or w > max_width or h > max_height) return scanoutStatus(reply, false); - // current_width = w; - // current_height = h; - // return scanoutStatus(reply, setScanoutRect()); - // }, - else => return 0, - } - return 0; -} - -pub fn main(init: process.Init) void { - const argument = init.arguments.get(1) orelse { - _ = logging.write("display/intel-985: missing device id (argv[1])\n"); - return; - }; - device_id = std.fmt.parseInt(u64, argument, 10) catch { - std.log.info("malformed device id '{s}'", .{argument}); - return; - }; - service.run(256, .{ - .service = .scanout, - .init = initialise, - .on_message = onMessage, - }); -} diff --git a/system/drivers/pci-bus/pci-bus.zig b/system/drivers/pci-bus/pci-bus.zig index bf430e2..1f540e2 100644 --- a/system/drivers/pci-bus/pci-bus.zig +++ b/system/drivers/pci-bus/pci-bus.zig @@ -153,6 +153,12 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void var descriptor = std.mem.zeroes(device.DeviceDescriptor); descriptor.class = @intFromEnum(device.DeviceClass.pci_device); descriptor.pci_class = class_triple; + // Vendor/device from the first config dword (0x00): low half vendor, high half + // device. These carry to the manager's /etc/devices.csv matcher so a function + // can bind on its exact 1AF4:1050 identity, not just its class triple. + const vendor_device = configRead(bus, dev, function, 0x00); + descriptor.vendor = @truncate(vendor_device); + descriptor.device = @truncate(vendor_device >> 16); descriptor.resources[0] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)), @@ -164,6 +170,11 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void // write all-ones, read the writable mask back, restore. Header type 0 only. const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F; if (header_type == 0) { + // Subsystem id lives at 0x2C only on type-0 (device) headers, not on + // bridges: dword low half is subsystem-vendor, high half subsystem-device. + // Repack vendor-first so it reads like the CSV's `ssvid:ssid`. + const subsystem_dword = configRead(bus, dev, function, 0x2C); + descriptor.subsystem = (@as(u32, @truncate(subsystem_dword)) << 16) | @as(u32, @truncate(subsystem_dword >> 16)); const command = configRead16(bus, dev, function, 0x04); configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11)); var i: u64 = 0; @@ -215,10 +226,14 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void return; }; const report = device_manager_protocol.ChildAdded{ + .bus = @intFromEnum(device_manager_protocol.BusKind.pci), .parent = bridge_id, .bus_address = (bus << 8) | (dev << 3) | function, .identity = class_triple, .device_id = registered, + .vendor = descriptor.vendor, + .device = descriptor.device, + .subsystem = descriptor.subsystem, }; var reply: [device_manager_protocol.message_maximum]u8 = undefined; _ = ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch { diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 9469ea2..f8c3efd 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -379,6 +379,7 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI }; const report = device_manager_protocol.ChildAdded{ + .bus = @intFromEnum(device_manager_protocol.BusKind.usb), .parent = controller_id, .bus_address = (@as(u64, port) << 8) | interface.number, .identity = identity, diff --git a/system/drivers/virtio-gpu/virtio-gpu.zig b/system/drivers/virtio-gpu/virtio-gpu.zig index 375b9af..85dd930 100644 --- a/system/drivers/virtio-gpu/virtio-gpu.zig +++ b/system/drivers/virtio-gpu/virtio-gpu.zig @@ -33,11 +33,6 @@ const vg = @import("virtio-gpu-protocol.zig"); /// the compositor in the announce so it packs colours in the surface's byte order. const display_format_bgrx: u32 = 1; -/// The PCI vendor/device ids of a modern virtio-gpu (Red Hat / virtio; GPU is a -/// virtio-1.0-only device, so the id is always the modern 0x1050 — no legacy variant). -const virtio_vendor: u16 = 0x1AF4; -const virtio_gpu_device: u16 = 0x1050; - /// The scanout resource + shared surface are sized to the *largest* mode we offer; a mode /// change (V5) re-points the scanout rectangle within it, so the resource, its backing, and /// the shared surface never churn — and the surface's row stride is always `max_width`, which @@ -210,19 +205,15 @@ fn initialise(endpoint: ipc.Handle) bool { return false; }; - // Config space is resource 0. Confirm it really is a virtio-gpu, then enable memory-space - // decode + bus mastering (the device DMAs the ring and backing out of RAM); pci-bus only - // preserves whatever the firmware left, and a secondary display is often left disabled. + // Config space is resource 0. The registry (/etc/devices.csv) bound this driver by the + // exact virtio-gpu identity (vendor 0x1AF4 / device 0x1050), so there is no re-confirm to + // do here any more — map config space and enable memory-space decode + bus mastering (the + // device DMAs the ring and backing out of RAM; pci-bus only preserves whatever the firmware + // left, and a secondary display is often left disabled). var function = pci.Function.map(device_id, descriptor) orelse { std.log.info("config-space map failed", .{}); return false; }; - const vendor = function.vendorId(); - const dev = function.deviceId(); - if (vendor != virtio_vendor or dev != virtio_gpu_device) { - std.log.info("not a virtio-gpu (vendor 0x{x} device 0x{x})", .{ vendor, dev }); - return false; - } function.enableMemoryAndBusMaster(); // Walk the capability list for the virtio common-config and notify structures (V3 needs diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 915af16..d84609e 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -220,7 +220,7 @@ fn onInit(endpoint: ipc.Handle) bool { else std.log.info("reported {s} (device {d}, {d} resources)", .{ hid, entry.device_id, entry.resource_count }); if (manager) |h| { - var report = device_manager_protocol.ChildAdded{ .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id }; + var report = device_manager_protocol.ChildAdded{ .bus = @intFromEnum(device_manager_protocol.BusKind.acpi), .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id }; @memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]); var reply: [device_manager_protocol.message_maximum]u8 = undefined; _ = ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 3b0f35c..a8f0897 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -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], ®istry_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); } } }