From 9fa68f3bc51f5bca4cbeba50e50bcc0e8dd9bd74 Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Tue, 28 Oct 2025 00:04:17 +0100 Subject: [PATCH] feat: Add configuration and scripts for Sunshine and Gamescope integration --- ansible/files/game_station/sunshine/apps.json | 11 +++ .../files/game_station/sunshine/sunshine.conf | 2 + .../files/game_station/system/launch_kde.sh | 63 ++++++++++-- .../game_station/system/launch_session.sh | 41 +++++--- .../files/game_station/system/launch_steam.sh | 44 +++++++-- .../system/steamos-session-select | 13 +++ .../game_station/systemd/user/desktop.slice | 3 + .../game_station/systemd/user/gamescope.slice | 3 + .../game_station/systemd/user/steam.slice | 3 + .../game_station/systemd/user/sunshine.slice | 3 + ansible/playbook_game_station.yml | 97 ++++++++++--------- 11 files changed, 207 insertions(+), 76 deletions(-) create mode 100644 ansible/files/game_station/sunshine/apps.json create mode 100644 ansible/files/game_station/sunshine/sunshine.conf create mode 100644 ansible/files/game_station/systemd/user/desktop.slice create mode 100644 ansible/files/game_station/systemd/user/gamescope.slice create mode 100644 ansible/files/game_station/systemd/user/steam.slice create mode 100644 ansible/files/game_station/systemd/user/sunshine.slice diff --git a/ansible/files/game_station/sunshine/apps.json b/ansible/files/game_station/sunshine/apps.json new file mode 100644 index 0000000..542b120 --- /dev/null +++ b/ansible/files/game_station/sunshine/apps.json @@ -0,0 +1,11 @@ +{ + "env": { + "PATH": "$(PATH)" + }, + "apps": [ + { + "name": "SteamDeck", + "image-path": "steam.png" + } + ] +} \ No newline at end of file diff --git a/ansible/files/game_station/sunshine/sunshine.conf b/ansible/files/game_station/sunshine/sunshine.conf new file mode 100644 index 0000000..62dcfab --- /dev/null +++ b/ansible/files/game_station/sunshine/sunshine.conf @@ -0,0 +1,2 @@ +system_tray=disable +log_path = /home/{{ user_name }}/.local/logs/sunshine.log diff --git a/ansible/files/game_station/system/launch_kde.sh b/ansible/files/game_station/system/launch_kde.sh index fb036a8..3191111 100644 --- a/ansible/files/game_station/system/launch_kde.sh +++ b/ansible/files/game_station/system/launch_kde.sh @@ -5,19 +5,64 @@ export PATH="$HOME/.local/bin:$PATH" log() { echo "[$(date --iso-8601=seconds)] $*" | tee -a "$LOGFILE"; } -DESKTOP_BIN="$(command -v startplasma-wayland || true)" +DESKTOP_BIN="$(command -v plasmashell || true)" if [ -z "$DESKTOP_BIN" ]; then - log "ERROR: startplasma-wayland not found in PATH" + log "ERROR: plasmashell not found in PATH" exit 127 fi -log "Launching Plasma: $DESKTOP_BIN" -exec $DESKTOP_BIN >>"$LOGFILE" 2>&1 & +export XDG_CURRENT_DESKTOP=plasma +export XDG_SESSION_TYPE=wayland +export XDG_DATA_DIRS=/usr/local/share/:/usr/share/ +export XDG_CONFIG_DIRS=/usr/local/etc/xdg:/etc/xdg +export XDG_CURRENT_DESKTOP=KDE +export XDG_RUNTIME_DIR=/run/user/$(id -u) -sleep 5 -systemd-run --user --slice=sunshine.slice --scope sunshine >>"$LOGFILE" 2>&1 || { - log "ERROR: Failed to start Sunshine" - exit 1 +export QT_QPA_PLATFORM=wayland +export GDK_BACKEND=wayland +export CLUTTER_BACKEND=wayland +export SDL_VIDEODRIVER=wayland +export MOZ_ENABLE_WAYLAND=1 +export KDEDIRS=/usr +export KDE_FULL_SESSION=true +export DESKTOP_SESSION=plasma + +unset DISPLAY + +mkdir -p $XDG_RUNTIME_DIR +chmod 700 $XDG_RUNTIME_DIR + +log "Launching Plasma: $DESKTOP_BIN" +systemd-run --user --scope --quiet \ + --unit=plasma.scope \ + --slice=desktop.slice \ + "$DESKTOP_BIN" >>"$LOGFILE" 2>&1 & +plasma_scope_pid=$! +log "Plasma started with scope pid $plasma_scope_pid" + +{ + SUNSHINE_BIN="$(command -v sunshine || true)" + if [ -z "$SUNSHINE_BIN" ]; then + log "ERROR: sunshine not found in PATH" + else + SUNSHINE_LOG_FILE="$HOME/.local/logs/sunshine.log" + log "Found Sunshine: $SUNSHINE_BIN, starting after 5s delay" + sleep 5 + systemd-run --user --scope --quiet \ + --unit=sunshine.scope \ + --slice=sunshine.slice \ + "$SUNSHINE_BIN" "$HOME/.config/sunshine/sunshine.conf" + fi } & -tail -f /dev/null \ No newline at end of file +wait "$plasma_scope_pid" +status=$? + +# Stop Sunshine gracefully +if systemctl --user is-active --quiet sunshine.scope; then + log "Stopping Sunshine" + systemctl --user stop sunshine.scope +fi + +log "Plasma session ended." +exit "$status" \ No newline at end of file diff --git a/ansible/files/game_station/system/launch_session.sh b/ansible/files/game_station/system/launch_session.sh index 007ebb3..7a896c4 100644 --- a/ansible/files/game_station/system/launch_session.sh +++ b/ansible/files/game_station/system/launch_session.sh @@ -1,21 +1,22 @@ #!/bin/bash set -euo pipefail -LOGFILE="$HOME/.local/logs/gamescope.log" +LOG_FILE="$HOME/.local/logs/session.log" -log() { echo "[$(date --iso-8601=seconds)] $*" | tee -a "$LOGFILE"; } +log() { echo "[$(date --iso-8601=seconds)] $*" | tee -a "$LOG_FILE"; } -CONF_FILE="$HOME/.config/gamescope/gamescope.conf" -[ -f "$CONF_FILE" ] && source "$CONF_FILE" - -OUTPUT_CONNECTOR=${OUTPUT_CONNECTOR:-HDMI-A-1} -FRAMERATE=${FRAMERATE:-60} -RESOLUTION=${RESOLUTION:-1920x1080} -WIDTH="${RESOLUTION%x*}" -HEIGHT="${RESOLUTION#*x}" start_gamescope() { log "Starting Gamescope session..." + CONF_FILE="$HOME/.config/gamescope/gamescope.conf" + [ -f "$CONF_FILE" ] && source "$CONF_FILE" + + OUTPUT_CONNECTOR=${OUTPUT_CONNECTOR:-HDMI-A-1} + FRAMERATE=${FRAMERATE:-60} + RESOLUTION=${RESOLUTION:-1920x1080} + WIDTH="${RESOLUTION%x*}" + HEIGHT="${RESOLUTION#*x}" + export __GL_GSYNC_ALLOWED=1 export __GL_VRR_ALLOWED=1 # export __GL_THREADED_OPTIMIZATIONS=1 @@ -38,7 +39,7 @@ start_gamescope() { GAMESCOPE_ARGS="--generate-drm-mode fixed --fade-out-duration 200 --cursor-scale-height 720 -f -w $WIDTH -h $HEIGHT --rt --mangoapp -e -O \"$OUTPUT_CONNECTOR\" -r $FRAMERATE $HDR_FLAG" log "Launching Gamescope: $GAMESCOPE_BIN $GAMESCOPE_ARGS -- $@" - exec "$GAMESCOPE_BIN" ${GAMESCOPE_ARGS} -- "$@" >>"$LOGFILE" 2>&1 + exec dbus-run-session -- "$GAMESCOPE_BIN" ${GAMESCOPE_ARGS} -- "$@" >>"$LOG_FILE" 2>&1 } start_kwin() { @@ -49,9 +50,18 @@ start_kwin() { exit 127 fi - KWIN_ARGS="--width $WIDTH --height $HEIGHT" - log "Launching KWin: $KWIN_BIN" - exec "$KWIN_BIN" ${KWIN_ARGS} "$@" >>"$LOGFILE" 2>&1 + export XDG_CURRENT_DESKTOP=plasma + export XDG_SESSION_TYPE=wayland + export XDG_DATA_DIRS=/usr/local/share/:/usr/share/ + export XDG_CONFIG_DIRS=/usr/local/etc/xdg:/etc/xdg + export XDG_CURRENT_DESKTOP=KDE + export XDG_RUNTIME_DIR=/run/user/$(id -u) + + # Ensure we don't run nested; width/height/fullscreen are for the windowed backend. + unset DISPLAY WAYLAND_DISPLAY + KWIN_ARGS="--drm --exit-with-session" + log "Launching KWin (DRM): $KWIN_BIN ${KWIN_ARGS} $*" + exec dbus-run-session -- "$KWIN_BIN" ${KWIN_ARGS} "$@" >>"$LOG_FILE" 2>&1 } if [[ -f "$HOME/.desktop-session-plasma" ]]; then @@ -61,4 +71,5 @@ if [[ -f "$HOME/.desktop-session-plasma" ]]; then else log "Gamescope session selected." start_gamescope /usr/local/bin/launch_steam.sh -fi \ No newline at end of file +fi + diff --git a/ansible/files/game_station/system/launch_steam.sh b/ansible/files/game_station/system/launch_steam.sh index ea6b577..16f69ce 100644 --- a/ansible/files/game_station/system/launch_steam.sh +++ b/ansible/files/game_station/system/launch_steam.sh @@ -3,6 +3,8 @@ set -euo pipefail LOGFILE="$HOME/.local/logs/steam.log" export PATH="$HOME/.local/bin:$PATH" +export XDG_RUNTIME_DIR=/run/user/$(id -u) + export SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0 # There is no way to set a color space for an NV12 @@ -62,6 +64,9 @@ export XCURSOR_SCALE=256 log() { echo "[$(date --iso-8601=seconds)] $*" | tee -a "$LOGFILE"; } +mkdir -p $XDG_RUNTIME_DIR +chmod 700 $XDG_RUNTIME_DIR + if [ -r /proc/sys/user/max_user_namespaces ]; then max_ns="$(cat /proc/sys/user/max_user_namespaces || echo 0)" if [ "${max_ns:-0}" -eq 0 ]; then @@ -99,14 +104,39 @@ log "Updating steam: /opt/steamcmd/steamcmd.sh +quit" log "Updating Steam ROM Manager" srm add >> "$LOGFILE" 2>&1 || log "ERROR: Steam ROM Manager update failed" +log "Launching Steam" STEAM_ARGS=("-steamdeck" "-steamos3" "-steampal" "-gamepadui" "-pipewire-dmabuf") -log "Launching Steam: $STEAM_BIN ${STEAM_ARGS[*]}" -exec $STEAM_BIN "${STEAM_ARGS[@]}" & +systemd-run --user --scope --quiet \ + --unit=steam.scope \ + --slice=steam.slice \ + "$STEAM_BIN" "${STEAM_ARGS[@]}" >>"$LOGFILE" 2>&1 & +steam_scope_pid=$! +log "Steam started with pid $steam_scope_pid" -sleep 5 -systemd-run --user --slice=sunshine.slice --scope sunshine >>"$LOGFILE" 2>&1 || { - log "ERROR: Failed to start Sunshine" - exit 1 +{ + SUNSHINE_BIN="$(command -v sunshine || true)" + if [ -z "$SUNSHINE_BIN" ]; then + log "ERROR: sunshine not found in PATH" + else + SUNSHINE_LOG_FILE="$HOME/.local/logs/sunshine.log" + log "Found Sunshine: $SUNSHINE_BIN, starting after 5s delay" + sleep 5 + systemd-run --user --scope --quiet \ + --unit=sunshine.scope \ + --slice=sunshine.slice \ + "$SUNSHINE_BIN" "$HOME/.config/sunshine/sunshine.conf" + fi } & -tail -f /dev/null \ No newline at end of file +# Wait for Steam to exit +wait "$steam_scope_pid" +status=$? + +# Stop Sunshine gracefully +if systemctl --user is-active --quiet sunshine.scope; then + log "Stopping Sunshine" + systemctl --user stop sunshine.scope +fi + +log "Steam session ended." +exit "$status" \ No newline at end of file diff --git a/ansible/files/game_station/system/steamos-session-select b/ansible/files/game_station/system/steamos-session-select index 8674a47..b95fa92 100644 --- a/ansible/files/game_station/system/steamos-session-select +++ b/ansible/files/game_station/system/steamos-session-select @@ -23,5 +23,18 @@ case "$session" in ;; esac +if systemctl --user is-active --quiet steam.scope; then + log "Stopping steam.scope" + systemctl --user stop steam.scope +fi +if systemctl --user is-active --quiet plasma.scope; then + log "Stopping plasma.scope" + systemctl --user stop plasma.scope +fi +if systemctl --user is-active --quiet sunshine.scope; then + log "Stopping sunshine.scope" + systemctl --user stop sunshine.scope +fi + log "Restarting graphical session service..." sudo systemctl restart gamescope-session.service \ No newline at end of file diff --git a/ansible/files/game_station/systemd/user/desktop.slice b/ansible/files/game_station/systemd/user/desktop.slice new file mode 100644 index 0000000..4618983 --- /dev/null +++ b/ansible/files/game_station/systemd/user/desktop.slice @@ -0,0 +1,3 @@ +[Slice] +CPUWeight=100 +AllowedCPUs={{ general_cores }} diff --git a/ansible/files/game_station/systemd/user/gamescope.slice b/ansible/files/game_station/systemd/user/gamescope.slice new file mode 100644 index 0000000..4618983 --- /dev/null +++ b/ansible/files/game_station/systemd/user/gamescope.slice @@ -0,0 +1,3 @@ +[Slice] +CPUWeight=100 +AllowedCPUs={{ general_cores }} diff --git a/ansible/files/game_station/systemd/user/steam.slice b/ansible/files/game_station/systemd/user/steam.slice new file mode 100644 index 0000000..4618983 --- /dev/null +++ b/ansible/files/game_station/systemd/user/steam.slice @@ -0,0 +1,3 @@ +[Slice] +CPUWeight=100 +AllowedCPUs={{ general_cores }} diff --git a/ansible/files/game_station/systemd/user/sunshine.slice b/ansible/files/game_station/systemd/user/sunshine.slice new file mode 100644 index 0000000..571d093 --- /dev/null +++ b/ansible/files/game_station/systemd/user/sunshine.slice @@ -0,0 +1,3 @@ +[Slice] +CPUWeight=100 +AllowedCPUs={{ sunshine_cores }} diff --git a/ansible/playbook_game_station.yml b/ansible/playbook_game_station.yml index 2aaecc1..a513b5b 100644 --- a/ansible/playbook_game_station.yml +++ b/ansible/playbook_game_station.yml @@ -3,14 +3,14 @@ hosts: game_station vars: user_name: "gameuser" - gamescope_conf_dir: "/home/{{ user_name }}/.config/gamescope" system_bin_dir: "/usr/local/bin" + user_config_dir: "/home/{{ user_name }}/.config" user_bin_dir: "/home/{{ user_name }}/.local/bin" systemd_user_dir: "/home/{{ user_name }}/.config/systemd/user" systemd_system_dir: "/etc/systemd/system" grub_cfg: "/etc/default/grub" system_cores: "0,8" - gamescope_cores: "2-7,10-15" + general_cores: "2-7,10-15" sunshine_cores: "1,9" tasks: @@ -99,27 +99,15 @@ - sddm # System actions - - name: Create Gamescope slice - become: true - ansible.builtin.copy: - dest: "{{ systemd_system_dir }}/gamescope.slice" - owner: root - group: root - mode: "0644" - content: | - [Slice] - CPUWeight=100 - AllowedCPUs={{ gamescope_cores }} - - - name: Install Systemd service files + - name: Install Systemd system files become: true ansible.builtin.copy: src: "{{ item }}" - dest: "/etc/systemd/system/{{ item | basename }}" + dest: "{{ systemd_system_dir }}/{{ item | basename }}" mode: "0644" owner: root group: root - loop: "{{ query('fileglob', 'game_station/systemd/system/*.service') }}" + loop: "{{ query('fileglob', 'game_station/systemd/system/*') }}" notify: Reload systemd daemon for system services - name: Install System scripts @@ -191,18 +179,32 @@ group: "{{ user_name }}" mode: "0755" loop: - - "{{ gamescope_conf_dir }}" - "{{ user_bin_dir }}" - "{{ systemd_user_dir }}" + - "{{ user_config_dir }}" + - "{{ user_config_dir }}/gamescope" + - "{{ user_config_dir }}/sunshine" + - "{{ user_config_dir }}/steam-rom-manager/userData" - "/home/{{ user_name }}/.local/logs" - - "/opt/steamcmd" - - "/home/{{ user_name }}/.config/steam-rom-manager/userData" + - "/home/{{ user_name }}/.local/run" - "/home/{{ user_name }}/Emulation" - "/home/{{ user_name }}/Emulation/snes" - "/home/{{ user_name }}/Emulation/psp" - "/home/{{ user_name }}/Emulation/genesis" - "/home/{{ user_name }}/RetroArch/shaders" - "/home/{{ user_name }}/RetroArch/config" + - "/opt/steamcmd" + + - name: Install Systemd user files + become: true + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ systemd_user_dir }}/{{ item | basename }}" + owner: "{{ user_name }}" + group: "{{ user_name }}" + mode: "0644" + loop: "{{ query('fileglob', 'game_station/systemd/user/*') }}" + notify: Reload systemd daemon for user services - name: Download steamtools become: true @@ -242,30 +244,6 @@ group: "{{ user_name }}" mode: "0644" - - name: Write Gamescope configuration - become: true - ansible.builtin.copy: - dest: "{{ gamescope_conf_dir }}/gamescope.conf" - owner: "{{ user_name }}" - mode: "0644" - content: | - OUTPUT_CONNECTOR=HDMI-A-1 - FRAMERATE=120 - RESOLUTION={{ lookup('env', 'GAME_RESOLUTION') | default('1920x1080') }} - HDR_ENABLED={{ lookup('env', 'HDR_ENABLED') | default('false') }} - DRM_DEVICE=/dev/dri/card1 - - - name: Create Sunshine slice - become: true - ansible.builtin.copy: - dest: "{{ systemd_user_dir }}/sunshine.slice" - owner: "{{ user_name }}" - mode: "0644" - content: | - [Slice] - CPUWeight=100 - AllowedCPUs={{ sunshine_cores }} - - name: Install User scripts become: true ansible.builtin.copy: @@ -276,6 +254,29 @@ mode: "0755" loop: "{{ query('fileglob', 'game_station/user/*') }}" + - name: Write Gamescope configuration + become: true + ansible.builtin.copy: + dest: "{{ user_config_dir }}/gamescope/gamescope.conf" + owner: "{{ user_name }}" + mode: "0644" + content: | + OUTPUT_CONNECTOR=HDMI-A-1 + FRAMERATE=120 + RESOLUTION={{ lookup('env', 'GAME_RESOLUTION') | default('1920x1080') }} + HDR_ENABLED={{ lookup('env', 'HDR_ENABLED') | default('false') }} + DRM_DEVICE=/dev/dri/card1 + + - name: Install Sunshine files + become: true + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ user_config_dir }}/sunshine/{{ item | basename }}" + owner: "{{ user_name }}" + group: "{{ user_name }}" + mode: "0644" + loop: "{{ query('fileglob', 'game_station/sunshine/*') }}" + - name: Enable Gamescope session service on boot become: true ansible.builtin.systemd: @@ -283,7 +284,6 @@ enabled: true state: started - handlers: - name: Update grub ansible.builtin.command: grub2-mkconfig -o /boot/grub2/grub.cfg @@ -294,3 +294,10 @@ become: true ansible.builtin.systemd: daemon_reload: true + + - name: Reload systemd daemon for user services + become: true + become_user: "{{ user_name }}" + ansible.builtin.systemd: + daemon_reload: true + scope: user