# M19–M20 execution plan: discovery migration The operational plan for [device-manager.md](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](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. 7. **Firmware neutrality is the contract** (2026-07-13). The generic layer is everything at and above the device-manager protocol — descriptors, containment, reports, matching, supervision — and none of it may become x86-specific. Discovery is one swappable process per firmware: the acpi service on x86; an **fdt service** on the Raspberry Pis (claims a `devicetree-blob` node, reports children from the flattened device tree — pure data, no bytecode, no port grant, strictly simpler than ACPI). The manager owns the tree as *data* and touches no hardware, ever — AML runs in a crashable, supervised discoverer precisely so a firmware-bytecode fault can never take down the supervisor. Two consequences recorded now: `DeviceDescriptor`'s 8-byte `hid` cannot hold an FDT `compatible` string ("brcm,bcm2835-aux-uart") — identity widens before the fdt service exists; and cross-firmware surfaces are named by **domain, not firmware** (M21 defines a *power* protocol, not an "ACPI events" protocol — PSCI/mailbox sources feed the same subscribers on ARM). **Landed early (2026-07-13):** both services exist as placeholders (system/services/acpi, system/services/ fdt) and the build's `-Ddiscovery=acpi|fdt` option fills the ramdisk's neutral `discovery` slot — the manager will spawn "discovery" by that name in M20.3 and never learn which firmware it is on. ## Status - [x] **M19.0** — prerequisites (bridge apertures from the memory map's *gaps* — the single-hole rule died on OVMF's flash at the top of 4 GiB, caught by the new every-BAR-contained assert in `discovery`; idempotent `device_register` proven in `bus`; `ChildAdded.device_id`; m17-m18-plan.md archived; suite 54/54). - [x] **M19.1** — pci-bus driver, scan only (claims the bridge, maps ECAM through its grant, brute-force walk with the multifunction rule; the manager matches pci_host_bridge → pci-bus per device with the full protocol contract; `pci-scan` builds its expected marker from the kernel's own count — equivalence on the first run; suite 55/55). - [x] **M19.2** — register + report (BAR probe mirrored byte-for-byte from the kernel's addBars so dedupe returns the kernel's node ids during coexistence; the bridge gained the io_port aperture I/O BARs need; reports carry the registered device_id; pci-scan drills a forced restart and asserts the PCI node count never grows — plus harness hardening: a failing case now preserves its serial as -failed-serial.log, and the heavy scenarios run at 150s; suite 55/55). - [x] **M19.3** — the flip: kernel `enumeratePci`/`addBars`/`PciHeader` all deleted (bridge node stays); manager matches PCI drivers from reported identity, deduped by registered id. Surfaced and fixed a real SMP race the flip created — ring-3 device_register made the broker table concurrent, so mmio_map's lock-free read intermittently tore hpet's resource length (user fault) and overflowed `r.len-1` into a kernel panic; now the broker read is under the big lock and the arithmetic is guarded, and pci-bus skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart hammered 6×). - [ ] **merge** `feat/pci-bus` → main, push. - [ ] **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. - [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**. --- ## Phase notes **M19.0 apertures:** the boot memory map already crosses the handoff ([boot-handoff]), but discovery never sees it today — expect a small pass-through (kernel init hands the map to the platform layer) before the holes computation, which 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 spawn and identity (pre-settled 2026-07-13):** the manager spawns `discovery` by its neutral ramdisk name at startup, as an ordinary protocol driver (hello, supervision) — from M20.1 on, on every boot. For reporting ACPI devices, `ChildAdded` gains `hid: [8]u8` (EISA ids fit; zero = none): firmware *string* identity travels beside the numeric `identity` field until the FDT-driven widening replaces both (decision 7). **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.)