Summary
The main window runs at 10–15 fps with multiple panadapters open, despite high-end hardware. CPU profiling shows the app is single-threaded-bound on the GUI thread in Qt's RHI backing-store composite path — not in AetherSDR's own logic (only ~2% of samples) and not GPU-starved. This issue captures the profile; the chosen fix is gated on an in-flight investigation of Qt's rhiFlush backing-store behavior.
Repro / environment
- Radio: FlexRadio FLEX-8600, 4 panadapters (4 slices), 20m.
- Build:
RelWithDebInfo (-O2 -g -DNDEBUG), current main (post-v26.6.4).
- RHI backend: OpenGL (
SpectrumWidget: QRhi initialized, backend: OpenGL). On Linux we never call QRhiWidget::setApi(), so it defaults to OpenGL.
- Hardware: i9-13980HX (32 threads), RTX 4090 Laptop + Intel UHD (hybrid), KDE Wayland. Process holds NVIDIA render-node fds → rendering on the 4090, GPU not starved.
- Symptom: ~10–15 fps on the main window (measured via MangoHud).
Profile (perf, DWARF call-graph, main thread, ~18k samples / 6s)
Thread distribution: total ~105–112% CPU across 47 threads, but the GUI/main thread alone is ~79% of one core; every other thread (panadapter worker, audio, Qt pool) is ≤10%. Classic single-GUI-thread cap.
Main-thread self-time by library:
| Library |
Self % |
What |
| libQt6Gui |
~45% |
QImage copy / fill / blend (raster compositing) |
| libnvidia-eglcore |
~29% |
NVIDIA GL driver — present / composite / texture upload |
| libc |
~16% |
memcpy/memset behind the QImage ops |
| Qt6Core / Qt6Widgets |
~4% |
event dispatch |
| AetherSDR (our code) |
~2% |
— |
Call graph (caller view):
QApplicationPrivate::notify_helper (45%)
└─ QWidget::event (42.7%)
├─ QWidgetRepaintManager::paintAndFlush (31.8%)
│ └─ flush → QPlatformBackingStore::rhiFlush (27.2%)
│ ├─ QImage::detach() → QImage::copy(QRect const&) ~15.2% ← full backing-store stage copy / present
│ └─ QRhi::endFrame → swapchain present ~11.8%
├─ QRhiWidget::paintEvent → renderGpuFrame → QImage::fill(transparent) ~7% (appears ×2 → multiple panadapters)
└─ QPushButton::paintEvent → QCommonStyle::drawControl ~3.5%
Interpretation
With 4 QRhiWidget panadapters in one top-level window mixed with raster sibling widgets, Qt appears to repeatedly (a) copy/fill the raster backing store on the CPU via QImage (~61% across Qt6Gui + libc) and (b) push it through the NVIDIA GL composite/present (~29%) — per panadapter present. That CPU composite, not the GPU, is the 10–15 fps cap.
Two candidate fixes (pending investigation)
- Reduce the CPU
QImage backing-store recopy (~61%). If Qt re-stages an unchanged full-window backing store on every RHI present (rather than dirty-rect / cached), guarding/caching that — or giving each panadapter its own native subsurface (WA_NativeWindow) / a single shared RHI surface so they bypass the shared raster backing-store composite — is the big win. Open question: is rhiFlush's copy full-window or dirty-rect, and is the staged texture cached across frames?
- Backend swap to Vulkan (~29%). Would attack the NVIDIA GL-driver present cost (and possibly some staging). Requires
setApi(Vulkan) + a QVulkanInstance in main.cpp; not a launch-flag. May not touch the ~61% CPU QImage cost if that's backend-agnostic.
Status
A scoped deep-research investigation into Qt 6.11's exact rhiFlush backing-store / dirty-rect behavior is in flight to decide between fix #1 and #2 (and rank the mitigations: WA_NativeWindow per panadapter, per-panadapter top-level QWindow, single full-window RHI surface, WA_StaticContents, QOpenGLWidget vs QRhiWidget, Vulkan backend). Findings will be posted as a comment.
Prior related perf work: GPU-composited slice flags (#3617, #3695), flag re-raster idle-pause (#3746/#3764).
Summary
The main window runs at 10–15 fps with multiple panadapters open, despite high-end hardware. CPU profiling shows the app is single-threaded-bound on the GUI thread in Qt's RHI backing-store composite path — not in AetherSDR's own logic (only ~2% of samples) and not GPU-starved. This issue captures the profile; the chosen fix is gated on an in-flight investigation of Qt's
rhiFlushbacking-store behavior.Repro / environment
RelWithDebInfo(-O2 -g -DNDEBUG), currentmain(post-v26.6.4).SpectrumWidget: QRhi initialized, backend: OpenGL). On Linux we never callQRhiWidget::setApi(), so it defaults to OpenGL.Profile (perf, DWARF call-graph, main thread, ~18k samples / 6s)
Thread distribution: total ~105–112% CPU across 47 threads, but the GUI/main thread alone is ~79% of one core; every other thread (panadapter worker, audio, Qt pool) is ≤10%. Classic single-GUI-thread cap.
Main-thread self-time by library:
QImagecopy / fill / blend (raster compositing)memcpy/memsetbehind the QImage opsCall graph (caller view):
Interpretation
With 4
QRhiWidgetpanadapters in one top-level window mixed with raster sibling widgets, Qt appears to repeatedly (a) copy/fill the raster backing store on the CPU viaQImage(~61% across Qt6Gui + libc) and (b) push it through the NVIDIA GL composite/present (~29%) — per panadapter present. That CPU composite, not the GPU, is the 10–15 fps cap.Two candidate fixes (pending investigation)
QImagebacking-store recopy (~61%). If Qt re-stages an unchanged full-window backing store on every RHI present (rather than dirty-rect / cached), guarding/caching that — or giving each panadapter its own native subsurface (WA_NativeWindow) / a single shared RHI surface so they bypass the shared raster backing-store composite — is the big win. Open question: isrhiFlush's copy full-window or dirty-rect, and is the staged texture cached across frames?setApi(Vulkan)+ aQVulkanInstanceinmain.cpp; not a launch-flag. May not touch the ~61% CPUQImagecost if that's backend-agnostic.Status
A scoped deep-research investigation into Qt 6.11's exact
rhiFlushbacking-store / dirty-rect behavior is in flight to decide between fix #1 and #2 (and rank the mitigations:WA_NativeWindowper panadapter, per-panadapter top-levelQWindow, single full-window RHI surface,WA_StaticContents, QOpenGLWidget vs QRhiWidget, Vulkan backend). Findings will be posted as a comment.Prior related perf work: GPU-composited slice flags (#3617, #3695), flag re-raster idle-pause (#3746/#3764).