From 26b161107a8a5c619691e6079e9a4acc2f15377c Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 29 Jul 2026 11:08:44 +0800 Subject: [PATCH] fix(xwayland): sync prelaunch stacking after X11 map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prelaunch wrapper may already be active when its managed XWayland surface is attached. Waylib's user event handler runs before wlroots handles MapNotify, and wlroots initially restacks the managed X11 window below. Raising it during the Splash handoff can therefore be overwritten, leaving Treeland's active window visually on top while X11 input targets an older window underneath. Track X11 MapNotify and UnmapNotify state in WXWaylandSurface, then emit a queued completion signal after wlroots has handled the map. Defer the active prelaunch wrapper's one-shot Treeland and native X11 stack synchronization until that point, preserve stale work across temporary deactivation, retry it when the wrapper is reactivated, cancel it on lifecycle changes, and exclude proxy and override-redirect surfaces. Mirror the _NET_WM_STATE_ABOVE/BELOW hints when synchronizing so windows stacked at the bottom by updateXWaylandStackingState() do not get raised above their native peers. Harden the tracking so only genuine server MapNotify/UnmapNotify events drive it (synthetic XSendEvent notifications are ignored), keep the native X11 stack in step when the ABOVE/BELOW hints change after the one-shot synchronization, and clear the pending sync before restacking so a re-entrant activation cannot repeat it. Log: 修复XWayland预启动窗口切换后点击穿透的问题 Influence: XWayland预启动Splash切换到真实窗口时的堆叠顺序 --- src/surface/surfacewrapper.cpp | 76 +++++++++++++++++++ src/surface/surfacewrapper.h | 3 + .../protocols/private/wxwaylandsurface_p.h | 2 + waylib/src/server/protocols/wxwayland.cpp | 44 ++++++++++- .../src/server/protocols/wxwaylandsurface.cpp | 37 +++++++++ .../src/server/protocols/wxwaylandsurface.h | 10 +++ 6 files changed, 168 insertions(+), 4 deletions(-) diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index c7d7d52616..af9321f87a 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -201,6 +201,7 @@ void SurfaceWrapper::invalidate() { Q_ASSERT_X(!m_wrapperAboutToRemove, Q_FUNC_INFO, "Can't call `invalidate` twice!"); m_wrapperAboutToRemove = true; + m_pendingPrelaunchXWaylandStackSync = false; Q_EMIT aboutToBeInvalidated(); if (!m_skipDockPreView) @@ -538,6 +539,10 @@ void SurfaceWrapper::setup() &WXWaylandSurface::belowChanged, this, &SurfaceWrapper::updateXWaylandStackingState); + connect(xwaylandSurface, + &WXWaylandSurface::x11MapCompleted, + this, + &SurfaceWrapper::syncPrelaunchXWaylandStacking); updateX11SkipFlags(); updateXWaylandStackingState(); } @@ -607,6 +612,12 @@ void SurfaceWrapper::setActivate(bool activate) Q_ASSERT(!activate || hasActiveCapability()); m_isActivated = activate; + // Retry a deferred prelaunch X11 stack sync on activation (not only from the + // splash handoff): a map that happened while deactivated is repaired here, + // mirroring the raising-activation policy of Helper::setActivatedSurface. + if (activate) + syncPrelaunchXWaylandStacking(); + if (m_attention && m_isActivated) setAttention(false); @@ -806,6 +817,8 @@ void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, m_decoration->stackBefore(m_surfaceItem); } + requestPrelaunchXWaylandStackSync(); + m_surfaceItem->setVisible(true); if (m_type == Type::XWayland && (m_surfaceState == State::Maximized || m_surfaceState == State::Fullscreen)) { @@ -834,6 +847,60 @@ void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, updateHasActiveCapability(ActiveControlState::MappedOrSplash, surface() && surface()->mapped()); } +void SurfaceWrapper::requestPrelaunchXWaylandStackSync() +{ + if (m_isProxy || m_type != Type::XWayland) + return; + + auto *xwaylandSurface = qobject_cast(m_shellSurface); + if (!xwaylandSurface || xwaylandSurface->isBypassManager()) + return; + + m_pendingPrelaunchXWaylandStackSync = true; + if (!xwaylandSurface->isX11Mapped()) { + qCDebug(lcTlSurface) + << "Deferring prelaunch XWayland stacking until X11 map completes for" << appId(); + return; + } + + syncPrelaunchXWaylandStacking(); +} + +void SurfaceWrapper::syncPrelaunchXWaylandStacking() +{ + if (!m_pendingPrelaunchXWaylandStackSync) + return; + + auto *xwaylandSurface = qobject_cast(m_shellSurface); + if (m_wrapperAboutToRemove || m_isProxy || m_type != Type::XWayland || !xwaylandSurface + || xwaylandSurface->isBypassManager()) { + m_pendingPrelaunchXWaylandStackSync = false; + return; + } + + if (!m_isActivated || !xwaylandSurface->isX11Mapped()) + return; + + // Clear before restacking so a re-entrant activation during the child + // reorder cannot repeat the restack. + m_pendingPrelaunchXWaylandStackSync = false; + + // wlroots initially places a managed XWayland window at the bottom of the native X11 + // stack. A prelaunch wrapper is already activated, so the normal activation path cannot + // observe a wrapper change and raise the newly attached X11 window. Synchronize both + // stacks after wlroots has finished handling XCB_MAP_NOTIFY, mirroring the stacking + // direction updateXWaylandStackingState() gives to _NET_WM_STATE_ABOVE/BELOW windows. + if (!xwaylandSurface->isAbove() && xwaylandSurface->isBelow()) { + stackToFirst(); + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_BELOW); + } else { + stackToLast(); + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_ABOVE); + } + qCDebug(lcTlSurface) + << "Synchronized active prelaunch XWayland stacking after X11 map for" << appId(); +} + WSurface *SurfaceWrapper::surface() const { if (!m_shellSurface) @@ -2719,10 +2786,19 @@ void SurfaceWrapper::updateXWaylandStackingState() const bool below = !above && xwaylandSurface->isBelow(); setAlwaysOnBottom(below); setAlwaysOnTop(above); + + // Guards are mandatory: wlroots only puts a surface into its stack list + // after the map handler, and restack() asserts the window is not + // override-redirect. + const bool canRestack = xwaylandSurface->isX11Mapped() && !xwaylandSurface->isBypassManager(); if (above) { stackToLast(); + if (canRestack) + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_ABOVE); } else if (below) { stackToFirst(); + if (canRestack) + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_BELOW); } } diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index 5cd3ee2e47..551e2643bc 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -487,6 +487,8 @@ public Q_SLOTS: void updateActivateCapability(); void updateFocusCapability(); void completeSplashTransition(const QSizeF &targetImplicitSize, bool hideDecoration = false); + void requestPrelaunchXWaylandStackSync(); + void syncPrelaunchXWaylandStacking(); // wayland set by treeland-dde-shell, x11 set by bypassManager/windowTypes void setSkipDockPreView(bool skip); @@ -590,6 +592,7 @@ public Q_SLOTS: bool m_socketEnabled{ false }; bool m_windowAnimationEnabled{ true }; + bool m_pendingPrelaunchXWaylandStackSync{ false }; const QString m_appId; }; diff --git a/waylib/src/server/protocols/private/wxwaylandsurface_p.h b/waylib/src/server/protocols/private/wxwaylandsurface_p.h index b49b8508ae..b48d88dd7b 100644 --- a/waylib/src/server/protocols/private/wxwaylandsurface_p.h +++ b/waylib/src/server/protocols/private/wxwaylandsurface_p.h @@ -68,6 +68,8 @@ class Q_DECL_HIDDEN WXWaylandSurfacePrivate : public WToplevelSurfacePrivate uint minimized:1; uint fullscreen:1; uint activated:1; + bool x11Mapped = false; + quint64 x11MapGeneration = 0; private: // XWayland owns this handle and destroys it after notifying the diff --git a/waylib/src/server/protocols/wxwayland.cpp b/waylib/src/server/protocols/wxwayland.cpp index bdf6afde35..123aa7aadb 100644 --- a/waylib/src/server/protocols/wxwayland.cpp +++ b/waylib/src/server/protocols/wxwayland.cpp @@ -67,6 +67,7 @@ class Q_DECL_HIDDEN WXWaylandPrivate : public WWaylandResourcePrivate void xcbPollReplies(); void xcbAsyncTimeoutForWindow(xcb_window_t windowId); + void setX11Mapped(xcb_window_t windowId, bool mapped); W_DECLARE_PUBLIC(WXWayland) @@ -89,10 +90,6 @@ bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *ev return false; const uint8_t response_type = event->response_type & ~0x80; - if (response_type != XCB_PROPERTY_NOTIFY) - return false; - - auto *pe = reinterpret_cast(event); auto *self = WXWayland::fromHandle(xwayland); if (!self) @@ -100,6 +97,31 @@ bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *ev auto *d = self->d_func(); + // XSendEvent can forge MAP_NOTIFY/UNMAP_NOTIFY to the root window + // (SubstructureNotifyMask). Only genuine server notifications may drive + // the X11 map state machine; wlroots still processes the event below. + const bool synthetic = (event->response_type & 0x80) != 0; + + // This handler runs before wlroots handles the event; the queued completion + // in WXWaylandSurface::setX11Mapped therefore lands after wlroots restacked + // the newly mapped window below. Do not make that signal synchronous. + if (!synthetic && response_type == XCB_MAP_NOTIFY) { + auto *me = reinterpret_cast(event); + d->setX11Mapped(me->window, true); + return false; + } + + if (!synthetic && response_type == XCB_UNMAP_NOTIFY) { + auto *ue = reinterpret_cast(event); + d->setX11Mapped(ue->window, false); + return false; + } + + if (response_type != XCB_PROPERTY_NOTIFY) + return false; + + auto *pe = reinterpret_cast(event); + // Trigger async property reading infrastructure if this window is being tracked. if (!d->asyncProps.isEmpty()) { d->xcbPollReplies(); @@ -139,6 +161,20 @@ bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *ev return false; } +void WXWaylandPrivate::setX11Mapped(xcb_window_t windowId, bool mapped) +{ + for (auto *surface : std::as_const(surfaceList)) { + QPointer guard(surface); + if (!guard) + continue; + if (guard->handle()->window_id != windowId) + continue; + + guard->setX11Mapped(mapped); + break; + } +} + void WXWaylandPrivate::init() { W_Q(WXWayland); diff --git a/waylib/src/server/protocols/wxwaylandsurface.cpp b/waylib/src/server/protocols/wxwaylandsurface.cpp index 877b99dbba..bf20da9446 100644 --- a/waylib/src/server/protocols/wxwaylandsurface.cpp +++ b/waylib/src/server/protocols/wxwaylandsurface.cpp @@ -58,6 +58,7 @@ void WXWaylandSurfacePrivate::init() }); q->listeners()->add(&m_handle->events.dissociate, this, [this, q] (void *) { Q_ASSERT(surface); + q->setX11Mapped(false); Q_EMIT q->aboutToDissociate(); delete surface; surface = nullptr; @@ -519,6 +520,12 @@ bool WXWaylandSurface::isBelow() const return d->handle()->below; } +bool WXWaylandSurface::isX11Mapped() const +{ + W_DC(WXWaylandSurface); + return d->x11Mapped; +} + bool WXWaylandSurface::isModal() const { W_DC(WXWaylandSurface); @@ -662,4 +669,34 @@ void WXWaylandSurface::restack(WXWaylandSurface *sibling, StackMode mode) wlr_xwayland_surface_restack(handle(), nullptr, static_cast(mode)); } +void WXWaylandSurface::setX11Mapped(bool mapped) +{ + W_D(WXWaylandSurface); + + if (d->x11Mapped == mapped) + return; + + d->x11Mapped = mapped; + const quint64 generation = ++d->x11MapGeneration; + if (!mapped) + return; + + // wlroots invokes the user event handler before its own XCB_MAP_NOTIFY handler. + // Defer notification so consumers run after wlroots performs the initial X11 restack. + QPointer guard(this); + QMetaObject::invokeMethod( + this, + [guard, generation] { + if (!guard) + return; + + const auto *d = guard->d_func(); + if (!d->x11Mapped || d->x11MapGeneration != generation) + return; + + Q_EMIT guard->x11MapCompleted(); + }, + Qt::QueuedConnection); +} + WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wxwaylandsurface.h b/waylib/src/server/protocols/wxwaylandsurface.h index e0cd61215c..95a99ffe63 100644 --- a/waylib/src/server/protocols/wxwaylandsurface.h +++ b/waylib/src/server/protocols/wxwaylandsurface.h @@ -12,6 +12,7 @@ WAYLIB_SERVER_BEGIN_NAMESPACE class WXWayland; +class WXWaylandPrivate; class WSeat; class WXWaylandSurfacePrivate; class WAYLIB_SERVER_EXPORT WXWaylandSurface : public WToplevelSurface @@ -115,6 +116,7 @@ class WAYLIB_SERVER_EXPORT WXWaylandSurface : public WToplevelSurface bool isBypassManager() const; bool isAbove() const; bool isBelow() const; + bool isX11Mapped() const; bool isModal() const; WindowTypes windowTypes() const; DecorationsFlags decorationsFlags() const; @@ -136,6 +138,10 @@ public Q_SLOTS: void associated(); // Emitted before WXWaylandSurfacePrivate handles notify_dissociate cleanup. void aboutToDissociate(); + // Emitted asynchronously (queued) after wlroots has handled XCB_MAP_NOTIFY, + // and only while the X11 window is still mapped. Assumes the Wayland event + // loop runs on the single Qt main thread (see WServerPrivate). + void x11MapCompleted(); void parentXWaylandSurfaceChanged(); void childrenChanged(); @@ -150,6 +156,10 @@ public Q_SLOTS: void requestConfigure(QRect geometry, ConfigureFlags flags); void requestActivate(); + +private: + friend class WXWaylandPrivate; + void setX11Mapped(bool mapped); }; WAYLIB_SERVER_END_NAMESPACE