fix(wgc): stop holding the frame mutex across blocking WriteSample calls#119
Conversation
The video-writer thread held the shared frame-state mutex across IMFSinkWriter::WriteSample, which the main thread's stop-wait also locks to check control.stopRequested. With the software H.264 encoder fallback (preferSoftwareEncoder: true), WriteSample is slow enough that the writer thread could keep re-acquiring the lock every frame faster than the main thread could win it after a stop request, starving the stop-wait indefinitely — explaining why no [stop-timing] line ever printed. Split MFEncoder::writeFrame/writeBgraFrame into a capture step (GPU readback + IMFSample construction, still under the shared mutex since it touches latestFrameTexture) and a submit step (WriteSample, only under the encoder's own low-contention writerMutex_), and call submit outside the shared mutex in main.cpp's writeVideoFrames. Fixes #115. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughChangesMFEncoder pipeline
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant FrameWriterLoop
participant MFEncoder
participant IMFSinkWriter
FrameWriterLoop->>MFEncoder: Capture video/webcam samples while mutex is held
MFEncoder-->>FrameWriterLoop: Return IMFSample objects
FrameWriterLoop->>MFEncoder: Submit samples after releasing mutex
MFEncoder->>IMFSinkWriter: WriteSample
IMFSinkWriter-->>MFEncoder: Return write status
MFEncoder-->>FrameWriterLoop: Report success or encode failure
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
electron/native/wgc-capture/src/mf_encoder.cpp (1)
613-630: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the duplicated timestamp-advance block into a private helper.
Lines 613-630 here are byte-for-byte identical to
captureBgraSample(lines 676-693): samesampleDuration, samewriterMutex_-guardedsinkWriter_/finalized_check, and samefirstTimestampHns_/lastTimestampHns_advancement. In this platform-fragile recorder path, having the timestamp-monotonicity logic in two places risks silent divergence during future edits (e.g. only one copy gets a fix), which would corrupt output ordering.Both functions are
MFEncodermembers operating on the same instance state, so a single private helper cleanly captures the contract.As per coding guidelines, this is "security-sensitive recorder code" that "must be treated as" platform-fragile, so reducing duplicated correctness-critical logic is worthwhile.♻️ Suggested helper (add declaration to mf_encoder.h)
// mf_encoder.h (private section) bool beginVideoSampleTimestamp(int64_t timestampHns, int64_t& outSampleTime, int64_t& outSampleDuration);// mf_encoder.cpp bool MFEncoder::beginVideoSampleTimestamp(int64_t timestampHns, int64_t& outSampleTime, int64_t& outSampleDuration) { outSampleDuration = 10'000'000LL / fps_; std::scoped_lock writerLock(writerMutex_); if (!sinkWriter_ || finalized_) { return false; } if (firstTimestampHns_ < 0) { firstTimestampHns_ = timestampHns; } int64_t sampleTime = timestampHns - firstTimestampHns_; if (sampleTime <= lastTimestampHns_) { sampleTime = lastTimestampHns_ + outSampleDuration; } lastTimestampHns_ = sampleTime; outSampleTime = sampleTime; return true; }Then both capture methods reduce to a single
if (!beginVideoSampleTimestamp(...)) return false;before the buffer/copy work.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@electron/native/wgc-capture/src/mf_encoder.cpp` around lines 613 - 630, Extract the duplicated timestamp initialization and monotonicity logic from captureBgraSample and its neighboring capture method into a private MFEncoder helper named beginVideoSampleTimestamp. Have the helper compute duration, lock writerMutex_, validate sinkWriter_ and finalized_, update firstTimestampHns_ and lastTimestampHns_, and return the sample time and duration through output parameters; update both callers to use it and return false when it fails.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@electron/native/wgc-capture/src/mf_encoder.cpp`:
- Around line 613-630: Extract the duplicated timestamp initialization and
monotonicity logic from captureBgraSample and its neighboring capture method
into a private MFEncoder helper named beginVideoSampleTimestamp. Have the helper
compute duration, lock writerMutex_, validate sinkWriter_ and finalized_, update
firstTimestampHns_ and lastTimestampHns_, and return the sample time and
duration through output parameters; update both callers to use it and return
false when it fails.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 35496426-79d0-4e8a-9979-03d5bfdebc63
📒 Files selected for processing (3)
electron/native/wgc-capture/src/main.cppelectron/native/wgc-capture/src/mf_encoder.cppelectron/native/wgc-capture/src/mf_encoder.h
Follow-up to #119 (Fixes #115). CodeRabbit flagged, on the now-superseded #121, that the video-writer thread's per-frame cv.wait() has no timeout: if a notification were ever missed, the thread would block indefinitely, hanging videoWriterThread.join() and the whole stop sequence — exactly the failure mode this release is eliminating. #119 already fixed the actual root cause (the blocking WriteSample call no longer runs under this mutex, so the main thread's stop-wait is never starved), but the wait itself was still unbounded. Switch it to wait_for(100ms) so the loop always re-checks stopRequested/encodeFailed even in that circumstance, instead of relying solely on a notification reaching an already-waiting thread. Verified: rebuilt wgc-capture.exe on top of current main (includes #119), re-ran the #115 repro (screen-only, all extras disabled) and the full-featured regression (system audio + cursor) - both stop in under 110ms with a full [stop-timing] log and a valid MP4.
Follow-up to #119 (Fixes #115). CodeRabbit flagged, on the now-superseded #121, that the video-writer thread's per-frame cv.wait() has no timeout: if a notification were ever missed, the thread would block indefinitely, hanging videoWriterThread.join() and the whole stop sequence — exactly the failure mode this release is eliminating. #119 already fixed the actual root cause (the blocking WriteSample call no longer runs under this mutex, so the main thread's stop-wait is never starved), but the wait itself was still unbounded. Switch it to wait_for(100ms) so the loop always re-checks stopRequested/encodeFailed even in that circumstance, instead of relying solely on a notification reaching an already-waiting thread. Verified: rebuilt wgc-capture.exe on top of current main (includes #119), re-ran the #115 repro (screen-only, all extras disabled) and the full-featured regression (system audio + cursor) - both stop in under 110ms with a full [stop-timing] log and a valid MP4.
Summary
stopcommand whenpreferSoftwareEncoder: trueand system audio/mic/webcam/cursor are all disabled.writeVideoFrames(main.cpp) held the shared frame-state mutex acrossIMFSinkWriter::WriteSample, which the main thread's stop-wait also locks to checkcontrol.stopRequested. With the software H.264 fallback,WriteSampleis slow enough that the writer thread could keep re-acquiring the lock every frame faster than the main thread could win it after a stop request, starving the stop-wait — matching the report that no[stop-timing]line ever printed at all.MFEncoder::writeFrame/writeBgraFrameinto acaptureVideoSample/captureBgraSamplestep (GPU readback +IMFSampleconstruction, still under the shared mutex since it toucheslatestFrameTexture) and asubmitVideoSamplestep (the blockingWriteSamplecall, only under the encoder's own low-contentionwriterMutex_).main.cpp'swriteVideoFramesnow calls submit outside the shared mutex.Test plan
wgc-capture.exelocally via MSVC (VS 18 Insiders) + CMake/Ninja — compiles cleanly, no new warnings.captureSystemAudio/captureMic/captureCursor/webcamEnabled: false,preferSoftwareEncoder: true), sentstopover stdin after 4s: helper now exits ~97ms later with a valid non-zero MP4 (previously the process could hang indefinitely).🤖 Generated with Claude Code
Summary by CodeRabbit