diff --git a/CMakeLists.txt b/CMakeLists.txt index 39cf3f5f..8b04adef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -464,6 +464,7 @@ set(SOURCE_FILES MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp MobileGL/MG_State/GLState/Core.cpp + MobileGL/MG_State/GLState/ShareGroupState.cpp MobileGL/MG_State/EGLState/Core.cpp MobileGL/MG_State/GLState/ErrorState/Error.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/PipeApplier.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 # 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. diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index a980ed35..ef19e1ba 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -976,8 +976,17 @@ namespace MobileGL::MG_Backend::DirectGLES { 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) { - DestroyEGLContext(); ResetEGLRuntimeState(); } @@ -1008,8 +1017,9 @@ namespace MobileGL::MG_Backend::DirectGLES { 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) { - DestroyEGLContext(); ResetEGLRuntimeState(); } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 0e93d4b3..91ec2e78 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -16180,10 +16180,225 @@ namespace MobileGL::MG_Backend::DirectGLES { 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 EGLContext g_Context = EGL_NO_CONTEXT; - static EGLSurface g_Surface = EGL_NO_SURFACE; - static EGLConfig g_Config = nullptr; + // eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0. + static Uint32 g_displayUsers = 0; + + 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 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 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& NativeContexts() { + static Vector* contexts = new Vector(); + return *contexts; + } + static Vector& NativeSessions() { + static Vector* sessions = new Vector(); + return *sessions; + } + + static std::atomic 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 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 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 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 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 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 // 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) { outWidth = 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; } EGLint width = 0; EGLint height = 0; - if (!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_WIDTH, &width) || - !g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_HEIGHT, &height) || + if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &width) || + !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height) || width <= 0 || height <= 0) { return false; } @@ -16273,15 +16491,17 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif // 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 && - g_Config != nullptr) { + activeConfig != nullptr) { EGLint eglDepth = 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(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(eglStencil); } } @@ -16291,9 +16511,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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 // because the branch is unreachable for a surface MobileGL chose itself: - // InitDisplayAndContext 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 - // and the EGL fallback above always answers. A caller that supplies its own + // ChooseConfigForSurface asks eglChooseConfig for EGL_DEPTH_SIZE 24 and + // EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so the context's config always + // 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 // 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. @@ -16565,32 +16785,54 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - static Bool InitDisplayAndContext(EGLint surfaceBit, NativeWindowType window = static_cast(0)) { - DestroyEGLContext(); - + // P14 S4. THE CONTEXT FOR `tuple`, AND THE SURFACE'S CONFIG, WITH NO TEARDOWN IN FRONT. + // + // 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 - // 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(); #endif + } - g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY); - if (g_Display == EGL_NO_DISPLAY) return false; + static Bool EnsureNativeContext(NativeContextTuple& tuple, EGLint surfaceBit, NativeWindowType window) { + if (!EnsureDisplayInitialized()) return false; + if (tuple.Context != EGL_NO_CONTEXT) return true; - if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) return false; - g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API); + if (tuple.Config == nullptr && !ChooseConfigForSurface(surfaceBit, tuple.Config, window)) return false; - 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 // (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, EGL_CONTEXT_MINOR_VERSION, minorVersion, EGL_NONE}; - g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs); - if (g_Context != EGL_NO_CONTEXT) { + tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs); + if (tuple.Context != EGL_NO_CONTEXT) { + ++g_displayUsers; return true; } } const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; - g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs); - return g_Context != EGL_NO_CONTEXT; + tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs); + 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 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(0)); + } + + void DestroyNativeContextFor(Uint64 contextToken) { + const Uint64 sessionKey = CurrentNativeSessionKey(); + if (contextToken == 0) return; + const std::lock_guard 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 { @@ -16620,7 +16930,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void ApplyRequestedSwapInterval() { 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); MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval, ok ? "ok" : "failed"); @@ -16639,10 +16950,19 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool InitWindowSurface(NativeWindowType window) { if (!window) return false; - if (!InitDisplayAndContext(EGL_WINDOW_BIT, window)) return false; + DiagnoseEsprytSlotArmAtBringUp(); - g_Surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, g_Config, window, nullptr); - if (g_Surface == EGL_NO_SURFACE) return false; + NativeContextTuple& tuple = ActiveNativeContext(); + 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; @@ -16659,25 +16979,35 @@ namespace MobileGL::MG_Backend::DirectGLES { PublishDefaultFramebufferDepthStencilFormat(); #endif - MGLOG_D("EGL context created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, g_Surface, - g_Context, window); + MGLOG_D("EGL window surface created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, + surface, tuple.Context, window); return true; } Bool InitPbufferSurface(EGLint width, EGLint height) { 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(0))) return false; const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; - g_Surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, g_Config, surfaceAttribs); - if (g_Surface == EGL_NO_SURFACE) return false; + const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, surfaceAttribs); + 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; PublishDefaultFramebufferDepthStencilFormat(); - MGLOG_D("EGL pbuffer context created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display, - g_Surface, g_Context, width, height); + MGLOG_D("EGL pbuffer surface created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display, + surface, tuple.Context, width, height); return true; } @@ -16745,17 +17075,24 @@ namespace MobileGL::MG_Backend::DirectGLES { void InvalidateEglVerifiedStamp(); Bool MakeCurrent() { - if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE || - g_Context == EGL_NO_CONTEXT) { + // P14 S4: the context comes from the calling thread's tuple (the session's bound token), + // 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"); 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; MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error); return false; } InvalidateEglVerifiedStamp(); + t_boundNativeContext = tuple.Context; 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 // 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 // "not current" only defers buffer ops, which is always safe. InvalidateEglVerifiedStamp(); + t_boundNativeContext = EGL_NO_CONTEXT; g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } @@ -16828,7 +17166,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } Bool IsBackendContextCurrentOnThisThread() { - if (g_Context == EGL_NO_CONTEXT) { + const NativeContextTuple& tuple = ActiveNativeContext(); + if (tuple.Context == EGL_NO_CONTEXT) { return false; } 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) { return true; } - if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) { + if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != tuple.Context) { return false; } 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}; } - 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) { // 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() { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) @@ -17328,16 +17679,39 @@ namespace MobileGL::MG_Backend::DirectGLES { for (FrameFence& slot : g_frameFenceRing) slot = {}; g_completedFrameSerial.store(g_currentFrameSerial.load(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); - if (g_Context != EGL_NO_CONTEXT) { - g_EGLFuncs.eglDestroyContext(g_Display, g_Context); - g_Context = EGL_NO_CONTEXT; + + const Uint64 sessionKey = CurrentNativeSessionKey(); + if (g_Display == EGL_NO_DISPLAY) return; + { + const std::lock_guard 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) { - g_EGLFuncs.eglDestroySurface(g_Display, g_Surface); - g_Surface = EGL_NO_SURFACE; + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue; + 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_Display = EGL_NO_DISPLAY; } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index b6d59252..648e0668 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -204,6 +204,61 @@ namespace MobileGL::MG_Backend::DirectGLES { void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs); void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs); 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(); // Transform feedback capture spans, performed by the real ES driver. The diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index 914f285a..e1693deb 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -820,6 +820,10 @@ namespace MobileGL::MG_Pipe { // The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline // 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 // pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply // 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. // 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.) - 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 // ============================================================================ diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index acecc544..aed115df 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -17,6 +17,13 @@ // MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's // translation unit is unchanged (G1). #include +// 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 #endif #include #include @@ -52,6 +59,35 @@ namespace MobileGL::MG_Impl::EGLImpl { 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() { static std::recursive_mutex mutex; return mutex; @@ -329,7 +365,35 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { 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) { @@ -380,6 +444,12 @@ namespace MobileGL::MG_Impl::EGLImpl { const auto oldDraw = state->GetCurrentSurface(EGL_DRAW); const auto oldRead = state->GetCurrentSurface(EGL_READ); 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(); 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()); +#if MOBILEGL_BUILD_DISAGGREGATED + EmitContextBinding(state, EGL_NO_CONTEXT, oldContextToken); +#endif 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, read, ctx); +#if MOBILEGL_BUILD_DISAGGREGATED + EmitContextBinding(state, ctx, oldContextToken); +#endif return EGL_TRUE; } @@ -430,7 +506,29 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { 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(contextToken)); + } + } +#endif + return EGL_TRUE; } EGLBoolean DestroySurface(EGLDisplay dpy, EGLSurface surface) { @@ -468,6 +566,10 @@ namespace MobileGL::MG_Impl::EGLImpl { // re-initializes lazily via GetStateEnsureInitialized(); process exit // then has nothing left to destroy. 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(); } return EGL_TRUE; diff --git a/MobileGL/MG_Impl/Init.cpp b/MobileGL/MG_Impl/Init.cpp index 87fdab19..927d78b6 100644 --- a/MobileGL/MG_Impl/Init.cpp +++ b/MobileGL/MG_Impl/Init.cpp @@ -48,6 +48,10 @@ namespace MobileGL::MG_Impl { MakeUnique(fbo0, colorTex, depthTex, stencilTex); MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).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) NSOpenGLImpl::InstallHooks(); #endif diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 9806b6d4..1ad056f3 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -1966,6 +1966,19 @@ namespace MobileGL::MG_Pipe { // object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption, // "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 // 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 diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 92a9957c..45efb2ca 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -477,11 +477,173 @@ namespace MobileGL::MG_Pipe { static_assert(kCapabilityCount <= 64, "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 // (MG_Impl/Pipe/SlotAllocator.cpp): resource_destroy and delete_vertex_elements are // raised from ~BufferObject / ~VertexArrayObject, and those objects are released by // 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> Groups; + std::unordered_map> Contexts; + std::unordered_map ContextGroup; + std::unordered_map GroupContexts; + }; + std::unordered_map 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 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 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()).first; + } + auto applier = MakeUnique(*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 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()).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(*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 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 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 guard(g_applierSessionsMutex); + const auto sessionIt = g_applierSessions.find(sessionKey); + return sessionIt == g_applierSessions.end() ? 0 : static_cast(sessionIt->second.Groups.size()); + } + + namespace { // 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 @@ -490,8 +652,8 @@ namespace MobileGL::MG_Pipe { const MGPipeResourceOps* g_resourceOps = nullptr; MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) { - if (handle.Slot >= g_applier.RenderStateCsos.size()) return nullptr; - MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[handle.Slot]; + if (handle.Slot >= MGPipeApplier().RenderStateCsos.size()) return nullptr; + MGPipeRenderStateCsoRecord& record = MGPipeApplier().RenderStateCsos[handle.Slot]; if (!record.Live || record.Gen != handle.Gen) return nullptr; 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 // recycled under it. FindCso above is the same three questions for the CSO store. MGPipeResourceRecord* FindResource(MGPipeHandle res) { - if (res.Slot >= g_applier.Resources.size()) return nullptr; - MGPipeResourceRecord& record = g_applier.Resources[res.Slot]; + if (res.Slot >= MGPipeApplier().Resources.size()) return nullptr; + MGPipeResourceRecord& record = MGPipeApplier().Resources[res.Slot]; if (!record.Live || record.Gen != res.Gen) return nullptr; return &record; } MGPipeVertexElementsRecord* FindVertexElements(MGPipeHandle cso) { - if (cso.Slot >= g_applier.VertexElementsCsos.size()) return nullptr; - MGPipeVertexElementsRecord& record = g_applier.VertexElementsCsos[cso.Slot]; + if (cso.Slot >= MGPipeApplier().VertexElementsCsos.size()) return nullptr; + MGPipeVertexElementsRecord& record = MGPipeApplier().VertexElementsCsos[cso.Slot]; if (!record.Live || record.Gen != cso.Gen) return nullptr; 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 // another kind's space, and that is the "act outside its own storage" class. Vector* ResourceTableForTarget(Uint8 target) { - if (target == kMGPipeResourceTargetBuffer) return &g_applier.Resources; + if (target == kMGPipeResourceTargetBuffer) return &MGPipeApplier().Resources; if (target == static_cast(MGPipeResourceTarget::Renderbuffer)) { - return &g_applier.RenderbufferResources; + return &MGPipeApplier().RenderbufferResources; } if (target < static_cast(MGPipeResourceTarget::Count)) { // Every remaining enumerator is a texture target, and they share one table // because they share one kind: MGPipeKind::Texture. Tex1D..TexCubeArray, // Tex2DMS/MSArray, TexBuffer and TexRect are all one slot space. - return &g_applier.TextureResources; + return &MGPipeApplier().TextureResources; } return nullptr; } @@ -579,11 +741,11 @@ namespace MobileGL::MG_Pipe { Vector* ResourceTableForKind(Uint32 kind) { switch (static_cast(kind)) { case MGPipeKind::Buffer: - return &g_applier.Resources; + return &MGPipeApplier().Resources; case MGPipeKind::Texture: - return &g_applier.TextureResources; + return &MGPipeApplier().TextureResources; case MGPipeKind::Renderbuffer: - return &g_applier.RenderbufferResources; + return &MGPipeApplier().RenderbufferResources; default: return nullptr; } @@ -710,7 +872,7 @@ namespace MobileGL::MG_Pipe { MGPipeResourceRecord* record = FindResource(res); MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen, kResourceRefusalNote); - if (record == nullptr) ++g_applier.RefusedResourceCalls; + if (record == nullptr) ++MGPipeApplier().RefusedResourceCalls; return record; } @@ -723,7 +885,7 @@ namespace MobileGL::MG_Pipe { MGPipeResourceRecord* record = FindIn(table, res); MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen, kResourceRefusalNote); - if (record == nullptr) ++g_applier.RefusedResourceCalls; + if (record == nullptr) ++MGPipeApplier().RefusedResourceCalls; return record; } @@ -736,7 +898,7 @@ namespace MobileGL::MG_Pipe { Record* record = FindIn(table, handle); MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, handle.Slot, handle.Gen, kResourceRefusalNote); - if (record == nullptr) ++g_applier.RefusedObjectCalls; + if (record == nullptr) ++MGPipeApplier().RefusedObjectCalls; return record; } @@ -744,7 +906,7 @@ namespace MobileGL::MG_Pipe { MGPipeVertexElementsRecord* record = FindVertexElements(cso); MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen, kResourceRefusalNote); - if (record == nullptr) ++g_applier.RefusedVertexInputCalls; + if (record == nullptr) ++MGPipeApplier().RefusedVertexInputCalls; return record; } @@ -1198,7 +1360,7 @@ namespace MobileGL::MG_Pipe { // hook; the monolith shape of everything above them is unchanged. Bool ApplyTextureUpload(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions) { MGPipeResourceRecord* stored = - ResolveResourceIn(g_applier.TextureResources, "resource_subdata", record.Res); + ResolveResourceIn(MGPipeApplier().TextureResources, "resource_subdata", record.Res); if (stored == nullptr) return false; const char* fault = SubDataTextureFault(record, regions); @@ -1319,28 +1481,28 @@ namespace MobileGL::MG_Pipe { // ---------------------------------------------------------------------------- MGPipeShaderCsoRecord* ShaderCsoRecordAt(Uint32 slot) { if (MGPipeIsCompositeShaderSlot(slot)) { - return RecordAt(g_applier.CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase, + return RecordAt(MGPipeApplier().CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase, kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase); } // 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 // 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) { if (MGPipeIsCompositeShaderSlot(cso.Slot)) { - return FindIn(g_applier.CompositeShaderCsos, + return FindIn(MGPipeApplier().CompositeShaderCsos, 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* record = FindShaderCso(cso); MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen, kResourceRefusalNote); - if (record == nullptr) ++g_applier.RefusedObjectCalls; + if (record == nullptr) ++MGPipeApplier().RefusedObjectCalls; return record; } @@ -1388,13 +1550,14 @@ namespace MobileGL::MG_Pipe { #endif } // 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 // MGPipeApplierState::TextureShutterSerial / ContextSerial for the bump rule. - Uint64 MGPipeApplierTextureShutterSerial() { return g_applier.TextureShutterSerial; } - Uint64 MGPipeApplierContextSerial() { return g_applier.ContextSerial; } - void MGPipeApplierNoteTextureStateMoved() { ++g_applier.TextureShutterSerial; } + Uint64 MGPipeApplierTextureShutterSerial() { return MGPipeApplier().TextureShutterSerial; } + Uint64 MGPipeApplierContextSerial() { return MGPipeApplier().ContextSerial; } + void MGPipeApplierNoteTextureStateMoved() { ++MGPipeApplier().TextureShutterSerial; } void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; } const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; } @@ -1427,7 +1590,7 @@ namespace MobileGL::MG_Pipe { } #endif if (g_resourceOps != nullptr) return false; - ++g_applier.RefusedNoConsumer; + ++MGPipeApplier().RefusedNoConsumer; return true; } @@ -1438,11 +1601,11 @@ namespace MobileGL::MG_Pipe { // zero" rule is written out. void ClearShaderBufferWindows() { for (Uint32 cls = 0; cls < kMGPipeShaderBufferClassCount; ++cls) { - g_applier.BoundShaderBuffers[cls] = {}; - g_applier.ShaderBufferStart[cls] = 0; - g_applier.ShaderBufferCount[cls] = 0; + MGPipeApplier().BoundShaderBuffers[cls] = {}; + MGPipeApplier().ShaderBufferStart[cls] = 0; + MGPipeApplier().ShaderBufferCount[cls] = 0; 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(); } #endif - g_applier.RenderStateCsos.clear(); - g_applier.BoundRenderStateCso = kMGPipeNullHandle; - g_applier.Residual = ResidualValueBlock{}; - g_applier.HasResidual = false; - g_applier.ScatteredChunkBits = 0; - g_applier.ResidualCapabilitiesCompared = 0; - g_applier.ResidualDivergences = 0; - g_applier.PatchCarrierComparisons = 0; - g_applier.PatchCarrierDivergences = 0; + MGPipeApplier().RenderStateCsos.clear(); + MGPipeApplier().BoundRenderStateCso = kMGPipeNullHandle; + MGPipeApplier().Residual = ResidualValueBlock{}; + MGPipeApplier().HasResidual = false; + MGPipeApplier().ScatteredChunkBits = 0; + MGPipeApplier().ResidualCapabilitiesCompared = 0; + MGPipeApplier().ResidualDivergences = 0; + MGPipeApplier().PatchCarrierComparisons = 0; + MGPipeApplier().PatchCarrierDivergences = 0; // 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 // 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 backend's own bring-up and teardown, not by a state reset. - g_applier.RefusedResourceCalls = 0; - g_applier.RefusedVertexInputCalls = 0; - g_applier.RefusedObjectCalls = 0; - g_applier.RefusedNoConsumer = 0; - g_applier.BoundVertexElements = kMGPipeNullHandle; - g_applier.VertexBuffers = {}; - g_applier.VertexBufferStart = 0; - g_applier.VertexBufferCount = 0; - g_applier.VertexFetchBaseInstance = 0; - g_applier.IndexBuffer = MGPIndexBuffer{}; + MGPipeApplier().RefusedResourceCalls = 0; + MGPipeApplier().RefusedVertexInputCalls = 0; + MGPipeApplier().RefusedObjectCalls = 0; + MGPipeApplier().RefusedNoConsumer = 0; + MGPipeApplier().BoundVertexElements = kMGPipeNullHandle; + MGPipeApplier().VertexBuffers = {}; + MGPipeApplier().VertexBufferStart = 0; + MGPipeApplier().VertexBufferCount = 0; + MGPipeApplier().VertexFetchBaseInstance = 0; + MGPipeApplier().IndexBuffer = MGPIndexBuffer{}; // 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` - // 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 // 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. - g_applier.MapPersistentRoundtrips = 0; + MGPipeApplier().MapPersistentRoundtrips = 0; // 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 // 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; // - 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. - ++g_applier.VertexBuffersSerial; - ++g_applier.IndexBufferSerial; + ++MGPipeApplier().VertexBuffersSerial; + ++MGPipeApplier().IndexBufferSerial; // 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 // 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 // GetTextureContextId's whole job (the backends key their per-context memos on it). - ++g_applier.TextureShutterSerial; - ++g_applier.ContextSerial; + ++MGPipeApplier().TextureShutterSerial; + ++MGPipeApplier().ContextSerial; // ---- 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, @@ -1566,21 +1729,21 @@ namespace MobileGL::MG_Pipe { // 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 // dropping its record here would lose it for the life of the process. - g_applier.BoundFramebuffer = {}; - g_applier.BoundSamplerViews = {}; - g_applier.SamplerViewStart = 0; - g_applier.SamplerViewCount = 0; - g_applier.BoundSamplerStates = {}; - g_applier.SamplerStateStart = 0; - g_applier.SamplerStateCount = 0; - g_applier.BoundShaderImages = {}; - g_applier.ShaderImageStart = 0; - g_applier.ShaderImageCount = 0; - g_applier.DrawProgram = kMGPipeNullHandle; - g_applier.DispatchProgram = kMGPipeNullHandle; - g_applier.BoundShaderCso = kMGPipeNullHandle; + MGPipeApplier().BoundFramebuffer = {}; + MGPipeApplier().BoundSamplerViews = {}; + MGPipeApplier().SamplerViewStart = 0; + MGPipeApplier().SamplerViewCount = 0; + MGPipeApplier().BoundSamplerStates = {}; + MGPipeApplier().SamplerStateStart = 0; + MGPipeApplier().SamplerStateCount = 0; + MGPipeApplier().BoundShaderImages = {}; + MGPipeApplier().ShaderImageStart = 0; + MGPipeApplier().ShaderImageCount = 0; + MGPipeApplier().DrawProgram = kMGPipeNullHandle; + MGPipeApplier().DispatchProgram = kMGPipeNullHandle; + MGPipeApplier().BoundShaderCso = kMGPipeNullHandle; #if MOBILEGL_BUILD_DISAGGREGATED - g_applier.BoundStreamOutputLifetimeId = 0; + MGPipeApplier().BoundStreamOutputLifetimeId = 0; // Begin is emitted once per span, not on make-current. Its immutable object // snapshot survives with the resource/program records until End or release; // 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 // would be suppressed as unchanged and the server would draw with a cleared window. ClearShaderBufferWindows(); - ++g_applier.FramebufferSerial; - ++g_applier.SamplerViewsSerial; - ++g_applier.SamplerStatesSerial; - ++g_applier.ShaderImagesSerial; - ++g_applier.ProgramBindingSerial; - ++g_applier.ShaderBuffersSerial; + ++MGPipeApplier().FramebufferSerial; + ++MGPipeApplier().SamplerViewsSerial; + ++MGPipeApplier().SamplerStatesSerial; + ++MGPipeApplier().ShaderImagesSerial; + ++MGPipeApplier().ProgramBindingSerial; + ++MGPipeApplier().ShaderBuffersSerial; } void MGPipeApplierReleaseObjectRecords() { @@ -1615,37 +1778,37 @@ namespace MobileGL::MG_Pipe { // 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 // be able to match a value it has already seen. - g_applier.Resources.clear(); - g_applier.VertexElementsCsos.clear(); - g_applier.BoundVertexElements = kMGPipeNullHandle; - ++g_applier.VertexBuffersSerial; - ++g_applier.IndexBufferSerial; + MGPipeApplier().Resources.clear(); + MGPipeApplier().VertexElementsCsos.clear(); + MGPipeApplier().BoundVertexElements = kMGPipeNullHandle; + ++MGPipeApplier().VertexBuffersSerial; + ++MGPipeApplier().IndexBufferSerial; // 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. - ++g_applier.TextureShutterSerial; - ++g_applier.ContextSerial; + ++MGPipeApplier().TextureShutterSerial; + ++MGPipeApplier().ContextSerial; // 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 // handle the next call resolves against. - g_applier.TextureResources.clear(); - g_applier.RenderbufferResources.clear(); - g_applier.SamplerCsos.clear(); - g_applier.SamplerViewCsos.clear(); - g_applier.ShaderCsos.clear(); - g_applier.CompositeShaderCsos.clear(); + MGPipeApplier().TextureResources.clear(); + MGPipeApplier().RenderbufferResources.clear(); + MGPipeApplier().SamplerCsos.clear(); + MGPipeApplier().SamplerViewCsos.clear(); + MGPipeApplier().ShaderCsos.clear(); + MGPipeApplier().CompositeShaderCsos.clear(); // 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 // 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 - // 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. - g_applier.FramebufferRecords.clear(); - g_applier.DrawProgram = kMGPipeNullHandle; - g_applier.DispatchProgram = kMGPipeNullHandle; - g_applier.BoundShaderCso = kMGPipeNullHandle; + MGPipeApplier().FramebufferRecords.clear(); + MGPipeApplier().DrawProgram = kMGPipeNullHandle; + MGPipeApplier().DispatchProgram = kMGPipeNullHandle; + MGPipeApplier().BoundShaderCso = kMGPipeNullHandle; #if MOBILEGL_BUILD_DISAGGREGATED - g_applier.BoundStreamOutputLifetimeId = 0; - g_applier.StreamOutputSpans.clear(); + MGPipeApplier().BoundStreamOutputLifetimeId = 0; + MGPipeApplier().StreamOutputSpans.clear(); #endif // 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 @@ -1657,35 +1820,35 @@ namespace MobileGL::MG_Pipe { // 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 // outlive a make-current, and here they do not outlive anything. - g_applier.BoundFramebuffer = {}; - g_applier.BoundSamplerViews = {}; - g_applier.SamplerViewStart = 0; - g_applier.SamplerViewCount = 0; - g_applier.BoundSamplerStates = {}; - g_applier.SamplerStateStart = 0; - g_applier.SamplerStateCount = 0; - g_applier.BoundShaderImages = {}; - g_applier.ShaderImageStart = 0; - g_applier.ShaderImageCount = 0; + MGPipeApplier().BoundFramebuffer = {}; + MGPipeApplier().BoundSamplerViews = {}; + MGPipeApplier().SamplerViewStart = 0; + MGPipeApplier().SamplerViewCount = 0; + MGPipeApplier().BoundSamplerStates = {}; + MGPipeApplier().SamplerStateStart = 0; + MGPipeApplier().SamplerStateCount = 0; + MGPipeApplier().BoundShaderImages = {}; + MGPipeApplier().ShaderImageStart = 0; + MGPipeApplier().ShaderImageCount = 0; // 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 // 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 // on a slot the next context re-mints. ClearShaderBufferWindows(); - ++g_applier.FramebufferSerial; - ++g_applier.SamplerViewsSerial; - ++g_applier.SamplerStatesSerial; - ++g_applier.ShaderImagesSerial; - ++g_applier.ProgramBindingSerial; - ++g_applier.ShaderBuffersSerial; + ++MGPipeApplier().FramebufferSerial; + ++MGPipeApplier().SamplerViewsSerial; + ++MGPipeApplier().SamplerStatesSerial; + ++MGPipeApplier().ShaderImagesSerial; + ++MGPipeApplier().ProgramBindingSerial; + ++MGPipeApplier().ShaderBuffersSerial; } void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) { MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot, "create_render_state named the reserved slot 0"); MGPipeRenderStateCsoRecord* recordAt = - RecordAt(g_applier.RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots); + RecordAt(MGPipeApplier().RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots); if (recordAt == nullptr) { #if MOBILEGL_BUILD_DISAGGREGATED MGPipeSessionFail(MGPipeFatalFamily::ProtocolCorruption, @@ -1753,12 +1916,12 @@ namespace MobileGL::MG_Pipe { MGPipeScatterPipelineBytes(record->PipelineBytes.data(), MGPipeApplyAccess::RenderState(inputs)); 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, // whatever mask minted it - so the pipeline chunks are all "moved" here, and all // enter the applier's ledger of the bytes it owns. const Uint32 moved = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineChunks); - g_applier.ScatteredChunkBits |= moved; + MGPipeApplier().ScatteredChunkBits |= 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 // 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. - if (g_applier.BoundRenderStateCso == handle.Handle) { - g_applier.BoundRenderStateCso = kMGPipeNullHandle; + if (MGPipeApplier().BoundRenderStateCso == handle.Handle) { + MGPipeApplier().BoundRenderStateCso = kMGPipeNullHandle; } } @@ -1781,7 +1944,7 @@ namespace MobileGL::MG_Pipe { MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs)); MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version); const Uint32 moved = MGPipeGlobalChunkBitsOfDynamicMask(dyn.ChunkMask); - g_applier.ScatteredChunkBits |= moved; + MGPipeApplier().ScatteredChunkBits |= 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 // 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. - g_applier.IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0; + MGPipeApplier().IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0; #if MOBILEGL_BUILD_DISAGGREGATED - g_applier.BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId; + MGPipeApplier().BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId; #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 // 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. - if ((g_applier.ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) { - ++g_applier.PatchCarrierComparisons; + if ((MGPipeApplier().ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) { + ++MGPipeApplier().PatchCarrierComparisons; const Bool agrees = working.PatchVertices == patch.Vertices && std::memcmp(&working.PatchDefaultOuterLevel, &outer, sizeof(outer)) == 0 && std::memcmp(&working.PatchDefaultInnerLevel, &inner, sizeof(inner)) == 0; if (!agrees) { - ++g_applier.PatchCarrierDivergences; + ++MGPipeApplier().PatchCarrierDivergences; MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipePatchCarriersDiffer") " 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)", @@ -1921,9 +2084,9 @@ namespace MobileGL::MG_Pipe { } void MGPipeApplySetResidualValueState(const ResidualValueBlock& block) { - g_applier.Residual = block; - g_applier.HasResidual = true; - g_applier.ResidualCapabilitiesCompared = 0; + MGPipeApplier().Residual = block; + MGPipeApplier().HasResidual = true; + MGPipeApplier().ResidualCapabilitiesCompared = 0; // 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 @@ -1953,16 +2116,16 @@ namespace MobileGL::MG_Pipe { // tautology. The redundancy this wire exists to check is between the CARRIED bits and // the ASSEMBLED block. const RenderStateParameters& working = MGPipeApplyAccess::RenderState(gPipeInputs); - const Uint32 owned = g_applier.ScatteredChunkBits; + const Uint32 owned = MGPipeApplier().ScatteredChunkBits; for (SizeT i = 0; i < kCapabilityCount; ++i) { const CapabilityInput cap = static_cast(i); const Uint32 sources = CapabilitySourceChunks(cap); if ((owned & sources) != sources) continue; - ++g_applier.ResidualCapabilitiesCompared; + ++MGPipeApplier().ResidualCapabilitiesCompared; const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0; const Bool assembledBit = MGPipeApplyAccess::DeriveCapability(working, cap); if (carried == assembledBit) continue; - ++g_applier.ResidualDivergences; + ++MGPipeApplier().ResidualDivergences; MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeResidualDiverged, \"%s\"") " carried=%d assembled=%d", kCapabilityNames[i], static_cast(carried), @@ -2279,7 +2442,7 @@ namespace MobileGL::MG_Pipe { // THE FOUR BUFFER-ONLY CALLS, AND WHAT POLICES THAT. resource_flush_range, // 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 // 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 @@ -2428,7 +2591,7 @@ namespace MobileGL::MG_Pipe { // 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 // 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); if (record == nullptr) return nullptr; @@ -2586,7 +2749,7 @@ namespace MobileGL::MG_Pipe { } MGPipeVertexElementsRecord* recordAt = - RecordAt(g_applier.VertexElementsCsos, desc.Cso.Slot, kMGPipeMaxVertexElementsSlots); + RecordAt(MGPipeApplier().VertexElementsCsos, desc.Cso.Slot, kMGPipeMaxVertexElementsSlots); if (recordAt == nullptr) { MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") " 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 // state, not an error, and the backend has a branch for it. if (MGPipeHandleIsNull(handle.Handle)) { - g_applier.BoundVertexElements = kMGPipeNullHandle; + MGPipeApplier().BoundVertexElements = kMGPipeNullHandle; return; } // 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. const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle); if (record == nullptr) return; - g_applier.BoundVertexElements = handle.Handle; + MGPipeApplier().BoundVertexElements = handle.Handle; } void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) { @@ -2663,8 +2826,8 @@ namespace MobileGL::MG_Pipe { const Uint32 gen = record->Gen; *record = MGPipeVertexElementsRecord{}; record->Gen = gen; - if (g_applier.BoundVertexElements == handle.Handle) { - g_applier.BoundVertexElements = kMGPipeNullHandle; + if (MGPipeApplier().BoundVertexElements == handle.Handle) { + 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 // four entries has said nothing about the rest. 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; - g_applier.VertexBufferCount = hdr.Count; + MGPipeApplier().VertexBufferStart = hdr.Start; + MGPipeApplier().VertexBufferCount = hdr.Count; // 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 @@ -2710,8 +2873,8 @@ namespace MobileGL::MG_Pipe { // 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 // would keep the previous shift. - g_applier.VertexFetchBaseInstance = hdr.BaseInstance; - ++g_applier.VertexBuffersSerial; + MGPipeApplier().VertexFetchBaseInstance = hdr.BaseInstance; + ++MGPipeApplier().VertexBuffersSerial; } 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 // (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. - g_applier.IndexBuffer = record; - ++g_applier.IndexBufferSerial; + MGPipeApplier().IndexBuffer = record; + ++MGPipeApplier().IndexBufferSerial; } void MGPipeDeriveRenderStateFields(PipeInputs& inputs) { @@ -2839,7 +3002,7 @@ namespace MobileGL::MG_Pipe { if (NoP4aConsumer()) return; 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 // what makes that an argument rather than an assumption. if (record == nullptr) return; @@ -2851,11 +3014,11 @@ namespace MobileGL::MG_Pipe { // target: "this record describes the framebuffer it names; no binding changes". if (state.Target == static_cast(MGPipeFramebufferTarget::Draw) || state.Target == static_cast(MGPipeFramebufferTarget::Both)) { - g_applier.BoundFramebuffer[static_cast(MGPipeFramebufferTarget::Draw)] = state.Fbo; + MGPipeApplier().BoundFramebuffer[static_cast(MGPipeFramebufferTarget::Draw)] = state.Fbo; } if (state.Target == static_cast(MGPipeFramebufferTarget::Read) || state.Target == static_cast(MGPipeFramebufferTarget::Both)) { - g_applier.BoundFramebuffer[static_cast(MGPipeFramebufferTarget::Read)] = state.Fbo; + MGPipeApplier().BoundFramebuffer[static_cast(MGPipeFramebufferTarget::Read)] = state.Fbo; } // 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* @@ -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 // 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. - ++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 @@ -2941,7 +3104,7 @@ namespace MobileGL::MG_Pipe { } MGPipeSamplerCsoRecord* recordAt = - RecordAt(g_applier.SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots); + RecordAt(MGPipeApplier().SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots); if (recordAt == nullptr) { MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") " 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(MGPipeKind::SamplerCso), "delete_sampler_state on kind %u", handle.Kind); MGPipeSamplerCsoRecord* record = - ResolveObject(g_applier.SamplerCsos, "delete_sampler_state", handle.Handle); + ResolveObject(MGPipeApplier().SamplerCsos, "delete_sampler_state", handle.Handle); if (record == nullptr) return; // 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; MGPipeSamplerViewRecord* recordAt = - RecordAt(g_applier.SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots); + RecordAt(MGPipeApplier().SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots); if (recordAt == nullptr) { MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") " 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 // 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. - MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, view.Texture); + MGPipeResourceRecord* texture = FindIn(MGPipeApplier().TextureResources, view.Texture); if (texture != nullptr) texture->ViewCso = view.Cso; } @@ -3039,13 +3202,13 @@ namespace MobileGL::MG_Pipe { MOBILEGL_ASSERT(handle.Kind == static_cast(MGPipeKind::SamplerViewCso), "delete_sampler_view on kind %u", handle.Kind); MGPipeSamplerViewRecord* record = - ResolveObject(g_applier.SamplerViewCsos, "delete_sampler_view", handle.Handle); + ResolveObject(MGPipeApplier().SamplerViewCsos, "delete_sampler_view", handle.Handle); if (record == nullptr) return; // 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 // 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) { 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 // moment the parameters move, whether or not anything is bound. MGPipeResourceRecord* record = - ResolveObject(g_applier.TextureResources, "set_texture_params", params.Res); + ResolveObject(MGPipeApplier().TextureResources, "set_texture_params", params.Res); if (record == nullptr) return false; // 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) { if (NoP4aConsumer()) return; if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail, - g_applier.BoundSamplerViews, g_applier.SamplerViewStart, - g_applier.SamplerViewCount)) { + MGPipeApplier().BoundSamplerViews, MGPipeApplier().SamplerViewStart, + MGPipeApplier().SamplerViewCount)) { return; } - ++g_applier.SamplerViewsSerial; + ++MGPipeApplier().SamplerViewsSerial; // P5c (rv): a texture bind is what the bind-generation shutter guarded. MGPipeApplierNoteTextureStateMoved(); } @@ -3133,11 +3296,11 @@ namespace MobileGL::MG_Pipe { void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) { if (NoP4aConsumer()) return; if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail, - g_applier.BoundSamplerStates, g_applier.SamplerStateStart, - g_applier.SamplerStateCount)) { + MGPipeApplier().BoundSamplerStates, MGPipeApplier().SamplerStateStart, + MGPipeApplier().SamplerStateCount)) { return; } - ++g_applier.SamplerStatesSerial; + ++MGPipeApplier().SamplerStatesSerial; // P5c (rv): a sampler bind or parameter change is what the shutters guarded. MGPipeApplierNoteTextureStateMoved(); } @@ -3150,11 +3313,11 @@ namespace MobileGL::MG_Pipe { // formats on Adreno will accept is the backend's, not this applier's. if (NoP4aConsumer()) return; if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail, - g_applier.BoundShaderImages, g_applier.ShaderImageStart, - g_applier.ShaderImageCount)) { + MGPipeApplier().BoundShaderImages, MGPipeApplier().ShaderImageStart, + MGPipeApplier().ShaderImageCount)) { return; } - ++g_applier.ShaderImagesSerial; + ++MGPipeApplier().ShaderImagesSerial; // P5c (rv): an image bind moves the texture bind generation's guarded set too. 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 // 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). - 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]; - g_applier.ShaderBufferStart[hdr.Class] = hdr.Start; - g_applier.ShaderBufferCount[hdr.Class] = hdr.Count; + MGPipeApplier().ShaderBufferStart[hdr.Class] = hdr.Start; + MGPipeApplier().ShaderBufferCount[hdr.Class] = hdr.Count; 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 // 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 // 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 - @@ -3501,14 +3664,14 @@ namespace MobileGL::MG_Pipe { // error; a DEAD handle leaves the previous binding untouched and is counted, which is // bind_render_state's precedent for the same question. if (MGPipeHandleIsNull(handle.Handle)) { - g_applier.BoundShaderCso = kMGPipeNullHandle; - ++g_applier.ProgramBindingSerial; + MGPipeApplier().BoundShaderCso = kMGPipeNullHandle; + ++MGPipeApplier().ProgramBindingSerial; return; } const MGPipeShaderCsoRecord* record = ResolveShaderCso("bind_shader_state", handle.Handle); if (record == nullptr) return; - g_applier.BoundShaderCso = handle.Handle; - ++g_applier.ProgramBindingSerial; + MGPipeApplier().BoundShaderCso = handle.Handle; + ++MGPipeApplier().ProgramBindingSerial; } 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 // refusal counter firing for a state the applier itself created. Bool cleared = false; - if (g_applier.BoundShaderCso == handle.Handle) { - g_applier.BoundShaderCso = kMGPipeNullHandle; + if (MGPipeApplier().BoundShaderCso == handle.Handle) { + MGPipeApplier().BoundShaderCso = kMGPipeNullHandle; cleared = true; } - if (g_applier.DrawProgram == handle.Handle) { - g_applier.DrawProgram = kMGPipeNullHandle; + if (MGPipeApplier().DrawProgram == handle.Handle) { + MGPipeApplier().DrawProgram = kMGPipeNullHandle; cleared = true; } - if (g_applier.DispatchProgram == handle.Handle) { - g_applier.DispatchProgram = kMGPipeNullHandle; + if (MGPipeApplier().DispatchProgram == handle.Handle) { + MGPipeApplier().DispatchProgram = kMGPipeNullHandle; 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 @@ -3555,14 +3718,14 @@ namespace MobileGL::MG_Pipe { "set_draw_program on kind %u", handle.Kind); if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason. if (MGPipeHandleIsNull(handle.Handle)) { - g_applier.DrawProgram = kMGPipeNullHandle; - ++g_applier.ProgramBindingSerial; + MGPipeApplier().DrawProgram = kMGPipeNullHandle; + ++MGPipeApplier().ProgramBindingSerial; return; } const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_draw_program", handle.Handle); if (record == nullptr) return; - g_applier.DrawProgram = handle.Handle; - ++g_applier.ProgramBindingSerial; + MGPipeApplier().DrawProgram = handle.Handle; + ++MGPipeApplier().ProgramBindingSerial; } void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) { @@ -3570,14 +3733,14 @@ namespace MobileGL::MG_Pipe { "set_dispatch_program on kind %u", handle.Kind); if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason. if (MGPipeHandleIsNull(handle.Handle)) { - g_applier.DispatchProgram = kMGPipeNullHandle; - ++g_applier.ProgramBindingSerial; + MGPipeApplier().DispatchProgram = kMGPipeNullHandle; + ++MGPipeApplier().ProgramBindingSerial; return; } const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_dispatch_program", handle.Handle); if (record == nullptr) return; - g_applier.DispatchProgram = handle.Handle; - ++g_applier.ProgramBindingSerial; + MGPipeApplier().DispatchProgram = handle.Handle; + ++MGPipeApplier().ProgramBindingSerial; } void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) { diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 01bd7264..109fa397 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -513,7 +513,88 @@ namespace MobileGL::MG_Pipe { 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 Resources; + Vector 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 TextureResources; + Vector RenderbufferResources; + Vector SamplerCsos; + Vector SamplerViewCsos; + Vector 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 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 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 // (MGPipeHandles.h kMGPipeFirstAllocatableSlot). Vector RenderStateCsos; @@ -553,69 +634,6 @@ namespace MobileGL::MG_Pipe { Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only 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 Resources; - Vector 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 TextureResources; - Vector RenderbufferResources; - Vector SamplerCsos; - Vector SamplerViewCsos; - Vector 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 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 FramebufferRecords; - // 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 // under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no @@ -911,6 +929,53 @@ namespace MobileGL::MG_Pipe { } #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& Resources; + Vector& VertexElementsCsos; + Vector& TextureResources; + Vector& RenderbufferResources; + Vector& SamplerCsos; + Vector& SamplerViewCsos; + Vector& ShaderCsos; + Vector& CompositeShaderCsos; + Vector& 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 // 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 @@ -932,9 +997,65 @@ namespace MobileGL::MG_Pipe { 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(); + // ===================================================================================== + // 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) // --------------------------------------------------------------------------------- diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index d763b1a3..8b253634 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -72,8 +72,9 @@ // that the expansion, the two generated tables and this number agree. // // class entries group (as the plan tabulates it) -// kScreen 12 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the -// appended server-side fence wait 1 and P5c's applier_reset 1 +// 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 and +// P14 S1's bind_context 1 // kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2 // kCtxCso 13 CSO create/bind/delete // 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, // bind_stream_output, set_storage_block_binding, // 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 // 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 // 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. +// - 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 #define MGP_CALL_LIST(X) \ @@ -355,7 +363,18 @@ /* ---- has an MGPipeApply* entry point or a monolith producer: monolith reads into its ---- */ \ /* ---- driver PBO inside the backend's own ReadPixels, as before (G1). ---- */ \ 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 // Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index 66484d92..9504a43a 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -297,6 +297,10 @@ #define MGP_FIELDS_MGPApplierReset(F) \ 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 // array is one field, exactly as MGPFramebufferState's Color[8] is one. #define MGP_FIELDS_MGPContextValues(F) \ @@ -423,7 +427,7 @@ P(MGPSurfaceInfo) \ P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \ P(MGPCopyFromFramebuffer) \ - P(MGPApplierReset) P(MGPContextValues) \ + P(MGPApplierReset) P(MGPBindContext) P(MGPContextValues) \ P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \ P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \ P(StencilFaceState) \ diff --git a/MobileGL/MG_Pipe/generated/PipeTables.inc b/MobileGL/MG_Pipe/generated/PipeTables.inc index b3f7be69..c5872d57 100644 --- a/MobileGL/MG_Pipe/generated/PipeTables.inc +++ b/MobileGL/MG_Pipe/generated/PipeTables.inc @@ -12,7 +12,7 @@ // 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. -// 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). struct MGPipeScreen { void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply); @@ -27,6 +27,7 @@ struct MGPipeScreen { void (*FenceDestroy)(const MGPHandleOnly* payload); void (*FenceWaitServer)(const MGPFenceWait* payload); void (*ApplierReset)(const MGPApplierReset* payload); + void (*BindContext)(const MGPBindContext* payload); }; // context: 71 calls. A null entry means the backend does not implement this @@ -105,9 +106,9 @@ struct MGPipeContext { void (*GetTextureImageToBuffer)(const MGPReadbackToBuffer* payload); }; -inline constexpr SizeT kMGPipeScreenCallCount = 12; +inline constexpr SizeT kMGPipeScreenCallCount = 13; 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 // member that no generator knows about. diff --git a/MobileGL/MG_Pipe/generated/PipeThunks.inc b/MobileGL/MG_Pipe/generated/PipeThunks.inc index 26385966..535d82b9 100644 --- a/MobileGL/MG_Pipe/generated/PipeThunks.inc +++ b/MobileGL/MG_Pipe/generated/PipeThunks.inc @@ -348,3 +348,7 @@ inline void MGP_ReadPixelsToBuffer(const MGPReadbackToBuffer* payload) { inline void MGP_GetTextureImageToBuffer(const MGPReadbackToBuffer* payload) { gMGPipeContext.GetTextureImageToBuffer(payload); } + +inline void MGP_BindContext(const MGPBindContext* payload) { + gMGPipeScreen.BindContext(payload); +} diff --git a/MobileGL/MG_Pipe/generated/PipeVerify.inc b/MobileGL/MG_Pipe/generated/PipeVerify.inc index 2a929f06..4f5a0f3f 100644 --- a/MobileGL/MG_Pipe/generated/PipeVerify.inc +++ b/MobileGL/MG_Pipe/generated/PipeVerify.inc @@ -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 MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& 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 MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField); inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField); @@ -269,6 +270,8 @@ struct MGPipeHasFieldVerifier : std::true_type {}; template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> struct MGPipeHasFieldVerifier : std::true_type {}; @@ -690,6 +693,11 @@ inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, con 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) { MGP_FIELDS_MGPContextValues(MGP_VERIFY_FIELD) return true; @@ -757,4 +765,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind #undef MGP_VERIFY_FIELD -inline constexpr SizeT kMGPipeVerifiedPayloadCount = 83; +inline constexpr SizeT kMGPipeVerifiedPayloadCount = 84; diff --git a/MobileGL/MG_Pipe/generated/PipeWire.inc b/MobileGL/MG_Pipe/generated/PipeWire.inc index a25a5086..9b811d32 100644 --- a/MobileGL/MG_Pipe/generated/PipeWire.inc +++ b/MobileGL/MG_Pipe/generated/PipeWire.inc @@ -153,7 +153,8 @@ enum class MGPWireOp : Uint16 { DeleteStreamOutput = 81, ReadPixelsToBuffer = 82, GetTextureImageToBuffer = 83, - kOpCount = 84, + BindContext = 84, + kOpCount = 85, }; // THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is @@ -258,6 +259,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast(MGPWireOp::kOpCount) /* 81 DeleteStreamOutput */ static_cast(kNone), /* 82 ReadPixelsToBuffer */ static_cast(kNone), /* 83 GetTextureImageToBuffer */ static_cast(kNone), + /* 84 BindContext */ static_cast(kNone), }; static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_cast(MGPWireOp::kOpCount), @@ -361,6 +363,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast(MGPWireOp::kOpCount /* 81 DeleteStreamOutput */ static_cast(kCtxVerb), /* 82 ReadPixelsToBuffer */ static_cast(kCtxVerb), /* 83 GetTextureImageToBuffer */ static_cast(kCtxObject), + /* 84 BindContext */ static_cast(kScreen), }; static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) == static_cast(MGPWireOp::kOpCount), @@ -468,6 +471,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount /* 81 DeleteStreamOutput */ static_cast(kWaitNone), /* 82 ReadPixelsToBuffer */ static_cast(kWaitNone), /* 83 GetTextureImageToBuffer */ static_cast(kWaitNone), + /* 84 BindContext */ static_cast(kWaitNone), }; static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) == static_cast(MGPWireOp::kOpCount), @@ -1198,6 +1202,14 @@ static_assert(sizeof(MGPWireRec_GetTextureImageToBuffer) == ((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackToBuffer) + 7u) & ~SizeT(7u)), "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) { MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call, static_cast(size), static_cast(remaining)); @@ -1496,6 +1508,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, case MGPWireOp::GetTextureImageToBuffer: MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImageToBuffer, "GetTextureImageToBuffer"); break; + case MGPWireOp::BindContext: + MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindContext, "BindContext"); + break; case MGPWireOp::kInvalid: case MGPWireOp::kOpCount: default: diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc index 12f5c8bc..dc6fed1d 100644 --- a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -431,6 +431,8 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = { {"MGPCopyFromFramebuffer.SubImage", offsetof(MGPCopyFromFramebuffer, SubImage), sizeof(((MGPCopyFromFramebuffer*)nullptr)->SubImage)}, {"MGPApplierReset", 0, sizeof(MGPApplierReset)}, {"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.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)}, {"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)}, @@ -762,4 +764,5 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = { {"DeleteStreamOutput", "MGPStreamOutputBind", static_cast(MGPWireOp::DeleteStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, {"ReadPixelsToBuffer", "MGPReadbackToBuffer", static_cast(MGPWireOp::ReadPixelsToBuffer), static_cast(kNone), static_cast(kWaitNone)}, {"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast(MGPWireOp::GetTextureImageToBuffer), static_cast(kNone), static_cast(kWaitNone)}, + {"BindContext", "MGPBindContext", static_cast(MGPWireOp::BindContext), static_cast(kNone), static_cast(kWaitNone)}, }; diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index d0541a44..4d9511e8 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -1120,6 +1120,52 @@ namespace MobileGL::MG_Remote::Client { Uint64 g_applierResetContextSerial = 0; } // 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() { // 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: diff --git a/MobileGL/MG_Remote/Client/WireTables.h b/MobileGL/MG_Remote/Client/WireTables.h index 1cc75f59..eb1ccfb0 100644 --- a/MobileGL/MG_Remote/Client/WireTables.h +++ b/MobileGL/MG_Remote/Client/WireTables.h @@ -125,6 +125,43 @@ namespace MobileGL::MG_Remote::Client { // caller then makes the direct call the record replaced. 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, // 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 diff --git a/MobileGL/MG_Remote/FatalFunnel.cpp b/MobileGL/MG_Remote/FatalFunnel.cpp index c7cb0ef6..0ed34eb8 100644 --- a/MobileGL/MG_Remote/FatalFunnel.cpp +++ b/MobileGL/MG_Remote/FatalFunnel.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -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 // 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. - std::atomic g_latchArmed{false}; - std::atomic g_latched{false}; - std::atomic g_latchCount{0}; - std::mutex g_latchMutex; - MGFatalFamily g_latchedFamily = MGFatalFamily::ProtocolCorruption; - char g_latchedLine[512] = {}; + // + // P14 S2: ONE OF THESE PER SESSION (FatalFunnel.h's domain block). A domain's storage is + // heap-allocated by FatalLatchDomainOpen and lives until FatalLatchDomainClose; domain 0 + // is this process-wide one, which is what every thread that binds nothing uses. + struct LatchState { + int id = 0; + std::atomic armed{false}; + std::atomic latched{false}; + std::atomic 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 g_domains; + int g_nextDomain = 1; + + LatchState& Latch() { return *t_latch; } } // namespace - void ArmSessionLatch() { g_latchArmed.store(true, std::memory_order_release); } + int FatalLatchDomainOpen() { + const std::lock_guard 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 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 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() { - const std::lock_guard lock(g_latchMutex); - g_latchArmed.store(false, std::memory_order_release); - g_latchCount.store(0, std::memory_order_relaxed); - g_latchedFamily = MGFatalFamily::ProtocolCorruption; - g_latchedLine[0] = '\0'; - g_latched.store(false, std::memory_order_release); + LatchState& latch = Latch(); + const std::lock_guard lock(latch.mutex); + latch.armed.store(false, std::memory_order_release); + latch.count.store(0, std::memory_order_relaxed); + latch.latchedFamily = MGFatalFamily::ProtocolCorruption; + 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() { - const std::lock_guard lock(g_latchMutex); - return g_latchedFamily; + LatchState& latch = Latch(); + const std::lock_guard 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, ...) { char line[512]; @@ -135,23 +191,25 @@ namespace MobileGL::MG_Remote { FormatFaultLine(line, fmt, args); va_end(args); + LatchState& latch = Latch(); + // 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). - if (!g_latchArmed.load(std::memory_order_acquire)) { + if (!latch.armed.load(std::memory_order_acquire)) { SessionFail(family, "%s", line); } 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); bool first = false; { - const std::lock_guard lock(g_latchMutex); - if (!g_latched.load(std::memory_order_relaxed)) { - std::snprintf(g_latchedLine, sizeof(g_latchedLine), "%s", line); - g_latchedFamily = family; - g_latched.store(true, std::memory_order_release); + const std::lock_guard lock(latch.mutex); + if (!latch.latched.load(std::memory_order_relaxed)) { + std::snprintf(latch.latchedLine, sizeof(latch.latchedLine), "%s", line); + latch.latchedFamily = family; + latch.latched.store(true, std::memory_order_release); first = true; } } diff --git a/MobileGL/MG_Remote/FatalFunnel.h b/MobileGL/MG_Remote/FatalFunnel.h index 0f89bb75..025f6831 100644 --- a/MobileGL/MG_Remote/FatalFunnel.h +++ b/MobileGL/MG_Remote/FatalFunnel.h @@ -62,8 +62,10 @@ namespace MobileGL::MG_Remote { // 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: - // ServerMain::RunSession, i.e. the spawn / TCP session child (with or without --serve), whose - // process IS the session. Unarmed, SessionLatch IS SessionFail - same line, same abort - and + // ServerMain::RunSession, i.e. a served session - the spawn / TCP session child (with or + // 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: // * 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 @@ -85,6 +87,10 @@ namespace MobileGL::MG_Remote { // 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 // 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(); // True once the first fault has latched. One acquire load: DrainRing asks it before every pop. bool SessionLatched(); @@ -95,6 +101,31 @@ namespace MobileGL::MG_Remote { // Every fault this armed process latched or declined after the first (>= 1 once latched). ::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 // 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. diff --git a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp index 608bd870..12770443 100644 --- a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp +++ b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp @@ -51,10 +51,20 @@ namespace MobileGL::MG_Remote { // cp's one schema addition, on the same append-only terms. static_assert(static_cast(SurfaceControlOp::InitCapabilities) == static_cast(::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(SurfaceControlOp::CreateContext) == + static_cast(::MobileGL::Wire::SurfaceOpKind::CreateContext)); + static_assert(static_cast(SurfaceControlOp::DestroyContext) == + static_cast(::MobileGL::Wire::SurfaceOpKind::DestroyContext)); static_assert(static_cast(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 && static_cast(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 && static_cast(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10 && - static_cast(::MobileGL::Wire::SurfaceOpKind::InitCapabilities) == 11); + static_cast(::MobileGL::Wire::SurfaceOpKind::InitCapabilities) == 11 && + static_cast(::MobileGL::Wire::SurfaceOpKind::CreateContext) == 12 && + static_cast(::MobileGL::Wire::SurfaceOpKind::DestroyContext) == 13); static_assert(static_cast(::MobileGL::Wire::WindowKind::MetalLayer) == 6); // P12's append, on the same terms: the value is wire ABI. static_assert(static_cast(::MobileGL::Wire::WindowKind::ServerOwned) == 7); @@ -112,7 +122,7 @@ namespace MobileGL::MG_Remote { // Fatal{ProtocolCorruption} at ServerApplyWireSurfaceOp rather than as // the missing row it is. if (::flatbuffers::IsOutRange(kind, ::MobileGL::Wire::SurfaceOpKind::InitializeDisplay, - ::MobileGL::Wire::SurfaceOpKind::InitCapabilities)) { + ::MobileGL::Wire::SurfaceOpKind::DestroyContext)) { return false; } const SurfaceControlOp op = static_cast(static_cast(kind)); @@ -174,7 +184,8 @@ namespace MobileGL::MG_Remote { } const auto op = ::MobileGL::Wire::CreateSurfaceOp( *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()); ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope); return SurfaceWireError::None; @@ -195,6 +206,8 @@ namespace MobileGL::MG_Remote { decoded.width = op.width(); decoded.height = op.height(); decoded.swapInterval = op.swapInterval(); + decoded.shareGroupToken = op.shareGroupToken(); + decoded.contextFlags = op.contextFlags(); if (OpNeedsAWindowBackend(kind)) { if (op.windowKind() == ::MobileGL::Wire::WindowKind::ServerOwned) { // P12 (D2). Asked BEFORE the Android refusal below, which it is not: the window is diff --git a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h index dc1f997c..8891c799 100644 --- a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h +++ b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h @@ -285,11 +285,13 @@ enum class SurfaceOpKind : uint8_t { ReleaseResources = 9, SetWindowHandle = 10, InitCapabilities = 11, + CreateContext = 12, + DestroyContext = 13, MIN = None, - MAX = InitCapabilities + MAX = DestroyContext }; -inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[12] { +inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[14] { static const SurfaceOpKind values[] = { SurfaceOpKind::None, SurfaceOpKind::InitializeDisplay, @@ -302,13 +304,15 @@ inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[12] { SurfaceOpKind::SetSwapInterval, SurfaceOpKind::ReleaseResources, SurfaceOpKind::SetWindowHandle, - SurfaceOpKind::InitCapabilities + SurfaceOpKind::InitCapabilities, + SurfaceOpKind::CreateContext, + SurfaceOpKind::DestroyContext }; return values; } inline const char * const *EnumNamesSurfaceOpKind() { - static const char * const names[13] = { + static const char * const names[15] = { "None", "InitializeDisplay", "CreateWindowSurface", @@ -321,13 +325,15 @@ inline const char * const *EnumNamesSurfaceOpKind() { "ReleaseResources", "SetWindowHandle", "InitCapabilities", + "CreateContext", + "DestroyContext", nullptr }; return names; } 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(e); return EnumNamesSurfaceOpKind()[index]; } @@ -1809,7 +1815,9 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_HEIGHT = 18, VT_SWAPINTERVAL = 20, VT_READSURFACE = 22, - VT_CONTEXT = 24 + VT_CONTEXT = 24, + VT_SHAREGROUPTOKEN = 26, + VT_CONTEXTFLAGS = 28 }; uint64_t seq() const { return GetField(VT_SEQ, 0); @@ -1844,6 +1852,12 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint64_t context() const { return GetField(VT_CONTEXT, 0); } + uint64_t shareGroupToken() const { + return GetField(VT_SHAREGROUPTOKEN, 0); + } + uint32_t contextFlags() const { + return GetField(VT_CONTEXTFLAGS, 0); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -1858,6 +1872,8 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyField(verifier, VT_SWAPINTERVAL, 4) && VerifyField(verifier, VT_READSURFACE, 8) && VerifyField(verifier, VT_CONTEXT, 8) && + VerifyField(verifier, VT_SHAREGROUPTOKEN, 8) && + VerifyField(verifier, VT_CONTEXTFLAGS, 4) && verifier.EndTable(); } }; @@ -1899,6 +1915,12 @@ struct SurfaceOpBuilder { void add_context(uint64_t context) { fbb_.AddElement(SurfaceOp::VT_CONTEXT, context, 0); } + void add_shareGroupToken(uint64_t shareGroupToken) { + fbb_.AddElement(SurfaceOp::VT_SHAREGROUPTOKEN, shareGroupToken, 0); + } + void add_contextFlags(uint32_t contextFlags) { + fbb_.AddElement(SurfaceOp::VT_CONTEXTFLAGS, contextFlags, 0); + } explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -1922,14 +1944,18 @@ inline ::flatbuffers::Offset CreateSurfaceOp( int32_t height = 0, int32_t swapInterval = 0, uint64_t readSurface = 0, - uint64_t context = 0) { + uint64_t context = 0, + uint64_t shareGroupToken = 0, + uint32_t contextFlags = 0) { SurfaceOpBuilder builder_(_fbb); + builder_.add_shareGroupToken(shareGroupToken); builder_.add_context(context); builder_.add_readSurface(readSurface); builder_.add_nativeToken(nativeToken); builder_.add_surface(surface); builder_.add_display(display); builder_.add_seq(seq); + builder_.add_contextFlags(contextFlags); builder_.add_swapInterval(swapInterval); builder_.add_height(height); builder_.add_width(width); diff --git a/MobileGL/MG_Remote/Protocol/mg_protocol_base.h b/MobileGL/MG_Remote/Protocol/mg_protocol_base.h index 81409bf9..016761c7 100644 --- a/MobileGL/MG_Remote/Protocol/mg_protocol_base.h +++ b/MobileGL/MG_Remote/Protocol/mg_protocol_base.h @@ -70,7 +70,9 @@ extern "C" { // 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. // 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_MAJOR_OF(version) ((uint32_t)(version) >> 16) diff --git a/MobileGL/MG_Remote/Protocol/protocol.fbs b/MobileGL/MG_Remote/Protocol/protocol.fbs index d4cf2852..acb8d29c 100644 --- a/MobileGL/MG_Remote/Protocol/protocol.fbs +++ b/MobileGL/MG_Remote/Protocol/protocol.fbs @@ -256,6 +256,25 @@ enum SurfaceOpKind : ubyte { // (Client/BackendObject_Remote.cpp). Without the tag the codec refused it by // name and every spawned eglMakeCurrent failed with InprocOnlyOpOnTheWire. 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 { @@ -303,6 +322,12 @@ table SurfaceOp { // ("no surface" / "no context") for every other op and for a ReleaseCurrent. readSurface: 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 { diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index a2e2ff5a..70b0b5ec 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -1462,6 +1462,53 @@ namespace MobileGL::MG_Remote::Server { 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(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) { // §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, diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index e799036c..f5e8e806 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -245,11 +245,21 @@ namespace MobileGL::MG_Remote::Server { Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) 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 // may not arm against a stub). ApplierResets counts the records ACCEPTED (serial // checked, reset run); ObjectDeaths counts every record the sink dispatched. Uint64 ApplierResets() const { return m_applierResets; } 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 // assert the sequence rather than only the count. Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; } @@ -377,6 +387,11 @@ namespace MobileGL::MG_Remote::Server { Uint64 m_applierResetSerial = 0; Uint64 m_applierResets = 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 writes into a caller buffer, so one staging vector per session sits // between them. Grown, never shrunk, and never handed out past the call. diff --git a/MobileGL/MG_Remote/Server/ServerDisplay.h b/MobileGL/MG_Remote/Server/ServerDisplay.h index e195f7f1..8b7ee2b0 100644 --- a/MobileGL/MG_Remote/Server/ServerDisplay.h +++ b/MobileGL/MG_Remote/Server/ServerDisplay.h @@ -82,7 +82,10 @@ namespace MobileGL::MG_Remote::Server { NoWindow, // a display, but no window was attached within the wait Cancelled, // the caller's cancel predicate said so 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 { diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index fab60fb4..bd3cad08 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -10,12 +10,14 @@ #include "ServerLoop.h" #include "ApplyThreadPolicy.h" +#include "SessionRuntime.h" #include #include #include #include #include +#include #include #include @@ -284,6 +286,8 @@ namespace MobileGL::MG_Remote::Server { m_haveCurrentTuple = false; // 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. + // 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_holdsWindowLease = false; m_serverOwnedSurfaces.clear(); @@ -345,6 +349,11 @@ namespace MobileGL::MG_Remote::Server { // Apply thread only (RunSurfaceControlFrame's dispatch put us here), so // m_haveCurrentTuple/m_cur* need no // 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; const EglBindAction action = ClassifyEglMakeCurrent(m_haveCurrentTuple, m_curDpy, m_curDraw, 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 // 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 - // look like client reads to the guards. Relaxed for the reason stated beside the key's - // 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); + // look like client reads to the guards. + // + // 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 scope; + if (m_runtime != nullptr) scope = std::make_unique(*m_runtime); + Detail::t_onApplyThread = true; + Detail::g_applyThreadKey.fetch_add(1, std::memory_order_relaxed); NameThisThread("mgl-srv-apply"); // 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 @@ -426,7 +444,6 @@ namespace MobileGL::MG_Remote::Server { // "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), // and so does the spin before each park. - ServerSession& session = *m_session; const char* raw = AffinityStringFromConfig(); const Bool automatic = raw == nullptr || raw[0] == '\0' || std::strcmp(raw, "auto") == 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 // thing that would catch that, and it would catch it as a hang, not as a failed // 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(); @@ -1189,6 +1211,10 @@ namespace MobileGL::MG_Remote::Server { } 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. static ServerLoop& instance = *new ServerLoop{}; return instance; @@ -1360,7 +1386,9 @@ namespace MobileGL::MG_Remote::Server { return MOBILEGL_OK; case SurfaceControlOp::CreatePbufferSurface: // 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)) { frame.ok = false; frame.refusal = static_cast(SurfaceRefusalCode::SurfaceModeMismatch); @@ -1437,6 +1465,48 @@ namespace MobileGL::MG_Remote::Server { frame.ok = outcome.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: backend->SetEGLSwapInterval(frame.swapInterval); frame.ok = true; @@ -1590,10 +1660,17 @@ namespace MobileGL::MG_Remote::Server { break; } 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 " - "(%s after at most %u ms) - the display's surface is not up. Refused (NoServerWindow); the " - "session is not latched", - width, height, ServerWindowAcquireName(acquired), timeoutMs); + "(%s after at most %u ms)%s. Refused (NoServerWindow); the session is not latched", + 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; } @@ -1603,7 +1680,9 @@ namespace MobileGL::MG_Remote::Server { frame.refusal = static_cast(SurfaceRefusalCode::None); const Uint32 wantWidth = frame.width > 0 ? static_cast(frame.width) : 0u; const Uint32 wantHeight = frame.height > 0 ? static_cast(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)) { frame.refusal = static_cast(SurfaceRefusalCode::SurfaceModeMismatch); 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; } + // 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) { SurfaceControlFrame frame; frame.kind = SurfaceControlOp::SwapBuffersInprocOnly; diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h index c0ff6e4f..b157eb90 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.h +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -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 // 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. + // 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 { - // RELAXED ON BOTH SIDES, and that is a claim about recycling, not a shrug. The only - // dangerous outcome would be a NEW thread that reuses the exited apply thread's TLS - // block (hence its thread pointer) while still reading the pre-clear key. It cannot - // happen: the clear is sequenced before the apply thread's exit, the exit releases the - // TLS block under libc's own lock, and the thread that later receives that block takes - // 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 g_applyThreadKey{0}; + // A fresh thread's TLS word is zero-initialized, so a thread that inherits the exited + // apply thread's TLS block - the recycling case the old relaxed-load argument was about - + // starts out answering false and can only become the apply thread by running + // ApplyThreadMain. There is no stale value to read and nothing to order. + inline thread_local Bool t_onApplyThread = false; - // The calling thread's identity as the same integer. aarch64 - the split's only - // measured target - reads TPIDR_EL0 with a single instruction and no call. Everywhere - // else falls back to std::this_thread::get_id() memcpy'd into the key: still one libc - // call, but the atomic load and the out-of-line pthread_equal that - // 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(reinterpret_cast(__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 has padding on this platform: copying its object " - "representation would make the apply-thread key unstable within one " - "thread. Hash the id (std::hash) instead."); - Uint64 key = 0; - std::memcpy(&key, &id, sizeof(id)); - return key; -#endif - } + // The count of live apply threads, as a relaxed atomic: an identity-like gauge, not a + // handshake (the real ordering lives in the ring, the doorbell and m_controlMutex), and + // its one reader wants "is it zero". + inline std::atomic g_applyThreadKey{0}; } // 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 // 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. + // + // 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 }; const char* SessionSurfaceModeName(SessionSurfaceMode mode); // The decision, pure, so a unit case can drive all six pairs without a backend. 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 { public: // Creates the apply thread, names it mgl-srv-apply, applies @@ -214,6 +192,11 @@ namespace MobileGL::MG_Remote::Server { // landing on fast cores. 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 // ON THAT THREAD before it exits. Blocking by contract - see the header note. 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 // share of the cost of the answer. static Bool OnApplyThread() { - const Uint64 key = Detail::g_applyThreadKey.load(std::memory_order_relaxed); - // Short-circuit ON PURPOSE: with TransportMode::Monolith, and in every process - // before Start() and after Stop(), the key is 0 and the caller pays one relaxed - // load and one branch - it never even reads its own identity. - return key != 0 && key == Detail::CurrentThreadKey(); + // P14 S2: the caller's own TLS word. One constant-initialized load, no atomic, no + // comparison and - unlike the process-wide key - the right answer with N sessions. + return Detail::t_onApplyThread; } // 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); // AcquireFor's cancel predicate: the loop is stopping, or the session latched. 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; + // 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. Bool m_holdsWindowLease = false; // 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 // 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. + // + // 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; EGLDisplay m_curDpy = EGL_NO_DISPLAY; 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 // OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit. 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); void ServerSetEGLSwapInterval(Int interval); void ServerReleaseEGLSurface(EGLSurface surface); diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index 228e1010..d696f304 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -14,6 +14,7 @@ #include "ServerDisplay.h" #include "ServerLoop.h" #include "ServerSession.h" +#include "SessionRuntime.h" #include "SurfaceControlFrame.h" #include #include @@ -343,7 +344,7 @@ std::atomic g_inProcessBackendPin{-1}; // in the disaggregated server, does nothing unless set, and says so when it is. void DelaySessionHandshakeForTest() { 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); if (ms <= 0) return; 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 control, std::vector(); + Server::Detail::ThreadSessionScope threadScope(*runtime); ArmSessionLatch(); // The child bypasses MobileGL::Initialize: arm its own role counters. MobileGL::MG_Util::PipeStats::Init(); - auto& session = Server::ServerSessionInstance(); - auto& loop = Server::ServerLoopInstance(); + auto& session = runtime->Session(); + auto& loop = runtime->Loop(); // 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 // 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 control, std::vector(std::min(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() { - // Run() only returns with the session thread joined; a listener failure (73) is the - // one way out with it live, and it is never left running behind the supervisor. - if (m_sessionThread.joinable()) m_sessionThread.join(); + // Run() only returns with every session thread joined; a listener failure (73) is the one + // way out with one live, and a session is never left running behind the supervisor. + for (auto& session : m_live) { + if (session->thread.joinable()) session->thread.join(); + } } int Run() { @@ -1352,15 +1394,11 @@ public: static_cast(accepted)); return 73; } - // exit_group's close window, as the forked loop's: a client reconnecting the moment - // its session ended must not be told Busy by a session that is exiting. - const auto reapDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(50); - while (m_active > 0 && std::chrono::steady_clock::now() < reapDeadline) { - if (Reap()) break; - ::usleep(1000); - } - if (m_active > 0) { - RefuseBusy(*control, "one session is already active"); + // P14 S2: THE CAP, NOT "ONE AT A TIME". A peer over the limit is refused by the same + // words the forked `--max-sessions` supervisor uses, and is not a latch: the sessions + // already running carry on and the connection is simply closed. + if (m_live.size() >= m_maxSessions) { + RefuseBusy(*control, "the session limit is reached"); continue; } StartSessionThread(std::move(control)); @@ -1368,48 +1406,71 @@ public: } private: - // The same reap as the TCP shape's ReapSessionThread: join, count faults, reset the latch - // for the next session in this process. - bool Reap() { - if (m_active <= 0 || !m_sessionDone.load(std::memory_order_acquire)) return false; - if (m_sessionThread.joinable()) m_sessionThread.join(); - const int code = m_sessionExit.load(std::memory_order_acquire); - m_active = -1; - if (code != 0) ++m_sessionsFaulted; - ResetSessionLatch(); - WireLogError("MG_Remote server: in-process unix session #%lu pid=%d reaped exit=%d%s sessionsFaulted=%lu", - m_sessionOrdinal, static_cast(::getpid()), code, - code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted); - return true; + // One live session: its control thread, and the two facts the reap needs from it. The session + // itself (its ServerSession, its apply thread, its PipeInputs block) is owned by RunSession's + // own frame, on that thread - nothing here reaches into it. + struct LiveSession { + std::thread thread; + std::atomic done{false}; + std::atomic exit{0}; + unsigned long long ordinal = 0; + }; + + // The same reap as the TCP shape's ReapSessionThread: join, count faults, reset the latch for + // the next session in this process. Every finished session is reaped, in whatever order they + // 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(live.ordinal), + static_cast(::getpid()), code, + code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted, + m_live.size() - 1); + session = m_live.erase(session); + } } void StartSessionThread(std::unique_ptr connection) { - if (m_sessionThread.joinable()) m_sessionThread.join(); // reaped already; never live here - m_sessionDone.store(false, std::memory_order_release); - m_sessionExit.store(0, std::memory_order_release); - ++m_sessionOrdinal; + // No hand-off pair and no pre-read Hello: the session reads its own first frame and binds + // its shm segments from the fds the pair's second connection carries. + auto live = std::make_unique(); + live->ordinal = ++m_sessionOrdinal; try { - // No hand-off pair and no pre-read Hello: the session reads its own first frame and - // binds its shm segments from the fds the pair's second connection carries. - m_sessionThread = std::thread([this, conn = std::move(connection)]() mutable { + live->thread = std::thread([live = live.get(), conn = std::move(connection)]() mutable { const int code = RunSession(std::move(conn), {}, {}, -1, DataSource::None, 10000, /*inProcess=*/true); - m_sessionExit.store(code, std::memory_order_release); - m_sessionDone.store(true, std::memory_order_release); + live->exit.store(code, std::memory_order_release); + live->done.store(true, std::memory_order_release); }); } catch (...) { 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)"); return; } - m_active = ::getpid(); - WireLogError("MG_Remote server: in-process unix session #%lu started on a thread (pid=%d)", - m_sessionOrdinal, static_cast(::getpid())); + m_live.push_back(std::move(live)); + WireLogError("MG_Remote server: in-process unix session #%llu started on a thread (pid=%d, " + "%zu live of at most %u; %u accepted in this process)", + static_cast(m_sessionOrdinal), + static_cast(::getpid()), m_live.size(), m_maxSessions, + Server::LiveServerSessionCount()); } int StopServing() { - while (m_active > 0) { - if (!Reap()) ::usleep(10000); + // P14 S2: EVERY live session, not one. Each reads g_inProcessStop on its own control + // 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(::getpid())); return 0; @@ -1419,11 +1480,10 @@ private: // The connections that have not been paired yet live here between Accept calls. Server::PairAcceptor m_pairs; unsigned long& m_sessionsFaulted; - std::thread m_sessionThread; - std::atomic m_sessionDone{false}; - std::atomic m_sessionExit{0}; - unsigned long m_sessionOrdinal = 0; - pid_t m_active = -1; + unsigned m_maxSessions; + // P14 S2: every live session, not one. Reaped by Run()'s Reap and by StopServing. + std::vector> m_live; + unsigned long long m_sessionOrdinal = 0; }; // 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); result = supervisor.Run(); } else { - UnixInProcessSupervisor supervisor(listener, sessionsFaulted); + UnixInProcessSupervisor supervisor(listener, sessionsFaulted, + UnixInProcessSupervisor::MaxSessionsFromEnvironment()); result = supervisor.Run(); } LogSupervisorSummary(sessionsFaulted); diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index faa7ac28..b207a070 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -10,6 +10,7 @@ #include "../Transport/SocketTransport.h" #include "ServerSession.h" +#include "SessionRuntime.h" #include #include "../CapsCodec.h" @@ -528,11 +529,24 @@ namespace MobileGL::MG_Remote::Server { // One callback table and one process segment resolver: an in-progress Accept // reserves their owner too. CAS closes the race between distinct session objects // 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 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 g_liveSessions{0}; } // namespace + Uint32 ServerSessionCount() { return g_liveSessions.load(std::memory_order_acquire); } + ServerSession& ServerSessionInstance() { + if (ServerSession* const own = Detail::CurrentSession()) return *own; // 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 // use-after-free rather than a tidy teardown. @@ -540,7 +554,10 @@ namespace MobileGL::MG_Remote::Server { 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() { m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer); @@ -584,8 +601,17 @@ namespace MobileGL::MG_Remote::Server { MobileGLResult ServerSession::Accept(Transport::ITransport& transport, const std::vector* 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; - 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)) { if (expectedOwner == this) return MOBILEGL_ERR_INVALID_ARGUMENT; 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_adoptInbox.Clear(); 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(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. MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED; @@ -844,6 +883,10 @@ namespace MobileGL::MG_Remote::Server { m_segments.InstallProcessResolver(); 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 // §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() { + // 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; g_active.compare_exchange_strong(expectedActive, nullptr, std::memory_order_acq_rel, std::memory_order_acquire); if (m_accepted) { - // Uninstall AFTER the apply thread has joined, never before: a record still in - // flight can still resolve a segment offset (table 3's fourth column). - Wire::SegmentTable::UninstallProcessResolver(); - // The reverse-channel producers go with the session that owns them - release - // only the entries that are still OURS, never one a later owner installed. - if (MG_Pipe::gMGPipeCallbacks.OnGlError == &ServerOnGlError) { - MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr; - } - if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) { - MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = 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; + // P14 S2: DROP THIS SESSION FIRST, THEN ASK WHETHER ANY IS LEFT. The two process-wide + // hooks below (the segment resolver and the reverse-channel table) are shared by every + // session in the process, so a session that ends while another is live must leave both + // installed - its own threads are done with them (the apply thread is joined above and + // this control thread unbinds its own binding on the way out of RunSession). + const Uint32 remaining = g_liveSessions.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (remaining == 0) { + // Uninstall AFTER the apply thread has joined, never before: a record still in + // flight can still resolve a segment offset (table 3's fourth column). + Wire::SegmentTable::UninstallProcessResolver(); + // The reverse-channel producers go with the session that owns them - release + // only the entries that are still OURS, never one a later owner installed. + if (MG_Pipe::gMGPipeCallbacks.OnGlError == &ServerOnGlError) { + MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr; + } + if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) { + MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = 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 @@ -1205,6 +1260,61 @@ namespace MobileGL::MG_Remote::Server { 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(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(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::RingControl& ServerSession::Control() { diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index b11963af..11cbb285 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -65,6 +65,10 @@ namespace MobileGL::MG_Remote::Server { class ServerSession { 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(); ServerSession(); @@ -190,6 +194,11 @@ namespace MobileGL::MG_Remote::Server { Bool PeerSharedSegments() const { return m_peerSharedSegments; } // 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). + // + // 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(); Transport::SessionSegments& Shm(); @@ -322,6 +331,45 @@ namespace MobileGL::MG_Remote::Server { Bool AdoptT0Quiet() const { return m_adoptT0Quiet; } 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(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: MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::ILink& link, const Uint8* nonce); @@ -347,6 +395,9 @@ namespace MobileGL::MG_Remote::Server { Bool m_consumedSet = false; Bool m_sizesSet = 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; Bool m_peerSharedSegments = true; // 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_t0Declined = 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 m_contexts; + std::atomic 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 // 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(); + // 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 diff --git a/MobileGL/MG_Remote/Server/SessionRuntime.cpp b/MobileGL/MG_Remote/Server/SessionRuntime.cpp new file mode 100644 index 00000000..72301170 --- /dev/null +++ b/MobileGL/MG_Remote/Server/SessionRuntime.cpp @@ -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 +#include +#include + +#include + +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(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(&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(&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 \ No newline at end of file diff --git a/MobileGL/MG_Remote/Server/SessionRuntime.h b/MobileGL/MG_Remote/Server/SessionRuntime.h new file mode 100644 index 00000000..c7f7aec8 --- /dev/null +++ b/MobileGL/MG_Remote/Server/SessionRuntime.h @@ -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 +#include + +#include "ServerLoop.h" +#include "ServerSession.h" + +#include + +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 diff --git a/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp b/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp index 2f5c5dbd..ec89ef46 100644 --- a/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp +++ b/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp @@ -24,6 +24,8 @@ namespace MobileGL::MG_Remote::Server { case SurfaceControlOp::ReleaseResources: return "ReleaseResources"; case SurfaceControlOp::SetWindowHandle: return "SetWindowHandle"; case SurfaceControlOp::InitCapabilities: return "InitCapabilities"; + case SurfaceControlOp::CreateContext: return "CreateContext"; + case SurfaceControlOp::DestroyContext: return "DestroyContext"; case SurfaceControlOp::SwapBuffersInprocOnly: return "SwapBuffersInprocOnly"; case SurfaceControlOp::InitWindowSurfaceInprocOnly: return "InitWindowSurfaceInprocOnly"; 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 // cross, because under spawn the apply thread is in another process. 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; default: return false; diff --git a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h index c7811b4a..60000feb 100644 --- a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h +++ b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h @@ -65,13 +65,25 @@ namespace MobileGL::MG_Remote::Server { // every spawned eglMakeCurrent died at "surface op kind 11 cannot cross // the wire (InprocOnlyOpOnTheWire)", one call after the backend came up. 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 // 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 // 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 - ProbeForTesting = 14, // MG_Test's arbitrary-work seam through the same channel + // + // THEIR VALUES MOVED 12/13/14 -> 14/15/16 when P14 S1 appended the two context ops. That + // 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. @@ -119,6 +131,11 @@ namespace MobileGL::MG_Remote::Server { Int width = 0; Int height = 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 blocking handshake returns. `ok` is the forwarder's Bool answer; eglMajor/eglMinor // 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 // without extending this binding is a compile error, so new fields cannot evade it. const auto& [kind, seq, display, surface, readSurface, context, windowBackend, - nativeToken, width, height, swapInterval, ok, eglMajor, eglMinor, eventHead, - refusal] = frame; + nativeToken, width, height, swapInterval, shareGroupToken, contextFlags, ok, + eglMajor, eglMinor, eventHead, refusal] = frame; return IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && - IsSurfaceControlValue && IsSurfaceControlValue && + IsSurfaceControlValue && + IsSurfaceControlValue && + IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue; } diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index b9b2ad97..5d3463c1 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -263,7 +263,8 @@ namespace MobileGL::MG_Remote::Wire { X(SetProgramBindings, MGPProgramBindings) \ X(DeleteStreamOutput, MGPStreamOutputBind) \ X(ReadPixelsToBuffer, MGPReadbackToBuffer) \ - X(GetTextureImageToBuffer, MGPReadbackToBuffer) + X(GetTextureImageToBuffer, MGPReadbackToBuffer) \ + X(BindContext, MGPBindContext) namespace { @@ -375,9 +376,19 @@ namespace MobileGL::MG_Remote::Wire { // 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. + // + // 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; + thread_local SegmentTable* t_threadResolverTable = nullptr; 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) { return nullptr; } @@ -484,24 +495,35 @@ namespace MobileGL::MG_Remote::Wire { return static_cast(view.Base) + offset; } + void SegmentTable::BindThreadResolver() { t_threadResolverTable = this; } + + void SegmentTable::UnbindThreadResolver() { t_threadResolverTable = nullptr; } + void SegmentTable::InstallProcessResolver() { - if (MG_Pipe::gMGPipeSegmentResolver != nullptr && g_processResolverTable != this) { - // Table 3: there is exactly ONE gMGPipeSegmentResolver per process, the SERVER - // role installs it before the apply thread starts, and the client never resolves a - // span at all. Two roles racing on one inline variable is loud rather than silent - // because of this line. + if (t_threadResolverTable != nullptr && t_threadResolverTable != this) { + // Table 3, P14 S2: one thread resolves through ONE table. Two tables on one thread is + // the shape this line exists to make loud - a session's threads are per session, so a + // well-formed process never reaches it, while a second SESSION (its own threads, its + // own table) is exactly what the per-thread binding is for and does not. WireProtocolFatal("SegmentTable::InstallProcessResolver", - "a process segment resolver is already installed; the server role " - "installs it once, before the apply thread starts"); + "a process segment resolver is already installed ON THIS THREAD for " + "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; - MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk; + BindThreadResolver(); } void SegmentTable::UninstallProcessResolver() { // Teardown order (table 3): uninstall AFTER the join, never before - a record still in - // flight can still resolve. - MG_Pipe::gMGPipeSegmentResolver = nullptr; + // flight can still resolve. P14 S2: this unbinds the calling thread and drops the + // 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; } @@ -2533,6 +2555,16 @@ namespace MobileGL::MG_Remote::Wire { return m_verbs != nullptr && m_verbs->OnObjectDeath(*static_cast(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(payload)); + // ---- 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 diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 6cef4e4f..8eca0bfd 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -95,10 +95,23 @@ namespace MobileGL::MG_Remote::Wire { const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const; // 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 - // the failure this function exists to make loud. + // a DIFFERENT table is already installed ON THE CALLING THREAD, because one thread + // 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(); + // 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(); + // P14 S2: binds the CALLING thread to this table (ThreadSessionScope's two calls). + void BindThreadResolver(); + static void UnbindThreadResolver(); private: Transport::ILink* m_link = nullptr; @@ -661,6 +674,19 @@ namespace MobileGL::MG_Remote::Wire { (void)death; 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. diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index 5e4d01d5..b67f264c 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -233,6 +233,11 @@ namespace MobileGL { 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); DestroyPendingSurfaceIfUnused(drawSurface); DestroyPendingSurfaceIfUnused(readSurface); @@ -306,6 +311,12 @@ namespace MobileGL { 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); } else { ++threadIt; @@ -654,6 +665,30 @@ namespace MobileGL { .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 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(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(shareGroup); + if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_CLIENT_VERSION); value) { contextObject.ClientVersion = *value; contextObject.MajorVersion = *value; @@ -779,6 +814,37 @@ namespace MobileGL { 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 lock(m_mutex); + const auto* ctx = TryGetContext(context); + return ctx != nullptr ? ctx->ClientContextToken : 0; + } + + Uint64 EGLContext::GetContextShareGroupToken(EGLContextHandle context) const { + const std::lock_guard lock(m_mutex); + const auto* ctx = TryGetContext(context); + return ctx != nullptr ? ctx->ShareGroupToken : 0; + } + + SharedPtr EGLContext::GetContextGLState(EGLContextHandle context) const { + const std::lock_guard lock(m_mutex); + const auto* ctx = TryGetContext(context); + return ctx != nullptr ? ctx->GLStateObject : nullptr; + } + + Uint64 EGLContext::CurrentContextClientToken() const { + const std::lock_guard 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 { const std::lock_guard lock(m_mutex); 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 restoreThreadGLState; + if (const auto currentIt = m_threadCurrents.find(threadKey); + currentIt != m_threadCurrents.end()) { + restoreThreadGLState = currentIt->second.RestoreThreadGLState; + } else { + restoreThreadGLState = pGLContext; + } + ReleaseThreadUnlocked(threadKey); m_threadCurrents[threadKey] = ThreadCurrentState{ @@ -1283,9 +1359,15 @@ namespace MobileGL { .DrawSurface = draw, .ReadSurface = read, .Context = context, + .RestoreThreadGLState = Move(restoreThreadGLState), }; if (context != nullptr) { 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; } diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index 9af78056..cc422c0c 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -9,6 +9,7 @@ #pragma once #include #include +#include namespace MobileGL { namespace MG_State { @@ -69,6 +70,20 @@ namespace MobileGL { Bool IsCurrentContextOpenGLCompatibilityProfile() 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 GetContextGLState(EGLContextHandle context) const; + // Surface EGLSurfaceHandle CreateWindowSurface(EGLDisplayHandle display, EGLConfigHandle config, NativeWindowType window, const EGLint* attribList); @@ -180,6 +195,17 @@ namespace MobileGL { EGLDisplayHandle Display = EGL_NO_DISPLAY; EGLConfigHandle Config = 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 GLStateObject; EGLenum ClientAPI = EGL_OPENGL_API; EGLint ClientVersion = 1; EGLint MajorVersion = 1; @@ -226,6 +252,10 @@ namespace MobileGL { EGLSurfaceHandle DrawSurface = EGL_NO_SURFACE; EGLSurfaceHandle ReadSurface = EGL_NO_SURFACE; 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 RestoreThreadGLState; }; template @@ -269,6 +299,9 @@ namespace MobileGL { Uint64 m_nextContextHandle = 1; Uint64 m_nextSyncHandle = 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 m_displayLookup; UnorderedMap m_displays; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.cpp b/MobileGL/MG_State/GLState/BufferState/BufferState.cpp index ddf308cd..b414065a 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.cpp @@ -9,7 +9,7 @@ #include "BufferState.h" namespace MobileGL::MG_State::GLState { - BufferState::BufferState() : m_indexGenerator(1024, 1) { + BufferState::BufferState() { for (SizeT i = 0; i < m_bindingSlots.size(); ++i) { m_bindingSlots[i] = BindingSlot(GlobalBufferTargets[i]); } @@ -18,28 +18,6 @@ namespace MobileGL::MG_State::GLState { } } - const SharedPtr& BufferState::GetBufferObject(Uint index) { - auto it = m_bufferObjects.find(index); - if (it != m_bufferObjects.end()) { - return it->second; - } - static SharedPtr nullBufferObject = nullptr; - return nullBufferObject; - } - - void BufferState::GenerateNames(Uint number, Vector& buffers) { - buffers.resize(number); - m_indexGenerator.Generate(number, buffers.data()); - } - - const SharedPtr& BufferState::CreateBufferObject(Uint index) { - auto& bufferObj = m_bufferObjects[index]; - if (!bufferObj) { - bufferObj = MakeShared(index); - } - return bufferObj; - } - BindingSlot& BufferState::GetBindingSlot(BufferTarget target) { for (auto& bindingSlot : m_bindingSlots) { if (bindingSlot.GetTarget() == target) { @@ -50,53 +28,33 @@ namespace MobileGL::MG_State::GLState { return m_bindingSlots[0]; } - void BufferState::MarkBufferObjectForDeletion(Uint index) { - if (m_indexGenerator.IsValid(index)) { - auto it = m_bufferObjects.find(index); - if (it != m_bufferObjects.end()) { - for (auto& bindingSlot : m_bindingSlots) { - if (bindingSlot.GetBoundObject() == it->second) { - bindingSlot.Bind(nullptr); - } - } - for (auto& bindingPointArray : m_bufferBindPointTargets) { - for (auto& bindingPoint : bindingPointArray) { - if (bindingPoint.GetBoundObject() == it->second) { - bindingPoint.Bind(nullptr); - bindingPoint.ClearRange(); + void BufferState::UnbindBufferObject(const SharedPtr& bufferObject) { + if (!bufferObject) return; + for (auto& bindingSlot : m_bindingSlots) { + if (bindingSlot.GetBoundObject() == bufferObject) { + bindingSlot.Bind(nullptr); + } + } + for (auto& bindingPointArray : m_bufferBindPointTargets) { + for (auto& bindingPoint : bindingPointArray) { + if (bindingPoint.GetBoundObject() == bufferObject) { + bindingPoint.Bind(nullptr); + bindingPoint.ClearRange(); #if MOBILEGL_PIPE_PUSH - // 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 - // 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 - // 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 - // backend for the dead one - Espryt dies with Fatal{ProtocolCorruption, - // "BackendSlotTable.Generation"} (DeletedBoundBufferScenario). - NoteBindPointChanged(BufferBindPointTargets[static_cast( - &bindingPointArray - m_bufferBindPointTargets.data())]); + // 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 + // 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 + // 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 + // backend for the dead one - Espryt dies with Fatal{ProtocolCorruption, + // "BackendSlotTable.Generation"} (DeletedBoundBufferScenario). + NoteBindPointChanged(BufferBindPointTargets[static_cast( + &bindingPointArray - m_bufferBindPointTargets.data())]); #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& BufferState::GetBindingPoint(BufferTarget target, Uint index) { diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.h b/MobileGL/MG_State/GLState/BufferState/BufferState.h index 7a92c2b4..0398d607 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.h @@ -28,13 +28,12 @@ namespace MobileGL::MG_State::GLState { // costs the unused tail of three arrays. 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 { public: BufferState(); - const SharedPtr& GetBufferObject(Uint index); - void GenerateNames(Uint number, Vector& buffers); - const SharedPtr& CreateBufferObject(Uint index); BindingSlot& GetBindingSlot(BufferTarget target); // For glBindBufferBase / glBindBufferRange BindingSlotRange1D& GetBindingPoint(BufferTarget target, Uint index); @@ -60,9 +59,9 @@ namespace MobileGL::MG_State::GLState { if (it == BufferBindPointTargets.end()) return 0; return m_touchedBindPointCount[std::distance(BufferBindPointTargets.begin(), it)]; } - void MarkBufferObjectForDeletion(Uint index); - Bool ValidateName(Uint index) const; - Bool ValidateBufferObject(Uint index) const; + // Detaches a deleted buffer from THIS context's bindings only (GL 4.6 core 6.1: a delete + // resets the deleting context's bindings, and other contexts keep their own). + void UnbindBufferObject(const SharedPtr& bufferObject); #if MOBILEGL_PIPE_PUSH // 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; Array m_bindPointGeneration{}; #endif - UnorderedMap> m_bufferObjects; - IndexGenerator m_indexGenerator; Array, GlobalBufferTargets.size()> m_bindingSlots; // TODO: query the count somewhere globally? // For glBindBufferBase / glBindBufferRange diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 49c1064a..51e1b16b 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -18,9 +18,52 @@ #include namespace MobileGL::MG_State { + namespace { + // Leak-at-exit storage; see GlobalObjects.cpp. + SharedPtr& DefaultGLContextSlot() { + static SharedPtr* slot = new SharedPtr(); + return *slot; + } + + SharedPtr& DefaultFramebufferSlot() { + static SharedPtr* slot = new SharedPtr(); + return *slot; + } + } + + const SharedPtr& ProcessDefaultGLContext() { + auto& slot = DefaultGLContextSlot(); + if (!slot) { + slot = MakeShared(); + } + 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 framebuffer) { + DefaultFramebufferSlot() = Move(framebuffer); + } + + const SharedPtr& DefaultFramebuffer() { + return DefaultFramebufferSlot(); + } + void Init() { MGLOG_D("Initializing MobileGL State..."); - pGLContext = MakeUnique(); + // 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(); + pGLContext = ProcessDefaultGLContext(); pEGLContext = MakeUnique(); } @@ -89,11 +132,11 @@ namespace MobileGL::MG_State { // Buffer void GLContext::GenBufferNames(Uint number, Vector& buffers) { - m_bufferState.GenerateNames(number, buffers); + Group().GenerateBufferNames(number, buffers); } const SharedPtr& GLContext::GetBufferObject(Uint index) { - return m_bufferState.GetBufferObject(index); + return Group().GetBufferObject(index); } BindingSlot& GLContext::GetBufferBindingSlot(BufferTarget target) { @@ -111,7 +154,7 @@ namespace MobileGL::MG_State { } const SharedPtr& GLContext::CreateBufferObject(Uint index) { - return m_bufferState.CreateBufferObject(index); + return Group().CreateBufferObject(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 — // with one VAO per chunk section, vanilla's steady buffer churn made it // 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(); if (vao != nullptr) { 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 { - return m_bufferState.ValidateName(index); + return m_shareGroup->ValidateBufferName(index); } Bool GLContext::ValidateBufferObject(Uint index) const { - return m_bufferState.ValidateBufferObject(index); + return m_shareGroup->ValidateBufferObject(index); } // VertexArray @@ -269,11 +313,11 @@ namespace MobileGL::MG_State { // Texture void GLContext::GenTextureNames(Uint number, Vector& textures) { - m_textureState.GenerateNames(number, textures); + Group().GenerateTextureNames(number, textures); } const SharedPtr& GLContext::GetTextureObject(Uint index) { - return m_textureState.GetTextureObject(index); + return Group().GetTextureObject(index); } const SharedPtr& GLContext::GetDefaultTextureObject(TextureTarget target) const { @@ -281,14 +325,14 @@ namespace MobileGL::MG_State { } const SharedPtr& GLContext::CreateTextureObject(Uint index, TextureTarget target) { - return m_textureState.CreateTextureObject(index, target); + return Group().CreateTextureObject(index, target); } const SharedPtr& GLContext::CreateTextureViewObject( Uint index, TextureTarget target, const SharedPtr& storageOwner, Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) { - return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer, - numLayers); + return Group().CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer, + numLayers); } 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 // 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). - if (const auto& textureObject = m_textureState.GetTextureObject(index)) { + const auto textureObject = Group().GetTextureObject(index); + if (textureObject) { for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount); ++targetIndex) { 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) { @@ -334,11 +382,11 @@ namespace MobileGL::MG_State { } Bool GLContext::ValidateTextureName(Uint index) const { - return m_textureState.ValidateName(index); + return m_shareGroup->ValidateTextureName(index); } Bool GLContext::ValidateTextureObject(Uint index) const { - return m_textureState.ValidateTextureObject(index); + return m_shareGroup->ValidateTextureObject(index); } Int GLContext::GetActiveTextureUnit() const { @@ -351,51 +399,67 @@ namespace MobileGL::MG_State { // Program Uint GLContext::CreateProgram() { - return m_programState.CreateProgram(); + return Group().Programs().CreateProgram(); } Uint GLContext::CreateShader(const ShaderStage stage) { - return m_programState.CreateShader(stage); + return Group().Programs().CreateShader(stage); } void GLContext::MarkProgramForDeletion(const Uint index) { - return m_programState.MarkProgramObjectForDeletion(index); + return Group().Programs().MarkProgramObjectForDeletion(index, m_currentProgram); } void GLContext::MarkShaderForDeletion(const Uint index) { - return m_programState.MarkShaderObjectForDeletion(index); + return Group().Programs().MarkShaderObjectForDeletion(index, m_currentProgram); } 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 { - return m_programState.ValidateProgramObject(index); + return m_shareGroup->Programs().ValidateProgramObject(index); } Bool GLContext::ValidateShaderName(const Uint index) const { - return m_programState.ValidateShaderObject(index); + return m_shareGroup->Programs().ValidateShaderObject(index); } const SharedPtr& GLContext::GetProgramObject(const Uint index) { - return m_programState.GetProgramObject(index); + return Group().Programs().GetProgramObject(index); } const SharedPtr& GLContext::GetShaderObject(const Uint index) { - return m_programState.GetShaderObject(index); + return Group().Programs().GetShaderObject(index); } void GLContext::JoinAllPendingShaderWork() { - m_programState.JoinAllPendingWork(); + Group().Programs().JoinAllPendingWork(m_currentProgram); } void GLContext::UseProgram(Uint program) { - return m_programState.UseProgram(program); + const SharedPtr 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& GLContext::GetCurrentProgram() { - return m_programState.GetCurrentProgram(); + return m_currentProgram; } // Copies every default-block uniform value `source` holds into the same-named uniform of @@ -608,7 +672,7 @@ namespace MobileGL::MG_State { const SharedPtr& GLContext::GetProgramForDraw() { static const SharedPtr nullProgram = nullptr; - const auto& currentProgram = m_programState.GetCurrentProgram(); + const auto& currentProgram = GetCurrentProgram(); if (currentProgram) { // P1 join site J1, plain glUseProgram half. The backends read a program's // lifetimeId / backendStateVersion / UBO content version to decide whether @@ -762,7 +826,7 @@ namespace MobileGL::MG_State { const SharedPtr& GLContext::GetProgramForDispatch() { static const SharedPtr nullProgram = nullptr; - const auto& currentProgram = m_programState.GetCurrentProgram(); + const auto& currentProgram = GetCurrentProgram(); if (currentProgram) { // Same join contract as GetProgramForDraw's glUseProgram half - see the note // there. A dispatch reads the same non-artifact versions a draw does. @@ -784,7 +848,7 @@ namespace MobileGL::MG_State { } const SharedPtr& GLContext::GetProgramForUniform() { - const auto& currentProgram = m_programState.GetCurrentProgram(); + const auto& currentProgram = GetCurrentProgram(); if (currentProgram) return currentProgram; static const SharedPtr nullProgram = nullptr; if (m_boundProgramPipeline == 0) return nullProgram; @@ -1220,21 +1284,21 @@ namespace MobileGL::MG_State { // Sampler void GLContext::GenSamplerNames(Uint number, Vector& samplers) { - m_samplerState.GenerateNames(number, samplers); + Group().Samplers().GenerateNames(number, samplers); } const SharedPtr& GLContext::GetSamplerObject(Uint index) { - return m_samplerState.GetSamplerObject(index); + return Group().Samplers().GetSamplerObject(index); } const SharedPtr& GLContext::CreateSamplerObject(Uint index) { - return m_samplerState.CreateSamplerObject(index); + return Group().Samplers().CreateSamplerObject(index); } void GLContext::MarkSamplerObjectForDeletion(Uint index) { // Unbind the sampler from all texture units 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) { auto& textureUnit = m_textureState.GetUnitObject(unit); 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 { - return m_samplerState.ValidateName(index); + return m_shareGroup->Samplers().ValidateName(index); } Bool GLContext::ValidateSamplerObject(Uint index) const { - return m_samplerState.ValidateSamplerObject(index); + return m_shareGroup->Samplers().ValidateSamplerObject(index); } // Renderbuffer void GLContext::GenRenderbufferNames(Uint number, Vector& renderbuffers) { - m_renderbufferState.GenerateNames(number, renderbuffers); + Group().GenerateRenderbufferNames(number, renderbuffers); } const SharedPtr& GLContext::GetRenderbufferObject(Uint index) { - return m_renderbufferState.GetRenderbufferObject(index); + return Group().GetRenderbufferObject(index); } BindingSlot& GLContext::GetRenderbufferBindingSlot(RenderbufferTarget target) { @@ -1267,19 +1331,21 @@ namespace MobileGL::MG_State { } const SharedPtr& GLContext::CreateRenderbufferObject(Uint index) { - return m_renderbufferState.CreateRenderbufferObject(index); + return Group().CreateRenderbufferObject(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 { - return m_renderbufferState.ValidateName(index); + return m_shareGroup->ValidateRenderbufferName(index); } Bool GLContext::ValidateRenderbufferObject(Uint index) const { - return m_renderbufferState.ValidateRenderbufferObject(index); + return m_shareGroup->ValidateRenderbufferObject(index); } Uint64 GLContext::AllocateTransformFeedbackLifetimeId() { @@ -1288,12 +1354,23 @@ namespace MobileGL::MG_State { return nextId.fetch_add(1, std::memory_order_relaxed); } - GLContext::GLContext() { + GLContext::GLContext() : GLContext(MakeShared()) {} + + GLContext::GLContext(SharedPtr shareGroup) + : m_shareGroup(shareGroup ? Move(shareGroup) : MakeShared()) { // 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. // Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain // const read instead of an operator[] insert on the draw path. m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId; + InstallDefaultFramebuffer(DefaultFramebuffer()); + } + + void GLContext::InstallDefaultFramebuffer(const SharedPtr& 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 { @@ -1562,5 +1639,5 @@ namespace MobileGL::MG_State { } // namespace GLState // Leak-at-exit storage; see GlobalObjects.cpp. - UniquePtr& pGLContext = *new UniquePtr(); + thread_local SharedPtr pGLContext; } // namespace MobileGL::MG_State diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 813ddcce..53c7e76b 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -16,6 +16,7 @@ #include "ProgramState/ProgramState.h" #include "ProgramState/ProgramPipelineObject.h" #include "SamplerState/SamplerState.h" +#include "ShareGroupState.h" #include "TextureState/TextureState.h" #include "FramebufferState/FramebufferState.h" #include "VertexArrayState/VertexArrayState.h" @@ -76,7 +77,19 @@ namespace MobileGL { class GLContext { 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(); + explicit GLContext(SharedPtr shareGroup); + + // The GL objects this context shares with every context that shares with it. + const SharedPtr& 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& framebuffer); // Error void RecordError(ErrorCode code, UniquePtr info); @@ -188,7 +201,7 @@ namespace MobileGL { // 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. ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { - return m_programState.GetShaderCompileAdoptionMap(); + return Group().Programs().GetShaderCompileAdoptionMap(); } void UseProgram(Uint program); const SharedPtr& GetCurrentProgram(); @@ -606,6 +619,10 @@ namespace MobileGL { void InvalidateCompileEnv(); private: + ShareGroupState& Group() const { return *m_shareGroup; } + + // The GL objects shared with every context that shares with this one. + SharedPtr m_shareGroup; // State Components ErrorState m_errorState; BufferState m_bufferState; @@ -685,10 +702,11 @@ namespace MobileGL { IndexGenerator m_programPipelineNames; Uint m_boundProgramPipeline = 0; 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 m_currentProgram; RenderState m_renderState; FramebufferState m_framebufferState; - SamplerState m_samplerState; RenderbufferState m_renderbufferState; mutable SharedPtr m_compileEnv; @@ -698,7 +716,32 @@ namespace MobileGL { }; } // namespace GLState - extern UniquePtr& 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 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& 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 framebuffer); + const SharedPtr& DefaultFramebuffer(); // 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 diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp index 6843bdc7..a8b01cb2 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp @@ -46,6 +46,14 @@ namespace MobileGL::MG_State::GLState { return framebufferObject; } + void FramebufferState::RegisterFramebufferObject(Uint index, SharedPtr object) { + if (!object) return; + if (index == 0 && !m_indexGenerator.IsValid(0)) { + m_indexGenerator.Insert(0); + } + m_framebufferObjects[index] = Move(object); + } + BindingSlot& FramebufferState::GetBindingSlot(FramebufferTarget target) { for (auto& bindingSlot : m_bindingSlots) { if (bindingSlot.GetTarget() == target) { diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h index fbde98c5..c00cbc46 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h @@ -20,6 +20,9 @@ namespace MobileGL::MG_State::GLState { const SharedPtr& GetFramebufferObject(Uint index); void GenerateNames(Uint number, Vector& framebuffers); const SharedPtr& 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 object); BindingSlot& GetBindingSlot(FramebufferTarget target); void MarkFramebufferObjectForDeletion(Uint index); Bool ValidateName(Uint index) const; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp index ab5a7a11..352c53d4 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp @@ -25,19 +25,24 @@ namespace MobileGL::MG_State::GLState { 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& currentProgram) { if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here auto& programObject = m_programObjects[program]; if (programObject != nullptr) { programObject->MarkAsDeleted(); // 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. - if (programObject == m_currentProgram) return; - DestroyProgramSlot(program); + if (programObject == currentProgram) return; + DestroyProgramSlot(program, currentProgram); } } - void ProgramState::DestroyProgramSlot(const Uint program) { + void ProgramState::DestroyProgramSlot(const Uint program, const SharedPtr& currentProgram) { auto& programObject = m_programObjects[program]; // 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 @@ -55,7 +60,7 @@ namespace MobileGL::MG_State::GLState { for (const auto& shader : attachedShaders) { const Uint shaderName = shader->GetExternalIndex(); 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; } - void ProgramState::UseProgram(Uint program) { - const SharedPtr 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 shaderId = 0; m_programShaderNameGenerator.Generate(1, &shaderId); @@ -100,7 +86,7 @@ namespace MobileGL::MG_State::GLState { return m_shaderObjects[shader]; } - void ProgramState::JoinAllPendingWork() { + void ProgramState::JoinAllPendingWork(const SharedPtr& currentProgram) { // 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 // 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 // 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. - if (m_currentProgram) m_currentProgram->JoinLinkAndSpirv(); + if (currentProgram) currentProgram->JoinLinkAndSpirv(); } - void ProgramState::MarkShaderObjectForDeletion(Uint shader) { + void ProgramState::MarkShaderObjectForDeletion(Uint shader, const SharedPtr& currentProgram) { if (!CheckIndexAvail(shader, m_shaderObjects)) return; auto& shaderObject = m_shaderObjects[shader]; if (shaderObject != nullptr) { @@ -138,11 +124,12 @@ namespace MobileGL::MG_State::GLState { // from every program. The GL CTS compiles shaders through exactly this // create-attach-delete-source-compile sequence (uniform_block.common.name_matching). shaderObject->MarkAsDeleted(); - ReleaseShaderNameIfOrphaned(shader); + ReleaseShaderNameIfOrphaned(shader, currentProgram); } } - Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject) const { + Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject, + const SharedPtr& currentProgram) const { for (const auto& programObject : m_programObjects) { if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) { 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 // 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& currentProgram) { if (!CheckIndexAvail(shader, m_shaderObjects)) return; auto& shaderObject = m_shaderObjects[shader]; 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 // 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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h index f02b28ae..512dec3b 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h @@ -14,28 +14,31 @@ #include "ShaderPreprocessCache.h" 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 { public: // This function WILL actually create the program object. // To retrieve created program object, use GetProgramObject() Uint CreateProgram(); const SharedPtr& GetProgramObject(Uint id); - void MarkProgramObjectForDeletion(Uint program); + void MarkProgramObjectForDeletion(Uint program, const SharedPtr& currentProgram); Bool ValidateProgramObject(Uint program) const; - - void UseProgram(Uint program); + // Whether `program` names a slot in the table at all, i.e. in range - not whether that + // slot currently holds an object. + Bool HasProgramSlot(Uint program) const; Uint CreateShader(ShaderStage stage); const SharedPtr& GetShaderObject(Uint shader); - void MarkShaderObjectForDeletion(Uint shader); + void MarkShaderObjectForDeletion(Uint shader, const SharedPtr& currentProgram); // Frees a deletion-flagged shader's name once no program holds a GL-visible // attachment to it (the deferred half of glDeleteShader-while-attached). - void ReleaseShaderNameIfOrphaned(Uint shader); + void ReleaseShaderNameIfOrphaned(Uint shader, const SharedPtr& currentProgram); Bool ValidateShaderObject(Uint shader) const; - const SharedPtr& GetCurrentProgram() const { return m_currentProgram; } - - // Joins every outstanding compile and link this context still owns, publishing each + // Joins every outstanding compile and link this share group still owns, publishing each // one's artifacts through the ordinary gates. The single caller is // glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero // 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 // dropped are not joined - nothing can observe them, and waiting on them would make // a GL call's cost depend on garbage. - void JoinAllPendingWork(); + void JoinAllPendingWork(const SharedPtr& currentProgram); // 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 @@ -59,10 +62,11 @@ namespace MobileGL::MG_State::GLState { ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { return *m_shaderCompileAdoptionMap; } private: - Bool ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject) const; + Bool ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject, + const SharedPtr& currentProgram) const; // Frees the name slot and releases orphaned attached shaders; the immediate half // of glDeleteProgram (deferred while the program is current). - void DestroyProgramSlot(Uint program); + void DestroyProgramSlot(Uint program, const SharedPtr& currentProgram); template static Bool CheckIndexAvail(const SizeT idx, const Vector& vec) { @@ -96,7 +100,5 @@ namespace MobileGL::MG_State::GLState { Vector> m_programObjects; Vector> m_shaderObjects; - - SharedPtr m_currentProgram; }; } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp b/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp index 53de346b..56a4d312 100644 --- a/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp +++ b/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp @@ -10,34 +10,12 @@ #include "MG_Util/Types.h" namespace MobileGL::MG_State::GLState { - RenderbufferState::RenderbufferState() : m_indexGenerator(1024, 1) { + RenderbufferState::RenderbufferState() { for (SizeT i = 0; i < m_bindingSlots.size(); ++i) { m_bindingSlots[i] = BindingSlot(static_cast(i)); } } - const SharedPtr& RenderbufferState::GetRenderbufferObject(Uint index) { - auto it = m_renderbufferObjects.find(index); - if (it != m_renderbufferObjects.end()) { - return it->second; - } - static SharedPtr nullRenderbufferObject = nullptr; - return nullRenderbufferObject; - } - - void RenderbufferState::GenerateNames(Uint number, Vector& renderbuffers) { - renderbuffers.resize(number); - m_indexGenerator.Generate(number, renderbuffers.data()); - } - - const SharedPtr& RenderbufferState::CreateRenderbufferObject(Uint index) { - auto& bufferObject = m_renderbufferObjects[index]; - if (!bufferObject) { - bufferObject = MakeShared(index); - } - return bufferObject; - } - BindingSlot& RenderbufferState::GetBindingSlot(RenderbufferTarget target) { for (auto& bindingSlot : m_bindingSlots) { if (bindingSlot.GetTarget() == target) { @@ -48,26 +26,12 @@ namespace MobileGL::MG_State::GLState { return m_bindingSlots[0]; } - void RenderbufferState::MarkRenderbufferObjectForDeletion(Uint index) { - if (m_indexGenerator.IsValid(index)) { - auto it = m_renderbufferObjects.find(index); - if (it != m_renderbufferObjects.end()) { - for (auto& bindingSlot : m_bindingSlots) { - if (bindingSlot.GetBoundObject() == it->second) { - bindingSlot.Bind(nullptr); - } - } - m_renderbufferObjects.erase(index); + void RenderbufferState::UnbindRenderbufferObject(const SharedPtr& renderbufferObject) { + if (!renderbufferObject) return; + for (auto& bindingSlot : m_bindingSlots) { + if (bindingSlot.GetBoundObject() == renderbufferObject) { + bindingSlot.Bind(nullptr); } - 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 diff --git a/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.h b/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.h index 76376303..f29ef695 100644 --- a/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.h +++ b/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.h @@ -12,21 +12,16 @@ #include "RenderbufferObject.h" 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 { public: RenderbufferState(); - const SharedPtr& GetRenderbufferObject(Uint index); - void GenerateNames(Uint number, Vector& renderbuffers); - const SharedPtr& CreateRenderbufferObject(Uint index); BindingSlot& GetBindingSlot(RenderbufferTarget target); - void MarkRenderbufferObjectForDeletion(Uint index); - Bool ValidateName(Uint index) const; - Bool ValidateRenderbufferObject(Uint index) const; + void UnbindRenderbufferObject(const SharedPtr& renderbufferObject); private: - UnorderedMap> m_renderbufferObjects; - IndexGenerator m_indexGenerator; Array, static_cast(RenderbufferTarget::RenderbufferTargetCount)> m_bindingSlots; }; diff --git a/MobileGL/MG_State/GLState/ShareGroupState.cpp b/MobileGL/MG_State/GLState/ShareGroupState.cpp new file mode 100644 index 00000000..516d32a2 --- /dev/null +++ b/MobileGL/MG_State/GLState/ShareGroupState.cpp @@ -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 MakeTextureObjectForTarget(Uint index, TextureTarget target) { + switch (target) { + case TextureTarget::Texture1D: + return MakeShared(index); + case TextureTarget::TextureCubeMap: + return MakeShared(index); + case TextureTarget::Texture2D: + return MakeShared(index); + case TextureTarget::Texture3D: + return MakeShared(index); + case TextureTarget::TextureBuffer: + return MakeShared(index); + + // These texture types are still stubbed: + case TextureTarget::TextureRectangle: + return MakeShared(index); + case TextureTarget::Texture2DMultisample: + return MakeShared(index); + case TextureTarget::Texture1DArray: + return MakeShared(index); + case TextureTarget::Texture2DArray: + return MakeShared(index); + case TextureTarget::TextureCubeMapArray: + return MakeShared(index); + case TextureTarget::Texture2DMultisampleArray: + return MakeShared(index); + default: + MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target); + return nullptr; + } + } + + // Buffer + const SharedPtr& ShareGroupState::GetBufferObject(Uint index) { + auto it = m_bufferObjects.find(index); + if (it != m_bufferObjects.end()) { + return it->second; + } + static SharedPtr nullBufferObject = nullptr; + return nullBufferObject; + } + + void ShareGroupState::GenerateBufferNames(Uint number, Vector& buffers) { + buffers.resize(number); + m_bufferNames.Generate(number, buffers.data()); + } + + const SharedPtr& ShareGroupState::CreateBufferObject(Uint index) { + auto& bufferObj = m_bufferObjects[index]; + if (!bufferObj) { + bufferObj = MakeShared(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& textures) { + textures.resize(number); + m_textureNames.Generate(number, textures.data()); + } + + const SharedPtr& ShareGroupState::GetTextureObject(Uint index) { + auto it = m_textureObjects.find(index); + if (it != m_textureObjects.end()) { + return it->second; + } + static SharedPtr nullTextureObject = nullptr; + return nullTextureObject; + } + + const SharedPtr& ShareGroupState::CreateTextureObject(Uint index, TextureTarget target) { + auto& textureObject = m_textureObjects[index]; + textureObject = MakeTextureObjectForTarget(index, target); + if (!textureObject) { + static SharedPtr nullTextureObject = nullptr; + return nullTextureObject; + } + return textureObject; + } + + const SharedPtr& ShareGroupState::CreateTextureViewObject( + Uint index, TextureTarget target, const SharedPtr& 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(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& ShareGroupState::GetRenderbufferObject(Uint index) { + auto it = m_renderbufferObjects.find(index); + if (it != m_renderbufferObjects.end()) { + return it->second; + } + static SharedPtr nullRenderbufferObject = nullptr; + return nullRenderbufferObject; + } + + void ShareGroupState::GenerateRenderbufferNames(Uint number, Vector& renderbuffers) { + renderbuffers.resize(number); + m_renderbufferNames.Generate(number, renderbuffers.data()); + } + + const SharedPtr& ShareGroupState::CreateRenderbufferObject(Uint index) { + auto& bufferObject = m_renderbufferObjects[index]; + if (!bufferObject) { + bufferObject = MakeShared(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 diff --git a/MobileGL/MG_State/GLState/ShareGroupState.h b/MobileGL/MG_State/GLState/ShareGroupState.h new file mode 100644 index 00000000..2f0785b0 --- /dev/null +++ b/MobileGL/MG_State/GLState/ShareGroupState.h @@ -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 +#include +#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& GetBufferObject(Uint index); + void GenerateBufferNames(Uint number, Vector& buffers); + const SharedPtr& CreateBufferObject(Uint index); + void MarkBufferObjectForDeletion(Uint index); + Bool ValidateBufferName(Uint index) const; + Bool ValidateBufferObject(Uint index) const; + + // Texture + void GenerateTextureNames(Uint number, Vector& textures); + const SharedPtr& GetTextureObject(Uint index); + const SharedPtr& CreateTextureObject(Uint index, TextureTarget target); + const SharedPtr& CreateTextureViewObject(Uint index, TextureTarget target, + const SharedPtr& 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& GetRenderbufferObject(Uint index); + void GenerateRenderbufferNames(Uint number, Vector& renderbuffers); + const SharedPtr& 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> m_bufferObjects; + IndexGenerator m_bufferNames{1024, 1}; + UnorderedMap> m_textureObjects; + IndexGenerator m_textureNames{1024, 1}; + UnorderedMap> m_renderbufferObjects; + IndexGenerator m_renderbufferNames{1024, 1}; + SamplerState m_samplerState; + ProgramState m_programState; + }; +} // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index 60d249d5..a3eceacc 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -19,6 +19,7 @@ #include "TextureObjectBuffer.h" #include "TextureObjectStubs.h" #include "TextureObjectView.h" +#include "MG_State/GLState/ShareGroupState.h" namespace MobileGL::MG_State::GLState { static std::atomic s_nextTextureStateContextId = 1; @@ -27,42 +28,10 @@ namespace MobileGL::MG_State::GLState { return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed); } - static SharedPtr MakeTextureObjectForTarget(Uint index, TextureTarget target) { - switch (target) { - case TextureTarget::Texture1D: - return MakeShared(index); - case TextureTarget::TextureCubeMap: - return MakeShared(index); - case TextureTarget::Texture2D: - return MakeShared(index); - case TextureTarget::Texture3D: - return MakeShared(index); - case TextureTarget::TextureBuffer: - return MakeShared(index); - - // These texture types are still stubbed: - case TextureTarget::TextureRectangle: - return MakeShared(index); - case TextureTarget::Texture2DMultisample: - return MakeShared(index); - case TextureTarget::Texture1DArray: - return MakeShared(index); - case TextureTarget::Texture2DArray: - return MakeShared(index); - case TextureTarget::TextureCubeMapArray: - return MakeShared(index); - case TextureTarget::Texture2DMultisampleArray: - return MakeShared(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) { + TextureState::TextureState() : m_contextId(AllocateContextId()) { // 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 - // 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. for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) { m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast(i)); @@ -75,83 +44,35 @@ namespace MobileGL::MG_State::GLState { } } - const SharedPtr& TextureState::GetTextureObject(Uint index) { - auto it = m_textureObjects.find(index); - if (it != m_textureObjects.end()) { - return it->second; - } - static SharedPtr nullTextureObject = nullptr; - return nullTextureObject; - } - const SharedPtr& TextureState::GetDefaultTextureObject(TextureTarget target) const { MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount, "GetDefaultTextureObject: invalid texture target %d", (int)target); return m_defaultTextureObjects[(int)target]; } - void TextureState::GenerateNames(Uint number, Vector& textures) { - textures.resize(number); - m_indexGenerator.Generate(number, textures.data()); - } - - const SharedPtr& TextureState::CreateTextureObject(Uint index, TextureTarget target) { - auto& textureObject = m_textureObjects[index]; - textureObject = MakeTextureObjectForTarget(index, target); - if (!textureObject) { - static SharedPtr nullTextureObject = nullptr; - return nullTextureObject; - } - return textureObject; - } - - const SharedPtr& TextureState::CreateTextureViewObject( - Uint index, TextureTarget target, const SharedPtr& 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(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()]); - } - } + void TextureState::UnbindTextureFromUnits(const SharedPtr& textureObject) { + if (!textureObject) return; + // 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() == textureObject) { + // 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) { @@ -179,12 +100,4 @@ namespace MobileGL::MG_State::GLState { void TextureState::SetActiveTextureUnit(Int 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 diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index f7231ac2..fdea0394 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -15,6 +15,11 @@ #include "TextureUnit.h" 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 MakeTextureObjectForTarget(Uint index, TextureTarget target); + struct ImageTextureBinding { SharedPtr Texture; 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 { public: // 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; TextureState(); - void GenerateNames(Uint number, Vector& textures); - const SharedPtr& 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& CreateTextureViewObject(Uint index, TextureTarget target, - const SharedPtr& storageOwner, - Uint minLevel, Uint numLevels, Uint minLayer, - Uint numLayers); - const SharedPtr& GetTextureObject(Uint index); // 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 // 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 - // and by-name lookups keep failing for 0) and can never be deleted. + // GenTextures name: it lives outside the share group's table (so glIsTexture(0) stays + // GL_FALSE and by-name lookups keep failing for 0) and can never be deleted. const SharedPtr& GetDefaultTextureObject(TextureTarget target) const; TextureUnit& GetUnitObject(Int unit); ImageTextureBinding& GetImageTextureBinding(Int unit); const ImageTextureBinding& GetImageTextureBinding(Int unit) const; Int GetActiveTextureUnit() const; void SetActiveTextureUnit(Int unit); - void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation); - Bool ValidateName(Uint index) const; - Bool ValidateTextureObject(Uint index) const; + // Rebinds every unit and image unit that names `textureObject` back to the target's + // default texture object; this context's half of a texture delete (GL 4.6 core 8.1). + void UnbindTextureFromUnits(const SharedPtr& textureObject); // 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 @@ -209,8 +208,6 @@ namespace MobileGL::MG_State::GLState { Int m_activeTextureUnit = 0; Array m_textureUnits; Array m_imageTextureBindings; - IndexGenerator m_indexGenerator; - UnorderedMap> m_textureObjects; // 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. Array, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects; diff --git a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp index 6a1eb1b0..de4bdf8c 100644 --- a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp @@ -105,7 +105,7 @@ namespace { Uint32 m_blockKb = 64; Uint32 m_adoptTier = 2; Bool m_pipeStats = false; - UniquePtr m_context; + SharedPtr m_context; }; #endif diff --git a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp index 641f4926..bc1db758 100644 --- a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp +++ b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp @@ -109,7 +109,7 @@ namespace { #endif MG_State::pGLContext = Move(m_previous); } - UniquePtr m_previous; + SharedPtr m_previous; }; SizeT Index(MGPipeInputField field) { return static_cast(field); } diff --git a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp index 94e5d6a8..79b4e095 100644 --- a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp @@ -303,7 +303,7 @@ namespace { FramebufferScope(const FramebufferScope&) = delete; FramebufferScope& operator=(const FramebufferScope&) = delete; - UniquePtr m_previousContext; + SharedPtr m_previousContext; Uint64 m_previousPush = 0; }; diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index d0258743..61b5452d 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -148,7 +148,10 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) { // 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 // set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record. - EXPECT_EQ(ClassCount(), 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(), 13u); EXPECT_EQ(ClassCount(), 8u); EXPECT_EQ(ClassCount(), 13u); EXPECT_EQ(ClassCount(), 19u); @@ -644,7 +647,12 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { // opcodes 82..83, one payload, no flag at all - no reply slot, no blob, no tail. EXPECT_EQ(static_cast(MGPWireOp::ReadPixelsToBuffer), 82); EXPECT_EQ(static_cast(MGPWireOp::GetTextureImageToBuffer), 83); - EXPECT_EQ(static_cast(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(MGPWireOp::BindContext), 84); + EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 85); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast(kNone)); EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u); @@ -653,6 +661,13 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast(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(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 // else, and the extended draw row keeps its two flags. EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast(kHasBlob)); @@ -674,6 +689,10 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) { // 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. 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); // 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 @@ -1036,8 +1055,11 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) { // (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 // 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. - EXPECT_EQ(kMGPipeVerifiedPayloadCount, 83u); + // pack-buffer readbacks' one payload, MGPReadbackToBuffer (CONTRACT-P9.md §1): 83. P14 S1 + // 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::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::value); diff --git a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp index 6be9bc29..f8b8214d 100644 --- a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp @@ -84,7 +84,7 @@ namespace { MGPipeSetPoisonOmission(nullptr, nullptr); MG_State::pGLContext = Move(m_previous); } - UniquePtr m_previous; + SharedPtr m_previous; }; #if MOBILEGL_PIPE_POISON diff --git a/MobileGL/MG_Test/Pipe/RenderStateSpansTest.cpp b/MobileGL/MG_Test/Pipe/RenderStateSpansTest.cpp index 286c38d7..188f09a8 100644 --- a/MobileGL/MG_Test/Pipe/RenderStateSpansTest.cpp +++ b/MobileGL/MG_Test/Pipe/RenderStateSpansTest.cpp @@ -568,7 +568,7 @@ namespace { // 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). struct ContextGuard { - UniquePtr Previous; + SharedPtr Previous; ContextGuard() : Previous(Move(MG_State::pGLContext)) { MG_State::pGLContext = MakeUnique(); MGPipeApplierReset(); @@ -786,7 +786,7 @@ namespace { GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; #else struct ContextGuard { - UniquePtr Previous; + SharedPtr Previous; ContextGuard() : Previous(Move(MG_State::pGLContext)) { MG_State::pGLContext = MakeUnique(); MGPipeApplierReset(); @@ -950,7 +950,7 @@ namespace { // 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). struct ApplierContextGuard { - UniquePtr Previous; + SharedPtr Previous; ApplierContextGuard() : Previous(Move(MG_State::pGLContext)) { MG_State::pGLContext = MakeUnique(); MGPipeApplierReset(); diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp index e200348d..22fcac21 100644 --- a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -1946,7 +1946,7 @@ namespace { MGPipeResourceOps m_ops{}; Uint64 m_previousPush = 0; - UniquePtr m_previousContext; + SharedPtr m_previousContext; }; GLContext& Ctx() { return *MG_State::pGLContext; } diff --git a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp index d75bcd24..b80796a5 100644 --- a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp @@ -309,7 +309,7 @@ namespace { TextureScope(const TextureScope&) = delete; TextureScope& operator=(const TextureScope&) = delete; - UniquePtr m_previousContext; + SharedPtr m_previousContext; Uint64 m_previousPush = 0; }; diff --git a/MobileGL/MG_Test/Pipe/TrackerTest.cpp b/MobileGL/MG_Test/Pipe/TrackerTest.cpp index 7ec73332..6554cf77 100644 --- a/MobileGL/MG_Test/Pipe/TrackerTest.cpp +++ b/MobileGL/MG_Test/Pipe/TrackerTest.cpp @@ -163,7 +163,7 @@ namespace { } } - UniquePtr m_previous; + SharedPtr m_previous; }; TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) { @@ -238,7 +238,7 @@ namespace { } TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) { - UniquePtr held = Move(MG_State::pGLContext); + SharedPtr held = Move(MG_State::pGLContext); MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context MG_State::pGLContext = Move(held); SUCCEED(); @@ -353,7 +353,7 @@ namespace { Uint64 m_payloadBytes = 0; Uint64 m_binds = 0; Uint64 m_savedPush = 0; - UniquePtr m_previous; + SharedPtr m_previous; }; TEST_F(TrackerWalk, EveryBitHasAName) { @@ -988,7 +988,7 @@ namespace { return bits; } - UniquePtr m_previous; + SharedPtr m_previous; }; TEST_F(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) { @@ -1075,7 +1075,7 @@ namespace { static const MGPipeCsoCache::Counters& Cso() { return MGPipeCsoCacheInstance().GetCounters(); } Uint64 m_savedPush = 0; - UniquePtr m_previous; + SharedPtr m_previous; }; 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 // outlive it. MGPipeSetPendingBaseInstance(11); - UniquePtr parked = Move(MG_State::pGLContext); + SharedPtr parked = Move(MG_State::pGLContext); Draw(); EXPECT_EQ(MGPipePendingBaseInstance(), 0u) << "the no-live-context exit left the draw's base instance standing for the next verb"; diff --git a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp index 509767f5..cd7878aa 100644 --- a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp @@ -174,7 +174,7 @@ namespace { EmitterScope(const EmitterScope&) = delete; EmitterScope& operator=(const EmitterScope&) = delete; - UniquePtr m_previousContext; + SharedPtr m_previousContext; }; GLContext& Ctx() { return *MG_State::pGLContext; } diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 6dfacb6d..1ae91085 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -268,7 +268,7 @@ namespace { MobileGL::Vector bindCalls; private: - MobileGL::UniquePtr previousContext; + MobileGL::SharedPtr previousContext; MobileGL::MG_External::GLESFunctionsTable previousFunctions; MobileGL::Uint previousActiveUnit; 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 // again (the dedupe), attribute 3 is a client-memory array (null Res, nothing to ensure). 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 savedVertexBuffers = + st.VertexBuffers; st.VertexBufferStart = 0; st.VertexBufferCount = 4; for (Uint32 i = 0; i < 4; ++i) { @@ -5338,7 +5345,7 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT // 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 // disagree, which is a red for the wrong reason. - UniquePtr previousContext = Move(MG_State::pGLContext); + SharedPtr previousContext = Move(MG_State::pGLContext); MG_State::pGLContext = MakeUnique(); MG_State::pGLContext->CreateVertexArrayObject(1); MG_State::pGLContext->BindVertexArray(1); @@ -5369,7 +5376,9 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT "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 = Move(previousContext); MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, a); @@ -5399,14 +5408,15 @@ TEST(DirectGLESVertexInputDraw, TheIndexArmTakesItsBufferAndItsSerialFromTheReco ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(publishedHandle)); 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.Res = publishedHandle; st.IndexBufferSerial = 11; // The frontend's element slot has moved to another buffer with nothing emitted, in a real // context for the reason the case above gives. - UniquePtr previousContext = Move(MG_State::pGLContext); + SharedPtr previousContext = Move(MG_State::pGLContext); MG_State::pGLContext = MakeUnique(); MG_State::pGLContext->CreateVertexArrayObject(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 " "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 = Move(previousContext); MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, publishedHandle); diff --git a/MobileGL/MG_Test/State/CMakeLists.txt b/MobileGL/MG_Test/State/CMakeLists.txt index d8ea1948..cbb2da66 100644 --- a/MobileGL/MG_Test/State/CMakeLists.txt +++ b/MobileGL/MG_Test/State/CMakeLists.txt @@ -144,3 +144,22 @@ if (MSVC) endif() 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) diff --git a/MobileGL/MG_Test/State/ContextStateOwnershipTest.cpp b/MobileGL/MG_Test/State/ContextStateOwnershipTest.cpp new file mode 100644 index 00000000..1aa7e69f --- /dev/null +++ b/MobileGL/MG_Test/State/ContextStateOwnershipTest.cpp @@ -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 + +#include "Includes.h" +#include "Init.h" +#include +#include +#include +#include +#include +#include + +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 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()); +} diff --git a/MobileGL/MG_Test/Wire/ApplierOwnershipTest.cpp b/MobileGL/MG_Test/Wire/ApplierOwnershipTest.cpp new file mode 100644 index 00000000..6a0c03c0 --- /dev/null +++ b/MobileGL/MG_Test/Wire/ApplierOwnershipTest.cpp @@ -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 + +#include + +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 diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index 29803e4b..3d5040cf 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -19,7 +19,11 @@ set(MOBILEGL_WIRE_TESTS ServerDisplayTest # P11 B1: the apply thread's affinity / spin chosen per session from the peer's dial mode # (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) @@ -56,6 +60,12 @@ if (NOT WIN32) # 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. 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` # 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. @@ -78,7 +88,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ${MGL_ROOT}/3rdparty/flatbuffers/include ) 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 # the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy # session, P12ServerRig.h, and so its include paths.) diff --git a/MobileGL/MG_Test/Wire/MultiSessionTest.cpp b/MobileGL/MG_Test/Wire/MultiSessionTest.cpp new file mode 100644 index 00000000..70242545 --- /dev/null +++ b/MobileGL/MG_Test/Wire/MultiSessionTest.cpp @@ -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 + +#include +#include +#include +#include + +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 returned{false}; + std::atomic 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(sizeof(command))) { + std::snprintf(report.note, sizeof(report.note), "peer write failed"); + return report; + } + if (::read(fromChild, &report, sizeof(report)) != static_cast(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(started)); + (void)::write(reports[1], &ready, sizeof(ready)); + for (;;) { + PeerCommand command{}; + if (::read(commands[0], &command, sizeof(command)) != static_cast(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(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(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(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(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; +} diff --git a/MobileGL/MG_Test/Wire/P12ServerRig.h b/MobileGL/MG_Test/Wire/P12ServerRig.h index d75ba9f1..75b7321f 100644 --- a/MobileGL/MG_Test/Wire/P12ServerRig.h +++ b/MobileGL/MG_Test/Wire/P12ServerRig.h @@ -328,9 +328,18 @@ namespace { return true; } - // A HEALTHY SESSION: one record applied and, with headless EGL, a green clear read back - - // PeerLatchTest's session B, verbatim in what it sends. - inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) { + // A HEALTHY SESSION: one record applied and, with headless EGL, a clear read back - + // PeerLatchTest's session B, verbatim in what it sends. P14 S2: the clear's colour is + // 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{}; barrier.Bits = 0x2000u; r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0; @@ -384,8 +393,11 @@ namespace { clear.DrawBufferIndex = 0; clear.BufferMask = GL_COLOR_BUFFER_BIT; clear.ValueClass = P::kMGPipeClearValueClassFloat; - const float green[4] = {0.0f, 1.0f, 0.0f, 1.0f}; - std::memcpy(clear.ColorValue, green, sizeof(green)); + const float color[4] = {static_cast(rgba & 0xFFu) / 255.0f, + static_cast((rgba >> 8) & 0xFFu) / 255.0f, + static_cast((rgba >> 16) & 0xFFu) / 255.0f, + static_cast((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"); P::MGPReadbackInfo read{}; read.Res = P::kMGPipeNullHandle; @@ -402,6 +414,12 @@ namespace { if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back"); std::memcpy(&r.pixel, texel, sizeof(r.pixel)); } + + // The green every existing caller asked for, with the two-argument signature a + // `std::function` can be built from. + inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) { + HealthySessionWithColor(client, r, 0xFF00FF00u); + } } // namespace P12 } // namespace diff --git a/MobileGL/MG_Test/Wire/PeerLatchTest.cpp b/MobileGL/MG_Test/Wire/PeerLatchTest.cpp index ae41acea..f8e154f5 100644 --- a/MobileGL/MG_Test/Wire/PeerLatchTest.cpp +++ b/MobileGL/MG_Test/Wire/PeerLatchTest.cpp @@ -1226,6 +1226,20 @@ namespace { reset.ContextSerial = 7777; 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, "Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}", false, [](Client::ClientSession& c, PeerReport& r) { diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 536e60e1..e120223e 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -1623,7 +1623,7 @@ namespace { // 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 // have; installed for the case's life and put back afterwards (SplitBufferTest's shape). - UniquePtr savedContext; + SharedPtr savedContext; // Empty on success, else the step that failed - the harness's SkipReason shape. std::string BringUp() { @@ -3537,6 +3537,103 @@ TEST(ServerLoopTest, SessionSurfaceModeAdmitsOnlyTheModeTheFirstSurfaceChose) { } #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(static_cast(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) { // 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 diff --git a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp index 33d01777..5eb2bc7f 100644 --- a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp +++ b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp @@ -261,22 +261,24 @@ TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting) CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(), ::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint); } -// P11 B2 (CONTRACT-P11 B2): CONTROL REVISION 5. A peer built at revision 4 - before -// LinkTerms.adoptTier - carries the fingerprint its revision mixed, and is refused by name -// (Refuse{WireFingerprint}), never read as a revision-5 Hello that happens to ask T2. The same shape -// PAIR's 3 -> 4 had. RED ONCE by leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 4 with the schema -// change: the two fingerprints are then equal and the Welcome goes out. -TEST(SessionHandshakeTest, ARevisionFourPeerIsRefusedByNameAtRevisionFive) { - static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 5, "B2 bumped the control revision 4 -> 5"); +// P11 B2 (CONTRACT-P11 B2) AND P14 S1. CONTROL REVISION 6. A peer built at the revision before +// the current one - revision 4 carried no LinkTerms.adoptTier, revision 5 no context-lifecycle +// SurfaceOp kinds - carries the fingerprint its revision mixed, and is refused by name +// (Refuse{WireFingerprint}), never read as a current-revision Hello that happens to ask T2 or to +// send a SurfaceOpKind the receiver does not know. The same shape PAIR's 3 -> 4 had. RED ONCE by +// leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 5 with the schema change: the two fingerprints +// 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(); #if MOBILEGL_BUILD_DISAGGREGATED - inputs.ControlSchemaRevision = (static_cast(4) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision; + inputs.ControlSchemaRevision = (static_cast(5) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision; #else - inputs.ControlSchemaRevision = 4; + inputs.ControlSchemaRevision = 5; #endif - const Uint64 revisionFour = Transport::MixAbiFingerprint(inputs); - ASSERT_NE(revisionFour, WireFingerprint()); - CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFour, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect, + const Uint64 revisionFive = Transport::MixAbiFingerprint(inputs); + ASSERT_NE(revisionFive, WireFingerprint()); + CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFive, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect, ::MobileGL::Wire::RefuseCode::WireFingerprint); } diff --git a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp index cfccecc8..52a46ec5 100644 --- a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp +++ b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp @@ -197,6 +197,73 @@ TEST(SurfaceControlFrameTest, TheEleventhFramedOpIsInitCapabilitiesAndItCrossesT 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(SurfaceControlOp::CreateContext) == 12u); + static_assert(static_cast(SurfaceControlOp::DestroyContext) == 13u); + EXPECT_EQ(static_cast(::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(::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(kind), static_cast(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) { using MG_Backend::WindowBackend; const struct { @@ -241,6 +308,11 @@ TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) { SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent, SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval, 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) { SurfaceControlFrame frame; @@ -277,6 +349,10 @@ TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) { EXPECT_EQ(back.windowBackend, frame.windowBackend) << Server::SurfaceControlOpName(op); 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); } } diff --git a/docs/Disaggregated/ARCHITECTURE.md b/docs/Disaggregated/ARCHITECTURE.md index d6367180..67607dce 100644 --- a/docs/Disaggregated/ARCHITECTURE.md +++ b/docs/Disaggregated/ARCHITECTURE.md @@ -33,3 +33,4 @@ | 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) | | 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) | diff --git a/docs/Disaggregated/design/11-state-ownership.md b/docs/Disaggregated/design/11-state-ownership.md index 25d94d13..bb7475b9 100644 --- a/docs/Disaggregated/design/11-state-ownership.md +++ b/docs/Disaggregated/design/11-state-ownership.md @@ -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) 原样核对。 4. **wireFingerprint 随以上变更 bump**;两端同 commit + 显式 `-DMOBILEGL_BUILD_STAMP=` 重建,不接受 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) 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 推送快门)。 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)` 构造,`GLContext()` 建一个只属于它的 share group(测试与进程默认 context 用)。 + +**`pGLContext` 改成 TLS**:`extern thread_local SharedPtr 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) 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。 -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 而非全局。 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) 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 世代。 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 一节所列,仍未动。 + + ## 分期与验收 | 期 | 内容 | 验收 | diff --git a/scripts/ci/ph_latch_sites.py b/scripts/ci/ph_latch_sites.py index a57f52ad..06317400 100644 --- a/scripts/ci/ph_latch_sites.py +++ b/scripts/ci/ph_latch_sites.py @@ -117,8 +117,11 @@ MECHANICS = [ (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", [("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", - 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", "AnUnarmedSessionLatchDiesWithItsLineLikeSessionFail")]), ] diff --git a/scripts/ci/protocol_revision_pins.json b/scripts/ci/protocol_revision_pins.json index 48d45546..55a7ae17 100644 --- a/scripts/ci/protocol_revision_pins.json +++ b/scripts/ci/protocol_revision_pins.json @@ -3,5 +3,6 @@ "2": "b30928e6fc93ddc015fd86f634a6820f775a6f2f6de5a848d273981e706b2597", "3": "da09f0acb48cf245703f28556ef5a38bc9e25241ebbf7b6baa9ab1765b7ddd50", "4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f", - "5": "4632750fd68b889cb6df709df9d150f9910468c0c530399de01bef87ba793904" + "5": "4632750fd68b889cb6df709df9d150f9910468c0c530399de01bef87ba793904", + "6": "fd76c08349a399edb75a6b36753347d19b79243d7bacdd61323f2921db56dad3" }