Steam: Forcing NVIDIA GPU on a Dual-GPU Linux System

I recently ran into a frustrating problem on my GPU workstation: Steam games running through Proton were silently defaulting to the AMD iGPU instead of the NVIDIA RTX 4080. Performance was terrible and it took a while to figure out why. This post documents the root cause and the fix.

The Setup

ComponentDetail
CPUAMD Ryzen 9 7900X
iGPUAMD Raphael (/dev/dri/renderD128)
dGPUNVIDIA RTX 4080 (/dev/dri/renderD129)
DisplayHDMI connected to AMD iGPU (motherboard port)
OSUbuntu 24.04
DriverNVIDIA proprietary 580.x

The monitor is plugged into the motherboard HDMI port (AMD iGPU), not the NVIDIA card. This is intentional (it keeps the NVIDIA GPU free for compute), but it creates a chain of problems for Proton/Vulkan.

Why Proton Picks the Wrong GPU

There are several layers to this problem, each compounding the next.

The display GPU gets priority in Vulkan

Mesa’s VK_LAYER_MESA_device_select implicit Vulkan layer reorders physical devices so the display-connected GPU comes first. VKD3D-Proton (the DX12→Vulkan translation layer) picks the first suitable device. Since AMD owns the display, AMD gets picked.

NVIDIA PRIME offload requires a kernel parameter

NVIDIA PRIME render offload (__NV_PRIME_RENDER_OFFLOAD=1) only works if nvidia-drm.modeset=1 is set as a kernel parameter. Without it, the NVIDIA kernel module doesn’t support atomic modesetting and PRIME presentation is non-functional: the env vars are silently no-ops.

pressure-vessel strips your environment variables

Steam’s Proton compatibility layer runs games inside a pressure-vessel container (bubblewrap/bwrap). This container:

  • Overrides VK_DRIVER_FILES, VK_ICD_FILENAMES, VK_IMPLICIT_LAYER_PATH with its own container-internal paths
  • Strips VK_ADD_IMPLICIT_LAYER_PATH entirely; it never reaches the game process
  • Sources Vulkan ICD files from the host via symlinks: container:/dev/dri/... -> /run/host/usr/share/vulkan/icd.d/

The NVIDIA Optimus Vulkan layer has a broken JSON format

/usr/share/vulkan/implicit_layer.d/nvidia_layers.json uses "file_format_version": "1.0.1" but declares layers with the "layers": [...] array syntax. Array syntax requires version 1.2.0+ (Khronos Vulkan-Loader Layer Interface spec). The Vulkan loader inside the container silently rejects the file, so VK_LAYER_NV_optimus is never loaded. This is a latent bug in the NVIDIA driver package for Ubuntu.

The Fix

Step 1: Enable NVIDIA kernel modesetting

sudo sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 nvidia-drm.modeset=1"/' /etc/default/grub
sudo update-grub
sudo reboot

Verify after reboot:

sudo cat /sys/module/nvidia_drm/parameters/modeset
# Should output: Y

Step 2: Disable the AMD Vulkan ICD

Since all ICD files inside the pressure-vessel container are symlinks to /run/host/usr/share/vulkan/icd.d/, renaming the host file breaks the symlink inside the container. VKD3D sees only NVIDIA — no Optimus layer or environment variable tricks needed.

sudo mv /usr/share/vulkan/icd.d/radeon_icd.json /usr/share/vulkan/icd.d/radeon_icd.json.disabled

What this does NOT break:

  • X11 display output (X11 uses DRM/KMS directly, not the Vulkan ICD)
  • Hardware video decode (VAAPI/VDPAU uses Mesa OpenGL/DRI, not Vulkan ICD)
  • AMD OpenCL (uses Mesa OpenCL, not Vulkan ICD)

What this does break:

  • AMD Vulkan enumeration on the host (vulkaninfo won’t show the AMD device)
  • Any host application that explicitly wants AMD Vulkan (nothing on this machine)

To restore it later:

sudo mv /usr/share/vulkan/icd.d/radeon_icd.json.disabled /usr/share/vulkan/icd.d/radeon_icd.json

Verifying It Works

Launch a GPU-intensive game and check:

# Game process should appear with non-zero SM utilization
nvidia-smi pmon -c 5 -d 1
 
# Game process should appear on renderD129 (NVIDIA), not only renderD128 (AMD)
for dev in /dev/dri/renderD*; do
  echo "$dev: $(sudo fuser $dev 2>/dev/null)"
done

The game will open handles to both AMD renderD128 (for PRIME display composition) and NVIDIA renderD129 (for actual rendering). You’ll see the game process in nvidia-smi output with non-zero SM utilization while in-game.

How pressure-vessel Wires Up Vulkan ICDs

Understanding this symlink chain is what ultimately led to the solution:

Inside container:
  /usr/lib/pressure-vessel/overrides/share/vulkan/icd.d/nvidia_icd.json
    -> /run/host/usr/share/vulkan/icd.d/nvidia_icd.json  (symlink)
        -> host: /usr/share/vulkan/icd.d/nvidia_icd.json  (real file, present)

  /usr/lib/pressure-vessel/overrides/share/vulkan/icd.d/radeon_icd.json
    -> /run/host/usr/share/vulkan/icd.d/radeon_icd.json  (BROKEN after fix)

Because pressure-vessel builds its ICD list from host symlinks, any ICD file you disable on the host is automatically absent inside the container. There’s no need to touch anything inside the container or fight with VK_ICD_FILENAMES.

Notes on the NVIDIA Optimus Layer Bug

The broken nvidia_layers.json is worth noting even though it’s no longer the active issue (AMD ICD is disabled):

VK_IMPLICIT_LAYER_PATH (inside container):
  /usr/lib/pressure-vessel/overrides/share/vulkan/implicit_layer.d/
    6.json  -> /run/host/usr/share/vulkan/implicit_layer.d/nvidia_layers.json  (rejected: bad format)
    7.json  -> /run/host/usr/share/vulkan/implicit_layer.d/nvidia_layers.json  (rejected: bad format)

The file declares "file_format_version": "1.0.1" but uses the newer "layers": [...] array syntax that requires 1.2.0+. The Vulkan loader rejects it silently. This means VK_LAYER_NV_optimus and the NVIDIA Optimus approach are broken on Ubuntu without patching the JSON file (Khronos Vulkan-Loader Layer Interface spec).

See Also