diff --git a/app/gui/StreamSegue.qml b/app/gui/StreamSegue.qml index 9aefec5d..f95b7e3d 100644 --- a/app/gui/StreamSegue.qml +++ b/app/gui/StreamSegue.qml @@ -48,18 +48,6 @@ Item { console.error(text) } - function displayLaunchWarning(text) - { - // This toast appears for 3 seconds, just shorter than how long - // Session will wait for it to be displayed. This gives it time - // to transition to invisible before continuing. - var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '') - toast.text = text - toast.timeout = 3000 - toast.visible = true - console.warn(text) - } - function quitStarting() { // Avoid the push transition animation @@ -132,7 +120,6 @@ Item { session.stageFailed.connect(stageFailed) session.connectionStarted.connect(connectionStarted) session.displayLaunchError.connect(displayLaunchError) - session.displayLaunchWarning.connect(displayLaunchWarning) session.quitStarting.connect(quitStarting) session.sessionFinished.connect(sessionFinished) session.readyForDeletion.connect(sessionReadyForDeletion) @@ -154,6 +141,19 @@ Item { onTriggered: stageSpinner.visible = 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 @@ -170,13 +170,41 @@ Item { // Stop GUI gamepad usage now SdlGamepadKeyNavigation.disable() - // 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() + // Initialize the session and probe for host/client capabilities + if (!session.initialize(window)) { + sessionFinished(0); + sessionReadyForDeletion(); + return; + } - // Run the streaming session to completion - session.exec(window) + // 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 + var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '') + toast.text = text + toast.y += yOffset + toast.visible = true + + // Hide the tooltip when the connection starts or fails + session.connectionStarted.connect(toast.hide) + session.sessionFinished.connect(toast.hide) + + // Offset the next toast below the previous one + yOffset = toast.y + toast.padding + toast.height + + // Allow the user 3.5 seconds to read the tooltip(s) + startSessionTimer.interval = 3500; + } + + // Start the timer to wait for toasts (or start the session immediately) + startSessionTimer.start() } sourceComponent: Item {} diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 5f36ed71..e2efaf3e 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -52,9 +52,12 @@ #include #include #include -#include #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +#endif + #define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org" CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { @@ -595,8 +598,10 @@ Session::~Session() SDL_DestroyMutex(m_DecoderLock); } -bool Session::initialize() +bool Session::initialize(QQuickWindow* qtWindow) { + m_QtWindow = qtWindow; + #ifdef Q_OS_DARWIN if (qEnvironmentVariableIntValue("I_WANT_BUGGY_FULLSCREEN") == 0) { // If we have a notch and the user specified one of the two native display modes @@ -937,37 +942,16 @@ bool Session::initialize() return false; } - if (m_Preferences->configurationWarnings) { - // Display launch warnings in Qt only after destroying SDL's window. - // This avoids conflicts between the windows on display subsystems - // such as KMSDRM that only support a single window. - for (const auto &text : m_LaunchWarnings) { - // Emit the warning to the UI - emit displayLaunchWarning(text); - - // Wait a little bit so the user can actually read what we just said. - // This wait is a little longer than the actual toast timeout (3 seconds) - // to allow it to transition off the screen before continuing. - uint32_t start = SDL_GetTicks(); - while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) { - SDL_Delay(5); - - if (!m_ThreadedExec) { - // Pump the UI loop while we wait if we're on the main thread - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - QCoreApplication::sendPostedEvents(); - } - } - } - } - return true; } void Session::emitLaunchWarning(QString text) { - // Queue this launch warning to be displayed after validation - m_LaunchWarnings.append(text); + if (m_Preferences->configurationWarnings) { + // Queue this launch warning to be displayed after validation + m_LaunchWarnings.append(text); + emit launchWarningsChanged(); + } } bool Session::validateLaunch(SDL_Window* testWindow) @@ -1717,72 +1701,8 @@ void Session::setShouldExitAfterQuit() m_ShouldExitAfterQuit = true; } -class ExecThread : public QThread +void Session::start() { -public: - ExecThread(Session* session) : - QThread(nullptr), - m_Session(session) - { - setObjectName("Session Exec"); - } - - void run() override - { - m_Session->execInternal(); - } - - Session* m_Session; -}; - -void Session::exec(QWindow* qtWindow) -{ - m_QtWindow = qtWindow; - - // Use a separate thread for the streaming session on X11 or Wayland - // to ensure we don't stomp on Qt's GL context. This breaks when using - // the Qt EGLFS backend, so we will restrict this to X11 - m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland(); - - if (m_ThreadedExec) { - // Run the streaming session on a separate thread for Linux/BSD - ExecThread execThread(this); - execThread.start(); - - // Until the SDL streaming window is created, we should continue - // to update the Qt UI to allow warning messages to display and - // make sure that the Qt window can hide itself. - while (!execThread.wait(10) && m_Window == nullptr) { - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - QCoreApplication::sendPostedEvents(); - } - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - QCoreApplication::sendPostedEvents(); - - // SDL is in charge now. Wait until the streaming thread exits - // to further update the Qt window. - execThread.wait(); - } - else { - // Run the streaming session on the main thread for Windows and macOS - execInternal(); - } -} - -void Session::execInternal() -{ - // Complete initialization in this deferred context to avoid - // calling expensive functions in the constructor (during the - // process of loading the StreamSegue). - // - // NB: This initializes the SDL video subsystem, so it must be - // called on the main thread. - if (!initialize()) { - emit sessionFinished(0); - emit readyForDeletion(); - return; - } - // Wait for any old session to finish cleanup s_ActiveSessionSemaphore.acquire(); @@ -1793,27 +1713,15 @@ void Session::execInternal() // NB: m_InputHandler must be initialize before starting the connection. m_InputHandler = new SdlInputHandler(*m_Preferences, m_StreamConfig.width, m_StreamConfig.height); - AsyncConnectionStartThread asyncConnThread(this); - if (!m_ThreadedExec) { - // Kick off the async connection thread while we sit here and pump the event loop - asyncConnThread.start(); - while (!asyncConnThread.wait(10)) { - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - QCoreApplication::sendPostedEvents(); - } - - // Pump the event loop one last time to ensure we pick up any events from - // the thread that happened while it was in the final successful QThread::wait(). - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - QCoreApplication::sendPostedEvents(); - } - else { - // We're already in a separate thread so run the connection operations - // synchronously and don't pump the event loop. The main thread is already - // pumping the event loop for us. - asyncConnThread.run(); - } + // Kick off the async connection thread then return to the caller to pump the event loop + auto thread = new AsyncConnectionStartThread(this); + QObject::connect(thread, &QThread::finished, this, &Session::exec); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} +void Session::exec() +{ // If the connection failed, clean up and abort the connection. if (!m_AsyncConnectionSuccess) { delete m_InputHandler; @@ -2434,6 +2342,13 @@ DispatchDeferredCleanup: SDL_QuitSubSystem(SDL_INIT_VIDEO); + // Reset this thread's OpenGL state back to what Qt expects +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QQuickOpenGLUtils::resetOpenGLState(); +#else + m_QtWindow->resetOpenGLState(); +#endif + // Cleanup can take a while, so dispatch it to a worker thread. // When it is complete, it will release our s_ActiveSessionSemaphore // reference. diff --git a/app/streaming/session.h b/app/streaming/session.h index 06b23e2d..f9da641a 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include @@ -96,13 +96,14 @@ class Session : public QObject friend class SdlInputHandler; friend class DeferredSessionCleanupTask; friend class AsyncConnectionStartThread; - friend class ExecThread; public: explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr); virtual ~Session(); - Q_INVOKABLE void exec(QWindow* qtWindow); + Q_INVOKABLE bool initialize(QQuickWindow* qtWindow); + Q_INVOKABLE void start(); + Q_PROPERTY(QStringList launchWarnings MEMBER m_LaunchWarnings NOTIFY launchWarningsChanged); static void getDecoderInfo(SDL_Window* window, @@ -132,8 +133,6 @@ signals: void displayLaunchError(QString text); - void displayLaunchWarning(QString text); - void quitStarting(); void sessionFinished(int portTestResult); @@ -141,10 +140,10 @@ signals: // Emitted after sessionFinished() when the session is ready to be destroyed void readyForDeletion(); -private: - void execInternal(); + void launchWarningsChanged(); - bool initialize(); +private: + void exec(); bool startConnectionAsync(); @@ -256,13 +255,12 @@ private: bool m_AudioDisabled; bool m_AudioMuted; Uint32 m_FullScreenFlag; - QWindow* m_QtWindow; - bool m_ThreadedExec; + QQuickWindow* m_QtWindow; bool m_UnexpectedTermination; SdlInputHandler* m_InputHandler; int m_MouseEmulationRefCount; int m_FlushingWindowEventsRef; - QList m_LaunchWarnings; + QStringList m_LaunchWarnings; bool m_ShouldExitAfterQuit; bool m_AsyncConnectionSuccess;