Skip to content

Add AYN button toggle for a bottom-screen performance HUD#90

Open
brycesub wants to merge 35 commits into
virtudude:mainfrom
brycesub:bottom-screen-perf
Open

Add AYN button toggle for a bottom-screen performance HUD#90
brycesub wants to merge 35 commits into
virtudude:mainfrom
brycesub:bottom-screen-perf

Conversation

@brycesub

Copy link
Copy Markdown

Summary

  • add an AYN-button listener that swaps MangoApp between the nested top-screen overlay and a full-stat bottom-screen HUD
  • pin the desktop MangoApp to the Thor's DSI-1 panel, force fullscreen behavior, and prevent it from stealing focus
  • merge packaged KWin rules into the active per-user rulebook before nested gaming starts
  • bind both HUD services to nested gaming and include VRAM in the bottom-screen statistics

Depends on #80, which provides the nested-gaming session this HUD integrates with.

Validation

  • all tests/*.sh
  • ShellCheck on the HUD, nested-gaming, bootstrap, and test scripts
  • just check
  • device-side systemd-analyze --user verify for both HUD units
  • live AYN Thor validation: bottom window measured 1240x1080 at the DSI-1 offset, remained fullscreen/above/skip-taskbar, and did not become the active window

brycesub and others added 29 commits July 14, 2026 20:58
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
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
KWin asks the kscreen OSD service to show its layout picker whenever
two outputs appear in a combination it has not stored yet (kwin
workspace.cpp, updateOutputConfiguration). On dual-screen devices that
is the very first boot, so the picker pops up over Steam onboarding.
The kded kscreen module is not involved on Wayland; KWin has no config
knob for this.

Mask the user service instead: KWin's D-Bus call is fire-and-forget,
so the only trace is a one-line activation failure from dbus-broker.
Display Settings still work; only the picker OSD (and the output
identify overlay) go away.

Validated on the AYN Thor: with kwinoutputconfig.json absent, the
journal shows the activation request refused and no OSD appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASwEJacP64yJTjo66FRdu9
Gaming mode covers the top screen, so the desktop panel and icons
belong on the bottom one, DS-style. Make the secondary output the
priority-1 display and scale it to 150% to match the primary. Rotate
it per the new ARMADA_SECONDARY_ORIENTATION (the Thor's bottom panel
is portrait-native like the top), and center it below the primary in
post-scale logical coordinates.

Validated on the AYN Thor: DSI-1 lands at priority 1, scale 1.5,
rotation right, position 226,720 (centered under the 1280x720 top
screen).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASwEJacP64yJTjo66FRdu9
Drop the standalone off state; Steam's slider already covers hiding the
top overlay, so the button just switches which screen owns the HUD.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
Also correct the spec: hud-top sources the session env file itself
instead of EnvironmentFile=, which would silently fall back to the
desktop DISPLAY the user manager holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
Conflicts= carries no ordering, so a toggle could briefly run both
MangoApps against gamescope's single-consumer stats queue. An After=
edge between the HUD units makes systemd finish the old HUD's stop
before the new one starts, in both directions.

Also tighten the listener-guard test literal and fold the review's
focus-steal and slider-tracking checks into the validation checklist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YYLGYK7FVvW3jP7WVrNjg
@brycesub brycesub force-pushed the bottom-screen-perf branch from c5cebae to 8de7ea2 Compare July 15, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant