mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
- Layout only: token streams match up to split literals, continuations and using order - The size totals fall, so the structure baseline is lowered
1684 lines
88 KiB
C++
1684 lines
88 KiB
C++
// 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<Uint64> 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<Uint64> 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<Uint64> 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<NativeContextTuple*>& NativeContexts() {
|
|
static Vector<NativeContextTuple*>* contexts = new Vector<NativeContextTuple*>();
|
|
return *contexts;
|
|
}
|
|
Vector<NativeSessionState*>& NativeSessions() {
|
|
static Vector<NativeSessionState*>* sessions = new Vector<NativeSessionState*>();
|
|
return *sessions;
|
|
}
|
|
|
|
std::atomic<NativeContextKeyResolver> 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<TwinKeyResolver> 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<Uint64> g_nativeContextSerials{0};
|
|
|
|
std::mutex& PerNativeContextRegistryMutex() {
|
|
static auto* mutex = new std::mutex();
|
|
return *mutex;
|
|
}
|
|
Vector<PerNativeContextBase*>& PerNativeContextRegistry() {
|
|
static auto* registry = new Vector<PerNativeContextBase*>();
|
|
return *registry;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
std::atomic<Uint64> 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<Uint32> g_perNativeContextCacheSlots{0};
|
|
} // namespace
|
|
|
|
PerNativeContextBase::PerNativeContextBase()
|
|
: m_cacheSlot(g_perNativeContextCacheSlots.fetch_add(1, std::memory_order_relaxed) %
|
|
NativeContextThreadState::kCacheEntries) {
|
|
const std::lock_guard<std::mutex> lock(PerNativeContextRegistryMutex());
|
|
PerNativeContextRegistry().push_back(this);
|
|
}
|
|
|
|
PerNativeContextBase::~PerNativeContextBase() {
|
|
BumpPerNativeContextEpoch();
|
|
const std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(g_nativeRegistryMutex);
|
|
NativeContextTuple* found = nullptr;
|
|
for (auto* tuple : NativeContexts()) {
|
|
if (tuple->SessionKey == session && tuple->ContextToken == token) {
|
|
found = tuple;
|
|
break;
|
|
}
|
|
}
|
|
if (found == nullptr) {
|
|
found = new NativeContextTuple{};
|
|
found->SessionKey = session;
|
|
found->ContextToken = token;
|
|
NativeContexts().push_back(found);
|
|
(void)AcquireNativeSession(session);
|
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
|
}
|
|
t_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<std::mutex> lock(g_nativeRegistryMutex);
|
|
Uint64 count = 0;
|
|
for (auto* tuple : NativeContexts()) {
|
|
if (tuple->Context != EGL_NO_CONTEXT) ++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
Uint64 NativeContextCountForSession(Uint64 sessionKey) {
|
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
|
Uint64 count = 0;
|
|
for (auto* tuple : NativeContexts()) {
|
|
if (tuple->SessionKey == sessionKey && tuple->Context != EGL_NO_CONTEXT) ++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken) {
|
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
|
const NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken);
|
|
return tuple != nullptr ? tuple->Context : EGL_NO_CONTEXT;
|
|
}
|
|
|
|
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken) {
|
|
t_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<Int>(width);
|
|
outHeight = static_cast<Int>(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<GLuint>(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<GLint>(eglDepth);
|
|
}
|
|
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_STENCIL_SIZE, &eglStencil)) {
|
|
stencilBits = static_cast<GLint>(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<Uint32>(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<int>(depthFormat),
|
|
static_cast<int>(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<XOpenDisplayFn>(dlsym(x11Lib, "XOpenDisplay"));
|
|
auto* xDefaultScreen = reinterpret_cast<XDefaultScreenFn>(dlsym(x11Lib, "XDefaultScreen"));
|
|
auto* xDefaultVisual = reinterpret_cast<XDefaultVisualFn>(dlsym(x11Lib, "XDefaultVisual"));
|
|
auto* xVisualIDFromVisual = reinterpret_cast<XVisualIDFromVisualFn>(dlsym(x11Lib, "XVisualIDFromVisual"));
|
|
auto* xCloseDisplay = reinterpret_cast<XCloseDisplayFn>(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<EGLint>(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(<X11/Xlib.h>)
|
|
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<XOpenDisplayFn>(dlsym(x11Lib, "XOpenDisplay"));
|
|
auto* xGetWindowAttributes = reinterpret_cast<XGetWindowAttributesFn>(dlsym(x11Lib, "XGetWindowAttributes"));
|
|
auto* xVisualIDFromVisual = reinterpret_cast<XVisualIDFromVisualFn>(dlsym(x11Lib, "XVisualIDFromVisual"));
|
|
auto* xCloseDisplay = reinterpret_cast<XCloseDisplayFn>(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>(window), &attrs) && attrs.visual) {
|
|
visualId = static_cast<EGLint>(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<NativeWindowType>(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<EGLConfig> configs(static_cast<SizeT>(numConfigs));
|
|
if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, configs.data(), numConfigs, &numConfigs) ||
|
|
numConfigs == 0) {
|
|
return false;
|
|
}
|
|
configs.resize(static_cast<SizeT>(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<EGLConfig> 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<SizeT>(numAlphaFree));
|
|
if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(), numAlphaFree,
|
|
&numAlphaFree)) {
|
|
numAlphaFree = 0;
|
|
}
|
|
alphaFreeConfigs.resize(static_cast<SizeT>(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<ResetNotification> 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<unsigned>(surfaceBit), reinterpret_cast<void*>(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<std::mutex> lock(g_nativeRegistryMutex);
|
|
AcquireNativeSession(sessionKey);
|
|
for (auto* candidate : NativeContexts()) {
|
|
if (candidate->SessionKey == sessionKey && candidate->ContextToken == contextToken) {
|
|
tuple = candidate;
|
|
break;
|
|
}
|
|
}
|
|
if (tuple == nullptr) {
|
|
tuple = new NativeContextTuple{};
|
|
tuple->SessionKey = sessionKey;
|
|
tuple->ContextToken = contextToken;
|
|
NativeContexts().push_back(tuple);
|
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
|
}
|
|
}
|
|
if (tuple->Context != EGL_NO_CONTEXT) return true; // idempotent
|
|
tuple->ShareGroupToken = shareGroupToken;
|
|
return EnsureNativeContext(*tuple, EGL_PBUFFER_BIT, static_cast<NativeWindowType>(0));
|
|
}
|
|
|
|
void DestroyNativeContextFor(Uint64 contextToken) {
|
|
const Uint64 sessionKey = CurrentNativeSessionKey();
|
|
if (contextToken == 0) return;
|
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
|
NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken);
|
|
if (tuple == nullptr || tuple->Context == EGL_NO_CONTEXT) return;
|
|
#if 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<decltype(support.SetDamageRegion)>(
|
|
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<std::mutex> lock(g_nativeRegistryMutex);
|
|
const auto* entry = FindSessionSurfaceLocked(session, surface);
|
|
if (entry == nullptr || entry->Window == static_cast<NativeWindowType>(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<std::mutex> lock(g_nativeRegistryMutex);
|
|
const auto* entry = FindSessionSurfaceLocked(session, surface);
|
|
if (entry == nullptr) return 0;
|
|
window = entry->Window != static_cast<NativeWindowType>(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<std::mutex> lock(g_nativeRegistryMutex);
|
|
const auto* entry = FindSessionSurfaceLocked(session, surface);
|
|
if (entry == nullptr || entry->Window == static_cast<NativeWindowType>(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<EGLint>(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<unsigned>(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<NativeWindowType>(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<std::mutex> lock(g_nativeRegistryMutex);
|
|
session.Surfaces.push_back({static_cast<NativeWindowType>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<const void*>(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<void*>(windowSurface), static_cast<unsigned>(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<void*>(window), static_cast<unsigned>(error));
|
|
return EGL_NO_SURFACE;
|
|
}
|
|
NativeSessionState& session = ActiveNativeSession();
|
|
{
|
|
const std::lock_guard<std::mutex> 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<void*>(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<void*>(window), static_cast<unsigned>(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<NativeWindowType>(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<NativeWindowType>(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<std::thread::id> 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<Uint64> g_currentFrameSerial{0};
|
|
std::atomic<Uint64> 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<std::mutex> 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<std::mutex> 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
|