From 5ca804d827a36009bdc0686809f40549c44c1507 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:19:39 +0100 Subject: [PATCH] The acpi service evaluates _CRS/_STA in ring 3 and reports devices (M20.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AML method evaluation now runs in userspace touching real hardware: the service builds an interpreter with a ring-3 Hal (port I/O routed through its claimed acpi-tables node; a scratch page backs SystemMemory maps so a stray OperationRegion degrades to zeros instead of faulting a process that cannot map arbitrary physical memory). It walks the namespace and, for each present _HID device that is not a PCI root, evaluates _CRS, registers it under acpi-tables, and reports it with its EISA-decoded hid. Containment for this needed the broker's irq check to become range-based — an interrupt line is still indivisible, but a parent may own a range, so the acpi-tables node's broad irq window contains its children's legacy lines (a length-1 range is exactly the old equality, so single-irq parents are unaffected). ChildAdded gained a hid field for firmware string identity. Matching those reports to drivers stays off until M20.3, so ps2-bus still comes up via the kernel path — no regression. The acpi-report scenario proves the PS/2 keyboard (io 0x60/0x64 + IRQ) and mouse (IRQ) are reported with their resources. Suite 57/57. --- docs/m19-m20-plan.md | 14 +- system/devices/acpi.zig | 6 +- system/kernel/devices-broker.zig | 11 +- system/kernel/tests.zig | 34 ++ system/services/acpi/acpi.zig | 294 ++++++++++++++++-- .../device-manager-protocol.zig | 6 +- .../device-manager/device-manager.zig | 2 +- test/qemu_test.py | 12 + 8 files changed, 349 insertions(+), 30 deletions(-) diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index 1bc05aa..92e1b09 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -114,11 +114,15 @@ branch is green; keep branches; push everything. deterministic via argv, no log-scraping); the manager spawns `discovery` at startup. Parse-only touches no hardware. Suite 56/56. -- [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS` - resources registered under `acpi-tables` (its io_port + the memory-map - holes give containment), reported to the manager. Spawn-from-reports for - ACPI matches stays off. Scenario: the reported set includes the PS/2 - keyboard and mouse nodes with their IRQ resources. +- [x] **M20.2** — register + report: the service evaluates `_STA`/`_CRS` in + ring 3 (interpreter Hal = port I/O over the claimed node; a scratch page + backs SystemMemory maps so a stray region can't fault it) and registers + + reports each present `_HID` device under `acpi-tables`. Containment: the + broker's irq check became range-based (len-1 == the old equality) so the + node's broad irq window covers children's legacy lines; io ports fall in + the broad io grant. ChildAdded gained `hid`. Matching stays off. The + `acpi-report` scenario asserts the PS/2 keyboard (3 resources) and mouse + (1 resource) among the reports. Suite 57/57. - [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers (ps2-bus) from reports. The `input` and `device-manager` scenarios are diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index e7f78d4..e4318bc 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -439,8 +439,12 @@ fn publishAcpiTablesNode(device_tree: *DeviceTree) !void { // that names them is parsed, so the grant is the whole space — the honest // trust boundary of docs/m19-m20-plan.md decision 5. _ = node.addResource(.io_port, 0, 1 << 16); - // The SCI (M21 events); harmless to record now. + // A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines + // 1 and 12, the RTC, …), and the service registers those devices under this + // node, so it must own a superset. The range [0, 256) covers every GSI; the + // SCI (recorded first, len 1) stays distinct so M21 can pick it out. if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1); + _ = node.addResource(.irq, 0, 256); } /// The number of Device objects in the namespace built during discovery, or 0. diff --git a/system/kernel/devices-broker.zig b/system/kernel/devices-broker.zig index 8460c4e..1bd7c75 100644 --- a/system/kernel/devices-broker.zig +++ b/system/kernel/devices-broker.zig @@ -131,7 +131,16 @@ pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor { /// and would otherwise vacuously "fit" anywhere. fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool { if (parent.kind != child.kind) return false; - if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) return parent.start == child.start; + if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) { + // Range containment: an interrupt line is still indivisible (a child owns + // exactly one GSI), but a parent may own a *range* of lines so a broad + // owner — the acpi-tables node, whose firmware names any legacy IRQ — + // can contain its children's specific lines. A length-1 parent range is + // exactly the old equality rule, so existing single-IRQ parents are + // unaffected. + const span = if (parent.len == 0) 1 else parent.len; + return child.start >= parent.start and child.start < parent.start + span; + } if (child.len == 0 or parent.len == 0) return false; // No overflow: a resource that wraps the address space is not containable. const child_end = std.math.add(u64, child.start, child.len) catch return false; diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index fd4a764..9145d20 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -148,6 +148,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { pciScanTest(boot_information); } else if (eql(case, "acpi-parse")) { acpiParseTest(boot_information); + } else if (eql(case, "acpi-report")) { + acpiReportTest(boot_information); } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1914,6 +1916,38 @@ fn pciScanTest(boot_information: *const BootInformation) void { result(); } +/// M20.2: the acpi service registers + reports its _HID devices. Boot normally +/// (the manager spawns discovery); the harness's expect regex requires the two +/// PS/2 nodes among the service's report lines, each with its _CRS resources — +/// the ring-3 _CRS/_STA evaluation working end to end. The kernel test only +/// starts the manager. +fn acpiReportTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: acpi-report\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + process.setInitialRamdisk(image); + var spawned = false; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "device-manager")) continue; + _ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0; + spawned = true; + break; + } + check("device-manager spawned", spawned); + result(); +} + /// M20.1: the ring-3 AML parse agrees with the kernel's. The manager spawns /// the discovery service (the acpi build variant); it claims the acpi-tables /// node, maps the blobs, parses them, and logs its Device count — which must diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 67c9fc9..f343050 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -1,29 +1,54 @@ //! /system/services/acpi — the ACPI discovery service: the x86 firmware //! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). Claims the //! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port -//! grant, the SCI), and runs the **shared AML module** in ring 3 — the same -//! parser the kernel uses for `\_S5`, now the sole builder of the device -//! namespace. +//! grant, a broad irq window, the SCI), and runs the **shared AML module** in +//! ring 3 — the same parser and interpreter the kernel uses. //! -//! M20.1 (this increment): claim the node, map each AML blob through the -//! ordinary mmio grant, parse them into a namespace, and log the Device count — -//! which the `acpi-parse` scenario checks equals the kernel's own parse. -//! Parsing touches no hardware (the io_port grant and the interpreter's -//! OperationRegion evaluation come in with `_CRS`/`_STA` at M20.2). Registering -//! and reporting the namespace devices, and retiring the kernel's device build, -//! follow in M20.2 and M20.3. +//! M20.2 (this increment): after parsing, walk the namespace and, for each +//! present Device with a hardware id (`_HID`), evaluate its current resource +//! settings (`_CRS`) through a ring-3 `Hal` (port I/O over the claimed node), +//! register it under the acpi-tables node (its I/O ports and IRQs contained by +//! the node's broad grants), and report it to the device manager with its +//! EISA-decoded hid as identity. Matching those reports to drivers (ps2-bus) +//! and retiring the kernel's own device build follow in M20.3. const std = @import("std"); const runtime = @import("runtime"); const aml = @import("aml"); const device = runtime.device; +const protocol = runtime.device_manager_protocol; fn writeLine(comptime fmt: []const u8, arguments: anytype) void { var line: [128]u8 = undefined; _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); } -/// Find the acpi-tables node the kernel published, or null. +// The claimed acpi-tables node and the resource index of its broad io_port +// window — the Hal routes every port access through this one claim. +var node_id: u64 = 0; +var io_resource_index: u64 = 0; + +// A scratch page returned for SystemMemory OperationRegion maps: the service +// cannot map arbitrary physical memory from ring 3, so such regions are +// unsupported and degrade to harmless zeros rather than faulting. The M20.2 +// targets (ps2, the legacy devices) use SystemIO and static templates. +var mmio_scratch: [4096]u8 align(4096) = .{0} ** 4096; + +fn halMapMmio(physical: u64, len: u64, writable: bool) u64 { + _ = physical; + _ = len; + _ = writable; + return @intFromPtr(&mmio_scratch); +} + +fn halPioRead(width: u8, port: u16) u32 { + return device.ioRead(node_id, io_resource_index, port, width) orelse 0; +} + +fn halPioWrite(width: u8, port: u16, value: u32) void { + _ = device.ioWrite(node_id, io_resource_index, port, width, value); +} + fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor { const total = device.enumerate(buffer); for (buffer[0..@min(total, buffer.len)]) |d| { @@ -45,22 +70,24 @@ pub fn main(init: runtime.process.Init) void { _ = runtime.system.write("acpi: no acpi-tables node to claim\n"); return; }; - if (!device.claim(node.id)) { + node_id = node.id; + if (!device.claim(node_id)) { _ = runtime.system.write("acpi: unable to claim acpi-tables\n"); return; } - // Map each memory resource (an AML blob) and collect the byte slices. The - // grant preserves each blob's sub-page offset, so the mapped pointer lands - // straight on the bytecode. + // Map each memory resource (an AML blob) and note the io_port resource. var blocks: [8][]const u8 = undefined; var block_count: usize = 0; + var found_io = false; for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| { - if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue; - const base = device.mmioMap(node.id, index) orelse { - writeLine("acpi: mmio_map failed for blob {d}\n", .{index}); + if (resource.kind == @intFromEnum(device.ResourceKind.io_port) and !found_io) { + io_resource_index = index; + found_io = true; continue; - }; + } + if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue; + const base = device.mmioMap(node_id, index) orelse continue; const pointer: [*]const u8 = @ptrFromInt(base); blocks[block_count] = pointer[0..@intCast(resource.len)]; block_count += 1; @@ -84,13 +111,238 @@ pub fn main(init: runtime.process.Init) void { } else { writeLine("acpi-parse: mismatch (ring-3 {d} vs kernel {d})\n", .{ devices, want }); } + // Self-verify mode is standalone (no manager); stop before reporting. + while (true) runtime.system.sleep(1000); } - // Registration and reports arrive in M20.2; stay resident so the claim - // holds and the service is here to grow into the supervised discoverer. + // Register + report the present _HID devices (M20.2). + var arena = std.heap.ArenaAllocator.init(runtime.allocator()); + var interpreter = aml.Interpreter.init(&namespace, .{ + .mapMmio = halMapMmio, + .pioRead = halPioRead, + .pioWrite = halPioWrite, + }, arena.allocator()); + + const manager = runtime.ipc.lookup(.device_manager); + var reported: u32 = 0; + walkDevices(namespace.root, &interpreter, manager, &reported); + writeLine("acpi: reported {d} device(s) to the manager\n", .{reported}); + + // Stay resident: the claim holds, and the service is here to grow into the + // supervised discoverer (M20.3, then the M21 event side on the SCI). while (true) runtime.system.sleep(1000); } +/// Depth-first walk: register + report each present device with a _HID, then +/// descend. Scopes (\_SB, \_GPE …) are descended without producing a node. +fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter, manager: ?runtime.ipc.Handle, reported: *u32) void { + var child = node.first_child; + while (child) |c| : (child = c.next_sibling) { + if (c.kind != .device) { + walkDevices(c, interpreter, manager, reported); + continue; + } + if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree + + if (readHid(c, interpreter)) |hid| { + // Skip PCI roots — pci-bus already reports PCI functions; ACPI adds + // only the non-PCI _HID devices (docs/m19-m20-plan.md M20.2). + if (!std.mem.eql(u8, hid[0..7], "PNP0A03") and !std.mem.eql(u8, hid[0..7], "PNP0A08")) { + registerAndReport(c, hid, interpreter, manager, reported); + } + } + walkDevices(c, interpreter, manager, reported); + } +} + +fn registerAndReport(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter, manager: ?runtime.ipc.Handle, reported: *u32) void { + var descriptor = std.mem.zeroes(device.DeviceDescriptor); + descriptor.class = @intFromEnum(device.DeviceClass.acpi_device); + descriptor.pci_class = device.no_pci_class; + const hid_len: u64 = std.mem.indexOfScalar(u8, &hid, 0) orelse hid.len; + descriptor.hid_len = hid_len; + @memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]); + applyCrs(&descriptor, node, interpreter); + + const registered = device.register(node_id, &descriptor) orelse { + writeLine("acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]}); + return; + }; + writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ hid[0..@intCast(hid_len)], registered, descriptor.resource_count }); + reported.* += 1; + + if (manager) |h| { + var report = protocol.ChildAdded{ + .parent = node_id, + .bus_address = registered, + .identity = 0, + .device_id = registered, + }; + @memcpy(report.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]); + var reply: [protocol.message_maximum]u8 = undefined; + _ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; + } +} + +/// _STA bit 0 (present); absent method or a failed evaluation is treated as +/// present, per the ACPI rules. +fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool { + const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true; + const obj = interpreter.evaluate(sta, &.{}) catch return true; + const status = obj.asInteger() catch return true; + return (status & 0x01) != 0; +} + +/// The device's EISA-decoded _HID (e.g. "PNP0303"), or null. +fn readHid(node: *aml.Node, interpreter: *aml.Interpreter) ?[8]u8 { + const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return null; + var buffer: [8]u8 = .{0} ** 8; + if (hid.kind == .method) { + const obj = interpreter.evaluate(hid, &.{}) catch return null; + switch (obj) { + .integer => |n| { + _ = eisaIdToStr(@truncate(n), &buffer); + return buffer; + }, + else => return null, + } + } + if (hid.kind != .name or hid.value.len == 0) return null; + const v = hid.value; + switch (v[0]) { + 0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => { + var p: usize = 0; + const n = readIntObj(v, &p) orelse return null; + _ = eisaIdToStr(@truncate(n), &buffer); + return buffer; + }, + else => return null, + } +} + +// --- _CRS resource-template decode (ported from the kernel's acpi.zig) -------- + +fn applyCrs(descriptor: *device.DeviceDescriptor, node: *aml.Node, interpreter: *aml.Interpreter) void { + const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return; + const obj = interpreter.evaluate(crs, &.{}) catch return; + const bytes = switch (obj) { + .buffer => |b| b, + else => return, + }; + var i: usize = 0; + while (i < bytes.len) { + const tag = bytes[i]; + if (tag & 0x80 == 0) { + const len: usize = tag & 0x07; + const body = i + 1; + if (body + len > bytes.len) break; + switch ((tag >> 3) & 0x0F) { + 0x04 => if (len >= 2) { // IRQ mask + const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8); + var b: usize = 0; + while (b < 16) : (b += 1) { + if (mask & (@as(u16, 1) << @intCast(b)) != 0) addResource(descriptor, .irq, b, 1); + } + }, + 0x08 => if (len >= 7) addResource(descriptor, .io_port, rd16(bytes, body + 1), bytes[body + 6]), + 0x09 => if (len >= 3) addResource(descriptor, .io_port, rd16(bytes, body), bytes[body + 2]), + 0x0F => break, + else => {}, + } + i = body + len; + } else { + if (i + 3 > bytes.len) break; + const len: usize = @intCast(rd16(bytes, i + 1)); + const body = i + 3; + if (body + len > bytes.len) break; + switch (tag) { + 0x85 => if (len >= 17) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 13)), + 0x86 => if (len >= 9) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 5)), + 0x89 => if (len >= 2) { + const count = bytes[body + 1]; + var k: usize = 0; + while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) { + addResource(descriptor, .irq, rd32(bytes, body + 2 + k * 4), 1); + } + }, + else => {}, + } + i = body + len; + } + } +} + +fn addResource(descriptor: *device.DeviceDescriptor, kind: device.ResourceKind, start: u64, len: u64) void { + if (descriptor.resource_count >= descriptor.resources.len) return; + descriptor.resources[@intCast(descriptor.resource_count)] = .{ .kind = @intFromEnum(kind), .start = start, .len = len }; + descriptor.resource_count += 1; +} + +// --- small helpers ported verbatim from the kernel's acpi.zig ---------------- + +fn seg4(comptime s: *const [4:0]u8) [4]u8 { + return s[0..4].*; +} + +fn hexDigit(n: u8) u8 { + return if (n < 10) '0' + n else 'A' + (n - 10); +} + +fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 { + const b0: u16 = @intCast(id & 0xFF); + const b1: u16 = @intCast((id >> 8) & 0xFF); + const b2: u8 = @truncate(id >> 16); + const b3: u8 = @truncate(id >> 24); + const mfg = (b0 << 8) | b1; + buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F)); + buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F)); + buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F)); + buffer[3] = hexDigit((b2 >> 4) & 0xF); + buffer[4] = hexDigit(b2 & 0xF); + buffer[5] = hexDigit((b3 >> 4) & 0xF); + buffer[6] = hexDigit(b3 & 0xF); + buffer[7] = 0; + return buffer[0..7]; +} + +fn readIntObj(bytes: []const u8, p: *usize) ?u64 { + if (p.* >= bytes.len) return null; + const op = bytes[p.*]; + p.* += 1; + switch (op) { + 0x00 => return 0, + 0x01 => return 1, + 0xFF => return 1, + 0x0A => { + if (p.* >= bytes.len) return null; + const v = bytes[p.*]; + p.* += 1; + return v; + }, + 0x0B => { + if (p.* + 2 > bytes.len) return null; + const v = rd16(bytes, p.*); + p.* += 2; + return v; + }, + 0x0C => { + if (p.* + 4 > bytes.len) return null; + const v = rd32(bytes, p.*); + p.* += 4; + return v; + }, + else => return null, + } +} + +fn rd16(bytes: []const u8, off: usize) u64 { + return @as(u64, bytes[off]) | (@as(u64, bytes[off + 1]) << 8); +} + +fn rd32(bytes: []const u8, off: usize) u64 { + return rd16(bytes, off) | (rd16(bytes, off + 2) << 16); +} + pub const panic = runtime.panic; comptime { _ = &runtime.start._start; // pull the runtime entry shim into the image diff --git a/system/services/device-manager/device-manager-protocol.zig b/system/services/device-manager/device-manager-protocol.zig index bf494a3..d557ec7 100644 --- a/system/services/device-manager/device-manager-protocol.zig +++ b/system/services/device-manager/device-manager-protocol.zig @@ -74,12 +74,16 @@ pub const ChildAdded = extern struct { /// Where on the bus (for USB: the root port number, 1-based). bus_address: u64, /// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI: - /// the class triple). + /// the class triple; for ACPI devices, 0 — identity is the hid below). identity: u64, /// The kernel device id this child was `device_register`ed as — what the /// manager hands a matched driver as its argv assignment — or `no_device` /// for an unregistered leaf (a USB port before the descriptor track). device_id: u64 = no_device, + /// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices + /// discovered by firmware string rather than a numeric bus identity. Empty + /// (all zero) otherwise. Widens for FDT `compatible` strings later. + hid: [8]u8 = .{0} ** 8, }; pub const child_added_size = @sizeOf(ChildAdded); diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 39cfc2b..83d8c0e 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -151,7 +151,7 @@ const Child = struct { reporter: u32 = 0, // the reporting driver instance's process id }; -const maximum_children = 32; +const maximum_children = 64; // ACPI adds ~34 device nodes (M20.2), plus PCI + USB var children: [maximum_children]Child = .{Child{}} ** maximum_children; /// Record (or refresh) a reported child. Refreshing matters: a restarted bus diff --git a/test/qemu_test.py b/test/qemu_test.py index 3cf74a1..20761ad 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -281,6 +281,18 @@ CASES = [ "timeout": 60, "expect": r"acpi-parse: ok", "fail": r"acpi-parse: mismatch|DANOS-TEST-RESULT: FAIL"}, + # M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers + + # reports its _HID devices — the two PS/2 nodes must appear with resources + # (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/m19-m20-plan.md). + {"name": "acpi-report", + "smp": 4, + "timeout": 60, + "qemu_extra": ["-device", "qemu-xhci,id=xhci", + "-device", "usb-kbd,bus=xhci.0", + "-device", "usb-mouse,bus=xhci.0"], + "expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*" + r"acpi: reported PNP0F13 \(device \d+, 1 resources\)", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map # grant) finds exactly the functions the kernel's own walk recorded. {"name": "pci-scan",