From d1373e28a64fc0d31c6bf2ceac76a5297622bb86 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 1 Oct 2026 19:13:46 +0200 Subject: [PATCH] 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;