From a299363b59c81eac2364230b7109d557ae2acb37 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:07:11 +0100 Subject: [PATCH 1/4] The AML interpreter runs in ring 3: the acpi service parses (M20.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AML module becomes a build module compiled into both the kernel (for the \_S5 sleep state it still needs) and the new acpi service — one source, two builds, no fork. The kernel publishes a single acpi-tables node: the DSDT/SSDT blobs as memory resources, a broad io_port grant (the honest trust boundary — firmware AML names whatever ports it chose, known only after parsing), and the SCI for the M21 event track. The acpi service claims the node, maps each blob through the ordinary mmio grant (which preserves the sub-page offset onto the bytecode), and runs the same parser the kernel does. It self-verifies its namespace Device count against the kernel's — 34 = 34 — deterministically via an argv the acpi-parse test passes, so no racing the shared serial buffer. Parse-only touches no hardware; OperationRegion evaluation waits for _CRS/_STA in M20.2. The manager spawns 'discovery' (the neutral ramdisk name) at startup. Suite 56/56. --- build.zig | 8 ++ docs/m19-m20-plan.md | 15 +-- system/devices/acpi.zig | 41 +++++++ system/devices/aml/aml.zig | 14 +++ system/devices/device-abi.zig | 5 + system/devices/platform.zig | 7 ++ system/kernel/tests.zig | 45 ++++++++ system/services/acpi/acpi.zig | 105 ++++++++++++++---- .../device-manager/device-manager.zig | 6 + system/services/fdt/fdt.zig | 3 +- test/qemu_test.py | 7 ++ 11 files changed, 229 insertions(+), 27 deletions(-) diff --git a/build.zig b/build.zig index 3dbc40b..675893d 100644 --- a/build.zig +++ b/build.zig @@ -131,6 +131,13 @@ pub fn build(b: *std.Build) void { }); // 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 + // same parser the kernel does (docs/m19-m20-plan.md decision 1). Pure Zig, + // no kernel imports — one source, two builds. + const aml_module = b.addModule("aml", .{ + .root_source_file = b.path("system/devices/aml/aml.zig"), + }); + const acpi_ids_module = b.addModule("acpi-ids", .{ .root_source_file = b.path("system/devices/acpi-ids.zig"), }); @@ -358,6 +365,7 @@ pub fn build(b: *std.Build) void { .fdt => "system/services/fdt/fdt.zig", }; const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source); + if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module); const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // 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. diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index b98979b..1bc05aa 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -106,13 +106,14 @@ branch is green; keep branches; push everything. skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart hammered 6×). - [x] **merge** `feat/pci-bus` → main, push (merged 2026-07-13). -- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at - system/services/acpi/acpi.zig): kernel publishes `acpi-tables` - (decision 5) — memory over the table blobs, the broad io_port grant, and - **the SCI as an irq resource** (from the FADT; unused until M21 but free - to record now). The service claims it, maps the blobs, runs the shared - AML module in ring 3, logs the namespace device count and `_HID`s. - Scenario `acpi-parse`: user-space count equals the kernel walk's count. +- [x] **M20.1** — acpi service, parse only: the AML interpreter is now a build + module compiled into both kernel and service; the kernel publishes the + `acpi-tables` node (AML blobs as memory resources, the broad io_port grant, + the SCI); the service claims it, maps the blobs, runs the shared parser in + ring 3, and self-verifies its Device count against the kernel's (34 = 34, + deterministic via argv, no log-scraping); the manager spawns `discovery` + at startup. Parse-only touches no hardware. Suite 56/56. + - [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS` resources registered under `acpi-tables` (its io_port + the memory-map holes give containment), reported to the manager. Spawn-from-reports for diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index 8e008db..e7f78d4 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -41,6 +41,9 @@ pub const RegisterAccess = struct { /// Everything the power subsystem needs, extracted from the FADT and the AML /// sleep packages during discovery. Populated by `discover`, read by `power`. pub const PowerInformation = struct { + /// The System Control Interrupt's GSI (FADT SCI_INT) — the line ACPI events + /// (power button, GPEs) arrive on. Published to the acpi service for M21. + sci_interrupt: u16 = 0, /// The SMM command port and the value that switches the platform into ACPI mode. smi_cmd: u16 = 0, acpi_enable: u8 = 0, @@ -408,6 +411,42 @@ pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryR // AML parse failed (e.g. out of memory); power stays best-effort with // whatever the FADT alone provided. } + + // Publish the acpi-tables node (docs/m19-m20-plan.md M20): the AML blobs as + // memory resources for the acpi service to map and parse in ring 3, a broad + // io_port grant for the OperationRegion access its interpreter needs, and + // the SCI for the events track (M21). Exactly one node, one trusted + // claimant. Kept even when the kernel-side device building (above) retires + // in M20.3 — the kernel still owns the *static* tables and \_S5. + publishAcpiTablesNode(device_tree) catch {}; +} + +/// Build the acpi-tables node (see the call site in discover). Best-effort: a +/// failure here leaves the kernel-seeded tree working, only the ring-3 service +/// finds nothing to claim. +fn publishAcpiTablesNode(device_tree: *DeviceTree) !void { + const node = try device_tree.addChild(device_tree.root, .acpi_tables, "acpi-tables"); + // One memory resource per AML block — page-aligned base down, length padded + // up to cover the bytecode, so mmio_map hands the service a pointer into it. + var i: usize = 0; + while (i < aml_block_count and i < device_model.maximum_resources - 2) : (i += 1) { + // mmio_map preserves the sub-page offset, so the service maps this and + // gets a pointer straight to the bytecode. + _ = node.addResource(.memory, aml_block_physical[i], aml_block_len[i]); + } + // The broad I/O grant: OperationRegions name whatever ports the firmware + // chose (EC, PM1, GPE, SMBus); which ports cannot be known before the AML + // that names them is parsed, so the grant is the whole space — the honest + // trust boundary of docs/m19-m20-plan.md decision 5. + _ = node.addResource(.io_port, 0, 1 << 16); + // The SCI (M21 events); harmless to record now. + if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1); +} + +/// The number of Device objects in the namespace built during discovery, or 0. +pub fn amlDeviceCount() usize { + if (namespace) |*ns| return aml.deviceCount(ns); + return 0; } /// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch @@ -670,6 +709,7 @@ const fadt_pm1a_cnt_blk = 64; // u32 (I/O port) const fadt_pm1b_cnt_blk = 68; // u32 (I/O port) const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter const fadt_pm1_cnt_len = 89; // u8 (bytes) +const fadt_sci_int = 46; // u16 (the SCI's GSI) const fadt_flags = 112; // u32 const fadt_reset_register = 116; // GAS (12 bytes) const fadt_reset_value = 128; // u8 @@ -687,6 +727,7 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void { const len: usize = header.length; const pi = &power_information; + pi.sci_interrupt = @truncate(fadt(u16, base, len, fadt_sci_int) orelse 0); pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0); pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0; pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0; diff --git a/system/devices/aml/aml.zig b/system/devices/aml/aml.zig index 8a37648..cbe6b83 100644 --- a/system/devices/aml/aml.zig +++ b/system/devices/aml/aml.zig @@ -50,6 +50,20 @@ pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseRes return .{ .namespace = namespace, .consumed = consumed, .total = total }; } +/// Count the Device objects in a parsed namespace — what the acpi service +/// (docs/m19-m20-plan.md M20) reports, and what the kernel's own parse counts +/// so the two can be checked equal across the ring-3 move. +pub fn deviceCount(namespace: *const Namespace) usize { + return countKind(namespace.root, .device); +} + +fn countKind(node: *const Node, kind: NodeKind) usize { + var n: usize = if (node.kind == kind) 1 else 0; + var c = node.first_child; + while (c) |child| : (c = child.next_sibling) n += countKind(child, kind); + return n; +} + /// Look up the `\_S{state}` sleep package in a parsed namespace and return its /// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent. pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType { diff --git a/system/devices/device-abi.zig b/system/devices/device-abi.zig index 9b04bf4..33cf15e 100644 --- a/system/devices/device-abi.zig +++ b/system/devices/device-abi.zig @@ -28,6 +28,11 @@ pub const DeviceClass = enum(u32) { /// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a /// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`). acpi_device, + /// The ACPI tables themselves, published as one node for the user-space acpi + /// service (docs/m19-m20-plan.md M20): memory resources over the AML blobs, + /// a broad io_port grant for OperationRegion access, and the SCI interrupt. + /// The one node whose claimant is trusted to run firmware bytecode. + acpi_tables, unknown, }; diff --git a/system/devices/platform.zig b/system/devices/platform.zig index 98ce62f..200e6d4 100644 --- a/system/devices/platform.zig +++ b/system/devices/platform.zig @@ -40,6 +40,13 @@ pub fn platformInformation() PlatformInformation { } /// AML parse integrity/diagnostics (namespace node count, bytes consumed). +/// The number of Device objects in the kernel's own AML namespace, or 0 if the +/// parse produced none — the `acpi-parse` test compares the ring-3 service's +/// count against this. +pub fn amlDeviceCount() usize { + return acpi.amlDeviceCount(); +} + pub fn amlStats() AmlStats { return acpi.aml_stats; } diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 47a6596..fd4a764 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -146,6 +146,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { deviceListTest(boot_information); } else if (eql(case, "pci-scan")) { pciScanTest(boot_information); + } else if (eql(case, "acpi-parse")) { + acpiParseTest(boot_information); } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1912,6 +1914,49 @@ fn pciScanTest(boot_information: *const BootInformation) void { result(); } +/// M20.1: the ring-3 AML parse agrees with the kernel's. The manager spawns +/// the discovery service (the acpi build variant); it claims the acpi-tables +/// node, maps the blobs, parses them, and logs its Device count — which must +/// equal what the kernel's own parse produced (the equivalence that licenses +/// retiring the kernel's device build in M20.3). +fn acpiParseTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: acpi-parse\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + + // The kernel's own count, from the namespace it already built for \_S5. + const kernel_devices = platform.amlDeviceCount(); + check("the kernel namespace has devices to compare against", kernel_devices >= 1); + + // Spawn the discovery service directly with that count as argv: it parses + // the same blobs in ring 3 and self-verifies, printing "acpi-parse: ok" iff + // the counts match. The harness's expect regex is that marker — deterministic, + // no racing the shared serial buffer. + process.setInitialRamdisk(image); + var count_text: [16]u8 = undefined; + const count_arg = std.fmt.bufPrint(&count_text, "{d}", .{kernel_devices}) catch "0"; + var spawned = false; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "discovery")) continue; + _ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", count_arg }, scheduler.currentId(), null) catch 0; + spawned = true; + break; + } + check("discovery service spawned", spawned); + result(); +} + /// The whole user-side surface at once: spawn process-test's supervisor role, /// which — entirely from ring 3 — creates an exit endpoint, spawns its two /// children supervised, sees them in process_enumerate, kills them (one blocked, diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 740e185..67c9fc9 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -1,27 +1,94 @@ //! /system/services/acpi — the ACPI discovery service: the x86 firmware -//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). **Placeholder: -//! not implemented until M20.1** — it exists so the build's `-Ddiscovery` -//! option has both of its values and the ramdisk's neutral `discovery` slot is -//! wired before the implementation lands. +//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). Claims the +//! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port +//! grant, the SCI), and runs the **shared AML module** in ring 3 — the same +//! parser the kernel uses for `\_S5`, now the sole builder of the device +//! namespace. //! -//! What it becomes (the plan's decisions 5 and 7): claim the `acpi-tables` -//! node the kernel publishes (table blobs + the broad io_port grant + the SCI), -//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal` -//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell -//! it moved. Then the bus-driver shape: `device_register` the namespace -//! devices (`_HID`, `_CRS` resources, containment against the node's -//! apertures), report each to the device manager, stay resident under its -//! supervision. M21 grows the event side on the same claim: the SCI, PM1 fixed -//! events, GPEs, Notify — published through the domain-named power protocol, -//! never an "ACPI events" protocol. +//! M20.1 (this increment): claim the node, map each AML blob through the +//! ordinary mmio grant, parse them into a namespace, and log the Device count — +//! which the `acpi-parse` scenario checks equals the kernel's own parse. +//! Parsing touches no hardware (the io_port grant and the interpreter's +//! OperationRegion evaluation come in with `_CRS`/`_STA` at M20.2). Registering +//! and reporting the namespace devices, and retiring the kernel's device build, +//! follow in M20.2 and M20.3. +const std = @import("std"); const runtime = @import("runtime"); +const aml = @import("aml"); +const device = runtime.device; -pub fn main() void { - // Not implemented: exit cleanly and silently (a bare spawn by the - // initial-ramdisk sweep must not derange other tests' markers). The - // supervisor reads a clean exit as "meant to stop" — correct for a - // placeholder. +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [128]u8 = undefined; + _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +/// Find the acpi-tables node the kernel published, or null. +fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor { + const total = device.enumerate(buffer); + for (buffer[0..@min(total, buffer.len)]) |d| { + if (d.class == @intFromEnum(device.DeviceClass.acpi_tables)) return d; + } + return null; +} + +pub fn main(init: runtime.process.Init) void { + // When the acpi-parse scenario spawns this directly, argv[1] is the kernel's + // own device count to self-verify against — deterministic, no log-scraping. + const expected: ?usize = if (init.arguments.get(1)) |a| (std.fmt.parseInt(usize, a, 10) catch null) else null; + + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("acpi: out of memory\n"); + return; + }; + const node = findTablesNode(buffer) orelse { + _ = runtime.system.write("acpi: no acpi-tables node to claim\n"); + return; + }; + if (!device.claim(node.id)) { + _ = runtime.system.write("acpi: unable to claim acpi-tables\n"); + return; + } + + // Map each memory resource (an AML blob) and collect the byte slices. The + // grant preserves each blob's sub-page offset, so the mapped pointer lands + // straight on the bytecode. + var blocks: [8][]const u8 = undefined; + var block_count: usize = 0; + for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| { + if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue; + const base = device.mmioMap(node.id, index) orelse { + writeLine("acpi: mmio_map failed for blob {d}\n", .{index}); + continue; + }; + const pointer: [*]const u8 = @ptrFromInt(base); + blocks[block_count] = pointer[0..@intCast(resource.len)]; + block_count += 1; + if (block_count == blocks.len) break; + } + if (block_count == 0) { + _ = runtime.system.write("acpi: no AML blobs on the node\n"); + return; + } + + const result = aml.parse(runtime.allocator(), blocks[0..block_count]) catch { + _ = runtime.system.write("acpi: AML parse failed\n"); + return; + }; + var namespace = result.namespace; + const devices = aml.deviceCount(&namespace); + writeLine("acpi: parsed {d} AML blob(s), {d} namespace devices\n", .{ block_count, devices }); + if (expected) |want| { + if (devices == want) { + _ = runtime.system.write("acpi-parse: ok\n"); + } else { + writeLine("acpi-parse: mismatch (ring-3 {d} vs kernel {d})\n", .{ devices, want }); + } + } + + // Registration and reports arrive in M20.2; stay resident so the claim + // holds and the service is here to grow into the supervised discoverer. + while (true) runtime.system.sleep(1000); } pub const panic = runtime.panic; diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index ed2b4cb..39cfc2b 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -347,6 +347,12 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { } } + // The discovery service (docs/m19-m20-plan.md M20): one per firmware, packed + // under the neutral name "discovery", spawned once at startup. It finds and + // claims the acpi-tables (or devicetree-blob) node itself. Not a per-device + // match — it is the discoverer, not a driver bound to one device. + addDriver("discovery", protocol.no_device, false); + if (test_restart_mode) { // The driver-restart scenario's fixture: claims device 0 (the tree // root, otherwise unclaimed), hellos, then faults — driving backoff, diff --git a/system/services/fdt/fdt.zig b/system/services/fdt/fdt.zig index f3e2d83..cba17e9 100644 --- a/system/services/fdt/fdt.zig +++ b/system/services/fdt/fdt.zig @@ -21,7 +21,8 @@ const runtime = @import("runtime"); -pub fn main() void { +pub fn main(init: runtime.process.Init) void { + _ = init; // Not implemented: exit cleanly and silently (a bare spawn by the // initial-ramdisk sweep must not derange other tests' markers). The // supervisor reads a clean exit as "meant to stop" — correct for a diff --git a/test/qemu_test.py b/test/qemu_test.py index 17771d7..3cf74a1 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -274,6 +274,13 @@ CASES = [ r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: child added", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses + # them) finds exactly the Device count the kernel's own parse produced. + {"name": "acpi-parse", + "smp": 4, + "timeout": 60, + "expect": r"acpi-parse: ok", + "fail": r"acpi-parse: mismatch|DANOS-TEST-RESULT: FAIL"}, # M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map # grant) finds exactly the functions the kernel's own walk recorded. {"name": "pci-scan", From 5ca804d827a36009bdc0686809f40549c44c1507 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:19:39 +0100 Subject: [PATCH 2/4] The acpi service evaluates _CRS/_STA in ring 3 and reports devices (M20.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AML method evaluation now runs in userspace touching real hardware: the service builds an interpreter with a ring-3 Hal (port I/O routed through its claimed acpi-tables node; a scratch page backs SystemMemory maps so a stray OperationRegion degrades to zeros instead of faulting a process that cannot map arbitrary physical memory). It walks the namespace and, for each present _HID device that is not a PCI root, evaluates _CRS, registers it under acpi-tables, and reports it with its EISA-decoded hid. Containment for this needed the broker's irq check to become range-based — an interrupt line is still indivisible, but a parent may own a range, so the acpi-tables node's broad irq window contains its children's legacy lines (a length-1 range is exactly the old equality, so single-irq parents are unaffected). ChildAdded gained a hid field for firmware string identity. Matching those reports to drivers stays off until M20.3, so ps2-bus still comes up via the kernel path — no regression. The acpi-report scenario proves the PS/2 keyboard (io 0x60/0x64 + IRQ) and mouse (IRQ) are reported with their resources. Suite 57/57. --- docs/m19-m20-plan.md | 14 +- system/devices/acpi.zig | 6 +- system/kernel/devices-broker.zig | 11 +- system/kernel/tests.zig | 34 ++ system/services/acpi/acpi.zig | 294 ++++++++++++++++-- .../device-manager-protocol.zig | 6 +- .../device-manager/device-manager.zig | 2 +- test/qemu_test.py | 12 + 8 files changed, 349 insertions(+), 30 deletions(-) diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index 1bc05aa..92e1b09 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -114,11 +114,15 @@ branch is green; keep branches; push everything. deterministic via argv, no log-scraping); the manager spawns `discovery` at startup. Parse-only touches no hardware. Suite 56/56. -- [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS` - resources registered under `acpi-tables` (its io_port + the memory-map - holes give containment), reported to the manager. Spawn-from-reports for - ACPI matches stays off. Scenario: the reported set includes the PS/2 - keyboard and mouse nodes with their IRQ resources. +- [x] **M20.2** — register + report: the service evaluates `_STA`/`_CRS` in + ring 3 (interpreter Hal = port I/O over the claimed node; a scratch page + backs SystemMemory maps so a stray region can't fault it) and registers + + reports each present `_HID` device under `acpi-tables`. Containment: the + broker's irq check became range-based (len-1 == the old equality) so the + node's broad irq window covers children's legacy lines; io ports fall in + the broad io grant. ChildAdded gained `hid`. Matching stays off. The + `acpi-report` scenario asserts the PS/2 keyboard (3 resources) and mouse + (1 resource) among the reports. Suite 57/57. - [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers (ps2-bus) from reports. The `input` and `device-manager` scenarios are diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index e7f78d4..e4318bc 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -439,8 +439,12 @@ fn publishAcpiTablesNode(device_tree: *DeviceTree) !void { // that names them is parsed, so the grant is the whole space — the honest // trust boundary of docs/m19-m20-plan.md decision 5. _ = node.addResource(.io_port, 0, 1 << 16); - // The SCI (M21 events); harmless to record now. + // A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines + // 1 and 12, the RTC, …), and the service registers those devices under this + // node, so it must own a superset. The range [0, 256) covers every GSI; the + // SCI (recorded first, len 1) stays distinct so M21 can pick it out. if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1); + _ = node.addResource(.irq, 0, 256); } /// The number of Device objects in the namespace built during discovery, or 0. diff --git a/system/kernel/devices-broker.zig b/system/kernel/devices-broker.zig index 8460c4e..1bd7c75 100644 --- a/system/kernel/devices-broker.zig +++ b/system/kernel/devices-broker.zig @@ -131,7 +131,16 @@ pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor { /// and would otherwise vacuously "fit" anywhere. fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool { if (parent.kind != child.kind) return false; - if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) return parent.start == child.start; + if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) { + // Range containment: an interrupt line is still indivisible (a child owns + // exactly one GSI), but a parent may own a *range* of lines so a broad + // owner — the acpi-tables node, whose firmware names any legacy IRQ — + // can contain its children's specific lines. A length-1 parent range is + // exactly the old equality rule, so existing single-IRQ parents are + // unaffected. + const span = if (parent.len == 0) 1 else parent.len; + return child.start >= parent.start and child.start < parent.start + span; + } if (child.len == 0 or parent.len == 0) return false; // No overflow: a resource that wraps the address space is not containable. const child_end = std.math.add(u64, child.start, child.len) catch return false; diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index fd4a764..9145d20 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -148,6 +148,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { pciScanTest(boot_information); } else if (eql(case, "acpi-parse")) { acpiParseTest(boot_information); + } else if (eql(case, "acpi-report")) { + acpiReportTest(boot_information); } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1914,6 +1916,38 @@ fn pciScanTest(boot_information: *const BootInformation) void { result(); } +/// M20.2: the acpi service registers + reports its _HID devices. Boot normally +/// (the manager spawns discovery); the harness's expect regex requires the two +/// PS/2 nodes among the service's report lines, each with its _CRS resources — +/// the ring-3 _CRS/_STA evaluation working end to end. The kernel test only +/// starts the manager. +fn acpiReportTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: acpi-report\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + process.setInitialRamdisk(image); + var spawned = false; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "device-manager")) continue; + _ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0; + spawned = true; + break; + } + check("device-manager spawned", spawned); + result(); +} + /// M20.1: the ring-3 AML parse agrees with the kernel's. The manager spawns /// the discovery service (the acpi build variant); it claims the acpi-tables /// node, maps the blobs, parses them, and logs its Device count — which must diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 67c9fc9..f343050 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -1,29 +1,54 @@ //! /system/services/acpi — the ACPI discovery service: the x86 firmware //! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). Claims the //! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port -//! grant, the SCI), and runs the **shared AML module** in ring 3 — the same -//! parser the kernel uses for `\_S5`, now the sole builder of the device -//! namespace. +//! grant, a broad irq window, the SCI), and runs the **shared AML module** in +//! ring 3 — the same parser and interpreter the kernel uses. //! -//! M20.1 (this increment): claim the node, map each AML blob through the -//! ordinary mmio grant, parse them into a namespace, and log the Device count — -//! which the `acpi-parse` scenario checks equals the kernel's own parse. -//! Parsing touches no hardware (the io_port grant and the interpreter's -//! OperationRegion evaluation come in with `_CRS`/`_STA` at M20.2). Registering -//! and reporting the namespace devices, and retiring the kernel's device build, -//! follow in M20.2 and M20.3. +//! M20.2 (this increment): after parsing, walk the namespace and, for each +//! present Device with a hardware id (`_HID`), evaluate its current resource +//! settings (`_CRS`) through a ring-3 `Hal` (port I/O over the claimed node), +//! register it under the acpi-tables node (its I/O ports and IRQs contained by +//! the node's broad grants), and report it to the device manager with its +//! EISA-decoded hid as identity. Matching those reports to drivers (ps2-bus) +//! and retiring the kernel's own device build follow in M20.3. const std = @import("std"); const runtime = @import("runtime"); const aml = @import("aml"); const device = runtime.device; +const protocol = runtime.device_manager_protocol; fn writeLine(comptime fmt: []const u8, arguments: anytype) void { var line: [128]u8 = undefined; _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); } -/// Find the acpi-tables node the kernel published, or null. +// The claimed acpi-tables node and the resource index of its broad io_port +// window — the Hal routes every port access through this one claim. +var node_id: u64 = 0; +var io_resource_index: u64 = 0; + +// A scratch page returned for SystemMemory OperationRegion maps: the service +// cannot map arbitrary physical memory from ring 3, so such regions are +// unsupported and degrade to harmless zeros rather than faulting. The M20.2 +// targets (ps2, the legacy devices) use SystemIO and static templates. +var mmio_scratch: [4096]u8 align(4096) = .{0} ** 4096; + +fn halMapMmio(physical: u64, len: u64, writable: bool) u64 { + _ = physical; + _ = len; + _ = writable; + return @intFromPtr(&mmio_scratch); +} + +fn halPioRead(width: u8, port: u16) u32 { + return device.ioRead(node_id, io_resource_index, port, width) orelse 0; +} + +fn halPioWrite(width: u8, port: u16, value: u32) void { + _ = device.ioWrite(node_id, io_resource_index, port, width, value); +} + fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor { const total = device.enumerate(buffer); for (buffer[0..@min(total, buffer.len)]) |d| { @@ -45,22 +70,24 @@ pub fn main(init: runtime.process.Init) void { _ = runtime.system.write("acpi: no acpi-tables node to claim\n"); return; }; - if (!device.claim(node.id)) { + node_id = node.id; + if (!device.claim(node_id)) { _ = runtime.system.write("acpi: unable to claim acpi-tables\n"); return; } - // Map each memory resource (an AML blob) and collect the byte slices. The - // grant preserves each blob's sub-page offset, so the mapped pointer lands - // straight on the bytecode. + // Map each memory resource (an AML blob) and note the io_port resource. var blocks: [8][]const u8 = undefined; var block_count: usize = 0; + var found_io = false; for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| { - if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue; - const base = device.mmioMap(node.id, index) orelse { - writeLine("acpi: mmio_map failed for blob {d}\n", .{index}); + if (resource.kind == @intFromEnum(device.ResourceKind.io_port) and !found_io) { + io_resource_index = index; + found_io = true; continue; - }; + } + if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue; + const base = device.mmioMap(node_id, index) orelse continue; const pointer: [*]const u8 = @ptrFromInt(base); blocks[block_count] = pointer[0..@intCast(resource.len)]; block_count += 1; @@ -84,13 +111,238 @@ pub fn main(init: runtime.process.Init) void { } else { writeLine("acpi-parse: mismatch (ring-3 {d} vs kernel {d})\n", .{ devices, want }); } + // Self-verify mode is standalone (no manager); stop before reporting. + while (true) runtime.system.sleep(1000); } - // Registration and reports arrive in M20.2; stay resident so the claim - // holds and the service is here to grow into the supervised discoverer. + // Register + report the present _HID devices (M20.2). + var arena = std.heap.ArenaAllocator.init(runtime.allocator()); + var interpreter = aml.Interpreter.init(&namespace, .{ + .mapMmio = halMapMmio, + .pioRead = halPioRead, + .pioWrite = halPioWrite, + }, arena.allocator()); + + const manager = runtime.ipc.lookup(.device_manager); + var reported: u32 = 0; + walkDevices(namespace.root, &interpreter, manager, &reported); + writeLine("acpi: reported {d} device(s) to the manager\n", .{reported}); + + // Stay resident: the claim holds, and the service is here to grow into the + // supervised discoverer (M20.3, then the M21 event side on the SCI). while (true) runtime.system.sleep(1000); } +/// Depth-first walk: register + report each present device with a _HID, then +/// descend. Scopes (\_SB, \_GPE …) are descended without producing a node. +fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter, manager: ?runtime.ipc.Handle, reported: *u32) void { + var child = node.first_child; + while (child) |c| : (child = c.next_sibling) { + if (c.kind != .device) { + walkDevices(c, interpreter, manager, reported); + continue; + } + if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree + + if (readHid(c, interpreter)) |hid| { + // Skip PCI roots — pci-bus already reports PCI functions; ACPI adds + // only the non-PCI _HID devices (docs/m19-m20-plan.md M20.2). + if (!std.mem.eql(u8, hid[0..7], "PNP0A03") and !std.mem.eql(u8, hid[0..7], "PNP0A08")) { + registerAndReport(c, hid, interpreter, manager, reported); + } + } + walkDevices(c, interpreter, manager, reported); + } +} + +fn registerAndReport(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter, manager: ?runtime.ipc.Handle, reported: *u32) void { + var descriptor = std.mem.zeroes(device.DeviceDescriptor); + descriptor.class = @intFromEnum(device.DeviceClass.acpi_device); + descriptor.pci_class = device.no_pci_class; + const hid_len: u64 = std.mem.indexOfScalar(u8, &hid, 0) orelse hid.len; + descriptor.hid_len = hid_len; + @memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]); + applyCrs(&descriptor, node, interpreter); + + const registered = device.register(node_id, &descriptor) orelse { + writeLine("acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]}); + return; + }; + writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ hid[0..@intCast(hid_len)], registered, descriptor.resource_count }); + reported.* += 1; + + if (manager) |h| { + var report = protocol.ChildAdded{ + .parent = node_id, + .bus_address = registered, + .identity = 0, + .device_id = registered, + }; + @memcpy(report.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]); + var reply: [protocol.message_maximum]u8 = undefined; + _ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; + } +} + +/// _STA bit 0 (present); absent method or a failed evaluation is treated as +/// present, per the ACPI rules. +fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool { + const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true; + const obj = interpreter.evaluate(sta, &.{}) catch return true; + const status = obj.asInteger() catch return true; + return (status & 0x01) != 0; +} + +/// The device's EISA-decoded _HID (e.g. "PNP0303"), or null. +fn readHid(node: *aml.Node, interpreter: *aml.Interpreter) ?[8]u8 { + const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return null; + var buffer: [8]u8 = .{0} ** 8; + if (hid.kind == .method) { + const obj = interpreter.evaluate(hid, &.{}) catch return null; + switch (obj) { + .integer => |n| { + _ = eisaIdToStr(@truncate(n), &buffer); + return buffer; + }, + else => return null, + } + } + if (hid.kind != .name or hid.value.len == 0) return null; + const v = hid.value; + switch (v[0]) { + 0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => { + var p: usize = 0; + const n = readIntObj(v, &p) orelse return null; + _ = eisaIdToStr(@truncate(n), &buffer); + return buffer; + }, + else => return null, + } +} + +// --- _CRS resource-template decode (ported from the kernel's acpi.zig) -------- + +fn applyCrs(descriptor: *device.DeviceDescriptor, node: *aml.Node, interpreter: *aml.Interpreter) void { + const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return; + const obj = interpreter.evaluate(crs, &.{}) catch return; + const bytes = switch (obj) { + .buffer => |b| b, + else => return, + }; + var i: usize = 0; + while (i < bytes.len) { + const tag = bytes[i]; + if (tag & 0x80 == 0) { + const len: usize = tag & 0x07; + const body = i + 1; + if (body + len > bytes.len) break; + switch ((tag >> 3) & 0x0F) { + 0x04 => if (len >= 2) { // IRQ mask + const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8); + var b: usize = 0; + while (b < 16) : (b += 1) { + if (mask & (@as(u16, 1) << @intCast(b)) != 0) addResource(descriptor, .irq, b, 1); + } + }, + 0x08 => if (len >= 7) addResource(descriptor, .io_port, rd16(bytes, body + 1), bytes[body + 6]), + 0x09 => if (len >= 3) addResource(descriptor, .io_port, rd16(bytes, body), bytes[body + 2]), + 0x0F => break, + else => {}, + } + i = body + len; + } else { + if (i + 3 > bytes.len) break; + const len: usize = @intCast(rd16(bytes, i + 1)); + const body = i + 3; + if (body + len > bytes.len) break; + switch (tag) { + 0x85 => if (len >= 17) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 13)), + 0x86 => if (len >= 9) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 5)), + 0x89 => if (len >= 2) { + const count = bytes[body + 1]; + var k: usize = 0; + while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) { + addResource(descriptor, .irq, rd32(bytes, body + 2 + k * 4), 1); + } + }, + else => {}, + } + i = body + len; + } + } +} + +fn addResource(descriptor: *device.DeviceDescriptor, kind: device.ResourceKind, start: u64, len: u64) void { + if (descriptor.resource_count >= descriptor.resources.len) return; + descriptor.resources[@intCast(descriptor.resource_count)] = .{ .kind = @intFromEnum(kind), .start = start, .len = len }; + descriptor.resource_count += 1; +} + +// --- small helpers ported verbatim from the kernel's acpi.zig ---------------- + +fn seg4(comptime s: *const [4:0]u8) [4]u8 { + return s[0..4].*; +} + +fn hexDigit(n: u8) u8 { + return if (n < 10) '0' + n else 'A' + (n - 10); +} + +fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 { + const b0: u16 = @intCast(id & 0xFF); + const b1: u16 = @intCast((id >> 8) & 0xFF); + const b2: u8 = @truncate(id >> 16); + const b3: u8 = @truncate(id >> 24); + const mfg = (b0 << 8) | b1; + buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F)); + buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F)); + buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F)); + buffer[3] = hexDigit((b2 >> 4) & 0xF); + buffer[4] = hexDigit(b2 & 0xF); + buffer[5] = hexDigit((b3 >> 4) & 0xF); + buffer[6] = hexDigit(b3 & 0xF); + buffer[7] = 0; + return buffer[0..7]; +} + +fn readIntObj(bytes: []const u8, p: *usize) ?u64 { + if (p.* >= bytes.len) return null; + const op = bytes[p.*]; + p.* += 1; + switch (op) { + 0x00 => return 0, + 0x01 => return 1, + 0xFF => return 1, + 0x0A => { + if (p.* >= bytes.len) return null; + const v = bytes[p.*]; + p.* += 1; + return v; + }, + 0x0B => { + if (p.* + 2 > bytes.len) return null; + const v = rd16(bytes, p.*); + p.* += 2; + return v; + }, + 0x0C => { + if (p.* + 4 > bytes.len) return null; + const v = rd32(bytes, p.*); + p.* += 4; + return v; + }, + else => return null, + } +} + +fn rd16(bytes: []const u8, off: usize) u64 { + return @as(u64, bytes[off]) | (@as(u64, bytes[off + 1]) << 8); +} + +fn rd32(bytes: []const u8, off: usize) u64 { + return rd16(bytes, off) | (rd16(bytes, off + 2) << 16); +} + pub const panic = runtime.panic; comptime { _ = &runtime.start._start; // pull the runtime entry shim into the image diff --git a/system/services/device-manager/device-manager-protocol.zig b/system/services/device-manager/device-manager-protocol.zig index bf494a3..d557ec7 100644 --- a/system/services/device-manager/device-manager-protocol.zig +++ b/system/services/device-manager/device-manager-protocol.zig @@ -74,12 +74,16 @@ pub const ChildAdded = extern struct { /// Where on the bus (for USB: the root port number, 1-based). bus_address: u64, /// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI: - /// the class triple). + /// the class triple; for ACPI devices, 0 — identity is the hid below). identity: u64, /// The kernel device id this child was `device_register`ed as — what the /// 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 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. + hid: [8]u8 = .{0} ** 8, }; pub const child_added_size = @sizeOf(ChildAdded); diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 39cfc2b..83d8c0e 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -151,7 +151,7 @@ const Child = struct { reporter: u32 = 0, // the reporting driver instance's process id }; -const maximum_children = 32; +const maximum_children = 64; // ACPI adds ~34 device nodes (M20.2), plus PCI + USB var children: [maximum_children]Child = .{Child{}} ** maximum_children; /// Record (or refresh) a reported child. Refreshing matters: a restarted bus diff --git a/test/qemu_test.py b/test/qemu_test.py index 3cf74a1..20761ad 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -281,6 +281,18 @@ CASES = [ "timeout": 60, "expect": r"acpi-parse: ok", "fail": r"acpi-parse: mismatch|DANOS-TEST-RESULT: FAIL"}, + # M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers + + # reports its _HID devices — the two PS/2 nodes must appear with resources + # (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/m19-m20-plan.md). + {"name": "acpi-report", + "smp": 4, + "timeout": 60, + "qemu_extra": ["-device", "qemu-xhci,id=xhci", + "-device", "usb-kbd,bus=xhci.0", + "-device", "usb-mouse,bus=xhci.0"], + "expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*" + r"acpi: reported PNP0F13 \(device \d+, 1 resources\)", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map # grant) finds exactly the functions the kernel's own walk recorded. {"name": "pci-scan", From e6d0bb7ef0cd2885d4df6b3693386d9da130f2ee Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:32:15 +0100 Subject: [PATCH 3/4] The flip: ACPI enumeration leaves the kernel (M20.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel no longer folds AML Device objects into the device tree — the ring-3 acpi service is the sole builder of _HID device nodes. The kernel keeps building the namespace only for the \_S5 sleep type, and still seeds the static tables (MADT, HPET, MCFG, FADT) and the acpi-tables node. The device manager matches ps2-bus from the service's _HID reports (PNP0303 / PNP0F13, singleton-deduped) instead of boot-snapshot nodes; its dead boot-snapshot ps2 arm is gone. The service registers every device before reporting any, so a driver the manager spawns on the first report already sees the full set — no keyboard-before-mouse race. The acpi-ps2 scenario proves the whole chain: report -> spawn -> ps2-bus finds the controller and attaches its keyboard, entirely in ring 3. The ioport test moved to the acpi-tables I/O window, since the kernel-built PS/2 node it used to scan for no longer exists. The retired device-building functions in acpi.zig are dead but retained (a botched mechanical deletion is worse mid-migration than a follow-up sweep, which is flagged as a task). Suite 58/58. --- docs/m19-m20-plan.md | 13 ++-- system/devices/acpi.zig | 9 ++- system/kernel/tests.zig | 22 ++++--- system/services/acpi/acpi.zig | 61 ++++++++++++------- .../device-manager/device-manager.zig | 30 ++++++--- test/qemu_test.py | 12 +++- 6 files changed, 101 insertions(+), 46 deletions(-) diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index 92e1b09..5a5cbe6 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -123,11 +123,14 @@ branch is green; keep branches; push everything. the broad io grant. ChildAdded gained `hid`. Matching stays off. The `acpi-report` scenario asserts the PS/2 keyboard (3 resources) and mouse (1 resource) among the reports. Suite 57/57. -- [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static - tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers - (ps2-bus) from reports. The `input` and `device-manager` scenarios are - the assertion. discovery.md + acpi.md + device-manager.md updated; - device-manager.md increment 8 closed. +- [x] **M20.3** — the flip: the kernel's `wireAcpiDevices` call is gone (the + device-building helpers are retained-but-dead pending a focused sweep, + spawned as a task; static tables + `\_S5` + the acpi-tables node stay). + The manager matches ps2-bus from ACPI `_HID` reports; the service + registers all devices before reporting any (no keyboard-before-mouse + race). The `acpi-ps2` scenario proves report → spawn → ps2-bus attaches + its keyboard; `ioport` retargeted to the acpi-tables I/O window (the + kernel-built PS/2 node is gone). Suite 58/58. - [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**. --- diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index e4318bc..abf5015 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -405,8 +405,13 @@ pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryR aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total }; power_information.s5 = aml.sleepState(&namespace.?, 5); power_information.s3 = aml.sleepState(&namespace.?, 3); - // Fold the namespace's Device objects into the generic tree. - wireAcpiDevices(device_tree, &namespace.?, hal) catch {}; + // The namespace's Device objects are no longer folded into the kernel + // tree (M20.3): the ring-3 acpi service claims the acpi-tables node + // (published below), re-parses the same blobs, and registers + reports + // the _HID devices itself. The kernel keeps the namespace only for the + // \_S5 sleep type above. The device-building helpers below + // (wireAcpiDevices and friends) are retained but unreferenced — a + // focused dead-code sweep follows the migration. } else |_| { // AML parse failed (e.g. out of memory); power stays best-effort with // whatever the FADT alone provided. diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 9145d20..b2a1398 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -150,6 +150,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { acpiParseTest(boot_information); } else if (eql(case, "acpi-report")) { acpiReportTest(boot_information); + } else if (eql(case, "acpi-ps2")) { + acpiReportTest(boot_information); // same spawn; the harness regex differs } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1134,12 +1136,18 @@ fn ioPortTest() void { var buffer: [64]device_abi.DeviceDescriptor = undefined; const n = @min(devices_broker.enumerate(&buffer), buffer.len); + // Post-M20.3 the PS/2 node is registered at runtime by the ring-3 acpi + // service, so it is absent from this boot snapshot. Exercise the same + // io_port claim/resolve mechanism against the acpi-tables node's broad I/O + // grant — the window that now carries port authority (the service uses it + // for exactly this). The PS/2 status port 0x64 is offset 0x64 within it. var found_id: ?u64 = null; var found_res: u64 = 0; outer: for (buffer[0..n]) |d| { + if (d.class != @intFromEnum(device_abi.DeviceClass.acpi_tables)) continue; for (0..d.resource_count) |ri| { const r = d.resources[ri]; - if (r.kind == @intFromEnum(device_abi.ResourceKind.io_port) and r.start == 0x64 and r.len >= 1) { + if (r.kind == @intFromEnum(device_abi.ResourceKind.io_port) and r.start == 0 and r.len > 0x64) { found_id = d.id; found_res = ri; break :outer; @@ -1147,18 +1155,18 @@ fn ioPortTest() void { } } const id = found_id orelse { - check("discovered the PS/2 status port (io_port 0x64)", false); + check("discovered the acpi-tables I/O window", false); result(); return; }; - check("discovered the PS/2 status port (io_port 0x64)", true); + check("discovered the acpi-tables I/O window", true); const me = scheduler.current(); check("claimed the io_port device", devices_broker.claim(id, me.id)); - check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0, 1) == 0x64); - check("an over-wide access is refused", process.resolveIoPort(me, id, found_res, 0, 2) == null); - check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 1, 1) == null); - check("an unclaimed device id is refused", process.resolveIoPort(me, 0xDEAD_BEEF, found_res, 0, 1) == null); + check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0x64, 1) == 0x64); + check("a 4-byte access at the last port is refused", process.resolveIoPort(me, id, found_res, 0xFFFF, 4) == null); + check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 0x10000, 1) == null); + check("an unclaimed device id is refused", process.resolveIoPort(me, 0xDEAD_BEEF, found_res, 0x64, 1) == null); // The kernel actually issues the `in`. Reaching this line at all proves it didn't // fault; a width-1 read must return a single byte. diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index f343050..bebaf6c 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -28,6 +28,11 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void { var node_id: u64 = 0; var io_resource_index: u64 = 0; +// Pass-1 registration record (see main): what pass 2 reports. +const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 }; +var registered: [64]Registered = undefined; +var registered_count: usize = 0; + // A scratch page returned for SystemMemory OperationRegion maps: the service // cannot map arbitrary physical memory from ring 3, so such regions are // unsupported and degrade to harmless zeros rather than faulting. The M20.2 @@ -123,10 +128,31 @@ pub fn main(init: runtime.process.Init) void { .pioWrite = halPioWrite, }, arena.allocator()); + // Pass 1: register every present _HID device under acpi-tables, remembering + // each (hid, device id). Pass 2: report them all. Registering before any + // report reaches the manager means a driver it spawns on the first report + // already sees the whole set (no keyboard-before-mouse race for ps2-bus). + registered_count = 0; + walkDevices(namespace.root, &interpreter); + const manager = runtime.ipc.lookup(.device_manager); - var reported: u32 = 0; - walkDevices(namespace.root, &interpreter, manager, &reported); - writeLine("acpi: reported {d} device(s) to the manager\n", .{reported}); + var i: usize = 0; + while (i < registered_count) : (i += 1) { + const entry = registered[i]; + writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ entry.hid[0..entry.hid_len], entry.device_id, entry.resource_count }); + if (manager) |h| { + var report = protocol.ChildAdded{ + .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: [protocol.message_maximum]u8 = undefined; + _ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; + } + } + writeLine("acpi: reported {d} device(s) to the manager\n", .{registered_count}); // Stay resident: the claim holds, and the service is here to grow into the // supervised discoverer (M20.3, then the M21 event side on the SCI). @@ -135,11 +161,11 @@ pub fn main(init: runtime.process.Init) void { /// Depth-first walk: register + report each present device with a _HID, then /// descend. Scopes (\_SB, \_GPE …) are descended without producing a node. -fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter, manager: ?runtime.ipc.Handle, reported: *u32) void { +fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter) void { var child = node.first_child; while (child) |c| : (child = c.next_sibling) { if (c.kind != .device) { - walkDevices(c, interpreter, manager, reported); + walkDevices(c, interpreter); continue; } if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree @@ -148,14 +174,15 @@ fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter, manager: ?runtime // Skip PCI roots — pci-bus already reports PCI functions; ACPI adds // only the non-PCI _HID devices (docs/m19-m20-plan.md M20.2). if (!std.mem.eql(u8, hid[0..7], "PNP0A03") and !std.mem.eql(u8, hid[0..7], "PNP0A08")) { - registerAndReport(c, hid, interpreter, manager, reported); + registerDevice(c, hid, interpreter); } } - walkDevices(c, interpreter, manager, reported); + walkDevices(c, interpreter); } } -fn registerAndReport(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter, manager: ?runtime.ipc.Handle, reported: *u32) void { +fn registerDevice(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter) void { + if (registered_count >= registered.len) return; var descriptor = std.mem.zeroes(device.DeviceDescriptor); descriptor.class = @intFromEnum(device.DeviceClass.acpi_device); descriptor.pci_class = device.no_pci_class; @@ -164,24 +191,12 @@ fn registerAndReport(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter, @memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]); applyCrs(&descriptor, node, interpreter); - const registered = device.register(node_id, &descriptor) orelse { + const id = device.register(node_id, &descriptor) orelse { writeLine("acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]}); return; }; - writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ hid[0..@intCast(hid_len)], registered, descriptor.resource_count }); - reported.* += 1; - - if (manager) |h| { - var report = protocol.ChildAdded{ - .parent = node_id, - .bus_address = registered, - .identity = 0, - .device_id = registered, - }; - @memcpy(report.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]); - var reply: [protocol.message_maximum]u8 = undefined; - _ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; - } + registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count }; + registered_count += 1; } /// _STA bit 0 (present); absent method or a failed evaluation is treated as diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 83d8c0e..2d4338a 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -34,15 +34,11 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void { /// this comes from a manifest (docs/device-manager.md: the third bus type /// triggers it); for now a static map. `null` = no driver for this class yet. fn driverFor(d: device.DeviceDescriptor) ?[]const u8 { - // detect device via DeviceClass + // The HPET timer node is still kernel-seeded (from the HPET table, not AML). + // PS/2 and other _HID devices now arrive as acpi-service reports and match + // in onChildAdded (M20.3), not from this boot snapshot. if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet"; - // detect device via hid - const hid = d.hid[0..@intCast(d.hid_len)]; - const id = acpi_ids.HardwareId.fromHid(hid) orelse return null; - return switch (id) { - .ps2_keyboard, .ps2_mouse => "ps2-bus", - else => null, - }; + return null; } /// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller: @@ -61,6 +57,16 @@ fn pciDriverForIdentity(identity: u64) ?[]const u8 { }; } +/// 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 "ps2-bus"; // PS/2 keyboard + if (std.mem.eql(u8, hid, "PNP0F13")) return "ps2-bus"; // PS/2 mouse + return null; +} + /// Whether some driver entry already serves registered device `device_id` — /// a re-report after a bus restart must not spawn a second instance. fn driverForDevice(device_id: u64) bool { @@ -415,6 +421,14 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { if (pciDriverForIdentity(report.identity)) |child_driver| { if (!driverForDevice(report.device_id)) addDriver(child_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, protocol.no_device, false); + } + } } } else { status = -1; diff --git a/test/qemu_test.py b/test/qemu_test.py index 20761ad..bd30af4 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -281,12 +281,22 @@ CASES = [ "timeout": 60, "expect": r"acpi-parse: ok", "fail": r"acpi-parse: mismatch|DANOS-TEST-RESULT: FAIL"}, + # M20.3: the flip — ps2-bus now comes up from the acpi service's report, not + # a kernel-built node. Ordered: report -> spawn -> the driver attaches its + # keyboard, proving discovery runs entirely in ring 3 (docs/m19-m20-plan.md). + {"name": "acpi-ps2", + "smp": 4, + "timeout": 150, + "expect": r"acpi: reported PNP0303[\s\S]*" + r"device-manager: spawned ps2-bus[\s\S]*" + r"ps2-bus: keyboard driver attached", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers + # reports its _HID devices — the two PS/2 nodes must appear with resources # (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/m19-m20-plan.md). {"name": "acpi-report", "smp": 4, - "timeout": 60, + "timeout": 150, "qemu_extra": ["-device", "qemu-xhci,id=xhci", "-device", "usb-kbd,bus=xhci.0", "-device", "usb-mouse,bus=xhci.0"], From 0628944b1560d9308ded23a46be4801e4ded6089 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:32:37 +0100 Subject: [PATCH 4/4] Docs: close the discovery migration (M20.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit discovery.md records ACPI enumeration leaving the kernel; device-manager.md increment 8 marked done — enumeration now runs entirely in ring 3. --- docs/device-manager.md | 6 ++++-- docs/discovery.md | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/device-manager.md b/docs/device-manager.md index b28d998..2b50bc9 100644 --- a/docs/device-manager.md +++ b/docs/device-manager.md @@ -136,8 +136,10 @@ published exit events, signals + `runtime.process`). On top of those: the mouse and keyboard QEMU already hangs off it. 7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats to a manager-internal seam. -8. **Discovery migration**: pci-bus driver first, acpi service second, kernel scan - retired last. (AML-in-user-space is its own track.) +8. **Discovery migration** — DONE (M19–M20, 2026-07-13): pci-bus driver (M19) + then the acpi service (M20) moved enumeration to ring 3; the kernel seeds + only the host bridge and the acpi-tables node. See + [m19-m20-plan.md](m19-m20-plan.md). ## Settled questions (2026-07-12) diff --git a/docs/discovery.md b/docs/discovery.md index 6dad24b..94a0579 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -177,3 +177,17 @@ window). The per-function walk moved to the ring-3 `pci-bus` driver ECAM scan through its mmio grant, and `device_register`s what it finds, which the device manager mirrors and matches. The ACPI namespace walk follows in M20; the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side. + +## Update (M20.3, 2026-07-13): ACPI enumeration left the kernel too + +The kernel no longer folds the AML namespace's Device objects into the device +tree. It still parses the *static* tables (MADT for SMP, HPET for the tick, MCFG +for the host bridge, FADT) and still builds the AML namespace — but only to read +the `\\_S5` sleep type for poweroff. Device discovery is the ring-3 **acpi +service** ([device-manager.md](device-manager.md)): it claims the `acpi-tables` +node the kernel publishes (the AML blobs, a broad io_port grant, the SCI), +re-parses the same blobs with the shared AML module, evaluates `_STA`/`_CRS`, +and registers + reports each `_HID` device — the device manager matches drivers +(ps2-bus) from those reports. With M19's pci-bus driver, discovery now runs +entirely in user space; the kernel seeds only the host bridge and the +acpi-tables node.