Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/LOCK-DPMS-LESSONS.md
Fimeg 45a4e44392 docs: lock-before-blank is an invariant; note the missing deploy lease
LOCK-DPMS-LESSONS §1: the idle clause described 300s-lock-before-600s-blank,
which no longer exists. sessiond enforces the ordering now.

TASK-08: record that (e) is still missing — restarting the shell over a locked
session still hits "Tried to show lockscreen surfaces without active lock".
Reproduced during this deploy.
2026-07-25 19:19:47 -04:00

7.4 KiB

Lock / DPMS / wake — the lessons doc

The hard-won rules for everything between "screen goes dark" and "user is back in." Doctrine lives in SESSION-AUTHORITY-DOCTRINE.md; the boot choreography lives in session-authority-boot-order.md; the raw archaeology lives in DUMP-power-idle-lock-2026-07-15.md. This page is the distilled operating knowledge — what we know is true on blueline, with the receipts.

1. Ordering: lock, then off

Blanking an unlocked session leaks the desktop for a frame on wake, and a wake that races the lock can land input on an unlocked surface.

  • Power button (blueline-power-button): screen on → session lock IPC → wait ≤2s for the compositor-acked "locked": true → blank. If the lock IPC fails, blank anyway — dark-but-unlocked is recoverable, lit-and- unlocked in a pocket is not.
  • Suspend: PrepareForSleep → lock secure → release the delay inhibitor. Fail-closed, ~5s budget.
  • Idle: sessiond enforces it. Every path to a dark panel routes through request_blank() in device_state.rs, which locks first, waits LOCK_ACK_BUDGET (2s) for the compositor ack, and blanks anyway on timeout while recording an error-security — the panel fails open, the claim never does (doctrine §8).

This used to be a coincidence and is now an invariant (2026-07-25). The line here previously read "the native IdleCoordinator locks (300s) well before hypridle's dpms-off (600s), so idle blanking always finds the session already locked." That was true only because 300 < 600 — two numbers in a config file. Anything that skipped the 300s lock (an idle inhibitor, the native coordinator disabled, a dead shell) still met the 600s blank, and the screen went dark on an unlocked session with nothing said about it. hypridle's listeners are now deleted (Pixel3Arch cadaec9); do not re-add one. A timer in that file cannot see the lock, and that blindness is the bug.

2. One DPMS authority

blueline-screen-toggle serializes every panel power transition. Nothing else calls hyprctl dispatch dpms — when hypridle still had a raw dpms call after the toggle became authority, double-tap wake raced on-resume and the touch controller calibrated against a half-ramped panel (Pixel3Arch 73d0cd0). Any new wake source (power button, dt2w, proximity) goes through the toggle, or it will reintroduce the race.

Proximity landed 2026-07-20 (Pixel3Arch): blueline-screen-toggle itself gates DT2W wake on ProximityNear, and a new blueline-proximity-lock user service (same monitor-sensor watch pattern as blueline-autorotate) blanks an already-on locked screen the moment proximity goes near, via blueline-screen-toggle off — no second DPMS authority introduced. Gated on session state's .locked from the souveraine IPC; unlocked screens are untouched. iio-sensor-proxy is now always-on rather than start/stop on demand (the old wake-lockup hazard tied to it is confirmed gone — see Pixel3Arch PAF/slpi.md).

3. The FTS touch controller has opinions

  • On panel-unprepare it drops to gesture mode (that is what makes dt2w work while the screen is off).
  • On sense-on it self-calibrates against the panel. If the panel is still ramping, calibration is garbage and touch goes deaf or erratic. The kernel signature is a continuous spew of fts_status_event_handler: … invalid … Strength soft Force cal. A single line after a wake is noise; a stream that keeps flowing minutes later is a wedged controller.
  • The serialized DPMS path fixed this for screen-off/on cycles. The suspend-resume path still races (found 2026-07-17): s2idle exit re-runs panel-prepare → FTS reset → Sense ON with nobody pacing it, and a dt2w wake from suspend produced exactly the wedged-controller spew and a dead swipe-to-unlock on the lockscreen. Open gap; candidate fixes are kernel-side (pace sense-on after panel ramp — check what Android does, Android is the reference) or a post-resume rebind hook, which needs care because rebinding while the panel is off may drop the dt2w feature flags.
  • Recovery without reboot: echo 2-0049 > /sys/bus/i2c/drivers/fts/unbind && sleep 1 && echo 2-0049 > /sys/bus/i2c/drivers/fts/bind (root). Firmware re-verifies, sense restarts. Expect a line or two of invalid-cal immediately after; continuous spew means it did not take.

4. Suspend truths

  • Suspend is s2idle. The USB gadget (usb0 NCM/ACM) dies with it — an ssh session dropping and usb0: Lost carrier in the journal during suspend entry is normal, not a crash. Do not chase gadget "flaps" without first checking journalctl for PM: suspend entry.
  • dt2w wakes the phone from s2idle (gesture mode survives suspend).
  • NetworkManager/ModemManager sleep-monitor churn around suspend entry is choreography, not failure.
  • Warm reboots poison TZ/remoteproc state on blueline — recovery from a bad state is a true cold boot (unplugged power-off), never reboot.

5. Lock architecture (who holds what)

  • souveraine-sessiond takes ext-session-lock at boot before the shell exists; no desktop surface can precede the lock. The shell inherits the lock later over the bridge socket (misc.allow_session_lock_restore).
  • The sessiond↔shell handoff necessarily drops one lock client for ~1.05s. Hyprland's lockdead_screen_delay default (1000ms) painted a red "lockscreen crashed" flash in that gap; it is 4000ms in the tracked hyprland.lua. The session stays locked throughout — the flash was cosmetic.
  • Shell heartbeat loss → sessiond fail-closes: re-acquires the lock and presents its bare fallback PIN surface. Proven in anger 2026-07-17 when the shell died silently post-unlock. PAM at the fallback surface is a real unlock.
  • screenLocked (request) and screenLockSecure (compositor ack) are different facts. Personal-tier gates key off secure. The compositor can end a lock unilaterally (ext_session_lock_v1_finished); the shell resyncs screenLocked when that happens (LockScreen.qml onLockStateChanged) — before that fix, a stale lockRequested:true both lied to the gates and blocked re-lock.
  • Unlock is never an agent verb.

6. Shell surface rules

  • Lock-path QML deploys only while the phone is UNLOCKED. WlSessionLock.surfaceComponent cannot change while the lock is active; hot-deploying over an active lock wedges the lockscreen until reboot.
  • Two-stage lock: glance first, PIN pad on swipe-up (60px) / tap / any key; retreats on swipe-down (80px, empty PIN only) or 25s idle.
  • hypridle's after_sleep_cmd fires the lockFocus global shortcut on EVERY wake. It must only refocus the (hidden) PIN field — refocus must never imply PIN reveal, or every wake would skip the glance stage.
  • The shell runs under souveraine-shell.service (journal keeps stderr, restart on failure, non-clean exits append to ~/.local/state/souveraine/crashes.log, gives up after 8 tries and leaves the fail-closed lock in charge). It was a raw exec once; the crash reason died with it and unlock landed on a black desktop.

7. Open gaps

  • Suspend-resume FTS calibration race (§3) — the one live regression.
  • Crash reporting (queue item 10): crashes.log + coredumps + failed units surfaced as banner/sensoria, not just survived.
  • Proximity wake must never touch lock state (DUMP-power-idle-lock-2026-07-15.md §2) — same doctrine as dt2w: wake sources move the panel, only sessiond moves the lock.