fix(spectrum): GPU path now renders FFT trace ABOVE the background image#3124
Merged
Conversation
Pre-fix the two paint paths disagreed on z-order for the user-set background
image vs the FFT trace:
Software fallback: bg image → grid → FFT trace → band plan → markers
GPU (QRhi) path: waterfall → FFT trace → static overlay (bg + grid +
band plan + markers)
Result: in GPU mode the bg image painted ON TOP of the FFT trace, tinting
and occluding it. Software mode rendered the FFT correctly on top of the
bg image.
This commit splits the GPU static overlay into two textures so the bg
image can render BELOW the FFT while the chrome (grid, band plan, scales,
markers) stays above:
Clear → Waterfall → BG quad → FFT fill+line → Chrome overlay quad
## Changes
src/gui/SpectrumWidget.h
* `m_overlayBg` QImage — bg image alone, parallel to m_overlayStatic
* `m_bgGpuTex` QRhiTexture — uploaded copy of m_overlayBg
* `m_bgSrb` QRhiShaderResourceBindings — points at m_bgGpuTex via the
shared m_ovSampler
* `m_overlayBgNeedsUpload` bool — drives the upload step
src/gui/SpectrumWidget.cpp
* initOverlayPipeline(): parallel scaffolding for the bg texture +
SRB. Reuses m_ovPipeline, m_ovVbo, m_ovSampler (same alpha-blending
shader, same quad geometry, same linear sampler).
* Resize path: bg texture follows m_overlayStatic's pixel size.
* Static-overlay paint: bg image painting moved into a separate
`if (m_overlayStaticDirty)` block that draws into m_overlayBg.
The original block still draws grid + band plan + scales + markers
into m_overlayStatic, just without the bg image step.
* Upload step: m_overlayBg uploaded to m_bgGpuTex alongside the
existing m_overlayStatic upload.
* Render order: new bg quad render block inserted between the
waterfall pass and the FFT fill/line pass.
## Compositing math preserved
Both paths still use the same `(1.0 - m_bgOpacity / 100.0)` opacity curve
for the bg image, just applied to different layers — software path
overlays a dark tint at `m_bgOpacity * 255 / 100` alpha on top of a full-
opacity bg; GPU path draws the bg image directly with reduced opacity
against the dark clear colour. Visual result is equivalent at the endpoints
and follows the same linear interpolation between them.
## Software path unchanged
The software fallback (`#else !AETHER_GPU_SPECTRUM`) already rendered FFT
above bg. No edits there.
## Build verified clean
AetherSDR builds clean on Linux x86_64 GPU path. All 320 targets,
including every test, build without errors.
## What's still inconsistent (not fixed here)
Grid lines and band-plan stripes are also drawn into the static overlay
in GPU mode, so they currently sit ABOVE the FFT trace where software
mode draws them BELOW. Visually mild (grid lines are thin), and out of
scope for the "bg vs FFT" fix Jeremy asked for. If the grid-z-order
becomes a real annoyance, the same split-into-pre/post-FFT-overlays
pattern extends to a third texture for grid + band plan.
73, Jeremy KK7GWY & Claude (AI dev partner)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mage
Builds on the FFT-above-bg z-order fix earlier in this branch. Adds a
new bottom-most layer to the spectrum area: a solid fill colour the
operator picks via a colour swatch button in the Spectrum Overlay →
Display tab, next to the existing "Background:" label / Choose… /
Clear row.
Z-order in the spectrum area, bottom → top:
1. m_bgFillColor (user-pickable, default #0a0a14)
2. Background image (opacity = 1 - m_bgOpacity/100; fade the
BG Opacity slider to see the fill colour
bleed through)
3. Grid lines (software path) — kept above FFT in GPU
path's chrome overlay
4. FFT trace
5. Band plan + markers (above FFT in both paths)
## UI
Spectrum Overlay → Display tab, "Background:" row layout changes from:
Background: [Choose...] [Clear]
to:
Background: [color] [Choose...] [Clear]
The colour swatch's background is the current fill colour; click opens a
QColorDialog seeded with the current value, accept emits a new colour
and the swatch updates on the spot.
## Compositing
Both paint paths now use the same algorithm:
1. fillRect(specRect, m_bgFillColor) // full opacity
2. if image: drawImage at opacity (1 - m_bgOpacity/100)
The pre-fix software path drew the image at full opacity then overlaid
a dark `#0a0a14` tint at `m_bgOpacity * 255 / 100` alpha — visually
similar at endpoints but the new approach is symmetric across paths and
correctly composites with whatever fill colour the user picks (the
previous code hard-coded the tint colour).
GPU compositing math is identical to the pre-fix branch tip — the bg
image still uses `setOpacity(1 - m_bgOpacity/100)` against the bg
texture, which itself is now seeded with `m_bgFillColor` instead of
transparent.
## Persistence
AppSettings key: SpectrumBackgroundFillColor (per-panadapter via
settingsKey()); saved as `#rrggbb`. Default `#0a0a14` matches the
pre-feature visual exactly, so first-launch operators see no change
until they pick a new colour.
## Code surface
src/gui/SpectrumOverlayMenu.{h,cpp}
+ signal backgroundFillColorChanged(QColor)
+ m_bgFillColorBtn QPushButton (colour-styled swatch)
+ syncExtraDisplaySettings() takes a new bgFillColor parameter
(defaulted so unrelated callers stay valid)
src/gui/SpectrumWidget.{h,cpp}
+ m_bgFillColor + setBackgroundFillColor + backgroundFillColor
accessors
+ load/save via AppSettings on construct/setter
+ fill the bg layer with m_bgFillColor in both paint paths
src/gui/MainWindow.cpp
+ wire the signal to setBackgroundFillColor + sync the swatch
## Build verified clean
All 320 targets including every test target.
73, Jeremy KK7GWY & Claude (AI dev partner)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bg-image layer scaffolding added earlier in this branch missed its delete lines in the release sweep, leaking a QRhiTexture + a QRhiShaderResourceBindings on every SpectrumWidget destruction. Same lifecycle as the m_ov* resources it sits next to, so add the deletes in the same teardown block. Caught by the stale-code audit during PR self-review before merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Display panel's "Reset to Defaults" button resets every other
SpectrumWidget display knob (FFT alpha, waterfall scheme, bg image, bg
opacity, etc.) but missed m_bgFillColor — so an operator who set a
custom fill colour would keep it after a reset while everything else
snapped back to defaults.
Add the three parallel lines so the reset path matches the rest:
1. sw->setBackgroundFillColor(QColor(0x0a, 0x0a, 0x14)) — runtime value
2. s.setValue("BackgroundFillColor", "#0a0a14") — persisted setting
3. syncExtraDisplaySettings(..., QColor(0x0a, 0x0a, 0x14)) — swatch UI
Default colour mirrors the pre-feature visual exactly (the same
#0a0a14 used as m_bgFillColor's default initializer).
Caught during the same PR self-review pass that found the m_bgGpuTex
teardown leak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MainWindow has a per-pan re-initialization path (around line 2810) that reloads BackgroundImage and BackgroundOpacity from AppSettings after certain operations (pan reset, band switch, etc.). The new BackgroundFillColor setting needs the same treatment so a custom fill colour survives those events instead of silently snapping back to the SpectrumWidget's loaded value (which may or may not still match, depending on whether the user changed it after the last load). Same pattern as the existing setBackgroundImage/setBackgroundOpacity calls — read from AppSettings, validate, push through the setter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NF0T
pushed a commit
that referenced
this pull request
Jun 5, 2026
…ayDirty() (#3338) (#3411) ## Problem The 9 spot-display setters in `SpectrumWidget.h` call `update()`, which on the GPU path reuses the cached `m_overlayStatic` chrome texture without re-baking it. `drawSpotMarkers()` never re-runs, so toggling **Spot Lines** (or changing spot font size, start %, colours, etc.) has no immediate visual effect — the change only takes hold when something else triggers `markOverlayDirty()` (e.g. a new incoming spot). This regression was introduced by #3124 (GPU path caches the chrome overlay) and became consistently visible after #3299 (discrete GPU preference on Windows) caused more users to hit the GPU path. ## Fix Replace `update()` with `markOverlayDirty()` in all 9 setters. `markOverlayDirty()` calls `update()` internally — identical behaviour on the CPU path, correct invalidation on the GPU path. All other display setters in the file (background opacity, MQTT display, prop forecast, etc.) already use `markOverlayDirty()`. The spot setters were the only outliers. ## Testing Verified build compiles cleanly on macOS. GPU-path visual verification (toggle Spot Lines with spots visible → lines appear/disappear immediately) requires Windows with discrete GPU — the reporter in #3338 should be able to confirm. Fixes #3338
11 tasks
NF0T
pushed a commit
that referenced
this pull request
Jun 21, 2026
…U path (#3606) (#3713) ## Summary Fixes #3606. In the GPU (`QRhi`) render path the panadapter **grid** (the dB / frequency lines that form the rectangular cells) was composited **on top of** the FFT trace, so it painted over the signal peaks — visible interference in the reporter's screenshot. This moves the grid below the trace, matching the software path and conventional panadapter rendering. ## Root cause — GPU vs. software z-order parity gap The software (`QPainter`) path already draws the grid correctly, **behind** the trace: ```cpp drawGrid(p, specRect); // grid first drawSpectrum(p, specRect); // trace on top ``` The GPU path composites the frame from texture layers. Per `renderGpuFrame()` the order is (bottom → top): 1. waterfall 2. **background layer** `m_overlayBg` (fill + background image) — explicitly drawn *below* the FFT (this is what #3124 fixed for the background image) 3. FFT fill + line (the trace / peaks) 4. **static overlay** `m_overlayStatic` — drawn *above* the FFT 5. slice flags The grid was baked into the **static overlay** (step 4), so it composited *after* — i.e. over — the peaks. #3124 moved the background *image* below the trace but left the grid on top; this closes the remaining half of that parity gap. > The maintainer triage bot independently reached the same diagnosis and proposed this exact move — see the [triage comment on #3606](#3606). ## The change Move **only** the grid into `m_overlayBg` (the layer already composited below the FFT). Everything else in the static overlay — band plan, slice/TNF markers, frequency scale, indicators — stays on top, exactly as the software path draws them after `drawSpectrum()`. Resulting GPU order, now identical to software: ``` waterfall → bg fill/image → grid → FFT fill/line → band plan / markers / scales / flags ``` ## Performance — neutral by construction The grid is in the render hot path, so worth being explicit: this is **zero-cost**. `m_overlayBg` and `m_overlayStatic` are both rebuilt under the *same* `m_overlayStaticDirty` gate and uploaded on the same cadence. The grid was already painted once per dirty event; it is now painted into a different cached layer — same paint, same upload, no per-frame work added. No interaction with the waterfall or FFT pipelines. ## Constitution principle honored **Principle XI — Fixes Are Demonstrated.** Verified visually: grid renders behind the peaks (before/after screenshots below). The software path is the reference for correct z-order. <!-- before: grid painting over the peaks (current release) --> <!-- after: grid sitting behind the peaks --> ## Test plan - [x] Local build passes (Windows, Qt 6.8.3 msvc2022_64, GPU path on) - [x] Visual check: grid now composites behind the FFT trace, matching the software path - [ ] Existing tests pass (CI) - [x] Reproduction / expected result documented (#3606) - [ ] Cross-platform GPU paths (Metal/Vulkan) — same code path; a macOS/Linux screenshot welcome ## Checklist - [x] Commits are signed (SSH) - [x] No new flat-key `AppSettings` calls (N/A) - [x] Code is clean-room (Principle IV) - [x] All meter UI uses `MeterSmoother` (N/A) - [x] Documentation updated if user-visible behavior changed (N/A — code comments at the move sites) - [x] Security-sensitive changes reference a GHSA if applicable (N/A) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…age (aethersdr#3124) ## Summary Pre-fix the two spectrum paint paths disagreed on z-order for the user-set background image vs the FFT trace: | Path | Z-order (bottom → top) | |---|---| | Software fallback | bg image → grid → **FFT** → band plan → markers | | GPU (QRhi) | waterfall → **FFT** → static overlay (bg + grid + band plan + markers) | Result: in GPU mode the bg image painted **on top of** the FFT trace, tinting and occluding it. Software mode rendered the FFT correctly on top of the bg image. This PR splits the GPU static overlay into **two textures** so the bg image can render BELOW the FFT while the chrome (grid, band plan, scales, markers) stays above. Render order is now: \`\`\` Clear → Waterfall → BG quad → FFT fill+line → Chrome overlay quad \`\`\` ## What changed ### \`src/gui/SpectrumWidget.h\` - \`m_overlayBg\` QImage — bg image alone, parallel to \`m_overlayStatic\` - \`m_bgGpuTex\` QRhiTexture — uploaded copy of \`m_overlayBg\` - \`m_bgSrb\` QRhiShaderResourceBindings — points at \`m_bgGpuTex\` via the shared \`m_ovSampler\` - \`m_overlayBgNeedsUpload\` bool — drives the upload step ### \`src/gui/SpectrumWidget.cpp\` - \`initOverlayPipeline()\`: parallel scaffolding for the bg texture + SRB. **Reuses** \`m_ovPipeline\`, \`m_ovVbo\`, \`m_ovSampler\` — same alpha-blending shader, same quad geometry, same linear sampler. Only the SRB switches between draws so the same overlay pipeline paints two different textures. - Resize path: bg texture follows \`m_overlayStatic\`'s pixel size. - Static-overlay paint: bg image painting moved out into its own \`if (m_overlayStaticDirty)\` block that draws only into \`m_overlayBg\`. The original block still composes grid + band plan + scales + markers into \`m_overlayStatic\`, just without the bg image step. - Upload step: \`m_overlayBg\` uploaded to \`m_bgGpuTex\` alongside the existing \`m_overlayStatic\` upload (same upload batch). - Render order: new bg-quad render block inserted between the waterfall pass and the FFT fill/line pass. ## Compositing math preserved Both paths still use the same \`(1.0 - m_bgOpacity / 100.0)\` opacity curve for the bg image — software path overlays a dark tint at \`m_bgOpacity * 255 / 100\` alpha on top of a full-opacity bg; GPU path draws the bg image directly with reduced opacity against the dark clear colour. Visual result is equivalent at the endpoints and follows the same linear interpolation between them, just with the bg image now sitting *below* the FFT. ## Software path unchanged The software fallback (\`#else !AETHER_GPU_SPECTRUM\`) already rendered FFT above bg. No edits there. ## Build verified clean - [x] AetherSDR builds clean on Linux x86_64 GPU path - [x] All 320 targets including every test target build without errors ## What's still inconsistent (out of scope) Grid lines and band-plan stripes are also drawn into the static overlay in GPU mode, so they currently sit *above* the FFT trace where software mode draws them *below*. Visually mild (grid lines are thin), and out of scope for the bg-vs-FFT fix Jeremy asked for. If the grid z-order becomes a real annoyance, the same split-into-pre/post-FFT-overlays pattern extends to a third texture for grid + band plan. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: set a background image with Choose…, drag the BG Opacity slider — confirm the FFT trace remains crisply visible on top of the image at all opacity values, on both GPU and software paths (toggle via \`AETHER_NO_GPU=1\` env var) 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…ayDirty() (aethersdr#3338) (aethersdr#3411) ## Problem The 9 spot-display setters in `SpectrumWidget.h` call `update()`, which on the GPU path reuses the cached `m_overlayStatic` chrome texture without re-baking it. `drawSpotMarkers()` never re-runs, so toggling **Spot Lines** (or changing spot font size, start %, colours, etc.) has no immediate visual effect — the change only takes hold when something else triggers `markOverlayDirty()` (e.g. a new incoming spot). This regression was introduced by aethersdr#3124 (GPU path caches the chrome overlay) and became consistently visible after aethersdr#3299 (discrete GPU preference on Windows) caused more users to hit the GPU path. ## Fix Replace `update()` with `markOverlayDirty()` in all 9 setters. `markOverlayDirty()` calls `update()` internally — identical behaviour on the CPU path, correct invalidation on the GPU path. All other display setters in the file (background opacity, MQTT display, prop forecast, etc.) already use `markOverlayDirty()`. The spot setters were the only outliers. ## Testing Verified build compiles cleanly on macOS. GPU-path visual verification (toggle Spot Lines with spots visible → lines appear/disappear immediately) requires Windows with discrete GPU — the reporter in aethersdr#3338 should be able to confirm. Fixes aethersdr#3338
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
Pre-fix the two spectrum paint paths disagreed on z-order for the user-set background image vs the FFT trace:
Result: in GPU mode the bg image painted on top of the FFT trace, tinting and occluding it. Software mode rendered the FFT correctly on top of the bg image.
This PR splits the GPU static overlay into two textures so the bg image can render BELOW the FFT while the chrome (grid, band plan, scales, markers) stays above. Render order is now:
```
Clear → Waterfall → BG quad → FFT fill+line → Chrome overlay quad
```
What changed
`src/gui/SpectrumWidget.h`
`src/gui/SpectrumWidget.cpp`
Compositing math preserved
Both paths still use the same `(1.0 - m_bgOpacity / 100.0)` opacity curve for the bg image — software path overlays a dark tint at `m_bgOpacity * 255 / 100` alpha on top of a full-opacity bg; GPU path draws the bg image directly with reduced opacity against the dark clear colour. Visual result is equivalent at the endpoints and follows the same linear interpolation between them, just with the bg image now sitting below the FFT.
Software path unchanged
The software fallback (`#else !AETHER_GPU_SPECTRUM`) already rendered FFT above bg. No edits there.
Build verified clean
What's still inconsistent (out of scope)
Grid lines and band-plan stripes are also drawn into the static overlay in GPU mode, so they currently sit above the FFT trace where software mode draws them below. Visually mild (grid lines are thin), and out of scope for the bg-vs-FFT fix Jeremy asked for. If the grid z-order becomes a real annoyance, the same split-into-pre/post-FFT-overlays pattern extends to a third texture for grid + band plan.
Test plan
73, Jeremy KK7GWY & Claude (AI dev partner)
🤖 Generated with Claude Code