[education/kstars] kstars/ekos/guide: Guide: fix GUI freeze on streaming guide + normal dither (self-join deadlock)
Jasem Mutlaq <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 038c22968ed3069246872e8323ff86aee5cba081 by Jasem Mutlaq, on behalf of Andreas R..
Committed on 18/08/2026 at 17:33.
Pushed by mutlaqja into branch 'master'.
Guide: fix GUI freeze on streaming guide + normal dither (self-join deadlock)
## Summary
Fixes a hard GUI freeze with the internal guider in **streaming** mode when the capture module
requests a **normal (multi-pulse) dither**. Ekos locks up the instant "Dithering…" is logged
(D-Bus unresponsive, needs kill). One-pulse dither and single-frame guiding are unaffected.
## Root cause — self-join on the guide frame's star-detection future
1. A stream frame is processed on the GUI thread: `processData → setCaptureComplete → guide →
… → FITSData::findStars(SEP)`. When the pool is busy, `waitForFinished()` runs the extraction
inline on the GUI thread and `StellarSolver::extract()` spins a nested QEventLoop.
2. That nested loop dispatches the imaging camera's queued newImage → `CameraState::checkDithering`
→ `Ekos::Capture::dither` → `Guide::dither`.
3. For streaming + non-one-pulse dither, `Guide::dither` called `InternalGuider::dither()`
synchronously → `findLocalStarPosition → FITSData::findStars` on the **same FITSData whose
extraction future is still running inline one level down the stack** → `waitForFinished` blocks
forever → deadlock.
## Fix
Streaming normal dither now uses the same deferred path as single-frame guiding: `Guide::dither()`
sets `GUIDE_DITHERING` and returns for all internal multi-pulse dithers; iterations are driven by
`setCaptureComplete()` as each frame arrives (top level, never re-entrant). `capture()` is issued
only in single-frame mode. Matches the frame-routing contract documented in `processData()`.
File: `kstars/ekos/guide/guide.cpp` (+20 / −9).
## Validation (on-sky, real rig: WARPDRIVE/OnStep + Xena 585M OAG, streaming, 2-axis)
- Unpatched current master: froze on the 2nd dither; self-join backtrace captured.
- Patched: 8+ dithers, 10 lights, zero freezes, GUI responsive throughout.
Note: it's a race (dither must land during a guide-frame SEP), so it doesn't fire every dither —
confirmed via the backtrace plus multiple clean post-patch dithers.
M +20 -9 kstars/ekos/guide/guide.cpp
https://invent.kde.org/education/kstars/-/commit/038c22968ed3069246872e8323ff86aee5cba081
diff --git a/kstars/ekos/guide/guide.cpp b/kstars/ekos/guide/guide.cpp
index af7ade11fb..f01827cd3b 100644
--- a/kstars/ekos/guide/guide.cpp
+++ b/kstars/ekos/guide/guide.cpp
@@ -1795,15 +1795,26 @@ bool Guide::dither()
ditherLabel->setText("Dither");
ditherLabel->setFont(QFont(font().family(), 10));
- // In streaming mode captureOneFrame() is a no-op — there is no discrete capture to
- // start. The capture()-then-wait-for-setCaptureComplete() path below therefore never
- // delivers a frame to kick off the first InternalGuider::dither() call and would stall.
- // Instead, call m_GuiderInstance->dither() directly so it can process the most recently
- // received stream frame immediately and drive subsequent iterations through
- // setCaptureComplete() as each new frame arrives.
- if (guiderType == GUIDE_INTERNAL && !Options::ditherWithOnePulse() && !m_StreamingGuide)
- {
- if (m_State != GUIDE_GUIDING)
+ // Internal guider, multi-pulse dither: drive the dither iterations through
+ // setCaptureComplete(), which calls InternalGuider::dither() once per delivered frame
+ // while in GUIDE_DITHERING (see the frame-routing note in processData()).
+ //
+ // Single-frame mode needs an explicit capture() to deliver that first frame. Streaming
+ // mode does NOT: frames arrive continuously, so setStatus(GUIDE_DITHERING) alone lets the
+ // next stream frame kick off the first iteration.
+ //
+ // Crucially, do NOT call InternalGuider::dither() synchronously here in streaming mode.
+ // This slot is invoked from the capture module's dither request, which can be dispatched
+ // inside the nested QEventLoop that StellarSolver::extract() spins while a stream frame is
+ // being processed (findStars runs inline on the GUI thread when the pool is busy). A
+ // synchronous dither() -> findLocalStarPosition() -> FITSData::findStars() would then wait
+ // on the star-detection QFuture of the very frame being processed one level down the same
+ // stack — a self-join deadlock that freezes the GUI (observed with normal dither; one-pulse
+ // dither is unaffected because it never runs findStars). Deferring keeps the dither's star
+ // detection at the top level of the next frame instead.
+ if (guiderType == GUIDE_INTERNAL && !Options::ditherWithOnePulse())
+ {
+ if (m_State != GUIDE_GUIDING && !m_StreamingGuide)
capture();
setStatus(GUIDE_DITHERING);