[Fix] (EGL): create and end a context's objects under its own binding and the stream lock, and defer destroying a current context.

This commit is contained in:
2026-10-03 05:37:50 -04:00
parent 2fe011adf6
commit a9f22ebb63
5 changed files with 315 additions and 47 deletions
@@ -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;
+128 -28
View File
@@ -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<MG_State::GLState::GLContext> glState,
Bool lost) {
if (token == 0 || lost || !StreamGateActive()) {
std::optional<ClientSession::ScopedWireMute> 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<unsigned long long>(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<std::recursive_mutex> 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<std::recursive_mutex> destroyLock(EGLOperationMutex());
// which never created them.
const Bool lost = ContextIsLost(state, ctx);
std::optional<ClientSession::ScopedWireMute> 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<MG_State::GLState::GLContext> 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<std::recursive_mutex> 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<unsigned long long>(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<std::recursive_mutex> 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;
}
+62 -17
View File
@@ -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<std::recursive_mutex> 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<GLState::GLContext>(shareGroup);
if (deferGLState) {
contextObject.PendingShareGroup = Move(shareGroup);
} else {
contextObject.GLStateObject = MakeShared<GLState::GLContext>(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<std::recursive_mutex> 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<GLState::GLContext>(Move(object.PendingShareGroup));
object.PendingShareGroup = nullptr;
}
return true;
}
Bool EGLContext::DestroyContext(EGLDisplayHandle display, EGLContextHandle context, Bool* deferred) {
const std::lock_guard<std::recursive_mutex> 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::ReapedContext> EGLContext::TakeReapedContexts() {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
Vector<ReapedContext> reaped;
reaped.swap(m_reapedContexts);
return reaped;
}
Bool EGLContext::QueryContext(EGLDisplayHandle display, EGLContextHandle context, EGLint attribute,
EGLint* value) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
@@ -820,7 +863,7 @@ namespace MobileGL {
return false;
}
const auto* contextObject = TryGetContext(context);
if (!contextObject) {
if (!contextObject || contextObject->DestroyPending) {
const_cast<EGLContext*>(this)->SetError(EGL_BAD_CONTEXT);
return false;
}
@@ -867,13 +910,15 @@ namespace MobileGL {
Bool EGLContext::ValidateContext(EGLContextHandle context) const {
const std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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
+27 -2
View File
@@ -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<GLState::GLContext> 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<ReapedContext> 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<GLState::GLContext> GLStateObject;
// The group GLStateObject joins when it is built later (CreateContext's
// deferGLState); null once it is built.
SharedPtr<GLState::ShareGroupState> 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<EGLConfigHandle, ConfigObject> m_configs;
UnorderedMap<EGLSurfaceHandle, SurfaceObject> m_surfaces;
UnorderedMap<EGLContextHandle, ContextObject> m_contexts;
Vector<ReapedContext> m_reapedContexts;
UnorderedMap<EGLSyncHandle, SyncObject> m_syncs;
UnorderedMap<EGLImageHandle, ImageObject> m_images;
Vector<Uint64> m_orphanedSharedImages;
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include <MG_State/EGLState/Core.h>
#include <MG_State/GLState/Core.h>
#include <EGL/eglext.h>
#include <future>
#include <memory>
@@ -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<MobileGL::MG_State::GLState::GLContext> glState = state.GetContextGLState(fixture->Context);
ASSERT_FALSE(glState.expired());
std::promise<void> current;
std::promise<void> 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<MobileGL::MG_State::GLState::GLContext> 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();