Skip to content
6 changes: 6 additions & 0 deletions system_files/etc/gamescope-session-plus/sessions.d/steam
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ if [[ -n "${ARMADA_PANEL_REFRESH_RATES:-}" ]]; then
unset STEAM_DISPLAY_REFRESH_LIMITS
fi
fi
# Gamescope only lights up the primary panel; silence the secondary
# touchscreen so fingers on the dark glass don't move Steam's cursor.
# The desktop session re-enables it (see desktop-bootstrap).
if [[ -n "${ARMADA_SECONDARY_TOUCHSCREEN:-}" ]]; then
/usr/libexec/armada/touchscreen-inhibit "$ARMADA_SECONDARY_TOUCHSCREEN" 1 2>/dev/null || true
fi
USE_ROTATION_SHADER="${ARMADA_GAMESCOPE_USE_ROTATION_SHADER:-0}"
if [[ -n "${ARMADA_GAMESCOPE_FAKE_OUTPUT_MM:-}" ]]; then
export GAMESCOPE_FAKE_OUTPUT_MM="$ARMADA_GAMESCOPE_FAKE_OUTPUT_MM"
Expand Down
10 changes: 10 additions & 0 deletions system_files/etc/xdg/powerdevilrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Armada: no automatic idle suspend. PowerDevil's idle detection does not
# count gamepad input as activity, so with the persistent desktop session
# (nested gaming) it would suspend mid-game. Suspend stays available via the
# power button (powerbuttond -> fake-suspend). Users can still override this
# per-user in System Settings.
[AC][SuspendAndShutdown]
AutoSuspendAction=0

[Battery][SuspendAndShutdown]
AutoSuspendAction=0
25 changes: 25 additions & 0 deletions system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,30 @@ if [[ "$WRITE_PATH" == /sys/class/hwmon/hwmon*/power*_cap ]]; then
exit 1
fi

