Skip to content

feat(gpu): Display-menu render-GPU selector for multi-GPU systems#3656

Merged
ten9876 merged 3 commits into
mainfrom
feat/gpu-selector
Jun 20, 2026
Merged

feat(gpu): Display-menu render-GPU selector for multi-GPU systems#3656
ten9876 merged 3 commits into
mainfrom
feat/gpu-selector

Conversation

@ten9876

@ten9876 ten9876 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a render-GPU selector to the panadapter Display menu so users on multi-GPU systems can choose which adapter the QRhi spectrum/waterfall renders on. The dropdown appears only when more than one GPU is present. Because the graphics adapter can't be hot-swapped under a live render context, the choice is persisted and applied on the next launch (with a "Restart to apply" note).

Motivation: on a hybrid laptop (Intel iGPU + NVIDIA dGPU) AetherSDR defaulted to the integrated GPU under Wayland, with no in-app way to move the flagship GPU render path to the discrete card.

How it works

GpuSelector::applyAtStartup() runs in main() before QApplication (the adapter env must be set before the GL/D3D context is created) and sets the right lever per windowing system:

Platform Mechanism
Linux / Wayland (EGL) __NV_PRIME_RENDER_OFFLOAD=1 (+ __VK_LAYER_NV_optimus); no GLX vars
Linux / X11 + XWayland (GLX) __NV_PRIME_RENDER_OFFLOAD=1 + __GLX_VENDOR_LIBRARY_NAME
Windows (D3D11/D3D12 RHI) QT_D3D_ADAPTER_INDEX

