Skip to content

perf(spectrum): defer waterfall-history reprojection off the scroll path#3576

Closed
svabi79 wants to merge 1 commit into
aethersdr:mainfrom
svabi79:perf/defer-waterfall-history-reproject
Closed

perf(spectrum): defer waterfall-history reprojection off the scroll path#3576
svabi79 wants to merge 1 commit into
aethersdr:mainfrom
svabi79:perf/defer-waterfall-history-reproject

Conversation

@svabi79

@svabi79 svabi79 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #3575. Horizontal frequency scrolling was choppy on wide displays because reprojectWaterfall() reprojected the full waterfall history image (up to ~0.5 GB / 24k rows at ultrawide widths) on the CPU on every pan step — ~108 ms each (measured), vs ~3 ms for the visible waterfall. That history is only consumed by time-scrollback (rebuildWaterfallViewport), never by the live view, so the work was wasted during normal tuning.

This reprojects the visible waterfall immediately, but defers + coalesces the history reprojection: the net transform is remembered and applied once after scrolling settles (debounced), history appends pause while a reprojection is pending so frames are never mixed, and a pending reprojection is flushed on demand at the top of rebuildWaterfallViewport. The live view is visually unchanged.

Constitution principle honored

Principle XI — Fixes Are Demonstrated. Root cause and fix were both confirmed by instrumentation and the built-in aether.perf telemetry (before/after below), not by code-reading alone.

Test plan

  • Local build passes — Windows, Qt 6.8.3 msvc2022_64, VS 2022; no errors.
  • Measured before/after with QT_LOGGING_RULES=aether.perf.debug=true, ultrawide (5144×1440), continuous scroll up to 49 tune-cmds/s:
metric (continuous-scroll windows) before after
uiLagMaxMs 110–164 ms 27–38 ms
panAgeP95Ms / wfAgeP95Ms 120–155 ms 24–30 ms
renderP95Ms (GPU) 6–8 ms 6–7 ms (unchanged — GPU was never the bottleneck)
  • Instrumented split confirmed the cause: histMs avg 108 ms / step vs visMs avg 3.3 ms.
  • Behavior verified on a real radio — live view is visually identical; only the (off-screen) history-reproject scheduling changed.
  • Existing tests pass (CI) — pending.

Scope / follow-up

A small residual remains: one ~108 ms history reproject per scroll gesture (fires on stop / during slow pausey scrolling), visible as an occasional hitch. That is out of scope here and tracked in a follow-up issue — candidate fixes are moving the reprojection off the main thread or storing per-row frequency frames (no global reproject). This PR is the focused, high-leverage fix for the common continuous-scroll case.

Notes

Platform-independent change (reprojectWaterfall is plain Qt QImage) — benefits Windows/macOS/Linux. src/gui/SpectrumWidget.* is not a maintainer-only CODEOWNERS path, but flagging for review given it touches the render hot path.

Checklist

  • Commits are signed (SSH, verified)
  • No new flat-key AppSettings calls (N/A)
  • All meter UI uses MeterSmoother (N/A)
  • Documentation updated if user-visible behavior changed (N/A — no visible behavior change)
  • Security-sensitive changes reference a GHSA if applicable (N/A)

Horizontal frequency scrolling was choppy on wide displays because the
full waterfall *history* image was reprojected on the CPU on every pan
step. At ultrawide widths that image is ~0.5 GB (up to 24k rows), so the
drawImage cost was ~108 ms/step (measured) versus ~3 ms for the visible
waterfall — blowing the frame budget ~7x. The history is only consumed by
time-scrollback (rebuildWaterfallViewport), never by the live view, so the
work was wasted during normal tuning.

Reproject the visible waterfall immediately (cheap), but defer and coalesce
the history reprojection: remember the net transform and apply it once after
scrolling settles (debounced), pause history appends while pending so frames
are never mixed, and flush on demand before a viewport rebuild. The live view
is visually unchanged.

Measured (continuous scroll, up to 49 tune-cmds/s): uiLagMax 110-164 ms ->
27-38 ms; pan/wf frame-age p95 120-155 ms -> 24-30 ms (4-5x). A small residual
(one ~108 ms reproject per scroll gesture, on stop) remains; tracked separately.

