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
-59
View File
@@ -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,
});
}
@@ -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,
});
}
+15
View File
@@ -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 {
@@ -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,
+5 -14
View File
@@ -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
+1 -1
View File
@@ -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 {};
@@ -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);
}
}
}