Summary
This is an RFC requesting design feedback on a new "3-Dimensional Spectrum Stream" panadapter render mode — a Yaesu / Rohde-&-Schwarz-style perspective stacked-trace surface.
A new Display-pane option, Spectrum: 2D / 3D, switches the spectrum region between the classic FFT-trace and the spectrum stream surface. The waterfall, frequency scale, slice/VFO markers, spots, memories, and nav-hints all keep working underneath/over it.
Motivation
Stacked-trace 3D spectrum displays make weak signals "in the weeds" easier to spot than a flat 2D trace or a waterfall — ARRL's lab measured Yaesu's as 2–4 dB more sensitive than its own waterfall. Several users have asked for a comparable view. The goal is a fast, good-looking, cross-platform (macOS/Windows/Linux) implementation that reuses our existing Qt + QRhi infrastructure.
Status
Live-verified RX-only on a FLEX-8400M: dense/smooth surface, visible noise floor with a floor→peak colour gradient, dim receding trace lines, fluid pan (panFps ≈ 24, uiLag ≈ 1 ms, 0 % FFT loss); 2D round-trips unchanged.
Design (current)
Rendering — GPU height-map mesh (AETHER_GPU_SPECTRUM, 3D-only):
- New shaders
resources/shaders/dss_mesh.{vert,frag} (compiled to .qsb via the existing qt_add_shaders). They output NDC directly (like spectrum.vert); occlusion is by back-to-front draw order, not a depth test — we don't assume a depth-stencil attachment exists on the QRhiWidget render target on every backend.
- A static vertex grid (768 cols × 96 rows) of "curtain" quads (ridge vertex + floor vertex) plus a ridge-only line buffer for the outline. Geometry never rebuilds, so pan/zoom are free (just a uniform + redraw).
- Per-vertex height is sampled from a ring-buffered R16F height texture (cols × rows of dBm). Each new FFT frame uploads one row (~1.5 KB) instead of re-rasterizing/uploading a full image.
- Two pipelines share the shaders: opaque
TriangleStrip fill + alpha-blended LineStrip outline.
- A
ThreeDssRenderer class is the shared smoothed-dBm ring store (peak-preserving downsample + 3-tap spatial + temporal IIR) feeding both the mesh upload and the CPU fallback.
Amplitude / noise floor: ridge height = clamp((dbm − floorDbm) / rangeDb) with a non-linear Z-curve. floorDbm is anchored to the measured noise floor (m_measuredNoiseFloorDbm, the same estimator the 2D auto-floor uses) minus a user "floor depth", so the floor carpet lifts into view. rangeDb is Ref-derived (clamped 45–120 dB). Colour is independent of height: a 256×1 palette LUT gives a vertical floor→peak gradient.
Palette: follows the existing waterfall colour-scheme combo (no new palette UI).
Trace outline: a dim, depth-faded per-row hairline (Yaesu style), distinct from the solid fill.
Controls (Display pane): the Spectrum: 2D / 3D combo + a 3D Floor depth slider (0–24 dB). Both persisted per-panadapter. (Bridge-addressable: spectrumRenderModeCombo, dssFloorDepthSlider.)
Fallback: if the mesh pipeline can't be created (e.g. R16F unsupported), it falls back to a capped cached-QImage quad rendered by DssRenderer on the CPU. 2D is byte-identical to today. Currently, it's pretty poor performance and I wouldn't recommend shipping it.
Key design decisions (seeking validation)
- GPU mesh over CPU raster. The CPU painter's-algorithm path couldn't deliver the sample density without choking pan; the mesh makes density, pan, occlusion, and the outline nearly free. CPU path retained as software fallback.
- No depth test; back-to-front draw for occlusion. Avoids depending on a depth attachment across Metal/Vulkan/D3D. Correct, but means no per-pixel depth (fine for this geometry).
- R16F ring height texture, Nearest sampling. Grid is as dense as the texture, so Nearest is enough; R16F keeps uploads tiny. Fallback path if R16F is unsupported.
- 3DSS replaces only the spectrum region, waterfall stays below (matches the 2D split), rather than a full-surface takeover.
- Amplitude anchored to the measured noise floor (not the raw Ref window), so the floor is always visible regardless of band.
Open questions — feedback wanted
- Default visual style: Yaesu-style stacked ridges vs. an optional R&S/Tektronix density/persistence colour mode (colour = hit-count over freq×amplitude) as an alternate later. Keep stacked as default?
- Pan history reprojection: the mesh rows don't yet store per-row centre/bandwidth, so panning smears the history horizontally until it scrolls off (the waterfall reprojects per row).
- Controls surface: currently 2 knobs (mode + floor depth) reusing the waterfall palette. Add more (span, Z-curve "shape", scroll speed SLOW1…FAST3, vertical stretch), or keep it minimal?
- History depth / cadence: 96 rows at FFT-arrival cadence. Add a decoupled steady ~30 fps scroll + a SPEED control?
- Palette: reuse the waterfall scheme (current) vs. add perceptual options (turbo/viridis) for the floor→peak ramp?
- Scope/UX: anything about the 2D/3D toggle, persistence, or per-panadapter behaviour that should change before this becomes a PR?
Known limitations / planned follow-ups
- Per-row frequency reprojection on pan (see Q2).
- Optional perceptual palette, phosphor-style persistence, and a scroll-SPEED control (researched, deferred).
- GPU-upload telemetry counters appear unpopulated in this build (unrelated pre-existing gap); fps was validated via GUI-thread metrics.
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat
Summary
This is an RFC requesting design feedback on a new "3-Dimensional Spectrum Stream" panadapter render mode — a Yaesu / Rohde-&-Schwarz-style perspective stacked-trace surface.
A new Display-pane option, Spectrum: 2D / 3D, switches the spectrum region between the classic FFT-trace and the spectrum stream surface. The waterfall, frequency scale, slice/VFO markers, spots, memories, and nav-hints all keep working underneath/over it.
Motivation
Stacked-trace 3D spectrum displays make weak signals "in the weeds" easier to spot than a flat 2D trace or a waterfall — ARRL's lab measured Yaesu's as 2–4 dB more sensitive than its own waterfall. Several users have asked for a comparable view. The goal is a fast, good-looking, cross-platform (macOS/Windows/Linux) implementation that reuses our existing Qt + QRhi infrastructure.
Status
Live-verified RX-only on a FLEX-8400M: dense/smooth surface, visible noise floor with a floor→peak colour gradient, dim receding trace lines, fluid pan (
panFps ≈ 24,uiLag ≈ 1 ms, 0 % FFT loss); 2D round-trips unchanged.Design (current)
Rendering — GPU height-map mesh (
AETHER_GPU_SPECTRUM, 3D-only):resources/shaders/dss_mesh.{vert,frag}(compiled to.qsbvia the existingqt_add_shaders). They output NDC directly (likespectrum.vert); occlusion is by back-to-front draw order, not a depth test — we don't assume a depth-stencil attachment exists on the QRhiWidget render target on every backend.TriangleStripfill + alpha-blendedLineStripoutline.ThreeDssRendererclass is the shared smoothed-dBm ring store (peak-preserving downsample + 3-tap spatial + temporal IIR) feeding both the mesh upload and the CPU fallback.Amplitude / noise floor: ridge height =
clamp((dbm − floorDbm) / rangeDb)with a non-linear Z-curve.floorDbmis anchored to the measured noise floor (m_measuredNoiseFloorDbm, the same estimator the 2D auto-floor uses) minus a user "floor depth", so the floor carpet lifts into view.rangeDbis Ref-derived (clamped 45–120 dB). Colour is independent of height: a 256×1 palette LUT gives a vertical floor→peak gradient.Palette: follows the existing waterfall colour-scheme combo (no new palette UI).
Trace outline: a dim, depth-faded per-row hairline (Yaesu style), distinct from the solid fill.
Controls (Display pane): the
Spectrum: 2D / 3Dcombo + a3D Floordepth slider (0–24 dB). Both persisted per-panadapter. (Bridge-addressable:spectrumRenderModeCombo,dssFloorDepthSlider.)Fallback: if the mesh pipeline can't be created (e.g. R16F unsupported), it falls back to a capped cached-QImage quad rendered by
DssRendereron the CPU. 2D is byte-identical to today. Currently, it's pretty poor performance and I wouldn't recommend shipping it.Key design decisions (seeking validation)
Open questions — feedback wanted
Known limitations / planned follow-ups
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat