diff --git a/system_files/etc/gamescope-session-plus/sessions.d/steam b/system_files/etc/gamescope-session-plus/sessions.d/steam index 258bf80..8a6e958 100644 --- a/system_files/etc/gamescope-session-plus/sessions.d/steam +++ b/system_files/etc/gamescope-session-plus/sessions.d/steam @@ -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" diff --git a/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write b/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write index 412df77..a039f53 100755 --- a/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write +++ b/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write @@ -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 diff --git a/system_files/usr/lib/armada/devices/ayn-thor.conf b/system_files/usr/lib/armada/devices/ayn-thor.conf index b063aab..0a84172 100644 --- a/system_files/usr/lib/armada/devices/ayn-thor.conf +++ b/system_files/usr/lib/armada/devices/ayn-thor.conf @@ -2,7 +2,14 @@ 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_PANEL_ORIENTATION=right ARMADA_PANEL_NATIVE_WIDTH=1080 ARMADA_PANEL_NATIVE_HEIGHT=1920 diff --git a/system_files/usr/lib/armada/devices/defaults.conf b/system_files/usr/lib/armada/devices/defaults.conf index 1b162c5..9125821 100644 --- a/system_files/usr/lib/armada/devices/defaults.conf +++ b/system_files/usr/lib/armada/devices/defaults.conf @@ -3,6 +3,15 @@ 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= ARMADA_PANEL_TYPE=internal ARMADA_PANEL_ORIENTATION=normal ARMADA_PANEL_NATIVE_WIDTH= diff --git a/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules b/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules index 306a54e..1c0be0f 100644 --- a/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules +++ b/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules @@ -4,5 +4,7 @@ SUBSYSTEM=="cpu", KERNEL=="cpu[0-9]*", \ SUBSYSTEM=="devfreq", KERNEL=="*.gpu", \ RUN+="/bin/sh -c 'chgrp wheel /sys/class/devfreq/%k/governor /sys/class/devfreq/%k/max_freq /sys/class/devfreq/%k/min_freq 2>/dev/null; chmod g+w /sys/class/devfreq/%k/governor /sys/class/devfreq/%k/max_freq /sys/class/devfreq/%k/min_freq 2>/dev/null'" -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=="backlight", RUN+="/usr/libexec/armada/backlight-acl %k" + +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'" diff --git a/system_files/usr/libexec/armada/backlight-acl b/system_files/usr/libexec/armada/backlight-acl new file mode 100755 index 0000000..a718874 --- /dev/null +++ b/system_files/usr/libexec/armada/backlight-acl @@ -0,0 +1,16 @@ +#!/usr/bin/bash +# udev helper: grant Steam (wheel) direct write access to a backlight. +# +# On devices with more than one panel (ARMADA_PRIMARY_BACKLIGHT set) the +# grant is withheld from every backlight: Steam writes the first backlight +# it finds directly whenever it can, which bypasses the steamos-priv-write +# steering to the primary panel. Without write access Steam falls back to +# the polkit helper (its standard path on SteamOS), which steers and +# rescales the write. +set -euo pipefail + +eval "$(/usr/libexec/armada/device-env)" +[[ -n "${ARMADA_PRIMARY_BACKLIGHT:-}" ]] && exit 0 + +chgrp wheel "/sys/class/backlight/$1/brightness" 2>/dev/null || true +chmod g+w "/sys/class/backlight/$1/brightness" 2>/dev/null || true diff --git a/system_files/usr/libexec/armada/desktop-bootstrap b/system_files/usr/libexec/armada/desktop-bootstrap index 1bd9420..badbec7 100755 --- a/system_files/usr/libexec/armada/desktop-bootstrap +++ b/system_files/usr/libexec/armada/desktop-bootstrap @@ -20,17 +20,24 @@ 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 + 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}" @@ -57,3 +64,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 diff --git a/system_files/usr/libexec/armada/device-env b/system_files/usr/libexec/armada/device-env index faf005f..239d4ee 100755 --- a/system_files/usr/libexec/armada/device-env +++ b/system_files/usr/libexec/armada/device-env @@ -36,6 +36,10 @@ vars=( ARMADA_DEVICE_NAME ARMADA_SOC_CLASS ARMADA_PRIMARY_CONNECTOR + ARMADA_PRIMARY_BACKLIGHT + ARMADA_SECONDARY_CONNECTOR + ARMADA_PRIMARY_TOUCHSCREEN + ARMADA_SECONDARY_TOUCHSCREEN ARMADA_PANEL_TYPE ARMADA_PANEL_ORIENTATION ARMADA_PANEL_NATIVE_WIDTH diff --git a/system_files/usr/libexec/armada/setup-dual-screen b/system_files/usr/libexec/armada/setup-dual-screen new file mode 100755 index 0000000..08afbb3 --- /dev/null +++ b/system_files/usr/libexec/armada/setup-dual-screen @@ -0,0 +1,124 @@ +#!/usr/bin/python3 +"""One-time desktop layout for dual-screen devices, run by desktop-bootstrap. + +Places the secondary output below the primary (centered), makes the primary +the priority-1 output, and pins each touchscreen to its own output so taps +land on the screen being touched. +""" +import glob +import json +import shlex +import subprocess +import sys + + +def device_env(): + out = subprocess.run( + ["/usr/libexec/armada/device-env"], + capture_output=True, text=True, check=True, + ).stdout + env = {} + for line in out.splitlines(): + key, _, value = line.partition("=") + env[key] = shlex.split(value)[0] if value else "" + return env + + +def logical_size(output): + mode = next( + (m for m in output.get("modes", []) + if str(m.get("id")) == str(output.get("currentModeId"))), + None, + ) + if mode is None: + return None + width = mode["size"]["width"] + height = mode["size"]["height"] + # KScreen rotation flags: 2 (left) and 8 (right) swap the axes. + if output.get("rotation") in (2, 8): + width, height = height, width + scale = output.get("scale") or 1 + return round(width / scale), round(height / scale) + + +def input_devices(): + """Map input device name -> (vendor, product, event node).""" + devices = {} + for sysdir in glob.glob("/sys/class/input/input*"): + try: + name = open(f"{sysdir}/name").read().strip() + vendor = int(open(f"{sysdir}/id/vendor").read(), 16) + product = int(open(f"{sysdir}/id/product").read(), 16) + event = next( + e.rsplit("/", 1)[-1] for e in glob.glob(f"{sysdir}/event*") + ) + except (OSError, StopIteration, ValueError): + continue + devices[name] = (vendor, product, event) + return devices + + +def pin_touchscreen(name, connector, devices): + if name not in devices: + return + vendor, product, event = devices[name] + # Persisted mapping, applied by KWin when the device is added. + subprocess.run( + ["kwriteconfig6", "--file", "kcminputrc", + "--group", "Libinput", "--group", str(vendor), + "--group", str(product), "--group", name, + "--key", "OutputName", connector], + check=False, + ) + # Live mapping for the current session. + subprocess.run( + ["gdbus", "call", "--session", "--dest", "org.kde.KWin", + "--object-path", f"/org/kde/KWin/InputDevice/{event}", + "--method", "org.freedesktop.DBus.Properties.Set", + "org.kde.KWin.InputDevice", "outputName", f"<'{connector}'>"], + check=False, capture_output=True, + ) + + +def main(): + env = device_env() + primary = env.get("ARMADA_PRIMARY_CONNECTOR") + secondary = env.get("ARMADA_SECONDARY_CONNECTOR") + if not primary or not secondary: + return 0 + + # Some kscreen-doctor versions append human-readable text after the + # JSON document, so parse only the first document. + doc = subprocess.run( + ["kscreen-doctor", "-j"], + capture_output=True, text=True, check=True, + ).stdout + outputs = json.JSONDecoder().raw_decode(doc)[0]["outputs"] + by_name = {o["name"]: o for o in outputs} + if primary not in by_name or secondary not in by_name: + print(f"setup-dual-screen: outputs not found: {primary}, {secondary}", + file=sys.stderr) + return 1 + + primary_size = logical_size(by_name[primary]) + secondary_size = logical_size(by_name[secondary]) + if primary_size is None or secondary_size is None: + return 1 + x = max(0, round((primary_size[0] - secondary_size[0]) / 2)) + subprocess.run( + ["kscreen-doctor", + f"output.{primary}.priority.1", + f"output.{secondary}.priority.2", + f"output.{primary}.position.0,0", + f"output.{secondary}.position.{x},{primary_size[1]}"], + check=True, + ) + + devices = input_devices() + pin_touchscreen(env.get("ARMADA_PRIMARY_TOUCHSCREEN"), primary, devices) + pin_touchscreen(env.get("ARMADA_SECONDARY_TOUCHSCREEN"), secondary, devices) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/system_files/usr/libexec/armada/touchscreen-inhibit b/system_files/usr/libexec/armada/touchscreen-inhibit new file mode 100755 index 0000000..7ebc8ab --- /dev/null +++ b/system_files/usr/libexec/armada/touchscreen-inhibit @@ -0,0 +1,12 @@ +#!/usr/bin/bash +# Inhibit (1) or re-enable (0) an input device by name. Used to silence the +# secondary touchscreen while gamescope owns the only visible screen. +set -euo pipefail + +name="${1:?usage: touchscreen-inhibit <1|0>}" +state="${2:?usage: touchscreen-inhibit <1|0>}" + +for dev in /sys/class/input/input*/; do + [[ "$(cat "${dev}name" 2>/dev/null)" == "${name}" ]] || continue + echo "${state}" > "${dev}inhibited" +done