From e3fd29e4d7dc5723d8d0da7d19e2698daec74456 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 10 Sep 2026 21:00:57 -0500 Subject: [PATCH] Fix a livelock that can occur between SDL, libdecor-gtk, and Qt The Qt UI continuously redrawing prevents libdecor-gtk's glib dispatch loop from ever returning since it constantly posts new messages to draw again. The result is that Moonlight randomly hangs (with the spinner still animating) at the "Starting app..." phase of connection establishment and never proceeds until the livelock is broken by loss of focus, hiding the window, or similar. For posterity, the hanging stack: QtWaylandClient::QWaylandWindow::waitForFrameSync (this=this@entry=..., timeout=timeout@entry=...) at qwaylandwindow.cpp:891 QtWaylandClient::QWaylandGLContext::swapBuffers (this=..., surface=...) at qwaylandglcontext.cpp:494 QRhiGles2::endFrame (this=..., swapChain=..., flags=...) at qrhigles2.cpp:2390 QRhi::endFrame (this=..., swapChain=..., flags=...) at qrhi.cpp:11600 QSGGuiThreadRenderLoop::renderWindow (this=..., window=...) at qsgrenderloop.cpp:714 QQuickWindow::event (this=..., event=...) at qquickwindow.cpp:1663 QCoreApplication::notifyInternal2 (receiver=..., event=...) at qcoreapplication.cpp:1126 QCoreApplication::sendEvent (receiver=..., event=...) at qcoreapplication.cpp:1571 QPlatformWindow::deliverUpdateRequest (this=...) at qplatformwindow.cpp:831 QObject::event (this=..., e=...) at qobject.cpp:1480 QCoreApplication::notifyInternal2 (receiver=..., event=...) at qcoreapplication.cpp:1126 QCoreApplication::sendEvent (receiver=..., event=...) at qcoreapplication.cpp:1571 QCoreApplicationPrivate::sendPostedEvents (receiver=..., event_type=..., data=...) at qcoreapplication.cpp:1926 postEventSourceDispatch (s=...) at qeventdispatcher_glib.cpp:248 g_main_dispatch (context=...) at gmain.c:3591 g_main_context_dispatch_unlocked (context=...) at gmain.c:4451 g_main_context_iterate_unlocked (context=context@entry=..., block=block@entry=..., dispatch=dispatch@entry=..., self=...) at gmain.c:4516 g_main_context_iteration (context=..., context@entry=..., may_block=may_block@entry=...) at gmain.c:4582 libdecor_plugin_gtk_dispatch (plugin=..., timeout=...) at libdecor-gtk.c:509 Wayland_PumpEvents (_this=...) at SDL_waylandevents.c:610 Wayland_GLES_LoadLibrary (_this=..., path=...) at SDL_waylandopengles.c:51 SDL_GL_LoadLibrary_REAL (path=...) at SDL_video.c:4735 SDL_RecreateWindow (window=..., flags=...) at SDL_video.c:2796 GLES2_CreateRenderer (renderer=..., window=..., create_props=...) at SDL_render_gles2.c:2329 SDL_CreateRendererWithProperties_REAL (props=props@entry=...) at SDL_render.c:1126 SDL_CreateRenderer_REAL (window=..., name=...) at SDL_render.c:1287 SDL_CreateRenderer_REAL (window=..., idx=..., flags=...) at sdl2_compat.c:5986 EGLRenderer::initialize (this=..., params=...) at eglvid.cpp:445 FFmpegVideoDecoder::initializeRendererInternal (this=..., renderer=..., params=...) at ffmpeg.cpp:323 FFmpegVideoDecoder::createFrontendRenderer (this=..., params=..., useAlternateFrontend=...) at ffmpeg.cpp:426 FFmpegVideoDecoder::completeInitialization (this=..., decoder=..., requiredFormat=..., params=..., testMode=..., useAlternateFrontend=...) at ffmpeg.cpp:486 FFmpegVideoDecoder::tryInitializeRenderer(AVCodec const*, AVPixelFormat, _DECODER_PARAMETERS*, AVCodecHWConfig const*, IFFmpegRenderer::InitFailureReason*, std::function) (this=..., decoder=..., requiredFormat=..., params=..., hwConfig=..., failureReason=..., createRendererFunc=...) at ffmpeg.cpp:1200 FFmpegVideoDecoder::tryInitializeHwAccelDecoder (this=..., params=..., pass=..., terminallyFailedHardwareDecoders=...) at ffmpeg.cpp:1544 FFmpegVideoDecoder::initialize (this=..., params=...) at ffmpeg.cpp:1701 Session::chooseDecoder (vds=..., renderer=..., window=..., videoFormat=..., width=..., height=..., frameRate=..., enableVsync=..., enableFramePacing=..., testOnly=..., chosenDecoder=...) at session.cpp:322 Session::getDecoderAvailability (window=..., vds=..., videoFormat=..., width=..., height=..., frameRate=...) at session.cpp:498 Session::initialize (this=..., qtWindow=...) at session.cpp:755 --- app/gui/StreamSegue.qml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/gui/StreamSegue.qml b/app/gui/StreamSegue.qml index 377b5337..d7c0c5d1 100644 --- a/app/gui/StreamSegue.qml +++ b/app/gui/StreamSegue.qml @@ -126,22 +126,9 @@ Item { SystemProperties.waitForAsyncLoad() // Kick off the stream - spinnerTimer.start() streamLoader.active = true } - Timer { - id: spinnerTimer - - // Display the spinner appearance a bit to allow us to reach - // the code in Session.exec() that pumps the event loop. - // If we display it immediately, it will briefly hang in the - // middle of the animation on Windows, which looks very - // obviously broken. - interval: 100 - onTriggered: stageSpinner.visible = true - } - Timer { id: startSessionTimer onTriggered: { @@ -178,6 +165,11 @@ Item { return; } + // This spinner is shown only after session.initialize() has completed + // to prevent active animations from running during decoder probing, + // which causes re-entrant event loop livelocks with libdecor-gtk. + stageSpinner.visible = true + // Don't wait unless we have toasts to display startSessionTimer.interval = 0