diff --git a/local/docs/LG-GRAM-16Z90TP-COMPATIBILITY-PLAN.md b/local/docs/LG-GRAM-16Z90TP-COMPATIBILITY-PLAN.md index e5d8e7449c..fbb6979a84 100644 --- a/local/docs/LG-GRAM-16Z90TP-COMPATIBILITY-PLAN.md +++ b/local/docs/LG-GRAM-16Z90TP-COMPATIBILITY-PLAN.md @@ -202,10 +202,11 @@ whole desktop path (Qt6/KWin) on this machine.** ### PHASE 4 — Touchpad: I2C-HID chain bring-up (M) -> **STATUS: 4.1–4.3 COMPLETE (2026-07-20).** The entire I2C transfer chain was +> **STATUS: 4.1–4.4 COMPLETE (2026-07-21).** The entire I2C transfer chain was > stubbed end-to-end and has been replaced with real implementations (see -> "stub sweep" below). 4.4 (multitouch report parsing) and 4.5 (host -> validation) remain open and require the running system. +> "stub sweep" below). 4.4 (multitouch report parsing) landed on 2026-07-21. +> Only 4.5 (host runtime validation) remains open and requires the running +> system. **Stub sweep (all found during this phase, all replaced with real code):** @@ -279,7 +280,7 @@ pinning (pre-existing, separate task), CallerCtx authZ on i2c schemes | 4.1 | Identify actual ACPI `_HID` of the two LPSS I2C controllers on the host | — | ✅ DONE — **no `_HID` exists on this platform** (host-verified via sysfs: PCI function 00:15.0/00:15.1 ↔ `\_SB_.PC00.I2C0/I2C1`, DesignWare adapter spawned by intel-lpss MFD with `bxt_i2c_info`, 133 MHz). PCI-ID discovery implemented instead, per Linux's model | | 4.2 | Controller bring-up: BAR/IRQ, DesignWare init sequence, bus speed | `drivers/i2c/designware/`, `intel-lpss-i2cd` | ✅ DONE (polling engine; IRQ-driven completion is a documented IRQ-plan follow-up, not a stub) | | 4.3 | `i2c-hidd`: probe 04CA:00CC (PNP0C50), `_DSM` HID descriptor, report descriptor fetch | `i2c-hidd`, `i2cd`, endpoint chain | ✅ CODE-READY — protocol chain verified end-to-end in code (i2c-hidd's bare `I2cTransferRequest` → i2cd routing → provider endpoint). Runtime proof pending host boot (Phase 1) | -| 4.4 | Multitouch digitizer report parsing (ABS_MT_* slots, tracking IDs) + forwarding through inputd → evdevd gesture module | `i2c-hidd/hid.rs`, `evdevd/gesture.rs` | M — OPEN | +| 4.4 | Multitouch digitizer report parsing (ABS_MT_* slots, tracking IDs) + forwarding through inputd → evdevd gesture module | `i2c-hidd/{hid,input,report_desc}.rs` | ✅ CODE-COMPLETE (2026-07-21) — HID 1.11 §6.2.2 descriptor parser + decoder in new `report_desc.rs` (digitizer contacts, generic-desktop X/Y/wheel, buttons, keyboard page fallback, sign-extended fields, usage-min/max expansion, global-state stack). Integrated via `forward_layout_report`: first touching contact → absolute `MouseEvent`; two simultaneous contacts → vertical `ScrollEvent` (two-finger scroll); buttons → `ButtonEvent`. 3 unit tests pass on host. Runtime palm rejection / gesture tuning stays open with 4.5 (needs boot) | | 4.5 | Host validation: pointer, click, two-finger scroll; palm-rejection tuning | — | S — OPEN (needs boot) | **Adapter naming chain (documented for future consumers):** @@ -290,9 +291,10 @@ provider daemon (intel-lpss-i2cd registered alias `PC00.I2C0` discovered by matching `FixedMemory32 == BAR0`) → engine executes on the controller's BAR0. **Gate 4 (partial):** base fork cooks for `x86_64-unknown-redox` with the new -chain; all unit tests pass (dw-i2c 3, intel-lpss-i2cd 3, others green); -binaries staged at verified paths. Full gate (touchpad usable on host) open -until Phase 1 boot + 4.4. +chain; all unit tests pass (dw-i2c 3, intel-lpss-i2cd 3, i2c-hidd 3, others +green); binaries staged at verified paths. Full gate (touchpad usable on host) +is now blocked only on Phase 1 boot — 4.4 (multitouch parsing) landed +2026-07-21. --- @@ -300,16 +302,83 @@ until Phase 1 boot + 4.4. This phase removes the biggest "not a real laptop" gap: event-driven power state. -| # | Work item | Files | Effort | -|---|---|---|---| -| 5.1 | **GPE dispatch infrastructure in acpid**: SCI via `/scheme/irq` (ACPI `sci_int` from FADT), GPE status/enable block access, per-GPE handler registry | `local/sources/base/drivers/acpid/src/{main,acpi}.rs` | L | -| 5.2 | EC event loop: `SCI_EVT` poll/IRQ → `QR_EC` query → dispatch `_Qxx` AML methods; wire battery/AC change notifications (`\_SB.BAT0` notify 0x80) | `acpid/ec.rs`, new `acpid/gpe.rs` | M | -| 5.3 | Lid switch (PNP0C0D `_LID`) + power/sleep fixed events (PM1_STS PWRBTN/SLPBTN) → scheme surfaces + kstop events | acpid + sessiond consumers | M | -| 5.4 | Event-driven `redbear-upower` (replace polling with notify-driven refresh) | `redbear-upower` | S | -| 5.5 | Fan: real PNP0C0B/EC fan control replacing thermald soft model | `thermald/fan.rs`, acpid EC | M | +> **STATUS: CODE-COMPLETE (2026-07-21).** All five sub-items (P5.1–P5.5) +> landed plus a prerequisite P5.0; affected crates compile for +> `x86_64-unknown-redox`; `cook base` and `cook redbear-upower` succeed; +> i2c-hidd (3) + dw-i2c (5) + intel-lpss-i2cd (1) tests pass on host. +> acpid host-side tests can't link (pre-existing libredox limitation — +> tests must run via `redoxer`). Runtime Gate 5 remains open pending +> Phase 1 bare-metal boot. -**Gate 5:** Unplug/replug AC → instant battery/AC event; close/open lid → event -logged; power button → clean shutdown flow; fan responds to thermal load. +**Prerequisite work (P5.0):** + +The upstream `acpi-rs` crate (rev 90cbe88) **panicked** on every AML +`Notify (target, value)` opcode — the interpreter path was literally +`unimplemented!()`. Since `Notify` is the backbone of ACPI event flow +(EC queries fan it out, battery/AC/lid/thermal devices use it, _Qxx +methods end with `Notify(\_SB.BAT0, 0x80)`), this is unworkable on real +laptop firmware. Fixed by **vendoring the acpi-rs crate into the base +fork** (`drivers/acpi-rs/`) with a real `Opcode::Notify` executor: + +- `src/aml/mod.rs`: resolves the target ACPI path, then invokes + `Handler::handle_notify(target_path, value)`. +- `src/lib.rs`: new `Handler::handle_notify` default (no-op) so + existing users of the crate (amlserde, others) keep compiling. +- `drivers/acpid/Cargo.toml` + `drivers/amlserde/Cargo.toml`: switched + to `acpi = { path = "../acpi-rs", features = ["alloc", "aml"] }` so + every consumer sees the same fork (avoids the dual-version trap). +- `Cargo.toml` (workspace): added `"drivers/acpi-rs"` to members. + +This is a **vendored fork**, not a `local/sources/acpi-rs/` submodule. +It lives inside the base fork and travels with it. Upstream acpi-rs +has Notify unimplemented at the time of vendoring; if upstream lands a +real Notify implementation we will re-evaluate convergence per the +upstream-first rule (CONSOLE-TO-KDE § "upstream-first for fast-moving +components"). + +**Stub sweep in this phase:** + +1. **acpi-rs `Notify` opcode was `unimplemented!()`** → vendored fork + with a real executor that resolves the target path and calls back + into the handler. +2. **acpid had no SCI/GPE/PM1 path at all** → new `gpe.rs` (FADT-driven + GPE block map, write-1-to-clear status, read-modify-write enable + preserving firmware state on every other GPE) + `power_events.rs` + (init + SCI dispatch following Linux `evgpe.c` / `ec.c` / `button.c`). +3. **EC query protocol had no event-loop caller** → `pub fn sci_evt_set()` + and `pub fn query()` exposed on `Ec`, called by the SCI handler with + a 32-iteration bounded drain guard. +4. **No lid/button/fan scheme surface** → scheme.rs grew `Lid`, + `LidState`, `ButtonDir`, `Button`, `Notifications`, `Fan`, + `FanState`, `FanSpeed` handle kinds with proper directory-vs-file + separation. +5. **`evaluate_acpi_method` took `&mut self`** → changed to `&self` + everywhere; AML mutation goes through the existing `RwLock`-guarded + `AmlSymbols` inner state, which is how the EC query loop and the + notification drain can run from a read-only context. + +**Review-fix round (2026-07-20):** this phase inherited all Phase 4 +review fixes (LPSS PCI bring-up, I2C engine ordering, firmware op-mode +honesty) because it shares the base fork. No additional review has +been run on the Phase 5 surface yet; the implementation follows the +same Linux 7.1 reference patterns and bounded-validation discipline. + +| # | Work item | Files | Status | +|---|---|---|---| +| 5.0 | *(prerequisite)* Real AML `Notify` opcode (was `unimplemented!()` in upstream acpi-rs) | `drivers/acpi-rs/` (vendored fork), `drivers/acpid/Cargo.toml`, `drivers/amlserde/Cargo.toml`, workspace `Cargo.toml` | ✅ DONE — vendored fork at base `drivers/acpi-rs/`; `Handler::handle_notify` hook added; all acpi consumers in the base workspace pointed at the path dep | +| 5.1 | **GPE dispatch infrastructure in acpid**: SCI via `/scheme/irq` (ACPI `sci_int` from FADT), GPE status/enable block access, per-GPE handler registry | `drivers/acpid/src/gpe.rs` (new), `drivers/acpid/src/main.rs` | ✅ DONE — `GpeBlocks::from_fadt` builds the register map; PM1 fixed-event bits (PWRBTN/SLPBTN/RTC) per ACPI 6.4 §4.8.3.1; enable preserve + status write-1-to-clear; main loop subscribes `/scheme/irq/{sci_irq}` (default 9) and dispatches via `handle_sci`. 2 unit tests cover block mapping + GPE1 base offset | +| 5.2 | EC event loop: `SCI_EVT` poll/IRQ → `QR_EC` query → dispatch `_Qxx` AML methods; wire battery/AC change notifications (`\_SB.BAT0` notify 0x80) | `drivers/acpid/src/power_events.rs` (new), `drivers/acpid/src/ec.rs`, `drivers/acpid/src/notifications.rs` (new), `drivers/acpid/src/aml_physmem.rs` | ✅ DONE — ECDT parse for EC device path + GPE (ACPI 6.4 §5.2.16) with `_HID = PNP0C09` probe fallback over conventional paths (mirrors Linux `acpi_ec_ecdt_start`); `handle_sci` drains the EC query queue (bounded at 32 iterations); `Ec::sci_evt_set()` / `Ec::query()` exposed; `_Qxx` AML methods evaluated; `AmlNotifications` shared queue receives `Notify` events pushed by the vendored acpi-rs `handle_notify` hook | +| 5.3 | Lid switch (PNP0C0D `_LID`) + power/sleep fixed events (PM1_STS PWRBTN/SLPBTN) → scheme surfaces + kstop events | `drivers/acpid/src/scheme.rs`, `drivers/acpid/src/power_events.rs`, `drivers/acpid/src/acpi.rs` | ✅ DONE — lid device discovered via `_LID` evaluation over candidate paths (EC child + `\_SB`); PM1 fixed events cleared-before-dispatch; surfaces at `/scheme/acpi/{lid/state, button/{power,sleep}, notifications}`; PowerButton increments edge counter and triggers the main-loop shutdown path; SleepButton increments counter and calls `enter_s2idle`; AML `Notify(\_SB.LID_,…)` refreshes `_LID` and emits `LidChanged(open)` on edge transitions | +| 5.4 | Event-driven `redbear-upower` (replace polling with notify-driven refresh) | `local/recipes/system/redbear-upower/source/src/main.rs` | ✅ DONE — added 250 ms `notify_interval` tokio arm draining `/scheme/acpi/notifications`; on non-empty drain, snapshots power state and emits `Changed` D-Bus signal if it changed. The existing 30 s poll arm stays as a safety net for missed events | +| 5.5 | Fan: real PNP0C0B/EC fan control replacing thermald soft model | `drivers/acpid/src/scheme.rs`, `drivers/acpid/src/power_events.rs`, `drivers/acpid/src/acpi.rs` | ✅ DONE — **implementation differs from the original plan wording**: instead of replacing thermald's soft fan model, acpid now owns the ACPI 4.0 fan method surface directly. Probes `_FST`-exposing devices over conventional placements (EC child + LPCB + `\_SB`), exposes read+write at `/scheme/acpi/fan//{state,speed}` (`_FST` on read, `_FSL` on write, percent clamped 0–100). thermald keeps its soft model for now; convergence between the two is a Phase 9.x hardening task | + +**Gate 5 (code-complete, runtime open):** Unplug/replug AC → instant +battery/AC event; close/open lid → event logged; power button → clean +shutdown flow; fan responds to thermal load. All code paths exist and +compile; none have been exercised on the host yet (Phase 1 boot is the +prerequisite). The 250 ms notification drain in redbear-upower should +turn the existing 30 s worst-case state lag into a near-instant event, +but this is a runtime claim and remains to be measured. --- diff --git a/local/recipes/system/redbear-upower/source/src/main.rs b/local/recipes/system/redbear-upower/source/src/main.rs index 44479f5af6..8d6f09a899 100644 --- a/local/recipes/system/redbear-upower/source/src/main.rs +++ b/local/recipes/system/redbear-upower/source/src/main.rs @@ -18,6 +18,12 @@ const BUS_NAME: &str = "org.freedesktop.UPower"; const UPOWER_PATH: &str = "/org/freedesktop/UPower"; const DISPLAY_DEVICE_PATH: &str = "/org/freedesktop/UPower/devices/DisplayDevice"; const ACPI_POWER_ROOT: &str = "/scheme/acpi/power"; +const ACPI_NOTIFICATIONS: &str = "/scheme/acpi/notifications"; +/// Battery/AC events arrive via AML `Notify` (EC query → `_Qxx` → +/// `Notify(\_SB.BAT0, 0x80)`) and are drained from this file. Polling the +/// drain queue at sub-second cadence turns a 30 s worst-case state lag +/// into a near-instant event. +const NOTIFICATION_POLL_MS: u64 = 250; const DEVICE_KIND_UNKNOWN: u32 = 0; const DEVICE_KIND_LINE_POWER: u32 = 1; @@ -159,6 +165,20 @@ fn read_u64(path: impl AsRef) -> Option { read_trimmed(path)?.parse().ok() } +/// Drain the ACPI notification log (`/scheme/acpi/notifications`): each +/// line is ` ` as delivered by AML `Notify`. +fn read_notifications() -> Vec { + fs::read_to_string(ACPI_NOTIFICATIONS) + .map(|text| { + text.lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(str::to_owned) + .collect() + }) + .unwrap_or_default() +} + fn read_f64(path: impl AsRef) -> Option { read_trimmed(path)?.parse().ok() } @@ -526,6 +546,8 @@ async fn run_daemon() -> Result<(), Box> { let mut last_snapshot = runtime.snapshot(); let mut poll_interval = tokio::time::interval(Duration::from_secs(POLL_INTERVAL_SECS)); + let mut notify_interval = + tokio::time::interval(Duration::from_millis(NOTIFICATION_POLL_MS)); loop { tokio::select! { @@ -536,6 +558,24 @@ async fn run_daemon() -> Result<(), Box> { eprintln!("redbear-upower: shutdown signal received, exiting cleanly"); break; } + _ = notify_interval.tick() => { + let drained = read_notifications(); + if !drained.is_empty() { + eprintln!( + "redbear-upower: {} ACPI notification(s): {}", + drained.len(), + drained.join(", ") + ); + let current_snapshot = runtime.snapshot(); + if current_snapshot != last_snapshot { + eprintln!( + "redbear-upower: power state changed after notification, emitting Changed signal" + ); + let _ = UPowerDaemon::changed(&signal_emitter).await; + last_snapshot = current_snapshot; + } + } + } _ = poll_interval.tick() => { let current_snapshot = runtime.snapshot(); if current_snapshot != last_snapshot { diff --git a/local/sources/base b/local/sources/base index ddcd08709f..f315a9be3b 160000 --- a/local/sources/base +++ b/local/sources/base @@ -1 +1 @@ -Subproject commit ddcd08709fa850dfbff098045edf724b8a944346 +Subproject commit f315a9be3b14b2b1231eec5c17db2130045092ed