Skip to content

Fix macOS GPU rendering: CPU optimization + build fix#719

Merged
ten9876 merged 3 commits into
aethersdr:mainfrom
boydsoftprez:testing/upstream-gpu-v0.8.0
Apr 5, 2026
Merged

Fix macOS GPU rendering: CPU optimization + build fix#719
ten9876 merged 3 commits into
aethersdr:mainfrom
boydsoftprez:testing/upstream-gpu-v0.8.0

Conversation

@boydsoftprez

@boydsoftprez boydsoftprez commented Apr 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix macOS build: Enable Qt6GuiPrivate on macOS — required for QRhi headers (rhi/qrhi.h) since aqtinstall was dropped in macOS: drop aqtinstall, use Homebrew for everything #711
  • Port GPU optimizations: Retina DPR overlay rendering (2x device pixel resolution for sharp text), overlay upload caching (m_overlayNeedsUpload flag), and render-time state change detection
  • Fix macOS CPU ~100% → ~35% idle with three targeted optimizations:
    1. paintEvent GPU guard: Skip redundant QPainter rendering when QRhi is active — the full software path (grid, spectrum, waterfall, overlays) was running every frame alongside GPU render()
    2. VFO repaint cascade prevention: VfoWidget::updatePosition() called raise() on 4 buttons every frame from renderGpuFrame(), forcing paintSiblingsRecursive and double GPU rendering per frame. Fixed with position-change guards and removing per-frame raise()
    3. Overlay dirty fix: setWfBlackLevel() called markOverlayDirty() 25x/sec from auto-black calculation, forcing full QPainter overlay re-rendering every frame. Waterfall params don't affect the overlay. Added change guards to setWfBlackLevel, setWfColorGain, setFrequencyRange, setDbmRange, and setSliceOverlay

Profiling results (macOS, Apple Silicon, Metal backend)

Metric Before After
Idle CPU ~100% ~35%
renderGpuFrame samples 161 (double render) 13
flushSubWindow compositing 232 69
paintSiblingsRecursive 151 46
Overlay repaints/sec 25 (every frame) 0 (only on state change)

Remaining ~35% is primarily unavoidable Qt WA_NativeWindow backing store compositing overhead (QCALayerBackingStore::flushSubWindow software blit).

Test plan

  • macOS: build with Homebrew Qt6, verify QRhi initialized, backend: Metal in log
  • macOS: CPU < 40% idle (connected, no tuning)
  • Verify overlay text sharp on Retina displays
  • Verify zoom, tune, click-to-tune, scroll-to-tune, resize
  • Verify VFO positioning on launch, tune, and band change
  • Verify multi-slice (add second slice, close slice)
  • Verify waterfall fill slider responsive
  • Linux: build unaffected (no Qt6GuiPrivate issue)

🤖 Generated with Claude Code

boydsoftprez and others added 3 commits April 4, 2026 22:34
…tate detection

Fixes ported from our GPU branch onto upstream's v0.8.0 codebase:

1. WA_NativeWindow + setApi(Metal) — required for QRhi initialization on macOS
2. Retina DPR overlay — render overlay QImages at device pixel resolution
   with setDevicePixelRatio() for sharp text on Retina displays
3. Overlay upload caching — skip 5.8MB texture upload when overlay content
   unchanged (only upload after m_overlayStaticDirty triggers repaint)
4. State change detection — monitor centerMhz, bandwidthMhz, refLevel,
   dynamicRange, spectrumFrac in renderGpuFrame to catch zoom/scroll that
   bypass markOverlayDirty()
5. Qt6GuiPrivate enabled on macOS (needed for rhi/qrhi.h)

Known tradeoff: WA_NativeWindow on macOS adds ~75% CPU overhead from
native window compositing (paintSiblingsRecursive). Without it, QRhi
doesn't initialize (grey screen). This is a Qt/macOS limitation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erlays

Three optimizations that reduce macOS CPU from ~100% to ~35% idle:

1. paintEvent GPU guard: When QRhi is initialized, skip the full QPainter
   rendering path and delegate to SPECTRUM_BASE_CLASS::paintEvent() which
   triggers the GPU render() pipeline. The QPainter path was running the
   complete software rendering (grid, spectrum, waterfall, overlays) every
   frame even though render() already handles everything via Metal.

2. VFO cascade prevention: VfoWidget::updatePosition() was calling move()
   and raise() on 4-5 child widgets every frame from renderGpuFrame().
   The raise() calls force z-order changes that trigger Qt's
   paintSiblingsRecursive, causing double rendering per frame. Fix: add
   position-change guard (skip if pos unchanged) and remove per-frame
   raise() calls (buttons are raised once on creation/activation).

3. Overlay dirty fix: setWfBlackLevel() was calling markOverlayDirty()
   25x/sec from the auto-black calculation, forcing full QPainter overlay
   re-rendering every frame. Black level only affects waterfall, not the
   overlay (grid/scales/markers). Fix: use update() instead, and add
   change guards to setWfBlackLevel, setWfColorGain, setFrequencyRange,
   setDbmRange, and setSliceOverlay to skip when values unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upstream CMakeLists.txt skips finding Qt6GuiPrivate on macOS to
avoid Homebrew/aqtinstall conflicts. But Qt6GuiPrivate is required for
QRhi headers (rhi/qrhi.h) used by the GPU spectrum rendering. With
Homebrew-only builds (aqtinstall dropped in aethersdr#711), this conflict no
longer applies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 enabled auto-merge (squash) April 5, 2026 02:39
@ten9876 ten9876 merged commit 19d031c into aethersdr:main Apr 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants