Skip to content

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

Open
jensenpat wants to merge 3 commits into
aethersdr:mainfrom
jensenpat:feat/3dss-dbm-scale
Open

feat(gui): re-add dBm / noise-floor scale in 3D stacked-trace mode#3937
jensenpat wants to merge 3 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.

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.

2 participants