Merge feat/acpi-service: ACPI interpretation in ring 3 (M20)

The AML interpreter as a shared build module, the acpi-tables node, the
acpi service (parse, evaluate _CRS/_STA, register + report), and the flip
that retired the kernel's ACPI device build — discovery's second and final
subsystem to leave ring 0.
This commit is contained in:
Daniel Samson
2026-07-13 03:32:54 +01:00
15 changed files with 651 additions and 59 deletions
+8
View File
@@ -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 // ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
// nodes. Also shared reference data. // 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", .{ const acpi_ids_module = b.addModule("acpi-ids", .{
.root_source_file = b.path("system/devices/acpi-ids.zig"), .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", .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); 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"); 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 // 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. // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
+4 -2
View File
@@ -136,8 +136,10 @@ published exit events, signals + `runtime.process`). On top of those:
the mouse and keyboard QEMU already hangs off it. the mouse and keyboard QEMU already hangs off it.
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats 7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
to a manager-internal seam. to a manager-internal seam.
8. **Discovery migration**: pci-bus driver first, acpi service second, kernel scan 8. **Discovery migration** — DONE (M19–M20, 2026-07-13): pci-bus driver (M19)
retired last. (AML-in-user-space is its own track.) 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) ## Settled questions (2026-07-12)
+14
View File
@@ -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 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 device manager mirrors and matches. The ACPI namespace walk follows in M20;
the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side. 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.
+25 -17
View File
@@ -106,23 +106,31 @@ branch is green; keep branches; push everything.
skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart
hammered 6×). hammered 6×).
- [x] **merge** `feat/pci-bus` → main, push (merged 2026-07-13). - [x] **merge** `feat/pci-bus` → main, push (merged 2026-07-13).
- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at - [x] **M20.1** — acpi service, parse only: the AML interpreter is now a build
system/services/acpi/acpi.zig): kernel publishes `acpi-tables` module compiled into both kernel and service; the kernel publishes the
(decision 5) — memory over the table blobs, the broad io_port grant, and `acpi-tables` node (AML blobs as memory resources, the broad io_port grant,
**the SCI as an irq resource** (from the FADT; unused until M21 but free the SCI); the service claims it, maps the blobs, runs the shared parser in
to record now). The service claims it, maps the blobs, runs the shared ring 3, and self-verifies its Device count against the kernel's (34 = 34,
AML module in ring 3, logs the namespace device count and `_HID`s. deterministic via argv, no log-scraping); the manager spawns `discovery`
Scenario `acpi-parse`: user-space count equals the kernel walk's count. 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 - [x] **M20.2** — register + report: the service evaluates `_STA`/`_CRS` in
holes give containment), reported to the manager. Spawn-from-reports for ring 3 (interpreter Hal = port I/O over the claimed node; a scratch page
ACPI matches stays off. Scenario: the reported set includes the PS/2 backs SystemMemory maps so a stray region can't fault it) and registers +
keyboard and mouse nodes with their IRQ resources. reports each present `_HID` device under `acpi-tables`. Containment: the
- [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static broker's irq check became range-based (len-1 == the old equality) so the
tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers node's broad irq window covers children's legacy lines; io ports fall in
(ps2-bus) from reports. The `input` and `device-manager` scenarios are the broad io grant. ChildAdded gained `hid`. Matching stays off. The
the assertion. discovery.md + acpi.md + device-manager.md updated; `acpi-report` scenario asserts the PS/2 keyboard (3 resources) and mouse
device-manager.md increment 8 closed. (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**. - [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**.
--- ---
+52 -2
View File
@@ -41,6 +41,9 @@ pub const RegisterAccess = struct {
/// Everything the power subsystem needs, extracted from the FADT and the AML /// Everything the power subsystem needs, extracted from the FADT and the AML
/// sleep packages during discovery. Populated by `discover`, read by `power`. /// sleep packages during discovery. Populated by `discover`, read by `power`.
pub const PowerInformation = struct { 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. /// The SMM command port and the value that switches the platform into ACPI mode.
smi_cmd: u16 = 0, smi_cmd: u16 = 0,
acpi_enable: u8 = 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 }; aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
power_information.s5 = aml.sleepState(&namespace.?, 5); power_information.s5 = aml.sleepState(&namespace.?, 5);
power_information.s3 = aml.sleepState(&namespace.?, 3); power_information.s3 = aml.sleepState(&namespace.?, 3);
// Fold the namespace's Device objects into the generic tree. // The namespace's Device objects are no longer folded into the kernel
wireAcpiDevices(device_tree, &namespace.?, hal) catch {}; // 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 |_| { } else |_| {
// AML parse failed (e.g. out of memory); power stays best-effort with // AML parse failed (e.g. out of memory); power stays best-effort with
// whatever the FADT alone provided. // 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 /// 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_pm1b_cnt_blk = 68; // u32 (I/O port)
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
const fadt_pm1_cnt_len = 89; // u8 (bytes) 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_flags = 112; // u32
const fadt_reset_register = 116; // GAS (12 bytes) const fadt_reset_register = 116; // GAS (12 bytes)
const fadt_reset_value = 128; // u8 const fadt_reset_value = 128; // u8
@@ -687,6 +736,7 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void {
const len: usize = header.length; const len: usize = header.length;
const pi = &power_information; 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.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_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0; pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
+14
View File
@@ -50,6 +50,20 @@ pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseRes
return .{ .namespace = namespace, .consumed = consumed, .total = total }; 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 /// 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. /// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType { pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
+5
View File
@@ -28,6 +28,11 @@ pub const DeviceClass = enum(u32) {
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a /// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`). /// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
acpi_device, 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, unknown,
}; };
+7
View File
@@ -40,6 +40,13 @@ pub fn platformInformation() PlatformInformation {
} }
/// AML parse integrity/diagnostics (namespace node count, bytes consumed). /// 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 { pub fn amlStats() AmlStats {
return acpi.aml_stats; return acpi.aml_stats;
} }
+10 -1
View File
@@ -131,7 +131,16 @@ pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
/// and would otherwise vacuously "fit" anywhere. /// and would otherwise vacuously "fit" anywhere.
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool { fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
if (parent.kind != child.kind) return false; 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; if (child.len == 0 or parent.len == 0) return false;
// No overflow: a resource that wraps the address space is not containable. // 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; const child_end = std.math.add(u64, child.start, child.len) catch return false;
+94 -7
View File
@@ -146,6 +146,12 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
deviceListTest(boot_information); deviceListTest(boot_information);
} else if (eql(case, "pci-scan")) { } else if (eql(case, "pci-scan")) {
pciScanTest(boot_information); 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")) { } else if (eql(case, "initial-ramdisk")) {
initialRamdiskTest(boot_information); initialRamdiskTest(boot_information);
} else if (eql(case, "vfs")) { } else if (eql(case, "vfs")) {
@@ -1130,12 +1136,18 @@ fn ioPortTest() void {
var buffer: [64]device_abi.DeviceDescriptor = undefined; var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len); 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_id: ?u64 = null;
var found_res: u64 = 0; var found_res: u64 = 0;
outer: for (buffer[0..n]) |d| { outer: for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.acpi_tables)) continue;
for (0..d.resource_count) |ri| { for (0..d.resource_count) |ri| {
const r = d.resources[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_id = d.id;
found_res = ri; found_res = ri;
break :outer; break :outer;
@@ -1143,18 +1155,18 @@ fn ioPortTest() void {
} }
} }
const id = found_id orelse { 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(); result();
return; 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(); const me = scheduler.current();
check("claimed the io_port device", devices_broker.claim(id, me.id)); 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 in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0x64, 1) == 0x64);
check("an over-wide access is refused", process.resolveIoPort(me, id, found_res, 0, 2) == null); 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, 1, 1) == 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, 0, 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 // 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. // fault; a width-1 read must return a single byte.
@@ -1912,6 +1924,81 @@ fn pciScanTest(boot_information: *const BootInformation) void {
result(); 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, /// 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 /// which — entirely from ring 3 — creates an exit endpoint, spawns its two
/// children supervised, sees them in process_enumerate, kills them (one blocked, /// children supervised, sees them in process_enumerate, kills them (one blocked,
+353 -19
View File
@@ -1,27 +1,361 @@
//! /system/services/acpi — the ACPI discovery service: the x86 firmware //! /system/services/acpi — the ACPI discovery service: the x86 firmware
//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). **Placeholder: //! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). Claims the
//! not implemented until M20.1** — it exists so the build's `-Ddiscovery` //! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port
//! option has both of its values and the ramdisk's neutral `discovery` slot is //! grant, a broad irq window, the SCI), and runs the **shared AML module** in
//! wired before the implementation lands. //! ring 3 — the same parser and interpreter the kernel uses.
//! //!
//! What it becomes (the plan's decisions 5 and 7): claim the `acpi-tables` //! M20.2 (this increment): after parsing, walk the namespace and, for each
//! node the kernel publishes (table blobs + the broad io_port grant + the SCI), //! present Device with a hardware id (`_HID`), evaluate its current resource
//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal` //! settings (`_CRS`) through a ring-3 `Hal` (port I/O over the claimed node),
//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell //! register it under the acpi-tables node (its I/O ports and IRQs contained by
//! it moved. Then the bus-driver shape: `device_register` the namespace //! the node's broad grants), and report it to the device manager with its
//! devices (`_HID`, `_CRS` resources, containment against the node's //! EISA-decoded hid as identity. Matching those reports to drivers (ps2-bus)
//! apertures), report each to the device manager, stay resident under its //! and retiring the kernel's own device build follow in M20.3.
//! 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.
const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const aml = @import("aml");
const device = runtime.device;
const protocol = runtime.device_manager_protocol;
pub fn main() void { fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
// Not implemented: exit cleanly and silently (a bare spawn by the var line: [128]u8 = undefined;
// initial-ramdisk sweep must not derange other tests' markers). The _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
// supervisor reads a clean exit as "meant to stop" — correct for a }
// placeholder.
// 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; pub const panic = runtime.panic;
@@ -74,12 +74,16 @@ pub const ChildAdded = extern struct {
/// Where on the bus (for USB: the root port number, 1-based). /// Where on the bus (for USB: the root port number, 1-based).
bus_address: u64, bus_address: u64,
/// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI: /// 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, identity: u64,
/// The kernel device id this child was `device_register`ed as — what the /// 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` /// manager hands a matched driver as its argv assignment — or `no_device`
/// for an unregistered leaf (a USB port before the descriptor track). /// for an unregistered leaf (a USB port before the descriptor track).
device_id: u64 = no_device, 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); pub const child_added_size = @sizeOf(ChildAdded);
@@ -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 /// 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. /// triggers it); for now a static map. `null` = no driver for this class yet.
fn driverFor(d: device.DeviceDescriptor) ?[]const u8 { 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"; if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
// detect device via hid return null;
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,
};
} }
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller: /// 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` — /// Whether some driver entry already serves registered device `device_id` —
/// a re-report after a bus restart must not spawn a second instance. /// a re-report after a bus restart must not spawn a second instance.
fn driverForDevice(device_id: u64) bool { fn driverForDevice(device_id: u64) bool {
@@ -151,7 +157,7 @@ const Child = struct {
reporter: u32 = 0, // the reporting driver instance's process id 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; var children: [maximum_children]Child = .{Child{}} ** maximum_children;
/// Record (or refresh) a reported child. Refreshing matters: a restarted bus /// 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) { if (test_restart_mode) {
// The driver-restart scenario's fixture: claims device 0 (the tree // The driver-restart scenario's fixture: claims device 0 (the tree
// root, otherwise unclaimed), hellos, then faults — driving backoff, // 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 (pciDriverForIdentity(report.identity)) |child_driver| {
if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true); 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 { } else {
status = -1; status = -1;
+2 -1
View File
@@ -21,7 +21,8 @@
const runtime = @import("runtime"); 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 // Not implemented: exit cleanly and silently (a bare spawn by the
// initial-ramdisk sweep must not derange other tests' markers). The // initial-ramdisk sweep must not derange other tests' markers). The
// supervisor reads a clean exit as "meant to stop" — correct for a // supervisor reads a clean exit as "meant to stop" — correct for a
+29
View File
@@ -274,6 +274,35 @@ CASES = [
r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: restarting usb-xhci-bus[\s\S]*"
r"device-manager: child added", r"device-manager: child added",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "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 # 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. # grant) finds exactly the functions the kernel's own walk recorded.
{"name": "pci-scan", {"name": "pci-scan",