205 lines
13 KiB
Markdown
205 lines
13 KiB
Markdown
# 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 <case>-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.)
|