Skip to content

feat(gui): re-add dBm / noise-floor scale in 3D stacked-trace mode#3937

Merged
ten9876 merged 4 commits into
aethersdr:mainfrom
jensenpat:feat/3dss-dbm-scale
Jul 1, 2026
Merged

feat(gui): re-add dBm / noise-floor scale in 3D stacked-trace mode#3937
ten9876 merged 4 commits into
aethersdr:mainfrom
jensenpat:feat/3dss-dbm-scale

Conversation

@jensenpat

@jensenpat jensenpat commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What

Re-adds the right-edge dBm / noise-floor scale to the panadapter when the
spectrum is in 3D stacked-trace mode, as a full-height linear axis that
reads just like the 2D scale, and wires it to sync + persist with the
Display-pane 3D Floor slider.

How

Full-height linear scale. The 3D scale spans the whole spectrum region: the
measured noise floor sits at the baseline and Ref at the top, evenly spaced. The
2D and 3D scales are now structurally identical (they differ only in top/range),
so the tick drawing is factored into a shared drawDbmScaleLabels(topDbm, rangeDb):

  • 2D passes Ref / dynamic rangebyte-identical to before.
  • 3D passes dssFloorDbm() + dssSpanDb() / dssSpanDb().

The perspective surface is foreshortened, so the ticks are an amplitude
reference (floor/Ref anchored), not pixel-aligned to individual receding
ridges — by design.

Sync + persist with the 3D Floor slider. dssFloorDbm() folds in
m_dssFloorOffsetDb, driven by the Display-pane 3D Floor slider
(dssFloorDepthSlider). setDssFloorDepth() now calls markOverlayDirty() so
the GPU-cached overlay (where the strip lives) rebuilds when the slider moves —
previously only update() ran, leaving the scale stale in the GPU path. The
value persists as Display3DFloorDepth and is pushed back into the slider on
startup via syncDisplaySettings, so it round-trips across restarts.

Preserved behavior

  • 2D is byte-identical (shared chrome + shared label helper, Ref/range args).
  • Both GPU (renderGpuFrame) and CPU (paintEvent) overlay paths use the 3D branch.
  • Existing strip drag / arrow / wheel ref-level interactions already fired in 3D
    (they drive dssSpanDb()); the strip is now drawn over them with matching
    click targets.

Proof (agent automation bridge)

Verified live against a FLEX-8400M by driving the bridge:

  • Full-height linear — 3D scale renders Ref at top → floor at the baseline,
    evenly spaced.
  • Tracks the slider livedssFloorDepthSlider 0 → baseline −98 dBm,
    24 → baseline −122 dBm (floor follows the slider, 24 dB shift).
  • Persists — set slider 18 → quit → relaunch → slider restored to 18.
  • 2D unchanged.

3D stacked-trace — full-height linear scale

3D stacked trace, full-height linear dBm scale

Floor tracks the 3D Floor slider (live + persisted)

3D Floor = 0 → baseline −98   |   3D Floor = 24 → baseline −122

2D — unchanged

2D waterfall, dBm scale unchanged

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

The right-edge dBm scale was suppressed when the panadapter switched to
3D stacked-trace mode, because the surface's vertical axis is not a linear
dBm axis. Re-add it with ticks projected onto the front (live) trace's
ridge band, so a peak read against the scale gives its true dBm — the
reference behaviour of a real stacked-trace scope.

The 3D scale reuses the exact floor/span/zCurve values the CPU fallback
and the GPU mesh render from (dssFloorDbm / dssSpanDb / m_dssZCurve via
DssRenderer::kFrontMaxRidgeFrac), so it can never drift from the drawn
surface: the noise floor sits at the baseline, Ref at the top of the
ridge band, with the same pow(strength, zCurve) floor-lift.

Strip chrome (background, border, ref-adjust ▲▼ arrows) is factored into
a shared drawDbmScaleChrome() so the strip geometry and click targets are
identical in both modes — the existing drag/arrow/wheel ref-level
interactions already fired in 3D (they drive dssSpanDb), they just had no
strip drawn. The 2D scale is byte-identical (drawDbmScale now calls the
shared chrome then the unchanged label loop).

