Files
danos/docs/m19-m20-plan.md
T
Daniel Samson 3a78dcab3f Scope ACPI events and system power as M21; record the SCI on acpi-tables
Battery, AC, lid, and the power button ride the acpi service as reported
children with small class drivers — the xHCI split repeated. QEMU can
only prove the power-button path (system_powerdown injects the real fixed
event), so battery/EC are interface-complete and hardware-validated on
the laptop. Per-device power states (D-states, suspend/resume) stay out
of scope: suspend has the shape of a lifecycle signal every driver must
answer, and it has no consumer until laptop sleep.
2026-07-13 01:21:23 +01:00

10 KiB
Raw Blame History

M19–M20 execution plan: discovery migration

The operational plan for device-manager.md's increment 8: discovery leaves the kernel — a pci-bus driver (M19) and an acpi service (M20), with the kernel's device enumeration retired behind them. Same rules as m17-m18-plan.md: one phase at a time, each green before the next; this file is the build order and the checklist.

Definition of green, every phase: zig build clean, zig build test clean, python3 test/qemu_test.py passes (existing scenarios plus the phase's new one), and the relevant design doc updated. Commit per green phase (no co-author trailers). The full suite is the regression net — the existing driver-restart / usb-report / device-list / input scenarios must stay green through the migration, which is the whole point: the system must not be able to tell who enumerated it.

Workflow: dedicated worktree; branches off main — feat/pci-bus (M19.0–19.3), feat/acpi-service (M20.1–20.3); auto-merge to main when a branch is green; keep branches; push everything.

Settled decisions (2026-07-13 — veto before the loop starts)

  1. What "retiring the kernel scan" means. The kernel keeps, forever, the parses it needs before user space exists: RSDP/XSDT location, MADT (SMP), the HPET table (the tick), FADT + the AML \_S5 evaluation (poweroff — the power tests prove it), and MCFG (the host bridge node). What retires is device enumeration: the ECAM function walk (M19.3) and the DSDT/SSDT namespace walk that builds device nodes (M20.3). The AML module stays a shared build module compiled into both the kernel (for \_S5) and the acpi service (for everything else) — same source, two builds, no fork.
  2. Bridge apertures come from the firmware memory map, not AML. Registered PCI functions carry BAR resources, and containment demands the bridge own windows that cover them. The apertures are derived kernel-side from the boot memory map's MMIO holes (regions that are neither RAM nor tables) — mechanical, AML-free, and available at boot regardless of what later moved to user space. (The bridge today carries only ECAM + bus range; this is the prerequisite M19.0 exists for.)
  3. device_register becomes idempotent on exact match. A re-registration with identical (parent, class, resources) returns the existing id instead of appending. The kernel table has no unregister, so without this a restarted registering bus would duplicate its children on every respawn — idempotence makes restart-and-re-report safe for every future bus, not just PCI.
  4. The manager matches from reports. ChildAdded gains a device_id field (the kernel-registered id, no_device for unregistered leaves like USB ports). After the M19.3 flip, PCI driver matching keys off reported identity (the class triple) instead of the manager's boot-time snapshot — the snapshot match remains only for what the kernel still seeds. One flip phase changes both sides at once so no device is ever matched twice.
  5. The acpi service's authority is one node. The kernel publishes an acpi-tables device: memory resources covering the table blobs plus a broad io_port resource — the documented trust grant to exactly one process (AML OperationRegions reach EC/PM ports; the claim-gated io_read/io_write calls already exist). The service claims it, maps the tables, and runs the shared AML module in ring 3 behind a Hal backed by mmio_map + io_read/io_write.
  6. Both new processes are protocol drivers under the manager: hello, supervision, restart with backoff — all inherited from M18.1 for free. Registration idempotence (decision 3) is what makes their restarts sound.

Status

  • M19.0 — prerequisites on feat/pci-bus: bridge MMIO apertures from the memory-map holes; device_register idempotence (+ kernel unit checks); ChildAdded.device_id; archive note on m17-m18-plan.md.
  • M19.1 — pci-bus driver, scan only: claim the host bridge, map the ECAM window, walk bus/device/function headers, log what it finds. Scenario pci-scan: the kernel test compares the driver's reported count against the broker table's pci_device count — equivalence, per class.
  • M19.2 — register + report: each function registered under the bridge (config-space slice + BARs, pci_class in the descriptor), reported with child_added { device_id, identity = class triple }. Manager mirrors; spawn-from-reports stays off. Scenario extends pci-scan: registered ids resolve, no duplicates after a forced driver restart (idempotence proven end to end).
  • M19.3 — the flip: kernel enumeratePci call removed (bridge node stays); manager matches PCI drivers from reports. One commit. The existing xHCI scenarios (driver-restart, usb-report, device-list) are the assertion — xhci must come up spawned off a pci-bus report, and the suite must not be able to tell the difference. discovery.md updated.
  • merge feat/pci-bus → main, push.
  • M20.1 — acpi service, parse only: 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 _HIDs. 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.
  • merge feat/acpi-service → main, push — loop ends here.

Phase notes

M19.0 apertures: the boot memory map already crosses the handoff ([boot-handoff]); the holes computation belongs where the bridge node is built (parseMcfg). Sanity-check on QEMU q35: the xHCI BAR (0xc0000000-region values seen in the M18 logs) must land inside a derived aperture, asserted in the kernel unit test.

M19.1 scanning without owning config access twice: the driver reads config space through its ECAM mmio_map grant of the bridge window — the same bytes the kernel walk read. Vendor-id 0xFFFF skip, header-type multifunction rule, no bridge recursion (matches the kernel's current single-segment walk).

M19.2 BAR sizing: the classic size probe (write all-ones, read mask, restore) is deferred — the BARs' current programmed values and types are enough for containment-checked registration at bring-up; sizing lands with the first driver that needs to move a BAR. Log what is registered so the scenario can assert it.

M19.3 what the manager still seeds from the snapshot: everything the kernel still enumerates (timers, ACPI nodes until M20.3). The PCI arm of pciDriverFor switches source; driverFor doesn't move until M20.3.

M20.1 Hal in ring 3: mapMmio → device.mmioMap over the claimed acpi-tables node (plus a table-offset map for blobs); pioRead/pioWrite → device.ioRead/ioWrite against its io_port resource. The interpreter cannot tell it moved — that is the assertion of acpi-parse.

M20.2 containment for _CRS: io ports fall inside the node's broad io_port resource; MMIO windows (HPET, LAPIC ranges some firmwares list) fall inside the memory-map holes added to the node in M20.1. Anything that doesn't fit is logged and skipped, loudly — bring-up honesty over silent drops.

M20.3 ps2 ordering: ps2-bus binds nodes the acpi service now reports, so its spawn moves behind the report (the manager's matching handles this once the source flips); the input scenario proves the keyboard still types.

Explicitly out of scope: PCI bridge recursion (single segment, flat bus walk stays); BAR reprogramming/sizing; disk/PCIe hotplug; interrupt routing changes (_PRT stays wherever it is today); the USB descriptor track; multi-segment ECAM; per-device power states (D-states, _PSx/_PRx, suspend/resume — a future lifecycle-vocabulary extension, since "suspend" has the shape of a signal every driver must answer, and it has no consumer until laptop sleep); CPU P/C-states.

M21 preview — ACPI events + system power (planned next, not in this loop)

The acpi service grows the event side (settled direction 2026-07-13; detailed phases when M20 lands):

  • 21.1 SCI + fixed events: irq_bind the SCI (the resource M20.1 already records), read/clear PM1 status, publish the power-button event to subscribers (the same pub/sub shape the manager uses).
  • 21.2 GPE + Notify: Notify dispatch in the shared AML interpreter, GPE block handling, Notify(device, code) published per reported node. The acpi service is a bus here: battery (PNP0C0A), AC (ACPI0003), and lid (PNP0C0D) nodes are reported children; small class drivers bind them and speak an evaluate/subscribe protocol to the service — the xHCI split, repeated. The embedded controller (_Qxx queries) rides this phase; QEMU emulates no battery/EC, so those paths are interface-complete and validated on real hardware (the laptop is the win condition), while the plumbing is proven by the power button.
  • 21.3 the capstone: QEMU system_powerdown → acpi service event → init runs the M17 stop sequence over its children → kernel \_S5 — orderly shutdown as the scenario that proves lifecycle + events compose. (The harness grows a QMP poke to inject the event.)