From 127b8cf88b9624cfb2950b6b16c9c247654d49e5 Mon Sep 17 00:00:00 2001 From: rickpeters Date: Tue, 13 Oct 2026 18:18:09 +0200 Subject: [PATCH] feat: SteamOS conversion on NVIDIA with a patched gamescope (GBM scan-out) The conversion is offered on supported NVIDIA cards again; it builds and installs steamify-gamescope-nvidia and sets gamescope_drm_gbm_scanout=1. It and Gaming on NVIDIA exclude each other. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01KkpbsMw4rQyBsxDG6ebHK2 --- CHANGELOG.md | 2 ++ TECHNICAL.md | 12 +++++++ lib/backend.sh | 11 +++++-- lib/gamescope-nvidia.sh | 71 +++++++++++++++++++++++++++++++++++++++++ lib/login-manager.sh | 3 ++ lib/menu.sh | 20 +++++++++--- lib/nvidia.sh | 6 ++-- steamify.sh | 2 +- ui/qml/AppState.qml | 11 +++++-- 9 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 lib/gamescope-nvidia.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 35deee2..cbb8b2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ one per merged pull request. ## 2.11.2 - 2026-10-13 +- **feat: the SteamOS conversion is offered on NVIDIA PCs again (RTX 20 series or newer): it builds and installs a patched gamescope from [steamify-gamescope-nvidia](https://github.com/theupriser/steamify-gamescope-nvidia) (GBM-allocated scan-out buffers, NVIDIA's corrupted gaming-mode picture, gamescope#2309; a few minutes to build) and sets `gamescope_drm_gbm_scanout=1` for the session (`lib/gamescope-nvidia.sh`). It and "Gaming on NVIDIA" exclude each other; single user mode works with either. Tested on an RTX 5080 (driver 615.78.08): Steam's gaming UI is clean with it and shows artifacts without. The package is pinned to one gamescope release: updating it is not automatic yet** + - **feat: "Gaming on NVIDIA" also keeps video memory over suspend (`NVreg_PreserveVideoMemoryAllocations=1` in `/etc/modprobe.d/90-steamify-nvidia.conf`, the initramfs is rebuilt), enables `nvidia-suspend`/`nvidia-resume`/`nvidia-hibernate`, and installs `lib32-nvidia-utils` and `lib32-libxkbcommon` (32-bit games, the 32-bit MangoHud preload) on a supported card; off removes only what Steamify added. Untested on hardware yet** ## 2.11.1 - 2026-10-02 diff --git a/TECHNICAL.md b/TECHNICAL.md index a94ee5f..ad212ee 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -238,6 +238,18 @@ no fix known). The kernel parameters `nvidia-drm.modeset=1`/`fbdev=1` and loadin window straight to the display. What does work on an RTX 5080: Steam's Big Picture (`steam -gamepadui`) as a normal window of the Plasma desktop session, smooth and clean. +Since 2.11.2 there is a second way: gamescope with GBM-allocated scan-out buffers (`lib/gamescope-nvidia.sh`). On an RTX 5080 +(driver 615.78.08, 3440x1440@175) Steam's gaming UI in gamescope's DRM session is clean with it and shows artifacts everywhere +without. The patch is nine commits of [farnoy/gamescope](https://github.com/farnoy/gamescope) (`nvidia-gbm-wayland`), kept in +[steamify-gamescope-nvidia](https://github.com/theupriser/steamify-gamescope-nvidia) with its PKGBUILD; `gsn_install_package` clones +that repository, builds the package as the user (`makepkg -sf`) and installs it with `pacman -U --ask 4` (the package provides and +conflicts with `gamescope`). It is opt-in inside gamescope: `gamescope_drm_gbm_scanout=1` in +`~/.config/gamescope-session/environment` and `~/.config/environment.d/99-steamify-gamescope-nvidia.conf`. The SteamOS conversion +(`gaming_enable`) does this on a supported card (`nvidia_supported`), so on those PCs it is offered next to "Gaming on NVIDIA" +and the two exclude each other (`toggle_component`, `backend.sh`, `AppState.qml`); single user mode works with either. Turning the +conversion off removes the setting and keeps the package. The package is pinned to the gamescope release the patch was made for +(`UPSTREAM_VERSION` in that repository): pacman does not update it, and Steamify does not rebuild it yet. + So with an NVIDIA GPU (PCI vendor 0x10de, display class, its driver providing `nvidia_drm`: `nvidia_present`) the SteamOS conversion (`gaming`, and its options `boot`, `single`, `glyphs`) isn't offered, unless it is already on (so it can be turned off). `nvidia` ("Gaming on NVIDIA", `lib/nvidia.sh`) takes its place, hidden while the conversion is on (both would start diff --git a/lib/backend.sh b/lib/backend.sh index f205da8..30c9baa 100644 --- a/lib/backend.sh +++ b/lib/backend.sh @@ -147,10 +147,15 @@ backend_apply() { for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done done # The same rules as the menu's toggles. - # (Single user mode only needs the conversion where it is offered: not on NVIDIA PCs.) - [[ "${WANTED[single]:-0}" == 1 ]] && component_available gaming && WANTED[gaming]=1 + # (Single user mode only needs the conversion where it is offered: not on NVIDIA PCs, where it works without it.) + [[ "${WANTED[single]:-0}" == 1 ]] && component_available gaming && ! nvidia_present && WANTED[gaming]=1 + # On NVIDIA the conversion and "Gaming on NVIDIA" exclude each other: the one that is not on yet wins. + if nvidia_present && [[ "${WANTED[gaming]:-0}" == 1 && "${WANTED[nvidia]:-0}" == 1 ]]; then + if [[ "${CURRENT[gaming]:-0}" == 1 ]]; then WANTED[gaming]=0; WANTED[boot]=0; WANTED[silent]=0; WANTED[glyphs]=0 + else WANTED[nvidia]=0; WANTED[bigpicture]=0; WANTED[nvsilent]=0; fi + fi if component_available gaming; then - [[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; } + [[ "${WANTED[gaming]:-0}" == 0 ]] && { nvidia_present || WANTED[single]=0; WANTED[boot]=0; } else WANTED[boot]=0 fi diff --git a/lib/gamescope-nvidia.sh b/lib/gamescope-nvidia.sh new file mode 100644 index 0000000..8819e31 --- /dev/null +++ b/lib/gamescope-nvidia.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# gamescope's own gaming mode on NVIDIA: a patched gamescope that allocates its scan-out buffers with GBM, which cures +# the corrupted picture (https://github.com/ValveSoftware/gamescope/issues/2309). The patch and the build recipe live in +# https://github.com/theupriser/steamify-gamescope-nvidia; the package `steamify-gamescope-nvidia` is built here, on the PC, +# and replaces `gamescope` (it provides it). The patch is opt-in: gamescope reads `gamescope_drm_gbm_scanout=1` from its +# environment, which is set for the gaming session (gamescope-session's environment file, and environment.d). +# Used by the SteamOS conversion (`gaming_enable`/`gaming_disable`) on a supported NVIDIA card (`nvidia_supported`). +# Sourced by steamify.sh; not meant to be run on its own. + +GSN_PKG=steamify-gamescope-nvidia +GSN_REPO=${GSN_REPO:-https://github.com/theupriser/steamify-gamescope-nvidia.git} +GSN_ENV_LINE='gamescope_drm_gbm_scanout=1' +GSN_ENV_D="${GSN_ENV_D:-$HOME/.config/environment.d/99-steamify-gamescope-nvidia.conf}" +GSN_SESSION_ENV="${GSN_SESSION_ENV:-$HOME/.config/gamescope-session/environment}" + +gsn_package_installed() { pacman -Q "$GSN_PKG" >/dev/null 2>&1; } + +gsn_env_set() { [[ -f "$GSN_ENV_D" ]] && grep -qsx "$GSN_ENV_LINE" "$GSN_SESSION_ENV"; } + +gsn_status() { gsn_package_installed && gsn_env_set; } + +gsn_install_package() { + # Builds the package from the repo (makepkg runs as the user, never as root) and installs it. The build needs a few + # minutes: gamescope compiles from source. --ask 4 lets pacman replace the stock gamescope it conflicts with. + gsn_package_installed && return 0 + info "Building the patched gamescope for NVIDIA (a few minutes)..." + sudo pacman -S --needed --noconfirm base-devel git || { err "Installing the build tools failed."; return 1; } + local dir pkg + dir="$(mktemp -d)" || return 1 + if ! git clone --depth 1 "$GSN_REPO" "$dir/src"; then + err "Couldn't download $GSN_REPO." + rm -rf "$dir"; return 1 + fi + if ! (cd "$dir/src" && makepkg -sf --noconfirm --nocheck); then + err "Building the patched gamescope failed (see above). The stock gamescope stays." + rm -rf "$dir"; return 1 + fi + pkg="$(ls "$dir"/src/"$GSN_PKG"-*.pkg.tar.* 2>/dev/null | grep -v -- '-debug-' | head -1)" + if [[ -z "$pkg" ]] || ! sudo pacman -U --noconfirm --ask 4 "$pkg"; then + err "Installing the patched gamescope failed. The stock gamescope stays." + rm -rf "$dir"; return 1 + fi + rm -rf "$dir" + ok "Patched gamescope installed." +} + +gsn_env_enable() { + mkdir -p "$(dirname "$GSN_ENV_D")" "$(dirname "$GSN_SESSION_ENV")" + echo "$GSN_ENV_LINE" > "$GSN_ENV_D" + touch "$GSN_SESSION_ENV" + grep -qsx "$GSN_ENV_LINE" "$GSN_SESSION_ENV" || echo "$GSN_ENV_LINE" >> "$GSN_SESSION_ENV" +} + +gsn_env_disable() { + rm -f "$GSN_ENV_D" + [[ -f "$GSN_SESSION_ENV" ]] && sed -i "/^${GSN_ENV_LINE}\$/d" "$GSN_SESSION_ENV" + return 0 +} + +gsn_enable() { + # Only on a supported NVIDIA card; elsewhere the stock gamescope is right. + nvidia_supported || return 0 + gsn_install_package || return 1 + gsn_env_enable + ok "gamescope draws NVIDIA's picture through GBM (gamescope_drm_gbm_scanout=1) from the next gaming mode start." +} + +gsn_disable() { + # The setting goes; the patched package stays, like the other packages (steam, gamescope-session-cachyos). + gsn_env_disable +} diff --git a/lib/login-manager.sh b/lib/login-manager.sh index 7cef57f..2781f71 100644 --- a/lib/login-manager.sh +++ b/lib/login-manager.sh @@ -94,6 +94,8 @@ gaming_enable() { # LOGIN_MANAGER (sddm with single user, else plasmalogin) is set by the # menu. Re-running with the other value switches over cleanly. install_required_packages || return 1 + # NVIDIA: gamescope's picture is corrupted unless it is the patched one (no-op elsewhere). + gsn_enable || return 1 case "$LOGIN_MANAGER" in sddm) switch_to_sddm || return 1 @@ -122,6 +124,7 @@ gaming_disable() { switch_to_plasmalogin remove_desktop_shortcut steam_disable + gsn_disable krevert gaming os_name_disable ok "The PC boots to the normal login screen again (from the next boot)." diff --git a/lib/menu.sh b/lib/menu.sh index 22824ae..2d9103e 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -57,9 +57,10 @@ declare -A CURRENT WANTED component_available() { case "$1" in - # gamescope's own session is broken on NVIDIA: the conversion is replaced by "Gaming on NVIDIA" there (shown - # anyway when it's already on, so it can be turned off). - gaming|boot|silent|glyphs) ! nvidia_present || gaming_status ;; + # gamescope's own session shows a corrupted picture on NVIDIA unless it is the patched one (lib/gamescope-nvidia.sh, + # installed with the conversion on a supported card); "Gaming on NVIDIA" is the alternative there. Older NVIDIA + # cards only get the alternative (the conversion is shown anyway when it's already on, so it can be turned off). + gaming|boot|silent|glyphs) ! nvidia_present || gaming_status || nvidia_supported ;; nvidia) nvidia_available ;; bigpicture|nvsilent) bigpicture_available ;; machine|poweroff) machine_available ;; @@ -297,8 +298,8 @@ toggle_component() { WANTED[$c]=$(( 1 - WANTED[$c] )) # Single user mode only makes sense on top of the SteamOS conversion. # (On NVIDIA the conversion isn't offered: single user mode logs in by itself, see lib/single-user.sh.) - if [[ "$c" == single && "${WANTED[single]}" == 1 ]] && component_available gaming; then WANTED[gaming]=1; fi - if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=0; WANTED[boot]=0; fi + if [[ "$c" == single && "${WANTED[single]}" == 1 ]] && component_available gaming && ! nvidia_present; then WANTED[gaming]=1; fi + if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then nvidia_present || WANTED[single]=0; WANTED[boot]=0; fi # Where to boot to is part of the conversion, too. if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi # Big Picture is opt-out: ticked along with "Gaming on NVIDIA". @@ -310,6 +311,15 @@ toggle_component() { if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[silent]=0; WANTED[glyphs]=0; fi if [[ "$c" == silent && "${WANTED[silent]}" == 1 ]]; then WANTED[gaming]=1; fi if [[ "$c" == glyphs && "${WANTED[glyphs]}" == 1 ]]; then WANTED[gaming]=1; fi + # On NVIDIA the SteamOS conversion and "Gaming on NVIDIA" exclude each other (both would start Steam at login); + # single user mode works with either. + if nvidia_present; then + if [[ "$c" == nvidia && "${WANTED[nvidia]}" == 1 ]]; then + WANTED[gaming]=0; WANTED[boot]=0; WANTED[silent]=0; WANTED[glyphs]=0 + elif [[ "$c" =~ ^(gaming|boot|silent|glyphs)$ && "${WANTED[$c]}" == 1 && "${WANTED[gaming]}" == 1 ]]; then + WANTED[nvidia]=0; WANTED[bigpicture]=0; WANTED[nvsilent]=0 + fi + fi # The power-off fix is opt-out: ticked along with Steam Machine support. if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi diff --git a/lib/nvidia.sh b/lib/nvidia.sh index 28eed6c..a09f792 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -37,9 +37,9 @@ nvidia_present() { return 1 } -# Shown on NVIDIA PCs, unless the SteamOS conversion is already on there (it -# has to be turned off first: both would start Steam at login). -nvidia_available() { nvidia_present && ! gaming_status 2>/dev/null; } +# Shown on NVIDIA PCs. It and the SteamOS conversion exclude each other (both +# would start Steam at login): the menu unticks one when the other is ticked. +nvidia_available() { nvidia_present; } bigpicture_available() { nvidia_available; } nvidia_status() { diff --git a/steamify.sh b/steamify.sh index a87cfdf..cf39a0d 100755 --- a/steamify.sh +++ b/steamify.sh @@ -19,7 +19,7 @@ VERSION=2.11.2 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh nvidia controllers cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier first-login menu backend; do +for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh nvidia gamescope-nvidia controllers cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier first-login menu backend; do # shellcheck source=/dev/null source "$SCRIPT_DIR/lib/$lib.sh" done diff --git a/ui/qml/AppState.qml b/ui/qml/AppState.qml index 6148d9c..b15c5ff 100644 --- a/ui/qml/AppState.qml +++ b/ui/qml/AppState.qml @@ -78,14 +78,21 @@ Item { w[id] = !w[id]; // The same rules as the terminal menu (toggle_component). var hasGaming = items.some(function (i) { return i.id === "gaming"; }); - if (id === "gaming" && !w.gaming) { w.single = false; w.silent = false; w.glyphs = false; } - if (id === "single" && w.single && hasGaming) w.gaming = true; + var hasNvidia = items.some(function (i) { return i.id === "nvidia"; }); + // Single user mode works without the conversion on NVIDIA PCs. + if (id === "gaming" && !w.gaming) { if (!hasNvidia) w.single = false; w.silent = false; w.glyphs = false; } + if (id === "single" && w.single && hasGaming && !hasNvidia) w.gaming = true; if (id === "nvidia") w.bigpicture = w.nvidia; if (id === "bigpicture" && w.bigpicture) { w.nvidia = true; w.nvsilent = false; } if (id === "nvsilent" && w.nvsilent) { w.nvidia = true; w.bigpicture = false; } if (id === "nvidia" && !w.nvidia) w.nvsilent = false; if (id === "silent" && w.silent) w.gaming = true; if (id === "glyphs" && w.glyphs) w.gaming = true; + // On NVIDIA the conversion and "Gaming on NVIDIA" exclude each other (both would start Steam at login). + if (hasGaming && hasNvidia) { + if (id === "nvidia" && w.nvidia) { w.gaming = false; w.silent = false; w.glyphs = false; } + else if ((id === "gaming" || id === "silent" || id === "glyphs") && w[id] && w.gaming) { w.nvidia = false; w.bigpicture = false; w.nvsilent = false; } + } if (id === "machine") w.poweroff = w.machine; if (id === "poweroff" && w.poweroff) w.machine = true; if (id === "machine" && !w.machine) w.kpin = false;