diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f2fd4b02..9fe245284a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ for finer detail. ## Unreleased ### Fixed +- The doctor's reverse-PRIME warning (#270, a black panel after login on a + hybrid-GPU laptop) now also fires when a forced or drifted GPU render pin + coexists with a stored hybrid or passthrough choice, which previously + silenced the check. - Lock, suspend, lid-close and wake now share one fail-closed lifecycle across Hyprland and niri. Lockscreen generations change atomically during updates, every sleep waits for compositor-secure qylock, and output recovery retries diff --git a/ryoku/cli/CHANGELOG.md b/ryoku/cli/CHANGELOG.md index bf638e1db8..a35d546d0c 100644 --- a/ryoku/cli/CHANGELOG.md +++ b/ryoku/cli/CHANGELOG.md @@ -33,6 +33,13 @@ (`internal/doctor/reconcile_boot_rw.go`). ### Fixed +- **The doctor's reverse-PRIME guard (#270) now sees the pin the machine + actually runs.** The render-pin-vs-panel check resolved the pin through + `ryoku-gpu order`, the policy's recommendation, which refuses to speak + whenever a stored hybrid or passthrough choice opts out: a RYOKU_GPU_FORCE + pin kept under a hybrid stamp, or one drifted in by an older release, hid + from the guard while the panel sat on the iGPU under a NVIDIA-first pin. + The check now reads `order --effective`, the pin file's own value. - **`ryoku update` no longer dies where taking a sleep inhibitor is denied.** The transaction runs under `systemd-inhibit --mode=block`, which is polkit-gated in sessions with no agent (SSH, a headless run): there it exits diff --git a/ryoku/cli/internal/doctor/reconcile_gpu_pin_panel.go b/ryoku/cli/internal/doctor/reconcile_gpu_pin_panel.go index 75db8071c2..500c0a5e27 100644 --- a/ryoku/cli/internal/doctor/reconcile_gpu_pin_panel.go +++ b/ryoku/cli/internal/doctor/reconcile_gpu_pin_panel.go @@ -29,9 +29,13 @@ var drmRoot = "/sys/class/drm" // pinFirstDriver resolves the leftmost entry of the effective AQ_DRM_DEVICES // pin to its kernel driver, "" when there is no pin or it cannot be resolved. // The verdict comes from ryoku-gpu itself (the same tool the stale-pin -// reconciler trusts), so this never re-implements the pin policy. +// reconciler trusts), so this never re-implements the pin policy. --effective +// reads the pin the compositor would actually read, straight from the pin +// file: the plain order is the policy's recommendation and refuses to speak +// exactly when a stored hybrid/passthrough choice coexists with a pin: the +// drifted and forced-pin states the #270 guard most needs to see. var pinFirstDriver = func() string { - out, err := sys.RunOut("ryoku-gpu", "order") + out, err := sys.RunOut("ryoku-gpu", "order", "--effective") if err != nil { return "" } diff --git a/system/CHANGELOG.md b/system/CHANGELOG.md index 46356cb965..73e52276dd 100644 --- a/system/CHANGELOG.md +++ b/system/CHANGELOG.md @@ -98,6 +98,12 @@ package. ### Fixed +- `hardware/gpu/ryoku-gpu`: `order --effective [PATH]` prints the AQ_DRM_DEVICES + value the compositor would actually read, straight from the pin file, while + the plain `order` keeps reporting the policy's recommendation (and keeps + exiting 1 under a stored hybrid/passthrough choice). doctor's reverse-PRIME + guard (#270) audits the machine as it is, and previously went blind exactly + when a forced or drifted pin coexisted with an opt-out mode stamp. - `hardware/power/ryoku-power-cutover`: a killed generation guard releases its locks. The hold's keep-alive coprocess inherited the launch and generation flock fds, so a holder killed mid-swap left the locks pinned by diff --git a/system/hardware/gpu/ryoku-gpu b/system/hardware/gpu/ryoku-gpu index 8e9210df4c..af1554fc8d 100755 --- a/system/hardware/gpu/ryoku-gpu +++ b/system/hardware/gpu/ryoku-gpu @@ -223,6 +223,21 @@ cmd_detect() { } cmd_order() { + local dest="${1:-}" + # --effective: the AQ_DRM_DEVICES value the compositor would actually read + # at its next start, straight out of the pin file, or exit 1 when there is + # none. Deliberately indifferent to the stored mode choice: a forced pin + # kept under a hybrid stamp, or a pin drifted in by an older release, is + # exactly what the doctor's render-pin-vs-panel guard (#270) has to see, + # and the plain order (the policy's recommendation) refuses to speak in + # those states. + if [[ $dest == "--effective" ]]; then + local conf="${2:-$GPU_CONF_DEFAULT}" value + value="$(sed -n 's/^hl\.env("AQ_DRM_DEVICES", *"\([^"]*\)")$/\1/p' "$conf" 2>/dev/null | head -n1)" + [[ -n $value ]] || return 1 + printf '%s\n' "$value" + return 0 + fi analyze if ! beneficial; then echo "ryoku-gpu: no discrete-GPU pin needed on this machine" >&2 @@ -457,7 +472,10 @@ Pin the strongest GPU as Hyprland's primary render device on multi-GPU machines. Commands: detect [--json] Show detected GPUs (strongest first) and the chosen primary. - order Print the recommended AQ_DRM_DEVICES value (exit 1 if none). + order [--effective [PATH]] + Print the recommended AQ_DRM_DEVICES value (exit 1 if none); + with --effective, print the pin file's own value instead + (what the compositor would read), exit 1 when unpinned. persist [PATH] Write the Hyprland pin (Lua) to PATH, to ~/.config/hypr/gpu.lua by default, or to stdout when PATH is '-'. disable [PATH] Reset the pin file to a managed comment-only placeholder. @@ -475,7 +493,7 @@ case "${1:-status}" in detect) if [[ "${2:-}" == "--json" ]]; then gpu_records_json; else cmd_detect; fi ;; - order) cmd_order ;; + order) cmd_order "${2:-}" ;; persist) cmd_persist "${2:-}" ;; install-udev) cmd_install_udev ;; status) cmd_status ;; diff --git a/tests/gpu-pin-policy.sh b/tests/gpu-pin-policy.sh index cbaf7d3eed..8e7b67599f 100755 --- a/tests/gpu-pin-policy.sh +++ b/tests/gpu-pin-policy.sh @@ -133,4 +133,21 @@ egpu() { RYOKU_GPU_DRM_ROOT="$drme" RYOKU_GPU_DRI_DIR="$dri" RYOKU_GPU_CONF="$tm egpu persist >/dev/null [[ "$(egpu check-pin)" == "ok" ]] || fail "eGPU pin on a laptop must be ok, got $(egpu check-pin)" +# --- 10. order --effective sees a pin the policy order refuses to speak about: +# a RYOKU_GPU_FORCE pin kept under a stored hybrid choice is the drifted state +# behind #270, and doctor's render-pin-vs-panel guard must still see it. +run mode hybrid >/dev/null +RYOKU_GPU_FORCE=1 run persist >/dev/null # forced pin, hybrid stamp kept +grep -Eq '^hl\.env\("AQ_DRM_DEVICES"' "$conf" || fail "force wrote no pin under hybrid" +if run order >/dev/null 2>&1; then + fail "plain order must keep refusing under a stored hybrid choice" +fi +v="$(run order --effective)" \ + && [[ "$v" == *"ryoku-gpu-0000-02-00-0:"*"ryoku-gpu-0000-01-00-0" ]] \ + || fail "order --effective must print the pin file's value, got ${v:-}" +run disable >/dev/null +if run order --effective >/dev/null 2>&1; then + fail "order --effective must exit 1 on an unpinned machine" +fi + echo "gpu-pin-policy: all cases passed"