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/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. diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index b98979b..5a5cbe6 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -106,23 +106,31 @@ 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. -- [ ] **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. -- [ ] **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.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. + +- [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. +- [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 8e008db..abf5015 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, @@ -402,12 +405,57 @@ 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. } + + // 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); + // 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. +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 +718,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 +736,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/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 47a6596..b2a1398 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -146,6 +146,12 @@ 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, "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")) { @@ -1130,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; @@ -1143,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. @@ -1912,6 +1924,81 @@ 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 +/// 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..bebaf6c 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -1,27 +1,361 @@ //! /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, a broad irq window, the SCI), and runs the **shared AML module** in +//! ring 3 — the same parser and interpreter the kernel uses. //! -//! 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.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; -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); +} + +// 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; + +// 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 +// 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| { + 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; + }; + 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 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.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; + 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 }); + } + // Self-verify mode is standalone (no manager); stop before reporting. + while (true) runtime.system.sleep(1000); + } + + // 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()); + + // 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 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). + 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) void { + var child = node.first_child; + while (child) |c| : (child = c.next_sibling) { + if (c.kind != .device) { + walkDevices(c, interpreter); + 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")) { + registerDevice(c, hid, interpreter); + } + } + walkDevices(c, interpreter); + } +} + +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; + 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 id = device.register(node_id, &descriptor) orelse { + writeLine("acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]}); + return; + }; + 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 +/// 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; 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 ed2b4cb..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 { @@ -151,7 +157,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 @@ -347,6 +353,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, @@ -409,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/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..bd30af4 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -274,6 +274,35 @@ 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"}, + # 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": 150, + "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",