From 190fdc9f957db9cba4a4a3b3d18a34e6a5e3a725 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sun, 19 Jul 2026 09:11:25 +0200 Subject: [PATCH 1/2] fix(recording): stop the WGC helper from hanging on stop with no audio/webcam/cursor The final "wait for stop" in wgc-capture's main() shared the same mutex/cv as the video-writer thread's per-frame encode loop. That loop holds the mutex for the full duration of each frame's GPU copy + software encode call, so if the encode pipeline stalls (slow software encoder, flaky GPU driver), the main thread can't even acquire the lock to check whether `stop` was requested -- turning a stall into a silent, unbounded hang with no [stop-timing] diagnostic output at all (issue #115). This reproduces specifically when audio/mic/webcam/cursor are all disabled because in that configuration nothing else is left holding/releasing that mutex on a faster cadence to give the stop check a chance to run. Give CaptureControl a dedicated stopMutex/stopCv used only to signal "stop was requested", decoupled from the frame-processing mutex. The stdin reader thread now notifies both cvs on stop, and the final stop-wait uses only the dedicated pair, so stop detection never depends on the frame pump releasing its lock. Fixes #115 --- electron/native/wgc-capture/src/main.cpp | 27 ++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index 30a3069d6..ac586f3e9 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -57,6 +57,19 @@ struct CaptureControl { std::atomic paused = false; std::mutex mutex; std::condition_variable cv; + // Dedicated mutex/cv pair used ONLY to signal "stop was requested" to the + // main thread. The frame-processing pipeline (writeVideoFrames) holds + // `mutex` for the full duration of each frame's GPU copy + encode call, + // which can stall for a long time on slow software encoders or flaky GPU + // drivers (e.g. hybrid-graphics laptops). If the final "wait for stop" + // below shared that same mutex, it would have to wait for the encode + // pipeline to release the lock before it could even check whether stop + // was requested — turning an encode stall into a silent, unbounded stop + // hang with no diagnostic output (see issue #115). Keeping stop signaling + // on its own uncontended mutex means the stop request is always observed + // promptly, regardless of what the frame pump is doing. + std::mutex stopMutex; + std::condition_variable stopCv; std::chrono::steady_clock::time_point pauseStartedAt; std::chrono::steady_clock::duration totalPausedDuration{}; @@ -359,6 +372,7 @@ void readCaptureCommands(CaptureControl& control, const std::function Date: Sun, 19 Jul 2026 09:20:48 +0200 Subject: [PATCH 2/2] fix(recording): bound the video-writer frame wait to avoid a residual hang Per CodeRabbit review on #121: the video-writer thread's frame-wait held an unbounded cv.wait() on the same predicate this PR already fixed for the stop-path. Switch it to wait_for(100ms) so the loop always re-checks stopRequested/encodeFailed even in the (currently unreachable, but defense-in-depth) case of a missed notification, instead of relying solely on notify_all always reaching an already-waiting thread. Verified: rebuilt wgc-capture.exe, re-ran the exact #115 repro (screen-only, all extras disabled) and the full-featured regression (system audio + cursor) - both stop promptly with a full [stop-timing] log and a valid non-zero MP4. --- electron/native/wgc-capture/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index ac586f3e9..7c05153a4 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -624,7 +624,7 @@ int main(int argc, char* argv[]) { while (!control.stopRequested && !encodeFailed) { { std::unique_lock lock(mutex); - control.cv.wait(lock, [&] { + control.cv.wait_for(lock, std::chrono::milliseconds(100), [&] { return control.stopRequested.load() || encodeFailed.load() || (!control.paused.load() && latestFrameTexture);