From a9f22ebb6379e111d3215f29ab54a2029eb59348 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 3 Oct 2026 02:25:56 -0400 Subject: [PATCH] [Fix] (EGL): create and end a context's objects under its own binding and the stream lock, and defer destroying a current context. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 4 + MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 156 ++++++++++++++---- MobileGL/MG_State/EGLState/Core.cpp | 79 +++++++-- MobileGL/MG_State/EGLState/Core.h | 29 +++- MobileGL/MG_Test/EGLState/EGLStateTest.cpp | 94 +++++++++++ 5 files changed, 315 insertions(+), 47 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 245e76c6..9464fa48 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -17138,6 +17138,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // own thread with its own context, and binding a context back onto another window's // surface drew one window's frame into the other. NativeSessionState& session = ActiveNativeSession(); + // Nothing to make it current on yet: a context bound for the records of its own creation + // (EGLImpl's CreateContext) before the session has any surface. Its first make-current + // gives it one. + if (tuple.Draw == EGL_NO_SURFACE && session.Draw == EGL_NO_SURFACE) return true; if (tuple.Draw != session.Draw && SessionOwnsSurface(session, tuple.Draw)) { session.Draw = tuple.Draw; session.Read = SessionOwnsSurface(session, tuple.Read) ? tuple.Read : tuple.Draw; diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 95b33ef6..00f3b7d7 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -88,6 +88,8 @@ namespace MobileGL::MG_Impl::EGLImpl { } #if MOBILEGL_BUILD_DISAGGREGATED + using MG_Remote::Client::ClientSession; + // P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE CONTEXT BINDING, AND // THE ONE PLACE IT IS EMITTED. // @@ -159,6 +161,82 @@ namespace MobileGL::MG_Impl::EGLImpl { } namespace { + // RECORDS WRITTEN FOR A CONTEXT THAT IS NOT THE CALLING THREAD'S. eglCreateContext builds + // the new context's GL state (its default objects are created there) and eglDestroyContext + // ends a context's objects, on whatever thread calls them - one with another context + // current, or none. Those records belong to the context being created or destroyed, so the + // stream is bound to it first; the next GL entry point binds the stream back to its own + // thread's context (GLStreamScope). The caller holds StreamMutex(). + void StreamBindContextLocked(Uint64 token, Uint64 shareGroupToken) { + if (token == g_boundContextToken) return; + if (MG_Remote::Client::EmitBindContextRecord(token)) { + g_boundContextToken = token; + g_boundShareGroupToken = shareGroupToken; + } + } + + // THE ONE WAY A CONTEXT ENDS on a split client, whether eglDestroyContext ended it at once + // or its deferred destroy completed when the last thread released it. `glState` is the + // context's GL state, whose last owner this is: dropping it ends the context's own objects + // (and its share group's, when it is the group's last context). They end under the + // context's own binding, the residual fill's object pins included (a pinned vertex array + // or program of this context would otherwise end inside some later thread's draw), and + // only then does the server drop the context - the order the records need, because the + // server files nothing under a context it no longer has. A context lost with an ended + // session goes in silence, as before. + void FinishContextDestroy(Uint64 token, Uint64 shareGroupToken, SharedPtr glState, + Bool lost) { + if (token == 0 || lost || !StreamGateActive()) { + std::optional silence; + if (lost) silence.emplace(); + glState.reset(); + return; + } + StreamBindContextLocked(token, shareGroupToken); + // The pins live in the client's own residual block - its own memory only when the + // server is another process; in the one-process shape the apply thread reads that block + // too, and only a barriered apply's return may touch it (ClientSession::ReleaseFillPins). + if (ClientSession* session = ClientSession::Active(); + session != nullptr && session->Started() && MG_Config::Transport != MG_Config::TransportMode::InProcess) { + session->WaitForApplyToCatchUp("eglDestroyContext"); + session->ReleaseFillPins(); + } + glState.reset(); + // The server drops its binding to 0 when the bound context is destroyed + // (ServerSession::DestroyContext's compare-and-swap); the gate mirrors it so the next + // write re-binds. + if (g_boundContextToken == token) { + g_boundContextToken = 0; + g_boundShareGroupToken = 0; + } + const auto emitted = MG_Remote::Client::SendDestroyContextFrame(token); + if (emitted == MG_Remote::Client::ContextFrameEmit::Refused) { + MGLOG_E_ONCE("eglDestroyContext: the server holds no context token %llu; the " + "session's table and this client's EGL book have drifted apart", + static_cast(token)); + } + } + } // namespace + + void FinishReapedContexts(MG_State::EGLState::EGLContext* state) { + if (state == nullptr) return; + if (!StreamGateActive()) { + (void)state->TakeReapedContexts(); // dropping them ends their GL state + return; + } + for (auto& reaped : state->TakeReapedContexts()) { + const Bool lost = reaped.GLStateObject != nullptr && + (reaped.GLStateObject->IsFromEndedWireSession() || ClientSession::SessionLatchedLost()); + FinishContextDestroy(reaped.ClientContextToken, reaped.ShareGroupToken, Move(reaped.GLStateObject), lost); + } + } + + namespace { +#else + // A monolith has no stream to bind: a context whose deferred destroy completed simply ends. + void FinishReapedContexts(MG_State::EGLState::EGLContext* state) { + if (state != nullptr) (void)state->TakeReapedContexts(); + } #endif String CurrentThreadIdString() { std::ostringstream stream; @@ -713,8 +791,13 @@ namespace MobileGL::MG_Impl::EGLImpl { // The calling thread may still have a lost context current: what is built here is the // fresh session's all the same. const ClientSession::ScopedCurrentSessionWork currentSession; + // A split client builds the context's GL state only once the server has its token (below). + const Bool splitClient = MG_Config::Transport != MG_Config::TransportMode::Monolith && + !MG_Remote::Client::RunsAsTheServerRole(); +#else + constexpr Bool splitClient = false; #endif - EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list); + EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list, splitClient); if (context == EGL_NO_CONTEXT) { return EGL_NO_CONTEXT; } @@ -754,7 +837,7 @@ namespace MobileGL::MG_Impl::EGLImpl { replacedGroup = state->GetContextShareGroupToken(shareCtx); shareCtx = ReplacementShareContext(state, dpy, replacedGroup); } - context = state->CreateContext(dpy, config, shareCtx, attrib_list); + context = state->CreateContext(dpy, config, shareCtx, attrib_list, splitClient); } if (context == EGL_NO_CONTEXT) { state->SetError(EGL_BAD_ALLOC); @@ -769,6 +852,16 @@ namespace MobileGL::MG_Impl::EGLImpl { state->SetError(EGL_BAD_ACCESS); return EGL_NO_CONTEXT; } + // NOW THE CONTEXT'S GL STATE, under its own binding. Building it creates the context's + // default objects (a texture per target, ...), and the records that create them belong + // to THIS context's share group on the server - not to whichever context the stream + // was last bound to by some other thread, which is where they landed when the state was + // built before the token crossed: a context in a share group of its own then drew with + // default objects its group never had. + if (emitted == MG_Remote::Client::ContextFrameEmit::Sent) { + StreamBindContextLocked(state->GetContextClientToken(context), state->GetContextShareGroupToken(context)); + } + (void)state->BuildContextGLState(context); } #endif return context; @@ -828,6 +921,13 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { return EGL_FALSE; } + // A release (explicit, or the implicit one of binding another context) may complete the + // deferred destroy of the context it releases; that destroy is finished on the way out, + // under the lock, whichever way this call returns. + struct FinishReapedOnExit { + EGLStateContext* State; + ~FinishReapedOnExit() { FinishReapedContexts(State); } + } finishReaped{state}; const auto oldDisplay = state->GetCurrentDisplay(); const auto oldDraw = state->GetCurrentSurface(EGL_DRAW); @@ -928,41 +1028,34 @@ namespace MobileGL::MG_Impl::EGLImpl { return EGL_FALSE; } #if MOBILEGL_BUILD_DISAGGREGATED + // Under the operation lock, like every GL call: what ends here writes records. + const std::lock_guard destroyLock(EGLOperationMutex()); // P14 S1. Read BEFORE the client's own book drops the context: after DestroyContext // the handle no longer resolves and the token is unrecoverable. The frame is sent - // AFTER the client's state machine has accepted the destroy, so one EGL refused (a - // context still current on some thread, EGL_BAD_ACCESS) never reaches the server - the - // two ends stay in step without the server having to guess why a token vanished. + // AFTER the client's state machine has accepted the destroy, so one EGL refused never + // reaches the server - the two ends stay in step without the server having to guess why + // a token vanished. const Uint64 contextToken = state->GetContextClientToken(ctx); + const Uint64 shareGroupToken = state->GetContextShareGroupToken(ctx); // A LOST CONTEXT IS DESTROYED IN SILENCE. Its objects die here, and a death that reached // the wire would land on whichever session is current now - a fresh one after a recovery, - // which never created them. Under the operation lock, like every GL call. - const std::lock_guard destroyLock(EGLOperationMutex()); + // which never created them. const Bool lost = ContextIsLost(state, ctx); - std::optional silence; - if (lost) silence.emplace(); -#endif - if (!state->DestroyContext(dpy, ctx)) { + // The context's GL state, held across the book's drop so that its objects end in + // FinishContextDestroy, under the context's own binding, rather than inside EGLState. + SharedPtr glState = state->GetContextGLState(ctx); + Bool deferred = false; + if (!state->DestroyContext(dpy, ctx, &deferred)) { 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()); - // The server drops its binding to 0 when the current context is destroyed - // (ServerSession::DestroyContext's compare-and-swap); the gate mirrors it so the next - // write re-binds. - if (g_boundContextToken == contextToken) { - g_boundContextToken = 0; - g_boundShareGroupToken = 0; - } - const auto emitted = MG_Remote::Client::SendDestroyContextFrame(contextToken); - if (emitted == MG_Remote::Client::ContextFrameEmit::Refused) { - MGLOG_E_ONCE("eglDestroyContext: the server holds no context token %llu; the " - "session's table and this client's EGL book have drifted apart", - static_cast(contextToken)); - } + // CURRENT TO A THREAD (this one or another): EGL destroys it once it is released, and that + // release finishes it (FinishReapedContexts). Until then its thread keeps drawing in it. + if (deferred) return EGL_TRUE; + FinishContextDestroy(contextToken, shareGroupToken, Move(glState), lost); +#else + if (!state->DestroyContext(dpy, ctx)) { + return EGL_FALSE; } #endif return EGL_TRUE; @@ -1027,11 +1120,18 @@ namespace MobileGL::MG_Impl::EGLImpl { // Always succeeds (EGL 1.4 ยง3.11): after the last eglTerminate tore MobileGL down there is // simply nothing of this thread's left to release. if (!MG_State::pEGLContext) return EGL_TRUE; + const std::lock_guard operationLock(EGLOperationMutex()); auto* state = GetState(); if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) { (void)backendObject->MakeEGLCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } state->ReleaseThread(); +#if MOBILEGL_BUILD_DISAGGREGATED + // The release edge, as eglMakeCurrent's release sends it: this thread has no context now, + // and the binding must not keep naming the one it had. + EmitContextBinding(state, EGL_NO_CONTEXT, 0); +#endif + FinishReapedContexts(state); return EGL_TRUE; } diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index a4f91874..d1820b86 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -250,17 +250,34 @@ namespace MobileGL { } // P14 S3: the thread's GL state goes back to what it was before this EGL // context was made current on it. - if (currentIt->second.RestoreThreadGLState) { - pGLContext = Move(currentIt->second.RestoreThreadGLState); + // + // A THREAD THAT HAD NONE (one that never had GL state before its first bind) GOES + // TO THE PROCESS DEFAULT, never keeps the released context's. Keeping it made the + // thread's TLS the last owner of a context destroyed after the release, so the + // context's objects ended in the thread-exit destructor of that TLS: outside every + // entry point and its lock, writing their records into the middle of another + // thread's - torn records, a reply read from the wrong slot, a lost session. + pGLContext = currentIt->second.RestoreThreadGLState ? Move(currentIt->second.RestoreThreadGLState) + : ProcessDefaultGLContext(); #if MOBILEGL_BUILD_DISAGGREGATED - // A fallback built against a server session that has since ended (a device - // loss, recovered) is lost with it: the thread falls back to today's default. - if (pGLContext->IsFromEndedWireSession()) pGLContext = ProcessDefaultGLContext(); + // A fallback built against a server session that has since ended (a device loss, + // recovered) is lost with it: the thread falls back to today's default. + if (pGLContext->IsFromEndedWireSession()) pGLContext = ProcessDefaultGLContext(); #endif - } m_threadCurrents.erase(currentIt); DestroyPendingSurfaceIfUnused(drawSurface); DestroyPendingSurfaceIfUnused(readSurface); + // A context destroyed while it was current is destroyed now that nothing has it + // current; the caller finishes it (TakeReapedContexts). + if (context != nullptr && m_contextOwners.find(context) == m_contextOwners.end()) { + const auto contextIt = m_contexts.find(context); + if (contextIt != m_contexts.end() && contextIt->second.DestroyPending) { + m_reapedContexts.push_back(ReapedContext{context, contextIt->second.ClientContextToken, + contextIt->second.ShareGroupToken, + Move(contextIt->second.GLStateObject)}); + m_contexts.erase(contextIt); + } + } } Bool EGLContext::IsSurfaceCurrentUnlocked(EGLSurfaceHandle surface) const { @@ -656,7 +673,8 @@ namespace MobileGL { EGLContext::EGLContextHandle EGLContext::CreateContext(EGLDisplayHandle display, EGLConfigHandle config, EGLContextHandle shareCtx, - const EGLint* attribList) { + const EGLint* attribList, + Bool deferGLState) { const std::lock_guard lock(m_mutex); const auto* displayObject = TryGetDisplay(display); if (!displayObject) { @@ -737,7 +755,11 @@ namespace MobileGL { } // P14 S3: the context's own GL state, sharing the objects (and the name spaces) // of the group above, with bindings, errors and the current program of its own. - contextObject.GLStateObject = MakeShared(shareGroup); + if (deferGLState) { + contextObject.PendingShareGroup = Move(shareGroup); + } else { + contextObject.GLStateObject = MakeShared(shareGroup); + } if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_CLIENT_VERSION); value) { contextObject.ClientVersion = *value; @@ -791,10 +813,23 @@ namespace MobileGL { return context; } - Bool EGLContext::DestroyContext(EGLDisplayHandle display, EGLContextHandle context) { + Bool EGLContext::BuildContextGLState(EGLContextHandle context) { const std::lock_guard lock(m_mutex); + const auto contextIt = m_contexts.find(context); + if (contextIt == m_contexts.end()) return false; + ContextObject& object = contextIt->second; + if (!object.GLStateObject) { + object.GLStateObject = MakeShared(Move(object.PendingShareGroup)); + object.PendingShareGroup = nullptr; + } + return true; + } + + Bool EGLContext::DestroyContext(EGLDisplayHandle display, EGLContextHandle context, Bool* deferred) { + const std::lock_guard lock(m_mutex); + if (deferred != nullptr) *deferred = false; auto contextIt = m_contexts.find(context); - if (contextIt == m_contexts.end()) { + if (contextIt == m_contexts.end() || contextIt->second.DestroyPending) { SetError(EGL_BAD_CONTEXT); return false; } @@ -803,15 +838,23 @@ namespace MobileGL { return false; } - auto ownerIt = m_contextOwners.find(context); - if (ownerIt != m_contextOwners.end()) { - SetError(EGL_BAD_ACCESS); - return false; + if (m_contextOwners.find(context) != m_contextOwners.end()) { + // EGL 1.5 3.7.2: current to a thread, so destroyed once it no longer is. + contextIt->second.DestroyPending = true; + if (deferred != nullptr) *deferred = true; + return true; } m_contexts.erase(contextIt); return true; } + Vector EGLContext::TakeReapedContexts() { + const std::lock_guard lock(m_mutex); + Vector reaped; + reaped.swap(m_reapedContexts); + return reaped; + } + Bool EGLContext::QueryContext(EGLDisplayHandle display, EGLContextHandle context, EGLint attribute, EGLint* value) const { const std::lock_guard lock(m_mutex); @@ -820,7 +863,7 @@ namespace MobileGL { return false; } const auto* contextObject = TryGetContext(context); - if (!contextObject) { + if (!contextObject || contextObject->DestroyPending) { const_cast(this)->SetError(EGL_BAD_CONTEXT); return false; } @@ -867,13 +910,15 @@ namespace MobileGL { Bool EGLContext::ValidateContext(EGLContextHandle context) const { const std::lock_guard lock(m_mutex); - return m_contexts.find(context) != m_contexts.end(); + const auto* ctx = TryGetContext(context); + return ctx != nullptr && !ctx->DestroyPending; } + // A context whose destroy is pending is no context for any new use of its handle. Bool EGLContext::ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const { const std::lock_guard lock(m_mutex); const auto* ctx = TryGetContext(context); - return ctx && ctx->Display == display; + return ctx && !ctx->DestroyPending && ctx->Display == display; } // P14 S1. 0 for an unknown handle or a thread with no current context: 0 is the diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index ac641c0c..399c0121 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -59,9 +59,28 @@ namespace MobileGL { EGLint* value) const; // Context + // `deferGLState`: the context is created without its GL state, which + // BuildContextGLState builds later. Building it creates the context's default + // objects, and a split client has to tell its server which context they belong to + // first (the server must know the context before anything is filed under it). EGLContextHandle CreateContext(EGLDisplayHandle display, EGLConfigHandle config, - EGLContextHandle shareCtx, const EGLint* attribList); - Bool DestroyContext(EGLDisplayHandle display, EGLContextHandle context); + EGLContextHandle shareCtx, const EGLint* attribList, + Bool deferGLState = false); + Bool BuildContextGLState(EGLContextHandle context); + // EGL 1.5 3.7.2: a context current to some thread is not destroyed until it is no + // longer current. Such a destroy answers true with `*deferred` set: the handle is + // invalid for any new use from here on, and the release that ends its last binding + // moves it to the reaped list (TakeReapedContexts), where the caller finishes it. + Bool DestroyContext(EGLDisplayHandle display, EGLContextHandle context, Bool* deferred = nullptr); + struct ReapedContext { + EGLContextHandle Handle = nullptr; + Uint64 ClientContextToken = 0; + Uint64 ShareGroupToken = 0; + SharedPtr GLStateObject; + }; + // The contexts whose deferred destroy a release has just completed, oldest first. + // The caller owns them now: dropping a GLStateObject destroys the context's objects. + Vector TakeReapedContexts(); Bool QueryContext(EGLDisplayHandle display, EGLContextHandle context, EGLint attribute, EGLint* value) const; Bool ValidateContext(EGLContextHandle context) const; @@ -267,6 +286,11 @@ namespace MobileGL { // share group's ShareGroupState (shared with every context that shares with // this one); its bindings, errors and current program are its own. SharedPtr GLStateObject; + // The group GLStateObject joins when it is built later (CreateContext's + // deferGLState); null once it is built. + SharedPtr PendingShareGroup; + // eglDestroyContext was called while the context was current to a thread. + Bool DestroyPending = false; EGLenum ClientAPI = EGL_OPENGL_API; EGLint ClientVersion = 1; EGLint MajorVersion = 1; @@ -379,6 +403,7 @@ namespace MobileGL { UnorderedMap m_configs; UnorderedMap m_surfaces; UnorderedMap m_contexts; + Vector m_reapedContexts; UnorderedMap m_syncs; UnorderedMap m_images; Vector m_orphanedSharedImages; diff --git a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp index 21183709..c1e68ccb 100644 --- a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp +++ b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -149,6 +150,99 @@ TEST(EGLStateMakeCurrent, SurfacelessBindIsAcceptedAndRecordsNoSurface) { EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); } +// EGL 1.5 3.7.2: a context current to a thread is destroyed once it is no longer current. The +// destroy answers EGL_TRUE (it used to be EGL_BAD_ACCESS), the handle is dead for any new use at +// once, the thread keeps it current until it releases it, and that release hands the context - +// with its GL state, whose last owner the caller now is - to whoever finishes the destroy. +TEST(EGLStateDestroyContext, DestroyingAContextCurrentOnAThreadIsDeferredUntilItsRelease) { + auto fixture = CreateFixture(); + auto& state = fixture->State; + const MobileGL::Uint64 token = state.GetContextClientToken(fixture->Context); + std::weak_ptr glState = state.GetContextGLState(fixture->Context); + ASSERT_FALSE(glState.expired()); + + std::promise current; + std::promise destroyed; + auto currentFuture = current.get_future(); + auto destroyedFuture = destroyed.get_future(); + bool stillCurrentAfterDestroy = false; + bool rebindRefused = false; + EGLint rebindError = EGL_SUCCESS; + size_t reapedWhileCurrent = 0; + std::thread owner([&] { + EXPECT_TRUE(state.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context)); + current.set_value(); + destroyedFuture.wait(); + stillCurrentAfterDestroy = state.GetCurrentContext() == fixture->Context; + rebindRefused = !state.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context); + rebindError = state.ConsumeError(); + reapedWhileCurrent = state.TakeReapedContexts().size(); + EXPECT_TRUE(state.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + }); + currentFuture.wait(); + bool deferred = false; + EXPECT_TRUE(state.DestroyContext(fixture->Display, fixture->Context, &deferred)); + EXPECT_TRUE(deferred); + EXPECT_FALSE(state.ValidateContext(fixture->Context)); + EXPECT_FALSE(state.DestroyContext(fixture->Display, fixture->Context)); + EXPECT_EQ(state.ConsumeError(), EGL_BAD_CONTEXT); + destroyed.set_value(); + owner.join(); + + EXPECT_TRUE(stillCurrentAfterDestroy); + EXPECT_TRUE(rebindRefused); + EXPECT_EQ(rebindError, EGL_BAD_CONTEXT); + EXPECT_EQ(reapedWhileCurrent, 0u); + auto reaped = state.TakeReapedContexts(); + ASSERT_EQ(reaped.size(), 1u); + EXPECT_EQ(reaped[0].Handle, fixture->Context); + EXPECT_EQ(reaped[0].ClientContextToken, token); + ASSERT_NE(reaped[0].GLStateObject, nullptr); + // Nothing else holds the GL state - neither the book nor the thread that released it. + reaped.clear(); + EXPECT_TRUE(glState.expired()); +} + +// A thread that had no GL state before its first bind does not keep the context's after releasing +// it. Keeping it made the thread's TLS the last owner of a context destroyed after the release, and +// the context's objects then died in the thread-exit destructor of that TLS, outside every entry +// point and its lock. +TEST(EGLStateDestroyContext, ReleasingThreadDoesNotKeepTheContextsGLState) { + auto fixture = CreateFixture(); + auto& state = fixture->State; + std::weak_ptr glState = state.GetContextGLState(fixture->Context); + std::thread worker([&] { + EXPECT_TRUE(state.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context)); + EXPECT_EQ(MobileGL::MG_State::pGLContext.get(), glState.lock().get()); + EXPECT_TRUE(state.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + EXPECT_NE(MobileGL::MG_State::pGLContext.get(), glState.lock().get()); + bool deferred = true; + EXPECT_TRUE(state.DestroyContext(fixture->Display, fixture->Context, &deferred)); + EXPECT_FALSE(deferred); + // Ended here, by the destroy - not later, when this thread exits. + EXPECT_TRUE(glState.expired()); + }); + worker.join(); +} + +// A context created with its GL state deferred has none until BuildContextGLState builds it (a +// split client tells its server about the context first, then builds the state under it). +TEST(EGLStateCreateContext, DeferredGLStateIsBuiltOnRequestInTheShareGroup) { + auto fixture = CreateFixture(); + auto& state = fixture->State; + const auto shared = state.CreateContext(fixture->Display, fixture->Config, fixture->Context, nullptr, + /*deferGLState=*/true); + ASSERT_NE(shared, EGL_NO_CONTEXT); + EXPECT_EQ(state.GetContextGLState(shared), nullptr); + EXPECT_EQ(state.GetContextShareGroupToken(shared), state.GetContextShareGroupToken(fixture->Context)); + ASSERT_TRUE(state.BuildContextGLState(shared)); + const auto built = state.GetContextGLState(shared); + ASSERT_NE(built, nullptr); + EXPECT_EQ(built->GetShareGroup(), state.GetContextGLState(fixture->Context)->GetShareGroup()); + ASSERT_TRUE(state.BuildContextGLState(shared)); // idempotent + EXPECT_EQ(state.GetContextGLState(shared), built); +} + // The extension relaxes only the both-absent case: one surface without the other stays a mismatch. TEST(EGLStateMakeCurrent, OneSidedSurfaceIsStillAMismatch) { auto fixture = CreateFixture();