#!/bin/bash # ========================================================== # ProxMenux - GPU Passthrough to Virtual Machine (VM) # ========================================================== # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : GPL-3.0 # Version : 1.0 # Last Updated: 03/04/2026 # ========================================================== # Description: # Automates full GPU passthrough (VFIO) from Proxmox host to a VM. # Supports Intel iGPU, AMD and NVIDIA GPUs. # # Features: # - IOMMU detection and activation offer # - Multi-GPU selection menu # - IOMMU group analysis (all group devices passed together) # - Single-GPU warning (host loses physical video output) # - Switch mode: detects GPU used in LXC or another VM # - AMD ROM dump via sysfs # - Idempotent host config (modules, vfio.conf, blacklist) # - VM config: hostpci entries, NVIDIA KVM hiding # ========================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)" LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts" LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$LOCAL_SCRIPTS/utils.sh" if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL" UTILS_FILE="$LOCAL_SCRIPTS/utils.sh" elif [[ ! -f "$UTILS_FILE" ]]; then UTILS_FILE="$BASE_DIR/utils.sh" fi LOG_FILE="/tmp/add_gpu_vm.log" screen_capture="/tmp/proxmenux_add_gpu_vm_screen_$$.txt" if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" fi if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" fi if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh" ]]; then source "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh" elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh" ]]; then source "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh" fi load_language initialize_cache # ========================================================== # Global state # ========================================================== declare -a ALL_GPU_PCIS=() # "0000:01:00.0" declare -a ALL_GPU_TYPES=() # intel / amd / nvidia declare -a ALL_GPU_NAMES=() # human-readable name declare -a ALL_GPU_DRIVERS=() # current kernel driver SELECTED_GPU="" # intel / amd / nvidia SELECTED_GPU_PCI="" # 0000:01:00.0 SELECTED_GPU_NAME="" declare -a IOMMU_DEVICES=() # all PCI addrs in IOMMU group (endpoint devices) declare -a IOMMU_VFIO_IDS=() # vendor:device for vfio-pci ids= declare -a EXTRA_AUDIO_DEVICES=() # sibling audio function(s), typically *.1 IOMMU_GROUP="" IOMMU_PENDING_REBOOT=false SELECTED_VMID="" VM_NAME="" GPU_COUNT=0 SINGLE_GPU_SYSTEM=false SWITCH_FROM_LXC=false SWITCH_LXC_LIST="" SWITCH_FROM_VM=false SWITCH_VM_SRC="" SWITCH_VM_ACTION="" # keep_gpu_disable_onboot | remove_gpu_keep_onboot TARGET_VM_ALREADY_HAS_GPU=false VM_SWITCH_ALREADY_VFIO=false PREFLIGHT_HOST_REBOOT_REQUIRED=true AMD_ROM_FILE="" HOST_CONFIG_CHANGED=false # set to true whenever host VFIO config is actually written PRESELECT_VMID="" WIZARD_CALL=false GPU_WIZARD_RESULT_FILE="" declare -a LXC_AFFECTED_CTIDS=() declare -a LXC_AFFECTED_NAMES=() declare -a LXC_AFFECTED_RUNNING=() # 1 or 0 declare -a LXC_AFFECTED_ONBOOT=() # 1 or 0 LXC_SWITCH_ACTION="" # keep_gpu_disable_onboot | remove_gpu_keep_onboot # ========================================================== # Helpers # ========================================================== _get_pci_driver() { local pci_full="$1" local driver_link="/sys/bus/pci/devices/${pci_full}/driver" if [[ -L "$driver_link" ]]; then basename "$(readlink "$driver_link")" else echo "none" fi } _add_line_if_missing() { local line="$1" local file="$2" touch "$file" if ! grep -qF "$line" "$file"; then echo "$line" >> "$file" HOST_CONFIG_CHANGED=true fi } _append_unique() { local needle="$1" shift local item for item in "$@"; do [[ "$item" == "$needle" ]] && return 1 done return 0 } _vm_is_running() { local vmid="$1" qm status "$vmid" 2>/dev/null | grep -q "status: running" } _vm_onboot_enabled() { local vmid="$1" qm config "$vmid" 2>/dev/null | grep -qE "^onboot:\s*1" } _ct_is_running() { local ctid="$1" pct status "$ctid" 2>/dev/null | grep -q "status: running" } _ct_onboot_enabled() { local ctid="$1" pct config "$ctid" 2>/dev/null | grep -qE "^onboot:\s*1" } _lxc_conf_uses_selected_gpu() { local conf="$1" case "$SELECTED_GPU" in nvidia) grep -qE "dev[0-9]+:.*(/dev/nvidia|/dev/nvidia-caps)" "$conf" 2>/dev/null ;; amd) grep -qE "dev[0-9]+:.*(/dev/dri|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" "$conf" 2>/dev/null ;; intel) grep -qE "dev[0-9]+:.*(/dev/dri)|lxc\.mount\.entry:.*dev/dri" "$conf" 2>/dev/null ;; *) grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" "$conf" 2>/dev/null ;; esac } _lxc_switch_action_label() { case "$LXC_SWITCH_ACTION" in keep_gpu_disable_onboot) echo "$(translate 'Keep GPU in LXC config + disable Start on boot')" ;; remove_gpu_keep_onboot) echo "$(translate 'Remove GPU from LXC config + keep Start on boot unchanged')" ;; *) echo "$(translate 'No specific LXC action selected')" ;; esac } _vm_switch_action_label() { case "$SWITCH_VM_ACTION" in keep_gpu_disable_onboot) echo "$(translate 'Keep GPU in source VM config + disable Start on boot if enabled')" ;; remove_gpu_keep_onboot) echo "$(translate 'Remove GPU from source VM config + keep Start on boot unchanged')" ;; *) echo "$(translate 'No specific VM action selected')" ;; esac } _set_wizard_result() { local result="$1" [[ -z "${GPU_WIZARD_RESULT_FILE:-}" ]] && return 0 printf '%s\n' "$result" >"$GPU_WIZARD_RESULT_FILE" 2>/dev/null || true } _file_has_exact_line() { local line="$1" local file="$2" [[ -f "$file" ]] || return 1 grep -qFx "$line" "$file" } evaluate_host_reboot_requirement() { if [[ "$IOMMU_PENDING_REBOOT" == "true" ]]; then PREFLIGHT_HOST_REBOOT_REQUIRED=true return 0 fi # Fast path for VM-to-VM reassignment where GPU is already bound to vfio if [[ "$VM_SWITCH_ALREADY_VFIO" == "true" ]]; then PREFLIGHT_HOST_REBOOT_REQUIRED=false return 0 fi local needs_change=false local current_driver current_driver=$(_get_pci_driver "$SELECTED_GPU_PCI") [[ "$current_driver" != "vfio-pci" ]] && needs_change=true # /etc/modules expected lines local modules_file="/etc/modules" local modules=("vfio" "vfio_iommu_type1" "vfio_pci") local kernel_major kernel_minor kernel_major=$(uname -r | cut -d. -f1) kernel_minor=$(uname -r | cut -d. -f2) if (( kernel_major < 6 || ( kernel_major == 6 && kernel_minor < 2 ) )); then modules+=("vfio_virqfd") fi local mod for mod in "${modules[@]}"; do _file_has_exact_line "$mod" "$modules_file" || needs_change=true done # vfio-pci ids local vfio_conf="/etc/modprobe.d/vfio.conf" local ids_line ids_part ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1) if [[ -z "$ids_line" ]]; then needs_change=true else [[ "$ids_line" == *"disable_vga=1"* ]] || needs_change=true ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//') local existing_ids=() IFS=',' read -ra existing_ids <<< "$ids_part" local required found existing for required in "${IOMMU_VFIO_IDS[@]}"; do found=false for existing in "${existing_ids[@]}"; do [[ "$existing" == "$required" ]] && found=true && break done $found || needs_change=true done fi # modprobe options files _file_has_exact_line "options vfio_iommu_type1 allow_unsafe_interrupts=1" \ /etc/modprobe.d/iommu_unsafe_interrupts.conf || needs_change=true _file_has_exact_line "options kvm ignore_msrs=1" \ /etc/modprobe.d/kvm.conf || needs_change=true # AMD softdep if [[ "$SELECTED_GPU" == "amd" ]]; then _file_has_exact_line "softdep radeon pre: vfio-pci" "$vfio_conf" || needs_change=true _file_has_exact_line "softdep amdgpu pre: vfio-pci" "$vfio_conf" || needs_change=true _file_has_exact_line "softdep snd_hda_intel pre: vfio-pci" "$vfio_conf" || needs_change=true fi # host driver blacklist local blacklist_file="/etc/modprobe.d/blacklist.conf" case "$SELECTED_GPU" in nvidia) _file_has_exact_line "blacklist nouveau" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist nvidia" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist nvidia_drm" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist nvidia_modeset" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist nvidia_uvm" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist nvidiafb" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist lbm-nouveau" "$blacklist_file" || needs_change=true _file_has_exact_line "options nouveau modeset=0" "$blacklist_file" || needs_change=true [[ -f /etc/modules-load.d/nvidia-vfio.conf ]] && needs_change=true grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null && needs_change=true local svc for svc in nvidia-persistenced.service nvidia-persistenced nvidia-powerd.service nvidia-fabricmanager.service; do if systemctl is-active --quiet "$svc" 2>/dev/null || systemctl is-enabled --quiet "$svc" 2>/dev/null; then needs_change=true fi done ;; amd) _file_has_exact_line "blacklist radeon" "$blacklist_file" || needs_change=true _file_has_exact_line "blacklist amdgpu" "$blacklist_file" || needs_change=true ;; intel) _file_has_exact_line "blacklist i915" "$blacklist_file" || needs_change=true ;; esac if [[ "$needs_change" == "true" ]]; then PREFLIGHT_HOST_REBOOT_REQUIRED=true else PREFLIGHT_HOST_REBOOT_REQUIRED=false fi } parse_cli_args() { while [[ $# -gt 0 ]]; do case "$1" in --vmid) if [[ -n "${2:-}" ]]; then PRESELECT_VMID="$2" shift 2 else shift fi ;; --wizard) WIZARD_CALL=true shift ;; --result-file) if [[ -n "${2:-}" ]]; then GPU_WIZARD_RESULT_FILE="$2" shift 2 else shift fi ;; *) shift ;; esac done } _get_vm_run_title() { if [[ "$SWITCH_FROM_LXC" == "true" && "$SWITCH_FROM_VM" == "true" ]]; then echo "$(translate 'GPU Passthrough to VM (reassign from LXC and another VM)')" elif [[ "$SWITCH_FROM_LXC" == "true" ]]; then echo "$(translate 'GPU Passthrough to VM (from LXC)')" elif [[ "$SWITCH_FROM_VM" == "true" ]]; then echo "$(translate 'GPU Passthrough to VM (reassign from another VM)')" else echo "$(translate 'GPU Passthrough to VM')" fi } _is_pci_slot_assigned_to_vm() { if declare -F _pci_slot_assigned_to_vm >/dev/null 2>&1; then _pci_slot_assigned_to_vm "$1" "$2" return $? fi local pci_full="$1" local vmid="$2" local slot_base slot_base="${pci_full#0000:}" slot_base="${slot_base%.*}" # 01:00 qm config "$vmid" 2>/dev/null \ | grep -qE "^hostpci[0-9]+:.*(0000:)?${slot_base}(\\.[0-7])?([,[:space:]]|$)" } # Match a specific PCI function when possible. # For function .0, also accept slot-only entries (e.g. 01:00) as equivalent. _is_pci_function_assigned_to_vm() { if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then _pci_function_assigned_to_vm "$1" "$2" return $? fi local pci_full="$1" local vmid="$2" local bdf slot func pattern bdf="${pci_full#0000:}" # 01:00.0 slot="${bdf%.*}" # 01:00 func="${bdf##*.}" # 0 if [[ "$func" == "0" ]]; then pattern="^hostpci[0-9]+:.*(0000:)?(${bdf}|${slot})([,:[:space:]]|$)" else pattern="^hostpci[0-9]+:.*(0000:)?${bdf}([,[:space:]]|$)" fi qm config "$vmid" 2>/dev/null | grep -qE "$pattern" } ensure_selected_gpu_not_already_in_target_vm() { while _is_pci_slot_assigned_to_vm "$SELECTED_GPU_PCI" "$SELECTED_VMID"; do local current_driver current_driver=$(_get_pci_driver "$SELECTED_GPU_PCI") # GPU is already assigned to this VM, but host is not in VFIO mode. # Continue so the script can re-activate VM passthrough on the host. if [[ "$current_driver" != "vfio-pci" ]]; then TARGET_VM_ALREADY_HAS_GPU=true local popup_title popup_title=$(_get_vm_run_title) dialog --backtitle "ProxMenux" \ --title "${popup_title}" \ --msgbox "\n$(translate 'The selected GPU is already assigned to this VM, but the host is not currently using vfio-pci for this device.')\n\n$(translate 'Current driver'): ${current_driver}\n\n$(translate 'The script will continue to restore VM passthrough mode on the host and reuse existing hostpci entries.')" \ 13 78 return 0 fi # Single GPU system: nothing else to choose if [[ $GPU_COUNT -le 1 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate 'GPU Already Added')" \ --msgbox "\n$(translate 'The selected GPU is already assigned to this VM.')\n\n$(translate 'No changes are required.')" \ 9 66 exit 0 fi # Build menu with GPUs that are NOT already assigned to this VM local menu_items=() local i available available=0 for i in "${!ALL_GPU_PCIS[@]}"; do local pci label pci="${ALL_GPU_PCIS[$i]}" _is_pci_slot_assigned_to_vm "$pci" "$SELECTED_VMID" && continue label="${ALL_GPU_NAMES[$i]} [${ALL_GPU_DRIVERS[$i]}] — ${pci}" menu_items+=("$i" "$label") available=$((available + 1)) done if [[ $available -eq 0 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate 'All GPUs Already Assigned')" \ --msgbox "\n$(translate 'All detected GPUs are already assigned to this VM.')\n\n$(translate 'No additional GPU can be added.')" \ 10 70 exit 0 fi local choice local -a clear_opt=() [[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear) choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \ --title "$(translate 'GPU Already Assigned to This VM')" \ --menu "\n$(translate 'The selected GPU is already present in this VM. Select another GPU to continue:')" \ 18 82 10 \ "${menu_items[@]}" \ 2>&1 >/dev/tty) || exit 0 SELECTED_GPU="${ALL_GPU_TYPES[$choice]}" SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}" SELECTED_GPU_NAME="${ALL_GPU_NAMES[$choice]}" done } # ========================================================== # Phase 1 — Step 1: Detect host GPUs # ========================================================== detect_host_gpus() { while IFS= read -r line; do local pci_short pci_full name type driver pci_info pci_short=$(echo "$line" | awk '{print $1}') pci_full="0000:${pci_short}" # Prefer full vendor/model descriptor for clearer menus. pci_info=$(lspci -nn -s "${pci_short}" 2>/dev/null | sed 's/^[^ ]* //') name="${pci_info#*: }" [[ "$name" == "$pci_info" ]] && name="$pci_info" name=$(echo "$name" | sed -E 's/ \(rev [^)]+\)$//' | cut -c1-72) [[ -z "$name" ]] && name="$(translate 'Unknown GPU')" if echo "$line" | grep -qi "Intel"; then type="intel" elif echo "$line" | grep -qiE "AMD|Advanced Micro|Radeon"; then type="amd" elif echo "$line" | grep -qi "NVIDIA"; then type="nvidia" else type="other" fi driver=$(_get_pci_driver "$pci_full") ALL_GPU_PCIS+=("$pci_full") ALL_GPU_TYPES+=("$type") ALL_GPU_NAMES+=("$name") ALL_GPU_DRIVERS+=("$driver") done < <(lspci -nn | grep -iE "VGA compatible controller|3D controller|Display controller" \ | grep -iv "Ethernet\|Network\|Audio") GPU_COUNT=${#ALL_GPU_PCIS[@]} if [[ $GPU_COUNT -eq 0 ]]; then _set_wizard_result "no_gpu" dialog --backtitle "ProxMenux" \ --title "$(translate 'No GPU Detected')" \ --msgbox "\n$(translate 'No compatible GPU was detected on this host.')" 8 60 exit 0 fi [[ $GPU_COUNT -eq 1 ]] && SINGLE_GPU_SYSTEM=true } # ========================================================== # Phase 1 — Step 2: Check IOMMU, offer to enable it # ========================================================== check_iommu_enabled() { if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then return 0 fi local configured_next_boot=false if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \ grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then configured_next_boot=true fi if [[ "$configured_next_boot" == "true" ]]; then IOMMU_PENDING_REBOOT=true HOST_CONFIG_CHANGED=true dialog --backtitle "ProxMenux" \ --title "$(translate 'IOMMU Pending Reboot')" \ --msgbox "\n$(translate 'IOMMU is already configured for next boot, but it is not active yet.')\n\n$(translate 'GPU passthrough configuration will continue now and will become effective after host reboot.')" \ 11 78 return 0 fi local msg msg="\n$(translate 'IOMMU is not active on this system.')\n\n" msg+="$(translate 'GPU passthrough to VMs requires IOMMU to be enabled in the kernel.')\n\n" msg+="$(translate 'Do you want to enable IOMMU now?')\n\n" msg+="$(translate 'Note: A system reboot will be required after enabling IOMMU.')\n" msg+="$(translate 'Configuration will continue now and be effective after reboot.')" dialog --backtitle "ProxMenux" \ --title "$(translate 'IOMMU Required')" \ --yesno "$msg" 15 72 local response=$? [[ "$WIZARD_CALL" != "true" ]] && clear if [[ $response -eq 0 ]]; then [[ "$WIZARD_CALL" != "true" ]] && show_proxmenux_logo msg_title "$(translate 'Enabling IOMMU')" if ! _enable_iommu_cmdline; then echo msg_error "$(translate 'Failed to configure IOMMU automatically.')" echo msg_success "$(translate 'Press Enter to continue...')" read -r exit 0 fi IOMMU_PENDING_REBOOT=true HOST_CONFIG_CHANGED=true echo msg_success "$(translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.')" echo if [[ "$WIZARD_CALL" != "true" ]]; then msg_success "$(translate 'Press Enter to continue...')" read -r fi return 0 fi exit 0 } _enable_iommu_cmdline() { local cpu_vendor cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}') local iommu_param if [[ "$cpu_vendor" == "GenuineIntel" ]]; then iommu_param="intel_iommu=on" msg_info "$(translate 'Intel CPU detected')" elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then iommu_param="amd_iommu=on" msg_info "$(translate 'AMD CPU detected')" else msg_error "$(translate 'Unknown CPU vendor. Cannot determine IOMMU parameter.')" return 1 fi local cmdline_file="/etc/kernel/cmdline" local grub_file="/etc/default/grub" if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then # systemd-boot / ZFS if ! grep -q "$iommu_param" "$cmdline_file"; then cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)" sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file" proxmox-boot-tool refresh >/dev/null 2>&1 || true msg_ok "$(translate 'IOMMU parameters added to /etc/kernel/cmdline')" else msg_ok "$(translate 'IOMMU already configured in /etc/kernel/cmdline')" fi elif [[ -f "$grub_file" ]]; then # GRUB if ! grep -q "$iommu_param" "$grub_file"; then cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)" sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file" update-grub >/dev/null 2>&1 || true msg_ok "$(translate 'IOMMU parameters added to GRUB')" else msg_ok "$(translate 'IOMMU already configured in GRUB')" fi else msg_error "$(translate 'Neither /etc/kernel/cmdline nor /etc/default/grub found.')" return 1 fi } # ========================================================== # Phase 1 — Step 3: GPU selection # ========================================================== select_gpu() { # Single GPU: auto-select, no menu needed if [[ $GPU_COUNT -eq 1 ]]; then SELECTED_GPU="${ALL_GPU_TYPES[0]}" SELECTED_GPU_PCI="${ALL_GPU_PCIS[0]}" SELECTED_GPU_NAME="${ALL_GPU_NAMES[0]}" return 0 fi # Multiple GPUs: show menu local menu_items=() local i for i in "${!ALL_GPU_PCIS[@]}"; do local label="${ALL_GPU_NAMES[$i]} [${ALL_GPU_DRIVERS[$i]}] — ${ALL_GPU_PCIS[$i]}" menu_items+=("$i" "$label") done local choice local -a clear_opt=() [[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear) choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \ --title "$(translate 'Select GPU for VM Passthrough')" \ --menu "\n$(translate 'Select the GPU to pass through to the VM:')" \ 18 82 10 \ "${menu_items[@]}" \ 2>&1 >/dev/tty) || exit 0 SELECTED_GPU="${ALL_GPU_TYPES[$choice]}" SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}" SELECTED_GPU_NAME="${ALL_GPU_NAMES[$choice]}" } # ========================================================== # Phase 1 — Step 4: Single-GPU warning # ========================================================== warn_single_gpu() { [[ "$SINGLE_GPU_SYSTEM" != "true" ]] && return 0 local msg msg="\n\Zb\Z1⚠ $(translate 'WARNING: This is a single GPU system')\Zn\n\n" msg+="$(translate 'When this GPU is passed through to a VM, the Proxmox host will lose all video output on the physical monitor.')\n\n" msg+="$(translate 'After the reboot, you will only be able to access the Proxmox host via:')\n" msg+=" • SSH\n" msg+=" • Proxmox Web UI (https)\n" msg+=" • Serial console\n\n" msg+="$(translate 'The VM guest will have exclusive access to the GPU.')\n\n" msg+="\Z1$(translate 'Important: some GPUs may still fail in passthrough and can affect host stability or overall performance depending on hardware/firmware quality.')\Zn\n\n" msg+="$(translate 'Make sure you have SSH or Web UI access before rebooting.')\n\n" msg+="$(translate 'Do you want to continue?')" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Single GPU Warning')" \ --yesno "$msg" 22 76 [[ $? -ne 0 ]] && exit 0 } # ========================================================== # Phase 1 — Step 4b: Hardware passthrough compatibility check # ========================================================== # Returns: apu | dedicated | unknown _detect_amd_gpu_subtype() { local name_lower name_lower=$(echo "$SELECTED_GPU_NAME" | tr '[:upper:]' '[:lower:]') # Known AMD APU / integrated GPU codenames (mobile and desktop) local apu_codenames=( "lucienne" "renoir" "cezanne" "van gogh" "barcelo" "rembrandt" "phoenix" "hawk point" "strix" "mendocino" "pollock" "raphael" "dragon range" "raven" "picasso" ) for codename in "${apu_codenames[@]}"; do echo "$name_lower" | grep -qi "$codename" && echo "apu" && return done # Markers of discrete / dedicated cards if echo "$name_lower" | grep -qiE "radeon rx|radeon pro|radeon vii|radeon r[0-9]|firepro|instinct|navi|polaris|vega|fiji|ellesmere|baffin"; then echo "dedicated" return fi echo "unknown" } # Returns: flr | bus | pm | none | unknown _check_pci_reset_method() { local pci_full="$1" local reset_file="/sys/bus/pci/devices/${pci_full}/reset_method" if [[ ! -f "$reset_file" ]]; then [[ -f "/sys/bus/pci/devices/${pci_full}/reset" ]] && echo "unknown" || echo "none" return fi local method method=$(cat "$reset_file" 2>/dev/null) echo "$method" | grep -q "flr" && echo "flr" && return echo "$method" | grep -q "pm" && echo "pm" && return echo "$method" | grep -q "bus" && echo "bus" && return echo "unknown" } # Returns: igpu | dedicated | unknown _detect_intel_gpu_subtype() { local name_lower pci_full name_lower=$(echo "$SELECTED_GPU_NAME" | tr '[:upper:]' '[:lower:]') pci_full="$SELECTED_GPU_PCI" # Typical integrated Intel GPU PCI function [[ "$pci_full" == "0000:00:02.0" ]] && echo "igpu" && return # Common integrated markers if echo "$name_lower" | grep -qiE "uhd|hd graphics|iris|xe graphics|integrated"; then echo "igpu" return fi # Common dedicated markers (Intel Arc family) if echo "$name_lower" | grep -qiE "arc|a3[0-9]{2}|a5[0-9]{2}|a7[0-9]{2}|a7[5-9]0|b5[0-9]{2}|b7[0-9]{2}"; then echo "dedicated" return fi echo "unknown" } check_intel_vm_compatibility() { local pci_full="$SELECTED_GPU_PCI" local gpu_subtype reset_method power_state vendor device viddid gpu_subtype=$(_detect_intel_gpu_subtype) reset_method=$(_check_pci_reset_method "$pci_full") power_state=$(cat "/sys/bus/pci/devices/${pci_full}/power_state" 2>/dev/null | tr -d '[:space:]') vendor=$(cat "/sys/bus/pci/devices/${pci_full}/vendor" 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]') device=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]') viddid="${vendor}:${device}" # ── BLOCKER: Known unsupported Intel Apollo Lake iGPU IDs ──────────── if [[ "$viddid" == "8086:5a84" || "$viddid" == "8086:5a85" ]]; then local msg msg="\n\Zb\Z1$(translate 'GPU Not Compatible with VM Passthrough')\Zn\n\n" msg+="$(translate 'The selected Intel GPU belongs to Apollo Lake generation and is blocked by policy for VM passthrough due to host instability risk.')\n\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n" msg+=" \ZbID: ${viddid}\Zn\n\n" msg+="$(translate 'This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.')\n\n" msg+="$(translate 'Recommended: use GPU with LXC workloads instead of VM passthrough on this hardware.')" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Blocked GPU ID')" \ --msgbox "$msg" 20 84 exit 0 fi # ── BLOCKER: Intel GPU in D3cold ────────────────────────────────────── if [[ "$power_state" == "D3cold" ]]; then local msg msg="\n\Zb\Z1$(translate 'GPU Not Available for Reliable VM Passthrough')\Zn\n\n" msg+="$(translate 'The selected Intel GPU is currently in power state D3cold'):\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n\n" msg+="$(translate 'Detected power state'): \Zb${power_state}\Zn\n\n" msg+="$(translate 'This state has a high probability of VM startup/reset failures.')\n\n" msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'High-Risk GPU Power State')" \ --msgbox "$msg" 20 80 exit 0 fi # ── BLOCKER: no usable reset method ────────────────────────────────── if [[ "$reset_method" == "none" ]]; then local msg msg="\n\Zb\Z1$(translate 'Incompatible Reset Capability for Intel GPU')\Zn\n\n" msg+="$(translate 'The selected Intel GPU does not expose a PCI reset interface'):\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n\n" msg+=" $(translate 'Detected reset method'): \Zb${reset_method}\Zn\n\n" msg+="$(translate 'Without a usable reset path, passthrough reliability is poor and VM')\n" msg+="$(translate 'startup/restart errors are likely.')\n\n" msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Reset Capability Blocked')" \ --msgbox "$msg" 20 80 exit 0 fi # ── BLOCKER: Intel dGPU without FLR ────────────────────────────────── if [[ "$gpu_subtype" == "dedicated" && "$reset_method" != "flr" ]]; then local msg msg="\n\Zb\Z1$(translate 'Incompatible Reset Capability for Intel dGPU')\Zn\n\n" msg+="$(translate 'An Intel dedicated GPU has been detected without FLR support'):\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n\n" msg+=" $(translate 'Detected reset method'): \Zb${reset_method}\Zn\n\n" msg+="$(translate 'For dedicated GPUs, FLR is required by this policy to reduce VM')\n" msg+="$(translate 'start/restart failures and reset instability.')\n\n" msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Reset Capability Blocked')" \ --msgbox "$msg" 20 80 exit 0 fi # ── WARNING: Intel subtype unknown and reset is not FLR ────────────── if [[ "$gpu_subtype" == "unknown" && "$reset_method" != "flr" ]]; then local msg msg="\n\Z4\Zb$(translate 'Warning: Limited PCI Reset Support')\Zn\n\n" msg+="$(translate 'The selected Intel GPU has non-FLR reset support and unknown subtype'):\n" msg+=" $(translate 'Detected subtype'): \Zb${gpu_subtype}\Zn\n" msg+=" $(translate 'Detected reset method'): \Zb${reset_method}\Zn\n\n" msg+="$(translate 'Passthrough may work, but startup/restart reliability is not guaranteed.')\n\n" msg+="$(translate 'Do you want to continue anyway?')" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Reset Capability Warning')" \ --yesno "$msg" 18 78 [[ $? -ne 0 ]] && exit 0 fi } check_gpu_vm_compatibility() { [[ "$SELECTED_GPU" != "amd" && "$SELECTED_GPU" != "intel" ]] && return 0 if [[ "$SELECTED_GPU" == "intel" ]]; then check_intel_vm_compatibility return 0 fi local pci_full="$SELECTED_GPU_PCI" local gpu_subtype reset_method power_state gpu_subtype=$(_detect_amd_gpu_subtype) reset_method=$(_check_pci_reset_method "$pci_full") power_state=$(cat "/sys/bus/pci/devices/${pci_full}/power_state" 2>/dev/null | tr -d '[:space:]') # ── BLOCKER: AMD device currently in D3cold ────────────────────────── # D3cold on AMD passthrough candidates is a high-risk state for VM use. # In practice this often leads to failed power-up/reset when QEMU starts. if [[ "$power_state" == "D3cold" ]]; then local msg msg="\n\Zb\Z1$(translate 'GPU Not Available for Reliable VM Passthrough')\Zn\n\n" msg+="$(translate 'The selected AMD GPU is currently in power state D3cold'):\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n\n" msg+="$(translate 'Detected power state'): \Zb${power_state}\Zn\n\n" msg+="$(translate 'This state indicates a high risk of passthrough failure due to'):\n" msg+=" • $(translate 'Inaccessible device during VM startup')\n" msg+=" • $(translate 'Failed transitions from D3cold to D0')\n" msg+=" • $(translate 'Potential QEMU startup/assertion failures')\n\n" msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'High-Risk GPU Power State')" \ --msgbox "$msg" 22 80 exit 0 fi # ── BLOCKER: AMD APU without FLR reset ─────────────────────────────── # Validated in testing: Lucienne/Renoir/Cezanne + bus-only reset → # "write error: Inappropriate ioctl for device" on PCI reset # "Unable to change power state from D3cold to D0" # QEMU pci_irq_handler assertion failure → VM does not start if [[ "$gpu_subtype" == "apu" && "$reset_method" != "flr" ]]; then local msg msg="\n\Zb\Z1$(translate 'GPU Not Compatible with VM Passthrough')\Zn\n\n" msg+="$(translate 'An AMD integrated GPU (APU) has been detected'):\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n\n" msg+="$(translate 'Although VFIO can bind to this device, full passthrough to a VM is')\n" msg+="$(translate 'not reliable on this hardware due to the following limitations'):\n\n" msg+=" • $(translate 'PCI reset method'): \Zb${reset_method}\Zn" msg+=" — $(translate 'Function Level Reset (FLR) not available')\n" msg+=" • $(translate 'SoC-integrated GPU: tight coupling with other SoC components')\n" msg+=" • $(translate 'Power state D3cold/D0 transitions may be inaccessible')\n" [[ "$power_state" == "D3cold" ]] && \ msg+=" • \Z1$(translate 'Current power state: D3cold (device currently inaccessible)')\Zn\n" msg+="\n$(translate 'Attempting passthrough with this GPU typically results in'):\n" msg+=" — write error: Inappropriate ioctl for device\n" msg+=" — Unable to change power state from D3cold to D0\n" msg+=" — QEMU IRQ assertion failure → VM does not start\n\n" msg+="\Zb$(translate 'Configuration has been stopped to prevent leaving the VM in an unusable state.')\Zn" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Incompatible GPU for VM Passthrough')" \ --msgbox "$msg" 26 80 exit 0 fi # ── BLOCKER: AMD dedicated GPU without FLR reset ───────────────────── # User policy: for dGPU + no FLR, do not continue automatically. if [[ "$gpu_subtype" == "dedicated" && "$reset_method" != "flr" ]]; then local msg msg="\n\Zb\Z1$(translate 'Incompatible Reset Capability for AMD dGPU')\Zn\n\n" msg+="$(translate 'An AMD dedicated GPU has been detected without FLR support'):\n" msg+=" ${SELECTED_GPU_NAME}\n" msg+=" ${SELECTED_GPU_PCI}\n\n" msg+=" $(translate 'Detected reset method'): \Zb${reset_method}\Zn\n\n" msg+="$(translate 'Without Function Level Reset (FLR), passthrough is not considered reliable')\n" msg+="$(translate 'for this policy and may fail after first use or on subsequent VM starts.')\n\n" msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Reset Capability Blocked')" \ --msgbox "$msg" 20 80 exit 0 fi # ── WARNING: Unknown AMD subtype without FLR ───────────────────────── # Keep optional path for unknown classifications only. if [[ "$gpu_subtype" == "unknown" && "$reset_method" != "flr" ]]; then local msg msg="\n\Z4\Zb$(translate 'Warning: Limited PCI Reset Support')\Zn\n\n" msg+="$(translate 'The selected AMD GPU does not report FLR reset support'):\n" msg+=" $(translate 'Detected subtype'): \Zb${gpu_subtype}\Zn\n" msg+=" $(translate 'Detected reset method'): \Zb${reset_method}\Zn\n\n" msg+="$(translate 'Passthrough may fail depending on hardware/firmware implementation.')\n\n" msg+="$(translate 'Do you want to continue anyway?')" dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'Reset Capability Warning')" \ --yesno "$msg" 18 78 [[ $? -ne 0 ]] && exit 0 fi } # ========================================================== # Phase 1 — Step 5: IOMMU group analysis # ========================================================== analyze_iommu_group() { local pci_full="$SELECTED_GPU_PCI" local group_link="/sys/bus/pci/devices/${pci_full}/iommu_group" if [[ ! -L "$group_link" ]]; then if [[ "$IOMMU_PENDING_REBOOT" == "true" ]]; then IOMMU_GROUP="pending-reboot" IOMMU_DEVICES=("$pci_full") IOMMU_VFIO_IDS=() local vid did vid=$(cat "/sys/bus/pci/devices/${pci_full}/vendor" 2>/dev/null | sed 's/0x//') did=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//') [[ -n "$vid" && -n "$did" ]] && IOMMU_VFIO_IDS+=("${vid}:${did}") dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'IOMMU Group Pending')" \ --msgbox "\n$(translate 'IOMMU groups are not available yet because reboot is pending.')\n\n$(translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.')\n\n$(translate 'Selected GPU function'):\n • ${pci_full}" \ 14 82 return 0 fi dialog --backtitle "ProxMenux" \ --title "$(translate 'IOMMU Group Error')" \ --msgbox "\n$(translate 'Could not determine the IOMMU group for the selected GPU.')\n\n$(translate 'Make sure IOMMU is properly enabled and the system has been rebooted after activation.')" \ 10 72 exit 1 fi IOMMU_GROUP=$(basename "$(readlink "$group_link")") IOMMU_DEVICES=() IOMMU_VFIO_IDS=() local group_dir="/sys/kernel/iommu_groups/${IOMMU_GROUP}/devices" local display_lines="" local extra_devices=0 for dev_path in "${group_dir}/"*; do [[ -e "$dev_path" ]] || continue local dev dev=$(basename "$dev_path") # Skip PCI bridges and host bridges (class 0x0604 / 0x0600) local dev_class dev_class=$(cat "/sys/bus/pci/devices/${dev}/class" 2>/dev/null) if [[ "$dev_class" == "0x0604" || "$dev_class" == "0x0600" ]]; then continue fi IOMMU_DEVICES+=("$dev") # Collect vendor:device ID local vid did vid=$(cat "/sys/bus/pci/devices/${dev}/vendor" 2>/dev/null | sed 's/0x//') did=$(cat "/sys/bus/pci/devices/${dev}/device" 2>/dev/null | sed 's/0x//') [[ -n "$vid" && -n "$did" ]] && IOMMU_VFIO_IDS+=("${vid}:${did}") # Build display line local dev_name dev_driver dev_name=$(lspci -nn -s "${dev#0000:}" 2>/dev/null | sed 's/^[^ ]* //' | cut -c1-52) dev_driver=$(_get_pci_driver "$dev") display_lines+=" • ${dev} ${dev_name} [${dev_driver}]\n" [[ "$dev" != "$pci_full" ]] && extra_devices=$((extra_devices + 1)) done local msg msg="$(translate 'IOMMU Group'): ${IOMMU_GROUP}\n\n" msg+="$(translate 'The following devices will all be passed to the VM') " msg+="($(translate 'IOMMU isolation rule')):\n\n" msg+="${display_lines}" if [[ $extra_devices -gt 0 ]]; then msg+="\n\Z1$(translate 'All devices in the same IOMMU group must be passed together.')\Zn" fi dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'IOMMU Group') ${IOMMU_GROUP}" \ --msgbox "\n${msg}" 22 82 } detect_optional_gpu_audio() { EXTRA_AUDIO_DEVICES=() local sibling_audio="${SELECTED_GPU_PCI%.*}.1" local dev_path="/sys/bus/pci/devices/${sibling_audio}" [[ -d "$dev_path" ]] || return 0 local class_hex class_hex=$(cat "${dev_path}/class" 2>/dev/null | sed 's/^0x//') [[ "${class_hex:0:2}" == "04" ]] || return 0 local already_in_group=false dev for dev in "${IOMMU_DEVICES[@]}"; do if [[ "$dev" == "$sibling_audio" ]]; then already_in_group=true break fi done if [[ "$already_in_group" == "true" ]]; then return 0 fi EXTRA_AUDIO_DEVICES+=("$sibling_audio") local vid did new_id vid=$(cat "${dev_path}/vendor" 2>/dev/null | sed 's/0x//') did=$(cat "${dev_path}/device" 2>/dev/null | sed 's/0x//') if [[ -n "$vid" && -n "$did" ]]; then new_id="${vid}:${did}" if _append_unique "$new_id" "${IOMMU_VFIO_IDS[@]}"; then IOMMU_VFIO_IDS+=("$new_id") fi fi } # ========================================================== # Phase 1 — Step 6: VM selection # ========================================================== select_vm() { if [[ -n "$PRESELECT_VMID" ]]; then if qm config "$PRESELECT_VMID" >/dev/null 2>&1; then SELECTED_VMID="$PRESELECT_VMID" VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}') return 0 fi dialog --backtitle "ProxMenux" \ --title "$(translate 'Invalid VMID')" \ --msgbox "\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72 exit 1 fi local menu_items=() while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*VMID ]] && continue local vmid name status vmid=$(echo "$line" | awk '{print $1}') name=$(echo "$line" | awk '{print $2}') status=$(echo "$line" | awk '{print $3}') [[ -z "$vmid" || ! "$vmid" =~ ^[0-9]+$ ]] && continue menu_items+=("$vmid" "${name:-VM-${vmid}} (${status})") done < <(qm list 2>/dev/null) if [[ ${#menu_items[@]} -eq 0 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate 'No VMs Found')" \ --msgbox "\n$(translate 'No Virtual Machines found on this system.')\n\n$(translate 'Create a VM first (machine type q35 + UEFI BIOS), then run this option again.')" \ 10 68 exit 0 fi SELECTED_VMID=$(dialog --backtitle "ProxMenux" \ --title "$(translate 'Select Virtual Machine')" \ --menu "\n$(translate 'Select the VM to add the GPU to:')" \ 20 72 12 \ "${menu_items[@]}" \ 2>&1 >/dev/tty) || exit 0 VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}') } # ========================================================== # Phase 1 — Step 7: Machine type check (must be q35) # ========================================================== check_vm_machine_type() { local machine_line machine_line=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^machine:" | awk '{print $2}') if echo "$machine_line" | grep -q "q35"; then return 0 fi local msg msg="\n$(translate 'The selected VM') \"${VM_NAME}\" (${SELECTED_VMID}) " msg+="$(translate 'is not configured as machine type q35.')\n\n" msg+="$(translate 'PCIe GPU passthrough requires:')\n" msg+=" • $(translate 'Machine type: q35')\n" msg+=" • $(translate 'BIOS: OVMF (UEFI)')\n\n" msg+="$(translate 'Changing the machine type on an existing installed VM is not safe: it changes the chipset and PCI slot layout, which typically prevents the guest OS from booting.')\n\n" msg+="$(translate 'To use GPU passthrough, please create a new VM configured with:')\n" msg+=" • $(translate 'Machine: q35')\n" msg+=" • $(translate 'BIOS: OVMF (UEFI)')\n" msg+=" • $(translate 'Storage controller: VirtIO SCSI')" dialog --backtitle "ProxMenux" \ --title "$(translate 'Incompatible Machine Type')" \ --msgbox "$msg" 20 78 exit 0 } # ========================================================== # Phase 1 — Step 8: Switch mode detection # ========================================================== check_switch_mode() { local pci_slot="${SELECTED_GPU_PCI#0000:}" # 01:00.0 pci_slot="${pci_slot%.*}" # 01:00 # ── LXC conflict check ──────────────────────────────── LXC_AFFECTED_CTIDS=() LXC_AFFECTED_NAMES=() LXC_AFFECTED_RUNNING=() LXC_AFFECTED_ONBOOT=() LXC_SWITCH_ACTION="" local lxc_affected=() local running_count=0 local onboot_count=0 for conf in /etc/pve/lxc/*.conf; do [[ -f "$conf" ]] || continue _lxc_conf_uses_selected_gpu "$conf" || continue local ctid ct_name running_flag onboot_flag ctid=$(basename "$conf" .conf) ct_name=$(pct config "$ctid" 2>/dev/null | awk '/^hostname:/ {print $2}') [[ -z "$ct_name" ]] && ct_name="CT-${ctid}" running_flag=0 onboot_flag=0 _ct_is_running "$ctid" && running_flag=1 _ct_onboot_enabled "$ctid" && onboot_flag=1 LXC_AFFECTED_CTIDS+=("$ctid") LXC_AFFECTED_NAMES+=("$ct_name") LXC_AFFECTED_RUNNING+=("$running_flag") LXC_AFFECTED_ONBOOT+=("$onboot_flag") lxc_affected+=("CT ${ctid} (${ct_name})") [[ "$running_flag" == "1" ]] && running_count=$((running_count + 1)) [[ "$onboot_flag" == "1" ]] && onboot_count=$((onboot_count + 1)) done if [[ ${#lxc_affected[@]} -gt 0 ]]; then SWITCH_FROM_LXC=true SWITCH_LXC_LIST=$(IFS=', '; echo "${lxc_affected[*]}") local msg action_choice msg="\n$(translate 'The selected GPU is currently used by the following LXC container(s):')\n\n" local i for i in "${!LXC_AFFECTED_CTIDS[@]}"; do local status_txt onboot_txt status_txt="$(translate 'stopped')" onboot_txt="onboot=0" [[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]] && status_txt="$(translate 'running')" [[ "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]] && onboot_txt="onboot=1" msg+=" • CT ${LXC_AFFECTED_CTIDS[$i]} (${LXC_AFFECTED_NAMES[$i]}) [${status_txt}, ${onboot_txt}]\n" done msg+="\n$(translate 'VM passthrough requires exclusive VFIO binding of the GPU.')\n" msg+="$(translate 'Choose how to handle affected LXC containers before switching to VM mode.')\n\n" [[ "$running_count" -gt 0 ]] && \ msg+="\Z1$(translate 'Running containers detected'): ${running_count}\Zn\n" [[ "$onboot_count" -gt 0 ]] && \ msg+="\Z1\Zb$(translate 'Start on boot enabled (onboot=1)'): ${onboot_count}\Zn\n" msg+="\n\Z1$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn" action_choice=$(dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'GPU Used in LXC Containers')" \ --default-item "2" \ --menu "$msg" 25 96 8 \ "1" "$(translate 'Keep GPU in LXC config (disable Start on boot)')" \ "2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')" \ 2>&1 >/dev/tty) || exit 0 case "$action_choice" in 1) LXC_SWITCH_ACTION="keep_gpu_disable_onboot" ;; 2) LXC_SWITCH_ACTION="remove_gpu_keep_onboot" ;; *) exit 0 ;; esac else SWITCH_FROM_LXC=false fi # ── VM conflict check (different VM than selected) ──── local vm_src_id="" vm_src_name="" for conf in /etc/pve/qemu-server/*.conf; do [[ -f "$conf" ]] || continue local vmid vmid=$(basename "$conf" .conf) [[ "$vmid" == "$SELECTED_VMID" ]] && continue # same target VM, no conflict if grep -qE "hostpci[0-9]+:.*${pci_slot}" "$conf"; then vm_src_id="$vmid" vm_src_name=$(grep "^name:" "$conf" 2>/dev/null | awk '{print $2}') break fi done if [[ -n "$vm_src_id" ]]; then local src_running=false _vm_is_running "$vm_src_id" && src_running=true if [[ "$src_running" == "true" ]]; then local msg msg="\n$(translate 'The selected GPU is already assigned to another VM that is currently running:')\n\n" msg+=" VM ${vm_src_id} (${vm_src_name:-VM-${vm_src_id}})\n\n" msg+="$(translate 'The same GPU cannot be used by two VMs at the same time.')\n\n" msg+="$(translate 'Next step: stop that VM first, then run')\n" msg+=" Hardware Graphics → Add GPU to VM\n" msg+="$(translate 'to move the GPU safely.')" dialog --backtitle "ProxMenux" \ --title "$(translate 'GPU Busy in Running VM')" \ --msgbox "$msg" 16 78 exit 0 fi SWITCH_FROM_VM=true SWITCH_VM_SRC="$vm_src_id" SWITCH_VM_ACTION="remove_gpu_keep_onboot" local selected_driver selected_driver=$(_get_pci_driver "$SELECTED_GPU_PCI") if [[ "$selected_driver" == "vfio-pci" && "$SWITCH_FROM_LXC" != "true" ]]; then VM_SWITCH_ALREADY_VFIO=true fi local src_onboot target_onboot src_onboot="0" target_onboot="0" _vm_onboot_enabled "$vm_src_id" && src_onboot="1" _vm_onboot_enabled "$SELECTED_VMID" && target_onboot="1" local msg msg="\n$(translate 'The selected GPU is already configured for passthrough to:')\n\n" msg+=" VM ${vm_src_id} (${vm_src_name:-VM-${vm_src_id}})\n\n" msg+="$(translate 'That VM is currently stopped, so the GPU can be reassigned now.')\n" msg+="\Z1\Zb$(translate 'Important: both VMs cannot be running at the same time with the same GPU.')\Zn\n\n" msg+="$(translate 'Target VM'): VM ${SELECTED_VMID} (${VM_NAME:-VM-${SELECTED_VMID}})\n" msg+="$(translate 'Source VM'): VM ${vm_src_id} (${vm_src_name:-VM-${vm_src_id}})\n\n" if [[ "$src_onboot" == "1" && "$target_onboot" == "1" ]]; then msg+="\Z1\Zb$(translate 'Warning: both VMs have autostart enabled (onboot=1).')\Zn\n" msg+="\Z1\Zb$(translate 'Disable autostart on one VM to avoid startup conflicts.')\Zn\n\n" fi if [[ "$VM_SWITCH_ALREADY_VFIO" == "true" ]]; then msg+="$(translate 'Host GPU is already bound to vfio-pci. Host reconfiguration/reboot should not be required for this VM-to-VM reassignment.')\n\n" fi msg+="$(translate 'Choose conflict policy for the source VM:')" local vm_action_choice vm_action_choice=$(dialog --clear --backtitle "ProxMenux" --colors \ --title "$(translate 'GPU Already Assigned to Another VM')" \ --default-item "1" \ --menu "$msg" 24 98 8 \ "1" "$(translate 'Keep GPU in source VM config (disable Start on boot if enabled)')" \ "2" "$(translate 'Remove GPU from source VM config (keep Start on boot)')" \ 2>&1 >/dev/tty) || exit 0 case "$vm_action_choice" in 1) SWITCH_VM_ACTION="keep_gpu_disable_onboot" ;; 2) SWITCH_VM_ACTION="remove_gpu_keep_onboot" ;; *) exit 0 ;; esac fi } # ========================================================== # Phase 1 — Step 9: Confirmation summary # ========================================================== confirm_summary() { local msg msg="\n$(translate 'The following changes will be applied'):\n" msg+="\n ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" msg+=" $(translate 'GPU') : ${SELECTED_GPU_NAME}\n" msg+=" $(translate 'PCI Address') : ${SELECTED_GPU_PCI}\n" if [[ "$IOMMU_PENDING_REBOOT" == "true" ]]; then msg+=" $(translate 'IOMMU Group') : $(translate 'pending (reboot required to enumerate full group)')\n" else msg+=" $(translate 'IOMMU Group') : ${IOMMU_GROUP} (${#IOMMU_DEVICES[@]} $(translate 'devices'))\n" fi msg+=" $(translate 'Target VM') : ${VM_NAME:-VM-${SELECTED_VMID}} (${SELECTED_VMID})\n" msg+=" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" msg+=" \Zb$(translate 'Host'):\Zn\n" if [[ "$PREFLIGHT_HOST_REBOOT_REQUIRED" != "true" ]]; then msg+=" • $(translate 'Host VFIO configuration already up to date')\n" msg+=" • $(translate 'No host VFIO reconfiguration expected')\n" msg+=" • $(translate 'No host reboot expected')\n\n" else msg+=" • $(translate 'VFIO modules in /etc/modules')\n" msg+=" • $(translate 'vfio-pci IDs in /etc/modprobe.d/vfio.conf')\n" [[ "$SELECTED_GPU" == "amd" ]] && \ msg+=" • $(translate 'AMD softdep configured')\n" [[ "$SELECTED_GPU" == "amd" ]] && \ msg+=" • $(translate 'GPU ROM dump to /usr/share/kvm/')\n" msg+=" • $(translate 'GPU driver blacklisted')\n" msg+=" • $(translate 'initramfs updated')\n" msg+=" • \Zb$(translate 'System reboot required')\Zn\n\n" fi msg+=" \Zb$(translate 'VM') ${SELECTED_VMID}:\Zn\n" [[ "$TARGET_VM_ALREADY_HAS_GPU" == "true" ]] && \ msg+=" • $(translate 'Existing hostpci entries detected — they will be reused')\n" msg+=" • $(translate 'Virtual display normalized to vga: std (compatibility)')\n" if [[ "$IOMMU_PENDING_REBOOT" == "true" ]]; then msg+=" • $(translate 'hostpci entries for selected GPU functions (full IOMMU group will be enforced after reboot)')\n" else msg+=" • $(translate 'hostpci entries for all IOMMU group devices')\n" fi [[ ${#EXTRA_AUDIO_DEVICES[@]} -gt 0 ]] && \ msg+=" • $(translate 'Additional GPU audio function will be added'): ${EXTRA_AUDIO_DEVICES[*]}\n" [[ "$SELECTED_GPU" == "nvidia" ]] && \ msg+=" • $(translate 'NVIDIA KVM hiding (cpu hidden=1)')\n" if [[ "$SWITCH_FROM_LXC" == "true" ]]; then msg+="\n \Z1• $(translate 'Affected LXC containers'): ${SWITCH_LXC_LIST}\Zn\n" msg+=" \Z1• $(translate 'Selected LXC action'): $(_lxc_switch_action_label)\Zn\n" if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" ]]; then msg+=" \Z1• $(translate 'To use the GPU again in LXC, run Add GPU to LXC from GPUs and Coral-TPU Menu')\Zn\n" fi fi if [[ "$SWITCH_FROM_VM" == "true" ]]; then if [[ "$SWITCH_VM_ACTION" == "keep_gpu_disable_onboot" ]]; then msg+="\n \Z1• $(translate 'GPU will remain configured in source VM'): ${SWITCH_VM_SRC}\Zn\n" msg+=" \Z1• $(translate 'Start on boot will be disabled on source VM only if currently enabled')\Zn\n" msg+=" \Z1• $(translate 'GPU guard hook will block concurrent start when another VM is already using this GPU')\Zn\n" else msg+="\n \Z1• $(translate 'GPU will be removed from source VM config'): ${SWITCH_VM_SRC}\Zn\n" fi msg+=" \Z1• $(translate 'Selected VM action'): $(_vm_switch_action_label)\Zn\n" fi msg+="\n$(translate 'Do you want to proceed?')" local run_title run_title=$(_get_vm_run_title) dialog --clear --backtitle "ProxMenux" --colors \ --title "${run_title}" \ --yesno "$msg" 28 78 [[ $? -ne 0 ]] && exit 0 } # ========================================================== # Phase 2 — Processing # ========================================================== # ── VFIO modules in /etc/modules ───────────────────────── add_vfio_modules() { msg_info "$(translate 'Configuring VFIO modules...')" local modules=("vfio" "vfio_iommu_type1" "vfio_pci") local kernel_major kernel_minor kernel_major=$(uname -r | cut -d. -f1) kernel_minor=$(uname -r | cut -d. -f2) if (( kernel_major < 6 || ( kernel_major == 6 && kernel_minor < 2 ) )); then modules+=("vfio_virqfd") fi for mod in "${modules[@]}"; do _add_line_if_missing "$mod" /etc/modules done msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture" } # ── vfio-pci IDs — merge with existing ones ───────────── configure_vfio_pci_ids() { msg_info "$(translate 'Configuring vfio-pci device IDs...')" local vfio_conf="/etc/modprobe.d/vfio.conf" touch "$vfio_conf" # Collect existing IDs (if any) local existing_ids=() local existing_line existing_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1) if [[ -n "$existing_line" ]]; then local ids_part ids_part=$(echo "$existing_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//') IFS=',' read -ra existing_ids <<< "$ids_part" fi # Merge: add new IDs not already present local all_ids=("${existing_ids[@]}") for new_id in "${IOMMU_VFIO_IDS[@]}"; do local found=false for existing in "${existing_ids[@]}"; do [[ "$existing" == "$new_id" ]] && found=true && break done $found || all_ids+=("$new_id") done local ids_str ids_str=$(IFS=','; echo "${all_ids[*]}") local existing_full_line existing_full_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1) local new_full_line="options vfio-pci ids=${ids_str} disable_vga=1" if [[ "$existing_full_line" != "$new_full_line" ]]; then sed -i '/^options vfio-pci ids=/d' "$vfio_conf" echo "$new_full_line" >> "$vfio_conf" HOST_CONFIG_CHANGED=true fi msg_ok "$(translate 'vfio-pci IDs configured') (${ids_str})" | tee -a "$screen_capture" } # ── IOMMU interrupt remapping ───────────────────────────── configure_iommu_options() { _add_line_if_missing "options vfio_iommu_type1 allow_unsafe_interrupts=1" \ /etc/modprobe.d/iommu_unsafe_interrupts.conf _add_line_if_missing "options kvm ignore_msrs=1" \ /etc/modprobe.d/kvm.conf msg_ok "$(translate 'IOMMU interrupt remapping configured')" | tee -a "$screen_capture" } # ── AMD softdep ────────────────────────────────────────── add_softdep_amd() { msg_info "$(translate 'Configuring AMD softdep...')" local vfio_conf="/etc/modprobe.d/vfio.conf" _add_line_if_missing "softdep radeon pre: vfio-pci" "$vfio_conf" _add_line_if_missing "softdep amdgpu pre: vfio-pci" "$vfio_conf" _add_line_if_missing "softdep snd_hda_intel pre: vfio-pci" "$vfio_conf" msg_ok "$(translate 'AMD softdep configured in /etc/modprobe.d/vfio.conf')" | tee -a "$screen_capture" } # ── Blacklist GPU drivers (idempotent) ─────────────────── blacklist_gpu_drivers() { msg_info "$(translate 'Blacklisting GPU host drivers...')" local blacklist_file="/etc/modprobe.d/blacklist.conf" touch "$blacklist_file" case "$SELECTED_GPU" in nvidia) _add_line_if_missing "blacklist nouveau" "$blacklist_file" _add_line_if_missing "blacklist nvidia" "$blacklist_file" _add_line_if_missing "blacklist nvidia_drm" "$blacklist_file" _add_line_if_missing "blacklist nvidia_modeset" "$blacklist_file" _add_line_if_missing "blacklist nvidia_uvm" "$blacklist_file" _add_line_if_missing "blacklist nvidiafb" "$blacklist_file" _add_line_if_missing "blacklist lbm-nouveau" "$blacklist_file" _add_line_if_missing "options nouveau modeset=0" "$blacklist_file" ;; amd) _add_line_if_missing "blacklist radeon" "$blacklist_file" _add_line_if_missing "blacklist amdgpu" "$blacklist_file" ;; intel) _add_line_if_missing "blacklist i915" "$blacklist_file" ;; esac msg_ok "$(translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf')" | tee -a "$screen_capture" } sanitize_nvidia_host_stack_for_vfio() { msg_info "$(translate 'Sanitizing NVIDIA host services for VFIO mode...')" local changed=false local state_dir="/var/lib/proxmenux" local state_file="${state_dir}/nvidia-host-services.state" local svc local -a services=( "nvidia-persistenced.service" "nvidia-powerd.service" "nvidia-fabricmanager.service" ) mkdir -p "$state_dir" >/dev/null 2>&1 || true : > "$state_file" for svc in "${services[@]}"; do local was_enabled=0 was_active=0 if systemctl is-enabled --quiet "$svc" 2>/dev/null; then was_enabled=1 fi if systemctl is-active --quiet "$svc" 2>/dev/null; then was_active=1 fi if (( was_enabled == 1 || was_active == 1 )); then echo "${svc} enabled=${was_enabled} active=${was_active}" >>"$state_file" fi if systemctl is-active --quiet "$svc" 2>/dev/null; then systemctl stop "$svc" >>"$LOG_FILE" 2>&1 || true changed=true fi if systemctl is-enabled --quiet "$svc" 2>/dev/null; then systemctl disable "$svc" >>"$LOG_FILE" 2>&1 || true changed=true fi done [[ -s "$state_file" ]] || rm -f "$state_file" if [[ -f /etc/modules-load.d/nvidia-vfio.conf ]]; then mv /etc/modules-load.d/nvidia-vfio.conf /etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio >>"$LOG_FILE" 2>&1 || true changed=true fi if grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null; then sed -i '/^nvidia$/d;/^nvidia_uvm$/d;/^nvidia_drm$/d;/^nvidia_modeset$/d' /etc/modules changed=true fi if $changed; then HOST_CONFIG_CHANGED=true msg_ok "$(translate 'NVIDIA host services/autoload disabled for VFIO mode')" | tee -a "$screen_capture" else msg_ok "$(translate 'NVIDIA host services/autoload already aligned for VFIO mode')" | tee -a "$screen_capture" fi } # ── AMD ROM dump: sysfs first, VFCT ACPI table as fallback ─────────────── _dump_rom_via_vfct() { local rom_dest="$1" local vfct_file="/sys/firmware/acpi/tables/VFCT" [[ -f "$vfct_file" ]] || return 1 # VFCT table layout: # Offset 0-35 : standard ACPI header (36 bytes) # Offset 36-47 : VFCT-specific fields (12 bytes) # Offset 48 : first GPU_BIOS_IMAGE object # +0 VendorID (2 bytes) # +2 DeviceID (2 bytes) # +4 SubsystemVendorID (2 bytes) # +6 SubsystemDeviceID (2 bytes) # +8 PCIBus/Device/Function/Reserved (4 bytes) # +12 ImageLength (4 bytes, little-endian) # +16 VBIOS image data local img_length img_length=$(od -An -tu4 -j60 -N4 "$vfct_file" 2>/dev/null | tr -d '[:space:]') if [[ -z "$img_length" || "$img_length" -le 0 ]]; then return 1 fi dd if="$vfct_file" bs=1 skip=64 count="$img_length" of="$rom_dest" 2>/dev/null [[ -s "$rom_dest" ]] } dump_amd_rom() { local pci_full="$SELECTED_GPU_PCI" local rom_path="/sys/bus/pci/devices/${pci_full}/rom" local kvm_dir="/usr/share/kvm" mkdir -p "$kvm_dir" local vid did vid=$(cat "/sys/bus/pci/devices/${pci_full}/vendor" 2>/dev/null | sed 's/0x//') did=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//') local rom_filename="vbios_${vid}_${did}.bin" local rom_dest="${kvm_dir}/${rom_filename}" # ── Method 1: sysfs /rom ────────────────────────────── if [[ -f "$rom_path" ]]; then msg_info "$(translate 'Dumping AMD GPU ROM BIOS via sysfs...')" echo 1 > "$rom_path" 2>/dev/null if cat "$rom_path" > "$rom_dest" 2>>"$LOG_FILE" && [[ -s "$rom_dest" ]]; then echo 0 > "$rom_path" 2>/dev/null AMD_ROM_FILE="$rom_filename" msg_ok "$(translate 'GPU ROM dumped to') ${rom_dest}" | tee -a "$screen_capture" return 0 fi echo 0 > "$rom_path" 2>/dev/null rm -f "$rom_dest" msg_warn "$(translate 'sysfs ROM dump failed — trying ACPI VFCT table...')" else msg_info "$(translate 'No sysfs ROM entry — trying ACPI VFCT table...')" fi # ── Method 2: ACPI VFCT table ──────────────────────── if _dump_rom_via_vfct "$rom_dest"; then AMD_ROM_FILE="$rom_filename" msg_ok "$(translate 'GPU ROM extracted from ACPI VFCT table to') ${rom_dest}" | tee -a "$screen_capture" return 0 fi rm -f "$rom_dest" msg_warn "$(translate 'ROM dump not available — configuring without romfile.')" msg_warn "$(translate 'Passthrough may still work without a ROM file.')" } _remove_selected_gpu_from_lxc_conf() { local conf="$1" case "$SELECTED_GPU" in nvidia) sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' "$conf" ;; amd) sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf" sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' "$conf" sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf" sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf" ;; intel) sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf" sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf" sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf" ;; *) sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf" sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' "$conf" sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' "$conf" sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf" sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf" ;; esac } # ── Apply selected action for affected LXC (switch mode) ─ cleanup_lxc_configs() { [[ "$SWITCH_FROM_LXC" != "true" ]] && return 0 [[ ${#LXC_AFFECTED_CTIDS[@]} -eq 0 ]] && return 0 msg_info "$(translate 'Applying selected LXC switch action...')" local i for i in "${!LXC_AFFECTED_CTIDS[@]}"; do local ctid conf ctid="${LXC_AFFECTED_CTIDS[$i]}" conf="/etc/pve/lxc/${ctid}.conf" if [[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]]; then msg_info "$(translate 'Stopping LXC') ${ctid}..." if pct stop "$ctid" >>"$LOG_FILE" 2>&1; then msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture" else msg_warn "$(translate 'Could not stop LXC') ${ctid}" | tee -a "$screen_capture" fi else msg_ok "$(translate 'LXC already stopped') ${ctid}" | tee -a "$screen_capture" fi if [[ "$LXC_SWITCH_ACTION" == "keep_gpu_disable_onboot" ]]; then if [[ "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]]; then if pct set "$ctid" -onboot 0 >>"$LOG_FILE" 2>&1; then msg_warn "$(translate 'Start on boot disabled for LXC') ${ctid}" | tee -a "$screen_capture" else msg_error "$(translate 'Failed to disable Start on boot for LXC') ${ctid}" | tee -a "$screen_capture" fi fi fi if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" && -f "$conf" ]]; then _remove_selected_gpu_from_lxc_conf "$conf" msg_ok "$(translate 'GPU access removed from LXC') ${ctid}" | tee -a "$screen_capture" fi done if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" ]]; then msg_warn "$(translate 'If needed again, re-add GPU to LXC from GPUs and Coral-TPU Menu → Add GPU to LXC.')" | tee -a "$screen_capture" fi } # ── Remove GPU from another VM config (switch mode) ────── cleanup_vm_config() { [[ "$SWITCH_FROM_VM" != "true" ]] && return 0 [[ -z "$SWITCH_VM_SRC" ]] && return 0 local pci_slot="${SELECTED_GPU_PCI#0000:}" pci_slot="${pci_slot%.*}" # 01:00 if [[ "$VM_SWITCH_ACTION" == "keep_gpu_disable_onboot" ]]; then msg_info "$(translate 'Keeping GPU in source VM config') ${SWITCH_VM_SRC}..." if _vm_onboot_enabled "$SWITCH_VM_SRC"; then if qm set "$SWITCH_VM_SRC" -onboot 0 >>"$LOG_FILE" 2>&1; then msg_warn "$(translate 'Start on boot disabled for VM') ${SWITCH_VM_SRC}" | tee -a "$screen_capture" else msg_error "$(translate 'Failed to disable Start on boot for VM') ${SWITCH_VM_SRC}" | tee -a "$screen_capture" fi else msg_ok "$(translate 'Start on boot already disabled for VM') ${SWITCH_VM_SRC}" | tee -a "$screen_capture" fi msg_ok "$(translate 'GPU kept in source VM config') ${SWITCH_VM_SRC}" | tee -a "$screen_capture" return 0 fi local src_conf="/etc/pve/qemu-server/${SWITCH_VM_SRC}.conf" if [[ -f "$src_conf" ]]; then msg_info "$(translate 'Removing GPU from VM') ${SWITCH_VM_SRC}..." sed -i "/^hostpci[0-9]\+:.*${pci_slot}/d" "$src_conf" msg_ok "$(translate 'GPU removed from VM') ${SWITCH_VM_SRC}" | tee -a "$screen_capture" fi } # ── VM display normalization for passthrough stability ──── ensure_vm_display_std() { msg_info "$(translate 'Checking VM virtual display model...')" local current_vga current_base current_vga=$(qm config "$SELECTED_VMID" 2>/dev/null | awk '/^vga:/ {print $2}') current_base="${current_vga%%,*}" if [[ -z "$current_base" ]]; then if qm set "$SELECTED_VMID" --vga std >>"$LOG_FILE" 2>&1; then msg_ok "$(translate 'Virtual display set to'): vga: std" | tee -a "$screen_capture" else msg_warn "$(translate 'Could not set VM virtual display to vga: std')" | tee -a "$screen_capture" fi return 0 fi if [[ "$current_base" == "std" ]]; then msg_ok "$(translate 'Virtual display already set to'): vga: std" | tee -a "$screen_capture" return 0 fi if qm set "$SELECTED_VMID" --vga std >>"$LOG_FILE" 2>&1; then msg_ok "$(translate 'Virtual display changed from') ${current_base} $(translate 'to') std" | tee -a "$screen_capture" else msg_warn "$(translate 'Could not change VM virtual display to vga: std')" | tee -a "$screen_capture" fi } # ── Configure VM: add hostpci entries ───────────────────── configure_vm() { msg_info "$(translate 'Configuring VM') ${SELECTED_VMID}..." # Find next free hostpciN index local idx=0 if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0) else while qm config "$SELECTED_VMID" 2>/dev/null | grep -q "^hostpci${idx}:"; do idx=$((idx + 1)) done fi # Primary GPU: pcie=1, x-vga=1 only for NVIDIA/AMD (not Intel iGPU), romfile if AMD local gpu_opts="pcie=1" [[ "$SELECTED_GPU" == "nvidia" || "$SELECTED_GPU" == "amd" ]] && gpu_opts+=",x-vga=1" [[ -n "$AMD_ROM_FILE" ]] && gpu_opts+=",romfile=${AMD_ROM_FILE}" if _is_pci_function_assigned_to_vm "$SELECTED_GPU_PCI" "$SELECTED_VMID"; then msg_ok "$(translate 'GPU already present in target VM — existing hostpci entry reused')" | tee -a "$screen_capture" else if qm set "$SELECTED_VMID" --hostpci${idx} "${SELECTED_GPU_PCI},${gpu_opts}" >>"$LOG_FILE" 2>&1; then msg_ok "$(translate 'GPU added'): hostpci${idx}: ${SELECTED_GPU_PCI},${gpu_opts}" | tee -a "$screen_capture" else msg_error "$(translate 'Failed to add GPU to target VM'): ${SELECTED_GPU_PCI}" | tee -a "$screen_capture" return 1 fi idx=$((idx + 1)) fi # Remaining IOMMU group devices (audio, USB controllers, etc.) for dev in "${IOMMU_DEVICES[@]}"; do [[ "$dev" == "$SELECTED_GPU_PCI" ]] && continue if _is_pci_function_assigned_to_vm "$dev" "$SELECTED_VMID"; then msg_ok "$(translate 'Device already present in target VM — existing hostpci entry reused'): ${dev}" | tee -a "$screen_capture" continue fi if qm set "$SELECTED_VMID" --hostpci${idx} "${dev},pcie=1" >>"$LOG_FILE" 2>&1; then msg_ok "$(translate 'Device added'): hostpci${idx}: ${dev},pcie=1" | tee -a "$screen_capture" else msg_error "$(translate 'Failed to add IOMMU group device'): ${dev}" | tee -a "$screen_capture" return 1 fi idx=$((idx + 1)) done # Optional sibling GPU audio function (typically *.1) when split from IOMMU group for dev in "${EXTRA_AUDIO_DEVICES[@]}"; do if _is_pci_function_assigned_to_vm "$dev" "$SELECTED_VMID"; then msg_ok "$(translate 'GPU audio already present in target VM — existing hostpci entry reused'): ${dev}" | tee -a "$screen_capture" continue fi if qm set "$SELECTED_VMID" --hostpci${idx} "${dev},pcie=1" >>"$LOG_FILE" 2>&1; then msg_ok "$(translate 'GPU audio added'): hostpci${idx}: ${dev},pcie=1" | tee -a "$screen_capture" else msg_error "$(translate 'Failed to add GPU audio function'): ${dev}" | tee -a "$screen_capture" return 1 fi idx=$((idx + 1)) done # NVIDIA: hide KVM hypervisor from guest [[ "$SELECTED_GPU" == "nvidia" ]] && _configure_nvidia_kvm_hide return 0 } _configure_nvidia_kvm_hide() { msg_info "$(translate 'Configuring NVIDIA KVM hiding...')" # CPU: host,hidden=1 local current_cpu current_cpu=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^cpu:" | awk '{print $2}') if ! echo "$current_cpu" | grep -q "hidden=1"; then qm set "$SELECTED_VMID" --cpu "host,hidden=1,flags=+pcid" >>"$LOG_FILE" 2>&1 msg_ok "$(translate 'CPU set to host,hidden=1,flags=+pcid')" | tee -a "$screen_capture" else msg_ok "$(translate 'NVIDIA CPU hiding already configured')" | tee -a "$screen_capture" fi # args: kvm=off + vendor_id spoof local current_args current_args=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^args:" | sed 's/^args: //') if ! echo "$current_args" | grep -q "kvm=off"; then local kvm_args="-cpu 'host,+kvm_pv_unhalt,+kvm_pv_eoi,hv_vendor_id=NV43FIX,kvm=off'" local new_args if [[ -n "$current_args" ]]; then new_args="${current_args} ${kvm_args}" else new_args="$kvm_args" fi qm set "$SELECTED_VMID" --args "$new_args" >>"$LOG_FILE" 2>&1 msg_ok "$(translate 'NVIDIA KVM args configured (kvm=off, vendor_id spoof)')" | tee -a "$screen_capture" else msg_ok "$(translate 'NVIDIA KVM hiding already configured')" | tee -a "$screen_capture" fi } # ── Update initramfs ───────────────────────────────────── update_initramfs_host() { msg_info "$(translate 'Updating initramfs (this may take a minute)...')" update-initramfs -u -k all >>"$LOG_FILE" 2>&1 msg_ok "$(translate 'initramfs updated')" | tee -a "$screen_capture" } # ========================================================== # Main # ========================================================== main() { parse_cli_args "$@" : >"$LOG_FILE" : >"$screen_capture" [[ "$WIZARD_CALL" == "true" ]] && _set_wizard_result "cancelled" # ── Phase 1: all dialogs (no terminal output) ───────── detect_host_gpus check_iommu_enabled select_gpu warn_single_gpu select_vm ensure_selected_gpu_not_already_in_target_vm check_gpu_vm_compatibility analyze_iommu_group detect_optional_gpu_audio check_vm_machine_type check_switch_mode evaluate_host_reboot_requirement confirm_summary # ── Phase 2: processing ─────────────────────────────── local run_title run_title=$(_get_vm_run_title) if [[ "$WIZARD_CALL" == "true" ]]; then echo else clear show_proxmenux_logo msg_title "${run_title}" fi if [[ "$VM_SWITCH_ALREADY_VFIO" == "true" ]]; then msg_ok "$(translate 'Host already in VFIO mode — skipping host reconfiguration for VM reassignment')" | tee -a "$screen_capture" else add_vfio_modules configure_vfio_pci_ids configure_iommu_options [[ "$SELECTED_GPU" == "amd" ]] && add_softdep_amd blacklist_gpu_drivers [[ "$SELECTED_GPU" == "amd" ]] && dump_amd_rom fi [[ "$SELECTED_GPU" == "nvidia" ]] && sanitize_nvidia_host_stack_for_vfio cleanup_lxc_configs cleanup_vm_config ensure_vm_display_std if ! configure_vm; then msg_error "$(translate 'VM passthrough configuration failed. Review the log and VM config.')" [[ "$WIZARD_CALL" == "true" ]] && _set_wizard_result "failed" rm -f "$screen_capture" exit 1 fi if declare -F attach_proxmenux_gpu_guard_to_vm >/dev/null 2>&1; then ensure_proxmenux_gpu_guard_hookscript attach_proxmenux_gpu_guard_to_vm "$SELECTED_VMID" sync_proxmenux_gpu_guard_hooks fi [[ "$HOST_CONFIG_CHANGED" == "true" ]] && update_initramfs_host # ── Phase 3: summary ───────────────────────────────── if [[ "$WIZARD_CALL" == "true" ]]; then echo else show_proxmenux_logo msg_title "${run_title}" cat "$screen_capture" echo fi if [[ "$WIZARD_CALL" == "true" ]]; then if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then _set_wizard_result "applied_reboot_required" else _set_wizard_result "applied" fi rm -f "$screen_capture" return 0 fi echo -e "${TAB}${BL}📄 Log: ${LOG_FILE}${CL}" if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then echo -e "${TAB}${DGN}- $(translate 'Host VFIO configuration changed — reboot required before starting the VM.')${CL}" else echo -e "${TAB}${DGN}- $(translate 'Host VFIO config was already up to date — no reboot needed.')${CL}" fi case "$SELECTED_GPU" in nvidia) echo -e "${TAB}${DGN}- $(translate 'Install NVIDIA drivers from nvidia.com inside the guest.')${CL}" echo -e "${TAB}${DGN}- $(translate 'If Code 43 error appears, KVM hiding is already configured.')${CL}" ;; amd) echo -e "${TAB}${DGN}- $(translate 'Install AMD GPU drivers inside the guest.')${CL}" echo -e "${TAB}${DGN}- $(translate 'If passthrough fails on Windows: install RadeonResetBugFix.')${CL}" [[ -n "$AMD_ROM_FILE" ]] && \ echo -e "${TAB}${DGN}- $(translate 'ROM file used'): /usr/share/kvm/${AMD_ROM_FILE}${CL}" ;; intel) echo -e "${TAB}${DGN}- $(translate 'Install Intel Graphics Driver inside the guest.')${CL}" echo -e "${TAB}${DGN}- $(translate 'Enable Remote Desktop (RDP) before disabling the virtual display.')${CL}" ;; esac echo msg_info2 "$(translate 'If you want to use a physical monitor on the passthrough GPU:')" echo " • $(translate 'First install the GPU drivers inside the guest and verify remote access (RDP/SSH).')" echo " • $(translate 'Then change the VM display to none (vga: none) when the guest is stable.')" echo msg_success "$(translate 'GPU passthrough configured for VM') ${SELECTED_VMID} (${VM_NAME})." echo rm -f "$screen_capture" if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then whiptail --title "$(translate 'Reboot Required')" \ --yesno "$(translate 'A reboot is required for VFIO binding to take effect. Do you want to restart now?')" 10 68 if [[ $? -eq 0 ]]; then msg_warn "$(translate 'Rebooting the system...')" reboot else msg_info2 "$(translate 'You can reboot later manually.')" msg_success "$(translate 'Press Enter to continue...')" read -r fi else msg_success "$(translate 'Press Enter to continue...')" read -r fi } main "$@"