mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-06 04:13:48 +00:00
1636 lines
65 KiB
Bash
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 "$@"
|