From 414b73388e63b9a17fe76cd67671cd8bd5d171fa Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 10:54:16 +0200 Subject: [PATCH 01/17] chore: Version 2.9.7 --- CHANGELOG.md | 2 ++ steamify.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b878672..c3e6465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes, per version and per commit. Versions follow `steamify.sh`. Versions before 0.7.0 were numbered afterwards, one per merged pull request. +## 2.9.7 - 2026-10-01 + ## 2.9.6 - 2026-09-30 - **fix(ci): a dev ISO from a release branch is built with that branch's own Steamify (input steamify_ref of the ISO workflow), not the newest published release: release/2.9.6 kept producing 2.9.5 ISOs** diff --git a/steamify.sh b/steamify.sh index 7a0a987..0943d72 100755 --- a/steamify.sh +++ b/steamify.sh @@ -15,7 +15,7 @@ set -uo pipefail # Release version, see CHANGELOG.md. -VERSION=2.9.6 +VERSION=2.9.7 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" From 71f11a9f8a54618f220c8e01ca42394f27e90883 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 10:54:27 +0200 Subject: [PATCH 02/17] feat: NVIDIA fix for gaming mode (kernel parameters and early modules) Untested on hardware; .no-release-yet keeps it out of the release. --- AGENTS.md | 5 +++ CHANGELOG.md | 2 + TECHNICAL.md | 14 ++++++ lib/login-manager.sh | 2 + lib/menu.sh | 2 +- lib/nvidia.sh | 102 +++++++++++++++++++++++++++++++++++++++++++ steamify.sh | 2 +- 7 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 lib/nvidia.sh diff --git a/AGENTS.md b/AGENTS.md index 54da57b..65a1e46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,6 +127,11 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine it. Only the release branch's pull request into `main` is the user's: never commit or merge to `main`, the user merges that one. + A `.no-release-yet` file at the root of a feature/bugfix branch means + it must not be merged into the release branch yet (work in progress, + untested on hardware). When the feature is complete, ask the user + whether it may be released; on a yes, delete the file, then open the + pull request and merge it. Keep a feature/bugfix branch up to date by merging (or rebasing on) its release branch, the release branch by merging `main` when that moved. diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e6465..3a9858e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ one per merged pull request. ## 2.9.7 - 2026-10-01 +- **feat: the SteamOS conversion fixes a corrupted gaming mode screen with an NVIDIA GPU: nvidia-drm.modeset=1 and fbdev=1 on the kernel command line, NVIDIA modules in the initramfs (lib/nvidia.sh)** + ## 2.9.6 - 2026-09-30 - **fix(ci): a dev ISO from a release branch is built with that branch's own Steamify (input steamify_ref of the ISO workflow), not the newest published release: release/2.9.6 kept producing 2.9.5 ISOs** diff --git a/TECHNICAL.md b/TECHNICAL.md index 20da47e..c04f9ce 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -206,6 +206,19 @@ or upgraded. A newly added kernel doesn't come with its headers, so `ensure-kernel-headers.service` checks at every boot and installs any missing `-headers` package, which makes DKMS build the driver for it. +## NVIDIA fix for gaming mode + +gamescope drives the display itself through DRM/KMS, which with NVIDIA needs +kernel modesetting and the framebuffer driver, and the driver loaded early; +without them gaming mode can show a corrupted image. With an NVIDIA GPU and +its driver installed (`lib/nvidia.sh`), the SteamOS conversion adds +`nvidia-drm.modeset=1 nvidia-drm.fbdev=1` to the kernel command line (Limine, +systemd-boot or GRUB; the file is backed up first) and +`/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia +nvidia_modeset nvidia_uvm nvidia_drm)`), then rebuilds the initramfs and boot +entries. Only what's missing is changed; it applies after a reboot. Turning +the conversion off removes both. Untested on hardware so far. + ## HDMI-CEC The **HDMI-CEC** item (on every PC; ticked by default only on a Steam @@ -510,6 +523,7 @@ immediately, which can turn into a loop - see | `lib/steam-game.sh` | Add as non-Steam game: Steamify in the Steam library (`patches/steam-shortcuts.py`) | | `lib/update-notifier.sh` | Update notifications: the notifier from `patches/` and its user timer | | `lib/first-login.sh` | `--defaults` without a session (the Steam Machine ISO's installer): the one-time first-login step that sets up single user's launcher on Plasma's new layout and opens the app | +| `lib/nvidia.sh` | NVIDIA fix for gaming mode: kernel parameters and early modules (part of the SteamOS conversion) | | `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) | | `services/` | The systemd units the scripts install (`service_file`, `@KEY@` placeholders); see its README | | `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README | diff --git a/lib/login-manager.sh b/lib/login-manager.sh index bd35013..40d0280 100644 --- a/lib/login-manager.sh +++ b/lib/login-manager.sh @@ -109,6 +109,7 @@ gaming_enable() { esac create_desktop_shortcut steam_enable + nvidia_enable || return 1 os_name_enable || return 1 } @@ -124,6 +125,7 @@ gaming_disable() { steam_disable krevert gaming os_name_disable + nvidia_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 4356cee..2c99303 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -25,7 +25,7 @@ NO_PRESELECT=(boot cec kpin hdmi) # FEATURE_BASELINE. FEATURE_BASELINE=2.1.0 declare -A FEATURE_VERSION=( - [gaming]=2.9.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 + [gaming]=2.9.7 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 [launcher]=2.1.0 [cec]=2.7.0 [machine]=2.9.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [steamgame]=2.5.1 [kpin]=2.1.0 [hdmi]=2.1.0 ) diff --git a/lib/nvidia.sh b/lib/nvidia.sh new file mode 100644 index 0000000..ef08329 --- /dev/null +++ b/lib/nvidia.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# NVIDIA fix for gaming mode, part of the SteamOS conversion +# (gaming_enable/gaming_disable). gamescope drives the display itself +# through DRM/KMS; with NVIDIA that needs kernel modesetting and the +# framebuffer driver (nvidia-drm.modeset=1 nvidia-drm.fbdev=1) and the +# driver in the initramfs, or the screen shows a corrupted image. +# Does nothing without an NVIDIA GPU that has its driver installed. +# Sourced by steamify.sh; not meant to be run on its own. + +NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1" +NVIDIA_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf +NVIDIA_MARK="# steamify-nvidia" + +nvidia_present() { + # An NVIDIA GPU (PCI vendor 0x10de, display class) and its kernel driver. + local d + for d in /sys/class/drm/card[0-9]*/device; do + [[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue + [[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] && modinfo nvidia_drm >/dev/null 2>&1 && return 0 + done + return 1 +} + +nvidia_boot_file() { + # Same boot loaders as the kernel command line elsewhere (lib/hdmi-refresh.sh). + hdmi_boot_file +} + +nvidia_param_set() { + # 0 when $1 (e.g. nvidia-drm.modeset=1) is on the kernel command line or + # already in the boot loader's file (not booted yet). + local f; f="$(nvidia_boot_file)" + grep -qw -- "$1" /proc/cmdline 2>/dev/null || + { [[ -n "$f" ]] && grep -qw -- "$1" "$f" 2>/dev/null; } +} + +nvidia_params_missing() { + local p + for p in $NVIDIA_PARAMS; do nvidia_param_set "$p" || return 0; done + return 1 +} + +nvidia_initramfs_ok() { + [[ -f "$NVIDIA_INITRAMFS_CONF" ]] || grep -qE '^MODULES=.*nvidia_drm' /etc/mkinitcpio.conf 2>/dev/null +} + +nvidia_rebuild_boot() { + local f; f="$(nvidia_boot_file)" + info "Rebuilding the initramfs and boot entries..." + case "$f" in + /etc/default/limine) sudo limine-mkinitcpio ;; + /etc/sdboot-manage.conf) sudo /usr/bin/mkinitcpio -P && sudo sdboot-manage gen ;; + /etc/default/grub) sudo /usr/bin/mkinitcpio -P && sudo grub-mkconfig -o /boot/grub/grub.cfg ;; + *) sudo /usr/bin/mkinitcpio -P ;; + esac +} + +nvidia_enable() { + nvidia_present || return 0 + local f changed=0 p; f="$(nvidia_boot_file)" + if nvidia_params_missing; then + if [[ -z "$f" ]]; then + warn "NVIDIA GPU found, but no supported boot loader: add '$NVIDIA_PARAMS' to the kernel command line yourself." + else + info "NVIDIA GPU found: adding $NVIDIA_PARAMS to the kernel command line (fixes a corrupted screen in gaming mode)..." + backup_file "$f" + case "$f" in + /etc/default/limine) + printf 'KERNEL_CMDLINE[default]+=" %s" %s\n' "$NVIDIA_PARAMS" "$NVIDIA_MARK" | sudo tee -a "$f" >/dev/null ;; + /etc/sdboot-manage.conf) + sudo sed -i -E "s/^(LINUX_OPTIONS=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; + /etc/default/grub) + sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; + esac + changed=1 + fi + fi + if ! nvidia_initramfs_ok; then + info "Loading the NVIDIA modules early (initramfs)..." + printf 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)\n' | + sudo install -Dm644 /dev/stdin "$NVIDIA_INITRAMFS_CONF" && changed=1 + fi + if [[ $changed == 1 ]]; then + nvidia_rebuild_boot || { err "Rebuilding the boot entries failed."; return 1; } + ok "NVIDIA gaming mode fix applied; it takes effect after a reboot." + fi + return 0 +} + +nvidia_disable() { + local f changed=0; f="$(nvidia_boot_file)" + if [[ -n "$f" ]] && grep -q "$NVIDIA_PARAMS" "$f" 2>/dev/null; then + case "$f" in + /etc/default/limine) sudo sed -i "/$NVIDIA_MARK\$/d" "$f" ;; + *) sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f" ;; + esac + changed=1 + fi + [[ -f "$NVIDIA_INITRAMFS_CONF" ]] && { sudo rm -f "$NVIDIA_INITRAMFS_CONF"; changed=1; } + [[ $changed == 1 ]] && nvidia_rebuild_boot + return 0 +} diff --git a/steamify.sh b/steamify.sh index 0943d72..1050c83 100755 --- a/steamify.sh +++ b/steamify.sh @@ -19,7 +19,7 @@ VERSION=2.9.7 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 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 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 From d354eace93428e5b0591564d91465c1e80de5207 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 10:54:49 +0200 Subject: [PATCH 03/17] docs: AGENTS.md: never work on main or a release branch, hardware-specific fixes, git identity --- AGENTS.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 65a1e46..b065799 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,13 @@ 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`. +- **Hardware-specific fixes** that belong to a component but aren't a menu + row (e.g. `lib/nvidia.sh`: NVIDIA's kernel parameters and early modules + for gaming mode, called from `gaming_enable`/`gaming_disable`) detect the + hardware themselves, change only what's missing, and undo it on disable. + Add them to the source loop in `steamify.sh` and the table in + `TECHNICAL.md`. They can't be tested without the hardware: say so in the + changelog and keep the branch behind `.no-release-yet` until it was. - **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 @@ -126,7 +133,15 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine `sudo apt install gh`, then `gh auth login`) instead of working around it. Only the release branch's pull request into `main` is the user's: never commit - or merge to `main`, the user merges that one. + or merge to `main`, the user merges that one. Never work directly on + `main` or a `release/*` branch either: edits, even uncommitted ones, + go on a `feature/`/`bugfix/` branch (create it before editing; the + release branch only gets the version-bump commit and merged PRs). + Don't push a new `release/*` branch before it's needed: a push can + start CI builds (a dev ISO). + A machine without a git identity: pass the repo's author per command + (`git -c user.name=... -c user.email=... commit`, taken from `git + log`) instead of changing the git config. A `.no-release-yet` file at the root of a feature/bugfix branch means it must not be merged into the release branch yet (work in progress, untested on hardware). When the feature is complete, ask the user From ad6cf2652185f3cf3f79e75a12f405642a33b19d Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 11:07:32 +0200 Subject: [PATCH 04/17] test: NVIDIA fix against a fake RTX 5080, stub modinfo and temp boot loader files --- CHANGELOG.md | 1 + lib/nvidia.sh | 12 ++++++----- tests/nvidia-test.sh | 48 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 tests/nvidia-test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9858e..615ddd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ one per merged pull request. ## 2.9.7 - 2026-10-01 - **feat: the SteamOS conversion fixes a corrupted gaming mode screen with an NVIDIA GPU: nvidia-drm.modeset=1 and fbdev=1 on the kernel command line, NVIDIA modules in the initramfs (lib/nvidia.sh)** +- **test: tests/nvidia-test.sh runs the NVIDIA fix against a fake RTX 5080 beside an iGPU, a stub modinfo and temp copies of the Limine, systemd-boot and GRUB files (no root, VM or hardware); NVIDIA_DRM_DIR points detection at a fake sysfs tree** ## 2.9.6 - 2026-09-30 diff --git a/lib/nvidia.sh b/lib/nvidia.sh index ef08329..c7d0cb9 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -10,11 +10,13 @@ NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1" NVIDIA_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf NVIDIA_MARK="# steamify-nvidia" +# The DRM devices in sysfs; the tests point this at a fake tree. +NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm} nvidia_present() { # An NVIDIA GPU (PCI vendor 0x10de, display class) and its kernel driver. local d - for d in /sys/class/drm/card[0-9]*/device; do + for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do [[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue [[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] && modinfo nvidia_drm >/dev/null 2>&1 && return 0 done @@ -65,11 +67,11 @@ nvidia_enable() { info "NVIDIA GPU found: adding $NVIDIA_PARAMS to the kernel command line (fixes a corrupted screen in gaming mode)..." backup_file "$f" case "$f" in - /etc/default/limine) + */limine) printf 'KERNEL_CMDLINE[default]+=" %s" %s\n' "$NVIDIA_PARAMS" "$NVIDIA_MARK" | sudo tee -a "$f" >/dev/null ;; - /etc/sdboot-manage.conf) + */sdboot-manage.conf) sudo sed -i -E "s/^(LINUX_OPTIONS=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; - /etc/default/grub) + */grub) sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; esac changed=1 @@ -91,7 +93,7 @@ nvidia_disable() { local f changed=0; f="$(nvidia_boot_file)" if [[ -n "$f" ]] && grep -q "$NVIDIA_PARAMS" "$f" 2>/dev/null; then case "$f" in - /etc/default/limine) sudo sed -i "/$NVIDIA_MARK\$/d" "$f" ;; + */limine) sudo sed -i "/$NVIDIA_MARK\$/d" "$f" ;; *) sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f" ;; esac changed=1 diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh new file mode 100644 index 0000000..2a9a0d4 --- /dev/null +++ b/tests/nvidia-test.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Tests lib/nvidia.sh against a fake RTX 5080 (PCI 10de:2c02) beside an +# Intel iGPU, a stub modinfo, and temp copies of the boot loader files. No +# root, VM or NVIDIA hardware needed: bash tests/nvidia-test.sh +cd "$(dirname "$0")/.." || exit 1 +T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT +fail=0 +check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; } + +source lib/common.sh; source lib/hdmi-refresh.sh; source lib/nvidia.sh +sudo() { "$@"; } +info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; } +backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; } +modinfo() { [[ "$FAKE_DRIVER" == 1 ]]; } +REBUILDS=0; nvidia_rebuild_boot() { REBUILDS=$((REBUILDS + 1)); } +NVIDIA_INITRAMFS_CONF="$T/90-steamify-nvidia.conf" +mkdir -p "$T/drm/card0/device" "$T/drm/card1/device" +echo 0x8086 > "$T/drm/card0/device/vendor"; echo 0x030000 > "$T/drm/card0/device/class" +echo 0x10de > "$T/drm/card1/device/vendor"; echo 0x030000 > "$T/drm/card1/device/class" +echo 0x2c02 > "$T/drm/card1/device/device" +NVIDIA_DRM_DIR="$T/drm" + +FAKE_DRIVER=1; check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present +FAKE_DRIVER=0; check "skips when the driver isn't installed" '! nvidia_present' +FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "skips without an NVIDIA GPU" '! nvidia_present' +NVIDIA_DRM_DIR="$T/drm" + +for loader in limine sdboot grub; do + case $loader in + limine) F="$T/limine"; printf 'KERNEL_CMDLINE[default]="quiet splash"\n' > "$F" ;; + sdboot) F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet splash"\n' > "$F" ;; + grub) F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"\n' > "$F" ;; + esac + nvidia_boot_file() { echo "$F"; } + rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; ORIG="$(cat "$F")" + nvidia_enable + check "$loader: parameters added" 'grep -q "nvidia-drm.modeset=1" "$F" && grep -q "nvidia-drm.fbdev=1" "$F"' + check "$loader: initramfs drop-in written" 'grep -q nvidia_drm "$NVIDIA_INITRAMFS_CONF"' + check "$loader: backup made, rebuilt once" '[[ -f "$F.bak" && $REBUILDS == 1 ]]' + SUM="$(cat "$F")"; nvidia_enable + check "$loader: second run changes nothing" '[[ "$(cat "$F")" == "$SUM" && $REBUILDS == 1 ]]' + nvidia_disable + check "$loader: disable restores the file and drops the drop-in" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" ]]' +done + +FAKE_DRIVER=0; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_enable +check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]' +exit $fail From 799f5abccaa4f74b2bf94d3082ba7ecd8eafea4f Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 12:48:56 +0200 Subject: [PATCH 05/17] fix: NVIDIA fix on Limine edits the existing cmdline line; early modules only when every kernel has them Found by running it in a VM: the appended += line became literal text on the kernel command line, and a kernel without the modules made limine-mkinitcpio skip its boot entry. --- CHANGELOG.md | 1 + TECHNICAL.md | 7 ++++++- lib/nvidia.sh | 30 +++++++++++++++++++++++------- tests/nvidia-test.sh | 7 +++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615ddd7..309b639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ one per merged pull request. - **feat: the SteamOS conversion fixes a corrupted gaming mode screen with an NVIDIA GPU: nvidia-drm.modeset=1 and fbdev=1 on the kernel command line, NVIDIA modules in the initramfs (lib/nvidia.sh)** - **test: tests/nvidia-test.sh runs the NVIDIA fix against a fake RTX 5080 beside an iGPU, a stub modinfo and temp copies of the Limine, systemd-boot and GRUB files (no root, VM or hardware); NVIDIA_DRM_DIR points detection at a fake sysfs tree** +- **fix: NVIDIA fix on Limine edits the KERNEL_CMDLINE[default] line (an appended += line ended up as literal text on the kernel command line, found in a VM); the modules are only loaded early when every installed kernel has them, and a Limine boot entry without the parameters is an error** ## 2.9.6 - 2026-09-30 diff --git a/TECHNICAL.md b/TECHNICAL.md index c04f9ce..0d8d160 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -216,7 +216,12 @@ its driver installed (`lib/nvidia.sh`), the SteamOS conversion adds systemd-boot or GRUB; the file is backed up first) and `/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)`), then rebuilds the initramfs and boot -entries. Only what's missing is changed; it applies after a reboot. Turning +entries. The early-load drop-in is only written when every installed kernel +has the NVIDIA modules (mkinitcpio fails on a missing module, and +`limine-mkinitcpio` then skips that kernel's boot entry, parameters included). +On Limine the parameters go into the existing `KERNEL_CMDLINE[default]="..."` +line: an extra appended `+=` line is pasted into the command line as text. +Only what's missing is changed; it applies after a reboot. Turning the conversion off removes both. Untested on hardware so far. ## HDMI-CEC diff --git a/lib/nvidia.sh b/lib/nvidia.sh index c7d0cb9..ad0b89e 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -9,7 +9,6 @@ NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1" NVIDIA_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf -NVIDIA_MARK="# steamify-nvidia" # The DRM devices in sysfs; the tests point this at a fake tree. NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm} @@ -46,6 +45,19 @@ nvidia_initramfs_ok() { [[ -f "$NVIDIA_INITRAMFS_CONF" ]] || grep -qE '^MODULES=.*nvidia_drm' /etc/mkinitcpio.conf 2>/dev/null } +nvidia_modules_everywhere() { + # 0 when every installed kernel has the nvidia_drm module: mkinitcpio fails + # on a MODULES entry a kernel lacks, and limine-mkinitcpio then skips that + # kernel's initramfs and boot entry, so the parameters never reach it. + local k found=0 + for k in "${NVIDIA_MODULES_DIR:-/usr/lib/modules}"/*/; do + [[ -f "$k/pkgbase" || -d "$k/kernel" || -d "$k/updates" || -d "$k/extramodules" ]] || continue + find "$k" -name 'nvidia-drm.ko*' -print -quit 2>/dev/null | grep -q . || return 1 + found=1 + done + [[ $found == 1 ]] +} + nvidia_rebuild_boot() { local f; f="$(nvidia_boot_file)" info "Rebuilding the initramfs and boot entries..." @@ -68,7 +80,7 @@ nvidia_enable() { backup_file "$f" case "$f" in */limine) - printf 'KERNEL_CMDLINE[default]+=" %s" %s\n' "$NVIDIA_PARAMS" "$NVIDIA_MARK" | sudo tee -a "$f" >/dev/null ;; + sudo sed -i -E "s/^(KERNEL_CMDLINE\[default\]=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; */sdboot-manage.conf) sudo sed -i -E "s/^(LINUX_OPTIONS=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; */grub) @@ -77,13 +89,20 @@ nvidia_enable() { changed=1 fi fi - if ! nvidia_initramfs_ok; then + if ! nvidia_initramfs_ok && ! nvidia_modules_everywhere; then + warn "Not every installed kernel has the NVIDIA modules: skipping loading them early (the kernel parameters still apply)." + elif ! nvidia_initramfs_ok; then info "Loading the NVIDIA modules early (initramfs)..." printf 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)\n' | sudo install -Dm644 /dev/stdin "$NVIDIA_INITRAMFS_CONF" && changed=1 fi if [[ $changed == 1 ]]; then nvidia_rebuild_boot || { err "Rebuilding the boot entries failed."; return 1; } + if [[ "$f" == */limine && -f "${NVIDIA_LIMINE_CONF:-/boot/limine.conf}" ]] && + ! grep -q 'nvidia-drm.modeset=1' "${NVIDIA_LIMINE_CONF:-/boot/limine.conf}"; then + err "The boot entries don't have the NVIDIA parameters: the initramfs build failed (see above)." + return 1 + fi ok "NVIDIA gaming mode fix applied; it takes effect after a reboot." fi return 0 @@ -92,10 +111,7 @@ nvidia_enable() { nvidia_disable() { local f changed=0; f="$(nvidia_boot_file)" if [[ -n "$f" ]] && grep -q "$NVIDIA_PARAMS" "$f" 2>/dev/null; then - case "$f" in - */limine) sudo sed -i "/$NVIDIA_MARK\$/d" "$f" ;; - *) sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f" ;; - esac + sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f" changed=1 fi [[ -f "$NVIDIA_INITRAMFS_CONF" ]] && { sudo rm -f "$NVIDIA_INITRAMFS_CONF"; changed=1; } diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index 2a9a0d4..ff35623 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -19,6 +19,8 @@ echo 0x8086 > "$T/drm/card0/device/vendor"; echo 0x030000 > "$T/drm/card0/device echo 0x10de > "$T/drm/card1/device/vendor"; echo 0x030000 > "$T/drm/card1/device/class" echo 0x2c02 > "$T/drm/card1/device/device" NVIDIA_DRM_DIR="$T/drm" +mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst" +NVIDIA_MODULES_DIR="$T/mods" FAKE_DRIVER=1; check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present FAKE_DRIVER=0; check "skips when the driver isn't installed" '! nvidia_present' @@ -43,6 +45,11 @@ for loader in limine sdboot grub; do check "$loader: disable restores the file and drops the drop-in" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" ]]' done +# A second kernel without the NVIDIA modules: no early-load drop-in, parameters still set. +mkdir -p "$T/mods/6.6.0-lts/kernel"; F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet"\n' > "$F" +nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_enable +check "kernel without nvidia modules: no drop-in, parameters set" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && grep -q "nvidia-drm.fbdev=1" "$F"' +rm -rf "$T/mods/6.6.0-lts" FAKE_DRIVER=0; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_enable check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]' exit $fail From 5aa4c65254a07c9809598ba01d1a0056e78a455a Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 13:07:07 +0200 Subject: [PATCH 06/17] fix: NVIDIA fix checks the edit and the generated boot entries for every boot loader (limine.conf, systemd-boot entries, grub.cfg) --- lib/nvidia.sh | 23 ++++++++++++++++++++--- tests/nvidia-test.sh | 9 +++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/nvidia.sh b/lib/nvidia.sh index ad0b89e..947652f 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -69,6 +69,20 @@ nvidia_rebuild_boot() { esac } +nvidia_boot_has_params() { + # 0 when the generated boot entries carry the parameters (or can't be + # checked): limine.conf, systemd-boot's entries, grub.cfg. + local target + case "$1" in + */limine) target="${NVIDIA_LIMINE_CONF:-/boot/limine.conf}" ;; + */sdboot-manage.conf) target="${NVIDIA_SDBOOT_DIR:-/boot/loader/entries}" ;; + */grub) target="${NVIDIA_GRUB_CFG:-/boot/grub/grub.cfg}" ;; + *) return 0 ;; + esac + sudo test -e "$target" || return 0 + sudo grep -rqs 'nvidia-drm.modeset=1' "$target" +} + nvidia_enable() { nvidia_present || return 0 local f changed=0 p; f="$(nvidia_boot_file)" @@ -86,6 +100,10 @@ nvidia_enable() { */grub) sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; esac + if ! grep -q "nvidia-drm.fbdev=1" "$f"; then + err "Couldn't add the NVIDIA parameters to $f (no kernel command line setting in it): add '$NVIDIA_PARAMS' yourself." + return 1 + fi changed=1 fi fi @@ -98,9 +116,8 @@ nvidia_enable() { fi if [[ $changed == 1 ]]; then nvidia_rebuild_boot || { err "Rebuilding the boot entries failed."; return 1; } - if [[ "$f" == */limine && -f "${NVIDIA_LIMINE_CONF:-/boot/limine.conf}" ]] && - ! grep -q 'nvidia-drm.modeset=1' "${NVIDIA_LIMINE_CONF:-/boot/limine.conf}"; then - err "The boot entries don't have the NVIDIA parameters: the initramfs build failed (see above)." + if ! nvidia_boot_has_params "$f"; then + err "The boot entries don't have the NVIDIA parameters: the initramfs or boot entry build failed (see above)." return 1 fi ok "NVIDIA gaming mode fix applied; it takes effect after a reboot." diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index ff35623..d908b04 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -21,6 +21,7 @@ echo 0x2c02 > "$T/drm/card1/device/device" NVIDIA_DRM_DIR="$T/drm" mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst" NVIDIA_MODULES_DIR="$T/mods" +NVIDIA_LIMINE_CONF="$T/none/limine.conf"; NVIDIA_SDBOOT_DIR="$T/none/entries"; NVIDIA_GRUB_CFG="$T/none/grub.cfg" # not generated here: not checked FAKE_DRIVER=1; check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present FAKE_DRIVER=0; check "skips when the driver isn't installed" '! nvidia_present' @@ -50,6 +51,14 @@ mkdir -p "$T/mods/6.6.0-lts/kernel"; F="$T/sdboot-manage.conf"; printf 'LINUX_OP nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_enable check "kernel without nvidia modules: no drop-in, parameters set" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && grep -q "nvidia-drm.fbdev=1" "$F"' rm -rf "$T/mods/6.6.0-lts" +# Generated boot entries without the parameters (the initramfs build failed): an error. +printf 'linux /vmlinuz root=/dev/sda1\n' > "$T/grub.cfg"; NVIDIA_GRUB_CFG="$T/grub.cfg" +F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet"\n' > "$F"; nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF" +check "boot entries without the parameters: error" '! nvidia_enable >/dev/null' +NVIDIA_GRUB_CFG="$T/none/grub.cfg" +# A config file without the setting to edit: an error, not "applied". +F="$T/sdboot-manage.conf"; printf '#LINUX_OPTIONS=""\n' > "$F"; nvidia_boot_file() { echo "$F"; }; REBUILDS=0 +check "no setting to edit: error, nothing rebuilt" '! nvidia_enable >/dev/null && [[ $REBUILDS == 0 ]]' FAKE_DRIVER=0; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_enable check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]' exit $fail From 68d28b95ed44b1802161b1f527f60673c69661ca Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 13:20:01 +0200 Subject: [PATCH 07/17] test: nvidia-hardware-test.sh to check, apply and visually verify the NVIDIA fix on a real PC --- CHANGELOG.md | 1 + TECHNICAL.md | 5 +- tests/nvidia-hardware-test.sh | 87 +++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/nvidia-hardware-test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 309b639..d0081f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ one per merged pull request. - **feat: the SteamOS conversion fixes a corrupted gaming mode screen with an NVIDIA GPU: nvidia-drm.modeset=1 and fbdev=1 on the kernel command line, NVIDIA modules in the initramfs (lib/nvidia.sh)** - **test: tests/nvidia-test.sh runs the NVIDIA fix against a fake RTX 5080 beside an iGPU, a stub modinfo and temp copies of the Limine, systemd-boot and GRUB files (no root, VM or hardware); NVIDIA_DRM_DIR points detection at a fake sysfs tree** - **fix: NVIDIA fix on Limine edits the KERNEL_CMDLINE[default] line (an appended += line ended up as literal text on the kernel command line, found in a VM); the modules are only loaded early when every installed kernel has them, and a Limine boot entry without the parameters is an error** +- **test: tests/nvidia-hardware-test.sh for a real NVIDIA PC: check (read-only state of the detection, the live nvidia_drm modeset/fbdev, versions), apply, and visual (was the gaming mode picture clean), with a report file to paste** ## 2.9.6 - 2026-09-30 diff --git a/TECHNICAL.md b/TECHNICAL.md index 0d8d160..c88d777 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -222,7 +222,10 @@ has the NVIDIA modules (mkinitcpio fails on a missing module, and On Limine the parameters go into the existing `KERNEL_CMDLINE[default]="..."` line: an extra appended `+=` line is pasted into the command line as text. Only what's missing is changed; it applies after a reboot. Turning -the conversion off removes both. Untested on hardware so far. +the conversion off removes both. `tests/nvidia-test.sh` tests it against a fake GPU +and fake boot loader files; `tests/nvidia-hardware-test.sh check|apply|visual` is for a +real NVIDIA PC (read-only check, the real apply, and a prompt for whether the picture +was clean), and writes `~/steamify-nvidia-report.txt`. ## HDMI-CEC diff --git a/tests/nvidia-hardware-test.sh b/tests/nvidia-hardware-test.sh new file mode 100644 index 0000000..7f2dc50 --- /dev/null +++ b/tests/nvidia-hardware-test.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Test of the NVIDIA fix on a real PC with an NVIDIA GPU (the unit test fakes +# one: tests/nvidia-test.sh). Run from the Steamify checkout: +# bash tests/nvidia-hardware-test.sh check read-only: what is detected, what the live kernel uses, what apply would change +# bash tests/nvidia-hardware-test.sh apply really applies the fix (sudo), then reboot and run check again +# bash tests/nvidia-hardware-test.sh visual after trying gaming mode: was the picture clean? (asks you) +# Every run appends to ~/steamify-nvidia-report.txt: paste that file when asking for help. +cd "$(dirname "$0")/.." || exit 1 +REPORT="${NVIDIA_REPORT:-$HOME/steamify-nvidia-report.txt}" +source lib/common.sh; source lib/hdmi-refresh.sh; source lib/nvidia.sh +mode="${1:-check}" +fail=0 +say() { echo "$*" | tee -a "$REPORT"; } +pass() { say "PASS $*"; } +bad() { say "FAIL $*"; fail=1; } +note() { say " $*"; } +root_cat() { cat "$1" 2>/dev/null || sudo cat "$1" 2>/dev/null || echo "(unreadable)"; } + +say ""; say "===== $(date '+%F %T') mode=$mode kernel=$(uname -r)" + +report_system() { + say "-- GPU and software" + lspci -nn 2>/dev/null | grep -iE 'vga|3d|display' | while read -r l; do note "$l"; done + command -v nvidia-smi >/dev/null && note "driver: $(nvidia-smi --query-gpu=name,driver_version --format=csv,noheader 2>&1 | head -n 1)" + for p in nvidia-open nvidia-utils nvidia-dkms gamescope gamescope-session-cachyos linux-cachyos-nvidia-open; do + v="$(pacman -Q "$p" 2>/dev/null)" && note "$v" + done + note "boot loader file: $(nvidia_boot_file || true)" + note "cmdline: $(cat /proc/cmdline)" +} + +check_state() { + say "-- Steamify's view" + if nvidia_present; then pass "an NVIDIA GPU with its driver is detected"; else bad "no NVIDIA GPU/driver detected: the fix does nothing here"; fi + local p + for p in $NVIDIA_PARAMS; do + if grep -qw -- "$p" /proc/cmdline; then pass "$p is on the running kernel's command line" + elif nvidia_param_set "$p"; then bad "$p is in the boot loader's file but not booted yet: reboot" + else bad "$p is missing (run: bash tests/nvidia-hardware-test.sh apply)"; fi + done + say "-- the live NVIDIA driver" + local m + for m in modeset fbdev; do + v="$(root_cat /sys/module/nvidia_drm/parameters/$m)" + if [[ "$v" == Y ]]; then pass "nvidia_drm $m = Y"; else bad "nvidia_drm $m = $v (want Y)"; fi + done + lsmod | grep -q '^nvidia_drm' && pass "nvidia_drm is loaded" || bad "nvidia_drm is not loaded" + local c found=0 + for c in /sys/class/drm/card[0-9]*/device; do + [[ "$(cat "$c/vendor" 2>/dev/null)" == 0x10de ]] || continue + found=1; note "NVIDIA card: ${c%/device} driver=$(basename "$(readlink -f "$c/driver" 2>/dev/null)")" + for s in "${c%/device}"/card*-*/status; do [[ -f "$s" ]] && note " $(basename "$(dirname "$s")"): $(cat "$s")"; done + done + [[ $found == 1 ]] || bad "no NVIDIA card in /sys/class/drm" + say "-- initramfs" + if nvidia_modules_everywhere; then note "every installed kernel has nvidia_drm: the early-load drop-in is used" + else note "not every installed kernel has nvidia_drm: the early-load drop-in is skipped (parameters still apply)"; fi + nvidia_initramfs_ok && note "NVIDIA modules are in the initramfs config" || note "NVIDIA modules are not in the initramfs config" + say "-- what apply would change" + if nvidia_params_missing; then note "adds $NVIDIA_PARAMS to $(nvidia_boot_file)"; else note "kernel parameters: nothing to change"; fi + if ! nvidia_initramfs_ok && nvidia_modules_everywhere; then note "writes $NVIDIA_INITRAMFS_CONF"; fi +} + +case "$mode" in + check) + report_system; check_state ;; + apply) + report_system + say "-- applying" + if nvidia_enable 2>&1 | tee -a "$REPORT"; [[ ${PIPESTATUS[0]} == 0 ]]; then pass "nvidia_enable finished" + else bad "nvidia_enable failed (output above)"; fi + say "Now reboot, then run: bash tests/nvidia-hardware-test.sh check" ;; + visual) + report_system + say "-- try gaming mode now (log in to it or reboot into it), look at the screen, then answer" + read -rp "Is the gaming mode picture clean (no corruption, no flicker)? [y/n/partly] " a + read -rp "What did you see (one line, e.g. 'garbled colours at start, fine after login')? " what + say "visual result: $a: $what" + [[ "$a" == y ]] && pass "picture clean" || bad "picture not clean ($a)" + say "-- gamescope and NVIDIA lines from this boot's log" + { journalctl -b --no-pager 2>/dev/null || sudo journalctl -b --no-pager 2>/dev/null; } | + grep -iE 'gamescope|nvidia|drm|nvrm|wlserver|vulkan' | tail -n 80 | tee -a "$REPORT" | tail -n 5 + note "(more in $REPORT)" ;; + *) echo "usage: $0 check|apply|visual"; exit 2 ;; +esac +say ""; say "Report: $REPORT" +exit $fail From 30b315e6788cef293dbe3d709b1430fb0af9271b Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 14:42:06 +0200 Subject: [PATCH 08/17] feat: NVIDIA modules in the initramfs only while every kernel has them, decided again by a pacman hook at every kernel change --- CHANGELOG.md | 1 + TECHNICAL.md | 13 ++++++--- lib/nvidia.sh | 37 +++++++++++++++----------- patches/README.md | 1 + patches/steamify-nvidia-initramfs.hook | 17 ++++++++++++ patches/steamify-nvidia-initramfs.sh | 36 +++++++++++++++++++++++++ tests/nvidia-test.sh | 21 ++++++++++++--- 7 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 patches/steamify-nvidia-initramfs.hook create mode 100644 patches/steamify-nvidia-initramfs.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index d0081f7..ab84904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ one per merged pull request. - **test: tests/nvidia-test.sh runs the NVIDIA fix against a fake RTX 5080 beside an iGPU, a stub modinfo and temp copies of the Limine, systemd-boot and GRUB files (no root, VM or hardware); NVIDIA_DRM_DIR points detection at a fake sysfs tree** - **fix: NVIDIA fix on Limine edits the KERNEL_CMDLINE[default] line (an appended += line ended up as literal text on the kernel command line, found in a VM); the modules are only loaded early when every installed kernel has them, and a Limine boot entry without the parameters is an error** - **test: tests/nvidia-hardware-test.sh for a real NVIDIA PC: check (read-only state of the detection, the live nvidia_drm modeset/fbdev, versions), apply, and visual (was the gaming mode picture clean), with a report file to paste** +- **fix: the NVIDIA modules are loaded early only while every installed kernel has them, decided again at every kernel or driver change by a pacman hook (steamify-nvidia-initramfs): a later kernel without the modules (another kernel, a failed DKMS build, a downgrade) no longer makes its initramfs and Limine boot entry fail** ## 2.9.6 - 2026-09-30 diff --git a/TECHNICAL.md b/TECHNICAL.md index c88d777..0b3b9b7 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -216,13 +216,18 @@ its driver installed (`lib/nvidia.sh`), the SteamOS conversion adds systemd-boot or GRUB; the file is backed up first) and `/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)`), then rebuilds the initramfs and boot -entries. The early-load drop-in is only written when every installed kernel -has the NVIDIA modules (mkinitcpio fails on a missing module, and -`limine-mkinitcpio` then skips that kernel's boot entry, parameters included). +entries. The early-load drop-in is only there while every installed kernel has the +NVIDIA modules (mkinitcpio fails on a missing module, and `limine-mkinitcpio` +then skips that kernel's boot entry, parameters included). That is decided again +at every kernel or driver change by a pacman hook +(`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`, after DKMS builds, +before the initramfs is built) running `/usr/local/libexec/steamify-nvidia-initramfs`: +another kernel without the modules, a failed DKMS build or a downgrade removes +the drop-in, and it comes back once every kernel has them again. On Limine the parameters go into the existing `KERNEL_CMDLINE[default]="..."` line: an extra appended `+=` line is pasted into the command line as text. Only what's missing is changed; it applies after a reboot. Turning -the conversion off removes both. `tests/nvidia-test.sh` tests it against a fake GPU +the conversion off removes both, the hook and its script. `tests/nvidia-test.sh` tests it against a fake GPU and fake boot loader files; `tests/nvidia-hardware-test.sh check|apply|visual` is for a real NVIDIA PC (read-only check, the real apply, and a prompt for whether the picture was clean), and writes `~/steamify-nvidia-report.txt`. diff --git a/lib/nvidia.sh b/lib/nvidia.sh index 947652f..e280356 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -9,6 +9,9 @@ NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1" NVIDIA_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf +# The script and pacman hook that keep the initramfs file right at every kernel change. +NVIDIA_SCRIPT=${NVIDIA_SCRIPT:-/usr/local/libexec/steamify-nvidia-initramfs} +NVIDIA_HOOK=${NVIDIA_HOOK:-/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook} # The DRM devices in sysfs; the tests point this at a fake tree. NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm} @@ -46,16 +49,14 @@ nvidia_initramfs_ok() { } nvidia_modules_everywhere() { - # 0 when every installed kernel has the nvidia_drm module: mkinitcpio fails - # on a MODULES entry a kernel lacks, and limine-mkinitcpio then skips that - # kernel's initramfs and boot entry, so the parameters never reach it. - local k found=0 - for k in "${NVIDIA_MODULES_DIR:-/usr/lib/modules}"/*/; do - [[ -f "$k/pkgbase" || -d "$k/kernel" || -d "$k/updates" || -d "$k/extramodules" ]] || continue - find "$k" -name 'nvidia-drm.ko*' -print -quit 2>/dev/null | grep -q . || return 1 - found=1 - done - [[ $found == 1 ]] + # 0 when every installed kernel has the nvidia_drm module (the script's own check). + patch_file steamify-nvidia-initramfs.sh | NVIDIA_MODULES_DIR="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" bash -s check +} + +nvidia_hook_install() { + patch_file steamify-nvidia-initramfs.sh | sudo install -Dm755 /dev/stdin "$NVIDIA_SCRIPT" && + patch_file steamify-nvidia-initramfs.hook | fill SCRIPT="$NVIDIA_SCRIPT" | + sudo install -Dm644 /dev/stdin "$NVIDIA_HOOK" } nvidia_rebuild_boot() { @@ -107,12 +108,17 @@ nvidia_enable() { changed=1 fi fi - if ! nvidia_initramfs_ok && ! nvidia_modules_everywhere; then - warn "Not every installed kernel has the NVIDIA modules: skipping loading them early (the kernel parameters still apply)." - elif ! nvidia_initramfs_ok; then + nvidia_hook_install || { err "Installing the NVIDIA initramfs hook failed."; return 1; } + local had=0; [[ -f "$NVIDIA_INITRAMFS_CONF" ]] && had=1 + sudo env NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" "$NVIDIA_SCRIPT" sync + if [[ -f "$NVIDIA_INITRAMFS_CONF" && $had == 0 ]]; then info "Loading the NVIDIA modules early (initramfs)..." - printf 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)\n' | - sudo install -Dm644 /dev/stdin "$NVIDIA_INITRAMFS_CONF" && changed=1 + changed=1 + elif [[ ! -f "$NVIDIA_INITRAMFS_CONF" && $had == 1 ]]; then + warn "A kernel without the NVIDIA modules is installed: not loading them early any more (rebuilding the initramfs without them)." + changed=1 + elif [[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && ! nvidia_initramfs_ok; then + warn "Not every installed kernel has the NVIDIA modules: not loading them early for now (the kernel parameters still apply; this is checked again at every kernel update)." fi if [[ $changed == 1 ]]; then nvidia_rebuild_boot || { err "Rebuilding the boot entries failed."; return 1; } @@ -132,6 +138,7 @@ nvidia_disable() { changed=1 fi [[ -f "$NVIDIA_INITRAMFS_CONF" ]] && { sudo rm -f "$NVIDIA_INITRAMFS_CONF"; changed=1; } + sudo rm -f "$NVIDIA_HOOK" "$NVIDIA_SCRIPT" [[ $changed == 1 ]] && nvidia_rebuild_boot return 0 } diff --git a/patches/README.md b/patches/README.md index 5b67b00..b5491af 100644 --- a/patches/README.md +++ b/patches/README.md @@ -14,4 +14,5 @@ read and reviewed on their own. `lib/*.sh` reads them with `patch_file | `steam-shortcuts.py` | `lib/steam-game.sh` (Add as non-Steam game) | Reads and writes Steam's binary `shortcuts.vdf` (find, add or update, remove) | | `steamify-notifier.py` | `lib/update-notifier.sh` (Update notifications) | The daily check: notification and tray icon when there's a new release | | `steamify-os-release.sh`, `steamify-os-release.hook` | `lib/login-manager.sh` (SteamOS conversion) | What Steam's System settings show besides the OS name (which stays CachyOS's): `VERSION_ID=steamos-X.Y`, `VERSION_CODENAME=steam-machine`, `VARIANT`/`VARIANT_ID` Steamify with its version, in `/etc/os-release`; the pacman hook runs it again after cachyos-hooks (`fill SCRIPT=... VERSION=... STEAMOS=...`) | +| `steamify-nvidia-initramfs.sh`, `steamify-nvidia-initramfs.hook` | `lib/nvidia.sh` (SteamOS conversion, NVIDIA fix) | The pacman hook (before the initramfs is built, after DKMS) and its script: the NVIDIA modules go into the initramfs only while every installed kernel has them, decided again at every kernel or driver change (`fill SCRIPT=...`) | | `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port | diff --git a/patches/steamify-nvidia-initramfs.hook b/patches/steamify-nvidia-initramfs.hook new file mode 100644 index 0000000..815087a --- /dev/null +++ b/patches/steamify-nvidia-initramfs.hook @@ -0,0 +1,17 @@ +# Installed by Steamify CachyOS (SteamOS conversion, NVIDIA fix): decides +# again whether the NVIDIA modules go into the initramfs, after DKMS has built +# its modules (71-dkms-install) and before the initramfs is built +# (90-mkinitcpio-install); hooks run in file name order. +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Path +Target = usr/lib/modules/*/vmlinuz +Target = usr/lib/modules/*/extramodules/nvidia* +Target = usr/lib/modules/*/updates/* + +[Action] +Description = Steamify: NVIDIA modules in the initramfs only if every kernel has them +When = PostTransaction +Exec = @SCRIPT@ sync diff --git a/patches/steamify-nvidia-initramfs.sh b/patches/steamify-nvidia-initramfs.sh new file mode 100644 index 0000000..5c58384 --- /dev/null +++ b/patches/steamify-nvidia-initramfs.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Installed by Steamify CachyOS (SteamOS conversion, NVIDIA fix), run by the +# pacman hook steamify-nvidia-initramfs.hook before the initramfs is built. +# The NVIDIA modules go into the initramfs (early KMS) only while EVERY +# installed kernel has them: mkinitcpio fails on a module a kernel lacks, and +# limine-mkinitcpio then skips that kernel's boot entry, kernel parameters +# included. Decided again at every kernel or driver change, so a new kernel +# without the modules (another kernel, a failed DKMS build, a downgrade) gets +# a working initramfs and the file comes back once it has them. +# steamify-nvidia-initramfs [sync|check] +# sync (default) write or remove the drop-in to match +# check exit status 0 when every installed kernel has nvidia_drm +conf="${NVIDIA_INITRAMFS_CONF:-/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf}" +mods="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" + +everywhere() { + local k found=0 + for k in "$mods"/*/; do + [[ -f "$k/vmlinuz" || -f "$k/pkgbase" ]] || continue # an installed kernel, not a leftover folder + find "$k" -name 'nvidia-drm.ko*' -print -quit 2>/dev/null | grep -q . || return 1 + found=1 + done + [[ $found == 1 ]] +} + +case "${1:-sync}" in + check) everywhere ;; + sync) + if everywhere; then + [[ -f "$conf" ]] || { mkdir -p "$(dirname "$conf")" && + echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' > "$conf"; } + else + rm -f "$conf" + fi ;; + *) echo "usage: $0 [sync|check]" >&2; exit 2 ;; +esac diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index d908b04..615eb74 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -7,7 +7,7 @@ T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT fail=0 check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; } -source lib/common.sh; source lib/hdmi-refresh.sh; source lib/nvidia.sh +SCRIPT_DIR="$PWD"; source lib/common.sh; source lib/hdmi-refresh.sh; source lib/nvidia.sh sudo() { "$@"; } info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; } backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; } @@ -19,7 +19,8 @@ echo 0x8086 > "$T/drm/card0/device/vendor"; echo 0x030000 > "$T/drm/card0/device echo 0x10de > "$T/drm/card1/device/vendor"; echo 0x030000 > "$T/drm/card1/device/class" echo 0x2c02 > "$T/drm/card1/device/device" NVIDIA_DRM_DIR="$T/drm" -mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst" +mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst"; : > "$T/mods/6.1.0-cachyos/pkgbase" +NVIDIA_SCRIPT="$T/libexec/steamify-nvidia-initramfs"; NVIDIA_HOOK="$T/hooks/85-steamify-nvidia-initramfs.hook" NVIDIA_MODULES_DIR="$T/mods" NVIDIA_LIMINE_CONF="$T/none/limine.conf"; NVIDIA_SDBOOT_DIR="$T/none/entries"; NVIDIA_GRUB_CFG="$T/none/grub.cfg" # not generated here: not checked @@ -39,15 +40,27 @@ for loader in limine sdboot grub; do nvidia_enable check "$loader: parameters added" 'grep -q "nvidia-drm.modeset=1" "$F" && grep -q "nvidia-drm.fbdev=1" "$F"' check "$loader: initramfs drop-in written" 'grep -q nvidia_drm "$NVIDIA_INITRAMFS_CONF"' + check "$loader: script and pacman hook installed" '[[ -x "$NVIDIA_SCRIPT" ]] && grep -q "^Exec = $NVIDIA_SCRIPT sync" "$NVIDIA_HOOK"' check "$loader: backup made, rebuilt once" '[[ -f "$F.bak" && $REBUILDS == 1 ]]' SUM="$(cat "$F")"; nvidia_enable check "$loader: second run changes nothing" '[[ "$(cat "$F")" == "$SUM" && $REBUILDS == 1 ]]' nvidia_disable - check "$loader: disable restores the file and drops the drop-in" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" ]]' + check "$loader: disable restores the file, drops the drop-in, script and hook" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" && ! -e "$NVIDIA_SCRIPT" && ! -e "$NVIDIA_HOOK" ]]' done +# The pacman hook's script decides again at every kernel change. +syncit() { NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="$NVIDIA_MODULES_DIR" bash "$NVIDIA_SCRIPT" sync; } +nvidia_hook_install; rm -f "$NVIDIA_INITRAMFS_CONF" +syncit; check "hook script: every kernel has the modules -> drop-in written" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]' +mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase" +syncit; check "hook script: a new kernel without modules -> drop-in removed" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]]' +: > "$T/mods/6.6.0-lts/kernel/nvidia-drm.ko.zst" +syncit; check "hook script: the modules arrive (DKMS built) -> drop-in back" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]' +rm -rf "$T/mods/6.6.0-lts"; mkdir -p "$T/mods/5.0.0-leftover"; : > "$T/mods/5.0.0-leftover/modules.dep" +syncit; check "hook script: a leftover folder of a removed kernel is ignored" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]' +rm -rf "$T/mods/5.0.0-leftover"; nvidia_disable # A second kernel without the NVIDIA modules: no early-load drop-in, parameters still set. -mkdir -p "$T/mods/6.6.0-lts/kernel"; F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet"\n' > "$F" +mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase"; F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet"\n' > "$F" nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_enable check "kernel without nvidia modules: no drop-in, parameters set" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && grep -q "nvidia-drm.fbdev=1" "$F"' rm -rf "$T/mods/6.6.0-lts" From 1b7e2e88425a0386469570a6f565edd68b192f58 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 15:07:16 +0200 Subject: [PATCH 09/17] fix: never install an empty NVIDIA hook or script (a failed patch_file piped into install succeeded with no input) --- lib/nvidia.sh | 10 +++++++--- tests/nvidia-test.sh | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/nvidia.sh b/lib/nvidia.sh index e280356..df5021a 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -54,9 +54,13 @@ nvidia_modules_everywhere() { } nvidia_hook_install() { - patch_file steamify-nvidia-initramfs.sh | sudo install -Dm755 /dev/stdin "$NVIDIA_SCRIPT" && - patch_file steamify-nvidia-initramfs.hook | fill SCRIPT="$NVIDIA_SCRIPT" | - sudo install -Dm644 /dev/stdin "$NVIDIA_HOOK" + # Never installs an empty script or hook (a piped patch_file that failed + # still lets install succeed with no input). + local script hook + script="$(patch_file steamify-nvidia-initramfs.sh)" && [[ -n "$script" ]] && + hook="$(patch_file steamify-nvidia-initramfs.hook | fill SCRIPT="$NVIDIA_SCRIPT")" && [[ -n "$hook" ]] || return 1 + printf '%s\n' "$script" | sudo install -Dm755 /dev/stdin "$NVIDIA_SCRIPT" && + printf '%s\n' "$hook" | sudo install -Dm644 /dev/stdin "$NVIDIA_HOOK" } nvidia_rebuild_boot() { diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index 615eb74..550df10 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -48,6 +48,8 @@ for loader in limine sdboot grub; do check "$loader: disable restores the file, drops the drop-in, script and hook" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" && ! -e "$NVIDIA_SCRIPT" && ! -e "$NVIDIA_HOOK" ]]' done +# A missing patches/ file must be an error, never an empty script or hook. +( SCRIPT_DIR="$T/no-patches"; NVIDIA_SCRIPT="$T/empty/script"; NVIDIA_HOOK="$T/empty/hook"; ! nvidia_hook_install 2>/dev/null && [[ ! -s "$T/empty/script" ]] ) && echo "ok a missing patch file is an error, nothing empty installed" || { echo "FAIL a missing patch file is an error, nothing empty installed"; fail=1; } # The pacman hook's script decides again at every kernel change. syncit() { NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="$NVIDIA_MODULES_DIR" bash "$NVIDIA_SCRIPT" sync; } nvidia_hook_install; rm -f "$NVIDIA_INITRAMFS_CONF" From b5c86e1e726e151444c78f9656e9fc41aa34cf48 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 15:20:02 +0200 Subject: [PATCH 10/17] feat: NVIDIA fix only for RTX 20 series or newer, with the VRAM booster's chwd legacy check (vram_nvidia_legacy_id) --- CHANGELOG.md | 1 + TECHNICAL.md | 6 ++++-- lib/nvidia.sh | 23 ++++++++++++++++++++--- lib/vram-booster.sh | 10 ++++++++-- tests/nvidia-hardware-test.sh | 10 ++++++++-- tests/nvidia-test.sh | 13 +++++++++++-- 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab84904..72cd481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ one per merged pull request. - **fix: NVIDIA fix on Limine edits the KERNEL_CMDLINE[default] line (an appended += line ended up as literal text on the kernel command line, found in a VM); the modules are only loaded early when every installed kernel has them, and a Limine boot entry without the parameters is an error** - **test: tests/nvidia-hardware-test.sh for a real NVIDIA PC: check (read-only state of the detection, the live nvidia_drm modeset/fbdev, versions), apply, and visual (was the gaming mode picture clean), with a report file to paste** - **fix: the NVIDIA modules are loaded early only while every installed kernel has them, decided again at every kernel or driver change by a pacman hook (steamify-nvidia-initramfs): a later kernel without the modules (another kernel, a failed DKMS build, a downgrade) no longer makes its initramfs and Limine boot entry fail** +- **feat: the NVIDIA fix is only for RTX 20 series or newer, decided like the VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists); older cards are left alone** ## 2.9.6 - 2026-09-30 diff --git a/TECHNICAL.md b/TECHNICAL.md index 0b3b9b7..bb17342 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -210,8 +210,10 @@ missing `-headers` package, which makes DKMS build the driver for it. gamescope drives the display itself through DRM/KMS, which with NVIDIA needs kernel modesetting and the framebuffer driver, and the driver loaded early; -without them gaming mode can show a corrupted image. With an NVIDIA GPU and -its driver installed (`lib/nvidia.sh`), the SteamOS conversion adds +without them gaming mode can show a corrupted image. With a supported NVIDIA GPU (RTX 20 series or newer, decided like the VRAM +booster does: a card on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids` +is older and left alone, `vram_nvidia_legacy_id`; without those lists every +card counts) and its driver installed (`lib/nvidia.sh`), the SteamOS conversion adds `nvidia-drm.modeset=1 nvidia-drm.fbdev=1` to the kernel command line (Limine, systemd-boot or GRUB; the file is backed up first) and `/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia diff --git a/lib/nvidia.sh b/lib/nvidia.sh index df5021a..79518d7 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -4,7 +4,8 @@ # through DRM/KMS; with NVIDIA that needs kernel modesetting and the # framebuffer driver (nvidia-drm.modeset=1 nvidia-drm.fbdev=1) and the # driver in the initramfs, or the screen shows a corrupted image. -# Does nothing without an NVIDIA GPU that has its driver installed. +# Does nothing without a supported NVIDIA GPU (RTX 20 series or newer) that has +# its driver installed. # Sourced by steamify.sh; not meant to be run on its own. NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1" @@ -16,15 +17,31 @@ NVIDIA_HOOK=${NVIDIA_HOOK:-/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm} nvidia_present() { - # An NVIDIA GPU (PCI vendor 0x10de, display class) and its kernel driver. + # A supported NVIDIA GPU: PCI vendor 0x10de, display class, its kernel + # driver installed, and not older than the RTX 20 series, decided like the + # VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists, every + # other card runs NVIDIA's open modules). Older cards are left alone. local d for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do [[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue - [[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] && modinfo nvidia_drm >/dev/null 2>&1 && return 0 + [[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue + vram_nvidia_legacy_id "$(cat "$d/device" 2>/dev/null)" && continue + modinfo nvidia_drm >/dev/null 2>&1 && return 0 done return 1 } +nvidia_legacy_gpu() { + # 0 when the only NVIDIA GPU(s) are older than RTX 20 (reported, not fixed). + local d found=0 + for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do + [[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue + vram_nvidia_legacy_id "$(cat "$d/device" 2>/dev/null)" || return 1 + found=1 + done + [[ $found == 1 ]] +} + nvidia_boot_file() { # Same boot loaders as the kernel command line elsewhere (lib/hdmi-refresh.sh). hdmi_boot_file diff --git a/lib/vram-booster.sh b/lib/vram-booster.sh index 8336d08..fc421a8 100644 --- a/lib/vram-booster.sh +++ b/lib/vram-booster.sh @@ -44,6 +44,13 @@ vram_nvidia() { # 470xx, 390xx); every other NVIDIA card runs the open kernel modules. VRAM_CHWD_IDS=/var/lib/chwd/ids +vram_nvidia_legacy_id() { + # vram_nvidia_legacy_id : 0 when chwd + # lists the card for a closed legacy branch, i.e. it is older than the + # RTX 20 series. Also what lib/nvidia.sh uses to decide if a card is supported. + grep -qwi "${1#0x}" "$VRAM_CHWD_IDS"/nvidia-*.ids 2>/dev/null +} + vram_nvidia_case() { # Why an NVIDIA card's VRAM isn't registered, so the menu can say what to # do: "update" (open modules older than 615), "switch " @@ -62,8 +69,7 @@ vram_nvidia_case() { esac for d in /sys/bus/pci/devices/*; do [[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue - id="$(cat "$d/device")"; id="${id#0x}" - grep -qwi "$id" "$VRAM_CHWD_IDS"/nvidia-*.ids 2>/dev/null && { echo legacy; return; } + vram_nvidia_legacy_id "$(cat "$d/device")" && { echo legacy; return; } done for p in 580xx 470xx 390xx; do pacman -Q "nvidia-$p-dkms" >/dev/null 2>&1 && { echo "switch nvidia-dkms-$p"; return; } diff --git a/tests/nvidia-hardware-test.sh b/tests/nvidia-hardware-test.sh index 7f2dc50..8bcde84 100644 --- a/tests/nvidia-hardware-test.sh +++ b/tests/nvidia-hardware-test.sh @@ -7,7 +7,7 @@ # Every run appends to ~/steamify-nvidia-report.txt: paste that file when asking for help. cd "$(dirname "$0")/.." || exit 1 REPORT="${NVIDIA_REPORT:-$HOME/steamify-nvidia-report.txt}" -source lib/common.sh; source lib/hdmi-refresh.sh; source lib/nvidia.sh +source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh mode="${1:-check}" fail=0 say() { echo "$*" | tee -a "$REPORT"; } @@ -31,7 +31,13 @@ report_system() { check_state() { say "-- Steamify's view" - if nvidia_present; then pass "an NVIDIA GPU with its driver is detected"; else bad "no NVIDIA GPU/driver detected: the fix does nothing here"; fi + if nvidia_present; then pass "a supported NVIDIA GPU (RTX 20 series or newer) with its driver is detected" + elif nvidia_legacy_gpu; then bad "the NVIDIA GPU is older than RTX 20 (on chwd's legacy lists in $VRAM_CHWD_IDS): the fix is not applied" + else bad "no NVIDIA GPU with its driver detected: the fix does nothing here"; fi + for c in /sys/class/drm/card[0-9]*/device; do + [[ "$(cat "$c/vendor" 2>/dev/null)" == 0x10de ]] && note "NVIDIA PCI device id $(cat "$c/device") $(vram_nvidia_legacy_id "$(cat "$c/device")" && echo '(on a chwd legacy list)' || echo '(not on a legacy list: RTX 20 or newer)')" + done + ls "$VRAM_CHWD_IDS"/nvidia-*.ids >/dev/null 2>&1 || note "no chwd legacy lists found: every NVIDIA card counts as supported" local p for p in $NVIDIA_PARAMS; do if grep -qw -- "$p" /proc/cmdline; then pass "$p is on the running kernel's command line" diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index 550df10..4f17498 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -7,7 +7,7 @@ T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT fail=0 check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; } -SCRIPT_DIR="$PWD"; source lib/common.sh; source lib/hdmi-refresh.sh; source lib/nvidia.sh +SCRIPT_DIR="$PWD"; source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh sudo() { "$@"; } info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; } backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; } @@ -24,7 +24,16 @@ NVIDIA_SCRIPT="$T/libexec/steamify-nvidia-initramfs"; NVIDIA_HOOK="$T/hooks/85-s NVIDIA_MODULES_DIR="$T/mods" NVIDIA_LIMINE_CONF="$T/none/limine.conf"; NVIDIA_SDBOOT_DIR="$T/none/entries"; NVIDIA_GRUB_CFG="$T/none/grub.cfg" # not generated here: not checked -FAKE_DRIVER=1; check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present +mkdir -p "$T/chwd"; VRAM_CHWD_IDS="$T/chwd" +FAKE_DRIVER=1; check "finds the NVIDIA card as card1 beside an iGPU (no legacy lists)" nvidia_present +printf '1c03\n1b80\n' > "$T/chwd/nvidia-580.ids" # a GTX 1060 and a GTX 1080: chwd's 580xx legacy list +check "RTX 5080 (2c02) is not on a legacy list: supported" nvidia_present +echo 0x1c03 > "$T/drm/card1/device/device" +check "GTX 1060 (on chwd's legacy list, older than RTX 20): not supported" '! nvidia_present' +check "...and reported as a legacy GPU" nvidia_legacy_gpu +echo 0x2c02 > "$T/drm/card1/device/device"; check "RTX 5080 is not a legacy GPU" '! nvidia_legacy_gpu' +printf '2c02\n' > "$T/chwd/nvidia-580.ids"; check "a card id that is on a legacy list is skipped, whatever it is" '! nvidia_present' +printf '1c03\n' > "$T/chwd/nvidia-580.ids" FAKE_DRIVER=0; check "skips when the driver isn't installed" '! nvidia_present' FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "skips without an NVIDIA GPU" '! nvidia_present' NVIDIA_DRM_DIR="$T/drm" From 4092fdc9eef0ad1ce4fefd3cb6f7e3e0a62f3452 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 15:40:02 +0200 Subject: [PATCH 11/17] chore: .no-release-yet: this NVIDIA branch is not ready to be released --- .no-release-yet | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .no-release-yet diff --git a/.no-release-yet b/.no-release-yet new file mode 100644 index 0000000..8279841 --- /dev/null +++ b/.no-release-yet @@ -0,0 +1,5 @@ +This branch is not ready to be released: do not merge it into the release branch. +When the feature is complete and tested, ask the user whether it may be released; +on a yes, delete this file, then open the pull request into the release branch. +Open work, research and the test instructions for the NVIDIA PC are NOT in this repo: +see steamify-cachyos-dev, folder nvidia/ (github.com/theupriser/steamify-cachyos-dev). From 76cf1b4adb3f86defb65dc3b1d00c6e35862e4f7 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 17:19:23 +0200 Subject: [PATCH 12/17] fix: NVIDIA fix handles Limine's += command line, underscore spelling; hardware test sets SCRIPT_DIR --- CHANGELOG.md | 1 + lib/nvidia.sh | 10 ++++++---- tests/nvidia-hardware-test.sh | 2 ++ tests/nvidia-test.sh | 4 +++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72cd481..4107e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ one per merged pull request. - **test: tests/nvidia-hardware-test.sh for a real NVIDIA PC: check (read-only state of the detection, the live nvidia_drm modeset/fbdev, versions), apply, and visual (was the gaming mode picture clean), with a report file to paste** - **fix: the NVIDIA modules are loaded early only while every installed kernel has them, decided again at every kernel or driver change by a pacman hook (steamify-nvidia-initramfs): a later kernel without the modules (another kernel, a failed DKMS build, a downgrade) no longer makes its initramfs and Limine boot entry fail** - **feat: the NVIDIA fix is only for RTX 20 series or newer, decided like the VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists); older cards are left alone** +- **fix: the NVIDIA fix also edits Limine's `KERNEL_CMDLINE[default]+="..."` line (CachyOS writes that form; the fix found no line to edit and stopped) and counts `nvidia_drm.modeset=1` (underscore) as already set; tests/nvidia-hardware-test.sh sets `SCRIPT_DIR` (the hook script was read from `/patches`)** ## 2.9.6 - 2026-09-30 diff --git a/lib/nvidia.sh b/lib/nvidia.sh index 79518d7..dd0a817 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -50,9 +50,11 @@ nvidia_boot_file() { nvidia_param_set() { # 0 when $1 (e.g. nvidia-drm.modeset=1) is on the kernel command line or # already in the boot loader's file (not booted yet). - local f; f="$(nvidia_boot_file)" - grep -qw -- "$1" /proc/cmdline 2>/dev/null || - { [[ -n "$f" ]] && grep -qw -- "$1" "$f" 2>/dev/null; } + # The kernel treats "nvidia_drm" and "nvidia-drm" alike: CachyOS' own + # setup may already have written the underscore spelling. + local f re; f="$(nvidia_boot_file)"; re="${1//nvidia-drm/nvidia[-_]drm}" + grep -qwE -- "$re" /proc/cmdline 2>/dev/null || + { [[ -n "$f" ]] && grep -qwE -- "$re" "$f" 2>/dev/null; } } nvidia_params_missing() { @@ -116,7 +118,7 @@ nvidia_enable() { backup_file "$f" case "$f" in */limine) - sudo sed -i -E "s/^(KERNEL_CMDLINE\[default\]=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; + sudo sed -i -E "s/^(KERNEL_CMDLINE\[default\]\+?=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; */sdboot-manage.conf) sudo sed -i -E "s/^(LINUX_OPTIONS=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;; */grub) diff --git a/tests/nvidia-hardware-test.sh b/tests/nvidia-hardware-test.sh index 8bcde84..dac209c 100644 --- a/tests/nvidia-hardware-test.sh +++ b/tests/nvidia-hardware-test.sh @@ -6,6 +6,8 @@ # bash tests/nvidia-hardware-test.sh visual after trying gaming mode: was the picture clean? (asks you) # Every run appends to ~/steamify-nvidia-report.txt: paste that file when asking for help. cd "$(dirname "$0")/.." || exit 1 +# patch_file (lib/common.sh) reads the hook script from $SCRIPT_DIR/patches. +SCRIPT_DIR="$PWD" REPORT="${NVIDIA_REPORT:-$HOME/steamify-nvidia-report.txt}" source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh mode="${1:-check}" diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index 4f17498..52cee29 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -38,9 +38,11 @@ FAKE_DRIVER=0; check "skips when the driver isn't installed" '! nvidia_present' FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "skips without an NVIDIA GPU" '! nvidia_present' NVIDIA_DRM_DIR="$T/drm" -for loader in limine sdboot grub; do +for loader in limine limineplus sdboot grub; do case $loader in limine) F="$T/limine"; printf 'KERNEL_CMDLINE[default]="quiet splash"\n' > "$F" ;; + # CachyOS writes its line as "+=" and may already have nvidia_drm.modeset=1 in it. + limineplus) F="$T/limine"; printf 'KERNEL_CMDLINE[default]+="quiet splash nvidia_drm.modeset=1"\n' > "$F" ;; sdboot) F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet splash"\n' > "$F" ;; grub) F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"\n' > "$F" ;; esac From 948dc1f587ebcd5be6632c07e15e49477fca193d Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 19:04:58 +0200 Subject: [PATCH 13/17] feat: "Gaming on NVIDIA" replaces the SteamOS conversion on NVIDIA PCs gamescope's gaming mode shows a corrupted picture on NVIDIA (NVIDIA bug 5240452), also nested in KDE and in a KWin-only session. With an NVIDIA GPU the conversion (gaming, boot, single, glyphs) is hidden unless already on, and nvidia takes its place: Steam installed when missing and started at login on the Plasma desktop (user unit), with the sub-option bigpicture for Big Picture or the normal window. The kernel parameters and early NVIDIA modules (RTX 20 series or newer) are kept as part of it. tests/nvidia-test.sh covers the component and the menu rules against a fake GPU. --- AGENTS.md | 12 +- CHANGELOG.md | 9 +- README.md | 6 + TECHNICAL.md | 60 +++++----- lib/backend.sh | 2 + lib/login-manager.sh | 2 - lib/menu.sh | 17 ++- lib/nvidia.sh | 101 ++++++++++++++-- services/README.md | 1 + services/steamify-steam-autostart.service | 15 +++ tests/nvidia-test.sh | 136 +++++++++++++++++----- ui/qml/AppState.qml | 2 + ui/qml/Texts.qml | 6 + 13 files changed, 281 insertions(+), 88 deletions(-) create mode 100644 services/steamify-steam-autostart.service diff --git a/AGENTS.md b/AGENTS.md index b065799..950c19f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,13 +48,11 @@ 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`. -- **Hardware-specific fixes** that belong to a component but aren't a menu - row (e.g. `lib/nvidia.sh`: NVIDIA's kernel parameters and early modules - for gaming mode, called from `gaming_enable`/`gaming_disable`) detect the - hardware themselves, change only what's missing, and undo it on disable. - Add them to the source loop in `steamify.sh` and the table in - `TECHNICAL.md`. They can't be tested without the hardware: say so in the - changelog and keep the branch behind `.no-release-yet` until it was. +- **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. - **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 4107e56..7459959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,8 @@ one per merged pull request. ## 2.9.7 - 2026-10-01 -- **feat: the SteamOS conversion fixes a corrupted gaming mode screen with an NVIDIA GPU: nvidia-drm.modeset=1 and fbdev=1 on the kernel command line, NVIDIA modules in the initramfs (lib/nvidia.sh)** -- **test: tests/nvidia-test.sh runs the NVIDIA fix against a fake RTX 5080 beside an iGPU, a stub modinfo and temp copies of the Limine, systemd-boot and GRUB files (no root, VM or hardware); NVIDIA_DRM_DIR points detection at a fake sysfs tree** -- **fix: NVIDIA fix on Limine edits the KERNEL_CMDLINE[default] line (an appended += line ended up as literal text on the kernel command line, found in a VM); the modules are only loaded early when every installed kernel has them, and a Limine boot entry without the parameters is an error** -- **test: tests/nvidia-hardware-test.sh for a real NVIDIA PC: check (read-only state of the detection, the live nvidia_drm modeset/fbdev, versions), apply, and visual (was the gaming mode picture clean), with a report file to paste** -- **fix: the NVIDIA modules are loaded early only while every installed kernel has them, decided again at every kernel or driver change by a pacman hook (steamify-nvidia-initramfs): a later kernel without the modules (another kernel, a failed DKMS build, a downgrade) no longer makes its initramfs and Limine boot entry fail** -- **feat: the NVIDIA fix is only for RTX 20 series or newer, decided like the VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists); older cards are left alone** -- **fix: the NVIDIA fix also edits Limine's `KERNEL_CMDLINE[default]+="..."` line (CachyOS writes that form; the fix found no line to edit and stopped) and counts `nvidia_drm.modeset=1` (underscore) as already set; tests/nvidia-hardware-test.sh sets `SCRIPT_DIR` (the hook script was read from `/patches`)** +- **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture, ticked along with it) starts it in Big Picture, unticked in its normal window (lib/nvidia.sh, services/steamify-steam-autostart.service). On a supported card (RTX 20 series or newer, chwd's legacy lists like the VRAM booster) it also puts nvidia-drm.modeset=1 and fbdev=1 on the kernel command line (Limine incl. its `+=` line, systemd-boot, GRUB) and the NVIDIA modules in the initramfs while every installed kernel has them (a pacman hook decides at every kernel change). The conversion and its options are hidden there unless already on** +- **test: tests/nvidia-test.sh runs it against a fake RTX 5080 beside an iGPU, a stub pacman and systemctl, and a temp home (no root, VM or hardware); verified on a real RTX 5080: gamescope's session, a nested gamescope and a KWin-only session all showed artifacts or stutter, Big Picture on the Plasma desktop did not** ## 2.9.6 - 2026-09-30 diff --git a/README.md b/README.md index 08a0da8..b00f5e9 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ Steam Machine. as OS Version (e.g. `steamos-3.9`), `steam-machine` as OS Codename and Steamify's version as OS Variant. The OS name stays CachyOS ([details](TECHNICAL.md#what-steams-system-settings-show)). + - **NVIDIA graphics cards:** gamescope's gaming mode shows a corrupted + picture there (an open NVIDIA driver bug), so the conversion isn't + offered. **Gaming on NVIDIA** takes its place: Steam on the desktop, + installed if it's missing and started at login, in **Big Picture** or its + normal window as you choose + ([details](TECHNICAL.md#gaming-on-nvidia)). 2. **SteamOS theme** - the Vapor look of SteamOS for the desktop, with **Add to Steam** in right-click menus ([details](TECHNICAL.md#steamos-desktop-look)). diff --git a/TECHNICAL.md b/TECHNICAL.md index bb17342..35b27c7 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -206,33 +206,39 @@ or upgraded. A newly added kernel doesn't come with its headers, so `ensure-kernel-headers.service` checks at every boot and installs any missing `-headers` package, which makes DKMS build the driver for it. -## NVIDIA fix for gaming mode +## Gaming on NVIDIA -gamescope drives the display itself through DRM/KMS, which with NVIDIA needs -kernel modesetting and the framebuffer driver, and the driver loaded early; -without them gaming mode can show a corrupted image. With a supported NVIDIA GPU (RTX 20 series or newer, decided like the VRAM -booster does: a card on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids` -is older and left alone, `vram_nvidia_legacy_id`; without those lists every -card counts) and its driver installed (`lib/nvidia.sh`), the SteamOS conversion adds -`nvidia-drm.modeset=1 nvidia-drm.fbdev=1` to the kernel command line (Limine, -systemd-boot or GRUB; the file is backed up first) and -`/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia -nvidia_modeset nvidia_uvm nvidia_drm)`), then rebuilds the initramfs and boot -entries. The early-load drop-in is only there while every installed kernel has the -NVIDIA modules (mkinitcpio fails on a missing module, and `limine-mkinitcpio` -then skips that kernel's boot entry, parameters included). That is decided again -at every kernel or driver change by a pacman hook -(`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`, after DKMS builds, -before the initramfs is built) running `/usr/local/libexec/steamify-nvidia-initramfs`: -another kernel without the modules, a failed DKMS build or a downgrade removes -the drop-in, and it comes back once every kernel has them again. -On Limine the parameters go into the existing `KERNEL_CMDLINE[default]="..."` -line: an extra appended `+=` line is pasted into the command line as text. -Only what's missing is changed; it applies after a reboot. Turning -the conversion off removes both, the hook and its script. `tests/nvidia-test.sh` tests it against a fake GPU -and fake boot loader files; `tests/nvidia-hardware-test.sh check|apply|visual` is for a -real NVIDIA PC (read-only check, the real apply, and a prompt for whether the picture -was clean), and writes `~/steamify-nvidia-report.txt`. +gamescope's own gaming mode shows a corrupted picture on NVIDIA graphics cards: NVIDIA's open bug 5240452 (flicker and +artifacts in the DRM session at modes above 2560x1440@120, seen up to driver 595 and on RTX 3070 Ti, 4090, 5090 and 5080; +no fix known). The kernel parameters `nvidia-drm.modeset=1`/`fbdev=1` and loading the NVIDIA modules early don't change it +(the driver already reports both `Y`), and gamescope started nested inside KDE artifacts too when KWin hands its fullscreen +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. + +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 +Steam at login): it installs `steam` when missing (recorded in state `nvidia`, removed again only then) and enables the user +unit `steamify-steam-autostart.service` (`services/`, `ExecStart=/usr/bin/steam @ARGS@`, Plasma only). Its sub-option +`bigpicture` ("Steam starts in Big Picture", ticked along with it) rewrites the unit with `-gamepadui`; unticked, Steam +starts in its normal window. The PC always boots into the desktop: there is no gamescope session. Takes effect at the next +login. + +On a supported card (RTX 20 series or newer: not on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids`, the VRAM booster's +check, `vram_nvidia_legacy_id`; `nvidia_supported`) enabling it also does what NVIDIA's DRM stack wants, though it didn't cure +gamescope: `nvidia-drm.modeset=1 nvidia-drm.fbdev=1` on the kernel command line (Limine, systemd-boot or GRUB; the file is +backed up first; on Limine into the existing `KERNEL_CMDLINE[default]="..."` or `+="..."` line, never an extra appended line, +which ends up as text; the kernel treats `nvidia_drm` and `nvidia-drm` alike) and +`/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)`), then the initramfs +and boot entries are rebuilt and checked for the parameters. The early-load drop-in is only there while every installed kernel +has the NVIDIA modules (mkinitcpio fails on a missing module, and `limine-mkinitcpio` then skips that kernel's boot entry): +a pacman hook (`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`, after DKMS, before the initramfs is built) runs +`/usr/local/libexec/steamify-nvidia-initramfs` at every kernel or driver change and decides again. Turning it off removes +all of it. `tests/nvidia-hardware-test.sh check|apply|visual` is for a real NVIDIA PC (writes `~/steamify-nvidia-report.txt`). `tests/nvidia-test.sh` tests it against a fake GPU, a stub `pacman` and `systemctl`, and a temp home. + +Tried and dropped (notes in steamify-cachyos-dev, `nvidia/HARDWARE-RESULTS.md`): a gamescope session that opens Big Picture in Plasma (it needed the `start-gamescope-session` +command shadowed in `/usr/local/bin`, and showed the desktop for a few seconds, then a Steam window with a black border); +a KWin session without Plasma (the mouse stuttered every second or two). ## HDMI-CEC @@ -538,7 +544,7 @@ immediately, which can turn into a loop - see | `lib/steam-game.sh` | Add as non-Steam game: Steamify in the Steam library (`patches/steam-shortcuts.py`) | | `lib/update-notifier.sh` | Update notifications: the notifier from `patches/` and its user timer | | `lib/first-login.sh` | `--defaults` without a session (the Steam Machine ISO's installer): the one-time first-login step that sets up single user's launcher on Plasma's new layout and opens the app | -| `lib/nvidia.sh` | NVIDIA fix for gaming mode: kernel parameters and early modules (part of the SteamOS conversion) | +| `lib/nvidia.sh` | Gaming on NVIDIA: Steam on the desktop, started at login, optionally in Big Picture (replaces the SteamOS conversion there) | | `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) | | `services/` | The systemd units the scripts install (`service_file`, `@KEY@` placeholders); see its README | | `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README | diff --git a/lib/backend.sh b/lib/backend.sh index 1e351ec..8cdf4d5 100644 --- a/lib/backend.sh +++ b/lib/backend.sh @@ -148,6 +148,8 @@ backend_apply() { # The same rules as the menu's toggles. [[ "${WANTED[single]:-0}" == 1 ]] && WANTED[gaming]=1 [[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; } + [[ "${WANTED[bigpicture]:-0}" == 1 ]] && WANTED[nvidia]=1 + [[ "${WANTED[nvidia]:-0}" == 0 ]] && WANTED[bigpicture]=0 [[ "${WANTED[poweroff]:-0}" == 1 ]] && WANTED[machine]=1 [[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0 [[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1 diff --git a/lib/login-manager.sh b/lib/login-manager.sh index 40d0280..bd35013 100644 --- a/lib/login-manager.sh +++ b/lib/login-manager.sh @@ -109,7 +109,6 @@ gaming_enable() { esac create_desktop_shortcut steam_enable - nvidia_enable || return 1 os_name_enable || return 1 } @@ -125,7 +124,6 @@ gaming_disable() { steam_disable krevert gaming os_name_disable - nvidia_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 2c99303..512ceab 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -5,12 +5,12 @@ # Menu order. Components are turned on in this order and off in reverse; # gaming must come first (single user builds on it). -COMPONENTS=(gaming boot theme glyphs single launcher steamgame notify vram cec machine poweroff kpin hdmi bios) +COMPONENTS=(gaming boot nvidia bigpicture theme glyphs single launcher steamgame notify vram cec machine poweroff kpin hdmi bios) # One-off actions rather than on/off components: never preselected, never # re-applied, not listed as on or off. ACTIONS=(bios) # Sub-options, shown indented under their parent and only while it's ticked. -declare -A PARENT=([boot]=gaming [steamgame]=launcher [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine) +declare -A PARENT=([boot]=gaming [bigpicture]=nvidia [steamgame]=launcher [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine) # Never preselected on a first run: booting into the desktop is a choice, # gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or # upset other devices on the TV, even on SteamOS), except on a Steam Machine, @@ -25,7 +25,7 @@ NO_PRESELECT=(boot cec kpin hdmi) # FEATURE_BASELINE. FEATURE_BASELINE=2.1.0 declare -A FEATURE_VERSION=( - [gaming]=2.9.7 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 + [gaming]=2.9.0 [boot]=2.1.0 [nvidia]=2.9.7 [bigpicture]=2.9.7 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 [launcher]=2.1.0 [cec]=2.7.0 [machine]=2.9.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [steamgame]=2.5.1 [kpin]=2.1.0 [hdmi]=2.1.0 ) @@ -33,6 +33,8 @@ declare -A FEATURE_VERSION=( declare -A LABEL=( [gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop" [boot]="Boot into the desktop instead of gaming mode" + [nvidia]="Gaming on NVIDIA: Steam on the desktop, started at login" + [bigpicture]="Steam starts in Big Picture: the controller-friendly Steam (untick: Steam's normal window)" [theme]="Install SteamOS theme: Vapor look (cachyos-vapor)" [glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode" [single]="Single user mode: no password, lock screen or log out (SDDM)" @@ -51,6 +53,11 @@ 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|single|glyphs) ! nvidia_present || gaming_status ;; + nvidia) nvidia_available ;; + bigpicture) bigpicture_available ;; machine|poweroff) machine_available ;; vram) vram_available ;; steamgame) steamgame_available ;; @@ -141,6 +148,7 @@ component_selectable() { detect_components() { local c any=false + for c in "${COMPONENTS[@]}"; do CURRENT[$c]=0; WANTED[$c]=0; done for c in "${COMPONENTS[@]}"; do component_available "$c" || continue if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi @@ -270,6 +278,9 @@ toggle_component() { if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then 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". + if [[ "$c" == nvidia ]]; then WANTED[bigpicture]=${WANTED[nvidia]}; fi + if [[ "$c" == bigpicture && "${WANTED[bigpicture]}" == 1 ]]; then WANTED[nvidia]=1; 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 dd0a817..dd42053 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -1,22 +1,99 @@ #!/bin/bash -# NVIDIA fix for gaming mode, part of the SteamOS conversion -# (gaming_enable/gaming_disable). gamescope drives the display itself -# through DRM/KMS; with NVIDIA that needs kernel modesetting and the -# framebuffer driver (nvidia-drm.modeset=1 nvidia-drm.fbdev=1) and the -# driver in the initramfs, or the screen shows a corrupted image. -# Does nothing without a supported NVIDIA GPU (RTX 20 series or newer) that has -# its driver installed. +# Gaming on NVIDIA PCs: the SteamOS conversion (gamescope session) isn't offered there, because gamescope's DRM session shows a +# corrupted picture on NVIDIA (NVIDIA's open bug 5240452: modes above 2560x1440@120). `nvidia` is the replacement: Steam on the +# Plasma desktop, installed when missing and started at login; `bigpicture` (a sub-option) makes it start in Big Picture. +# On a supported card (RTX 20 series or newer, `nvidia_supported`) it also sets up what NVIDIA's Wayland/DRM stack wants: +# nvidia-drm.modeset=1 and fbdev=1 on the kernel command line and the NVIDIA modules in the initramfs. That didn't cure +# gamescope's picture (the driver already reported both `Y`), but it is the standard setup and may be needed. # Sourced by steamify.sh; not meant to be run on its own. +NVIDIA_UNIT=steamify-steam-autostart.service NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1" -NVIDIA_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf +NVIDIA_INITRAMFS_CONF=${NVIDIA_INITRAMFS_CONF:-/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf} # The script and pacman hook that keep the initramfs file right at every kernel change. NVIDIA_SCRIPT=${NVIDIA_SCRIPT:-/usr/local/libexec/steamify-nvidia-initramfs} NVIDIA_HOOK=${NVIDIA_HOOK:-/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook} +# The running kernel's command line; the tests point this at a file. +NVIDIA_CMDLINE=${NVIDIA_CMDLINE:-/proc/cmdline} # The DRM devices in sysfs; the tests point this at a fake tree. NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm} +nvidia_unit_file() { echo "$HOME/.config/systemd/user/$NVIDIA_UNIT"; } + nvidia_present() { + # An NVIDIA GPU (PCI vendor 0x10de, display class) whose driver provides + # nvidia_drm. nouveau has none, and gamescope is not a problem there. + local d + for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do + [[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue + [[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue + modinfo nvidia_drm >/dev/null 2>&1 && return 0 + done + 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; } +bigpicture_available() { nvidia_available; } + +nvidia_status() { + pacman -Q steam >/dev/null 2>&1 && [[ -f "$(nvidia_unit_file)" ]] && + user_systemctl is-enabled -q "$NVIDIA_UNIT" 2>/dev/null +} + +nvidia_write_unit() { + # nvidia_write_unit [args]: the autostart unit, Steam started with those arguments. + local unit; unit="$(nvidia_unit_file)" + mkdir -p "$(dirname "$unit")" + service_file steamify-steam-autostart.service ARGS="$1" > "$unit" || { err "Writing $unit failed."; return 1; } + user_systemctl daemon-reload +} + +nvidia_enable() { + info "Gaming on NVIDIA: Steam on the desktop, started at login..." + if ! pacman -Q steam >/dev/null 2>&1; then + sudo pacman -S --needed --noconfirm steam || { err "Installing Steam failed."; return 1; } + state_set nvidia installed_pkgs steam + fi + # Keeps what the Big Picture option set up; a first run starts Steam normally. + local args=""; bigpicture_status && args=-gamepadui + nvidia_write_unit "$args" || return 1 + user_systemctl enable "$NVIDIA_UNIT" >/dev/null 2>&1 || { err "Enabling $NVIDIA_UNIT failed."; return 1; } + nvidia_kernel_enable || return 1 + ok "Steam starts at your next login." +} + +nvidia_disable() { + local pkgs unit; unit="$(nvidia_unit_file)" + user_systemctl disable --now "$NVIDIA_UNIT" >/dev/null 2>&1 + rm -f "$unit" + user_systemctl daemon-reload + pkgs="$(state_get nvidia installed_pkgs)" + # Only what Steamify installed; games and the Steam folder stay. + # shellcheck disable=SC2086 # a list of package names + [[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1 + state_clear nvidia + nvidia_kernel_disable + ok "Steam no longer starts at login." +} + +bigpicture_status() { grep -qs -- '-gamepadui' "$(nvidia_unit_file)"; } + +bigpicture_enable() { + # The unit comes from nvidia_enable (it runs first); without it there is nothing to change. + [[ -f "$(nvidia_unit_file)" ]] || return 0 + nvidia_write_unit -gamepadui && ok "Steam starts in Big Picture at your next login." +} + +bigpicture_disable() { + [[ -f "$(nvidia_unit_file)" ]] || return 0 + nvidia_write_unit "" && ok "Steam starts in its normal window at your next login." +} + +# --- Kernel parameters and early modules (a supported card only) + +nvidia_supported() { # A supported NVIDIA GPU: PCI vendor 0x10de, display class, its kernel # driver installed, and not older than the RTX 20 series, decided like the # VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists, every @@ -53,7 +130,7 @@ nvidia_param_set() { # The kernel treats "nvidia_drm" and "nvidia-drm" alike: CachyOS' own # setup may already have written the underscore spelling. local f re; f="$(nvidia_boot_file)"; re="${1//nvidia-drm/nvidia[-_]drm}" - grep -qwE -- "$re" /proc/cmdline 2>/dev/null || + grep -qwE -- "$re" "$NVIDIA_CMDLINE" 2>/dev/null || { [[ -n "$f" ]] && grep -qwE -- "$re" "$f" 2>/dev/null; } } @@ -107,8 +184,8 @@ nvidia_boot_has_params() { sudo grep -rqs 'nvidia-drm.modeset=1' "$target" } -nvidia_enable() { - nvidia_present || return 0 +nvidia_kernel_enable() { + nvidia_supported || return 0 local f changed=0 p; f="$(nvidia_boot_file)" if nvidia_params_missing; then if [[ -z "$f" ]]; then @@ -154,7 +231,7 @@ nvidia_enable() { return 0 } -nvidia_disable() { +nvidia_kernel_disable() { local f changed=0; f="$(nvidia_boot_file)" if [[ -n "$f" ]] && grep -q "$NVIDIA_PARAMS" "$f" 2>/dev/null; then sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f" diff --git a/services/README.md b/services/README.md index 110e98a..3ffd152 100644 --- a/services/README.md +++ b/services/README.md @@ -11,6 +11,7 @@ in the shell code. |---|---|---| | `steamify-boot-desktop.service` | `lib/boot-session.sh` (Boot into: desktop) | Sets the Plasma session before the login manager starts | | `sync-steamos-session.path`, `.service` | `lib/login-manager.sh` (SteamOS conversion, plasma-login-manager) | The sync bridge: copies Steam's session choice into `plasmalogin.conf` | +| `steamify-steam-autostart.service` | `lib/nvidia.sh` (Gaming on NVIDIA) | Steam at login on the Plasma desktop, normal or in Big Picture (user unit) | | `steam-desktop-autostart.service` | `lib/steam-desktop.sh` (SteamOS conversion) | Steam in the background on the Plasma desktop (user unit) | | `ensure-kernel-headers.service` | `lib/steam-machine.sh` (Steam Machine support) | Installs missing kernel headers at boot, so DKMS builds for every kernel | | `steamify-update-check.service`, `.timer` | `lib/update-notifier.sh` (Update notifications) | The daily and login check for a new release (user units) | diff --git a/services/steamify-steam-autostart.service b/services/steamify-steam-autostart.service new file mode 100644 index 0000000..89753eb --- /dev/null +++ b/services/steamify-steam-autostart.service @@ -0,0 +1,15 @@ +[Unit] +Description=Steam at login (Steamify, NVIDIA PCs) +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +# Only in the Plasma desktop session. +ExecCondition=/bin/sh -c '[ "$XDG_CURRENT_DESKTOP" = KDE ]' +ExecStart=/usr/bin/steam @ARGS@ +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=graphical-session.target diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index 52cee29..ed5c6c9 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -1,43 +1,109 @@ #!/bin/bash -# Tests lib/nvidia.sh against a fake RTX 5080 (PCI 10de:2c02) beside an -# Intel iGPU, a stub modinfo, and temp copies of the boot loader files. No -# root, VM or NVIDIA hardware needed: bash tests/nvidia-test.sh +# Tests lib/nvidia.sh ("Gaming on NVIDIA" and its Big Picture option) and the menu rules around it, against a fake RTX 5080 +# (PCI 10de:2c02) beside an Intel iGPU, a stub modinfo, pacman and systemctl, and a temp home. No root, VM or NVIDIA +# hardware needed: bash tests/nvidia-test.sh cd "$(dirname "$0")/.." || exit 1 T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT fail=0 check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; } -SCRIPT_DIR="$PWD"; source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh -sudo() { "$@"; } +SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME" +source lib/common.sh; source lib/state.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh; source lib/menu.sh +# Runs as the user, and refuses anything under the real system folders: a test must never touch them. +sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; } info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; } -backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; } -modinfo() { [[ "$FAKE_DRIVER" == 1 ]]; } -REBUILDS=0; nvidia_rebuild_boot() { REBUILDS=$((REBUILDS + 1)); } -NVIDIA_INITRAMFS_CONF="$T/90-steamify-nvidia.conf" +kwriteconfig6() { local f g k v; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2";; --key) k="$2";; esac; [[ $# -ge 2 ]] && v="$2"; shift; done; printf '%s=%s\n' "$k" "$v" > "$f"; } +kreadconfig6() { local f k; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2";; --key) k="$2";; esac; shift; done; sed -n "s/^$k=//p" "$f" 2>/dev/null; } +# A pretend package database and systemd: what the tests assert on. +PKGS=""; PACMAN_LOG="$T/pacman.log"; : > "$PACMAN_LOG"; ENABLED=0 +pacman() { + case "$1" in + -Q) [[ " $PKGS " == *" $2 "* ]] ;; + -S) echo "S $*" >> "$PACMAN_LOG"; PKGS+=" ${*: -1}" ;; + -Rns) echo "R $*" >> "$PACMAN_LOG"; PKGS=" ${PKGS//${*: -1}/} " ;; + esac +} +user_systemctl() { + case "$1" in + enable) ENABLED=1 ;; + disable) ENABLED=0 ;; + is-enabled) [[ $ENABLED == 1 ]] ;; + esac + return 0 +} +FAKE_DRIVER=1; modinfo() { [[ "$FAKE_DRIVER" == 1 ]]; } +GAMING_ON=0; gaming_status() { [[ "$GAMING_ON" == 1 ]]; } +UNIT="$HOME/.config/systemd/user/$NVIDIA_UNIT" + mkdir -p "$T/drm/card0/device" "$T/drm/card1/device" echo 0x8086 > "$T/drm/card0/device/vendor"; echo 0x030000 > "$T/drm/card0/device/class" echo 0x10de > "$T/drm/card1/device/vendor"; echo 0x030000 > "$T/drm/card1/device/class" echo 0x2c02 > "$T/drm/card1/device/device" NVIDIA_DRM_DIR="$T/drm" -mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst"; : > "$T/mods/6.1.0-cachyos/pkgbase" +backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; } +REBUILDS=0; nvidia_rebuild_boot() { REBUILDS=$((REBUILDS + 1)); } +: > "$T/cmdline"; NVIDIA_CMDLINE="$T/cmdline" # the running kernel's command line: without the parameters +NVIDIA_INITRAMFS_CONF="$T/90-steamify-nvidia.conf" NVIDIA_SCRIPT="$T/libexec/steamify-nvidia-initramfs"; NVIDIA_HOOK="$T/hooks/85-steamify-nvidia-initramfs.hook" +mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst"; : > "$T/mods/6.1.0-cachyos/pkgbase" NVIDIA_MODULES_DIR="$T/mods" NVIDIA_LIMINE_CONF="$T/none/limine.conf"; NVIDIA_SDBOOT_DIR="$T/none/entries"; NVIDIA_GRUB_CFG="$T/none/grub.cfg" # not generated here: not checked - mkdir -p "$T/chwd"; VRAM_CHWD_IDS="$T/chwd" -FAKE_DRIVER=1; check "finds the NVIDIA card as card1 beside an iGPU (no legacy lists)" nvidia_present -printf '1c03\n1b80\n' > "$T/chwd/nvidia-580.ids" # a GTX 1060 and a GTX 1080: chwd's 580xx legacy list -check "RTX 5080 (2c02) is not on a legacy list: supported" nvidia_present -echo 0x1c03 > "$T/drm/card1/device/device" -check "GTX 1060 (on chwd's legacy list, older than RTX 20): not supported" '! nvidia_present' -check "...and reported as a legacy GPU" nvidia_legacy_gpu -echo 0x2c02 > "$T/drm/card1/device/device"; check "RTX 5080 is not a legacy GPU" '! nvidia_legacy_gpu' -printf '2c02\n' > "$T/chwd/nvidia-580.ids"; check "a card id that is on a legacy list is skipped, whatever it is" '! nvidia_present' -printf '1c03\n' > "$T/chwd/nvidia-580.ids" -FAKE_DRIVER=0; check "skips when the driver isn't installed" '! nvidia_present' -FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "skips without an NVIDIA GPU" '! nvidia_present' +# The component's kernel step edits this boot loader file (systemd-boot's) in the tests, never a real one. +printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot-manage.conf"; nvidia_boot_file() { echo "$T/sdboot-manage.conf"; } + +# --- detection +check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present +FAKE_DRIVER=0; check "no nvidia_drm module (nouveau): not an NVIDIA PC" '! nvidia_present' +FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA GPU: not an NVIDIA PC" '! nvidia_present' NVIDIA_DRM_DIR="$T/drm" +# --- which items the menu shows +check "NVIDIA PC: the SteamOS conversion and its options are hidden" '! component_available gaming && ! component_available boot && ! component_available single && ! component_available glyphs' +check "NVIDIA PC: Gaming on NVIDIA and Big Picture are shown" 'component_available nvidia && component_available bigpicture' +GAMING_ON=1 +check "NVIDIA PC with the conversion already on: it is shown, so it can be turned off" 'component_available gaming && component_available boot' +check "...and Gaming on NVIDIA is hidden (both would start Steam)" '! component_available nvidia && ! component_available bigpicture' +GAMING_ON=0; NVIDIA_DRM_DIR="$T/none" +check "no NVIDIA: the conversion is shown as before" 'component_available gaming && component_available boot' +check "...and Gaming on NVIDIA is hidden" '! component_available nvidia && ! component_available bigpicture' +NVIDIA_DRM_DIR="$T/drm" + +# --- turning it on +check "off to begin with" '! nvidia_status && ! bigpicture_status' +nvidia_enable +check "Steam is installed when missing, and recorded as ours" 'grep -q "^S " "$PACMAN_LOG" && [[ "$(state_get nvidia installed_pkgs)" == steam ]]' +check "the autostart unit is written: Steam without arguments" 'grep -q "^ExecStart=/usr/bin/steam $" "$UNIT"' +check "it is on" 'nvidia_status && ! bigpicture_status' +n="$(wc -l < "$PACMAN_LOG")"; nvidia_enable +check "a second run installs nothing again" '[[ "$(wc -l < "$PACMAN_LOG")" == "$n" ]]' + +# --- Big Picture +bigpicture_enable +check "Big Picture: Steam starts with -gamepadui" 'grep -q "^ExecStart=/usr/bin/steam -gamepadui$" "$UNIT" && bigpicture_status' +nvidia_enable +check "turning Gaming on NVIDIA on again keeps Big Picture" 'bigpicture_status' +bigpicture_disable +check "Big Picture off: Steam's normal window again" '! bigpicture_status && nvidia_status' + +# --- turning it off +bigpicture_enable; nvidia_disable +check "off: the unit is gone and Steam, which we installed, is removed" '! nvidia_status && [[ ! -e "$UNIT" ]] && ! pacman -Q steam && [[ -z "$(state_get nvidia installed_pkgs)" ]]' +check "Big Picture without the unit does nothing" 'bigpicture_enable && bigpicture_disable && [[ ! -e "$UNIT" ]]' + +# --- Steam that was there already is never removed +PKGS=" steam"; : > "$PACMAN_LOG" +nvidia_enable; nvidia_disable +check "Steam installed before is kept, and not installed again" '[[ ! -s "$PACMAN_LOG" ]] && pacman -Q steam' + +# --- kernel parameters and early modules (a supported card only: RTX 20 series or newer) +check "RTX 5080 (2c02) is supported: no chwd legacy lists" nvidia_supported +printf '1c03\n1b80\n' > "$T/chwd/nvidia-580.ids" # a GTX 1060 and a GTX 1080: chwd's 580xx legacy list +check "RTX 5080 is not on a legacy list" 'nvidia_supported && ! nvidia_legacy_gpu' +echo 0x1c03 > "$T/drm/card1/device/device" +check "GTX 1060 (older than RTX 20): no kernel setup, but still an NVIDIA PC" '! nvidia_supported && nvidia_legacy_gpu && nvidia_present' +echo 0x2c02 > "$T/drm/card1/device/device" + for loader in limine limineplus sdboot grub; do case $loader in limine) F="$T/limine"; printf 'KERNEL_CMDLINE[default]="quiet splash"\n' > "$F" ;; @@ -48,14 +114,14 @@ for loader in limine limineplus sdboot grub; do esac nvidia_boot_file() { echo "$F"; } rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; ORIG="$(cat "$F")" - nvidia_enable + nvidia_kernel_enable check "$loader: parameters added" 'grep -q "nvidia-drm.modeset=1" "$F" && grep -q "nvidia-drm.fbdev=1" "$F"' check "$loader: initramfs drop-in written" 'grep -q nvidia_drm "$NVIDIA_INITRAMFS_CONF"' check "$loader: script and pacman hook installed" '[[ -x "$NVIDIA_SCRIPT" ]] && grep -q "^Exec = $NVIDIA_SCRIPT sync" "$NVIDIA_HOOK"' check "$loader: backup made, rebuilt once" '[[ -f "$F.bak" && $REBUILDS == 1 ]]' - SUM="$(cat "$F")"; nvidia_enable + SUM="$(cat "$F")"; nvidia_kernel_enable check "$loader: second run changes nothing" '[[ "$(cat "$F")" == "$SUM" && $REBUILDS == 1 ]]' - nvidia_disable + nvidia_kernel_disable check "$loader: disable restores the file, drops the drop-in, script and hook" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" && ! -e "$NVIDIA_SCRIPT" && ! -e "$NVIDIA_HOOK" ]]' done @@ -71,20 +137,30 @@ syncit; check "hook script: a new kernel without modules -> drop-in removed" '[[ syncit; check "hook script: the modules arrive (DKMS built) -> drop-in back" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]' rm -rf "$T/mods/6.6.0-lts"; mkdir -p "$T/mods/5.0.0-leftover"; : > "$T/mods/5.0.0-leftover/modules.dep" syncit; check "hook script: a leftover folder of a removed kernel is ignored" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]' -rm -rf "$T/mods/5.0.0-leftover"; nvidia_disable +rm -rf "$T/mods/5.0.0-leftover"; nvidia_kernel_disable # A second kernel without the NVIDIA modules: no early-load drop-in, parameters still set. mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase"; F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet"\n' > "$F" -nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_enable +nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_kernel_enable check "kernel without nvidia modules: no drop-in, parameters set" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && grep -q "nvidia-drm.fbdev=1" "$F"' rm -rf "$T/mods/6.6.0-lts" # Generated boot entries without the parameters (the initramfs build failed): an error. printf 'linux /vmlinuz root=/dev/sda1\n' > "$T/grub.cfg"; NVIDIA_GRUB_CFG="$T/grub.cfg" F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet"\n' > "$F"; nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF" -check "boot entries without the parameters: error" '! nvidia_enable >/dev/null' +check "boot entries without the parameters: error" '! nvidia_kernel_enable >/dev/null' NVIDIA_GRUB_CFG="$T/none/grub.cfg" # A config file without the setting to edit: an error, not "applied". F="$T/sdboot-manage.conf"; printf '#LINUX_OPTIONS=""\n' > "$F"; nvidia_boot_file() { echo "$F"; }; REBUILDS=0 -check "no setting to edit: error, nothing rebuilt" '! nvidia_enable >/dev/null && [[ $REBUILDS == 0 ]]' -FAKE_DRIVER=0; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_enable +check "no setting to edit: error, nothing rebuilt" '! nvidia_kernel_enable >/dev/null && [[ $REBUILDS == 0 ]]' +FAKE_DRIVER=0; NVIDIA_DRM_DIR="$T/drm"; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_kernel_enable check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]' + +# --- the menu rules +declare -A WANTED=([nvidia]=1 [bigpicture]=1 [gaming]=0 [boot]=0 [single]=0 [machine]=0 [poweroff]=0 [launcher]=0 [steamgame]=0) +component_selectable() { return 0; } +toggle_component nvidia +check "unticking Gaming on NVIDIA unticks Big Picture" '[[ "${WANTED[nvidia]}" == 0 && "${WANTED[bigpicture]}" == 0 ]]' +toggle_component bigpicture +check "ticking Big Picture ticks Gaming on NVIDIA" '[[ "${WANTED[nvidia]}" == 1 && "${WANTED[bigpicture]}" == 1 ]]' +check "Big Picture follows Gaming on NVIDIA in the menu" '[[ " ${COMPONENTS[*]} " == *" nvidia bigpicture "* && "${PARENT[bigpicture]}" == nvidia ]]' + exit $fail diff --git a/ui/qml/AppState.qml b/ui/qml/AppState.qml index c4bef7e..fe3c459 100644 --- a/ui/qml/AppState.qml +++ b/ui/qml/AppState.qml @@ -77,6 +77,8 @@ Item { // The same rules as the terminal menu (toggle_component). if (id === "gaming" && !w.gaming) w.single = false; if (id === "single" && w.single) w.gaming = true; + if (id === "nvidia") w.bigpicture = w.nvidia; + if (id === "bigpicture" && w.bigpicture) w.nvidia = true; if (id === "machine") w.poweroff = w.machine; if (id === "poweroff" && w.poweroff) w.machine = true; if (id === "machine" && !w.machine) w.kpin = false; diff --git a/ui/qml/Texts.qml b/ui/qml/Texts.qml index c92fa6b..a240fbf 100644 --- a/ui/qml/Texts.qml +++ b/ui/qml/Texts.qml @@ -10,6 +10,12 @@ QtObject { boot: { label: "Boot into", hint: "Where the PC starts", body: "Where the PC starts after a restart. Switching back and forth works the same either way.", changes: ["Desktop: the Plasma session is set before login", "Gaming: the SteamOS default"] }, + nvidia: { label: "Gaming on NVIDIA", hint: "Steam on the desktop, started at login", + body: "gamescope's own gaming mode shows a corrupted picture on NVIDIA graphics cards (an open NVIDIA driver bug), so it isn't offered here. This puts Steam on the Plasma desktop instead and starts it when you log in.", + changes: ["Steam is installed when it's missing", "Steam starts at login (a systemd user service)", "Off: the service is removed again; Steam is only removed if Steamify installed it"] }, + bigpicture: { label: "Steam starts in Big Picture", hint: "The controller-friendly Steam, or the normal window", + body: "Steam opens in Big Picture at login, which works well with a controller on the TV. Untick it for Steam's normal window.", + changes: ["Steam is started with -gamepadui", "Off: Steam starts in its normal window"] }, theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam", body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.", changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] }, From 3eb22502ec88456358dd78a96d265063b631cf97 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 19:13:46 +0200 Subject: [PATCH 14/17] feat: single user mode on NVIDIA PCs: SDDM autologin into the Plasma session, without the conversion --- CHANGELOG.md | 1 + TECHNICAL.md | 8 ++++++++ lib/backend.sh | 9 +++++++-- lib/login-manager.sh | 25 +++++++++++++++---------- lib/menu.sh | 7 ++++--- lib/single-user.sh | 30 ++++++++++++++++++++++++++++++ tests/nvidia-test.sh | 23 +++++++++++++++++++++-- ui/qml/AppState.qml | 3 ++- 8 files changed, 88 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7459959..8a04062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ one per merged pull request. ## 2.9.7 - 2026-10-01 - **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture, ticked along with it) starts it in Big Picture, unticked in its normal window (lib/nvidia.sh, services/steamify-steam-autostart.service). On a supported card (RTX 20 series or newer, chwd's legacy lists like the VRAM booster) it also puts nvidia-drm.modeset=1 and fbdev=1 on the kernel command line (Limine incl. its `+=` line, systemd-boot, GRUB) and the NVIDIA modules in the initramfs while every installed kernel has them (a pacman hook decides at every kernel change). The conversion and its options are hidden there unless already on** +- **feat: single user mode also works on NVIDIA PCs, without the SteamOS conversion: it switches to SDDM and logs in to the Plasma session by itself (/etc/sddm.conf.d/zzz-steamify-autologin.conf), and no longer ticks the conversion where that isn't offered (lib/single-user.sh)** - **test: tests/nvidia-test.sh runs it against a fake RTX 5080 beside an iGPU, a stub pacman and systemctl, and a temp home (no root, VM or hardware); verified on a real RTX 5080: gamescope's session, a nested gamescope and a KWin-only session all showed artifacts or stutter, Big Picture on the Plasma desktop did not** ## 2.9.6 - 2026-09-30 diff --git a/TECHNICAL.md b/TECHNICAL.md index 35b27c7..91e48fc 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -43,6 +43,14 @@ user wallet, with anything saved in it meanwhile, is kept as `kdewallet.kwl.steamify-single-user` and used again the next time it's on. Passwords aren't shared between the two wallets. +**Without the conversion (NVIDIA PCs, where gamescope's session is broken)** nothing else logs in by itself, so single user mode +does that too: `single_login_enable` (`lib/single-user.sh`) switches to SDDM and writes +`/etc/sddm.conf.d/zzz-steamify-autologin.conf` (`User=`, `Session=plasma.desktop`, `Relogin=true`; named to sort after +`steam-set-session`'s `zz-steamos-autologin.conf`, which may still say gamescope from an earlier conversion), and drops any +`[Autologin]` from `/etc/sddm.conf`. Turning it off removes the file and goes back to plasma-login-manager. With the +conversion on, the conversion does the login as before. Ticking single user mode only ticks the conversion where the +conversion is offered. + **SDDM** is what SteamOS uses, and CachyOS's `steam-set-session` supports it directly: it writes `/etc/sddm.conf.d/zz-steamos-autologin.conf`, which SDDM honours. The script installs and enables `sddm` (active from the next boot), diff --git a/lib/backend.sh b/lib/backend.sh index 8cdf4d5..1e7e8a1 100644 --- a/lib/backend.sh +++ b/lib/backend.sh @@ -146,8 +146,13 @@ backend_apply() { for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done done # The same rules as the menu's toggles. - [[ "${WANTED[single]:-0}" == 1 ]] && WANTED[gaming]=1 - [[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; } + # (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 + if component_available gaming; then + [[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; } + else + WANTED[boot]=0 + fi [[ "${WANTED[bigpicture]:-0}" == 1 ]] && WANTED[nvidia]=1 [[ "${WANTED[nvidia]:-0}" == 0 ]] && WANTED[bigpicture]=0 [[ "${WANTED[poweroff]:-0}" == 1 ]] && WANTED[machine]=1 diff --git a/lib/login-manager.sh b/lib/login-manager.sh index bd35013..7cef57f 100644 --- a/lib/login-manager.sh +++ b/lib/login-manager.sh @@ -167,6 +167,20 @@ switch_to_sddm() { ok "SDDM is the login manager from the next boot on." } +remove_sddm_base_autologin() { + # /etc/sddm.conf is read last: an [Autologin] there would override our fragments. + if [[ -f /etc/sddm.conf ]] && grep -q '^\[Autologin\]' /etc/sddm.conf; then + backup_file /etc/sddm.conf + sudo awk ' + /^\[Autologin\]/ { skip=1; next } + /^\[/ { skip=0 } + !skip { print } + ' /etc/sddm.conf | sudo tee /etc/sddm.conf.tmp > /dev/null && + sudo mv /etc/sddm.conf.tmp /etc/sddm.conf + info "Removed [Autologin] from /etc/sddm.conf (backup kept)." + fi +} + configure_sddm_autologin() { # User= and Relogin= live in our own fragment; steam-set-session keeps # Session= up to date in zz-steamos-autologin.conf, which sorts later @@ -182,16 +196,7 @@ Session=gamescope-session.desktop Relogin=true EOF - if [[ -f /etc/sddm.conf ]] && grep -q '^\[Autologin\]' /etc/sddm.conf; then - backup_file /etc/sddm.conf - sudo awk ' - /^\[Autologin\]/ { skip=1; next } - /^\[/ { skip=0 } - !skip { print } - ' /etc/sddm.conf | sudo tee /etc/sddm.conf.tmp > /dev/null && - sudo mv /etc/sddm.conf.tmp /etc/sddm.conf - info "Removed [Autologin] from /etc/sddm.conf (backup kept)." - fi + remove_sddm_base_autologin # Start in gamescope, through CachyOS's own tool so its file is current. sudo /usr/lib/steamos/steam-set-session gamescope-session.desktop diff --git a/lib/menu.sh b/lib/menu.sh index 512ceab..69e67db 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -55,7 +55,7 @@ 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|single|glyphs) ! nvidia_present || gaming_status ;; + gaming|boot|glyphs) ! nvidia_present || gaming_status ;; nvidia) nvidia_available ;; bigpicture) bigpicture_available ;; machine|poweroff) machine_available ;; @@ -230,7 +230,7 @@ defaults_options() { warn "Leaving out ${LABEL[$c]%%:*}: not available on this PC." fi done - [[ "${WANTED[single]}" == 1 ]] && WANTED[gaming]=1 + [[ "${WANTED[single]}" == 1 ]] && component_available gaming && WANTED[gaming]=1 for c in "${COMPONENTS[@]}"; do [[ "${WANTED[$c]}" == 1 && -n "${PARENT[$c]:-}" ]] && WANTED[${PARENT[$c]}]=1 done @@ -274,7 +274,8 @@ toggle_component() { component_selectable "$c" || return 1 WANTED[$c]=$(( 1 - WANTED[$c] )) # Single user mode only makes sense on top of the SteamOS conversion. - if [[ "$c" == single && "${WANTED[single]}" == 1 ]]; then WANTED[gaming]=1; fi + # (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 # Where to boot to is part of the conversion, too. if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi diff --git a/lib/single-user.sh b/lib/single-user.sh index dfa6423..2adc95c 100644 --- a/lib/single-user.sh +++ b/lib/single-user.sh @@ -119,7 +119,36 @@ single_launcher_drop() { return 0 } +# Without the SteamOS conversion (NVIDIA PCs: gamescope's session is broken there) nothing logs in by itself, so single user +# mode does it: SDDM, autologin into the Plasma session. Named to sort after steam-set-session's zz-steamos-autologin.conf, +# which may still say gamescope from an earlier conversion. +SINGLE_AUTOLOGIN=${SINGLE_AUTOLOGIN:-/etc/sddm.conf.d/zzz-steamify-autologin.conf} + +single_login_needed() { nvidia_present && ! gaming_status; } + +single_login_enable() { + single_login_needed || return 0 + switch_to_sddm || return 1 + info "Logging in to the desktop by itself (SDDM autologin for $TARGET_USER)..." + sudo mkdir -p "$(dirname "$SINGLE_AUTOLOGIN")" + sudo tee "$SINGLE_AUTOLOGIN" > /dev/null << EOF +[Autologin] +User=$TARGET_USER +Session=plasma.desktop +Relogin=true +EOF + remove_sddm_base_autologin +} + +single_login_disable() { + [[ -f "$SINGLE_AUTOLOGIN" ]] || return 0 + sudo rm -f "$SINGLE_AUTOLOGIN" + # The way back is CachyOS's own login manager, unless the conversion is on (it keeps SDDM). + gaming_status || switch_to_plasmalogin +} + single_enable() { + single_login_enable || return 1 info "Turning off the lock screen, user switching and logging out..." stop_plasmashell_for_edit @@ -145,6 +174,7 @@ single_enable() { } single_disable() { + single_login_disable info "Restoring the lock screen, user switching and logging out..." stop_plasmashell_for_edit if has_journal single; then diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index ed5c6c9..4af3cf3 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -8,7 +8,7 @@ fail=0 check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; } SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME" -source lib/common.sh; source lib/state.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh; source lib/menu.sh +source lib/common.sh; source lib/state.sh; source lib/login-manager.sh; source lib/single-user.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh; source lib/menu.sh # Runs as the user, and refuses anything under the real system folders: a test must never touch them. sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; } info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; } @@ -59,7 +59,7 @@ FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA GPU: not an NVIDIA PC" NVIDIA_DRM_DIR="$T/drm" # --- which items the menu shows -check "NVIDIA PC: the SteamOS conversion and its options are hidden" '! component_available gaming && ! component_available boot && ! component_available single && ! component_available glyphs' +check "NVIDIA PC: the SteamOS conversion and its gamescope options are hidden" '! component_available gaming && ! component_available boot && ! component_available glyphs' check "NVIDIA PC: Gaming on NVIDIA and Big Picture are shown" 'component_available nvidia && component_available bigpicture' GAMING_ON=1 check "NVIDIA PC with the conversion already on: it is shown, so it can be turned off" 'component_available gaming && component_available boot' @@ -154,6 +154,23 @@ check "no setting to edit: error, nothing rebuilt" '! nvidia_kernel_enable >/dev FAKE_DRIVER=0; NVIDIA_DRM_DIR="$T/drm"; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_kernel_enable check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]' +# --- single user mode on NVIDIA: its own login step (SDDM, autologin into Plasma) without the conversion +FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/drm" # an earlier section ended without the driver +SW="$T/switches.log"; : > "$SW"; TARGET_USER=tester; SINGLE_AUTOLOGIN="$T/sddm.conf.d/zzz-steamify-autologin.conf" +switch_to_sddm() { echo sddm >> "$SW"; }; switch_to_plasmalogin() { echo plasmalogin >> "$SW"; } +remove_sddm_base_autologin() { :; } # would look at the real /etc/sddm.conf +GAMING_ON=0 +check "single user mode is offered on NVIDIA without the conversion..." 'component_available single' +GAMING_ON=1; check "...and with it" 'component_available single'; GAMING_ON=0 +single_login_enable +check "single user: SDDM, autologin of the user into the Plasma session" 'grep -qx "User=tester" "$SINGLE_AUTOLOGIN" && grep -qx "Session=plasma.desktop" "$SINGLE_AUTOLOGIN" && grep -qx "Relogin=true" "$SINGLE_AUTOLOGIN" && grep -qx sddm "$SW"' +c1="$(cat "$SINGLE_AUTOLOGIN")"; single_login_enable +check "single user login: a second run changes nothing" '[[ "$(cat "$SINGLE_AUTOLOGIN")" == "$c1" ]]' +single_login_disable +check "single user off: the file is gone and plasma-login-manager is the login manager again" '[[ ! -e "$SINGLE_AUTOLOGIN" ]] && grep -qx plasmalogin "$SW"' +: > "$SW"; GAMING_ON=1; single_login_enable +check "with the conversion on, single user mode leaves the login to it" '[[ ! -e "$SINGLE_AUTOLOGIN" && ! -s "$SW" ]]' +GAMING_ON=0 # --- the menu rules declare -A WANTED=([nvidia]=1 [bigpicture]=1 [gaming]=0 [boot]=0 [single]=0 [machine]=0 [poweroff]=0 [launcher]=0 [steamgame]=0) component_selectable() { return 0; } @@ -161,6 +178,8 @@ toggle_component nvidia check "unticking Gaming on NVIDIA unticks Big Picture" '[[ "${WANTED[nvidia]}" == 0 && "${WANTED[bigpicture]}" == 0 ]]' toggle_component bigpicture check "ticking Big Picture ticks Gaming on NVIDIA" '[[ "${WANTED[nvidia]}" == 1 && "${WANTED[bigpicture]}" == 1 ]]' +WANTED[single]=0; WANTED[gaming]=0; toggle_component single +check "ticking single user mode does not tick the conversion where it isn't offered" '[[ "${WANTED[single]}" == 1 && "${WANTED[gaming]}" == 0 ]]' check "Big Picture follows Gaming on NVIDIA in the menu" '[[ " ${COMPONENTS[*]} " == *" nvidia bigpicture "* && "${PARENT[bigpicture]}" == nvidia ]]' exit $fail diff --git a/ui/qml/AppState.qml b/ui/qml/AppState.qml index fe3c459..f24bac6 100644 --- a/ui/qml/AppState.qml +++ b/ui/qml/AppState.qml @@ -75,8 +75,9 @@ Item { var w = Object.assign({}, want); 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; - if (id === "single" && w.single) w.gaming = true; + if (id === "single" && w.single && hasGaming) w.gaming = true; if (id === "nvidia") w.bigpicture = w.nvidia; if (id === "bigpicture" && w.bigpicture) w.nvidia = true; if (id === "machine") w.poweroff = w.machine; From 80b1fac9c104c6b4c1a8ce6627aa748b36f1348c Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 19:19:51 +0200 Subject: [PATCH 15/17] feat: Big Picture on NVIDIA starts on an empty Plasma session, so restored windows don't cover it --- CHANGELOG.md | 2 +- TECHNICAL.md | 5 +++-- lib/nvidia.sh | 7 ++++++- tests/nvidia-test.sh | 17 +++++++++++++++-- ui/qml/Texts.qml | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a04062..c5a25c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ one per merged pull request. ## 2.9.7 - 2026-10-01 -- **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture, ticked along with it) starts it in Big Picture, unticked in its normal window (lib/nvidia.sh, services/steamify-steam-autostart.service). On a supported card (RTX 20 series or newer, chwd's legacy lists like the VRAM booster) it also puts nvidia-drm.modeset=1 and fbdev=1 on the kernel command line (Limine incl. its `+=` line, systemd-boot, GRUB) and the NVIDIA modules in the initramfs while every installed kernel has them (a pacman hook decides at every kernel change). The conversion and its options are hidden there unless already on** +- **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture, ticked along with it) starts it in Big Picture, unticked in its normal window, and Big Picture starts on an empty Plasma session (no windows of the last session on top of it) (lib/nvidia.sh, services/steamify-steam-autostart.service). On a supported card (RTX 20 series or newer, chwd's legacy lists like the VRAM booster) it also puts nvidia-drm.modeset=1 and fbdev=1 on the kernel command line (Limine incl. its `+=` line, systemd-boot, GRUB) and the NVIDIA modules in the initramfs while every installed kernel has them (a pacman hook decides at every kernel change). The conversion and its options are hidden there unless already on** - **feat: single user mode also works on NVIDIA PCs, without the SteamOS conversion: it switches to SDDM and logs in to the Plasma session by itself (/etc/sddm.conf.d/zzz-steamify-autologin.conf), and no longer ticks the conversion where that isn't offered (lib/single-user.sh)** - **test: tests/nvidia-test.sh runs it against a fake RTX 5080 beside an iGPU, a stub pacman and systemctl, and a temp home (no root, VM or hardware); verified on a real RTX 5080: gamescope's session, a nested gamescope and a KWin-only session all showed artifacts or stutter, Big Picture on the Plasma desktop did not** diff --git a/TECHNICAL.md b/TECHNICAL.md index 91e48fc..dc56f8b 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -228,8 +228,9 @@ conversion (`gaming`, and its options `boot`, `single`, `glyphs`) isn't offered, off). `nvidia` ("Gaming on NVIDIA", `lib/nvidia.sh`) takes its place, hidden while the conversion is on (both would start Steam at login): it installs `steam` when missing (recorded in state `nvidia`, removed again only then) and enables the user unit `steamify-steam-autostart.service` (`services/`, `ExecStart=/usr/bin/steam @ARGS@`, Plasma only). Its sub-option -`bigpicture` ("Steam starts in Big Picture", ticked along with it) rewrites the unit with `-gamepadui`; unticked, Steam -starts in its normal window. The PC always boots into the desktop: there is no gamescope session. Takes effect at the next +`bigpicture` ("Steam starts in Big Picture", ticked along with it) rewrites the unit with `-gamepadui` and sets +`ksmserverrc [General] loginMode=emptySession` (`kset`, undone with `krevert bigpicture`), so windows of the last session +(Discord, a browser) aren't restored on top of Big Picture; unticked, Steam starts in its normal window. The PC always boots into the desktop: there is no gamescope session. Takes effect at the next login. On a supported card (RTX 20 series or newer: not on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids`, the VRAM booster's diff --git a/lib/nvidia.sh b/lib/nvidia.sh index dd42053..e624e2b 100644 --- a/lib/nvidia.sh +++ b/lib/nvidia.sh @@ -74,6 +74,7 @@ nvidia_disable() { # shellcheck disable=SC2086 # a list of package names [[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1 state_clear nvidia + krevert bigpicture nvidia_kernel_disable ok "Steam no longer starts at login." } @@ -83,10 +84,14 @@ bigpicture_status() { grep -qs -- '-gamepadui' "$(nvidia_unit_file)"; } bigpicture_enable() { # The unit comes from nvidia_enable (it runs first); without it there is nothing to change. [[ -f "$(nvidia_unit_file)" ]] || return 0 - nvidia_write_unit -gamepadui && ok "Steam starts in Big Picture at your next login." + nvidia_write_unit -gamepadui || return 1 + # Windows of the last session (Discord, a browser) would be restored on top of Big Picture. + kset bigpicture ksmserverrc General loginMode emptySession + ok "Steam starts in Big Picture at your next login, on an empty desktop." } bigpicture_disable() { + krevert bigpicture [[ -f "$(nvidia_unit_file)" ]] || return 0 nvidia_write_unit "" && ok "Steam starts in its normal window at your next login." } diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index 4af3cf3..c157ade 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -12,8 +12,19 @@ source lib/common.sh; source lib/state.sh; source lib/login-manager.sh; source l # Runs as the user, and refuses anything under the real system folders: a test must never touch them. sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; } info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; } -kwriteconfig6() { local f g k v; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2";; --key) k="$2";; esac; [[ $# -ge 2 ]] && v="$2"; shift; done; printf '%s=%s\n' "$k" "$v" > "$f"; } -kreadconfig6() { local f k; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2";; --key) k="$2";; esac; shift; done; sed -n "s/^$k=//p" "$f" 2>/dev/null; } +# KDE's config tools on plain files under the temp home (a relative file name is relative to ~/.config, as in KDE). +kwriteconfig6() { + local f="" k="" v="" del=0 + while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group) shift ;; --delete) del=1 ;; *) v="$1" ;; esac; shift; done + [[ "$f" == /* ]] || f="$HOME/.config/$f"; mkdir -p "$(dirname "$f")" + if [[ $del == 1 ]]; then sed -i "/^$k=/d" "$f" 2>/dev/null; else sed -i "/^$k=/d" "$f" 2>/dev/null; printf '%s=%s\n' "$k" "$v" >> "$f"; fi +} +kreadconfig6() { + local f="" k="" d="" r + while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --default) d="$2"; shift ;; --group) shift ;; esac; shift; done + [[ "$f" == /* ]] || f="$HOME/.config/$f" + r="$(sed -n "s/^$k=//p" "$f" 2>/dev/null)"; printf '%s\n' "${r:-$d}" +} # A pretend package database and systemd: what the tests assert on. PKGS=""; PACMAN_LOG="$T/pacman.log"; : > "$PACMAN_LOG"; ENABLED=0 pacman() { @@ -83,8 +94,10 @@ bigpicture_enable check "Big Picture: Steam starts with -gamepadui" 'grep -q "^ExecStart=/usr/bin/steam -gamepadui$" "$UNIT" && bigpicture_status' nvidia_enable check "turning Gaming on NVIDIA on again keeps Big Picture" 'bigpicture_status' +check "Big Picture: Plasma starts with an empty session" '[[ "$(kreadconfig6 --file ksmserverrc --group General --key loginMode)" == emptySession ]]' bigpicture_disable check "Big Picture off: Steam's normal window again" '! bigpicture_status && nvidia_status' +check "...and the session setting is back as it was (not set)" '[[ -z "$(kreadconfig6 --file ksmserverrc --group General --key loginMode)" ]]' # --- turning it off bigpicture_enable; nvidia_disable diff --git a/ui/qml/Texts.qml b/ui/qml/Texts.qml index a240fbf..c2f979d 100644 --- a/ui/qml/Texts.qml +++ b/ui/qml/Texts.qml @@ -15,7 +15,7 @@ QtObject { changes: ["Steam is installed when it's missing", "Steam starts at login (a systemd user service)", "Off: the service is removed again; Steam is only removed if Steamify installed it"] }, bigpicture: { label: "Steam starts in Big Picture", hint: "The controller-friendly Steam, or the normal window", body: "Steam opens in Big Picture at login, which works well with a controller on the TV. Untick it for Steam's normal window.", - changes: ["Steam is started with -gamepadui", "Off: Steam starts in its normal window"] }, + changes: ["Steam is started with -gamepadui", "Plasma starts with an empty session, so old windows don't cover Big Picture", "Off: Steam starts in its normal window"] }, theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam", body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.", changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] }, From 439b632d71e5ad90dd6b4a06432effe9da683af0 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 21:05:05 +0200 Subject: [PATCH 16/17] chore: Version 2.10.0 --- CHANGELOG.md | 2 +- lib/menu.sh | 2 +- steamify.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a25c3..74e17e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes, per version and per commit. Versions follow `steamify.sh`. Versions before 0.7.0 were numbered afterwards, one per merged pull request. -## 2.9.7 - 2026-10-01 +## 2.10.0 - 2026-10-01 - **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture, ticked along with it) starts it in Big Picture, unticked in its normal window, and Big Picture starts on an empty Plasma session (no windows of the last session on top of it) (lib/nvidia.sh, services/steamify-steam-autostart.service). On a supported card (RTX 20 series or newer, chwd's legacy lists like the VRAM booster) it also puts nvidia-drm.modeset=1 and fbdev=1 on the kernel command line (Limine incl. its `+=` line, systemd-boot, GRUB) and the NVIDIA modules in the initramfs while every installed kernel has them (a pacman hook decides at every kernel change). The conversion and its options are hidden there unless already on** - **feat: single user mode also works on NVIDIA PCs, without the SteamOS conversion: it switches to SDDM and logs in to the Plasma session by itself (/etc/sddm.conf.d/zzz-steamify-autologin.conf), and no longer ticks the conversion where that isn't offered (lib/single-user.sh)** diff --git a/lib/menu.sh b/lib/menu.sh index 69e67db..823d9ce 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -25,7 +25,7 @@ NO_PRESELECT=(boot cec kpin hdmi) # FEATURE_BASELINE. FEATURE_BASELINE=2.1.0 declare -A FEATURE_VERSION=( - [gaming]=2.9.0 [boot]=2.1.0 [nvidia]=2.9.7 [bigpicture]=2.9.7 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 + [gaming]=2.9.0 [boot]=2.1.0 [nvidia]=2.10.0 [bigpicture]=2.10.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 [launcher]=2.1.0 [cec]=2.7.0 [machine]=2.9.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [steamgame]=2.5.1 [kpin]=2.1.0 [hdmi]=2.1.0 ) diff --git a/steamify.sh b/steamify.sh index 1050c83..7cc07b9 100755 --- a/steamify.sh +++ b/steamify.sh @@ -15,7 +15,7 @@ set -uo pipefail # Release version, see CHANGELOG.md. -VERSION=2.9.7 +VERSION=2.10.0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" From 6f79dca05c78d6bc80f2bbe7c08c70e3b9de3c8b Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 21:11:46 +0200 Subject: [PATCH 17/17] fix: Add as non-Steam game isn't offered on NVIDIA PCs (no gaming mode there) --- CHANGELOG.md | 1 + README.md | 2 +- lib/menu.sh | 4 +++- tests/nvidia-test.sh | 8 ++++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e17e7..37f14d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ one per merged pull request. - **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture, ticked along with it) starts it in Big Picture, unticked in its normal window, and Big Picture starts on an empty Plasma session (no windows of the last session on top of it) (lib/nvidia.sh, services/steamify-steam-autostart.service). On a supported card (RTX 20 series or newer, chwd's legacy lists like the VRAM booster) it also puts nvidia-drm.modeset=1 and fbdev=1 on the kernel command line (Limine incl. its `+=` line, systemd-boot, GRUB) and the NVIDIA modules in the initramfs while every installed kernel has them (a pacman hook decides at every kernel change). The conversion and its options are hidden there unless already on** - **feat: single user mode also works on NVIDIA PCs, without the SteamOS conversion: it switches to SDDM and logs in to the Plasma session by itself (/etc/sddm.conf.d/zzz-steamify-autologin.conf), and no longer ticks the conversion where that isn't offered (lib/single-user.sh)** +- **fix: "Add as non-Steam game" isn't offered on NVIDIA PCs (there is no gaming mode there for it to give the controller; shown when already on, so it can be turned off)** - **test: tests/nvidia-test.sh runs it against a fake RTX 5080 beside an iGPU, a stub pacman and systemctl, and a temp home (no root, VM or hardware); verified on a real RTX 5080: gamescope's session, a nested gamescope and a KWin-only session all showed artifacts or stutter, Big Picture on the Plasma desktop did not** ## 2.9.6 - 2026-09-30 diff --git a/README.md b/README.md index b00f5e9..e3367f5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Steam Machine. With **Add as non-Steam game** (on by default) it's in your Steam library too: started from Steam it gets your controller (a Steam Controller only talks to Steam), in gaming mode as well. Steam closes for a moment while - it's added, so that's done from the desktop. + it's added, so that's done from the desktop. Not offered on PCs with an NVIDIA graphics card, which have no gaming mode. 6. **Update notifications** - on by default: a notification and a tray icon when there's a new Steamify, with Open Steamify and Skip this version. It never updates anything by itself diff --git a/lib/menu.sh b/lib/menu.sh index 823d9ce..d6a2b81 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -60,7 +60,9 @@ component_available() { bigpicture) bigpicture_available ;; machine|poweroff) machine_available ;; vram) vram_available ;; - steamgame) steamgame_available ;; + # Steamify in the Steam library is for gaming mode's controller: not needed for Big Picture on the desktop (NVIDIA PCs; + # shown anyway when it's already on, so it can be turned off). + steamgame) steamgame_available && { ! nvidia_present || steamgame_status; } ;; kpin) kpin_available ;; hdmi) hdmi_available ;; bios) bios_available ;; diff --git a/tests/nvidia-test.sh b/tests/nvidia-test.sh index c157ade..3d5ad98 100644 --- a/tests/nvidia-test.sh +++ b/tests/nvidia-test.sh @@ -80,6 +80,14 @@ check "no NVIDIA: the conversion is shown as before" 'component_available gaming check "...and Gaming on NVIDIA is hidden" '! component_available nvidia && ! component_available bigpicture' NVIDIA_DRM_DIR="$T/drm" +# --- "Add as non-Steam game" is for gaming mode's controller: not offered on an NVIDIA PC (unless already on) +steamgame_available() { return 0; }; STEAMGAME_ON=0; steamgame_status() { [[ "$STEAMGAME_ON" == 1 ]]; } +check "NVIDIA PC: Add as non-Steam game is hidden" '! component_available steamgame' +STEAMGAME_ON=1; check "...unless it is already on (so it can be turned off)" 'component_available steamgame'; STEAMGAME_ON=0 +NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA: it is offered as before" 'component_available steamgame' +steamgame_available() { return 1; }; check "no NVIDIA, no Steam account yet: still not offered" '! component_available steamgame' +steamgame_available() { return 0; }; NVIDIA_DRM_DIR="$T/drm" + # --- turning it on check "off to begin with" '! nvidia_status && ! bigpicture_status' nvidia_enable