if [[ "$WRITE_PATH" == /sys/class/backlight/*/brightness ]]; then
[[ "$WRITE_VALUE" =~ ^[0-9]+$ ]] || exit 1
# Steam adjusts the first backlight it finds; on dual-screen devices
# that can be the secondary panel. Steer writes to the primary
# backlight, rescaling between the two devices' brightness ranges.
eval "$(/usr/libexec/armada/device-env)"
src_dir="$(dirname "$WRITE_PATH")"
tgt_dir="$src_dir"
if [[ -n "${ARMADA_PRIMARY_BACKLIGHT:-}" ]]; then
tgt_dir="/sys/class/backlight/${ARMADA_PRIMARY_BACKLIGHT}"
fi
[[ -e "$tgt_dir/brightness" ]] || exit 1
val="$WRITE_VALUE"
if [[ "$tgt_dir" != "$src_dir" && -e "$src_dir/max_brightness" ]]; then
src_max="$(cat "$src_dir/max_brightness")"
tgt_max="$(cat "$tgt_dir/max_brightness")"
if [[ "$src_max" -gt 0 && "$src_max" -ne "$tgt_max" ]]; then
val=$(( val * tgt_max / src_max ))
fi
fi
echo "$val" > "$tgt_dir/brightness"
log "backlight: $WRITE_VALUE -> $tgt_dir/brightness ($val)"
exit 0
fi

echo "refusing unsupported write: $WRITE_VALUE -> $WRITE_PATH" >&2
exit 1
10 changes: 9 additions & 1 deletion system_files/usr/lib/armada/devices/ayn-thor.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ ARMADA_DEVICE_ID=ayn-thor
ARMADA_DEVICE_NAME='AYN Thor'
ARMADA_SOC_CLASS=SM8550

ARMADA_PRIMARY_CONNECTOR=DSI-1
# Dual screen: top panel is DSI-2 (mdss_dsi1), bottom panel is DSI-1
# (mdss_dsi0). Gamescope drives only the top panel; the desktop session
# uses both (see desktop-bootstrap).
ARMADA_PRIMARY_CONNECTOR=DSI-2
ARMADA_PRIMARY_BACKLIGHT=ae96000.dsi.0
ARMADA_PRIMARY_TOUCHSCREEN=top_touchscreen
ARMADA_SECONDARY_CONNECTOR=DSI-1
ARMADA_SECONDARY_TOUCHSCREEN=bottom_touchscreen
ARMADA_GAMING_SESSION=nested
ARMADA_PANEL_ORIENTATION=right
ARMADA_PANEL_NATIVE_WIDTH=1080
ARMADA_PANEL_NATIVE_HEIGHT=1920
Expand Down
13 changes: 13 additions & 0 deletions system_files/usr/lib/armada/devices/defaults.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ ARMADA_DEVICE_NAME=unknown
ARMADA_SOC_CLASS=

ARMADA_PRIMARY_CONNECTOR=
# Backlight Steam brightness writes are steered to. Only needed on devices
# with more than one backlight (Steam grabs the first one it finds).
ARMADA_PRIMARY_BACKLIGHT=
# Set on dual-screen devices; the desktop session extends onto this output.
ARMADA_SECONDARY_CONNECTOR=
# Input device names used to pin each touchscreen to its own output in the
# desktop session (only needed when a secondary connector is set).
ARMADA_PRIMARY_TOUCHSCREEN=
ARMADA_SECONDARY_TOUCHSCREEN=
# embedded (default): gamescope owns the GPU in gaming mode.
# nested: gaming mode runs as a nested gamescope inside the desktop session
# (dual-screen devices), making mode switches near-instant.
ARMADA_GAMING_SESSION=embedded
ARMADA_PANEL_TYPE=internal
ARMADA_PANEL_ORIENTATION=normal
ARMADA_PANEL_NATIVE_WIDTH=
Expand Down
3 changes: 3 additions & 0 deletions system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ SUBSYSTEM=="devfreq", KERNEL=="*.gpu", \

SUBSYSTEM=="backlight", \
RUN+="/bin/sh -c 'chgrp wheel /sys/class/backlight/%k/brightness 2>/dev/null; chmod g+w /sys/class/backlight/%k/brightness 2>/dev/null'"

SUBSYSTEM=="input", KERNEL=="input[0-9]*", \
RUN+="/bin/sh -c 'chgrp wheel /sys/class/input/%k/inhibited 2>/dev/null; chmod g+w /sys/class/input/%k/inhibited 2>/dev/null'"
32 changes: 27 additions & 5 deletions system_files/usr/libexec/armada/desktop-bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@ else
systemctl --user import-environment "${activation_env[@]}" 2>/dev/null || true
fi

if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then
eval "$(/usr/libexec/armada/device-env)"
fi

# The gaming session inhibits the secondary touchscreen (the desktop is the
# only session that lights up the secondary panel); re-enable it every start.
if [[ -n "${ARMADA_SECONDARY_TOUCHSCREEN:-}" ]]; then
/usr/libexec/armada/touchscreen-inhibit "$ARMADA_SECONDARY_TOUCHSCREEN" 0 2>/dev/null || true
fi

# Nested-gaming devices boot into this session with gaming on the primary
# screen; a fresh session start is either boot or an explicit return to
# gamemode (Switch to Desktop keeps the session and never re-runs this).
if [[ ${ARMADA_GAMING_SESSION:-} == nested ]] && ! pgrep -x gamescope-wl >/dev/null; then
systemd-run --user --collect /usr/libexec/armada/nested-gaming 2>/dev/null || true
fi

config_dir="${XDG_CONFIG_HOME:-${HOME}/.config}/armada"
rotation_done="${config_dir}/desktop-rotation.done"
scale_done="${config_dir}/desktop-scale.done"
dual_done="${config_dir}/desktop-dualscreen.done"

[[ ! -e "${rotation_done}" || ! -e "${scale_done}" ]] || exit 0
[[ ! -e "${rotation_done}" || ! -e "${scale_done}" || ! -e "${dual_done}" ]] || exit 0
mkdir -p "${config_dir}"

if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then
eval "$(/usr/libexec/armada/device-env)"
fi

display_connector="${ARMADA_PRIMARY_CONNECTOR:-}"
panel_orientation="${ARMADA_PANEL_ORIENTATION:-normal}"

Expand All @@ -57,3 +71,11 @@ fi
if [[ ! -e "${scale_done}" ]] && kscreen-doctor "output.${display_connector}.scale.1.5" >/dev/null 2>&1; then
touch "${scale_done}"
fi

if [[ ! -e "${dual_done}" ]]; then
if [[ -z "${ARMADA_SECONDARY_CONNECTOR:-}" ]]; then
touch "${dual_done}"
elif /usr/libexec/armada/setup-dual-screen; then
touch "${dual_done}"
fi
fi
5 changes: 5 additions & 0 deletions system_files/usr/libexec/armada/device-env
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ vars=(
ARMADA_DEVICE_NAME
ARMADA_SOC_CLASS
ARMADA_PRIMARY_CONNECTOR
ARMADA_PRIMARY_BACKLIGHT
ARMADA_SECONDARY_CONNECTOR
ARMADA_PRIMARY_TOUCHSCREEN
ARMADA_SECONDARY_TOUCHSCREEN
ARMADA_GAMING_SESSION
ARMADA_PANEL_TYPE
ARMADA_PANEL_ORIENTATION
ARMADA_PANEL_NATIVE_WIDTH
Expand Down
40 changes: 40 additions & 0 deletions system_files/usr/libexec/armada/nested-gaming
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/bash
# Gaming mode on dual-screen devices (ARMADA_GAMING_SESSION=nested): run
# gamescope+Steam as a nested fullscreen window on the primary screen of the
# already-running desktop session. steamos-manager starts and stops this;
# see docs/superpowers/specs/2026-07-07-thor-second-screen-phase2-design.md.
set -euo pipefail

pgrep -x gamescope-wl >/dev/null && exit 0

eval "$(/usr/libexec/armada/device-env)"

# The desktop shows rotated panels in landscape; gamescope renders landscape.
width="${ARMADA_PANEL_NATIVE_WIDTH:-1080}"
height="${ARMADA_PANEL_NATIVE_HEIGHT:-1920}"
case "${ARMADA_PANEL_ORIENTATION:-normal}" in
left|right) gs_w="$height" gs_h="$width" ;;
*) gs_w="$width" gs_h="$height" ;;
esac

# Expose the refresh range so Steam offers per-game refresh switching;
# nested-refresh-bridge applies requests to the host compositor.
if [[ -n "${ARMADA_PANEL_REFRESH_RATES:-}" ]]; then
IFS=',' read -r -a _rates <<< "$ARMADA_PANEL_REFRESH_RATES"
if (( ${#_rates[@]} > 1 )); then
export STEAM_DISPLAY_REFRESH_LIMITS="${_rates[0]},${_rates[-1]}"
fi
# Advertised to Steam via gamescope_control (armada gamescope patch);
# the Wayland backend cannot enumerate the host output's modes itself.
export GAMESCOPE_NESTED_REFRESH_RATES="$ARMADA_PANEL_REFRESH_RATES"
/usr/libexec/armada/nested-refresh-bridge &
fi

# --prefer-output pins the fullscreen window to the primary screen (armada
# gamescope patch: the Wayland backend passes it to the fullscreen request;
# otherwise the host compositor picks whichever output was last active).
prefer_output=()
[[ -n "${ARMADA_PRIMARY_CONNECTOR:-}" ]] && prefer_output=(--prefer-output "$ARMADA_PRIMARY_CONNECTOR")

exec gamescope -e --fullscreen -W "$gs_w" -H "$gs_h" --steam "${prefer_output[@]}" -- \
/usr/libexec/armada/launch-steam -gamepadui -steamos3 -steampal -steamdeck -noverifyfiles
71 changes: 71 additions & 0 deletions system_files/usr/libexec/armada/nested-refresh-bridge
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/bash
# Bridge Steam's refresh-rate requests to KWin for the nested gaming session.
#
# In the embedded session gamescope owns the display and applies
# GAMESCOPE_DYNAMIC_REFRESH (set by Steam on gamescope's Xwayland root)
# itself; nested gamescope cannot modeset the host output. Watch the atom on
# the nested Xwayland and apply the nearest supported panel rate through
# kscreen-doctor. Spawned by nested-gaming; exits with the session.
set -euo pipefail

eval "$(/usr/libexec/armada/device-env)"

[[ ${ARMADA_GAMING_SESSION:-} == nested ]] || exit 0
[[ -n ${ARMADA_PRIMARY_CONNECTOR:-} && -n ${ARMADA_PANEL_REFRESH_RATES:-} ]] || exit 0
[[ -n ${ARMADA_PANEL_NATIVE_WIDTH:-} && -n ${ARMADA_PANEL_NATIVE_HEIGHT:-} ]] || exit 0

IFS=',' read -r -a rates <<< "$ARMADA_PANEL_REFRESH_RATES"
max_rate="${rates[-1]}"
mode_res="${ARMADA_PANEL_NATIVE_WIDTH}x${ARMADA_PANEL_NATIVE_HEIGHT}"

# The nested gamescope's Xwayland is where Steam sets the atom; find its
# display number from Steam's environment.
nested_display=""
for _ in $(seq 1 60); do
steam_pid=$(pgrep -x steam | head -1 || true)
if [[ -n "$steam_pid" ]]; then
nested_display=$(tr '\0' '\n' < "/proc/${steam_pid}/environ" 2>/dev/null \
| sed -n 's/^DISPLAY=//p' | head -1)
[[ -n "$nested_display" ]] && break
fi
sleep 2
done
[[ -n "$nested_display" ]] || exit 0

snap_rate() {
local want=$1 best="${rates[0]}" best_delta=-1 rate delta
for rate in "${rates[@]}"; do
delta=$(( want > rate ? want - rate : rate - want ))
# Ties go to the higher rate: rates are listed ascending.
if (( best_delta < 0 || delta <= best_delta )); then
best=$rate best_delta=$delta
fi
done
printf '%s' "$best"
}

current=""
apply() {
local hz=$1
[[ "$hz" =~ ^[0-9]+$ ]] || hz=0
(( hz == 0 )) && hz=$max_rate
hz=$(snap_rate "$hz")
[[ "$hz" == "$current" ]] && return 0
if kscreen-doctor "output.${ARMADA_PRIMARY_CONNECTOR}.mode.${mode_res}@${hz}" >/dev/null 2>&1; then
current=$hz
fi
}

# Seed the atom so xprop -spy has a property to watch from the start.
xprop -display "$nested_display" -root -f GAMESCOPE_DYNAMIC_REFRESH 32c \
-set GAMESCOPE_DYNAMIC_REFRESH 0 2>/dev/null || exit 0

# stdbuf: xprop block-buffers when piped, which would delay events forever.
stdbuf -oL xprop -display "$nested_display" -root -spy GAMESCOPE_DYNAMIC_REFRESH 2>/dev/null \
| while read -r line; do
apply "${line##*= }"
done

# Nested Xwayland is gone (gaming session ended): restore the default rate.
current=""
apply "$max_rate"
13 changes: 11 additions & 2 deletions system_files/usr/libexec/armada/session-control
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
# zz- sorts after armada.conf so its Session= wins (User=/Relogin= stay there).
set -euo pipefail

# On nested-gaming devices the desktop session hosts gaming mode as a
# nested gamescope (launched by desktop-bootstrap), so "gamemode" boots
# into the plasma session too.
gamemode_session=gamescope-session-steam.desktop
if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then
eval "$(/usr/libexec/armada/device-env)"
[[ ${ARMADA_GAMING_SESSION:-} == nested ]] && gamemode_session=armada-plasma.desktop
fi

case "${1:-}" in
switch-desktop) session=armada-plasma.desktop ;;
switch-gamemode) session=gamescope-session-steam.desktop ;;
default-gamemode) session=gamescope-session-steam.desktop; default_only=1 ;;
switch-gamemode) session=$gamemode_session ;;
default-gamemode) session=$gamemode_session; default_only=1 ;;
*) echo "usage: $0 {switch-desktop|switch-gamemode|default-gamemode}" >&2; exit 1 ;;
esac

Expand Down
Loading