From 6b3a3816269bc8b6d80860f9ca8a77b8791c57aa Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:33:35 +0100 Subject: [PATCH] ps2: one instance, every node the machine has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 8042 is a single controller described by two ACPI nodes — PNP0303 carries the 0x60/0x64 ports, PNP0F13 is the mouse — so it cannot be split across two processes without them fighting over the same registers. That is why ps2-bus is a singleton, and why it used to find and claim both nodes itself. The manager now gives it every matching node instead. The keyboard node rides the spawn, because it holds the ports and is needed immediately; the mouse node is transferred to the already-running instance. Late arrival is safe here and the ordering is natural rather than lucky: the mouse is not touched until after the controller handshakes and identify. Measured, the handover lands at 0.336 and the driver reaches the mouse at 0.456. Because the count is however many matched, a machine with no PS/2 ports or only one works without a special case — which matters, since the bus is mostly emulated now and machines vary. ps2-bus claims nothing. irq_bind on the mouse node is the proof it holds it: that call is ownership-gated, so a failure means the handover did not land rather than a hardware fault, and the log says so. acpi-ps2 asserts both delegations with the spawned device backreferenced, so the node that rides the spawn must be the one the driver was spawned for. Disabling the second delegation fails it. Two things worth recording. The first discrimination patch was not valid Zig, so nothing ran and a stale binary reported a pass — checked the build before believing it. And with the second delegation disabled, acpi-ps2 fails while input still passes: the mouse works without its IRQ binding, so exactly one case covers that path. Suite 118/118. --- system/drivers/ps2-bus/ps2-bus.zig | 34 +++++++-------- .../device-manager/device-manager.zig | 41 +++++++++++++++++-- test/qemu_test.py | 10 ++++- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/system/drivers/ps2-bus/ps2-bus.zig b/system/drivers/ps2-bus/ps2-bus.zig index fcc0c72..9911562 100644 --- a/system/drivers/ps2-bus/ps2-bus.zig +++ b/system/drivers/ps2-bus/ps2-bus.zig @@ -114,10 +114,9 @@ pub fn main() void { _ = logging.write("/system/drivers/ps2-bus: found PS/2 controller\n"); _ = logging.write("/system/drivers/ps2-bus: initializing controller\n"); - device.claim(controller_device_descriptor.id) catch |e| { - std.log.warn("unable to claim controller: {s}", .{@errorName(e)}); - return; - }; + // The controller node arrived with the spawn — the manager holds the hardware + // and names it in the call that creates this process + // (docs/os-development/device-authority.md). Nothing to claim. const controller = ps2.Controller.init(controller_device_descriptor) orelse { _ = logging.write("/system/drivers/ps2-bus: controller is missing its IO ports\n"); @@ -255,18 +254,21 @@ pub fn main() void { if (port_device_types[@intFromEnum(ps2.Port.two)] != null) { if (ps2.findMouseDescriptor(buffer)) |descriptor| { if (findInterruptResourceIndex(descriptor)) |auxiliary_index| { - if (device.claim(descriptor.id)) |_| { - if (device.irqBind(descriptor.id, auxiliary_index, endpoint)) { - maybe_auxiliary_interrupt = .{ - .device_id = descriptor.id, - .interrupt_index = auxiliary_index, - .gsi = descriptor.resources[auxiliary_index].start, - }; - } else { - _ = logging.write("/system/drivers/ps2-bus: auxiliary irq_bind failed\n"); - } - } else |e| { - std.log.warn("auxiliary claim failed: {s}", .{@errorName(e)}); + // The mouse node is a *second* device for this one instance — the + // 8042 is one controller with two ports, so it cannot be split across + // two processes. It is transferred to us after the spawn, which is + // safe because we only reach it here, long after the controller + // handshakes and identify. irq_bind is the proof we hold it: it is + // gated on ownership, so a failure here means the handover has not + // landed rather than a hardware problem. + if (device.irqBind(descriptor.id, auxiliary_index, endpoint)) { + maybe_auxiliary_interrupt = .{ + .device_id = descriptor.id, + .interrupt_index = auxiliary_index, + .gsi = descriptor.resources[auxiliary_index].start, + }; + } else { + _ = logging.write("/system/drivers/ps2-bus: auxiliary irq_bind failed (mouse node not delegated?)\n"); } } } diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 126c2b0..0692992 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -220,6 +220,13 @@ fn childCountOf(reporter: u32) u32 { /// The driver entry a live process id belongs to. Zero is not a process id here: /// it is what `onDriverExit` writes back to retire an id it has already acted on, /// so a second notification for the same death matches nothing. +fn driverByName(name: []const u8) ?*Driver { + for (&drivers) |*driver| { + if (driver.used and std.mem.eql(u8, driver.name(), name)) return driver; + } + return null; +} + fn driverByProcess(process_id: u32) ?*Driver { if (process_id == 0) return null; for (&drivers) |*driver| { @@ -249,6 +256,7 @@ const delegated_drivers = [_][]const u8{ "usb-xhci-bus", "pci-bus", "virtio-gpu", + "ps2-bus", }; /// Matched on the **last path component**, because a driver reaches this table under @@ -512,9 +520,36 @@ fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdd if (match.ambiguous) std.log.info("/system/configuration/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); + // An hid-matched driver is a singleton over one piece of hardware + // described by several nodes: the 8042 is a single controller whose + // I/O ports live under the keyboard node (PNP0303) while the mouse + // is a second node (PNP0F13). Two processes would fight over the + // same 0x60/0x64 registers, so there is exactly one instance — and + // it needs *every* matching device, however many the machine has + // (some have none, some one port, some two). + // + // The first rides the spawn; the rest are transferred to the + // running instance. Late arrival is fine here because the order is + // natural: the instance needs the controller node immediately and + // reaches the mouse only after the 8042 handshakes and identify. + if (!alreadySupervised(match.driver)) { + addDriver(match.driver, device_id, false); + } else if (driverByName(match.driver)) |running| { + if (running.process_id != 0) { + device.claim(device_id) catch |e| switch (e) { + error.AlreadyClaimed => {}, + else => { + std.log.warn("cannot hold device {d} for {s}: {s}", .{ device_id, match.driver, @errorName(e) }); + return status; + }, + }; + device.transfer(device_id, running.process_id) catch |e| { + std.log.warn("could not give device {d} to {s}: {s}", .{ device_id, match.driver, @errorName(e) }); + return status; + }; + std.log.info("delegated device {d} to {s} (already running)", .{ device_id, match.driver }); + } + } } else { // A per-device driver: one instance, the registered id as argv[1]. if (!driverForDevice(device_id)) addDriver(match.driver, device_id, true); diff --git a/test/qemu_test.py b/test/qemu_test.py index 708f373..1fcc6ad 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -780,8 +780,16 @@ CASES = [ {"name": "acpi-ps2", "smp": 4, "timeout": 150, + # The 8042 is ONE controller described by two ACPI nodes, so the single ps2-bus + # instance must receive BOTH. The keyboard node rides the spawn — it carries the + # 0x60/0x64 ports and is needed immediately — and the mouse node is transferred to + # the already-running instance, which is safe because it is not touched until after + # the controller handshakes and identify. Two processes cannot split this: they + # would fight over the same registers. "expect": r"discovery: device \d+\s+bus=acpi hid=PNP0303[\s\S]*" - r"device-manager: spawned \S*ps2-bus[\s\S]*" + r"device-manager: delegated device (\d+) to \S*ps2-bus[\s\S]*" + r"device-manager: spawned \S*ps2-bus for device \1[\s\S]*" + r"device-manager: delegated device \d+ to \S*ps2-bus \(already running\)[\s\S]*" r"ps2-bus: keyboard driver attached", "fail": r"DANOS-TEST-RESULT: FAIL"}, # M21.1: the SCI + power button. Boot the manager (which spawns the acpi