Opens a larger view. Escape closes it.

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"