It honours an explicit user env override (won't clobber a hand-set __NV_PRIME_RENDER_OFFLOAD / QT_D3D_ADAPTER_INDEX), no-ops on "Auto" or a missing GPU, and logs the decision once the log handler is up.

GPU enumeration:

  • Linux — DRM sysfs (/sys/class/drm/card*/device vendor id + boot_vga), stable PCI-address ids.
  • Windows — DXGI EnumAdapters1 (adapter index → QT_D3D_ADAPTER_INDEX, skips the software/WARP adapter).

The choice persists as one nested-JSON Graphics key via AppSettings (Principle V).

Files

  • src/core/GpuSelector.{h,cpp} (new) — enumeration, persistence, startup apply.
  • src/main.cpp — load settings + applyAtStartup() before QApplication; log the decision.
  • src/gui/SpectrumOverlayMenu.{h,cpp} — the Display-menu dropdown + restart note.
  • CMakeLists.txt — register the source; link dxgi on Windows.

Test plan

  • Local build (Linux, GPU=ON, Ninja) — links clean.
  • Verified on a hybrid Intel iGPU + NVIDIA RTX 4090 laptop under native Wayland: selecting NVIDIA (discrete) moves the render context to the dGPU — process opens renderD128 (the NVIDIA node) and nvidia-smi lists AetherSDR as a graphics process (~226 MiB). Selecting Auto/Integrated returns to the iGPU.
  • Persistence round-trips; selector hidden on single-GPU systems.
  • Windows path not yet tested on hardware — it's compile-guarded (#ifdef Q_OS_WIN) so CI builds it, but the QT_D3D_ADAPTER_INDEX runtime behaviour needs a Windows tester before merge.
  • Existing tests pass (CI).

Notes for reviewers

  • RFC gate (Constitution v2.0.0): this is a new user-facing feature; flagging that it likely wants an RFC before merge.
  • No live switching by design — the adapter is fixed at context creation, so the UX is persist-and-restart, not instant. This matches how the underlying graphics stack works.
  • Cross-platform: GPU selection is inherently platform-specific (PRIME/EGL on Linux, DXGI/QT_D3D_ADAPTER_INDEX on Windows); the UI, persistence, and enumeration are structured uniformly with per-platform apply.

🤖 Generated with Claude Code

Adds a GPU dropdown in the panadapter Display menu (shown only when >1 GPU is
present) that picks which adapter the QRhi spectrum/waterfall renders on. The
adapter can't be hot-swapped under a live context, so the choice persists and
applies on the next launch.

- src/core/GpuSelector.{h,cpp}: enumerate GPUs (Linux DRM sysfs vendor/boot_vga;
  Windows DXGI EnumAdapters1), persist as one nested-JSON "Graphics" key
  (Principle V), and applyAtStartup() — called before QApplication — sets the
  adapter-selection env for the active windowing system:
    * Wayland (EGL): __NV_PRIME_RENDER_OFFLOAD (+ Vulkan hint); no GLX vars
    * X11/XWayland (GLX): __NV_PRIME_RENDER_OFFLOAD + __GLX_VENDOR_LIBRARY_NAME
    * Windows: QT_D3D_ADAPTER_INDEX
  Honours an explicit user env override; logs the decision once the log handler
  is up.
- main.cpp: load settings + applyAtStartup() before QApplication.
- SpectrumOverlayMenu: the selector + "Restart to apply" note.
- CMakeLists: register source, link dxgi on Windows.

Verified on a hybrid Intel iGPU + NVIDIA RTX 4090 laptop under native Wayland:
selecting NVIDIA puts the render context on the dGPU (renderD128, nvidia-smi
shows AetherSDR as a graphics process). Windows path compile-guarded for CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ten9876 ten9876 requested review from a team as code owners June 19, 2026 03:41
@ten9876 ten9876 added enhancement Improvement to existing feature spectrum Panadapter and waterfall labels Jun 19, 2026

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @ten9876 — it's a well-structured, well-documented feature. The system boundaries (DXGI factory/adapter, DRM sysfs reads) all have proper failure handling, it honors explicit user env overrides, the Wayland-vs-X11 lever split is correct and clearly reasoned, and persistence follows the nested-JSON-under-one-key convention (Principle V) — the Graphics blob matches the established EqApplet/AppSettings pattern exactly. CI is green across all platforms.

I confirmed the one thing that looked risky — applyAtStartup() calling AppSettings::load() before QApplication. It's safe: AppSettings derives its path from GenericConfigLocation, which doesn't depend on applicationName/organizationName (set later at main.cpp:163-165), so the pre-QApplication read hits the same file the GUI writes. The comment is accurate.

A few things worth addressing or noting:

1. Linux apply path only actually works for NVIDIA (the menu offers more than it can deliver).
In applyAtStartup() the Linux branch keys solely off chosen->name.contains("NVIDIA"). The else branch sets __GLX_VENDOR_LIBRARY_NAME=mesa (X11) or nothing (Wayland) — neither of which moves rendering onto a specific discrete card. Notably the chosen GPU's PCI address (chosen->id = pci:<addr>) is never used to target an adapter. So on an AMD-iGPU + AMD-dGPU or Intel + AMD-dGPU laptop, the dropdown lists the discrete AMD card as selectable, but picking it is effectively a no-op — AMD PRIME offload needs DRI_PRIME=1 (or DRI_PRIME=pci-…), which isn't set anywhere. Two reasonable options:

  • Add a DRI_PRIME lever for the non-NVIDIA discrete case (using the enumerated PCI id), or
  • For now, only surface entries the apply path can actually action (e.g. NVIDIA discrete + Auto), so the UI doesn't promise a switch it won't perform.

Either way, a one-line note in the tooltip/PR about the current NVIDIA-Optimus-only scope would set expectations.

2. Windows: interaction with the existing NvOptimusEnablement export.
main.cpp:67-68 already exports NvOptimusEnablement = 1 / AmdPowerXpressRequestHighPerformance = 1, which hint the driver to bind the process to the dGPU on hybrid laptops. The new QT_D3D_ADAPTER_INDEX selects the Qt RHI adapter at a different layer. When a user picks Integrated on Windows, these two pull in opposite directions — the RHI adapter index should win for the render surface, but it's worth having the Windows tester confirm that "Integrated" actually lands on the iGPU and isn't overridden. Good candidate for the hardware-validation checklist item you already flagged as pending.

3. Minor — enumeration re-runs. available() re-reads DRM sysfs / recreates the DXGI factory on every call, and buildDisplayPanel() calls hasMultiple() + savedChoiceId() + available() (3 enumerations per build, plus once at startup). Not a correctness issue, but if the Display panel rebuilds frequently you may want to cache the enumeration. Low priority.

On the RFC gate you self-flagged: agreed this is a new user-facing feature under Constitution v2.0.0 and likely wants an RFC before merge — leaving that call to the maintainers.

None of the above are blockers to the design; (1) is the one I'd most want resolved (or scoped/documented) before merge so the menu doesn't offer choices it can't honor. Nice work.


🤖 aethersdr-agent · cost: $5.6353 · model: claude-opus-4-8

…3656)

