From 26d8603b523af973682b7e602f1158ae62f21c9b Mon Sep 17 00:00:00 2001 From: David Edmundson Date: Fri, 28 Jan 2022 16:24:32 +0000 Subject: Introduce path for surviving compositor restarts This patch introduces an optional mechanism for clients to survive a crash and reconnect seemingly seamlessly. In the event of a disconnect from the compositor socket we simply try to reconnect again and replay any data needed so that we maintain a consistent state to where we left off. From an application point-of-view any open popups will be dismissed and we we potentially get a new framecallback, but it will be almost entirely transparent. Users of custom QWaylandClientExtensions will be notified via the activeChanged signal and rebuild as though the compositor had withdrawn and re-announced the global. OpenGL contexts will be marked as invalid, and handled the same way as a GPU reset. On the next frame RHI will notice these are invalid and recreate them, only now against a new wl_display and new EGLDisplay. Users of low level EGL/native objects might be affected, but the alternative at this point is being closed anyway. The entire codepath is only activated via an environment variable. Change-Id: I6c4acc885540e14cead7640794df86dd974fef4f Reviewed-by: Qt CI Bot Reviewed-by: Eskil Abrahamsen Blomfeldt --- src/client/qwaylanddisplay.cpp | 161 ++++++++++++++++++--- src/client/qwaylanddisplay_p.h | 5 + src/client/qwaylandintegration.cpp | 11 ++ src/client/qwaylandintegration_p.h | 1 + src/client/qwaylandshmbackingstore.cpp | 13 +- src/client/qwaylandwindow.cpp | 20 +++ src/client/qwaylandwindow_p.h | 5 +- .../client/wayland-egl/qwaylandeglwindow.cpp | 6 + .../client/wayland-egl/qwaylandeglwindow_p.h | 1 + .../client/wayland-egl/qwaylandglcontext.cpp | 9 ++ .../client/wayland-egl/qwaylandglcontext_p.h | 1 + 11 files changed, 212 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/client/qwaylanddisplay.cpp b/src/client/qwaylanddisplay.cpp index 46c85a6a..5c3431a0 100644 --- a/src/client/qwaylanddisplay.cpp +++ b/src/client/qwaylanddisplay.cpp @@ -65,18 +65,6 @@ #include // for std::tie -static void checkWaylandError(struct wl_display *display) -{ - int ecode = wl_display_get_error(display); - if ((ecode == EPIPE || ecode == ECONNRESET)) { - // special case this to provide a nicer error - qWarning("The Wayland connection broke. Did the Wayland compositor die?"); - } else { - qWarning("The Wayland connection experienced a fatal error: %s", strerror(ecode)); - } - _exit(1); -} - QT_BEGIN_NAMESPACE namespace QtWaylandClient { @@ -114,9 +102,13 @@ public: * not only the one issued from event thread's waitForReading(), which means functions * called from dispatch_pending() can safely spin an event loop. */ + if (m_quitting) + return; + for (;;) { if (dispatchQueuePending() < 0) { - checkWaylandError(m_wldisplay); + Q_EMIT waylandError(); + m_quitting = true; return; } @@ -154,6 +146,7 @@ public: Q_SIGNALS: void needReadAndDispatch(); + void waylandError(); protected: void run() override @@ -328,11 +321,17 @@ QWaylandDisplay::QWaylandDisplay(QWaylandIntegration *waylandIntegration) qRegisterMetaType("uint32_t"); mDisplay = wl_display_connect(nullptr); - if (!mDisplay) { + if (mDisplay) { + setupConnection(); + } else { qErrnoWarning(errno, "Failed to create wl_display"); - return; } + mWaylandTryReconnect = qEnvironmentVariableIsSet("QT_WAYLAND_RECONNECT"); +} + +void QWaylandDisplay::setupConnection() +{ struct ::wl_registry *registry = wl_display_get_registry(mDisplay); init(registry); @@ -404,7 +403,110 @@ void QWaylandDisplay::ensureScreen() Q_ASSERT(!QGuiApplication::screens().empty()); } -// Called in main thread, either from queued signal or directly. +void QWaylandDisplay::reconnect() +{ + qCWarning(lcQpaWayland) << "Attempting wayland reconnect"; + m_eventThread->stop(); + m_frameEventQueueThread->stop(); + m_eventThread->wait(); + m_frameEventQueueThread->wait(); + + qDeleteAll(mWaitingScreens); + mWaitingScreens.clear(); + + // mCompositor + mShm.reset(); + mCursorThemes.clear(); + mCursor.reset(); + mDndSelectionHandler.reset(); + mWindowExtension.reset(); + mSubCompositor.reset(); + mTouchExtension.reset(); + mQtKeyExtension.reset(); + mWindowManagerIntegration.reset(); + mTabletManager.reset(); + mPointerGestures.reset(); +#if QT_CONFIG(wayland_client_primary_selection) + mPrimarySelectionManager.reset(); +#endif + mTextInputMethodManager.reset(); + mTextInputManagerv1.reset(); + mTextInputManagerv2.reset(); + mTextInputManagerv4.reset(); + mHardwareIntegration.reset(); + mXdgOutputManager.reset(); + mViewporter.reset(); + mFractionalScaleManager.reset(); + + mWaylandIntegration->reset(); + + qDeleteAll(std::exchange(mInputDevices, {})); + mLastInputDevice = nullptr; + + auto screens = mScreens; + mScreens.clear(); + + for (const RegistryGlobal &global : mGlobals) { + emit globalRemoved(global); + } + mGlobals.clear(); + + mLastInputSerial = 0; + mLastInputWindow.clear(); + mLastKeyboardFocus.clear(); + mActiveWindows.clear(); + + const auto windows = QGuiApplication::allWindows(); + for (auto window : windows) { + if (auto waylandWindow = dynamic_cast(window->handle())) + waylandWindow->closeChildPopups(); + } + // Remove windows that do not need to be recreated and now closed popups + QList recreateWindows; + for (auto window : std::as_const(windows)) { + auto waylandWindow = dynamic_cast((window)->handle()); + if (waylandWindow && waylandWindow->wlSurface()) { + waylandWindow->reset(); + recreateWindows.push_back(waylandWindow); + } + } + + if (mSyncCallback) { + wl_callback_destroy(mSyncCallback); + mSyncCallback = nullptr; + } + + mDisplay = wl_display_connect(nullptr); + if (!mDisplay) + _exit(1); + + setupConnection(); + initialize(); + + if (m_frameEventQueue) + wl_event_queue_destroy(m_frameEventQueue); + initEventThread(); + + emit reconnected(); + + auto needsRecreate = [](QPlatformWindow *window) { + return window && !static_cast(window)->wlSurface(); + }; + auto window = recreateWindows.begin(); + while (!recreateWindows.isEmpty()) { + if (!needsRecreate((*window)->QPlatformWindow::parent()) && !needsRecreate((*window)->transientParent())) { + (*window)->reinit(); + window = recreateWindows.erase(window); + } else { + ++window; + } + if (window == recreateWindows.end()) + window = recreateWindows.begin(); + } + + mWaylandIntegration->reconfigureInputContext(); +} + void QWaylandDisplay::flushRequests() { m_eventThread->readAndDispatchEvents(); @@ -419,6 +521,8 @@ void QWaylandDisplay::initEventThread() new EventThread(mDisplay, /* default queue */ nullptr, EventThread::EmitToDispatch)); connect(m_eventThread.get(), &EventThread::needReadAndDispatch, this, &QWaylandDisplay::flushRequests, Qt::QueuedConnection); + connect(m_eventThread.get(), &EventThread::waylandError, this, + &QWaylandDisplay::checkWaylandError, Qt::QueuedConnection); m_eventThread->start(); // wl_display_disconnect() free this. @@ -428,10 +532,31 @@ void QWaylandDisplay::initEventThread() m_frameEventQueueThread->start(); } +void QWaylandDisplay::checkWaylandError() +{ + int ecode = wl_display_get_error(mDisplay); + if ((ecode == EPIPE || ecode == ECONNRESET)) { + qWarning("The Wayland connection broke. Did the Wayland compositor die?"); + if (mWaylandTryReconnect) { + reconnect(); + return; + } + } else { + qWarning("The Wayland connection experienced a fatal error: %s", strerror(ecode)); + } + _exit(-1); +} + void QWaylandDisplay::blockingReadEvents() { - if (wl_display_dispatch(mDisplay) < 0) - checkWaylandError(mDisplay); + if (wl_display_dispatch(mDisplay) < 0) { + int ecode = wl_display_get_error(mDisplay); + if ((ecode == EPIPE || ecode == ECONNRESET)) + qWarning("The Wayland connection broke during blocking read event. Did the Wayland compositor die?"); + else + qWarning("The Wayland connection experienced a fatal error during blocking read event: %s", strerror(ecode)); + _exit(-1); + } } void QWaylandDisplay::checkTextInputProtocol() diff --git a/src/client/qwaylanddisplay_p.h b/src/client/qwaylanddisplay_p.h index 640f33b8..f5c1bcbb 100644 --- a/src/client/qwaylanddisplay_p.h +++ b/src/client/qwaylanddisplay_p.h @@ -200,10 +200,14 @@ public slots: void flushRequests(); signals: + void reconnected(); void globalAdded(const RegistryGlobal &global); void globalRemoved(const RegistryGlobal &global); private: + void checkWaylandError(); + void reconnect(); + void setupConnection(); void handleWaylandSync(); void requestWaylandSync(); @@ -283,6 +287,7 @@ private: QList mActiveWindows; struct wl_callback *mSyncCallback = nullptr; static const wl_callback_listener syncCallbackListener; + bool mWaylandTryReconnect = false; bool mClientSideInputContextRequested = [] () { const QString& requested = QPlatformInputContextFactory::requested(); diff --git a/src/client/qwaylandintegration.cpp b/src/client/qwaylandintegration.cpp index 13bacc9b..45a247f9 100644 --- a/src/client/qwaylandintegration.cpp +++ b/src/client/qwaylandintegration.cpp @@ -503,6 +503,17 @@ QWaylandShellIntegration *QWaylandIntegration::createShellIntegration(const QStr } } +void QWaylandIntegration::reset() +{ + mServerBufferIntegration.reset(); + mServerBufferIntegrationInitialized = false; + + mInputDeviceIntegration.reset(); + + mClientBufferIntegration.reset(); + mClientBufferIntegrationInitialized = false; +} + } QT_END_NAMESPACE diff --git a/src/client/qwaylandintegration_p.h b/src/client/qwaylandintegration_p.h index 6221e689..1ba7a633 100644 --- a/src/client/qwaylandintegration_p.h +++ b/src/client/qwaylandintegration_p.h @@ -103,6 +103,7 @@ protected: QScopedPointer mDisplay; protected: + void reset(); virtual QPlatformNativeInterface *createPlatformNativeInterface(); QScopedPointer mClientBufferIntegration; diff --git a/src/client/qwaylandshmbackingstore.cpp b/src/client/qwaylandshmbackingstore.cpp index b9bfc9c8..779ee45c 100644 --- a/src/client/qwaylandshmbackingstore.cpp +++ b/src/client/qwaylandshmbackingstore.cpp @@ -136,7 +136,18 @@ QWaylandShmBackingStore::QWaylandShmBackingStore(QWindow *window, QWaylandDispla : QPlatformBackingStore(window) , mDisplay(display) { - + QObject::connect(mDisplay, &QWaylandDisplay::reconnected, window, [this]() { + auto copy = mBuffers; + // clear available buffers so we create new ones + // actual deletion is deferred till after resize call so we can copy + // contents from the back buffer + mBuffers.clear(); + mFrontBuffer = nullptr; + // resize always resets mBackBuffer + if (mRequestedSize.isValid() && waylandWindow()) + resize(mRequestedSize); + qDeleteAll(copy); + }); } QWaylandShmBackingStore::~QWaylandShmBackingStore() diff --git a/src/client/qwaylandwindow.cpp b/src/client/qwaylandwindow.cpp index a77cce5a..90f4c609 100644 --- a/src/client/qwaylandwindow.cpp +++ b/src/client/qwaylandwindow.cpp @@ -291,11 +291,21 @@ void QWaylandWindow::reset() mFrameCallbackElapsedTimer.invalidate(); mWaitingForFrameCallback = false; } + if (mFrameCallbackCheckIntervalTimerId != -1) { + killTimer(mFrameCallbackCheckIntervalTimerId); + mFrameCallbackCheckIntervalTimerId = -1; + } + mFrameCallbackTimedOut = false; mWaitingToApplyConfigure = false; + mCanResize = true; + mResizeDirty = false; + mOpaqueArea = QRegion(); mMask = QRegion(); + mQueuedBuffer = nullptr; + mQueuedBufferDamage = QRegion(); mDisplay->handleWindowDestroyed(this); } @@ -1623,6 +1633,16 @@ void QWaylandWindow::closeChildPopups() { popup->reset(); } } + +void QWaylandWindow::reinit() +{ + if (window()->isVisible()) { + initWindow(); + if (hasPendingUpdateRequest()) + deliverUpdateRequest(); + } +} + } QT_END_NAMESPACE diff --git a/src/client/qwaylandwindow_p.h b/src/client/qwaylandwindow_p.h index 22b42a9a..d0640e7e 100644 --- a/src/client/qwaylandwindow_p.h +++ b/src/client/qwaylandwindow_p.h @@ -229,6 +229,9 @@ public: void removeChildPopup(QWaylandWindow* child); void closeChildPopups(); + virtual void reinit(); + void reset(); + public slots: void applyConfigure(); @@ -325,8 +328,6 @@ private: void initializeWlSurface(); bool shouldCreateShellSurface() const; bool shouldCreateSubSurface() const; - void reset(); - static void closePopups(QWaylandWindow *parent); QPlatformScreen *calculateScreenFromSurfaceEvents() const; void setOpaqueArea(const QRegion &opaqueArea); bool isOpaque() const; diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp index 8221d5d9..ca7d58f3 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp +++ b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp @@ -140,6 +140,12 @@ void QWaylandEglWindow::invalidateSurface() m_contentFBO = nullptr; } +void QWaylandEglWindow::reinit() +{ + QWaylandWindow::reinit(); + m_clientBufferIntegration = static_cast(mDisplay->clientBufferIntegration()); +} + EGLSurface QWaylandEglWindow::eglSurface() const { return m_eglSurface; diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h index 5b9aa987..cfcdd577 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h +++ b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h @@ -50,6 +50,7 @@ public: void bindContentFBO(); void invalidateSurface() override; + void reinit() override; private: QWaylandEglClientBufferIntegration *m_clientBufferIntegration = nullptr; diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp index 0b64db5c..050b3eba 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp +++ b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp @@ -195,6 +195,10 @@ QWaylandGLContext::QWaylandGLContext(EGLDisplay eglDisplay, QWaylandDisplay *dis const QSurfaceFormat &fmt, QPlatformOpenGLContext *share) : QEGLPlatformContext(fmt, share, eglDisplay), m_display(display) { + m_reconnectionWatcher = QObject::connect(m_display, &QWaylandDisplay::reconnected, [this]() { + invalidateContext(); + }); + switch (format().renderableType()) { case QSurfaceFormat::OpenVG: m_api = EGL_OPENVG_API; @@ -260,6 +264,7 @@ void QWaylandGLContext::destroyTemporaryOffscreenSurface(EGLSurface eglSurface) QWaylandGLContext::~QWaylandGLContext() { + QObject::disconnect(m_reconnectionWatcher); delete m_blitter; m_blitter = nullptr; if (m_decorationsContext != EGL_NO_CONTEXT) @@ -280,6 +285,10 @@ void QWaylandGLContext::endFrame() bool QWaylandGLContext::makeCurrent(QPlatformSurface *surface) { + if (!isValid()) { + return false; + } + // in QWaylandGLContext() we called eglBindAPI with the correct value. However, // eglBindAPI's documentation says: // "eglBindAPI defines the current rendering API for EGL in the thread it is called from" diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h index 00ef99b1..b985c667 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h +++ b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h @@ -62,6 +62,7 @@ private: wl_surface *m_wlSurface = nullptr; wl_egl_window *m_eglWindow = nullptr; QWaylandEglWindow *m_currentWindow = nullptr; + QMetaObject::Connection m_reconnectionWatcher; }; } -- cgit v1.2.1