feat(gui): re-add dBm / noise-floor scale in 3D stacked-trace mode#3937
Conversation
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>
There was a problem hiding this comment.
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 indss_mesh.vert(v=0) mapsbaseY = 1.0→specRect.bottom(), andridge = pow(sLin, max(zCurve,0.05)) * frontMaxRidgeFrac→topY = 1.0 - ridge. The overlay'sspecRect.bottom() - strength^zc * height*kFrontMaxRidgeFracreproduces that term for term, including thestd::max(0.05, m_dssZCurve)clamp that mirrors the shader'smax(zCurve, 0.05). Since the GPU mesh viewport (renderGpuFrame) and the CPU fallback quad are both stretched tospecRect, plot-space[0,1]maps tospecRectin 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 bywand raise their baseline, so a single amplitude axis is only honest forv=0, and the PR text says exactly that.- Chrome factoring is faithful.
drawDbmScaleChrome()is the unchanged strip background / border / ▲▼ block, and 2DdrawDbmScale()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 atDBM_STRIP_W/DBM_ARROW_H) is preserved in 3D. - Defensive ordering is right: chrome is drawn before the
rangeDb <= 0.0fearly-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>
|
Pushed follow-up commit What changed:
Validation from a clean worktree based on this PR head:
Note: |
ten9876
left a comment
There was a problem hiding this comment.
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.
|
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 ( @ten9876's two concerns are the right blockers. Holding off on approval pending their resolution:
The span-rounding note (round CI is clean (all 6 checks pass). Ready to approve once the two items are resolved and @ten9876's review is addressed. |
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
left a comment
There was a problem hiding this comment.
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)/2to 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.
#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>
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):Ref/dynamic range— byte-identical to before.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 inm_dssFloorOffsetDb, driven by the Display-pane 3D Floor slider(
dssFloorDepthSlider).setDssFloorDepth()now callsmarkOverlayDirty()sothe GPU-cached overlay (where the strip lives) rebuilds when the slider moves —
previously only
update()ran, leaving the scale stale in the GPU path. Thevalue persists as
Display3DFloorDepthand is pushed back into the slider onstartup via
syncDisplaySettings, so it round-trips across restarts.Preserved behavior
renderGpuFrame) and CPU (paintEvent) overlay paths use the 3D branch.(they drive
dssSpanDb()); the strip is now drawn over them with matchingclick targets.
Proof (agent automation bridge)
Verified live against a FLEX-8400M by driving the bridge:
evenly spaced.
dssFloorDepthSlider0 → baseline −98 dBm,24 → baseline −122 dBm (floor follows the slider, 24 dB shift).
3D stacked-trace — full-height linear scale
Floor tracks the 3D Floor slider (live + persisted)
3D Floor = 0 → baseline −98 | 3D Floor = 24 → baseline −122
2D — unchanged
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat