From b06c2a9bf2f5ffc012e14b23e822b82ee4179c74 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 2 Oct 2026 23:59:54 -0400 Subject: [PATCH] [Fix] (Remote): a surfaceless make-current's hidden pbuffer no longer latches the session offscreen. --- MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 27 +++++++++++- .../MG_Remote/Client/BackendObject_Remote.cpp | 19 +++++++- .../MG_Remote/Client/BackendObject_Remote.h | 8 ++++ MobileGL/MG_Remote/Server/ServerLoop.cpp | 19 ++++++-- MobileGL/MG_Remote/Server/ServerLoop.h | 5 ++- .../MG_Remote/Server/SurfaceControlFrame.h | 12 +++++- .../MG_Test/Wire/ServerOwnedSurfaceTest.cpp | 43 ++++++++++++++++++- 7 files changed, 122 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index c72fa137..8264c26e 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -235,6 +235,11 @@ namespace MobileGL::MG_Impl::EGLImpl { return *standIns; } + // eglCreatePbufferSurface for the stand-in, except that a split client creates it on the + // server as a stand-in, which takes no surface mode there (a server-owned window may follow). + EGLSurface CreateSurfacelessStandInPbuffer(EGLStateContext* state, EGLDisplay dpy, EGLConfig config, + const EGLint* attribs); + // The display's stand-in, made now if it has none (or the one it had did not survive). // EGL_NO_SURFACE when the backend could not make a pbuffer; the EGL error is then set. EGLSurface SurfacelessStandIn(EGLStateContext* state, EGLDisplay dpy) { @@ -251,7 +256,7 @@ namespace MobileGL::MG_Impl::EGLImpl { return EGL_NO_SURFACE; } const EGLint attribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; - const EGLSurface standIn = CreatePbufferSurface(dpy, config, attribs); + const EGLSurface standIn = CreateSurfacelessStandInPbuffer(state, dpy, config, attribs); if (standIn != EGL_NO_SURFACE) standIns[dpy] = standIn; return standIn; } @@ -428,6 +433,26 @@ namespace MobileGL::MG_Impl::EGLImpl { return false; } #endif + + EGLSurface CreateSurfacelessStandInPbuffer(EGLStateContext* state, EGLDisplay dpy, EGLConfig config, + const EGLint* attribs) { +#if MOBILEGL_BUILD_DISAGGREGATED + auto* remote = dynamic_cast(GetBackendObject(state)); + if (remote == nullptr) return CreatePbufferSurface(dpy, config, attribs); + if (!RecoverBeforeCreatingSurface(state)) return EGL_NO_SURFACE; + const EGLSurface surface = state->CreatePbufferSurface(dpy, config, attribs); + if (surface == EGL_NO_SURFACE) return EGL_NO_SURFACE; + if (!remote->CreateEGLSurfacelessStandIn(surface, GetAttribValue(attribs, EGL_WIDTH, 1), + GetAttribValue(attribs, EGL_HEIGHT, 1))) { + state->DestroySurface(dpy, surface); + state->SetError(EGL_BAD_ALLOC); + return EGL_NO_SURFACE; + } + return surface; +#else + return CreatePbufferSurface(dpy, config, attribs); +#endif + } } // namespace EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window, diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index 6289d23b..a9e89590 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -289,9 +289,21 @@ namespace MobileGL::MG_Remote::Client { } Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) { + return CreatePbufferOnServer(surface, width, height, /*standIn=*/false); + } + + Bool BackendObject_Remote::CreateEGLSurfacelessStandIn(EGLSurface surface, EGLint width, EGLint height) { + if (!CreatePbufferOnServer(surface, width, height, /*standIn=*/true)) return false; + const std::lock_guard lock(m_eglStateMutex); + m_standInSurfaces.insert(surface); + return true; + } + + Bool BackendObject_Remote::CreatePbufferOnServer(EGLSurface surface, EGLint width, EGLint height, Bool standIn) { WaitForApplyBeforeEglForwarder("CreateEGLPbufferSurface"); Server::SurfaceRefusalCode refusal = Server::SurfaceRefusalCode::None; - if (!Server::ServerCreateEGLPbufferSurface(surface, width, height, &refusal)) { + if (!Server::ServerCreateEGLPbufferSurface(surface, width, height, &refusal, + standIn ? Server::kPbufferFlagSurfacelessStandIn : 0u)) { if (refusal == Server::SurfaceRefusalCode::SurfaceModeMismatch) { // P12 (D4), named on THIS side too: the server logged its half. MGLOG_E("MG_Remote client: SurfaceModeMismatch - eglCreatePbufferSurface (%dx%d) in a session " @@ -468,6 +480,7 @@ namespace MobileGL::MG_Remote::Client { { const std::lock_guard lock(m_eglStateMutex); homed = m_homedSurfaces.erase(surface) != 0; + m_standInSurfaces.erase(surface); } if (homed && !ClientSession::SessionLatchedLost()) Server::ServerReleaseEGLSurface(surface); MG_Backend::BackendObject::ReleaseEGLSurface(surface); @@ -481,6 +494,7 @@ namespace MobileGL::MG_Remote::Client { { const std::lock_guard lock(m_eglStateMutex); m_homedSurfaces.clear(); + m_standInSurfaces.clear(); } MG_Backend::BackendObject::ReleaseEGLResources(); } @@ -516,7 +530,8 @@ namespace MobileGL::MG_Remote::Client { Bool recreated = false; const char* kind = "pbuffer"; if (state.Kind == SurfaceKind::Pbuffer) { - recreated = Server::ServerCreateEGLPbufferSurface(surface, state.Width, state.Height); + const Uint32 flags = m_standInSurfaces.count(surface) != 0 ? Server::kPbufferFlagSurfacelessStandIn : 0u; + recreated = Server::ServerCreateEGLPbufferSurface(surface, state.Width, state.Height, nullptr, flags); } else if (ClientSession::IsServerOwnedWindowSurface(surface)) { kind = "server-owned window"; const Server::ServerOwnedWindowReply reply = diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index 18eb7cf8..b06c9834 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -108,6 +108,11 @@ namespace MobileGL::MG_Remote::Client { // session are forgotten without a release (their server is gone). void OnSessionReplaced(); + // EGL_KHR_surfaceless_context's hidden pbuffer (EGLImpl's SurfacelessStandIn): an ordinary + // pbuffer on this side, created on the server with kPbufferFlagSurfacelessStandIn so it + // takes no surface mode there, and re-created the same way on a fresh session. + Bool CreateEGLSurfacelessStandIn(EGLSurface surface, EGLint width, EGLint height); + protected: Bool InitPbufferSurface(EGLint width, EGLint height) override; @@ -125,6 +130,7 @@ namespace MobileGL::MG_Remote::Client { // The server side of `surface` on the CURRENT session: re-created from its client // registration when the session that had it was replaced. True when it exists now. Bool EnsureSurfaceHomed(EGLSurface surface); + Bool CreatePbufferOnServer(EGLSurface surface, EGLint width, EGLint height, Bool standIn); void NoteHomed(EGLSurface surface) { const std::lock_guard lock(m_eglStateMutex); m_homedSurfaces.insert(surface); @@ -140,6 +146,8 @@ namespace MobileGL::MG_Remote::Client { // nor presented to nor attached on the new server, which never issued it. Guarded by the // base's m_eglStateMutex. std::unordered_set m_homedSurfaces; + // The surfaceless stand-ins among this object's pbuffers. Guarded by m_eglStateMutex. + std::unordered_set m_standInSurfaces; std::unordered_map m_sessionSharedImages; // id -> references taken // The ids a replaced session held: releasing one is a success with nothing sent. std::unordered_set m_lostSessionSharedImages; diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 1f194491..8a76f348 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -1513,12 +1513,16 @@ namespace MobileGL::MG_Remote::Server { static_cast(frame.width), static_cast(frame.height)); return MOBILEGL_OK; - case SurfaceControlOp::CreatePbufferSurface: + case SurfaceControlOp::CreatePbufferSurface: { + // A surfaceless stand-in (kPbufferFlagSurfacelessStandIn) is no rendering path: it is + // admitted whatever the mode and latches none, so a client that binds surfacelessly + // before it creates its server-owned window still gets that window. + const Bool standIn = (frame.contextFlags & kPbufferFlagSurfacelessStandIn) != 0; // P12 (D4): a pbuffer in a session that went on-screen is the other mode, refused by // name and not latched - the session and its window surface carry on. P14 S2: the mode // is the session's own - latched by its first surface, on its own loop - and never // another session's. - if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) { + if (!standIn && !SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) { frame.ok = false; frame.refusal = static_cast(SurfaceRefusalCode::SurfaceModeMismatch); MGLOG_E("MG_Remote server: SurfaceModeMismatch - CreatePbufferSurface (seq %llu, %dx%d) in a " @@ -1535,11 +1539,18 @@ namespace MobileGL::MG_Remote::Server { // make-current that follows is forwarded and deduped one layer down (ID-54). if (frame.ok) { ForgetCurrentTuple(); + if (standIn) { + MGLOG_I("MG_Remote server: surface=pbuffer %dx%d (surfaceless stand-in; the session's surface " + "mode stays %s)", + frame.width, frame.height, SessionSurfaceModeName(m_surfaceMode)); + return MOBILEGL_OK; + } if (m_surfaceMode == SessionSurfaceMode::None) m_surfaceMode = SessionSurfaceMode::Offscreen; // P12: THE ARM PROOF (ID-124), once per surface: which of the two paths ran. MGLOG_I("MG_Remote server: surface=pbuffer %dx%d", frame.width, frame.height); } return MOBILEGL_OK; + } case SurfaceControlOp::MakeCurrent: { // C7 / ID-54: the apply thread binds the native context ONCE per context lifetime // and holds it for life. ApplyMakeCurrent forwards a bind only for a tuple it does @@ -2133,12 +2144,14 @@ namespace MobileGL::MG_Remote::Server { return rc == MOBILEGL_OK && frame.ok; } - Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height, SurfaceRefusalCode* refusal) { + Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height, SurfaceRefusalCode* refusal, + Uint32 flags) { SurfaceControlFrame frame; frame.kind = SurfaceControlOp::CreatePbufferSurface; frame.surface = TokenFromHandle(surface); frame.width = width; frame.height = height; + frame.contextFlags = flags; const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); if (refusal != nullptr) *refusal = static_cast(frame.refusal); return rc == MOBILEGL_OK && frame.ok; diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h index c22ad5b6..c91c916b 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.h +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -702,9 +702,10 @@ namespace MobileGL::MG_Remote::Server { Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle); Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height); // P12: `refusal`, when given, receives the server's named refusal (SurfaceModeMismatch for a - // pbuffer in an on-screen session), None otherwise. + // pbuffer in an on-screen session), None otherwise. `flags`: kPbufferFlagSurfacelessStandIn + // for the hidden pbuffer of a surfaceless make-current, which takes no surface mode. Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height, - SurfaceRefusalCode* refusal = nullptr); + SurfaceRefusalCode* refusal = nullptr, Uint32 flags = 0); // P12 (on-screen server window), D1/D2. CreateWindowSurface on the SERVER's window: one frame, // WindowKind::ServerOwned with nativeToken 0 on the wire, no SetWindowHandle. `width`/`height` // is the size the client asked for (0/0 = the server window's own size); the reply carries the diff --git a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h index 60000feb..2c21514b 100644 --- a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h +++ b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h @@ -99,6 +99,15 @@ namespace MobileGL::MG_Remote::Server { // substitutes its own window there; no pointer crosses the wire (Rule G/H). inline constexpr Int kServerOwnedWindowBackend = 0x10000; + // CreatePbufferSurface's flags, in the frame's `contextFlags` word (the wire's SurfaceOp + // contextFlags, 0 on every op but CreateContext before this, so an older server reads none). + // SURFACELESS STAND-IN: the hidden 1x1 pbuffer a client binds the backend to for a surfaceless + // make-current (EGL_KHR_surfaceless_context). It is never presented or read back, so it is no + // rendering path: it neither latches the session's surface mode nor is refused by it (D4). + // Without it, a client that made a context current with no surface before it created its + // server-owned window latched its session offscreen, and the window was refused. + inline constexpr Uint32 kPbufferFlagSurfacelessStandIn = 1u; + // P12. Why the server declined a surface op, carried back in the reply half (`refusal`). The // values ARE the wire's SurfaceRefusal (protocol.fbs); SurfaceOpCodec pins the agreement with // static_asserts, the way it pins SurfaceControlOp against SurfaceOpKind. None of them latches @@ -133,7 +142,8 @@ namespace MobileGL::MG_Remote::Server { Int swapInterval = 0; // P14 S1. CreateContext's two values beyond `context` (which is the client context token // itself): the share group the context was derived into, and the reserved flags word the - // schema carries so a later attribute has a slot. 0 on every other op. + // schema carries so a later attribute has a slot. CreatePbufferSurface carries its own + // flags in the same word (kPbufferFlagSurfacelessStandIn). 0 on every other op. Uint64 shareGroupToken = 0; Uint32 contextFlags = 0; // The reply half: written by the dispatch on the apply thread, read by the poster after diff --git a/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp b/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp index b6821b97..86fed7cd 100644 --- a/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp @@ -18,7 +18,9 @@ // * without the knob a NULL window is the client's own EGL_BAD_NATIVE_WINDOW and the server never // hears of it; // * D4: a session whose first surface was a pbuffer (offscreen) is refused a ServerOwned surface as -// SurfaceModeMismatch - named by the server and by the client - and carries on. +// SurfaceModeMismatch - named by the server and by the client - and carries on; +// * a surfaceless make-current (EGL_KHR_surfaceless_context) commits the session to neither mode: +// the hidden pbuffer the backend is bound to is not the session's first surface. #include "P12ServerRig.h" @@ -35,6 +37,7 @@ namespace { ServerOwnedFirst, // the ServerOwned create, then a pbuffer NullWindowWithoutKnob, // the knob unset: a NULL window, then a pbuffer PbufferThenServerOwned, // a pbuffer first (offscreen), then the ServerOwned create + SurfacelessThenServerOwned, // a surfaceless make-current first, then the ServerOwned create }; struct EglPeerReport { @@ -95,7 +98,13 @@ namespace { r.pbufferCreated = pbuffer != EGL_NO_SURFACE ? 1 : 0; if (r.pbufferCreated) r.madeCurrent = EGL::MakeCurrent(dpy, pbuffer, pbuffer, context) == EGL_TRUE; }; - if (scenario == Scenario::PbufferThenServerOwned) { + if (scenario == Scenario::SurfacelessThenServerOwned) { + r.madeCurrent = EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context) == EGL_TRUE; + (void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + tryWindow(); + r.madeCurrentAfter = + EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context) == EGL_TRUE; + } else if (scenario == Scenario::PbufferThenServerOwned) { tryPbuffer(); tryWindow(); if (r.pbufferCreated) { @@ -243,3 +252,33 @@ TEST(ServerOwnedSurface, AnOffscreenSessionRefusesAServerOwnedSurfaceAsSurfaceMo EXPECT_NE(client.Own().find("SurfaceModeMismatch - eglCreateWindowSurface"), std::string::npos) << client.Own(); EXPECT_EQ(r.madeCurrentAfter, r.madeCurrent) << "the session did not keep its pbuffer after the refusal"; } + +// A SURFACELESS BIND IS NO SURFACE MODE. KWin and Qt make a context current with no surface when +// EGL_KHR_surfaceless_context is advertised, and the backend is then bound to a hidden 1x1 pbuffer. +// That pbuffer must not latch the session offscreen: a client with MOBILEGL_IPC_SURFACE=server +// creates its window after such a bind, and a refusal there is a black desktop. The window here is +// refused for the reason a displayless server has (NoServerDisplay), never SurfaceModeMismatch, and +// the surfaceless bind still works after it. Red with the stand-in created as an ordinary pbuffer: +// the server answers SurfaceModeMismatch. +TEST(ServerOwnedSurface, ASurfacelessBindDoesNotLatchTheSessionOffscreen) { + ServerProcess server; + ASSERT_TRUE(LaunchSupervisor("surfaceless", &server)) << server.Log(); + ClientLog client("surfaceless", server); + const EglPeerReport r = RunEglPeer(server, Scenario::SurfacelessThenServerOwned, /*surfaceKnob=*/true, client.base); + ASSERT_EQ(r.initialized, 1) << r.note; + if (r.madeCurrent == 0) GTEST_SKIP() << "the surfaceless bind needs a pbuffer, and this host has no headless EGL"; + EXPECT_EQ(r.windowCreated, 0) << "a server with no display created a server-owned window surface"; + EXPECT_EQ(r.windowError, EGL_BAD_NATIVE_WINDOW); + std::string reap; + ASSERT_TRUE(WaitFor([&] { return !(reap = ReapLine(server, r.serverPid)).empty(); }, 15000)) << server.Log(); + const std::string serverLog = server.Log(); + EXPECT_NE(serverLog.find("surface=pbuffer 1x1 (surfaceless stand-in"), std::string::npos) + << "no stand-in arm proof:\n" << serverLog; + EXPECT_EQ(serverLog.find("SurfaceModeMismatch"), std::string::npos) + << "the surfaceless bind latched the session offscreen:\n" << serverLog; + EXPECT_NE(serverLog.find("Refuse ServerOwned: this server owns no display"), std::string::npos) << serverLog; + EXPECT_EQ(serverLog.find("Fatal{"), std::string::npos) << serverLog; + EXPECT_NE(reap.find("exit=0 "), std::string::npos) << reap; + EXPECT_NE(client.Own().find("Refuse ServerOwned (NoServerDisplay)"), std::string::npos) << client.Own(); + EXPECT_EQ(r.madeCurrentAfter, 1) << "the surfaceless bind failed after the refused window"; +}