The Linux apply path only actioned NVIDIA (PRIME offload); the non-NVIDIA
branch set __GLX_VENDOR_LIBRARY_NAME=mesa (X11) or nothing (Wayland), neither of
which moves rendering onto a *specific* card — so picking an AMD discrete GPU
was a silent no-op while the menu still offered it.

Now the non-NVIDIA branch targets the chosen card by PCI address via the Mesa
loader's DRI_PRIME (works under both X11 and Wayland), converting the enumerated
id "pci:0000:01:00.0" → "pci-0000_01_00_0". Selecting an AMD discrete GPU now
offloads to it; selecting the integrated GPU pins it explicitly. NVIDIA stays on
the __NV_PRIME_RENDER_OFFLOAD path (DRI_PRIME is Mesa-only). DRI_PRIME added to
the explicit-user-override guard.

Validated: NVIDIA offload path on an Intel+NVIDIA box; the DRI_PRIME path
follows Mesa's documented PCI-tag semantics (no AMD-dGPU box here to soak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ten9876

ten9876 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Two updates from review:

1. Fixed the Linux non-NVIDIA no-op (review point #1). Commit 9c5ab5ef adds a DRI_PRIME=pci-<addr> lever for the AMD/Intel case — picking an AMD discrete GPU now actually offloads to it (works under X11 and Wayland) instead of being a silent no-op, and DRI_PRIME is added to the explicit-override guard. NVIDIA stays on the __NV_PRIME_RENDER_OFFLOAD path. Validated on the Intel+NVIDIA box; the DRI_PRIME path follows Mesa's documented PCI-tag semantics (no AMD-dGPU here to soak).

2. RFC filed → #3666 (Constitution v2.0.0 gate, self-flagged). This PR is held until that RFC is approved. The RFC is transparent that the implementation preceded it (built during exploratory work) and documents the cross-platform levers, alternatives, and the honest validation status (NVIDIA ✅; AMD DRI_PRIME + Windows QT_D3D_ADAPTER_INDEX need hardware soak).

Remaining bot notes: the Windows NvOptimusEnablement-vs-Integrated interaction (#2) and the enumeration caching (#3) are captured in the RFC's validation list / noted as low-priority respectively.

…s, gate unsoaked paths

The aethersdr-agent review of RFC #3666 raised three points, all confirmed
against source and fixed here:

1. Windows "Integrated" re-armed the #1921 crash. NvOptimusEnablement is
   force-exported precisely because the Intel iGPU D3D11 driver corrupts its
   stack during QRhiWidget reparenting; letting a user point
   QT_D3D_ADAPTER_INDEX at that adapter re-arms it. The Windows integrated
   adapter is now present-but-not-selectable (GpuInfo::selectable=false): the
   menu greys it with a "#1921" tooltip and applyAtStartup() refuses it as
   defence-in-depth (keeping the system default → NvOptimus drives the dGPU).

2. applyAtStartup() constructed AppSettings::instance() before QApplication,
   running migrateSettingsPath() (AppConfigLocation, app-name-dependent) before
   setApplicationName() — silently and permanently skipping the old→new
   settings migration for upgrading users. It now reads the "Graphics"/"gpu"
   key straight from the settings XML via QXmlStreamReader (mirroring the
   UiScale bootstrap), never touching the singleton early.

3. Only the Linux NVIDIA PRIME-offload path is hardware-soaked. The Linux
   AMD/Intel DRI_PRIME and all Windows paths are now flagged
   GpuInfo::experimental and labelled "(experimental)" in the menu.

Linux verified live: saved Intel iGPU choice read from file and applied as
DRI_PRIME=pci-0000_00_02_0 (MangoHud confirms renderD129 active). Windows
branch compile-checked in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ten9876 ten9876 merged commit a830653 into main Jun 20, 2026
6 checks passed
@ten9876 ten9876 deleted the feat/gpu-selector branch June 20, 2026 02:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing feature spectrum Panadapter and waterfall

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant