GPU-accelerated spectrum/waterfall rendering via QRhi (#391)#698
Merged
Conversation
SpectrumWidget conditionally inherits QRhiWidget when AETHER_GPU_SPECTRUM is enabled. Renders solid dark background via beginPass/endPass. Child widgets (VfoWidget, OverlayMenu, zoom buttons) composite correctly on top of the QRhi surface. - CMake: AETHER_GPU_SPECTRUM option with Qt6::GuiPrivate, Qt6::ShaderTools - Conditional base class via SPECTRUM_BASE_CLASS macro - paintEvent wrapped in #ifndef, QRhi stubs in #ifdef - Validated: renders, child widgets visible, mouse events work Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Waterfall QImage uploaded as RGBA8 texture, rendered as textured quad with ring buffer scrolling via fragment shader fract(uv.y + offset). All overlays (grid, FFT, band plan, slices, TNF, spots, scales) painted into an overlay QImage via QPainter, composited as alpha-blended texture. Key fix: fract() must be in the fragment shader, not vertex shader. Per-vertex fract() linearizes between two identical values when the UV range spans exactly 1.0 (fract(0+offset) == fract(1+offset)), producing constant UV across the quad. This was likely the same bug that killed the previous GPU attempt. Shaders compiled at build time via qt_add_shaders() — no binary .qsb files in the repo, shader errors are build errors not silent runtime failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace full 3.5MB QImage upload per frame with per-row incremental uploads (~6KB each). Track last uploaded row, walk the ring buffer delta, upload only changed rows with setDestinationTopLeft(). CPU reduction: 97% (QPainter) → 52% (full upload) → 40% (incremental). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split overlay into static (grid, band plan, scales, slices, TNF, spots) and dynamic (FFT spectrum line) layers. Static overlay only repainted when markOverlayDirty() is called from state-change methods. Per-frame work: memcpy cached static, paint FFT on top, upload composite. Replaced all update() calls with markOverlayDirty() except the two data paths (updateSpectrum, updateWaterfallRow) which only need a render, not a static overlay repaint. CPU progression: 97% (QPainter) → 52% (full upload) → 40% (incremental waterfall) → 35% (static overlay caching). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
markOverlayDirty() was declared inside #ifdef AETHER_GPU_SPECTRUM but called from non-guarded code. The method handles both paths internally so it must be visible regardless of the GPU flag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AETHER_GPU_SPECTRUM now defaults to ON for shipped binaries. Added qt6-base-private-dev, qt6-shader-baker, and qt6-shadertools-dev to the CI Docker image. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GPU spectrum defaults to ON but falls back to OFF if Qt6::ShaderTools or Qt6::GuiPrivate aren't found, instead of failing the build. Allows CI to build successfully while the Docker image is being updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Qt6::ShaderTools and Qt6::GuiPrivate must be present. Build fails loudly if missing, forcing the CI image to be updated rather than silently shipping without GPU acceleration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All release builds (AppImage, Windows, macOS DMG, macOS PKG) were missing qtwebsockets (TCI server), qtshadertools (GPU spectrum). ARM AppImage also missing serialport, hidapi, fftw3, portaudio, qt6-base-private-dev. This is why TCI wasn't in shipped binaries — the aqtinstall modules list only had qtmultimedia and qtserialport. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All packages listed for Arch, Ubuntu, Fedora, macOS with collapsible table explaining what each dependency enables. Includes GPU spectrum (qt6-shadertools), TCI (qt6-websockets), StreamDeck (hidapi), NR2 (fftw3) and all other auto-detected features. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QRhiWidget requires Qt 6.7+. CI Docker image has Qt 6.4 (Ubuntu 24.04) which doesn't have QRhiWidget. GPU spectrum now auto-disables on older Qt instead of failing the build. Release workflows use Qt 6.7 via aqtinstall and get GPU enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GPU-accelerated waterfall and overlay rendering using Qt6's QRhiWidget, reducing main thread CPU from 97% to 35% (64% reduction).
What Changed
Phase 1 — QRhiWidget Foundation
QRhiWidgetwhenAETHER_GPU_SPECTRUM=ONSPECTRUM_BASE_CLASSmacro — QPainter fallback preservedPhase 2 — GPU Waterfall + Overlay Rendering
m_waterfallQImage uploaded as RGBA8 GPU texturefract(uv.y + rowOffset)in fragment shader (not vertex — key fix)markOverlayDirty()), FFT spectrum line composited per-frame viamemcpy+ QPainterKey Bug Fix
fract()must be in the fragment shader, not vertex shader. Per-vertexfract()when UV spans exactly 0→1 producesfract(0+offset) == fract(1+offset), giving constant UV across the quad (every pixel samples the same row = vertical lines). This was likely the same bug that killed the previous GPU attempt.Build System
AETHER_GPU_SPECTRUMCMake option (default OFF)Qt6::GuiPrivatefor QRhi headers,Qt6::ShaderToolsfor build-time shader compilationqt_add_shaders()cross-compiles GLSL → SPIR-V/HLSL/MSL — no binary .qsb files in repoShaders
texturedquad.vert/frag— waterfall quad with ring buffer UV offsetoverlay.vert/frag— alpha-blended overlay passthroughCPU Progression
Test plan
🤖 Generated with Claude Code