Skip to content
Go back

Arch Linux + Hyprland on GPD Win4 with iGPU + eGPU

Updated:
Edit page

Arch Linux + Hyprland on GPD Win4 with iGPU + eGPU

fastfetch

Document Objective

Written as a reference for anyone using both an iGPU and an eGPU and wanting to maximize their capability while switching between them frequently. The concrete use case is specific to the GPD Win4 — two AMD GPUs on Arch Linux + hyprland.lua, offloading rendering onto the eGPU — but the approach generalizes: other hardware stacks and configurations, such as two NVIDIA GPUs, follow the same principles for consideration.

Hardware Stack

1. iGPU Only

2. With eGPU

flowchart LR
    subgraph GPD["GPD Win4"]
        igpu["iGPU<br/>Radeon 780M"]
    end
    egpu["eGPU<br/>RX 7600M XT"]
    monitor["External Monitor"]

    igpu -- "OCULINK (PCIe)" --> egpu
    egpu -- "HDMI" --> monitor

Expected:

Verification (quick self-check, run first)

lspci | grep -i 7600M                         # eGPU detected? bus id?
# stable offload check (derived id, not DRI_PRIME=1 which is index-relative)
DRI_PRIME="pci-0000_$(lspci | awk '/7600M/{print $1}' | tr '.:' '__')" glxinfo | grep -i renderer  # -> RX 7600M XT
egpu glxinfo | grep -i renderer               # same via wrapper
vulkaninfo --summary                          # both GPUs visible (verified; --list-devices is not a valid flag in this vulkaninfo)
hyprctl monitors all                          # active monitors per output
readlink -f ~/.config/hypr/cards/{egpu,igpu}  # symlink targets

Is the eGPU taking workload? While a game runs via egpu <game>, watch GPU busy %:

watch -n 1 'for g in egpu igpu; do n=/sys/class/drm/$(basename "$(readlink -f ~/.config/hypr/cards/$g)"); echo "$g: $(cat "$n/device/gpu_busy_percent")%"; done'

If the eGPU line is high (60%+ in a real game) while iGPU stays low, the eGPU is doing the work. Richer view (already installed): amdgpu_top or amdgpu_top --smi.

Example — a lightweight Steam game running on the eGPU (eGPU busy ~25%, below the 60% bar):

egpu

Verified Hardware Mapping (Aug 2026, this boot)

GPUPCI idvendor:device/dev/drioutputs
eGPU RX 7600M XT (Navi 33)03:00.01002:7480card1 / renderD128HDMI-A-1, DP-1, DP-2
iGPU Radeon 780M (Phoenix)66:00.01002:15bfcard2 / renderD129eDP-1 (built-in), DP-3..8

IDs are per-boot snapshots, not promises. cardN/renderD* and even PCI bus IDs can change randomly between reboots. Always resolve dynamically (lspci, /dev/dri/by-path/) or use the symlinks ~/.config/hypr/cards/{egpu,igpu}.

Verified evidence: in 3 of 8 boot logs the iGPU appeared at 63:00.0 (eGPU absent) instead of 66:00.0. eGPU-present boots have been consistently 03:00.0/66:00.0.

Regenerate the table (verified):

for c in /dev/dri/by-path/pci-*-card; do
    pci=$(basename "${c%-card}"); pci=${pci#pci-0000:}
    card=$(basename "$(readlink -f "$c")")
    render=$(basename "$(readlink -f "${c%-card}-render")")
    dev=$(lspci -nn -s "0000:$pci")
    vid=$(grep -oE '\[[0-9a-f]{4}:[0-9a-f]{4}\]' <<<"$dev" | tr -d '[]')
    name=$(sed -E 's/^[0-9:.]+ //; s/^VGA compatible controller \[0300\]: //; s/ \[[0-9a-f]{4}:[0-9a-f]{4}\] \(rev [0-9a-f]+\)$//' <<<"$dev")
    outs=$(ls -d /sys/class/drm/${card}-* 2>/dev/null | grep -v Writeback | sed "s|.*/${card}-||" | paste -sd,)
    printf "%s | %s | %s | %s / %s | %s\n" "$name" "$pci" "$vid" "$card" "$render" "$outs"
done

Current State

~/.bashrc has an egpu() launcher — GL/EGL-only offload, resolving the eGPU’s PCI id at call time (no hardcoded id):

# egpu
# Custom eGPU launcher shortcut
egpu() {
    if [ -z "$1" ]; then
        echo "Usage: egpu <command>"
        echo "Example: egpu steam"
        return 1
    fi
    local id
    id=$(lspci | awk '/7600M/ {print $1}')
    if [ -z "$id" ]; then
        echo "eGPU (RX 7600M XT) not detected" >&2
        return 1
    fi
    env DRI_PRIME="pci-0000_${id//[.:]/_}" "$@"
}

Verified: egpu glxinfo -> RX 7600M XT; plain glxinfo -> 780M. A stale hardcoded pci-0000_03_00_0 was replaced by this dynamic form — lspci emits 03:00.0 with a dot, so /7600M/ {print $1} output is rewritten with ${id//[.:]/_} (03:00.003_00_0). One lspci call per run; clear “not detected” error if the eGPU is absent.

How it works: DRI_PRIME=pci-0000_<bus>_<dev>_<func> renders GL/EGL on that GPU only; scanout is Hyprland’s job. Per-command only — desktop stays on iGPU.

Not covered:

  1. Vulkan — ignores DRI_PRIME; games enumerate both GPUs themselves (see Proposed #1).
  2. Display driving — compositor decides scanout; already works (see mapping).
mkdir -p ~/.config/hypr/cards
ln -s /dev/dri/by-path/pci-0000:03:00.0-card  ~/.config/hypr/cards/egpu
ln -s /dev/dri/by-path/pci-0000:66:00.0-card  ~/.config/hypr/cards/igpu

by-path is PCI-stable, so the symlinks survive cardN shifts. Verified: egpu->card1, igpu->card2.

Caveat: igpu (pointing at 66:00.0) dangles in eGPU-absent boots — the iGPU then sits at 63:00.0. Harmless as long as nothing references it.

eGPU Display Ownership (Optional)

Env var for this stack is AQ_DRM_DEVICES (WLR_DRM_DEVICES is gone in Aquamarine 0.14+). In ~/.bash_profile, igpu first = primary:

export AQ_DRM_DEVICES="$HOME/.config/hypr/cards/igpu:$HOME/.config/hypr/cards/egpu"

Proposed egpu() Improvements (Not Applied)

  1. Vulkan offloadDRI_PRIME is GL/EGL-only; Vulkan apps ignore it and enumerate both GPUs themselves (usually offering their own in-game adapter picker). Two options:

    • MESA_VK_DEVICE_SELECT=1002:7480 (layer installed). Verified: reorders the eGPU to GPU0 so apps that default to the first adapter pick it up, but it does not hide the other GPU. Name-picking apps (that list and let you choose) are unaffected.
    • Recommendation: skip it. Most games with a visible adapter picker already let you choose the eGPU; MESA_VK_DEVICE_SELECT only helps apps that blindly default to adapter 0 without letting you change it. Only add it if a specific game mis-defaults to the iGPU and has no in-game picker.

Open Questions (decided)

btw, i use arch


Edit page
Share this post on:

Next Post
Happy Birthday, Linux — My Journey from AIX to Arch