Skip to content

GPU-accelerated spectrum/waterfall rendering via QRhi (#391)#698

Merged
ten9876 merged 11 commits into
mainfrom
feature/gpu-spectrum
Apr 4, 2026
Merged

GPU-accelerated spectrum/waterfall rendering via QRhi (#391)#698
ten9876 merged 11 commits into
mainfrom
feature/gpu-spectrum

Conversation

@ten9876

@ten9876 ten9876 commented Apr 4, 2026

Copy link
Copy Markdown
Collaborator

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

  • SpectrumWidget conditionally inherits QRhiWidget when AETHER_GPU_SPECTRUM=ON
  • Compile-time toggle via SPECTRUM_BASE_CLASS macro — QPainter fallback preserved
  • Child widgets (VfoWidget, OverlayMenu, zoom buttons) composite correctly on QRhi surface

Phase 2 — GPU Waterfall + Overlay Rendering

  • Waterfall m_waterfall QImage uploaded as RGBA8 GPU texture
  • Ring buffer scrolling via fract(uv.y + rowOffset) in fragment shader (not vertex — key fix)
  • Incremental row uploads: only changed rows (~6KB/frame vs 3.5MB full upload)
  • All overlays (grid, band plan, scales, slices, TNF, spots) painted into overlay QImage via QPainter, uploaded as alpha-blended texture
  • Static overlay caching: only repainted on state changes (markOverlayDirty()), FFT spectrum line composited per-frame via memcpy + QPainter

Key Bug Fix

fract() must be in the fragment shader, not vertex shader. Per-vertex fract() when UV spans exactly 0→1 produces fract(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_SPECTRUM CMake option (default OFF)
  • Qt6::GuiPrivate for QRhi headers, Qt6::ShaderTools for build-time shader compilation
  • qt_add_shaders() cross-compiles GLSL → SPIR-V/HLSL/MSL — no binary .qsb files in repo

Shaders

  • texturedquad.vert/frag — waterfall quad with ring buffer UV offset
  • overlay.vert/frag — alpha-blended overlay passthrough

CPU Progression

Stage CPU Change
Baseline (QPainter) ~97%
GPU waterfall (full upload) ~52% -45%
Incremental row uploads ~40% -57%
Static overlay caching ~35% -64%

Test plan

  • Linux OpenGL — renders correctly, 35% CPU
  • macOS Metal
  • Windows D3D11
  • Multi-pan (4 instances)
  • Resize without crash
  • Mouse interaction (click-to-tune, scroll, filter drag)

🤖 Generated with Claude Code

ten9876 and others added 11 commits April 4, 2026 13:28
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>
@ten9876 ten9876 merged commit 8710f58 into main Apr 4, 2026
2 checks passed
@ten9876 ten9876 deleted the feature/gpu-spectrum branch April 4, 2026 22:26
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.

1 participant