Skip to content

Initialise dock later to bypass startup crash - #13

Merged
zackees merged 1 commit into
obs-rust:masterfrom
rdoupe:fix/hidden-dock-startup
Oct 26, 2026
Merged

zackees merged 1 commit into
obs-rust:masterfrom
rdoupe:fix/hidden-dock-startup

Conversation

@rdoupe

@rdoupe rdoupe commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Refs obs-rust/obs-studio#2524

Port of upstream obsproject/obs-browser#554 (open upstream, no review decision yet; the only review comment asks whether one delayed single-shot call would be cleaner than the retry timer, which the author wants to leave for a later rework). The upstream author is credited with a Co-Authored-By line.

What and why

On Windows, OBS crashes at startup when a browser dock was closed before OBS quit. The Twitch Chat dock is the usual case: it is the dock people close. Upstream now gives this as the cause of obsproject/obs-studio#14088.

  • QCefWidgetInternal::showEvent called Init() straight away, and Init() creates the browser.
  • When OBS restores the saved dock layout, Qt can show a dock and close it again at once. If the browser is created in between, CEF crashes (upstream's Windows stack trace is in its PR).
  • The fix creates the browser on the next event loop pass instead: QTimer::singleShot(0, this, &QCefWidgetInternal::Init). The 500 ms retry timer is unchanged.

Difference from upstream: upstream uses QTimer::singleShot(0, [this]() { Init(); }). That overload has no context object, so if the widget is destroyed before the event loop runs, Init() runs on a destroyed widget. Passing the widget as the context makes Qt drop the call in that case. The review comment on upstream suggests QMetaObject::invokeMethod(this, &QCefWidgetInternal::Init), but that would not defer anything: with the default connection type and the widget on the calling thread, the call is direct.

RED → GREEN

  • RED: none. No regression test is feasible on the platforms our tests run on:
    • Linux does not reproduce the crash. In obs-rust, OBSBasic::OBSInit restores the dock layout before the main window is shown, so a dock saved closed is never shown at startup and showEvent/Init() never run for it.
    • 10 launches of the app under gdb (obs-rust master with this repo at 63fd678, CEF 8078, private Xvfb and dbus, fresh HOME, loopback-only network namespace, no audio server, core dumps off): no crash. The config had two custom browser docks, "Chat" saved closed and "Other" saved open. Breakpoints on QCefWidgetInternal::showEvent and Init were never hit for either dock.
    • I also forced the upstream sequence by hand. From gdb, after startup, I called setVisible(true) and then setVisible(false) on the Chat dock in the same event loop pass, and showed the Other dock. 10 runs on 63fd678 and 10 with this fix: no crash in any run.
    • Upstream reproduced it on Windows 10 only, and we have no Windows CI that runs the app with a saved layout. A test that only checks that Init() is not called from showEvent would test the code's shape, not the crash.
  • GREEN: e4d577e. The same 10 startup launches with the fix: no crash, and the closed dock stays closed. In the forced show/hide runs, the Other dock creates its browser through the deferred Init() and renders its page (screenshot checked).
  • Real app: the runs above use the full obs-rust app on Linux. Not run on Windows or macOS.

Notes

  • clang-format 22.1.3 clean.
  • A quit with a browser dock that was closed during the session sometimes waits on the "You just closed a dockable window" message box. This happened in the forced runs both with and without the fix (4 of 10 runs on 63fd678, 6 of 10 with it), so it is not caused by this change.

QCefWidgetInternal::showEvent created the browser at once (Init()).
While OBS restores a saved dock layout at startup, Qt can show a
browser dock and close it again straight away; on Windows, creating
the browser in between crashes CEF. This is most often seen with a
Twitch Chat dock that was closed before OBS quit.

Create the browser on the next event loop pass instead, through a
zero-delay single-shot timer with the widget as its context: Qt drops
the call if the widget is destroyed before it runs. The upstream
change captured `this` in a lambda without a context object, which
would call Init() on a destroyed widget in that case. The 500 ms retry
timer is unchanged.

Ported from upstream obs-browser PR 554 (open upstream at the time of
porting).

Refs obs-rust/obs-studio#2524

Co-Authored-By: Matt Gajownik <matt@obsproject.com>

@zackees zackees left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed: the deferred Init() uses the widget as its context object, which is safer than upstream's lambda. Init() already guards against re-entry from the 500 ms timer. The reason there's no RED test (no Windows app CI with a saved layout) is reasonable and documented. Merging.

@zackees
zackees merged commit bd22ab8 into obs-rust:master Oct 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants