Nested gaming session for dual-screen devices (Thor phase 2)#80
Nested gaming session for dual-screen devices (Thor phase 2)#80brycesub wants to merge 12 commits into
Conversation
|
Pushed the refresh-rate bridge, closing the known limitation in the PR description: 🤖 Generated with Claude Code |
|
Will take me some time to review and test all this but appreciate the work! Very exciting for the Thor. |
|
Found during end-to-end image testing: the persistent desktop session brings KDE powerdevil's idle suspend with it, and systemd suspend wedges on these devices (stuck Follow-up needed before this ships as the Thor default: powerdevil defaults that disable idle suspend on nested devices (or wire the desktop session into the fake-suspend flow). I'll push a commit for this. 🤖 Generated with Claude Code |
09fafb4 to
920ef43
Compare
With the bottom panel enabled in the kernel, the top screen becomes DSI-2 and a second backlight (ae94000.dsi.0) appears that Steam picks up for brightness control instead of the top panel's. - ayn-thor.conf: primary connector is DSI-2; declare the primary backlight, secondary connector, and both touchscreen device names - steamos-priv-write: accept backlight brightness writes and steer them to ARMADA_PRIMARY_BACKLIGHT, rescaling between brightness ranges (bottom is 0-255, top is 0-4096) - desktop-bootstrap/setup-dual-screen: one-time desktop layout for dual-screen devices - primary on top at priority 1, secondary centered below, each touchscreen pinned to its own output (KWin otherwise maps both to the same screen) Requires the armada-packages kernel with the Thor bottom panel enabled; on the old kernel DSI-2 does not exist and gamescope falls back to the only connected output, so ordering is safe. Fixes virtudude#28 (phase 1: no bottom-screen UI in gaming mode yet) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
Gamescope only lights up the primary panel, but the secondary touchscreen still feeds it input - fingers resting on the dark glass move Steam's cursor. Inhibit the device (via /sys/class/input/*/inhibited, made wheel-writable like the other session-managed sysfs knobs) when the gaming session starts and re-enable it when the desktop session starts. A future Armada Control toggle could expose this as a user choice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
kscreen-doctor -o -j prints the human-readable listing after the JSON document, so json.loads raised JSONDecodeError and the script died before applying the layout or pinning the touchscreens. On first boot that left both touchscreens mapped to the same output: taps on the top screen landed on the bottom desktop, where a stray double-tap on the Return to Game Mode icon logged the session out. Request JSON alone and parse only the first document so trailing text can never take the script down again. Verified on an AYN Thor: layout applied, both touchscreens pinned to their own panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeRkgMBMwWNcnwMk4j5kVx
The perf-ACL udev rule made every backlight's brightness group-writable, so Steam's direct sysfs write (its first choice before falling back to steamos-polkit-helpers/steamos-priv-write) succeeded on whichever backlight it enumerated first - on Thor the secondary bottom panel. The priv-write steering to ARMADA_PRIMARY_BACKLIGHT never engaged except for out-of-range values, so the brightness slider dimmed the desktop screen instead of the gaming one. Observed on hardware: at Steam startup a saved 0-4096-scale value hit the 255-max bottom panel (EINVAL, then rescaled to 166 and correctly steered 2666 -> top panel via priv-write), but ordinary slider writes were in-range for the bottom panel and landed there directly. The udev grant now goes through backlight-acl, which withholds it from every backlight when ARMADA_PRIMARY_BACKLIGHT is set: all Steam writes fail EACCES and funnel through the steering helper, the same path Steam uses on stock SteamOS where backlight sysfs is not user-writable. Single-backlight devices keep the direct grant. Part of virtudude#28 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011roK8EGErjD8eEjjZNNW9w
With ARMADA_GAMING_SESSION=nested (Thor), gaming mode runs as a nested gamescope fullscreen on the primary screen inside the persistent desktop session, with the plasma desktop live on the secondary screen. Mode switches become near-instant: SwitchToDesktopMode just ends the nested gamescope (the desktop is already underneath) and SwitchToGameMode launches it via a transient user unit. steamos-manager handles the switch in its session-bus instance only; the embedded session-swap path is unchanged for single-screen devices and for the system-bus instance. Validated on AYN Thor: instant switch both directions through Steam's UI, game performance indistinguishable from embedded gaming mode. Part of virtudude#28 phase 2 (design doc in docs/superpowers/specs/) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
armada-session-default resets the SDDM session to "gamemode" each boot; on ARMADA_GAMING_SESSION=nested devices that session is now the plasma session, and desktop-bootstrap launches the nested gamescope on a fresh session start. Boot lands in gaming mode with the desktop already live on the secondary screen, and the embedded gamescope session is no longer used on these devices. Known limitation: nested gaming runs at the mode KWin selected for the primary output (120Hz on Thor); Steam's per-game refresh switching is not bridged yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
Steam communicates refresh switching by setting GAMESCOPE_DYNAMIC_REFRESH on gamescope's Xwayland root; the embedded DRM backend applies it but nested gamescope cannot modeset the host output. nested-refresh-bridge watches the atom on the nested Xwayland (stdbuf -oL: xprop block-buffers into pipes) and applies the nearest supported panel rate via kscreen-doctor, restoring the max rate when dynamic refresh is disabled or the gaming session ends. nested-gaming exports STEAM_DISPLAY_REFRESH_LIMITS so Steam offers the refresh UI, and Steam's in-between requests (e.g. 90 on a 60/120 panel) snap to the nearest mode, ties to the higher. Validated on hardware via the exact atom writes Steam performs: 60 -> panel 60Hz, 90 -> 120Hz, 0 -> 120Hz. Closes the known limitation from the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
Pairs with the armada-packages gamescope patch that reads GAMESCOPE_NESTED_REFRESH_RATES: without it Steam's refresh slider collapses to the single current rate because the nested backend cannot enumerate the host output's modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
Pass --prefer-output with the device's primary connector; the armada gamescope patch makes the nested Wayland backend fullscreen onto that output instead of whichever one the host compositor considered active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
PowerDevil's idle detection does not count gamepad input as activity; with the persistent desktop session hosting nested gaming it suspends the device mid-game (fake-suspend blanks both screens and everything looks wedged until the power button or a reboot). This also explains an intermittent all-black-screens report during prototype testing. Ship a system default that disables automatic suspend on AC and battery; suspend stays available through the power button (powerbuttond -> fake-suspend), and users can re-enable idle suspend in System Settings. Low-battery behavior keeps the upstream default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn
Two clean-install bugs, one root cause each: Steam applies client updates by exiting and relying on its wrapper to relaunch it (sometimes twice in a row, as during first-run onboarding). The embedded session gets this from SDDM Relogin restarting the whole gamescope session; nested mode had no equivalent, so the update left a Steam-less gamescope on screen and a dead gaming mode. Gaming mode now runs as armada-nested-gaming.service (user unit) with Restart=always: nested-gaming launches gamescope with the -R readiness pipe (the same handshake gamescope-session-plus uses), runs Steam as its own child with the environment gamescope would hand it (DISPLAY, GAMESCOPE_WAYLAND_DISPLAY, host WAYLAND_DISPLAY unset), and exits when Steam does so systemd brings the pair back up. StartLimit caps crash loops and PartOf= graphical-session.target stops the unit at logout instead of letting it thrash without a compositor. The desktop "Return to Game Mode" icon went steamos-session-select → os-session-select → sudo session-control switch-gamemode: a full plasma logout + SDDM relogin (~20s of desktop) even on nested devices, so impatient taps killed the Steam that was already starting - mid update-install during onboarding. os-session-select now starts/stops the unit directly when the desktop compositor is alive, like steamos-manager does; both switch directions are instant and idempotent, which also makes the icon's double-activation per tap harmless. steamos-manager and desktop-bootstrap converge on the same unit, replacing the transient systemd-run instances and the pgrep guard they needed. Validated on AYN Thor: Steam exit auto-relaunches in ~2s into a fresh gamescope; icon and D-Bus switches instant both ways including double-fired taps; explicit stop is never restarted. Part of virtudude#28 phase 2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011roK8EGErjD8eEjjZNNW9w
920ef43 to
c4c1dd4
Compare
A gamescope-wl the unit does not own (stale instance from an older deployment, manual launch) survived SwitchToDesktopMode's unit stop and left Steam waiting on "Switching to desktop..." indefinitely. Both switch paths now pkill gamescope-wl after stopping the unit; for unit-owned processes this duplicates the SIGTERM systemd already sent, so it is harmless in the normal case. Part of virtudude#28 phase 2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011roK8EGErjD8eEjjZNNW9w
Phase 2 of AYN Thor dual-screen support (#28): gaming mode with both screens live — Steam on the top screen, a full plasma desktop on the bottom — and near-instant mode switching.
Builds on #78 (its two commits are included here; this PR reduces to the two
thor-nested-gamingcommits once #78 merges). Also needs virtudude/armada-packages#2 (gamescopewl_touchpatch), or the top screen loses touch in nested mode.Architecture
With
ARMADA_GAMING_SESSION=nested(set for Thor), the compositor stack is inverted: KWin stays the only DRM master driving both panels, and gaming mode runs as a nested gamescope fullscreen window on the primary screen (/usr/libexec/armada/nested-gaming).steamos-manager's session-bus instance handles Steam'sSwitchToDesktopMode/SwitchToGameModeby stopping/starting the nested gamescope — the desktop is already underneath, so switching is near-instant (no SDDM session swap, no logout). Single-screen devices and the system-bus instance keep the existing session-swap path.armada-session-defaultstill resets to "gamemode" each boot; on nested devices that session is nowarmada-plasma, anddesktop-bootstraplaunches the nested gamescope on fresh session start. Boot lands in gaming mode with the desktop live on the bottom screen.Validated on hardware (AYN Thor)
Refresh switching
Steam's per-game refresh switching works through
nested-refresh-bridge: it watches theGAMESCOPE_DYNAMIC_REFRESHatom Steam sets on the nested Xwayland root and applies the nearest supported panel mode via kscreen-doctor (restoring the max rate when dynamic refresh is off or the session ends).Design doc:
docs/superpowers/specs/(phase 2) — happy to include it in the PR if wanted.🤖 Generated with Claude Code
https://claude.ai/code/session_01XMbeBSJJG1pNfdqkUcG1gn