mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user