diff --git a/src/display.sh b/src/display.sh new file mode 100644 index 00000000..ed5b633c --- /dev/null +++ b/src/display.sh @@ -0,0 +1,1256 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Docker environment variables + +: "${GPU:="N"}" # GPU acceleration +: "${VGA:="virtio"}" # VGA adapter +: "${DISPLAY:="web"}" # Display type +: "${LOSSY:="N"}" # Lossy VNC compression +: "${VNC_PORT:="5900"}" # VNC port +: "${RENDERNODE:=""}" # Render node +: "${VRAM_SIZE:="4G"}" # VirtIO GPU memory budget + +# Sanitize variables +VGA=$(strip "$VGA") +LOSSY=$(strip "$LOSSY") +DISPLAY=$(strip "$DISPLAY") +VNC_PORT=$(strip "$VNC_PORT") +RENDERNODE=$(strip "$RENDERNODE") +VRAM_SIZE=$(strip "$VRAM_SIZE") +WSS_SOCKET="${WSS_SOCKET:-$QEMU_DIR/vnc-ws.sock}" + +VGA_DEVICE="${VGA%%,*}" +VGA_OPTIONS="${VGA#"$VGA_DEVICE"}" + +case "${VGA_DEVICE,,}" in + "std" | "vga" ) + VGA_DEVICE="VGA" + VGA_ARG="-device" ;; + "vmware" | "vmware-svga" ) + VGA_DEVICE="vmware-svga" + VGA_ARG="-device" ;; + "virtio" ) + VGA_DEVICE="virtio-vga" + VGA_ARG="-device" ;; + "virtio-"* ) + VGA_DEVICE="${VGA_DEVICE,,}" + VGA_ARG="-device" ;; + * ) + VGA_ARG="-vga" ;; +esac + +VGA="${VGA_DEVICE}${VGA_OPTIONS}" +VGA_ARG+=" ${VGA}" + +# QEMU accepts a VNC display number rather than a TCP port, +# so translate the configured port back to its :N display index. +port=$(( VNC_PORT - 5900 )) + +LOSSY_OPT="" +enabled "$LOSSY" && LOSSY_OPT=",lossy=on" + +# Preserve the historic :0 setting as an alias for the managed web display. +[[ "$DISPLAY" == ":0" ]] && DISPLAY="web" + +case "${DISPLAY,,}" in + + "vnc" ) + DISPLAY_OPTS="-display vnc=:${port}${LOSSY_OPT} ${VGA_ARG}" ;; + "web" ) + DISPLAY_OPTS="-display vnc=:${port},websocket=unix:${WSS_SOCKET}${LOSSY_OPT} ${VGA_ARG}" ;; + "disabled" ) + DISPLAY_OPTS="-display none ${VGA_ARG}" ;; + "none" ) + DISPLAY_OPTS="-display none -vga none" ;; + *) + DISPLAY_OPTS="-display ${DISPLAY} ${VGA_ARG}" ;; + +esac + +gpuFallbackAllowed() { + + local app="${APP:-}" + + case "${app,,}" in + "chrome"* ) return 0 ;; + esac + + return 1 +} + +gpuSetupFailure() { + + local reason="$1" + + if gpuFallbackAllowed; then + warn "$reason; falling back to software rendering." + return 0 + fi + + error "$reason" + exit 87 +} + +vmwareGpuSetupFailure() { + + local reason="$1" + + error "$reason" + exit 87 +} + +vmwareLibraryPath() { + + local library="$1" + local path="" + + if command -v ldconfig >/dev/null 2>&1; then + path="$(ldconfig -p 2>/dev/null | awk -v library="$library" '$1 == library && !found { print $NF; found = 1 }')" + if [ -n "$path" ] && [ -r "$path" ]; then + printf '%s\n' "$path" + return 0 + fi + fi + + for path in /usr/lib/*/"$library" /usr/lib/"$library" /usr/lib64/"$library" \ + /usr/local/lib/*/"$library" /usr/local/lib/"$library" /usr/local/lib64/"$library"; do + [ -r "$path" ] || continue + printf '%s\n' "$path" + return 0 + done + + return 1 +} + +vmwareLibraryReady() { + + local library="$1" + local path="" + local dependencies="" + VMWARE_LIBRARY_REASON="" + + if ! path="$(vmwareLibraryPath "$library")"; then + VMWARE_LIBRARY_REASON="the $library runtime library is not available in the container" + return 1 + fi + + if command -v ldd >/dev/null 2>&1; then + dependencies="$(ldd "$path" 2>&1 || true)" + if grep -q '=> not found' <<< "$dependencies"; then + VMWARE_LIBRARY_REASON="$library has missing runtime dependencies" + return 1 + fi + fi + + return 0 +} + +# VMVGA lets DXVK select the Vulkan device itself, so this render node is only +# a host-GPU sanity check and is never passed to QEMU or DXVK. +vmwareRenderNodeReady() { + + local node gpu_fd + VMWARE_RENDER_NODE="" + VMWARE_RENDER_NODE_FOUND="N" + VMWARE_RENDER_REASON="" + + if [ ! -d /dev/dri ]; then + VMWARE_RENDER_REASON="'/dev/dri' was not added to the devices section of your compose file" + return 1 + fi + + for node in /dev/dri/renderD*; do + + [ -e "$node" ] || continue + VMWARE_RENDER_NODE_FOUND="Y" + [ -c "$node" ] || continue + + if { exec {gpu_fd}<>"$node"; } 2>/dev/null; then + { exec {gpu_fd}>&-; } 2>/dev/null || true + VMWARE_RENDER_NODE="$node" + return 0 + fi + + done + + if [[ "$VMWARE_RENDER_NODE_FOUND" != "Y" ]]; then + VMWARE_RENDER_REASON="/dev/dri is available, but no GPU render nodes were found" + else + VMWARE_RENDER_REASON="no accessible GPU render node was found" + fi + + return 1 +} + +vmwareVersionAtLeast() { + + local version="$1" + local required_major="$2" + local required_minor="$3" + local major minor + + IFS='.' read -r major minor _ <<< "$version" + [[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]] || return 1 + + (( major > required_major || (major == required_major && minor >= required_minor) )) +} + +# Match the VMVGA renderer preflight: Vulkan 1.3, its headless WSI instance +# extensions, and the device extensions required by the DXVK D3D9 backend. +vmwareVulkanReady() { + + local summary details loader selected extension + local api type name driver + VMWARE_VULKAN_REASON="" + + if ! command -v vulkaninfo >/dev/null 2>&1; then + VMWARE_VULKAN_REASON="vulkaninfo is not available in the container" + return 1 + fi + + if ! summary="$(vulkaninfo --summary 2>&1)"; then + VMWARE_VULKAN_REASON="Vulkan device enumeration failed" + return 1 + fi + + loader="$(sed -n 's/^Vulkan Instance Version:[[:space:]]*//p' <<< "$summary")" + if [ -z "$loader" ] || ! vmwareVersionAtLeast "$loader" 1 3; then + VMWARE_VULKAN_REASON="Vulkan 1.3 or newer is required by VMVGA" + return 1 + fi + + selected="$(awk ' + function emit() { + if (found || !in_gpu || api == "" || type == "") { + return + } + + split(api, version, ".") + if (type != "PHYSICAL_DEVICE_TYPE_CPU" && + (version[1] > 1 || (version[1] == 1 && version[2] >= 3))) { + print api "|" type "|" name "|" driver + found = 1 + } + } + + /^GPU[0-9]+:/ { + emit() + in_gpu = 1 + api = "" + type = "" + name = "" + driver = "" + next + } + + in_gpu && /^[[:space:]]*apiVersion[[:space:]]*=/ { + api = $0 + sub(/^.*=[[:space:]]*/, "", api) + if (match(api, /\([0-9]+\.[0-9]+(\.[0-9]+)?\)/)) { + api = substr(api, RSTART + 1, RLENGTH - 2) + } else { + sub(/[[:space:]].*$/, "", api) + } + next + } + + in_gpu && /^[[:space:]]*deviceType[[:space:]]*=/ { + type = $0 + sub(/^.*=[[:space:]]*/, "", type) + sub(/[[:space:]].*$/, "", type) + next + } + + in_gpu && /^[[:space:]]*deviceName[[:space:]]*=/ { + name = $0 + sub(/^.*=[[:space:]]*/, "", name) + next + } + + in_gpu && /^[[:space:]]*driverName[[:space:]]*=/ { + driver = $0 + sub(/^.*=[[:space:]]*/, "", driver) + next + } + + END { emit() } + ' <<< "$summary")" + + if [ -z "$selected" ]; then + VMWARE_VULKAN_REASON="no Vulkan 1.3 capable hardware device was found" + return 1 + fi + + IFS='|' read -r api type name driver <<< "$selected" + + if ! details="$(vulkaninfo 2>&1)"; then + VMWARE_VULKAN_REASON="Vulkan capability enumeration failed" + return 1 + fi + + for extension in VK_KHR_surface VK_EXT_headless_surface; do + if ! grep -Eq "^[[:space:]]*${extension}[[:space:]:]" <<< "$details"; then + VMWARE_VULKAN_REASON="the Vulkan loader does not support $extension required by VMVGA" + return 1 + fi + done + + for extension in VK_KHR_maintenance5 VK_EXT_robustness2; do + if ! grep -Eq "^[[:space:]]*${extension}[[:space:]:]" <<< "$details"; then + VMWARE_VULKAN_REASON="no Vulkan device exposes $extension required by VMVGA" + return 1 + fi + done + + VMWARE_VULKAN_API="$api" + VMWARE_VULKAN_DEVICE="$name" + VMWARE_VULKAN_DRIVER="$driver" + return 0 +} + +vmwareGpuSetup() { + + VMWARE_LIBRARY_REASON="" + VMWARE_RENDER_REASON="" + VMWARE_VULKAN_REASON="" + + if ! vmwareRenderNodeReady; then + vmwareGpuSetupFailure "VMware GPU acceleration requires a usable host GPU, but $VMWARE_RENDER_REASON" + fi + + local library + for library in libvulkan.so.1 libdxvk_d3d9.so.0 libdxvk_d3d11.so.0; do + if ! vmwareLibraryReady "$library"; then + vmwareGpuSetupFailure "VMware GPU acceleration is unavailable because $VMWARE_LIBRARY_REASON" + fi + done + + if ! vmwareVulkanReady; then + vmwareGpuSetupFailure "VMware GPU acceleration is unavailable because $VMWARE_VULKAN_REASON" + fi + + echo + info "Hardware rendering enabled:" + info + info "Device: ${VMWARE_VULKAN_DEVICE:-GPU}" + [ -n "${VMWARE_VULKAN_DRIVER:-}" ] && info "Driver: $VMWARE_VULKAN_DRIVER" + info "Vulkan: $VMWARE_VULKAN_API" + info "Backend: DXVK" + info "Render: automatic" + info + + return 0 +} + +enabled "$GPU" || return 0 + +msg="Configuring display drivers..." +enabled "$DEBUG" && echo "$msg" + +if [[ "$ARCH" != "amd64" ]]; then + if [[ "${VGA_DEVICE,,}" == "vmware-svga" ]]; then + vmwareGpuSetupFailure "GPU acceleration is only supported for the AMD64 platform" + fi + gpuSetupFailure "GPU acceleration is only supported for the AMD64 platform" + return 0 +fi + +if [[ "${VGA_DEVICE,,}" == "vmware-svga" ]]; then + vmwareGpuSetup + return 0 +fi + +if [[ "${BOOT_MODE:-}" == "windows_legacy" ]]; then + gpuSetupFailure "GPU acceleration is not supported by your Windows version" + return 0 +fi + +case "${VGA_DEVICE,,}" in + "none" ) + VGA_DEVICE="virtio-gpu-gl" ;; + "virtio-vga" ) + VGA_DEVICE="virtio-vga-gl" ;; + "virtio-gpu" ) + VGA_DEVICE="virtio-gpu-gl" ;; + "virtio-vga-gl"* | "virtio-gpu-gl"* ) ;; + * ) + gpuSetupFailure "GPU acceleration requires a VirtIO GPU display, but VGA='$VGA'" + return 0 ;; +esac + +VGA="${VGA_DEVICE}${VGA_OPTIONS}" + +VRAM_SIZE="${VRAM_SIZE// /}" +[ -z "$VRAM_SIZE" ] && VRAM_SIZE="4G" + +# Match the size conventions used by RAM_SIZE: small bare values are GiB, +# while larger bare values remain MiB for compatibility with numeric settings. +if [ -z "${VRAM_SIZE//[0-9. ]}" ]; then + [ "${VRAM_SIZE%%.*}" -lt "130" ] && VRAM_SIZE="${VRAM_SIZE}G" || VRAM_SIZE="${VRAM_SIZE}M" +fi + +VRAM_SIZE=$(echo "${VRAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g') +if ! VRAM_BYTES=$(numfmt --from=iec "$VRAM_SIZE" 2>/dev/null); then + error "Invalid VRAM_SIZE: $VRAM_SIZE" + exit 16 +fi + +# The host-visible PCI aperture must be a positive power-of-two size. Requiring +# whole MiB also keeps the renderer's advertised VRAM value exact. +if (( VRAM_BYTES < 1048576 || (VRAM_BYTES & (VRAM_BYTES - 1)) != 0 )); then + error "VRAM_SIZE must be a power-of-two size of at least 1M: $VRAM_SIZE" + exit 16 +fi + +VKR_DEVICE_MEMORY_LIMIT_BYTES="$VRAM_BYTES" +override_vram_size="$(( VRAM_BYTES / 1048576 ))" +export VKR_DEVICE_MEMORY_LIMIT_BYTES override_vram_size + +# Return the PCI vendor for a usable DRM render node. Any malformed, missing, +# inaccessible or disappearing node is rejected before hardware rendering is enabled. + +gpuNodeVendor() { + + local node="$1" + + local render_name="${node##*/}" + [[ "$render_name" =~ ^renderD[0-9]{3}$ ]] || return 1 + + local render_number="${render_name#renderD}" + (( 10#$render_number >= 128 )) || return 1 + [ -c "$node" ] || return 1 + + local gpu_fd + if ! { exec {gpu_fd}<>"$node"; } 2>/dev/null; then + return 1 + fi + + { exec {gpu_fd}>&-; } 2>/dev/null || true + + local vendor_file="/sys/class/drm/${render_name}/device/vendor" + [ -r "$vendor_file" ] || return 1 + + if ! IFS= read -r GPU_VENDOR < "$vendor_file"; then + return 1 + fi + + GPU_VENDOR="${GPU_VENDOR,,}" + return 0 +} + +# qemu-render omits Mesa's legacy i915 Gallium driver. Reject the Gen3 Intel +# devices that have no Crocus or Iris fallback before enabling EGL rendering. + +intelMesaReady() { + + local node="$1" + local render_name="${node##*/}" + local device="" + local device_file="/sys/class/drm/${render_name}/device/device" + + [ -r "$device_file" ] || return 1 + IFS= read -r device < "$device_file" || return 1 + + case "${device,,}" in + "0x2582" | "0x258a" | "0x2592" | "0x2772" | "0x27a2" | "0x27ae" | \ + "0x29b2" | "0x29c2" | "0x29d2" | "0xa001" | "0xa011" ) return 1 ;; + esac + + return 0 +} + +# Venus requires a Vulkan userspace driver in addition to the normal EGL/GBM +# rendering path. Keep this separate so normal VirGL/OpenGL does not require Vulkan. + +venusEnabled() { + + [[ ",${VGA,,}," =~ ,venus=(on|true|yes|1), ]] +} + +disableVenus() { + + VGA="$(sed -E 's/,venus=(on|true|yes|1)(,|$)/\2/I' <<< "$VGA")" +} + +drmNativeEnabled() { + + [[ ",${VGA,,}," =~ ,drm_native_context=(on|true|yes|1), ]] +} + +disableDrmNative() { + + VGA="$(sed -E 's/,drm_native_context=(on|true|yes|1)(,|$)/\2/I' <<< "$VGA")" +} + +vulkanLibraryAvailable() { + + local library="$1" + + compgen -G "/usr/lib/*/${library}" >/dev/null 2>&1 \ + || [ -e "/usr/lib/${library}" ] \ + || [ -e "/usr/lib64/${library}" ] +} + +vulkanManifestAvailable() { + + local manifest="$1" + + compgen -G "/etc/vulkan/icd.d/${manifest}*.json" >/dev/null 2>&1 \ + || compgen -G "/usr/share/vulkan/icd.d/${manifest}*.json" >/dev/null 2>&1 +} + +vulkanRuntimeReady() { + + local summary details selected extensions api type + local external_memory_fd external_memory_dma_buf + local prime="" + local vendor="${GPU_VENDOR,,}" + local device="${GPU_DEVICE,,}" + VULKAN_REASON="" + + if ! command -v vulkaninfo >/dev/null 2>&1; then + VULKAN_REASON="vulkaninfo is not available in the container" + return 1 + fi + + if [ -z "$device" ]; then + VULKAN_REASON="the selected GPU PCI device ID cannot be determined" + return 1 + fi + + # Mesa can scope Vulkan enumeration to the exact DRM device by PCI address. + # Keep this probe local: QEMU still receives the selected render node normally. + case "$vendor" in + "0x8086" | "0x1002" ) + if [[ "$GPU_PCI_SLOT" =~ ^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7]$ ]]; then + prime="pci-${GPU_PCI_SLOT//[:.]/_}!" + fi ;; + esac + + if [ -n "$prime" ]; then + if ! summary="$(DRI_PRIME="$prime" vulkaninfo --summary 2>&1)"; then + VULKAN_REASON="Vulkan device enumeration failed for the selected GPU" + return 1 + fi + elif ! summary="$(vulkaninfo --summary 2>&1)"; then + VULKAN_REASON="Vulkan device enumeration failed" + return 1 + fi + + selected="$(awk -v want_vendor="$vendor" -v want_device="$device" ' + function emit() { + if (!found && in_gpu && tolower(vendor) == want_vendor && + tolower(device) == want_device && api != "" && type != "") { + print api "|" type + found = 1 + } + } + + /^GPU[0-9]+:/ { + emit() + in_gpu = 1 + vendor = "" + device = "" + api = "" + type = "" + next + } + + in_gpu && /^[[:space:]]*apiVersion[[:space:]]*=/ { + api = $0 + sub(/^.*=[[:space:]]*/, "", api) + if (match(api, /\([0-9]+\.[0-9]+(\.[0-9]+)?\)/)) { + api = substr(api, RSTART + 1, RLENGTH - 2) + } else { + sub(/[[:space:]].*$/, "", api) + } + next + } + + in_gpu && /^[[:space:]]*vendorID[[:space:]]*=/ { + vendor = $0 + sub(/^.*=[[:space:]]*/, "", vendor) + sub(/[[:space:]].*$/, "", vendor) + next + } + + in_gpu && /^[[:space:]]*deviceID[[:space:]]*=/ { + device = $0 + sub(/^.*=[[:space:]]*/, "", device) + sub(/[[:space:]].*$/, "", device) + next + } + + in_gpu && /^[[:space:]]*deviceType[[:space:]]*=/ { + type = $0 + sub(/^.*=[[:space:]]*/, "", type) + sub(/[[:space:]].*$/, "", type) + next + } + + END { emit() } + ' <<< "$summary")" + + if [ -z "$selected" ]; then + VULKAN_REASON="the selected GPU is not available through Vulkan" + return 1 + fi + + IFS='|' read -r api type <<< "$selected" + if [[ "$type" == "PHYSICAL_DEVICE_TYPE_CPU" ]]; then + VULKAN_REASON="the selected Vulkan device is a software CPU renderer" + return 1 + fi + + local major minor + IFS='.' read -r major minor _ <<< "$api" + if ! [[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]]; then + VULKAN_REASON="the selected GPU reports an invalid Vulkan API version '$api'" + return 1 + fi + + if (( major < 1 || (major == 1 && minor < 1) )); then + VULKAN_REASON="the selected GPU supports Vulkan $major.$minor, but Venus requires Vulkan 1.1 or newer" + return 1 + fi + + if [ -n "$prime" ]; then + if ! details="$(DRI_PRIME="$prime" vulkaninfo 2>&1)"; then + VULKAN_REASON="Vulkan capability enumeration failed for the selected GPU" + return 1 + fi + elif ! details="$(vulkaninfo 2>&1)"; then + VULKAN_REASON="Vulkan capability enumeration failed" + return 1 + fi + + extensions="$(awk -v want_vendor="$vendor" -v want_device="$device" ' + function finish() { + if (!found && in_gpu && tolower(vendor) == want_vendor && + tolower(device) == want_device) { + print external_memory_fd "|" external_memory_dma_buf + found = 1 + } + } + + /^GPU[0-9]+:/ { + finish() + in_gpu = 1 + vendor = "" + device = "" + external_memory_fd = 0 + external_memory_dma_buf = 0 + next + } + + in_gpu && /^[[:space:]]*vendorID[[:space:]]*=/ { + vendor = $0 + sub(/^.*=[[:space:]]*/, "", vendor) + sub(/[[:space:]].*$/, "", vendor) + next + } + + in_gpu && /^[[:space:]]*deviceID[[:space:]]*=/ { + device = $0 + sub(/^.*=[[:space:]]*/, "", device) + sub(/[[:space:]].*$/, "", device) + next + } + + in_gpu && /VK_KHR_external_memory_fd/ { + external_memory_fd = 1 + next + } + + in_gpu && /VK_EXT_external_memory_dma_buf/ { + external_memory_dma_buf = 1 + next + } + + END { + finish() + } + ' <<< "$details")" + + IFS='|' read -r external_memory_fd external_memory_dma_buf <<< "$extensions" + + if [[ "$external_memory_fd" != "1" ]]; then + VULKAN_REASON="the selected GPU does not support VK_KHR_external_memory_fd required by Venus" + return 1 + fi + + if [[ "${APP,,}" == "windows" && "$external_memory_dma_buf" != "1" ]]; then + VULKAN_REASON="the selected GPU does not support VK_EXT_external_memory_dma_buf required by Helios" + return 1 + fi + + return 0 +} + +mesaVulkanReady() { + + local vendor="$1" + local library manifest + VULKAN_REASON="" + + if ! vulkanLibraryAvailable "libvulkan.so.1"; then + VULKAN_REASON="the Vulkan loader is not available in the container" + return 1 + fi + + case "$vendor" in + "0x8086" ) + for library in libvulkan_intel.so libvulkan_intel_hasvk.so; do + if ! vulkanLibraryAvailable "$library"; then + VULKAN_REASON="the Intel Vulkan driver library '$library' is not available in the container" + return 1 + fi + done + + for manifest in intel_icd intel_hasvk_icd; do + if ! vulkanManifestAvailable "$manifest"; then + VULKAN_REASON="the Intel Vulkan ICD '$manifest' is not available in the container" + return 1 + fi + done ;; + + "0x1002" ) + if ! vulkanLibraryAvailable "libvulkan_radeon.so"; then + VULKAN_REASON="the AMD Vulkan driver library 'libvulkan_radeon.so' is not available in the container" + return 1 + fi + + if ! vulkanManifestAvailable "radeon_icd"; then + VULKAN_REASON="the AMD Vulkan ICD 'radeon_icd' is not available in the container" + return 1 + fi ;; + esac + + vulkanRuntimeReady || return 1 + + return 0 +} + +# NVIDIA uses the proprietary host driver injected by NVIDIA Container Toolkit +# rather than a Mesa Gallium driver from qemu-minimal. Require the complete EGL +# and GBM path before selecting an NVIDIA render node. Venus additionally needs +# the Vulkan loader, NVIDIA ICD and NVIDIA Vulkan userspace libraries. + +nvidiaDriverVersion() { + + local data="" + NVIDIA_DRIVER_VERSION="" + + if [ -r /proc/driver/nvidia/version ]; then + data="$(head -n 1 /proc/driver/nvidia/version 2>/dev/null || true)" + elif [ -r /sys/module/nvidia/version ]; then + data="$(cat /sys/module/nvidia/version 2>/dev/null || true)" + fi + + if [[ "$data" =~ ([0-9]{3,})\.([0-9]+)(\.[0-9]+)? ]]; then + NVIDIA_DRIVER_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}${BASH_REMATCH[3]:-}" + return 0 + fi + + return 1 +} + +nvidiaVulkanReady() { + + local icd="" + local major minor + + if ! nvidiaDriverVersion; then + NVIDIA_REASON="the NVIDIA driver version cannot be determined" + return 1 + fi + + [[ "$NVIDIA_DRIVER_VERSION" =~ ^([0-9]+)\.([0-9]+) ]] || return 1 + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + + if (( major < 570 || (major == 570 && minor < 86) )); then + NVIDIA_REASON="NVIDIA driver $NVIDIA_DRIVER_VERSION is older than the 570.86 minimum required by Venus" + return 1 + fi + + if ! compgen -G '/usr/lib/*/libvulkan.so.1' >/dev/null 2>&1 \ + && [ ! -e /usr/lib/libvulkan.so.1 ] \ + && [ ! -e /usr/lib64/libvulkan.so.1 ]; then + NVIDIA_REASON="the Vulkan loader is not available in the container" + return 1 + fi + + for icd in /etc/vulkan/icd.d/nvidia_icd*.json /usr/share/vulkan/icd.d/nvidia_icd*.json; do + [ -r "$icd" ] && break + icd="" + done + + if [ -z "$icd" ]; then + NVIDIA_REASON="the NVIDIA Vulkan ICD is not available in the container" + return 1 + fi + + if ! compgen -G '/usr/lib/*/libGLX_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/*/nvidia/*/libGLX_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/nvidia/*/libGLX_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/libGLX_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib64/nvidia/*/libGLX_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib64/libGLX_nvidia.so.*' >/dev/null 2>&1; then + NVIDIA_REASON="the NVIDIA Vulkan driver library is not available in the container" + return 1 + fi + + if ! compgen -G '/usr/lib/*/libnvidia-glvkspirv.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/*/nvidia/*/libnvidia-glvkspirv.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/nvidia/*/libnvidia-glvkspirv.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/libnvidia-glvkspirv.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib64/nvidia/*/libnvidia-glvkspirv.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib64/libnvidia-glvkspirv.so.*' >/dev/null 2>&1; then + NVIDIA_REASON="the NVIDIA Vulkan SPIR-V compiler library is not available in the container" + return 1 + fi + + if ! vulkanRuntimeReady; then + NVIDIA_REASON="$VULKAN_REASON" + return 1 + fi + + return 0 +} + +nvidiaGpuReady() { + + local modeset="" + NVIDIA_REASON="" + + if ! compgen -G '/usr/lib/*/libEGL_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/libEGL_nvidia.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib64/libEGL_nvidia.so.*' >/dev/null 2>&1; then + NVIDIA_REASON="the NVIDIA EGL driver is not available in the container" + return 1 + fi + + if ! compgen -G '/usr/lib/*/libnvidia-egl-gbm.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib/libnvidia-egl-gbm.so.*' >/dev/null 2>&1 \ + && ! compgen -G '/usr/lib64/libnvidia-egl-gbm.so.*' >/dev/null 2>&1; then + NVIDIA_REASON="the NVIDIA EGL GBM platform library is not available in the container" + return 1 + fi + + if ! compgen -G '/usr/lib/*/gbm/nvidia-drm_gbm.so' >/dev/null 2>&1 \ + && [ ! -e /usr/lib/gbm/nvidia-drm_gbm.so ] \ + && [ ! -e /usr/lib64/gbm/nvidia-drm_gbm.so ]; then + NVIDIA_REASON="the NVIDIA GBM backend is not available in the container" + return 1 + fi + + if [ ! -r /usr/share/glvnd/egl_vendor.d/10_nvidia.json ] \ + || [ ! -r /usr/share/egl/egl_external_platform.d/15_nvidia_gbm.json ]; then + NVIDIA_REASON="the NVIDIA EGL vendor configuration is not available in the container" + return 1 + fi + + if [ ! -r /sys/module/nvidia_drm/parameters/modeset ] \ + || ! IFS= read -r modeset < /sys/module/nvidia_drm/parameters/modeset; then + NVIDIA_REASON="the nvidia-drm KMS state cannot be determined" + return 1 + fi + + case "${modeset,,}" in + "y" | "1" ) ;; + * ) + NVIDIA_REASON="nvidia-drm modesetting is disabled" + return 1 ;; + esac + + return 0 +} + +modernVirtioGpuGuest() { + + case "${APP,,}" in + "macos" ) return 1 ;; + esac + + return 0 +} + +drmNativeGpuGuest() { + + case "${APP,,}" in + "qemu" ) return 0 ;; + esac + + return 1 +} + +hostBlobsSupported() { + + kernelAtLeast 6 13 +} + +# qemu-render builds the AMDGPU and i915 native-context backends. Host support +# can be validated here; arbitrary guest kernel/Mesa support is negotiated later. +drmNativeReady() { + + DRM_REASON="" + + if [[ "${APP,,}" == "windows" ]]; then + DRM_REASON="DRM native contexts require a Linux guest" + return 1 + fi + + if ! hostBlobsSupported; then + DRM_REASON="Linux 6.13 or newer is required for DRM native contexts" + return 1 + fi + + if ! [[ ",${VGA,,}," =~ ,blob=(on|true|yes|1), ]]; then + DRM_REASON="virtio-gpu host blobs are not enabled" + return 1 + fi + + case "$GPU_VENDOR" in + "0x1002" ) + if [[ "$GPU_DRIVER" != "amdgpu" ]]; then + DRM_REASON="the AMD GPU is using '$GPU_DRIVER' instead of the amdgpu DRM driver" + return 1 + fi ;; + "0x8086" ) + if [[ "$GPU_DRIVER" != "i915" ]]; then + DRM_REASON="the Intel GPU is using '$GPU_DRIVER' instead of the i915 DRM driver" + return 1 + fi ;; + * ) + DRM_REASON="no native DRM renderer is available for this GPU" + return 1 ;; + esac + + return 0 +} + +venusGuestPatRequired() { + + local cpu_vendor driver device="" + + # TCG does not use the Intel KVM guest-PAT quirk. + disabled "${KVM:-}" && return 1 + + isIntelCpu || return 1 + + case "$GPU_VENDOR" in + "0x1002" | "0x10de" ) + # RADV/NVIDIA dGPU on an Intel CPU. + return 0 ;; + "0x8086" ) + driver=$(readlink -f "/sys/class/drm/${RENDER_NAME}/device/driver" 2>/dev/null || true) + driver="${driver##*/}" + [[ "$driver" == "xe" ]] && return 0 + + if [ -r "/sys/class/drm/${RENDER_NAME}/device/device" ]; then + IFS= read -r device < "/sys/class/drm/${RENDER_NAME}/device/device" || device="" + device="${device,,}" + fi + + # Meteor Lake requires guest PAT even when it is still using i915. + case "$device" in + "0x7d40" | "0x7d45" | "0x7d55" | "0x7d60" | "0x7dd5" ) return 0 ;; + esac ;; + esac + + return 1 +} + +venusGuestPatReady() { + + VULKAN_PAT_REASON="" + venusGuestPatRequired || return 0 + + if ! hasFlag "ss"; then + VULKAN_PAT_REASON="the Intel CPU cannot safely honor guest PAT because self-snoop is unavailable" + return 1 + fi + + if ! kernelAtLeast 6 16; then + VULKAN_PAT_REASON="Linux 6.16 or newer is required for guest PAT support on this Intel CPU/GPU combination" + return 1 + fi + + VIRTGPU_GUEST_PAT="Y" + return 0 +} + +GPU_VENDOR="" +DRM_REASON="" +NVIDIA_NODE="" +NVIDIA_REASON="" +VULKAN_REASON="" +VULKAN_PAT_REASON="" +VULKAN_STATE_REASON="" +OPENGL_46_REASON="" +DRM_STATE_REASON="" +VIRTGPU_GUEST_PAT="" + +if [ -n "$RENDERNODE" ]; then + + if ! gpuNodeVendor "$RENDERNODE"; then + gpuSetupFailure "GPU render node '$RENDERNODE' is unavailable or inaccessible" + return 0 + fi + + case "$GPU_VENDOR" in + "0x8086" ) + if ! intelMesaReady "$RENDERNODE"; then + gpuSetupFailure "Intel GPU at $RENDERNODE is not supported by qemu-render" + return 0 + fi ;; + "0x1002" ) ;; + "0x10de" ) + if ! nvidiaGpuReady; then + gpuSetupFailure "NVIDIA GPU at $RENDERNODE cannot be used for hardware rendering because $NVIDIA_REASON" + return 0 + fi ;; + * ) + gpuSetupFailure "Unsupported GPU at $RENDERNODE" + return 0 ;; + esac + +else + + if [ ! -d /dev/dri ]; then + gpuSetupFailure "GPU acceleration was requested, but '/dev/dri' was not added to the devices section of your compose file" + return 0 + fi + + RENDER_NODE_FOUND="N" + + for node in /dev/dri/renderD*; do + + [ -e "$node" ] || continue + RENDER_NODE_FOUND="Y" + + if ! gpuNodeVendor "$node"; then + continue + fi + + case "$GPU_VENDOR" in + "0x8086" ) + if intelMesaReady "$node"; then + RENDERNODE="$node" + break + fi ;; + "0x1002" ) + RENDERNODE="$node" + break ;; + "0x10de" ) + NVIDIA_NODE="$node" + if nvidiaGpuReady; then + RENDERNODE="$node" + break + fi ;; + esac + + done + + if [ -z "$RENDERNODE" ]; then + + if [ -n "$NVIDIA_NODE" ] && [ -n "$NVIDIA_REASON" ]; then + gpuSetupFailure "NVIDIA GPU at $NVIDIA_NODE cannot be used for hardware rendering because $NVIDIA_REASON" + elif [[ "$RENDER_NODE_FOUND" != "Y" ]]; then + gpuSetupFailure "/dev/dri is available, but no GPU render nodes were found" + else + gpuSetupFailure "No usable GPU render node found" + fi + + return 0 + fi + +fi + +# Re-read the selected node after auto-detection so the vendor name and device +# number below are based on the final render node and survive hotplug races. +if ! gpuNodeVendor "$RENDERNODE"; then + gpuSetupFailure "GPU render node '$RENDERNODE' became unavailable" + return 0 +fi + +RENDER_NAME="${RENDERNODE##*/}" +CARD_NUMBER="${RENDER_NAME#renderD}" +GPU_DEVICE="" +GPU_DRIVER="" + +if [ -r "/sys/class/drm/${RENDER_NAME}/device/device" ]; then + IFS= read -r GPU_DEVICE < "/sys/class/drm/${RENDER_NAME}/device/device" || GPU_DEVICE="" + GPU_DEVICE="${GPU_DEVICE,,}" +fi + +GPU_DRIVER=$(readlink -f "/sys/class/drm/${RENDER_NAME}/device/driver" 2>/dev/null || true) +GPU_DRIVER="${GPU_DRIVER##*/}" +GPU_DEVICE_NAME="" +GPU_PCI_SLOT=$(readlink -f "/sys/class/drm/${RENDER_NAME}/device" 2>/dev/null || true) +GPU_PCI_SLOT="${GPU_PCI_SLOT##*/}" + +if [[ "$GPU_PCI_SLOT" =~ ^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7]$ ]]; then + GPU_DEVICE_NAME=$(lspci -D -s "$GPU_PCI_SLOT" -vmm 2>/dev/null \ + | sed -n 's/^Device:[[:space:]]*//p' | head -n 1 || true) +fi + +case "$GPU_VENDOR" in + "0x8086" ) GPU_NAME="Intel" ;; + "0x1002" ) GPU_NAME="AMD" ;; + "0x10de" ) GPU_NAME="NVIDIA" ;; + * ) GPU_NAME="GPU" ;; +esac + +if [ ! -d /dev/dri ]; then + mkdir -m 755 /dev/dri 2>/dev/null || true +fi + +# Derive the matching DRM card from the validated render node number. +CARD_DEVICE="/dev/dri/card$((10#$CARD_NUMBER - 128))" + +# Containers normally have no udev, so reconstruct the matching DRM card and +# render character devices from the render-node minor number when necessary. +if [ ! -c "$CARD_DEVICE" ]; then + if mknod "$CARD_DEVICE" c 226 $((10#$CARD_NUMBER - 128)) 2>/dev/null; then + chmod 666 "$CARD_DEVICE" 2>/dev/null || true + fi +fi + +if [ ! -c "$RENDERNODE" ]; then + if mknod "$RENDERNODE" c 226 "$((10#$CARD_NUMBER))" 2>/dev/null; then + chmod 666 "$RENDERNODE" 2>/dev/null || true + fi +fi + +if ! gpuNodeVendor "$RENDERNODE"; then + gpuSetupFailure "GPU render node '$RENDERNODE' became unavailable" + return 0 +fi + +if venusEnabled; then + + case "$GPU_VENDOR" in + "0x8086" | "0x1002" ) + if ! mesaVulkanReady "$GPU_VENDOR"; then + VULKAN_STATE_REASON="$VULKAN_REASON" + disableVenus + fi ;; + "0x10de" ) + if ! nvidiaVulkanReady; then + VULKAN_STATE_REASON="$NVIDIA_REASON" + disableVenus + fi ;; + esac + + if venusEnabled && ! venusGuestPatReady; then + VULKAN_STATE_REASON="$VULKAN_PAT_REASON" + disableVenus + fi + +fi + +if modernVirtioGpuGuest; then + + if hostBlobsSupported; then + + VGA+=",hostmem=$VRAM_BYTES,max_hostmem=$VRAM_BYTES,blob=true" + VGA+=",host3d_blob_limit=$VRAM_BYTES" + + if drmNativeGpuGuest; then + VGA+=",drm_native_context=on" + fi + + case "$GPU_VENDOR" in + "0x8086" | "0x1002" ) + if ! mesaVulkanReady "$GPU_VENDOR"; then + VULKAN_STATE_REASON="$VULKAN_REASON" + elif ! venusGuestPatReady; then + VULKAN_STATE_REASON="$VULKAN_PAT_REASON" + else + VGA+=",venus=true" + fi ;; + "0x10de" ) + if ! nvidiaVulkanReady; then + VULKAN_STATE_REASON="$NVIDIA_REASON" + elif ! venusGuestPatReady; then + VULKAN_STATE_REASON="$VULKAN_PAT_REASON" + else + VGA+=",venus=true" + fi ;; + esac + + else + + OPENGL_46_REASON="requires virtio-gpu host blobs (Linux 6.13+ host kernel)" + VULKAN_STATE_REASON="requires virtio-gpu host blobs (Linux 6.13+ host kernel)" + + if drmNativeGpuGuest; then + DRM_STATE_REASON="requires virtio-gpu host blobs (Linux 6.13+ host kernel)" + fi + + fi +fi + +if [[ "${APP,,}" == "windows" ]] && ! venusEnabled; then + error "Windows GPU acceleration requires Vulkan via Venus, but Venus could not be enabled${VULKAN_STATE_REASON:+: $VULKAN_STATE_REASON}." + exit 87 +fi + +if drmNativeEnabled && ! drmNativeReady; then + DRM_STATE_REASON="$DRM_REASON" + disableDrmNative +fi + +OPENGL_46=" " +[[ ",${VGA,,}," =~ ,blob=(on|true|yes|1), ]] && OPENGL_46=" ✓ " + +VULKAN_STATE=" " +venusEnabled && VULKAN_STATE=" ✓ " + +DRM_STATE=" " +drmNativeEnabled && DRM_STATE=" ✓ " + +echo +info "Hardware rendering enabled:" +info + +info "Device: $GPU_NAME${GPU_DEVICE_NAME:+ $GPU_DEVICE_NAME}" + +if [ -n "$GPU_DEVICE" ]; then + info "PCI ID: ${GPU_VENDOR#0x}:${GPU_DEVICE#0x}" +fi + +info "Driver: ${GPU_DRIVER:-unknown}" + +if [[ "$GPU_VENDOR" == "0x10de" ]]; then + nvidiaDriverVersion || NVIDIA_DRIVER_VERSION="unknown" + info "Version: $NVIDIA_DRIVER_VERSION" +else + MESA_VERSION="$(dpkg-query -W -f='${Provides}\n' qemu-render 2>/dev/null \ + | sed -n 's/.*libgbm1 (= \([^)]*\)).*/\1/p' || true)" + + [ -n "$MESA_VERSION" ] && info "Mesa: $MESA_VERSION" +fi + +info "Render: $RENDERNODE" +info + +if modernVirtioGpuGuest; then + info "Vulkan: [$VULKAN_STATE]${VULKAN_STATE_REASON:+ $VULKAN_STATE_REASON}" + if drmNativeGpuGuest; then + info "DRM Native: [$DRM_STATE]${DRM_STATE_REASON:+ $DRM_STATE_REASON}" + fi + info "OpenGL 4.3: [ ✓ ]" + info "OpenGL 4.6: [$OPENGL_46]${OPENGL_46_REASON:+ $OPENGL_46_REASON}" +fi + +DISPLAY_OPTS="-display egl-headless,rendernode=$RENDERNODE" +DISPLAY_OPTS+=" -device $VGA" + +[[ "${DISPLAY,,}" == "vnc" ]] && DISPLAY_OPTS+=" -vnc :${port}${LOSSY_OPT}" +[[ "${DISPLAY,,}" == "web" ]] && DISPLAY_OPTS+=" -vnc :${port},websocket=unix:${WSS_SOCKET}${LOSSY_OPT}" + +return 0