[Fix] (Remote): a surfaceless make-current's hidden pbuffer no longer latches the session offscreen.

This commit is contained in:
2026-10-02 23:59:54 -04:00
parent 8b5295401f
commit b06c2a9bf2
7 changed files with 122 additions and 11 deletions
+26 -1
View File
@@ -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<MG_Remote::Client::BackendObject_Remote*>(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,
@@ -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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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 =
@@ -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<std::recursive_mutex> 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<EGLSurface> m_homedSurfaces;
// The surfaceless stand-ins among this object's pbuffers. Guarded by m_eglStateMutex.
std::unordered_set<EGLSurface> m_standInSurfaces;
std::unordered_map<Uint64, Uint32> m_sessionSharedImages; // id -> references taken
// The ids a replaced session held: releasing one is a success with nothing sent.
std::unordered_set<Uint64> m_lostSessionSharedImages;
+16 -3
View File
@@ -1513,12 +1513,16 @@ namespace MobileGL::MG_Remote::Server {
static_cast<Uint32>(frame.width),
static_cast<Uint32>(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<Uint8>(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<SurfaceRefusalCode>(frame.refusal);
return rc == MOBILEGL_OK && frame.ok;
+3 -2
View File
@@ -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
@@ -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
@@ -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";
}