Proven live against a FLEX-8400M via the agent automation bridge:
toggling spectrumRenderModeCombo 2D↔3D and grabbing SpectrumWidget shows
the scale present in 3D (floor −104 at the baseline up through −20),
2D unchanged, and the toggle round-trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensenpat jensenpat added GUI User interface New Feature New feature request labels Jul 1, 2026
@jensenpat jensenpat marked this pull request as ready for review July 1, 2026 03:44
@jensenpat jensenpat requested a review from a team as a code owner July 1, 2026 03:44
@jensenpat jensenpat removed New Feature New feature request GUI User interface labels Jul 1, 2026

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jensenpat — this is a clean, well-reasoned change. I checked the 3D tick mapping against the actual render path and it lines up.

What I verified

  • yForDbm() matches the surface geometry exactly. The front row in dss_mesh.vert (v=0) maps baseY = 1.0specRect.bottom(), and ridge = pow(sLin, max(zCurve,0.05)) * frontMaxRidgeFractopY = 1.0 - ridge. The overlay's specRect.bottom() - strength^zc * height*kFrontMaxRidgeFrac reproduces that term for term, including the std::max(0.05, m_dssZCurve) clamp that mirrors the shader's max(zCurve, 0.05). Since the GPU mesh viewport (renderGpuFrame) and the CPU fallback quad are both stretched to specRect, plot-space [0,1] maps to specRect in both paths, so the scale tracks the drawn ridge in either renderer. Good call scoping the scale to the front (live) trace — receding rows shrink by w and raise their baseline, so a single amplitude axis is only honest for v=0, and the PR text says exactly that.
  • Chrome factoring is faithful. drawDbmScaleChrome() is the unchanged strip background / border / ▲▼ block, and 2D drawDbmScale() calls it then runs the original label loop verbatim — 2D is byte-identical as claimed, and the strip geometry (hence the existing drag/arrow/wheel ref-level hit targets at DBM_STRIP_W / DBM_ARROW_H) is preserved in 3D.
  • Defensive ordering is right: chrome is drawn before the rangeDb <= 0.0f early-return, so even a degenerate span still paints the strip and keeps click targets live. (dssSpanDb() clamps to [45,120] so that guard is effectively dead, but harmless.)

No convention, lifetime, or resource concerns — no QSettings, no raw pointers dereferenced, and the change is confined to the two files in scope.

One minor note (not blocking)

The scale reads rangeDb = dssSpanDb() unrounded, but the surface (both renderGpuFrame UBO at line ~8663 and buildDssImage at line ~7554) renders from std::round(dssSpanDb() * 2.0f) / 2.0f — quantized to 0.5 dB. floorDbm is already quantized inside dssFloorDbm(), so only the span differs, by ≤0.25 dB. That's ~1–2 px of tick drift near Ref (less lower down thanks to the pow compression), so visually a non-issue — but since the PR body emphasizes the scale using "the exact values the mesh renders from," rounding rangeDb the same way here would make that literally true and immune to any future span-quantization change.

Nice work — the shader-accurate mapping and the shared-chrome approach are the right way to do this.


🤖 aethersdr-agent · cost: $3.0507 · model: claude-opus-4-8

…loor slider

Rework the 3D stacked-trace dBm scale from a front-ridge-aligned nonlinear
strip (bottom ~46%, pow-curve spacing) to a full-height LINEAR axis: the
measured noise floor sits at the baseline and Ref at the top, evenly spaced,
so levels read exactly like the familiar 2D scale.

The 2D and 3D scales are now structurally identical, differing only in
top/range, so the tick-drawing is factored into a shared drawDbmScaleLabels()
(top dBm + span). 2D stays byte-identical (drawDbmScale passes Ref /
dynamic range); 3D passes dssFloorDbm()+span / span.

Sync + persist with the Display-pane "3D Floor" slider: setDssFloorDepth()
now markOverlayDirty() so the GPU-cached overlay (where the strip lives)
rebuilds when the slider moves — previously only update() ran, leaving the
scale stale in the GPU path. The floor value already persists as
Display3DFloorDepth and is pushed back into the slider on startup via
syncDisplaySettings, so it round-trips across restarts.

Proven live against a FLEX-8400M via the agent automation bridge:
 - 3D scale renders full-height linear (Ref at top, floor at baseline);
 - dssFloorDepthSlider 0 → baseline −98, 24 → baseline −122 (floor tracks
   the slider live);
 - set slider 18 → quit → relaunch → slider restored to 18 (persists);
 - 2D unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rfoust rfoust self-assigned this Jul 1, 2026
@rfoust

rfoust commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Pushed follow-up commit 873e8c6c to this PR.

