The AML interpreter now handles the Notify opcode (0x86, previously unhandled): it resolves the target device, evaluates the code, and records the pair in a bounded per-evaluate queue the caller drains with takeNotifications. A host unit test with hand-encoded AML — a method that issues Notify(DEV_, 0x80) — proves the device and code come back; aml.zig joins the zig build test loop so the interpreter is covered on the host. The acpi service's SCI handler now services general-purpose events too: for each set-and-enabled GPE bit it evaluates the \_GPE._Lxx (level) or _Exx (edge) handler method, drains the Notify queue that produced, and publishes a domain event per notified device — PNP0C0A battery, ACPI0003 AC, PNP0C0D lid, else generic notify — then clears the status bit and acks. The embedded controller's _Qxx queries are out of scope (hardware track). QEMU raises no GPEs on this config, so the QEMU suite is the regression net (the power button still works with GPE servicing in the path); correctness is the unit test. Suite 59/59.
139 lines
8.0 KiB
Markdown
139 lines
8.0 KiB
Markdown
# 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
|
|
(`<case>-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
|
|
|
|
- [x] **M21.0** — baseline (dead-code sweep confirmed landed on main — no
|
|
acpi.zig conflict; `feat/power-events` cut; QMP channel in the harness:
|
|
always-on unix socket, client with the capabilities handshake, per-case
|
|
`qmp_after` hook, and a hook-must-deliver pass gate that the smoke case
|
|
now proves with a harmless query-status; suite 58/58).
|
|
- [x] **M21.1** — SCI + the power button (kernel appends the FADT as an
|
|
acpi-tables memory resource, tagged by its "FACP" header; `power-protocol`
|
|
module + `ServiceId.power = 5`; the acpi service converted to
|
|
`runtime.service.run`, registers `.power`, reads PM1 event/control + GPE
|
|
ports from its FADT copy, enables ACPI mode if SCI_EN is clear, binds the
|
|
SCI (the len-1 irq), sets PWRBTN_EN; the SCI handler clears PM1_STS,
|
|
logs `power: button pressed`, publishes `power_button`, acks. Scenario
|
|
`power-button` injects a real `system_powerdown` via QMP; initial-ramdisk
|
|
timeout 30→60s for the service's added boot work; suite 59/59).
|
|
- [x] **M21.2** — Notify + GPE dispatch (interpreter handles `notify_opcode`
|
|
into a bounded queue, cleared per-evaluate, drained via
|
|
`takeNotifications`; the service walks GPE status/enable bytes, evaluates
|
|
`\_GPE._Lxx`/`_Exx` per active bit, maps notified nodes to events
|
|
(battery/ac/lid/generic), clears GPE_STS write-1, acks. EC `_Qxx` out.
|
|
Host unit test with hand-encoded AML proves the queue; aml.zig joined the
|
|
`zig build test` loop. QEMU raises no GPEs — suite is regression net,
|
|
59/59).
|
|
- [ ] **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.
|