home-server
display-mode.sh
#!/usr/bin/env bash
# Toggle gamescope output between the physical desk monitor and the virtual
# streaming display, and (in stream mode) drive real HDR for Moonlight.
# desk -> HDMI-A-1, monitor preferred mode, monitor handles its own HDR
# stream -> DP-1 phantom @ 3024x1890@120, HDR10 PQ via the firmware HDR EDID
# DP-1 advertises HDR through drm.edid_firmware (a copy of the HDMI monitor's
# HDR EDID), so ENABLE_GAMESCOPE_HDR=1 -> --hdr-enabled engages REAL HDR; we no
# longer force output (--hdr-debug-force-output hung the atomic modeset on the
# EDID-less connector and tripped the lockout). SCREEN_WIDTH/HEIGHT force
# 3024x1890 since the reused EDID's preferred mode is the monitor's 3440x1440.
# BACKEND=drm in both modes so a leaked WAYLAND_DISPLAY cannot make gamescope
# nest on the xdg backend. Switch via stop -> wait-for-exit -> start (NOT
# restart) so the new gamescope does not race the dying one for the DRM master.
# After start we block until gamescope is compositing on the target connector so
# callers (e.g. the Sunshine stack) can start capture only once it is ready.
# NOTE: the real compositor process is "gamescope-wl" and its argv starts with
# "/usr/bin/gamescope " possibly followed by -W (stream) or -- (desk); match the
# trailing space so both arg orders hit but the gamescope-session-plus helpers
# (path "gamescope-session-plus", no following space) never do.
set -euo pipefail
CONF="$HOME/.config/environment.d/10-gamescope-session.conf"
TRACKER="/tmp/chimeraos-short-session-tracker"
SVC="gamescope-session-plus@steam.service"
GSPAT="/usr/bin/gamescope "
gs_output() { pgrep -af "$GSPAT" | grep -o "prefer-output [A-Za-z0-9*,-]*" | head -1; }
case "${1:-}" in
desk)
CONN="HDMI-A-1"
# ENABLE_GAMESCOPE_HDR=0 is set EXPLICITLY, not just omitted: variables
# already imported into the running `systemd --user` manager are never
# removed when a line disappears from environment.d, and that manager
# outlives SDDM restarts. Omitting the line therefore leaves a stale
# ENABLE_GAMESCOPE_HDR=1 in place (2026-09-12: caused gamescope to keep
# --hdr-enabled after switching modes). gamescope-session-plus tests
# [ "$ENABLE_GAMESCOPE_HDR" == "1" ], so an explicit 0 reliably wins.
{ printf "OUTPUT_CONNECTOR=HDMI-A-1\n"
printf "BACKEND=drm\n"
printf "ENABLE_GAMESCOPE_HDR=0\n"
printf "GAMESCOPE_COMPOSITE_FORCE=0\n"
} > "$CONF" ;;
stream)
CONN="DP-1"
{ printf "OUTPUT_CONNECTOR=DP-1\n"
printf "BACKEND=drm\n"
printf "ENABLE_GAMESCOPE_HDR=1\n"
printf "SCREEN_WIDTH=3024\n"
printf "SCREEN_HEIGHT=1890\n"
printf "GAMESCOPE_COMPOSITE_FORCE=0\n"
} > "$CONF" ;;
deck)
# Steam Deck OLED streaming: DP-1 phantom at 3024x1890, HDR DISABLED.
# Same resolution as `stream` mode, deliberately: 3024x1890 is exactly
# 1.6:1 == the Deck's 16:10 aspect, so no letterboxing, just ~2.2x the
# Deck's 1280x800 pixels (trivial for the 9070 XT).
# 2560x1600 (exact 2x of the Deck panel) was tried and FAILS -- black
# stream. It can be added to the EDID blob and DP-1 then lists it and
# even reports enabled=enabled, but nothing is scanned out. Only the
# mode forced by the kernel karg `video=DP-1:3024x1890MR@120e`
# (DRM_MODE_TYPE_USERDEF) is actually driveable on a phantom connector;
# EDID modes describe what a PHYSICAL display would accept, and there
# is no physical display here. `video=` takes ONE mode per connector,
# so 3024x1890 has to serve both the MacBook and the Deck.
# Deliberately no ENABLE_GAMESCOPE_HDR here: the Deck's RADV/VanGogh
# Vulkan driver advertises no HDR10 colourspace, so Moonlight always
# requests SDR. Compositing in HDR and tone-mapping down to SDR on this
# EDID-less phantom connector produced BLACK FRAMES (2026-09-12) -- the
# stream was only visible when HDR was toggled back on. Compositing
# natively in SDR avoids that conversion path entirely.
CONN="DP-1"
{ printf "OUTPUT_CONNECTOR=DP-1\n"
printf "BACKEND=drm\n"
printf "ENABLE_GAMESCOPE_HDR=0\n"
printf "SCREEN_WIDTH=3024\n"
printf "SCREEN_HEIGHT=1890\n"
# GAMESCOPE_COMPOSITE_FORCE: tried =1 (force composition, disable
# direct scan-out) on 2026-09-12 as a theory for the mid-game blank --
# WRONG. The 'Couldn't get drm fb for plane [0]' warning floods from
# the moment CLIENT CONNECTED fires, present even in fully-working
# sessions (Big Picture streamed fine with it flooding the whole
# time) -- it is not the cause of the blank-out. Forcing composition
# instead broke Big Picture itself (blank immediately on connect).
# Reverted to 0 (matches desk/stream). Real cause still open.
printf "GAMESCOPE_COMPOSITE_FORCE=0\n"
} > "$CONF" ;;
status)
echo "config:"; sed "s/^/ /" "$CONF" 2>/dev/null || echo " (default)"
echo "gamescope: $(gs_output || echo "not running")"
pgrep -af "$GSPAT" | grep -q "hdr-enabled" && echo "hdr: ENABLED (stream)" || echo "hdr: off"
exit 0 ;;
*) echo "usage: $0 {desk|stream|deck|status}"; exit 1 ;;
esac
echo "Wrote $1 mode to $CONF"
# Push the new values into `systemd --user` explicitly.
# environment.d is IMPORT-ONLY: the user manager reads it once at login and
# never re-reads it, and it outlives SDDM restarts -- so without this, a mode
# switch leaves the manager serving the PREVIOUS mode's values. Writing the
# file is not enough. (2026-09-12: after switching deck mode from 2560x1600 to
# 3024x1890 the manager still reported SCREEN_WIDTH=2560, and a newly-added
# variable -- GAMESCOPE_COMPOSITE_FORCE -- was missing entirely, because
# nothing had ever imported it.) Every mode writes every variable explicitly
# for the same reason: omitting one leaves the stale value in place rather
# than unsetting it.
while IFS= read -r _line; do
[ -z "$_line" ] && continue
case "$_line" in \#*) continue ;; esac
systemctl --user set-environment "$_line" || true
done < "$CONF"
echo "Stopping current gamescope session..."
systemctl --user stop "$SVC" || true
# Wait for gamescope to fully exit so the DRM master is released before the
# new session opens card1.
for _ in $(seq 1 40); do
pgrep -x gamescope-wl >/dev/null 2>&1 || pgrep -af "$GSPAT" >/dev/null 2>&1 || break
sleep 0.25
done
sleep 1
rm -f "$TRACKER"
echo "Starting gamescope session in $1 mode..."
systemctl --user start "$SVC"
# Block until gamescope is compositing on the target connector.
ready=0
for _ in $(seq 1 60); do
if gs_output | grep -q "prefer-output $CONN"; then ready=1; break; fi
sleep 0.5
done
[ "$ready" = 1 ] && echo "gamescope ready on $CONN" || echo "WARN: gamescope not on $CONN after wait"