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"