Repository navigation
Do not hand over to the Qt main thread once its loop has stopped - #11
Merged
Merged
Conversation
obs_browser_initialize() hands BrowserInit() to the Qt main thread with a blocking queued call when it is called from another thread (obs-rust#10). Once Qt's main loop has finished, nothing runs that call, so a first browser source created off the main thread while OBS quits would hang it (obs-rust/obs-studio#2391, from the review of obs-rust#10). The hand-over moves unchanged into RunOnQtMainThread() (browser-qt-main-thread.hpp), with WatchQtMainLoop() called from obs_module_load() (a no-op for now). qt-main-thread.cpp, run by qt_main_thread.py against Qt Core only: while the loop runs, a worker thread's call runs on the main thread (passes); after the loop has finished, the call must return within 2 s without running (fails: it waits forever). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RunOnQtMainThread() waited on a blocking queued call; once Qt's main loop has finished (OBS quits), nothing runs it, so a first browser source created off the main thread then hung OBS on quit (obs-rust/obs-studio#2391, from the review of obs-rust#10). WatchQtMainLoop(), called from obs_module_load() on the main thread, sets a flag on QCoreApplication::aboutToQuit. RunOnQtMainThread() no longer hands over when that flag or QCoreApplication::closingDown() is set: it queues the call without blocking and waits for it in 50 ms steps, and if the loop stops before the call has started, it cancels the call (a queued call that runs later does nothing) and returns false. The caller then leaves CEF uninitialized. tests/qt_main_thread.py: both cases pass, 20 of 20 runs. tests/first_source_off_main_thread.py on macOS: both scenarios render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zackees
approved these changes
Oct 25, 2026
zackees
left a comment
Member
There was a problem hiding this comment.
Coordinator review: approved, and thanks for picking up the note from #10. You're right that closingDown() alone isn't enough: it's false between the end of exec() and the application's destruction, so aboutToQuit is the right signal.
- The handshake is sound. State changes all happen under
handover->mutex: the caller cancels only while the call is stillQueued, and the main-thread lambda checks forCancelledbefore it runs. Sofuncruns at most once, and the caller never returns while it's running. A call that runs after cancellation is a no-op. WatchQtMainLoop()runs at module load on the main thread, while the app exists.obs_browser_initialize()treats a refused hand-over as a failed initialization, which the callers already handle.- The Qt Core-only test covers both cases (the loop running, and the loop finished).
Note: the 50 ms poll doesn't change the other case from #10. If the main thread were blocked waiting on the caller, this still waits (now polling rather than deadlocked). No current path does that.
6 of 7 tasks
MCPVOT
pushed a commit
to MCPVOT/obs-studio
that referenced
this pull request
Oct 25, 2026
…rust#2402) obs-rust/obs-browser#11: on macOS, do not hand BrowserInit() to the Qt main thread once its loop has stopped, so a first browser source created off the main thread while OBS quits no longer hangs it (obs-rust#2391). obs-rust/obs-browser#12: browser sources and panels use the CEF sandbox on macOS, with libcef_sandbox.dylib from the CEF framework; without it, the helpers run unsandboxed and OBS shows the fallback warning (obs-rust#1092). docs/features.md: the sandbox line covers Linux and macOS, the obs-rust#2387 line names the quit case. Fixes obs-rust#2391 Refs obs-rust#1092 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs obs-rust/obs-studio#2391
What and why
From the review of #10:
obs_browser_initialize()handsBrowserInit()to the Qt main thread with a blocking queued call when it is called from another thread (macOS). Once Qt's main loop has finished, nothing runs that call, so a first browser source created off the main thread while OBS quits would hang it on quit.QCoreApplication::closingDown()alone does not cover it: it is only true while the application object is destroyed, not between the end ofexec()and that. So:RunOnQtMainThread()(browser-qt-main-thread.hpp, no CEF).WatchQtMainLoop(), called fromobs_module_load()on the main thread, sets a flag onQCoreApplication::aboutToQuit.RunOnQtMainThread()does not hand over when that flag orclosingDown()is set. It queues the call without blocking and waits for it in 50 ms steps; if the loop stops before the call has started, it cancels it (a queued call that runs later does nothing) and returnsfalse.obs_browser_initialize()then leaves CEF uninitialized, as for a failed initialization.On the main thread and without a Qt application it still calls
BrowserInit()directly. Linux and Windows (their own CEF thread) are unchanged.RED → GREEN
tests/qt_main_thread.pycompilestests/qt-main-thread.cppagainst Qt Core only (no CEF, no display;--qt-lib-diron macOS,pkg-config Qt6Coreon Linux):643f00f(the hand-over moved unchanged into the helper): while the loop runs, a worker thread's call runs on the main thread (passes); after the loop has finished, the call is still waiting after 2 s (fails).c01896a: both pass, 20 of 20 runs.tests/first_source_off_main_thread.pyon macOS 26.7.1 with this branch:websocket-firstandcollectionboth render.AI note: Claude Code (Claude Opus 5.5) wrote the change, the tests and this text under the submitter's direction, and ran the tests above on the submitter's machine.