[codex] Fix Kiwi waterfall pan and zoom stalls#3825
Conversation
2ae22c3 to
a5a2dde
Compare
a5a2dde to
10364a5
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses KiwiSDR-induced UI stalls (issue #3816) by moving KiwiSDR client work off the GUI thread, reducing UI-thread workload from Kiwi waterfall delivery, and stabilizing Kiwi FFT trace behavior during pan/zoom gestures to avoid floor “re-anchoring” and trace jumps.
Changes:
- Run
KiwiSdrClientinstances on a dedicatedQThread, routing control calls viainvokeMethod()and delivering signals back to the manager/UI via queued connections. - Coalesce Kiwi waterfall row emissions (min interval) and add gesture-settle signals so remote waterfall view updates aren’t reset on every drag/zoom step.
- Introduce
KiwiSdrTraceMath+ a focused regression test to keep FFT trace-floor behavior stable across partial/stale row coverage during gestures.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/kiwi_sdr_trace_math_test.cpp | Adds a regression test covering Kiwi trace-floor estimation/mapping/stabilization. |
| src/gui/KiwiSdrTraceMath.h | New shared trace-math helpers for mapping Kiwi rows to the FFT trace and stabilizing the trace floor. |
| src/gui/SpectrumWidget.h | Adds gesture/settle signals and new Kiwi trace-floor state to support stable pan/zoom + trace rendering. |
| src/gui/SpectrumWidget.cpp | Implements deferred/settled pan+range behavior, Kiwi trace stabilization, and safer stream-state save/restore to avoid COW copies. |
| src/gui/MainWindow.h | Adds Kiwi UI sync coalescing flags/state. |
| src/gui/MainWindow.cpp | Avoids routing pan-range requests to Flex when the pan is Kiwi-backed (local-only path). |
| src/gui/MainWindow_Wiring.cpp | Defers Kiwi waterfall view resets during gestures; updates view on settle signals. |
| src/gui/MainWindow_KiwiSdr.cpp | Coalesces Kiwi UI sync work and adjusts bandwidth limits for Kiwi full-bandwidth waterfall behavior. |
| src/core/KiwiSdrManager.h | Adds client-thread + invoke/destroy helpers and caches client state/flags for cross-thread operation. |
| src/core/KiwiSdrManager.cpp | Moves Kiwi client work to a dedicated thread, updates signal wiring for queued delivery, and adds safe teardown. |
| src/core/KiwiSdrClient.h | Extends telemetry signal to carry the value; adds pending-row coalescing state. |
| src/core/KiwiSdrClient.cpp | Implements waterfall-row coalescing and relaxes frame acceptance to avoid stalls on request churn. |
| CMakeLists.txt | Adds the new kiwi_sdr_trace_math_test executable and ctest entry. |
| qDebug() << "Pan range request:" << source | ||
| << "center" << centerMhz | ||
| << "bandwidth" << bandwidthMhz | ||
| << "local=kiwi"; | ||
| return; |
There was a problem hiding this comment.
Thanks for this, @rfoust — this is a careful, well-structured fix. I checked out the branch and reviewed the threading move, the row coalescing, the pan/zoom gesture handling, and the new trace-math. All 6 CI checks (build, check-macos, check-windows, CodeQL, a11y) are green, and the overall design holds up well.
What's good
- Threading move is done correctly. Clients are moved to a dedicated
QThread; all cross-thread calls go throughinvokeClient()(queued), and all client→manager signals areQt::QueuedConnectionwith aclient(id) != cstaleness guard. The getters (telemetry/state/waterfallAvailable) were correctly switched from direct cross-thread reads to cachedm_*maps, which is exactly what makes the move safe. The destructor teardown is also sound:destroyClient(id, /*blocking=*/true)usesBlockingQueuedConnection(guarded byc->thread() != currentThread()) to tear each client down on its own thread beforequit()/wait(), and the redundantQThread::finished → deleteLateris harmless. saveCurrentWaterfallStreamStatemove-semantics. Switching the large waterfallQImage/vectors tostd::moveto avoid COW-detach-and-copy on the GUI thread is a nice, real win.- Trace-math extraction is clean and properly tested.
KiwiSdrTraceMath.his pure/header-only, andkiwi_sdr_trace_math_test.cppdirectly covers the reported symptom — floor stays anchored when a partial/stale row arrives mid-gesture (stabilizeTraceFloorwithallowFloorAdapt=false), plus DC-edge mapping and gradual upward adaptation. - UI-sync coalescing (
scheduleKiwiSdrUiSyncflag batching) and gesture-gated waterfall view updates (settle signals instead of per-step resets) are the right shape for the stall fix.
One confirmed issue (Copilot's finding is valid)
src/gui/MainWindow.cpp:5844 — the unguarded qDebug() in applyPanRangeRequest():
if (!kiwiSdrProfileForPan(panId).isEmpty()) {
qDebug() << "Pan range request:" << source
<< "center" << centerMhz ...;
return;
}This sits on the early-return path taken for every Kiwi pan/zoom range request, and frequencyRangeChangeRequested fires repeatedly during zoom-drag gestures — so this logs at gesture frequency and does string formatting on the GUI thread, in precisely the hot path this PR is trying to keep responsive. Suggest removing it, or gating behind a qCDebug(<category>) so it's off unless the category is enabled.
Minor (non-blocking)
- On pan-drag release,
m_draggingPanis set false beforeapplyPanDragCenter(..., force=true)and the subsequentemit panDragSettled(...), so thecenterChangeRequestedhandler andpanDragSettledcan both driveupdateKiwiWaterfallViewonce. It's idempotent so no harm — just noting it's a double-commit on release.
Nothing here blocks; the one actionable item is the hot-path qDebug. Nice work on the threading discipline and the regression test.
🤖 aethersdr-agent · cost: $7.8410 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Reviewed the substantive pieces in depth — strong, expert-level work.
Threading (highest-risk): single worker QThread; clients created parentless + moveToThread'd with deleteLater-on-finished; all client→manager signals QueuedConnection WITH stale-guards (if (client(id) != c) return) against in-flight queued signals after a client is replaced; GUI→worker via invokeClient (queued); destroyClient's BlockingQueuedConnection correctly guarded (c->thread() != currentThread()) and safe since the worker never waits back on the GUI; shutdown does blocking-destroy each client then quit()+wait(3000) with a setParent(nullptr)+self-delete-on-finish fallback to avoid destroy-while-running; qRegisterMetaType<QVector> for the cross-thread args. The subtle bits most refactors miss are all handled.
Trace math (KiwiSdrTraceMath.h): isolated pure functions with std::isfinite guards throughout (stops a stale/partial bin re-anchoring the floor → the trace jump), area-weighted averaging for zoom-out, graceful fallback, 100-line regression test.
Pan/zoom: settle-timer debounce so a gesture doesn't fire repeated remote Kiwi window resets, and the local drag path owns the visual during an active drag (stale remote rows ignored).
Platform-agnostic, no TX surface, live-verified. The one field watch-point is heavy multi-Kiwi-profile churn, which the stale-guards + blocking-destroy are built for. Thanks @rfoust — this closes the KiwiSDR GUI-thread-lag problem.
Summary
Fixes #3816. Moves KiwiSDR receiver/network work off the GUI thread, coalesces Kiwi waterfall row delivery, and changes pan/zoom handling so AetherSDR keeps the local panadapter responsive while avoiding repeated remote Kiwi waterfall window resets during gestures. This also stabilizes the Kiwi FFT trace math so partial/stale waterfall rows during pan or zoom no longer re-anchor the FFT floor and make the trace jump upward.
Constitution principle honored
Principle XI — Fixes Are Demonstrated. The change is backed by a focused KiwiSDR trace-math regression test, local builds, focused Kiwi test runs, and live receiver testing against the reported pan/zoom behavior.
Test plan
cmake --build build --target AetherSDR kiwi_sdr_trace_math_test)ctest --test-dir build -R 'kiwi_sdr_trace_math_test|kiwi' --output-on-failure)git diff --checkpassespython3 tools/check_a11y.py)Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls — use nested-JSON-under-one-key (Principle V)MeterSmoother(AGENTS.md convention)