mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-20 15:27:17 +00:00
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<IFFmpegRenderer* ()>)
(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
235 lines
7.6 KiB
QML
235 lines
7.6 KiB
QML
import QtQuick 2.0
|
|
import QtQuick.Controls 2.2
|
|
import QtQuick.Window 2.2
|
|
|
|
import SdlGamepadKeyNavigation 1.0
|
|
import Session 1.0
|
|
import SystemProperties 1.0
|
|
|
|
Item {
|
|
property Session session
|
|
property string appName
|
|
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
|
|
qsTr("Starting %1...").arg(appName)
|
|
property bool isResume : false
|
|
property bool quitAfter : false
|
|
|
|
function stageStarting(stage)
|
|
{
|
|
// Update the spinner text
|
|
stageText = qsTr("Starting %1...").arg(stage)
|
|
}
|
|
|
|
function stageFailed(stage, errorCode, failingPorts)
|
|
{
|
|
// Display the error dialog after Session::exec() returns
|
|
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode)
|
|
|
|
if (failingPorts) {
|
|
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
|
|
}
|
|
}
|
|
|
|
function connectionStarted()
|
|
{
|
|
// Hide the UI contents so the user doesn't
|
|
// see them briefly when we pop off the StackView
|
|
stageSpinner.visible = false
|
|
stageLabel.visible = false
|
|
hintText.visible = false
|
|
|
|
// Hide the window now that streaming has begun
|
|
window.visible = false
|
|
}
|
|
|
|
function displayLaunchError(text)
|
|
{
|
|
// Display the error dialog after Session::exec() returns
|
|
streamSegueErrorDialog.text = text
|
|
console.error(text)
|
|
}
|
|
|
|
function quitStarting()
|
|
{
|
|
// Avoid the push transition animation
|
|
var component = Qt.createComponent("QuitSegue.qml")
|
|
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
|
|
|
|
// Show the Qt window again to show quit segue
|
|
window.visible = true
|
|
}
|
|
|
|
function sessionFinished(portTestResult)
|
|
{
|
|
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
|
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
|
}
|
|
|
|
// Re-enable GUI gamepad usage now
|
|
SdlGamepadKeyNavigation.enable()
|
|
|
|
// Pop the StreamSegue off the stack if this is a GUI-based app launch
|
|
if (!quitAfter) {
|
|
stackView.pop()
|
|
}
|
|
|
|
if (quitAfter && !streamSegueErrorDialog.text) {
|
|
// If this was a CLI launch without errors, exit now
|
|
Qt.quit()
|
|
}
|
|
else {
|
|
// Show the Qt window again after streaming
|
|
window.visible = true
|
|
|
|
// Display any launch errors. We do this after
|
|
// the Qt UI is visible again to prevent losing
|
|
// focus on the dialog which would impact gamepad
|
|
// users.
|
|
if (streamSegueErrorDialog.text) {
|
|
streamSegueErrorDialog.quitAfter = quitAfter
|
|
streamSegueErrorDialog.open()
|
|
}
|
|
}
|
|
}
|
|
|
|
function sessionReadyForDeletion()
|
|
{
|
|
// Garbage collect the Session object since it's pretty heavyweight
|
|
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
|
session = null
|
|
gc()
|
|
}
|
|
|
|
StackView.onDeactivating: {
|
|
// Show the toolbar again when popped off the stack
|
|
toolBar.visible = true
|
|
|
|
// Re-enable GUI gamepad usage now
|
|
SdlGamepadKeyNavigation.enable()
|
|
}
|
|
|
|
StackView.onActivated: {
|
|
// Hide the toolbar before we start loading
|
|
toolBar.visible = false
|
|
|
|
// Hook up our signals
|
|
session.stageStarting.connect(stageStarting)
|
|
session.stageFailed.connect(stageFailed)
|
|
session.connectionStarted.connect(connectionStarted)
|
|
session.displayLaunchError.connect(displayLaunchError)
|
|
session.quitStarting.connect(quitStarting)
|
|
session.sessionFinished.connect(sessionFinished)
|
|
session.readyForDeletion.connect(sessionReadyForDeletion)
|
|
|
|
// Ensure the SystemProperties async thread is finished,
|
|
// since it may currently be using the SDL video subsystem
|
|
SystemProperties.waitForAsyncLoad()
|
|
|
|
// Kick off the stream
|
|
streamLoader.active = true
|
|
}
|
|
|
|
Timer {
|
|
id: startSessionTimer
|
|
onTriggered: {
|
|
// Garbage collect QML stuff before we start streaming,
|
|
// since we'll probably be streaming for a while and we
|
|
// won't be able to GC during the stream.
|
|
gc()
|
|
|
|
// Run the streaming session to completion
|
|
session.start()
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: streamLoader
|
|
active: false
|
|
asynchronous: true
|
|
|
|
onLoaded: {
|
|
// Set the hint text. We do this here rather than
|
|
// in the hintText control itself to synchronize
|
|
// with Session.exec() which requires no concurrent
|
|
// gamepad usage.
|
|
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
|
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q"))
|
|
|
|
// Stop GUI gamepad usage now
|
|
SdlGamepadKeyNavigation.disable()
|
|
|
|
// Initialize the session and probe for host/client capabilities
|
|
if (!session.initialize(window)) {
|
|
sessionFinished(0);
|
|
sessionReadyForDeletion();
|
|
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
|
|
|
|
// Display the toasts together in a vertical centered arrangement
|
|
var yOffset = 0
|
|
for (var i = 0; i < session.launchWarnings.length; i++) {
|
|
var text = session.launchWarnings[i]
|
|
console.warn(text)
|
|
|
|
// Show the tooltip for 3 seconds
|
|
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
|
toast.timeout = 3000
|
|
toast.text = text
|
|
toast.y += yOffset
|
|
toast.visible = true
|
|
|
|
// Offset the next toast below the previous one
|
|
yOffset = toast.y + toast.padding + toast.height
|
|
|
|
// Allow an extra 500 ms for the tooltip's fade-out animation to finish
|
|
startSessionTimer.interval = toast.timeout + 500;
|
|
}
|
|
|
|
// Start the timer to wait for toasts (or start the session immediately)
|
|
startSessionTimer.start()
|
|
}
|
|
|
|
sourceComponent: Item {}
|
|
}
|
|
|
|
Row {
|
|
anchors.centerIn: parent
|
|
spacing: 5
|
|
|
|
BusyIndicator {
|
|
id: stageSpinner
|
|
running: visible
|
|
visible: false
|
|
}
|
|
|
|
Label {
|
|
id: stageLabel
|
|
height: stageSpinner.height
|
|
text: stageText
|
|
font.pointSize: 20
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|
|
|
|
Label {
|
|
id: hintText
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 50
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
font.pointSize: 18
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|