2026-04-04 00:54:57 +02:00
#!/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
# ==========================================================
2026-04-05 11:24:08 +02:00
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 "
2026-04-04 00:54:57 +02:00
BASE_DIR = "/usr/local/share/proxmenux"
2026-04-05 11:24:08 +02:00
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
2026-04-04 00:54:57 +02:00
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
2026-04-05 11:24:08 +02:00
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
2026-04-06 13:39:07 +02:00
if [ [ -f " $LOCAL_SCRIPTS_LOCAL /global/gpu_hook_guard_helpers.sh " ] ] ; then
source " $LOCAL_SCRIPTS_LOCAL /global/gpu_hook_guard_helpers.sh "
elif [ [ -f " $LOCAL_SCRIPTS_DEFAULT /global/gpu_hook_guard_helpers.sh " ] ] ; then
source " $LOCAL_SCRIPTS_DEFAULT /global/gpu_hook_guard_helpers.sh "
fi
2026-04-04 00:54:57 +02:00
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=
2026-04-05 11:24:08 +02:00
declare -a EXTRA_AUDIO_DEVICES = ( ) # sibling audio function(s), typically *.1
2026-04-04 00:54:57 +02:00
IOMMU_GROUP = ""
2026-04-06 19:13:31 +02:00
IOMMU_PENDING_REBOOT = false
2026-04-04 00:54:57 +02:00
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 = ""
2026-04-06 21:50:46 +02:00
SWITCH_VM_ACTION = "" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
2026-04-04 00:54:57 +02:00
TARGET_VM_ALREADY_HAS_GPU = false
2026-04-05 11:24:08 +02:00
VM_SWITCH_ALREADY_VFIO = false
PREFLIGHT_HOST_REBOOT_REQUIRED = true
2026-04-04 00:54:57 +02:00
AMD_ROM_FILE = ""
HOST_CONFIG_CHANGED = false # set to true whenever host VFIO config is actually written
2026-04-05 11:24:08 +02:00
PRESELECT_VMID = ""
WIZARD_CALL = false
GPU_WIZARD_RESULT_FILE = ""
2026-04-06 13:39:07 +02:00
declare -a LXC_AFFECTED_CTIDS = ( )
declare -a LXC_AFFECTED_NAMES = ( )
declare -a LXC_AFFECTED_RUNNING = ( ) # 1 or 0
declare -a LXC_AFFECTED_ONBOOT = ( ) # 1 or 0
LXC_SWITCH_ACTION = "" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
2026-04-04 00:54:57 +02:00
# ==========================================================
# 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
}
2026-04-05 11:24:08 +02:00
_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"
}
2026-04-06 13:39:07 +02:00
_ct_is_running( ) {
local ctid = " $1 "
pct status " $ctid " 2>/dev/null | grep -q "status: running"
}
_ct_onboot_enabled( ) {
local ctid = " $1 "
pct config " $ctid " 2>/dev/null | grep -qE "^onboot:\s*1"
}
_lxc_conf_uses_selected_gpu( ) {
local conf = " $1 "
case " $SELECTED_GPU " in
nvidia)
grep -qE "dev[0-9]+:.*(/dev/nvidia|/dev/nvidia-caps)" " $conf " 2>/dev/null
; ;
amd)
grep -qE "dev[0-9]+:.*(/dev/dri|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" " $conf " 2>/dev/null
; ;
intel)
grep -qE "dev[0-9]+:.*(/dev/dri)|lxc\.mount\.entry:.*dev/dri" " $conf " 2>/dev/null
; ;
*)
grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" " $conf " 2>/dev/null
; ;
esac
}
_lxc_switch_action_label( ) {
case " $LXC_SWITCH_ACTION " in
keep_gpu_disable_onboot) echo " $( translate 'Keep GPU in LXC config + disable Start on boot' ) " ; ;
remove_gpu_keep_onboot) echo " $( translate 'Remove GPU from LXC config + keep Start on boot unchanged' ) " ; ;
*) echo " $( translate 'No specific LXC action selected' ) " ; ;
esac
}
2026-04-06 21:50:46 +02:00
_vm_switch_action_label( ) {
case " $SWITCH_VM_ACTION " in
keep_gpu_disable_onboot) echo " $( translate 'Keep GPU in source VM config + disable Start on boot if enabled' ) " ; ;
remove_gpu_keep_onboot) echo " $( translate 'Remove GPU from source VM config + keep Start on boot unchanged' ) " ; ;
*) echo " $( translate 'No specific VM action selected' ) " ; ;
esac
}
2026-04-05 11:24:08 +02:00
_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( ) {
2026-04-06 19:13:31 +02:00
if [ [ " $IOMMU_PENDING_REBOOT " = = "true" ] ] ; then
PREFLIGHT_HOST_REBOOT_REQUIRED = true
return 0
fi
2026-04-05 11:24:08 +02:00
# 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
2026-04-06 18:40:57 +02:00
_file_has_exact_line "blacklist nvidia_drm" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist nvidia_modeset" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist nvidia_uvm" " $blacklist_file " || needs_change = true
2026-04-05 11:24:08 +02:00
_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
2026-04-06 18:40:57 +02:00
[ [ -f /etc/modules-load.d/nvidia-vfio.conf ] ] && needs_change = true
grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null && needs_change = true
local svc
for svc in nvidia-persistenced.service nvidia-persistenced nvidia-powerd.service nvidia-fabricmanager.service; do
if systemctl is-active --quiet " $svc " 2>/dev/null || systemctl is-enabled --quiet " $svc " 2>/dev/null; then
needs_change = true
fi
done
2026-04-05 11:24:08 +02:00
; ;
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
}
2026-04-04 00:54:57 +02:00
_get_vm_run_title( ) {
if [ [ " $SWITCH_FROM_LXC " = = "true" && " $SWITCH_FROM_VM " = = "true" ] ] ; then
2026-04-05 11:24:08 +02:00
echo " $( translate 'GPU Passthrough to VM (reassign from LXC and another VM)' ) "
2026-04-04 00:54:57 +02:00
elif [ [ " $SWITCH_FROM_LXC " = = "true" ] ] ; then
2026-04-05 11:24:08 +02:00
echo " $( translate 'GPU Passthrough to VM (from LXC)' ) "
2026-04-04 00:54:57 +02:00
elif [ [ " $SWITCH_FROM_VM " = = "true" ] ] ; then
2026-04-05 11:24:08 +02:00
echo " $( translate 'GPU Passthrough to VM (reassign from another VM)' ) "
2026-04-04 00:54:57 +02:00
else
echo " $( translate 'GPU Passthrough to VM' ) "
fi
}
_is_pci_slot_assigned_to_vm( ) {
2026-04-05 11:24:08 +02:00
if declare -F _pci_slot_assigned_to_vm >/dev/null 2>& 1; then
_pci_slot_assigned_to_vm " $1 " " $2 "
return $?
fi
2026-04-04 00:54:57 +02:00
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( ) {
2026-04-05 11:24:08 +02:00
if declare -F _pci_function_assigned_to_vm >/dev/null 2>& 1; then
_pci_function_assigned_to_vm " $1 " " $2 "
return $?
fi
2026-04-04 00:54:57 +02:00
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
2026-04-06 21:12:13 +02:00
label = " ${ ALL_GPU_NAMES [ $i ] } [ ${ ALL_GPU_DRIVERS [ $i ] } ] — ${ pci } "
2026-04-04 00:54:57 +02:00
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
2026-04-06 22:34:37 +02:00
local choice
2026-04-06 22:51:30 +02:00
choice = $( dialog --stdout --clear --backtitle "ProxMenux" --colors \
2026-04-06 22:21:38 +02:00
--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 [@] } " \
2026-04-06 22:51:30 +02:00
2>/dev/tty) || exit 0
2026-04-04 00:54:57 +02:00
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
2026-04-06 21:12:13 +02:00
local pci_short pci_full name type driver pci_info
2026-04-04 00:54:57 +02:00
pci_short = $( echo " $line " | awk '{print $1}' )
pci_full = " 0000: ${ pci_short } "
2026-04-06 21:12:13 +02:00
# Prefer full vendor/model descriptor for clearer menus.
pci_info = $( lspci -nn -s " ${ pci_short } " 2>/dev/null | sed 's/^[^ ]* //' )
name = " ${ pci_info #* : } "
[ [ " $name " = = " $pci_info " ] ] && name = " $pci_info "
name = $( echo " $name " | sed -E 's/ \(rev [^)]+\)$//' | cut -c1-72)
[ [ -z " $name " ] ] && name = " $( translate 'Unknown GPU' ) "
2026-04-04 00:54:57 +02:00
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
2026-04-05 11:24:08 +02:00
_set_wizard_result "no_gpu"
2026-04-04 00:54:57 +02:00
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( ) {
2026-04-05 11:24:08 +02:00
if declare -F _pci_is_iommu_active >/dev/null 2>& 1 && _pci_is_iommu_active; then
return 0
fi
2026-04-06 19:13:31 +02:00
local configured_next_boot = false
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
configured_next_boot = true
fi
if [ [ " $configured_next_boot " = = "true" ] ] ; then
IOMMU_PENDING_REBOOT = true
HOST_CONFIG_CHANGED = true
dialog --backtitle "ProxMenux" \
--title " $( translate 'IOMMU Pending Reboot' ) " \
--msgbox " \n $( translate 'IOMMU is already configured for next boot, but it is not active yet.' ) \n\n $( translate 'GPU passthrough configuration will continue now and will become effective after host reboot.' ) " \
11 78
2026-04-04 00:54:57 +02:00
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 "
2026-04-06 19:13:31 +02:00
msg += " $( translate 'Configuration will continue now and be effective after reboot.' ) "
2026-04-04 00:54:57 +02:00
dialog --backtitle "ProxMenux" \
--title " $( translate 'IOMMU Required' ) " \
--yesno " $msg " 15 72
local response = $?
2026-04-05 11:24:08 +02:00
[ [ " $WIZARD_CALL " != "true" ] ] && clear
2026-04-04 00:54:57 +02:00
if [ [ $response -eq 0 ] ] ; then
2026-04-05 11:24:08 +02:00
[ [ " $WIZARD_CALL " != "true" ] ] && show_proxmenux_logo
2026-04-04 00:54:57 +02:00
msg_title " $( translate 'Enabling IOMMU' ) "
2026-04-06 19:13:31 +02:00
if ! _enable_iommu_cmdline; then
echo
msg_error " $( translate 'Failed to configure IOMMU automatically.' ) "
echo
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
exit 0
fi
IOMMU_PENDING_REBOOT = true
HOST_CONFIG_CHANGED = true
2026-04-04 00:54:57 +02:00
echo
2026-04-06 19:13:31 +02:00
msg_success " $( translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.' ) "
2026-04-04 00:54:57 +02:00
echo
2026-04-06 19:13:31 +02:00
if [ [ " $WIZARD_CALL " != "true" ] ] ; then
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
fi
return 0
2026-04-04 00:54:57 +02:00
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
2026-04-06 21:12:13 +02:00
local label = " ${ ALL_GPU_NAMES [ $i ] } [ ${ ALL_GPU_DRIVERS [ $i ] } ] — ${ ALL_GPU_PCIS [ $i ] } "
2026-04-04 00:54:57 +02:00
menu_items += ( " $i " " $label " )
done
2026-04-06 22:34:37 +02:00
local choice
2026-04-06 22:51:30 +02:00
choice = $( dialog --stdout --clear --backtitle "ProxMenux" --colors \
2026-04-06 22:21:38 +02:00
--title " $( translate 'Select GPU for VM Passthrough' ) " \
--menu " \n $( translate 'Select the GPU to pass through to the VM:' ) " \
18 82 10 \
" ${ menu_items [@] } " \
2026-04-06 22:51:30 +02:00
2>/dev/tty) || exit 0
2026-04-04 00:54:57 +02:00
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 "
2026-04-06 21:50:46 +02:00
msg += " \Z1 $( translate 'Important: some GPUs may still fail in passthrough and can affect host stability or overall performance depending on hardware/firmware quality.' ) \Zn\n\n "
2026-04-04 00:54:57 +02:00
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 "
2026-04-06 13:39:07 +02:00
local gpu_subtype reset_method power_state vendor device viddid
2026-04-04 00:54:57 +02:00
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:]' )
2026-04-06 13:39:07 +02:00
vendor = $( cat " /sys/bus/pci/devices/ ${ pci_full } /vendor " 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]' )
device = $( cat " /sys/bus/pci/devices/ ${ pci_full } /device " 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]' )
viddid = " ${ vendor } : ${ device } "
# ── BLOCKER: Known unsupported Intel Apollo Lake iGPU IDs ────────────
if [ [ " $viddid " = = "8086:5a84" || " $viddid " = = "8086:5a85" ] ] ; then
local msg
msg = " \n\Zb\Z1 $( translate 'GPU Not Compatible with VM Passthrough' ) \Zn\n\n "
msg += " $( translate 'The selected Intel GPU belongs to Apollo Lake generation and is blocked by policy for VM passthrough due to host instability risk.' ) \n\n "
msg += " ${ SELECTED_GPU_NAME } \n "
msg += " ${ SELECTED_GPU_PCI } \n "
msg += " \ZbID: ${ viddid } \Zn\n\n "
msg += " $( translate 'This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.' ) \n\n "
msg += " $( translate 'Recommended: use GPU with LXC workloads instead of VM passthrough on this hardware.' ) "
dialog --backtitle "ProxMenux" --colors \
--title " $( translate 'Blocked GPU ID' ) " \
--msgbox " $msg " 20 84
exit 0
fi
2026-04-04 00:54:57 +02:00
# ── 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" ] ] && \
2026-04-06 21:50:46 +02:00
msg += " • \Z1 $( translate 'Current power state: D3cold (device currently inaccessible)' ) \Zn\n "
2026-04-04 00:54:57 +02:00
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
2026-04-06 19:13:31 +02:00
if [ [ " $IOMMU_PENDING_REBOOT " = = "true" ] ] ; then
IOMMU_GROUP = "pending-reboot"
IOMMU_DEVICES = ( " $pci_full " )
IOMMU_VFIO_IDS = ( )
local vid did
vid = $( cat " /sys/bus/pci/devices/ ${ pci_full } /vendor " 2>/dev/null | sed 's/0x//' )
did = $( cat " /sys/bus/pci/devices/ ${ pci_full } /device " 2>/dev/null | sed 's/0x//' )
[ [ -n " $vid " && -n " $did " ] ] && IOMMU_VFIO_IDS += ( " ${ vid } : ${ did } " )
dialog --backtitle "ProxMenux" --colors \
--title " $( translate 'IOMMU Group Pending' ) " \
--msgbox " \n $( translate 'IOMMU groups are not available yet because reboot is pending.' ) \n\n $( translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.' ) \n\n $( translate 'Selected GPU function' ) :\n • ${ pci_full } " \
14 82
return 0
fi
2026-04-04 00:54:57 +02:00
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
2026-04-06 21:50:46 +02:00
msg += " \n\Z1 $( translate 'All devices in the same IOMMU group must be passed together.' ) \Zn "
2026-04-04 00:54:57 +02:00
fi
dialog --backtitle "ProxMenux" --colors \
--title " $( translate 'IOMMU Group' ) ${ IOMMU_GROUP } " \
--msgbox " \n ${ msg } " 22 82
}
2026-04-05 11:24:08 +02:00
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
}
2026-04-04 00:54:57 +02:00
# ==========================================================
# Phase 1 — Step 6: VM selection
# ==========================================================
select_vm( ) {
2026-04-05 11:24:08 +02:00
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
2026-04-04 00:54:57 +02:00
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
2026-04-06 22:51:30 +02:00
SELECTED_VMID = $( dialog --stdout --backtitle "ProxMenux" \
2026-04-04 00:54:57 +02:00
--title " $( translate 'Select Virtual Machine' ) " \
--menu " \n $( translate 'Select the VM to add the GPU to:' ) " \
20 72 12 \
" ${ menu_items [@] } " \
2026-04-06 22:51:30 +02:00
2>/dev/tty) || exit 0
2026-04-04 00:54:57 +02:00
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 ────────────────────────────────
2026-04-06 13:39:07 +02:00
LXC_AFFECTED_CTIDS = ( )
LXC_AFFECTED_NAMES = ( )
LXC_AFFECTED_RUNNING = ( )
LXC_AFFECTED_ONBOOT = ( )
LXC_SWITCH_ACTION = ""
2026-04-04 00:54:57 +02:00
local lxc_affected = ( )
2026-04-06 13:39:07 +02:00
local running_count = 0
local onboot_count = 0
2026-04-04 00:54:57 +02:00
for conf in /etc/pve/lxc/*.conf; do
[ [ -f " $conf " ] ] || continue
2026-04-06 13:39:07 +02:00
_lxc_conf_uses_selected_gpu " $conf " || continue
local ctid ct_name running_flag onboot_flag
ctid = $( basename " $conf " .conf)
ct_name = $( pct config " $ctid " 2>/dev/null | awk '/^hostname:/ {print $2}' )
[ [ -z " $ct_name " ] ] && ct_name = " CT- ${ ctid } "
running_flag = 0
onboot_flag = 0
_ct_is_running " $ctid " && running_flag = 1
_ct_onboot_enabled " $ctid " && onboot_flag = 1
LXC_AFFECTED_CTIDS += ( " $ctid " )
LXC_AFFECTED_NAMES += ( " $ct_name " )
LXC_AFFECTED_RUNNING += ( " $running_flag " )
LXC_AFFECTED_ONBOOT += ( " $onboot_flag " )
lxc_affected += ( " CT ${ ctid } ( ${ ct_name } ) " )
[ [ " $running_flag " = = "1" ] ] && running_count = $(( running_count + 1 ))
[ [ " $onboot_flag " = = "1" ] ] && onboot_count = $(( onboot_count + 1 ))
2026-04-04 00:54:57 +02:00
done
if [ [ ${# lxc_affected [@] } -gt 0 ] ] ; then
SWITCH_FROM_LXC = true
SWITCH_LXC_LIST = $( IFS = ', ' ; echo " ${ lxc_affected [*] } " )
2026-04-06 13:39:07 +02:00
local msg action_choice
msg = " \n $( translate 'The selected GPU is currently used by the following LXC container(s):' ) \n\n "
local i
for i in " ${ !LXC_AFFECTED_CTIDS[@] } " ; do
local status_txt onboot_txt
status_txt = " $( translate 'stopped' ) "
onboot_txt = "onboot=0"
[ [ " ${ LXC_AFFECTED_RUNNING [ $i ] } " = = "1" ] ] && status_txt = " $( translate 'running' ) "
[ [ " ${ LXC_AFFECTED_ONBOOT [ $i ] } " = = "1" ] ] && onboot_txt = "onboot=1"
msg += " • CT ${ LXC_AFFECTED_CTIDS [ $i ] } ( ${ LXC_AFFECTED_NAMES [ $i ] } ) [ ${ status_txt } , ${ onboot_txt } ]\n "
2026-04-04 00:54:57 +02:00
done
msg += " \n $( translate 'VM passthrough requires exclusive VFIO binding of the GPU.' ) \n "
2026-04-06 13:39:07 +02:00
msg += " $( translate 'Choose how to handle affected LXC containers before switching to VM mode.' ) \n\n "
[ [ " $running_count " -gt 0 ] ] && \
2026-04-06 21:50:46 +02:00
msg += " \Z1 $( translate 'Running containers detected' ) : ${ running_count } \Zn\n "
2026-04-06 13:39:07 +02:00
[ [ " $onboot_count " -gt 0 ] ] && \
msg += " \Z1\Zb $( translate 'Start on boot enabled (onboot=1)' ) : ${ onboot_count } \Zn\n "
2026-04-06 21:50:46 +02:00
msg += " \n\Z1 $( translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.' ) \Zn "
2026-04-06 13:39:07 +02:00
2026-04-06 22:51:30 +02:00
action_choice = $( dialog --stdout --backtitle "ProxMenux" --colors \
2026-04-04 00:54:57 +02:00
--title " $( translate 'GPU Used in LXC Containers' ) " \
2026-04-06 13:39:07 +02:00
--default-item "2" \
--menu " $msg " 25 96 8 \
"1" " $( translate 'Keep GPU in LXC config (disable Start on boot)' ) " \
"2" " $( translate 'Remove GPU from LXC config (keep Start on boot)' ) " \
2026-04-06 22:51:30 +02:00
2>/dev/tty) || exit 0
2026-04-06 13:39:07 +02:00
case " $action_choice " in
1) LXC_SWITCH_ACTION = "keep_gpu_disable_onboot" ; ;
2) LXC_SWITCH_ACTION = "remove_gpu_keep_onboot" ; ;
*) exit 0 ; ;
esac
else
SWITCH_FROM_LXC = false
2026-04-04 00:54:57 +02:00
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
2026-04-05 11:24:08 +02:00
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
2026-04-04 00:54:57 +02:00
SWITCH_FROM_VM = true
SWITCH_VM_SRC = " $vm_src_id "
2026-04-06 21:50:46 +02:00
SWITCH_VM_ACTION = "remove_gpu_keep_onboot"
2026-04-05 11:24:08 +02:00
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"
2026-04-04 00:54:57 +02:00
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 "
2026-04-05 11:24:08 +02:00
msg += " $( translate 'That VM is currently stopped, so the GPU can be reassigned now.' ) \n "
2026-04-06 21:50:46 +02:00
msg += " \Z1\Zb $( translate 'Important: both VMs cannot be running at the same time with the same GPU.' ) \Zn\n\n "
msg += " $( translate 'Target VM' ) : VM ${ SELECTED_VMID } ( ${ VM_NAME :- VM - ${ SELECTED_VMID } } )\n "
msg += " $( translate 'Source VM' ) : VM ${ vm_src_id } ( ${ vm_src_name :- VM - ${ vm_src_id } } )\n\n "
2026-04-05 11:24:08 +02:00
if [ [ " $src_onboot " = = "1" && " $target_onboot " = = "1" ] ] ; then
2026-04-06 21:50:46 +02:00
msg += " \Z1\Zb $( translate 'Warning: both VMs have autostart enabled (onboot=1).' ) \Zn\n "
msg += " \Z1\Zb $( translate 'Disable autostart on one VM to avoid startup conflicts.' ) \Zn\n\n "
2026-04-05 11:24:08 +02:00
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
2026-04-06 21:50:46 +02:00
msg += " $( translate 'Choose conflict policy for the source VM:' ) "
2026-04-04 00:54:57 +02:00
2026-04-06 22:34:37 +02:00
local vm_action_choice
2026-04-06 22:51:30 +02:00
vm_action_choice = $( dialog --stdout --clear --backtitle "ProxMenux" --colors \
2026-04-06 22:21:38 +02:00
--title " $( translate 'GPU Already Assigned to Another VM' ) " \
--default-item "1" \
--menu " $msg " 24 98 8 \
"1" " $( translate 'Keep GPU in source VM config (disable Start on boot if enabled)' ) " \
"2" " $( translate 'Remove GPU from source VM config (keep Start on boot)' ) " \
2026-04-06 22:51:30 +02:00
2>/dev/tty) || exit 0
2026-04-06 21:50:46 +02:00
case " $vm_action_choice " in
1) SWITCH_VM_ACTION = "keep_gpu_disable_onboot" ; ;
2) SWITCH_VM_ACTION = "remove_gpu_keep_onboot" ; ;
*) exit 0 ; ;
esac
2026-04-04 00:54:57 +02:00
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 "
2026-04-06 19:13:31 +02:00
if [ [ " $IOMMU_PENDING_REBOOT " = = "true" ] ] ; then
msg += " $( translate 'IOMMU Group' ) : $( translate 'pending (reboot required to enumerate full group)' ) \n "
else
msg += " $( translate 'IOMMU Group' ) : ${ IOMMU_GROUP } ( ${# IOMMU_DEVICES [@] } $( translate 'devices' ) )\n "
fi
2026-04-04 00:54:57 +02:00
msg += " $( translate 'Target VM' ) : ${ VM_NAME :- VM - ${ SELECTED_VMID } } ( ${ SELECTED_VMID } )\n "
msg += " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
msg += " \Zb $( translate 'Host' ) :\Zn\n "
2026-04-05 11:24:08 +02:00
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
2026-04-04 00:54:57 +02:00
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 "
2026-04-06 19:13:31 +02:00
if [ [ " $IOMMU_PENDING_REBOOT " = = "true" ] ] ; then
msg += " • $( translate 'hostpci entries for selected GPU functions (full IOMMU group will be enforced after reboot)' ) \n "
else
msg += " • $( translate 'hostpci entries for all IOMMU group devices' ) \n "
fi
2026-04-05 11:24:08 +02:00
[ [ ${# EXTRA_AUDIO_DEVICES [@] } -gt 0 ] ] && \
msg += " • $( translate 'Additional GPU audio function will be added' ) : ${ EXTRA_AUDIO_DEVICES [*] } \n "
2026-04-04 00:54:57 +02:00
[ [ " $SELECTED_GPU " = = "nvidia" ] ] && \
msg += " • $( translate 'NVIDIA KVM hiding (cpu hidden=1)' ) \n "
2026-04-06 13:39:07 +02:00
if [ [ " $SWITCH_FROM_LXC " = = "true" ] ] ; then
2026-04-06 21:50:46 +02:00
msg += " \n \Z1• $( translate 'Affected LXC containers' ) : ${ SWITCH_LXC_LIST } \Zn\n "
msg += " \Z1• $( translate 'Selected LXC action' ) : $( _lxc_switch_action_label) \Zn\n "
2026-04-06 13:39:07 +02:00
if [ [ " $LXC_SWITCH_ACTION " = = "remove_gpu_keep_onboot" ] ] ; then
2026-04-06 21:50:46 +02:00
msg += " \Z1• $( translate 'To use the GPU again in LXC, run Add GPU to LXC from GPUs and Coral-TPU Menu' ) \Zn\n "
2026-04-06 13:39:07 +02:00
fi
fi
2026-04-06 21:50:46 +02:00
if [ [ " $SWITCH_FROM_VM " = = "true" ] ] ; then
if [ [ " $SWITCH_VM_ACTION " = = "keep_gpu_disable_onboot" ] ] ; then
msg += " \n \Z1• $( translate 'GPU will remain configured in source VM' ) : ${ SWITCH_VM_SRC } \Zn\n "
msg += " \Z1• $( translate 'Start on boot will be disabled on source VM only if currently enabled' ) \Zn\n "
msg += " \Z1• $( translate 'GPU guard hook will block concurrent start when another VM is already using this GPU' ) \Zn\n "
else
msg += " \n \Z1• $( translate 'GPU will be removed from source VM config' ) : ${ SWITCH_VM_SRC } \Zn\n "
fi
msg += " \Z1• $( translate 'Selected VM action' ) : $( _vm_switch_action_label) \Zn\n "
fi
2026-04-04 00:54:57 +02:00
msg += " \n $( translate 'Do you want to proceed?' ) "
local run_title
run_title = $( _get_vm_run_title)
2026-04-06 22:26:54 +02:00
dialog --clear --backtitle "ProxMenux" --colors \
2026-04-06 22:21:38 +02:00
--title " ${ run_title } " \
--yesno " $msg " 28 78
2026-04-06 22:34:37 +02:00
[ [ $? -ne 0 ] ] && exit 0
2026-04-04 00:54:57 +02:00
}
# ==========================================================
# 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 "
2026-04-06 18:40:57 +02:00
_add_line_if_missing "blacklist nvidia_drm" " $blacklist_file "
_add_line_if_missing "blacklist nvidia_modeset" " $blacklist_file "
_add_line_if_missing "blacklist nvidia_uvm" " $blacklist_file "
2026-04-04 00:54:57 +02:00
_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 "
}
2026-04-06 18:40:57 +02:00
sanitize_nvidia_host_stack_for_vfio( ) {
msg_info " $( translate 'Sanitizing NVIDIA host services for VFIO mode...' ) "
local changed = false
local state_dir = "/var/lib/proxmenux"
local state_file = " ${ state_dir } /nvidia-host-services.state "
local svc
local -a services = (
"nvidia-persistenced.service"
"nvidia-powerd.service"
"nvidia-fabricmanager.service"
)
mkdir -p " $state_dir " >/dev/null 2>& 1 || true
: > " $state_file "
for svc in " ${ services [@] } " ; do
local was_enabled = 0 was_active = 0
if systemctl is-enabled --quiet " $svc " 2>/dev/null; then
was_enabled = 1
fi
if systemctl is-active --quiet " $svc " 2>/dev/null; then
was_active = 1
fi
if ( ( was_enabled = = 1 || was_active = = 1 ) ) ; then
echo " ${ svc } enabled= ${ was_enabled } active= ${ was_active } " >>" $state_file "
fi
if systemctl is-active --quiet " $svc " 2>/dev/null; then
systemctl stop " $svc " >>" $LOG_FILE " 2>& 1 || true
changed = true
fi
if systemctl is-enabled --quiet " $svc " 2>/dev/null; then
systemctl disable " $svc " >>" $LOG_FILE " 2>& 1 || true
changed = true
fi
done
[ [ -s " $state_file " ] ] || rm -f " $state_file "
if [ [ -f /etc/modules-load.d/nvidia-vfio.conf ] ] ; then
mv /etc/modules-load.d/nvidia-vfio.conf /etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio >>" $LOG_FILE " 2>& 1 || true
changed = true
fi
if grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null; then
sed -i '/^nvidia$/d;/^nvidia_uvm$/d;/^nvidia_drm$/d;/^nvidia_modeset$/d' /etc/modules
changed = true
fi
if $changed ; then
HOST_CONFIG_CHANGED = true
msg_ok " $( translate 'NVIDIA host services/autoload disabled for VFIO mode' ) " | tee -a " $screen_capture "
else
msg_ok " $( translate 'NVIDIA host services/autoload already aligned for VFIO mode' ) " | tee -a " $screen_capture "
fi
}
2026-04-04 00:54:57 +02:00
# ── 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.' ) "
}
2026-04-06 13:39:07 +02:00
_remove_selected_gpu_from_lxc_conf( ) {
local conf = " $1 "
case " $SELECTED_GPU " in
nvidia)
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' " $conf "
; ;
amd)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' " $conf "
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' " $conf "
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' " $conf "
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' " $conf "
; ;
intel)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' " $conf "
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' " $conf "
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' " $conf "
; ;
*)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' " $conf "
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' " $conf "
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' " $conf "
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' " $conf "
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' " $conf "
; ;
esac
}
# ── Apply selected action for affected LXC (switch mode) ─
2026-04-04 00:54:57 +02:00
cleanup_lxc_configs( ) {
[ [ " $SWITCH_FROM_LXC " != "true" ] ] && return 0
2026-04-06 13:39:07 +02:00
[ [ ${# LXC_AFFECTED_CTIDS [@] } -eq 0 ] ] && return 0
2026-04-04 00:54:57 +02:00
2026-04-06 13:39:07 +02:00
msg_info " $( translate 'Applying selected LXC switch action...' ) "
local i
for i in " ${ !LXC_AFFECTED_CTIDS[@] } " ; do
local ctid conf
ctid = " ${ LXC_AFFECTED_CTIDS [ $i ] } "
conf = " /etc/pve/lxc/ ${ ctid } .conf "
if [ [ " ${ LXC_AFFECTED_RUNNING [ $i ] } " = = "1" ] ] ; then
msg_info " $( translate 'Stopping LXC' ) ${ ctid } ... "
if pct stop " $ctid " >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'LXC stopped' ) ${ ctid } " | tee -a " $screen_capture "
else
msg_warn " $( translate 'Could not stop LXC' ) ${ ctid } " | tee -a " $screen_capture "
fi
else
msg_ok " $( translate 'LXC already stopped' ) ${ ctid } " | tee -a " $screen_capture "
fi
if [ [ " $LXC_SWITCH_ACTION " = = "keep_gpu_disable_onboot" ] ] ; then
if [ [ " ${ LXC_AFFECTED_ONBOOT [ $i ] } " = = "1" ] ] ; then
if pct set " $ctid " -onboot 0 >>" $LOG_FILE " 2>& 1; then
msg_warn " $( translate 'Start on boot disabled for LXC' ) ${ ctid } " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to disable Start on boot for LXC' ) ${ ctid } " | tee -a " $screen_capture "
fi
fi
fi
if [ [ " $LXC_SWITCH_ACTION " = = "remove_gpu_keep_onboot" && -f " $conf " ] ] ; then
_remove_selected_gpu_from_lxc_conf " $conf "
msg_ok " $( translate 'GPU access removed from LXC' ) ${ ctid } " | tee -a " $screen_capture "
2026-04-04 00:54:57 +02:00
fi
done
2026-04-06 13:39:07 +02:00
if [ [ " $LXC_SWITCH_ACTION " = = "remove_gpu_keep_onboot" ] ] ; then
msg_warn " $( translate 'If needed again, re-add GPU to LXC from GPUs and Coral-TPU Menu → Add GPU to LXC.' ) " | tee -a " $screen_capture "
fi
2026-04-04 00:54:57 +02:00
}
# ── 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
2026-04-06 21:50:46 +02:00
if [ [ " $VM_SWITCH_ACTION " = = "keep_gpu_disable_onboot" ] ] ; then
msg_info " $( translate 'Keeping GPU in source VM config' ) ${ SWITCH_VM_SRC } ... "
if _vm_onboot_enabled " $SWITCH_VM_SRC " ; then
if qm set " $SWITCH_VM_SRC " -onboot 0 >>" $LOG_FILE " 2>& 1; then
msg_warn " $( translate 'Start on boot disabled for VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to disable Start on boot for VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
fi
else
msg_ok " $( translate 'Start on boot already disabled for VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
fi
msg_ok " $( translate 'GPU kept in source VM config' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
return 0
fi
2026-04-04 00:54:57 +02: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
2026-04-05 11:24:08 +02:00
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
2026-04-04 00:54:57 +02:00
# 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
2026-04-06 22:51:30 +02:00
if qm set " $SELECTED_VMID " --hostpci${ idx } " ${ SELECTED_GPU_PCI } , ${ gpu_opts } " >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'GPU added' ) : hostpci ${ idx } : ${ SELECTED_GPU_PCI } , ${ gpu_opts } " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to add GPU to target VM' ) : ${ SELECTED_GPU_PCI } " | tee -a " $screen_capture "
return 1
fi
2026-04-04 00:54:57 +02:00
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
2026-04-06 22:51:30 +02:00
if qm set " $SELECTED_VMID " --hostpci${ idx } " ${ dev } ,pcie=1 " >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'Device added' ) : hostpci ${ idx } : ${ dev } ,pcie=1 " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to add IOMMU group device' ) : ${ dev } " | tee -a " $screen_capture "
return 1
fi
2026-04-04 00:54:57 +02:00
idx = $(( idx + 1 ))
done
2026-04-05 11:24:08 +02:00
# 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
2026-04-06 22:51:30 +02:00
if qm set " $SELECTED_VMID " --hostpci${ idx } " ${ dev } ,pcie=1 " >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'GPU audio added' ) : hostpci ${ idx } : ${ dev } ,pcie=1 " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to add GPU audio function' ) : ${ dev } " | tee -a " $screen_capture "
return 1
fi
2026-04-05 11:24:08 +02:00
idx = $(( idx + 1 ))
done
2026-04-04 00:54:57 +02:00
# NVIDIA: hide KVM hypervisor from guest
[ [ " $SELECTED_GPU " = = "nvidia" ] ] && _configure_nvidia_kvm_hide
2026-04-06 22:51:30 +02:00
return 0
2026-04-04 00:54:57 +02:00
}
_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( ) {
2026-04-05 11:24:08 +02:00
parse_cli_args " $@ "
2026-04-04 00:54:57 +02:00
: >" $LOG_FILE "
: >" $screen_capture "
2026-04-05 11:24:08 +02:00
[ [ " $WIZARD_CALL " = = "true" ] ] && _set_wizard_result "cancelled"
2026-04-04 00:54:57 +02:00
# ── 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
2026-04-05 11:24:08 +02:00
detect_optional_gpu_audio
2026-04-04 00:54:57 +02:00
check_vm_machine_type
check_switch_mode
2026-04-05 11:24:08 +02:00
evaluate_host_reboot_requirement
2026-04-04 00:54:57 +02:00
confirm_summary
# ── Phase 2: processing ───────────────────────────────
local run_title
run_title = $( _get_vm_run_title)
2026-04-05 11:24:08 +02:00
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
2026-04-06 18:40:57 +02:00
[ [ " $SELECTED_GPU " = = "nvidia" ] ] && sanitize_nvidia_host_stack_for_vfio
2026-04-04 00:54:57 +02:00
cleanup_lxc_configs
cleanup_vm_config
ensure_vm_display_std
2026-04-06 22:51:30 +02:00
if ! configure_vm; then
msg_error " $( translate 'VM passthrough configuration failed. Review the log and VM config.' ) "
[ [ " $WIZARD_CALL " = = "true" ] ] && _set_wizard_result "failed"
rm -f " $screen_capture "
exit 1
fi
2026-04-06 13:39:07 +02:00
if declare -F attach_proxmenux_gpu_guard_to_vm >/dev/null 2>& 1; then
ensure_proxmenux_gpu_guard_hookscript
attach_proxmenux_gpu_guard_to_vm " $SELECTED_VMID "
sync_proxmenux_gpu_guard_hooks
fi
2026-04-04 00:54:57 +02:00
[ [ " $HOST_CONFIG_CHANGED " = = "true" ] ] && update_initramfs_host
# ── Phase 3: summary ─────────────────────────────────
2026-04-05 11:24:08 +02:00
if [ [ " $WIZARD_CALL " = = "true" ] ] ; then
echo
else
show_proxmenux_logo
msg_title " ${ run_title } "
cat " $screen_capture "
echo
fi
2026-04-04 00:54:57 +02:00
2026-04-05 11:24:08 +02:00
if [ [ " $WIZARD_CALL " = = "true" ] ] ; then
2026-04-06 18:40:57 +02:00
if [ [ " $HOST_CONFIG_CHANGED " = = "true" ] ] ; then
_set_wizard_result "applied_reboot_required"
else
_set_wizard_result "applied"
fi
2026-04-05 11:24:08 +02:00
rm -f " $screen_capture "
return 0
fi
2026-04-04 00:54:57 +02:00
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB } ${ BL } 📄 Log: ${ LOG_FILE } ${ CL } "
2026-04-04 00:54:57 +02:00
if [ [ " $HOST_CONFIG_CHANGED " = = "true" ] ] ; then
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB } ${ DGN } - $( translate 'Host VFIO configuration changed — reboot required before starting the VM.' ) ${ CL } "
2026-04-04 00:54:57 +02:00
else
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB } ${ DGN } - $( translate 'Host VFIO config was already up to date — no reboot needed.' ) ${ CL } "
2026-04-04 00:54:57 +02:00
fi
case " $SELECTED_GPU " in
nvidia)
2026-04-05 11:24:08 +02:00
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 } "
2026-04-04 00:54:57 +02:00
; ;
amd)
2026-04-05 11:24:08 +02:00
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 } "
2026-04-04 00:54:57 +02:00
[ [ -n " $AMD_ROM_FILE " ] ] && \
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB } ${ DGN } - $( translate 'ROM file used' ) : /usr/share/kvm/ ${ AMD_ROM_FILE } ${ CL } "
2026-04-04 00:54:57 +02:00
; ;
intel)
2026-04-05 11:24:08 +02:00
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 } "
2026-04-04 00:54:57 +02:00
; ;
esac
2026-04-05 11:24:08 +02:00
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.' ) "
2026-04-04 00:54:57 +02:00
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
}
2026-04-05 11:24:08 +02:00
main " $@ "