diff --git a/docs/m21-plan.md b/docs/m21-plan.md new file mode 100644 index 0000000..305e951 --- /dev/null +++ b/docs/m21-plan.md @@ -0,0 +1,135 @@ +# M21 execution plan: ACPI events + system power + +The operational plan for the event side of the acpi service and orderly +shutdown — the capstone [m19-m20-plan.md](m19-m20-plan.md) previewed. Same +rules as its predecessors: 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). Failing cases preserve their serial logs +(`-failed-serial.log`). + +**Workflow:** dedicated worktree; branch `feat/power-events` off `main`; +auto-merge to main when the branch is green; keep the branch; push everything. + +## Settled decisions (2026-07-13, approved) + +1. **S5 is executed by the acpi service from ring 3.** No new syscall: the + broad port grant (M20 decision 5) already made this physically possible — + the service holds the PM1 control ports in its io grant and derives `_S5` + from its own namespace (`aml.sleepState`). Formalizing it adds no + authority. The kernel keeps `power.zig` for its own test paths and + panic-time use. +2. **The power surface is domain-named** (decision 7 of the last plan): a + `power-protocol` module + `ServiceId.power = 5`, registered by the acpi + service — on ARM, a PSCI/mailbox service registers the same id and + subscribers never know the difference. Messages: `subscribe` (endpoint as + the call's capability, the input/manager pattern), `shutdown` (accepted + only from PID 1 — init), and events published as buffered messages: + `power_button`, `lid`, `ac`, `battery`, generic `notify` with a code. +3. **The service learns event ports from its own FADT copy**: the kernel adds + the FADT as one more memory resource on the acpi-tables node; the service + tells it apart from the AML blobs by signature ("FACP" header — the blob + resources are header-stripped bytecode and start with no signature). The + kernel's own FADT parse is untouched. +4. **The acpi service converts to the harness** (`runtime.service.run`): + protocol messages (subscribe/shutdown), the SCI notification, and the + existing report flow fold into one loop — the shape it was always meant + to have. +5. **GPE/Notify correctness is proven by host unit tests** (synthetic AML + with a Notify inside a method body; aml.zig joins the `zig build test` + loop). The QEMU scenario proves the power button — a *fixed* event, + deterministically injectable via QMP `system_powerdown` — because QEMU + cannot raise GPEs deterministically on this config. Battery/AC/lid and the + embedded controller (`_Qxx`) are interface-complete here and validated on + real hardware (the laptop) later. + +## Ground truth the phases build on (verified 2026-07-13) + +- `system/devices/power.zig` `shutdown()` is the kernel's S5 write + (SLP_TYP|SLP_EN to PM1a/PM1b control); there is no power syscall. +- init (`system/services/init/init.zig`) spawns vfs/input/device-manager + fire-and-forget — no child ids kept, no signals, no event loop. The whole + stop toolkit exists in `runtime.process` (stop/sendSignal/bindSignals). +- `test/qemu_test.py` has no QMP channel (serial is a one-way file). +- The kernel parses PM1 *control* blocks and SCI_INT from the FADT; the PM1 + **event** blocks (offsets 56/60, len at 88) and **GPE0/GPE1** blocks + (offsets 80/84, lens 92/93) are unparsed — the service reads them from its + FADT copy (decision 3). +- The acpi-tables node carries the SCI as its only `len == 1` irq resource + (the broad window is len 256) — that is how the service finds it to + `irqBind`. +- `notify_opcode = 0x86` exists in `system/devices/aml/opcodes.zig` but the + interpreter never handles it — a GPE `_Lxx` body containing Notify fails + evaluation today. Everything else a GPE handler needs (field access, + control flow, method calls) is proven by the ring-3 `_STA`/`_CRS` work. +- The dead-code sweep (spawned task) also edits `system/devices/acpi.zig`; + M21.0 checks whether it landed and rebases before touching that file. + +## Status + +- [ ] **M21.0** — baseline: rebase over anything newly merged (the dead-code + sweep touches acpi.zig); cut `feat/power-events`; add the QMP channel to + the harness (`-qmp unix:.../qmp.sock,server,nowait`, a small client with + the `qmp_capabilities` handshake, a per-case `qmp_after` hook that sends + a command N seconds after boot); existing suite stays green. +- [ ] **M21.1** — SCI + the power button: kernel appends the FADT as an + acpi-tables memory resource; new `power-protocol` module + + `ServiceId.power`; the acpi service converts to the harness, registers + `.power`, parses the event/GPE blocks from its FADT copy, enables ACPI + mode if needed (SMI dance, spin on SCI_EN), binds the SCI, sets + PWRBTN_EN; on SCI reads/clears PM1_STS and publishes `power_button` + (log: `power: button pressed`), always irqAck. Scenario `power-button`: + `qmp_after system_powerdown` → expect the log line. +- [ ] **M21.2** — Notify + GPE dispatch: interpreter handles `notify_opcode` + into a bounded queue drained after evaluate(); on GPE status bits the + service evaluates `\_GPE._Lxx`/`_Exx`, maps notified nodes to events + (PNP0C0A→battery, ACPI0003→ac, PNP0C0D→lid, else generic), clears + GPE_STS, acks. EC `_Qxx` explicitly out (hardware track). Host unit + tests for Notify in aml.zig; aml.zig joins the `zig build test` loop. +- [ ] **M21.3** — orderly shutdown: init keeps child ids (spawnSupervised + + exit endpoint), binds signals, subscribes to `.power`; on `power_button` + logs `init: shutting down`, runs `stop(child, 2000, endpoint)` in + reverse spawn order, then sends `shutdown` to `.power`; the acpi service + (sender PID 1 only) logs `power: entering S5` and writes SLP_TYP|SLP_EN + from ring 3. Scenario `orderly-shutdown`: boot via init, `qmp_after + system_powerdown`, ordered regex button→shutting-down→entering-S5, pass + on QEMU exit. Docs + memory updated. +- [ ] **merge** `feat/power-events` → main, push, keep the branch — **loop + ends here**. + +--- + +## Phase notes + +**M21.0 QMP:** open the unix socket after Popen, complete the +`qmp_capabilities` handshake, then send the hook's command (for these +scenarios: `{"execute": "system_powerdown"}`). The socket is additive — no +existing case may notice it. Note e3fe3f3 recently reworked how the harness +boots; adapt to its current shape rather than the pre-rework description. + +**M21.1 SCI details:** PM1_STS is at the event block base (write-1-to-clear); +PM1_EN at base + block_len/2; PWRBTN bit is 8 in both. If PM1b exists, mirror +reads/writes to both blocks. Enable ACPI mode only when SCI_EN (PM1 control +bit 0) is clear — OVMF boots may already have it set. The publish path reuses +the manager's subscriber table pattern (bounded, drop-on-failed-send). + +**M21.2 GPE walk:** GPE0_STS bytes live at the GPE0 block base, GPE0_EN in +the block's upper half; for a set+enabled bit n, the handler method is +`_L%02X` (level) or `_E%02X` (edge) under `\_GPE`. Evaluate, drain the notify +queue, clear the status bit, ack. A missing handler method is clear-and-log, +not an error. + +**M21.3 ordering:** init subscribes with retries — the acpi service registers +`.power` well after init starts. The stop sequence runs vfs last (other +services may flush through it). The S5 write mirrors `power.zig`'s +`sleepValue` (SLP_TYP bits [12:10], SLP_EN bit 13); if the write returns, log +`power: S5 write did not take` so the scenario fails loudly instead of +hanging. + +**Explicitly out of scope:** the embedded controller and `_Qxx` queries, +battery `_BST`/`_BIF` evaluation beyond the interface stubs, lid/AC on QEMU +(no emulation), reboot over the power protocol, S3 sleep, per-device D-states +(a future lifecycle-vocabulary extension), thermal zones.