[Fix] (EGL): contexts recreated naming a lost share context share one new group instead of none.

This commit is contained in:
2026-10-03 00:42:34 -04:00
parent 9f30fc500f
commit 8024b645cb
2 changed files with 158 additions and 7 deletions
+58 -6
View File
@@ -38,6 +38,7 @@
#if defined(__linux__) && !defined(__ANDROID__)
#include <dlfcn.h>
#endif
#include <algorithm>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
@@ -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<Uint64, Vector<EGLContext>>& ReplacementShareContexts() {
static auto* replacements = new std::unordered_map<Uint64, Vector<EGLContext>>();
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<EGLContext>& 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<std::recursive_mutex> 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<std::recursive_mutex> streamLock(EGLOperationMutex());
+100 -1
View File
@@ -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<ShareReport>(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();
}