Files
ProxMenux/scripts/gpu_tpu/add_gpu_vm.sh
2026-04-05 11:24:08 +02:00

1636 lines
65 KiB
Bash

#!/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
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=""
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=""
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=""
# ==========================================================
# 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"
}
_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() {
# 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 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
;;
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]}" == "vfio-pci" ]] && label+=" [VFIO]"
label+="${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
choice=$(dialog --backtitle "ProxMenux" \
--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_short=$(echo "$line" | awk '{print $1}')
pci_full="0000:${pci_short}"
# Human-readable name: between first ":" and first "["
name=$(echo "$line" | sed 's/^[^:]*[^:]: //' | sed 's/ \[.*//' | cut -c1-62)
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
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
[[ -d /sys/kernel/iommu_groups ]] && \
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
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 'You must run this option again after rebooting.')"
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')"
_enable_iommu_cmdline
echo
msg_success "$(translate 'IOMMU configured. Please reboot and run GPU passthrough to VM again.')"
echo
msg_success "$(translate 'Press Enter to continue...')"
read -r
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]}" == "vfio-pci" ]] && label+=" [VFIO]"
label+="${ALL_GPU_PCIS[$i]}"
menu_items+=("$i" "$label")
done
local choice
choice=$(dialog --backtitle "ProxMenux" \
--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+="$(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
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:]')
# ── 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+=" • \Z3$(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
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\Z3$(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 ────────────────────────────────
local lxc_affected=()
for conf in /etc/pve/lxc/*.conf; do
[[ -f "$conf" ]] || continue
if grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)" "$conf"; then
local ctid ct_name
ctid=$(basename "$conf" .conf)
ct_name=$(pct config "$ctid" 2>/dev/null | grep "^hostname:" | awk '{print $2}')
lxc_affected+=("CT ${ctid} (${ct_name:-CT-${ctid}})")
fi
done
if [[ ${#lxc_affected[@]} -gt 0 ]]; then
SWITCH_FROM_LXC=true
SWITCH_LXC_LIST=$(IFS=', '; echo "${lxc_affected[*]}")
local msg
msg="\n$(translate 'The selected GPU is currently shared with the following LXC containers via device passthrough:')\n\n"
for ct in "${lxc_affected[@]}"; do
msg+="${ct}\n"
done
msg+="\n$(translate 'VM passthrough requires exclusive VFIO binding of the GPU.')\n"
msg+="$(translate 'GPU device access will be removed from those LXC containers.')\n\n"
msg+="\Z3$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn\n\n"
msg+="$(translate 'Do you want to continue?')"
dialog --backtitle "ProxMenux" \
--title "$(translate 'GPU Used in LXC Containers')" \
--yesno "$msg" 18 76
[[ $? -ne 0 ]] && exit 0
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"
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+="\Z3$(translate 'Important: both VMs cannot be running at the same time with the same GPU.')\Zn\n\n"
msg+="$(translate 'The existing hostpci entry will be removed from that VM and configured on'): "
msg+="VM ${SELECTED_VMID} (${VM_NAME:-VM-${SELECTED_VMID}})\n\n"
if [[ "$src_onboot" == "1" && "$target_onboot" == "1" ]]; then
msg+="\Z3$(translate 'Warning: both VMs have autostart enabled (onboot=1).')\Zn\n"
msg+="\Z3$(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 'Do you want to continue?')"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'GPU Already Assigned to Another VM')" \
--yesno "$msg" 24 88
[[ $? -ne 0 ]] && exit 0
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"
msg+=" $(translate 'IOMMU Group') : ${IOMMU_GROUP} (${#IOMMU_DEVICES[@]} $(translate 'devices'))\n"
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"
msg+="$(translate 'hostpci entries for all IOMMU group devices')\n"
[[ ${#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"
[[ "$SWITCH_FROM_LXC" == "true" ]] && \
msg+="\n \Z3• $(translate 'GPU will be removed from LXC containers'): ${SWITCH_LXC_LIST}\Zn\n"
[[ "$SWITCH_FROM_VM" == "true" ]] && \
msg+="\n \Z3• $(translate 'GPU will be removed from VM') ${SWITCH_VM_SRC}\Zn\n"
msg+="\n$(translate 'Do you want to proceed?')"
local run_title
run_title=$(_get_vm_run_title)
dialog --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 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"
}
# ── 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 GPU from LXC configs (switch mode) ────────────
cleanup_lxc_configs() {
[[ "$SWITCH_FROM_LXC" != "true" ]] && return 0
msg_info "$(translate 'Removing GPU device access from LXC containers...')"
for conf in /etc/pve/lxc/*.conf; do
[[ -f "$conf" ]] || continue
if grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)" "$conf"; then
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"
local ctid
ctid=$(basename "$conf" .conf)
msg_ok "$(translate 'GPU removed from LXC') ${ctid}" | tee -a "$screen_capture"
fi
done
}
# ── 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
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
qm set "$SELECTED_VMID" --hostpci${idx} "${SELECTED_GPU_PCI},${gpu_opts}" >>"$LOG_FILE" 2>&1
msg_ok "$(translate 'GPU added'): hostpci${idx}: ${SELECTED_GPU_PCI},${gpu_opts}" | tee -a "$screen_capture"
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
qm set "$SELECTED_VMID" --hostpci${idx} "${dev},pcie=1" >>"$LOG_FILE" 2>&1
msg_ok "$(translate 'Device added'): hostpci${idx}: ${dev},pcie=1" | tee -a "$screen_capture"
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
qm set "$SELECTED_VMID" --hostpci${idx} "${dev},pcie=1" >>"$LOG_FILE" 2>&1
msg_ok "$(translate 'GPU audio added'): hostpci${idx}: ${dev},pcie=1" | tee -a "$screen_capture"
idx=$((idx + 1))
done
# NVIDIA: hide KVM hypervisor from guest
[[ "$SELECTED_GPU" == "nvidia" ]] && _configure_nvidia_kvm_hide
}
_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
cleanup_lxc_configs
cleanup_vm_config
ensure_vm_display_std
configure_vm
[[ "$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
_set_wizard_result "applied"
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 "$@"