What changed:

  • Plain click-drag on the 3D dBm strip now adjusts the 3D Floor value instead of changing the panadapter dBm range.
  • Ctrl/Meta-drag still changes the scale/span.
  • The floor drag direction is corrected: dragging up raises the floor/trace, dragging down lowers it.
  • The 3D dBm strip stays a full-height floor-anchored amplitude reference. I avoided the perspective-projected axis because that compresses labels into the lower ridge-height band.
  • The Display panel 3D Floor slider now syncs when the floor is changed from the dBm strip.
  • m_draggingDssFloor is included in the existing dBm-drag guard so pan level echoes and auto-noise-floor updates do not fight the gesture while dragging.

Validation from a clean worktree based on this PR head:

  • git diff --check
  • python3 tools/check_a11y.py src/gui/SpectrumWidget.cpp src/gui/SpectrumWidget.h src/gui/SpectrumOverlayMenu.cpp src/gui/SpectrumOverlayMenu.h src/gui/MainWindow_Wiring.cpp
  • cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
  • cmake --build build --target AetherSDR -- -j$(sysctl -n hw.ncpu)
  • cmake --build build --target kiwi_sdr_trace_math_test perf_telemetry_test -- -j$(sysctl -n hw.ncpu)
  • ctest --test-dir build --output-on-failure -R 'kiwi_sdr_trace_math_test|perf_telemetry_test'

Note: dss_renderer_test is not present as a target on this PR base. The exact mouse drag gesture still benefits from manual UI confirmation.

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High-effort review (8 finder angles + verification). The 3D dBm scale mostly works, but there are two spots where the scale can disagree with the surface (inline). Summary:

  • Scale lags the live noise-floor drift (concrete, fixable): the surface tracks the measured floor per-frame via the UBO, but this scale overlay only redraws on markOverlayDirty(), which the noise-floor update path never calls — so on a static pan the surface visibly shifts while the labels stay put (up to 0.5 dB).
  • Linear axis vs. z-curved surface (design call): ticks are placed linearly while surface height is pow(sLin, zCurve), so mid-scale ticks don't line up with the ridges. It's documented as a 'full-height reference' — worth deciding whether to honor the z-curve for the front row or reframe the axis.
  • Minor: round the scale's span to match the mesh (suggestion inline); the new floor-drag gesture is mouse-only (mitigated by the keyboard-reachable '3D Floor' slider).

Verified non-issues so you don't chase them: setDssFloorDepth does clamp [0,24] and has a change-guard (no per-mousemove full rebuild); the FP-loop / double-draw / tooltip-race concerns don't hold on the single GUI thread. Requesting changes mainly for the floor-drift lag plus a decision on the linear axis — the rest is polish.

Comment thread src/gui/SpectrumWidget.cpp
Comment thread src/gui/SpectrumWidget.cpp Outdated
Comment thread src/gui/SpectrumWidget.cpp
Comment thread src/gui/SpectrumWidget.cpp
@NF0T

NF0T commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@NF0T review — PR #3937

Six files, clean factoring, solid implementation. Read the full thread including @ten9876's CHANGES_REQUESTED before forming a view.

What's well done. The chrome/label split (drawDbmScaleChrome + drawDbmScaleLabels) is the right architecture — shared geometry means 2D is byte-identical and the 3D strip inherits the existing drag/arrow/wheel hit targets for free. aethersdr-agent verified the tick-to-shader mapping: dssFloorDbm() and dssSpanDb() feed the same values the GPU UBO and CPU fallback render from, so the scale can't drift from the surface due to parameter mismatch. The m_draggingDssFloor guard in anyDragActive() is correct (prevents pan-level echoes fighting the gesture). dssFloorDepthResolvedsyncDssFloorDepth round-trip via signal/slot is the right wiring shape.

@ten9876's two concerns are the right blockers. Holding off on approval pending their resolution:

  1. Floor-drift lag — the measured noise floor updates per-frame via the UBO, but markOverlayDirty() is never called from the floor-update path, so the scale labels lag when the radio's noise floor auto-drifts. This is a concrete bug and fixable: the measured-floor update path needs a markOverlayDirty() call in 3D mode.

  2. Linear axis vs. z-curved surface — the design call on whether ticks should honor pow(sLin, zCurve) for the front row or stay as a "full-height reference" is @ten9876's to make. Either choice is defensible; it just needs a decision.

The span-rounding note (round dssSpanDb() to 0.5 dB to match the mesh UBO) is worth folding in alongside whichever of the above gets implemented.

CI is clean (all 6 checks pass). Ready to approve once the two items are resolved and @ten9876's review is addressed.

