// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESContext.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 "DirectGLESInternal.h" namespace MobileGL::MG_Backend::DirectGLES { namespace PrgramImpl { // The identity+version key above is only monotonic WITHIN one GLContext: a // library teardown + re-init frees every FramebufferObject and restarts the // draw slot's counter at zero, so a recycled FBO address with coinciding // fresh versions would false-hit. Cleared at the same boundaries as the // structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache). void InvalidateBroadcastMemo() { g_broadcastMemoValid = false; g_broadcastMemoHandleValid = false; } } // namespace PrgramImpl static void ReleasePackedWordScratchTexture() { // The ES context (and the name with it) is gone; deleting here would target a recycled // name in the successor context. g_packedWordScratchTextureId = 0; g_packedWordScratchWidth = 0; g_packedWordScratchHeight = 0; } // --------------------------------------------------------------------------------- // 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. EGLDisplay g_Display = EGL_NO_DISPLAY; // eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0. static Uint32 g_displayUsers = 0; // How many times the display has been terminated. eglTerminate destroys every EGLImage made on // it, and a re-initialized display hands back the same EGLDisplay handle, so a holder destroys // its EGLImage itself only while this has not moved since the image was made. std::atomic g_displayTerminations{0}; #if MOBILEGL_BUILD_DISAGGREGATED namespace SharedImageImpl { // The display (and every EGLImage and renderbuffer made on it) is gone: forget the present // targets without calling into the driver, releasing the images they held. void ForgetPresentTargetsAfterTerminate(); } // namespace SharedImageImpl #endif static void NoteDisplayTerminated() { g_displayTerminations.fetch_add(1, std::memory_order_acq_rel); #if MOBILEGL_BUILD_DISAGGREGATED SharedImageImpl::ForgetPresentTargetsAfterTerminate(); #endif } 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. std::atomic g_nativeRegistryEpoch{1}; // P15: moves whenever the size QueryCurrentSurfaceSize would answer may have moved - after every // swap (a window surface takes a new size at its next swap) and at every change of a session's // draw surface (NoteDrawSurfaceMoved at each `session.Draw =`). Surface creation and destruction // also move g_nativeRegistryEpoch, which the size memo keys on as well. std::atomic g_surfaceSizeEpoch{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. Vector& NativeContexts() { static Vector* contexts = new Vector(); return *contexts; } Vector& NativeSessions() { static Vector* sessions = new Vector(); return *sessions; } std::atomic g_nativeContextKeyResolver{nullptr}; thread_local NativeThreadState t_native; void SetNativeContextKeyResolver(NativeContextKeyResolver resolver) { g_nativeContextKeyResolver.store(resolver, std::memory_order_release); } // P14 S6: the twin tables' key. Same probe/fallback shape as the native tuple's above and // for the same reason (the whole single-context world must keep getting one key), and // deliberately a SEPARATE resolver: this one answers for a table that is consulted on paths // the native tuple is not, and a case that drives twins without EGL must be able to set it // alone. See DirectGLES.h for what the two halves mean. static std::atomic g_twinKeyResolver{nullptr}; void SetTwinKeyResolver(TwinKeyResolver resolver) { g_twinKeyResolver.store(resolver, std::memory_order_release); } Bool CurrentTwinKey(TwinKey* outKey) { outKey->SessionKey = 0; outKey->ShareGroupKey = 0; const TwinKeyResolver resolver = g_twinKeyResolver.load(std::memory_order_acquire); if (resolver == nullptr) return false; return resolver(outKey); } 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. // (t_native.activeTuple / activeEpoch / activeSession / activeToken) // 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). // (t_native.boundContext) // And the surface it was made current with: a bind to the same context on a different // surface (one client context drawing to several windows) is a real switch. // (t_native.boundDraw) // ...and that context's lifetime serial, the key of its container objects. // (t_nativeContextThread.BoundSerial, DirectGLES.h) std::atomic g_nativeContextSerials{0}; std::mutex& PerNativeContextRegistryMutex() { static auto* mutex = new std::mutex(); return *mutex; } Vector& PerNativeContextRegistry() { static auto* registry = new Vector(); return *registry; } } // namespace namespace { std::atomic g_perNativeContextEpoch{1}; } // namespace Uint64 PerNativeContextEpoch() { return g_perNativeContextEpoch.load(std::memory_order_acquire); } void BumpPerNativeContextEpoch() { g_perNativeContextEpoch.fetch_add(1, std::memory_order_acq_rel); } namespace { std::atomic g_perNativeContextCacheSlots{0}; } // namespace PerNativeContextBase::PerNativeContextBase() : m_cacheSlot(g_perNativeContextCacheSlots.fetch_add(1, std::memory_order_relaxed) % NativeContextThreadState::kCacheEntries) { const std::lock_guard lock(PerNativeContextRegistryMutex()); PerNativeContextRegistry().push_back(this); } PerNativeContextBase::~PerNativeContextBase() { BumpPerNativeContextEpoch(); const std::lock_guard lock(PerNativeContextRegistryMutex()); auto& registry = PerNativeContextRegistry(); registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end()); } void ForgetNativeContextObjects(Uint64 serial) { if (serial == 0) return; const std::lock_guard lock(PerNativeContextRegistryMutex()); for (PerNativeContextBase* holder : PerNativeContextRegistry()) holder->ForgetContext(serial); } // 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. NativeContextTuple& ActiveNativeContext() { NativeThreadState& tls = t_native; Uint64 session = 0; Uint64 token = 0; if (tls.forcedValid) { session = tls.forcedSession; token = tls.forcedToken; } else { const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire); if (resolver != nullptr) (void)resolver(&session, &token); } const Uint64 epoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); if (tls.activeTuple != nullptr && tls.activeEpoch == epoch && tls.activeSession == session && tls.activeToken == token) { return *tls.activeTuple; } 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_native.activeTuple = found; t_native.activeEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); t_native.activeSession = session; t_native.activeToken = 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_native.forcedSession = sessionKey; t_native.forcedToken = contextToken; t_native.forcedValid = true; // The cache is keyed on the effective key, so drop it rather than let a stale entry answer. t_native.activeTuple = nullptr; t_native.activeEpoch = 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 // framebuffer's shape, so that publish - and no pbuffer's - may carry the surface's extent. // Split builds only: the pull build's publish is unchanged (G1). static Bool g_publishWindowExtent = false; #endif Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) { outWidth = 0; outHeight = 0; // P15: ANSWERED FROM A MEMO while nothing that moves the size moved. Two eglQuerySurface // calls behind the registry lock on every dirty render-state sync (draws and clears whose // scissor box the app never wrote) cost 0.065 ms/frame on FCL Espryt. Uint64 memoSession = 0, memoToken = 0; ResolveNativeKey(memoSession, memoToken); const Uint64 registryEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); const Uint64 sizeEpoch = g_surfaceSizeEpoch.load(std::memory_order_acquire); NativeThreadState& memo = t_native; if (memo.sizeMemoValid && memo.sizeMemoSession == memoSession && memo.sizeMemoToken == memoToken && memo.sizeMemoRegistryEpoch == registryEpoch && memo.sizeMemoSizeEpoch == sizeEpoch) { outWidth = memo.sizeMemoWidth; outHeight = memo.sizeMemoHeight; return true; } // 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, surface, EGL_WIDTH, &width) || !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height) || width <= 0 || height <= 0) { return false; } outWidth = static_cast(width); outHeight = static_cast(height); memo.sizeMemoValid = true; memo.sizeMemoSession = memoSession; memo.sizeMemoToken = memoToken; memo.sizeMemoRegistryEpoch = registryEpoch; memo.sizeMemoSizeEpoch = sizeEpoch; memo.sizeMemoWidth = outWidth; memo.sizeMemoHeight = outHeight; return true; } // The frontend's default framebuffer is a placeholder FramebufferObject whose attachments // carry a format and nothing else (MG_Impl/Init.cpp), and it is built before any surface // exists - so it starts on a guess, GL_DEPTH32F_STENCIL8. Every attachment query about the // default framebuffer is answered out of that guess, and being wrong is not cosmetic: GL // blits depth/stencil only between IDENTICAL formats, so a caller that reads // GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was told about and blits // gets GL_INVALID_OPERATION and a silently dropped blit - colour bits included, because a // rejected glBlitFramebuffer transfers nothing at all. DirectVulkan already publishes its // real format when it creates the swapchain (SwapchainObject::Create); this is the // DirectGLES half, and here the answer can simply be asked of the ES default framebuffer. // // Only the format is published. The placeholder's 512x512 extent is left alone: all three // attachments share it, and FramebufferObject::CheckCompleteness requires them to agree, so // resizing depth/stencil without colour would report the default framebuffer incomplete. static void PublishDefaultFramebufferDepthStencilFormat() { auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo; // P8-SE: THE PLACEHOLDER IS ONLY THE MONOLITH ARM'S TARGET. With a transport the answer is // posted (OnSurfaceChanged, below) and the client writes its own placeholder; a spawn or tcp // server process has no frontend at all, so pDefaultFramebufferInfo is null there. Gating on // it returned before the post, the client kept Init.cpp's GL_DEPTH32F_STENCIL8 guess for a // D24S8 pbuffer, and a blit into a buffer allocated from that guess lost depth and stencil // (docs/archive/notes/p8/SE.md). Inproc only passed because client and server share the process. if (!g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return; if (!defaultFBOInfo && MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == nullptr) return; // GL_DEPTH / GL_STENCIL are the default framebuffer's spellings; a user framebuffer // would need GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT and answers GL_INVALID_ENUM // for these. Nothing else can be bound this early, but bind explicitly so the answer // describes the default framebuffer even if this is ever called later. GLint previousDrawFramebuffer = 0; g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFramebuffer); if (previousDrawFramebuffer != 0) { g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); } ClearGLErrors(); GLint depthBits = 0; GLint stencilBits = 0; GLint depthComponentType = GL_UNSIGNED_NORMALIZED; g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH, GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits); g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL, GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits); g_GLESFuncs.glGetFramebufferAttachmentParameteriv( GL_DRAW_FRAMEBUFFER, GL_DEPTH, GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &depthComponentType); ClearGLErrors(); if (previousDrawFramebuffer != 0) { g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDrawFramebuffer)); } FramebufferImpl::InvalidateFramebufferBindingCache(); FramebufferImpl::InvalidateFramebufferHandleArmMemos(); // 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. 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 && activeConfig != nullptr) { EGLint eglDepth = 0; EGLint eglStencil = 0; if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_DEPTH_SIZE, &eglDepth)) { depthBits = static_cast(eglDepth); } if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_STENCIL_SIZE, &eglStencil)) { stencilBits = static_cast(eglStencil); } } if (depthBits <= 0 && stencilBits <= 0) { // Nothing usable came back from either witness. Returning here leaves whatever a // 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: // 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. MGLOG_D("DirectGLES: default framebuffer reports no depth or stencil; leaving the " "placeholder attachment formats untouched"); return; } const Bool floatDepth = depthComponentType == GL_FLOAT; TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8; TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8; if (depthBits > 0 && stencilBits > 0) { // Packed: both frontend attachments name the same combined format, as DirectVulkan does. depthFormat = (floatDepth || depthBits > 24) ? TextureInternalFormat::Depth32FStencil8 : TextureInternalFormat::Depth24Stencil8; stencilFormat = depthFormat; } else if (depthBits > 0) { depthFormat = floatDepth ? TextureInternalFormat::DepthComponent32F : (depthBits <= 16) ? TextureInternalFormat::DepthComponent16 : TextureInternalFormat::DepthComponent24; stencilFormat = depthFormat; } else { depthFormat = TextureInternalFormat::StencilIndex8; stencilFormat = TextureInternalFormat::StencilIndex8; } // Null in a spawn / tcp server (above); only the monolith arm below writes through them. auto* depthTexture = defaultFBOInfo ? defaultFBOInfo->depthAttachment.get() : nullptr; auto* stencilTexture = defaultFBOInfo ? defaultFBOInfo->stencilAttachment.get() : nullptr; if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) { // P5c ev (CONTRACT-P5C §4.2): with an active transport the attachments are CLIENT // memory and this thread may not write them - the backend fills MGPSurfaceInfo // and posts, and the client applies it to its own pDefaultFramebufferInfo on the // GL thread. FORMAT ONLY, exactly as the monolith arm below: Width/Height stay 0, // which is the consumer's cue to leave the placeholder extent untouched. The // callback's presence IS the transport probe - the server session installs it at // Accept, and under monolith nobody ever does. MG_Pipe::MGPSurfaceInfo info{}; info.InternalFormat = static_cast(depthFormat); info.IsDefault = 1; #if MOBILEGL_BUILD_DISAGGREGATED // P12 (on-screen server window), D3: A WINDOW SURFACE OF A REMOTE SESSION ALSO SAYS HOW // BIG IT IS. Under spawn / tcp (this server process set Transport=Spawn in RunSession) // the only window surface is the server's own - a headless client's ServerOwned one - // and that client learns its extent here and from the reply, as Magma's swapchain // already tells it. An extent switches the client's consumer to the reallocating arm // (ClientSession.cpp ApplySurfaceChangedToClient), so it is deliberately NOT published // for a pbuffer, nor under inproc / monolith: those keep the format-only shape above. if (g_publishWindowExtent && MG_Config::Transport == MG_Config::TransportMode::Spawn) { // THE DISPLAY'S EXTENT, NEVER THE SURFACE'S: eglQuerySurface answers the buffer last // dequeued, which after a resize is still the old size - publishing it told the client a // size the window no longer had. Uint32 extentWidth = 0; Uint32 extentHeight = 0; if (MG_Pipe::MGPipeServerOwnedWindowExtent(&extentWidth, &extentHeight)) { info.Width = extentWidth; info.Height = extentHeight; } } #endif MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info); } else { if (depthTexture) depthTexture->SetInternalFormat(depthFormat); if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat); } MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment " "formats depth=%d stencil=%d", depthBits, stencilBits, floatDepth ? 1 : 0, static_cast(depthFormat), static_cast(stencilFormat)); } #if defined(__linux__) && !defined(__ANDROID__) static void* OpenX11Lib() { void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW); if (!x11Lib) { x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW); } return x11Lib; } #endif static EGLint QueryDefaultX11VisualId() { #if defined(__linux__) && !defined(__ANDROID__) const char* displayName = std::getenv("DISPLAY"); if (!displayName || !MG_Util::X11::MayDialX11Display(displayName)) { return 0; } void* x11Lib = OpenX11Lib(); if (!x11Lib) { return 0; } using XOpenDisplayFn = void* (*)(const char*); using XDefaultScreenFn = int (*)(void*); using XDefaultVisualFn = void* (*)(void*, int); using XVisualIDFromVisualFn = unsigned long (*)(void*); using XCloseDisplayFn = int (*)(void*); auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); auto* xDefaultScreen = reinterpret_cast(dlsym(x11Lib, "XDefaultScreen")); auto* xDefaultVisual = reinterpret_cast(dlsym(x11Lib, "XDefaultVisual")); auto* xVisualIDFromVisual = reinterpret_cast(dlsym(x11Lib, "XVisualIDFromVisual")); auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); if (!xOpenDisplay || !xDefaultScreen || !xDefaultVisual || !xVisualIDFromVisual || !xCloseDisplay) { dlclose(x11Lib); return 0; } void* display = xOpenDisplay(displayName); if (!display) { dlclose(x11Lib); return 0; } const int screen = xDefaultScreen(display); void* visual = xDefaultVisual(display, screen); const auto visualId = visual ? static_cast(xVisualIDFromVisual(visual)) : 0; xCloseDisplay(display); dlclose(x11Lib); return visualId; #else return 0; #endif } static EGLint QueryX11WindowVisualId(NativeWindowType window) { #if defined(__linux__) && !defined(__ANDROID__) && __has_include() if (!window) { return 0; } const char* displayName = std::getenv("DISPLAY"); if (!displayName || !MG_Util::X11::MayDialX11Display(displayName)) { return 0; } void* x11Lib = OpenX11Lib(); if (!x11Lib) { return 0; } using XOpenDisplayFn = Display* (*)(const char*); using XGetWindowAttributesFn = int (*)(Display*, Window, XWindowAttributes*); using XVisualIDFromVisualFn = unsigned long (*)(Visual*); using XCloseDisplayFn = int (*)(Display*); auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); auto* xGetWindowAttributes = reinterpret_cast(dlsym(x11Lib, "XGetWindowAttributes")); auto* xVisualIDFromVisual = reinterpret_cast(dlsym(x11Lib, "XVisualIDFromVisual")); auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); if (!xOpenDisplay || !xGetWindowAttributes || !xVisualIDFromVisual || !xCloseDisplay) { dlclose(x11Lib); return 0; } Display* display = xOpenDisplay(displayName); if (!display) { dlclose(x11Lib); return 0; } XWindowAttributes attrs{}; EGLint visualId = 0; if (xGetWindowAttributes(display, static_cast(window), &attrs) && attrs.visual) { visualId = static_cast(xVisualIDFromVisual(attrs.visual)); } xCloseDisplay(display); dlclose(x11Lib); return visualId; #else (void)window; return 0; #endif } static Bool GetConfigAttrib(EGLConfig config, EGLint attr, EGLint& value) { return g_EGLFuncs.eglGetConfigAttrib && g_EGLFuncs.eglGetConfigAttrib(g_Display, config, attr, &value); } static Bool ConfigSupports(EGLConfig config, EGLint surfaceBit) { EGLint surfaceType = 0; EGLint renderableType = 0; if (!GetConfigAttrib(config, EGL_SURFACE_TYPE, surfaceType)) { return false; } if (!GetConfigAttrib(config, EGL_RENDERABLE_TYPE, renderableType)) { return false; } return (surfaceType & surfaceBit) && (renderableType & EGL_OPENGL_ES3_BIT); } static Bool ChooseConfigForSurface(EGLint surfaceBit, EGLConfig& outConfig, NativeWindowType window = static_cast(0)) { const EGLint configAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, EGL_NONE}; EGLint numConfigs = 0; if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, nullptr, 0, &numConfigs) || numConfigs == 0) { return false; } Vector configs(static_cast(numConfigs)); if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, configs.data(), numConfigs, &numConfigs) || numConfigs == 0) { return false; } configs.resize(static_cast(numConfigs)); if (surfaceBit == EGL_WINDOW_BIT) { // X11 drivers can reserve every alpha-8 config for 32-bit ARGB // visuals (NVIDIA), so a default-visual (depth 24) window only // matches an alpha-0 config; keep those as a second candidate tier // for the visual match or eglCreateWindowSurface hits BAD_CONFIG. Vector alphaFreeConfigs; const EGLint alphaFreeAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, EGL_NONE}; EGLint numAlphaFree = 0; if (g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, nullptr, 0, &numAlphaFree) && numAlphaFree > 0) { alphaFreeConfigs.resize(static_cast(numAlphaFree)); if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(), numAlphaFree, &numAlphaFree)) { numAlphaFree = 0; } alphaFreeConfigs.resize(static_cast(numAlphaFree)); } const EGLint windowVisualId = QueryX11WindowVisualId(window); const EGLint visualIds[] = {windowVisualId, QueryDefaultX11VisualId()}; for (const auto visualId : visualIds) { if (visualId == 0) { continue; } for (const auto* candidates : {&configs, &alphaFreeConfigs}) { for (const auto config : *candidates) { EGLint nativeVisualId = 0; if (ConfigSupports(config, surfaceBit) && GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) && nativeVisualId == visualId) { outConfig = config; return true; } } } } } for (const auto config : configs) { if (ConfigSupports(config, surfaceBit)) { outConfig = config; return true; } } outConfig = configs.front(); return true; } // 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 (ARCHITECTURE.md §13.2). 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() { DiagnoseEsprytSlotArm(); } // RESET NOTIFICATION, decided once per process by its first native context (EnsureNativeContext // says why). MOBILEGL_ESPRYT_RESET_NOTIFICATION=0 keeps the driver's default. namespace { // EGL_EXT_create_context_robustness (eglext.h is not part of this file's include set). constexpr EGLint kEglResetNotificationStrategyExt = 0x3138; constexpr EGLint kEglLoseContextOnResetExt = 0x31BF; enum class ResetNotification : Uint8 { Undecided, On, Off }; std::atomic g_resetNotification{ResetNotification::Undecided}; Bool ResetNotificationWanted() { const ResetNotification decided = g_resetNotification.load(std::memory_order_acquire); if (decided != ResetNotification::Undecided) return decided == ResetNotification::On; if (const char* value = std::getenv("MOBILEGL_ESPRYT_RESET_NOTIFICATION"); value != nullptr && value[0] == '0') return false; if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) return false; const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); if (extensions == nullptr) return false; constexpr const char kWord[] = "EGL_EXT_create_context_robustness"; for (const char* at = std::strstr(extensions, kWord); at != nullptr; at = std::strstr(at + 1, kWord)) { const char after = at[sizeof(kWord) - 1]; if ((at == extensions || at[-1] == ' ') && (after == '\0' || after == ' ')) return true; } return false; } void NoteResetNotificationRefused() { ResetNotification expected = ResetNotification::Undecided; if (g_resetNotification.compare_exchange_strong(expected, ResetNotification::Off, std::memory_order_acq_rel)) MGLOG_W("DirectGLES: the driver refused EGL_LOSE_CONTEXT_ON_RESET; a context that hangs the GPU is " "not told, and its session cannot be ended for it"); } void NoteResetNotificationDecided(Bool used) { ResetNotification expected = ResetNotification::Undecided; const ResetNotification decided = used ? ResetNotification::On : ResetNotification::Off; if (g_resetNotification.compare_exchange_strong(expected, decided, std::memory_order_acq_rel)) MGLOG_I("DirectGLES: native contexts %s reset notification (EGL_LOSE_CONTEXT_ON_RESET): a context " "whose work hangs or faults the GPU %s", used ? "ask for" : "do not ask for", used ? "is named by the driver and ends its own session" : "is not told"); } } // namespace Bool NativeContextsNotifyResets() { return g_resetNotification.load(std::memory_order_acquire) == ResetNotification::On; } static Bool EnsureNativeContext(NativeContextTuple& tuple, EGLint surfaceBit, NativeWindowType window) { if (!EnsureDisplayInitialized()) { MGLOG_E("EnsureNativeContext: the EGL display did not initialize"); return false; } if (tuple.Context != EGL_NO_CONTEXT) return true; if (tuple.Config == nullptr && !ChooseConfigForSurface(surfaceBit, tuple.Config, window)) { MGLOG_E("EnsureNativeContext: no EGL config for surface bit 0x%x (window=%p)", static_cast(surfaceBit), reinterpret_cast(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; } } // P14: AND OTHERWISE ANY LIVE NATIVE CONTEXT IN THE PROCESS. The backend keeps buffers, // textures and syncs of its own in process-wide state (the UBO/unpack/upload rings, the // buffer pool, scratch textures, frame fences) and every session's apply thread uses // them; they are only names in another session's context if all the contexts are one // share group. Client isolation does not come from the driver's namespaces - the twin // tables are bucketed per {session, share group} and never hand one bucket's names to // another - so a process-wide group costs nothing in separation. if (share == EGL_NO_CONTEXT) { for (auto* other : NativeContexts()) { if (other != &tuple && other->Context != EGL_NO_CONTEXT) { 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 // request yields a 3.0 context that lacks the 3.1/3.2 texture targets the // capability probes exercise; mobile drivers ignore the minor and hand out // their maximum either way. // // RESET NOTIFICATION. With the driver's default strategy a context whose work hung the GPU // is never told: after Adreno's ~2 s timeout reset it silently drops everything that context // submits (the client's window just stops), and its client never learns it should recreate. // Asking for EGL_LOSE_CONTEXT_ON_RESET lets the driver name the guilty context // (glGetGraphicsResetStatus answers GUILTY, once) and LatchIfDeviceLost ends that session // alone, so the client reads a lost context and can recover. (A ~10 s reset that a failed // preemption ends was not reported to the server's contexts in measurement - see // GpuHangWatch.h.) Every context of the process shares one group, and contexts that share // must agree on the strategy, so it is decided once, by the first context, and kept. for (const EGLint minorVersion : {2, 1, 0}) { const Bool notify = ResetNotificationWanted(); const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, minorVersion, notify ? kEglResetNotificationStrategyExt : EGL_NONE, kEglLoseContextOnResetExt, EGL_NONE}; tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs); if (tuple.Context == EGL_NO_CONTEXT && notify && share == EGL_NO_CONTEXT) { // The process's first context: if the same version WITHOUT the strategy is accepted, // it was the strategy the driver refused, and no context of the process asks for it. const EGLint plainAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, minorVersion, EGL_NONE}; tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, plainAttribs); if (tuple.Context != EGL_NO_CONTEXT) NoteResetNotificationRefused(); } if (tuple.Context != EGL_NO_CONTEXT) { NoteResetNotificationDecided(notify); tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; ++g_displayUsers; return true; } } const Bool notify = ResetNotificationWanted(); const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, notify ? kEglResetNotificationStrategyExt : EGL_NONE, kEglLoseContextOnResetExt, EGL_NONE}; tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs); if (tuple.Context == EGL_NO_CONTEXT) return false; NoteResetNotificationDecided(notify); tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; ++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 MOBILEGL_BUILD_DISAGGREGATED // Its last shared-image reads, fenced while it is still current. if (t_native.boundContext == tuple->Context) SharedImageImpl::PublishPendingReads(); #endif 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); } // This thread no longer has it current either way; a context created later at the same // address must not read as "already bound". if (t_native.boundContext == tuple->Context) { t_native.boundContext = EGL_NO_CONTEXT; t_native.boundDraw = EGL_NO_SURFACE; t_nativeContextThread.BoundSerial = 0; } ForgetNativeContextObjects(tuple->Serial); tuple->Serial = 0; 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; // NOT WHILE A SESSION STILL HOLDS SURFACES. eglTerminate destroys them with the display, and // the session keeps naming them: the next context made current on one (an application that // destroys its only context and creates another on the same surface, as eglinfo does with // its surfaceless stand-in) bound a surface the driver no longer had. The session's own // teardown terminates the display once its surfaces are gone (DestroyNativeStateOfSession). Bool surfacesRemain = false; for (const auto* session : NativeSessions()) surfacesRemain = surfacesRemain || !session->Surfaces.empty(); if (g_displayUsers == 0 && !surfacesRemain && g_Display != EGL_NO_DISPLAY && g_EGLFuncs.eglTerminate) { g_EGLFuncs.eglTerminate(g_Display); NoteDisplayTerminated(); 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. static Bool SessionOwnsSurface(const NativeSessionState& session, EGLSurface surface) { if (surface == EGL_NO_SURFACE) return false; for (const auto& entry : session.Surfaces) { if (entry.Surface == surface) return true; } return false; } 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; // A MULTI-WINDOW CLIENT: each context draws to the surface it was last made current with, // not to whichever surface the session bound most recently - Qt renders each window on its // own thread with its own context, and binding a context back onto another window's // surface drew one window's frame into the other. NativeSessionState& session = ActiveNativeSession(); // Nothing to make it current on yet: a context bound for the records of its own creation // (EGLImpl's CreateContext), which no make-current has given a surface (or whose surface // is gone). Binding it to whichever surface the session holds would hand it another // context's surface - one the driver may not even have any more. Its first make-current // binds it natively. if (tuple.Draw == EGL_NO_SURFACE) return true; if (tuple.Draw != session.Draw && SessionOwnsSurface(session, tuple.Draw)) { session.Draw = tuple.Draw; NoteDrawSurfaceMoved(); session.Read = SessionOwnsSurface(session, tuple.Read) ? tuple.Read : tuple.Draw; } if (t_native.boundContext == tuple.Context && t_native.boundDraw == session.Draw) return true; return MakeCurrent(); } EGLSurface CurrentSessionDrawSurface() { return ActiveNativeSession().Draw; } EGLSurface CurrentContextDrawSurface() { return ActiveNativeContext().Draw; } namespace { // Whether the driver of `display` has EGL_EXT_buffer_age (or EGL_KHR_partial_update, which // defines the same query). Asked once per display. // EGL_BUFFER_AGE_EXT (= EGL_BUFFER_AGE_KHR), spelled out: this file sees only the core EGL header. constexpr EGLint kEglBufferAge = 0x313D; // How the driver answers a buffer age, if at all. EGL_EXT_buffer_age promises the whole // buffer's content for the age it reports. EGL_KHR_partial_update alone promises it only // OUTSIDE the frame's damage region, which is the whole surface until eglSetDamageRegionKHR // narrows it - a frame that asks the age and then draws part of the surface without setting // the region finds the rest undefined (on a tiler: tiles never loaded, noise on screen). struct NativeBufferAge { Bool Ext = false; // EGL_EXT_buffer_age Bool Partial = false; // EGL_KHR_partial_update EGLBoolean (*SetDamageRegion)(EGLDisplay, EGLSurface, EGLint*, EGLint) = nullptr; }; const NativeBufferAge& NativeBufferAgeSupport() { static EGLDisplay asked = EGL_NO_DISPLAY; static NativeBufferAge support; if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) { static const NativeBufferAge none; return none; } if (asked != g_Display) { asked = g_Display; const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); support = {}; support.Ext = extensions != nullptr && std::strstr(extensions, "EGL_EXT_buffer_age") != nullptr; support.Partial = extensions != nullptr && std::strstr(extensions, "EGL_KHR_partial_update") != nullptr; if (support.Partial && g_EGLFuncs.eglGetProcAddress) { support.SetDamageRegion = reinterpret_cast( g_EGLFuncs.eglGetProcAddress("eglSetDamageRegionKHR")); } if (support.SetDamageRegion == nullptr) support.Partial = false; MGLOG_I("DirectGLES: native buffer age: EGL_EXT_buffer_age %s, EGL_KHR_partial_update %s", support.Ext ? "yes" : "no", support.Partial ? "yes" : "no"); } return support; } } // namespace // The window surface whose age the driver was asked in the frame being drawn on this (the // session's apply) thread; the present ends the frame. thread_local EGLSurface t_ageAskedSurface = EGL_NO_SURFACE; // Whether the frame being drawn on this thread has had its flush ahead of the window // buffer (FlushAheadOfWindowBufferUse); the present ends the frame. thread_local Bool t_windowBufferFlushIssued = false; namespace { // A WINDOW SURFACE'S BUFFER AGE, on unless MOBILEGL_ESPRYT_WINDOW_AGE=0 (or, on Android, the // property debug.mobilegl.espryt_window_age=0); off, a window answers 0 and its client // repaints whole frames. It was opt-in while a window repainted by age sometimes showed a // client frame two or three presents old: the client's shared image had kept its previous // frame, its copy clipped by a scissor guard that trusted another session's render-state // shadow (ScopedScissorDisable) - whole repaints only happened to leave that shadow // agreeing with the client. Pbuffers (client windows) are not affected. Bool WindowBufferAgeEnabled() { static const Bool enabled = [] { if (const char* env = std::getenv("MOBILEGL_ESPRYT_WINDOW_AGE")) return std::strcmp(env, "0") != 0; #if defined(__ANDROID__) char property[PROP_VALUE_MAX] = {}; if (__system_property_get("debug.mobilegl.espryt_window_age", property) > 0) return std::strcmp(property, "0") != 0; #endif return true; }(); return enabled; } } // namespace // THE OFF-SCREEN PART OF A FRAME DOES NOT WAIT FOR THE WINDOW BUFFER. A window surface takes // its next buffer when the frame first uses the default framebuffer, and about every other // frame that is the buffer the display has only just released: its release fence is still // pending. Whatever the driver has not yet submitted then goes out at the swap, in one batch // behind that fence - the frame's off-screen passes included - so the GPU starts the whole // frame only after the display's next commit. A flush just before the first use lets the // off-screen work start now; only the window pass waits. Once per frame, on window surfaces // only (a pbuffer has no buffer to wait for), and a flush is not a wait. void FlushAheadOfWindowBufferUse() { if (t_windowBufferFlushIssued) return; t_windowBufferFlushIssued = true; NativeSessionState& session = ActiveNativeSession(); const EGLSurface surface = session.Draw; if (surface == EGL_NO_SURFACE) return; { const std::lock_guard lock(g_nativeRegistryMutex); const auto* entry = FindSessionSurfaceLocked(session, surface); if (entry == nullptr || entry->Window == static_cast(0)) return; } if (g_GLESFuncs.glFlush) g_GLESFuncs.glFlush(); } Int32 CurrentDrawBufferAge(Bool damageRegionFollows) { NativeSessionState& session = ActiveNativeSession(); const EGLSurface surface = session.Draw; if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return 0; Bool window = false; Uint64 presents = 0; { const std::lock_guard lock(g_nativeRegistryMutex); const auto* entry = FindSessionSurfaceLocked(session, surface); if (entry == nullptr) return 0; window = entry->Window != static_cast(0); presents = entry->Presents; } if (!window) return presents > 0 ? 1 : 0; if (!WindowBufferAgeEnabled()) return 0; // The driver's answer. On Android the query dequeues the buffer the next frame draws into - // what the frame's first draw would do anyway, and the client asks before it draws. const NativeBufferAge& support = NativeBufferAgeSupport(); const Bool askable = MG_Util::Damage::NativeAgeAskable(support.Ext, support.Partial, damageRegionFollows); if (presents == 0 || !askable || !g_EGLFuncs.eglQuerySurface) return 0; EGLint age = 0; if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &age) || age < 0) return 0; t_ageAskedSurface = surface; return age; } Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region) { NativeSessionState& session = ActiveNativeSession(); const EGLSurface surface = session.Draw; if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return true; { const std::lock_guard lock(g_nativeRegistryMutex); const auto* entry = FindSessionSurfaceLocked(session, surface); if (entry == nullptr || entry->Window == static_cast(0)) return true; } const NativeBufferAge& support = NativeBufferAgeSupport(); if (!support.Partial || !WindowBufferAgeEnabled()) return true; // EGL_KHR_partial_update wants the age asked in this frame before the region is set; a client // that was answered 0 without the driver being asked (its first frame) repaints everything. if (t_ageAskedSurface != surface) { EGLint ignored = 0; (void)g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &ignored); t_ageAskedSurface = surface; } EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; const EGLint count = region.IsFull() ? 0 : static_cast(MG_Util::Damage::PackRects(region, rects, MG_Util::Damage::kMaxRects)); if (support.SetDamageRegion(g_Display, surface, count > 0 ? rects : nullptr, count) != EGL_TRUE) { MGLOG_W_ONCE("DirectGLES: eglSetDamageRegionKHR refused a %d-rectangle region (EGL error 0x%x)", count, g_EGLFuncs.eglGetError ? static_cast(g_EGLFuncs.eglGetError()) : 0u); } return true; } Bool SelectSessionSurface(EGLSurface surface) { NativeSessionState& session = ActiveNativeSession(); if (!SessionOwnsSurface(session, surface)) return false; session.Draw = surface; NoteDrawSurfaceMoved(); session.Read = surface; return true; } Bool BindSessionSurface(EGLSurface surface) { NativeSessionState& session = ActiveNativeSession(); if (!SessionOwnsSurface(session, surface)) return false; session.Draw = surface; NoteDrawSurfaceMoved(); session.Read = surface; if (ActiveNativeContext().Context == EGL_NO_CONTEXT) return true; if (t_native.boundContext == ActiveNativeContext().Context && t_native.boundDraw == surface) return true; return MakeCurrent(); } EGLSurface CreateSessionPbuffer(EGLint width, EGLint height) { NativeContextTuple& tuple = ActiveNativeContext(); if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast(0))) return EGL_NO_SURFACE; const EGLint attribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, attribs); if (surface == EGL_NO_SURFACE) return EGL_NO_SURFACE; NativeSessionState& session = ActiveNativeSession(); const std::lock_guard lock(g_nativeRegistryMutex); session.Surfaces.push_back({static_cast(0), surface}); g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); return surface; } void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement) { if (previous == EGL_NO_SURFACE || replacement == EGL_NO_SURFACE || previous == replacement) return; NativeSessionState& session = ActiveNativeSession(); { const std::lock_guard lock(g_nativeRegistryMutex); for (auto* tuple : NativeContexts()) { if (tuple->SessionKey != session.SessionKey) continue; if (tuple->Draw == previous) tuple->Draw = replacement; if (tuple->Read == previous) tuple->Read = replacement; } auto& entries = session.Surfaces; entries.erase(std::remove_if( entries.begin(), entries.end(), [&](const NativeSessionState::SurfaceEntry& entry) { return entry.Surface == previous; }), entries.end()); g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); } if (session.Draw == previous) { session.Draw = replacement; NoteDrawSurfaceMoved(); } if (session.Read == previous) session.Read = replacement; // The calling thread drew to the old one: draw to the new one from here (the old is then // no longer current, and its destruction below is immediate). if (t_native.boundDraw == previous && ActiveNativeContext().Context != EGL_NO_CONTEXT) (void)MakeCurrent(); if (g_EGLFuncs.eglDestroySurface && g_Display != EGL_NO_DISPLAY) g_EGLFuncs.eglDestroySurface(g_Display, previous); } void DestroySessionSurface(EGLSurface surface) { if (surface == EGL_NO_SURFACE) return; NativeSessionState& session = ActiveNativeSession(); { const std::lock_guard lock(g_nativeRegistryMutex); auto& entries = session.Surfaces; const auto it = std::find_if(entries.begin(), entries.end(), [&](const NativeSessionState::SurfaceEntry& entry) { return entry.Surface == surface; }); if (it == entries.end()) return; entries.erase(it); for (auto* tuple : NativeContexts()) { if (tuple->SessionKey != session.SessionKey) continue; if (tuple->Draw == surface) tuple->Draw = EGL_NO_SURFACE; if (tuple->Read == surface) tuple->Read = EGL_NO_SURFACE; } g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); } if (session.Draw == surface || session.Read == surface) { // Fall back onto a surface the session still has, so the next bind has somewhere to go. const EGLSurface fallback = session.Surfaces.empty() ? EGL_NO_SURFACE : session.Surfaces.front().Surface; session.Draw = fallback; NoteDrawSurfaceMoved(); session.Read = fallback; if (t_native.boundDraw == surface && fallback != EGL_NO_SURFACE && ActiveNativeContext().Context != EGL_NO_CONTEXT) { (void)MakeCurrent(); } } // A surface still current on this thread is destroyed by EGL once it is not (EGL 1.5 // 3.5.6), so this needs no unbind first. if (g_EGLFuncs.eglDestroySurface && g_Display != EGL_NO_DISPLAY) g_EGLFuncs.eglDestroySurface(g_Display, surface); if (t_native.boundDraw == surface && session.Draw == EGL_NO_SURFACE) t_native.boundDraw = EGL_NO_SURFACE; } namespace { // Last swap interval the app requested through eglSwapInterval; -1 = never // requested (keep the EGL default of 1). Re-applied when the window surface // is (re)created since interval is per-surface state. Int g_requestedSwapInterval = -1; void ApplyRequestedSwapInterval() { if (g_requestedSwapInterval < 0) 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"); } } // namespace void SetSwapInterval(Int interval) { g_requestedSwapInterval = interval; ApplyRequestedSwapInterval(); } #if MOBILEGL_BUILD_DISAGGREGATED void ForgetRequestedSwapInterval() { g_requestedSwapInterval = -1; } #endif // A crashed predecessor session may have left its surface bound to this window (the // server-owned display window is one ANativeWindow for the process's lifetime, and // several drivers refuse a second eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW). // Teardown is keyed on the session the surface belongs to, so a session that died without // one leaks it; find and destroy those before asking the driver. static void DestroyStaleSurfacesOnWindow(NativeWindowType window) { const std::lock_guard lock(g_nativeRegistryMutex); for (auto* session : NativeSessions()) { auto& entries = session->Surfaces; for (auto it = entries.begin(); it != entries.end();) { if (it->Window == window) { if (g_EGLFuncs.eglDestroySurface) g_EGLFuncs.eglDestroySurface(g_Display, it->Surface); if (session->Draw == it->Surface) session->Draw = EGL_NO_SURFACE; if (session->Read == it->Surface) session->Read = EGL_NO_SURFACE; it = entries.erase(it); } else { ++it; } } } g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); } // The default framebuffer's shape after a window surface was bound - with its extent when the // window is the server's own (see the push arm of the publish). static void PublishWindowSurfaceShape(NativeWindowType window) { #if MOBILEGL_BUILD_DISAGGREGATED // P12 (D3): this publish may carry the window's extent (see the push arm of the publish) - // review fix: only when the window is the SERVER's own (a headless client's ServerOwned // surface). A window the client named keeps the format-only publish without the knob. g_publishWindowExtent = MG_Pipe::MGPipeServerOwnedWindow() != nullptr && reinterpret_cast(window) == MG_Pipe::MGPipeServerOwnedWindow(); PublishDefaultFramebufferDepthStencilFormat(); g_publishWindowExtent = false; #else (void)window; PublishDefaultFramebufferDepthStencilFormat(); #endif } // THE SERVER WINDOW CHANGED SIZE (the app's layout moved the view: a bar shown or hidden, a // rotation). The new extent - the display's, set by the server loop before this runs - is published // as the creation publishes it, at once: the client sizes its viewport and default framebuffer from // it, and a compositor with nothing to redraw makes no present to hang this on. Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window) { if (surface == EGL_NO_SURFACE || !window) return false; // Bound, because the publish reads the default framebuffer's format. if (!BindSessionSurface(surface) || t_native.boundDraw != surface) return false; PublishWindowSurfaceShape(window); return true; } EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface) { if (windowSurface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; if (!SessionOwnsSurface(ActiveNativeSession(), windowSurface)) return EGL_NO_SURFACE; // 1x1: nothing is shown while the window is gone, and nothing is published for it - the // client keeps the extent it has until the resume publishes the new window's. const EGLSurface placeholder = CreateSessionPbuffer(1, 1); if (placeholder == EGL_NO_SURFACE) { const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; MGLOG_E("DirectGLES: no placeholder pbuffer could stand in for the window surface %p (EGL error 0x%x)", reinterpret_cast(windowSurface), static_cast(error)); return EGL_NO_SURFACE; } ReplaceSessionSurface(windowSurface, placeholder); // The rebind onto the placeholder failed and the window surface is still current here - EGL // destroys a current surface only once it is not, and the window goes when this returns. if (t_native.boundDraw == windowSurface) (void)ReleaseCurrent(); return placeholder; } EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window) { if (!window || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; NativeContextTuple& tuple = ActiveNativeContext(); if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return EGL_NO_SURFACE; DestroyStaleSurfacesOnWindow(window); const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); if (surface == EGL_NO_SURFACE) { const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; MGLOG_E("DirectGLES: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x on resume", reinterpret_cast(window), static_cast(error)); return EGL_NO_SURFACE; } NativeSessionState& session = ActiveNativeSession(); { const std::lock_guard lock(g_nativeRegistryMutex); session.Surfaces.push_back({window, surface}); g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); } if (placeholder != EGL_NO_SURFACE && SessionOwnsSurface(session, placeholder)) { ReplaceSessionSurface(placeholder, surface); } // Bound, because the publish below reads the default framebuffer. if (!BindSessionSurface(surface) || t_native.boundDraw != surface) { MGLOG_E("DirectGLES: the resumed window surface %p could not be made current; its extent is published " "at its next bind", reinterpret_cast(surface)); return surface; } ApplyRequestedSwapInterval(); PublishWindowSurfaceShape(window); return surface; } Bool InitWindowSurface(NativeWindowType window) { if (!window) return false; DiagnoseEsprytSlotArmAtBringUp(); NativeContextTuple& tuple = ActiveNativeContext(); if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return false; DestroyStaleSurfacesOnWindow(window); const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); if (surface == EGL_NO_SURFACE) { const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; MGLOG_E("InitWindowSurface: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x", reinterpret_cast(window), static_cast(error)); return false; } NativeSessionState& session = ActiveNativeSession(); session.Surfaces.push_back({window, surface}); session.Draw = surface; NoteDrawSurfaceMoved(); session.Read = surface; tuple.Draw = surface; tuple.Read = surface; if (!MakeCurrent()) return false; ApplyRequestedSwapInterval(); PublishWindowSurfaceShape(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; 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}; 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({static_cast(0), surface}); session.Draw = surface; NoteDrawSurfaceMoved(); session.Read = surface; tuple.Draw = surface; tuple.Read = surface; if (!MakeCurrent()) return false; PublishDefaultFramebufferDepthStencilFormat(); 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; } // The single backend ES context migrates between app threads (FCL/pojav-style // LWJGL hands the EGL context from JVM thread to JVM thread). Ownership must // live in ONE global slot: a per-thread flag can never be cleared on the // LOSING thread when another thread takes (or destroys/releases) the context, // leaving a stale "current" claim behind. A stale claim makes buffer ops issue // GL calls that silently no-op (no context is current on that thread) while // still updating shadow bookkeeping (bind cache, synced serials), permanently // desynchronizing backend buffer state. std::atomic g_backendContextOwnerThread{}; // Bumped whenever the backend ES context is destroyed; fence and // timer-query handles created under an older generation belong to a // dead context and must never be passed back to GL (mirrors // BufferImpl's context tracking). Uint g_syncContextGeneration = 1; // One-fence-per-frame ring driving the buffer-storage pool's recycle gate. // A buffer retired during frame N is safe to reuse once frame N's fence has // signaled. Touched only in Present()/DestroyEGLContext() on the owning // thread -> no lock. Each fence carries the sync generation so a dead-context // GLsync is never polled/deleted. Depth 4 >> the 2-3 frames Adreno keeps in // flight; wrap-before-signal only happens during a stall and just degrades to // the allocate path. std::atomic g_currentFrameSerial{0}; std::atomic g_completedFrameSerial{0}; FrameFence g_frameFenceRing[kFrameFenceRingDepth]; // Defined next to the stamp globals below; every owner-id writer must reset the // EGL verification stamp BEFORE publishing the new owner. void InvalidateEglVerifiedStamp(); // EVERY SHADOW OF THE CURRENT NATIVE CONTEXT'S BINDINGS, dropped whenever the context the // calling thread renders into may have changed (MakeCurrent, a turn handoff between two // sessions' apply threads). Re-issuing a bind is cheap; trusting a shadow that describes // another context skips one the driver needed (a texture sampled from the wrong unit, a // draw against another context's VAO). Requires a current context: it re-establishes // GL_TEXTURE0 so the active-unit shadow is true rather than guessed. static void InvalidateNativeBindingShadows() { // Conservatively drop the redundant-glUseProgram guard: re-issuing one bind // after a MakeCurrent is cheaper than trusting a possibly-reset context. PrgramImpl::g_lastUsedBackendProgramId = 0; // The GLContext becoming current may be a fresh one whose slot versions // restarted at zero; the broadcast memo's key is only monotonic within one. PrgramImpl::InvalidateBroadcastMemo(); BufferImpl::InvalidateArrayBufferBindingCache(); BufferImpl::InvalidateIndexedBufferBindingCache(); BufferImpl::InvalidatePixelBufferBindingCaches(); BufferImpl::InvalidateTransformFeedbackBindingShadows(); FramebufferImpl::InvalidateFramebufferBindingCache(); FramebufferImpl::InvalidateFramebufferHandleArmMemos(); VertexArrayImpl::InvalidateVAOBindingCache(); PixelStoreImpl::InvalidatePackStateCache(); // The render-state shadow belongs in this list for the same reason as the ones above: // it describes the real ES context, which outlives the MobileGL context that is // becoming current. See InvalidateSyncedRenderState. RenderStateImpl::InvalidateSyncedRenderState(); TextureImpl::g_boundTexturesCache = {}; SamplerImpl::g_boundSamplersCache = {}; if (g_GLESFuncs.glActiveTexture) g_GLESFuncs.glActiveTexture(GL_TEXTURE0); TextureImpl::g_activeTextureUnit = 0; } Bool MakeCurrent() { // 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 MOBILEGL_BUILD_DISAGGREGATED // Shared-image reads can only be fenced from the context that made them. if (t_native.boundContext != EGL_NO_CONTEXT && t_native.boundContext != tuple.Context) { SharedImageImpl::PublishPendingReads(); } #endif 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; } g_EGLFuncs.eglSwapBuffersWithDamageEXT = MG_Util::BackendLoader::ResolveSwapBuffersWithDamage(g_EGLFuncs); InvalidateEglVerifiedStamp(); t_native.boundContext = tuple.Context; t_native.boundDraw = session.Draw; t_nativeContextThread.BoundSerial = tuple.Serial; 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. BufferImpl::RegisterBufferBackendOps(); InvalidateNativeBindingShadows(); // eglSwapInterval requires a current context; a request made while none was // current (and dropped by the driver) is retried here. ApplyRequestedSwapInterval(); return true; } // P14: THE BACKEND'S TURN MOVED TO ANOTHER APPLY THREAD (ServerLoop's BackendTurn). Each // session's apply thread keeps its own native context current, but the backend's owner // thread and every binding shadow are process-wide and still describe the context of the // thread that held the turn before. Take ownership for this thread and forget them. void OnBackendTurnHandoff() { InvalidateEglVerifiedStamp(); if (t_native.boundContext == EGL_NO_CONTEXT) { g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return; } g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); InvalidateNativeBindingShadows(); } Bool ReleaseCurrent() { if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) { InvalidateEglVerifiedStamp(); g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } #if MOBILEGL_BUILD_DISAGGREGATED if (t_native.boundContext != EGL_NO_CONTEXT) SharedImageImpl::PublishPendingReads(); #endif if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; MGLOG_E_ONCE("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error); return false; } // Clearing the global owner works from ANY thread (a release request can // legally arrive on a thread other than the current owner); erring towards // "not current" only defers buffer ops, which is always safe. InvalidateEglVerifiedStamp(); t_native.boundContext = EGL_NO_CONTEXT; t_native.boundDraw = EGL_NO_SURFACE; t_nativeContextThread.BoundSerial = 0; g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } void InvalidateEglVerifiedStamp() { // ~0 frame serial matches no real frame, so the next owner-thread call // re-verifies against EGL itself. g_eglVerifiedFrameSerial.store(~0ull, std::memory_order_relaxed); g_eglVerifiedContextGeneration.store(0, std::memory_order_relaxed); } // 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. static void DestroyNativeStateOfSession(Uint64 sessionKey); void DestroyEGLContext() { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) MG_Pipe::MGPipeServerSetContextLive(false); #endif // P14: ANOTHER SESSION'S CONTEXT STILL LIVES. Every native context is in one share group, so // the buffers, textures and syncs this process's backend holds outlive this session's // contexts: bumping the process-wide generations here told every live session that its // GL objects were dead, and they were rebuilt EMPTY (a client exiting or closing its last // window turned the compositor's textures to noise). Only this session's own state goes; // the process-wide reset below is for the last context in the process. const Uint64 endingSession = CurrentNativeSessionKey(); Bool othersLive = false; { const std::lock_guard lock(g_nativeRegistryMutex); for (auto* tuple : NativeContexts()) { if (tuple->SessionKey != endingSession && tuple->Context != EGL_NO_CONTEXT) { othersLive = true; break; } } } if (othersLive) { BufferImpl::OnServerSessionEndedWithOthersLive(); DestroyNativeStateOfSession(endingSession); return; } BufferImpl::OnBackendContextDestroyed(); XfbImpl::OnBackendContextDestroyed(); MultiDrawImpl::OnBackendContextDestroyed(); OnRestartSubstitutionContextDestroyed(); ScratchFBOImpl::OnBackendContextDestroyed(); ReleasePackedWordScratchTexture(); FramebufferImpl::InvalidateFramebufferBindingCache(); FramebufferImpl::InvalidateFramebufferHandleArmMemos(); VertexArrayImpl::InvalidateVAOBindingCache(); PixelStoreImpl::InvalidatePackStateCache(); PrgramImpl::InvalidateBroadcastMemo(); // Texture ids belong to the dying context; wrappers destroyed later must // not glDeleteTextures a recycled name in a successor context. ++g_backendContextGeneration; g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); // Outstanding fence handles now refer to a dead context; treat them as // signaled from here on. ++g_syncContextGeneration; // The frame-fence ring's syncs belong to the dead context too; abandon them // (the context reclaims its syncs) and floor the completed watermark to the // current serial so buffers retired under the old context read as GPU-idle. for (FrameFence& slot : g_frameFenceRing) slot = {}; g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed), std::memory_order_relaxed); DestroyNativeStateOfSession(CurrentNativeSessionKey()); } // This session's native contexts and surfaces, and the display when they were the last. static void DestroyNativeStateOfSession(Uint64 sessionKey) { if (g_Display == EGL_NO_DISPLAY) return; #if MOBILEGL_BUILD_DISAGGREGATED if (t_native.boundContext != EGL_NO_CONTEXT) SharedImageImpl::PublishPendingReads(); #endif { 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); } t_native.boundContext = EGL_NO_CONTEXT; t_native.boundDraw = EGL_NO_SURFACE; t_nativeContextThread.BoundSerial = 0; for (auto* tuple : NativeContexts()) { if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue; ForgetNativeContextObjects(tuple->Serial); tuple->Serial = 0; 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 auto& entry : session->Surfaces) { if (entry.Surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(g_Display, entry.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); NoteDisplayTerminated(); g_Display = EGL_NO_DISPLAY; } } } // namespace MobileGL::MG_Backend::DirectGLES