[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);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.