diff --git a/bin/omarchy-hw-nvidia-display b/bin/omarchy-hw-nvidia-display new file mode 100755 index 00000000..1cdf5b70 --- /dev/null +++ b/bin/omarchy-hw-nvidia-display @@ -0,0 +1,45 @@ +#!/bin/bash + +# omarchy:summary=Detect whether NVIDIA drives the display (rather than a hybrid iGPU). + +# On hybrid laptops the display is usually wired to the integrated GPU while +# the NVIDIA card only handles offloaded rendering. Forcing NVIDIA's VA-API and +# GLX drivers globally then breaks Chromium-based browsers (black video with +# audio still playing), so the Hyprland NVIDIA env only applies when NVIDIA is +# the display GPU. +# +# sysfs exposes which VGA device drives the display via boot_vga. When no +# device claims it (attribute missing or unreadable), assume NVIDIA is the +# display GPU to preserve the current behavior on machines we cannot tell. +# +# Read the cached sysfs IDs rather than lspci, which reads PCI config space and +# resumes runtime-suspended GPUs. +pci_devices_path="${OMARCHY_PCI_DEVICES_PATH:-/sys/bus/pci/devices}" + +shopt -s nullglob + +nvidia_boot=0 +other_boot=0 + +for device in "$pci_devices_path"/*; do + [[ $(< "$device/class") == 0x03* ]] || continue + boot_vga=0 + if [[ -f $device/boot_vga ]]; then + boot_vga=$(< "$device/boot_vga") + fi + if [[ $(< "$device/vendor") == "0x10de" ]]; then + if (( boot_vga == 1 )); then + nvidia_boot=1 + fi + else + if (( boot_vga == 1 )); then + other_boot=1 + fi + fi +done + +if (( other_boot == 1 && nvidia_boot == 0 )); then + exit 1 +else + exit 0 +fi diff --git a/default/hypr/nvidia.lua b/default/hypr/nvidia.lua index 663fe79e..a3a15546 100644 --- a/default/hypr/nvidia.lua +++ b/default/hypr/nvidia.lua @@ -1,6 +1,7 @@ local paths = require("default.hypr.paths") local nvidia = paths.omarchy_path .. "/bin/omarchy-hw-nvidia" +local nvidia_display = paths.omarchy_path .. "/bin/omarchy-hw-nvidia-display" local nvidia_gsp = paths.omarchy_path .. "/bin/omarchy-hw-nvidia-gsp" local nvidia_without_gsp = paths.omarchy_path .. "/bin/omarchy-hw-nvidia-without-gsp" @@ -8,12 +9,20 @@ local nvidia_without_gsp = paths.omarchy_path .. "/bin/omarchy-hw-nvidia-without -- lspci reads PCI config space, which resumes a runtime-suspended GPU, and on a -- hybrid laptop that wake alone outlasts Hyprland's 1.5s config reload budget. if o.shell_succeeds(o.shell_quote(nvidia)) then + -- On hybrid laptops the display is wired to the iGPU, so forcing NVIDIA's + -- VA-API/GLX drivers globally breaks Chromium-based browsers (black video). + -- Only apply those when NVIDIA actually drives the display. + local nvidia_is_display = o.shell_succeeds(o.shell_quote(nvidia_display)) if o.shell_succeeds(o.shell_quote(nvidia_gsp)) then hl.env("NVD_BACKEND", "direct") - hl.env("LIBVA_DRIVER_NAME", "nvidia") - hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") + if nvidia_is_display then + hl.env("LIBVA_DRIVER_NAME", "nvidia") + hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") + end elseif o.shell_succeeds(o.shell_quote(nvidia_without_gsp)) then hl.env("NVD_BACKEND", "egl") - hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") + if nvidia_is_display then + hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") + end end end diff --git a/migrations/1790282866.sh b/migrations/1790282866.sh new file mode 100644 index 00000000..92c9734a --- /dev/null +++ b/migrations/1790282866.sh @@ -0,0 +1,8 @@ +echo "Apply the NVIDIA video driver fix on hybrid laptops" + +if omarchy-hw-nvidia && ! omarchy-hw-nvidia-display; then + # Hyprland applies env at startup. Reloading the updated defaults cannot + # clear the old overrides inherited by the compositor and its applications. + omarchy-state set reboot-required + echo "Reboot to stop forcing NVIDIA VA-API/GLX drivers on the integrated GPU." +fi diff --git a/test/shell.d/hw-nvidia-test.sh b/test/shell.d/hw-nvidia-test.sh index da461ce4..eba3f7ab 100755 --- a/test/shell.d/hw-nvidia-test.sh +++ b/test/shell.d/hw-nvidia-test.sh @@ -3,97 +3,58 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" +require_command lua tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT +export OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" OMARCHY_PATH="$ROOT" PATH="$ROOT/bin:$PATH" -# Each argument is a PCI device as "vendor:device:class", in sysfs's own format. -write_pci_devices() { +# Exit statuses, expected NVD_BACKEND/LIBVA/GLX values, then vendor:device:class[:boot_vga]. +while IFS='|' read -r description nvidia gsp without_gsp display expected_env devices; do rm -rf "$tmp_dir/devices" mkdir -p "$tmp_dir/devices" - - local index=0 - local spec - for spec in "$@"; do - local slot - slot=$(printf '0000:%02x:00.0' "$index") - mkdir -p "$tmp_dir/devices/$slot" - printf '%s\n' "${spec%%:*}" >"$tmp_dir/devices/$slot/vendor" - printf '%s\n' "$(cut -d: -f2 <<<"$spec")" >"$tmp_dir/devices/$slot/device" - printf '%s\n' "${spec##*:}" >"$tmp_dir/devices/$slot/class" - index=$((index + 1)) - done -} - -hw_nvidia() { - OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" "$ROOT/bin/omarchy-hw-$1" -} - -assert_detects() { - local description="$1" nvidia="$2" gsp="$3" without_gsp="$4" - - local command - for command in nvidia gsp without-gsp; do - local expected - case $command in - nvidia) expected=$nvidia ;; - gsp) expected=$gsp ;; - without-gsp) expected=$without_gsp ;; - esac - - local detector=nvidia - [[ $command == "nvidia" ]] || detector="nvidia-$command" - - local actual=no - hw_nvidia "$detector" && actual=yes - - [[ $actual == "$expected" ]] || - fail "$description" "omarchy-hw-$detector: expected $expected, got $actual" + slot=0 + for spec in $devices; do + IFS=: read -r vendor device class boot_vga <<<"$spec" + device_dir="$tmp_dir/devices/$slot" + mkdir -p "$device_dir" + printf '%s\n' "$vendor" >"$device_dir/vendor" + printf '%s\n' "$device" >"$device_dir/device" + printf '%s\n' "$class" >"$device_dir/class" + if [[ -n $boot_vga ]]; then + printf '%s\n' "$boot_vga" >"$device_dir/boot_vga" + fi + slot=$((slot + 1)) done + for check in "nvidia:$nvidia" "nvidia-gsp:$gsp" "nvidia-without-gsp:$without_gsp" "nvidia-display:$display"; do + status=0 + "omarchy-hw-${check%:*}" || status=$? + [[ $status == "${check#*:}" ]] || fail "$description: ${check%:*}" "expected ${check#*:}, got $status" + done + + # Run the real Lua config and detectors; capture only Hyprland's env calls. + actual_env=$(lua <<'LUA' +package.path = os.getenv("ROOT") .. "/?.lua;" .. package.path +require("default.hypr.helpers") +local env = {} +hl = { env = function(key, value) env[key] = value end } +require("default.hypr.nvidia") +print(table.concat({ env.NVD_BACKEND or "-", env.LIBVA_DRIVER_NAME or "-", env.__GLX_VENDOR_LIBRARY_NAME or "-" }, " ")) +LUA + ) + [[ $actual_env == "$expected_env" ]] || fail "$description: driver environment" "expected $expected_env, got $actual_env" pass "$description" -} - -# AMD Cezanne integrated graphics. -write_pci_devices 0x1002:0x15e7:0x030000 -assert_detects "a machine without an NVIDIA GPU detects nothing" no no no - -# NVIDIA GA106M [RTX 3060 Mobile] alongside AMD Cezanne, the pair from issue #6660. -write_pci_devices 0x1002:0x15e7:0x030000 0x10de:0x2560:0x030200 -assert_detects "a hybrid Ampere laptop detects a GSP GPU" yes yes no - -# NVIDIA TU117M [GTX 1650 Mobile], the first generation with GSP firmware. -write_pci_devices 0x10de:0x1f91:0x030000 -assert_detects "Turing is the oldest generation with GSP firmware" yes yes no - -# NVIDIA GV100 [TITAN V], the newest generation without GSP firmware. -write_pci_devices 0x10de:0x1d81:0x030000 -assert_detects "Volta is the newest generation without GSP firmware" yes no yes - -# NVIDIA GP104 [GTX 1080]. -write_pci_devices 0x10de:0x1b80:0x030000 -assert_detects "Pascal detects a GPU without GSP firmware" yes no yes - -# NVIDIA GM108M [GeForce 830M], the oldest part the 580xx driver supports. -write_pci_devices 0x10de:0x1340:0x030000 -assert_detects "Maxwell detects a GPU without GSP firmware" yes no yes - -# NVIDIA GK110 [GTX 780]. Kepler predates GSP but also predates 580xx, so -# claiming it here would install a driver that cannot drive it. -write_pci_devices 0x10de:0x1004:0x030000 -assert_detects "Kepler is too old for either driver" yes no no - -# NVIDIA GF100 [GTX 470], older still. -write_pci_devices 0x10de:0x06cd:0x030000 -assert_detects "Fermi is too old for either driver" yes no no - -# NVIDIA GB203 [RTX 5080], newer than every other device ID here. -write_pci_devices 0x10de:0x2c02:0x030000 -assert_detects "Blackwell detects a GSP GPU" yes yes no - -# The GA106 audio function carries the NVIDIA vendor ID but is not a GPU. -write_pci_devices 0x10de:0x228e:0x040300 -assert_detects "a non-display NVIDIA function is not a GPU" no no no - -write_pci_devices -assert_detects "a machine with no PCI devices detects nothing" no no no +done <<'CASES' +AMD only|1|1|1|1|- - -|0x1002:0x15e7:0x030000:1 +NVIDIA audio only|1|1|1|0|- - -|0x10de:0x228e:0x040300 +No PCI devices|1|1|1|0|- - -| +Turing (first GSP)|0|0|1|0|direct nvidia nvidia|0x10de:0x1f91:0x030000 +Volta (last without GSP)|0|1|0|0|egl - nvidia|0x10de:0x1d81:0x030000 +Maxwell (first 580xx)|0|1|0|0|egl - nvidia|0x10de:0x1340:0x030000 +Kepler (unsupported)|0|1|1|0|- - -|0x10de:0x1004:0x030000 +AMD display with Ampere offload|0|0|1|1|direct - -|0x1002:0x15bf:0x030000:1 0x10de:0x25ac:0x030200:0 +Intel display with Maxwell offload|0|1|0|1|egl - -|0x8086:0x46a6:0x030000:1 0x10de:0x1340:0x030000:0 +NVIDIA display with inactive iGPU|0|0|1|0|direct nvidia nvidia|0x1002:0x15bf:0x030000:0 0x10de:0x2c02:0x030000:1 +Hybrid without boot_vga|0|0|1|0|direct nvidia nvidia|0x1002:0x15e7:0x030000 0x10de:0x2560:0x030200 +CASES