Platform-independent (plain Qt QImage path) — benefits Windows/macOS/Linux.

Fixes aethersdr#3575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@svabi79

svabi79 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #3578. The per-row-frequency-frame approach in #3578 was measured strictly better — it removes the residual ~108 ms gesture-end reproject hitch this defer/coalesce PR still incurred (see #3577), deletes the debounce/append-pause/flush machinery instead of adding it, and is more correct (rows placed by their own capture frame). Since this never merged, #3578 lands the final solution directly. Closing in favor of #3578.

@svabi79 svabi79 closed this Jun 14, 2026
NF0T pushed a commit that referenced this pull request Jun 14, 2026
…n reproject) (#3578)

## Summary

Fixes #3575. Horizontal frequency scrolling was choppy on wide displays
because `reprojectWaterfall()` reprojected the full waterfall
**history** image (up to ~0.5 GB / 24k rows at ultrawide widths) on the
CPU on **every pan step** — ~108 ms each (measured) vs ~3 ms for the
visible waterfall, blowing the 16 ms frame budget ~7×. The GPU render
path was never the bottleneck (`renderP95` 6–8 ms throughout).

The history image is only consumed by time-scrollback
(`rebuildWaterfallViewport`), never by the live view. So instead of
keeping the whole image aligned to the current frequency frame on every
pan, **each history row now records the center/bandwidth it was captured
at** (parallel array to `m_wfHistoryTimestamps`). The visible waterfall
is still reprojected immediately (cheap); the history image is **never
globally reprojected**. `rebuildWaterfallViewport` remaps only the ~700
visible rows from their own frame to the current one, on
time-scrollback.

## Constitution principle honored

**Principle XI — Fixes Are Demonstrated.** Root cause and fix were both
confirmed by instrumentation and the built-in `aether.perf` telemetry
(before/after below), not by code-reading alone.

## Measured (ultrawide 5144×1440, `aether.perf`)

| metric | before | after |
|---|---|---|
| `uiLagMaxMs` — continuous scroll | 110–164 ms | **31–42 ms** |
| `uiLagMaxMs` — short-scroll / stop windows | (108 ms per step) |
**20–33 ms** (no hitch) |
| `panAgeP95` / `wfAgeP95` | 120–155 ms | **24–30 ms** |
| `renderP95` (GPU) | 6–8 ms | 6–7 ms (unchanged) |

Instrumented split confirmed the cause: `histMs` avg **108 ms**/step vs
`visMs` **3.3 ms**. Time-scrollback verified frequency-correct (a row
captured at center A renders at the correct position after tuning to
center B).

## Test plan

- [x] Local build passes — Windows, Qt 6.8.3 msvc2022_64, VS 2022; GPU
path enabled; no errors.
- [x] Before/after measured with
`QT_LOGGING_RULES=aether.perf.debug=true` (table above), two runs each.
- [x] Time-scrollback manually verified frequency-correct and
responsive.
- [x] Existing tests pass (CI) — pending.

## History / supersedes

This **supersedes #3576** (a defer-and-coalesce intermediate that left a
~108 ms gesture-end hitch — see #3577) and **resolves #3577** in the
same change: the per-row approach removes the need for the
debounce/append-pause/flush machinery entirely (net deletes that code)
and is strictly more correct — rows captured at different centers during
a fast scroll are each placed by their own frame. #3576 was never
merged, so this lands the final solution directly.

## Notes

Platform-independent (`reprojectWaterfall`/`rebuildWaterfallViewport`
are plain Qt `QImage`) — benefits Windows/macOS/Linux. Touches the
render hot path in `src/gui/SpectrumWidget.*` (not a maintainer-only
CODEOWNERS path); flagging for review regardless.

## Checklist

- [x] Commits are signed (SSH, verified)
- [x] No new flat-key `AppSettings` calls (N/A)
- [x] All meter UI uses `MeterSmoother` (N/A)
- [x] Documentation updated if user-visible behavior changed (N/A — live
view visually unchanged)
- [x] Security-sensitive changes reference a GHSA if applicable (N/A)

Co-authored-by: Claude Opus 4.8 (1M context) <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.

Choppy frequency scrolling on wide displays: full ~0.5 GB waterfall-history reprojected on every pan step

1 participant