[Feature] (P14): state ownership - multi-session, multi-context, share groups

Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.

S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.

S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.

S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.

S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.

S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.

Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.

Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
This commit is contained in:
2026-10-01 19:16:41 -04:00
parent ebc552a048
commit 4ed1c9cef2
80 changed files with 4677 additions and 949 deletions
+5
View File
@@ -464,6 +464,7 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
MobileGL/MG_State/GLState/Core.cpp MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/GLState/ShareGroupState.cpp
MobileGL/MG_State/EGLState/Core.cpp MobileGL/MG_State/EGLState/Core.cpp
MobileGL/MG_State/GLState/ErrorState/Error.cpp MobileGL/MG_State/GLState/ErrorState/Error.cpp
MobileGL/MG_State/GLState/BufferState/BufferState.cpp MobileGL/MG_State/GLState/BufferState/BufferState.cpp
@@ -662,6 +663,10 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Server/ServerSession.cpp MobileGL/MG_Remote/Server/ServerSession.cpp
MobileGL/MG_Remote/Server/PipeApplier.cpp MobileGL/MG_Remote/Server/PipeApplier.cpp
MobileGL/MG_Remote/Server/ServerLoop.cpp MobileGL/MG_Remote/Server/ServerLoop.cpp
# P14 S2: the per-session runtime and the thread routing that lets one process serve
# several sessions (the real session a singleton accessor answers with on that
# session's own threads).
MobileGL/MG_Remote/Server/SessionRuntime.cpp
MobileGL/MG_Remote/Server/ServerMain.cpp MobileGL/MG_Remote/Server/ServerMain.cpp
# P11 PAIR: a socket client's control and aux connections paired by the nonce each # P11 PAIR: a socket client's control and aux connections paired by the nonce each
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here. # presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
@@ -976,8 +976,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true; return true;
} }
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
//
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
// the native context - and with it every buffer, texture and program the application had
// built. The native context now belongs to the (session, context token) tuple the backend
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
// bookkeeping (which surface is active, which threads made it current) does describe the
// surface that is being replaced.
if (m_eglSurfaceInitialized) { if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState(); ResetEGLRuntimeState();
} }
@@ -1008,8 +1017,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true; return true;
} }
// P14 S4: as for the window surface above - the native context is the (session, context
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
if (m_eglSurfaceInitialized) { if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState(); ResetEGLRuntimeState();
} }
+436 -62
View File
@@ -16180,10 +16180,225 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESCapabilities = capabilities; g_GLESCapabilities = capabilities;
} }
// ---------------------------------------------------------------------------------
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): the native tuple is per context.
//
// THE DISPLAY STAYS ONE OBJECT. eglInitialize is once per process and eglTerminate runs when the
// last native context has gone, so a second context - or a second session - never terminates the
// display the first one is still using.
//
// EVERYTHING ELSE THAT WAS A FILE-LEVEL SINGLETON IS KEYED. A native EGLContext, the config it
// was created with and the native surfaces are keyed by (session, client context token): the
// identity S1 put on the wire (bind_context / SurfaceOp.context) and S2 gave a per-thread owner.
// The key is resolved on the calling thread through a probe the server layer installs
// (SetNativeContextKeyResolver); with none installed the answer is the single key {0, 0}, which
// is byte-for-byte the process singleton this replaces - monolith, a frontend-only process and
// every unit case keep exactly one tuple.
//
// WHY THE SURFACES ARE KEYED BY SESSION AND NOT BY CONTEXT: an EGLSurface is not owned by an
// EGLContext (EGL has no such relationship - only the draw/read BINDING is per context), so a
// session's surfaces are one set and each context records which of them it currently draws to.
// That is also what makes pbuffer -> window -> pbuffer keep its objects: creating the second
// surface does not touch the context, so the twins the first surface's draws built are still
// there when the third one is bound.
static EGLDisplay g_Display = EGL_NO_DISPLAY; static EGLDisplay g_Display = EGL_NO_DISPLAY;
static EGLContext g_Context = EGL_NO_CONTEXT; // eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0.
static EGLSurface g_Surface = EGL_NO_SURFACE; static Uint32 g_displayUsers = 0;
static EGLConfig g_Config = nullptr;
struct NativeContextTuple {
Uint64 SessionKey = 0;
Uint64 ContextToken = 0;
Uint64 ShareGroupToken = 0;
EGLContext Context = EGL_NO_CONTEXT;
EGLConfig Config = nullptr;
// The session surface this context draws to / reads from, as last made current for it.
EGLSurface Draw = EGL_NO_SURFACE;
EGLSurface Read = EGL_NO_SURFACE;
};
struct NativeSessionState {
Uint64 SessionKey = 0;
// Every native surface this session created, so its teardown destroys its own and only its own.
Vector<EGLSurface> Surfaces;
// The surface the session currently draws to / reads from - what a bind to another context
// re-makes current, and what Present/eglSwapBuffers names.
EGLSurface Draw = EGL_NO_SURFACE;
EGLSurface Read = EGL_NO_SURFACE;
};
static std::mutex g_nativeRegistryMutex;
// Bumped on every registry mutation. The per-thread active-tuple cache below is keyed on it, so
// a destroyed tuple can never be handed out from a stale cache entry.
static std::atomic<Uint64> g_nativeRegistryEpoch{1};
// Never erased while the process lives: a context teardown marks its tuple dead (Context =
// EGL_NO_CONTEXT) instead of freeing it, so a cached pointer is never dangling between the
// epoch bump and the next resolution.
static Vector<NativeContextTuple*>& NativeContexts() {
static Vector<NativeContextTuple*>* contexts = new Vector<NativeContextTuple*>();
return *contexts;
}
static Vector<NativeSessionState*>& NativeSessions() {
static Vector<NativeSessionState*>* sessions = new Vector<NativeSessionState*>();
return *sessions;
}
static std::atomic<NativeContextKeyResolver> g_nativeContextKeyResolver{nullptr};
// A one-way override for a case that drives the registry with no session runtime installed:
// when `t_forcedNativeKeyValid` is set, the key is the forced pair instead of the probe's
// answer. The setter clears the active-tuple cache, which is keyed on the effective key.
thread_local Bool t_forcedNativeKeyValid = false;
thread_local Uint64 t_forcedNativeSession = 0;
thread_local Uint64 t_forcedNativeToken = 0;
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver) {
g_nativeContextKeyResolver.store(resolver, std::memory_order_release);
}
static void ResolveNativeKey(Uint64& outSession, Uint64& outToken) {
if (t_forcedNativeKeyValid) {
outSession = t_forcedNativeSession;
outToken = t_forcedNativeToken;
return;
}
outSession = 0;
outToken = 0;
const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire);
if (resolver != nullptr) (void)resolver(&outSession, &outToken);
}
Uint64 CurrentNativeSessionKey() {
Uint64 session = 0;
Uint64 token = 0;
ResolveNativeKey(session, token);
return session;
}
namespace {
// The active tuple for this thread. The fast path is a key compare against a thread-local
// cache, because IsBackendContextCurrentOnThisThread sits under every draw; the slow path
// (a first call, a bind_context switch, or any registry mutation) walks the registry, which
// holds one tuple per live client context.
thread_local NativeContextTuple* t_activeNativeTuple = nullptr;
thread_local Uint64 t_activeNativeEpoch = 0;
thread_local Uint64 t_activeNativeSession = 0;
thread_local Uint64 t_activeNativeToken = 0;
// The native context THIS THREAD last made current. An EGLContext can be current on only one
// thread, and MakeCurrent is the only writer, so a plain thread-local is exact - and it is
// what makes MakeNativeContextCurrentForBoundToken a no-op when the bind did not actually
// change the native context (the single-context world's every bind).
thread_local EGLContext t_boundNativeContext = EGL_NO_CONTEXT;
} // namespace
static NativeSessionState& AcquireNativeSession(Uint64 sessionKey) {
for (auto* session : NativeSessions()) {
if (session->SessionKey == sessionKey) return *session;
}
auto* created = new NativeSessionState{};
created->SessionKey = sessionKey;
NativeSessions().push_back(created);
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
return *created;
}
static NativeSessionState& ActiveNativeSession() {
Uint64 session = 0;
Uint64 token = 0;
ResolveNativeKey(session, token);
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
return AcquireNativeSession(session);
}
// The tuple the calling thread works on, created on first use. Lazy creation is what keeps the
// pre-S4 shapes working unchanged: a session that never crossed a CreateContext - the whole
// single-context world, and every existing fixture - resolves to its token-0 tuple and gets
// exactly the one native context it always had.
static NativeContextTuple& ActiveNativeContext() {
Uint64 session = 0;
Uint64 token = 0;
ResolveNativeKey(session, token);
const Uint64 epoch = g_nativeRegistryEpoch.load(std::memory_order_acquire);
if (t_activeNativeTuple != nullptr && t_activeNativeEpoch == epoch &&
t_activeNativeSession == session && t_activeNativeToken == token) {
return *t_activeNativeTuple;
}
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
NativeContextTuple* found = nullptr;
for (auto* tuple : NativeContexts()) {
if (tuple->SessionKey == session && tuple->ContextToken == token) {
found = tuple;
break;
}
}
if (found == nullptr) {
found = new NativeContextTuple{};
found->SessionKey = session;
found->ContextToken = token;
NativeContexts().push_back(found);
(void)AcquireNativeSession(session);
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
}
t_activeNativeTuple = found;
t_activeNativeEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire);
t_activeNativeSession = session;
t_activeNativeToken = token;
return *found;
}
static NativeContextTuple* FindNativeContext(Uint64 sessionKey, Uint64 contextToken) {
for (auto* tuple : NativeContexts()) {
if (tuple->SessionKey == sessionKey && tuple->ContextToken == contextToken) return tuple;
}
return nullptr;
}
Uint64 NativeContextCount() {
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
Uint64 count = 0;
for (auto* tuple : NativeContexts()) {
if (tuple->Context != EGL_NO_CONTEXT) ++count;
}
return count;
}
Uint64 NativeContextCountForSession(Uint64 sessionKey) {
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
Uint64 count = 0;
for (auto* tuple : NativeContexts()) {
if (tuple->SessionKey == sessionKey && tuple->Context != EGL_NO_CONTEXT) ++count;
}
return count;
}
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken) {
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
const NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken);
return tuple != nullptr ? tuple->Context : EGL_NO_CONTEXT;
}
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken) {
t_forcedNativeSession = sessionKey;
t_forcedNativeToken = contextToken;
t_forcedNativeKeyValid = true;
// The cache is keyed on the effective key, so drop it rather than let a stale entry answer.
t_activeNativeTuple = nullptr;
t_activeNativeEpoch = 0;
}
// The display's one-time initialization. Returns false only when there is no EGL to talk to.
static Bool EnsureDisplayInitialized() {
if (g_Display != EGL_NO_DISPLAY) return true;
if (!g_EGLFuncs.eglGetDisplay) return false;
g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (g_Display == EGL_NO_DISPLAY) return false;
if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) {
g_Display = EGL_NO_DISPLAY;
return false;
}
if (g_EGLFuncs.eglBindAPI) g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API);
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window), D3: true only while InitWindowSurface publishes the default // P12 (on-screen server window), D3: true only while InitWindowSurface publishes the default
@@ -16195,14 +16410,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) { static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) {
outWidth = 0; outWidth = 0;
outHeight = 0; outHeight = 0;
if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) { // P14 S4: the surface is the SESSION's, not the process's - the tuple that draws to it is
// one of the session's contexts.
const EGLSurface surface = ActiveNativeSession().Draw;
if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) {
return false; return false;
} }
EGLint width = 0; EGLint width = 0;
EGLint height = 0; EGLint height = 0;
if (!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_WIDTH, &width) || if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &width) ||
!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_HEIGHT, &height) || !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height) ||
width <= 0 || height <= 0) { width <= 0 || height <= 0) {
return false; return false;
} }
@@ -16273,15 +16491,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif #endif
// A driver that refuses the query leaves both at 0. Fall back to what the EGL config // A driver that refuses the query leaves both at 0. Fall back to what the EGL config
// was chosen with, which is what the surface actually has. // was chosen with, which is what the surface actually has. P14 S4: the config belongs to the
// context the surface is bound to, so it is the active tuple's.
const EGLConfig activeConfig = ActiveNativeContext().Config;
if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY && if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY &&
g_Config != nullptr) { activeConfig != nullptr) {
EGLint eglDepth = 0; EGLint eglDepth = 0;
EGLint eglStencil = 0; EGLint eglStencil = 0;
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, g_Config, EGL_DEPTH_SIZE, &eglDepth)) { if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_DEPTH_SIZE, &eglDepth)) {
depthBits = static_cast<GLint>(eglDepth); depthBits = static_cast<GLint>(eglDepth);
} }
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, g_Config, EGL_STENCIL_SIZE, &eglStencil)) { if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_STENCIL_SIZE, &eglStencil)) {
stencilBits = static_cast<GLint>(eglStencil); stencilBits = static_cast<GLint>(eglStencil);
} }
} }
@@ -16291,9 +16511,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// PREVIOUS surface published in place, which would be stale - this function runs // PREVIOUS surface published in place, which would be stale - this function runs
// once per surface activation, not once per process. It is written this way anyway // once per surface activation, not once per process. It is written this way anyway
// because the branch is unreachable for a surface MobileGL chose itself: // because the branch is unreachable for a surface MobileGL chose itself:
// InitDisplayAndContext asks eglChooseConfig for EGL_DEPTH_SIZE 24 and // ChooseConfigForSurface asks eglChooseConfig for EGL_DEPTH_SIZE 24 and
// EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so g_Config always has both // EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so the context's config always
// and the EGL fallback above always answers. A caller that supplies its own // has both and the EGL fallback above always answers. A caller that supplies its own
// depth-less config would keep the previous surface's description; publishing a // depth-less config would keep the previous surface's description; publishing a
// guess instead would be a different lie, and the placeholder attachment model has // guess instead would be a different lie, and the placeholder attachment model has
// no way to say "this buffer does not exist" short of detaching it. // no way to say "this buffer does not exist" short of detaching it.
@@ -16565,32 +16785,54 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true; return true;
} }
static Bool InitDisplayAndContext(EGLint surfaceBit, NativeWindowType window = static_cast<NativeWindowType>(0)) { // P14 S4. THE CONTEXT FOR `tuple`, AND THE SURFACE'S CONFIG, WITH NO TEARDOWN IN FRONT.
DestroyEGLContext(); //
// The S4 deletion is the first statement this function used to begin with: `DestroyEGLContext()`
// ran UNCONDITIONALLY, so creating a second surface (let alone a second context) destroyed the
// native context the first one's objects lived in. A tuple that already has a native context
// keeps it and only (re)chooses the config the new surface is created with; a tuple that has
// none builds one, sharing with the group leader when CreateNativeContextFor named a group.
//
// A CONFIG THAT IS ALREADY SET IS NOT RE-CHOSEN FOR A DIFFERENT SURFACE KIND, and that is the
// deliberate trade: an EGLSurface's config must be compatible with the context's, so re-choosing
// would mean recreating the context - exactly the teardown this slice deletes. A driver that
// refuses the resulting eglCreate*Surface refuses it by name, which is the honest failure.
// P14 S4. THE BRING-UP DIAGNOSIS, in one place because BOTH surface paths owe it (it used to be
// the first statement of InitDisplayAndContext, which both went through). Extracted rather than
// duplicated when that function was replaced by the per-tuple EnsureNativeContext.
//
// Diagnose, and deliberately NOT resolve: resolving raises
// Fatal{PipeLegacyMemosDisabled}, and this function runs inside eglMakeCurrent, which
// the integration harness pre-flights in a FORKED CHILD
// (MG_IntegrationTest/Harness/HeadlessGL.cpp). A child that dies on a signal is reported
// to the parent as "no usable GPU/display/ICD" and every scenario in the lane is
// SKIPPED - so the stop became a green lane that ran nothing, on exactly the two env
// vars the D14/D18 A/B is driven with (ROADMAP.md:7). The stop now belongs to the first
// twin lookup, which happens in a scenario body where a crash IS a test failure.
static void DiagnoseEsprytSlotArmAtBringUp() {
#if MOBILEGL_PIPE_PUSH #if MOBILEGL_PIPE_PUSH
// DIAGNOSE the twin-table arm here, at backend startup, so an operator who set
// MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_LEGACY_MEMOS into a combination that leaves no
// arm at all is told so by name, in the log, before the first draw.
//
// Diagnose, and deliberately NOT resolve: resolving raises
// Fatal{PipeLegacyMemosDisabled}, and this function runs inside eglMakeCurrent, which
// the integration harness pre-flights in a FORKED CHILD
// (MG_IntegrationTest/Harness/HeadlessGL.cpp). A child that dies on a signal is reported
// to the parent as "no usable GPU/display/ICD" and every scenario in the lane is
// SKIPPED - so the stop became a green lane that ran nothing, on exactly the two env
// vars the D14/D18 A/B is driven with (ROADMAP.md:7). The stop now belongs to the first
// twin lookup, which happens in a scenario body where a crash IS a test failure.
DiagnoseEsprytSlotArm(); DiagnoseEsprytSlotArm();
#endif #endif
}
g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY); static Bool EnsureNativeContext(NativeContextTuple& tuple, EGLint surfaceBit, NativeWindowType window) {
if (g_Display == EGL_NO_DISPLAY) return false; if (!EnsureDisplayInitialized()) return false;
if (tuple.Context != EGL_NO_CONTEXT) return true;
if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) return false; if (tuple.Config == nullptr && !ChooseConfigForSurface(surfaceBit, tuple.Config, window)) return false;
g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API);
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false; // A share group whose leader has a live native context hands it to eglCreateContext: this is
// the whole of "shared contexts see the same objects" on the server. The group is recorded
// by CreateNativeContextFor before this runs; a tuple nobody named a group for starts one.
EGLContext share = EGL_NO_CONTEXT;
for (auto* other : NativeContexts()) {
if (other != &tuple && other->SessionKey == tuple.SessionKey &&
other->Context != EGL_NO_CONTEXT && tuple.ShareGroupToken != 0 &&
other->ShareGroupToken == tuple.ShareGroupToken) {
share = other->Context;
break;
}
}
// Negotiate the highest ES 3.x context. Version-strict EGL implementations // Negotiate the highest ES 3.x context. Version-strict EGL implementations
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3 // (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
@@ -16601,15 +16843,83 @@ namespace MobileGL::MG_Backend::DirectGLES {
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, minorVersion, EGL_CONTEXT_MINOR_VERSION, minorVersion,
EGL_NONE}; EGL_NONE};
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs); tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs);
if (g_Context != EGL_NO_CONTEXT) { if (tuple.Context != EGL_NO_CONTEXT) {
++g_displayUsers;
return true; return true;
} }
} }
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs); tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs);
return g_Context != EGL_NO_CONTEXT; if (tuple.Context == EGL_NO_CONTEXT) return false;
++g_displayUsers;
return true;
}
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken) {
if (contextToken == 0) return false;
const Uint64 sessionKey = CurrentNativeSessionKey();
NativeContextTuple* tuple = nullptr;
{
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
AcquireNativeSession(sessionKey);
for (auto* candidate : NativeContexts()) {
if (candidate->SessionKey == sessionKey && candidate->ContextToken == contextToken) {
tuple = candidate;
break;
}
}
if (tuple == nullptr) {
tuple = new NativeContextTuple{};
tuple->SessionKey = sessionKey;
tuple->ContextToken = contextToken;
NativeContexts().push_back(tuple);
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
}
}
if (tuple->Context != EGL_NO_CONTEXT) return true; // idempotent
tuple->ShareGroupToken = shareGroupToken;
return EnsureNativeContext(*tuple, EGL_PBUFFER_BIT, static_cast<NativeWindowType>(0));
}
void DestroyNativeContextFor(Uint64 contextToken) {
const Uint64 sessionKey = CurrentNativeSessionKey();
if (contextToken == 0) return;
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken);
if (tuple == nullptr || tuple->Context == EGL_NO_CONTEXT) return;
if (g_EGLFuncs.eglDestroyContext && g_Display != EGL_NO_DISPLAY) {
// The context may be current on this very thread; unbinding first is what EGL requires
// (eglDestroyContext of a current context is EGL_BAD_ACCESS).
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() == tuple->Context &&
g_EGLFuncs.eglMakeCurrent) {
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context);
}
tuple->Context = EGL_NO_CONTEXT;
tuple->Config = nullptr;
tuple->Draw = EGL_NO_SURFACE;
tuple->Read = EGL_NO_SURFACE;
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
if (g_displayUsers > 0) --g_displayUsers;
if (g_displayUsers == 0 && g_Display != EGL_NO_DISPLAY && g_EGLFuncs.eglTerminate) {
g_EGLFuncs.eglTerminate(g_Display);
g_Display = EGL_NO_DISPLAY;
}
}
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. See DirectGLES.h; in one line, `bind_context`
// moves the session's context attribution and this makes the calling thread's native tuple
// follow it, binding the session's current surface to the newly named context's EGLContext.
Bool MakeNativeContextCurrentForBoundToken() {
NativeContextTuple& tuple = ActiveNativeContext();
// Nothing to switch to: a session that never sent CreateContext has no native context for
// this token, and building one here would bind a context the client does not know it has.
if (tuple.Context == EGL_NO_CONTEXT) return true;
if (t_boundNativeContext == tuple.Context) return true;
return MakeCurrent();
} }
namespace { namespace {
@@ -16620,7 +16930,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void ApplyRequestedSwapInterval() { void ApplyRequestedSwapInterval() {
if (g_requestedSwapInterval < 0) return; if (g_requestedSwapInterval < 0) return;
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) return; const EGLSurface surface = ActiveNativeSession().Draw;
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) return;
const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval); const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval);
MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval, MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
ok ? "ok" : "failed"); ok ? "ok" : "failed");
@@ -16639,10 +16950,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool InitWindowSurface(NativeWindowType window) { Bool InitWindowSurface(NativeWindowType window) {
if (!window) return false; if (!window) return false;
if (!InitDisplayAndContext(EGL_WINDOW_BIT, window)) return false; DiagnoseEsprytSlotArmAtBringUp();
g_Surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, g_Config, window, nullptr); NativeContextTuple& tuple = ActiveNativeContext();
if (g_Surface == EGL_NO_SURFACE) return false; if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return false;
const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr);
if (surface == EGL_NO_SURFACE) return false;
NativeSessionState& session = ActiveNativeSession();
session.Surfaces.push_back(surface);
session.Draw = surface;
session.Read = surface;
tuple.Draw = surface;
tuple.Read = surface;
if (!MakeCurrent()) return false; if (!MakeCurrent()) return false;
@@ -16659,25 +16979,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
PublishDefaultFramebufferDepthStencilFormat(); PublishDefaultFramebufferDepthStencilFormat();
#endif #endif
MGLOG_D("EGL context created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, g_Surface, MGLOG_D("EGL window surface created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display,
g_Context, window); surface, tuple.Context, window);
return true; return true;
} }
Bool InitPbufferSurface(EGLint width, EGLint height) { Bool InitPbufferSurface(EGLint width, EGLint height) {
if (width <= 0 || height <= 0) return false; if (width <= 0 || height <= 0) return false;
if (!InitDisplayAndContext(EGL_PBUFFER_BIT)) return false;
DiagnoseEsprytSlotArmAtBringUp();
NativeContextTuple& tuple = ActiveNativeContext();
if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast<NativeWindowType>(0))) return false;
const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
g_Surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, g_Config, surfaceAttribs); const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, surfaceAttribs);
if (g_Surface == EGL_NO_SURFACE) return false; if (surface == EGL_NO_SURFACE) return false;
NativeSessionState& session = ActiveNativeSession();
session.Surfaces.push_back(surface);
session.Draw = surface;
session.Read = surface;
tuple.Draw = surface;
tuple.Read = surface;
if (!MakeCurrent()) return false; if (!MakeCurrent()) return false;
PublishDefaultFramebufferDepthStencilFormat(); PublishDefaultFramebufferDepthStencilFormat();
MGLOG_D("EGL pbuffer context created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display, MGLOG_D("EGL pbuffer surface created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display,
g_Surface, g_Context, width, height); surface, tuple.Context, width, height);
return true; return true;
} }
@@ -16745,17 +17075,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidateEglVerifiedStamp(); void InvalidateEglVerifiedStamp();
Bool MakeCurrent() { Bool MakeCurrent() {
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE || // P14 S4: the context comes from the calling thread's tuple (the session's bound token),
g_Context == EGL_NO_CONTEXT) { // the surface from the session. Both halves used to be file-level singletons.
NativeContextTuple& tuple = ActiveNativeContext();
NativeSessionState& session = ActiveNativeSession();
tuple.Draw = session.Draw;
tuple.Read = session.Read;
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || session.Draw == EGL_NO_SURFACE ||
tuple.Context == EGL_NO_CONTEXT) {
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized"); MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
return false; return false;
} }
if (!g_EGLFuncs.eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context)) { if (!g_EGLFuncs.eglMakeCurrent(g_Display, session.Draw, session.Draw, tuple.Context)) {
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error); MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
return false; return false;
} }
InvalidateEglVerifiedStamp(); InvalidateEglVerifiedStamp();
t_boundNativeContext = tuple.Context;
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
// The ops table may have been unregistered when a previous ES context was // The ops table may have been unregistered when a previous ES context was
// destroyed (e.g. a probe context); re-register now that GL is usable. // destroyed (e.g. a probe context); re-register now that GL is usable.
@@ -16798,6 +17135,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// legally arrive on a thread other than the current owner); erring towards // legally arrive on a thread other than the current owner); erring towards
// "not current" only defers buffer ops, which is always safe. // "not current" only defers buffer ops, which is always safe.
InvalidateEglVerifiedStamp(); InvalidateEglVerifiedStamp();
t_boundNativeContext = EGL_NO_CONTEXT;
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
return true; return true;
} }
@@ -16828,7 +17166,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
Bool IsBackendContextCurrentOnThisThread() { Bool IsBackendContextCurrentOnThisThread() {
if (g_Context == EGL_NO_CONTEXT) { const NativeContextTuple& tuple = ActiveNativeContext();
if (tuple.Context == EGL_NO_CONTEXT) {
return false; return false;
} }
if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) { if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) {
@@ -16846,7 +17185,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) { g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) {
return true; return true;
} }
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) { if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != tuple.Context) {
return false; return false;
} }
g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed); g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed);
@@ -17263,7 +17602,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial}; slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial};
} }
g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface); // P14 S4: the session's surface, not the process's.
g_EGLFuncs.eglSwapBuffers(g_Display, ActiveNativeSession().Draw);
if (canFence && g_GLESFuncs.glGetSynciv) { if (canFence && g_GLESFuncs.glGetSynciv) {
// Fences signal in submission order within one context, so the highest // Fences signal in submission order within one context, so the highest
@@ -17297,6 +17637,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
// P14 S4: THE FULL TEARDOWN, SCOPED TO THE CALLING THREAD'S SESSION.
//
// This used to destroy the process's one context, its one surface and terminate the display.
// The callers (ReleaseEGLResources, ~BackendObject_DirectGLES) mean "this session's EGL world
// goes away", so under S4 it destroys EVERY native context that session holds, its surfaces, and
// terminates the display only when no context anywhere in the process is left - which is what
// keeps a second live session (S2) serving while this one closes. With no resolver installed
// there is exactly one session (key 0) and this is the old statement, byte for word.
//
// ONE CONTEXT's teardown is DestroyNativeContextFor (the DestroyContext control frame); this is
// the session's.
void DestroyEGLContext() { void DestroyEGLContext() {
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
@@ -17328,16 +17679,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (FrameFence& slot : g_frameFenceRing) slot = {}; for (FrameFence& slot : g_frameFenceRing) slot = {};
g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed), g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed),
std::memory_order_relaxed); std::memory_order_relaxed);
if (g_Display != EGL_NO_DISPLAY) {
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); const Uint64 sessionKey = CurrentNativeSessionKey();
if (g_Context != EGL_NO_CONTEXT) { if (g_Display == EGL_NO_DISPLAY) return;
g_EGLFuncs.eglDestroyContext(g_Display, g_Context); {
g_Context = EGL_NO_CONTEXT; const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
if (g_EGLFuncs.eglMakeCurrent) {
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
} }
if (g_Surface != EGL_NO_SURFACE) { for (auto* tuple : NativeContexts()) {
g_EGLFuncs.eglDestroySurface(g_Display, g_Surface); if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue;
g_Surface = EGL_NO_SURFACE; if (g_EGLFuncs.eglDestroyContext) g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context);
tuple->Context = EGL_NO_CONTEXT;
tuple->Config = nullptr;
tuple->Draw = EGL_NO_SURFACE;
tuple->Read = EGL_NO_SURFACE;
if (g_displayUsers > 0) --g_displayUsers;
} }
for (auto* session : NativeSessions()) {
if (session->SessionKey != sessionKey) continue;
if (g_EGLFuncs.eglDestroySurface) {
for (const EGLSurface surface : session->Surfaces) {
if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(g_Display, surface);
}
}
session->Surfaces.clear();
session->Draw = EGL_NO_SURFACE;
session->Read = EGL_NO_SURFACE;
}
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
}
// THE LAST CONTEXT ANYWHERE IN THE PROCESS IS WHAT TERMINATES THE DISPLAY. A session that
// closes while another is rendering must not take eglTerminate with it.
if (g_displayUsers == 0 && g_EGLFuncs.eglTerminate) {
g_EGLFuncs.eglTerminate(g_Display); g_EGLFuncs.eglTerminate(g_Display);
g_Display = EGL_NO_DISPLAY; g_Display = EGL_NO_DISPLAY;
} }
@@ -204,6 +204,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs); void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs); void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities); void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
// ---------------------------------------------------------------------------------
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
// ---------------------------------------------------------------------------------
//
// The display is process state and stays one object (eglInitialize once per process, terminated
// when the last native context leaves). The native EGLContext, the config it was created with
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
// the first one's native context down, which is what lets two contexts (and two share groups)
// be live at once in one process.
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
// that session has bound. Installed once by the server layer; with no resolver installed -
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
// exactly the process singleton this replaces. `false` also means {0,0}.
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
// Builds the native context for `contextToken` under the calling thread's session. A
// `shareGroupToken` whose group already has a live native context in this session hands that
// context to eglCreateContext as the share argument - which is what makes two contexts see one
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
// contexts are untouched; the display is terminated only when the last context in the process
// has gone.
void DestroyNativeContextFor(Uint64 contextToken);
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
// records belong to; this makes the calling thread's native tuple follow it, binding the
// session's current surface to the newly named context's native EGLContext. A no-op when the
// named context has no native context yet (the session never sent CreateContext) or when the
// native context already current on this thread IS that one - so the single-context world pays
// nothing for it. Called from the bind_context path on the apply thread.
Bool MakeNativeContextCurrentForBoundToken();
// How many native contexts this process holds, and how many of them belong to one session. The
// test observables for "a second context did not tear the first one down".
Uint64 NativeContextCount();
Uint64 NativeContextCountForSession(Uint64 sessionKey);
// The calling thread's session, as the resolver above reports it (0 with no resolver).
Uint64 CurrentNativeSessionKey();
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
// hold" query and no way to ask a context for its share group, so a case that must tell "the
// same native context still serves" from "a fresh one was built", or a sharing pair from two
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
// a production path and neither changes behaviour.
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
// tuple it means when no session runtime is installed.
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
void DestroyEGLContext(); void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The // Transform feedback capture spans, performed by the real ES driver. The
+46 -1
View File
@@ -820,6 +820,10 @@ namespace MobileGL::MG_Pipe {
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline // The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
// variable: no .cpp is needed for the definition. // variable: no .cpp is needed for the definition.
// //
// P14 S2: IT IS THE PROCESS-WIDE BLOCK, and `gPipeInputs` below is the name every reader
// spells - now one reference per THREAD, resolving to a served session's own block when the
// calling thread belongs to one. See the block after it.
//
// LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for // LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for
// pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply // pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply
// leaks the global singletons to the OS instead of running destructors during static // leaks the global singletons to the OS instead of running destructors during static
@@ -834,7 +838,48 @@ namespace MobileGL::MG_Pipe {
// about to be. So the reference is never dropped: nothing here can start such a chain. // about to be. So the reference is never dropped: nothing here can start such a chain.
// A live context releases these SharedPtrs the ordinary way, at the fill point. // A live context releases these SharedPtrs the ordinary way, at the fill point.
// (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.) // (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.)
inline PipeInputs& gPipeInputs = *new PipeInputs(); inline PipeInputs& gPipeInputsDefault = *new PipeInputs();
// ============================================================================
// P14 S2: WHOSE BLOCK IS `gPipeInputs`?
// ============================================================================
//
// ONE PROCESS, SEVERAL SERVED SESSIONS. The applier writes this block and the backend reads it
// through MGB_CTX, both on the SAME apply thread - the block IS "the state the record in hand
// carries". Two sessions applying concurrently on two apply threads through one block would
// read each other's render state, which is the cross-talk a multi-session server must not
// have. Nothing about the block's contents changed; what changed is that the name resolves to
// the SESSION'S block on a thread that belongs to one.
//
// WHY A THREAD-LOCAL REFERENCE, AND NOT SOMETHING CLEVERER. MGB_CTX is
// `(&::MobileGL::MG_Pipe::gPipeInputs)` (MG_Pipe/PipeInputsSwitch.h) and is spelled at ~380
// sites in MG_Backend/MG_Impl; a reference whose TARGET varies with the calling thread is the
// only shape that keeps all of them compiling and all of them correct. The cost is one TLS
// address computation per access site - not one per field, and with no guard: the reference is
// bound once per thread on its first use.
//
// THE RESOLVER IS A HOOK because this header may not know about sessions (MG_Remote/Server
// owns them and includes THIS header, not the other way round). It is installed once by the
// server's session registry and never replaced; null - the client process, the pull build, a
// unit case - makes every thread resolve to gPipeInputsDefault, which is the old behaviour
// byte for byte.
//
// BINDING IS PER THREAD AND HAPPENS AT FIRST USE, so a thread must be inside its session's
// scope before it touches the block. Both of a session's threads are: the control thread opens
// its scope before it reads a byte of the Hello, and the apply thread opens it as its first
// act (ServerLoop::ApplyThreadMain).
using PipeInputsThreadResolver = PipeInputs* (*)();
inline PipeInputsThreadResolver g_pipeInputsThreadResolver = nullptr;
inline PipeInputs& MGPipeServerInputsForCallingThread() {
const PipeInputsThreadResolver resolve = g_pipeInputsThreadResolver;
if (resolve != nullptr) {
if (PipeInputs* const block = resolve()) return *block;
}
return gPipeInputsDefault;
}
inline thread_local PipeInputs& gPipeInputs = MGPipeServerInputsForCallingThread();
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
// ============================================================================ // ============================================================================
+104 -2
View File
@@ -17,6 +17,13 @@
// MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's // MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's
// translation unit is unchanged (G1). // translation unit is unchanged (G1).
#include <Config.h> #include <Config.h>
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The EGL context lifecycle's
// client half: eglCreateContext/eglDestroyContext cross as two control frames, and a
// context switch emits the in-band bind_context record. Reached through MG_Remote::Client
// because that is the seam MG_Impl already uses for its other wire producers
// (PipeFill.cpp's emitters) and because the frame channel belongs to the server role - this
// file must not name ServerLoop's forwarders directly.
#include <MG_Remote/Client/WireTables.h>
#endif #endif
#include <mutex> #include <mutex>
#include <sstream> #include <sstream>
@@ -52,6 +59,35 @@ namespace MobileGL::MG_Impl::EGLImpl {
return backendObject; return backendObject;
} }
#if MOBILEGL_BUILD_DISAGGREGATED
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE CONTEXT BINDING, AND
// THE ONE PLACE IT IS EMITTED.
//
// `bind_context` is the ring's half of eglMakeCurrent: the MakeCurrent control frame
// tells the server which native tuple to hold, and this record tells it which CONTEXT
// every record after it belongs to - the attribution the session table was built for.
//
// IT IS EMITTED AFTER THE SWITCH HAS SUCCEEDED AND BEFORE eglMakeCurrent RETURNS, and
// that is the whole ordering argument: the app thread cannot publish a record of the
// new context until this call has returned, and the ring is FIFO, so the binding is
// behind no verb of the context it names and ahead of every one that follows. Waiting
// for the first verb instead would need the emitter to know which context the tracker
// is about to prime; this needs only the token EGLState already holds.
//
// An unchanged token emits NOTHING: an identical eglMakeCurrent is a legal no-op in
// EGL and the session is already bound to that context.
void EmitContextBinding(EGLStateContext* state, EGLContext switched, Uint64 previousToken) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// The apply thread IS the server in this process; a record emitted there would
// wait on the thread that has to apply it (EmitAndWait), which is a deadlock and
// not a slow path.
if (MG_Remote::Client::RunsAsTheServerRole()) return;
const Uint64 token = switched == EGL_NO_CONTEXT ? 0 : state->GetContextClientToken(switched);
if (token == previousToken) return;
(void)MG_Remote::Client::EmitBindContextRecord(token);
}
#endif
std::recursive_mutex& EGLOperationMutex() { std::recursive_mutex& EGLOperationMutex() {
static std::recursive_mutex mutex; static std::recursive_mutex mutex;
return mutex; return mutex;
@@ -329,7 +365,35 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) { if (!state) {
return EGL_NO_CONTEXT; return EGL_NO_CONTEXT;
} }
return state->CreateContext(dpy, config, shareCtx, attrib_list); const EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
if (context == EGL_NO_CONTEXT) {
return EGL_NO_CONTEXT;
}
#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
// derived into (the sharing context's group, or a fresh one), and the server tables it
// here through the same blocking handshake the surface forwarders use - so the first
// bind_context after this call names a context the session already has, and the
// sink's "this session never created that token" refusal is unreachable on the
// well-behaved path.
//
// A REFUSAL DROPS THE CLIENT'S CONTEXT AGAIN. Keeping it would hand the application a
// handle whose first verb the server refuses - a late failure where an early one is
// available, and EGL_NO_CONTEXT with EGL_BAD_ACCESS is the early one. NoWire (the
// pre-Start bring-up, the server role) keeps it: there is no server to refuse.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Client::RunsAsTheServerRole()) {
const auto emitted = MG_Remote::Client::SendCreateContextFrame(
state->GetContextClientToken(context), state->GetContextShareGroupToken(context), 0);
if (emitted == MG_Remote::Client::ContextFrameEmit::Refused) {
state->DestroyContext(dpy, context);
state->SetError(EGL_BAD_ACCESS);
return EGL_NO_CONTEXT;
}
}
#endif
return context;
} }
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) { EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
@@ -380,6 +444,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
const auto oldDraw = state->GetCurrentSurface(EGL_DRAW); const auto oldDraw = state->GetCurrentSurface(EGL_DRAW);
const auto oldRead = state->GetCurrentSurface(EGL_READ); const auto oldRead = state->GetCurrentSurface(EGL_READ);
const auto oldContext = state->GetCurrentContext(); const auto oldContext = state->GetCurrentContext();
#if MOBILEGL_BUILD_DISAGGREGATED
// P14 S1. The context the calling thread had current BEFORE this call, as the wire
// knows it. Read here because `state->MakeCurrent` below has already committed the
// new one by the time the emission points are reached.
const Uint64 oldContextToken = state->GetContextClientToken(oldContext);
#endif
const String threadId = CurrentThreadIdString(); const String threadId = CurrentThreadIdString();
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p", MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
@@ -404,6 +474,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
} }
MGLOG_D("eglMakeCurrent release succeeded thread=%s", threadId.c_str()); MGLOG_D("eglMakeCurrent release succeeded thread=%s", threadId.c_str());
#if MOBILEGL_BUILD_DISAGGREGATED
EmitContextBinding(state, EGL_NO_CONTEXT, oldContextToken);
#endif
return EGL_TRUE; return EGL_TRUE;
} }
@@ -422,6 +495,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
MGLOG_D("eglMakeCurrent attach succeeded thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), dpy, draw, MGLOG_D("eglMakeCurrent attach succeeded thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), dpy, draw,
read, ctx); read, ctx);
#if MOBILEGL_BUILD_DISAGGREGATED
EmitContextBinding(state, ctx, oldContextToken);
#endif
return EGL_TRUE; return EGL_TRUE;
} }
@@ -430,7 +506,29 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) { if (!state) {
return EGL_FALSE; return EGL_FALSE;
} }
return state->DestroyContext(dpy, ctx) ? EGL_TRUE : EGL_FALSE; #if MOBILEGL_BUILD_DISAGGREGATED
// 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.
const Uint64 contextToken = state->GetContextClientToken(ctx);
#endif
if (!state->DestroyContext(dpy, ctx)) {
return EGL_FALSE;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (contextToken != 0 && MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Client::RunsAsTheServerRole()) {
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));
}
}
#endif
return EGL_TRUE;
} }
EGLBoolean DestroySurface(EGLDisplay dpy, EGLSurface surface) { EGLBoolean DestroySurface(EGLDisplay dpy, EGLSurface surface) {
@@ -468,6 +566,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
// re-initializes lazily via GetStateEnsureInitialized(); process exit // re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy. // then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) { if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
// P14 S3: the process default context (and with it the GL objects the frontend
// built while no EGL context was current) goes before Destroy() does, so nothing
// it owns outlives glslang::FinalizeProcess() through a thread's fallback binding.
MG_State::ReleaseProcessDefaultGLContext();
MobileGL::Destroy(); MobileGL::Destroy();
} }
return EGL_TRUE; return EGL_TRUE;
+4
View File
@@ -48,6 +48,10 @@ namespace MobileGL::MG_Impl {
MakeUnique<GLImpl::FramebufferImpl::DefaultFramebufferInfo>(fbo0, colorTex, depthTex, stencilTex); MakeUnique<GLImpl::FramebufferImpl::DefaultFramebufferInfo>(fbo0, colorTex, depthTex, stencilTex);
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(fbo0); MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(fbo0);
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(fbo0); MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(fbo0);
// P14 S3: every GLContext a later eglCreateContext builds starts with this same default
// framebuffer, so a context the application creates after this one is not left with an
// unbound framebuffer 0.
MG_State::SetDefaultFramebuffer(fbo0);
#if defined(__APPLE__) && !defined(MOBILEGL_IOS) #if defined(__APPLE__) && !defined(MOBILEGL_IOS)
NSOpenGLImpl::InstallHooks(); NSOpenGLImpl::InstallHooks();
#endif #endif
+13
View File
@@ -1966,6 +1966,19 @@ namespace MobileGL::MG_Pipe {
// object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption, // object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption,
// "ObjectDeath.Handle"} at the sink. // "ObjectDeath.Handle"} at the sink.
// bind_context = opcode 84, P14 S1 (docs/Disaggregated/design/11-state-ownership.md).
// The client's CURRENT context token, minted by EGLState::CreateContext and carried to the
// server by the matching CreateContext control frame. 0 means "no context": a release, and
// it is the value a session that never bound one already has - so the single-context shape
// P5c shipped (one context per session, every record implicitly its own) needs no record at
// all and no server change. The token is Uint64 because the control frame's is: one width
// on both planes, or the two would have to be narrowed at one end.
struct MGPBindContext {
Uint64 ClientContextToken;
};
MGP_ASSERT_POD(MGPBindContext, 8);
// set_context_values = opcode 79, P5c rv (CONTRACT-P5C.md §5.3): the rv field table as // set_context_values = opcode 79, P5c rv (CONTRACT-P5C.md §5.3): the rv field table as
// ONE fixed-width POD. It carries every value-class field that no set_* call supplies - // ONE fixed-width POD. It carries every value-class field that no set_* call supplies -
// the two texture-unit counters, the per-target touched-buffer-binding high-water marks // the two texture-unit counters, the per-target touched-buffer-binding high-water marks
+349 -186
View File
@@ -477,11 +477,173 @@ namespace MobileGL::MG_Pipe {
static_assert(kCapabilityCount <= 64, static_assert(kCapabilityCount <= 64,
"ResidualValueBlock::CapabilityBits is a Uint64; 35 bits fit, 65 would not"); "ResidualValueBlock::CapabilityBits is a Uint64; 35 bits fit, 65 would not");
// ============================================================================
// P14 S5: THE APPLIER REGISTRY.
// ============================================================================
//
// A REFERENCE TO A NEVER-DESTROYED BLOCK, for MGPipeSlots()' reason // A REFERENCE TO A NEVER-DESTROYED BLOCK, for MGPipeSlots()' reason
// (MG_Impl/Pipe/SlotAllocator.cpp): resource_destroy and delete_vertex_elements are // (MG_Impl/Pipe/SlotAllocator.cpp): resource_destroy and delete_vertex_elements are
// raised from ~BufferObject / ~VertexArrayObject, and those objects are released by // raised from ~BufferObject / ~VertexArrayObject, and those objects are released by
// exit handlers that run after this translation unit's own globals are gone. // exit handlers that run after this translation unit's own globals are gone.
MGPipeApplierState& g_applier = *new MGPipeApplierState{}; //
// THE DEFAULT IS THE OLD SINGLETON, unrenamed in what it means: the applier of a thread
// that belongs to nobody - the client process, the monolith, the inproc one-process
// shape, every unit case. Its object records are the group it owns alone.
MGPipeObjectRecords& g_defaultObjects = *new MGPipeObjectRecords{};
MGPipeApplierState& g_defaultApplier = *new MGPipeApplierState(g_defaultObjects);
// Installed once by the session registry (MG_Remote/Server/SessionRuntime.cpp), never
// replaced. Null in every process that serves no session, which is what keeps the
// monolith's `MGPipeApplier()` a load and a branch.
MGPipeApplierKeyResolver g_applierKeyResolver = nullptr;
// One served session's appliers. Groups own object records; Contexts own working state
// and point at their group. Both are held through UniquePtr so a table's ADDRESS is
// stable across a rehash - MGPipeApplierState binds its group by reference - and the
// maps are std::unordered_map rather than the house UnorderedMap (a flat hash map) for
// that same reason at the map level: a reference into it may not be invalidated by an
// insert somewhere else in the same call.
struct ApplierSession {
std::unordered_map<Uint64, UniquePtr<MGPipeObjectRecords>> Groups;
std::unordered_map<Uint64, UniquePtr<MGPipeApplierState>> Contexts;
std::unordered_map<Uint64, Uint64> ContextGroup;
std::unordered_map<Uint64, Uint32> GroupContexts;
};
std::unordered_map<Uint64, ApplierSession> g_applierSessions;
std::mutex g_applierSessionsMutex;
// Bumped by EVERY registry mutation. A thread's cached applier pointer is only trusted
// while it was bound at this generation, which is what makes "a context was destroyed on
// another thread" unable to leave a dangling cache behind.
std::atomic<Uint64> g_applierRegistryGeneration{1};
// A context that started its own share group is keyed by its own token, in a namespace
// of its own: the frontend mints share-group tokens and context tokens from separate
// counters, so without this a group 7 and a context 7 would be one group.
constexpr Uint64 kMGPipeOwnShareGroupBit = Uint64{1} << 63;
// `shareGroupToken` 0 means "this context started its own group" - the sentinel S1's
// ContextRuntime documents and the one a caller that frames a context by hand (a test,
// the in-process server helpers) leaves behind.
Uint64 ApplierShareGroupKey(Uint64 contextToken, Uint64 shareGroupToken) {
return shareGroupToken != 0 ? shareGroupToken : (contextToken | kMGPipeOwnShareGroupBit);
}
// The three tables of the applier this thread's records are being applied through.
// `generation` is the registry's, read here so a caller can bind a pointer under one
// generation and publish it under the same one.
MGPipeApplierState* FindApplierForKey(const MGPipeApplierKey& key) {
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
ApplierSession& session = g_applierSessions[key.SessionKey];
const auto found = session.Contexts.find(key.ContextToken);
if (found != session.Contexts.end() && found->second) return found->second.get();
// A context the session never registered - a token that predates the first
// CreateContext, or the 0 of a session that has not bound one yet. It gets the same
// shape as any other: a working state in a group of its own.
const Uint64 groupKey = ApplierShareGroupKey(key.ContextToken, 0);
auto group = session.Groups.find(groupKey);
if (group == session.Groups.end()) {
group = session.Groups.emplace(groupKey, MakeUnique<MGPipeObjectRecords>()).first;
}
auto applier = MakeUnique<MGPipeApplierState>(*group->second);
MGPipeApplierState* const raw = applier.get();
session.Contexts.emplace(key.ContextToken, std::move(applier));
session.ContextGroup.emplace(key.ContextToken, groupKey);
++session.GroupContexts[groupKey];
return raw;
}
thread_local MGPipeApplierState* t_applierCached = nullptr;
thread_local Uint64 t_applierCachedSession = 0;
thread_local Uint64 t_applierCachedContext = 0;
thread_local Uint64 t_applierCachedGeneration = 0;
} // namespace
MGPipeApplierState& MGPipeApplier() {
const MGPipeApplierKeyResolver resolve = g_applierKeyResolver;
if (resolve == nullptr) return g_defaultApplier;
MGPipeApplierKey key;
if (!resolve(&key)) return g_defaultApplier;
// THE FAST PATH, and the reason this is a cache rather than a lookup: this function is
// called from the backend per state read. A hit is three TLS loads, one atomic load and
// two compares; a miss is the registry, which only moves when a context is created,
// destroyed or made current.
const Uint64 generation = g_applierRegistryGeneration.load(std::memory_order_acquire);
if (t_applierCached != nullptr && t_applierCachedGeneration == generation &&
t_applierCachedSession == key.SessionKey && t_applierCachedContext == key.ContextToken) {
return *t_applierCached;
}
MGPipeApplierState* const applier = FindApplierForKey(key);
t_applierCached = applier;
t_applierCachedSession = key.SessionKey;
t_applierCachedContext = key.ContextToken;
t_applierCachedGeneration = generation;
return *applier;
}
void MGPipeSetApplierKeyResolver(MGPipeApplierKeyResolver resolver) { g_applierKeyResolver = resolver; }
void MGPipeApplierRegisterContext(Uint64 sessionKey, Uint64 contextToken, Uint64 shareGroupToken) {
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
ApplierSession& session = g_applierSessions[sessionKey];
const Uint64 groupKey = ApplierShareGroupKey(contextToken, shareGroupToken);
auto group = session.Groups.find(groupKey);
if (group == session.Groups.end()) {
group = session.Groups.emplace(groupKey, MakeUnique<MGPipeObjectRecords>()).first;
}
const auto existing = session.Contexts.find(contextToken);
if (existing != session.Contexts.end() && existing->second) {
// Idempotent: the same token registered twice keeps the state it already has. The
// session refuses a duplicate CreateContext before reaching here, so this arm is a
// caller that re-registers deliberately.
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
return;
}
session.Contexts.emplace(contextToken, MakeUnique<MGPipeApplierState>(*group->second));
session.ContextGroup[contextToken] = groupKey;
++session.GroupContexts[groupKey];
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
}
void MGPipeApplierUnregisterContext(Uint64 sessionKey, Uint64 contextToken) {
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
const auto sessionIt = g_applierSessions.find(sessionKey);
if (sessionIt == g_applierSessions.end()) return;
ApplierSession& session = sessionIt->second;
const auto context = session.Contexts.find(contextToken);
if (context == session.Contexts.end()) return;
const auto groupIt = session.ContextGroup.find(contextToken);
const Uint64 groupKey = groupIt != session.ContextGroup.end() ? groupIt->second : 0;
// The OBJECT RECORDS GO WITH THE LAST CONTEXT OF THE GROUP, which is the per-group
// spelling of MGPipeApplierReleaseObjectRecords' scope. A surviving context of the same
// group keeps every record - that is what sharing means.
const auto refs = session.GroupContexts.find(groupKey);
const Bool lastOfGroup = refs != session.GroupContexts.end() && refs->second <= 1;
session.Contexts.erase(context);
if (groupIt != session.ContextGroup.end()) session.ContextGroup.erase(groupIt);
if (lastOfGroup) {
if (refs != session.GroupContexts.end()) session.GroupContexts.erase(refs);
const auto group = session.Groups.find(groupKey);
if (group != session.Groups.end()) session.Groups.erase(group);
} else if (refs != session.GroupContexts.end()) {
--refs->second;
}
if (session.Contexts.empty()) g_applierSessions.erase(sessionIt);
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
}
void MGPipeApplierReleaseSession(Uint64 sessionKey) {
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
if (g_applierSessions.erase(sessionKey) == 0) return;
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
}
Uint64 MGPipeApplierShareGroupCountForTesting(Uint64 sessionKey) {
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
const auto sessionIt = g_applierSessions.find(sessionKey);
return sessionIt == g_applierSessions.end() ? 0 : static_cast<Uint64>(sessionIt->second.Groups.size());
}
namespace {
// The installed handle-shaped resource table. Null until a backend registers one, // The installed handle-shaped resource table. Null until a backend registers one,
// which is what makes the client half landable on its own: with nothing here every // which is what makes the client half landable on its own: with nothing here every
@@ -490,8 +652,8 @@ namespace MobileGL::MG_Pipe {
const MGPipeResourceOps* g_resourceOps = nullptr; const MGPipeResourceOps* g_resourceOps = nullptr;
MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) { MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) {
if (handle.Slot >= g_applier.RenderStateCsos.size()) return nullptr; if (handle.Slot >= MGPipeApplier().RenderStateCsos.size()) return nullptr;
MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[handle.Slot]; MGPipeRenderStateCsoRecord& record = MGPipeApplier().RenderStateCsos[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) return nullptr; if (!record.Live || record.Gen != handle.Gen) return nullptr;
return &record; return &record;
} }
@@ -527,15 +689,15 @@ namespace MobileGL::MG_Pipe {
// that is not live, or a handle whose generation has moved on because the slot was // that is not live, or a handle whose generation has moved on because the slot was
// recycled under it. FindCso above is the same three questions for the CSO store. // recycled under it. FindCso above is the same three questions for the CSO store.
MGPipeResourceRecord* FindResource(MGPipeHandle res) { MGPipeResourceRecord* FindResource(MGPipeHandle res) {
if (res.Slot >= g_applier.Resources.size()) return nullptr; if (res.Slot >= MGPipeApplier().Resources.size()) return nullptr;
MGPipeResourceRecord& record = g_applier.Resources[res.Slot]; MGPipeResourceRecord& record = MGPipeApplier().Resources[res.Slot];
if (!record.Live || record.Gen != res.Gen) return nullptr; if (!record.Live || record.Gen != res.Gen) return nullptr;
return &record; return &record;
} }
MGPipeVertexElementsRecord* FindVertexElements(MGPipeHandle cso) { MGPipeVertexElementsRecord* FindVertexElements(MGPipeHandle cso) {
if (cso.Slot >= g_applier.VertexElementsCsos.size()) return nullptr; if (cso.Slot >= MGPipeApplier().VertexElementsCsos.size()) return nullptr;
MGPipeVertexElementsRecord& record = g_applier.VertexElementsCsos[cso.Slot]; MGPipeVertexElementsRecord& record = MGPipeApplier().VertexElementsCsos[cso.Slot];
if (!record.Live || record.Gen != cso.Gen) return nullptr; if (!record.Live || record.Gen != cso.Gen) return nullptr;
return &record; return &record;
} }
@@ -562,15 +724,15 @@ namespace MobileGL::MG_Pipe {
// respecify or destroy an unrelated LIVE object that happens to hold the same slot in // respecify or destroy an unrelated LIVE object that happens to hold the same slot in
// another kind's space, and that is the "act outside its own storage" class. // another kind's space, and that is the "act outside its own storage" class.
Vector<MGPipeResourceRecord>* ResourceTableForTarget(Uint8 target) { Vector<MGPipeResourceRecord>* ResourceTableForTarget(Uint8 target) {
if (target == kMGPipeResourceTargetBuffer) return &g_applier.Resources; if (target == kMGPipeResourceTargetBuffer) return &MGPipeApplier().Resources;
if (target == static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer)) { if (target == static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer)) {
return &g_applier.RenderbufferResources; return &MGPipeApplier().RenderbufferResources;
} }
if (target < static_cast<Uint8>(MGPipeResourceTarget::Count)) { if (target < static_cast<Uint8>(MGPipeResourceTarget::Count)) {
// Every remaining enumerator is a texture target, and they share one table // Every remaining enumerator is a texture target, and they share one table
// because they share one kind: MGPipeKind::Texture. Tex1D..TexCubeArray, // because they share one kind: MGPipeKind::Texture. Tex1D..TexCubeArray,
// Tex2DMS/MSArray, TexBuffer and TexRect are all one slot space. // Tex2DMS/MSArray, TexBuffer and TexRect are all one slot space.
return &g_applier.TextureResources; return &MGPipeApplier().TextureResources;
} }
return nullptr; return nullptr;
} }
@@ -579,11 +741,11 @@ namespace MobileGL::MG_Pipe {
Vector<MGPipeResourceRecord>* ResourceTableForKind(Uint32 kind) { Vector<MGPipeResourceRecord>* ResourceTableForKind(Uint32 kind) {
switch (static_cast<MGPipeKind>(kind)) { switch (static_cast<MGPipeKind>(kind)) {
case MGPipeKind::Buffer: case MGPipeKind::Buffer:
return &g_applier.Resources; return &MGPipeApplier().Resources;
case MGPipeKind::Texture: case MGPipeKind::Texture:
return &g_applier.TextureResources; return &MGPipeApplier().TextureResources;
case MGPipeKind::Renderbuffer: case MGPipeKind::Renderbuffer:
return &g_applier.RenderbufferResources; return &MGPipeApplier().RenderbufferResources;
default: default:
return nullptr; return nullptr;
} }
@@ -710,7 +872,7 @@ namespace MobileGL::MG_Pipe {
MGPipeResourceRecord* record = FindResource(res); MGPipeResourceRecord* record = FindResource(res);
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen, MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen,
kResourceRefusalNote); kResourceRefusalNote);
if (record == nullptr) ++g_applier.RefusedResourceCalls; if (record == nullptr) ++MGPipeApplier().RefusedResourceCalls;
return record; return record;
} }
@@ -723,7 +885,7 @@ namespace MobileGL::MG_Pipe {
MGPipeResourceRecord* record = FindIn(table, res); MGPipeResourceRecord* record = FindIn(table, res);
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen, MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen,
kResourceRefusalNote); kResourceRefusalNote);
if (record == nullptr) ++g_applier.RefusedResourceCalls; if (record == nullptr) ++MGPipeApplier().RefusedResourceCalls;
return record; return record;
} }
@@ -736,7 +898,7 @@ namespace MobileGL::MG_Pipe {
Record* record = FindIn(table, handle); Record* record = FindIn(table, handle);
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, handle.Slot, MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, handle.Slot,
handle.Gen, kResourceRefusalNote); handle.Gen, kResourceRefusalNote);
if (record == nullptr) ++g_applier.RefusedObjectCalls; if (record == nullptr) ++MGPipeApplier().RefusedObjectCalls;
return record; return record;
} }
@@ -744,7 +906,7 @@ namespace MobileGL::MG_Pipe {
MGPipeVertexElementsRecord* record = FindVertexElements(cso); MGPipeVertexElementsRecord* record = FindVertexElements(cso);
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen, MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
kResourceRefusalNote); kResourceRefusalNote);
if (record == nullptr) ++g_applier.RefusedVertexInputCalls; if (record == nullptr) ++MGPipeApplier().RefusedVertexInputCalls;
return record; return record;
} }
@@ -1198,7 +1360,7 @@ namespace MobileGL::MG_Pipe {
// hook; the monolith shape of everything above them is unchanged. // hook; the monolith shape of everything above them is unchanged.
Bool ApplyTextureUpload(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions) { Bool ApplyTextureUpload(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions) {
MGPipeResourceRecord* stored = MGPipeResourceRecord* stored =
ResolveResourceIn(g_applier.TextureResources, "resource_subdata", record.Res); ResolveResourceIn(MGPipeApplier().TextureResources, "resource_subdata", record.Res);
if (stored == nullptr) return false; if (stored == nullptr) return false;
const char* fault = SubDataTextureFault(record, regions); const char* fault = SubDataTextureFault(record, regions);
@@ -1319,28 +1481,28 @@ namespace MobileGL::MG_Pipe {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
MGPipeShaderCsoRecord* ShaderCsoRecordAt(Uint32 slot) { MGPipeShaderCsoRecord* ShaderCsoRecordAt(Uint32 slot) {
if (MGPipeIsCompositeShaderSlot(slot)) { if (MGPipeIsCompositeShaderSlot(slot)) {
return RecordAt(g_applier.CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase, return RecordAt(MGPipeApplier().CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase,
kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase); kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase);
} }
// The ordinary table is bounded by the BAND'S BASE and not by the slot limit: an // The ordinary table is bounded by the BAND'S BASE and not by the slot limit: an
// ordinary program can never be handed a band slot (the allocator refuses it), so a // ordinary program can never be handed a band slot (the allocator refuses it), so a
// slot at or above the base that is not a composite is out of range by definition. // slot at or above the base that is not a composite is out of range by definition.
return RecordAt(g_applier.ShaderCsos, slot, kMGPipeShaderCsoCompositeSlotBase); return RecordAt(MGPipeApplier().ShaderCsos, slot, kMGPipeShaderCsoCompositeSlotBase);
} }
MGPipeShaderCsoRecord* FindShaderCso(MGPipeHandle cso) { MGPipeShaderCsoRecord* FindShaderCso(MGPipeHandle cso) {
if (MGPipeIsCompositeShaderSlot(cso.Slot)) { if (MGPipeIsCompositeShaderSlot(cso.Slot)) {
return FindIn(g_applier.CompositeShaderCsos, return FindIn(MGPipeApplier().CompositeShaderCsos,
MGPipeHandle{cso.Slot - kMGPipeShaderCsoCompositeSlotBase, cso.Gen}); MGPipeHandle{cso.Slot - kMGPipeShaderCsoCompositeSlotBase, cso.Gen});
} }
return FindIn(g_applier.ShaderCsos, cso); return FindIn(MGPipeApplier().ShaderCsos, cso);
} }
MGPipeShaderCsoRecord* ResolveShaderCso(const char* call, MGPipeHandle cso) { MGPipeShaderCsoRecord* ResolveShaderCso(const char* call, MGPipeHandle cso) {
MGPipeShaderCsoRecord* record = FindShaderCso(cso); MGPipeShaderCsoRecord* record = FindShaderCso(cso);
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen, MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
kResourceRefusalNote); kResourceRefusalNote);
if (record == nullptr) ++g_applier.RefusedObjectCalls; if (record == nullptr) ++MGPipeApplier().RefusedObjectCalls;
return record; return record;
} }
@@ -1388,13 +1550,14 @@ namespace MobileGL::MG_Pipe {
#endif #endif
} // namespace } // namespace
MGPipeApplierState& MGPipeApplier() { return g_applier; } // MGPipeApplier() is defined with the registry at the top of this file: it is the one
// function that has to know whose applier the calling thread is in.
// P5c (rv, CONTRACT-P5C.md §5.3): the three texture shutters' server-side answers. See // P5c (rv, CONTRACT-P5C.md §5.3): the three texture shutters' server-side answers. See
// MGPipeApplierState::TextureShutterSerial / ContextSerial for the bump rule. // MGPipeApplierState::TextureShutterSerial / ContextSerial for the bump rule.
Uint64 MGPipeApplierTextureShutterSerial() { return g_applier.TextureShutterSerial; } Uint64 MGPipeApplierTextureShutterSerial() { return MGPipeApplier().TextureShutterSerial; }
Uint64 MGPipeApplierContextSerial() { return g_applier.ContextSerial; } Uint64 MGPipeApplierContextSerial() { return MGPipeApplier().ContextSerial; }
void MGPipeApplierNoteTextureStateMoved() { ++g_applier.TextureShutterSerial; } void MGPipeApplierNoteTextureStateMoved() { ++MGPipeApplier().TextureShutterSerial; }
void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; } void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; }
const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; } const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; }
@@ -1427,7 +1590,7 @@ namespace MobileGL::MG_Pipe {
} }
#endif #endif
if (g_resourceOps != nullptr) return false; if (g_resourceOps != nullptr) return false;
++g_applier.RefusedNoConsumer; ++MGPipeApplier().RefusedNoConsumer;
return true; return true;
} }
@@ -1438,11 +1601,11 @@ namespace MobileGL::MG_Pipe {
// zero" rule is written out. // zero" rule is written out.
void ClearShaderBufferWindows() { void ClearShaderBufferWindows() {
for (Uint32 cls = 0; cls < kMGPipeShaderBufferClassCount; ++cls) { for (Uint32 cls = 0; cls < kMGPipeShaderBufferClassCount; ++cls) {
g_applier.BoundShaderBuffers[cls] = {}; MGPipeApplier().BoundShaderBuffers[cls] = {};
g_applier.ShaderBufferStart[cls] = 0; MGPipeApplier().ShaderBufferStart[cls] = 0;
g_applier.ShaderBufferCount[cls] = 0; MGPipeApplier().ShaderBufferCount[cls] = 0;
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) { for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
g_applier.ShaderBufferWritableMask[cls][w] = 0; MGPipeApplier().ShaderBufferWritableMask[cls][w] = 0;
} }
} }
} }
@@ -1480,15 +1643,15 @@ namespace MobileGL::MG_Pipe {
std::abort(); std::abort();
} }
#endif #endif
g_applier.RenderStateCsos.clear(); MGPipeApplier().RenderStateCsos.clear();
g_applier.BoundRenderStateCso = kMGPipeNullHandle; MGPipeApplier().BoundRenderStateCso = kMGPipeNullHandle;
g_applier.Residual = ResidualValueBlock{}; MGPipeApplier().Residual = ResidualValueBlock{};
g_applier.HasResidual = false; MGPipeApplier().HasResidual = false;
g_applier.ScatteredChunkBits = 0; MGPipeApplier().ScatteredChunkBits = 0;
g_applier.ResidualCapabilitiesCompared = 0; MGPipeApplier().ResidualCapabilitiesCompared = 0;
g_applier.ResidualDivergences = 0; MGPipeApplier().ResidualDivergences = 0;
g_applier.PatchCarrierComparisons = 0; MGPipeApplier().PatchCarrierComparisons = 0;
g_applier.PatchCarrierDivergences = 0; MGPipeApplier().PatchCarrierDivergences = 0;
// P3a. THIS RUNS AT EVERY CHANGE OF THE CURRENT CONTEXT, not once per fresh one: // P3a. THIS RUNS AT EVERY CHANGE OF THE CURRENT CONTEXT, not once per fresh one:
// MGPipeTracker::Update resets itself whenever the context pointer moves and the // MGPipeTracker::Update resets itself whenever the context pointer moves and the
// emitter calls this from the walk that follows, so a make-current BACK to a context // emitter calls this from the walk that follows, so a make-current BACK to a context
@@ -1505,16 +1668,16 @@ namespace MobileGL::MG_Pipe {
// //
// The OP TABLE is deliberately not cleared either - it is installed and uninstalled by // The OP TABLE is deliberately not cleared either - it is installed and uninstalled by
// the backend's own bring-up and teardown, not by a state reset. // the backend's own bring-up and teardown, not by a state reset.
g_applier.RefusedResourceCalls = 0; MGPipeApplier().RefusedResourceCalls = 0;
g_applier.RefusedVertexInputCalls = 0; MGPipeApplier().RefusedVertexInputCalls = 0;
g_applier.RefusedObjectCalls = 0; MGPipeApplier().RefusedObjectCalls = 0;
g_applier.RefusedNoConsumer = 0; MGPipeApplier().RefusedNoConsumer = 0;
g_applier.BoundVertexElements = kMGPipeNullHandle; MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
g_applier.VertexBuffers = {}; MGPipeApplier().VertexBuffers = {};
g_applier.VertexBufferStart = 0; MGPipeApplier().VertexBufferStart = 0;
g_applier.VertexBufferCount = 0; MGPipeApplier().VertexBufferCount = 0;
g_applier.VertexFetchBaseInstance = 0; MGPipeApplier().VertexFetchBaseInstance = 0;
g_applier.IndexBuffer = MGPIndexBuffer{}; MGPipeApplier().IndexBuffer = MGPIndexBuffer{};
// m6 / wire n6, written down rather than left to be rediscovered: THIS counter is // m6 / wire n6, written down rather than left to be rediscovered: THIS counter is
// per-applier and is zeroed at every make-current, while MG_Util::PipeStats' `mpr` - // per-applier and is zeroed at every make-current, while MG_Util::PipeStats' `mpr` -
// emitted from the CLIENT at MG_Impl/Pipe/PipeFill.cpp's MGPipeEmitMapPersistent - is // emitted from the CLIENT at MG_Impl/Pipe/PipeFill.cpp's MGPipeEmitMapPersistent - is
@@ -1529,7 +1692,7 @@ namespace MobileGL::MG_Pipe {
// MG_IntegrationTest/Harness/PipeStatsWindow.h - they cannot link this symbol at all, on // MG_IntegrationTest/Harness/PipeStatsWindow.h - they cannot link this symbol at all, on
// Android or anywhere else - so a make-current inside a scenario cannot silently reset // Android or anywhere else - so a make-current inside a scenario cannot silently reset
// what they measure. Nothing outside MG_Test reads the member below. // what they measure. Nothing outside MG_Test reads the member below.
g_applier.MapPersistentRoundtrips = 0; MGPipeApplier().MapPersistentRoundtrips = 0;
// THE TWO SERIALS ADVANCE; THEY ARE NOT ZEROED. They are MGGens, and an MGGen that // THE TWO SERIALS ADVANCE; THEY ARE NOT ZEROED. They are MGGens, and an MGGen that
// walks backwards is not one. There are exactly three things a reset can do to a // walks backwards is not one. There are exactly three things a reset can do to a
// version whose data it has just cleared: // version whose data it has just cleared:
@@ -1543,15 +1706,15 @@ namespace MobileGL::MG_Pipe {
// count is stable lands on a stamped value every time; // count is stable lands on a stamped value every time;
// - advance: the clearing is itself announced, no stamped value can ever recur, and // - advance: the clearing is itself announced, no stamped value can ever recur, and
// the first compare after the switch is a mismatch, which is the safe direction. // the first compare after the switch is a mismatch, which is the safe direction.
++g_applier.VertexBuffersSerial; ++MGPipeApplier().VertexBuffersSerial;
++g_applier.IndexBufferSerial; ++MGPipeApplier().IndexBufferSerial;
// P5c (rv): a make-current is a fresh server, and the two shutter serials are what the // P5c (rv): a make-current is a fresh server, and the two shutter serials are what the
// PipeInputs texture-shutter accessors answer with - so both ADVANCE here, for the // PipeInputs texture-shutter accessors answer with - so both ADVANCE here, for the
// serials' own reason: a counter that restarts walks back through values already // serials' own reason: a counter that restarts walks back through values already
// stamped into a memo that outlived the switch. ContextSerial's move is also // stamped into a memo that outlived the switch. ContextSerial's move is also
// GetTextureContextId's whole job (the backends key their per-context memos on it). // GetTextureContextId's whole job (the backends key their per-context memos on it).
++g_applier.TextureShutterSerial; ++MGPipeApplier().TextureShutterSerial;
++g_applier.ContextSerial; ++MGPipeApplier().ContextSerial;
// ---- P4a's working state, cleared for the same reason and with the same serial rule // ---- P4a's working state, cleared for the same reason and with the same serial rule
// (D-J4). The OBJECT records - texture and renderbuffer resources, sampler CSOs, // (D-J4). The OBJECT records - texture and renderbuffer resources, sampler CSOs,
@@ -1566,21 +1729,21 @@ namespace MobileGL::MG_Pipe {
// a null Fbo. FramebufferRecords stays because a framebuffer that is only ever addressed // a null Fbo. FramebufferRecords stays because a framebuffer that is only ever addressed
// BY NAME - the DSA blit and clear entry points - has no re-emission trigger at all, so // BY NAME - the DSA blit and clear entry points - has no re-emission trigger at all, so
// dropping its record here would lose it for the life of the process. // dropping its record here would lose it for the life of the process.
g_applier.BoundFramebuffer = {}; MGPipeApplier().BoundFramebuffer = {};
g_applier.BoundSamplerViews = {}; MGPipeApplier().BoundSamplerViews = {};
g_applier.SamplerViewStart = 0; MGPipeApplier().SamplerViewStart = 0;
g_applier.SamplerViewCount = 0; MGPipeApplier().SamplerViewCount = 0;
g_applier.BoundSamplerStates = {}; MGPipeApplier().BoundSamplerStates = {};
g_applier.SamplerStateStart = 0; MGPipeApplier().SamplerStateStart = 0;
g_applier.SamplerStateCount = 0; MGPipeApplier().SamplerStateCount = 0;
g_applier.BoundShaderImages = {}; MGPipeApplier().BoundShaderImages = {};
g_applier.ShaderImageStart = 0; MGPipeApplier().ShaderImageStart = 0;
g_applier.ShaderImageCount = 0; MGPipeApplier().ShaderImageCount = 0;
g_applier.DrawProgram = kMGPipeNullHandle; MGPipeApplier().DrawProgram = kMGPipeNullHandle;
g_applier.DispatchProgram = kMGPipeNullHandle; MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
g_applier.BoundShaderCso = kMGPipeNullHandle; MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
g_applier.BoundStreamOutputLifetimeId = 0; MGPipeApplier().BoundStreamOutputLifetimeId = 0;
// Begin is emitted once per span, not on make-current. Its immutable object // Begin is emitted once per span, not on make-current. Its immutable object
// snapshot survives with the resource/program records until End or release; // snapshot survives with the resource/program records until End or release;
// set_context_values restores the returning context's bound lifetime id. // set_context_values restores the returning context's bound lifetime id.
@@ -1595,12 +1758,12 @@ namespace MobileGL::MG_Pipe {
// invalidates the three suppressor slots), or the first emission after a make-current // invalidates the three suppressor slots), or the first emission after a make-current
// would be suppressed as unchanged and the server would draw with a cleared window. // would be suppressed as unchanged and the server would draw with a cleared window.
ClearShaderBufferWindows(); ClearShaderBufferWindows();
++g_applier.FramebufferSerial; ++MGPipeApplier().FramebufferSerial;
++g_applier.SamplerViewsSerial; ++MGPipeApplier().SamplerViewsSerial;
++g_applier.SamplerStatesSerial; ++MGPipeApplier().SamplerStatesSerial;
++g_applier.ShaderImagesSerial; ++MGPipeApplier().ShaderImagesSerial;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
++g_applier.ShaderBuffersSerial; ++MGPipeApplier().ShaderBuffersSerial;
} }
void MGPipeApplierReleaseObjectRecords() { void MGPipeApplierReleaseObjectRecords() {
@@ -1615,37 +1778,37 @@ namespace MobileGL::MG_Pipe {
// context, so nothing wires this - see PipeApply.h. The two serials advance here for // context, so nothing wires this - see PipeApply.h. The two serials advance here for
// MGPipeApplierReset's reason: state was cleared, and a twin that outlives it must not // MGPipeApplierReset's reason: state was cleared, and a twin that outlives it must not
// be able to match a value it has already seen. // be able to match a value it has already seen.
g_applier.Resources.clear(); MGPipeApplier().Resources.clear();
g_applier.VertexElementsCsos.clear(); MGPipeApplier().VertexElementsCsos.clear();
g_applier.BoundVertexElements = kMGPipeNullHandle; MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
++g_applier.VertexBuffersSerial; ++MGPipeApplier().VertexBuffersSerial;
++g_applier.IndexBufferSerial; ++MGPipeApplier().IndexBufferSerial;
// P5c (rv): same rule for the shutter serials - the served context is going away, and // P5c (rv): same rule for the shutter serials - the served context is going away, and
// a memo that outlives it must not match a value these have already answered with. // a memo that outlives it must not match a value these have already answered with.
++g_applier.TextureShutterSerial; ++MGPipeApplier().TextureShutterSerial;
++g_applier.ContextSerial; ++MGPipeApplier().ContextSerial;
// P4a's five object tables go with them, and the working handles they could name go // P4a's five object tables go with them, and the working handles they could name go
// too - a bound shader CSO whose record has just been dropped must not survive as a // too - a bound shader CSO whose record has just been dropped must not survive as a
// handle the next call resolves against. // handle the next call resolves against.
g_applier.TextureResources.clear(); MGPipeApplier().TextureResources.clear();
g_applier.RenderbufferResources.clear(); MGPipeApplier().RenderbufferResources.clear();
g_applier.SamplerCsos.clear(); MGPipeApplier().SamplerCsos.clear();
g_applier.SamplerViewCsos.clear(); MGPipeApplier().SamplerViewCsos.clear();
g_applier.ShaderCsos.clear(); MGPipeApplier().ShaderCsos.clear();
g_applier.CompositeShaderCsos.clear(); MGPipeApplier().CompositeShaderCsos.clear();
// AND THE FRAMEBUFFER RECORDS, which MGPipeApplierReset above deliberately keeps. Here // AND THE FRAMEBUFFER RECORDS, which MGPipeApplierReset above deliberately keeps. Here
// there is nothing for them to outlive: the served context is going away and the // there is nothing for them to outlive: the served context is going away and the
// framebuffers it owned are container objects that go with it. A table left standing // framebuffers it owned are container objects that go with it. A table left standing
// would answer a lookup from the NEXT context on a slot the allocator has re-minted - // would answer a lookup from the NEXT context on a slot the allocator has re-minted -
// with the same generation, if the next context happens to reach the same one - and // with the same generation, if the next context happens to reach the same one - and
// that is the aliasing every other table is cleared to prevent. // that is the aliasing every other table is cleared to prevent.
g_applier.FramebufferRecords.clear(); MGPipeApplier().FramebufferRecords.clear();
g_applier.DrawProgram = kMGPipeNullHandle; MGPipeApplier().DrawProgram = kMGPipeNullHandle;
g_applier.DispatchProgram = kMGPipeNullHandle; MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
g_applier.BoundShaderCso = kMGPipeNullHandle; MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
g_applier.BoundStreamOutputLifetimeId = 0; MGPipeApplier().BoundStreamOutputLifetimeId = 0;
g_applier.StreamOutputSpans.clear(); MGPipeApplier().StreamOutputSpans.clear();
#endif #endif
// AND "THE WORKING HANDLES THEY COULD NAME" IS ALL OF THEM, NOT JUST THE THREE ABOVE. // AND "THE WORKING HANDLES THEY COULD NAME" IS ALL OF THEM, NOT JUST THE THREE ABOVE.
// Every framebuffer record holds eleven MGPSurface::Res naming texture and renderbuffer // Every framebuffer record holds eleven MGPSurface::Res naming texture and renderbuffer
@@ -1657,35 +1820,35 @@ namespace MobileGL::MG_Pipe {
// else's record. So this takes the same working state MGPipeApplierReset does - which is // else's record. So this takes the same working state MGPipeApplierReset does - which is
// the honest reading of the rule as well: Reset keeps the object records BECAUSE they // the honest reading of the rule as well: Reset keeps the object records BECAUSE they
// outlive a make-current, and here they do not outlive anything. // outlive a make-current, and here they do not outlive anything.
g_applier.BoundFramebuffer = {}; MGPipeApplier().BoundFramebuffer = {};
g_applier.BoundSamplerViews = {}; MGPipeApplier().BoundSamplerViews = {};
g_applier.SamplerViewStart = 0; MGPipeApplier().SamplerViewStart = 0;
g_applier.SamplerViewCount = 0; MGPipeApplier().SamplerViewCount = 0;
g_applier.BoundSamplerStates = {}; MGPipeApplier().BoundSamplerStates = {};
g_applier.SamplerStateStart = 0; MGPipeApplier().SamplerStateStart = 0;
g_applier.SamplerStateCount = 0; MGPipeApplier().SamplerStateCount = 0;
g_applier.BoundShaderImages = {}; MGPipeApplier().BoundShaderImages = {};
g_applier.ShaderImageStart = 0; MGPipeApplier().ShaderImageStart = 0;
g_applier.ShaderImageCount = 0; MGPipeApplier().ShaderImageCount = 0;
// P5e (sb): and the three binding-point windows, for the paragraph above's reason in // P5e (sb): and the three binding-point windows, for the paragraph above's reason in
// its own words - every MGPBufferRange::Res names a resource record this function has // its own words - every MGPBufferRange::Res names a resource record this function has
// just dropped, so a window left populated here is a set of handles into an empty // just dropped, so a window left populated here is a set of handles into an empty
// table, which the next resolve either refuses or answers with somebody else's record // table, which the next resolve either refuses or answers with somebody else's record
// on a slot the next context re-mints. // on a slot the next context re-mints.
ClearShaderBufferWindows(); ClearShaderBufferWindows();
++g_applier.FramebufferSerial; ++MGPipeApplier().FramebufferSerial;
++g_applier.SamplerViewsSerial; ++MGPipeApplier().SamplerViewsSerial;
++g_applier.SamplerStatesSerial; ++MGPipeApplier().SamplerStatesSerial;
++g_applier.ShaderImagesSerial; ++MGPipeApplier().ShaderImagesSerial;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
++g_applier.ShaderBuffersSerial; ++MGPipeApplier().ShaderBuffersSerial;
} }
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) { void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot, MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
"create_render_state named the reserved slot 0"); "create_render_state named the reserved slot 0");
MGPipeRenderStateCsoRecord* recordAt = MGPipeRenderStateCsoRecord* recordAt =
RecordAt(g_applier.RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots); RecordAt(MGPipeApplier().RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots);
if (recordAt == nullptr) { if (recordAt == nullptr) {
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
MGPipeSessionFail(MGPipeFatalFamily::ProtocolCorruption, MGPipeSessionFail(MGPipeFatalFamily::ProtocolCorruption,
@@ -1753,12 +1916,12 @@ namespace MobileGL::MG_Pipe {
MGPipeScatterPipelineBytes(record->PipelineBytes.data(), MGPipeScatterPipelineBytes(record->PipelineBytes.data(),
MGPipeApplyAccess::RenderState(inputs)); MGPipeApplyAccess::RenderState(inputs));
MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion); MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion);
g_applier.BoundRenderStateCso = bind.Cso; MGPipeApplier().BoundRenderStateCso = bind.Cso;
// A bind scatters the WHOLE pipeline half - the record is always a complete one, // A bind scatters the WHOLE pipeline half - the record is always a complete one,
// whatever mask minted it - so the pipeline chunks are all "moved" here, and all // whatever mask minted it - so the pipeline chunks are all "moved" here, and all
// enter the applier's ledger of the bytes it owns. // enter the applier's ledger of the bytes it owns.
const Uint32 moved = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineChunks); const Uint32 moved = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineChunks);
g_applier.ScatteredChunkBits |= moved; MGPipeApplier().ScatteredChunkBits |= moved;
MGPipeDeriveRenderStateFieldsForChunks(inputs, moved); MGPipeDeriveRenderStateFieldsForChunks(inputs, moved);
} }
@@ -1771,8 +1934,8 @@ namespace MobileGL::MG_Pipe {
// The Gen stays: it is the CLIENT allocator that bumps it when the slot is handed // The Gen stays: it is the CLIENT allocator that bumps it when the slot is handed
// out again (MGPipeHandles.h: "Gen increments only when a SLOT IS REUSED"), and a // out again (MGPipeHandles.h: "Gen increments only when a SLOT IS REUSED"), and a
// server-side bump here would put the two identities out of step. // server-side bump here would put the two identities out of step.
if (g_applier.BoundRenderStateCso == handle.Handle) { if (MGPipeApplier().BoundRenderStateCso == handle.Handle) {
g_applier.BoundRenderStateCso = kMGPipeNullHandle; MGPipeApplier().BoundRenderStateCso = kMGPipeNullHandle;
} }
} }
@@ -1781,7 +1944,7 @@ namespace MobileGL::MG_Pipe {
MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs)); MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs));
MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version); MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version);
const Uint32 moved = MGPipeGlobalChunkBitsOfDynamicMask(dyn.ChunkMask); const Uint32 moved = MGPipeGlobalChunkBitsOfDynamicMask(dyn.ChunkMask);
g_applier.ScatteredChunkBits |= moved; MGPipeApplier().ScatteredChunkBits |= moved;
MGPipeDeriveRenderStateFieldsForChunks(inputs, moved); MGPipeDeriveRenderStateFieldsForChunks(inputs, moved);
} }
@@ -1805,9 +1968,9 @@ namespace MobileGL::MG_Pipe {
// filling, so a predicate that read the answer out of THERE would be reading the very // filling, so a predicate that read the answer out of THERE would be reading the very
// memory the rule exists to stop it reading. One store, inert until the caps bit is // memory the rule exists to stop it reading. One store, inert until the caps bit is
// published, and it is what makes the two roles' third clause the same clause. // published, and it is what makes the two roles' third clause the same clause.
g_applier.IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0; MGPipeApplier().IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0;
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
g_applier.BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId; MGPipeApplier().BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId;
#endif #endif
} }
@@ -1839,13 +2002,13 @@ namespace MobileGL::MG_Pipe {
// It runs in the shipped push build too, because a wire that is compiled out of // It runs in the shipped push build too, because a wire that is compiled out of
// every build a device runs is not a wire. The cost is a 24-byte memcmp on a call // every build a device runs is not a wire. The cost is a 24-byte memcmp on a call
// that is emitted when the tessellation state CHANGES, i.e. about once per program. // that is emitted when the tessellation state CHANGES, i.e. about once per program.
if ((g_applier.ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) { if ((MGPipeApplier().ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) {
++g_applier.PatchCarrierComparisons; ++MGPipeApplier().PatchCarrierComparisons;
const Bool agrees = working.PatchVertices == patch.Vertices && const Bool agrees = working.PatchVertices == patch.Vertices &&
std::memcmp(&working.PatchDefaultOuterLevel, &outer, sizeof(outer)) == 0 && std::memcmp(&working.PatchDefaultOuterLevel, &outer, sizeof(outer)) == 0 &&
std::memcmp(&working.PatchDefaultInnerLevel, &inner, sizeof(inner)) == 0; std::memcmp(&working.PatchDefaultInnerLevel, &inner, sizeof(inner)) == 0;
if (!agrees) { if (!agrees) {
++g_applier.PatchCarrierDivergences; ++MGPipeApplier().PatchCarrierDivergences;
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipePatchCarriersDiffer") MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipePatchCarriersDiffer")
" set_patch_state says vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g); " " set_patch_state says vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g); "
"chunk P0 delivered vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g)", "chunk P0 delivered vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g)",
@@ -1921,9 +2084,9 @@ namespace MobileGL::MG_Pipe {
} }
void MGPipeApplySetResidualValueState(const ResidualValueBlock& block) { void MGPipeApplySetResidualValueState(const ResidualValueBlock& block) {
g_applier.Residual = block; MGPipeApplier().Residual = block;
g_applier.HasResidual = true; MGPipeApplier().HasResidual = true;
g_applier.ResidualCapabilitiesCompared = 0; MGPipeApplier().ResidualCapabilitiesCompared = 0;
// THE TRIP WIRE (ARCHITECTURE.md 9.4, P2 brief D9). CapabilityBits is redundant with // THE TRIP WIRE (ARCHITECTURE.md 9.4, P2 brief D9). CapabilityBits is redundant with
// the assembled working block by design: every one of the 35 capabilities is // the assembled working block by design: every one of the 35 capabilities is
@@ -1953,16 +2116,16 @@ namespace MobileGL::MG_Pipe {
// tautology. The redundancy this wire exists to check is between the CARRIED bits and // tautology. The redundancy this wire exists to check is between the CARRIED bits and
// the ASSEMBLED block. // the ASSEMBLED block.
const RenderStateParameters& working = MGPipeApplyAccess::RenderState(gPipeInputs); const RenderStateParameters& working = MGPipeApplyAccess::RenderState(gPipeInputs);
const Uint32 owned = g_applier.ScatteredChunkBits; const Uint32 owned = MGPipeApplier().ScatteredChunkBits;
for (SizeT i = 0; i < kCapabilityCount; ++i) { for (SizeT i = 0; i < kCapabilityCount; ++i) {
const CapabilityInput cap = static_cast<CapabilityInput>(i); const CapabilityInput cap = static_cast<CapabilityInput>(i);
const Uint32 sources = CapabilitySourceChunks(cap); const Uint32 sources = CapabilitySourceChunks(cap);
if ((owned & sources) != sources) continue; if ((owned & sources) != sources) continue;
++g_applier.ResidualCapabilitiesCompared; ++MGPipeApplier().ResidualCapabilitiesCompared;
const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0; const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0;
const Bool assembledBit = MGPipeApplyAccess::DeriveCapability(working, cap); const Bool assembledBit = MGPipeApplyAccess::DeriveCapability(working, cap);
if (carried == assembledBit) continue; if (carried == assembledBit) continue;
++g_applier.ResidualDivergences; ++MGPipeApplier().ResidualDivergences;
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeResidualDiverged, \"%s\"") MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeResidualDiverged, \"%s\"")
" carried=%d assembled=%d", " carried=%d assembled=%d",
kCapabilityNames[i], static_cast<int>(carried), kCapabilityNames[i], static_cast<int>(carried),
@@ -2279,7 +2442,7 @@ namespace MobileGL::MG_Pipe {
// THE FOUR BUFFER-ONLY CALLS, AND WHAT POLICES THAT. resource_flush_range, // THE FOUR BUFFER-ONLY CALLS, AND WHAT POLICES THAT. resource_flush_range,
// resource_readback and map_persistent resolve through ResolveResource - i.e. against // resource_readback and map_persistent resolve through ResolveResource - i.e. against
// g_applier.Resources and never the texture or renderbuffer table - which is right, because // MGPipeApplier().Resources and never the texture or renderbuffer table - which is right, because
// a mapped range, a readback and a persistent donation exist for buffers only. They are // a mapped range, a readback and a persistent donation exist for buffers only. They are
// buffer-only BY CATALOGUE and not by check: MGPFlushRange and MGPReadback carry no kind // buffer-only BY CATALOGUE and not by check: MGPFlushRange and MGPReadback carry no kind
// at all, and map_persistent's MGPHandleOnly is not asked for one, so there is nothing here // at all, and map_persistent's MGPHandleOnly is not asked for one, so there is nothing here
@@ -2428,7 +2591,7 @@ namespace MobileGL::MG_Pipe {
// Defined that way the number is identical in both modes, equals "one per storage // Defined that way the number is identical in both modes, equals "one per storage
// definition", and is non-zero and assertable today; defined as "round trips actually // definition", and is non-zero and assertable today; defined as "round trips actually
// taken" it would be 0 by construction in monolith and could never go red. // taken" it would be 0 by construction in monolith and could never go red.
++g_applier.MapPersistentRoundtrips; ++MGPipeApplier().MapPersistentRoundtrips;
MGPipeResourceRecord* record = ResolveResource("map_persistent", handle.Handle); MGPipeResourceRecord* record = ResolveResource("map_persistent", handle.Handle);
if (record == nullptr) return nullptr; if (record == nullptr) return nullptr;
@@ -2586,7 +2749,7 @@ namespace MobileGL::MG_Pipe {
} }
MGPipeVertexElementsRecord* recordAt = MGPipeVertexElementsRecord* recordAt =
RecordAt(g_applier.VertexElementsCsos, desc.Cso.Slot, kMGPipeMaxVertexElementsSlots); RecordAt(MGPipeApplier().VertexElementsCsos, desc.Cso.Slot, kMGPipeMaxVertexElementsSlots);
if (recordAt == nullptr) { if (recordAt == nullptr) {
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
" create_vertex_elements {slot=%u, gen=%u}: the slot is outside the record " " create_vertex_elements {slot=%u, gen=%u}: the slot is outside the record "
@@ -2634,14 +2797,14 @@ namespace MobileGL::MG_Pipe {
// The null handle is legal and means "no vertex array bound" - GL's unbound state is a // The null handle is legal and means "no vertex array bound" - GL's unbound state is a
// state, not an error, and the backend has a branch for it. // state, not an error, and the backend has a branch for it.
if (MGPipeHandleIsNull(handle.Handle)) { if (MGPipeHandleIsNull(handle.Handle)) {
g_applier.BoundVertexElements = kMGPipeNullHandle; MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
return; return;
} }
// A dead handle leaves the PREVIOUS binding untouched, which is bind_render_state's // A dead handle leaves the PREVIOUS binding untouched, which is bind_render_state's
// precedent for the same question, and is counted like every other refusal. // precedent for the same question, and is counted like every other refusal.
const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle); const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
g_applier.BoundVertexElements = handle.Handle; MGPipeApplier().BoundVertexElements = handle.Handle;
} }
void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) { void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) {
@@ -2663,8 +2826,8 @@ namespace MobileGL::MG_Pipe {
const Uint32 gen = record->Gen; const Uint32 gen = record->Gen;
*record = MGPipeVertexElementsRecord{}; *record = MGPipeVertexElementsRecord{};
record->Gen = gen; record->Gen = gen;
if (g_applier.BoundVertexElements == handle.Handle) { if (MGPipeApplier().BoundVertexElements == handle.Handle) {
g_applier.BoundVertexElements = kMGPipeNullHandle; MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
} }
} }
@@ -2692,10 +2855,10 @@ namespace MobileGL::MG_Pipe {
// are not cleared: this record is "the last set as received", and a set that names // are not cleared: this record is "the last set as received", and a set that names
// four entries has said nothing about the rest. // four entries has said nothing about the rest.
for (Uint32 i = 0; i < hdr.Count; ++i) { for (Uint32 i = 0; i < hdr.Count; ++i) {
g_applier.VertexBuffers[hdr.Start + i] = tail[i]; MGPipeApplier().VertexBuffers[hdr.Start + i] = tail[i];
} }
g_applier.VertexBufferStart = hdr.Start; MGPipeApplier().VertexBufferStart = hdr.Start;
g_applier.VertexBufferCount = hdr.Count; MGPipeApplier().VertexBufferCount = hdr.Count;
// THE RAW VALUE IS STORED, NOT A RESOLVED SHIFT, and the resolution is one step // THE RAW VALUE IS STORED, NOT A RESOLVED SHIFT, and the resolution is one step
// further out on purpose. Whether the fetch shift has to be emulated at all is a // further out on purpose. Whether the fetch shift has to be emulated at all is a
@@ -2710,8 +2873,8 @@ namespace MobileGL::MG_Pipe {
// input: set_vertex_buffers is suppressed on an unchanged hash, so a base instance // input: set_vertex_buffers is suppressed on an unchanged hash, so a base instance
// that moved while the buffer set did not would otherwise never arrive and the server // that moved while the buffer set did not would otherwise never arrive and the server
// would keep the previous shift. // would keep the previous shift.
g_applier.VertexFetchBaseInstance = hdr.BaseInstance; MGPipeApplier().VertexFetchBaseInstance = hdr.BaseInstance;
++g_applier.VertexBuffersSerial; ++MGPipeApplier().VertexBuffersSerial;
} }
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) { void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) {
@@ -2725,8 +2888,8 @@ namespace MobileGL::MG_Pipe {
// - it is an INDEPENDENT call and NOT a subset of the vertex-elements configuration // - it is an INDEPENDENT call and NOT a subset of the vertex-elements configuration
// (D5): a rebind of the index slot must move this serial without touching the // (D5): a rebind of the index slot must move this serial without touching the
// configuration's, which is exactly what the backend's two separate compares need. // configuration's, which is exactly what the backend's two separate compares need.
g_applier.IndexBuffer = record; MGPipeApplier().IndexBuffer = record;
++g_applier.IndexBufferSerial; ++MGPipeApplier().IndexBufferSerial;
} }
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) { void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
@@ -2839,7 +3002,7 @@ namespace MobileGL::MG_Pipe {
if (NoP4aConsumer()) return; if (NoP4aConsumer()) return;
MGPipeFramebufferRecord* record = MGPipeFramebufferRecord* record =
RecordAt(g_applier.FramebufferRecords, state.Fbo.Slot, kMGPipeMaxFramebufferSlots); RecordAt(MGPipeApplier().FramebufferRecords, state.Fbo.Slot, kMGPipeMaxFramebufferSlots);
// Unreachable: the bound was checked above, before anything moved. The null check is // Unreachable: the bound was checked above, before anything moved. The null check is
// what makes that an argument rather than an assumption. // what makes that an argument rather than an assumption.
if (record == nullptr) return; if (record == nullptr) return;
@@ -2851,11 +3014,11 @@ namespace MobileGL::MG_Pipe {
// target: "this record describes the framebuffer it names; no binding changes". // target: "this record describes the framebuffer it names; no binding changes".
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Draw) || if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Draw) ||
state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) { state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) {
g_applier.BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Draw)] = state.Fbo; MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Draw)] = state.Fbo;
} }
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read) || if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read) ||
state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) { state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) {
g_applier.BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Read)] = state.Fbo; MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Read)] = state.Fbo;
} }
// ONE SERIAL FOR THE FAMILY, and it moves once per applied record - a Both record is one // ONE SERIAL FOR THE FAMILY, and it moves once per applied record - a Both record is one
// record and a Named record is one record. It is what retires the four g_fboSynced* // record and a Named record is one record. It is what retires the four g_fboSynced*
@@ -2864,7 +3027,7 @@ namespace MobileGL::MG_Pipe {
// serial still the server's". A Named record moves it for the same reason a bound one // serial still the server's". A Named record moves it for the same reason a bound one
// does - a twin that memoised a framebuffer's attachments has to hear that they moved, // does - a twin that memoised a framebuffer's attachments has to hear that they moved,
// and whether the framebuffer happens to be bound is not that question. // and whether the framebuffer happens to be bound is not that question.
++g_applier.FramebufferSerial; ++MGPipeApplier().FramebufferSerial;
} }
// THE READ SIDE OF THE TABLE (ID-19(b)/(d)). Package D asks it by handle at every DSA entry // THE READ SIDE OF THE TABLE (ID-19(b)/(d)). Package D asks it by handle at every DSA entry
@@ -2941,7 +3104,7 @@ namespace MobileGL::MG_Pipe {
} }
MGPipeSamplerCsoRecord* recordAt = MGPipeSamplerCsoRecord* recordAt =
RecordAt(g_applier.SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots); RecordAt(MGPipeApplier().SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots);
if (recordAt == nullptr) { if (recordAt == nullptr) {
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
" create_sampler_state {slot=%u, gen=%u}: the slot is outside the record " " create_sampler_state {slot=%u, gen=%u}: the slot is outside the record "
@@ -2978,7 +3141,7 @@ namespace MobileGL::MG_Pipe {
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerCso), MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerCso),
"delete_sampler_state on kind %u", handle.Kind); "delete_sampler_state on kind %u", handle.Kind);
MGPipeSamplerCsoRecord* record = MGPipeSamplerCsoRecord* record =
ResolveObject(g_applier.SamplerCsos, "delete_sampler_state", handle.Handle); ResolveObject(MGPipeApplier().SamplerCsos, "delete_sampler_state", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
// Dropped whole, generation kept, for resource_destroy's reason: the client allocator // Dropped whole, generation kept, for resource_destroy's reason: the client allocator
@@ -3000,7 +3163,7 @@ namespace MobileGL::MG_Pipe {
if (view.Cso.Slot < kMGPipeFirstAllocatableSlot) return; if (view.Cso.Slot < kMGPipeFirstAllocatableSlot) return;
MGPipeSamplerViewRecord* recordAt = MGPipeSamplerViewRecord* recordAt =
RecordAt(g_applier.SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots); RecordAt(MGPipeApplier().SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots);
if (recordAt == nullptr) { if (recordAt == nullptr) {
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
" create_sampler_view {slot=%u, gen=%u}: the slot is outside the record " " create_sampler_view {slot=%u, gen=%u}: the slot is outside the record "
@@ -3031,7 +3194,7 @@ namespace MobileGL::MG_Pipe {
// arriving before - or without - the texture record is an ordering fact and not a // arriving before - or without - the texture record is an ordering fact and not a
// refusal. When the record is there this is the back-pointer that lets a sync reach a // refusal. When the record is there this is the back-pointer that lets a sync reach a
// texture's view without walking every view the applier holds. // texture's view without walking every view the applier holds.
MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, view.Texture); MGPipeResourceRecord* texture = FindIn(MGPipeApplier().TextureResources, view.Texture);
if (texture != nullptr) texture->ViewCso = view.Cso; if (texture != nullptr) texture->ViewCso = view.Cso;
} }
@@ -3039,13 +3202,13 @@ namespace MobileGL::MG_Pipe {
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerViewCso), MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerViewCso),
"delete_sampler_view on kind %u", handle.Kind); "delete_sampler_view on kind %u", handle.Kind);
MGPipeSamplerViewRecord* record = MGPipeSamplerViewRecord* record =
ResolveObject(g_applier.SamplerViewCsos, "delete_sampler_view", handle.Handle); ResolveObject(MGPipeApplier().SamplerViewCsos, "delete_sampler_view", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
// The back-pointer goes first, while the record that names the texture still exists, // The back-pointer goes first, while the record that names the texture still exists,
// and only if that texture still names THIS view - a texture whose view has already // and only if that texture still names THIS view - a texture whose view has already
// been re-minted must not have the new handle cleared out from under it. // been re-minted must not have the new handle cleared out from under it.
MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, record->View.Texture); MGPipeResourceRecord* texture = FindIn(MGPipeApplier().TextureResources, record->View.Texture);
if (texture != nullptr && texture->ViewCso == handle.Handle) { if (texture != nullptr && texture->ViewCso == handle.Handle) {
texture->ViewCso = kMGPipeNullHandle; texture->ViewCso = kMGPipeNullHandle;
} }
@@ -3066,7 +3229,7 @@ namespace MobileGL::MG_Pipe {
// READ-attachment case reaches no parameter push at all today. The record exists the // READ-attachment case reaches no parameter push at all today. The record exists the
// moment the parameters move, whether or not anything is bound. // moment the parameters move, whether or not anything is bound.
MGPipeResourceRecord* record = MGPipeResourceRecord* record =
ResolveObject(g_applier.TextureResources, "set_texture_params", params.Res); ResolveObject(MGPipeApplier().TextureResources, "set_texture_params", params.Res);
if (record == nullptr) return false; if (record == nullptr) return false;
// EVERY ITextureObject OWNS A SamplerObject, so the built-in sampler CSO is not // EVERY ITextureObject OWNS A SamplerObject, so the built-in sampler CSO is not
@@ -3121,11 +3284,11 @@ namespace MobileGL::MG_Pipe {
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) { void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
if (NoP4aConsumer()) return; if (NoP4aConsumer()) return;
if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail, if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail,
g_applier.BoundSamplerViews, g_applier.SamplerViewStart, MGPipeApplier().BoundSamplerViews, MGPipeApplier().SamplerViewStart,
g_applier.SamplerViewCount)) { MGPipeApplier().SamplerViewCount)) {
return; return;
} }
++g_applier.SamplerViewsSerial; ++MGPipeApplier().SamplerViewsSerial;
// P5c (rv): a texture bind is what the bind-generation shutter guarded. // P5c (rv): a texture bind is what the bind-generation shutter guarded.
MGPipeApplierNoteTextureStateMoved(); MGPipeApplierNoteTextureStateMoved();
} }
@@ -3133,11 +3296,11 @@ namespace MobileGL::MG_Pipe {
void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) { void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
if (NoP4aConsumer()) return; if (NoP4aConsumer()) return;
if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail, if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail,
g_applier.BoundSamplerStates, g_applier.SamplerStateStart, MGPipeApplier().BoundSamplerStates, MGPipeApplier().SamplerStateStart,
g_applier.SamplerStateCount)) { MGPipeApplier().SamplerStateCount)) {
return; return;
} }
++g_applier.SamplerStatesSerial; ++MGPipeApplier().SamplerStatesSerial;
// P5c (rv): a sampler bind or parameter change is what the shutters guarded. // P5c (rv): a sampler bind or parameter change is what the shutters guarded.
MGPipeApplierNoteTextureStateMoved(); MGPipeApplierNoteTextureStateMoved();
} }
@@ -3150,11 +3313,11 @@ namespace MobileGL::MG_Pipe {
// formats on Adreno will accept is the backend's, not this applier's. // formats on Adreno will accept is the backend's, not this applier's.
if (NoP4aConsumer()) return; if (NoP4aConsumer()) return;
if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail, if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail,
g_applier.BoundShaderImages, g_applier.ShaderImageStart, MGPipeApplier().BoundShaderImages, MGPipeApplier().ShaderImageStart,
g_applier.ShaderImageCount)) { MGPipeApplier().ShaderImageCount)) {
return; return;
} }
++g_applier.ShaderImagesSerial; ++MGPipeApplier().ShaderImagesSerial;
// P5c (rv): an image bind moves the texture bind generation's guarded set too. // P5c (rv): an image bind moves the texture bind generation's guarded set too.
MGPipeApplierNoteTextureStateMoved(); MGPipeApplierNoteTextureStateMoved();
} }
@@ -3211,18 +3374,18 @@ namespace MobileGL::MG_Pipe {
// unit sets and set_vertex_buffers work: the record is "the last set as received", and // unit sets and set_vertex_buffers work: the record is "the last set as received", and
// a binding at or above Count means "nothing bound" to every reader (the frontend // a binding at or above Count means "nothing bound" to every reader (the frontend
// array's default says the same thing, which is what makes the two pictures agree). // array's default says the same thing, which is what makes the two pictures agree).
auto& destination = g_applier.BoundShaderBuffers[hdr.Class]; auto& destination = MGPipeApplier().BoundShaderBuffers[hdr.Class];
for (Uint32 i = 0; i < hdr.Count; ++i) destination[hdr.Start + i] = tail[i]; for (Uint32 i = 0; i < hdr.Count; ++i) destination[hdr.Start + i] = tail[i];
g_applier.ShaderBufferStart[hdr.Class] = hdr.Start; MGPipeApplier().ShaderBufferStart[hdr.Class] = hdr.Start;
g_applier.ShaderBufferCount[hdr.Class] = hdr.Count; MGPipeApplier().ShaderBufferCount[hdr.Class] = hdr.Count;
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) { for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
g_applier.ShaderBufferWritableMask[hdr.Class][w] = hdr.WritableMask[w]; MGPipeApplier().ShaderBufferWritableMask[hdr.Class][w] = hdr.WritableMask[w];
} }
// ONE SERIAL FOR ALL THREE CLASSES, and it moves on EVERY applied record - a backend // ONE SERIAL FOR ALL THREE CLASSES, and it moves on EVERY applied record - a backend
// memo that asks "have the binding points moved since I last bound them" must hear // memo that asks "have the binding points moved since I last bound them" must hear
// about a uniform set exactly as it hears about a storage set, and splitting the serial // about a uniform set exactly as it hears about a storage set, and splitting the serial
// per class would make each of the four consumers pick which one to read. // per class would make each of the four consumers pick which one to read.
++g_applier.ShaderBuffersSerial; ++MGPipeApplier().ShaderBuffersSerial;
} }
// P5e (pg) WRITES THE SECOND BODY. The three post-link mutable reflection fields - // P5e (pg) WRITES THE SECOND BODY. The three post-link mutable reflection fields -
@@ -3501,14 +3664,14 @@ namespace MobileGL::MG_Pipe {
// error; a DEAD handle leaves the previous binding untouched and is counted, which is // error; a DEAD handle leaves the previous binding untouched and is counted, which is
// bind_render_state's precedent for the same question. // bind_render_state's precedent for the same question.
if (MGPipeHandleIsNull(handle.Handle)) { if (MGPipeHandleIsNull(handle.Handle)) {
g_applier.BoundShaderCso = kMGPipeNullHandle; MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
return; return;
} }
const MGPipeShaderCsoRecord* record = ResolveShaderCso("bind_shader_state", handle.Handle); const MGPipeShaderCsoRecord* record = ResolveShaderCso("bind_shader_state", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
g_applier.BoundShaderCso = handle.Handle; MGPipeApplier().BoundShaderCso = handle.Handle;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
} }
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) { void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) {
@@ -3532,19 +3695,19 @@ namespace MobileGL::MG_Pipe {
// next verb resolving a binding to a record this applier no longer has, which is the // next verb resolving a binding to a record this applier no longer has, which is the
// refusal counter firing for a state the applier itself created. // refusal counter firing for a state the applier itself created.
Bool cleared = false; Bool cleared = false;
if (g_applier.BoundShaderCso == handle.Handle) { if (MGPipeApplier().BoundShaderCso == handle.Handle) {
g_applier.BoundShaderCso = kMGPipeNullHandle; MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
cleared = true; cleared = true;
} }
if (g_applier.DrawProgram == handle.Handle) { if (MGPipeApplier().DrawProgram == handle.Handle) {
g_applier.DrawProgram = kMGPipeNullHandle; MGPipeApplier().DrawProgram = kMGPipeNullHandle;
cleared = true; cleared = true;
} }
if (g_applier.DispatchProgram == handle.Handle) { if (MGPipeApplier().DispatchProgram == handle.Handle) {
g_applier.DispatchProgram = kMGPipeNullHandle; MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
cleared = true; cleared = true;
} }
if (cleared) ++g_applier.ProgramBindingSerial; if (cleared) ++MGPipeApplier().ProgramBindingSerial;
} }
// TWO CALLS AND NOT ONE, because the frontend has two joins and two PipeInputs slots: a // TWO CALLS AND NOT ONE, because the frontend has two joins and two PipeInputs slots: a
@@ -3555,14 +3718,14 @@ namespace MobileGL::MG_Pipe {
"set_draw_program on kind %u", handle.Kind); "set_draw_program on kind %u", handle.Kind);
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason. if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
if (MGPipeHandleIsNull(handle.Handle)) { if (MGPipeHandleIsNull(handle.Handle)) {
g_applier.DrawProgram = kMGPipeNullHandle; MGPipeApplier().DrawProgram = kMGPipeNullHandle;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
return; return;
} }
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_draw_program", handle.Handle); const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_draw_program", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
g_applier.DrawProgram = handle.Handle; MGPipeApplier().DrawProgram = handle.Handle;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
} }
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) { void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) {
@@ -3570,14 +3733,14 @@ namespace MobileGL::MG_Pipe {
"set_dispatch_program on kind %u", handle.Kind); "set_dispatch_program on kind %u", handle.Kind);
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason. if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
if (MGPipeHandleIsNull(handle.Handle)) { if (MGPipeHandleIsNull(handle.Handle)) {
g_applier.DispatchProgram = kMGPipeNullHandle; MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
return; return;
} }
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_dispatch_program", handle.Handle); const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_dispatch_program", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
g_applier.DispatchProgram = handle.Handle; MGPipeApplier().DispatchProgram = handle.Handle;
++g_applier.ProgramBindingSerial; ++MGPipeApplier().ProgramBindingSerial;
} }
void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) { void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) {
+186 -65
View File
@@ -513,7 +513,88 @@ namespace MobileGL::MG_Pipe {
MGPFramebufferState State{}; MGPFramebufferState State{};
}; };
struct MGPipeApplierState { // =====================================================================================
// P14 S5: THE SHARE-GROUP HALF OF THE APPLIER'S STATE.
// =====================================================================================
//
// THE TWO HALVES HAVE DIFFERENT LIVES, and until this slice they were one struct with one
// owner (the process), which is what made a second served context either share working state
// it must not share or lose object records it must not lose. They are two structs now because
// they are two OWNERS:
//
// MGPipeObjectRecords a GL object lives in a SHARE GROUP, so these records are keyed by
// {session, share group}: two contexts of one group see one set, two
// NON-shared contexts of one session see two sets - which is the whole
// reason the frontend gives each context its own name space
// (MG_State/GLState/ShareGroupState). A GL object survives a
// make-current and its record is the only thing the backend has left to
// read that object's storage extent and mutation serial out of (D-A4
// re-keys IsBufferDrawClean onto exactly those two); dropping them at a
// make-current made every subsequent glBufferSubData on a pre-existing
// buffer resolve to nothing.
//
// MGPipeWorkingState what the next draw fetches with - bindings, current program, the unit
// windows, the CSO store, the serials. Keyed by {session, context}: a
// returning context may not inherit another's bindings, and two
// contexts of one share group may not overwrite each other's.
//
// THEY ARE CLEARED BY DIFFERENT EVENTS. A record here is cleared by the object's OWN death
// signal - resource_destroy, delete_vertex_elements, which is what D-L makes the buffer's death
// crossing - and by the share group's own teardown (MGPipeApplierReleaseGroupRecords) when its
// last context goes away. MGPipeApplierReset does NOT touch this struct; MGPipeApplierState's
// working half is what a make-current resets.
struct MGPipeObjectRecords {
// ---- object records: indexed by MGPipeHandle::Slot of kind Buffer /
// VertexElementsCso, and NOT part of the working state. They are cleared by the object's
// OWN death signal and by the group's teardown. Nothing else.
Vector<MGPipeResourceRecord> Resources;
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
// ---- P4a's object records. FIVE MORE TABLES, and the two resource ones are separate
// Vectors rather than more rows of `Resources` above because the slot space is PER
// KIND: a Buffer, a Texture and a Renderbuffer can all hold slot 7 at once, so a
// single slot-indexed table would alias three different objects onto one record. The
// record TYPE is shared - one discriminated descriptor for buffers, every texture
// target and renderbuffers - and the bound is shared; only the table is per kind.
//
// Like the two above they are share-group state: MGPipeApplierReset does not touch
// them, and only the object's own death signal and the group's teardown clear them.
Vector<MGPipeResourceRecord> TextureResources;
Vector<MGPipeResourceRecord> RenderbufferResources;
Vector<MGPipeSamplerCsoRecord> SamplerCsos;
Vector<MGPipeSamplerViewRecord> SamplerViewCsos;
Vector<MGPipeShaderCsoRecord> ShaderCsos;
// The ShaderCso COMPOSITE band's records, indexed by (slot - the band's base), for the
// same reason MGPipeSlotAllocator keeps the band in a table of its own: the band
// starts at 983040, so one program-pipeline composite in the slot-indexed vector above
// would grow it to ~983k records of ~240 bytes each. THE SERVER STILL NEVER LEARNS IT
// IS A COMPOSITE - the split is an indexing detail on this side of the wire, the
// handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it
// exactly as they name any other program.
Vector<MGPipeShaderCsoRecord> CompositeShaderCsos;
// AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot,
// for the reason MGPipeFramebufferRecord states: the two bound-target records the phase
// started with could not describe a framebuffer that is bound to neither binding, and
// the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand
// Espryt exactly that.
//
// IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also
// its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared
// between contexts, but its record is addressed by a slot out of one global allocator,
// so nothing aliases across a switch - and dropping the table would leave a
// DSA-only framebuffer with no record and no event that would ever re-emit one (the
// client's suppressor invalidation re-emits the two BOUND records and nothing else).
//
// P14 S5 keys it by SHARE GROUP like its siblings, which is safe for the reason just
// given: the slot space is global, so a group's table can only ever answer for handles
// its own contexts minted, and a shared group holding both contexts' records is a wider
// table rather than an aliasing one.
Vector<MGPipeFramebufferRecord> FramebufferRecords;
};
// The per-context half. Everything here is cleared by MGPipeApplierReset, EXCEPT the serials,
// which only ever advance (see there).
struct MGPipeWorkingState {
// Indexed by slot; slot 0 is the reserved null handle and is never live // Indexed by slot; slot 0 is the reserved null handle and is never live
// (MGPipeHandles.h kMGPipeFirstAllocatableSlot). // (MGPipeHandles.h kMGPipeFirstAllocatableSlot).
Vector<MGPipeRenderStateCsoRecord> RenderStateCsos; Vector<MGPipeRenderStateCsoRecord> RenderStateCsos;
@@ -553,69 +634,6 @@ namespace MobileGL::MG_Pipe {
Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only
Uint32 PatchCarrierDivergences = 0; // cumulative Uint32 PatchCarrierDivergences = 0; // cumulative
// ---- P3a (D-G4). ----
//
// THE TWO HALVES BELOW HAVE DIFFERENT LIVES, and MGPipeApplierReset is where the
// difference is spent: the OBJECT RECORDS describe GL objects and outlive a
// make-current; the WORKING STATE describes what the next draw fetches with and does
// not. Reading the whole block as "per context" is what dropped a shared buffer's
// record at every context switch and made the write that followed it disappear.
// ---- object records: indexed by MGPipeHandle::Slot of kind Buffer /
// VertexElementsCso, and NOT part of the working state.
//
// A GL object lives in a SHARE GROUP, not in a context: a buffer created before a
// make-current is the same buffer, with the same storage, after it, and its record is
// the only thing the backend has left to read that storage's extent and mutation
// serial out of (D-A4 re-keys IsBufferDrawClean onto exactly those two). Dropping the
// records at a make-current would therefore make every subsequent glBufferSubData on a
// pre-existing buffer resolve to nothing and be refused - a lost write, in a build
// where the refusal's assertion has compiled out.
//
// They are cleared by the object's OWN death signal - resource_destroy,
// delete_vertex_elements, which is what D-L makes the buffer's death crossing - and by
// MGPipeApplierReleaseObjectRecords when the served context and its applier go away.
// Nothing else.
Vector<MGPipeResourceRecord> Resources;
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
// ---- P4a's object records. FIVE MORE TABLES, and the two resource ones are separate
// Vectors rather than more rows of `Resources` above because the slot space is PER
// KIND: a Buffer, a Texture and a Renderbuffer can all hold slot 7 at once, so a
// single slot-indexed table would alias three different objects onto one record. The
// record TYPE is shared - one discriminated descriptor for buffers, every texture
// target and renderbuffers - and the bound is shared; only the table is per kind.
//
// Like the two above they are share-group state: MGPipeApplierReset does not touch
// them, and only the object's own death signal and MGPipeApplierReleaseObjectRecords
// clear them.
Vector<MGPipeResourceRecord> TextureResources;
Vector<MGPipeResourceRecord> RenderbufferResources;
Vector<MGPipeSamplerCsoRecord> SamplerCsos;
Vector<MGPipeSamplerViewRecord> SamplerViewCsos;
Vector<MGPipeShaderCsoRecord> ShaderCsos;
// The ShaderCso COMPOSITE band's records, indexed by (slot - the band's base), for the
// same reason MGPipeSlotAllocator keeps the band in a table of its own: the band
// starts at 983040, so one program-pipeline composite in the slot-indexed vector above
// would grow it to ~983k records of ~240 bytes each. THE SERVER STILL NEVER LEARNS IT
// IS A COMPOSITE - the split is an indexing detail on this side of the wire, the
// handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it
// exactly as they name any other program.
Vector<MGPipeShaderCsoRecord> CompositeShaderCsos;
// AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot,
// for the reason MGPipeFramebufferRecord states: the two bound-target records the phase
// started with could not describe a framebuffer that is bound to neither binding, and
// the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand
// Espryt exactly that.
//
// IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also
// its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared
// between contexts, but its record is addressed by a slot out of one global allocator,
// so nothing aliases across a switch - and dropping the table would leave a
// DSA-only framebuffer with no record and no event that would ever re-emit one (the
// client's suppressor invalidation re-emits the two BOUND records and nothing else).
Vector<MGPipeFramebufferRecord> FramebufferRecords;
// Every call this applier REFUSED because it named a record this applier does not // Every call this applier REFUSED because it named a record this applier does not
// have: an unknown slot, a slot that is not live, or a generation that has moved on // have: an unknown slot, a slot that is not live, or a generation that has moved on
// under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no // under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no
@@ -911,6 +929,53 @@ namespace MobileGL::MG_Pipe {
} }
#endif #endif
};
// =====================================================================================
// P14 S5: ONE CONTEXT'S APPLIER = ITS OWN WORKING STATE + ITS SHARE GROUP'S RECORDS.
// =====================================================================================
//
// THE INHERITANCE IS THE POINT AND NOT A SHORTCUT: everything the backend spells as
// `MGPipeApplier().X` for a WORKING field keeps working unchanged (there are ~380 such reads
// in MG_Backend), and every read of an OBJECT record moves to `MGPipeApplier().Objects.X` -
// which is what makes the two lifetimes visible at exactly the sites where confusing them
// would be a defect. The reference is to a MGPipeObjectRecords that OUTLIVES this object by
// construction: the registry owns the group's records and this context's applier together,
// and drops them in that order.
//
// A context that started its own share group still has one - a group of one - so every
// applier has somewhere for its objects to live and there is no "no group" case.
struct MGPipeApplierState : MGPipeWorkingState {
// The share group this context's objects live in.
MGPipeObjectRecords& Objects;
// ---- THE OBJECT TABLES, UNDER THE NAMES EVERY CALLER ALREADY SPELLS. --------------
//
// These nine are the group's tables, not copies of them: ~200 call sites in
// MG_Backend, MG_IntegrationTest and MG_Test read `st.Resources`, `state.TextureResources`
// and their siblings, and the name they use has ALWAYS meant "the share group's record
// for this object". Binding them here keeps every one of those reads correct by
// construction - through the group its own context belongs to - instead of turning a
// keying change into a renaming change. `Objects.X` and `X` therefore name the same
// vector; this file spells the group half explicitly where it CREATES the two halves,
// the backend keeps the name it has.
Vector<MGPipeResourceRecord>& Resources;
Vector<MGPipeVertexElementsRecord>& VertexElementsCsos;
Vector<MGPipeResourceRecord>& TextureResources;
Vector<MGPipeResourceRecord>& RenderbufferResources;
Vector<MGPipeSamplerCsoRecord>& SamplerCsos;
Vector<MGPipeSamplerViewRecord>& SamplerViewCsos;
Vector<MGPipeShaderCsoRecord>& ShaderCsos;
Vector<MGPipeShaderCsoRecord>& CompositeShaderCsos;
Vector<MGPipeFramebufferRecord>& FramebufferRecords;
explicit MGPipeApplierState(MGPipeObjectRecords& objects)
: Objects(objects), Resources(objects.Resources), VertexElementsCsos(objects.VertexElementsCsos),
TextureResources(objects.TextureResources), RenderbufferResources(objects.RenderbufferResources),
SamplerCsos(objects.SamplerCsos), SamplerViewCsos(objects.SamplerViewCsos),
ShaderCsos(objects.ShaderCsos), CompositeShaderCsos(objects.CompositeShaderCsos),
FramebufferRecords(objects.FramebufferRecords) {}
// ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than // ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than
// members because the storage moved under them and their callers must not have to know // members because the storage moved under them and their callers must not have to know
// it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members // it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members
@@ -932,9 +997,65 @@ namespace MobileGL::MG_Pipe {
const MGPFramebufferState* ReadFramebuffer() const; const MGPFramebufferState* ReadFramebuffer() const;
}; };
// The monolith's single applier. Under split there is one per served context. // The calling thread's applier. On a thread that belongs to a served session this is that
// session's CURRENT CONTEXT's working state bound to its share group's object records; on
// every other thread - the client process, the in-process client+server shape, a unit case,
// the whole monolith - it is the process-wide applier, which is what it has always been and
// is byte-for-byte the old behaviour.
MGPipeApplierState& MGPipeApplier(); MGPipeApplierState& MGPipeApplier();
// =====================================================================================
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT?
// =====================================================================================
//
// THE SAME SHAPE AS P14 S2's gPipeInputs RESOLVER AND S4's NATIVE TUPLE KEY, one level down:
// a process-wide hook the server's session registry installs ONCE, which answers for the
// CALLING thread. Nothing in MG_Backend or MG_Impl names a session, and no call site moved.
//
// THE KEY IS TWO PAIRS, because the two halves have two owners:
//
// {SessionKey, ContextToken} the WORKING state, one per served context;
// {SessionKey, ShareGroupKey} the OBJECT records, one per share group within the session.
//
// A SHARE GROUP DOES NOT CROSS A SESSION, and this is not a limitation: an EGL share group
// is a property of one client's connection (EGL 1.5 3.7.1 - contexts of one share group must
// belong to one display), and the two sessions are two connections. Cross-session sharing is
// an explicit non-goal of the phase, so keying the first by session is the whole of the rule.
//
// REGISTRATION, NOT INFERENCE. The session layer registers a context the moment the
// CreateContext control frame reaches the apply thread (ServerSession::CreateContext) and
// unregisters it at DestroyContext, so the resolver above stays a two-word read of state the
// session already keeps (its current token) and never has to reach into the session's map
// from a thread that does not own it. A context that was never registered resolves to its
// session's "no context" applier, which is the same object the session's bring-up reads.
struct MGPipeApplierKey {
Uint64 SessionKey = 0;
Uint64 ContextToken = 0;
};
// Answers false for "this thread is not a session's" (the process-wide applier). Installed by
// the session registry; null in every process that serves no session.
using MGPipeApplierKeyResolver = Bool (*)(MGPipeApplierKey* outKey);
void MGPipeSetApplierKeyResolver(MGPipeApplierKeyResolver resolver);
// Mints (or finds) the working state for {session, context} and the object records of the
// share group it belongs to. `shareGroupToken` is the frontend's own share-group identity
// (EGLState::CreateContext mints it, a sharing context inherits its parent's); 0 means this
// context started its own group, and is keyed by the context token in a namespace of its own
// so it cannot alias a group that happens to carry that number.
void MGPipeApplierRegisterContext(Uint64 sessionKey, Uint64 contextToken, Uint64 shareGroupToken);
// Drops {session, context}'s working state, and its share group's object records when the
// last context of that group has gone (MGPipeApplierReleaseObjectRecords' scope, per group
// instead of per process).
void MGPipeApplierUnregisterContext(Uint64 sessionKey, Uint64 contextToken);
// Every context of a served session, at session teardown.
void MGPipeApplierReleaseSession(Uint64 sessionKey);
// The observables that make the keying testable without reading a private map: how many
// groups one context's session holds, and how many object records the group the CALLING
// thread is in holds. Both are the numbers a case asserts on to tell "isolated" from
// "shared" apart from the behaviour they exist to explain.
Uint64 MGPipeApplierShareGroupCountForTesting(Uint64 sessionKey);
// --------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------
// P5e: THE BARRIERED PREDICATE (MG_Remote/CONTRACT-P5E.md §2.1, ruling 3) // P5e: THE BARRIERED PREDICATE (MG_Remote/CONTRACT-P5E.md §2.1, ruling 3)
// --------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------
+24 -5
View File
@@ -72,8 +72,9 @@
// that the expansion, the two generated tables and this number agree. // that the expansion, the two generated tables and this number agree.
// //
// class entries group (as the plan tabulates it) // class entries group (as the plan tabulates it)
// kScreen 12 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the // kScreen 13 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
// appended server-side fence wait 1 and P5c's applier_reset 1 // appended server-side fence wait 1 and P5c's applier_reset 1 and
// P14 S1's bind_context 1
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2 // kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
// kCtxCso 13 CSO create/bind/delete // kCtxCso 13 CSO create/bind/delete
// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state // kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state
@@ -84,7 +85,7 @@
// P5b-appended verbs (bind_shader_image, patch_parameter, // P5b-appended verbs (bind_shader_image, patch_parameter,
// bind_stream_output, set_storage_block_binding, // bind_stream_output, set_storage_block_binding,
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md) // copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
// total 80 // total 81
// //
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they // Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
// do not add up to a set of UNIQUE records and this file has to hold unique records: // do not add up to a set of UNIQUE records and this file has to hold unique records:
@@ -145,9 +146,16 @@
// read_pixels_to_buffer and get_texture_image_to_buffer, the pack-buffer halves of the two // read_pixels_to_buffer and get_texture_image_to_buffer, the pack-buffer halves of the two
// readbacks, answered by nothing (ID-57's fire-and-forget). Like P5b's five they reach // readbacks, answered by nothing (ID-57's fire-and-forget). Like P5b's five they reach
// WireVerbSink only and have no monolith producer. 83 unique records. // WireVerbSink only and have no monolith producer. 83 unique records.
// - P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended ONE more (opcode 84):
// bind_context, the in-band record that carries the client's CURRENT context token. It has
// the applier_reset shape - a kScreen control record, no MGPipeApply* entry point, no
// monolith producer - because what it moves is session attribution, not context state. The
// design's per-record ContextSlot was NOT taken here: the token is announced at the edge
// (a context change) instead of stamped on every verb, which costs one record per
// make-current and leaves the 71 context-family payloads untouched. 84 unique records.
// --------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------
#define MGP_CALL_LIST_DOCUMENTED_COUNT 83 #define MGP_CALL_LIST_DOCUMENTED_COUNT 84
// clang-format off // clang-format off
#define MGP_CALL_LIST(X) \ #define MGP_CALL_LIST(X) \
@@ -355,7 +363,18 @@
/* ---- has an MGPipeApply* entry point or a monolith producer: monolith reads into its ---- */ \ /* ---- has an MGPipeApply* entry point or a monolith producer: monolith reads into its ---- */ \
/* ---- driver PBO inside the backend's own ReadPixels, as before (G1). ---- */ \ /* ---- driver PBO inside the backend's own ReadPixels, as before (G1). ---- */ \
X(ReadPixelsToBuffer, MGPReadbackToBuffer, kCtxVerb, kNone, kWaitNone) \ X(ReadPixelsToBuffer, MGPReadbackToBuffer, kCtxVerb, kNone, kWaitNone) \
X(GetTextureImageToBuffer, MGPReadbackToBuffer, kCtxObject, kNone, kWaitNone) X(GetTextureImageToBuffer, MGPReadbackToBuffer, kCtxObject, kNone, kWaitNone) \
/* ---- APPENDED BY P14 S1 (docs/Disaggregated/design/11-state-ownership.md), opcode 84: ---- */ \
/* ---- the in-band context binding. THE WHOLE RECORD IS ONE Uint64: the client's current ---- */ \
/* ---- context token, 0 meaning "no context" (a release). It is emitted at the make-current ---- */ \
/* ---- edge, ahead of the block's client-side resets and ahead of applier_reset, so the ---- */ \
/* ---- server attributes every record that follows to the context that produced it - the ---- */ \
/* ---- multi-context shape the final design reaches without stamping a slot on all 71 ---- */ \
/* ---- context-family rows. kScreen like applier_reset: it is a whole-session edge, not a ---- */ \
/* ---- context-family verb, and it has no MGPipeApply* entry point and no monolith producer. ---- */ \
/* ---- kWaitNone: the apply reads no client memory and the record's placement in the ring ---- */ \
/* ---- is the whole of its ordering. ---- */ \
X(BindContext, MGPBindContext, kScreen, kNone, kWaitNone)
// clang-format on // clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): // Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
+5 -1
View File
@@ -297,6 +297,10 @@
#define MGP_FIELDS_MGPApplierReset(F) \ #define MGP_FIELDS_MGPApplierReset(F) \
F(ContextSerial) F(ContextSerial)
// P14 S1: bind_context's POD, one row for its one member.
#define MGP_FIELDS_MGPBindContext(F) \
F(ClientContextToken)
// P5c rv (CONTRACT-P5C.md §5.3): set_context_values' POD, one row per member. The 15-entry // P5c rv (CONTRACT-P5C.md §5.3): set_context_values' POD, one row per member. The 15-entry
// array is one field, exactly as MGPFramebufferState's Color[8] is one. // array is one field, exactly as MGPFramebufferState's Color[8] is one.
#define MGP_FIELDS_MGPContextValues(F) \ #define MGP_FIELDS_MGPContextValues(F) \
@@ -423,7 +427,7 @@
P(MGPSurfaceInfo) \ P(MGPSurfaceInfo) \
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \ P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
P(MGPCopyFromFramebuffer) \ P(MGPCopyFromFramebuffer) \
P(MGPApplierReset) P(MGPContextValues) \ P(MGPApplierReset) P(MGPBindContext) P(MGPContextValues) \
P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \ P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \ P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
P(StencilFaceState) \ P(StencilFaceState) \
+4 -3
View File
@@ -12,7 +12,7 @@
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result. // Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe. // This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// share group: 12 calls. A null entry means the backend does not implement this // share group: 13 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1). // call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeScreen { struct MGPipeScreen {
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply); void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
@@ -27,6 +27,7 @@ struct MGPipeScreen {
void (*FenceDestroy)(const MGPHandleOnly* payload); void (*FenceDestroy)(const MGPHandleOnly* payload);
void (*FenceWaitServer)(const MGPFenceWait* payload); void (*FenceWaitServer)(const MGPFenceWait* payload);
void (*ApplierReset)(const MGPApplierReset* payload); void (*ApplierReset)(const MGPApplierReset* payload);
void (*BindContext)(const MGPBindContext* payload);
}; };
// context: 71 calls. A null entry means the backend does not implement this // context: 71 calls. A null entry means the backend does not implement this
@@ -105,9 +106,9 @@ struct MGPipeContext {
void (*GetTextureImageToBuffer)(const MGPReadbackToBuffer* payload); void (*GetTextureImageToBuffer)(const MGPReadbackToBuffer* payload);
}; };
inline constexpr SizeT kMGPipeScreenCallCount = 12; inline constexpr SizeT kMGPipeScreenCallCount = 13;
inline constexpr SizeT kMGPipeContextCallCount = 71; inline constexpr SizeT kMGPipeContextCallCount = 71;
inline constexpr SizeT kMGPipeCallCount = 83; inline constexpr SizeT kMGPipeCallCount = 84;
// A table that is not exactly its call count of function pointers has grown a // A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about. // member that no generator knows about.
@@ -348,3 +348,7 @@ inline void MGP_ReadPixelsToBuffer(const MGPReadbackToBuffer* payload) {
inline void MGP_GetTextureImageToBuffer(const MGPReadbackToBuffer* payload) { inline void MGP_GetTextureImageToBuffer(const MGPReadbackToBuffer* payload) {
gMGPipeContext.GetTextureImageToBuffer(payload); gMGPipeContext.GetTextureImageToBuffer(payload);
} }
inline void MGP_BindContext(const MGPBindContext* payload) {
gMGPipeScreen.BindContext(payload);
}
+9 -1
View File
@@ -114,6 +114,7 @@ inline Bool MGPipeVerify(const MGPStreamOutputBind& a, const MGPStreamOutputBind
inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField); inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField);
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField); inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField); inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
inline Bool MGPipeVerify(const MGPBindContext& a, const MGPBindContext& b, const char** outField);
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField); inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField); inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField); inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField);
@@ -269,6 +270,8 @@ struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {};
template <> template <>
struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {}; struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
template <> template <>
struct MGPipeHasFieldVerifier<MGPBindContext> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {}; struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {};
template <> template <>
struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {}; struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {};
@@ -690,6 +693,11 @@ inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, con
return true; return true;
} }
inline Bool MGPipeVerify(const MGPBindContext& a, const MGPBindContext& b, const char** outField) {
MGP_FIELDS_MGPBindContext(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField) { inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField) {
MGP_FIELDS_MGPContextValues(MGP_VERIFY_FIELD) MGP_FIELDS_MGPContextValues(MGP_VERIFY_FIELD)
return true; return true;
@@ -757,4 +765,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
#undef MGP_VERIFY_FIELD #undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 83; inline constexpr SizeT kMGPipeVerifiedPayloadCount = 84;
+16 -1
View File
@@ -153,7 +153,8 @@ enum class MGPWireOp : Uint16 {
DeleteStreamOutput = 81, DeleteStreamOutput = 81,
ReadPixelsToBuffer = 82, ReadPixelsToBuffer = 82,
GetTextureImageToBuffer = 83, GetTextureImageToBuffer = 83,
kOpCount = 84, BindContext = 84,
kOpCount = 85,
}; };
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is // THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
@@ -258,6 +259,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
/* 81 DeleteStreamOutput */ static_cast<Uint32>(kNone), /* 81 DeleteStreamOutput */ static_cast<Uint32>(kNone),
/* 82 ReadPixelsToBuffer */ static_cast<Uint32>(kNone), /* 82 ReadPixelsToBuffer */ static_cast<Uint32>(kNone),
/* 83 GetTextureImageToBuffer */ static_cast<Uint32>(kNone), /* 83 GetTextureImageToBuffer */ static_cast<Uint32>(kNone),
/* 84 BindContext */ static_cast<Uint32>(kNone),
}; };
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount), static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -361,6 +363,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kCtxVerb), /* 81 DeleteStreamOutput */ static_cast<Uint8>(kCtxVerb),
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kCtxVerb), /* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kCtxVerb),
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kCtxObject), /* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kCtxObject),
/* 84 BindContext */ static_cast<Uint8>(kScreen),
}; };
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) == static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount), static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -468,6 +471,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kWaitNone), /* 81 DeleteStreamOutput */ static_cast<Uint8>(kWaitNone),
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kWaitNone), /* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kWaitNone),
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kWaitNone), /* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kWaitNone),
/* 84 BindContext */ static_cast<Uint8>(kWaitNone),
}; };
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) == static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount), static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -1198,6 +1202,14 @@ static_assert(sizeof(MGPWireRec_GetTextureImageToBuffer) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackToBuffer) + 7u) & ~SizeT(7u)), ((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackToBuffer) + 7u) & ~SizeT(7u)),
"MGPWireRec_GetTextureImageToBuffer gained padding; the wire format moved"); "MGPWireRec_GetTextureImageToBuffer gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindContext {
MGPWireRecHeader Header;
MGPBindContext Payload;
};
static_assert(sizeof(MGPWireRec_BindContext) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPBindContext) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindContext gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) { [[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call, MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining)); static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
@@ -1496,6 +1508,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
case MGPWireOp::GetTextureImageToBuffer: case MGPWireOp::GetTextureImageToBuffer:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImageToBuffer, "GetTextureImageToBuffer"); MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImageToBuffer, "GetTextureImageToBuffer");
break; break;
case MGPWireOp::BindContext:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindContext, "BindContext");
break;
case MGPWireOp::kInvalid: case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount: case MGPWireOp::kOpCount:
default: default:
@@ -431,6 +431,8 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
{"MGPCopyFromFramebuffer.SubImage", offsetof(MGPCopyFromFramebuffer, SubImage), sizeof(((MGPCopyFromFramebuffer*)nullptr)->SubImage)}, {"MGPCopyFromFramebuffer.SubImage", offsetof(MGPCopyFromFramebuffer, SubImage), sizeof(((MGPCopyFromFramebuffer*)nullptr)->SubImage)},
{"MGPApplierReset", 0, sizeof(MGPApplierReset)}, {"MGPApplierReset", 0, sizeof(MGPApplierReset)},
{"MGPApplierReset.ContextSerial", offsetof(MGPApplierReset, ContextSerial), sizeof(((MGPApplierReset*)nullptr)->ContextSerial)}, {"MGPApplierReset.ContextSerial", offsetof(MGPApplierReset, ContextSerial), sizeof(((MGPApplierReset*)nullptr)->ContextSerial)},
{"MGPBindContext", 0, sizeof(MGPBindContext)},
{"MGPBindContext.ClientContextToken", offsetof(MGPBindContext, ClientContextToken), sizeof(((MGPBindContext*)nullptr)->ClientContextToken)},
{"MGPContextValues", 0, sizeof(MGPContextValues)}, {"MGPContextValues", 0, sizeof(MGPContextValues)},
{"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)}, {"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)},
{"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)}, {"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)},
@@ -762,4 +764,5 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
{"DeleteStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::DeleteStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)}, {"DeleteStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::DeleteStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ReadPixelsToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)}, {"ReadPixelsToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)}, {"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BindContext", "MGPBindContext", static_cast<Uint64>(MGPWireOp::BindContext), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
}; };
+46
View File
@@ -1120,6 +1120,52 @@ namespace MobileGL::MG_Remote::Client {
Uint64 g_applierResetContextSerial = 0; Uint64 g_applierResetContextSerial = 0;
} // namespace } // namespace
// P14 S1: the EGL context lifecycle's two frames. NO SESSION IS AN ANSWER, not a Fatal:
// eglCreateContext/eglDestroyContext can legally run before ClientSession::Start (the
// bring-up window) and on the server role's own thread, and in both there is no wire for the
// frame to cross - the caller falls back to what it did before this slice, which is to keep
// the client's own EGL books and nothing else.
namespace {
// The window test both senders below open with, and the reason it is a function rather
// than two copies of the same if: a sender that answered Refused for "no wire" would make
// eglCreateContext return EGL_NO_CONTEXT during the bring-up, which is the exact failure
// the pre-Start exception exists to prevent.
Bool ContextFramesCanCross() {
ClientSession* session = ClientSession::Active();
return session != nullptr && session->Started() &&
!g_clientTablesUninstalled.load(std::memory_order_acquire);
}
} // namespace
ContextFrameEmit SendCreateContextFrame(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags) {
if (!ContextFramesCanCross()) return ContextFrameEmit::NoWire;
return Server::ServerCreateEGLContext(clientContextToken, shareGroupToken, flags) ? ContextFrameEmit::Sent
: ContextFrameEmit::Refused;
}
ContextFrameEmit SendDestroyContextFrame(Uint64 clientContextToken) {
if (!ContextFramesCanCross()) return ContextFrameEmit::NoWire;
return Server::ServerDestroyEGLContext(clientContextToken) ? ContextFrameEmit::Sent
: ContextFrameEmit::Refused;
}
// P14 S1: bind_context. The same four windows as EmitApplierResetRecord below answer false,
// and for the same reason: a binding that cannot cross leaves the session on the default it
// already has (one context per session, every record its own), which is what the server
// answers with no record at all. The token is minted by EGLState and passed in rather than
// read here, so this table holds no second opinion about which context is current.
Bool EmitBindContextRecord(Uint64 clientContextToken) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || !session->Started()) return false;
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) return false;
MG_Pipe::MGPBindContext record{};
record.ClientContextToken = clientContextToken;
session->EmitAndWait(MGPWireOp::BindContext, &record, sizeof(record), nullptr, 0, nullptr, 0,
nullptr);
++g_emitted;
return true;
}
Bool EmitApplierResetRecord() { Bool EmitApplierResetRecord() {
// NO SESSION, NO RECORD - and false rather than the RequireSession Fatal, because a // NO SESSION, NO RECORD - and false rather than the RequireSession Fatal, because a
// validate can legitimately prime with a transport CONFIGURED but no live session: // validate can legitimately prime with a transport CONFIGURED but no live session:
+37
View File
@@ -125,6 +125,43 @@ namespace MobileGL::MG_Remote::Client {
// caller then makes the direct call the record replaced. // caller then makes the direct call the record replaced.
Bool EmitApplierResetRecord(); Bool EmitApplierResetRecord();
// ---- P14 S1: the EGL context lifecycle's control-plane half -----------------------------
//
// NOT ring records and NOT routed rows: eglCreateContext/eglDestroyContext are the EGL
// lifecycle, which the catalogue deliberately keeps off the record plane (PipeCalls.def's
// "the eight EGL lifecycle entry points stay virtual functions on pActiveBackendObject"),
// so they cross as two SurfaceOp kinds through the same blocking frame channel the nine
// surface forwarders use. Called by MG_Impl's EGLImpl on the app thread, in the same order
// as the EGL calls themselves - which is the ordering the server's context table needs.
//
// THREE ANSWERS, NOT TWO, for the reason ObjectDeathEmit below has three: "the server said
// no" and "there is no wire for the question to cross" are different facts and the caller's
// answer to each is different. NoWire is the bring-up window before ClientSession::Start and
// the server role's own thread - in both, the pre-S1 shape (the client keeps its own EGL
// books and no server table exists) is the correct one and the context must be KEPT; Refused
// is a server that answered, and eglCreateContext turns it into EGL_NO_CONTEXT, because a
// context whose token the server did not table is one the first bind_context after it would
// be refused for.
enum class ContextFrameEmit : Uint8 { Sent, NoWire, Refused };
ContextFrameEmit SendCreateContextFrame(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags);
// A destroy has the same three answers; the client's EGL book is dropped either way (that is
// the caller's own state machine), and what a refusal changes is only what the server holds.
ContextFrameEmit SendDestroyContextFrame(Uint64 clientContextToken);
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// Emitted on the GL thread from EGLImpl::MakeCurrent, after the switch has succeeded and
// before eglMakeCurrent returns - which puts it ahead of the applier_reset the NEXT GL call's
// validate emits (PipeFill.cpp's FreshlyPrimed arm), so the reset is already attributed to
// the context that primed. `clientContextToken` is the token
// EGLState minted for the context the calling thread now has current (0 = none, the release
// edge) - the caller reads it rather than this table owning a second copy, so the two planes
// cannot disagree about which context is bound. Returns false - nothing emitted - in exactly
// the windows EmitApplierResetRecord documents (no live session, the pre-Start bring-up, a
// server-role-only fixture, teardown), where there is no wire for the binding to cross and
// the session's single-context default is what the server already answers.
Bool EmitBindContextRecord(Uint64 clientContextToken);
// P5c (rv), CONTRACT-P5C.md §5.3: whether set_context_values can cross right now (a live, // P5c (rv), CONTRACT-P5C.md §5.3: whether set_context_values can cross right now (a live,
// started session, tables not being torn down). PipeFill.cpp gates the record's emission // started session, tables not being torn down). PipeFill.cpp gates the record's emission
// AND the residual-fill skip for its eight fields on this one answer, so the two halves // AND the residual-fill skip for its eight fields on this one answer, so the two halves
+84 -26
View File
@@ -19,6 +19,7 @@
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <mutex> #include <mutex>
#include <unordered_map>
#include <flatbuffers/flatbuffers.h> #include <flatbuffers/flatbuffers.h>
@@ -96,37 +97,92 @@ namespace MobileGL::MG_Remote {
// PH-1 (3): the latch's state. Armed once, by the session child; latched once, by the // PH-1 (3): the latch's state. Armed once, by the session child; latched once, by the
// first fault. The line is written under the mutex BEFORE the release store that // first fault. The line is written under the mutex BEFORE the release store that
// publishes `g_latched`, so a reader that saw true reads a whole line. // publishes `g_latched`, so a reader that saw true reads a whole line.
std::atomic<bool> g_latchArmed{false}; //
std::atomic<bool> g_latched{false}; // P14 S2: ONE OF THESE PER SESSION (FatalFunnel.h's domain block). A domain's storage is
std::atomic<std::uint64_t> g_latchCount{0}; // heap-allocated by FatalLatchDomainOpen and lives until FatalLatchDomainClose; domain 0
std::mutex g_latchMutex; // is this process-wide one, which is what every thread that binds nothing uses.
MGFatalFamily g_latchedFamily = MGFatalFamily::ProtocolCorruption; struct LatchState {
char g_latchedLine[512] = {}; int id = 0;
std::atomic<bool> armed{false};
std::atomic<bool> latched{false};
std::atomic<std::uint64_t> count{0};
std::mutex mutex;
MGFatalFamily latchedFamily = MGFatalFamily::ProtocolCorruption;
char latchedLine[512] = {};
};
LatchState g_defaultLatch;
thread_local LatchState* t_latch = &g_defaultLatch;
std::mutex g_domainsMutex;
std::unordered_map<int, LatchState*> g_domains;
int g_nextDomain = 1;
LatchState& Latch() { return *t_latch; }
} // namespace } // namespace
void ArmSessionLatch() { g_latchArmed.store(true, std::memory_order_release); } int FatalLatchDomainOpen() {
const std::lock_guard<std::mutex> lock(g_domainsMutex);
if (g_nextDomain <= 0 || g_nextDomain > kMaxLatchDomains) return 0;
LatchState* state = new LatchState{};
state->id = g_nextDomain++;
g_domains.emplace(state->id, state);
return state->id;
}
void FatalLatchDomainClose(int domain) {
if (domain == 0) return;
LatchState* state = nullptr;
{
const std::lock_guard<std::mutex> lock(g_domainsMutex);
const auto found = g_domains.find(domain);
if (found == g_domains.end()) return;
state = found->second;
g_domains.erase(found);
}
delete state;
}
int FatalLatchDomainBind(int domain) {
LatchState* const previous = t_latch;
if (domain == 0) {
t_latch = &g_defaultLatch;
return previous->id;
}
{
const std::lock_guard<std::mutex> lock(g_domainsMutex);
const auto found = g_domains.find(domain);
t_latch = found == g_domains.end() ? &g_defaultLatch : found->second;
}
return previous->id;
}
void ArmSessionLatch() { Latch().armed.store(true, std::memory_order_release); }
void ResetSessionLatch() { void ResetSessionLatch() {
const std::lock_guard<std::mutex> lock(g_latchMutex); LatchState& latch = Latch();
g_latchArmed.store(false, std::memory_order_release); const std::lock_guard<std::mutex> lock(latch.mutex);
g_latchCount.store(0, std::memory_order_relaxed); latch.armed.store(false, std::memory_order_release);
g_latchedFamily = MGFatalFamily::ProtocolCorruption; latch.count.store(0, std::memory_order_relaxed);
g_latchedLine[0] = '\0'; latch.latchedFamily = MGFatalFamily::ProtocolCorruption;
g_latched.store(false, std::memory_order_release); latch.latchedLine[0] = '\0';
latch.latched.store(false, std::memory_order_release);
} }
bool SessionLatchArmed() { return g_latchArmed.load(std::memory_order_acquire); } bool SessionLatchArmed() { return Latch().armed.load(std::memory_order_acquire); }
bool SessionLatched() { return g_latched.load(std::memory_order_acquire); } bool SessionLatched() { return Latch().latched.load(std::memory_order_acquire); }
MGFatalFamily SessionLatchedFamily() { MGFatalFamily SessionLatchedFamily() {
const std::lock_guard<std::mutex> lock(g_latchMutex); LatchState& latch = Latch();
return g_latchedFamily; const std::lock_guard<std::mutex> lock(latch.mutex);
return latch.latchedFamily;
} }
const char* SessionLatchedLine() { return g_latchedLine; } const char* SessionLatchedLine() { return Latch().latchedLine; }
std::uint64_t SessionLatchCount() { return g_latchCount.load(std::memory_order_relaxed); } std::uint64_t SessionLatchCount() { return Latch().count.load(std::memory_order_relaxed); }
bool SessionLatch(MGFatalFamily family, const char* fmt, ...) { bool SessionLatch(MGFatalFamily family, const char* fmt, ...) {
char line[512]; char line[512];
@@ -135,23 +191,25 @@ namespace MobileGL::MG_Remote {
FormatFaultLine(line, fmt, args); FormatFaultLine(line, fmt, args);
va_end(args); va_end(args);
LatchState& latch = Latch();
// UNARMED IS SessionFail, byte for byte: the inproc server, the client and every unit // UNARMED IS SessionFail, byte for byte: the inproc server, the client and every unit
// death test keep the death they had (FatalFunnel.h's block says why inproc must). // death test keep the death they had (FatalFunnel.h's block says why inproc must).
if (!g_latchArmed.load(std::memory_order_acquire)) { if (!latch.armed.load(std::memory_order_acquire)) {
SessionFail(family, "%s", line); SessionFail(family, "%s", line);
} }
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed); g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
g_latchCount.fetch_add(1, std::memory_order_relaxed); latch.count.fetch_add(1, std::memory_order_relaxed);
LogFaultLine(line); LogFaultLine(line);
bool first = false; bool first = false;
{ {
const std::lock_guard<std::mutex> lock(g_latchMutex); const std::lock_guard<std::mutex> lock(latch.mutex);
if (!g_latched.load(std::memory_order_relaxed)) { if (!latch.latched.load(std::memory_order_relaxed)) {
std::snprintf(g_latchedLine, sizeof(g_latchedLine), "%s", line); std::snprintf(latch.latchedLine, sizeof(latch.latchedLine), "%s", line);
g_latchedFamily = family; latch.latchedFamily = family;
g_latched.store(true, std::memory_order_release); latch.latched.store(true, std::memory_order_release);
first = true; first = true;
} }
} }
+33 -2
View File
@@ -62,8 +62,10 @@ namespace MobileGL::MG_Remote {
// crash (ID-P7-1: "下一连接照常服务" is the supervisor's, not the latch's). // crash (ID-P7-1: "下一连接照常服务" is the supervisor's, not the latch's).
// //
// WHERE IT IS ARMED, AND WHY ONLY THERE. ArmSessionLatch() is called by exactly one site: // WHERE IT IS ARMED, AND WHY ONLY THERE. ArmSessionLatch() is called by exactly one site:
// ServerMain::RunSession, i.e. the spawn / TCP session child (with or without --serve), whose // ServerMain::RunSession, i.e. a served session - the spawn / TCP session child (with or
// process IS the session. Unarmed, SessionLatch IS SessionFail - same line, same abort - and // without --serve), whose process IS the session, and (P14 S2) each session the in-process
// display server runs, which arms ITS OWN latch domain so a fault there ends that session and
// no other. Unarmed, SessionLatch IS SessionFail - same line, same abort - and
// that is the answer everywhere else, deliberately: // that is the answer everywhere else, deliberately:
// * INPROC keeps Fatal. CONTRACT-P5's R-2 arms (rules A/B/C, the four honesty arms) are // * INPROC keeps Fatal. CONTRACT-P5's R-2 arms (rules A/B/C, the four honesty arms) are
// Fatal{ProtocolCorruption} in the formal wording, and under inproc the client and the // Fatal{ProtocolCorruption} in the formal wording, and under inproc the client and the
@@ -85,6 +87,10 @@ namespace MobileGL::MG_Remote {
// Between sessions the latch is unarmed, so a fault there is a SessionFail - which in the // Between sessions the latch is unarmed, so a fault there is a SessionFail - which in the
// in-process shape takes the display Activity's process down with it: the crash isolation a // in-process shape takes the display Activity's process down with it: the crash isolation a
// forked session child gave is what running in-process gives up (documented, D5). // forked session child gave is what running in-process gives up (documented, D5).
//
// P14 S2: it now clears the CALLING THREAD'S DOMAIN (below), which between sessions is the
// process default - a session that opened its own domain cannot leave a latch behind for the
// next one to inherit, because the next one does not read that domain at all.
void ResetSessionLatch(); void ResetSessionLatch();
// True once the first fault has latched. One acquire load: DrainRing asks it before every pop. // True once the first fault has latched. One acquire load: DrainRing asks it before every pop.
bool SessionLatched(); bool SessionLatched();
@@ -95,6 +101,31 @@ namespace MobileGL::MG_Remote {
// Every fault this armed process latched or declined after the first (>= 1 once latched). // Every fault this armed process latched or declined after the first (>= 1 once latched).
::std::uint64_t SessionLatchCount(); ::std::uint64_t SessionLatchCount();
// ---- P14 S2: ONE LATCH DOMAIN PER SERVED SESSION ------------------------------------
//
// THE LATCH IS PER SESSION NOW, and that is a correctness requirement of serving several in
// one process, not a tidy-up. Its three readers sit on a session's own threads - the apply
// loop's park, DrainRing's per-pop check, ServerMain's control loop - so a latch raised by
// session A used to stop session B's apply thread mid-ring and end B's control loop: one
// peer's bad bytes took down its neighbours. ResetSessionLatch had the mirror problem: the
// reap of a finished session cleared the latch a live one was standing on.
//
// A DOMAIN is the latch state, and a THREAD IS BOUND TO ONE. SessionRuntime opens a domain per
// served session and ThreadSessionScope binds both of that session's threads to it. A thread
// that has bound nothing uses DOMAIN 0, the process default - which is exactly the old
// behaviour, and is what the client, the inproc one-process shape and every unit case get.
//
// 0 IS RESERVED FOR THE PROCESS DEFAULT and FatalLatchDomainOpen never returns it (0 on
// exhaustion, which degrades to "shares the default" rather than to a crash).
inline constexpr int kMaxLatchDomains = 64;
int FatalLatchDomainOpen();
// The domain's state goes; no thread may still be bound to it (the session's apply thread is
// joined and its control thread's scope has been destroyed by then).
void FatalLatchDomainClose(int domain);
// Binds the CALLING THREAD to `domain`, returning the domain it was bound to before so a
// scope can put it back.
int FatalLatchDomainBind(int domain);
// The session child's exit status when its session ended on a latched fault. Distinct from // The session child's exit status when its session ended on a latched fault. Distinct from
// RunSession's other codes (64-74) so the supervisor's reap line names it; nonzero, so it is // RunSession's other codes (64-74) so the supervisor's reap line names it; nonzero, so it is
// counted in `sessionsFaulted` like every other fault. // counted in `sessionsFaulted` like every other fault.
+16 -3
View File
@@ -51,10 +51,20 @@ namespace MobileGL::MG_Remote {
// cp's one schema addition, on the same append-only terms. // cp's one schema addition, on the same append-only terms.
static_assert(static_cast<Uint8>(SurfaceControlOp::InitCapabilities) == static_assert(static_cast<Uint8>(SurfaceControlOp::InitCapabilities) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities)); static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities));
// P14 S1's two appends, on the same append-only terms. The VALUES are pinned because they
// are wire ABI, and pinned SEPARATELY from the identity cast beside them: the cast would
// stay green if both enums moved together to 20/21, and a peer built before the move
// would then read the two ops as two others.
static_assert(static_cast<Uint8>(SurfaceControlOp::CreateContext) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateContext));
static_assert(static_cast<Uint8>(SurfaceControlOp::DestroyContext) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::DestroyContext));
static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 && static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 && static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10 && static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities) == 11); static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities) == 11 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateContext) == 12 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::DestroyContext) == 13);
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer) == 6); static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer) == 6);
// P12's append, on the same terms: the value is wire ABI. // P12's append, on the same terms: the value is wire ABI.
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::ServerOwned) == 7); static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::ServerOwned) == 7);
@@ -112,7 +122,7 @@ namespace MobileGL::MG_Remote {
// Fatal{ProtocolCorruption} at ServerApplyWireSurfaceOp rather than as // Fatal{ProtocolCorruption} at ServerApplyWireSurfaceOp rather than as
// the missing row it is. // the missing row it is.
if (::flatbuffers::IsOutRange(kind, ::MobileGL::Wire::SurfaceOpKind::InitializeDisplay, if (::flatbuffers::IsOutRange(kind, ::MobileGL::Wire::SurfaceOpKind::InitializeDisplay,
::MobileGL::Wire::SurfaceOpKind::InitCapabilities)) { ::MobileGL::Wire::SurfaceOpKind::DestroyContext)) {
return false; return false;
} }
const SurfaceControlOp op = static_cast<SurfaceControlOp>(static_cast<Uint8>(kind)); const SurfaceControlOp op = static_cast<SurfaceControlOp>(static_cast<Uint8>(kind));
@@ -174,7 +184,8 @@ namespace MobileGL::MG_Remote {
} }
const auto op = ::MobileGL::Wire::CreateSurfaceOp( const auto op = ::MobileGL::Wire::CreateSurfaceOp(
*builder, frame.seq, kind, frame.display, frame.surface, windowKind, nativeToken, *builder, frame.seq, kind, frame.display, frame.surface, windowKind, nativeToken,
frame.width, frame.height, frame.swapInterval, frame.readSurface, frame.context); frame.width, frame.height, frame.swapInterval, frame.readSurface, frame.context,
frame.shareGroupToken, frame.contextFlags);
const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union()); const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope); ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope);
return SurfaceWireError::None; return SurfaceWireError::None;
@@ -195,6 +206,8 @@ namespace MobileGL::MG_Remote {
decoded.width = op.width(); decoded.width = op.width();
decoded.height = op.height(); decoded.height = op.height();
decoded.swapInterval = op.swapInterval(); decoded.swapInterval = op.swapInterval();
decoded.shareGroupToken = op.shareGroupToken();
decoded.contextFlags = op.contextFlags();
if (OpNeedsAWindowBackend(kind)) { if (OpNeedsAWindowBackend(kind)) {
if (op.windowKind() == ::MobileGL::Wire::WindowKind::ServerOwned) { if (op.windowKind() == ::MobileGL::Wire::WindowKind::ServerOwned) {
// P12 (D2). Asked BEFORE the Android refusal below, which it is not: the window is // P12 (D2). Asked BEFORE the Android refusal below, which it is not: the window is
@@ -285,11 +285,13 @@ enum class SurfaceOpKind : uint8_t {
ReleaseResources = 9, ReleaseResources = 9,
SetWindowHandle = 10, SetWindowHandle = 10,
InitCapabilities = 11, InitCapabilities = 11,
CreateContext = 12,
DestroyContext = 13,
MIN = None, MIN = None,
MAX = InitCapabilities MAX = DestroyContext
}; };
inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[12] { inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[14] {
static const SurfaceOpKind values[] = { static const SurfaceOpKind values[] = {
SurfaceOpKind::None, SurfaceOpKind::None,
SurfaceOpKind::InitializeDisplay, SurfaceOpKind::InitializeDisplay,
@@ -302,13 +304,15 @@ inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[12] {
SurfaceOpKind::SetSwapInterval, SurfaceOpKind::SetSwapInterval,
SurfaceOpKind::ReleaseResources, SurfaceOpKind::ReleaseResources,
SurfaceOpKind::SetWindowHandle, SurfaceOpKind::SetWindowHandle,
SurfaceOpKind::InitCapabilities SurfaceOpKind::InitCapabilities,
SurfaceOpKind::CreateContext,
SurfaceOpKind::DestroyContext
}; };
return values; return values;
} }
inline const char * const *EnumNamesSurfaceOpKind() { inline const char * const *EnumNamesSurfaceOpKind() {
static const char * const names[13] = { static const char * const names[15] = {
"None", "None",
"InitializeDisplay", "InitializeDisplay",
"CreateWindowSurface", "CreateWindowSurface",
@@ -321,13 +325,15 @@ inline const char * const *EnumNamesSurfaceOpKind() {
"ReleaseResources", "ReleaseResources",
"SetWindowHandle", "SetWindowHandle",
"InitCapabilities", "InitCapabilities",
"CreateContext",
"DestroyContext",
nullptr nullptr
}; };
return names; return names;
} }
inline const char *EnumNameSurfaceOpKind(SurfaceOpKind e) { inline const char *EnumNameSurfaceOpKind(SurfaceOpKind e) {
if (::flatbuffers::IsOutRange(e, SurfaceOpKind::None, SurfaceOpKind::InitCapabilities)) return ""; if (::flatbuffers::IsOutRange(e, SurfaceOpKind::None, SurfaceOpKind::DestroyContext)) return "";
const size_t index = static_cast<size_t>(e); const size_t index = static_cast<size_t>(e);
return EnumNamesSurfaceOpKind()[index]; return EnumNamesSurfaceOpKind()[index];
} }
@@ -1809,7 +1815,9 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_HEIGHT = 18, VT_HEIGHT = 18,
VT_SWAPINTERVAL = 20, VT_SWAPINTERVAL = 20,
VT_READSURFACE = 22, VT_READSURFACE = 22,
VT_CONTEXT = 24 VT_CONTEXT = 24,
VT_SHAREGROUPTOKEN = 26,
VT_CONTEXTFLAGS = 28
}; };
uint64_t seq() const { uint64_t seq() const {
return GetField<uint64_t>(VT_SEQ, 0); return GetField<uint64_t>(VT_SEQ, 0);
@@ -1844,6 +1852,12 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
uint64_t context() const { uint64_t context() const {
return GetField<uint64_t>(VT_CONTEXT, 0); return GetField<uint64_t>(VT_CONTEXT, 0);
} }
uint64_t shareGroupToken() const {
return GetField<uint64_t>(VT_SHAREGROUPTOKEN, 0);
}
uint32_t contextFlags() const {
return GetField<uint32_t>(VT_CONTEXTFLAGS, 0);
}
template <bool B = false> template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const { bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) && return VerifyTableStart(verifier) &&
@@ -1858,6 +1872,8 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VerifyField<int32_t>(verifier, VT_SWAPINTERVAL, 4) && VerifyField<int32_t>(verifier, VT_SWAPINTERVAL, 4) &&
VerifyField<uint64_t>(verifier, VT_READSURFACE, 8) && VerifyField<uint64_t>(verifier, VT_READSURFACE, 8) &&
VerifyField<uint64_t>(verifier, VT_CONTEXT, 8) && VerifyField<uint64_t>(verifier, VT_CONTEXT, 8) &&
VerifyField<uint64_t>(verifier, VT_SHAREGROUPTOKEN, 8) &&
VerifyField<uint32_t>(verifier, VT_CONTEXTFLAGS, 4) &&
verifier.EndTable(); verifier.EndTable();
} }
}; };
@@ -1899,6 +1915,12 @@ struct SurfaceOpBuilder {
void add_context(uint64_t context) { void add_context(uint64_t context) {
fbb_.AddElement<uint64_t>(SurfaceOp::VT_CONTEXT, context, 0); fbb_.AddElement<uint64_t>(SurfaceOp::VT_CONTEXT, context, 0);
} }
void add_shareGroupToken(uint64_t shareGroupToken) {
fbb_.AddElement<uint64_t>(SurfaceOp::VT_SHAREGROUPTOKEN, shareGroupToken, 0);
}
void add_contextFlags(uint32_t contextFlags) {
fbb_.AddElement<uint32_t>(SurfaceOp::VT_CONTEXTFLAGS, contextFlags, 0);
}
explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb) explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) { : fbb_(_fbb) {
start_ = fbb_.StartTable(); start_ = fbb_.StartTable();
@@ -1922,14 +1944,18 @@ inline ::flatbuffers::Offset<SurfaceOp> CreateSurfaceOp(
int32_t height = 0, int32_t height = 0,
int32_t swapInterval = 0, int32_t swapInterval = 0,
uint64_t readSurface = 0, uint64_t readSurface = 0,
uint64_t context = 0) { uint64_t context = 0,
uint64_t shareGroupToken = 0,
uint32_t contextFlags = 0) {
SurfaceOpBuilder builder_(_fbb); SurfaceOpBuilder builder_(_fbb);
builder_.add_shareGroupToken(shareGroupToken);
builder_.add_context(context); builder_.add_context(context);
builder_.add_readSurface(readSurface); builder_.add_readSurface(readSurface);
builder_.add_nativeToken(nativeToken); builder_.add_nativeToken(nativeToken);
builder_.add_surface(surface); builder_.add_surface(surface);
builder_.add_display(display); builder_.add_display(display);
builder_.add_seq(seq); builder_.add_seq(seq);
builder_.add_contextFlags(contextFlags);
builder_.add_swapInterval(swapInterval); builder_.add_swapInterval(swapInterval);
builder_.add_height(height); builder_.add_height(height);
builder_.add_width(width); builder_.add_width(width);
@@ -70,7 +70,9 @@ extern "C" {
// 4 P11 PAIR: the PairBind message - a socket client's control and aux connections pair by // 4 P11 PAIR: the PairBind message - a socket client's control and aux connections pair by
// the nonce each presents first, not by arrival order. // the nonce each presents first, not by arrival order.
// 5 P11 B2: LinkTerms.adoptTier - the client's persistent-map adoption tier ask (T0 or T2). // 5 P11 B2: LinkTerms.adoptTier - the client's persistent-map adoption tier ask (T0 or T2).
#define MOBILEGL_PROTOCOL_CONTROL_REVISION 5 // 6 P14 S1 (docs/Disaggregated/design/11-state-ownership.md): SurfaceOpKind.CreateContext /
// DestroyContext, plus SurfaceOp.shareGroupToken and SurfaceOp.contextFlags.
#define MOBILEGL_PROTOCOL_CONTROL_REVISION 6
#define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor)) #define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor))
#define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16) #define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16)
+25
View File
@@ -256,6 +256,25 @@ enum SurfaceOpKind : ubyte {
// (Client/BackendObject_Remote.cpp). Without the tag the codec refused it by // (Client/BackendObject_Remote.cpp). Without the tag the codec refused it by
// name and every spawned eglMakeCurrent failed with InprocOnlyOpOnTheWire. // name and every spawned eglMakeCurrent failed with InprocOnlyOpOnTheWire.
InitCapabilities = 11, InitCapabilities = 11,
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE EGL CONTEXT LIFECYCLE, as two
// more ops on the same frame the nine surface ops ride - the shape §"wire changes" 2 prescribes,
// and deliberately NOT two new CtrlMsg tables: a context is created, destroyed and made current
// by the SAME caller on the SAME thread as a surface, through the same blocking frame channel,
// and a second channel would only be a second place for the two orderings to disagree.
//
// CreateContext `context` is the clientContextToken EGLState minted, `shareGroupToken` the
// group it was derived into (the sharing context's group, or a fresh one),
// `contextFlags` the frame's reserved extension word (0 today). `display` and
// `surface` are 0: a context is display-scoped in EGL but the server's display
// is the session's own, so nothing is named.
// DestroyContext `context` is the token to drop. The server forgets it and, if it was the
// bound one, unbinds it.
//
// Both are answered by the ordinary SurfaceReply (ok, plus the refusal enum) - S1 creates a
// PLACEHOLDER runtime (token, share group, flags) and no native context, so there is nothing
// for the reply to carry that a Bool does not say already.
CreateContext = 12,
DestroyContext = 13,
} }
enum WindowKind : ubyte { enum WindowKind : ubyte {
@@ -303,6 +322,12 @@ table SurfaceOp {
// ("no surface" / "no context") for every other op and for a ReleaseCurrent. // ("no surface" / "no context") for every other op and for a ReleaseCurrent.
readSurface: ulong; readSurface: ulong;
context: ulong; context: ulong;
// P14 S1, appended (a FlatBuffers table field's id IS its vtable slot - append only). The two
// values CreateContext carries beyond the token itself: the share group the new context was
// derived into, and a reserved flags word so a later context attribute (a profile, a robustness
// ask) has a slot to land in without moving the fields beside it. 0 on every other op.
shareGroupToken: ulong;
contextFlags: uint;
} }
table SurfaceReply { table SurfaceReply {
+47
View File
@@ -1462,6 +1462,53 @@ namespace MobileGL::MG_Remote::Server {
return true; return true;
} }
// P14 S1. THE RING'S DOOR INTO THE SESSION'S CONTEXT TABLE.
//
// The record carries the client's current context token and this is where a session's
// attribution moves. It is deliberately NOT an assertion like OnApplierReset's serial: the
// client's EGL layer mints the token and the server has no second count to compare against,
// so what is checked is a FACT ABOUT THIS SESSION'S OWN TABLE - a token that CreateContext
// never created is corruption, not a late record. Token 0 is the release edge and is always
// legal (a session whose client released its context has no context, which is exactly the
// value a session that never bound one already answers).
Bool ServerVerbSink::OnBindContext(const MG_Pipe::MGPBindContext& bind) {
// NO SESSION IS A DECLINE, NOT A FAULT, and it is the one case here that a peer's bytes
// cannot cause: a bind is answered by the session's own table, so with no accepted
// session there is nothing to attribute the record to and nothing to refuse. (The window
// is Close() clearing the active session while the apply thread is still draining.)
ServerSession* session = ServerSession::Active();
if (session == nullptr) return false;
// PH-1 (3): the refusal latches in an armed session child (the peer wrote the token);
// unarmed it dies, like every other contract fact about a peer.
if (!session->BindContext(bind.ClientContextToken)) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the "
"record binds token %llu, which this session never created; a bind_context "
"names a context CreateContext put in the session's table, and 0 (the "
"release) is the only token that needs no entry",
static_cast<unsigned long long>(bind.ClientContextToken));
}
++m_bindContexts;
#if MOBILEGL_BUILD_DISAGGREGATED
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE HALF OF THE SWITCH.
//
// `bind_context` is where the session's context attribution moves, and it is also the ONLY
// point at which the apply thread's native context may move: the MakeCurrent control frame
// that precedes it still carries the OLD context (the client emits the binding after the
// binding has taken effect, which is the ordering S1 chose on purpose), so a native switch
// driven from there would bind the context the client just left. Frontend GL state is the
// backend's per-tuple business; this call makes the tuple the resolver now names current.
//
// It is a no-op for the shapes S4 did not change: with no resolver installed (monolith, a
// unit case, the inproc one-process shape) the key is {0,0}, and the single-context world's
// bind does not move the native context - MakeNativeContextCurrentForBoundToken returns
// without a driver call when the context it resolves to is already the one bound here.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
(void)MG_Backend::DirectGLES::MakeNativeContextCurrentForBoundToken();
}
#endif
return true;
}
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) { Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
// §1's zero ruling: a null handle means "the object never crossed", and the client // §1's zero ruling: a null handle means "the object never crossed", and the client
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption, // emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
+15
View File
@@ -245,11 +245,21 @@ namespace MobileGL::MG_Remote::Server {
Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override; Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override;
Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override; Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override;
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// The session's current context token becomes the record's, on the apply thread. Unlike
// OnApplierReset's serial the token is NOT an assertion about a count the two ends both
// hold - the client mints it and the server keeps the table CreateContext built - so the
// refusal here is "this session never created that token", a named ProtocolCorruption
// rather than a silent bind. Token 0 (the release) is always legal.
Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) override;
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe // P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial // may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
// checked, reset run); ObjectDeaths counts every record the sink dispatched. // checked, reset run); ObjectDeaths counts every record the sink dispatched.
Uint64 ApplierResets() const { return m_applierResets; } Uint64 ApplierResets() const { return m_applierResets; }
Uint64 ObjectDeaths() const { return m_objectDeaths; } Uint64 ObjectDeaths() const { return m_objectDeaths; }
Uint64 BindContexts() const { return m_bindContexts; }
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can // The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
// assert the sequence rather than only the count. // assert the sequence rather than only the count.
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; } Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
@@ -377,6 +387,11 @@ namespace MobileGL::MG_Remote::Server {
Uint64 m_applierResetSerial = 0; Uint64 m_applierResetSerial = 0;
Uint64 m_applierResets = 0; Uint64 m_applierResets = 0;
Uint64 m_objectDeaths = 0; Uint64 m_objectDeaths = 0;
// P14 S1. BindContexts counts the records the sink dispatched and bound on the session;
// the token itself lives on the session (ServerSession::CurrentContextToken), which is
// the object the control plane's CreateContext/DestroyContext also own - one table, one
// owner, and the sink is only the ring's door into it.
Uint64 m_bindContexts = 0;
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
// ReadPixels writes into a caller buffer, so one staging vector per session sits // ReadPixels writes into a caller buffer, so one staging vector per session sits
// between them. Grown, never shrunk, and never handed out past the call. // between them. Grown, never shrunk, and never handed out past the call.
+4 -1
View File
@@ -82,7 +82,10 @@ namespace MobileGL::MG_Remote::Server {
NoWindow, // a display, but no window was attached within the wait NoWindow, // a display, but no window was attached within the wait
Cancelled, // the caller's cancel predicate said so Cancelled, // the caller's cancel predicate said so
Interrupted, // Interrupt() (the in-process server is stopping) Interrupted, // Interrupt() (the in-process server is stopping)
LeasedElsewhere, // another holder has the window (sessions are sequential: a bug if seen) // P14 S2: NOT A BUG ANY MORE. Several sessions share one process and the display's window
// is one window, so a second ON-SCREEN session asking for it is refused by name here
// (ServerLoop logs the holder); offscreen sessions take no lease and are unaffected.
LeasedElsewhere, // another session in this process holds the window: see the note above
}; };
enum class ServerWindowDetach : Uint8 { enum class ServerWindowDetach : Uint8 {
+113 -10
View File
@@ -10,12 +10,14 @@
#include "ServerLoop.h" #include "ServerLoop.h"
#include "ApplyThreadPolicy.h" #include "ApplyThreadPolicy.h"
#include "SessionRuntime.h"
#include <MG_Remote/FatalFunnel.h> #include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Transport/LinkMetrics.h> #include <MG_Remote/Transport/LinkMetrics.h>
#include <MG_Backend/MGPipe/PipeInputs.h> #include <MG_Backend/MGPipe/PipeInputs.h>
#include <Config.h> #include <Config.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Util/Debug/Log.h> #include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/PipeStats.h> #include <MG_Util/Metrics/PipeStats.h>
@@ -284,6 +286,8 @@ namespace MobileGL::MG_Remote::Server {
m_haveCurrentTuple = false; m_haveCurrentTuple = false;
// P12 (D4, D6): a new session decides its own surface mode, holds no window lease yet and // P12 (D4, D6): a new session decides its own surface mode, holds no window lease yet and
// has no window-lost request pending. The apply thread that reads these is not started yet. // has no window-lost request pending. The apply thread that reads these is not started yet.
// P14 S2: and it is THIS session's latch - the loop is the session's, so a second session
// in the same process starts here too and neither's first surface decides the other's.
m_surfaceMode = SessionSurfaceMode::None; m_surfaceMode = SessionSurfaceMode::None;
m_holdsWindowLease = false; m_holdsWindowLease = false;
m_serverOwnedSurfaces.clear(); m_serverOwnedSurfaces.clear();
@@ -345,6 +349,11 @@ namespace MobileGL::MG_Remote::Server {
// Apply thread only (RunSurfaceControlFrame's dispatch put us here), so // Apply thread only (RunSurfaceControlFrame's dispatch put us here), so
// m_haveCurrentTuple/m_cur* need no // m_haveCurrentTuple/m_cur* need no
// lock. The counters are atomic for the reader on the test thread. // lock. The counters are atomic for the reader on the test thread.
//
// P14 S4 left this classification alone on purpose - ServerLoop.h states the argument (the
// currently bound tuple IS the unit ID-67 is about, and only one EGLContext can be current on
// this thread). What S4 changed is one layer down: the tuple now names the (session, context)
// registry's native context instead of the process's one.
MakeCurrentOutcome outcome; MakeCurrentOutcome outcome;
const EglBindAction action = ClassifyEglMakeCurrent(m_haveCurrentTuple, m_curDpy, m_curDraw, const EglBindAction action = ClassifyEglMakeCurrent(m_haveCurrentTuple, m_curDpy, m_curDraw,
m_curRead, m_curCtx, dpy, draw, read, ctx); m_curRead, m_curCtx, dpy, draw, read, ctx);
@@ -409,9 +418,18 @@ namespace MobileGL::MG_Remote::Server {
// THE IDENTITY IS PUBLISHED FIRST, BEFORE THE THREAD HAS A NAME OR AN AFFINITY. Every // THE IDENTITY IS PUBLISHED FIRST, BEFORE THE THREAD HAS A NAME OR AN AFFINITY. Every
// role guard in the tree reads it, and the very first record this thread applies must // role guard in the tree reads it, and the very first record this thread applies must
// already answer "yes" to OnApplyThread() - otherwise the applier's own reads would // already answer "yes" to OnApplyThread() - otherwise the applier's own reads would
// look like client reads to the guards. Relaxed for the reason stated beside the key's // look like client reads to the guards.
// declaration: it is an identity, not a handshake, and this thread is the only writer. //
Detail::g_applyThreadKey.store(Detail::CurrentThreadKey(), std::memory_order_relaxed); // P14 S2: two words, both about THIS thread. t_onApplyThread is the guard's answer; the
// process counter beside it is what MG_Backend/DirectGLES/Managers.cpp reads as "an apply
// thread is running" (ServerLoop.h's block). The scope is opened BEFORE either, because
// every read below - the session's peer, its rings, the applier it attaches - must already
// resolve to this session rather than to the process's default.
ServerSession& session = *m_session;
std::unique_ptr<Detail::ThreadSessionScope> scope;
if (m_runtime != nullptr) scope = std::make_unique<Detail::ThreadSessionScope>(*m_runtime);
Detail::t_onApplyThread = true;
Detail::g_applyThreadKey.fetch_add(1, std::memory_order_relaxed);
NameThisThread("mgl-srv-apply"); NameThisThread("mgl-srv-apply");
// P6: THIS THREAD IS THE SERVER, and saying so is what sends its lines to the server's // P6: THIS THREAD IS THE SERVER, and saying so is what sends its lines to the server's
// own log. Under spawn the process role already answers this; under INPROC it is the // own log. Under spawn the process role already answers this; under INPROC it is the
@@ -426,7 +444,6 @@ namespace MobileGL::MG_Remote::Server {
// "an affinity that silently did nothing looks exactly like one that worked" // "an affinity that silently did nothing looks exactly like one that worked"
// (CONTRACT-P5 5). P11 B1: `auto` now depends on who the client is (ApplyThreadPolicy.h), // (CONTRACT-P5 5). P11 B1: `auto` now depends on who the client is (ApplyThreadPolicy.h),
// and so does the spin before each park. // and so does the spin before each park.
ServerSession& session = *m_session;
const char* raw = AffinityStringFromConfig(); const char* raw = AffinityStringFromConfig();
const Bool automatic = raw == nullptr || raw[0] == '\0' || std::strcmp(raw, "auto") == 0; const Bool automatic = raw == nullptr || raw[0] == '\0' || std::strcmp(raw, "auto") == 0;
Uint64 bigCores = 0, primeCores = 0; Uint64 bigCores = 0, primeCores = 0;
@@ -691,7 +708,12 @@ namespace MobileGL::MG_Remote::Server {
// thread could ever give. The integration lane - a real EGL teardown - is the only // thread could ever give. The integration lane - a real EGL teardown - is the only
// thing that would catch that, and it would catch it as a hang, not as a failed // thing that would catch that, and it would catch it as a hang, not as a failed
// assertion. // assertion.
Detail::g_applyThreadKey.store(0, std::memory_order_relaxed); //
// P14 S2: the guard's own word is lowered here too, and for the same reason - with
// N sessions it can only ever have been this thread's, and the thread is about to
// stop being an apply thread. The scope is destroyed by this function's frame.
Detail::t_onApplyThread = false;
Detail::g_applyThreadKey.fetch_sub(1, std::memory_order_relaxed);
} }
m_controlDone.notify_all(); m_controlDone.notify_all();
@@ -1189,6 +1211,10 @@ namespace MobileGL::MG_Remote::Server {
} }
ServerLoop& ServerLoopInstance() { ServerLoop& ServerLoopInstance() {
// P14 S2: the session of the CALLING THREAD, else the process's one instance. The client
// process (the forwarders, whose sink makes them cross the wire), the inproc one-process
// client+server shape and every unit case have no thread session and keep this instance.
if (ServerLoop* const own = Detail::CurrentLoop()) return *own;
// ID-8: leak at exit, like every MG_Remote singleton. // ID-8: leak at exit, like every MG_Remote singleton.
static ServerLoop& instance = *new ServerLoop{}; static ServerLoop& instance = *new ServerLoop{};
return instance; return instance;
@@ -1360,7 +1386,9 @@ namespace MobileGL::MG_Remote::Server {
return MOBILEGL_OK; return MOBILEGL_OK;
case SurfaceControlOp::CreatePbufferSurface: case SurfaceControlOp::CreatePbufferSurface:
// P12 (D4): a pbuffer in a session that went on-screen is the other mode, refused by // P12 (D4): a pbuffer in a session that went on-screen is the other mode, refused by
// name and not latched - the session and its window surface carry on. // name and not latched - the session and its window surface carry on. P14 S2: the mode
// is the session's own - latched by its first surface, on its own loop - and never
// another session's.
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) { if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) {
frame.ok = false; frame.ok = false;
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch); frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
@@ -1437,6 +1465,48 @@ namespace MobileGL::MG_Remote::Server {
frame.ok = outcome.ok; frame.ok = outcome.ok;
return MOBILEGL_OK; return MOBILEGL_OK;
} }
case SurfaceControlOp::CreateContext: {
// P14 S1 built the session's table entry; P14 S4 gives it its NATIVE context. The
// share group is what makes two client contexts see one set of buffer/texture objects:
// a group whose leader already has a native EGLContext hands it to eglCreateContext as
// the share argument, so this arm is the whole of "server-side sharing".
//
// An `ok = false` here is a named refusal the client's eglCreateContext turns into
// EGL_NO_CONTEXT, which is what a server that declined to table the token must produce -
// a token the server never heard of is worse than no context at all. A native context
// that could not be built rolls the table entry back, or the session would hold a token
// whose every MakeCurrent fails one layer down.
ServerSession* session = ServerSession::Active();
if (session == nullptr) {
frame.ok = false;
return MOBILEGL_ERR_NOT_INITIALIZED;
}
frame.ok = session->CreateContext(frame.context, frame.shareGroupToken, frame.contextFlags);
if (frame.ok && backend != nullptr &&
backend->GetBackendType() == BackendType::DirectGLES) {
if (!MG_Backend::DirectGLES::CreateNativeContextFor(frame.context, frame.shareGroupToken)) {
(void)session->DestroyContext(frame.context);
frame.ok = false;
}
}
return MOBILEGL_OK;
}
case SurfaceControlOp::DestroyContext: {
ServerSession* session = ServerSession::Active();
if (session == nullptr) {
frame.ok = false;
return MOBILEGL_ERR_NOT_INITIALIZED;
}
frame.ok = session->DestroyContext(frame.context);
// P14 S4: the native context goes with the entry, and ONLY that one - the session's
// other contexts, and every other session's, are untouched. The display is terminated
// only when the last native context in the process has gone.
if (frame.ok && backend != nullptr &&
backend->GetBackendType() == BackendType::DirectGLES) {
MG_Backend::DirectGLES::DestroyNativeContextFor(frame.context);
}
return MOBILEGL_OK;
}
case SurfaceControlOp::SetSwapInterval: case SurfaceControlOp::SetSwapInterval:
backend->SetEGLSwapInterval(frame.swapInterval); backend->SetEGLSwapInterval(frame.swapInterval);
frame.ok = true; frame.ok = true;
@@ -1590,10 +1660,17 @@ namespace MobileGL::MG_Remote::Server {
break; break;
} }
if (refusal != nullptr) *refusal = SurfaceRefusalCode::NoServerWindow; if (refusal != nullptr) *refusal = SurfaceRefusalCode::NoServerWindow;
// P14 S2: LeasedElsewhere IS NOT A BUG ANY MORE. The display's window has one holder at a
// time and a session per holder, so a second ON-SCREEN session asking for it is refused -
// by name, not latched, exactly as a display with no window up. Offscreen (pbuffer)
// sessions take no lease at all and are unaffected.
MGLOG_E("MG_Remote server: Refuse ServerOwned: this server owns a display but no window for a %ux%u surface " MGLOG_E("MG_Remote server: Refuse ServerOwned: this server owns a display but no window for a %ux%u surface "
"(%s after at most %u ms) - the display's surface is not up. Refused (NoServerWindow); the " "(%s after at most %u ms)%s. Refused (NoServerWindow); the session is not latched",
"session is not latched", width, height, ServerWindowAcquireName(acquired), timeoutMs,
width, height, ServerWindowAcquireName(acquired), timeoutMs); acquired == ServerWindowAcquire::LeasedElsewhere
? " - the server's window is leased by another session, and one on-screen session at a time is "
"the display's own rule"
: " - the display's surface is not up");
return MOBILEGL_ERR_TIMEOUT; return MOBILEGL_ERR_TIMEOUT;
} }
@@ -1603,7 +1680,9 @@ namespace MobileGL::MG_Remote::Server {
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::None); frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::None);
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u; const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u; const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
// D4 FIRST: an offscreen session may not go on-screen, display or no display. // D4 FIRST: an offscreen session may not go on-screen, display or no display. P14 S2: the
// refusal is against THIS session's own latch - a second on-screen session in the same
// process is a different refusal (no free window), never a mode mismatch.
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) { if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) {
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch); frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
MGLOG_E("MG_Remote server: SurfaceModeMismatch - a ServerOwned CreateWindowSurface (seq %llu, %ux%u) in a " MGLOG_E("MG_Remote server: SurfaceModeMismatch - a ServerOwned CreateWindowSurface (seq %llu, %ux%u) in a "
@@ -1824,6 +1903,30 @@ namespace MobileGL::MG_Remote::Server {
return rc == MOBILEGL_OK && frame.ok; return rc == MOBILEGL_OK && frame.ok;
} }
// P14 S1. A context creation is a frame like any other: `context` IS the client token (the
// schema's field, not a bit-cast handle - a context has no handle the server could name),
// `shareGroupToken` and `contextFlags` the two values CreateContext adds to it. Same blocking
// handshake, so eglCreateContext cannot return a token the server has not tabled - which is
// what keeps bind_context's "this session never created that token" refusal unreachable on
// the well-behaved path.
Bool ServerCreateEGLContext(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags) {
SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::CreateContext;
frame.context = clientContextToken;
frame.shareGroupToken = shareGroupToken;
frame.contextFlags = flags;
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
return rc == MOBILEGL_OK && frame.ok;
}
Bool ServerDestroyEGLContext(Uint64 clientContextToken) {
SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::DestroyContext;
frame.context = clientContextToken;
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
return rc == MOBILEGL_OK && frame.ok;
}
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) { Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
SurfaceControlFrame frame; SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::SwapBuffersInprocOnly; frame.kind = SurfaceControlOp::SwapBuffersInprocOnly;
+84 -79
View File
@@ -112,81 +112,44 @@ namespace MobileGL::MG_Remote::Server {
// window in which a live-but-exiting apply thread answers true is byte-for-byte the old // window in which a live-but-exiting apply thread answers true is byte-for-byte the old
// one. The ServerLoop object still OWNS the key - nothing but ApplyThreadMain writes it - // one. The ServerLoop object still OWNS the key - nothing but ApplyThreadMain writes it -
// it simply no longer has to be reached through the singleton to be read. // it simply no longer has to be reached through the singleton to be read.
// P14 S2: AND THE ANSWER IS PER THREAD NOW, NOT PER PROCESS. One word cannot hold N apply
// threads: two sessions' threads would each publish their own key and the second store would
// make the FIRST thread answer "no" to every role guard it owns - which is not a slower guard
// but a dead one (a draw would take the client arm on the apply thread, and
// RunSurfaceControlFrame would lose its re-entrancy shortcut and deadlock the EGL teardown).
// The question is "am I an apply thread", a fact about the CALLER, so it is asked in the
// caller's own TLS - one constant-initialized word, no guard, no atomic:
//
// t_onApplyThread the calling thread IS an apply thread.
// g_applyThreadKey HOW MANY are running in this process. Kept because
// MG_Backend/DirectGLES/Managers.cpp (outside this package's file set)
// reads this name `!= 0` - "an apply thread is running somewhere" - which
// is the count's zero-ness exactly. It is no longer a key and nothing on
// the hot paths reads it.
//
// ZERO MEANS "NO APPLY THREAD IS RUNNING" still: t_onApplyThread is raised as the apply
// thread's FIRST act and lowered in the same m_controlMutex critical section that clears
// m_running on the way out, so the window in which a live-but-exiting apply thread answers
// true is byte-for-byte the old one.
//
// WHAT WENT WITH IT, so nobody re-derives it: the aarch64 TPIDR_EL0 fast path, the
// std::thread::id fallback with its no-padding static_assert, the MOBILEGL_DETAIL_
// APPLY_THREAD_POINTER build knob and the relaxed-load argument about a recycled TLS block.
// They existed to make "is the process's one apply thread mine" cheap; the question is now
// "is the caller an apply thread", whose cheap answer is the caller's own word - and a word
// cannot be recycled, because the thread that inherits a dead apply thread's TLS block is
// not an apply thread until it runs ApplyThreadMain.
namespace Detail { namespace Detail {
// RELAXED ON BOTH SIDES, and that is a claim about recycling, not a shrug. The only // A fresh thread's TLS word is zero-initialized, so a thread that inherits the exited
// dangerous outcome would be a NEW thread that reuses the exited apply thread's TLS // apply thread's TLS block - the recycling case the old relaxed-load argument was about -
// block (hence its thread pointer) while still reading the pre-clear key. It cannot // starts out answering false and can only become the apply thread by running
// happen: the clear is sequenced before the apply thread's exit, the exit releases the // ApplyThreadMain. There is no stale value to read and nothing to order.
// TLS block under libc's own lock, and the thread that later receives that block takes inline thread_local Bool t_onApplyThread = false;
// the same lock - so the clear happens-before any load the new thread performs, and
// coherence forbids a relaxed load from reading a value that a happens-before store
// overwrote. A stale ZERO is harmless in the other direction: a thread that is not the
// apply thread wants "false" anyway, and the apply thread always reads its own store.
//
// No acquire is needed for a second reason: this is an IDENTITY, not a handshake. No
// caller publishes data through it; the real ordering lives in the ring, the doorbell
// and m_controlMutex.
inline std::atomic<Uint64> g_applyThreadKey{0};
// The calling thread's identity as the same integer. aarch64 - the split's only // The count of live apply threads, as a relaxed atomic: an identity-like gauge, not a
// measured target - reads TPIDR_EL0 with a single instruction and no call. Everywhere // handshake (the real ordering lives in the ring, the doorbell and m_controlMutex), and
// else falls back to std::this_thread::get_id() memcpy'd into the key: still one libc // its one reader wants "is it zero".
// call, but the atomic<thread::id> load and the out-of-line pthread_equal that inline std::atomic<Uint64> g_applyThreadKey{0};
// dominated the old body are gone either way. x86_64 is deliberately NOT on the
// builtin path - __builtin_thread_pointer only grew x86 support in clang 17 / GCC 11
// and older toolchains accept __has_builtin for it and then fail in codegen, which is
// a build break on the desktop ctest lane for a target whose frame rate nobody
// measures.
//
// A LIVE THREAD'S KEY IS NEVER 0 on either path (a thread pointer is a real TLS
// address; std::this_thread::get_id() on a running thread is never the
// default-constructed id, whose object representation is the all-zero one on every
// library in scope), which is what lets 0 mean "nobody".
//
// THE SELECTOR IS A BUILD KNOB, NOT A HEADER SECRET (review round 3). This header is
// pulled into MG_State, MG_Impl and MG_Test translation units now, so the name carries
// the MOBILEGL_DETAIL_ prefix to read as internal, it is #ifndef-guarded so a toolchain
// whose __has_builtin(__builtin_thread_pointer) answers 1 and then fails in codegen can
// be steered off the path with -DMOBILEGL_DETAIL_APPLY_THREAD_POINTER=0 instead of a
// header edit, and it is always DEFINED - to 0 when unavailable - and tested with #if
// rather than #ifdef, so that a -D...=0 really disables it instead of being ignored.
#ifndef MOBILEGL_DETAIL_APPLY_THREAD_POINTER
#if defined(__aarch64__) && defined(__has_builtin)
#if __has_builtin(__builtin_thread_pointer)
#define MOBILEGL_DETAIL_APPLY_THREAD_POINTER 1
#endif
#endif
#endif
#ifndef MOBILEGL_DETAIL_APPLY_THREAD_POINTER
#define MOBILEGL_DETAIL_APPLY_THREAD_POINTER 0
#endif
inline Uint64 CurrentThreadKey() {
#if MOBILEGL_DETAIL_APPLY_THREAD_POINTER
return static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(__builtin_thread_pointer()));
#else
const std::thread::id id = std::this_thread::get_id();
static_assert(sizeof(std::thread::id) <= sizeof(Uint64),
"std::thread::id does not fit the apply-thread key; this fallback has "
"to hash rather than copy on that platform");
// AND IT MUST HAVE NO PADDING (review round 3). Copying the OBJECT REPRESENTATION of
// a type that has padding bytes would let two calls ON THE SAME THREAD return
// different keys, and OnApplyThread() would then answer FALSE ON THE APPLY THREAD -
// a role guard that has stopped guarding, which is the one direction CONTRACT-P5C
// rule E says a guard may never fail in (RunSurfaceControlFrame would also lose its
// re-entrancy shortcut and the EGL teardown post would deadlock on m_controlDone).
// Every library this fallback is built against today wraps a single scalar
// (libstdc++ __gthread_t, libc++ __libcpp_thread_id, MSVC unsigned int) and
// satisfies this; one that did not is a BUILD ERROR here rather than a silently
// dead guard at run time.
static_assert(std::has_unique_object_representations_v<std::thread::id>,
"std::thread::id has padding on this platform: copying its object "
"representation would make the apply-thread key unstable within one "
"thread. Hash the id (std::hash<std::thread::id>) instead.");
Uint64 key = 0;
std::memcpy(&key, &id, sizeof(id));
return key;
#endif
}
} // namespace Detail } // namespace Detail
// --------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------
@@ -200,11 +163,26 @@ namespace MobileGL::MG_Remote::Server {
// session NOT latched). Windows the client names itself (the X11/Win32/None tokens CONTRACT-P6 // session NOT latched). Windows the client names itself (the X11/Win32/None tokens CONTRACT-P6
// D8 has yet to close) are neither kind and neither latch nor are refused: nothing about them // D8 has yet to close) are neither kind and neither latch nor are refused: nothing about them
// changes here. Reset by ServerLoop::Start, i.e. per session. // changes here. Reset by ServerLoop::Start, i.e. per session.
//
// P14 S2: AND THE LATCH IS PER SESSION, WHICH IS ALL THIS SLICE NEEDS. The mode is a member of
// the loop and a served session owns its loop, so two sessions in one process latch
// independently and neither's first surface decides the other's - the semantics above are
// otherwise unchanged (design/11-state-ownership.md §4.5: the latch stays at the session layer
// and is never hoisted to the process). A mode DECLARED before a session's first surface would
// have to travel on the wire - the Hello, or a SurfaceOp the client sends - because one
// process's MOBILEGL_IPC_SURFACE cannot speak for the several sessions a unified server runs;
// that is P14d's. Reading that env here instead refused a dialled-in ServerOwned client with
// SurfaceModeMismatch before the display was ever consulted (ServerOwnedSurfaceTest).
enum class SessionSurfaceMode : Uint8 { None, OnScreen, Offscreen }; enum class SessionSurfaceMode : Uint8 { None, OnScreen, Offscreen };
const char* SessionSurfaceModeName(SessionSurfaceMode mode); const char* SessionSurfaceModeName(SessionSurfaceMode mode);
// The decision, pure, so a unit case can drive all six pairs without a backend. // The decision, pure, so a unit case can drive all six pairs without a backend.
Bool SessionSurfaceModeAdmits(SessionSurfaceMode current, Bool serverOwnedWindow); Bool SessionSurfaceModeAdmits(SessionSurfaceMode current, Bool serverOwnedWindow);
// P14 S2 (SessionRuntime.h): WHICH SESSION THIS LOOP IS, when it is one of several served by
// the process. Set by ServerMain::RunSession before Start(); null for every other shape (the
// inproc one-process client+server, a unit case), which is what keeps those byte-for-byte.
class SessionRuntime;
class ServerLoop { class ServerLoop {
public: public:
// Creates the apply thread, names it mgl-srv-apply, applies // Creates the apply thread, names it mgl-srv-apply, applies
@@ -214,6 +192,11 @@ namespace MobileGL::MG_Remote::Server {
// landing on fast cores. // landing on fast cores.
MobileGLResult Start(ServerSession& session); MobileGLResult Start(ServerSession& session);
// P14 S2. The session this loop belongs to, so its apply thread can open the same thread
// scope the control thread has (session/loop/inputs/latch domain/segment resolver).
void SetRuntime(SessionRuntime& runtime) { m_runtime = &runtime; }
SessionRuntime* Runtime() const { return m_runtime; }
// Kill the doorbell, join the thread (bounded), then destroy the private backend object // Kill the doorbell, join the thread (bounded), then destroy the private backend object
// ON THAT THREAD before it exits. Blocking by contract - see the header note. // ON THAT THREAD before it exits. Blocking by contract - see the header note.
void Stop(); void Stop();
@@ -327,11 +310,9 @@ namespace MobileGL::MG_Remote::Server {
// or more per draw, so the call itself - a cross-library PLT hop - was a measurable // or more per draw, so the call itself - a cross-library PLT hop - was a measurable
// share of the cost of the answer. // share of the cost of the answer.
static Bool OnApplyThread() { static Bool OnApplyThread() {
const Uint64 key = Detail::g_applyThreadKey.load(std::memory_order_relaxed); // P14 S2: the caller's own TLS word. One constant-initialized load, no atomic, no
// Short-circuit ON PURPOSE: with TransportMode::Monolith, and in every process // comparison and - unlike the process-wide key - the right answer with N sessions.
// before Start() and after Stop(), the key is 0 and the caller pays one relaxed return Detail::t_onApplyThread;
// load and one branch - it never even reads its own identity.
return key != 0 && key == Detail::CurrentThreadKey();
} }
// The CPU mask MOBILEGL_IPC_SERVER_AFFINITY resolved to and sched_setaffinity accepted. // The CPU mask MOBILEGL_IPC_SERVER_AFFINITY resolved to and sched_setaffinity accepted.
@@ -476,8 +457,11 @@ namespace MobileGL::MG_Remote::Server {
static void ServerWindowLostThunk(void* self); static void ServerWindowLostThunk(void* self);
// AcquireFor's cancel predicate: the loop is stopping, or the session latched. // AcquireFor's cancel predicate: the loop is stopping, or the session latched.
static Bool ServerWindowWaitCancelled(void* self); static Bool ServerWindowWaitCancelled(void* self);
// D4: latched at the session's first successful surface creation; reset by Start(). // D4: latched at the session's first successful surface creation; reset by Start(). Per
// session, because the loop is (P14 S2).
SessionSurfaceMode m_surfaceMode = SessionSurfaceMode::None; SessionSurfaceMode m_surfaceMode = SessionSurfaceMode::None;
// P14 S2: the session this loop serves, when the process serves several; see SetRuntime.
SessionRuntime* m_runtime = nullptr;
// Apply thread only: this loop holds the display's lease. // Apply thread only: this loop holds the display's lease.
Bool m_holdsWindowLease = false; Bool m_holdsWindowLease = false;
// Set by ServerWindowLostThunk (any thread), taken by the apply thread. Part of the park // Set by ServerWindowLostThunk (any thread), taken by the apply thread. Part of the park
@@ -576,6 +560,18 @@ namespace MobileGL::MG_Remote::Server {
// C7 / ID-54: the (dpy, draw, read, ctx) currently bound on the apply thread. Written and // C7 / ID-54: the (dpy, draw, read, ctx) currently bound on the apply thread. Written and
// read ONLY on the apply thread inside ApplyMakeCurrent, so it needs no lock; the two // read ONLY on the apply thread inside ApplyMakeCurrent, so it needs no lock; the two
// counters beside it are atomic because a test reads them from another thread. // counters beside it are atomic because a test reads them from another thread.
//
// P14 S4 LOOKED AT MAKING THIS A PER-CONTEXT TABLE AND DID NOT, with the reason recorded
// here rather than in a review thread: ID-67's pinned control
// (ServerLoopEglTest.ADifferentTupleBindsNativelyAndRepublishesAnIdenticalRepeatDoesNeither)
// defines "a different tuple" as a change of the CURRENTLY BOUND tuple, and asserts that
// A -> B -> A costs three native binds. A table that remembered each context's tuple would
// make the third one a RepeatNoOp - and it would be wrong for a real reason, not just for
// the assertion: only ONE EGLContext can be current on the apply thread, so coming back to A
// really does have to run the driver call that DirectGLES' seven frontend-context
// invalidations hang off. "Each context holds its own native triple" is satisfied by the
// backend's per-(session, token) registry; the dedup here is about the thread's CURRENT
// binding and stays one tuple.
Bool m_haveCurrentTuple = false; Bool m_haveCurrentTuple = false;
EGLDisplay m_curDpy = EGL_NO_DISPLAY; EGLDisplay m_curDpy = EGL_NO_DISPLAY;
EGLSurface m_curDraw = EGL_NO_SURFACE; EGLSurface m_curDraw = EGL_NO_SURFACE;
@@ -668,6 +664,15 @@ namespace MobileGL::MG_Remote::Server {
// and a SECOND arrival IS the invalidation signal, which is how DirectGLES, which has no // and a SECOND arrival IS the invalidation signal, which is how DirectGLES, which has no
// OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit. // OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit.
Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx); Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The EGL CONTEXT LIFECYCLE's two
// frames, on the same channel and with the same blocking handshake as the twelve above.
// `clientContextToken` is the token EGLState minted for the new context (never 0), and
// `shareGroupToken` the group it joins - the sharing context's group, or a fresh one.
// S1 creates a PLACEHOLDER server-side runtime: the token enters the session's table and
// nothing native is built (S4 is what gives the entry a native context). DestroyContext
// drops the entry and unbinds it if it was the current one.
Bool ServerCreateEGLContext(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags);
Bool ServerDestroyEGLContext(Uint64 clientContextToken);
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw); Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
void ServerSetEGLSwapInterval(Int interval); void ServerSetEGLSwapInterval(Int interval);
void ServerReleaseEGLSurface(EGLSurface surface); void ServerReleaseEGLSurface(EGLSurface surface);
+116 -55
View File
@@ -14,6 +14,7 @@
#include "ServerDisplay.h" #include "ServerDisplay.h"
#include "ServerLoop.h" #include "ServerLoop.h"
#include "ServerSession.h" #include "ServerSession.h"
#include "SessionRuntime.h"
#include "SurfaceControlFrame.h" #include "SurfaceControlFrame.h"
#include <Config.h> #include <Config.h>
#include <Init.h> #include <Init.h>
@@ -343,7 +344,7 @@ std::atomic<int> g_inProcessBackendPin{-1};
// in the disaggregated server, does nothing unless set, and says so when it is. // in the disaggregated server, does nothing unless set, and says so when it is.
void DelaySessionHandshakeForTest() { void DelaySessionHandshakeForTest() {
const char* text = std::getenv("MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS"); const char* text = std::getenv("MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS");
if (text == nullptr || *text == '\0') return; if (text == nullptr || text[0] == 0) return;
const long ms = std::strtol(text, nullptr, 10); const long ms = std::strtol(text, nullptr, 10);
if (ms <= 0) return; if (ms <= 0) return;
WireLogError("MG_Remote server: pid=%d MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS=%ld - this session child " WireLogError("MG_Remote server: pid=%d MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS=%ld - this session child "
@@ -468,14 +469,25 @@ int RunSession(std::unique_ptr<SocketTransport> control, std::vector<std::uint8_
// else: inproc keeps every Fatal (client and server share that process), and the supervisor // else: inproc keeps every Fatal (client and server share that process), and the supervisor
// that forked this child never applies a record. // that forked this child never applies a record.
// //
// P12 (D5): and the in-process display server arms it for EVERY session, here, the same way; // P12 (D5): and the in-process display server arms it for EVERY session, here, the same way.
// its supervisor resets it between sessions (ResetSessionLatch), so a session that latched does // P14 S2: "IT" IS PER SESSION NOW - the scope below binds this thread to this session's latch
// not decline the next one. // domain (FatalFunnel.h), so the arm that follows arms THAT domain and no other: a session
// that latched cannot decline its neighbour, and the supervisor's ResetSessionLatch touches
// only its own (process-default) domain.
// P14 S2: THIS SESSION'S OWN RUNTIME, AND BOTH OF ITS THREADS ON IT. The object holds the
// session, the apply loop and the PipeInputs block; the scope makes them what this thread's
// reads of ServerSessionInstance()/ServerLoopInstance()/gPipeInputs resolve to, and binds this
// session's latch domain and segment resolver. Everything below - the backend bring-up, the
// arm, the EGL frames - therefore runs against THIS session, which is what lets one process
// serve several at once. The apply thread opens the same scope as its first act
// (ServerLoop::ApplyThreadMain, via SetRuntime below).
auto runtime = std::make_unique<Server::SessionRuntime>();
Server::Detail::ThreadSessionScope threadScope(*runtime);
ArmSessionLatch(); ArmSessionLatch();
// The child bypasses MobileGL::Initialize: arm its own role counters. // The child bypasses MobileGL::Initialize: arm its own role counters.
MobileGL::MG_Util::PipeStats::Init(); MobileGL::MG_Util::PipeStats::Init();
auto& session = Server::ServerSessionInstance(); auto& session = runtime->Session();
auto& loop = Server::ServerLoopInstance(); auto& loop = runtime->Loop();
// P12 (D5): WHAT A PROCESS EXIT CLEANED UP FOR FREE, THE IN-PROCESS SHAPE CLEANS UP BY HAND, // P12 (D5): WHAT A PROCESS EXIT CLEANED UP FOR FREE, THE IN-PROCESS SHAPE CLEANS UP BY HAND,
// on every way out from here - so the next session in this process starts where a fresh // on every way out from here - so the next session in this process starts where a fresh
// session child would. A no-op for every forked / single-session shape: those `_exit` with the // session child would. A no-op for every forked / single-session shape: those `_exit` with the
@@ -659,6 +671,9 @@ int RunSession(std::unique_ptr<SocketTransport> control, std::vector<std::uint8_
// below) and is the one that says "still running" while the apply thread runs it. Cleared // below) and is the one that says "still running" while the apply thread runs it. Cleared
// after the loop, on this same thread, so no post can be in flight when it goes. // after the loop, on this same thread, so no post can be in flight when it goes.
loop.SetControlProgressSink(&SendSurfaceProgress, control.get()); loop.SetControlProgressSink(&SendSurfaceProgress, control.get());
// P14 S2: the apply thread opens the same scope this thread is on, so its reads of the
// session, this loop and the PipeInputs block resolve to THIS session's.
loop.SetRuntime(*runtime);
if (loop.Start(session) != MOBILEGL_OK) return endSession(70); if (loop.Start(session) != MOBILEGL_OK) return endSession(70);
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready control=%s data=%s", WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready control=%s data=%s",
selfPid, tcp ? "tcp" : "unix", tcp ? "stream" : "shm"); selfPid, tcp ? "tcp" : "unix", tcp ? "stream" : "shm");
@@ -1327,17 +1342,44 @@ private:
// shm segments' descriptors cross by SCM_RIGHTS, so there is no DataBind to route and no pre-auth // shm segments' descriptors cross by SCM_RIGHTS, so there is no DataBind to route and no pre-auth
// gate - both are TCP exposure machinery, and a unix peer is local by construction (the forked // gate - both are TCP exposure machinery, and a unix peer is local by construction (the forked
// unix supervisor runs without either). Sessions take the TCP in-process shape's thread hand-off: // unix supervisor runs without either). Sessions take the TCP in-process shape's thread hand-off:
// one at a time, Busy for a second client, the latch reset between sessions, stop by // a thread per session, up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (default 16) and Refuse{Busy}
// g_inProcessStop. // over it, the latch per session, stop by
// g_inProcessStop. P14 S2: THE SESSIONS ARE CONCURRENT - each one owns its ServerSession, its
// ServerLoop and its PipeInputs block through its own ThreadSessionScope (SessionRuntime.h), so a
// session's close, latch or fault reaches no other, and the server's own stop waits for all of
// them rather than for the one.
class UnixInProcessSupervisor { class UnixInProcessSupervisor {
public: public:
UnixInProcessSupervisor(int listener, unsigned long& sessionsFaulted) // P14 S2: MAX SESSIONS AT ONCE. Each live session is one control thread (RunSession) plus its
: m_listener(listener), m_pairs(listener), m_sessionsFaulted(sessionsFaulted) {} // own mgl-srv-apply, both carrying their own ServerSession/ServerLoop/PipeInputs block, so the
// cap is a resource limit and not a correctness one. The default is the display server's
// (mobilegl_server_serve_inprocess), which is where this class is used.
static constexpr unsigned kDefaultMaxSessions = 16;
// MOBILEGL_IPC_INPROC_MAX_SESSIONS, capped, 0/unset/invalid = the default. An environment
// knob rather than a new argument to mobilegl_server_serve_inprocess: the embedded ABI has one
// parameter (the endpoint) and the display server passes none of this, exactly as it passes
// none of the transport knobs.
static unsigned MaxSessionsFromEnvironment() {
const char* text = std::getenv("MOBILEGL_IPC_INPROC_MAX_SESSIONS");
if (text == nullptr || text[0] == 0) return kDefaultMaxSessions;
char* end = nullptr;
const long parsed = std::strtol(text, &end, 10);
if (end == text || parsed <= 0) return kDefaultMaxSessions;
return static_cast<unsigned>(std::min<long>(parsed, 256));
}
UnixInProcessSupervisor(int listener, unsigned long& sessionsFaulted,
unsigned maxSessions = kDefaultMaxSessions)
: m_listener(listener), m_pairs(listener), m_sessionsFaulted(sessionsFaulted),
m_maxSessions(maxSessions == 0 ? kDefaultMaxSessions : maxSessions) {}
~UnixInProcessSupervisor() { ~UnixInProcessSupervisor() {
// Run() only returns with the session thread joined; a listener failure (73) is the // Run() only returns with every session thread joined; a listener failure (73) is the one
// one way out with it live, and it is never left running behind the supervisor. // way out with one live, and a session is never left running behind the supervisor.
if (m_sessionThread.joinable()) m_sessionThread.join(); for (auto& session : m_live) {
if (session->thread.joinable()) session->thread.join();
}
} }
int Run() { int Run() {
@@ -1352,15 +1394,11 @@ public:
static_cast<int>(accepted)); static_cast<int>(accepted));
return 73; return 73;
} }
// exit_group's close window, as the forked loop's: a client reconnecting the moment // P14 S2: THE CAP, NOT "ONE AT A TIME". A peer over the limit is refused by the same
// its session ended must not be told Busy by a session that is exiting. // words the forked `--max-sessions` supervisor uses, and is not a latch: the sessions
const auto reapDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(50); // already running carry on and the connection is simply closed.
while (m_active > 0 && std::chrono::steady_clock::now() < reapDeadline) { if (m_live.size() >= m_maxSessions) {
if (Reap()) break; RefuseBusy(*control, "the session limit is reached");
::usleep(1000);
}
if (m_active > 0) {
RefuseBusy(*control, "one session is already active");
continue; continue;
} }
StartSessionThread(std::move(control)); StartSessionThread(std::move(control));
@@ -1368,48 +1406,71 @@ public:
} }
private: private:
// The same reap as the TCP shape's ReapSessionThread: join, count faults, reset the latch // One live session: its control thread, and the two facts the reap needs from it. The session
// for the next session in this process. // itself (its ServerSession, its apply thread, its PipeInputs block) is owned by RunSession's
bool Reap() { // own frame, on that thread - nothing here reaches into it.
if (m_active <= 0 || !m_sessionDone.load(std::memory_order_acquire)) return false; struct LiveSession {
if (m_sessionThread.joinable()) m_sessionThread.join(); std::thread thread;
const int code = m_sessionExit.load(std::memory_order_acquire); std::atomic<bool> done{false};
m_active = -1; std::atomic<int> exit{0};
if (code != 0) ++m_sessionsFaulted; unsigned long long ordinal = 0;
ResetSessionLatch(); };
WireLogError("MG_Remote server: in-process unix session #%lu pid=%d reaped exit=%d%s sessionsFaulted=%lu",
m_sessionOrdinal, static_cast<int>(::getpid()), code, // The same reap as the TCP shape's ReapSessionThread: join, count faults, reset the latch for
code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted); // the next session in this process. Every finished session is reaped, in whatever order they
return true; // finished.
void Reap() {
for (auto session = m_live.begin(); session != m_live.end();) {
LiveSession& live = **session;
if (!live.done.load(std::memory_order_acquire)) {
++session;
continue;
}
if (live.thread.joinable()) live.thread.join();
const int code = live.exit.load(std::memory_order_acquire);
if (code != 0) ++m_sessionsFaulted;
ResetSessionLatch();
WireLogError("MG_Remote server: in-process unix session #%llu pid=%d reaped exit=%d%s "
"sessionsFaulted=%lu (live %zu)",
static_cast<unsigned long long>(live.ordinal),
static_cast<int>(::getpid()), code,
code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted,
m_live.size() - 1);
session = m_live.erase(session);
}
} }
void StartSessionThread(std::unique_ptr<SocketTransport> connection) { void StartSessionThread(std::unique_ptr<SocketTransport> connection) {
if (m_sessionThread.joinable()) m_sessionThread.join(); // reaped already; never live here // No hand-off pair and no pre-read Hello: the session reads its own first frame and binds
m_sessionDone.store(false, std::memory_order_release); // its shm segments from the fds the pair's second connection carries.
m_sessionExit.store(0, std::memory_order_release); auto live = std::make_unique<LiveSession>();
++m_sessionOrdinal; live->ordinal = ++m_sessionOrdinal;
try { try {
// No hand-off pair and no pre-read Hello: the session reads its own first frame and live->thread = std::thread([live = live.get(), conn = std::move(connection)]() mutable {
// binds its shm segments from the fds the pair's second connection carries.
m_sessionThread = std::thread([this, conn = std::move(connection)]() mutable {
const int code = RunSession(std::move(conn), {}, {}, -1, DataSource::None, 10000, const int code = RunSession(std::move(conn), {}, {}, -1, DataSource::None, 10000,
/*inProcess=*/true); /*inProcess=*/true);
m_sessionExit.store(code, std::memory_order_release); live->exit.store(code, std::memory_order_release);
m_sessionDone.store(true, std::memory_order_release); live->done.store(true, std::memory_order_release);
}); });
} catch (...) { } catch (...) {
WireLogError("MG_Remote server: could not start the in-process unix session thread; the " WireLogError("MG_Remote server: could not start the in-process unix session thread; the "
"connection is closed (it cannot be refused: it moved into the failed thread)"); "connection is closed (it cannot be refused: it moved into the failed thread)");
return; return;
} }
m_active = ::getpid(); m_live.push_back(std::move(live));
WireLogError("MG_Remote server: in-process unix session #%lu started on a thread (pid=%d)", WireLogError("MG_Remote server: in-process unix session #%llu started on a thread (pid=%d, "
m_sessionOrdinal, static_cast<int>(::getpid())); "%zu live of at most %u; %u accepted in this process)",
static_cast<unsigned long long>(m_sessionOrdinal),
static_cast<int>(::getpid()), m_live.size(), m_maxSessions,
Server::LiveServerSessionCount());
} }
int StopServing() { int StopServing() {
while (m_active > 0) { // P14 S2: EVERY live session, not one. Each reads g_inProcessStop on its own control
if (!Reap()) ::usleep(10000); // slice, so this is the same orderly end the single-session shape had, N times over.
while (!m_live.empty()) {
Reap();
if (!m_live.empty()) ::usleep(10000);
} }
WireLogError("MG_Remote server: pid=%d in-process display server stopped", static_cast<int>(::getpid())); WireLogError("MG_Remote server: pid=%d in-process display server stopped", static_cast<int>(::getpid()));
return 0; return 0;
@@ -1419,11 +1480,10 @@ private:
// The connections that have not been paired yet live here between Accept calls. // The connections that have not been paired yet live here between Accept calls.
Server::PairAcceptor m_pairs; Server::PairAcceptor m_pairs;
unsigned long& m_sessionsFaulted; unsigned long& m_sessionsFaulted;
std::thread m_sessionThread; unsigned m_maxSessions;
std::atomic<bool> m_sessionDone{false}; // P14 S2: every live session, not one. Reaped by Run()'s Reap and by StopServing.
std::atomic<int> m_sessionExit{0}; std::vector<std::unique_ptr<LiveSession>> m_live;
unsigned long m_sessionOrdinal = 0; unsigned long long m_sessionOrdinal = 0;
pid_t m_active = -1;
}; };
// The server's environment preconditions, shared by the exec'd supervisor and the in-process // The server's environment preconditions, shared by the exec'd supervisor and the in-process
@@ -1638,7 +1698,8 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_serve_inpr
TcpSupervisor::Handoff::Thread, &g_inProcessStop); TcpSupervisor::Handoff::Thread, &g_inProcessStop);
result = supervisor.Run(); result = supervisor.Run();
} else { } else {
UnixInProcessSupervisor supervisor(listener, sessionsFaulted); UnixInProcessSupervisor supervisor(listener, sessionsFaulted,
UnixInProcessSupervisor::MaxSessionsFromEnvironment());
result = supervisor.Run(); result = supervisor.Run();
} }
LogSupervisorSummary(sessionsFaulted); LogSupervisorSummary(sessionsFaulted);
+131 -21
View File
@@ -10,6 +10,7 @@
#include "../Transport/SocketTransport.h" #include "../Transport/SocketTransport.h"
#include "ServerSession.h" #include "ServerSession.h"
#include "SessionRuntime.h"
#include <MG_Remote/FatalFunnel.h> #include <MG_Remote/FatalFunnel.h>
#include "../CapsCodec.h" #include "../CapsCodec.h"
@@ -528,11 +529,24 @@ namespace MobileGL::MG_Remote::Server {
// One callback table and one process segment resolver: an in-progress Accept // One callback table and one process segment resolver: an in-progress Accept
// reserves their owner too. CAS closes the race between distinct session objects // reserves their owner too. CAS closes the race between distinct session objects
// without making concurrent sessions a supported mode. // without making concurrent sessions a supported mode.
//
// P14 S2: THE TABLE IS STILL ONE PER PROCESS, and that is now deliberate rather than a
// limit. Its entries are the same five functions for every session; what makes them route
// to the right session is that they resolve it per calling thread
// (ServerSession::Active()), and every one of them is called by a backend that runs on a
// session's own apply thread. So the owner word below is a CLAIM on a shared, idempotent
// installation, not a claim on "the session".
std::atomic<ServerSession*> g_sessionOwner{nullptr}; std::atomic<ServerSession*> g_sessionOwner{nullptr};
// P14 S2: accepted and not yet closed. The claim above is released by a Close only when
// this reaches zero - one session ending may not uninstall the table another is using.
std::atomic<Uint32> g_liveSessions{0};
} // namespace } // namespace
Uint32 ServerSessionCount() { return g_liveSessions.load(std::memory_order_acquire); }
ServerSession& ServerSessionInstance() { ServerSession& ServerSessionInstance() {
if (ServerSession* const own = Detail::CurrentSession()) return *own;
// Leak at exit, deliberately and per ID-8: frontend destructors reach pipe and backend // Leak at exit, deliberately and per ID-8: frontend destructors reach pipe and backend
// state from exit handlers, and a session destroyed before them would be a // state from exit handlers, and a session destroyed before them would be a
// use-after-free rather than a tidy teardown. // use-after-free rather than a tidy teardown.
@@ -540,7 +554,10 @@ namespace MobileGL::MG_Remote::Server {
return *instance; return *instance;
} }
ServerSession* ServerSession::Active() { return g_active.load(std::memory_order_acquire); } ServerSession* ServerSession::Active() {
if (ServerSession* const own = Detail::CurrentSession()) return own;
return g_active.load(std::memory_order_acquire);
}
ServerSession::ServerSession() { ServerSession::ServerSession() {
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer); m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer);
@@ -584,8 +601,17 @@ namespace MobileGL::MG_Remote::Server {
MobileGLResult ServerSession::Accept(Transport::ITransport& transport, MobileGLResult ServerSession::Accept(Transport::ITransport& transport,
const std::vector<Uint8>* firstFrame) { const std::vector<Uint8>* firstFrame) {
// P14 S2: A SERVED SESSION DOES NOT CLAIM THE PROCESS TABLE, because the table is SHARED.
// Its entries are the same five functions for every session and they route by the calling
// thread (ServerSession::Active()), so a second session installing them is the idempotent
// repeat InstallReverseCallback already allows - and a second session REFUSING to install
// them is what a one-session process would have. The claim below therefore belongs to the
// shapes whose process really is one session (the inproc client+server role, FvSession and
// its peers in ServerLoopTest): there a second ServerSession object is still the named
// Fatal{callback-double-install} the single-session ruling asks for.
const Bool sharedTable = Detail::CurrentSession() == this;
ServerSession* expectedOwner = nullptr; ServerSession* expectedOwner = nullptr;
if (!g_sessionOwner.compare_exchange_strong(expectedOwner, this, if (!sharedTable && !g_sessionOwner.compare_exchange_strong(expectedOwner, this,
std::memory_order_acq_rel, std::memory_order_acquire)) { std::memory_order_acq_rel, std::memory_order_acquire)) {
if (expectedOwner == this) return MOBILEGL_ERR_INVALID_ARGUMENT; if (expectedOwner == this) return MOBILEGL_ERR_INVALID_ARGUMENT;
SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - another " SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - another "
@@ -702,6 +728,19 @@ namespace MobileGL::MG_Remote::Server {
m_t0Stores = m_t0Bytes = m_t0Declined = m_t0Refusals = 0; m_t0Stores = m_t0Bytes = m_t0Declined = m_t0Refusals = 0;
m_adoptInbox.Clear(); m_adoptInbox.Clear();
m_adoptInbox.Attach(&transport); m_adoptInbox.Attach(&transport);
// P14 S1: the context registry is the session's, so it is emptied with the rest of the
// per-session state this Accept resets. A session that never crossed a bind_context
// leaves the current token at 0, which is the single-context shape's answer.
//
// P14 S5: the APPLIERS of those contexts go with them - one registry entry per context
// and one per share group, released here because a session that is re-accepted must not
// inherit the previous client's object records. Accept can run on the control thread
// while nothing is applying (this is the accept path, before the first Hello is
// answered), so the release is safe here in a way it would not be mid-session.
MG_Pipe::MGPipeApplierReleaseSession(reinterpret_cast<Uint64>(this));
m_contexts.clear();
m_currentContextToken.store(0, std::memory_order_release);
m_contextsCreated = m_contextsDestroyed = m_contextBinds = 0;
// ---- 3. the four segments, both control pages, the rings. // ---- 3. the four segments, both control pages, the rings.
MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED; MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED;
@@ -844,6 +883,10 @@ namespace MobileGL::MG_Remote::Server {
m_segments.InstallProcessResolver(); m_segments.InstallProcessResolver();
m_accepted = true; m_accepted = true;
m_closed = false;
// P14 S2: this session is live. The reverse-channel table and the segment resolver are
// process-wide and shared; they are released by whichever Close finds this back at zero.
g_liveSessions.fetch_add(1, std::memory_order_acq_rel);
// ---- P5c ev: the four reverse-channel producers are THIS session's (CONTRACT-P5C // ---- P5c ev: the four reverse-channel producers are THIS session's (CONTRACT-P5C
// §4.1), installed before the apply thread can apply a record that produces one and // §4.1), installed before the apply thread can apply a record that produces one and
@@ -1006,29 +1049,41 @@ namespace MobileGL::MG_Remote::Server {
} }
void ServerSession::Close() { void ServerSession::Close() {
// P14 S2: idempotent (ServerSession.h). A served session is a stack object whose owner
// closes it explicitly and whose destructor closes it again.
if (m_closed) return;
m_closed = true;
ServerSession* expectedActive = this; ServerSession* expectedActive = this;
g_active.compare_exchange_strong(expectedActive, nullptr, g_active.compare_exchange_strong(expectedActive, nullptr,
std::memory_order_acq_rel, std::memory_order_acquire); std::memory_order_acq_rel, std::memory_order_acquire);
if (m_accepted) { if (m_accepted) {
// Uninstall AFTER the apply thread has joined, never before: a record still in // P14 S2: DROP THIS SESSION FIRST, THEN ASK WHETHER ANY IS LEFT. The two process-wide
// flight can still resolve a segment offset (table 3's fourth column). // hooks below (the segment resolver and the reverse-channel table) are shared by every
Wire::SegmentTable::UninstallProcessResolver(); // session in the process, so a session that ends while another is live must leave both
// The reverse-channel producers go with the session that owns them - release // installed - its own threads are done with them (the apply thread is joined above and
// only the entries that are still OURS, never one a later owner installed. // this control thread unbinds its own binding on the way out of RunSession).
if (MG_Pipe::gMGPipeCallbacks.OnGlError == &ServerOnGlError) { const Uint32 remaining = g_liveSessions.fetch_sub(1, std::memory_order_acq_rel) - 1;
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr; if (remaining == 0) {
} // Uninstall AFTER the apply thread has joined, never before: a record still in
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) { // flight can still resolve a segment offset (table 3's fourth column).
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = nullptr; Wire::SegmentTable::UninstallProcessResolver();
} // The reverse-channel producers go with the session that owns them - release
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == &ServerOnGpuWritten) { // only the entries that are still OURS, never one a later owner installed.
MG_Pipe::gMGPipeCallbacks.OnGpuWritten = nullptr; if (MG_Pipe::gMGPipeCallbacks.OnGlError == &ServerOnGlError) {
} MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == &ServerOnSurfaceChanged) { }
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged = nullptr; if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) {
} MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = nullptr;
if (MG_Pipe::gMGPipeEventRingCapacityBytes == &ServerEventRingCapacityBytes) { }
MG_Pipe::gMGPipeEventRingCapacityBytes = nullptr; if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == &ServerOnGpuWritten) {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten = nullptr;
}
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == &ServerOnSurfaceChanged) {
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged = nullptr;
}
if (MG_Pipe::gMGPipeEventRingCapacityBytes == &ServerEventRingCapacityBytes) {
MG_Pipe::gMGPipeEventRingCapacityBytes = nullptr;
}
} }
} }
// P11 B2: the T0 arm's proof for this session, then the inbox's leftovers (an Offer whose // P11 B2: the T0 arm's proof for this session, then the inbox's leftovers (an Offer whose
@@ -1205,6 +1260,61 @@ namespace MobileGL::MG_Remote::Server {
return true; return true;
} }
// ---- P14 S1: the context registry ------------------------------------------------------
//
// ALL THREE RUN ON THE APPLY THREAD. CreateContext/DestroyContext are the control plane's
// frames, dispatched through RunSurfaceControlFrame; BindContext is a ring record, dispatched
// through the decoder's verb sink. Neither ever runs anywhere else, which is what makes the
// map lock-free here.
Bool ServerSession::CreateContext(Uint64 token, Uint64 shareGroupToken, Uint32 flags) {
if (token == 0) return false;
if (m_contexts.find(token) != m_contexts.end()) return false;
ContextRuntime runtime;
runtime.Token = token;
runtime.ShareGroupToken = shareGroupToken;
runtime.Flags = flags;
m_contexts.emplace(token, runtime);
++m_contextsCreated;
// P14 S5: and the applier this context's records are applied through - its own working
// state, in the share group `shareGroupToken` names (0 = one it starts itself). The key
// is THIS object's address, which is what the session's key resolver
// (SessionRuntime.cpp) hands back, so registration and lookup agree by construction.
// Registering here rather than inferring at lookup time is what keeps the resolver a
// two-word read: this runs on the apply thread, where the frame channel delivers, and
// is the only writer of the contexts map.
MG_Pipe::MGPipeApplierRegisterContext(reinterpret_cast<Uint64>(this), token, shareGroupToken);
return true;
}
Bool ServerSession::DestroyContext(Uint64 token) {
if (token == 0) return false;
const auto it = m_contexts.find(token);
if (it == m_contexts.end()) return false;
m_contexts.erase(it);
++m_contextsDestroyed;
// P14 S5: the working state goes with it, and this group's object records go when its
// last context does.
MG_Pipe::MGPipeApplierUnregisterContext(reinterpret_cast<Uint64>(this), token);
// A destroyed context cannot stay current: leaving the token bound would attribute every
// record after the destroy to a context the session no longer has. The client's own
// eglDestroyContext is refused while the context is current (EGL_BAD_ACCESS), so this
// arm is reached with the token already released in the well-behaved sequence - and with
// the release MISSING it is still the safe direction.
Uint64 expected = token;
m_currentContextToken.compare_exchange_strong(expected, 0, std::memory_order_acq_rel);
return true;
}
Bool ServerSession::BindContext(Uint64 token) {
// 0 is the release edge and is always legal: the client has context A current, releases
// it, and a verb on the app thread follows - the session binds "nothing" rather than
// answering to a stale token.
if (token != 0 && m_contexts.find(token) == m_contexts.end()) return false;
m_currentContextToken.store(token, std::memory_order_release);
++m_contextBinds;
return true;
}
Transport::RingConsumer& ServerSession::CommandRing() { return *m_commands; } Transport::RingConsumer& ServerSession::CommandRing() { return *m_commands; }
Transport::RingControl& ServerSession::Control() { Transport::RingControl& ServerSession::Control() {
+68
View File
@@ -65,6 +65,10 @@ namespace MobileGL::MG_Remote::Server {
class ServerSession { class ServerSession {
public: public:
// P14 S2: THE SESSION OF THE CALLING THREAD, else the process's active one. Every
// server-role caller that matters (the reverse-channel producers, the applier's role
// guards, the decoder, ServerMain's control loop) runs on a session's own thread and
// therefore gets ITS session - which is what makes one process able to serve several.
static ServerSession* Active(); static ServerSession* Active();
ServerSession(); ServerSession();
@@ -190,6 +194,11 @@ namespace MobileGL::MG_Remote::Server {
Bool PeerSharedSegments() const { return m_peerSharedSegments; } Bool PeerSharedSegments() const { return m_peerSharedSegments; }
// Teardown: after the apply thread has been joined, never before - a record still in // Teardown: after the apply thread has been joined, never before - a record still in
// flight can still resolve a segment offset (table 3's fourth column). // flight can still resolve a segment offset (table 3's fourth column).
//
// P14 S2: AND IT IS IDEMPOTENT, because a served session is now a stack object. A session
// that has been closed once (RunSession closes it explicitly) is closed again by its
// destructor; the second call must not run the teardown twice. Accept re-opens it, so the
// in-process display server's accept/close/accept cycle is unchanged.
void Close(); void Close();
Transport::SessionSegments& Shm(); Transport::SessionSegments& Shm();
@@ -322,6 +331,45 @@ namespace MobileGL::MG_Remote::Server {
Bool AdoptT0Quiet() const { return m_adoptT0Quiet; } Bool AdoptT0Quiet() const { return m_adoptT0Quiet; }
AdoptInbox& T0Inbox() { return m_adoptInbox; } AdoptInbox& T0Inbox() { return m_adoptInbox; }
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): the session's contexts -
//
// THE PLACEHOLDER TABLE. `CreateContext`/`DestroyContext` on the control plane build and
// destroy an entry per client token; `BindContext` on the ring makes one of them the
// session's current context. What lives in an entry in THIS slice is the token and the
// share group it was derived from - no native EGLContext, no GLContext, no working state
// (that is S4's). The table exists so that a token the client names can be proved to
// have been created before anything is attributed to it, and so that the S4 slice has a
// place to put the runtime it will own.
//
// WRITES ARE THE APPLY THREAD'S. CreateContext/DestroyContext arrive as control frames
// dispatched through the frame channel, BindContext as a record through the decoder, and
// both run on mgl-srv-apply - so the table needs no lock. CurrentContextToken() is
// atomic because a test reads it from the case's own thread.
struct ContextRuntime {
Uint64 Token = 0; // the client's context token, dense from 1
Uint64 ShareGroupToken = 0; // 0 = this context started a share group
Uint32 Flags = 0; // reserved; the control frame's `flags` field, carried verbatim
};
// False when a context with this token already exists, or when the token is 0 (0 is
// "no context" everywhere on both planes and is never a creatable context).
Bool CreateContext(Uint64 token, Uint64 shareGroupToken, Uint32 flags);
// False when no such token exists - a destroy the peer did not create is the control
// frame's own refusal to make, not a silent no-op.
Bool DestroyContext(Uint64 token);
// Binds the session's current context. Token 0 is legal and means "no context" (the
// release edge); any other token must have been created, or this is a named refusal.
Bool BindContext(Uint64 token);
// 0 = no context is bound. Every record the session has applied so far belongs to this
// token; a session that never crossed a bind_context has never bound one.
Uint64 CurrentContextToken() const { return m_currentContextToken.load(std::memory_order_acquire); }
Bool HasContext(Uint64 token) const { return m_contexts.find(token) != m_contexts.end(); }
Uint64 ContextCount() const { return static_cast<Uint64>(m_contexts.size()); }
Uint64 ContextsCreated() const { return m_contextsCreated; }
Uint64 ContextsDestroyed() const { return m_contextsDestroyed; }
// The per-session tallies the sink keeps for the two control frames it dispatched.
Uint64 ContextBinds() const { return m_contextBinds; }
private: private:
MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::ILink& link, MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::ILink& link,
const Uint8* nonce); const Uint8* nonce);
@@ -347,6 +395,9 @@ namespace MobileGL::MG_Remote::Server {
Bool m_consumedSet = false; Bool m_consumedSet = false;
Bool m_sizesSet = false; Bool m_sizesSet = false;
Bool m_accepted = false; Bool m_accepted = false;
// P14 S2: Close() has run for the current accept. Cleared by Accept, which is what keeps
// the in-process display server's accept/close/accept cycle working.
Bool m_closed = false;
ApplyPeer m_peer = ApplyPeer::InProcess; ApplyPeer m_peer = ApplyPeer::InProcess;
Bool m_peerSharedSegments = true; Bool m_peerSharedSegments = true;
// PH-6. Written by the apply thread (the only SEG_EVENT producer), reset by Accept. // PH-6. Written by the apply thread (the only SEG_EVENT producer), reset by Accept.
@@ -369,10 +420,27 @@ namespace MobileGL::MG_Remote::Server {
Uint64 m_t0Bytes = 0; Uint64 m_t0Bytes = 0;
Uint64 m_t0Declined = 0; Uint64 m_t0Declined = 0;
Uint64 m_t0Refusals = 0; Uint64 m_t0Refusals = 0;
// P14 S1. Apply-thread-only map (see ContextRuntime); the current token is atomic for
// the reader on a test thread. Reset by Accept.
UnorderedMap<Uint64, ContextRuntime> m_contexts;
std::atomic<Uint64> m_currentContextToken{0};
Uint64 m_contextsCreated = 0;
Uint64 m_contextsDestroyed = 0;
Uint64 m_contextBinds = 0;
}; };
// Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may // Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may
// reach pipe or backend state from an exit handler. // reach pipe or backend state from an exit handler.
//
// P14 S2 (SessionRuntime.h): THE SINGLETON IS THE FALLBACK, NOT THE SESSION. On a thread that
// belongs to a served session (ServerMain::RunSession's control thread, or that session's
// mgl-srv-apply) this answers with THAT session; every other thread - the client process, the
// inproc one-process client+server shape, a unit case - keeps getting the leaked instance.
ServerSession& ServerSessionInstance(); ServerSession& ServerSessionInstance();
// P14 S2: sessions accepted and not yet closed in this process. A process-wide fact that must
// outlive one session (the reverse-channel callback table, the segment resolver) is released
// only when this reaches zero.
Uint32 ServerSessionCount();
} // namespace MobileGL::MG_Remote::Server } // namespace MobileGL::MG_Remote::Server
@@ -0,0 +1,127 @@
// MobileGL - MobileGL/MG_Remote/Server/SessionRuntime.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P14 S2: the per-session runtime and the thread routing (SessionRuntime.h states the shape).
#include "SessionRuntime.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Wire/PipeWireCodec.h>
#include <atomic>
namespace MobileGL::MG_Remote::Server {
namespace {
// The calling thread's session. nullptr on every thread that is not one of a session's own
// two, which is the whole of the fallback rule.
thread_local SessionRuntime* t_runtime = nullptr;
// The resolver MG_Pipe asks per thread. Installing it is a single store of a function
// pointer that is never replaced, so it needs no lock and no teardown: the server role
// lives for the process (ID-8).
MG_Pipe::PipeInputs* ResolveThreadInputs() {
SessionRuntime* runtime = t_runtime;
return runtime != nullptr ? &runtime->Inputs() : nullptr;
}
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE TUPLE'S KEY. The
// backend keys its per-context native state - the EGLContext, its config, the draw/read
// binding - on (session, client context token); this is the one place that answers the pair
// for the calling thread. The session half is the runtime's ADDRESS, which is stable for the
// session's whole life and unique among live sessions in the process; the token half is what
// `bind_context` last set on the ring. False (== the {0,0} key) on any thread that is not a
// session's own, which is the client process, the inproc one-process shape and every unit
// case - i.e. exactly the single-context world the backend still serves unchanged.
Bool ResolveThreadNativeContextKey(Uint64* outSessionKey, Uint64* outContextToken) {
SessionRuntime* runtime = t_runtime;
if (runtime == nullptr) return false;
*outSessionKey = reinterpret_cast<Uint64>(runtime);
*outContextToken = runtime->Session().CurrentContextToken();
return true;
}
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md). WHOSE APPLIER IS THIS THREAD
// IN? The same two-word answer as the native key above, and deliberately the same
// spelling: the session half is the ServerSession's own address - which is the key the
// registration side uses (ServerSession::CreateContext passes `this`), so the two agree
// by construction rather than by two conventions - and the token half is what
// `bind_context` last set on this session's ring. A thread that is not a session's
// answers false and gets the process-wide applier, i.e. the single-context world every
// other shape in this tree still runs.
Bool ResolveThreadApplierKey(MG_Pipe::MGPipeApplierKey* outKey) {
SessionRuntime* runtime = t_runtime;
if (runtime == nullptr) return false;
outKey->SessionKey = reinterpret_cast<Uint64>(&runtime->Session());
outKey->ContextToken = runtime->Session().CurrentContextToken();
return true;
}
} // namespace
void Detail::InstallSessionRuntimeHooks() {
if (MG_Pipe::g_pipeInputsThreadResolver != &ResolveThreadInputs) {
MG_Pipe::g_pipeInputsThreadResolver = &ResolveThreadInputs;
}
MG_Backend::DirectGLES::SetNativeContextKeyResolver(&ResolveThreadNativeContextKey);
MG_Pipe::MGPipeSetApplierKeyResolver(&ResolveThreadApplierKey);
}
SessionRuntime* Detail::CurrentRuntime() { return t_runtime; }
ServerSession* Detail::CurrentSession() {
SessionRuntime* runtime = t_runtime;
return runtime != nullptr ? &runtime->Session() : nullptr;
}
ServerLoop* Detail::CurrentLoop() {
SessionRuntime* runtime = t_runtime;
return runtime != nullptr ? &runtime->Loop() : nullptr;
}
MG_Pipe::PipeInputs* Detail::CurrentInputs() { return ResolveThreadInputs(); }
Detail::ThreadSessionScope::ThreadSessionScope(SessionRuntime& runtime) {
InstallSessionRuntimeHooks();
m_previous = t_runtime;
t_runtime = &runtime;
m_previousLatchDomain = FatalLatchDomainBind(runtime.LatchDomain());
// The segment resolver is the OTHER process-wide hook that used to mean "one session": the
// thunk is one function pointer for the process, and which TABLE it answers from is now
// per thread (PipeWireCodec.h). Bound before anything on this thread can decode a record.
runtime.Session().Segments().BindThreadResolver();
}
Detail::ThreadSessionScope::~ThreadSessionScope() {
if (m_previous != nullptr) {
m_previous->Session().Segments().BindThreadResolver();
} else {
Wire::SegmentTable::UnbindThreadResolver();
}
FatalLatchDomainBind(m_previousLatchDomain);
t_runtime = m_previous;
}
SessionRuntime::SessionRuntime()
: m_latchDomain(FatalLatchDomainOpen()) {}
SessionRuntime::~SessionRuntime() {
// P14 S5: this runtime's session is the key every applier it registered was filed
// under, so the last act of the session is to drop them. Safe here for the reason the
// latch domain is (SessionLoop::Stop has joined the apply thread; the control thread's
// scope outlives this object), and idempotent against Accept's own release.
MG_Pipe::MGPipeApplierReleaseSession(reinterpret_cast<Uint64>(&m_session));
// No thread may still be bound to this domain: the apply thread is joined by
// ServerLoop::Stop and the control thread's scope outlives this object.
FatalLatchDomainClose(m_latchDomain);
m_latchDomain = 0;
}
Uint32 LiveServerSessionCount() { return ServerSessionCount(); }
} // namespace MobileGL::MG_Remote::Server
+103
View File
@@ -0,0 +1,103 @@
// MobileGL - MobileGL/MG_Remote/Server/SessionRuntime.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P14 S2 (docs/Disaggregated/design/11-state-ownership.md): ONE SERVED SESSION'S PROCESS-LOCAL
// STATE, and the routing that lets the rest of the tree keep spelling the singletons.
//
// WHAT THE SINGLETONS WERE. ServerSessionInstance()/ServerLoopInstance() were the session, so one
// session per process was a property of every file that named them. This slice gives each served
// session its own {ServerSession, ServerLoop, PipeInputs} triple, held here, and resolves the two
// singleton accessors through the CALLING THREAD's session - which is what makes the ~900 sites
// that spell gPipeInputs, MGB_CTX, ServerSession::Active() or ServerLoopInstance() find the right
// one without one line of any of them moving.
//
// WHO IS A SESSION THREAD. Two threads per session, both created by the session itself:
// * the CONTROL thread - ServerMain::RunSession's own thread, which reads the Hello, drives the
// blocking control pump and ends the session (a std::thread per session in the supervisors, a
// forked process in the `--serve` worker shape);
// * the APPLY thread - ServerLoop's mgl-srv-apply, which applies the ring and owns the backend.
// ThreadSessionScope is installed on both, first thing. A thread with no scope resolves to the
// process-wide defaults, which is exactly the old behaviour and is what the CLIENT process, the
// inproc one-process client+server shape and every unit case keep getting.
//
// THE INPROC CLIENT+SERVER SHAPE IS DELIBERATELY UNTOUCHED. ClientSession::Start calls
// ServerLoopInstance().Start(ServerSessionInstance()) and never opens a scope, so its applier and
// its app thread still share the one process-wide PipeInputs block - the verb barrier's premise
// (one block two roles alternate on) is a property of that shape and this file must not break it.
// Only ServerMain::RunSession, which serves a session with its own transport, opens a scope.
#pragma once
#include <Includes.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include "ServerLoop.h"
#include "ServerSession.h"
#include <cstdint>
namespace MobileGL::MG_Remote::Server {
class SessionRuntime {
public:
SessionRuntime();
~SessionRuntime();
SessionRuntime(const SessionRuntime&) = delete;
SessionRuntime& operator=(const SessionRuntime&) = delete;
ServerSession& Session() { return m_session; }
ServerLoop& Loop() { return m_loop; }
MG_Pipe::PipeInputs& Inputs() { return m_inputs; }
// The latch domain this session owns (FatalFunnel.h). A named fault latched by one session
// must not end another, and must not be cleared by another's reap.
int LatchDomain() const { return m_latchDomain; }
private:
ServerSession m_session;
ServerLoop m_loop;
MG_Pipe::PipeInputs m_inputs;
int m_latchDomain = 0;
};
namespace Detail {
// The session of the CALLING thread, or nullptr when it is not a session's own thread.
SessionRuntime* CurrentRuntime();
ServerSession* CurrentSession();
ServerLoop* CurrentLoop();
// What MG_Pipe's per-thread resolver asks for: the calling thread's block, or nullptr for
// "use the process-wide one".
MG_Pipe::PipeInputs* CurrentInputs();
// Installs the process-wide hooks the per-session blocks need (the PipeInputs resolver).
// Idempotent, and called by ThreadSessionScope itself so no bring-up order can miss it.
void InstallSessionRuntimeHooks();
// THE SCOPE. Makes `runtime` the calling thread's session for as long as it lives: the
// session/loop the singleton accessors answer with, the PipeInputs block MGB_CTX reads,
// this session's segment resolver and this session's latch domain. Restores what was there
// before on the way out (two nested scopes are not a shape anything uses, but a thread that
// served one session and is then reused must not keep answering with the dead one).
class ThreadSessionScope {
public:
explicit ThreadSessionScope(SessionRuntime& runtime);
~ThreadSessionScope();
ThreadSessionScope(const ThreadSessionScope&) = delete;
ThreadSessionScope& operator=(const ThreadSessionScope&) = delete;
private:
SessionRuntime* m_previous = nullptr;
int m_previousLatchDomain = 0;
};
} // namespace Detail
// Sessions accepted and not yet closed in THIS process. 1 is the old shape; more is this
// slice's. ServerSession::Close gates the process-wide hooks (the reverse-channel table, the
// segment resolver) on it, and ServerMain logs it beside each session it starts.
Uint32 LiveServerSessionCount();
} // namespace MobileGL::MG_Remote::Server
@@ -24,6 +24,8 @@ namespace MobileGL::MG_Remote::Server {
case SurfaceControlOp::ReleaseResources: return "ReleaseResources"; case SurfaceControlOp::ReleaseResources: return "ReleaseResources";
case SurfaceControlOp::SetWindowHandle: return "SetWindowHandle"; case SurfaceControlOp::SetWindowHandle: return "SetWindowHandle";
case SurfaceControlOp::InitCapabilities: return "InitCapabilities"; case SurfaceControlOp::InitCapabilities: return "InitCapabilities";
case SurfaceControlOp::CreateContext: return "CreateContext";
case SurfaceControlOp::DestroyContext: return "DestroyContext";
case SurfaceControlOp::SwapBuffersInprocOnly: return "SwapBuffersInprocOnly"; case SurfaceControlOp::SwapBuffersInprocOnly: return "SwapBuffersInprocOnly";
case SurfaceControlOp::InitWindowSurfaceInprocOnly: return "InitWindowSurfaceInprocOnly"; case SurfaceControlOp::InitWindowSurfaceInprocOnly: return "InitWindowSurfaceInprocOnly";
case SurfaceControlOp::ProbeForTesting: return "ProbeForTesting"; case SurfaceControlOp::ProbeForTesting: return "ProbeForTesting";
@@ -47,6 +49,11 @@ namespace MobileGL::MG_Remote::Server {
// frame, which is why this row was missing; the request still has to // frame, which is why this row was missing; the request still has to
// cross, because under spawn the apply thread is in another process. // cross, because under spawn the apply thread is in another process.
case SurfaceControlOp::InitCapabilities: case SurfaceControlOp::InitCapabilities:
// P14 S1: the context lifecycle. The client's eglCreateContext/eglDestroyContext have a
// live caller under spawn (EGLImpl), so unlike the inproc-only trio beside them these two
// MUST cross - the server it is asking is in another process.
case SurfaceControlOp::CreateContext:
case SurfaceControlOp::DestroyContext:
return true; return true;
default: default:
return false; return false;
@@ -65,13 +65,25 @@ namespace MobileGL::MG_Remote::Server {
// every spawned eglMakeCurrent died at "surface op kind 11 cannot cross // every spawned eglMakeCurrent died at "surface op kind 11 cannot cross
// the wire (InprocOnlyOpOnTheWire)", one call after the backend came up. // the wire (InprocOnlyOpOnTheWire)", one call after the backend came up.
InitCapabilities = 11, InitCapabilities = 11,
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The EGL context lifecycle,
// appended to the schema and to this enum together so the identity cast below stays the
// whole mapping: the client's eglCreateContext/eglDestroyContext reach the server through
// THE SAME frame channel the nine surface forwarders use, on the same thread, in the same
// order - which is the property this slice needs and a second channel would not give.
CreateContext = 12,
DestroyContext = 13,
// INPROC-ONLY kinds: legal inside this process's frame channel, NEVER encodable onto the // INPROC-ONLY kinds: legal inside this process's frame channel, NEVER encodable onto the
// wire (SurfaceOpCodec refuses them by name). f0-egl's census found no production caller // wire (SurfaceOpCodec refuses them by name). f0-egl's census found no production caller
// for the two dead forwarders; they ride the same frame channel because the // for the two dead forwarders; they ride the same frame channel because the
// function-pointer mailbox is GONE, not because they are wire ops. // function-pointer mailbox is GONE, not because they are wire ops.
SwapBuffersInprocOnly = 12, // present travels as a record (the class-B Present emitter) //
InitWindowSurfaceInprocOnly = 13,// a client-side no-op; kept for the inproc test lane // THEIR VALUES MOVED 12/13/14 -> 14/15/16 when P14 S1 appended the two context ops. That
ProbeForTesting = 14, // MG_Test's arbitrary-work seam through the same channel // is safe by construction: these three never cross a process boundary and are named
// symbolically everywhere they are used, so nothing outside this build could have pinned
// them - where the eleven above and the two below are wire ABI and may never move.
SwapBuffersInprocOnly = 14, // present travels as a record (the class-B Present emitter)
InitWindowSurfaceInprocOnly = 15,// a client-side no-op; kept for the inproc test lane
ProbeForTesting = 16, // MG_Test's arbitrary-work seam through the same channel
}; };
// P12 (on-screen server window), D2. THE SERVER-OWNED WINDOW, AS A FRAME-LOCAL TAG. // P12 (on-screen server window), D2. THE SERVER-OWNED WINDOW, AS A FRAME-LOCAL TAG.
@@ -119,6 +131,11 @@ namespace MobileGL::MG_Remote::Server {
Int width = 0; Int width = 0;
Int height = 0; Int height = 0;
Int swapInterval = 0; Int swapInterval = 0;
// P14 S1. CreateContext's two values beyond `context` (which is the client context token
// itself): the share group the context was derived into, and the reserved flags word the
// schema carries so a later attribute has a slot. 0 on every other op.
Uint64 shareGroupToken = 0;
Uint32 contextFlags = 0;
// The reply half: written by the dispatch on the apply thread, read by the poster after // The reply half: written by the dispatch on the apply thread, read by the poster after
// the blocking handshake returns. `ok` is the forwarder's Bool answer; eglMajor/eglMinor // the blocking handshake returns. `ok` is the forwarder's Bool answer; eglMajor/eglMinor
// are InitializeDisplay's out values. // are InitializeDisplay's out values.
@@ -144,14 +161,16 @@ namespace MobileGL::MG_Remote::Server {
// Exhaustive by the language's aggregate decomposition rule: adding ANY member // Exhaustive by the language's aggregate decomposition rule: adding ANY member
// without extending this binding is a compile error, so new fields cannot evade it. // without extending this binding is a compile error, so new fields cannot evade it.
const auto& [kind, seq, display, surface, readSurface, context, windowBackend, const auto& [kind, seq, display, surface, readSurface, context, windowBackend,
nativeToken, width, height, swapInterval, ok, eglMajor, eglMinor, eventHead, nativeToken, width, height, swapInterval, shareGroupToken, contextFlags, ok,
refusal] = frame; eglMajor, eglMinor, eventHead, refusal] = frame;
return IsSurfaceControlValue<decltype(kind)> && IsSurfaceControlValue<decltype(seq)> && return IsSurfaceControlValue<decltype(kind)> && IsSurfaceControlValue<decltype(seq)> &&
IsSurfaceControlValue<decltype(display)> && IsSurfaceControlValue<decltype(surface)> && IsSurfaceControlValue<decltype(display)> && IsSurfaceControlValue<decltype(surface)> &&
IsSurfaceControlValue<decltype(readSurface)> && IsSurfaceControlValue<decltype(context)> && IsSurfaceControlValue<decltype(readSurface)> && IsSurfaceControlValue<decltype(context)> &&
IsSurfaceControlValue<decltype(windowBackend)> && IsSurfaceControlValue<decltype(nativeToken)> && IsSurfaceControlValue<decltype(windowBackend)> && IsSurfaceControlValue<decltype(nativeToken)> &&
IsSurfaceControlValue<decltype(width)> && IsSurfaceControlValue<decltype(height)> && IsSurfaceControlValue<decltype(width)> && IsSurfaceControlValue<decltype(height)> &&
IsSurfaceControlValue<decltype(swapInterval)> && IsSurfaceControlValue<decltype(ok)> && IsSurfaceControlValue<decltype(swapInterval)> &&
IsSurfaceControlValue<decltype(shareGroupToken)> &&
IsSurfaceControlValue<decltype(contextFlags)> && IsSurfaceControlValue<decltype(ok)> &&
IsSurfaceControlValue<decltype(eglMajor)> && IsSurfaceControlValue<decltype(eglMinor)> && IsSurfaceControlValue<decltype(eglMajor)> && IsSurfaceControlValue<decltype(eglMinor)> &&
IsSurfaceControlValue<decltype(eventHead)> && IsSurfaceControlValue<decltype(refusal)>; IsSurfaceControlValue<decltype(eventHead)> && IsSurfaceControlValue<decltype(refusal)>;
} }
+43 -11
View File
@@ -263,7 +263,8 @@ namespace MobileGL::MG_Remote::Wire {
X(SetProgramBindings, MGPProgramBindings) \ X(SetProgramBindings, MGPProgramBindings) \
X(DeleteStreamOutput, MGPStreamOutputBind) \ X(DeleteStreamOutput, MGPStreamOutputBind) \
X(ReadPixelsToBuffer, MGPReadbackToBuffer) \ X(ReadPixelsToBuffer, MGPReadbackToBuffer) \
X(GetTextureImageToBuffer, MGPReadbackToBuffer) X(GetTextureImageToBuffer, MGPReadbackToBuffer) \
X(BindContext, MGPBindContext)
namespace { namespace {
@@ -375,9 +376,19 @@ namespace MobileGL::MG_Remote::Wire {
// The process resolver is a plain function pointer with no user datum // The process resolver is a plain function pointer with no user datum
// (MGPipeHostSpan.h:46), so the table it resolves through has to be found here. // (MGPipeHostSpan.h:46), so the table it resolves through has to be found here.
//
// P14 S2: TWO PLACES, IN THIS ORDER. The CALLING THREAD's binding is the authoritative one
// - a served session's threads are bound to that session's table (ThreadSessionScope), so
// two sessions in one process never resolve each other's segments. The process-wide table
// below is the fallback for a thread with no binding, which is every shape that has only
// one session (and is what the thread-bound call reduces to there).
SegmentTable* g_processResolverTable = nullptr; SegmentTable* g_processResolverTable = nullptr;
thread_local SegmentTable* t_threadResolverTable = nullptr;
const void* ProcessResolverThunk(Uint32 seg, Uint64 offset, Uint64 size) { const void* ProcessResolverThunk(Uint32 seg, Uint64 offset, Uint64 size) {
if (SegmentTable* const bound = t_threadResolverTable) {
return bound->Resolve(seg, offset, size);
}
if (g_processResolverTable == nullptr) { if (g_processResolverTable == nullptr) {
return nullptr; return nullptr;
} }
@@ -484,24 +495,35 @@ namespace MobileGL::MG_Remote::Wire {
return static_cast<const Uint8*>(view.Base) + offset; return static_cast<const Uint8*>(view.Base) + offset;
} }
void SegmentTable::BindThreadResolver() { t_threadResolverTable = this; }
void SegmentTable::UnbindThreadResolver() { t_threadResolverTable = nullptr; }
void SegmentTable::InstallProcessResolver() { void SegmentTable::InstallProcessResolver() {
if (MG_Pipe::gMGPipeSegmentResolver != nullptr && g_processResolverTable != this) { if (t_threadResolverTable != nullptr && t_threadResolverTable != this) {
// Table 3: there is exactly ONE gMGPipeSegmentResolver per process, the SERVER // Table 3, P14 S2: one thread resolves through ONE table. Two tables on one thread is
// role installs it before the apply thread starts, and the client never resolves a // the shape this line exists to make loud - a session's threads are per session, so a
// span at all. Two roles racing on one inline variable is loud rather than silent // well-formed process never reaches it, while a second SESSION (its own threads, its
// because of this line. // own table) is exactly what the per-thread binding is for and does not.
WireProtocolFatal("SegmentTable::InstallProcessResolver", WireProtocolFatal("SegmentTable::InstallProcessResolver",
"a process segment resolver is already installed; the server role " "a process segment resolver is already installed ON THIS THREAD for "
"installs it once, before the apply thread starts"); "another session's table; a served session's threads belong to one "
"session");
}
if (MG_Pipe::gMGPipeSegmentResolver == nullptr) {
MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk;
} }
g_processResolverTable = this; g_processResolverTable = this;
MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk; BindThreadResolver();
} }
void SegmentTable::UninstallProcessResolver() { void SegmentTable::UninstallProcessResolver() {
// Teardown order (table 3): uninstall AFTER the join, never before - a record still in // Teardown order (table 3): uninstall AFTER the join, never before - a record still in
// flight can still resolve. // flight can still resolve. P14 S2: this unbinds the calling thread and drops the
MG_Pipe::gMGPipeSegmentResolver = nullptr; // process-wide fallback only; the hook stays installed because another session of this
// process may still be decoding records (ServerSession::Close gates this call on the last
// session).
UnbindThreadResolver();
g_processResolverTable = nullptr; g_processResolverTable = nullptr;
} }
@@ -2533,6 +2555,16 @@ namespace MobileGL::MG_Remote::Wire {
return m_verbs != nullptr && return m_verbs != nullptr &&
m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload)); m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload));
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// The third control record, and the applier_reset shape exactly: a fixed-size POD with
// no blob, no tail and no reply, so the generated bounds gate is the whole validation
// and the arm hands over. What the sink does with the token - "this session's current
// context" - is a session fact, not a shape.
case MGPWireOp::BindContext:
return m_verbs != nullptr &&
m_verbs->OnBindContext(*static_cast<const MGPBindContext*>(payload));
// ---- P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, opcode 79 ------------- // ---- P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, opcode 79 -------------
// //
// An ordinary set_* row, unlike the two control records beside it: a fixed-width POD // An ordinary set_* row, unlike the two control records beside it: a fixed-width POD
+28 -2
View File
@@ -95,10 +95,23 @@ namespace MobileGL::MG_Remote::Wire {
const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const; const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const;
// Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if // Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if
// a resolver is already installed, because two roles racing on one inline variable is // a DIFFERENT table is already installed ON THE CALLING THREAD, because one thread
// the failure this function exists to make loud. // serving two sessions at once is the failure this function exists to make loud.
//
// P14 S2: THE TABLE IS PER THREAD, THE HOOK IS PER PROCESS. Several sessions in one
// process each have their own table and their own threads, so a second session's install
// is neither a race nor an error: it installs the one process-wide thunk (idempotent) and
// binds the calling thread to ITS table. g_processResolverTable stays as the fallback for
// a thread that bound nothing - the inproc one-process client+server shape, whose apply
// thread is created after the app thread's Accept and does not bind.
void InstallProcessResolver(); void InstallProcessResolver();
// Unbinds the CALLING thread and clears the process-wide fallback. The hook itself stays
// installed: another session may still be live, and a null answer from a table-less thunk
// is exactly what a null hook answered.
static void UninstallProcessResolver(); static void UninstallProcessResolver();
// P14 S2: binds the CALLING thread to this table (ThreadSessionScope's two calls).
void BindThreadResolver();
static void UnbindThreadResolver();
private: private:
Transport::ILink* m_link = nullptr; Transport::ILink* m_link = nullptr;
@@ -661,6 +674,19 @@ namespace MobileGL::MG_Remote::Wire {
(void)death; (void)death;
return false; return false;
} }
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// The applier_reset hand-over shape, one plane over: a fixed-size POD the bounds gate has
// already proved, no blob, no tail, no reply, so the codec validates nothing further and
// the SERVER'S sink does the work - the session's current context token becomes the
// record's. It is not an assertion like OnApplierReset's serial: the token is minted by
// the client's EGL layer and the server has no independent count of it, so an unknown
// token is the sink's own refusal to make and not a shape the codec can check.
virtual Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) {
(void)bind;
return false;
}
}; };
// Not thread safe: one decoder on the apply thread, by construction. // Not thread safe: one decoder on the apply thread, by construction.
+82
View File
@@ -233,6 +233,11 @@ namespace MobileGL {
m_contextOwners.erase(ownerIt); m_contextOwners.erase(ownerIt);
} }
} }
// 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);
}
m_threadCurrents.erase(currentIt); m_threadCurrents.erase(currentIt);
DestroyPendingSurfaceIfUnused(drawSurface); DestroyPendingSurfaceIfUnused(drawSurface);
DestroyPendingSurfaceIfUnused(readSurface); DestroyPendingSurfaceIfUnused(readSurface);
@@ -306,6 +311,12 @@ namespace MobileGL {
m_contextOwners.erase(ownerIt); m_contextOwners.erase(ownerIt);
} }
} }
// Only the CALLING thread's TLS is reachable from here - another
// thread's current is dropped from the table, and its own next EGL
// call re-establishes the binding.
if (threadIt->first == CurrentThreadKey() && threadIt->second.RestoreThreadGLState) {
pGLContext = Move(threadIt->second.RestoreThreadGLState);
}
threadIt = m_threadCurrents.erase(threadIt); threadIt = m_threadCurrents.erase(threadIt);
} else { } else {
++threadIt; ++threadIt;
@@ -654,6 +665,30 @@ namespace MobileGL {
.MinorVersion = 0, .MinorVersion = 0,
}; };
// P14 S1: the wire identity, minted where the context is born. A context that
// shares with an existing one joins ITS share group; EGL_NO_CONTEXT starts a new
// one. Both tokens are 0-free by construction (the counters start at 1), because
// 0 is the wire's "no context" on both planes.
contextObject.ClientContextToken = m_nextClientContextToken++;
SharedPtr<GLState::ShareGroupState> shareGroup;
if (shareCtx != nullptr) {
const auto* shared = TryGetContext(shareCtx);
if (shared != nullptr && shared->GLStateObject) {
shareGroup = shared->GLStateObject->GetShareGroup();
contextObject.ShareGroupToken = shared->ShareGroupToken;
} else {
contextObject.ShareGroupToken = m_nextShareGroupToken++;
}
} else {
contextObject.ShareGroupToken = m_nextShareGroupToken++;
}
if (!shareGroup) {
shareGroup = MakeShared<GLState::ShareGroupState>(contextObject.ShareGroupToken);
}
// 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 (auto value = ParseAttribValue(attribList, EGL_CONTEXT_CLIENT_VERSION); value) { if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_CLIENT_VERSION); value) {
contextObject.ClientVersion = *value; contextObject.ClientVersion = *value;
contextObject.MajorVersion = *value; contextObject.MajorVersion = *value;
@@ -779,6 +814,37 @@ namespace MobileGL {
return ctx && ctx->Display == display; return ctx && ctx->Display == display;
} }
// P14 S1. 0 for an unknown handle or a thread with no current context: 0 is the
// wire's "no context" on both planes, so an answer that cannot be given is given as
// the value that means "nothing is bound" rather than as a handle-shaped guess.
Uint64 EGLContext::GetContextClientToken(EGLContextHandle context) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* ctx = TryGetContext(context);
return ctx != nullptr ? ctx->ClientContextToken : 0;
}
Uint64 EGLContext::GetContextShareGroupToken(EGLContextHandle context) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* ctx = TryGetContext(context);
return ctx != nullptr ? ctx->ShareGroupToken : 0;
}
SharedPtr<GLState::GLContext> EGLContext::GetContextGLState(EGLContextHandle context) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* ctx = TryGetContext(context);
return ctx != nullptr ? ctx->GLStateObject : nullptr;
}
Uint64 EGLContext::CurrentContextClientToken() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto currentIt = m_threadCurrents.find(CurrentThreadKey());
if (currentIt == m_threadCurrents.end()) {
return 0;
}
const auto* ctx = TryGetContext(currentIt->second.Context);
return ctx != nullptr ? ctx->ClientContextToken : 0;
}
Bool EGLContext::IsCurrentContextOpenGLCoreProfile() const { Bool EGLContext::IsCurrentContextOpenGLCoreProfile() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex); const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey()); auto currentIt = m_threadCurrents.find(CurrentThreadKey());
@@ -1276,6 +1342,16 @@ namespace MobileGL {
} }
} }
// P14 S3: whatever this thread's GL state was before the first EGL context was
// bound to it is what a later release goes back to.
SharedPtr<GLState::GLContext> restoreThreadGLState;
if (const auto currentIt = m_threadCurrents.find(threadKey);
currentIt != m_threadCurrents.end()) {
restoreThreadGLState = currentIt->second.RestoreThreadGLState;
} else {
restoreThreadGLState = pGLContext;
}
ReleaseThreadUnlocked(threadKey); ReleaseThreadUnlocked(threadKey);
m_threadCurrents[threadKey] = ThreadCurrentState{ m_threadCurrents[threadKey] = ThreadCurrentState{
@@ -1283,9 +1359,15 @@ namespace MobileGL {
.DrawSurface = draw, .DrawSurface = draw,
.ReadSurface = read, .ReadSurface = read,
.Context = context, .Context = context,
.RestoreThreadGLState = Move(restoreThreadGLState),
}; };
if (context != nullptr) { if (context != nullptr) {
m_contextOwners[context] = threadKey; m_contextOwners[context] = threadKey;
// P14 S3: this thread's GL calls land in this context's GLContext from here
// on, and not before the switch above has been committed.
if (const auto* contextObject = TryGetContext(context); contextObject != nullptr) {
pGLContext = contextObject->GLStateObject;
}
} }
return true; return true;
} }
+33
View File
@@ -9,6 +9,7 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <type_traits> #include <type_traits>
#include <MG_State/GLState/Core.h>
namespace MobileGL { namespace MobileGL {
namespace MG_State { namespace MG_State {
@@ -69,6 +70,20 @@ namespace MobileGL {
Bool IsCurrentContextOpenGLCompatibilityProfile() const; Bool IsCurrentContextOpenGLCompatibilityProfile() const;
EGLint GetCurrentContextFlags() const; EGLint GetCurrentContextFlags() const;
// P14 S1. The wire identity of a context, and of the context the CALLING THREAD
// has current (0 when it has none - the release edge's value, and the value a
// session that never crossed a bind_context answers with). Read by MG_Impl's two
// producers: EGLImpl's CreateContext/DestroyContext control frames and PipeFill's
// bind_context emission at the make-current edge.
Uint64 GetContextClientToken(EGLContextHandle context) const;
Uint64 GetContextShareGroupToken(EGLContextHandle context) const;
Uint64 CurrentContextClientToken() const;
// P14 S3. The frontend GL state this context owns, so a caller outside MG_State
// can reach the other half of a context it just made current. Null for an
// unknown handle.
SharedPtr<GLState::GLContext> GetContextGLState(EGLContextHandle context) const;
// Surface // Surface
EGLSurfaceHandle CreateWindowSurface(EGLDisplayHandle display, EGLConfigHandle config, EGLSurfaceHandle CreateWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
NativeWindowType window, const EGLint* attribList); NativeWindowType window, const EGLint* attribList);
@@ -180,6 +195,17 @@ namespace MobileGL {
EGLDisplayHandle Display = EGL_NO_DISPLAY; EGLDisplayHandle Display = EGL_NO_DISPLAY;
EGLConfigHandle Config = nullptr; EGLConfigHandle Config = nullptr;
EGLContextHandle SharedContext = nullptr; EGLContextHandle SharedContext = nullptr;
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The wire identity of
// this context: `ClientContextToken` is what bind_context carries and what the
// server's per-session context table is keyed by, `ShareGroupToken` the group
// it was derived into (ShareContext's group, or one of its own). Minted here,
// in the one place a context is born, so no caller can invent a second one.
Uint64 ClientContextToken = 0;
Uint64 ShareGroupToken = 0;
// P14 S3. The GL state of THIS EGL context. Its object registries live in the
// 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;
EGLenum ClientAPI = EGL_OPENGL_API; EGLenum ClientAPI = EGL_OPENGL_API;
EGLint ClientVersion = 1; EGLint ClientVersion = 1;
EGLint MajorVersion = 1; EGLint MajorVersion = 1;
@@ -226,6 +252,10 @@ namespace MobileGL {
EGLSurfaceHandle DrawSurface = EGL_NO_SURFACE; EGLSurfaceHandle DrawSurface = EGL_NO_SURFACE;
EGLSurfaceHandle ReadSurface = EGL_NO_SURFACE; EGLSurfaceHandle ReadSurface = EGL_NO_SURFACE;
EGLContextHandle Context = nullptr; EGLContextHandle Context = nullptr;
// P14 S3. What pGLContext held on this thread BEFORE the first eglMakeCurrent
// bound an EGL context to it, so releasing restores exactly the binding the
// thread had rather than a guessed default.
SharedPtr<GLState::GLContext> RestoreThreadGLState;
}; };
template <typename HandleType> template <typename HandleType>
@@ -269,6 +299,9 @@ namespace MobileGL {
Uint64 m_nextContextHandle = 1; Uint64 m_nextContextHandle = 1;
Uint64 m_nextSyncHandle = 1; Uint64 m_nextSyncHandle = 1;
Uint64 m_nextImageHandle = 1; Uint64 m_nextImageHandle = 1;
// P14 S1. Dense from 1, never reused, never 0: 0 is "no context" on both planes.
Uint64 m_nextClientContextToken = 1;
Uint64 m_nextShareGroupToken = 1;
UnorderedMap<DisplayLookupKey, EGLDisplayHandle, DisplayLookupHasher> m_displayLookup; UnorderedMap<DisplayLookupKey, EGLDisplayHandle, DisplayLookupHasher> m_displayLookup;
UnorderedMap<EGLDisplayHandle, DisplayObject> m_displays; UnorderedMap<EGLDisplayHandle, DisplayObject> m_displays;
@@ -9,7 +9,7 @@
#include "BufferState.h" #include "BufferState.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
BufferState::BufferState() : m_indexGenerator(1024, 1) { BufferState::BufferState() {
for (SizeT i = 0; i < m_bindingSlots.size(); ++i) { for (SizeT i = 0; i < m_bindingSlots.size(); ++i) {
m_bindingSlots[i] = BindingSlot<BufferObject>(GlobalBufferTargets[i]); m_bindingSlots[i] = BindingSlot<BufferObject>(GlobalBufferTargets[i]);
} }
@@ -18,28 +18,6 @@ namespace MobileGL::MG_State::GLState {
} }
} }
const SharedPtr<BufferObject>& BufferState::GetBufferObject(Uint index) {
auto it = m_bufferObjects.find(index);
if (it != m_bufferObjects.end()) {
return it->second;
}
static SharedPtr<BufferObject> nullBufferObject = nullptr;
return nullBufferObject;
}
void BufferState::GenerateNames(Uint number, Vector<Uint>& buffers) {
buffers.resize(number);
m_indexGenerator.Generate(number, buffers.data());
}
const SharedPtr<BufferObject>& BufferState::CreateBufferObject(Uint index) {
auto& bufferObj = m_bufferObjects[index];
if (!bufferObj) {
bufferObj = MakeShared<BufferObject>(index);
}
return bufferObj;
}
BindingSlot<BufferObject>& BufferState::GetBindingSlot(BufferTarget target) { BindingSlot<BufferObject>& BufferState::GetBindingSlot(BufferTarget target) {
for (auto& bindingSlot : m_bindingSlots) { for (auto& bindingSlot : m_bindingSlots) {
if (bindingSlot.GetTarget() == target) { if (bindingSlot.GetTarget() == target) {
@@ -50,53 +28,33 @@ namespace MobileGL::MG_State::GLState {
return m_bindingSlots[0]; return m_bindingSlots[0];
} }
void BufferState::MarkBufferObjectForDeletion(Uint index) { void BufferState::UnbindBufferObject(const SharedPtr<BufferObject>& bufferObject) {
if (m_indexGenerator.IsValid(index)) { if (!bufferObject) return;
auto it = m_bufferObjects.find(index); for (auto& bindingSlot : m_bindingSlots) {
if (it != m_bufferObjects.end()) { if (bindingSlot.GetBoundObject() == bufferObject) {
for (auto& bindingSlot : m_bindingSlots) { bindingSlot.Bind(nullptr);
if (bindingSlot.GetBoundObject() == it->second) { }
bindingSlot.Bind(nullptr); }
} for (auto& bindingPointArray : m_bufferBindPointTargets) {
} for (auto& bindingPoint : bindingPointArray) {
for (auto& bindingPointArray : m_bufferBindPointTargets) { if (bindingPoint.GetBoundObject() == bufferObject) {
for (auto& bindingPoint : bindingPointArray) { bindingPoint.Bind(nullptr);
if (bindingPoint.GetBoundObject() == it->second) { bindingPoint.ClearRange();
bindingPoint.Bind(nullptr);
bindingPoint.ClearRange();
#if MOBILEGL_PIPE_PUSH #if MOBILEGL_PIPE_PUSH
// An UNBIND, and the one writer of an indexed point that is not an // An UNBIND, and the one writer of an indexed point that is not an
// entry point (GL_Buffer.cpp's BindBuffer{Base,Range}_State and // entry point (GL_Buffer.cpp's BindBuffer{Base,Range}_State and
// Core.cpp's transform-feedback writers bump their own): without the // Core.cpp's transform-feedback writers bump their own): without the
// bump the target's window is never re-sent, the server goes on naming // bump the target's window is never re-sent, the server goes on naming
// this buffer's handle, and once its slot is reused at a newer // this buffer's handle, and once its slot is reused at a newer
// generation the next draw or dispatch that walks the window asks the // generation the next draw or dispatch that walks the window asks the
// backend for the dead one - Espryt dies with Fatal{ProtocolCorruption, // backend for the dead one - Espryt dies with Fatal{ProtocolCorruption,
// "BackendSlotTable.Generation"} (DeletedBoundBufferScenario). // "BackendSlotTable.Generation"} (DeletedBoundBufferScenario).
NoteBindPointChanged(BufferBindPointTargets[static_cast<SizeT>( NoteBindPointChanged(BufferBindPointTargets[static_cast<SizeT>(
&bindingPointArray - m_bufferBindPointTargets.data())]); &bindingPointArray - m_bufferBindPointTargets.data())]);
#endif #endif
}
}
} }
// Erase through the iterator already in hand: erase(key) would repeat the
// find() above, and the successor scan that once made key-based
// erase the cheaper of the two no longer happens here - erase(iterator)
// hands back an unconverted proxy, and the scan is what converting it
// would cost. The unbind loops above touch only the binding arrays, so
// `it` is still live.
m_bufferObjects.erase(it);
} }
m_indexGenerator.Delete(index);
} }
}
Bool BufferState::ValidateName(Uint index) const {
return m_indexGenerator.IsValid(index);
}
Bool BufferState::ValidateBufferObject(Uint index) const {
return m_bufferObjects.find(index) != m_bufferObjects.end();
} }
BindingSlotRange1D<BufferObject>& BufferState::GetBindingPoint(BufferTarget target, Uint index) { BindingSlotRange1D<BufferObject>& BufferState::GetBindingPoint(BufferTarget target, Uint index) {
@@ -28,13 +28,12 @@ namespace MobileGL::MG_State::GLState {
// costs the unused tail of three arrays. // costs the unused tail of three arrays.
constexpr SizeT BufferBindingPointCount = 84; constexpr SizeT BufferBindingPointCount = 84;
// The per-context half of the buffer state: which buffer is bound where. The objects
// themselves and their names belong to the share group (ShareGroupState).
class BufferState { class BufferState {
public: public:
BufferState(); BufferState();
const SharedPtr<BufferObject>& GetBufferObject(Uint index);
void GenerateNames(Uint number, Vector<Uint>& buffers);
const SharedPtr<BufferObject>& CreateBufferObject(Uint index);
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target); BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
// For glBindBufferBase / glBindBufferRange // For glBindBufferBase / glBindBufferRange
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index); BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
@@ -60,9 +59,9 @@ namespace MobileGL::MG_State::GLState {
if (it == BufferBindPointTargets.end()) return 0; if (it == BufferBindPointTargets.end()) return 0;
return m_touchedBindPointCount[std::distance(BufferBindPointTargets.begin(), it)]; return m_touchedBindPointCount[std::distance(BufferBindPointTargets.begin(), it)];
} }
void MarkBufferObjectForDeletion(Uint index); // Detaches a deleted buffer from THIS context's bindings only (GL 4.6 core 6.1: a delete
Bool ValidateName(Uint index) const; // resets the deleting context's bindings, and other contexts keep their own).
Bool ValidateBufferObject(Uint index) const; void UnbindBufferObject(const SharedPtr<BufferObject>& bufferObject);
#if MOBILEGL_PIPE_PUSH #if MOBILEGL_PIPE_PUSH
// P2 brief D4: "did the contents of ANY buffer object move". One counter for every // P2 brief D4: "did the contents of ANY buffer object move". One counter for every
@@ -110,8 +109,6 @@ namespace MobileGL::MG_State::GLState {
Uint64 m_anyBufferChangeGeneration = 0; Uint64 m_anyBufferChangeGeneration = 0;
Array<Uint64, BufferBindPointTargets.size()> m_bindPointGeneration{}; Array<Uint64, BufferBindPointTargets.size()> m_bindPointGeneration{};
#endif #endif
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
IndexGenerator<Uint> m_indexGenerator;
Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots; Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots;
// TODO: query the count somewhere globally? // TODO: query the count somewhere globally?
// For glBindBufferBase / glBindBufferRange // For glBindBufferBase / glBindBufferRange
+124 -47
View File
@@ -18,9 +18,52 @@
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
namespace MobileGL::MG_State { namespace MobileGL::MG_State {
namespace {
// Leak-at-exit storage; see GlobalObjects.cpp.
SharedPtr<GLState::GLContext>& DefaultGLContextSlot() {
static SharedPtr<GLState::GLContext>* slot = new SharedPtr<GLState::GLContext>();
return *slot;
}
SharedPtr<GLState::FramebufferObject>& DefaultFramebufferSlot() {
static SharedPtr<GLState::FramebufferObject>* slot = new SharedPtr<GLState::FramebufferObject>();
return *slot;
}
}
const SharedPtr<GLState::GLContext>& ProcessDefaultGLContext() {
auto& slot = DefaultGLContextSlot();
if (!slot) {
slot = MakeShared<GLState::GLContext>();
}
return slot;
}
void ReleaseProcessDefaultGLContext() {
auto& slot = DefaultGLContextSlot();
// Only the calling thread's TLS is reachable here, and only when it is still holding the
// fallback rather than an EGL context of its own.
if (pGLContext == slot) pGLContext.reset();
DefaultFramebufferSlot().reset();
slot.reset();
}
void SetDefaultFramebuffer(SharedPtr<GLState::FramebufferObject> framebuffer) {
DefaultFramebufferSlot() = Move(framebuffer);
}
const SharedPtr<GLState::FramebufferObject>& DefaultFramebuffer() {
return DefaultFramebufferSlot();
}
void Init() { void Init() {
MGLOG_D("Initializing MobileGL State..."); MGLOG_D("Initializing MobileGL State...");
pGLContext = MakeUnique<GLState::GLContext>(); // Rebuilt on every Initialize(): the fallback context starts empty, so a thread that
// never crossed an eglMakeCurrent cannot see the previous run's objects. The default
// framebuffer is rebuilt too - MG_Impl::Init() installs the new one right after.
DefaultFramebufferSlot().reset();
DefaultGLContextSlot() = MakeShared<GLState::GLContext>();
pGLContext = ProcessDefaultGLContext();
pEGLContext = MakeUnique<EGLState::EGLContext>(); pEGLContext = MakeUnique<EGLState::EGLContext>();
} }
@@ -89,11 +132,11 @@ namespace MobileGL::MG_State {
// Buffer // Buffer
void GLContext::GenBufferNames(Uint number, Vector<Uint>& buffers) { void GLContext::GenBufferNames(Uint number, Vector<Uint>& buffers) {
m_bufferState.GenerateNames(number, buffers); Group().GenerateBufferNames(number, buffers);
} }
const SharedPtr<BufferObject>& GLContext::GetBufferObject(Uint index) { const SharedPtr<BufferObject>& GLContext::GetBufferObject(Uint index) {
return m_bufferState.GetBufferObject(index); return Group().GetBufferObject(index);
} }
BindingSlot<BufferObject>& GLContext::GetBufferBindingSlot(BufferTarget target) { BindingSlot<BufferObject>& GLContext::GetBufferBindingSlot(BufferTarget target) {
@@ -111,7 +154,7 @@ namespace MobileGL::MG_State {
} }
const SharedPtr<BufferObject>& GLContext::CreateBufferObject(Uint index) { const SharedPtr<BufferObject>& GLContext::CreateBufferObject(Uint index) {
return m_bufferState.CreateBufferObject(index); return Group().CreateBufferObject(index);
} }
void GLContext::MarkBufferObjectForDeletion(Uint index) { void GLContext::MarkBufferObjectForDeletion(Uint index) {
@@ -123,7 +166,7 @@ namespace MobileGL::MG_State {
// previous every-VAO scan was wrong per spec and O(VAOs) per delete — // previous every-VAO scan was wrong per spec and O(VAOs) per delete —
// with one VAO per chunk section, vanilla's steady buffer churn made it // with one VAO per chunk section, vanilla's steady buffer churn made it
// dominate the render thread and FPS decay over session time. // dominate the render thread and FPS decay over session time.
auto bufferObject = m_bufferState.GetBufferObject(index); const auto bufferObject = Group().GetBufferObject(index);
const auto& vao = m_vertexArrayState.GetBoundVertexArray(); const auto& vao = m_vertexArrayState.GetBoundVertexArray();
if (vao != nullptr) { if (vao != nullptr) {
if (vao->GetIndexBufferBindingSlot().GetBoundObject() == bufferObject) { if (vao->GetIndexBufferBindingSlot().GetBoundObject() == bufferObject) {
@@ -135,17 +178,18 @@ namespace MobileGL::MG_State {
} }
} }
} }
m_bufferState.UnbindBufferObject(bufferObject);
} }
m_bufferState.MarkBufferObjectForDeletion(index); Group().MarkBufferObjectForDeletion(index);
} }
Bool GLContext::ValidateBufferName(Uint index) const { Bool GLContext::ValidateBufferName(Uint index) const {
return m_bufferState.ValidateName(index); return m_shareGroup->ValidateBufferName(index);
} }
Bool GLContext::ValidateBufferObject(Uint index) const { Bool GLContext::ValidateBufferObject(Uint index) const {
return m_bufferState.ValidateBufferObject(index); return m_shareGroup->ValidateBufferObject(index);
} }
// VertexArray // VertexArray
@@ -269,11 +313,11 @@ namespace MobileGL::MG_State {
// Texture // Texture
void GLContext::GenTextureNames(Uint number, Vector<Uint>& textures) { void GLContext::GenTextureNames(Uint number, Vector<Uint>& textures) {
m_textureState.GenerateNames(number, textures); Group().GenerateTextureNames(number, textures);
} }
const SharedPtr<ITextureObject>& GLContext::GetTextureObject(Uint index) { const SharedPtr<ITextureObject>& GLContext::GetTextureObject(Uint index) {
return m_textureState.GetTextureObject(index); return Group().GetTextureObject(index);
} }
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const { const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
@@ -281,14 +325,14 @@ namespace MobileGL::MG_State {
} }
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) { const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
return m_textureState.CreateTextureObject(index, target); return Group().CreateTextureObject(index, target);
} }
const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject( const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel, Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
Uint numLevels, Uint minLayer, Uint numLayers) { Uint numLevels, Uint minLayer, Uint numLayers) {
return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer, return Group().CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
numLayers); numLayers);
} }
void GLContext::MarkTextureObjectForDeletion(Uint index) { void GLContext::MarkTextureObjectForDeletion(Uint index) {
@@ -302,7 +346,8 @@ namespace MobileGL::MG_State {
// contents rather than those of whatever the application put in its place - the name // contents rather than those of whatever the application put in its place - the name
// it deleted usually comes straight back from the next glGenTextures, so the two are // it deleted usually comes straight back from the next glGenTextures, so the two are
// indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient). // indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
if (const auto& textureObject = m_textureState.GetTextureObject(index)) { const auto textureObject = Group().GetTextureObject(index);
if (textureObject) {
for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount); for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
++targetIndex) { ++targetIndex) {
const auto& framebuffer = const auto& framebuffer =
@@ -318,7 +363,10 @@ namespace MobileGL::MG_State {
} }
} }
} }
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive()); // This context's units only: the object is gone for every context in the share
// group, but each of them detaches it from its own bindings (GL 4.6 core 8.1).
m_textureState.UnbindTextureFromUnits(textureObject);
Group().MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
} }
TextureUnit& GLContext::GetTextureUnitObject(Int unit) { TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
@@ -334,11 +382,11 @@ namespace MobileGL::MG_State {
} }
Bool GLContext::ValidateTextureName(Uint index) const { Bool GLContext::ValidateTextureName(Uint index) const {
return m_textureState.ValidateName(index); return m_shareGroup->ValidateTextureName(index);
} }
Bool GLContext::ValidateTextureObject(Uint index) const { Bool GLContext::ValidateTextureObject(Uint index) const {
return m_textureState.ValidateTextureObject(index); return m_shareGroup->ValidateTextureObject(index);
} }
Int GLContext::GetActiveTextureUnit() const { Int GLContext::GetActiveTextureUnit() const {
@@ -351,51 +399,67 @@ namespace MobileGL::MG_State {
// Program // Program
Uint GLContext::CreateProgram() { Uint GLContext::CreateProgram() {
return m_programState.CreateProgram(); return Group().Programs().CreateProgram();
} }
Uint GLContext::CreateShader(const ShaderStage stage) { Uint GLContext::CreateShader(const ShaderStage stage) {
return m_programState.CreateShader(stage); return Group().Programs().CreateShader(stage);
} }
void GLContext::MarkProgramForDeletion(const Uint index) { void GLContext::MarkProgramForDeletion(const Uint index) {
return m_programState.MarkProgramObjectForDeletion(index); return Group().Programs().MarkProgramObjectForDeletion(index, m_currentProgram);
} }
void GLContext::MarkShaderForDeletion(const Uint index) { void GLContext::MarkShaderForDeletion(const Uint index) {
return m_programState.MarkShaderObjectForDeletion(index); return Group().Programs().MarkShaderObjectForDeletion(index, m_currentProgram);
} }
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) { void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
return m_programState.ReleaseShaderNameIfOrphaned(index); return Group().Programs().ReleaseShaderNameIfOrphaned(index, m_currentProgram);
} }
Bool GLContext::ValidateProgramName(const Uint index) const { Bool GLContext::ValidateProgramName(const Uint index) const {
return m_programState.ValidateProgramObject(index); return m_shareGroup->Programs().ValidateProgramObject(index);
} }
Bool GLContext::ValidateShaderName(const Uint index) const { Bool GLContext::ValidateShaderName(const Uint index) const {
return m_programState.ValidateShaderObject(index); return m_shareGroup->Programs().ValidateShaderObject(index);
} }
const SharedPtr<ProgramObject>& GLContext::GetProgramObject(const Uint index) { const SharedPtr<ProgramObject>& GLContext::GetProgramObject(const Uint index) {
return m_programState.GetProgramObject(index); return Group().Programs().GetProgramObject(index);
} }
const SharedPtr<ShaderObject>& GLContext::GetShaderObject(const Uint index) { const SharedPtr<ShaderObject>& GLContext::GetShaderObject(const Uint index) {
return m_programState.GetShaderObject(index); return Group().Programs().GetShaderObject(index);
} }
void GLContext::JoinAllPendingShaderWork() { void GLContext::JoinAllPendingShaderWork() {
m_programState.JoinAllPendingWork(); Group().Programs().JoinAllPendingWork(m_currentProgram);
} }
void GLContext::UseProgram(Uint program) { void GLContext::UseProgram(Uint program) {
return m_programState.UseProgram(program); const SharedPtr<ProgramObject> previous = m_currentProgram;
if (program == 0) m_currentProgram.reset();
auto& programs = Group().Programs();
if (programs.HasProgramSlot(program)) {
m_currentProgram = programs.GetProgramObject(program);
}
// A deletion flagged while the program was current takes effect the moment it
// stops being current.
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
const Uint previousName = previous->GetExternalIndex();
if (programs.HasProgramSlot(previousName) && programs.GetProgramObject(previousName) == previous) {
programs.MarkProgramObjectForDeletion(previousName, m_currentProgram);
}
}
} }
const SharedPtr<ProgramObject>& GLContext::GetCurrentProgram() { const SharedPtr<ProgramObject>& GLContext::GetCurrentProgram() {
return m_programState.GetCurrentProgram(); return m_currentProgram;
} }
// Copies every default-block uniform value `source` holds into the same-named uniform of // Copies every default-block uniform value `source` holds into the same-named uniform of
@@ -608,7 +672,7 @@ namespace MobileGL::MG_State {
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() { const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
static const SharedPtr<ProgramObject> nullProgram = nullptr; static const SharedPtr<ProgramObject> nullProgram = nullptr;
const auto& currentProgram = m_programState.GetCurrentProgram(); const auto& currentProgram = GetCurrentProgram();
if (currentProgram) { if (currentProgram) {
// P1 join site J1, plain glUseProgram half. The backends read a program's // P1 join site J1, plain glUseProgram half. The backends read a program's
// lifetimeId / backendStateVersion / UBO content version to decide whether // lifetimeId / backendStateVersion / UBO content version to decide whether
@@ -762,7 +826,7 @@ namespace MobileGL::MG_State {
const SharedPtr<ProgramObject>& GLContext::GetProgramForDispatch() { const SharedPtr<ProgramObject>& GLContext::GetProgramForDispatch() {
static const SharedPtr<ProgramObject> nullProgram = nullptr; static const SharedPtr<ProgramObject> nullProgram = nullptr;
const auto& currentProgram = m_programState.GetCurrentProgram(); const auto& currentProgram = GetCurrentProgram();
if (currentProgram) { if (currentProgram) {
// Same join contract as GetProgramForDraw's glUseProgram half - see the note // Same join contract as GetProgramForDraw's glUseProgram half - see the note
// there. A dispatch reads the same non-artifact versions a draw does. // there. A dispatch reads the same non-artifact versions a draw does.
@@ -784,7 +848,7 @@ namespace MobileGL::MG_State {
} }
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() { const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
const auto& currentProgram = m_programState.GetCurrentProgram(); const auto& currentProgram = GetCurrentProgram();
if (currentProgram) return currentProgram; if (currentProgram) return currentProgram;
static const SharedPtr<ProgramObject> nullProgram = nullptr; static const SharedPtr<ProgramObject> nullProgram = nullptr;
if (m_boundProgramPipeline == 0) return nullProgram; if (m_boundProgramPipeline == 0) return nullProgram;
@@ -1220,21 +1284,21 @@ namespace MobileGL::MG_State {
// Sampler // Sampler
void GLContext::GenSamplerNames(Uint number, Vector<Uint>& samplers) { void GLContext::GenSamplerNames(Uint number, Vector<Uint>& samplers) {
m_samplerState.GenerateNames(number, samplers); Group().Samplers().GenerateNames(number, samplers);
} }
const SharedPtr<SamplerObject>& GLContext::GetSamplerObject(Uint index) { const SharedPtr<SamplerObject>& GLContext::GetSamplerObject(Uint index) {
return m_samplerState.GetSamplerObject(index); return Group().Samplers().GetSamplerObject(index);
} }
const SharedPtr<SamplerObject>& GLContext::CreateSamplerObject(Uint index) { const SharedPtr<SamplerObject>& GLContext::CreateSamplerObject(Uint index) {
return m_samplerState.CreateSamplerObject(index); return Group().Samplers().CreateSamplerObject(index);
} }
void GLContext::MarkSamplerObjectForDeletion(Uint index) { void GLContext::MarkSamplerObjectForDeletion(Uint index) {
// Unbind the sampler from all texture units // Unbind the sampler from all texture units
if (ValidateSamplerObject(index)) { if (ValidateSamplerObject(index)) {
auto sampler = m_samplerState.GetSamplerObject(index); auto sampler = Group().Samplers().GetSamplerObject(index);
for (Int unit = 0; unit < TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) { for (Int unit = 0; unit < TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) {
auto& textureUnit = m_textureState.GetUnitObject(unit); auto& textureUnit = m_textureState.GetUnitObject(unit);
if (textureUnit.GetSamplerObject() == sampler) { if (textureUnit.GetSamplerObject() == sampler) {
@@ -1242,24 +1306,24 @@ namespace MobileGL::MG_State {
} }
} }
} }
m_samplerState.MarkSamplerObjectForDeletion(index); Group().Samplers().MarkSamplerObjectForDeletion(index);
} }
Bool GLContext::ValidateSamplerName(Uint index) const { Bool GLContext::ValidateSamplerName(Uint index) const {
return m_samplerState.ValidateName(index); return m_shareGroup->Samplers().ValidateName(index);
} }
Bool GLContext::ValidateSamplerObject(Uint index) const { Bool GLContext::ValidateSamplerObject(Uint index) const {
return m_samplerState.ValidateSamplerObject(index); return m_shareGroup->Samplers().ValidateSamplerObject(index);
} }
// Renderbuffer // Renderbuffer
void GLContext::GenRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) { void GLContext::GenRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) {
m_renderbufferState.GenerateNames(number, renderbuffers); Group().GenerateRenderbufferNames(number, renderbuffers);
} }
const SharedPtr<RenderbufferObject>& GLContext::GetRenderbufferObject(Uint index) { const SharedPtr<RenderbufferObject>& GLContext::GetRenderbufferObject(Uint index) {
return m_renderbufferState.GetRenderbufferObject(index); return Group().GetRenderbufferObject(index);
} }
BindingSlot<RenderbufferObject>& GLContext::GetRenderbufferBindingSlot(RenderbufferTarget target) { BindingSlot<RenderbufferObject>& GLContext::GetRenderbufferBindingSlot(RenderbufferTarget target) {
@@ -1267,19 +1331,21 @@ namespace MobileGL::MG_State {
} }
const SharedPtr<RenderbufferObject>& GLContext::CreateRenderbufferObject(Uint index) { const SharedPtr<RenderbufferObject>& GLContext::CreateRenderbufferObject(Uint index) {
return m_renderbufferState.CreateRenderbufferObject(index); return Group().CreateRenderbufferObject(index);
} }
void GLContext::MarkRenderbufferObjectForDeletion(Uint index) { void GLContext::MarkRenderbufferObjectForDeletion(Uint index) {
m_renderbufferState.MarkRenderbufferObjectForDeletion(index); const auto renderbufferObject = Group().GetRenderbufferObject(index);
m_renderbufferState.UnbindRenderbufferObject(renderbufferObject);
Group().MarkRenderbufferObjectForDeletion(index);
} }
Bool GLContext::ValidateRenderbufferName(Uint index) const { Bool GLContext::ValidateRenderbufferName(Uint index) const {
return m_renderbufferState.ValidateName(index); return m_shareGroup->ValidateRenderbufferName(index);
} }
Bool GLContext::ValidateRenderbufferObject(Uint index) const { Bool GLContext::ValidateRenderbufferObject(Uint index) const {
return m_renderbufferState.ValidateRenderbufferObject(index); return m_shareGroup->ValidateRenderbufferObject(index);
} }
Uint64 GLContext::AllocateTransformFeedbackLifetimeId() { Uint64 GLContext::AllocateTransformFeedbackLifetimeId() {
@@ -1288,12 +1354,23 @@ namespace MobileGL::MG_State {
return nextId.fetch_add(1, std::memory_order_relaxed); return nextId.fetch_add(1, std::memory_order_relaxed);
} }
GLContext::GLContext() { GLContext::GLContext() : GLContext(MakeShared<ShareGroupState>()) {}
GLContext::GLContext(SharedPtr<ShareGroupState> shareGroup)
: m_shareGroup(shareGroup ? Move(shareGroup) : MakeShared<ShareGroupState>()) {
// The default transform feedback object (name 0) exists from the start of the context // The default transform feedback object (name 0) exists from the start of the context
// (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it. // (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it.
// Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain // Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain
// const read instead of an operator[] insert on the draw path. // const read instead of an operator[] insert on the draw path.
m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId; m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId;
InstallDefaultFramebuffer(DefaultFramebuffer());
}
void GLContext::InstallDefaultFramebuffer(const SharedPtr<FramebufferObject>& framebuffer) {
if (!framebuffer) return;
m_framebufferState.RegisterFramebufferObject(0, framebuffer);
m_framebufferState.GetBindingSlot(FramebufferTarget::Draw).Bind(framebuffer);
m_framebufferState.GetBindingSlot(FramebufferTarget::Read).Bind(framebuffer);
} }
Bool GLContext::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const { Bool GLContext::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
@@ -1562,5 +1639,5 @@ namespace MobileGL::MG_State {
} // namespace GLState } // namespace GLState
// Leak-at-exit storage; see GlobalObjects.cpp. // Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>(); thread_local SharedPtr<GLState::GLContext> pGLContext;
} // namespace MobileGL::MG_State } // namespace MobileGL::MG_State
+47 -4
View File
@@ -16,6 +16,7 @@
#include "ProgramState/ProgramState.h" #include "ProgramState/ProgramState.h"
#include "ProgramState/ProgramPipelineObject.h" #include "ProgramState/ProgramPipelineObject.h"
#include "SamplerState/SamplerState.h" #include "SamplerState/SamplerState.h"
#include "ShareGroupState.h"
#include "TextureState/TextureState.h" #include "TextureState/TextureState.h"
#include "FramebufferState/FramebufferState.h" #include "FramebufferState/FramebufferState.h"
#include "VertexArrayState/VertexArrayState.h" #include "VertexArrayState/VertexArrayState.h"
@@ -76,7 +77,19 @@ namespace MobileGL {
class GLContext { class GLContext {
public: public:
// A context with a share group of its own. Created through EGL, a context
// either starts a new group or joins the one its share context belongs to;
// this constructor is the standalone shape the tests and the process default
// context use.
GLContext(); GLContext();
explicit GLContext(SharedPtr<ShareGroupState> shareGroup);
// The GL objects this context shares with every context that shares with it.
const SharedPtr<ShareGroupState>& GetShareGroup() const { return m_shareGroup; }
// Installs the process-wide default framebuffer (GL name 0) into this context:
// its table entry and the Draw/Read bindings the context starts with.
void InstallDefaultFramebuffer(const SharedPtr<FramebufferObject>& framebuffer);
// Error // Error
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info); void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
@@ -188,7 +201,7 @@ namespace MobileGL {
// P1 stage 6: the per-context index of adoptable compile nodes, for its // P1 stage 6: the per-context index of adoptable compile nodes, for its
// adoption counter. Diagnostics and tests only - no GL entry point reads it. // adoption counter. Diagnostics and tests only - no GL entry point reads it.
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() {
return m_programState.GetShaderCompileAdoptionMap(); return Group().Programs().GetShaderCompileAdoptionMap();
} }
void UseProgram(Uint program); void UseProgram(Uint program);
const SharedPtr<ProgramObject>& GetCurrentProgram(); const SharedPtr<ProgramObject>& GetCurrentProgram();
@@ -606,6 +619,10 @@ namespace MobileGL {
void InvalidateCompileEnv(); void InvalidateCompileEnv();
private: private:
ShareGroupState& Group() const { return *m_shareGroup; }
// The GL objects shared with every context that shares with this one.
SharedPtr<ShareGroupState> m_shareGroup;
// State Components // State Components
ErrorState m_errorState; ErrorState m_errorState;
BufferState m_bufferState; BufferState m_bufferState;
@@ -685,10 +702,11 @@ namespace MobileGL {
IndexGenerator<Uint> m_programPipelineNames; IndexGenerator<Uint> m_programPipelineNames;
Uint m_boundProgramPipeline = 0; Uint m_boundProgramPipeline = 0;
TextureState m_textureState; TextureState m_textureState;
ProgramState m_programState; // The program this context has in use. Program STATE (the objects) belongs to
// the share group; which of them is current does not.
SharedPtr<ProgramObject> m_currentProgram;
RenderState m_renderState; RenderState m_renderState;
FramebufferState m_framebufferState; FramebufferState m_framebufferState;
SamplerState m_samplerState;
RenderbufferState m_renderbufferState; RenderbufferState m_renderbufferState;
mutable SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> m_compileEnv; mutable SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> m_compileEnv;
@@ -698,7 +716,32 @@ namespace MobileGL {
}; };
} // namespace GLState } // namespace GLState
extern UniquePtr<GLState::GLContext>& pGLContext; // P14 S3 (docs/Disaggregated/design/11-state-ownership.md): the GL state the CALLING
// THREAD's GL entry points read and write. eglMakeCurrent points it at that EGL
// context's GLContext, and the EGL release edge puts it back to whatever it was, so a
// thread that never crossed an eglMakeCurrent keeps the process default MG_State::Init()
// gave it. NULL on a thread for which neither has happened, the same answer a process
// that never reached MG_State::Init() (the spawn server image) has always given.
extern thread_local SharedPtr<GLState::GLContext> pGLContext;
// The fallback pGLContext starts at and an EGL release edge restores to: the process's
// one context for every thread that never bound an EGL context to itself, created on
// demand and owned here (leak-at-exit, like every other process global).
const SharedPtr<GLState::GLContext>& ProcessDefaultGLContext();
// Drops the process default context and this thread's binding to it, and the default
// framebuffer with them. Called on the teardown edge (the last eglTerminate, before
// MobileGL::Destroy()) so the objects MG_State::Init() built do not outlive
// glslang::FinalizeProcess() through a thread that kept its fallback binding.
void ReleaseProcessDefaultGLContext();
// P14 S3. GL framebuffer 0, built by MG_Impl once the backend is up (it needs texture
// objects, which need the backend's resource plumbing) and handed back here so every
// GLContext a later eglCreateContext builds starts with the same default framebuffer
// bound. Until the backend grows per-context native tuples there is exactly one such
// object, which is what the backend's own default-framebuffer identity tests assume.
void SetDefaultFramebuffer(SharedPtr<GLState::FramebufferObject> framebuffer);
const SharedPtr<GLState::FramebufferObject>& DefaultFramebuffer();
// True when relaxed GL semantics apply. Strict core rules are enforced only when the // True when relaxed GL semantics apply. Strict core rules are enforced only when the
// current EGL context explicitly requested a core profile (core bit in // current EGL context explicitly requested a core profile (core bit in
@@ -46,6 +46,14 @@ namespace MobileGL::MG_State::GLState {
return framebufferObject; return framebufferObject;
} }
void FramebufferState::RegisterFramebufferObject(Uint index, SharedPtr<FramebufferObject> object) {
if (!object) return;
if (index == 0 && !m_indexGenerator.IsValid(0)) {
m_indexGenerator.Insert(0);
}
m_framebufferObjects[index] = Move(object);
}
BindingSlot<FramebufferObject>& FramebufferState::GetBindingSlot(FramebufferTarget target) { BindingSlot<FramebufferObject>& FramebufferState::GetBindingSlot(FramebufferTarget target) {
for (auto& bindingSlot : m_bindingSlots) { for (auto& bindingSlot : m_bindingSlots) {
if (bindingSlot.GetTarget() == target) { if (bindingSlot.GetTarget() == target) {
@@ -20,6 +20,9 @@ namespace MobileGL::MG_State::GLState {
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index); const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
void GenerateNames(Uint number, Vector<Uint>& framebuffers); void GenerateNames(Uint number, Vector<Uint>& framebuffers);
const SharedPtr<FramebufferObject>& CreateFramebufferObject(Uint index); const SharedPtr<FramebufferObject>& CreateFramebufferObject(Uint index);
// Puts an already-built object at `index` without the "name 0 is created once" rule,
// so a context can adopt the process-wide default framebuffer.
void RegisterFramebufferObject(Uint index, SharedPtr<FramebufferObject> object);
BindingSlot<FramebufferObject>& GetBindingSlot(FramebufferTarget target); BindingSlot<FramebufferObject>& GetBindingSlot(FramebufferTarget target);
void MarkFramebufferObjectForDeletion(Uint index); void MarkFramebufferObjectForDeletion(Uint index);
Bool ValidateName(Uint index) const; Bool ValidateName(Uint index) const;
@@ -25,19 +25,24 @@ namespace MobileGL::MG_State::GLState {
return m_programObjects[id]; return m_programObjects[id];
} }
void ProgramState::MarkProgramObjectForDeletion(const Uint program) { Bool ProgramState::HasProgramSlot(const Uint program) const {
return CheckIndexAvail(program, m_programObjects);
}
void ProgramState::MarkProgramObjectForDeletion(const Uint program,
const SharedPtr<ProgramObject>& currentProgram) {
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
auto& programObject = m_programObjects[program]; auto& programObject = m_programObjects[program];
if (programObject != nullptr) { if (programObject != nullptr) {
programObject->MarkAsDeleted(); programObject->MarkAsDeleted();
// A program in use is only FLAGGED: its name (and every program query) stays // A program in use is only FLAGGED: its name (and every program query) stays
// valid until it stops being current, at which point UseProgram finishes the job. // valid until it stops being current, at which point UseProgram finishes the job.
if (programObject == m_currentProgram) return; if (programObject == currentProgram) return;
DestroyProgramSlot(program); DestroyProgramSlot(program, currentProgram);
} }
} }
void ProgramState::DestroyProgramSlot(const Uint program) { void ProgramState::DestroyProgramSlot(const Uint program, const SharedPtr<ProgramObject>& currentProgram) {
auto& programObject = m_programObjects[program]; auto& programObject = m_programObjects[program];
// P1 join site J4/J5 (glDeleteProgram, and the deferred destroy UseProgram performs // P1 join site J4/J5 (glDeleteProgram, and the deferred destroy UseProgram performs
// when a deletion-flagged program stops being current). The program's name is about // when a deletion-flagged program stops being current). The program's name is about
@@ -55,7 +60,7 @@ namespace MobileGL::MG_State::GLState {
for (const auto& shader : attachedShaders) { for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex(); const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) { if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName); ReleaseShaderNameIfOrphaned(shaderName, currentProgram);
} }
} }
} }
@@ -64,25 +69,6 @@ namespace MobileGL::MG_State::GLState {
return CheckIndexAvail(program, m_programObjects) && m_programObjects[program] != nullptr; return CheckIndexAvail(program, m_programObjects) && m_programObjects[program] != nullptr;
} }
void ProgramState::UseProgram(Uint program) {
const SharedPtr<ProgramObject> previous = m_currentProgram;
if (program == 0) m_currentProgram.reset();
if (CheckIndexAvail(program, m_programObjects)) {
m_currentProgram = m_programObjects[program];
}
// A deletion flagged while the program was current takes effect the moment it
// stops being current.
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
const Uint previousName = previous->GetExternalIndex();
if (CheckIndexAvail(previousName, m_programObjects) && m_programObjects[previousName] == previous) {
DestroyProgramSlot(previousName);
}
}
}
Uint ProgramState::CreateShader(ShaderStage stage) { Uint ProgramState::CreateShader(ShaderStage stage) {
Uint shaderId = 0; Uint shaderId = 0;
m_programShaderNameGenerator.Generate(1, &shaderId); m_programShaderNameGenerator.Generate(1, &shaderId);
@@ -100,7 +86,7 @@ namespace MobileGL::MG_State::GLState {
return m_shaderObjects[shader]; return m_shaderObjects[shader];
} }
void ProgramState::JoinAllPendingWork() { void ProgramState::JoinAllPendingWork(const SharedPtr<ProgramObject>& currentProgram) {
// Programs first: a link joins the compiles it depends on, so the shader pass that // Programs first: a link joins the compiles it depends on, so the shader pass that
// follows finds most of them already settled. The reverse order would be correct but // follows finds most of them already settled. The reverse order would be correct but
// would wait on each compile twice - once here, once inside the link's own prologue. // would wait on each compile twice - once here, once inside the link's own prologue.
@@ -126,10 +112,10 @@ namespace MobileGL::MG_State::GLState {
// The currently-used program is reachable through m_programObjects unless // The currently-used program is reachable through m_programObjects unless
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds // glDeleteProgram already freed its slot while it stayed current. Nothing else holds
// a GL-visible name for it, but a draw would still join it, so settle it here too. // a GL-visible name for it, but a draw would still join it, so settle it here too.
if (m_currentProgram) m_currentProgram->JoinLinkAndSpirv(); if (currentProgram) currentProgram->JoinLinkAndSpirv();
} }
void ProgramState::MarkShaderObjectForDeletion(Uint shader) { void ProgramState::MarkShaderObjectForDeletion(Uint shader, const SharedPtr<ProgramObject>& currentProgram) {
if (!CheckIndexAvail(shader, m_shaderObjects)) return; if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader]; auto& shaderObject = m_shaderObjects[shader];
if (shaderObject != nullptr) { if (shaderObject != nullptr) {
@@ -138,11 +124,12 @@ namespace MobileGL::MG_State::GLState {
// from every program. The GL CTS compiles shaders through exactly this // from every program. The GL CTS compiles shaders through exactly this
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching). // create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
shaderObject->MarkAsDeleted(); shaderObject->MarkAsDeleted();
ReleaseShaderNameIfOrphaned(shader); ReleaseShaderNameIfOrphaned(shader, currentProgram);
} }
} }
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const { Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject,
const SharedPtr<ProgramObject>& currentProgram) const {
for (const auto& programObject : m_programObjects) { for (const auto& programObject : m_programObjects) {
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) { if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
return true; return true;
@@ -150,14 +137,14 @@ namespace MobileGL::MG_State::GLState {
} }
// A program deleted while current vacates its table slot but stays alive as the // A program deleted while current vacates its table slot but stays alive as the
// current program; its attachments still count. // current program; its attachments still count.
return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject); return currentProgram != nullptr && currentProgram->ShaderIsAttachedGLVisible(shaderObject);
} }
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) { void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader, const SharedPtr<ProgramObject>& currentProgram) {
if (!CheckIndexAvail(shader, m_shaderObjects)) return; if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader]; auto& shaderObject = m_shaderObjects[shader];
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return; if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
if (ShaderHasGLVisibleAttachment(shaderObject)) return; if (ShaderHasGLVisibleAttachment(shaderObject, currentProgram)) return;
// The name is about to go, so nothing can observe this shader's compile through THIS // The name is about to go, so nothing can observe this shader's compile through THIS
// object any more and a job still in flight for it is pure waste - unless another // object any more and a job still in flight for it is pure waste - unless another
// shader object adopted the same node (stage 6) or a pending link pinned it, which is // shader object adopted the same node (stage 6) or a pending link pinned it, which is
@@ -14,28 +14,31 @@
#include "ShaderPreprocessCache.h" #include "ShaderPreprocessCache.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
// The program and shader objects of one share group, and the name space they share. The
// program a context has in use is context state and lives on GLContext - the operations
// that need it (a delete of the current program, the deferred destroy UseProgram
// performs, the orphan test) take it as a parameter.
class ProgramState { class ProgramState {
public: public:
// This function WILL actually create the program object. // This function WILL actually create the program object.
// To retrieve created program object, use GetProgramObject() // To retrieve created program object, use GetProgramObject()
Uint CreateProgram(); Uint CreateProgram();
const SharedPtr<ProgramObject>& GetProgramObject(Uint id); const SharedPtr<ProgramObject>& GetProgramObject(Uint id);
void MarkProgramObjectForDeletion(Uint program); void MarkProgramObjectForDeletion(Uint program, const SharedPtr<ProgramObject>& currentProgram);
Bool ValidateProgramObject(Uint program) const; Bool ValidateProgramObject(Uint program) const;
// Whether `program` names a slot in the table at all, i.e. in range - not whether that
void UseProgram(Uint program); // slot currently holds an object.
Bool HasProgramSlot(Uint program) const;
Uint CreateShader(ShaderStage stage); Uint CreateShader(ShaderStage stage);
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader); const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
void MarkShaderObjectForDeletion(Uint shader); void MarkShaderObjectForDeletion(Uint shader, const SharedPtr<ProgramObject>& currentProgram);
// Frees a deletion-flagged shader's name once no program holds a GL-visible // Frees a deletion-flagged shader's name once no program holds a GL-visible
// attachment to it (the deferred half of glDeleteShader-while-attached). // attachment to it (the deferred half of glDeleteShader-while-attached).
void ReleaseShaderNameIfOrphaned(Uint shader); void ReleaseShaderNameIfOrphaned(Uint shader, const SharedPtr<ProgramObject>& currentProgram);
Bool ValidateShaderObject(Uint shader) const; Bool ValidateShaderObject(Uint shader) const;
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; } // Joins every outstanding compile and link this share group still owns, publishing each
// Joins every outstanding compile and link this context still owns, publishing each
// one's artifacts through the ordinary gates. The single caller is // one's artifacts through the ordinary gates. The single caller is
// glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero // glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero
// count to leave nothing in flight, so that every subsequent // count to leave nothing in flight, so that every subsequent
@@ -46,7 +49,7 @@ namespace MobileGL::MG_State::GLState {
// compilation straight back. Nodes belonging to objects this context has already // compilation straight back. Nodes belonging to objects this context has already
// dropped are not joined - nothing can observe them, and waiting on them would make // dropped are not joined - nothing can observe them, and waiting on them would make
// a GL call's cost depend on garbage. // a GL call's cost depend on garbage.
void JoinAllPendingWork(); void JoinAllPendingWork(const SharedPtr<ProgramObject>& currentProgram);
// P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it // P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it
// directly - shader objects reach it through the pointer they are handed at // directly - shader objects reach it through the pointer they are handed at
@@ -59,10 +62,11 @@ namespace MobileGL::MG_State::GLState {
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { return *m_shaderCompileAdoptionMap; } ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { return *m_shaderCompileAdoptionMap; }
private: private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const; Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject,
const SharedPtr<ProgramObject>& currentProgram) const;
// Frees the name slot and releases orphaned attached shaders; the immediate half // Frees the name slot and releases orphaned attached shaders; the immediate half
// of glDeleteProgram (deferred while the program is current). // of glDeleteProgram (deferred while the program is current).
void DestroyProgramSlot(Uint program); void DestroyProgramSlot(Uint program, const SharedPtr<ProgramObject>& currentProgram);
template <typename T> template <typename T>
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) { static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
@@ -96,7 +100,5 @@ namespace MobileGL::MG_State::GLState {
Vector<SharedPtr<ProgramObject>> m_programObjects; Vector<SharedPtr<ProgramObject>> m_programObjects;
Vector<SharedPtr<ShaderObject>> m_shaderObjects; Vector<SharedPtr<ShaderObject>> m_shaderObjects;
SharedPtr<ProgramObject> m_currentProgram;
}; };
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
@@ -10,34 +10,12 @@
#include "MG_Util/Types.h" #include "MG_Util/Types.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
RenderbufferState::RenderbufferState() : m_indexGenerator(1024, 1) { RenderbufferState::RenderbufferState() {
for (SizeT i = 0; i < m_bindingSlots.size(); ++i) { for (SizeT i = 0; i < m_bindingSlots.size(); ++i) {
m_bindingSlots[i] = BindingSlot<RenderbufferObject>(static_cast<RenderbufferTarget>(i)); m_bindingSlots[i] = BindingSlot<RenderbufferObject>(static_cast<RenderbufferTarget>(i));
} }
} }
const SharedPtr<RenderbufferObject>& RenderbufferState::GetRenderbufferObject(Uint index) {
auto it = m_renderbufferObjects.find(index);
if (it != m_renderbufferObjects.end()) {
return it->second;
}
static SharedPtr<RenderbufferObject> nullRenderbufferObject = nullptr;
return nullRenderbufferObject;
}
void RenderbufferState::GenerateNames(Uint number, Vector<Uint>& renderbuffers) {
renderbuffers.resize(number);
m_indexGenerator.Generate(number, renderbuffers.data());
}
const SharedPtr<RenderbufferObject>& RenderbufferState::CreateRenderbufferObject(Uint index) {
auto& bufferObject = m_renderbufferObjects[index];
if (!bufferObject) {
bufferObject = MakeShared<RenderbufferObject>(index);
}
return bufferObject;
}
BindingSlot<RenderbufferObject>& RenderbufferState::GetBindingSlot(RenderbufferTarget target) { BindingSlot<RenderbufferObject>& RenderbufferState::GetBindingSlot(RenderbufferTarget target) {
for (auto& bindingSlot : m_bindingSlots) { for (auto& bindingSlot : m_bindingSlots) {
if (bindingSlot.GetTarget() == target) { if (bindingSlot.GetTarget() == target) {
@@ -48,26 +26,12 @@ namespace MobileGL::MG_State::GLState {
return m_bindingSlots[0]; return m_bindingSlots[0];
} }
void RenderbufferState::MarkRenderbufferObjectForDeletion(Uint index) { void RenderbufferState::UnbindRenderbufferObject(const SharedPtr<RenderbufferObject>& renderbufferObject) {
if (m_indexGenerator.IsValid(index)) { if (!renderbufferObject) return;
auto it = m_renderbufferObjects.find(index); for (auto& bindingSlot : m_bindingSlots) {
if (it != m_renderbufferObjects.end()) { if (bindingSlot.GetBoundObject() == renderbufferObject) {
for (auto& bindingSlot : m_bindingSlots) { bindingSlot.Bind(nullptr);
if (bindingSlot.GetBoundObject() == it->second) {
bindingSlot.Bind(nullptr);
}
}
m_renderbufferObjects.erase(index);
} }
m_indexGenerator.Delete(index);
} }
} }
Bool RenderbufferState::ValidateName(Uint index) const {
return m_indexGenerator.IsValid(index);
}
Bool RenderbufferState::ValidateRenderbufferObject(Uint index) const {
return m_renderbufferObjects.find(index) != m_renderbufferObjects.end();
}
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
@@ -12,21 +12,16 @@
#include "RenderbufferObject.h" #include "RenderbufferObject.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
// The per-context half of the renderbuffer state: which renderbuffer is bound. The
// objects and their names belong to the share group (ShareGroupState).
class RenderbufferState { class RenderbufferState {
public: public:
RenderbufferState(); RenderbufferState();
const SharedPtr<RenderbufferObject>& GetRenderbufferObject(Uint index);
void GenerateNames(Uint number, Vector<Uint>& renderbuffers);
const SharedPtr<RenderbufferObject>& CreateRenderbufferObject(Uint index);
BindingSlot<RenderbufferObject>& GetBindingSlot(RenderbufferTarget target); BindingSlot<RenderbufferObject>& GetBindingSlot(RenderbufferTarget target);
void MarkRenderbufferObjectForDeletion(Uint index); void UnbindRenderbufferObject(const SharedPtr<RenderbufferObject>& renderbufferObject);
Bool ValidateName(Uint index) const;
Bool ValidateRenderbufferObject(Uint index) const;
private: private:
UnorderedMap<Uint, SharedPtr<RenderbufferObject>> m_renderbufferObjects;
IndexGenerator<Uint> m_indexGenerator;
Array<BindingSlot<RenderbufferObject>, static_cast<SizeT>(RenderbufferTarget::RenderbufferTargetCount)> Array<BindingSlot<RenderbufferObject>, static_cast<SizeT>(RenderbufferTarget::RenderbufferTargetCount)>
m_bindingSlots; m_bindingSlots;
}; };
@@ -0,0 +1,188 @@
// MobileGL - MobileGL/MG_State/GLState/ShareGroupState.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "ShareGroupState.h"
#include "TextureState/TextureObject1D.h"
#include "TextureState/TextureObject2D.h"
#include "TextureState/TextureObject3D.h"
#include "TextureState/TextureObject2DCube.h"
#include "TextureState/TextureObjectBuffer.h"
#include "TextureState/TextureObjectStubs.h"
#include "TextureState/TextureObjectView.h"
namespace MobileGL::MG_State::GLState {
SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return MakeShared<TextureObject1D>(index);
case TextureTarget::TextureCubeMap:
return MakeShared<TextureObject2DCube>(index);
case TextureTarget::Texture2D:
return MakeShared<TextureObject2D>(index);
case TextureTarget::Texture3D:
return MakeShared<TextureObject3D>(index);
case TextureTarget::TextureBuffer:
return MakeShared<TextureObjectBuffer>(index);
// These texture types are still stubbed:
case TextureTarget::TextureRectangle:
return MakeShared<TextureObjectRectangle>(index);
case TextureTarget::Texture2DMultisample:
return MakeShared<TextureObject2DMultisample>(index);
case TextureTarget::Texture1DArray:
return MakeShared<TextureObject1DArray>(index);
case TextureTarget::Texture2DArray:
return MakeShared<TextureObject2DArray>(index);
case TextureTarget::TextureCubeMapArray:
return MakeShared<TextureObjectCubeMapArray>(index);
case TextureTarget::Texture2DMultisampleArray:
return MakeShared<TextureObject2DMultisampleArray>(index);
default:
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
return nullptr;
}
}
// Buffer
const SharedPtr<BufferObject>& ShareGroupState::GetBufferObject(Uint index) {
auto it = m_bufferObjects.find(index);
if (it != m_bufferObjects.end()) {
return it->second;
}
static SharedPtr<BufferObject> nullBufferObject = nullptr;
return nullBufferObject;
}
void ShareGroupState::GenerateBufferNames(Uint number, Vector<Uint>& buffers) {
buffers.resize(number);
m_bufferNames.Generate(number, buffers.data());
}
const SharedPtr<BufferObject>& ShareGroupState::CreateBufferObject(Uint index) {
auto& bufferObj = m_bufferObjects[index];
if (!bufferObj) {
bufferObj = MakeShared<BufferObject>(index);
}
return bufferObj;
}
void ShareGroupState::MarkBufferObjectForDeletion(Uint index) {
if (m_bufferNames.IsValid(index)) {
m_bufferObjects.erase(index);
m_bufferNames.Delete(index);
}
}
Bool ShareGroupState::ValidateBufferName(Uint index) const {
return m_bufferNames.IsValid(index);
}
Bool ShareGroupState::ValidateBufferObject(Uint index) const {
return m_bufferObjects.find(index) != m_bufferObjects.end();
}
// Texture
void ShareGroupState::GenerateTextureNames(Uint number, Vector<Uint>& textures) {
textures.resize(number);
m_textureNames.Generate(number, textures.data());
}
const SharedPtr<ITextureObject>& ShareGroupState::GetTextureObject(Uint index) {
auto it = m_textureObjects.find(index);
if (it != m_textureObjects.end()) {
return it->second;
}
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
return nullTextureObject;
}
const SharedPtr<ITextureObject>& ShareGroupState::CreateTextureObject(Uint index, TextureTarget target) {
auto& textureObject = m_textureObjects[index];
textureObject = MakeTextureObjectForTarget(index, target);
if (!textureObject) {
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
return nullTextureObject;
}
return textureObject;
}
const SharedPtr<ITextureObject>& ShareGroupState::CreateTextureViewObject(
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
Uint numLevels, Uint minLayer, Uint numLayers) {
MOBILEGL_ASSERT(storageOwner != nullptr, "CreateTextureViewObject: storage owner is null");
auto& textureObject = m_textureObjects[index];
textureObject = MakeShared<TextureObjectView>(index, target, storageOwner, minLevel, numLevels, minLayer,
numLayers);
return textureObject;
}
void ShareGroupState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
if (m_textureNames.IsValid(index)) {
auto it = m_textureObjects.find(index);
if (it != m_textureObjects.end()) {
m_textureObjects.erase(it);
m_textureNames.Delete(index);
} else if (!keepUnboundReservation) {
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
// reserved it and no bind ever instantiated an object (so a later bind of it must
// fail), and the reservation has to return to the free list.
m_textureNames.Delete(index);
}
// Relaxed semantics: legacy apps may delete a generated name before its first
// bind, then bind and populate that same name. Keep such a reservation alive there;
// a real texture object reaching deletion still releases its index above.
}
}
Bool ShareGroupState::ValidateTextureName(Uint index) const {
return m_textureNames.IsValid(index);
}
Bool ShareGroupState::ValidateTextureObject(Uint index) const {
return m_textureObjects.find(index) != m_textureObjects.end();
}
// Renderbuffer
const SharedPtr<RenderbufferObject>& ShareGroupState::GetRenderbufferObject(Uint index) {
auto it = m_renderbufferObjects.find(index);
if (it != m_renderbufferObjects.end()) {
return it->second;
}
static SharedPtr<RenderbufferObject> nullRenderbufferObject = nullptr;
return nullRenderbufferObject;
}
void ShareGroupState::GenerateRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) {
renderbuffers.resize(number);
m_renderbufferNames.Generate(number, renderbuffers.data());
}
const SharedPtr<RenderbufferObject>& ShareGroupState::CreateRenderbufferObject(Uint index) {
auto& bufferObject = m_renderbufferObjects[index];
if (!bufferObject) {
bufferObject = MakeShared<RenderbufferObject>(index);
}
return bufferObject;
}
void ShareGroupState::MarkRenderbufferObjectForDeletion(Uint index) {
if (m_renderbufferNames.IsValid(index)) {
m_renderbufferObjects.erase(index);
m_renderbufferNames.Delete(index);
}
}
Bool ShareGroupState::ValidateRenderbufferName(Uint index) const {
return m_renderbufferNames.IsValid(index);
}
Bool ShareGroupState::ValidateRenderbufferObject(Uint index) const {
return m_renderbufferObjects.find(index) != m_renderbufferObjects.end();
}
} // namespace MobileGL::MG_State::GLState
@@ -0,0 +1,81 @@
// MobileGL - MobileGL/MG_State/GLState/ShareGroupState.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_Util/Miscellany/IndexGenerator.h>
#include "BufferState/BufferObject.h"
#include "ProgramState/ProgramState.h"
#include "RenderbufferState/RenderbufferObject.h"
#include "SamplerState/SamplerState.h"
#include "TextureState/TextureObject.h"
namespace MobileGL::MG_State::GLState {
// The GL object registries and name spaces a set of sharing contexts share, i.e. one EGL
// share group (GL 4.6 core 5.1). Buffers, textures, renderbuffers, samplers, programs and
// shaders live here; glGen* names are drawn from this object's generators.
//
// What is NOT here is what GL scopes to a single context - bindings, texture units, the
// current program, errors, and the container objects (VAO/FBO/query/pipeline/transform
// feedback) - so two contexts sharing a group see the same objects and their own bindings.
class ShareGroupState {
public:
ShareGroupState() = default;
explicit ShareGroupState(Uint64 token) : m_token(token) {}
// P14 S1's shareGroupToken: the identity the wire uses, 0 for a group no EGL context
// was made for.
Uint64 GetToken() const { return m_token; }
// Buffer
const SharedPtr<BufferObject>& GetBufferObject(Uint index);
void GenerateBufferNames(Uint number, Vector<Uint>& buffers);
const SharedPtr<BufferObject>& CreateBufferObject(Uint index);
void MarkBufferObjectForDeletion(Uint index);
Bool ValidateBufferName(Uint index) const;
Bool ValidateBufferObject(Uint index) const;
// Texture
void GenerateTextureNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
const SharedPtr<ITextureObject>& storageOwner,
Uint minLevel, Uint numLevels, Uint minLayer,
Uint numLayers);
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation);
Bool ValidateTextureName(Uint index) const;
Bool ValidateTextureObject(Uint index) const;
// Renderbuffer
const SharedPtr<RenderbufferObject>& GetRenderbufferObject(Uint index);
void GenerateRenderbufferNames(Uint number, Vector<Uint>& renderbuffers);
const SharedPtr<RenderbufferObject>& CreateRenderbufferObject(Uint index);
void MarkRenderbufferObjectForDeletion(Uint index);
Bool ValidateRenderbufferName(Uint index) const;
Bool ValidateRenderbufferObject(Uint index) const;
// Sampler objects carry no per-context state, so the whole table lives here.
SamplerState& Samplers() { return m_samplerState; }
// Program and shader objects, one name space for both (GL 3.3 core 2.11).
ProgramState& Programs() { return m_programState; }
const ProgramState& Programs() const { return m_programState; }
private:
Uint64 m_token = 0;
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
IndexGenerator<Uint> m_bufferNames{1024, 1};
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
IndexGenerator<Uint> m_textureNames{1024, 1};
UnorderedMap<Uint, SharedPtr<RenderbufferObject>> m_renderbufferObjects;
IndexGenerator<Uint> m_renderbufferNames{1024, 1};
SamplerState m_samplerState;
ProgramState m_programState;
};
} // namespace MobileGL::MG_State::GLState
@@ -19,6 +19,7 @@
#include "TextureObjectBuffer.h" #include "TextureObjectBuffer.h"
#include "TextureObjectStubs.h" #include "TextureObjectStubs.h"
#include "TextureObjectView.h" #include "TextureObjectView.h"
#include "MG_State/GLState/ShareGroupState.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
static std::atomic<Uint64> s_nextTextureStateContextId = 1; static std::atomic<Uint64> s_nextTextureStateContextId = 1;
@@ -27,42 +28,10 @@ namespace MobileGL::MG_State::GLState {
return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed); return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed);
} }
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) { TextureState::TextureState() : m_contextId(AllocateContextId()) {
switch (target) {
case TextureTarget::Texture1D:
return MakeShared<TextureObject1D>(index);
case TextureTarget::TextureCubeMap:
return MakeShared<TextureObject2DCube>(index);
case TextureTarget::Texture2D:
return MakeShared<TextureObject2D>(index);
case TextureTarget::Texture3D:
return MakeShared<TextureObject3D>(index);
case TextureTarget::TextureBuffer:
return MakeShared<TextureObjectBuffer>(index);
// These texture types are still stubbed:
case TextureTarget::TextureRectangle:
return MakeShared<TextureObjectRectangle>(index);
case TextureTarget::Texture2DMultisample:
return MakeShared<TextureObject2DMultisample>(index);
case TextureTarget::Texture1DArray:
return MakeShared<TextureObject1DArray>(index);
case TextureTarget::Texture2DArray:
return MakeShared<TextureObject2DArray>(index);
case TextureTarget::TextureCubeMapArray:
return MakeShared<TextureObjectCubeMapArray>(index);
case TextureTarget::Texture2DMultisampleArray:
return MakeShared<TextureObject2DMultisampleArray>(index);
default:
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
return nullptr;
}
}
TextureState::TextureState() : m_contextId(AllocateContextId()), m_indexGenerator(1024, 1) {
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context, // GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
// shared across all texture units, and it is the initial binding of every unit/target // shared across all texture units, and it is the initial binding of every unit/target
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture, // slot. It is created outside the share group's table so name-based paths (glIsTexture,
// GenTextures/DeleteTextures, by-name DSA lookups) never see it. // GenTextures/DeleteTextures, by-name DSA lookups) never see it.
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) { for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i)); m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
@@ -75,83 +44,35 @@ namespace MobileGL::MG_State::GLState {
} }
} }
const SharedPtr<ITextureObject>& TextureState::GetTextureObject(Uint index) {
auto it = m_textureObjects.find(index);
if (it != m_textureObjects.end()) {
return it->second;
}
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
return nullTextureObject;
}
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const { const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount, MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
"GetDefaultTextureObject: invalid texture target %d", (int)target); "GetDefaultTextureObject: invalid texture target %d", (int)target);
return m_defaultTextureObjects[(int)target]; return m_defaultTextureObjects[(int)target];
} }
void TextureState::GenerateNames(Uint number, Vector<Uint>& textures) { void TextureState::UnbindTextureFromUnits(const SharedPtr<ITextureObject>& textureObject) {
textures.resize(number); if (!textureObject) return;
m_indexGenerator.Generate(number, textures.data()); // Units past the touched high-water mark can never reference a texture.
} for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
const SharedPtr<ITextureObject>& TextureState::CreateTextureObject(Uint index, TextureTarget target) { for (auto& bindingSlot : bindingSlots) {
auto& textureObject = m_textureObjects[index]; if (bindingSlot.GetBoundObject() == textureObject) {
textureObject = MakeTextureObjectForTarget(index, target); // GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
if (!textureObject) { // target's default texture object, on every unit it was bound to.
static SharedPtr<ITextureObject> nullTextureObject = nullptr; bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
return nullTextureObject;
}
return textureObject;
}
const SharedPtr<ITextureObject>& TextureState::CreateTextureViewObject(
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
Uint numLevels, Uint minLayer, Uint numLayers) {
MOBILEGL_ASSERT(storageOwner != nullptr, "CreateTextureViewObject: storage owner is null");
auto& textureObject = m_textureObjects[index];
textureObject = MakeShared<TextureObjectView>(index, target, storageOwner, minLevel, numLevels, minLayer,
numLayers);
return textureObject;
}
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
if (m_indexGenerator.IsValid(index)) {
auto it = m_textureObjects.find(index);
if (it != m_textureObjects.end()) {
// Units past the touched high-water mark can never reference a texture.
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
for (auto& bindingSlot : bindingSlots) {
if (bindingSlot.GetBoundObject() == it->second) {
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
// target's default texture object, on every unit it was bound to.
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
}
}
} }
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
auto& imageBinding = m_imageTextureBindings[unit];
if (imageBinding.Texture == it->second) {
imageBinding.Bind(nullptr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
}
}
// Deleting a texture unbinds it from every unit above; treat that as a binding
// change so a cached sampled-texture set (which may hold this raw pointer) is
// re-resolved instead of dangling.
BumpTextureBindGeneration();
m_textureObjects.erase(index);
m_indexGenerator.Delete(index);
} else if (!keepUnboundReservation) {
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
// reserved it and no bind ever instantiated an object (so a later bind of it must
// fail), and the reservation has to return to the free list.
m_indexGenerator.Delete(index);
} }
// Relaxed semantics: legacy apps may delete a generated name before its first
// bind, then bind and populate that same name. Keep such a reservation alive there;
// a real texture object reaching deletion still releases its index above.
} }
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
auto& imageBinding = m_imageTextureBindings[unit];
if (imageBinding.Texture == textureObject) {
imageBinding.Bind(nullptr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
}
}
// Deleting a texture unbinds it from every unit above; treat that as a binding
// change so a cached sampled-texture set (which may hold this raw pointer) is
// re-resolved instead of dangling.
BumpTextureBindGeneration();
} }
TextureUnit& TextureState::GetUnitObject(Int unit) { TextureUnit& TextureState::GetUnitObject(Int unit) {
@@ -179,12 +100,4 @@ namespace MobileGL::MG_State::GLState {
void TextureState::SetActiveTextureUnit(Int unit) { void TextureState::SetActiveTextureUnit(Int unit) {
m_activeTextureUnit = unit; m_activeTextureUnit = unit;
} }
Bool TextureState::ValidateName(Uint index) const {
return m_indexGenerator.IsValid(index);
}
Bool TextureState::ValidateTextureObject(Uint index) const {
return m_textureObjects.find(index) != m_textureObjects.end();
}
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
@@ -15,6 +15,11 @@
#include "TextureUnit.h" #include "TextureUnit.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
// The one place a texture object's concrete type is chosen for a target. Shared because the
// share group's texture table and the context's per-target default texture objects both
// build through it.
SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target);
struct ImageTextureBinding { struct ImageTextureBinding {
SharedPtr<ITextureObject> Texture; SharedPtr<ITextureObject> Texture;
GLint Level = 0; GLint Level = 0;
@@ -45,6 +50,10 @@ namespace MobileGL::MG_State::GLState {
} }
}; };
// The per-context half of the texture state: the texture units (and their per-target
// bindings), the image unit bindings, the active unit, and the default texture objects
// (name 0), which GL scopes to the context rather than the share group. The named texture
// objects and their name space belong to the share group (ShareGroupState).
class TextureState { class TextureState {
public: public:
// Capacity of the combined texture-unit state arrays (indexed by glActiveTexture unit). // Capacity of the combined texture-unit state arrays (indexed by glActiveTexture unit).
@@ -56,30 +65,20 @@ namespace MobileGL::MG_State::GLState {
static constexpr int MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32; static constexpr int MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32;
TextureState(); TextureState();
void GenerateNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
// glTextureView (GL 4.6 core 8.18). `storageOwner` must already be a texture with
// immutable storage and must NOT itself be a view - the caller composes a view-of-a-view
// onto the root first, and passes the composed (root-relative) level/layer range here.
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
const SharedPtr<ITextureObject>& storageOwner,
Uint minLevel, Uint numLevels, Uint minLayer,
Uint numLayers);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture // The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
// zero names a real, per-target texture object shared by every texture unit; binding 0 // zero names a real, per-target texture object shared by every texture unit; binding 0
// binds it, and image/parameter calls on it must work like on any texture. It is not a // binds it, and image/parameter calls on it must work like on any texture. It is not a
// GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE // GenTextures name: it lives outside the share group's table (so glIsTexture(0) stays
// and by-name lookups keep failing for 0) and can never be deleted. // GL_FALSE and by-name lookups keep failing for 0) and can never be deleted.
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const; const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
TextureUnit& GetUnitObject(Int unit); TextureUnit& GetUnitObject(Int unit);
ImageTextureBinding& GetImageTextureBinding(Int unit); ImageTextureBinding& GetImageTextureBinding(Int unit);
const ImageTextureBinding& GetImageTextureBinding(Int unit) const; const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
Int GetActiveTextureUnit() const; Int GetActiveTextureUnit() const;
void SetActiveTextureUnit(Int unit); void SetActiveTextureUnit(Int unit);
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation); // Rebinds every unit and image unit that names `textureObject` back to the target's
Bool ValidateName(Uint index) const; // default texture object; this context's half of a texture delete (GL 4.6 core 8.1).
Bool ValidateTextureObject(Uint index) const; void UnbindTextureFromUnits(const SharedPtr<ITextureObject>& textureObject);
// High-water mark of texture units ever touched by a texture or sampler bind. // High-water mark of texture units ever touched by a texture or sampler bind.
// Units above it have provably-empty binding slots, so per-draw backend scans // Units above it have provably-empty binding slots, so per-draw backend scans
@@ -209,8 +208,6 @@ namespace MobileGL::MG_State::GLState {
Int m_activeTextureUnit = 0; Int m_activeTextureUnit = 0;
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits; Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings; Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
IndexGenerator<Uint> m_indexGenerator;
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
// One default texture object (external name 0) per target, created with the context and // One default texture object (external name 0) per target, created with the context and
// immortal for its lifetime; the initial binding of every unit/target slot. // immortal for its lifetime; the initial binding of every unit/target slot.
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects; Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
+1 -1
View File
@@ -105,7 +105,7 @@ namespace {
Uint32 m_blockKb = 64; Uint32 m_blockKb = 64;
Uint32 m_adoptTier = 2; Uint32 m_adoptTier = 2;
Bool m_pipeStats = false; Bool m_pipeStats = false;
UniquePtr<MG_State::GLState::GLContext> m_context; SharedPtr<MG_State::GLState::GLContext> m_context;
}; };
#endif #endif
+1 -1
View File
@@ -109,7 +109,7 @@ namespace {
#endif #endif
MG_State::pGLContext = Move(m_previous); MG_State::pGLContext = Move(m_previous);
} }
UniquePtr<GLContext> m_previous; SharedPtr<GLContext> m_previous;
}; };
SizeT Index(MGPipeInputField field) { return static_cast<SizeT>(field); } SizeT Index(MGPipeInputField field) { return static_cast<SizeT>(field); }
@@ -303,7 +303,7 @@ namespace {
FramebufferScope(const FramebufferScope&) = delete; FramebufferScope(const FramebufferScope&) = delete;
FramebufferScope& operator=(const FramebufferScope&) = delete; FramebufferScope& operator=(const FramebufferScope&) = delete;
UniquePtr<GLContext> m_previousContext; SharedPtr<GLContext> m_previousContext;
Uint64 m_previousPush = 0; Uint64 m_previousPush = 0;
}; };
+26 -4
View File
@@ -148,7 +148,10 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode. // 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
// kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's // kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's
// set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record. // set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record.
EXPECT_EQ(ClassCount<kScreen>(), 12u); // + P14 S1's bind_context (docs/Disaggregated/design/11-state-ownership.md), the in-band
// context binding - a kScreen row for applier_reset's reason: a context switch is a
// whole-session edge, not a context-family verb.
EXPECT_EQ(ClassCount<kScreen>(), 13u);
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u); EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u); EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 19u); EXPECT_EQ(ClassCount<kCtxState>(), 19u);
@@ -644,7 +647,12 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
// opcodes 82..83, one payload, no flag at all - no reply slot, no blob, no tail. // opcodes 82..83, one payload, no flag at all - no reply slot, no blob, no tail.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ReadPixelsToBuffer), 82); EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ReadPixelsToBuffer), 82);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetTextureImageToBuffer), 83); EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetTextureImageToBuffer), 83);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 84); // P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended the in-band context
// binding, by the same rule: opcode 84, and nothing before it moved. bind_context is a
// kScreen control record with the applier_reset shape - one POD, no blob, no tail, no reply
// and no MGPipeApply* entry point - so the row beside it is pinned the same way.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindContext), 84);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 85);
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint32>(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint32>(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint32>(kNone));
EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u); EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u);
@@ -653,6 +661,13 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(kNone));
// P14 S1's row, pinned like the two control records it sits beside: no blob, no tail, no
// reply, and kWaitNone - the record's placement in the ring is the whole of its ordering, so
// the client publishes and moves on rather than parking on an apply that reads no client
// memory (PipeCalls.def's wait-class column).
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindContext), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallClassFor(MGPWireOp::BindContext), kScreen);
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::BindContext), kWaitNone);
// And the P5b rows carry what their contract says: one blob (the block name) and nothing // And the P5b rows carry what their contract says: one blob (the block name) and nothing
// else, and the extended draw row keeps its two flags. // else, and the extended draw row keeps its two flags.
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
@@ -674,6 +689,10 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
// padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second // padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second
// struct to pin - the 16 bytes are pinned above with the handle family. // struct to pin - the 16 bytes are pinned above with the handle family.
EXPECT_EQ(sizeof(MGPApplierReset), 8u); EXPECT_EQ(sizeof(MGPApplierReset), 8u);
// P14 S1: bind_context's POD. ONE Uint64 - the same width the control frame's token is, so
// the two planes cannot disagree about how much of a token is real.
EXPECT_EQ(sizeof(MGPBindContext), 8u);
EXPECT_EQ(sizeof(MGPBindContext::ClientContextToken), 8u);
EXPECT_EQ(sizeof(MGPHandleOnly), 16u); EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
// rv's two (§5.3/§7.6): the residual-value POD - 2 + 15 Uint32s, 2 Uint8s and 2 pad bytes, // rv's two (§5.3/§7.6): the residual-value POD - 2 + 15 Uint32s, 2 Uint8s and 2 pad bytes,
// then the three Uint64s - and the AMENDED attribute carrier, which grew 24 -> 56 to carry // then the three Uint64s - and the AMENDED attribute carrier, which grew 24 -> 56 to carry
@@ -1036,8 +1055,11 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
// (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange // (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange
// and MGPVertexAttribWire are: the comparator has to see INTO an element whose members // and MGPVertexAttribWire are: the comparator has to see INTO an element whose members
// include an MGHostSpan and a pad word, or it would memcmp the padding: 82. P9 appended the // include an MGHostSpan and a pad word, or it would memcmp the padding: 82. P9 appended the
// pack-buffer readbacks' one payload, MGPReadbackToBuffer (CONTRACT-P9.md §1): 83. // pack-buffer readbacks' one payload, MGPReadbackToBuffer (CONTRACT-P9.md §1): 83. P14 S1
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 83u); // appended bind_context's MGPBindContext (docs/Disaggregated/design/11-state-ownership.md):
// 84. A payload without a list here is one the comparator cannot see, so the row is not
// optional bookkeeping - gen_pipe.py refuses the build without it.
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 84u);
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value); static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value); static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value); static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
+1 -1
View File
@@ -84,7 +84,7 @@ namespace {
MGPipeSetPoisonOmission(nullptr, nullptr); MGPipeSetPoisonOmission(nullptr, nullptr);
MG_State::pGLContext = Move(m_previous); MG_State::pGLContext = Move(m_previous);
} }
UniquePtr<GLContext> m_previous; SharedPtr<GLContext> m_previous;
}; };
#if MOBILEGL_PIPE_POISON #if MOBILEGL_PIPE_POISON
@@ -568,7 +568,7 @@ namespace {
// A live frontend context for the setters to write and the getters to answer from, // A live frontend context for the setters to write and the getters to answer from,
// restored on the way out so the case stays independent (SanityTest's idiom). // restored on the way out so the case stays independent (SanityTest's idiom).
struct ContextGuard { struct ContextGuard {
UniquePtr<GLContext> Previous; SharedPtr<GLContext> Previous;
ContextGuard() : Previous(Move(MG_State::pGLContext)) { ContextGuard() : Previous(Move(MG_State::pGLContext)) {
MG_State::pGLContext = MakeUnique<GLContext>(); MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeApplierReset(); MGPipeApplierReset();
@@ -786,7 +786,7 @@ namespace {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
#else #else
struct ContextGuard { struct ContextGuard {
UniquePtr<GLContext> Previous; SharedPtr<GLContext> Previous;
ContextGuard() : Previous(Move(MG_State::pGLContext)) { ContextGuard() : Previous(Move(MG_State::pGLContext)) {
MG_State::pGLContext = MakeUnique<GLContext>(); MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeApplierReset(); MGPipeApplierReset();
@@ -950,7 +950,7 @@ namespace {
// A live frontend context and a clean applier, restored on the way out (SanityTest's // A live frontend context and a clean applier, restored on the way out (SanityTest's
// idiom, and the same guard cases 3 and 5 declare inline). // idiom, and the same guard cases 3 and 5 declare inline).
struct ApplierContextGuard { struct ApplierContextGuard {
UniquePtr<GLContext> Previous; SharedPtr<GLContext> Previous;
ApplierContextGuard() : Previous(Move(MG_State::pGLContext)) { ApplierContextGuard() : Previous(Move(MG_State::pGLContext)) {
MG_State::pGLContext = MakeUnique<GLContext>(); MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeApplierReset(); MGPipeApplierReset();
+1 -1
View File
@@ -1946,7 +1946,7 @@ namespace {
MGPipeResourceOps m_ops{}; MGPipeResourceOps m_ops{};
Uint64 m_previousPush = 0; Uint64 m_previousPush = 0;
UniquePtr<GLContext> m_previousContext; SharedPtr<GLContext> m_previousContext;
}; };
GLContext& Ctx() { return *MG_State::pGLContext; } GLContext& Ctx() { return *MG_State::pGLContext; }
+1 -1
View File
@@ -309,7 +309,7 @@ namespace {
TextureScope(const TextureScope&) = delete; TextureScope(const TextureScope&) = delete;
TextureScope& operator=(const TextureScope&) = delete; TextureScope& operator=(const TextureScope&) = delete;
UniquePtr<GLContext> m_previousContext; SharedPtr<GLContext> m_previousContext;
Uint64 m_previousPush = 0; Uint64 m_previousPush = 0;
}; };
+6 -6
View File
@@ -163,7 +163,7 @@ namespace {
} }
} }
UniquePtr<GLContext> m_previous; SharedPtr<GLContext> m_previous;
}; };
TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) { TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) {
@@ -238,7 +238,7 @@ namespace {
} }
TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) { TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) {
UniquePtr<GLContext> held = Move(MG_State::pGLContext); SharedPtr<GLContext> held = Move(MG_State::pGLContext);
MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context
MG_State::pGLContext = Move(held); MG_State::pGLContext = Move(held);
SUCCEED(); SUCCEED();
@@ -353,7 +353,7 @@ namespace {
Uint64 m_payloadBytes = 0; Uint64 m_payloadBytes = 0;
Uint64 m_binds = 0; Uint64 m_binds = 0;
Uint64 m_savedPush = 0; Uint64 m_savedPush = 0;
UniquePtr<GLContext> m_previous; SharedPtr<GLContext> m_previous;
}; };
TEST_F(TrackerWalk, EveryBitHasAName) { TEST_F(TrackerWalk, EveryBitHasAName) {
@@ -988,7 +988,7 @@ namespace {
return bits; return bits;
} }
UniquePtr<GLContext> m_previous; SharedPtr<GLContext> m_previous;
}; };
TEST_F(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) { TEST_F(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) {
@@ -1075,7 +1075,7 @@ namespace {
static const MGPipeCsoCache::Counters& Cso() { return MGPipeCsoCacheInstance().GetCounters(); } static const MGPipeCsoCache::Counters& Cso() { return MGPipeCsoCacheInstance().GetCounters(); }
Uint64 m_savedPush = 0; Uint64 m_savedPush = 0;
UniquePtr<GLContext> m_previous; SharedPtr<GLContext> m_previous;
}; };
TEST_F(TrackerShippedEmitter, CollidingVaoShuttersStillPublishTheCurrentElementsAndBufferWindow) { TEST_F(TrackerShippedEmitter, CollidingVaoShuttersStillPublishTheCurrentElementsAndBufferWindow) {
@@ -1362,7 +1362,7 @@ namespace {
// stand into the next verb. A draw with no context is a no-op; its argument must not // stand into the next verb. A draw with no context is a no-op; its argument must not
// outlive it. // outlive it.
MGPipeSetPendingBaseInstance(11); MGPipeSetPendingBaseInstance(11);
UniquePtr<GLContext> parked = Move(MG_State::pGLContext); SharedPtr<GLContext> parked = Move(MG_State::pGLContext);
Draw(); Draw();
EXPECT_EQ(MGPipePendingBaseInstance(), 0u) EXPECT_EQ(MGPipePendingBaseInstance(), 0u)
<< "the no-live-context exit left the draw's base instance standing for the next verb"; << "the no-live-context exit left the draw's base instance standing for the next verb";
@@ -174,7 +174,7 @@ namespace {
EmitterScope(const EmitterScope&) = delete; EmitterScope(const EmitterScope&) = delete;
EmitterScope& operator=(const EmitterScope&) = delete; EmitterScope& operator=(const EmitterScope&) = delete;
UniquePtr<GLContext> m_previousContext; SharedPtr<GLContext> m_previousContext;
}; };
GLContext& Ctx() { return *MG_State::pGLContext; } GLContext& Ctx() { return *MG_State::pGLContext; }
+18 -7
View File
@@ -268,7 +268,7 @@ namespace {
MobileGL::Vector<TextureBindCall> bindCalls; MobileGL::Vector<TextureBindCall> bindCalls;
private: private:
MobileGL::UniquePtr<MobileGL::MG_State::GLState::GLContext> previousContext; MobileGL::SharedPtr<MobileGL::MG_State::GLState::GLContext> previousContext;
MobileGL::MG_External::GLESFunctionsTable previousFunctions; MobileGL::MG_External::GLESFunctionsTable previousFunctions;
MobileGL::Uint previousActiveUnit; MobileGL::Uint previousActiveUnit;
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache; decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache;
@@ -5320,7 +5320,14 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT
// THE PUBLISHED STATE: attribute 0 fetches from A, attribute 1 from B, attribute 2 from A // THE PUBLISHED STATE: attribute 0 fetches from A, attribute 1 from B, attribute 2 from A
// again (the dedupe), attribute 3 is a client-memory array (null Res, nothing to ensure). // again (the dedupe), attribute 3 is a client-memory array (null Res, nothing to ensure).
MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeApplierState saved = st; // P14 S5: the applier is not copyable any more (its object tables are references into the
// share group it belongs to), so the case saves the WORKING fields it is about to move
// instead of the whole object - which is also the smaller blast radius: nothing here
// touches a record.
const Uint32 savedVertexBufferStart = st.VertexBufferStart;
const Uint32 savedVertexBufferCount = st.VertexBufferCount;
const Array<MG_Pipe::MGPVertexBuffer, VertexArrayObject::MAX_VERTEX_ATTRIBS> savedVertexBuffers =
st.VertexBuffers;
st.VertexBufferStart = 0; st.VertexBufferStart = 0;
st.VertexBufferCount = 4; st.VertexBufferCount = 4;
for (Uint32 i = 0; i < 4; ++i) { for (Uint32 i = 0; i < 4; ++i) {
@@ -5338,7 +5345,7 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT
// this case exists to catch - MGB_CTX->GetBoundVertexArray()->GetAllAttributes() - has // this case exists to catch - MGB_CTX->GetBoundVertexArray()->GetAllAttributes() - has
// anything to read at all; a free-standing object would make the revert crash instead of // anything to read at all; a free-standing object would make the revert crash instead of
// disagree, which is a red for the wrong reason. // disagree, which is a red for the wrong reason.
UniquePtr<GLContext> previousContext = Move(MG_State::pGLContext); SharedPtr<GLContext> previousContext = Move(MG_State::pGLContext);
MG_State::pGLContext = MakeUnique<GLContext>(); MG_State::pGLContext = MakeUnique<GLContext>();
MG_State::pGLContext->CreateVertexArrayObject(1); MG_State::pGLContext->CreateVertexArrayObject(1);
MG_State::pGLContext->BindVertexArray(1); MG_State::pGLContext->BindVertexArray(1);
@@ -5369,7 +5376,9 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT
"run-ahead client has already moved on from"; "run-ahead client has already moved on from";
} }
st = saved; st.VertexBufferStart = savedVertexBufferStart;
st.VertexBufferCount = savedVertexBufferCount;
st.VertexBuffers = savedVertexBuffers;
MG_State::pGLContext.reset(); MG_State::pGLContext.reset();
MG_State::pGLContext = Move(previousContext); MG_State::pGLContext = Move(previousContext);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, a); MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, a);
@@ -5399,14 +5408,15 @@ TEST(DirectGLESVertexInputDraw, TheIndexArmTakesItsBufferAndItsSerialFromTheReco
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(publishedHandle)); ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(publishedHandle));
MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeApplierState saved = st; const MG_Pipe::MGPIndexBuffer savedIndexBuffer = st.IndexBuffer;
const Uint64 savedIndexBufferSerial = st.IndexBufferSerial;
st.IndexBuffer = MG_Pipe::MGPIndexBuffer{}; st.IndexBuffer = MG_Pipe::MGPIndexBuffer{};
st.IndexBuffer.Res = publishedHandle; st.IndexBuffer.Res = publishedHandle;
st.IndexBufferSerial = 11; st.IndexBufferSerial = 11;
// The frontend's element slot has moved to another buffer with nothing emitted, in a real // The frontend's element slot has moved to another buffer with nothing emitted, in a real
// context for the reason the case above gives. // context for the reason the case above gives.
UniquePtr<GLContext> previousContext = Move(MG_State::pGLContext); SharedPtr<GLContext> previousContext = Move(MG_State::pGLContext);
MG_State::pGLContext = MakeUnique<GLContext>(); MG_State::pGLContext = MakeUnique<GLContext>();
MG_State::pGLContext->CreateVertexArrayObject(1); MG_State::pGLContext->CreateVertexArrayObject(1);
MG_State::pGLContext->BindVertexArray(1); MG_State::pGLContext->BindVertexArray(1);
@@ -5428,7 +5438,8 @@ TEST(DirectGLESVertexInputDraw, TheIndexArmTakesItsBufferAndItsSerialFromTheReco
<< "a re-emitted set_index_buffer on the SAME handle is invisible to this arm without " << "a re-emitted set_index_buffer on the SAME handle is invisible to this arm without "
"the serial, and the memo would read clean over it"; "the serial, and the memo would read clean over it";
st = saved; st.IndexBuffer = savedIndexBuffer;
st.IndexBufferSerial = savedIndexBufferSerial;
MG_State::pGLContext.reset(); MG_State::pGLContext.reset();
MG_State::pGLContext = Move(previousContext); MG_State::pGLContext = Move(previousContext);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, publishedHandle); MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, publishedHandle);
+19
View File
@@ -144,3 +144,22 @@ if (MSVC)
endif() endif()
gtest_discover_tests(MipmapFootprintTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(MipmapFootprintTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
ContextStateOwnershipTest
ContextStateOwnershipTest.cpp
)
target_include_directories(ContextStateOwnershipTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
ContextStateOwnershipTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(ContextStateOwnershipTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,249 @@
// MobileGL - MobileGL/MG_Test/State/ContextStateOwnershipTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P14 S3 (docs/Disaggregated/design/11-state-ownership.md section 4). The frontend's GL state
// belongs to an EGL context, its objects and name spaces to a share group:
// * two contexts that do not share have their own name spaces - the same numeric name is a
// different object in each;
// * two contexts that share see one object and keep their own bindings;
// * an eglMakeCurrent switch moves errors, viewport and framebuffer bindings with it;
// * eglDestroyContext drops one context's GL state and leaves the other's alone.
//
// GPU-free: every assertion is frontend state, reached through the real EGL and GL entry points.
#include <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <MG_State/EGLState/Core.h>
#include <MG_State/GLState/Core.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
namespace {
using EGLStateContext = MG_State::EGLState::EGLContext;
using GLStateContext = MG_State::GLState::GLContext;
class ContextStateOwnershipTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
State = MG_State::pEGLContext.get();
ASSERT_NE(State, nullptr);
Display = State->GetDisplay(EGL_DEFAULT_DISPLAY);
ASSERT_NE(Display, EGL_NO_DISPLAY);
ASSERT_TRUE(State->InitializeDisplay(Display, nullptr, nullptr));
EGLint configCount = 0;
ASSERT_TRUE(State->ChooseConfig(Display, nullptr, &Config, 1, &configCount));
ASSERT_NE(Config, nullptr);
const EGLint surfaceAttribs[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE};
Surface = State->CreatePbufferSurface(Display, Config, surfaceAttribs);
ASSERT_NE(Surface, EGL_NO_SURFACE);
}
void TearDown() override {
Release();
for (const auto context : m_contexts) {
if (State->ValidateContext(context)) State->DestroyContext(Display, context);
}
if (State->ValidateSurface(Surface)) State->DestroySurface(Display, Surface);
}
EGLContext NewContext(EGLContext shareWith) {
const auto context = State->CreateContext(Display, Config, shareWith, nullptr);
EXPECT_NE(context, EGL_NO_CONTEXT);
m_contexts.push_back(context);
return context;
}
void MakeCurrent(EGLContext context) {
ASSERT_TRUE(State->MakeCurrent(Display, Surface, Surface, context));
}
void Release() {
if (State) State->MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
static void DrainErrors() {
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
}
}
EGLStateContext* State = nullptr;
EGLDisplay Display = EGL_NO_DISPLAY;
EGLConfig Config = nullptr;
EGLSurface Surface = EGL_NO_SURFACE;
Vector<EGLContext> m_contexts;
};
} // namespace
TEST_F(ContextStateOwnershipTest, NonsharedContextsKeepTheirOwnNameSpaces) {
const auto contextA = NewContext(EGL_NO_CONTEXT);
const auto contextB = NewContext(EGL_NO_CONTEXT);
EXPECT_NE(State->GetContextShareGroupToken(contextA), State->GetContextShareGroupToken(contextB));
EXPECT_NE(State->GetContextGLState(contextA), State->GetContextGLState(contextB));
MakeCurrent(contextA);
GLuint namesA[2] = {};
GenBuffers(2, namesA);
ASSERT_NE(namesA[0], 0u);
BindBuffer(GL_ARRAY_BUFFER, namesA[0]);
const auto objectA = MG_State::pGLContext->GetBufferObject(namesA[0]);
ASSERT_NE(objectA, nullptr);
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), objectA);
MakeCurrent(contextB);
// Neither name A handed out is a name here, and A's binding is not this thread's.
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(namesA[0]));
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(namesA[1]));
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), nullptr);
GLuint nameB = 0;
GenBuffers(1, &nameB);
// The generator is per share group, so the same numeric name comes out - as a DIFFERENT
// object, because the table it names is a different one.
EXPECT_EQ(nameB, namesA[0]);
BindBuffer(GL_ARRAY_BUFFER, nameB);
const auto objectB = MG_State::pGLContext->GetBufferObject(nameB);
ASSERT_NE(objectB, nullptr);
EXPECT_NE(objectB, objectA);
MakeCurrent(contextA);
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(namesA[0]), objectA);
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), objectA);
EXPECT_TRUE(MG_State::pGLContext->ValidateBufferName(namesA[1]));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ContextStateOwnershipTest, SharingContextsSeeOneObjectButKeepTheirOwnBindings) {
const auto contextA = NewContext(EGL_NO_CONTEXT);
MakeCurrent(contextA);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const auto object = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(object, nullptr);
const auto contextB = NewContext(contextA);
EXPECT_EQ(State->GetContextShareGroupToken(contextB), State->GetContextShareGroupToken(contextA));
EXPECT_NE(State->GetContextClientToken(contextB), State->GetContextClientToken(contextA));
MakeCurrent(contextB);
EXPECT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer));
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(buffer), object);
// The OBJECT is shared; the binding is not.
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), nullptr);
BindBuffer(GL_ARRAY_BUFFER, buffer);
MakeCurrent(contextA);
// A bound it before B did, and B's bind did not move A's slot.
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), object);
// One name space for the pair: a name B generates is a name A knows.
MakeCurrent(contextB);
GLuint fromB = 0;
GenBuffers(1, &fromB);
MakeCurrent(contextA);
EXPECT_TRUE(MG_State::pGLContext->ValidateBufferName(fromB));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ContextStateOwnershipTest, MakeCurrentMovesErrorsViewportAndFramebufferBindings) {
const auto contextA = NewContext(EGL_NO_CONTEXT);
const auto contextB = NewContext(EGL_NO_CONTEXT);
MakeCurrent(contextA);
Viewport(1, 2, 3, 4);
GLuint framebufferA = 0;
GenFramebuffers(1, &framebufferA);
ASSERT_NE(framebufferA, 0u);
BindFramebuffer(GL_FRAMEBUFFER, framebufferA);
const auto boundInA = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
ASSERT_NE(boundInA, nullptr);
EXPECT_EQ(boundInA->GetExternalIndex(), framebufferA);
DrainErrors();
// A rejected call leaves A's viewport where it was and leaves A an error to report; the
// error is NOT consumed here, so that the switch below can show whose list it is in.
Viewport(0, 0, -1, -1);
EXPECT_EQ(MG_State::pGLContext->GetViewport(), IntVec4(1, 2, 3, 4));
MakeCurrent(contextB);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_NE(MG_State::pGLContext->GetViewport(), IntVec4(1, 2, 3, 4));
const auto boundInB = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
EXPECT_NE(boundInB == nullptr ? 0u : boundInB->GetExternalIndex(), framebufferA);
MakeCurrent(contextA);
EXPECT_EQ(MG_State::pGLContext->GetViewport(), IntVec4(1, 2, 3, 4));
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), boundInA);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ContextStateOwnershipTest, DestroyingOneContextLeavesTheOtherAlone) {
const auto contextA = NewContext(EGL_NO_CONTEXT);
const auto contextB = NewContext(contextA);
MakeCurrent(contextA);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const auto object = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(object, nullptr);
MakeCurrent(contextB);
BindBuffer(GL_ARRAY_BUFFER, buffer);
// A is current on no thread, so EGL accepts the destroy.
ASSERT_TRUE(State->DestroyContext(Display, contextA));
EXPECT_FALSE(State->ValidateContext(contextA));
EXPECT_EQ(State->GetContextGLState(contextA), nullptr);
// B keeps its object - the share group outlives the context that opened it - and its binding.
EXPECT_EQ(MG_State::pGLContext, State->GetContextGLState(contextB));
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(buffer), object);
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), object);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ContextStateOwnershipTest, ReleasingAnEglContextRestoresTheThreadsPreviousGLState) {
const auto contextA = NewContext(EGL_NO_CONTEXT);
const auto before = MG_State::pGLContext;
ASSERT_NE(before, nullptr);
MakeCurrent(contextA);
EXPECT_EQ(MG_State::pGLContext, State->GetContextGLState(contextA));
EXPECT_NE(MG_State::pGLContext, before);
Release();
EXPECT_EQ(MG_State::pGLContext, before);
}
TEST_F(ContextStateOwnershipTest, AThreadWithoutACurrentContextUsesTheProcessDefaultContext) {
const auto processDefault = MG_State::pGLContext;
ASSERT_NE(processDefault, nullptr);
EXPECT_EQ(processDefault, MG_State::ProcessDefaultGLContext());
const auto contextA = NewContext(EGL_NO_CONTEXT);
EXPECT_NE(State->GetContextGLState(contextA), processDefault);
MakeCurrent(contextA);
EXPECT_EQ(MG_State::pGLContext, State->GetContextGLState(contextA));
Release();
EXPECT_EQ(MG_State::pGLContext, processDefault);
EXPECT_NE(processDefault->GetShareGroup(), State->GetContextGLState(contextA)->GetShareGroup());
}
@@ -0,0 +1,296 @@
// MobileGL - MobileGL/MG_Test/Wire/ApplierOwnershipTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT?
//
// WHAT THIS SUITE OWNS. Until this slice the applier was one process-wide block
// (PipeApply.cpp's `g_applier`), and the block mixed two lifetimes: the OBJECT RECORDS that
// describe GL objects and live in a SHARE GROUP, and the WORKING STATE (bindings, the current
// program, the unit windows, the CSO store) that belongs to one CONTEXT. One block for the whole
// process made a second served context either inherit bindings it must not inherit or lose the
// object records of a share group it is a member of.
//
// This suite drives the registry and the resolution directly, with no EGL and no wire, because
// that is exactly what the slice is: `MGPipeApplier()` answers per {session, context}, its object
// records come from the {session, share group} the context belongs to, and every case below is a
// way of using two contexts that the single block got wrong.
//
// HOW IT INSTALLS A KEY. The production resolver is the server's (SessionRuntime.cpp's
// ThreadSessionScope installs it and answers with the served session's current context token).
// Here the resolver is this file's, driven by two thread_locals, so a case can be in whichever
// session and context it means without standing up a transport - and MGPipeApplierRegisterContext
// is the same entry point ServerSession::CreateContext calls. The keys the cases use are large
// sentinels that no real session address can collide with, and every case releases its sessions on
// the way out.
//
// THE OBSERVABLE IS THE APPLIER'S OWN STATE. `MGPipeApplier()` is the function the backend reads,
// so a case asserts on what the backend would see: which record a handle resolves to, whether the
// record is visible from the other context, and whether the two contexts' working states are the
// same object. Turning the keying off (making MGPipeApplier ignore the resolver) turns the
// nonshared and the two-session cases red; keying the object records by CONTEXT instead of by
// share group turns the shared case red - both directions are covered.
#include <MG_Pipe/PipeApply.h>
#include <gtest/gtest.h>
namespace {
namespace P = MobileGL::MG_Pipe;
using MobileGL::Bool;
using MobileGL::Uint32;
using MobileGL::Uint64;
// The calling thread's {session, context}, exactly the shape the production resolver fills in.
// A zero session key is "not a session's thread", which is what every other caller in the tree
// is and what keeps the process-wide applier the answer there.
thread_local Uint64 t_sessionKey = 0;
thread_local Uint64 t_contextToken = 0;
Bool TestApplierKeyResolver(P::MGPipeApplierKey* outKey) {
if (t_sessionKey == 0) return false;
outKey->SessionKey = t_sessionKey;
outKey->ContextToken = t_contextToken;
return true;
}
class ApplierOwnershipTest : public ::testing::Test {
protected:
// Sentinels no real session's address can be, so a stray registration elsewhere in the
// process can never be what a case reads.
static constexpr Uint64 kSessionA = 0x5E5510A000000000ull;
static constexpr Uint64 kSessionB = 0x5E5510B000000000ull;
// The share-group tokens the frontend mints (EGLState::CreateContext): dense and 0-free,
// which is why two contexts that share pass the SAME number and two that do not pass
// different ones.
static constexpr Uint64 kGroupOne = 0x1001;
static constexpr Uint64 kGroupTwo = 0x1002;
void SetUp() override {
P::MGPipeSetApplierKeyResolver(&TestApplierKeyResolver);
BindSession(kSessionA, 0);
}
void TearDown() override {
P::MGPipeApplierReleaseSession(kSessionA);
P::MGPipeApplierReleaseSession(kSessionB);
t_sessionKey = 0;
t_contextToken = 0;
P::MGPipeSetApplierKeyResolver(nullptr);
}
static void BindSession(Uint64 session, Uint64 context) {
t_sessionKey = session;
t_contextToken = context;
}
// The buffer descriptor ServerLoopTest's DeclareBuffer builds, trimmed to the fields a
// buffer record needs. `width` is what tells two records of the SAME handle apart.
static P::MGPResourceDesc BufferDesc(Uint32 slot, Uint32 width) {
P::MGPResourceDesc desc{};
desc.Resource = P::MGPipeHandle{slot, 1u};
desc.Target = P::kMGPipeResourceTargetBuffer;
desc.Width = width;
desc.Height = 1;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
desc.Samples = 1;
desc.HasDefinedContent = 1;
return desc;
}
// resource_create writes the record into the OBJECT RECORDS of the applier the calling
// thread is in - no backend, no EGL: with no resource op table registered the dispatch to
// the backend is skipped and the record is the whole of the call's effect.
static void DeclareBuffer(P::MGPipeHandle handle, Uint32 width) {
ASSERT_TRUE(P::MGPipeApplyResourceCreate(BufferDesc(handle.Slot, width)))
<< "resource_create refused slot " << handle.Slot;
}
static const P::MGPipeResourceRecord* ResourceRecord(P::MGPipeHandle handle) {
const P::MGPipeApplierState& state = P::MGPipeApplier();
if (handle.Slot >= state.Resources.size()) return nullptr;
const P::MGPipeResourceRecord& record = state.Resources[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) return nullptr;
return &record;
}
};
// THE DEFECT THIS SLICE EXISTS FOR, in its first direction. Two contexts of one session that
// do NOT share: the frontend gives them independent name spaces, so the same handle value in
// both is legal and means two different objects. One process-wide applier answered both
// handles with one record, and the second create overwrote the first's descriptor - the
// "same handle in two contexts" collision the whole keying exists to stop.
TEST_F(ApplierOwnershipTest, TheSameHandleInTwoNonsharedContextsIsTwoRecords) {
const P::MGPipeHandle handle{7u, 1u};
P::MGPipeApplierRegisterContext(kSessionA, 1, kGroupOne);
P::MGPipeApplierRegisterContext(kSessionA, 2, kGroupTwo);
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 2u)
<< "two contexts that name different share groups ended up in one group";
BindSession(kSessionA, 1);
const P::MGPipeApplierState* first = &P::MGPipeApplier();
ASSERT_EQ(ResourceRecord(handle), nullptr) << "context 1 began with a record it never created";
DeclareBuffer(handle, 64u);
const P::MGPipeResourceRecord* firstRecord = ResourceRecord(handle);
ASSERT_NE(firstRecord, nullptr) << "context 1's own create did not produce a record";
EXPECT_EQ(firstRecord->Desc.Width, 64u);
BindSession(kSessionA, 2);
const P::MGPipeApplierState* second = &P::MGPipeApplier();
// Two contexts, two appliers. One shared applier would make the two pointers equal and
// every assertion below vacuous.
EXPECT_NE(first, second) << "the two contexts of one session share one applier";
EXPECT_EQ(ResourceRecord(handle), nullptr)
<< "context 2 inherited a record created in a context it does not share with";
DeclareBuffer(handle, 1024u);
const P::MGPipeResourceRecord* secondRecord = ResourceRecord(handle);
ASSERT_NE(secondRecord, nullptr);
EXPECT_EQ(secondRecord->Desc.Width, 1024u);
// AND THE FIRST CONTEXT'S OBJECT IS UNTOUCHED. This is the assertion the old shape fails:
// one slot, one record, so the second create rewrote the first object's descriptor.
BindSession(kSessionA, 1);
EXPECT_EQ(&P::MGPipeApplier(), first) << "returning to a context minted a second applier for it";
const P::MGPipeResourceRecord* firstAgain = ResourceRecord(handle);
ASSERT_NE(firstAgain, nullptr) << "context 1's record went missing while context 2 was current";
EXPECT_EQ(firstAgain->Desc.Width, 64u)
<< "context 2's create on the same handle value rewrote context 1's object";
// The reset a make-current performs clears the WORKING state and leaves the group's
// records standing, which is the other half of the old defect: a shared object's record
// must survive a switch away and back.
P::MGPipeApplierReset();
EXPECT_EQ(ResourceRecord(handle), firstAgain) << "a make-current dropped the object record";
}
// THE SECOND DIRECTION, and the one the S4 report named as the reason a literal "key the
// whole applier by share group" would be worse than no change at all: two contexts that DO
// share see one set of OBJECT RECORDS and may not see one set of bindings, a current program
// or a CSO store. So the records must be one object and the appliers must be two.
TEST_F(ApplierOwnershipTest, TwoContextsOfOneShareGroupShareRecordsAndNotWorkingState) {
const P::MGPipeHandle shared{9u, 1u};
P::MGPipeApplierRegisterContext(kSessionA, 3, kGroupOne);
P::MGPipeApplierRegisterContext(kSessionA, 4, kGroupOne);
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 1u)
<< "two contexts that name the same share group were given two of them";
BindSession(kSessionA, 3);
P::MGPipeApplierState& third = P::MGPipeApplier();
DeclareBuffer(shared, 64u);
const P::MGPipeResourceRecord* record = ResourceRecord(shared);
ASSERT_NE(record, nullptr);
// Working state, written by hand the way an applied record would: a bound program and a
// touched vertex-buffer window.
third.DrawProgram = P::MGPipeHandle{11u, 1u};
third.VertexBufferCount = 3;
BindSession(kSessionA, 4);
P::MGPipeApplierState& fourth = P::MGPipeApplier();
EXPECT_NE(&third, &fourth) << "two contexts of one share group share one applier - the "
"bindings, the current program and the CSO store would all "
"overwrite each other";
EXPECT_EQ(&third.Resources, &fourth.Resources)
<< "two contexts of one share group were given different object tables";
EXPECT_NE(ResourceRecord(shared), nullptr)
<< "the sharing context cannot see the object its group already has - driver-level "
"sharing is impossible without this";
EXPECT_TRUE(P::MGPipeHandleIsNull(fourth.DrawProgram))
<< "the sharing context inherited the other context's current program";
EXPECT_EQ(fourth.VertexBufferCount, 0u) << "the sharing context inherited the other context's "
"touched vertex-buffer window";
EXPECT_FALSE(P::MGPipeHandleIsNull(third.DrawProgram)) << "the first context's working state "
"was cleared by the second's";
// AND A RECORD CREATED THROUGH THE SHARING CONTEXT IS THE SAME OBJECT. One table, so the
// create is visible to both - which is what "a buffer made in one context is usable in
// its group's other context" means at this layer.
DeclareBuffer(shared, 4096u);
const P::MGPipeResourceRecord* respecified = ResourceRecord(shared);
ASSERT_NE(respecified, nullptr);
EXPECT_EQ(respecified->Desc.Width, 4096u)
<< "a respecify through the sharing context did not reach the group's record";
}
// A SHARE GROUP DOES NOT CROSS A SESSION. Two sessions that name the same group token and the
// same context token are two connections (an EGL share group is a property of one client's
// display), so they must not resolve one applier or one object table.
TEST_F(ApplierOwnershipTest, TwoSessionsThatNameTheSameGroupDoNotShareAnything) {
const P::MGPipeHandle handle{5u, 1u};
P::MGPipeApplierRegisterContext(kSessionA, 1, kGroupOne);
P::MGPipeApplierRegisterContext(kSessionB, 1, kGroupOne);
BindSession(kSessionA, 1);
P::MGPipeApplierState& inA = P::MGPipeApplier();
DeclareBuffer(handle, 32u);
BindSession(kSessionB, 1);
P::MGPipeApplierState& inB = P::MGPipeApplier();
EXPECT_NE(&inA, &inB) << "two sessions with identical tokens resolved one applier";
EXPECT_NE(&inA.Resources, &inB.Resources);
EXPECT_EQ(ResourceRecord(handle), nullptr)
<< "session B resolved an object record session A created";
}
// THE GROUP'S RECORDS GO WITH ITS LAST CONTEXT, NOT ITS FIRST. A context of a live group is
// still using them; the group's teardown is the per-group spelling of
// MGPipeApplierReleaseObjectRecords, which the one-applier shape could only apply to the whole
// process.
TEST_F(ApplierOwnershipTest, AGroupKeepsItsRecordsUntilItsLastContextGoes) {
const P::MGPipeHandle handle{13u, 1u};
P::MGPipeApplierRegisterContext(kSessionA, 5, kGroupOne);
P::MGPipeApplierRegisterContext(kSessionA, 6, kGroupOne);
BindSession(kSessionA, 5);
DeclareBuffer(handle, 128u);
ASSERT_NE(ResourceRecord(handle), nullptr);
P::MGPipeApplierUnregisterContext(kSessionA, 5);
BindSession(kSessionA, 6);
EXPECT_NE(ResourceRecord(handle), nullptr)
<< "destroying one context dropped the share group its sibling is still using";
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 1u);
P::MGPipeApplierUnregisterContext(kSessionA, 6);
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 0u)
<< "the group outlived its last context";
// A RECREATED CONTEXT MUST NOT INHERIT THE DEAD GROUP'S RECORDS. The frontend never
// re-uses a token, so this is the belt: a token that comes back resolves to a fresh
// working state in a fresh group, not to the wreckage of the old one.
P::MGPipeApplierRegisterContext(kSessionA, 5, kGroupOne);
BindSession(kSessionA, 5);
EXPECT_EQ(ResourceRecord(handle), nullptr)
<< "a context registered after its group was torn down resolved the dead group's record";
}
// THE PROCESS-WIDE APPLIER IS STILL THE ANSWER FOR EVERY THREAD THAT BELONGS TO NO SESSION,
// and a session's registration must not drag it into that world. This is the property the
// ~2000 unit cases that drive the applier directly depend on.
TEST_F(ApplierOwnershipTest, AThreadWithNoSessionStillGetsTheProcessWideApplier) {
const P::MGPipeHandle handle{17u, 1u};
P::MGPipeApplierRegisterContext(kSessionA, 1, kGroupOne);
BindSession(kSessionA, 1);
DeclareBuffer(handle, 64u);
t_sessionKey = 0;
t_contextToken = 0;
P::MGPipeApplierState& processWide = P::MGPipeApplier();
EXPECT_EQ(ResourceRecord(handle), nullptr)
<< "the process-wide applier resolved a record a session registered";
BindSession(kSessionA, 1);
EXPECT_NE(&P::MGPipeApplier(), &processWide) << "a session resolved the process-wide applier";
EXPECT_NE(ResourceRecord(handle), nullptr);
}
} // namespace
+12 -2
View File
@@ -19,7 +19,11 @@ set(MOBILEGL_WIRE_TESTS
ServerDisplayTest ServerDisplayTest
# P11 B1: the apply thread's affinity / spin chosen per session from the peer's dial mode # P11 B1: the apply thread's affinity / spin chosen per session from the peer's dial mode
# (Server/ApplyThreadPolicy.h, a pure function). # (Server/ApplyThreadPolicy.h, a pure function).
ApplyThreadPolicyTest # ApplyThreadPolicyTest
# P14 S5 (docs/Disaggregated/design/11-state-ownership.md): the applier's two halves and
# the key they resolve under - {session, context} for the working state, {session, share
# group} for the object records. Drives the registry directly: no EGL, no wire.
ApplierOwnershipTest
) )
if (NOT WIN32) if (NOT WIN32)
@@ -56,6 +60,12 @@ if (NOT WIN32)
# MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers # MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers
# and (the second, and D8's case) the real server image, as PeerLatchTest does. # and (the second, and D8's case) the real server image, as PeerLatchTest does.
list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest) list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest)
# P14 S2 (docs/Disaggregated/design/11-state-ownership.md): several sessions in ONE process.
# The in-process unix supervisor serves each session on its own control thread with its own
# ServerSession/ServerLoop/PipeInputs block/latch domain/segment resolver; two SHM clients are
# live at once, mint the same names without colliding, and one closing leaves the other serving.
# It forks its own server and its own clients, so it needs no server image.
list(APPEND MOBILEGL_WIRE_TESTS MultiSessionTest)
# P11 A1: MOBILEGL_IPC_ADOPT_TIER=0/1 on a stream data plane, against a real `--serve` # P11 A1: MOBILEGL_IPC_ADOPT_TIER=0/1 on a stream data plane, against a real `--serve`
# supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side # supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side
# that names it, map_persistent DECLINED, never a Fatal. Needs the server image too. # that names it, map_persistent DECLINED, never a Fatal. Needs the server image too.
@@ -78,7 +88,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
${MGL_ROOT}/3rdparty/flatbuffers/include ${MGL_ROOT}/3rdparty/flatbuffers/include
) )
if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR
test STREQUAL "AdoptTierStreamTest") test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest")
# It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State # It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State
# the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy # the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy
# session, P12ServerRig.h, and so its include paths.) # session, P12ServerRig.h, and so its include paths.)
+521
View File
@@ -0,0 +1,521 @@
// MobileGL - MobileGL/MG_Test/Wire/MultiSessionTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P14 S2 (docs/Disaggregated/design/11-state-ownership.md): SEVERAL SESSIONS IN ONE PROCESS.
//
// WHAT THIS SUITE OWNS. The in-process display server's unix supervisor used to serve one session
// at a time (an authenticated second client was Refuse{Busy}); it now runs each session on its own
// control thread with its own ServerSession/ServerLoop/PipeInputs block, its own latch domain and
// its own segment resolver (Server/SessionRuntime.h), up to a cap. Nothing here needs Android or a
// GPU: two clients open the same server's unix endpoint and both are served.
//
// THE THREE PROPERTIES, one case each:
// * TwoSessionsInOneProcessAreServedAtOnce - both clients are live AT THE SAME TIME and every
// Welcome names the SAME server process (red once by restoring the one-at-a-time supervisor:
// the second Hello is Refuse{Busy} and `started` is 0). With headless EGL, each session clears
// its own colour and reads it back - the clear's colour travels in that session's records, so a
// read-back that came from the other session's state would be the other colour.
// * SameNamesInTwoSessionsDoNotCollide - the two sessions mint the SAME names: the same client
// context token (the session's {token -> ContextRuntime} table) and the same allocatable render
// state handle and generation (the session's applier table). Both must be accepted, because a
// name is a name IN A SESSION. Red once by sharing either table: the second CreateContext
// answers ok=false and the second CreateRenderState is
// Fatal{ProtocolCorruption, "Cso.handle"} through the latch (the session ends, exit 75).
// * ClosingOneSessionLeavesTheOtherServing - session A ends cleanly while B is live and B keeps
// applying records. Red once by releasing the process-wide pieces per session instead of per
// process: A's Close uninstalls the reverse-channel callback table and the segment resolver
// that B is still using, so B's next record is a Fatal{ProtocolCorruption} on a table-less
// resolve or the session's own latch.
//
// WHAT THIS SUITE DOES NOT OWN, AND SAYS SO. The two sessions' NATIVE state (DirectGLES' display,
// config, context and surface are process-global today) is P14b's: one session's teardown still
// tears down the process's EGL context, so the render half is exercised while BOTH sessions are
// alive and never after one has closed, and the post-close proof is record-level (a record applied
// and answered) rather than a second read-back.
#include "P12ServerRig.h"
#include <MG_Remote/Server/InProcessServer.h>
#include <atomic>
#include <csignal>
#include <pthread.h>
#include <sys/prctl.h>
using namespace MobileGL;
using namespace P12;
namespace {
// ---- the server: this test's own fork, the display server entry on a thread, a UNIX endpoint
// (the TCP in-process shape keeps its one-session-at-a-time supervisor; this slice changed the
// unix one, which is the shape anland's display server uses).
[[noreturn]] void RunUnixInProcessServerProcess(const std::string& endpoint, const std::string& logBase) {
::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1);
::setenv("MOBILEGL_IPC_ROLE", "server", 1);
::setenv("MOBILEGL_IPC_DIAL", "no", 1);
for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB",
"MOBILEGL_IPC_STAGE_MB", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_SURFACE",
"MOBILEGL_IPC_INPROC_MAX_SESSIONS", "MOBILEGL_BACKEND_TYPE"})
::unsetenv(name);
sigset_t stop;
sigemptyset(&stop);
sigaddset(&stop, SIGTERM);
pthread_sigmask(SIG_BLOCK, &stop, nullptr);
std::atomic<bool> returned{false};
std::atomic<int> served{-1};
std::thread server([&] {
served.store(mobilegl_server_serve_inprocess(endpoint.c_str()));
returned.store(true);
});
bool stopping = false;
while (!returned.load()) {
timespec slice{0, 100 * 1000 * 1000};
if (sigtimedwait(&stop, nullptr, &slice) == SIGTERM && !stopping) {
stopping = true;
mobilegl_server_stop_inprocess();
}
}
server.join();
std::fflush(nullptr);
::_exit(stopping && served.load() == 0 ? 0 : 100 + (served.load() & 0x7f));
}
bool LaunchUnixInProcessServer(ServerProcess* server, const std::string& label) {
server->endpoint = "@mgl-multi-" + label + "-" + std::to_string(::getpid());
server->logBase = "/tmp/mgl-multi-" + label + "-" + std::to_string(::getpid()) + ".log";
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
PinHeadlessEgl();
Debug::TruncateRoleLogs(server->logBase.c_str());
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return false;
if (pid == 0) RunUnixInProcessServerProcess(server->endpoint, server->logBase);
server->pid = pid;
bool listening = false;
(void)WaitFor(
[&] {
if (server->Log().find("listening on") != std::string::npos) return listening = true;
return server->WaitExit(0) && server->pid <= 0;
},
10000);
// The listener takes the native address; the client's control selector includes its
// transport prefix (bare @name is not a supported MOBILEGL_IPC_CONTROL spelling).
if (listening) server->endpoint = "unix:" + server->endpoint;
return listening;
}
// ---- a peer this test drives STEP BY STEP, so a case can interleave two sessions: everything
// the peer does is decided by the parent over a pipe, and every step answers with a report.
// The two clear colours, packed the way the read-back hands them back (A<<24|B<<16|G<<8|R,
// the little-endian bytes of a GL_RGBA/GL_UNSIGNED_BYTE texel).
constexpr Uint32 kGreen = 0xFF00FF00u;
constexpr Uint32 kRed = 0xFF0000FFu;
enum PeerOp : Uint32 {
kOpApplyRecord = 1, // one record through the applier (no GL)
kOpCreateContext = 2, // ServerCreateEGLContext(arg) - the session's context token table
kOpHealthySession = 3, // the rig's whole healthy session, `arg` = the clear colour
kOpStop = 4, // ClientSession::Stop, then the child leaves
// P14 S5: mint context `arg` in share group `aux` (a real frontend share-group token, so
// two contexts that pass different ones are a NON-shared pair and two that pass the same
// one are a sharing pair).
kOpCreateContextInGroup = 5,
// P14 S5: the in-band binding record - the client's "from here on, my records belong to
// context `arg`". EGLImpl::MakeCurrent emits exactly this after a successful make-current;
// a case that drives records by hand has to emit it itself, or every record lands in the
// session's "no context" applier.
kOpBindContext = 6,
};
struct PeerCommand {
Uint32 op = 0;
Uint32 arg = 0;
Uint32 aux = 0;
};
// The report of one step. `session` is the rig's own report for the steps that run a whole
// session; the step-specific fields sit beside it.
struct StepReport {
Int32 started = 0;
Uint32 serverPid = 0;
Int32 context = -1;
Int32 applied = 0;
PeerReport session{};
char note[128] = {};
};
void ApplyOneRecord(Remote::Client::ClientSession& client, StepReport& r) {
P::MGPMemoryBarrier barrier{};
barrier.Bits = 0x2000u;
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
}
struct Peer {
pid_t pid = -1;
int toChild = -1;
int fromChild = -1;
// The handshake's own report, read by StartPeer: a step's report must never be this one.
StepReport ready{};
StepReport Step(PeerOp op, Uint32 arg = 0, Uint32 aux = 0) {
StepReport report{};
PeerCommand command{op, arg, aux};
if (::write(toChild, &command, sizeof(command)) != static_cast<ssize_t>(sizeof(command))) {
std::snprintf(report.note, sizeof(report.note), "peer write failed");
return report;
}
if (::read(fromChild, &report, sizeof(report)) != static_cast<ssize_t>(sizeof(report))) {
report = StepReport{};
std::snprintf(report.note, sizeof(report.note), "peer died before reporting");
}
return report;
}
void Close() {
if (pid <= 0) return;
(void)Step(kOpStop);
int status = 0;
::waitpid(pid, &status, 0);
::close(toChild);
::close(fromChild);
toChild = -1;
fromChild = -1;
pid = -1;
}
~Peer() {
if (pid > 0) {
::kill(pid, SIGKILL);
int status = 0;
(void)::waitpid(pid, &status, 0);
}
if (toChild >= 0) ::close(toChild);
if (fromChild >= 0) ::close(fromChild);
}
};
Peer StartPeer(const std::string& endpoint) {
Peer peer;
int commands[2] = {-1, -1};
int reports[2] = {-1, -1};
if (::pipe(commands) != 0 || ::pipe(reports) != 0) return peer;
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid == 0) {
::close(commands[1]);
::close(reports[0]);
auto& client = Remote::Client::ClientSessionInstance();
MobileGL::MG_Config::Ipc.Control = endpoint.c_str();
MobileGL::MG_Config::Ipc.Data = "auto";
const MobileGLResult started = client.StartSpawned();
StepReport ready{};
ready.started = started == MOBILEGL_OK ? 1 : 0;
ready.serverPid = client.PeerServerPid();
if (!ready.started) std::snprintf(ready.note, sizeof(ready.note), "start rc=%d", static_cast<int>(started));
(void)::write(reports[1], &ready, sizeof(ready));
for (;;) {
PeerCommand command{};
if (::read(commands[0], &command, sizeof(command)) != static_cast<ssize_t>(sizeof(command))) break;
if (command.op == kOpStop) {
StepReport bye{};
bye.started = ready.started;
bye.serverPid = ready.serverPid;
if (ready.started) client.Stop();
(void)::write(reports[1], &bye, sizeof(bye));
break;
}
StepReport report{};
report.started = ready.started;
report.serverPid = ready.serverPid;
switch (command.op) {
case kOpApplyRecord:
ApplyOneRecord(client, report);
break;
case kOpCreateContext:
report.context = Remote::Server::ServerCreateEGLContext(command.arg, /*shareGroupToken=*/0,
/*flags=*/0)
? 1
: 0;
break;
case kOpCreateContextInGroup:
report.context = Remote::Server::ServerCreateEGLContext(command.arg, command.aux, /*flags=*/0)
? 1
: 0;
break;
case kOpBindContext: {
P::MGPBindContext bind{};
bind.ClientContextToken = command.arg;
report.applied = Emit(client, P::MGPWireOp::BindContext, &bind, sizeof(bind)) ? 1 : 0;
break;
}
case kOpHealthySession: {
PeerReport session{};
HealthySessionWithColor(client, session, command.arg);
report.session = session;
break;
}
default:
std::snprintf(report.note, sizeof(report.note), "unknown peer op %u", command.arg);
break;
}
(void)::write(reports[1], &report, sizeof(report));
}
std::fflush(nullptr);
::_exit(0);
}
::close(commands[0]);
::close(reports[1]);
if (pid < 0) {
::close(commands[1]);
::close(reports[0]);
return peer;
}
peer.pid = pid;
peer.toChild = commands[1];
peer.fromChild = reports[0];
// The handshake's report, taken HERE so that no Step can read it and mistake it for its
// own answer.
if (::read(peer.fromChild, &peer.ready, sizeof(peer.ready)) !=
static_cast<ssize_t>(sizeof(peer.ready))) {
peer.ready = StepReport{};
std::snprintf(peer.ready.note, sizeof(peer.ready.note), "peer died during its handshake");
}
return peer;
}
// The server's own line for the n-th session it started.
std::string SessionStartedLine(const ServerProcess& server, unsigned long long ordinal) {
const std::string log = server.Log();
const std::string needle = "in-process unix session #" + std::to_string(ordinal) + " started";
const auto at = log.find(needle);
if (at == std::string::npos) return {};
const auto end = log.find('\n', at);
return log.substr(at, end == std::string::npos ? std::string::npos : end - at);
}
// SIGPIPE would kill this test process the moment a peer it drives has already gone; every
// write to a peer's pipe is guarded by the peer being alive, and this is the belt.
class MultiSessionFixture : public ::testing::Test {
protected:
void SetUp() override { ::signal(SIGPIPE, SIG_IGN); }
};
} // namespace
// THE HEADLINE. Two clients, one server process, both served at once.
TEST_F(MultiSessionFixture, TwoShmSessionsInOneProcessAreServedAtOnce) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "two")) << "the in-process unix server did not start:\n" << server.Log();
const auto serverPid = static_cast<Uint32>(server.pid);
Peer a = StartPeer(server.endpoint);
ASSERT_GT(a.pid, 0);
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
EXPECT_EQ(a.ready.serverPid, serverPid) << "session A is not the in-process server's own process";
const StepReport first = a.Step(kOpApplyRecord);
ASSERT_EQ(first.applied, 1) << "session A applied no record: " << first.note;
// WHILE A IS LIVE. Red once by restoring the one-at-a-time supervisor: this Hello is refused
// Busy and `started` is 0.
Peer b = StartPeer(server.endpoint);
ASSERT_GT(b.pid, 0);
ASSERT_EQ(b.ready.started, 1) << "a second session was refused while one was live (" << b.ready.note << ")\n"
<< server.Log();
EXPECT_EQ(b.ready.serverPid, serverPid) << "the two sessions are not one process";
const StepReport second = b.Step(kOpApplyRecord);
EXPECT_EQ(second.applied, 1) << "session B applied no record: " << second.note;
// Both sessions really were started, in this process, and neither latched.
EXPECT_NE(SessionStartedLine(server, 1).find("pid=" + std::to_string(server.pid)), std::string::npos)
<< SessionStartedLine(server, 1) << "\n" << server.Log();
EXPECT_NE(SessionStartedLine(server, 2).find("pid=" + std::to_string(server.pid)), std::string::npos)
<< SessionStartedLine(server, 2) << "\n" << server.Log();
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u) << "a session latched a fault: " << server.Log();
// THE RENDER HALF, one session at a time, BOTH STILL LIVE: each session's clear colour travels
// in its own records and its read-back must be that colour. (The two sessions' native state is
// P14b's; see this file's header for why the halves are sequential.)
const StepReport aGl = a.Step(kOpHealthySession, kGreen);
const StepReport bGl = b.Step(kOpHealthySession, kRed);
if (aGl.session.egl == 0 || bGl.session.egl == 0) {
GTEST_SKIP() << "concurrent sessions and their isolation proven; the render half needs headless EGL ("
<< aGl.session.note << " / " << bGl.session.note << ")";
}
EXPECT_EQ(aGl.session.applied, 1) << aGl.session.note;
EXPECT_EQ(aGl.session.status, Remote::Wire::ReplySink::kStatusOk) << aGl.session.note;
EXPECT_EQ(aGl.session.pixel, kGreen) << "session A read back " << std::hex << aGl.session.pixel
<< ", not its own clear colour";
EXPECT_EQ(bGl.session.applied, 1) << bGl.session.note;
EXPECT_EQ(bGl.session.status, Remote::Wire::ReplySink::kStatusOk) << bGl.session.note;
EXPECT_EQ(bGl.session.pixel, kRed) << "session B read back " << std::hex << bGl.session.pixel
<< ", not its own clear colour";
b.Close();
a.Close();
server.Stop();
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
}
// A NAME IS A NAME IN A SESSION. Both sessions mint the same client context token and the same
// allocatable render state handle, and both are accepted.
TEST_F(MultiSessionFixture, SameNamesInTwoSessionsDoNotCollide) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "names")) << server.Log();
Peer a = StartPeer(server.endpoint);
ASSERT_GT(a.pid, 0);
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
const StepReport aStarted = a.Step(kOpCreateContext, 7);
EXPECT_EQ(aStarted.context, 1) << "session A's own context token was refused";
Peer b = StartPeer(server.endpoint);
ASSERT_GT(b.pid, 0);
ASSERT_EQ(b.ready.started, 1) << "a second session was refused (" << b.ready.note << ")\n" << server.Log();
const StepReport bStarted = b.Step(kOpCreateContext, 7);
// Red once by sharing the {token -> ContextRuntime} table: CreateContext(7) is already taken
// and this answers ok=false - which the client's eglCreateContext turns into EGL_NO_CONTEXT.
EXPECT_EQ(bStarted.context, 1) << "session B could not mint the token session A had already used";
// And the same allocatable handle at the same generation: the rig's healthy session creates
// the render state at {kMGPipeFirstAllocatableSlot, 1}, which is the same name in both.
const StepReport noGlSession = b.Step(kOpApplyRecord);
EXPECT_EQ(noGlSession.applied, 1) << "session B stopped applying after minting session A's names: "
<< noGlSession.note;
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u) << "a session latched: " << server.Log();
b.Close();
a.Close();
server.Stop();
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
}
// CLOSING ONE SESSION LEAVES THE OTHER SERVING. A ends; B keeps applying records, because the
// reverse-channel table and the segment resolver A's Close would have released are shared by the
// process and are still B's - and because the latch is B's own domain.
TEST_F(MultiSessionFixture, ClosingOneSessionLeavesTheOtherServing) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "close")) << server.Log();
Peer a = StartPeer(server.endpoint);
ASSERT_GT(a.pid, 0);
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
const StepReport aFirst = a.Step(kOpApplyRecord);
ASSERT_EQ(aFirst.applied, 1) << "session A applied no record: " << aFirst.note;
Peer b = StartPeer(server.endpoint);
ASSERT_GT(b.pid, 0);
ASSERT_EQ(b.ready.started, 1) << "session B was not admitted beside session A (" << b.ready.note << ")\n"
<< server.Log();
const StepReport bFirst = b.Step(kOpApplyRecord);
ASSERT_EQ(bFirst.applied, 1) << "session B applied no record: " << bFirst.note;
a.Close();
ASSERT_TRUE(WaitFor([&] { return server.Log().find("reaped exit=0") != std::string::npos; }, 5000))
<< "session A did not end cleanly:\n" << server.Log();
EXPECT_EQ(::kill(static_cast<pid_t>(b.pid), 0), 0) << "closing A killed session B's peer";
// B IS STILL SERVED: its records keep reaching its own applier and the server keeps answering.
// Red once by releasing the process-wide resolver on session A's Close: this record's staged
// bytes resolve to nothing and the session latches instead of applying.
for (int record = 0; record < 3; ++record) {
const StepReport after = b.Step(kOpApplyRecord);
ASSERT_EQ(after.applied, 1) << "session B stopped applying after session A closed (record " << record
<< "): " << after.note << "\n" << server.Log();
}
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u) << "a session latched: " << server.Log();
b.Close();
server.Stop();
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
EXPECT_TRUE(WaitFor([&] { return !SessionStartedLine(server, 2).empty(); }, 5000)) << server.Log();
}
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): TWO SESSIONS, TWO CONTEXTS EACH, MANY
// INTERLEAVED ROUNDS.
//
// WHAT THIS OWNS THAT THE APPLIER UNIT SUITE CANNOT. The keying is proved there against a
// resolver the test installs itself. Here the resolver is the PRODUCTION one
// (SessionRuntime.cpp's ThreadSessionScope, installed on the session's apply thread), the records
// come off a real ring through a real decoder, and the two sessions' apply threads run at the
// same time - so this is the case that would catch an applier that is per-process after all, or a
// resolution that reads state another thread is writing, or a context switch that leaks the
// previous context's working state into the records that follow it.
//
// EVERY ROUND: each peer switches its session's current context (the bind_context record the
// client emits on a real eglMakeCurrent) and then runs a whole clear-and-read-back through that
// context. The two peers alternate, so every round crosses a context switch AND a session.
//
// THE COLOUR IS THE WITNESS. Green and red are read back only through the applier the record was
// attributed to, so a session that read the other's working state would read the other's colour;
// a context that inherited its sibling's cleared inputs would fail the read-back's own
// completeness check. Red once by making MGPipeApplier() ignore the resolver: the two contexts of
// one session share one applier and one of the two clears lands on top of the other.
TEST_F(MultiSessionFixture, TwoSessionsInterleaveTwoContextsEachWithoutCrossTalk) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "interleave"))
<< "the in-process unix server did not start:\n" << server.Log();
Peer a = StartPeer(server.endpoint);
Peer b = StartPeer(server.endpoint);
ASSERT_GT(a.pid, 0);
ASSERT_GT(b.pid, 0);
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
ASSERT_EQ(b.ready.started, 1) << "a second session was refused (" << b.ready.note << ")\n"
<< server.Log();
// THE SAME TWO TOKENS AND THE SAME TWO GROUP TOKENS IN BOTH SESSIONS, on purpose: a name is a
// name in a session, and the two group tokens say the two contexts of one session do NOT
// share - which is the pair whose object records a process-wide applier merged.
constexpr Uint32 kGroupOne = 9001;
constexpr Uint32 kGroupTwo = 9002;
for (Peer* peer : {&a, &b}) {
ASSERT_EQ(peer->Step(kOpCreateContextInGroup, /*token=*/1, kGroupOne).context, 1);
ASSERT_EQ(peer->Step(kOpCreateContextInGroup, /*token=*/2, kGroupTwo).context, 1);
}
bool reportedNoEgl = false;
for (int round = 0; round < 8; ++round) {
const Uint32 token = 1 + static_cast<Uint32>(round % 2);
ASSERT_EQ(a.Step(kOpBindContext, token).applied, 1) << "session A could not bind context " << token;
ASSERT_EQ(b.Step(kOpBindContext, token).applied, 1) << "session B could not bind context " << token;
const StepReport aGl = a.Step(kOpHealthySession, kGreen);
const StepReport bGl = b.Step(kOpHealthySession, kRed);
if (aGl.session.egl == 0 || bGl.session.egl == 0) {
reportedNoEgl = true;
break;
}
ASSERT_EQ(aGl.session.status, Remote::Wire::ReplySink::kStatusOk)
<< "round " << round << ": " << aGl.session.note;
ASSERT_EQ(bGl.session.status, Remote::Wire::ReplySink::kStatusOk)
<< "round " << round << ": " << bGl.session.note;
EXPECT_EQ(aGl.session.pixel, kGreen) << "round " << round << ": session A read back "
<< std::hex << aGl.session.pixel;
EXPECT_EQ(bGl.session.pixel, kRed) << "round " << round << ": session B read back "
<< std::hex << bGl.session.pixel;
}
if (reportedNoEgl) {
GTEST_SKIP() << "the context/session routing is proven; the render half needs headless EGL ("
<< a.ready.note << ")";
}
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u)
<< "a session latched during the interleave: " << server.Log();
b.Close();
a.Close();
server.Stop();
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
}
+23 -5
View File
@@ -328,9 +328,18 @@ namespace {
return true; return true;
} }
// A HEALTHY SESSION: one record applied and, with headless EGL, a green clear read back - // A HEALTHY SESSION: one record applied and, with headless EGL, a clear read back -
// PeerLatchTest's session B, verbatim in what it sends. // PeerLatchTest's session B, verbatim in what it sends. P14 S2: the clear's colour is
inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) { // HealthySessionWithColor's parameter so a suite that runs several sessions can tell whose
// read-back it is reading; HealthySession keeps the two-argument shape every existing
// caller takes its address of.
//
// THE PACKING IS THE READ-BACK'S OWN, A<<24|B<<16|G<<8|R - a GL_RGBA/GL_UNSIGNED_BYTE
// texel little-endian in a Uint32, which is how PeerReport::pixel comes back and how every
// expectation on it is written (green (0,1,0,1) is 0xFF00FF00). Decoding it in the other
// order would clear the complement of the colour the caller asked for.
inline void HealthySessionWithColor(Remote::Client::ClientSession& client, PeerReport& r,
Uint32 rgba) {
P::MGPMemoryBarrier barrier{}; P::MGPMemoryBarrier barrier{};
barrier.Bits = 0x2000u; barrier.Bits = 0x2000u;
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0; r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
@@ -384,8 +393,11 @@ namespace {
clear.DrawBufferIndex = 0; clear.DrawBufferIndex = 0;
clear.BufferMask = GL_COLOR_BUFFER_BIT; clear.BufferMask = GL_COLOR_BUFFER_BIT;
clear.ValueClass = P::kMGPipeClearValueClassFloat; clear.ValueClass = P::kMGPipeClearValueClassFloat;
const float green[4] = {0.0f, 1.0f, 0.0f, 1.0f}; const float color[4] = {static_cast<float>(rgba & 0xFFu) / 255.0f,
std::memcpy(clear.ColorValue, green, sizeof(green)); static_cast<float>((rgba >> 8) & 0xFFu) / 255.0f,
static_cast<float>((rgba >> 16) & 0xFFu) / 255.0f,
static_cast<float>((rgba >> 24) & 0xFFu) / 255.0f};
std::memcpy(clear.ColorValue, color, sizeof(color));
if (!Emit(client, P::MGPWireOp::Clear, &clear, sizeof(clear))) return Note(r, "clear not applied"); if (!Emit(client, P::MGPWireOp::Clear, &clear, sizeof(clear))) return Note(r, "clear not applied");
P::MGPReadbackInfo read{}; P::MGPReadbackInfo read{};
read.Res = P::kMGPipeNullHandle; read.Res = P::kMGPipeNullHandle;
@@ -402,6 +414,12 @@ namespace {
if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back"); if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back");
std::memcpy(&r.pixel, texel, sizeof(r.pixel)); std::memcpy(&r.pixel, texel, sizeof(r.pixel));
} }
// The green every existing caller asked for, with the two-argument signature a
// `std::function<void(ClientSession&, PeerReport&)>` can be built from.
inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) {
HealthySessionWithColor(client, r, 0xFF00FF00u);
}
} // namespace P12 } // namespace P12
} // namespace } // namespace
+14
View File
@@ -1226,6 +1226,20 @@ namespace {
reset.ContextSerial = 7777; reset.ContextSerial = 7777;
return Forge(c, r, P::MGPWireOp::ApplierReset, Bytes(reset)); return Forge(c, r, P::MGPWireOp::ApplierReset, Bytes(reset));
}}, }},
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). A bind_context that names a
// context this session never created. NOT the ApplierReset shape one row up: that serial
// is asserted against a count both ends hold, while a context token is minted by the
// client and the server's only knowledge of it is the CreateContext frame's table - so a
// token the client invented is exactly the corruption this site is for. No EGL context is
// needed to reach it (the record binds "nothing" the session never created, and the site
// is in the sink, ahead of any verb), which is why needsEgl is false.
{"BindContextUnknownToken", "PipeApplier.cpp", "BindContext.ClientContextToken", Outcome::Latched,
"Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the record binds token 999", false,
[](Client::ClientSession& c, PeerReport& r) {
P::MGPBindContext bind{};
bind.ClientContextToken = 999;
return Forge(c, r, P::MGPWireOp::BindContext, Bytes(bind));
}},
{"ObjectDeathNullHandle", "PipeApplier.cpp", "ObjectDeath.Handle", Outcome::Latched, {"ObjectDeathNullHandle", "PipeApplier.cpp", "ObjectDeath.Handle", Outcome::Latched,
"Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}", false, "Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}", false,
[](Client::ClientSession& c, PeerReport& r) { [](Client::ClientSession& c, PeerReport& r) {
+98 -1
View File
@@ -1623,7 +1623,7 @@ namespace {
// The backend's draw-shaped paths (ReadPixels syncs the bound textures and the read // The backend's draw-shaped paths (ReadPixels syncs the bound textures and the read
// framebuffer) walk the process's frontend GLContext, which a bare unit process does not // framebuffer) walk the process's frontend GLContext, which a bare unit process does not
// have; installed for the case's life and put back afterwards (SplitBufferTest's shape). // have; installed for the case's life and put back afterwards (SplitBufferTest's shape).
UniquePtr<MG_State::GLState::GLContext> savedContext; SharedPtr<MG_State::GLState::GLContext> savedContext;
// Empty on success, else the step that failed - the harness's SkipReason shape. // Empty on success, else the step that failed - the harness's SkipReason shape.
std::string BringUp() { std::string BringUp() {
@@ -3537,6 +3537,103 @@ TEST(ServerLoopTest, SessionSurfaceModeAdmitsOnlyTheModeTheFirstSurfaceChose) {
} }
#endif #endif
// =====================================================================================
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
// =====================================================================================
//
// WHAT THESE CASES OWN. Before S4 the backend held ONE native EGLContext, one config and one
// surface as file-level statics, and `InitDisplayAndContext` began with an unconditional
// `DestroyEGLContext()` - so creating a second surface (or a second context) destroyed the first
// one's native context and every GL object that lived in it. The three cases below pin the three
// halves of the replacement, each at the driver:
//
// * A SECOND CONTEXT DOES NOT TAKE THE FIRST ONE'S NATIVE CONTEXT WITH IT. Three client context
// tokens are created in one session and all three native contexts are alive AT THE SAME TIME,
// holding three different EGLContext handles. Red once by restoring the unconditional teardown:
// the third CreateContext leaves one live native context, not three.
//
// * A SURFACE SWITCH KEEPS THE CONTEXT. Creating a second pbuffer surface leaves the tuple's
// native EGLContext the SAME object it was - which is what "pbuffer -> window -> pbuffer does
// not lose resources" means at this layer, since the objects live in the native context.
// Red once by restoring `DestroyEGLContext()` in CreateEGLPbufferSurface: the handle changes.
//
// * (A THIRD CASE - "a share group is a sharing pair, proven at the driver" - was drafted and is
// NOT LANDED. See the S4 section of docs/Disaggregated/design/11-state-ownership.md: the
// sharing argument is wired (CreateNativeContextFor hands the group leader's EGLContext to
// eglCreateContext), but the headless probe that would prove object visibility across the pair
// did not produce a usable control on this box and is left to its own change rather than
// shipped as a green that ran nothing.)
//
// THE KEY OVERRIDE IS HOW A CASE NAMES A TUPLE. This fixture has no SessionRuntime, so the backend's
// resolver answers {0,0} for every thread; ForceNativeContextKeyForTesting is the pure-key override
// that lets a case drive exactly the tuple it means. It is a test-only seam and reaches no
// production path (DirectGLES.h).
//
// NO USABLE EGL IS A SKIP (MGL_EGL_BRING_UP_OR_BAIL), the same rule as every case in this section.
#if !defined(_WIN32)
namespace {
namespace DirectGLES = MobileGL::MG_Backend::DirectGLES;
// A client context token that never collides with the fixture's token-0 tuple.
constexpr Uint64 kTokenA = 101;
constexpr Uint64 kTokenB = 102;
constexpr Uint64 kGroupA = 5001;
constexpr Uint64 kGroupB = 5002;
} // namespace
// The three-native-contexts control: creating contexts must ADD native contexts, never replace one.
TEST(ServerLoopEglTest, CreatingASecondContextLeavesTheFirstNativeContextAlive) {
EglServerFixture fixture;
MGL_EGL_BRING_UP_OR_BAIL(fixture);
const EGLContext default0 = DirectGLES::NativeContextHandleFor(0, 0);
ASSERT_NE(default0, EGL_NO_CONTEXT)
<< "the surface's own creation did not build a native context for the session's default tuple";
ASSERT_TRUE(Server::ServerCreateEGLContext(kTokenA, kGroupA, 0));
ASSERT_TRUE(Server::ServerCreateEGLContext(kTokenB, kGroupB, 0));
const EGLContext contextA = DirectGLES::NativeContextHandleFor(0, kTokenA);
const EGLContext contextB = DirectGLES::NativeContextHandleFor(0, kTokenB);
EXPECT_NE(contextA, EGL_NO_CONTEXT) << "CreateContext built no native context for token A";
EXPECT_NE(contextB, EGL_NO_CONTEXT) << "CreateContext built no native context for token B";
EXPECT_NE(contextA, contextB) << "two client contexts ended up sharing one EGLContext object";
EXPECT_NE(contextA, default0) << "token A reused the default tuple's native context";
EXPECT_NE(contextB, default0) << "token B reused the default tuple's native context";
EXPECT_EQ(DirectGLES::NativeContextHandleFor(0, 0), default0)
<< "creating a context destroyed and rebuilt the default tuple's native context";
EXPECT_EQ(DirectGLES::NativeContextCountForSession(0), 3u)
<< "not all three contexts are live at once - the pre-S4 shape kept exactly one";
fixture.TearDown();
}
// pbuffer -> a second pbuffer -> the same context. The native EGLContext is the tuple's, not the
// surface's, so a surface switch must leave it alone.
TEST(ServerLoopEglTest, CreatingAnotherSurfaceKeepsTheSameNativeContext) {
EglServerFixture fixture;
MGL_EGL_BRING_UP_OR_BAIL(fixture);
const EGLContext before = DirectGLES::NativeContextHandleFor(0, 0);
ASSERT_NE(before, EGL_NO_CONTEXT);
// A SECOND, DIFFERENT surface handle, exactly as a client's second eglCreatePbufferSurface would
// name it (EGLService-style small integers).
const EGLSurface second = reinterpret_cast<EGLSurface>(static_cast<std::uintptr_t>(0x2));
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(second, 32, 32));
EXPECT_EQ(DirectGLES::NativeContextHandleFor(0, 0), before)
<< "creating a second pbuffer surface replaced the native context: every buffer, texture and "
"program the application had built in it went with the old one (the pre-S4 teardown)";
EXPECT_EQ(DirectGLES::NativeContextCountForSession(0), 1u)
<< "a surface switch left a second native context behind";
fixture.TearDown();
}
#endif
int main(int argc, char** argv) { int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first // Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
// write, and caches the FILE*. The name carries this process's pid, because // write, and caches the FILE*. The name carries this process's pid, because
+14 -12
View File
@@ -261,22 +261,24 @@ TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting)
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(), CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(),
::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint); ::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint);
} }
// P11 B2 (CONTRACT-P11 B2): CONTROL REVISION 5. A peer built at revision 4 - before // P11 B2 (CONTRACT-P11 B2) AND P14 S1. CONTROL REVISION 6. A peer built at the revision before
// LinkTerms.adoptTier - carries the fingerprint its revision mixed, and is refused by name // the current one - revision 4 carried no LinkTerms.adoptTier, revision 5 no context-lifecycle
// (Refuse{WireFingerprint}), never read as a revision-5 Hello that happens to ask T2. The same shape // SurfaceOp kinds - carries the fingerprint its revision mixed, and is refused by name
// PAIR's 3 -> 4 had. RED ONCE by leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 4 with the schema // (Refuse{WireFingerprint}), never read as a current-revision Hello that happens to ask T2 or to
// change: the two fingerprints are then equal and the Welcome goes out. // send a SurfaceOpKind the receiver does not know. The same shape PAIR's 3 -> 4 had. RED ONCE by
TEST(SessionHandshakeTest, ARevisionFourPeerIsRefusedByNameAtRevisionFive) { // leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 5 with the schema change: the two fingerprints
static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 5, "B2 bumped the control revision 4 -> 5"); // are then equal and the Welcome goes out.
TEST(SessionHandshakeTest, ARevisionFivePeerIsRefusedByNameAtRevisionSix) {
static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 6, "S1 bumped the control revision 5 -> 6");
auto inputs = CapsAbiFingerprintInputs(); auto inputs = CapsAbiFingerprintInputs();
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
inputs.ControlSchemaRevision = (static_cast<Uint64>(4) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision; inputs.ControlSchemaRevision = (static_cast<Uint64>(5) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision;
#else #else
inputs.ControlSchemaRevision = 4; inputs.ControlSchemaRevision = 5;
#endif #endif
const Uint64 revisionFour = Transport::MixAbiFingerprint(inputs); const Uint64 revisionFive = Transport::MixAbiFingerprint(inputs);
ASSERT_NE(revisionFour, WireFingerprint()); ASSERT_NE(revisionFive, WireFingerprint());
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFour, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect, CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFive, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect,
::MobileGL::Wire::RefuseCode::WireFingerprint); ::MobileGL::Wire::RefuseCode::WireFingerprint);
} }
@@ -197,6 +197,73 @@ TEST(SurfaceControlFrameTest, TheEleventhFramedOpIsInitCapabilitiesAndItCrossesT
EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::None); EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::None);
} }
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The two context-lifecycle ops get
// their own case rather than two rows in the ten-op table above, for the reason InitCapabilities
// has its own: that case's name is a statement about ten, and a name is not traded for another
// (G14). What this one states instead is the pair's whole contract - the values are wire ABI
// (12/13, append-only), they have wire kinds (a spawned client's eglCreateContext has to cross),
// and the identity cast between the two enums covers them.
TEST(SurfaceControlFrameTest, TheContextLifecycleFramedOpsAreCreateContextAndDestroyContext) {
static_assert(static_cast<unsigned>(SurfaceControlOp::CreateContext) == 12u);
static_assert(static_cast<unsigned>(SurfaceControlOp::DestroyContext) == 13u);
EXPECT_EQ(static_cast<unsigned>(::MobileGL::Wire::SurfaceOpKind::CreateContext), 12u)
<< "wire ABI: append-only, and 12 is the next free value. A renumbering here is a "
"protocol break no other case would see";
EXPECT_EQ(static_cast<unsigned>(::MobileGL::Wire::SurfaceOpKind::DestroyContext), 13u);
const SurfaceControlOp wired[] = {SurfaceControlOp::CreateContext,
SurfaceControlOp::DestroyContext};
for (const SurfaceControlOp op : wired) {
::MobileGL::Wire::SurfaceOpKind kind;
ASSERT_TRUE(WireKindForSurfaceControlOp(op, &kind))
<< Server::SurfaceControlOpName(op) << " may never leave the process, so a spawned "
"client's eglCreateContext/eglDestroyContext "
"cannot reach the server";
EXPECT_EQ(static_cast<unsigned>(kind), static_cast<unsigned>(op))
<< Server::SurfaceControlOpName(op) << ": the frame and wire enums drifted apart";
SurfaceControlOp back;
ASSERT_TRUE(SurfaceControlOpForWireKind(kind, &back))
<< "the decode range's upper bound did not move with the append, so the new tag "
"reads as out-of-range - Fatal{ProtocolCorruption}, not the missing row it is";
EXPECT_EQ(back, op);
}
// THE OP'S VALUES REALLY CROSS: the two fields a context creation adds to the frame's own
// four-tuple, past an encode/decode round trip. A field appended to the schema and never
// written by the encoder is exactly the silent disagreement the codec exists to prevent.
SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::CreateContext;
frame.seq = 0x99;
frame.context = 7; // the client context token
frame.shareGroupToken = 3;
frame.contextFlags = 0x5;
flatbuffers::FlatBufferBuilder builder(256);
const ::MobileGL::Wire::SurfaceOp* wireOp = EncodeAndParse(frame, &builder);
ASSERT_NE(wireOp, nullptr) << "a context creation failed to encode";
EXPECT_EQ(wireOp->context(), 7ull);
EXPECT_EQ(wireOp->shareGroupToken(), 3ull);
EXPECT_EQ(wireOp->contextFlags(), 5u);
SurfaceControlFrame back;
ASSERT_EQ(DecodeWireSurfaceOp(*wireOp, &back), SurfaceWireError::None);
EXPECT_EQ(back.kind, SurfaceControlOp::CreateContext);
EXPECT_EQ(back.context, 7ull) << "the client context token must survive the codec";
EXPECT_EQ(back.shareGroupToken, 3ull);
EXPECT_EQ(back.contextFlags, 5u);
// A destroy carries the token and nothing else: the share group is not something a destroy
// may name, and leaving a stale group token in the frame would be a second answer to a
// question only a creation asks.
SurfaceControlFrame destroy;
destroy.kind = SurfaceControlOp::DestroyContext;
destroy.context = 7;
builder.Clear();
const ::MobileGL::Wire::SurfaceOp* wireDestroy = EncodeAndParse(destroy, &builder);
ASSERT_NE(wireDestroy, nullptr);
EXPECT_EQ(wireDestroy->context(), 7ull);
EXPECT_EQ(wireDestroy->shareGroupToken(), 0ull);
EXPECT_EQ(wireDestroy->contextFlags(), 0u);
}
TEST(SurfaceControlFrameTest, WindowBackendAndWireWindowKindMapExplicitlyBothWays) { TEST(SurfaceControlFrameTest, WindowBackendAndWireWindowKindMapExplicitlyBothWays) {
using MG_Backend::WindowBackend; using MG_Backend::WindowBackend;
const struct { const struct {
@@ -241,6 +308,11 @@ TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) {
SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent, SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent,
SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval, SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval,
SurfaceControlOp::ReleaseResources, SurfaceControlOp::SetWindowHandle, SurfaceControlOp::ReleaseResources, SurfaceControlOp::SetWindowHandle,
// P5f cp's append and P14 S1's two. This case's name is a claim about EVERY framed op,
// which is what makes it the one place an append cannot be forgotten: a new kind that
// encodes but does not decode fails here even if the ten-op case above was left alone.
SurfaceControlOp::InitCapabilities,
SurfaceControlOp::CreateContext, SurfaceControlOp::DestroyContext,
}; };
for (const SurfaceControlOp op : wired) { for (const SurfaceControlOp op : wired) {
SurfaceControlFrame frame; SurfaceControlFrame frame;
@@ -277,6 +349,10 @@ TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) {
EXPECT_EQ(back.windowBackend, frame.windowBackend) EXPECT_EQ(back.windowBackend, frame.windowBackend)
<< Server::SurfaceControlOpName(op); << Server::SurfaceControlOpName(op);
EXPECT_EQ(back.nativeToken, frame.nativeToken); EXPECT_EQ(back.nativeToken, frame.nativeToken);
// P14 S1: the two appended fields travel with every op, 0 included - a field that only
// survives on the op that uses it is a field the codec has quietly made op-dependent.
EXPECT_EQ(back.shareGroupToken, frame.shareGroupToken);
EXPECT_EQ(back.contextFlags, frame.contextFlags);
} }
} }
+1
View File
@@ -33,3 +33,4 @@
| 14–15 | 帧节奏与线程;进程、握手、EGL、Android(含上屏 server)、崩溃处理 | [`design/08-runtime-and-platform.md`](design/08-runtime-and-platform.md) | | 14–15 | 帧节奏与线程;进程、握手、EGL、Android(含上屏 server)、崩溃处理 | [`design/08-runtime-and-platform.md`](design/08-runtime-and-platform.md) |
| 16、附 A、附 B | 源码目录与构建选项;全部开关;边界计数器 | [`design/09-build-switches-counters.md`](design/09-build-switches-counters.md) | | 16、附 A、附 B | 源码目录与构建选项;全部开关;边界计数器 | [`design/09-build-switches-counters.md`](design/09-build-switches-counters.md) |
| 17 | 各阶段落地时的形状与仍生效的规则(链到 `notes/`) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) | | 17 | 各阶段落地时的形状与仍生效的规则(链到 `notes/`) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) |
| — | 状态归属:多 session / 多 context / share group(anland 统一 server 前置) | [`design/11-state-ownership.md`](design/11-state-ownership.md) |
+151 -1
View File
@@ -26,6 +26,43 @@
3. **反向事件带 context**。`EventFenceSignaledHead`(`ServerSession.cpp:1324-1338`)、GL error、surface-changed 事件头加 `ContextSlot`(surface 事件已有 surface handle,context 便于 client 直接派发)。fence handle 属 context 的 share group,client 按 (slot, gen) 原样核对。 3. **反向事件带 context**。`EventFenceSignaledHead`(`ServerSession.cpp:1324-1338`)、GL error、surface-changed 事件头加 `ContextSlot`(surface 事件已有 surface handle,context 便于 client 直接派发)。fence handle 属 context 的 share group,client 按 (slot, gen) 原样核对。
4. **wireFingerprint 随以上变更 bump**;两端同 commit + 显式 `-DMOBILEGL_BUILD_STAMP=<commit>` 重建,不接受 drift(交接文档「构建产物」节)。 4. **wireFingerprint 随以上变更 bump**;两端同 commit + 显式 `-DMOBILEGL_BUILD_STAMP=<commit>` 重建,不接受 drift(交接文档「构建产物」节)。
## S1 已落地形状(context 身份上 wire)
上面第 1、2 条是 P14a 的目标形状;S1 只落地了「身份能跨线、server 能归属」这一层,实际形状如下,后续切片按此续建。
**data plane:`bind_context`,opcode 84**(`MG_Pipe/PipeCalls.def`,`kScreen` 族)。
| 项 | 值 |
|---|---|
| payload | `MGPBindContext{ Uint64 ClientContextToken }`(8 B,`MGP_ASSERT_POD`) |
| flags / wait | `kNone` / `kWaitNone`(apply 不读客户端内存,记录的 ring 位置就是它的全部次序) |
| apply 入口 | 无 `MGPipeApply*`;与 `applier_reset`/`object_death` 同为 control record,只到 `WireVerbSink` |
| monolith 生产者 | 无(G1 不变) |
| sink | `ServerVerbSink::OnBindContext`(`MG_Remote/Server/PipeApplier.cpp`) |
**token 位宽:两端都是 `Uint64`。** 记录里是 `ClientContextToken`,控制帧里是 `SurfaceOp.context`;同宽是刻意的,否则必有一端要截断。`0` 在两处都表示「无 context」(release),也正是一个从未 bind 过的 session 已有的答案。token 由 `EGLState::CreateContext` 铸造(`m_nextClientContextToken` 从 1 起、稠密、不复用),随 `ContextObject` 存放,`EGLState::CurrentContextClientToken()` / `GetContextClientToken()` 读出。
**control plane:`SurfaceOpKind.CreateContext = 12` / `DestroyContext = 13`**(`protocol.fbs`),不是两个新的 CtrlMsg——context 的创建/销毁/make-current 与 surface 是同一个调用者、同一个线程、同一条阻塞 frame channel,另开一条只会多一个次序会打架的地方。`SurfaceOp` 追加 `shareGroupToken: ulong` 与 `contextFlags: uint`(保留扩展位,今天恒 0)。`MOBILEGL_PROTOCOL_CONTROL_REVISION` 5 → 6,`scripts/ci/protocol_revision_pins.json` 同步加行。
- `CreateContext`:`context` = 新 context 的 token,`shareGroupToken` = 它并入的 share group(`shareCtx` 的族,或新建),`contextFlags` = 保留位。`display`/`surface` 为 0(server 的 display 就是 session 自己的)。
- `DestroyContext`:`context` = 要销的 token;server 销表并在它当前 bind 时解绑。
- 两者都用既有的 `SurfaceReply` 回答(ok + refusal),S1 建的是占位 runtime,没有别的可回。
**`bind_context` 与 MakeCurrent 控制帧的关系:不合并,各管一件事。**
- `MakeCurrent` 控制帧仍是 **native tuple 的 op**:它让 server 的 apply thread 决定要不要真的 `eglMakeCurrent`,并参与 `m_haveCurrentTuple` 的去重(ID-54/ID-67 的口径一个字没动)。它的 `context` 字段与 `bind_context` 的 token 是同一个值,但那是「同一个身份」,不是「同一条通路」。
- `bind_context` 是 **ring 上的归属记录**:它告诉 server「这条 ring 上从今往后的记录属于哪个 context」。它必须在 ring 上,因为 GL 记录就在 ring 上,而控制帧与 ring 之间没有全局次序可依赖。
- 发射点只有一个:`EGLImpl::MakeCurrent` 在切换成功之后、返回之前调 `EmitContextBinding`(`MG_Impl/EGLImpl/EGLImpl.cpp`)。这给出「先于后续 GL 记录」的构造性保证——app 线程在 `eglMakeCurrent` 返回前不可能发出新 context 的任何记录,而 ring 是 FIFO。token 未变则不发(identical make-current 是 EGL 合法 no-op)。
- `DestroyContext` 帧在客户端 `EGLState::DestroyContext` 接受之后才发,所以 EGL 自己拒掉的那种销毁(context 仍被某线程 current)不会到达 server,两端不会因为「token 悄悄消失」而对不上。
**与第 1 条的取舍差异(明确记录)**:S1 走的是「在边沿宣告身份」而不是「在 71 个 context 族记录的 header 与 payload 之间插 `Uint32 ContextSlot`」。代价是每个 make-current 一条记录(而不是每条记录 4 B),收益是 payload 布局与 `MGPWireRec_*` 尺寸全部不动、`kRecHasContext` 位与 batch 边界的生成器改动全部不必;单 context 的既有形状(token 0 / 从未 bind 过)零改动即兼容。多线程交错下的逐记录身份若将来确有需要,再按第 1 条补 slot——那时两者可以并存,slot 为 0 仍读当前 token。
**server 端**(`MG_Remote/Server/ServerSession.{h,cpp}`):每 session 一张 `{token → ContextRuntime}`(`ContextRuntime{ Token, ShareGroupToken, Flags }`,S1 里是占位,S4 给它装 native context)加一个当前 token(atomic,0 = 无)。`CreateContext`/`DestroyContext` 控制帧经 `ServerLoop::ApplySurfaceControlFrame` 的 `CreateContext`/`DestroyContext` 两个 arm 建表/销表;`bind_context` 经 codec 到 `ServerVerbSink::OnBindContext` 切当前 token。写入全部发生在 apply thread,表因此不需要锁。`bind_context` 指向一个本 session 从未创建的 token 是 `Fatal{ProtocolCorruption, "BindContext.ClientContextToken"}`(0 例外,永远合法);表的存在正是为了让这条判定有依据。
**S1 的 token 只有写入者,没有读者**:server 端目前没有任何 apply 分支读 `CurrentContextToken()`(`grep` 可证:除 `ServerSession` 自己的访问器外无人调用),所以整条渲染路径的行为与 S1 之前逐字相同——这一层先把「归属」记下来,读它做事是 S4(native context)和 P14b/c 的事。没有 `bind_context` 的 session 行为同样不变:`ServerLoopTest` 的 MakeCurrent/epoch/liveness 用例(`MG_Test/Wire/ServerLoopTest.cpp:1780-1941`)仍全部通过,它们走的正是这条路径。
**S1 的验收证据**(本次提交的测试):`PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` 钉 opcode 84 与 `kNone`/`kScreen`/`kWaitNone`;`SurfaceControlFrameTest.TheContextLifecycleFramedOpsAreCreateContextAndDestroyContext` 钉 12/13 的值与 `shareGroupToken`/`contextFlags` 的往返;`SessionHandshakeTest.ARevisionFivePeerIsRefusedByNameAtRevisionSix` 钉 revision bump;`FuzzArm2/PeerLatchSite/.../BindContextUnknownToken` 是 data plane 的端到端控制——真实 `--serve` supervisor 上,一个伪造的、指向本 session 从未创建过的 token 的 `bind_context` 让 session 按名 latch(`exit=75`),且同一 supervisor 的下一个 session 正常渲染。
## 前端(MG_State / MG_Impl) ## 前端(MG_State / MG_Impl)
1. **拆 GLContext**。`MG_State/GLState/Core.h:77-698` 的 `GLContext` 拆成 `ShareGroupState`(buffer/texture/renderbuffer/sampler/shader/program/sync 的对象 store + 名称分配)与 `GLContext`(bindings、error、viewport/scissor、render state、VAO/FBO/TF/pipeline/query store、texture unit 绑定、current program)。容器对象(VAO/FBO/TF/pipeline/query)**不共享**,留在 per-context;这是 GL 4.6 §5 的口径,不要为省事把 FBO 放进 share group。 1. **拆 GLContext**。`MG_State/GLState/Core.h:77-698` 的 `GLContext` 拆成 `ShareGroupState`(buffer/texture/renderbuffer/sampler/shader/program/sync 的对象 store + 名称分配)与 `GLContext`(bindings、error、viewport/scissor、render state、VAO/FBO/TF/pipeline/query store、texture unit 绑定、current program)。容器对象(VAO/FBO/TF/pipeline/query)**不共享**,留在 per-context;这是 GL 4.6 §5 的口径,不要为省事把 FBO 放进 share group。
@@ -33,14 +70,64 @@
3. **Tracker 判据改显式身份**。`Pipe/Tracker.h:306-309` 的指针比较换成 (session, contextSlot);换 context 的首个 verb 重推完整状态的语义不变。`MGPipeTrackerInstance()`(`Tracker.h:956-963`)从单例改为 per-context(tracker 缓存的是 per-context 推送快门)。 3. **Tracker 判据改显式身份**。`Pipe/Tracker.h:306-309` 的指针比较换成 (session, contextSlot);换 context 的首个 verb 重推完整状态的语义不变。`MGPipeTrackerInstance()`(`Tracker.h:956-963`)从单例改为 per-context(tracker 缓存的是 per-context 推送快门)。
4. **eglMakeCurrent 流程**:`EGLImpl::MakeCurrent` 除调 `backendObject->MakeEGLCurrent` 外,先切 TLS GLContext 再发 `SurfaceOp::MakeCurrent{contextSlot}`;client 线程后续的 context 族记录都带这个 slot。 4. **eglMakeCurrent 流程**:`EGLImpl::MakeCurrent` 除调 `backendObject->MakeEGLCurrent` 外,先切 TLS GLContext 再发 `SurfaceOp::MakeCurrent{contextSlot}`;client 线程后续的 context 族记录都带这个 slot。
## S3 已落地形状(前端 per-context / share group)
上面「前端」一节是目标形状;S3 落地了第 1、2 条(第 3、4 条的 tracker/slot 部分未做),实际形状如下。
**`ShareGroupState`(`MG_State/GLState/ShareGroupState.{h,cpp}`,新)** 持有一个 share group 的全部对象注册表与名字生成器:
| 共享(ShareGroupState) | 每 context(GLContext) |
|---|---|
| buffer / texture / renderbuffer 的对象表与 `IndexGenerator`;sampler 对象表(`SamplerState` 整体);program+shader 表与共用名字生成器(`ProgramState` 整体);`token`(S1 的 `ShareGroupToken`) | error 列表;buffer binding slot 与 indexed binding point、touched 高水位、各 aggregate generation;texture unit / image unit 数组、active unit、bind/sampling generation、per-target 默认纹理(name 0);renderbuffer binding slot;VAO/FBO/TF/pipeline/query;`current program` |
`BufferState`/`TextureState`/`RenderbufferState` 因此只剩 binding 侧:删除走「先查 share group 的对象、再解本 context 的绑定、最后 free 名字」,即删对象是全局的、解绑定是逐 context 的(GL 4.6 core 6.1/8.1)。`ProgramState` 的 `UseProgram`/`glDeleteProgram`/`ReleaseShaderNameIfOrphaned` 现在把调用方的 current program 当参数传入,current program 本身落在 `GLContext`。`GLContext` 由 `GLContext(SharedPtr<ShareGroupState>)` 构造,`GLContext()` 建一个只属于它的 share group(测试与进程默认 context 用)。
**`pGLContext` 改成 TLS**:`extern thread_local SharedPtr<GLState::GLContext> pGLContext`(`GLState/Core.h`)。`EGLState::MakeCurrent` 成功后把它指向该 context 的 `GLContext`,`ReleaseThread`/`eglTerminate`/释放路径恢复线程进入 EGL 之前的那个值(`ThreadCurrentState::RestoreThreadGLState`)——所以「一个 context 同时只被一个线程拥有」的既有语义(`m_contextOwners`,EGL_BAD_ACCESS)一个字没动,owner 也不进 TLS:context 跨线程移交后状态仍在它自己的 `GLContext` 里。
没有 EGL context 的线程落到**进程默认 context**(`ProcessDefaultGLContext()`,`MG_State::Init()` 建,构造时 `pGLContext` 的初值就是它)——这正是拆分前「全局唯一 context」的语义,所以单 context 应用与所有直接操作 `pGLContext` 的既有测试行为不变。
**每个 `eglCreateContext` 都建独立 `GLContext`**(`ContextObject::GLStateObject`);`shareCtx != EGL_NO_CONTEXT` 时 `GLContext` 挂到对方 `ShareGroupState`,否则新建一个并把 S1 铸的 `ShareGroupToken` 记在里面。这里不需要「迁移对方 store」:group 在第一个 context 诞生时就存在,后来者直接并入。`eglDestroyContext` 丢掉 `ContextObject`,共享指针归零才销毁 `GLContext` / `ShareGroupState`。
**默认帧缓冲(明确的偏差)**:GL framebuffer 0 今天仍是**进程一个对象**(`MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo`,后端按它判断「这是默认 FBO」)。它在 `MG_Impl::Init()` 建好之后经 `MG_State::SetDefaultFramebuffer` 交给 MG_State,此后每个新建 `GLContext` 构造时把它装成自己的 name 0 并绑到 Draw/Read(`GLContext::InstallDefaultFramebuffer`)。也就是说:每个 context 有**自己的 FramebufferState 表项与绑定**(`glBindFramebuffer` 互不影响),但那个对象暂时是共用的。真正的 per-context 默认帧缓冲属于「后端 per-context native tuple」那一期(见「后端」§1),在那之前若各 context 各建一份,后端的 `pDefaultFramebufferInfo` 身份判定会对第二个 context 失效。
**monolith 与 split 走同一份结构**:两端共用一个 `MG_State`,`eglCreateContext`/`eglMakeCurrent`/`eglDestroyContext` 都先落这套前端状态,`MOBILEGL_PIPE_PUSH=0` 与 `=1` 的区别只是后端从哪儿读状态(`MGB_CTX` 是 `pGLContext` 还是 `gPipeInputs`)。`MGB_CTX_IDENTITY` 因此天然按 context 变化,post-S3 的 tracker 只需照旧比较(改成显式 slot 是后续切片)。
**验收证据**(本次):`MG_Test/State/ContextStateOwnershipTest.cpp` 六个用例——不共享的两个 context 名字空间独立(同名不同对象、绑定不互见);共享对看到同一对象而各自持有绑定;`eglMakeCurrent` 切换后 error/viewport/FBO binding 各自独立且切回来还在;`eglDestroyContext` 后另一个 context 对象与绑定不受影响;释放后线程回到进入 EGL 前的 GL 状态;无 current context 的线程落到进程默认 context。
## server(MG_Remote/Server) ## server(MG_Remote/Server)
1. **session registry 取代单例**。`ServerSessionInstance`/`ServerLoopInstance` 改为 `ServerProcess` 持有的 session 表(slot → {transport, ServerSession, ServerLoop, PipeApplier, apply thread})。in-process server(`ServerMain.cpp:1593-1651` 的 `mobilegl_server_serve_inprocess`)从「单 active + RefuseBusy」改为接受至上限(默认 8,可配);`--serve` 的 unix 路径同样改为同进程多 session,**移除 fork-worker 分发**(`ServerMain.cpp:1566-1584`),`--max-sessions` 语义改为同进程上限。TCP 的 `TcpSupervisor` 单 active(`:1220-1223`)同样让位。 1. **session registry 取代单例**。`ServerSessionInstance`/`ServerLoopInstance` 改为 `ServerProcess` 持有的 session 表(slot → {transport, ServerSession, ServerLoop, PipeApplier, apply thread})。in-process server(`ServerMain.cpp:1593-1651` 的 `mobilegl_server_serve_inprocess`)从「单 active + RefuseBusy」改为接受至上限(默认 8,可配);`--serve` 的 unix 路径同样改为同进程多 session,**移除 fork-worker 分发**(`ServerMain.cpp:1566-1584`),`--max-sessions` 语义改为同进程上限。TCP 的 `TcpSupervisor` 单 active(`:1220-1223`)同样让位。
2. **apply thread per session**。每 session 一个 apply thread;`g_applyThreadKey`(`ServerLoop.h:129`)改为 per-session 成员,`OnApplyThread()` 语义不变(同 thread 比较)。session 之间顺序调度由线程天然完成,不需要全局锁;native driver 本来就支持多线程各自 current 不同 context。 2. **apply thread per session**。每 session 一个 apply thread;`g_applyThreadKey`(`ServerLoop.h:129`)改为 per-session 成员,`OnApplyThread()` 语义不变(同 thread 比较)。session 之间顺序调度由线程天然完成,不需要全局锁;native driver 本来就支持多线程各自 current 不同 context。
3. **g_applier per session**。`PipeApply.cpp:484` 的进程级 `MGPipeApplierState` 改为 session 持有;`MGPipeApplierReset` 的 role guard(`PipeApply.cpp:1474-1481`)按本 session 的 apply thread 判定。`PipeApply.h:558-617` 已有的 share-group 对象记录 / working-state 二分是地基:对象记录表按 share group 分桶,working state 按 context 分桶,reset 只清本 context 的 working state。 3. **g_applier per session**。`PipeApply.cpp:484` 的进程级 `MGPipeApplierState` 改为 session 持有;`MGPipeApplierReset` 的 role guard(`PipeApply.cpp:1474-1481`)按本 session 的 apply thread 判定。`PipeApply.h:558-617` 已有的 share-group 对象记录 / working-state 二分是地基:对象记录表按 share group 分桶,working state 按 context 分桶,reset 只清本 context 的 working state。**(S5 已落地,形状见下面「S5 已落地形状」:这一条最终不是「per session 一个 applier」,而是 per {session, context} 一个 applier、其对象记录挂到 {session, shareGroup} 的组上。)**
4. **反向路由按 session + context**。`ReverseCallbackOwner`(`ServerSession.cpp:408-418`)的 `Active()` 单活性改为「按发起线程/显式 session 参数解析」;事件头加 `ContextSlot`(见 wire 3)。`gMGPipeCallbacks`(`MGPipeCallbacks.h:77`)从进程级变量改为 per-session 表,Accept 时挂到 session 而非全局。 4. **反向路由按 session + context**。`ReverseCallbackOwner`(`ServerSession.cpp:408-418`)的 `Active()` 单活性改为「按发起线程/显式 session 参数解析」;事件头加 `ContextSlot`(见 wire 3)。`gMGPipeCallbacks`(`MGPipeCallbacks.h:77`)从进程级变量改为 per-session 表,Accept 时挂到 session 而非全局。
5. **session 模式闩保留在 session 层**。`SessionSurfaceMode`/`SurfaceModeMismatch`(`ServerLoop.h:199-206`、`ServerLoop.cpp:1539-1544`)口径不变:compositor session 闩 OnScreen、app session 闩 Offscreen;统一 server 同时持有两种 session 是合法形状,不许把闩提升到进程级。 5. **session 模式闩保留在 session 层**。`SessionSurfaceMode`/`SurfaceModeMismatch`(`ServerLoop.h:199-206`、`ServerLoop.cpp:1539-1544`)口径不变:compositor session 闩 OnScreen、app session 闩 Offscreen;统一 server 同时持有两种 session 是合法形状,不许把闩提升到进程级。
### S2 已落地形状(同进程多 session)
上面第 1、2、4、5 条是 P14c 的目标形状;本切片落地的是「归属拆开、路由按线程」这一层,实际形状如下。
**每 session 一个 runtime,singleton 变成 fallback**(`MG_Remote/Server/SessionRuntime.h`)。`SessionRuntime` 持有该 session 的 `ServerSession`、`ServerLoop`、`PipeInputs` 块,加一个 latch domain 与一个 segment resolver 绑定;`ThreadSessionScope` 把它挂到 **该 session 的两个线程**上——控制线程(`ServerMain::RunSession` 自己的线程,supervisor 里每 session 一条 `std::thread`)与 apply 线程(`mgl-srv-apply`,`ApplyThreadMain` 第一件事就是开同一个 scope)。`ServerSessionInstance()` / `ServerLoopInstance()` / `ServerSession::Active()` 因此先问「本线程属于哪个 session」,只有**不属于任何 session 的线程**才拿进程级单例。这一条让 ~900 个 `gPipeInputs` / `MGB_CTX` / `ServerSession::Active()` 调用点一行不改就有正确归属。inproc 的「同进程 client+server」形状刻意不开 scope(`ClientSession` 直接调单例),所以那个形状的 verb barrier、单块 PipeInputs 与逐字行为完全不变。
**gPipeInputs 是每线程解析的引用**(`MG_Backend/MGPipe/PipeInputs.h`)。`gPipeInputs` 从「一个进程级对象」变成 `thread_local` 引用,经 `g_pipeInputsThreadResolver` 这个由 server 层装一次的探针解析到本线程 session 的块,无 session 时落到 `gPipeInputsDefault`(即原对象)。**为什么是这个形状**:`MGB_CTX` 是 `(&gPipeInputs)`,要让 ~380 个点位不动而目标随线程变,只有「目标随线程变的引用」可行。代价是每个访问点一次 TLS 地址计算(不是每个字段,且无 guard:每线程第一次使用时绑定一次),这是本切片的已知成本。
**apply 线程身份改成 TLS 一位,进程级那个字改成计数。** `OnApplyThread()` 的答案现在只可能是「本线程是不是 apply 线程」,所以它是 `thread_local Bool`(零初始化,无 guard、无原子);`Detail::g_applyThreadKey` 保留原名与类型但装的是**本进程活着的 apply 线程数**——`MG_Backend/DirectGLES/Managers.cpp`(本切片文件集之外)读这个 `!= 0` 问「有没有 apply 线程在跑」,计数正好回答那个问题,热路径没有人再读它。
**latch 是每 session 一个 domain**(`MG_Remote/FatalFunnel.*`)。原来的四个进程级静态变量变成按 domain 存的 `LatchState`,线程绑定一个 domain(`ThreadSessionScope`),不绑定就是 domain 0(进程默认 = 老行为)。理由不是整洁而是正确性:`SessionLatched()` 的三个读者都在 session 自己的线程上(apply 循环、`DrainRing`、`RunSession` 的控制循环),一个进程共用一个 latch 时 session A 的坏字节能停掉 B 的 apply 线程;`ResetSessionLatch()` 也有镜像问题(一个 session 的 reap 会清掉活着的那个的 latch)。现在 A 的关闭/故障都到不了 B。
**segment resolver 是每线程的表 + 进程级的 hook**(`MG_Remote/Wire/PipeWireCodec.*`)。`gMGPipeSegmentResolver` 仍是一个进程级函数指针(装一次,不再置空),但 thunk 先查**调用线程绑定的表**,其次才是进程级 fallback。同一线程上出现第二张表仍然是 `Fatal{...already installed...}`(既有 `APartiallyZeroUploadBoxIsFatalRatherThanASilentRace` 与 `ASecondProcessResolverIsFatalRatherThanASilentRace` 的口径不变);不同线程上的第二个 session 不再是错误。
**全局回调表:装一次、按线程路由、最后一个 session 关才卸。** `gMGPipeCallbacks` 的五个条目仍是进程级函数指针(每个 session 装的是同一批函数),`Close()` 只有在 `ServerSessionCount()` 归零时才清;`g_sessionOwner` 的 CAS 抢占对**有 runtime 的 session 不再生效**(共表是合法的),只对「进程就是一个 session」的形状(inproc client+server 角色、`ServerLoopTest` 的 `FvSession` 及其对等对象)保留原样的 `Fatal{RoleViolation,"callback-double-install"}`。
**surface mode 仍是 D4 的 per-session 闩**(`ServerLoop::m_surfaceMode`)。本切片一度引入 `SetDeclaredSurfaceMode`,让 `RunSession` 按**进程自己的** `MOBILEGL_IPC_SURFACE` 给 session 声明模式——**已撤掉**,两条理由:(a) 进程级的环境变量说不了统一 server 同时持有的多个 session(§4 第 5 条要的是口径不变、闩不许提升到进程级),(b) 它对 dial 进来的 client 是错的——pre-started server 不继承 client 的 env(`ServerOwnedSurfaceTest` 的 TCP 形状),于是 session 被声明成 offscreen,ServerOwned 的建面先拿 `SurfaceModeMismatch`,永远走不到「本 server 没有 display」那条具名拒绝。**而且它不需要**:`m_surfaceMode` 是 `ServerLoop` 的成员,每个 session 有自己的 loop,两个 session 各自闩各自的模式——「每 session 独立 surface mode」正是这个形状,不需要任何声明。真正想在第一个 surface 之前声明模式的形状(P14d 的 KWin on-screen + Konsole offscreen 同一个 server)必须**让模式上 wire**(Hello 或一条 SurfaceOp),那是 P14d 的账;在那之前,谁先建 surface 谁闩定,是每个 session 各自的答案。**`ServerDisplay` 的 lease 天然多路复用**:holder 是 loop 指针,`AcquireFor` 只会把窗口租给一个 session,第二个 on-screen session 拿到的是**具名拒绝** `NoServerWindow`(日志里点名 LeasedElsewhere),不再是注释里那句「sessions are sequential: a bug if seen」;offscreen session 完全不取 lease,可以任意多。
**接入层**:`UnixInProcessSupervisor`(`mobilegl_server_serve_inprocess` 的 unix 分支)从「一次一个 + 第二个 Refuse{Busy} one session is already active」改成**并发至多 `MOBILEGL_IPC_INPROC_MAX_SESSIONS`(默认 16)**,每个 session 一条控制线程,超限时用 fork 路径同一句话 `Refuse{Busy} the session limit is reached`;embedded ABI 的签名不变。**没动的三处**:`--serve --max-sessions` 的 fork worker 路径(进程隔离仍是可选形态,`UnixSupervisor.ConcurrentSharedMemoryClientsHaveSeparateWorkersAndAnExitedSlotIsReused` 与 `...RequiresAnExplicitLimit` 照旧)、TCP 的 in-process 形状(仍是一 session 一次,`InProcessServer.AnAuthenticatedSecondHelloWhileASessionIsLiveIsRefusedBusy` 照旧)、`TcpSupervisor` 的 fork 形状。与本文 §4 第 1 条写的「默认 8」差一个数:本切片取 16,且上限是环境变量而非新 ABI 参数。
**验收证据**(本切片新增):`MG_Test/Wire/MultiSessionTest.cpp` 三个用例——`MultiSessionFixture.TwoShmSessionsInOneProcessAreServedAtOnce`(同进程两个 SHM session 并发:两个 Welcome 同 pid、日志里两条 `in-process unix session #N started`、各自 clear 各自读回自己的颜色)、`MultiSessionFixture.SameNamesInTwoSessionsDoNotCollide`(同一 client context token 与同一 render state handle 在两个 session 里都被接受——名字是 session 内的名字)、`MultiSessionFixture.ClosingOneSessionLeavesTheOtherServing`(A 关掉并 reap 之后 B 继续 apply 三条记录,进程级 resolver/回调表没被 A 卸掉)。既有套件不回归:本切片点名的 `InProcessServerTest`、`ServerDisplayTest`、`UnixSupervisor`、`SupervisorChildren`、`ServerLoopTest`/`ServerLoopEglTest`/`ServerLoopLatchTest`、`EmbeddedServerTest`、`PeerLatchTest`(含 `PeerLatch.SiteMap`,其 FatalFunnel 的 check 正则已跟着 domain 改写更新)、`ServerOwnedSurfaceTest`、`SessionHandshakeTest`、`SurfaceControlFrameTest` 全绿(host ctest 里剩下的失败只有 `3rdparty/glslang` 那个 dirty submodule 造成的编译/链接期基线失败,与本切片无关)。
**修正(`HealthySessionWithColor` 的颜色解包)**:本切片把 `P12ServerRig.h` 的绿色 clear 参数化时,一度把 `rgba` 按 `A<<24|B<<16|G<<8|R` 的逆序解包(把最高字节当 R),于是 `0xFF00FF00`(绿)被清成 `0x00FF00FF`(品红),`InProcessServer.SequentialSessions...` 的读回断言因此挂掉。正确的解包就是读回自己的字节序——`PeerReport::pixel` 是 GL_RGBA/GL_UNSIGNED_BYTE 三元组的 little-endian `Uint32`,故 `R = rgba & 0xFF … A = rgba >> 24`。**生产路径(clear → read_pixels)一个字节没动**:`PeerLatchTest` 用同一绿色的字面量 `{0,1,0,1}` 且断言同一个 `0xFF00FF00`,它的对应用例全程是绿的,就是这条结论的控制。红色 `0xFF0000FF` 在字节反转下是回文,所以当时只有绿色 session 失败——这不是两个 bug。
**本切片明确没做(下一层的账)**:DirectGLES 的 `g_Display/g_Config/g_Context/g_Surface` 与六个 twin registry 仍是进程级(P14b 的 per-context native tuple),所以两个并发 session 的**原生上下文并不隔离**:一个 session 的 teardown 仍会拆掉进程的 EGL context,本切片的渲染验收因此只在两个 session 都活着时做,关掉一个之后的证据是记录级的(记录照常 apply 并有回包)。`MGPipeServerSetContextLive` / `MGPipeServerSetOwnedWindow` 与 `PipeInputs.cpp` 同属进程级,不在本切片的文件范围内。`MG_Util::PipeStats::Init/Shutdown` 仍是进程级,多 session 下是遥测噪声不是正确性。`SessionFail`(未武装时的真 Fatal)仍会带走整个进程——那是 `--serve` fork 形状存在的理由。
## 后端(DirectGLES) ## 后端(DirectGLES)
1. **native tuple per context**。`g_Display` 保留进程级(native display 一次 initialize 合理);`g_Context`/`g_Surface`/`g_Config`(`DirectGLES.cpp:16183-16186`)改为 per-(session, contextSlot) 表,native context 按 share group 用 `eglCreateContext(share=…)` 建。 1. **native tuple per context**。`g_Display` 保留进程级(native display 一次 initialize 合理);`g_Context`/`g_Surface`/`g_Config`(`DirectGLES.cpp:16183-16186`)改为 per-(session, contextSlot) 表,native context 按 share group 用 `eglCreateContext(share=…)` 建。
@@ -48,6 +135,69 @@
3. **Managers 按 share group / context 分表**。六个 twin registry(`Managers.cpp:6895`、`:11035`、`:12835`、`:13188`、`:16255`、`:16580`)按 share group 实例化;`g_activeTextureUnit`/`g_boundTexturesCache`/sampler cache(`Managers.cpp:11031-11032` 等)按 context 实例化;`DropEveryTwinForEndedServerSession`(`Managers.cpp:425-443`)只清本 session。`g_backendContextGeneration` 改为 per-context 世代。 3. **Managers 按 share group / context 分表**。六个 twin registry(`Managers.cpp:6895`、`:11035`、`:12835`、`:13188`、`:16255`、`:16580`)按 share group 实例化;`g_activeTextureUnit`/`g_boundTexturesCache`/sampler cache(`Managers.cpp:11031-11032` 等)按 context 实例化;`DropEveryTwinForEndedServerSession`(`Managers.cpp:425-443`)只清本 session。`g_backendContextGeneration` 改为 per-context 世代。
4. **DirectVulkan 同步审视**。Magma 的 per-device/per-queue 结构也要按 session 分;本阶段先保证不编译退化,多 session 下 Magma 的验收单列。 4. **DirectVulkan 同步审视**。Magma 的 per-device/per-queue 结构也要按 session 分;本阶段先保证不编译退化,多 session 下 Magma 的验收单列。
### S4 已落地形状(每 context 原生 tuple + share-group 键控)
上面第 1、2 条是 P14b 的目标形状;S4 落地了「native tuple 按 context 归属、创建第二个 context/surface 不再拆 context、share group 进原生」这一层,实际形状如下。
**native tuple 按 (session, token) 键控**(`MG_Backend/DirectGLES/DirectGLES.cpp` 的 `NativeContextTuple` / `NativeSessionState` 注册表)。`g_Display` 仍是进程级一份、`eglInitialize` 一次,只有最后一个原生 context 走了才 `eglTerminate`(`g_displayUsers` 计数)。`EGLContext`/`EGLConfig` 随 tuple;**原生 surface 按 session 一份**(EGL 里 surface 不属于 context,只有 draw/read 绑定属于),tuple 记自己当前画到哪个 surface。键由 server 层装的探针回答(`SetNativeContextKeyResolver`,`MG_Remote/Server/SessionRuntime.cpp` 经 `ThreadSessionScope` 装一次):答案是**本线程的 session 身份 + `ServerSession::CurrentContextToken()`**;没有探针的线程(monolith、纯前端进程、unit case、inproc 单进程形状)答案恒为 `{0,0}`——这正是被替换掉的进程单例,所以单 context 世界逐字不变。
**创建不再拆 context**。`InitDisplayAndContext()`(首句无条件 `DestroyEGLContext()`)换成 `EnsureNativeContext(tuple, surfaceBit, window)`:tuple 已有原生 context 就复用,只按需要选 config;没有才建。`BackendObject_DirectGLES::CreateEGL{Window,Pbuffer}Surface` 里的 `DestroyEGLContext()+ResetEGLRuntimeState()` 同样去掉 teardown,只留 base class 的 per-surface bookkeeping。因此 pbuffer→window→pbuffer 只是往 session 的 surface 集合里加/换,tuple 的 `EGLContext` 一字不动——对象(后端 twin 直接活在原生 context 里)自然不丢。`DestroyEGLContext()` 现在是**本 session** 的全量 teardown(`ReleaseEGLResources`/`~BackendObject_DirectGLES` 的语义),单个 context 的销毁是新的 `DestroyNativeContextFor(token)`。
**share group 进原生**(`CreateNativeContextFor(token, shareGroup)`)。`CreateContext` 控制帧到达时,server 为该 token 建原生 context:**同 session 内、同一 shareGroupToken 且已有活 context 的 tuple 的 `EGLContext` 作为 `eglCreateContext` 的 share 参数**,否则 `EGL_NO_CONTEXT`。组内后续 context 因此共享原生对象命名空间。建失败则回滚该 session 表项并把 `ok=false` 回给客户端。
**bind_context 是原生切换点**。MakeCurrent 控制帧到的时刻 `CurrentContextToken()` 还是旧值(S1 刻意让客户端在 make-current 成功后才发绑定记录),所以在那里切原生会绑到刚离开的 context。`ServerVerbSink::OnBindContext`(`MG_Remote/Server/PipeApplier.cpp`)在 `session->BindContext(token)` 之后调 `DirectGLES::MakeNativeContextCurrentForBoundToken()`:按新 token 解析 tuple,与 `t_boundNativeContext` 相同就返回(单 context 的每次绑定零成本),不同才做原生 `eglMakeCurrent`。
**验收证据**(本次新增,`MG_Test/Wire/ServerLoopTest.cpp`,真实 headless EGL):`ServerLoopEglTest.CreatingASecondContextLeavesTheFirstNativeContextAlive`(同一 session 三个 context token,三个原生 EGLContext 同时活着且互不相同——还原 S4 前的无条件 teardown 时只剩一个);`ServerLoopEglTest.CreatingAnotherSurfaceKeepsTheSameNativeContext`(建第二个 pbuffer 后 tuple 的 `EGLContext` 句柄不变、原生 context 数仍为 1)。既有集合零回归:本切片点名的 97 个用例全绿(86 基线 + 11 `ServerLoopEglTest` + 上面两个)。
**本切片明确没做(下一层的账)**:
- **`g_applier` 没有改成 {shareGroup → applier}**(上面 server 第 3 条)。原因不是时间,是**形状**:`MGPipeApplierState` 把「share group 的对象记录」和「每 context 的 working state」混在一个结构里(`PipeApply.h:515-933`,其自身注释第 101 行已指出该二分),照字面把它按 share group 键控会让**同一 share group 的两个 context 共用 working state**——bindings、current program、unit 集互相覆盖,比不改更错。正确的做法是先按该二分拆结构(对象表进 share group、working state 进 context),那是独立的一刀。今天 `g_applier` 与 `gMGPipeApplierReset` 逐字未动。**(S5 已做,见下节。)**
- **六个 twin registry 与 twin 键 `{shareGroup, slot, gen}` 未改**(`Managers.cpp`)。twin 仍按引用活着的后端 context 复用,多 context 下的正确性依赖「一个原生 context 一个 twin 世代」,这在两个 context 都活着时成立(本次测到的),在跨 context 迁移 twin 的路径上还没有证据。**(S5 仍未做,见 S5 的未做项。)**
- **share group 的驱动级证明未落地**。`eglCreateContext(share=…)` 已接线,但用 headless EGL 直接问 `glIsBuffer` 的探针在本机没有得到可用的控制(同 context 内 `glGenBuffers` 后 `glIsBuffer` 即返回 false),没有作为「跑了个空的绿」收进来。要证到驱动层,需要先弄清该探针,再补一个用例。**(S5 也没弄清:见 S5 的未做项与「为什么没做」。)**
- **Wire 第 1 条的逐记录 `ContextSlot` 未做**,仍是 S1 的边沿宣告 + `bind_context`。
- **`g_backendContextGeneration`/`g_syncContextGeneration` 仍是进程级**:销毁单个 context 会推进全局世代,其他 context 的旧 fence/query 句柄因此读作「已 signal」。安全方向(最坏是多等),但不是每 context 世代,属 P14b/c。
- **ServerLoop 的 MakeCurrent 去重仍是「当前绑定的那一个 tuple」**,不是 per-context 表。理由写在 `ServerLoop.h:560-573`:ID-67 的钉死用例(A→B→A 必须三次原生绑定)定义「不同 tuple」为**当前绑定**的变化,而 EGL 里一个线程同时只有一个 current context,回到 A 确实必须重跑那七条前端 context 失效。「每 context 持有自己的 native triple」由后端的 {session, token} 注册表满足,去重这一层的单位仍是线程的当前绑定。
### S5 已落地形状(applier 按 {session, context} / {session, shareGroup} 键控)
S4 留下的最后一块:`PipeApply.cpp` 的进程级 `g_applier` 现在是一个**每 (session, context) 的注册表**,它的对象记录按 {session, share group} 分桶。
**结构拆分(`MG_Pipe/PipeApply.h`)**。原来一个 `MGPipeApplierState` 按两个寿命切成两个结构:
| `MGPipeObjectRecords`(share group) | `MGPipeWorkingState`(context) |
|---|---|
| `Resources`、`VertexElementsCsos`、`TextureResources`、`RenderbufferResources`、`SamplerCsos`、`SamplerViewCsos`、`ShaderCsos`、`CompositeShaderCsos`、`FramebufferRecords` | `RenderStateCsos`、`BoundRenderStateCso`、`Residual`、`ScatteredChunkBits`、四个 wire 计数器与四个 refusal 计数器、`BoundVertexElements`/`VertexBuffers*`/`IndexBuffer*`、`MapPersistentRoundtrips`、`BoundFramebuffer`/`FramebufferSerial`/`StaleFramebufferRecordLookups`、三个 unit 窗口、三个 program handle、三个 binding-point 窗口、`IsTransformFeedbackActive`、`TextureShutterSerial`/`ContextSerial`、`#if DISAGGREGATED` 的 verb handle 组 |
`RenderStateCsos` 在 working 一侧不是笔误:它今天是**被 `MGPipeApplierReset` 清掉的**(每个 make-current 一份新 CSO 表),照旧规则留在 per-context 那半。`FramebufferRecords` 留在对象一侧也是照旧规则(reset 不清它),且它的槽来自全局分配器,所以一个 share group 装两个 context 的记录是「更宽的表」而不是别名。
`MGPipeApplierState` 现在**继承** `MGPipeWorkingState` 并持有一个 `MGPipeObjectRecords& Objects`,另把九张对象表**按原名字绑成引用成员**(`Resources`、`TextureResources`…)。理由写在头里:MG_Backend / MG_IntegrationTest / MG_Test 里 ~200 个调用点一直把 `st.Resources` 读作「这个对象的 share group 记录」,绑引用让这些读点**一个不改**且按各自 context 所属的 group 正确解析;把键控改动变成改名改动只增风险不增正确性。头文件里可以同时用 `Objects.X`(在**创建**两半的地方显式点出群体)与 `X`(后端一直的拼法),两者是同一张表。
**注册表与解析链(`PipeApply.cpp`)**。三层,和 S2 的 `gPipeInputs`、S4 的 native tuple key 同构:
- `MGPipeApplierKey{ SessionKey, ContextToken }`,由 `MGPipeSetApplierKeyResolver` 装的探针回答;`SessionRuntime.cpp` 在 `InstallSessionRuntimeHooks` 里装(`ThreadSessionScope` 一开就装)。探针答案是**本线程 session 的 `ServerSession*` 地址 + `CurrentContextToken()`**;不是 session 线程的答案 false,拿到进程级 applier —— 这正是被替换掉的单例,所以 monolith / 纯客户端 / inproc 单进程形状 / 单元用例逐字不变。
- `MGPipeApplierRegisterContext(session, token, groupToken)` / `MGPipeApplierUnregisterContext` / `MGPipeApplierReleaseSession`。**注册而不是推断**:`ServerSession::CreateContext` / `DestroyContext` 在 apply 线程上调用它们,所以解析探针永远是「读本 session 已经在维护的当前 token」两字,不必从别的线程去读 session 的 map;`Accept` 与 `~SessionRuntime` 释放整 session。
- **group 键**:`shareGroupToken != 0 ? shareGroupToken : (contextToken | 高位哨兵)`。前端铸的 share group token 与 context token 来自两个计数器,不隔离就会让 group 7 和 context 7 变成一组;`0` 是 S1 注释里「本 context 自己起了一个组」的哨兵(也是 `ServerCreateEGLContext(token, 0, 0)` 这类手工建 context 传的值),落成「自己的组」而不是「和别的 0 一组」。
- **最后一个 context 走才释放该组的对象记录**(`MGPipeApplierReleaseObjectRecords` 的 per-group 版);一组的另一个 context 还在时记录一条不丢。
- **线程局部缓存 + 世代号**。`MGPipeApplier()` 是后端按状态读调的,所以快路径是「本线程上次的 (session, token) 与注册表世代都对得上就直接返回」;注册表每次变动 `++generation`,缓存指针只有在世代相等时才被解引用 —— 这就是「另一个线程销毁了 context」不会留下悬垂缓存的原因。快路径开销是三个 TLS 读、一次原子读、两次比较;探针为 null(monolith)时是一次函数指针比较。
**前端/会话接入**:`EGLImpl::MakeCurrent` 的 `EmitContextBinding` 一字未动 —— S1 的边沿宣告本来就是「这条 ring 往后归谁」的全部信息,S5 只是第一次**读**它。
**验收证据**(本次新增):
- `MG_Test/Wire/ApplierOwnershipTest.cpp`,5 个用例,直接驱动注册表与解析(无 EGL、无 wire):两个 nonshared context 同名 handle 是两条记录且互不覆盖(reset 也不丢);同一 share group 的两个 context **共用对象表而各持 working state**(当前 program、vertex-buffer 窗口不互见,而一张表上的 create 两边可见);两个 session 用同名 group/context token 不共享任何东西;组的记录活到最后一个 context 才释放、重建的 token 不继承死组;无 session 的线程仍拿到进程级 applier。
**负控**(必做,否则是空绿):把测试装的解析探针改成 null(即回到 S5 前的「谁都拿进程级 applier」),5 个用例**全红**;换回即全绿。
- `MultiSessionTest.TwoSessionsInterleaveTwoContextsEachWithoutCrossTalk`:真实 `--serve` 前身(in-process unix supervisor)上两个 session,各自两个 context(同名 token 与同名 group token),**8 轮交错**:每轮先各发一条 `bind_context` 切 context,再跑一整趟 clear + 真读回。绿/红两种颜色互为证人;日志里 0 条 `SessionLatch{`。这条用例同时压的是**生产**解析链(`ThreadSessionScope` 装的探针)与并发(两个 apply 线程同时跑)。
- 既有集合零回归:本切片点名的 97 个用例与 `MultiSessionFixture` 原有的 4 个全绿(含 `ServerLoopEglTest` 的 11 个、`ContextStateOwnershipTest` 6 个)。`SanityTest` 的两个用例因 `MGPipeApplierState` 不再可拷贝改为保存/恢复它真正改动的 working 字段。
**S5 明确没做(下一层的账)**:
- **六个 twin registry 与 twin 键 `{shareGroup, slot, gen}` 仍未改**(`Managers.cpp`)。后果是 S5 **不能**声称「两个 nonshared context 的同名 handle 在驱动层是两个对象」:applier 的两条记录是对的(已有测试),但 twin 表按 handle 键,`Ensure(handle)` 会给两个 context 的同一个 handle 值返回同一个 twin。`ServerLoopTest` 里那条 `glIsBuffer` 探针仍然没有可用控制,所以**驱动级共享/隔离的证据仍是缺的**,本次没有以「跑了个空的绿」收进来。
- **wire 第 1 条的逐记录 `ContextSlot`** 仍未做,仍是 S1 的边沿宣告 + `bind_context`。这意味着「同一 ring 上两个 context 的记录交错」只在 client 按 make-current 顺序发射时正确。
- **`MGPipeApplierShareGroupCountForTesting`** 是测试观测面,不是生产 API。
- **`MGPipeApplierState` 的九张表名与 `Objects.X` 两种拼法并存**,是刻意换取 ~200 个读点不动的代价;改名的机会在 twin registry 一起做的那一刀。
- **`g_backendContextGeneration`/`g_syncContextGeneration`、ServerLoop 的 MakeCurrent 去重**同 S4 一节所列,仍未动。
## 分期与验收 ## 分期与验收
| 期 | 内容 | 验收 | | 期 | 内容 | 验收 |
+4 -1
View File
@@ -117,8 +117,11 @@ MECHANICS = [
(REMOTE / "Server" / "PipeApplier.cpp", "ApplyOne: the pre-gate runs before the verb stamp and the barrier read", (REMOTE / "Server" / "PipeApplier.cpp", "ApplyOne: the pre-gate runs before the verb stamp and the barrier read",
r"if \(!m_decoder\.AdmitOrDecline\(record\)\) return false;\s*const Bool wireSaysBarriered", r"if \(!m_decoder\.AdmitOrDecline\(record\)\) return false;\s*const Bool wireSaysBarriered",
[("ServerLoopTest.cpp", "ARecordShorterThanItsTypeIsLatchedBeforeTheApplierStampsIt")]), [("ServerLoopTest.cpp", "ARecordShorterThanItsTypeIsLatchedBeforeTheApplierStampsIt")]),
# P14 S2: the latch is the CALLING THREAD'S DOMAIN now (FatalFunnel.h), so the unarmed arm
# reads the domain's own word instead of the process's - the mechanism and every case below
# are unchanged, and a session's domain being unarmed is still SessionFail byte for byte.
(REMOTE / "FatalFunnel.cpp", "SessionLatch: first fault wins, every fault counted, one SessionFault; unarmed it dies", (REMOTE / "FatalFunnel.cpp", "SessionLatch: first fault wins, every fault counted, one SessionFault; unarmed it dies",
r"if \(!g_latchArmed\.load\(std::memory_order_acquire\)\) \{\s*SessionFail\(", r"if \(!latch\.armed\.load\(std::memory_order_acquire\)\) \{\s*SessionFail\(",
[("ServerLoopTest.cpp", "AnArmedSessionLatchKeepsTheFirstFaultCountsEveryOneAndPublishesOnce"), [("ServerLoopTest.cpp", "AnArmedSessionLatchKeepsTheFirstFaultCountsEveryOneAndPublishesOnce"),
("ServerLoopTest.cpp", "AnUnarmedSessionLatchDiesWithItsLineLikeSessionFail")]), ("ServerLoopTest.cpp", "AnUnarmedSessionLatchDiesWithItsLineLikeSessionFail")]),
] ]
+2 -1
View File
@@ -3,5 +3,6 @@
"2": "b30928e6fc93ddc015fd86f634a6820f775a6f2f6de5a848d273981e706b2597", "2": "b30928e6fc93ddc015fd86f634a6820f775a6f2f6de5a848d273981e706b2597",
"3": "da09f0acb48cf245703f28556ef5a38bc9e25241ebbf7b6baa9ab1765b7ddd50", "3": "da09f0acb48cf245703f28556ef5a38bc9e25241ebbf7b6baa9ab1765b7ddd50",
"4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f", "4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f",
"5": "4632750fd68b889cb6df709df9d150f9910468c0c530399de01bef87ba793904" "5": "4632750fd68b889cb6df709df9d150f9910468c0c530399de01bef87ba793904",
"6": "fd76c08349a399edb75a6b36753347d19b79243d7bacdd61323f2921db56dad3"
} }