@NF0T NF0T self-assigned this Jul 1, 2026
Two review fixes (aethersdr#3937):

- The 3D dBm scale is anchored to dssFloorDbm(), which drifts every FFT
  frame with the measured noise floor. The surface UBO re-reads it per
  frame, but the scale is drawn from the cached static overlay, which the
  floor-update path never marks dirty — so the surface shifted vertically
  while the labels stayed stale (up to the 0.5 dB quantum, indefinitely).
  Track the quantised 3D floor in the renderGpuFrame display-state change
  detector so a floor change marks the overlay dirty (3D only; 2D is
  Ref-anchored).

- Round the scale's span with std::round(span*2)/2 to match the surface
  (buildDssImage / renderGpuFrame round the same way), so labels and ridges
  agree to the pixel instead of a sub-dB top/bottom skew.

Left for the author: whether to apply the height z-curve to tick placement
(currently a documented linear "full-height reference" axis) is a design
call, and the strip drag stays mouse-only (mitigated by the 3D Floor slider).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — all four findings resolved.

Fixed in 2f5ac35:

  • Scale lagged the live noise-floor drift → the quantised 3D floor is now tracked in renderGpuFrame's display-state change detector, so a floor change marks the overlay dirty and the scale redraws in lock-step with the surface (3D-only; 2D stays Ref-anchored).
  • Span rounding mismatch → drawDbmScale3D now rounds the span std::round(span*2)/2 to match buildDssImage / renderGpuFrame, so labels and ridges agree to the pixel.

Accepted as-is (maintainer decision):

  • Linear vs. z-curved axis → keep the linear full-height amplitude reference. On a perspective/curved surface no axis can be truly pixel-accurate, and the front-ridge-aligned alternative would compress ticks into the lower ~46% of the strip. Documented behavior stands.
  • Floor-drag a11y → the keyboard-accessible "3D Floor" slider already drives the same setting, so the mouse-only strip gesture is an accepted convenience.

Build/macOS/Windows green. Clearing the earlier changes-requested.

@ten9876 ten9876 enabled auto-merge (squash) July 1, 2026 17:27
@ten9876 ten9876 merged commit 6e4fec6 into aethersdr:main Jul 1, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jul 2, 2026
#3961)

## Release documentation prep for **v26.7.1** (2026-07-02)

29 commits since v26.6.5. Docs-only + version bump — no code behavior
change.

### Version bump 26.6.5 → 26.7.1
The three canonical locations (per `AGENTS.md`):
- `CMakeLists.txt:2` — `project(AetherSDR VERSION 26.7.1)` →
`AETHERSDR_VERSION` (the app's version string)
- `README.md` — Current version line
- `AGENTS.md` — Current version line

### `CHANGELOG.md`
Promoted `[Unreleased]` → **`## [v26.7.1] — 2026-07-02`** in house style
(v-prefix, em-dash, "N commits since…" opener + headline), authored from
all merged PRs and categorized:
- **Added:** 3D stacked-trace spectrum (#3899), 3D dBm scale (#3937),
in-process NVIDIA BNR + AFX download-on-demand (#3902), TX meter
mouse-over readouts (#3936), SWR manual sweep range (#3885), CW sidetone
capture (#3895), KiwiSDR metadata scaffolding (#3898), bridge verbs
(#3920)
- **Changed:** BNR container/NIM backend removed, 60 fps + per-pixel GPU
FFT trace (#3958), FlexLib-sourced model capabilities (#3954/#2177), RX
speaker-latency cut (#3897), Display pane sections (#3935)
- **Fixed:** #3922, #3926, #3941, #3940, #3942, #3921, #3903, #3892,
#3924, #3891, #3890, #3900, #3947
- Fresh empty `[Unreleased]` restored above the release block.
Internal/CI/docs/self-fix PRs (#3931/#3916/#3934/#3929) omitted per
house style.

### `ROADMAP.md`
- Cycle header → "post-v26.7.1"
- NVIDIA BNR removed from **In flight** (shipped this cycle)
- Five v26.7.1 highlights prepended to **Recently shipped**

### `README.md`
- Highlights: GPU spectrum bullet now notes the **per-pixel FFT trace at
up to 60 fps** and the **3D stacked-trace** spectrum mode

### Reviewer notes
- Touches protected paths (`*.md` + `AGENTS.md` Tier-1 +
`CMakeLists.txt`) — needs `@aethersdr/infrastructure` sign-off.
- Tagging `v26.7.1` and cutting the release build are **not** part of
this PR (docs prep only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

4 participants