diff --git a/bin/deckshift-settings b/bin/deckshift-settings index de0efda..8a105de 100755 --- a/bin/deckshift-settings +++ b/bin/deckshift-settings @@ -145,6 +145,13 @@ refresh_monitor_data() { fi HYPR_MODES=$(hyprctl monitors all -j 2>/dev/null \ | jq -r --arg n "$target" '.[] | select(.name==$n) | .availableModes[]?') + # Configured connector might not currently be plugged in (e.g. external + # display unplugged). Bail out cleanly so the rest of the function doesn't + # run grep against empty input and trip pipefail under set -e. + if [[ -z "$HYPR_MODES" ]]; then + HYPR_NATIVE=""; HYPR_MAX_REFRESH="" + return + fi HYPR_NATIVE=$(echo "$HYPR_MODES" | head -1 | sed 's/@.*//') # Max refresh AT NATIVE resolution, rounded to integer Hz for display. # (Reporting the max across all modes would mislead — e.g. a 4K@60 panel @@ -213,13 +220,30 @@ list_gpus() { # ------------------------------------------------------------------------------ show_state() { - local connector width height refresh vk_adapter dri_prime + local connector width height refresh vk_adapter dri_prime prime_offload mesa_vk_select connector=$(effective OUTPUT_CONNECTOR) width=$(effective SCREEN_WIDTH) height=$(effective SCREEN_HEIGHT) refresh=$(effective CUSTOM_REFRESH_RATES) vk_adapter=$(effective VULKAN_ADAPTER) dri_prime=$(effective DRI_PRIME) + prime_offload=$(effective __NV_PRIME_RENDER_OFFLOAD) + mesa_vk_select=$(effective MESA_VK_DEVICE_SELECT) + + # Single GPU-mode line — shows the active mode rather than half-empty rows, + # since the modes are mutually exclusive. AMD hybrid is identified by both + # DRI_PRIME and MESA_VK_DEVICE_SELECT being set (the TUI sets both together); + # plain DRI_PRIME without MESA_VK_DEVICE_SELECT is "AMD/Intel direct". + local gpu_mode="" + if [[ -n "$prime_offload" ]]; then + gpu_mode="Hybrid PRIME offload (NVIDIA via iGPU)" + elif [[ -n "$dri_prime" && -n "$mesa_vk_select" ]]; then + gpu_mode="Hybrid PRIME offload (AMD via iGPU → ${mesa_vk_select})" + elif [[ -n "$vk_adapter" ]]; then + gpu_mode="NVIDIA direct (${vk_adapter})" + elif [[ -n "$dri_prime" ]]; then + gpu_mode="AMD/Intel direct (${dri_prime})" + fi local pending_label="" has_pending_changes && pending_label=" $(gum style --foreground 214 '(unsaved changes)')" @@ -235,8 +259,7 @@ Gaming Mode display settings${pending_label}: Monitor : ${monitor_label} Resolution : ${width:-?}x${height:-?} Refresh rate : ${refresh:-} Hz - GPU (NVIDIA) : ${vk_adapter:-} - GPU (AMD) : ${dri_prime:-} + GPU mode : ${gpu_mode} Config file : ${CONF} EOF @@ -365,6 +388,23 @@ choose_refresh_rate() { pending_set CUSTOM_REFRESH_RATES "$rate" } +# Keys this menu owns. Cleared at the start of every selection so switching +# between modes never leaves stale flags behind. +GPU_MODE_KEYS=( + VULKAN_ADAPTER GBM_BACKEND DRI_PRIME + __NV_PRIME_RENDER_OFFLOAD __VK_LAYER_NV_optimus __GLX_VENDOR_LIBRARY_NAME + MESA_VK_DEVICE_SELECT MESA_VK_DEVICE_SELECT_FORCE_DEFAULT_DEVICE +) + +# Convert a PCI slot like "01:00.0" or "0000:c3:00.0" to the DRI_PRIME tag +# format ("pci-0000_01_00_0"). Used by AMD/Intel paths. +_pci_slot_to_dri_tag() { + local slot="$1" + local tag="pci-$(echo "$slot" | sed 's/[:.]/_/g')" + [[ "$tag" != pci-0000* ]] && tag="pci-0000_${tag#pci-}" + echo "$tag" +} + choose_gpu() { local choice mapfile -t gpus < <(list_gpus) @@ -372,41 +412,105 @@ choose_gpu() { gum style --foreground 196 "No GPUs detected via lspci" return 1 fi + + # Hybrid detection. Two patterns: + # - hybrid-nvidia: NVIDIA dGPU + (AMD or Intel) iGPU. eDP usually on iGPU. + # Games offload to NVIDIA via __NV_PRIME_RENDER_OFFLOAD env vars. + # - hybrid-amd: 2+ non-NVIDIA GPUs (AMD+AMD or AMD+Intel). User picks which + # is the dGPU; games offload via DRI_PRIME + MESA_VK_DEVICE_SELECT. + local has_nvidia=false has_igpu=false + local non_nvidia_count=0 + local entry slot vendor_dev kind label + for entry in "${gpus[@]}"; do + IFS='|' read -r slot vendor_dev kind label <<<"$entry" + case "$kind" in + nvidia) has_nvidia=true ;; + amd|intel|other) + has_igpu=true + non_nvidia_count=$((non_nvidia_count + 1)) + ;; + esac + done + local hybrid_nvidia_available=false hybrid_amd_available=false + $has_nvidia && $has_igpu && hybrid_nvidia_available=true + (( non_nvidia_count >= 2 )) && hybrid_amd_available=true + local -a labels=() + $hybrid_nvidia_available && labels+=("[hybrid-nvidia] NVIDIA render-offload via iGPU — for hybrid laptops (eDP on iGPU)") + $hybrid_amd_available && labels+=("[hybrid-amd] AMD/Intel render-offload — pick which GPU to offload games to") for entry in "${gpus[@]}"; do IFS='|' read -r slot vendor_dev kind label <<<"$entry" labels+=("[$kind] $label ($vendor_dev @ $slot)") done labels+=("(clear GPU override — let system decide)") + choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select GPU for Gaming Mode") [[ -z "$choice" ]] && return 0 - if [[ "$choice" == "(clear"* ]]; then - pending_unset VULKAN_ADAPTER - pending_unset DRI_PRIME - pending_unset GBM_BACKEND - return 0 - fi - local idx=0 selected="" - for label in "${labels[@]}"; do - if [[ "$label" == "$choice" ]]; then - selected="${gpus[$idx]}" + + # Wipe every GPU-mode key first; each branch sets only what it needs. + local k + for k in "${GPU_MODE_KEYS[@]}"; do pending_unset "$k"; done + + case "$choice" in + "[hybrid-nvidia]"*) + pending_set __NV_PRIME_RENDER_OFFLOAD 1 + pending_set __VK_LAYER_NV_optimus NVIDIA_only + pending_set __GLX_VENDOR_LIBRARY_NAME nvidia + return 0 + ;; + "[hybrid-amd]"*) + # Sub-menu — pick which AMD/Intel GPU is the render target (dGPU). + local -a t_labels=() t_entries=() + for entry in "${gpus[@]}"; do + IFS='|' read -r slot vendor_dev kind label <<<"$entry" + case "$kind" in + amd|intel|other) + t_labels+=("[$kind] $label ($vendor_dev @ $slot)") + t_entries+=("$entry") + ;; + esac + done + local t_choice t_entry="" + t_choice=$(printf '%s\n' "${t_labels[@]}" | gchoose --header "Pick dGPU (render target) — usually the discrete one") + [[ -z "$t_choice" ]] && return 0 + local i + for ((i=0; i<${#t_labels[@]}; i++)); do + if [[ "${t_labels[$i]}" == "$t_choice" ]]; then + t_entry="${t_entries[$i]}" + break + fi + done + [[ -z "$t_entry" ]] && return 0 + IFS='|' read -r slot vendor_dev kind label <<<"$t_entry" + pending_set DRI_PRIME "$(_pci_slot_to_dri_tag "$slot")" + pending_set MESA_VK_DEVICE_SELECT "$vendor_dev" + pending_set MESA_VK_DEVICE_SELECT_FORCE_DEFAULT_DEVICE 1 + return 0 + ;; + "(clear"*) + return 0 + ;; + esac + + # Direct GPU selection — match the chosen label back to its gpus[] entry. + local selected="" + for entry in "${gpus[@]}"; do + IFS='|' read -r slot vendor_dev kind label <<<"$entry" + if [[ "[$kind] $label ($vendor_dev @ $slot)" == "$choice" ]]; then + selected="$entry" break fi - idx=$((idx + 1)) done + [[ -z "$selected" ]] && return 0 + IFS='|' read -r slot vendor_dev kind label <<<"$selected" case "$kind" in nvidia) pending_set VULKAN_ADAPTER "$vendor_dev" - pending_set GBM_BACKEND "nvidia-drm" - pending_unset DRI_PRIME + pending_set GBM_BACKEND nvidia-drm ;; amd|intel|other) - local dri_tag="pci-$(echo "$slot" | sed 's/[:.]/_/g')" - [[ "$dri_tag" != pci-0000* ]] && dri_tag="pci-0000_${dri_tag#pci-}" - pending_set DRI_PRIME "$dri_tag" - pending_unset VULKAN_ADAPTER - pending_unset GBM_BACKEND + pending_set DRI_PRIME "$(_pci_slot_to_dri_tag "$slot")" ;; esac } diff --git a/deckshift.sh b/deckshift.sh index 7e4cbf4..1f1da4d 100755 --- a/deckshift.sh +++ b/deckshift.sh @@ -1349,11 +1349,10 @@ setup_session_switching() { local user_home user_home=$(eval echo "~$current_user") - local monitor_width=1920 - local monitor_height=1080 - local monitor_refresh=60 - local monitor_output="" - + # GPU detection only — the installer no longer chooses a monitor, resolution + # or refresh rate. Those are user choices, made later via Walker → "DeckShift + # Settings". This avoids stale OUTPUT_CONNECTOR values when displays are + # unplugged and lets the user pick whatever fits their setup. local -a dgpu_monitors=() local dgpu_card="" local dgpu_type="" @@ -1370,10 +1369,9 @@ setup_session_switching() { NEEDS_REBOOT=1 return 1 fi - # No dGPU - check for APU + # No dGPU - check for AMD APU as a viable Gaming Mode GPU local apu_card="" - local apu_monitors=() - local card_name driver_link driver conn_dir conn_name status resolution mode_file + local card_name driver_link driver for card_path in /sys/class/drm/card[0-9]*; do card_name=$(basename "$card_path") @@ -1384,39 +1382,19 @@ setup_session_switching() { if [[ "$driver" == "amdgpu" ]] && is_amd_igpu_card "$card_path"; then apu_card="$card_name" - # Find monitors connected to APU - for connector in "$card_path"/"$card_name"-*/status; do - [[ -f "$connector" ]] || continue - conn_dir=$(dirname "$connector") - conn_name=$(basename "$conn_dir") - conn_name=${conn_name#card*-} - [[ "$conn_name" == Writeback* ]] && continue - status=$(cat "$connector" 2>/dev/null) - if [[ "$status" == "connected" ]]; then - resolution="" - mode_file="$conn_dir/modes" - [[ -f "$mode_file" ]] && [[ -s "$mode_file" ]] && resolution=$(head -1 "$mode_file" 2>/dev/null) - apu_monitors+=("$conn_name|$resolution") - fi - done break fi done - if [[ -n "$apu_card" && ${#apu_monitors[@]} -gt 0 ]]; then + if [[ -n "$apu_card" ]]; then echo "" info "No discrete GPU found, but detected AMD APU ($apu_card)" echo "" - echo " This system has an AMD APU which can run Gaming Mode." - echo " Detected monitors: ${#apu_monitors[@]}" - echo "" read -p " Set up Gaming Mode for APU? [Y/n]: " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then - # Use APU as the gaming GPU dgpu_card="$apu_card" dgpu_type="AMD APU" - dgpu_monitors=("${apu_monitors[@]}") info "Configuring Gaming Mode for AMD APU" else info "Skipping APU Gaming Mode setup" @@ -1424,60 +1402,14 @@ setup_session_switching() { fi else err "No discrete GPU (dGPU) or AMD APU found!" - echo " Gaming mode requires a supported GPU with a connected display." + echo " Gaming mode requires a supported GPU." return 1 fi fi info "Found $dgpu_type on $dgpu_card" - - if [[ ${#dgpu_monitors[@]} -eq 0 ]]; then - err "No monitors connected to dGPU!" - echo "" - echo " Gaming mode requires a monitor connected to the discrete GPU." - echo " Please connect an external monitor to your dGPU port (HDMI/DP/USB-C)" - echo " and re-run this installer." - echo "" - return 1 - fi - - if [[ ${#dgpu_monitors[@]} -eq 1 ]]; then - local entry="${dgpu_monitors[0]}" - monitor_output="${entry%%|*}" - local res="${entry##*|}" - if [[ -n "$res" ]]; then - monitor_width="${res%%x*}" - monitor_height="${res##*x}" - monitor_height="${monitor_height%%@*}" - [[ "$res" == *@* ]] && monitor_refresh="${res##*@}" && monitor_refresh="${monitor_refresh%%.*}" - fi - else - echo "" - echo " Multiple monitors connected to $dgpu_type:" - local i=1 - for entry in "${dgpu_monitors[@]}"; do - local name="${entry%%|*}" - local res="${entry##*|}" - echo " $i) $name ${res:+($res)}" - ((i++)) - done - echo "" - read -p "Select monitor for Gaming Mode [1-${#dgpu_monitors[@]}]: " selection - if [[ ! "$selection" =~ ^[0-9]+$ ]] || ((selection < 1 || selection > ${#dgpu_monitors[@]})); then - selection=1 - fi - local entry="${dgpu_monitors[$((selection-1))]}" - monitor_output="${entry%%|*}" - local res="${entry##*|}" - if [[ -n "$res" ]]; then - monitor_width="${res%%x*}" - monitor_height="${res##*x}" - monitor_height="${monitor_height%%@*}" - [[ "$res" == *@* ]] && monitor_refresh="${res##*@}" && monitor_refresh="${monitor_refresh%%.*}" - fi - fi - - info "Selected dGPU display: ${monitor_output} (${monitor_width}x${monitor_height}@${monitor_refresh}Hz)" + info "Display selection (monitor / resolution / refresh) is left to the user." + info "After install, launch Walker → 'DeckShift Settings' to configure." info "Checking for old custom session files to clean up..." @@ -1923,83 +1855,72 @@ UDISKS_POLKIT # Gamescope Session Configuration # - # This config file tells gamescope-session-plus (from ChimeraOS) how to - # set up the gaming display. It includes: - # - Resolution and refresh rate (auto-detected from your monitor) - # - Which display output to use (e.g. HDMI-1, DP-2) - # - GPU-specific settings (NVIDIA vs AMD have different requirements) + # The installer writes only GPU-specific and static keys here. Display keys + # (SCREEN_WIDTH, SCREEN_HEIGHT, CUSTOM_REFRESH_RATES, OUTPUT_CONNECTOR) are + # NOT written by the installer — they are owned by the user and managed via + # the settings TUI (Walker → "DeckShift Settings"). This keeps existing user + # selections intact across re-runs and avoids preselecting values that may + # not match the user's setup. # - # NVIDIA gets: GBM_BACKEND=nvidia-drm, VULKAN_ADAPTER pointing to the GPU - # AMD gets: ADAPTIVE_SYNC=1 (FreeSync), ENABLE_GAMESCOPE_HDR=1 (HDR support) - # - # NVIDIA is capped at 2560x1440 due to Gamescope limitations with NVIDIA GPUs. - info "Creating gamescope-session-plus configuration..." + # NVIDIA gets: VULKAN_ADAPTER + GBM_BACKEND=nvidia-drm + # AMD gets: ADAPTIVE_SYNC=1 + ENABLE_GAMESCOPE_HDR=1 + # Intel: no extra display flags (adaptive sync / HDR unreliable on Intel) + info "Updating gamescope-session-plus configuration..." local env_dir="${user_home}/.config/environment.d" local gamescope_conf="${env_dir}/gamescope-session-plus.conf" mkdir -p "$env_dir" + touch "$gamescope_conf" - local output_connector="" - [[ -n "$monitor_output" ]] && output_connector="OUTPUT_CONNECTOR=$monitor_output" - - local nvidia_device_id="" - if [[ "$dgpu_type" == "NVIDIA" ]]; then - nvidia_device_id=$(/usr/bin/lspci -nn | grep -i nvidia | grep -oP '\[10de:\K[0-9a-fA-F]+' | head -1) - if [ "$monitor_width" -gt 2560 ]; then - monitor_width=2560 + # Per-key updater — replaces in place if present, appends if missing. + # Same shape as the TUI's flush_pending so the two never fight. + set_conf_key() { + local key="$1" value="$2" + if grep -qE "^${key}=" "$gamescope_conf"; then + sed -i "s|^${key}=.*|${key}=${value}|" "$gamescope_conf" + else + echo "${key}=${value}" >> "$gamescope_conf" fi - if [ "$monitor_height" -gt 1440 ]; then - monitor_height=1440 - fi - fi + } + unset_conf_key() { + sed -i "/^$1=/d" "$gamescope_conf" + } + # Static keys — apply on every install, every GPU. + set_conf_key STEAM_ALLOW_DRIVE_UNMOUNT 1 + set_conf_key FCITX_NO_WAYLAND_DIAGNOSE 1 + set_conf_key SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS 0 + + # GPU-specific keys — set the right ones, clear stale ones from a prior + # install on a different GPU (e.g. user swapped NVIDIA → AMD). case "$dgpu_type" in "NVIDIA") - local vulkan_adapter="" - [[ -n "$nvidia_device_id" ]] && vulkan_adapter="VULKAN_ADAPTER=10de:${nvidia_device_id}" - cat > "$gamescope_conf" << GAMESCOPE_CONF -SCREEN_WIDTH=${monitor_width} -SCREEN_HEIGHT=${monitor_height} -CUSTOM_REFRESH_RATES=${monitor_refresh} -${output_connector} -${vulkan_adapter} -GBM_BACKEND=nvidia-drm -STEAM_ALLOW_DRIVE_UNMOUNT=1 -FCITX_NO_WAYLAND_DIAGNOSE=1 -SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0 -GAMESCOPE_CONF + local nvidia_device_id + nvidia_device_id=$(/usr/bin/lspci -nn | grep -i nvidia | grep -oP '\[10de:\K[0-9a-fA-F]+' | head -1) + [[ -n "$nvidia_device_id" ]] && set_conf_key VULKAN_ADAPTER "10de:${nvidia_device_id}" + set_conf_key GBM_BACKEND nvidia-drm + unset_conf_key ADAPTIVE_SYNC + unset_conf_key ENABLE_GAMESCOPE_HDR + unset_conf_key DRI_PRIME ;; "Intel") - # Intel doesn't get ADAPTIVE_SYNC / HDR by default — most Intel iGPUs - # don't support adaptive sync, and gamescope HDR on Intel is unreliable. - # Users with Intel Arc + a VRR display can enable both via the settings TUI. - cat > "$gamescope_conf" << GAMESCOPE_CONF -SCREEN_WIDTH=${monitor_width} -SCREEN_HEIGHT=${monitor_height} -CUSTOM_REFRESH_RATES=${monitor_refresh} -${output_connector} -STEAM_ALLOW_DRIVE_UNMOUNT=1 -FCITX_NO_WAYLAND_DIAGNOSE=1 -SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0 -GAMESCOPE_CONF + unset_conf_key VULKAN_ADAPTER + unset_conf_key GBM_BACKEND + unset_conf_key ADAPTIVE_SYNC + unset_conf_key ENABLE_GAMESCOPE_HDR ;; *) # AMD dGPU and AMD APU — adaptive sync (FreeSync) and HDR are well supported. - cat > "$gamescope_conf" << GAMESCOPE_CONF -SCREEN_WIDTH=${monitor_width} -SCREEN_HEIGHT=${monitor_height} -CUSTOM_REFRESH_RATES=${monitor_refresh} -${output_connector} -ADAPTIVE_SYNC=1 -ENABLE_GAMESCOPE_HDR=1 -STEAM_ALLOW_DRIVE_UNMOUNT=1 -FCITX_NO_WAYLAND_DIAGNOSE=1 -SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0 -GAMESCOPE_CONF + set_conf_key ADAPTIVE_SYNC 1 + set_conf_key ENABLE_GAMESCOPE_HDR 1 + unset_conf_key VULKAN_ADAPTER + unset_conf_key GBM_BACKEND ;; esac - info "Created $gamescope_conf" + unset -f set_conf_key unset_conf_key + + info "Updated $gamescope_conf (display keys left for the TUI)" # NVIDIA Gamescope Wrapper # @@ -2319,10 +2240,11 @@ sleep 2 sudo -n chvt 2 2>/dev/null || true sleep 0.5 -sudo -n systemctl stop sddm 2>/dev/null || true -sleep 1 -sudo -n systemctl start sddm & -disown +# Atomic restart — stop+start (with stop and start as separate sudo calls) +# was unreliable: stop/start aren't NOPASSWD-allowed individually (only +# `restart` is), and the disowned `start` could be killed by session teardown +# before SDDM actually came back up, leaving the user on a black screen. +sudo -n systemctl restart sddm exit 0 SWITCH_DESKTOP