feat(gpu): Display-menu render-GPU selector for multi-GPU systems#3656
Conversation
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>
There was a problem hiding this comment.
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_PRIMElever 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>
|
Two updates from review: 1. Fixed the Linux non-NVIDIA no-op (review point #1). Commit 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 Remaining bot notes: the Windows |
…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>
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 inmain()beforeQApplication(the adapter env must be set before the GL/D3D context is created) and sets the right lever per windowing system:__NV_PRIME_RENDER_OFFLOAD=1(+__VK_LAYER_NV_optimus); no GLX vars__NV_PRIME_RENDER_OFFLOAD=1+__GLX_VENDOR_LIBRARY_NAMEQT_D3D_ADAPTER_INDEXIt 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:
/sys/class/drm/card*/devicevendor id +boot_vga), stable PCI-address ids.EnumAdapters1(adapter index →QT_D3D_ADAPTER_INDEX, skips the software/WARP adapter).The choice persists as one nested-JSON
Graphicskey viaAppSettings(Principle V).Files
src/core/GpuSelector.{h,cpp}(new) — enumeration, persistence, startup apply.src/main.cpp— load settings +applyAtStartup()beforeQApplication; log the decision.src/gui/SpectrumOverlayMenu.{h,cpp}— the Display-menu dropdown + restart note.CMakeLists.txt— register the source; linkdxgion Windows.Test plan
renderD128(the NVIDIA node) andnvidia-smilists AetherSDR as a graphics process (~226 MiB). Selecting Auto/Integrated returns to the iGPU.#ifdef Q_OS_WIN) so CI builds it, but theQT_D3D_ADAPTER_INDEXruntime behaviour needs a Windows tester before merge.Notes for reviewers
QT_D3D_ADAPTER_INDEXon Windows); the UI, persistence, and enumeration are structured uniformly with per-platform apply.🤖 Generated with Claude Code