Skip to content

fix(spectrum): GPU path now renders FFT trace ABOVE the background image#3124

Merged
ten9876 merged 5 commits into
mainfrom
auto/fft-above-background
May 25, 2026
Merged

fix(spectrum): GPU path now renders FFT trace ABOVE the background image#3124
ten9876 merged 5 commits into
mainfrom
auto/fft-above-background

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

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

  • AetherSDR builds clean on Linux x86_64 GPU path
  • 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

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>
@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 07:56
ten9876 and others added 4 commits May 25, 2026 01:10
…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>
@ten9876 ten9876 merged commit f4c0798 into main May 25, 2026
4 checks passed
@ten9876 ten9876 deleted the auto/fft-above-background branch May 25, 2026 09:10
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
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
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