diff --git a/AGENTS.md b/AGENTS.md index c18c532..aff9ea2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,11 +48,15 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine `_available` is checked via `component_available` (e.g. `machine` only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse; `gaming` must stay first. Dependencies live in `toggle_component`. -- **NVIDIA** (`lib/nvidia.sh`): gamescope's session is broken on NVIDIA, so the SteamOS conversion (`gaming`, `boot`, `single`, - `glyphs`) is hidden there (unless already on) and `nvidia` + its sub-option `bigpicture` ("Gaming on NVIDIA") replace it: - Steam on the Plasma desktop, started at login. Never reintroduce a gamescope session for NVIDIA without re-testing on the - hardware (notes: steamify-cachyos-dev, `nvidia/`). It can't be tested without the hardware: keep the branch behind - `.no-release-yet` until it was. +- **NVIDIA** (`lib/gamescope-nvidia.sh`, `lib/nvidia.sh`): since 2.11.2 the SteamOS conversion (`gaming` and its options) runs on + NVIDIA PCs with a patched gamescope (GBM scan-out: gamescope#2309; Steam's UI kept at the display's size), downloaded as the + newest release of theupriser/steamify-gamescope-nvidia, with the pinned package (`GSN_VERSION`, `GSN_SHA256`), the copy the ISO + leaves, and a build here (`patches/gamescope-nvidia.PKGBUILD` + the two patches) as the fallback. "Gaming on NVIDIA" (`nvidia`, + `bigpicture`, `nvsilent`) isn't offered any more (`nvidia_available`); turning the conversion on turns it off (`nvidia_swap_out`). + A new pin = the release's tag + its sha256 in `lib/gamescope-nvidia.sh` and the PKGBUILD's pkgver/pkgrel, copied from the + repository (the patches in `patches/` must stay identical to that repository's). Steam must keep running with `-steamos3` + (without it games don't launch in gaming mode and the settings pages go): never "fix" the UI size by dropping flags or by + `XWAYLAND_COUNT`. Tested on an RTX 5080 only; keep the branch behind `.no-release-yet` until the whole flow ran on hardware. - **Feature versions.** Set `FEATURE_VERSION[]` (`lib/menu.sh`) to the new `VERSION` whenever what `_enable` sets up changes (a new component gets one too): installs recorded with an older version diff --git a/CHANGELOG.md b/CHANGELOG.md index c740638..115e689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ one per merged pull request. ## 2.11.2 - 2026-10-13 +- **feat: on NVIDIA the gaming session runs on the display's own mode and Steam's interface at its size: a second gamescope patch (`gamescope-ui-native.patch`, release `3.16.31-2` of steamify-gamescope-nvidia, now the pin) keeps Steam's UI X server at the output size when Steam (with `-steamos3`) asks for 1920x1080, and a wrapper (`gamescope-display-wrapper.sh`, set up by the conversion with a drop-in of `gamescope-session.service`) starts gamescope on the mode KDE has saved for the connected display, else the biggest mode at the highest refresh rate: before, gaming mode ran at the display's preferred mode (60 Hz on many monitors) with a 1080p Steam UI. Tested on an RTX 5080, 3440x1440@175: widescreen, sharp, a Proton game starts** + - **fix: on an NVIDIA PC that had single user mode on before the SteamOS conversion, "Return to Gaming Mode" and the boot choice work again: single user mode's desktop autologin (`zzz-steamify-autologin.conf`, read last) pinned the Plasma session; the conversion removes it, and turning the conversion off sets it up again** - **fix: when an option fails, its sub-options are not applied any more (a failed conversion used to leave its "Boot into the desktop", silent start and icons on without it)** diff --git a/lib/gamescope-nvidia.sh b/lib/gamescope-nvidia.sh index 5815293..4d2b0ad 100644 --- a/lib/gamescope-nvidia.sh +++ b/lib/gamescope-nvidia.sh @@ -20,8 +20,8 @@ GSN_PKG=steamify-gamescope-nvidia GSN_RELEASES="${GSN_RELEASES:-https://github.com/theupriser/steamify-gamescope-nvidia/releases}" # The package this Steamify release was tested with (patches/gamescope-nvidia.PKGBUILD names the same version): the # fallback. A new pin: the release's tag, the package's sha256, and the PKGBUILD's pkgver/pkgrel. -GSN_VERSION=3.16.31-1 -GSN_SHA256=9f20e5243866d1ddeb9ba0c6828bc7be1945f623f9a5d8094391f0df043002e1 +GSN_VERSION=3.16.31-2 +GSN_SHA256=3b981bed4dfdac383d22b8c22feaef191c2f2863bccf5676d1ec959c2c5ee3a3 # Where the ISO leaves a copy of the pinned package, for installs without GitHub. GSN_BACKUP_DIR="${GSN_BACKUP_DIR:-/usr/local/share/steamify/gamescope}" # How long a look at GitHub for a newer release counts (seconds), so opening the menu doesn't ask every time. @@ -123,7 +123,8 @@ gsn_build_package() { # On disk, not /tmp: the installer's chroot has a tmpfs there (RAM), and the build is 1-2 GB. mkdir -p "$cache" && dir="$(mktemp -d -p "$cache" gamescope-build.XXXXXX)" || return 1 patch_file gamescope-nvidia.PKGBUILD > "$dir/PKGBUILD" && - patch_file gamescope-gbm-scanout.patch > "$dir/gamescope-gbm-scanout.patch" || { + patch_file gamescope-gbm-scanout.patch > "$dir/gamescope-gbm-scanout.patch" && + patch_file gamescope-ui-native.patch > "$dir/gamescope-ui-native.patch" || { err "Couldn't write the build files." >&2 rm -rf "$dir"; return 1 } @@ -179,6 +180,43 @@ gsn_env_disable() { return 0 } +# --- The session starts gamescope on the display's own mode +# +# gamescope-session starts gamescope without a size or a refresh rate, so it takes the display's *preferred* mode, which +# many monitors declare as 60 Hz while the desktop runs them at their highest. patches/gamescope-display-wrapper.sh (put +# first in the gaming session's PATH by a drop-in of gamescope-session.service) starts it on the mode KDE has saved for +# the connected display, else the biggest mode at the highest refresh rate, read at every session start. +GSN_DISPLAY_WRAPPER="${GSN_DISPLAY_WRAPPER:-$STEAMIFY_BIN/gamescope}" +GSN_DISPLAY_DROPIN="${GSN_DISPLAY_DROPIN:-$HOME/.config/systemd/user/gamescope-session.service.d/steamify-display.conf}" + +gsn_display_status() { [[ -x "$GSN_DISPLAY_WRAPPER" && -f "$GSN_DISPLAY_DROPIN" ]]; } + +gsn_display_pending() { + # The patched gamescope is in use but the session still starts it on the display's preferred mode: a normal run sets it up. + gsn_package_installed && gsn_env_set && ! gsn_display_status +} + +gsn_display_enable() { + patch_file gamescope-display-wrapper.sh | install_executable "$GSN_DISPLAY_WRAPPER" 755 || return 1 + mkdir -p "$(dirname "$GSN_DISPLAY_DROPIN")" + cat > "$GSN_DISPLAY_DROPIN" << EOF +# Steamify: the gaming session starts gamescope on the display's own mode (see lib/gamescope-nvidia.sh). Turning the +# SteamOS conversion off removes this file. +[Service] +ExecStart= +ExecStart=/usr/bin/bash -c 'PATH="$(dirname "$GSN_DISPLAY_WRAPPER"):\$PATH" exec /usr/lib/steamos/gamescope-session' +EOF + user_systemctl daemon-reload +} + +gsn_display_disable() { + # Only our own files: the wrapper is recognised by its first comment line. + grep -qs "^# Steamify: gamescope-session starts gamescope" "$GSN_DISPLAY_WRAPPER" && rm -f "$GSN_DISPLAY_WRAPPER" + [[ -f "$GSN_DISPLAY_DROPIN" ]] && rm -f "$GSN_DISPLAY_DROPIN" && rmdir "$(dirname "$GSN_DISPLAY_DROPIN")" 2>/dev/null + user_systemctl daemon-reload + return 0 +} + gsn_enable() { # Only on a supported NVIDIA card; elsewhere the stock gamescope is right. nvidia_present || return 0 @@ -191,6 +229,7 @@ gsn_enable() { nvidia_extras_enable || return 1 gsn_install_package || return 1 gsn_env_enable + gsn_display_enable || warn "Couldn't set up the display mode for gaming mode: gamescope takes the display's preferred mode." ok "gamescope draws NVIDIA's picture through GBM (gamescope_drm_gbm_scanout=1) from the next gaming mode start." } @@ -198,6 +237,7 @@ gsn_disable() { # The setting goes, and what nvidia_*_enable added; the packages stay (patched gamescope, 32-bit libraries), like # the other packages of the conversion (steam, gamescope-session-cachyos). gsn_env_disable + gsn_display_disable nvidia_present || return 0 nvidia_extras_disable nvidia_kernel_disable diff --git a/lib/menu.sh b/lib/menu.sh index 66206f9..a58d110 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -117,7 +117,7 @@ feature_outdated() { [[ "$1" == gaming ]] && nvidia_migration_pending && return 0 [[ "${CURRENT[$1]:-0}" == 1 ]] || return 1 # NVIDIA: a newer patched gamescope is out (lib/gamescope-nvidia.sh): a normal run installs it. - [[ "$1" == gaming ]] && nvidia_present && gsn_update_pending && return 0 + [[ "$1" == gaming ]] && nvidia_present && { gsn_update_pending || gsn_display_pending; } && return 0 have="$(state_get features "$1" "$FEATURE_BASELINE")" [[ "$have" == off ]] && have="$FEATURE_BASELINE" want="${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}" diff --git a/patches/README.md b/patches/README.md index e63c914..ae5b89c 100644 --- a/patches/README.md +++ b/patches/README.md @@ -20,3 +20,5 @@ read and reviewed on their own. `lib/*.sh` reads them with `patch_file | `gamescope-nvidia.PKGBUILD` | `lib/gamescope-nvidia.sh` (gamescope on NVIDIA) | The recipe for the patched gamescope package (`steamify-gamescope-nvidia`, provides and conflicts with `gamescope`): Valve's gamescope at `pkgver` plus `gamescope-gbm-scanout.patch`; written with the patch to a temporary folder and built with `makepkg` | | `cros-ec-cec.c` | `lib/cec.sh` (HDMI-CEC) | Valve's Steam Machine CEC driver (GPL-2.0+, from evlaV/linux-integration at the pinned commit): the backup when downloading it from GitHub fails; used only if its sha256 equals `CEC_DRIVER_SHA256` (a new pin needs a new copy) | | `python-hid-shim.PKGBUILD` | `lib/packages.sh` (required packages) | An empty package that provides `python-hid`, for PCs that keep `python-hidapi` (they conflict; jupiter-hw-support, a dependency of gamescope-session-cachyos, wants python-hid, only the Steam Deck controller updater uses it): built with `makepkg` and installed before the conversion's packages | +| `gamescope-ui-native.patch` | `lib/gamescope-nvidia.sh` (gamescope on NVIDIA) | Small patch after the GBM one: keeps Steam's UI X server at the display's size when Steam (with `-steamos3`) asks for 1920x1080; `gamescope_xwayland_ui_native=0` turns it off | +| `gamescope-display-wrapper.sh` | `lib/gamescope-nvidia.sh` (gamescope on NVIDIA) | Installed as `gamescope` in Steamify's bin folder and put first in the gaming session's PATH: starts gamescope on the mode KDE has saved for the connected display, else the biggest mode at the highest refresh rate (modetest + `kwinoutputconfig.json`) | diff --git a/patches/gamescope-display-wrapper.sh b/patches/gamescope-display-wrapper.sh new file mode 100644 index 0000000..5a632be --- /dev/null +++ b/patches/gamescope-display-wrapper.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Steamify: gamescope-session starts gamescope without a size or a refresh rate, so it takes the display's *preferred* mode, +# which many monitors declare as 60 Hz (the faster modes sit in an extension block of the EDID). This starts it the way the +# desktop runs that display: the mode KDE has saved for the connected display (System Settings > Display), when the display +# still has it, else its biggest mode at the highest refresh rate. +# Read at every session start (modetest, part of libdrm which gamescope needs, and KDE's ~/.config/kwinoutputconfig.json), so +# changing the resolution in KDE, another monitor or a TV just works. Anything unclear (no modetest output, no modes, more than +# one display connected: gamescope chooses which one it uses) leaves gamescope's own choice alone. +real=/usr/bin/gamescope + +modes() { + # One line per mode of every connected connector: " " + local m + for m in nvidia-drm amdgpu i915 xe nouveau radeon; do + timeout 10 modetest -M "$m" -c 2>/dev/null | awk ' + /^[0-9]+[ \t]+[0-9]+[ \t]+(connected|disconnected|unknown)/ { conn = ($3 == "connected") ? $4 : ""; next } + conn != "" && /^[ \t]+#[0-9]+ [0-9]+x[0-9]+ [0-9.]+ / && !/interlace/ { + split($2, wh, "x"); printf "%s %d %d %.0f\n", conn, wh[1], wh[2], $3 + }' + done +} + +kde_mode() { + # kde_mode : " " as KDE has it saved for that connector, or nothing. + python3 -I - "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/kwinoutputconfig.json" 2>/dev/null << 'EOF' +import json, sys +try: + for block in json.load(open(sys.argv[2])): + if block.get("name") == "outputs": + for o in block.get("data", []): + m = o.get("mode") + if o.get("connectorName") == sys.argv[1] and m: + print(m["width"], m["height"], round(m["refreshRate"] / 1000)) +except Exception: + pass +EOF +} + +args=() +all="$(modes)" +conn="$(awk '{print $1}' <<< "$all" | sort -u)" +if [[ -n "$conn" && "$(wc -l <<< "$conn")" == 1 ]]; then + read -r w h r <<< "$(kde_mode "$conn")" + # KDE's mode only counts when the display really has it (a refresh rate within 1 Hz). + if [[ -z "${w:-}" ]] || ! awk -v w="$w" -v h="${h:-0}" -v r="${r:-0}" '$2 == w && $3 == h && ($4 - r < 2 && r - $4 < 2) {f = 1} END {exit !f}' <<< "$all"; then + read -r _ w h r < <(awk '{print $2 * $3, $2, $3, $4}' <<< "$all" | sort -k1,1nr -k4,4nr | head -1 | awk '{print "x", $2, $3, $4}') + fi + [[ "${w:-0}" -gt 0 && "${h:-0}" -gt 0 && "${r:-0}" -gt 0 ]] && args=(-W "$w" -H "$h" -r "$r") +fi +if [[ "${1:-}" == --print-args ]]; then echo "${args[*]}"; exit 0; fi +exec "$real" "${args[@]}" "$@" diff --git a/patches/gamescope-nvidia.PKGBUILD b/patches/gamescope-nvidia.PKGBUILD index 6b6b7e5..cde2ea0 100644 --- a/patches/gamescope-nvidia.PKGBUILD +++ b/patches/gamescope-nvidia.PKGBUILD @@ -2,14 +2,14 @@ # Based on Arch Linux's gamescope PKGBUILD (Maxime Gauduin, Giancarlo Razzolini, Sven-Hendrik Haase and contributors). # # gamescope with GBM-allocated scan-out buffers for NVIDIA (opt-in: gamescope_drm_gbm_scanout=1). -# The patch is gamescope-gbm-scanout.patch (next to this file; see its header for where it comes from). pkgver is the gamescope -# release the patch was made for and tested with. Kept in sync with https://github.com/theupriser/steamify-gamescope-nvidia; -# Steamify (lib/gamescope-nvidia.sh) writes both files into a temporary folder and runs makepkg there. +# The patches are gamescope-gbm-scanout.patch and gamescope-ui-native.patch (next to this file, applied in this order); see the +# README for where they come from. +# UPSTREAM_VERSION (next to this file) names the gamescope tag the patch is made for. pkgname=steamify-gamescope-nvidia _srcname=gamescope pkgver=3.16.31 -pkgrel=1 +pkgrel=2 pkgdesc='SteamOS session compositing window manager, with GBM scan-out for NVIDIA (opt-in: gamescope_drm_gbm_scanout=1)' arch=(x86_64) url=https://github.com/theupriser/steamify-gamescope-nvidia @@ -73,6 +73,7 @@ makedepends=( source=( gamescope::git+https://github.com/ValveSoftware/gamescope.git#commit=${pkgver} gamescope-gbm-scanout.patch + gamescope-ui-native.patch git+https://gitlab.freedesktop.org/emersion/libdisplay-info.git git+https://gitlab.freedesktop.org/emersion/libliftoff.git git+https://github.com/ValveSoftware/openvr.git @@ -89,6 +90,7 @@ b2sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' + 'SKIP' 'SKIP') prepare() { @@ -98,6 +100,8 @@ prepare() { # silently produce an unpatched gamescope under this package's name. git apply --check ../gamescope-gbm-scanout.patch || { echo "gbm patch does not apply to gamescope ${pkgver}" >&2; return 1; } git apply ../gamescope-gbm-scanout.patch + git apply --check ../gamescope-ui-native.patch || { echo "ui-native patch does not apply to gamescope ${pkgver}" >&2; return 1; } + git apply ../gamescope-ui-native.patch meson subprojects download diff --git a/patches/gamescope-ui-native.patch b/patches/gamescope-ui-native.patch new file mode 100644 index 0000000..78473fc --- /dev/null +++ b/patches/gamescope-ui-native.patch @@ -0,0 +1,39 @@ +Keeps Steam's UI X server (server 0) at the output size when Steam asks for a smaller one. + +With -steamos3 (what gamescope-session-cachyos passes) Steam sets its UI to 1920x1080 right after it starts +(GAMESCOPE_XWAYLAND_MODE_CONTROL), so on a bigger display, and on one that isn't 16:9, the whole interface is a scaled-up +1080p picture. Games already run at the display's size. With this the server stays at the output size and Steam draws its +window to match; Steam's SteamOS features (settings pages, game launching) keep working. Written for Steamify on an NVIDIA +PC (RTX 5080, 3440x1440@175, tested with Steam's UI and a Proton game); not part of the farnoy patch. On by default; the +setting gamescope_xwayland_ui_native=0 (in gamescope's environment) turns it off. +Applies with `git apply` after gamescope-gbm-scanout.patch (it ignores these lines). + +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 2cb5eb5..ce14c8d 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -129,6 +129,8 @@ LogScope g_WaitableLog("waitable"); + + gamescope::ConVar cv_overlay_unmultiplied_alpha{ "overlay_unmultiplied_alpha", false }; + ++gamescope::ConVar cv_xwayland_ui_native( "xwayland_ui_native", true, "Keep the Steam UI X server (server 0) at the output size when Steam asks for a smaller one." ); ++ + gamescope::ConVar cv_vr_show_forwarded_overlays{ "vr_show_forwarded_overlays", false }; + + std::string *g_pVROverlayKey = nullptr; +@@ -7240,6 +7242,15 @@ handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev) + height = std::min(height, currentOutputHeight); + } + ++ // Steam asks for a smaller size for its own UI (server 0, 1920x1080 with -steamos3), which makes the whole ++ // interface a scaled-up picture on a bigger display, and on one that isn't 16:9 as well. The UI server ++ // stays at the output size: Steam sizes its window to the server. ++ if ( server_idx == 0 && cv_xwayland_ui_native ) ++ { ++ width = currentOutputWidth; ++ height = currentOutputHeight; ++ } ++ + gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( server_idx ); + if ( server ) + {