From 8024b645cb06bba85442d0698e5e5b0dfe15a3e2 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 3 Oct 2026 00:42:34 -0400 Subject: [PATCH] [Fix] (EGL): contexts recreated naming a lost share context share one new group instead of none. --- MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 64 ++++++++++-- MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp | 101 ++++++++++++++++++- 2 files changed, 158 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index eff7271a..d893c117 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -38,6 +38,7 @@ #if defined(__linux__) && !defined(__ANDROID__) #include #endif +#include #include #include #include @@ -424,6 +425,36 @@ namespace MobileGL::MG_Impl::EGLImpl { return true; } + // THE SHARE GROUPS THAT REPLACE LOST ONES, by the lost group's token. An application that + // keeps one long-lived context to share with (Qt's global share context) recreates its + // other contexts after a loss still naming that context, which is lost and stays so. Those + // contexts are put in ONE new share group instead of each in a group of its own: the first + // starts it, the later ones share with a live member of it. Without this, every window of + // such an application lands in a separate share group after the loss, a shape the + // application never asked for. Guarded by EGLOperationMutex. + std::unordered_map>& ReplacementShareContexts() { + static auto* replacements = new std::unordered_map>(); + return *replacements; + } + + // The context a new context should share with in place of the lost `shareCtx`: a live + // member of the group that replaces the lost one, or EGL_NO_CONTEXT to start that group. + EGLContext ReplacementShareContext(EGLStateContext* state, EGLDisplay dpy, Uint64 lostGroup) { + Vector& members = ReplacementShareContexts()[lostGroup]; + members.erase(std::remove_if(members.begin(), members.end(), + [&](EGLContext member) { + return !state->ValidateContextOnDisplay(dpy, member) || + ContextIsLost(state, member); + }), + members.end()); + return members.empty() ? EGL_NO_CONTEXT : members.front(); + } + + void ForgetReplacementShareContext(EGLContext context) { + for (auto& [group, members] : ReplacementShareContexts()) + members.erase(std::remove(members.begin(), members.end(), context), members.end()); + } + // The surface entry points' guard: a surface created after a loss is created on a fresh // session. EGL_BAD_ALLOC when no session could be brought up. Bool RecoverBeforeCreatingSurface(EGLStateContext* state) { @@ -646,17 +677,26 @@ namespace MobileGL::MG_Impl::EGLImpl { // building it already emits the creates of its default objects. Sharing with a lost // context would share objects no reachable server holds. const std::lock_guard operationLock(EGLOperationMutex()); + // The lost share group this context replaces (ReplacementShareContexts), 0 for none. + Uint64 replacedGroup = 0; if (StreamGateActive()) { if (!RecoverLostSession()) { state->SetError(EGL_BAD_ALLOC); return EGL_NO_CONTEXT; } if (shareCtx != EGL_NO_CONTEXT && ContextIsLost(state, shareCtx)) { - MGLOG_I_ONCE("eglCreateContext: refused (EGL_BAD_CONTEXT) - the share context %p was lost with its " - "session (device loss), and its objects are on no server", - shareCtx); - state->SetError(EGL_BAD_CONTEXT); - return EGL_NO_CONTEXT; + // Its objects are on no server, so the context does not share them: it joins the + // group that replaces the lost one (the other contexts recreated in its place). + if (!state->ValidateContextOnDisplay(dpy, shareCtx)) { + state->SetError(EGL_BAD_CONTEXT); + return EGL_NO_CONTEXT; + } + replacedGroup = state->GetContextShareGroupToken(shareCtx); + const EGLContext lostShare = shareCtx; + shareCtx = ReplacementShareContext(state, dpy, replacedGroup); + MGLOG_I_ONCE("eglCreateContext: the share context %p was lost with its session (device loss); " + "contexts that name it share with each other in a new share group instead", + lostShare); } } // The calling thread may still have a lost context current: what is built here is the @@ -667,6 +707,9 @@ namespace MobileGL::MG_Impl::EGLImpl { if (context == EGL_NO_CONTEXT) { return EGL_NO_CONTEXT; } +#if MOBILEGL_BUILD_DISAGGREGATED + if (replacedGroup != 0) ReplacementShareContexts()[replacedGroup].push_back(context); +#endif #if MOBILEGL_BUILD_DISAGGREGATED // P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE TOKEN CROSSES BEFORE // eglCreateContext RETURNS. EGLState minted it, together with the share group it was @@ -694,11 +737,19 @@ namespace MobileGL::MG_Impl::EGLImpl { state->DestroyContext(dpy, context); } context = EGL_NO_CONTEXT; - if (RecoverLostSession()) context = state->CreateContext(dpy, config, shareCtx, attrib_list); + if (RecoverLostSession()) { + // The context it was to share with went with the session, too. + if (shareCtx != EGL_NO_CONTEXT && ContextIsLost(state, shareCtx)) { + replacedGroup = state->GetContextShareGroupToken(shareCtx); + shareCtx = ReplacementShareContext(state, dpy, replacedGroup); + } + context = state->CreateContext(dpy, config, shareCtx, attrib_list); + } if (context == EGL_NO_CONTEXT) { state->SetError(EGL_BAD_ALLOC); return EGL_NO_CONTEXT; } + if (replacedGroup != 0) ReplacementShareContexts()[replacedGroup].push_back(context); emitted = MG_Remote::Client::SendCreateContextFrame(state->GetContextClientToken(context), state->GetContextShareGroupToken(context), 0); } @@ -884,6 +935,7 @@ namespace MobileGL::MG_Impl::EGLImpl { return EGL_FALSE; } #if MOBILEGL_BUILD_DISAGGREGATED + ForgetReplacementShareContext(ctx); if (contextToken != 0 && !lost && MG_Config::Transport != MG_Config::TransportMode::Monolith && !MG_Remote::Client::RunsAsTheServerRole()) { const std::lock_guard streamLock(EGLOperationMutex()); diff --git a/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp b/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp index 6c2a319a..99f37eb3 100644 --- a/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp +++ b/MobileGL/MG_Test/Wire/ClientRecoveryTest.cpp @@ -25,7 +25,9 @@ // (the server's per-session refusal line stays silent), and the old texture's name is gone; // * the surface the application kept works with the new context (EGL keeps surfaces across a // power-management loss; only their contents go); -// * the other route back - eglTerminate, then eglInitialize - brings a fresh session up too. +// * the other route back - eglTerminate, then eglInitialize - brings a fresh session up too; +// * contexts recreated still naming a lost share context (Qt's global share context) share +// with EACH OTHER in one new group, not each in a group of its own. #include "P12ServerRig.h" @@ -359,6 +361,80 @@ namespace { (void)EGL::Terminate(egl.dpy); } + // QT'S ROUTE. A long-lived share context the application never recreates, and its working + // contexts created sharing with it; after the loss the working contexts are recreated, still + // naming the share context, which is lost. + struct ShareReport { + Int32 initialized = 0; + Int32 firstOk = 0; + Int32 secondOk = 0; + Int32 firstError = 0; + Int32 sameGroup = -1; + Int32 lostGroupReused = -1; + Int32 textureSeenAcross = -1; + Uint32 drawnAcross = 0; + char note[256] = {}; + }; + + void Note(ShareReport& r, const char* text) { + if (r.note[0] == '\0') std::snprintf(r.note, sizeof(r.note), "%s", text); + } + + void RunShareReplacementClient(ShareReport& r) { + Egl egl; + RecoveryReport bring{}; + if (!egl.Bring(bring)) return Note(r, bring.note); + const EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; + const EGLContext share = egl.Context(); + const EGLContext working = EGL::CreateContext(egl.dpy, egl.config, share, attribs); + if (working == EGL_NO_CONTEXT || EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, working) != EGL_TRUE) + return Note(r, "the working context did not go current"); + r.initialized = 1; + TexturedDraw old; + RecoveryReport built{}; + if (!old.Build(built, kGreen)) return Note(r, built.note); + old.Draw(); + (void)ReadPixel(); // device check #1 + (void)ClearAndRead(0.0f, 0.0f, 1.0f); // device check #2: lost + (void)WaitFor([] { return glGetGraphicsResetStatus() != GL_NO_ERROR; }, 5000); + (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + (void)EGL::DestroyContext(egl.dpy, working); + + // Two windows' contexts, recreated naming the lost share context. + const EGLContext first = EGL::CreateContext(egl.dpy, egl.config, share, attribs); + r.firstError = EGL::GetError(); + const EGLContext second = EGL::CreateContext(egl.dpy, egl.config, share, attribs); + r.firstOk = first != EGL_NO_CONTEXT; + r.secondOk = second != EGL_NO_CONTEXT; + if (!r.firstOk || !r.secondOk) return Note(r, "a context naming the lost share context was refused"); + const Uint64 lostGroup = MG_State::pEGLContext->GetContextShareGroupToken(share); + const Uint64 firstGroup = MG_State::pEGLContext->GetContextShareGroupToken(first); + r.sameGroup = firstGroup == MG_State::pEGLContext->GetContextShareGroupToken(second) ? 1 : 0; + r.lostGroupReused = firstGroup == lostGroup ? 1 : 0; + + // An object of one is the other's: a texture filled on the first is drawn on the second. + if (EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, first) != EGL_TRUE) + return Note(r, "the first replacement did not go current"); + TexturedDraw shared; + if (!shared.Build(built, kRed)) return Note(r, built.note); + (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (EGL::MakeCurrent(egl.dpy, egl.surface, egl.surface, second) != EGL_TRUE) + return Note(r, "the second replacement did not go current"); + r.textureSeenAcross = glIsTexture(shared.texture) == GL_TRUE ? 1 : 0; + GLuint vertexArray = 0; // a vertex array is per context + glGenVertexArrays(1, &vertexArray); + shared.vertexArray = vertexArray; + shared.Draw(); + r.drawnAcross = ReadPixel(); + + (void)EGL::MakeCurrent(egl.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + (void)EGL::DestroyContext(egl.dpy, second); + (void)EGL::DestroyContext(egl.dpy, first); + (void)EGL::DestroyContext(egl.dpy, share); + (void)EGL::DestroySurface(egl.dpy, egl.surface); + (void)EGL::Terminate(egl.dpy); + } + // A thread of the client that runs what it is handed, one job at a time, and keeps whatever // EGL context it made current between jobs - an application's other GL thread. class Worker { @@ -692,3 +768,26 @@ TEST_F(ClientRecovery, AThreadStillOnTheLostContextReachesNothingOfTheFreshSessi ExpectOneLossAndACleanSecondSession(server); server.Stop(); } + +// QT'S ROUTE: the windows' contexts are recreated still naming the long-lived share context, which +// is lost. They are created, in ONE new share group (the lost group's objects are on no server): +// a texture made on one is drawn on the other. Red with the share refused (EGL_BAD_CONTEXT): Qt +// then falls back to an unshared context per window. +TEST_F(ClientRecovery, ContextsNamingALostShareContextShareOneNewGroup) { + ServerProcess server; + ASSERT_TRUE(Launch(&server, "share")) << server.Log(); + ClientLog client("share"); + const ShareReport r = RunInClient(server, client.base, &RunShareReplacementClient); + const std::string diagnostics = "server:\n" + server.Log() + "\nclient:\n" + client.Text(); + ASSERT_EQ(r.initialized, 1) << r.note << "\n" << diagnostics; + ASSERT_EQ(r.firstOk, 1) << "eglCreateContext naming the lost share context failed (0x" << std::hex << r.firstError + << ")\n" << r.note; + ASSERT_EQ(r.secondOk, 1) << r.note; + EXPECT_EQ(r.sameGroup, 1) << "the replacements landed in separate share groups"; + EXPECT_EQ(r.lostGroupReused, 0) << "a replacement joined the lost share group"; + EXPECT_EQ(r.textureSeenAcross, 1) << "a texture of one replacement is not the other's"; + EXPECT_EQ(r.drawnAcross, kRed) << "the shared texture did not draw on the other replacement: " << std::hex + << r.drawnAcross << "\n" << r.note << "\n" << diagnostics; + ExpectOneLossAndACleanSecondSession(server); + server.Stop(); +}