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