mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (P14): state ownership - multi-session, multi-context, share groups
Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.
S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.
S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.
S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.
S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.
S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.
Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.
Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
This commit is contained in:
@@ -464,6 +464,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
|
||||||
|
|
||||||
MobileGL/MG_State/GLState/Core.cpp
|
MobileGL/MG_State/GLState/Core.cpp
|
||||||
|
MobileGL/MG_State/GLState/ShareGroupState.cpp
|
||||||
MobileGL/MG_State/EGLState/Core.cpp
|
MobileGL/MG_State/EGLState/Core.cpp
|
||||||
MobileGL/MG_State/GLState/ErrorState/Error.cpp
|
MobileGL/MG_State/GLState/ErrorState/Error.cpp
|
||||||
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
|
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
|
||||||
@@ -662,6 +663,10 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||||
|
# P14 S2: the per-session runtime and the thread routing that lets one process serve
|
||||||
|
# several sessions (the real session a singleton accessor answers with on that
|
||||||
|
# session's own threads).
|
||||||
|
MobileGL/MG_Remote/Server/SessionRuntime.cpp
|
||||||
MobileGL/MG_Remote/Server/ServerMain.cpp
|
MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||||
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
|
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
|
||||||
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
|
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
|
||||||
|
|||||||
@@ -976,8 +976,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
|
||||||
|
//
|
||||||
|
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
|
||||||
|
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
|
||||||
|
// the native context - and with it every buffer, texture and program the application had
|
||||||
|
// built. The native context now belongs to the (session, context token) tuple the backend
|
||||||
|
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
|
||||||
|
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
|
||||||
|
// bookkeeping (which surface is active, which threads made it current) does describe the
|
||||||
|
// surface that is being replaced.
|
||||||
if (m_eglSurfaceInitialized) {
|
if (m_eglSurfaceInitialized) {
|
||||||
DestroyEGLContext();
|
|
||||||
ResetEGLRuntimeState();
|
ResetEGLRuntimeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1008,8 +1017,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S4: as for the window surface above - the native context is the (session, context
|
||||||
|
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
|
||||||
if (m_eglSurfaceInitialized) {
|
if (m_eglSurfaceInitialized) {
|
||||||
DestroyEGLContext();
|
|
||||||
ResetEGLRuntimeState();
|
ResetEGLRuntimeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16180,10 +16180,225 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_GLESCapabilities = capabilities;
|
g_GLESCapabilities = capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): the native tuple is per context.
|
||||||
|
//
|
||||||
|
// THE DISPLAY STAYS ONE OBJECT. eglInitialize is once per process and eglTerminate runs when the
|
||||||
|
// last native context has gone, so a second context - or a second session - never terminates the
|
||||||
|
// display the first one is still using.
|
||||||
|
//
|
||||||
|
// EVERYTHING ELSE THAT WAS A FILE-LEVEL SINGLETON IS KEYED. A native EGLContext, the config it
|
||||||
|
// was created with and the native surfaces are keyed by (session, client context token): the
|
||||||
|
// identity S1 put on the wire (bind_context / SurfaceOp.context) and S2 gave a per-thread owner.
|
||||||
|
// The key is resolved on the calling thread through a probe the server layer installs
|
||||||
|
// (SetNativeContextKeyResolver); with none installed the answer is the single key {0, 0}, which
|
||||||
|
// is byte-for-byte the process singleton this replaces - monolith, a frontend-only process and
|
||||||
|
// every unit case keep exactly one tuple.
|
||||||
|
//
|
||||||
|
// WHY THE SURFACES ARE KEYED BY SESSION AND NOT BY CONTEXT: an EGLSurface is not owned by an
|
||||||
|
// EGLContext (EGL has no such relationship - only the draw/read BINDING is per context), so a
|
||||||
|
// session's surfaces are one set and each context records which of them it currently draws to.
|
||||||
|
// That is also what makes pbuffer -> window -> pbuffer keep its objects: creating the second
|
||||||
|
// surface does not touch the context, so the twins the first surface's draws built are still
|
||||||
|
// there when the third one is bound.
|
||||||
|
|
||||||
static EGLDisplay g_Display = EGL_NO_DISPLAY;
|
static EGLDisplay g_Display = EGL_NO_DISPLAY;
|
||||||
static EGLContext g_Context = EGL_NO_CONTEXT;
|
// eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0.
|
||||||
static EGLSurface g_Surface = EGL_NO_SURFACE;
|
static Uint32 g_displayUsers = 0;
|
||||||
static EGLConfig g_Config = nullptr;
|
|
||||||
|
struct NativeContextTuple {
|
||||||
|
Uint64 SessionKey = 0;
|
||||||
|
Uint64 ContextToken = 0;
|
||||||
|
Uint64 ShareGroupToken = 0;
|
||||||
|
EGLContext Context = EGL_NO_CONTEXT;
|
||||||
|
EGLConfig Config = nullptr;
|
||||||
|
// The session surface this context draws to / reads from, as last made current for it.
|
||||||
|
EGLSurface Draw = EGL_NO_SURFACE;
|
||||||
|
EGLSurface Read = EGL_NO_SURFACE;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NativeSessionState {
|
||||||
|
Uint64 SessionKey = 0;
|
||||||
|
// Every native surface this session created, so its teardown destroys its own and only its own.
|
||||||
|
Vector<EGLSurface> Surfaces;
|
||||||
|
// The surface the session currently draws to / reads from - what a bind to another context
|
||||||
|
// re-makes current, and what Present/eglSwapBuffers names.
|
||||||
|
EGLSurface Draw = EGL_NO_SURFACE;
|
||||||
|
EGLSurface Read = EGL_NO_SURFACE;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::mutex g_nativeRegistryMutex;
|
||||||
|
// Bumped on every registry mutation. The per-thread active-tuple cache below is keyed on it, so
|
||||||
|
// a destroyed tuple can never be handed out from a stale cache entry.
|
||||||
|
static std::atomic<Uint64> g_nativeRegistryEpoch{1};
|
||||||
|
// Never erased while the process lives: a context teardown marks its tuple dead (Context =
|
||||||
|
// EGL_NO_CONTEXT) instead of freeing it, so a cached pointer is never dangling between the
|
||||||
|
// epoch bump and the next resolution.
|
||||||
|
static Vector<NativeContextTuple*>& NativeContexts() {
|
||||||
|
static Vector<NativeContextTuple*>* contexts = new Vector<NativeContextTuple*>();
|
||||||
|
return *contexts;
|
||||||
|
}
|
||||||
|
static Vector<NativeSessionState*>& NativeSessions() {
|
||||||
|
static Vector<NativeSessionState*>* sessions = new Vector<NativeSessionState*>();
|
||||||
|
return *sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<NativeContextKeyResolver> g_nativeContextKeyResolver{nullptr};
|
||||||
|
|
||||||
|
// A one-way override for a case that drives the registry with no session runtime installed:
|
||||||
|
// when `t_forcedNativeKeyValid` is set, the key is the forced pair instead of the probe's
|
||||||
|
// answer. The setter clears the active-tuple cache, which is keyed on the effective key.
|
||||||
|
thread_local Bool t_forcedNativeKeyValid = false;
|
||||||
|
thread_local Uint64 t_forcedNativeSession = 0;
|
||||||
|
thread_local Uint64 t_forcedNativeToken = 0;
|
||||||
|
|
||||||
|
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver) {
|
||||||
|
g_nativeContextKeyResolver.store(resolver, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ResolveNativeKey(Uint64& outSession, Uint64& outToken) {
|
||||||
|
if (t_forcedNativeKeyValid) {
|
||||||
|
outSession = t_forcedNativeSession;
|
||||||
|
outToken = t_forcedNativeToken;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
outSession = 0;
|
||||||
|
outToken = 0;
|
||||||
|
const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire);
|
||||||
|
if (resolver != nullptr) (void)resolver(&outSession, &outToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 CurrentNativeSessionKey() {
|
||||||
|
Uint64 session = 0;
|
||||||
|
Uint64 token = 0;
|
||||||
|
ResolveNativeKey(session, token);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The active tuple for this thread. The fast path is a key compare against a thread-local
|
||||||
|
// cache, because IsBackendContextCurrentOnThisThread sits under every draw; the slow path
|
||||||
|
// (a first call, a bind_context switch, or any registry mutation) walks the registry, which
|
||||||
|
// holds one tuple per live client context.
|
||||||
|
thread_local NativeContextTuple* t_activeNativeTuple = nullptr;
|
||||||
|
thread_local Uint64 t_activeNativeEpoch = 0;
|
||||||
|
thread_local Uint64 t_activeNativeSession = 0;
|
||||||
|
thread_local Uint64 t_activeNativeToken = 0;
|
||||||
|
// The native context THIS THREAD last made current. An EGLContext can be current on only one
|
||||||
|
// thread, and MakeCurrent is the only writer, so a plain thread-local is exact - and it is
|
||||||
|
// what makes MakeNativeContextCurrentForBoundToken a no-op when the bind did not actually
|
||||||
|
// change the native context (the single-context world's every bind).
|
||||||
|
thread_local EGLContext t_boundNativeContext = EGL_NO_CONTEXT;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static NativeSessionState& AcquireNativeSession(Uint64 sessionKey) {
|
||||||
|
for (auto* session : NativeSessions()) {
|
||||||
|
if (session->SessionKey == sessionKey) return *session;
|
||||||
|
}
|
||||||
|
auto* created = new NativeSessionState{};
|
||||||
|
created->SessionKey = sessionKey;
|
||||||
|
NativeSessions().push_back(created);
|
||||||
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
return *created;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeSessionState& ActiveNativeSession() {
|
||||||
|
Uint64 session = 0;
|
||||||
|
Uint64 token = 0;
|
||||||
|
ResolveNativeKey(session, token);
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
return AcquireNativeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tuple the calling thread works on, created on first use. Lazy creation is what keeps the
|
||||||
|
// pre-S4 shapes working unchanged: a session that never crossed a CreateContext - the whole
|
||||||
|
// single-context world, and every existing fixture - resolves to its token-0 tuple and gets
|
||||||
|
// exactly the one native context it always had.
|
||||||
|
static NativeContextTuple& ActiveNativeContext() {
|
||||||
|
Uint64 session = 0;
|
||||||
|
Uint64 token = 0;
|
||||||
|
ResolveNativeKey(session, token);
|
||||||
|
const Uint64 epoch = g_nativeRegistryEpoch.load(std::memory_order_acquire);
|
||||||
|
if (t_activeNativeTuple != nullptr && t_activeNativeEpoch == epoch &&
|
||||||
|
t_activeNativeSession == session && t_activeNativeToken == token) {
|
||||||
|
return *t_activeNativeTuple;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
NativeContextTuple* found = nullptr;
|
||||||
|
for (auto* tuple : NativeContexts()) {
|
||||||
|
if (tuple->SessionKey == session && tuple->ContextToken == token) {
|
||||||
|
found = tuple;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found == nullptr) {
|
||||||
|
found = new NativeContextTuple{};
|
||||||
|
found->SessionKey = session;
|
||||||
|
found->ContextToken = token;
|
||||||
|
NativeContexts().push_back(found);
|
||||||
|
(void)AcquireNativeSession(session);
|
||||||
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
t_activeNativeTuple = found;
|
||||||
|
t_activeNativeEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire);
|
||||||
|
t_activeNativeSession = session;
|
||||||
|
t_activeNativeToken = token;
|
||||||
|
return *found;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeContextTuple* FindNativeContext(Uint64 sessionKey, Uint64 contextToken) {
|
||||||
|
for (auto* tuple : NativeContexts()) {
|
||||||
|
if (tuple->SessionKey == sessionKey && tuple->ContextToken == contextToken) return tuple;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 NativeContextCount() {
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
Uint64 count = 0;
|
||||||
|
for (auto* tuple : NativeContexts()) {
|
||||||
|
if (tuple->Context != EGL_NO_CONTEXT) ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 NativeContextCountForSession(Uint64 sessionKey) {
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
Uint64 count = 0;
|
||||||
|
for (auto* tuple : NativeContexts()) {
|
||||||
|
if (tuple->SessionKey == sessionKey && tuple->Context != EGL_NO_CONTEXT) ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken) {
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
const NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken);
|
||||||
|
return tuple != nullptr ? tuple->Context : EGL_NO_CONTEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken) {
|
||||||
|
t_forcedNativeSession = sessionKey;
|
||||||
|
t_forcedNativeToken = contextToken;
|
||||||
|
t_forcedNativeKeyValid = true;
|
||||||
|
// The cache is keyed on the effective key, so drop it rather than let a stale entry answer.
|
||||||
|
t_activeNativeTuple = nullptr;
|
||||||
|
t_activeNativeEpoch = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The display's one-time initialization. Returns false only when there is no EGL to talk to.
|
||||||
|
static Bool EnsureDisplayInitialized() {
|
||||||
|
if (g_Display != EGL_NO_DISPLAY) return true;
|
||||||
|
if (!g_EGLFuncs.eglGetDisplay) return false;
|
||||||
|
g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
if (g_Display == EGL_NO_DISPLAY) return false;
|
||||||
|
if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) {
|
||||||
|
g_Display = EGL_NO_DISPLAY;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (g_EGLFuncs.eglBindAPI) g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
// P12 (on-screen server window), D3: true only while InitWindowSurface publishes the default
|
// P12 (on-screen server window), D3: true only while InitWindowSurface publishes the default
|
||||||
@@ -16195,14 +16410,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) {
|
static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) {
|
||||||
outWidth = 0;
|
outWidth = 0;
|
||||||
outHeight = 0;
|
outHeight = 0;
|
||||||
if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) {
|
// P14 S4: the surface is the SESSION's, not the process's - the tuple that draws to it is
|
||||||
|
// one of the session's contexts.
|
||||||
|
const EGLSurface surface = ActiveNativeSession().Draw;
|
||||||
|
if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLint width = 0;
|
EGLint width = 0;
|
||||||
EGLint height = 0;
|
EGLint height = 0;
|
||||||
if (!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_WIDTH, &width) ||
|
if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &width) ||
|
||||||
!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_HEIGHT, &height) ||
|
!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height) ||
|
||||||
width <= 0 || height <= 0) {
|
width <= 0 || height <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -16273,15 +16491,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// A driver that refuses the query leaves both at 0. Fall back to what the EGL config
|
// A driver that refuses the query leaves both at 0. Fall back to what the EGL config
|
||||||
// was chosen with, which is what the surface actually has.
|
// was chosen with, which is what the surface actually has. P14 S4: the config belongs to the
|
||||||
|
// context the surface is bound to, so it is the active tuple's.
|
||||||
|
const EGLConfig activeConfig = ActiveNativeContext().Config;
|
||||||
if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY &&
|
if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY &&
|
||||||
g_Config != nullptr) {
|
activeConfig != nullptr) {
|
||||||
EGLint eglDepth = 0;
|
EGLint eglDepth = 0;
|
||||||
EGLint eglStencil = 0;
|
EGLint eglStencil = 0;
|
||||||
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, g_Config, EGL_DEPTH_SIZE, &eglDepth)) {
|
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_DEPTH_SIZE, &eglDepth)) {
|
||||||
depthBits = static_cast<GLint>(eglDepth);
|
depthBits = static_cast<GLint>(eglDepth);
|
||||||
}
|
}
|
||||||
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, g_Config, EGL_STENCIL_SIZE, &eglStencil)) {
|
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_STENCIL_SIZE, &eglStencil)) {
|
||||||
stencilBits = static_cast<GLint>(eglStencil);
|
stencilBits = static_cast<GLint>(eglStencil);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16291,9 +16511,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// PREVIOUS surface published in place, which would be stale - this function runs
|
// PREVIOUS surface published in place, which would be stale - this function runs
|
||||||
// once per surface activation, not once per process. It is written this way anyway
|
// once per surface activation, not once per process. It is written this way anyway
|
||||||
// because the branch is unreachable for a surface MobileGL chose itself:
|
// because the branch is unreachable for a surface MobileGL chose itself:
|
||||||
// InitDisplayAndContext asks eglChooseConfig for EGL_DEPTH_SIZE 24 and
|
// ChooseConfigForSurface asks eglChooseConfig for EGL_DEPTH_SIZE 24 and
|
||||||
// EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so g_Config always has both
|
// EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so the context's config always
|
||||||
// and the EGL fallback above always answers. A caller that supplies its own
|
// has both and the EGL fallback above always answers. A caller that supplies its own
|
||||||
// depth-less config would keep the previous surface's description; publishing a
|
// depth-less config would keep the previous surface's description; publishing a
|
||||||
// guess instead would be a different lie, and the placeholder attachment model has
|
// guess instead would be a different lie, and the placeholder attachment model has
|
||||||
// no way to say "this buffer does not exist" short of detaching it.
|
// no way to say "this buffer does not exist" short of detaching it.
|
||||||
@@ -16565,32 +16785,54 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool InitDisplayAndContext(EGLint surfaceBit, NativeWindowType window = static_cast<NativeWindowType>(0)) {
|
// P14 S4. THE CONTEXT FOR `tuple`, AND THE SURFACE'S CONFIG, WITH NO TEARDOWN IN FRONT.
|
||||||
DestroyEGLContext();
|
//
|
||||||
|
// The S4 deletion is the first statement this function used to begin with: `DestroyEGLContext()`
|
||||||
|
// ran UNCONDITIONALLY, so creating a second surface (let alone a second context) destroyed the
|
||||||
|
// native context the first one's objects lived in. A tuple that already has a native context
|
||||||
|
// keeps it and only (re)chooses the config the new surface is created with; a tuple that has
|
||||||
|
// none builds one, sharing with the group leader when CreateNativeContextFor named a group.
|
||||||
|
//
|
||||||
|
// A CONFIG THAT IS ALREADY SET IS NOT RE-CHOSEN FOR A DIFFERENT SURFACE KIND, and that is the
|
||||||
|
// deliberate trade: an EGLSurface's config must be compatible with the context's, so re-choosing
|
||||||
|
// would mean recreating the context - exactly the teardown this slice deletes. A driver that
|
||||||
|
// refuses the resulting eglCreate*Surface refuses it by name, which is the honest failure.
|
||||||
|
// P14 S4. THE BRING-UP DIAGNOSIS, in one place because BOTH surface paths owe it (it used to be
|
||||||
|
// the first statement of InitDisplayAndContext, which both went through). Extracted rather than
|
||||||
|
// duplicated when that function was replaced by the per-tuple EnsureNativeContext.
|
||||||
|
//
|
||||||
|
// Diagnose, and deliberately NOT resolve: resolving raises
|
||||||
|
// Fatal{PipeLegacyMemosDisabled}, and this function runs inside eglMakeCurrent, which
|
||||||
|
// the integration harness pre-flights in a FORKED CHILD
|
||||||
|
// (MG_IntegrationTest/Harness/HeadlessGL.cpp). A child that dies on a signal is reported
|
||||||
|
// to the parent as "no usable GPU/display/ICD" and every scenario in the lane is
|
||||||
|
// SKIPPED - so the stop became a green lane that ran nothing, on exactly the two env
|
||||||
|
// vars the D14/D18 A/B is driven with (ROADMAP.md:7). The stop now belongs to the first
|
||||||
|
// twin lookup, which happens in a scenario body where a crash IS a test failure.
|
||||||
|
static void DiagnoseEsprytSlotArmAtBringUp() {
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// DIAGNOSE the twin-table arm here, at backend startup, so an operator who set
|
|
||||||
// MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_LEGACY_MEMOS into a combination that leaves no
|
|
||||||
// arm at all is told so by name, in the log, before the first draw.
|
|
||||||
//
|
|
||||||
// Diagnose, and deliberately NOT resolve: resolving raises
|
|
||||||
// Fatal{PipeLegacyMemosDisabled}, and this function runs inside eglMakeCurrent, which
|
|
||||||
// the integration harness pre-flights in a FORKED CHILD
|
|
||||||
// (MG_IntegrationTest/Harness/HeadlessGL.cpp). A child that dies on a signal is reported
|
|
||||||
// to the parent as "no usable GPU/display/ICD" and every scenario in the lane is
|
|
||||||
// SKIPPED - so the stop became a green lane that ran nothing, on exactly the two env
|
|
||||||
// vars the D14/D18 A/B is driven with (ROADMAP.md:7). The stop now belongs to the first
|
|
||||||
// twin lookup, which happens in a scenario body where a crash IS a test failure.
|
|
||||||
DiagnoseEsprytSlotArm();
|
DiagnoseEsprytSlotArm();
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
static Bool EnsureNativeContext(NativeContextTuple& tuple, EGLint surfaceBit, NativeWindowType window) {
|
||||||
if (g_Display == EGL_NO_DISPLAY) return false;
|
if (!EnsureDisplayInitialized()) return false;
|
||||||
|
if (tuple.Context != EGL_NO_CONTEXT) return true;
|
||||||
|
|
||||||
if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) return false;
|
if (tuple.Config == nullptr && !ChooseConfigForSurface(surfaceBit, tuple.Config, window)) return false;
|
||||||
g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API);
|
|
||||||
|
|
||||||
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
|
// A share group whose leader has a live native context hands it to eglCreateContext: this is
|
||||||
|
// the whole of "shared contexts see the same objects" on the server. The group is recorded
|
||||||
|
// by CreateNativeContextFor before this runs; a tuple nobody named a group for starts one.
|
||||||
|
EGLContext share = EGL_NO_CONTEXT;
|
||||||
|
for (auto* other : NativeContexts()) {
|
||||||
|
if (other != &tuple && other->SessionKey == tuple.SessionKey &&
|
||||||
|
other->Context != EGL_NO_CONTEXT && tuple.ShareGroupToken != 0 &&
|
||||||
|
other->ShareGroupToken == tuple.ShareGroupToken) {
|
||||||
|
share = other->Context;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
|
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
|
||||||
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
|
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
|
||||||
@@ -16601,15 +16843,83 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
|
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
|
||||||
EGL_CONTEXT_MINOR_VERSION, minorVersion,
|
EGL_CONTEXT_MINOR_VERSION, minorVersion,
|
||||||
EGL_NONE};
|
EGL_NONE};
|
||||||
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
|
tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs);
|
||||||
if (g_Context != EGL_NO_CONTEXT) {
|
if (tuple.Context != EGL_NO_CONTEXT) {
|
||||||
|
++g_displayUsers;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||||
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs);
|
tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs);
|
||||||
return g_Context != EGL_NO_CONTEXT;
|
if (tuple.Context == EGL_NO_CONTEXT) return false;
|
||||||
|
++g_displayUsers;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken) {
|
||||||
|
if (contextToken == 0) return false;
|
||||||
|
const Uint64 sessionKey = CurrentNativeSessionKey();
|
||||||
|
NativeContextTuple* tuple = nullptr;
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
AcquireNativeSession(sessionKey);
|
||||||
|
for (auto* candidate : NativeContexts()) {
|
||||||
|
if (candidate->SessionKey == sessionKey && candidate->ContextToken == contextToken) {
|
||||||
|
tuple = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tuple == nullptr) {
|
||||||
|
tuple = new NativeContextTuple{};
|
||||||
|
tuple->SessionKey = sessionKey;
|
||||||
|
tuple->ContextToken = contextToken;
|
||||||
|
NativeContexts().push_back(tuple);
|
||||||
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tuple->Context != EGL_NO_CONTEXT) return true; // idempotent
|
||||||
|
tuple->ShareGroupToken = shareGroupToken;
|
||||||
|
return EnsureNativeContext(*tuple, EGL_PBUFFER_BIT, static_cast<NativeWindowType>(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyNativeContextFor(Uint64 contextToken) {
|
||||||
|
const Uint64 sessionKey = CurrentNativeSessionKey();
|
||||||
|
if (contextToken == 0) return;
|
||||||
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken);
|
||||||
|
if (tuple == nullptr || tuple->Context == EGL_NO_CONTEXT) return;
|
||||||
|
if (g_EGLFuncs.eglDestroyContext && g_Display != EGL_NO_DISPLAY) {
|
||||||
|
// The context may be current on this very thread; unbinding first is what EGL requires
|
||||||
|
// (eglDestroyContext of a current context is EGL_BAD_ACCESS).
|
||||||
|
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() == tuple->Context &&
|
||||||
|
g_EGLFuncs.eglMakeCurrent) {
|
||||||
|
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
}
|
||||||
|
g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context);
|
||||||
|
}
|
||||||
|
tuple->Context = EGL_NO_CONTEXT;
|
||||||
|
tuple->Config = nullptr;
|
||||||
|
tuple->Draw = EGL_NO_SURFACE;
|
||||||
|
tuple->Read = EGL_NO_SURFACE;
|
||||||
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
if (g_displayUsers > 0) --g_displayUsers;
|
||||||
|
if (g_displayUsers == 0 && g_Display != EGL_NO_DISPLAY && g_EGLFuncs.eglTerminate) {
|
||||||
|
g_EGLFuncs.eglTerminate(g_Display);
|
||||||
|
g_Display = EGL_NO_DISPLAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. See DirectGLES.h; in one line, `bind_context`
|
||||||
|
// moves the session's context attribution and this makes the calling thread's native tuple
|
||||||
|
// follow it, binding the session's current surface to the newly named context's EGLContext.
|
||||||
|
Bool MakeNativeContextCurrentForBoundToken() {
|
||||||
|
NativeContextTuple& tuple = ActiveNativeContext();
|
||||||
|
// Nothing to switch to: a session that never sent CreateContext has no native context for
|
||||||
|
// this token, and building one here would bind a context the client does not know it has.
|
||||||
|
if (tuple.Context == EGL_NO_CONTEXT) return true;
|
||||||
|
if (t_boundNativeContext == tuple.Context) return true;
|
||||||
|
return MakeCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -16620,7 +16930,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
void ApplyRequestedSwapInterval() {
|
void ApplyRequestedSwapInterval() {
|
||||||
if (g_requestedSwapInterval < 0) return;
|
if (g_requestedSwapInterval < 0) return;
|
||||||
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) return;
|
const EGLSurface surface = ActiveNativeSession().Draw;
|
||||||
|
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) return;
|
||||||
const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval);
|
const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval);
|
||||||
MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
|
MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
|
||||||
ok ? "ok" : "failed");
|
ok ? "ok" : "failed");
|
||||||
@@ -16639,10 +16950,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool InitWindowSurface(NativeWindowType window) {
|
Bool InitWindowSurface(NativeWindowType window) {
|
||||||
if (!window) return false;
|
if (!window) return false;
|
||||||
|
|
||||||
if (!InitDisplayAndContext(EGL_WINDOW_BIT, window)) return false;
|
DiagnoseEsprytSlotArmAtBringUp();
|
||||||
|
|
||||||
g_Surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, g_Config, window, nullptr);
|
NativeContextTuple& tuple = ActiveNativeContext();
|
||||||
if (g_Surface == EGL_NO_SURFACE) return false;
|
if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return false;
|
||||||
|
|
||||||
|
const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr);
|
||||||
|
if (surface == EGL_NO_SURFACE) return false;
|
||||||
|
NativeSessionState& session = ActiveNativeSession();
|
||||||
|
session.Surfaces.push_back(surface);
|
||||||
|
session.Draw = surface;
|
||||||
|
session.Read = surface;
|
||||||
|
tuple.Draw = surface;
|
||||||
|
tuple.Read = surface;
|
||||||
|
|
||||||
if (!MakeCurrent()) return false;
|
if (!MakeCurrent()) return false;
|
||||||
|
|
||||||
@@ -16659,25 +16979,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PublishDefaultFramebufferDepthStencilFormat();
|
PublishDefaultFramebufferDepthStencilFormat();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
MGLOG_D("EGL context created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, g_Surface,
|
MGLOG_D("EGL window surface created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display,
|
||||||
g_Context, window);
|
surface, tuple.Context, window);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool InitPbufferSurface(EGLint width, EGLint height) {
|
Bool InitPbufferSurface(EGLint width, EGLint height) {
|
||||||
if (width <= 0 || height <= 0) return false;
|
if (width <= 0 || height <= 0) return false;
|
||||||
if (!InitDisplayAndContext(EGL_PBUFFER_BIT)) return false;
|
|
||||||
|
DiagnoseEsprytSlotArmAtBringUp();
|
||||||
|
|
||||||
|
NativeContextTuple& tuple = ActiveNativeContext();
|
||||||
|
if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast<NativeWindowType>(0))) return false;
|
||||||
|
|
||||||
const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
|
const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
|
||||||
g_Surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, g_Config, surfaceAttribs);
|
const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, surfaceAttribs);
|
||||||
if (g_Surface == EGL_NO_SURFACE) return false;
|
if (surface == EGL_NO_SURFACE) return false;
|
||||||
|
NativeSessionState& session = ActiveNativeSession();
|
||||||
|
session.Surfaces.push_back(surface);
|
||||||
|
session.Draw = surface;
|
||||||
|
session.Read = surface;
|
||||||
|
tuple.Draw = surface;
|
||||||
|
tuple.Read = surface;
|
||||||
|
|
||||||
if (!MakeCurrent()) return false;
|
if (!MakeCurrent()) return false;
|
||||||
|
|
||||||
PublishDefaultFramebufferDepthStencilFormat();
|
PublishDefaultFramebufferDepthStencilFormat();
|
||||||
|
|
||||||
MGLOG_D("EGL pbuffer context created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display,
|
MGLOG_D("EGL pbuffer surface created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display,
|
||||||
g_Surface, g_Context, width, height);
|
surface, tuple.Context, width, height);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16745,17 +17075,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void InvalidateEglVerifiedStamp();
|
void InvalidateEglVerifiedStamp();
|
||||||
|
|
||||||
Bool MakeCurrent() {
|
Bool MakeCurrent() {
|
||||||
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE ||
|
// P14 S4: the context comes from the calling thread's tuple (the session's bound token),
|
||||||
g_Context == EGL_NO_CONTEXT) {
|
// the surface from the session. Both halves used to be file-level singletons.
|
||||||
|
NativeContextTuple& tuple = ActiveNativeContext();
|
||||||
|
NativeSessionState& session = ActiveNativeSession();
|
||||||
|
tuple.Draw = session.Draw;
|
||||||
|
tuple.Read = session.Read;
|
||||||
|
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || session.Draw == EGL_NO_SURFACE ||
|
||||||
|
tuple.Context == EGL_NO_CONTEXT) {
|
||||||
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
|
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!g_EGLFuncs.eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context)) {
|
if (!g_EGLFuncs.eglMakeCurrent(g_Display, session.Draw, session.Draw, tuple.Context)) {
|
||||||
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
|
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
|
||||||
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
InvalidateEglVerifiedStamp();
|
InvalidateEglVerifiedStamp();
|
||||||
|
t_boundNativeContext = tuple.Context;
|
||||||
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
|
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
|
||||||
// The ops table may have been unregistered when a previous ES context was
|
// The ops table may have been unregistered when a previous ES context was
|
||||||
// destroyed (e.g. a probe context); re-register now that GL is usable.
|
// destroyed (e.g. a probe context); re-register now that GL is usable.
|
||||||
@@ -16798,6 +17135,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// legally arrive on a thread other than the current owner); erring towards
|
// legally arrive on a thread other than the current owner); erring towards
|
||||||
// "not current" only defers buffer ops, which is always safe.
|
// "not current" only defers buffer ops, which is always safe.
|
||||||
InvalidateEglVerifiedStamp();
|
InvalidateEglVerifiedStamp();
|
||||||
|
t_boundNativeContext = EGL_NO_CONTEXT;
|
||||||
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -16828,7 +17166,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool IsBackendContextCurrentOnThisThread() {
|
Bool IsBackendContextCurrentOnThisThread() {
|
||||||
if (g_Context == EGL_NO_CONTEXT) {
|
const NativeContextTuple& tuple = ActiveNativeContext();
|
||||||
|
if (tuple.Context == EGL_NO_CONTEXT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) {
|
if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) {
|
||||||
@@ -16846,7 +17185,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) {
|
g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) {
|
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != tuple.Context) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed);
|
g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed);
|
||||||
@@ -17263,7 +17602,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial};
|
slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial};
|
||||||
}
|
}
|
||||||
|
|
||||||
g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface);
|
// P14 S4: the session's surface, not the process's.
|
||||||
|
g_EGLFuncs.eglSwapBuffers(g_Display, ActiveNativeSession().Draw);
|
||||||
|
|
||||||
if (canFence && g_GLESFuncs.glGetSynciv) {
|
if (canFence && g_GLESFuncs.glGetSynciv) {
|
||||||
// Fences signal in submission order within one context, so the highest
|
// Fences signal in submission order within one context, so the highest
|
||||||
@@ -17297,6 +17637,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S4: THE FULL TEARDOWN, SCOPED TO THE CALLING THREAD'S SESSION.
|
||||||
|
//
|
||||||
|
// This used to destroy the process's one context, its one surface and terminate the display.
|
||||||
|
// The callers (ReleaseEGLResources, ~BackendObject_DirectGLES) mean "this session's EGL world
|
||||||
|
// goes away", so under S4 it destroys EVERY native context that session holds, its surfaces, and
|
||||||
|
// terminates the display only when no context anywhere in the process is left - which is what
|
||||||
|
// keeps a second live session (S2) serving while this one closes. With no resolver installed
|
||||||
|
// there is exactly one session (key 0) and this is the old statement, byte for word.
|
||||||
|
//
|
||||||
|
// ONE CONTEXT's teardown is DestroyNativeContextFor (the DestroyContext control frame); this is
|
||||||
|
// the session's.
|
||||||
void DestroyEGLContext() {
|
void DestroyEGLContext() {
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||||
@@ -17328,16 +17679,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
for (FrameFence& slot : g_frameFenceRing) slot = {};
|
for (FrameFence& slot : g_frameFenceRing) slot = {};
|
||||||
g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed),
|
g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed),
|
||||||
std::memory_order_relaxed);
|
std::memory_order_relaxed);
|
||||||
if (g_Display != EGL_NO_DISPLAY) {
|
|
||||||
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
const Uint64 sessionKey = CurrentNativeSessionKey();
|
||||||
if (g_Context != EGL_NO_CONTEXT) {
|
if (g_Display == EGL_NO_DISPLAY) return;
|
||||||
g_EGLFuncs.eglDestroyContext(g_Display, g_Context);
|
{
|
||||||
g_Context = EGL_NO_CONTEXT;
|
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
|
||||||
|
if (g_EGLFuncs.eglMakeCurrent) {
|
||||||
|
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
}
|
}
|
||||||
if (g_Surface != EGL_NO_SURFACE) {
|
for (auto* tuple : NativeContexts()) {
|
||||||
g_EGLFuncs.eglDestroySurface(g_Display, g_Surface);
|
if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue;
|
||||||
g_Surface = EGL_NO_SURFACE;
|
if (g_EGLFuncs.eglDestroyContext) g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context);
|
||||||
|
tuple->Context = EGL_NO_CONTEXT;
|
||||||
|
tuple->Config = nullptr;
|
||||||
|
tuple->Draw = EGL_NO_SURFACE;
|
||||||
|
tuple->Read = EGL_NO_SURFACE;
|
||||||
|
if (g_displayUsers > 0) --g_displayUsers;
|
||||||
}
|
}
|
||||||
|
for (auto* session : NativeSessions()) {
|
||||||
|
if (session->SessionKey != sessionKey) continue;
|
||||||
|
if (g_EGLFuncs.eglDestroySurface) {
|
||||||
|
for (const EGLSurface surface : session->Surfaces) {
|
||||||
|
if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(g_Display, surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session->Surfaces.clear();
|
||||||
|
session->Draw = EGL_NO_SURFACE;
|
||||||
|
session->Read = EGL_NO_SURFACE;
|
||||||
|
}
|
||||||
|
g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
// THE LAST CONTEXT ANYWHERE IN THE PROCESS IS WHAT TERMINATES THE DISPLAY. A session that
|
||||||
|
// closes while another is rendering must not take eglTerminate with it.
|
||||||
|
if (g_displayUsers == 0 && g_EGLFuncs.eglTerminate) {
|
||||||
g_EGLFuncs.eglTerminate(g_Display);
|
g_EGLFuncs.eglTerminate(g_Display);
|
||||||
g_Display = EGL_NO_DISPLAY;
|
g_Display = EGL_NO_DISPLAY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
||||||
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
||||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// The display is process state and stays one object (eglInitialize once per process, terminated
|
||||||
|
// when the last native context leaves). The native EGLContext, the config it was created with
|
||||||
|
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
|
||||||
|
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
|
||||||
|
// the first one's native context down, which is what lets two contexts (and two share groups)
|
||||||
|
// be live at once in one process.
|
||||||
|
|
||||||
|
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
|
||||||
|
// that session has bound. Installed once by the server layer; with no resolver installed -
|
||||||
|
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
|
||||||
|
// exactly the process singleton this replaces. `false` also means {0,0}.
|
||||||
|
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
|
||||||
|
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
|
||||||
|
|
||||||
|
// Builds the native context for `contextToken` under the calling thread's session. A
|
||||||
|
// `shareGroupToken` whose group already has a live native context in this session hands that
|
||||||
|
// context to eglCreateContext as the share argument - which is what makes two contexts see one
|
||||||
|
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
|
||||||
|
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
|
||||||
|
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
|
||||||
|
// contexts are untouched; the display is terminated only when the last context in the process
|
||||||
|
// has gone.
|
||||||
|
void DestroyNativeContextFor(Uint64 contextToken);
|
||||||
|
|
||||||
|
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
|
||||||
|
// records belong to; this makes the calling thread's native tuple follow it, binding the
|
||||||
|
// session's current surface to the newly named context's native EGLContext. A no-op when the
|
||||||
|
// named context has no native context yet (the session never sent CreateContext) or when the
|
||||||
|
// native context already current on this thread IS that one - so the single-context world pays
|
||||||
|
// nothing for it. Called from the bind_context path on the apply thread.
|
||||||
|
Bool MakeNativeContextCurrentForBoundToken();
|
||||||
|
// How many native contexts this process holds, and how many of them belong to one session. The
|
||||||
|
// test observables for "a second context did not tear the first one down".
|
||||||
|
Uint64 NativeContextCount();
|
||||||
|
Uint64 NativeContextCountForSession(Uint64 sessionKey);
|
||||||
|
|
||||||
|
// The calling thread's session, as the resolver above reports it (0 with no resolver).
|
||||||
|
Uint64 CurrentNativeSessionKey();
|
||||||
|
|
||||||
|
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
|
||||||
|
// hold" query and no way to ask a context for its share group, so a case that must tell "the
|
||||||
|
// same native context still serves" from "a fresh one was built", or a sharing pair from two
|
||||||
|
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
|
||||||
|
// a production path and neither changes behaviour.
|
||||||
|
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
|
||||||
|
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
|
||||||
|
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
|
||||||
|
// tuple it means when no session runtime is installed.
|
||||||
|
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
|
||||||
|
|
||||||
void DestroyEGLContext();
|
void DestroyEGLContext();
|
||||||
|
|
||||||
// Transform feedback capture spans, performed by the real ES driver. The
|
// Transform feedback capture spans, performed by the real ES driver. The
|
||||||
|
|||||||
@@ -820,6 +820,10 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
||||||
// variable: no .cpp is needed for the definition.
|
// variable: no .cpp is needed for the definition.
|
||||||
//
|
//
|
||||||
|
// P14 S2: IT IS THE PROCESS-WIDE BLOCK, and `gPipeInputs` below is the name every reader
|
||||||
|
// spells - now one reference per THREAD, resolving to a served session's own block when the
|
||||||
|
// calling thread belongs to one. See the block after it.
|
||||||
|
//
|
||||||
// LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for
|
// LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for
|
||||||
// pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply
|
// pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply
|
||||||
// leaks the global singletons to the OS instead of running destructors during static
|
// leaks the global singletons to the OS instead of running destructors during static
|
||||||
@@ -834,7 +838,48 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// about to be. So the reference is never dropped: nothing here can start such a chain.
|
// about to be. So the reference is never dropped: nothing here can start such a chain.
|
||||||
// A live context releases these SharedPtrs the ordinary way, at the fill point.
|
// A live context releases these SharedPtrs the ordinary way, at the fill point.
|
||||||
// (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.)
|
// (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.)
|
||||||
inline PipeInputs& gPipeInputs = *new PipeInputs();
|
inline PipeInputs& gPipeInputsDefault = *new PipeInputs();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// P14 S2: WHOSE BLOCK IS `gPipeInputs`?
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// ONE PROCESS, SEVERAL SERVED SESSIONS. The applier writes this block and the backend reads it
|
||||||
|
// through MGB_CTX, both on the SAME apply thread - the block IS "the state the record in hand
|
||||||
|
// carries". Two sessions applying concurrently on two apply threads through one block would
|
||||||
|
// read each other's render state, which is the cross-talk a multi-session server must not
|
||||||
|
// have. Nothing about the block's contents changed; what changed is that the name resolves to
|
||||||
|
// the SESSION'S block on a thread that belongs to one.
|
||||||
|
//
|
||||||
|
// WHY A THREAD-LOCAL REFERENCE, AND NOT SOMETHING CLEVERER. MGB_CTX is
|
||||||
|
// `(&::MobileGL::MG_Pipe::gPipeInputs)` (MG_Pipe/PipeInputsSwitch.h) and is spelled at ~380
|
||||||
|
// sites in MG_Backend/MG_Impl; a reference whose TARGET varies with the calling thread is the
|
||||||
|
// only shape that keeps all of them compiling and all of them correct. The cost is one TLS
|
||||||
|
// address computation per access site - not one per field, and with no guard: the reference is
|
||||||
|
// bound once per thread on its first use.
|
||||||
|
//
|
||||||
|
// THE RESOLVER IS A HOOK because this header may not know about sessions (MG_Remote/Server
|
||||||
|
// owns them and includes THIS header, not the other way round). It is installed once by the
|
||||||
|
// server's session registry and never replaced; null - the client process, the pull build, a
|
||||||
|
// unit case - makes every thread resolve to gPipeInputsDefault, which is the old behaviour
|
||||||
|
// byte for byte.
|
||||||
|
//
|
||||||
|
// BINDING IS PER THREAD AND HAPPENS AT FIRST USE, so a thread must be inside its session's
|
||||||
|
// scope before it touches the block. Both of a session's threads are: the control thread opens
|
||||||
|
// its scope before it reads a byte of the Hello, and the apply thread opens it as its first
|
||||||
|
// act (ServerLoop::ApplyThreadMain).
|
||||||
|
using PipeInputsThreadResolver = PipeInputs* (*)();
|
||||||
|
inline PipeInputsThreadResolver g_pipeInputsThreadResolver = nullptr;
|
||||||
|
|
||||||
|
inline PipeInputs& MGPipeServerInputsForCallingThread() {
|
||||||
|
const PipeInputsThreadResolver resolve = g_pipeInputsThreadResolver;
|
||||||
|
if (resolve != nullptr) {
|
||||||
|
if (PipeInputs* const block = resolve()) return *block;
|
||||||
|
}
|
||||||
|
return gPipeInputsDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline thread_local PipeInputs& gPipeInputs = MGPipeServerInputsForCallingThread();
|
||||||
|
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -17,6 +17,13 @@
|
|||||||
// MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's
|
// MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's
|
||||||
// translation unit is unchanged (G1).
|
// translation unit is unchanged (G1).
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The EGL context lifecycle's
|
||||||
|
// client half: eglCreateContext/eglDestroyContext cross as two control frames, and a
|
||||||
|
// context switch emits the in-band bind_context record. Reached through MG_Remote::Client
|
||||||
|
// because that is the seam MG_Impl already uses for its other wire producers
|
||||||
|
// (PipeFill.cpp's emitters) and because the frame channel belongs to the server role - this
|
||||||
|
// file must not name ServerLoop's forwarders directly.
|
||||||
|
#include <MG_Remote/Client/WireTables.h>
|
||||||
#endif
|
#endif
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -52,6 +59,35 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
return backendObject;
|
return backendObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE CONTEXT BINDING, AND
|
||||||
|
// THE ONE PLACE IT IS EMITTED.
|
||||||
|
//
|
||||||
|
// `bind_context` is the ring's half of eglMakeCurrent: the MakeCurrent control frame
|
||||||
|
// tells the server which native tuple to hold, and this record tells it which CONTEXT
|
||||||
|
// every record after it belongs to - the attribution the session table was built for.
|
||||||
|
//
|
||||||
|
// IT IS EMITTED AFTER THE SWITCH HAS SUCCEEDED AND BEFORE eglMakeCurrent RETURNS, and
|
||||||
|
// that is the whole ordering argument: the app thread cannot publish a record of the
|
||||||
|
// new context until this call has returned, and the ring is FIFO, so the binding is
|
||||||
|
// behind no verb of the context it names and ahead of every one that follows. Waiting
|
||||||
|
// for the first verb instead would need the emitter to know which context the tracker
|
||||||
|
// is about to prime; this needs only the token EGLState already holds.
|
||||||
|
//
|
||||||
|
// An unchanged token emits NOTHING: an identical eglMakeCurrent is a legal no-op in
|
||||||
|
// EGL and the session is already bound to that context.
|
||||||
|
void EmitContextBinding(EGLStateContext* state, EGLContext switched, Uint64 previousToken) {
|
||||||
|
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||||
|
// The apply thread IS the server in this process; a record emitted there would
|
||||||
|
// wait on the thread that has to apply it (EmitAndWait), which is a deadlock and
|
||||||
|
// not a slow path.
|
||||||
|
if (MG_Remote::Client::RunsAsTheServerRole()) return;
|
||||||
|
const Uint64 token = switched == EGL_NO_CONTEXT ? 0 : state->GetContextClientToken(switched);
|
||||||
|
if (token == previousToken) return;
|
||||||
|
(void)MG_Remote::Client::EmitBindContextRecord(token);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
std::recursive_mutex& EGLOperationMutex() {
|
std::recursive_mutex& EGLOperationMutex() {
|
||||||
static std::recursive_mutex mutex;
|
static std::recursive_mutex mutex;
|
||||||
return mutex;
|
return mutex;
|
||||||
@@ -329,7 +365,35 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_NO_CONTEXT;
|
return EGL_NO_CONTEXT;
|
||||||
}
|
}
|
||||||
return state->CreateContext(dpy, config, shareCtx, attrib_list);
|
const EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
|
||||||
|
if (context == EGL_NO_CONTEXT) {
|
||||||
|
return EGL_NO_CONTEXT;
|
||||||
|
}
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE TOKEN CROSSES BEFORE
|
||||||
|
// eglCreateContext RETURNS. EGLState minted it, together with the share group it was
|
||||||
|
// derived into (the sharing context's group, or a fresh one), and the server tables it
|
||||||
|
// here through the same blocking handshake the surface forwarders use - so the first
|
||||||
|
// bind_context after this call names a context the session already has, and the
|
||||||
|
// sink's "this session never created that token" refusal is unreachable on the
|
||||||
|
// well-behaved path.
|
||||||
|
//
|
||||||
|
// A REFUSAL DROPS THE CLIENT'S CONTEXT AGAIN. Keeping it would hand the application a
|
||||||
|
// handle whose first verb the server refuses - a late failure where an early one is
|
||||||
|
// available, and EGL_NO_CONTEXT with EGL_BAD_ACCESS is the early one. NoWire (the
|
||||||
|
// pre-Start bring-up, the server role) keeps it: there is no server to refuse.
|
||||||
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||||
|
!MG_Remote::Client::RunsAsTheServerRole()) {
|
||||||
|
const auto emitted = MG_Remote::Client::SendCreateContextFrame(
|
||||||
|
state->GetContextClientToken(context), state->GetContextShareGroupToken(context), 0);
|
||||||
|
if (emitted == MG_Remote::Client::ContextFrameEmit::Refused) {
|
||||||
|
state->DestroyContext(dpy, context);
|
||||||
|
state->SetError(EGL_BAD_ACCESS);
|
||||||
|
return EGL_NO_CONTEXT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||||
@@ -380,6 +444,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
const auto oldDraw = state->GetCurrentSurface(EGL_DRAW);
|
const auto oldDraw = state->GetCurrentSurface(EGL_DRAW);
|
||||||
const auto oldRead = state->GetCurrentSurface(EGL_READ);
|
const auto oldRead = state->GetCurrentSurface(EGL_READ);
|
||||||
const auto oldContext = state->GetCurrentContext();
|
const auto oldContext = state->GetCurrentContext();
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P14 S1. The context the calling thread had current BEFORE this call, as the wire
|
||||||
|
// knows it. Read here because `state->MakeCurrent` below has already committed the
|
||||||
|
// new one by the time the emission points are reached.
|
||||||
|
const Uint64 oldContextToken = state->GetContextClientToken(oldContext);
|
||||||
|
#endif
|
||||||
const String threadId = CurrentThreadIdString();
|
const String threadId = CurrentThreadIdString();
|
||||||
|
|
||||||
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
|
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
|
||||||
@@ -404,6 +474,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MGLOG_D("eglMakeCurrent release succeeded thread=%s", threadId.c_str());
|
MGLOG_D("eglMakeCurrent release succeeded thread=%s", threadId.c_str());
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
EmitContextBinding(state, EGL_NO_CONTEXT, oldContextToken);
|
||||||
|
#endif
|
||||||
return EGL_TRUE;
|
return EGL_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +495,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
MGLOG_D("eglMakeCurrent attach succeeded thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), dpy, draw,
|
MGLOG_D("eglMakeCurrent attach succeeded thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), dpy, draw,
|
||||||
read, ctx);
|
read, ctx);
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
EmitContextBinding(state, ctx, oldContextToken);
|
||||||
|
#endif
|
||||||
return EGL_TRUE;
|
return EGL_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +506,29 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
return state->DestroyContext(dpy, ctx) ? EGL_TRUE : EGL_FALSE;
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P14 S1. Read BEFORE the client's own book drops the context: after DestroyContext
|
||||||
|
// the handle no longer resolves and the token is unrecoverable. The frame is sent
|
||||||
|
// AFTER the client's state machine has accepted the destroy, so one EGL refused (a
|
||||||
|
// context still current on some thread, EGL_BAD_ACCESS) never reaches the server - the
|
||||||
|
// two ends stay in step without the server having to guess why a token vanished.
|
||||||
|
const Uint64 contextToken = state->GetContextClientToken(ctx);
|
||||||
|
#endif
|
||||||
|
if (!state->DestroyContext(dpy, ctx)) {
|
||||||
|
return EGL_FALSE;
|
||||||
|
}
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
if (contextToken != 0 && MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||||
|
!MG_Remote::Client::RunsAsTheServerRole()) {
|
||||||
|
const auto emitted = MG_Remote::Client::SendDestroyContextFrame(contextToken);
|
||||||
|
if (emitted == MG_Remote::Client::ContextFrameEmit::Refused) {
|
||||||
|
MGLOG_E_ONCE("eglDestroyContext: the server holds no context token %llu; the "
|
||||||
|
"session's table and this client's EGL book have drifted apart",
|
||||||
|
static_cast<unsigned long long>(contextToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return EGL_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLBoolean DestroySurface(EGLDisplay dpy, EGLSurface surface) {
|
EGLBoolean DestroySurface(EGLDisplay dpy, EGLSurface surface) {
|
||||||
@@ -468,6 +566,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
// re-initializes lazily via GetStateEnsureInitialized(); process exit
|
// re-initializes lazily via GetStateEnsureInitialized(); process exit
|
||||||
// then has nothing left to destroy.
|
// then has nothing left to destroy.
|
||||||
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
|
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
|
||||||
|
// P14 S3: the process default context (and with it the GL objects the frontend
|
||||||
|
// built while no EGL context was current) goes before Destroy() does, so nothing
|
||||||
|
// it owns outlives glslang::FinalizeProcess() through a thread's fallback binding.
|
||||||
|
MG_State::ReleaseProcessDefaultGLContext();
|
||||||
MobileGL::Destroy();
|
MobileGL::Destroy();
|
||||||
}
|
}
|
||||||
return EGL_TRUE;
|
return EGL_TRUE;
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ namespace MobileGL::MG_Impl {
|
|||||||
MakeUnique<GLImpl::FramebufferImpl::DefaultFramebufferInfo>(fbo0, colorTex, depthTex, stencilTex);
|
MakeUnique<GLImpl::FramebufferImpl::DefaultFramebufferInfo>(fbo0, colorTex, depthTex, stencilTex);
|
||||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(fbo0);
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(fbo0);
|
||||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(fbo0);
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(fbo0);
|
||||||
|
// P14 S3: every GLContext a later eglCreateContext builds starts with this same default
|
||||||
|
// framebuffer, so a context the application creates after this one is not left with an
|
||||||
|
// unbound framebuffer 0.
|
||||||
|
MG_State::SetDefaultFramebuffer(fbo0);
|
||||||
#if defined(__APPLE__) && !defined(MOBILEGL_IOS)
|
#if defined(__APPLE__) && !defined(MOBILEGL_IOS)
|
||||||
NSOpenGLImpl::InstallHooks();
|
NSOpenGLImpl::InstallHooks();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1966,6 +1966,19 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption,
|
// object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption,
|
||||||
// "ObjectDeath.Handle"} at the sink.
|
// "ObjectDeath.Handle"} at the sink.
|
||||||
|
|
||||||
|
// bind_context = opcode 84, P14 S1 (docs/Disaggregated/design/11-state-ownership.md).
|
||||||
|
// The client's CURRENT context token, minted by EGLState::CreateContext and carried to the
|
||||||
|
// server by the matching CreateContext control frame. 0 means "no context": a release, and
|
||||||
|
// it is the value a session that never bound one already has - so the single-context shape
|
||||||
|
// P5c shipped (one context per session, every record implicitly its own) needs no record at
|
||||||
|
// all and no server change. The token is Uint64 because the control frame's is: one width
|
||||||
|
// on both planes, or the two would have to be narrowed at one end.
|
||||||
|
struct MGPBindContext {
|
||||||
|
Uint64 ClientContextToken;
|
||||||
|
};
|
||||||
|
MGP_ASSERT_POD(MGPBindContext, 8);
|
||||||
|
|
||||||
|
|
||||||
// set_context_values = opcode 79, P5c rv (CONTRACT-P5C.md §5.3): the rv field table as
|
// set_context_values = opcode 79, P5c rv (CONTRACT-P5C.md §5.3): the rv field table as
|
||||||
// ONE fixed-width POD. It carries every value-class field that no set_* call supplies -
|
// ONE fixed-width POD. It carries every value-class field that no set_* call supplies -
|
||||||
// the two texture-unit counters, the per-target touched-buffer-binding high-water marks
|
// the two texture-unit counters, the per-target touched-buffer-binding high-water marks
|
||||||
|
|||||||
+349
-186
@@ -477,11 +477,173 @@ namespace MobileGL::MG_Pipe {
|
|||||||
static_assert(kCapabilityCount <= 64,
|
static_assert(kCapabilityCount <= 64,
|
||||||
"ResidualValueBlock::CapabilityBits is a Uint64; 35 bits fit, 65 would not");
|
"ResidualValueBlock::CapabilityBits is a Uint64; 35 bits fit, 65 would not");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// P14 S5: THE APPLIER REGISTRY.
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
// A REFERENCE TO A NEVER-DESTROYED BLOCK, for MGPipeSlots()' reason
|
// A REFERENCE TO A NEVER-DESTROYED BLOCK, for MGPipeSlots()' reason
|
||||||
// (MG_Impl/Pipe/SlotAllocator.cpp): resource_destroy and delete_vertex_elements are
|
// (MG_Impl/Pipe/SlotAllocator.cpp): resource_destroy and delete_vertex_elements are
|
||||||
// raised from ~BufferObject / ~VertexArrayObject, and those objects are released by
|
// raised from ~BufferObject / ~VertexArrayObject, and those objects are released by
|
||||||
// exit handlers that run after this translation unit's own globals are gone.
|
// exit handlers that run after this translation unit's own globals are gone.
|
||||||
MGPipeApplierState& g_applier = *new MGPipeApplierState{};
|
//
|
||||||
|
// THE DEFAULT IS THE OLD SINGLETON, unrenamed in what it means: the applier of a thread
|
||||||
|
// that belongs to nobody - the client process, the monolith, the inproc one-process
|
||||||
|
// shape, every unit case. Its object records are the group it owns alone.
|
||||||
|
MGPipeObjectRecords& g_defaultObjects = *new MGPipeObjectRecords{};
|
||||||
|
MGPipeApplierState& g_defaultApplier = *new MGPipeApplierState(g_defaultObjects);
|
||||||
|
|
||||||
|
// Installed once by the session registry (MG_Remote/Server/SessionRuntime.cpp), never
|
||||||
|
// replaced. Null in every process that serves no session, which is what keeps the
|
||||||
|
// monolith's `MGPipeApplier()` a load and a branch.
|
||||||
|
MGPipeApplierKeyResolver g_applierKeyResolver = nullptr;
|
||||||
|
|
||||||
|
// One served session's appliers. Groups own object records; Contexts own working state
|
||||||
|
// and point at their group. Both are held through UniquePtr so a table's ADDRESS is
|
||||||
|
// stable across a rehash - MGPipeApplierState binds its group by reference - and the
|
||||||
|
// maps are std::unordered_map rather than the house UnorderedMap (a flat hash map) for
|
||||||
|
// that same reason at the map level: a reference into it may not be invalidated by an
|
||||||
|
// insert somewhere else in the same call.
|
||||||
|
struct ApplierSession {
|
||||||
|
std::unordered_map<Uint64, UniquePtr<MGPipeObjectRecords>> Groups;
|
||||||
|
std::unordered_map<Uint64, UniquePtr<MGPipeApplierState>> Contexts;
|
||||||
|
std::unordered_map<Uint64, Uint64> ContextGroup;
|
||||||
|
std::unordered_map<Uint64, Uint32> GroupContexts;
|
||||||
|
};
|
||||||
|
std::unordered_map<Uint64, ApplierSession> g_applierSessions;
|
||||||
|
std::mutex g_applierSessionsMutex;
|
||||||
|
// Bumped by EVERY registry mutation. A thread's cached applier pointer is only trusted
|
||||||
|
// while it was bound at this generation, which is what makes "a context was destroyed on
|
||||||
|
// another thread" unable to leave a dangling cache behind.
|
||||||
|
std::atomic<Uint64> g_applierRegistryGeneration{1};
|
||||||
|
|
||||||
|
// A context that started its own share group is keyed by its own token, in a namespace
|
||||||
|
// of its own: the frontend mints share-group tokens and context tokens from separate
|
||||||
|
// counters, so without this a group 7 and a context 7 would be one group.
|
||||||
|
constexpr Uint64 kMGPipeOwnShareGroupBit = Uint64{1} << 63;
|
||||||
|
|
||||||
|
// `shareGroupToken` 0 means "this context started its own group" - the sentinel S1's
|
||||||
|
// ContextRuntime documents and the one a caller that frames a context by hand (a test,
|
||||||
|
// the in-process server helpers) leaves behind.
|
||||||
|
Uint64 ApplierShareGroupKey(Uint64 contextToken, Uint64 shareGroupToken) {
|
||||||
|
return shareGroupToken != 0 ? shareGroupToken : (contextToken | kMGPipeOwnShareGroupBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The three tables of the applier this thread's records are being applied through.
|
||||||
|
// `generation` is the registry's, read here so a caller can bind a pointer under one
|
||||||
|
// generation and publish it under the same one.
|
||||||
|
MGPipeApplierState* FindApplierForKey(const MGPipeApplierKey& key) {
|
||||||
|
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
|
||||||
|
ApplierSession& session = g_applierSessions[key.SessionKey];
|
||||||
|
const auto found = session.Contexts.find(key.ContextToken);
|
||||||
|
if (found != session.Contexts.end() && found->second) return found->second.get();
|
||||||
|
// A context the session never registered - a token that predates the first
|
||||||
|
// CreateContext, or the 0 of a session that has not bound one yet. It gets the same
|
||||||
|
// shape as any other: a working state in a group of its own.
|
||||||
|
const Uint64 groupKey = ApplierShareGroupKey(key.ContextToken, 0);
|
||||||
|
auto group = session.Groups.find(groupKey);
|
||||||
|
if (group == session.Groups.end()) {
|
||||||
|
group = session.Groups.emplace(groupKey, MakeUnique<MGPipeObjectRecords>()).first;
|
||||||
|
}
|
||||||
|
auto applier = MakeUnique<MGPipeApplierState>(*group->second);
|
||||||
|
MGPipeApplierState* const raw = applier.get();
|
||||||
|
session.Contexts.emplace(key.ContextToken, std::move(applier));
|
||||||
|
session.ContextGroup.emplace(key.ContextToken, groupKey);
|
||||||
|
++session.GroupContexts[groupKey];
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local MGPipeApplierState* t_applierCached = nullptr;
|
||||||
|
thread_local Uint64 t_applierCachedSession = 0;
|
||||||
|
thread_local Uint64 t_applierCachedContext = 0;
|
||||||
|
thread_local Uint64 t_applierCachedGeneration = 0;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
MGPipeApplierState& MGPipeApplier() {
|
||||||
|
const MGPipeApplierKeyResolver resolve = g_applierKeyResolver;
|
||||||
|
if (resolve == nullptr) return g_defaultApplier;
|
||||||
|
MGPipeApplierKey key;
|
||||||
|
if (!resolve(&key)) return g_defaultApplier;
|
||||||
|
// THE FAST PATH, and the reason this is a cache rather than a lookup: this function is
|
||||||
|
// called from the backend per state read. A hit is three TLS loads, one atomic load and
|
||||||
|
// two compares; a miss is the registry, which only moves when a context is created,
|
||||||
|
// destroyed or made current.
|
||||||
|
const Uint64 generation = g_applierRegistryGeneration.load(std::memory_order_acquire);
|
||||||
|
if (t_applierCached != nullptr && t_applierCachedGeneration == generation &&
|
||||||
|
t_applierCachedSession == key.SessionKey && t_applierCachedContext == key.ContextToken) {
|
||||||
|
return *t_applierCached;
|
||||||
|
}
|
||||||
|
MGPipeApplierState* const applier = FindApplierForKey(key);
|
||||||
|
t_applierCached = applier;
|
||||||
|
t_applierCachedSession = key.SessionKey;
|
||||||
|
t_applierCachedContext = key.ContextToken;
|
||||||
|
t_applierCachedGeneration = generation;
|
||||||
|
return *applier;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeSetApplierKeyResolver(MGPipeApplierKeyResolver resolver) { g_applierKeyResolver = resolver; }
|
||||||
|
|
||||||
|
void MGPipeApplierRegisterContext(Uint64 sessionKey, Uint64 contextToken, Uint64 shareGroupToken) {
|
||||||
|
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
|
||||||
|
ApplierSession& session = g_applierSessions[sessionKey];
|
||||||
|
const Uint64 groupKey = ApplierShareGroupKey(contextToken, shareGroupToken);
|
||||||
|
auto group = session.Groups.find(groupKey);
|
||||||
|
if (group == session.Groups.end()) {
|
||||||
|
group = session.Groups.emplace(groupKey, MakeUnique<MGPipeObjectRecords>()).first;
|
||||||
|
}
|
||||||
|
const auto existing = session.Contexts.find(contextToken);
|
||||||
|
if (existing != session.Contexts.end() && existing->second) {
|
||||||
|
// Idempotent: the same token registered twice keeps the state it already has. The
|
||||||
|
// session refuses a duplicate CreateContext before reaching here, so this arm is a
|
||||||
|
// caller that re-registers deliberately.
|
||||||
|
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.Contexts.emplace(contextToken, MakeUnique<MGPipeApplierState>(*group->second));
|
||||||
|
session.ContextGroup[contextToken] = groupKey;
|
||||||
|
++session.GroupContexts[groupKey];
|
||||||
|
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplierUnregisterContext(Uint64 sessionKey, Uint64 contextToken) {
|
||||||
|
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
|
||||||
|
const auto sessionIt = g_applierSessions.find(sessionKey);
|
||||||
|
if (sessionIt == g_applierSessions.end()) return;
|
||||||
|
ApplierSession& session = sessionIt->second;
|
||||||
|
const auto context = session.Contexts.find(contextToken);
|
||||||
|
if (context == session.Contexts.end()) return;
|
||||||
|
const auto groupIt = session.ContextGroup.find(contextToken);
|
||||||
|
const Uint64 groupKey = groupIt != session.ContextGroup.end() ? groupIt->second : 0;
|
||||||
|
// The OBJECT RECORDS GO WITH THE LAST CONTEXT OF THE GROUP, which is the per-group
|
||||||
|
// spelling of MGPipeApplierReleaseObjectRecords' scope. A surviving context of the same
|
||||||
|
// group keeps every record - that is what sharing means.
|
||||||
|
const auto refs = session.GroupContexts.find(groupKey);
|
||||||
|
const Bool lastOfGroup = refs != session.GroupContexts.end() && refs->second <= 1;
|
||||||
|
session.Contexts.erase(context);
|
||||||
|
if (groupIt != session.ContextGroup.end()) session.ContextGroup.erase(groupIt);
|
||||||
|
if (lastOfGroup) {
|
||||||
|
if (refs != session.GroupContexts.end()) session.GroupContexts.erase(refs);
|
||||||
|
const auto group = session.Groups.find(groupKey);
|
||||||
|
if (group != session.Groups.end()) session.Groups.erase(group);
|
||||||
|
} else if (refs != session.GroupContexts.end()) {
|
||||||
|
--refs->second;
|
||||||
|
}
|
||||||
|
if (session.Contexts.empty()) g_applierSessions.erase(sessionIt);
|
||||||
|
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplierReleaseSession(Uint64 sessionKey) {
|
||||||
|
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
|
||||||
|
if (g_applierSessions.erase(sessionKey) == 0) return;
|
||||||
|
g_applierRegistryGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 MGPipeApplierShareGroupCountForTesting(Uint64 sessionKey) {
|
||||||
|
std::lock_guard<std::mutex> guard(g_applierSessionsMutex);
|
||||||
|
const auto sessionIt = g_applierSessions.find(sessionKey);
|
||||||
|
return sessionIt == g_applierSessions.end() ? 0 : static_cast<Uint64>(sessionIt->second.Groups.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
// The installed handle-shaped resource table. Null until a backend registers one,
|
// The installed handle-shaped resource table. Null until a backend registers one,
|
||||||
// which is what makes the client half landable on its own: with nothing here every
|
// which is what makes the client half landable on its own: with nothing here every
|
||||||
@@ -490,8 +652,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
const MGPipeResourceOps* g_resourceOps = nullptr;
|
const MGPipeResourceOps* g_resourceOps = nullptr;
|
||||||
|
|
||||||
MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) {
|
MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) {
|
||||||
if (handle.Slot >= g_applier.RenderStateCsos.size()) return nullptr;
|
if (handle.Slot >= MGPipeApplier().RenderStateCsos.size()) return nullptr;
|
||||||
MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[handle.Slot];
|
MGPipeRenderStateCsoRecord& record = MGPipeApplier().RenderStateCsos[handle.Slot];
|
||||||
if (!record.Live || record.Gen != handle.Gen) return nullptr;
|
if (!record.Live || record.Gen != handle.Gen) return nullptr;
|
||||||
return &record;
|
return &record;
|
||||||
}
|
}
|
||||||
@@ -527,15 +689,15 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// that is not live, or a handle whose generation has moved on because the slot was
|
// that is not live, or a handle whose generation has moved on because the slot was
|
||||||
// recycled under it. FindCso above is the same three questions for the CSO store.
|
// recycled under it. FindCso above is the same three questions for the CSO store.
|
||||||
MGPipeResourceRecord* FindResource(MGPipeHandle res) {
|
MGPipeResourceRecord* FindResource(MGPipeHandle res) {
|
||||||
if (res.Slot >= g_applier.Resources.size()) return nullptr;
|
if (res.Slot >= MGPipeApplier().Resources.size()) return nullptr;
|
||||||
MGPipeResourceRecord& record = g_applier.Resources[res.Slot];
|
MGPipeResourceRecord& record = MGPipeApplier().Resources[res.Slot];
|
||||||
if (!record.Live || record.Gen != res.Gen) return nullptr;
|
if (!record.Live || record.Gen != res.Gen) return nullptr;
|
||||||
return &record;
|
return &record;
|
||||||
}
|
}
|
||||||
|
|
||||||
MGPipeVertexElementsRecord* FindVertexElements(MGPipeHandle cso) {
|
MGPipeVertexElementsRecord* FindVertexElements(MGPipeHandle cso) {
|
||||||
if (cso.Slot >= g_applier.VertexElementsCsos.size()) return nullptr;
|
if (cso.Slot >= MGPipeApplier().VertexElementsCsos.size()) return nullptr;
|
||||||
MGPipeVertexElementsRecord& record = g_applier.VertexElementsCsos[cso.Slot];
|
MGPipeVertexElementsRecord& record = MGPipeApplier().VertexElementsCsos[cso.Slot];
|
||||||
if (!record.Live || record.Gen != cso.Gen) return nullptr;
|
if (!record.Live || record.Gen != cso.Gen) return nullptr;
|
||||||
return &record;
|
return &record;
|
||||||
}
|
}
|
||||||
@@ -562,15 +724,15 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// respecify or destroy an unrelated LIVE object that happens to hold the same slot in
|
// respecify or destroy an unrelated LIVE object that happens to hold the same slot in
|
||||||
// another kind's space, and that is the "act outside its own storage" class.
|
// another kind's space, and that is the "act outside its own storage" class.
|
||||||
Vector<MGPipeResourceRecord>* ResourceTableForTarget(Uint8 target) {
|
Vector<MGPipeResourceRecord>* ResourceTableForTarget(Uint8 target) {
|
||||||
if (target == kMGPipeResourceTargetBuffer) return &g_applier.Resources;
|
if (target == kMGPipeResourceTargetBuffer) return &MGPipeApplier().Resources;
|
||||||
if (target == static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer)) {
|
if (target == static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer)) {
|
||||||
return &g_applier.RenderbufferResources;
|
return &MGPipeApplier().RenderbufferResources;
|
||||||
}
|
}
|
||||||
if (target < static_cast<Uint8>(MGPipeResourceTarget::Count)) {
|
if (target < static_cast<Uint8>(MGPipeResourceTarget::Count)) {
|
||||||
// Every remaining enumerator is a texture target, and they share one table
|
// Every remaining enumerator is a texture target, and they share one table
|
||||||
// because they share one kind: MGPipeKind::Texture. Tex1D..TexCubeArray,
|
// because they share one kind: MGPipeKind::Texture. Tex1D..TexCubeArray,
|
||||||
// Tex2DMS/MSArray, TexBuffer and TexRect are all one slot space.
|
// Tex2DMS/MSArray, TexBuffer and TexRect are all one slot space.
|
||||||
return &g_applier.TextureResources;
|
return &MGPipeApplier().TextureResources;
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -579,11 +741,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Vector<MGPipeResourceRecord>* ResourceTableForKind(Uint32 kind) {
|
Vector<MGPipeResourceRecord>* ResourceTableForKind(Uint32 kind) {
|
||||||
switch (static_cast<MGPipeKind>(kind)) {
|
switch (static_cast<MGPipeKind>(kind)) {
|
||||||
case MGPipeKind::Buffer:
|
case MGPipeKind::Buffer:
|
||||||
return &g_applier.Resources;
|
return &MGPipeApplier().Resources;
|
||||||
case MGPipeKind::Texture:
|
case MGPipeKind::Texture:
|
||||||
return &g_applier.TextureResources;
|
return &MGPipeApplier().TextureResources;
|
||||||
case MGPipeKind::Renderbuffer:
|
case MGPipeKind::Renderbuffer:
|
||||||
return &g_applier.RenderbufferResources;
|
return &MGPipeApplier().RenderbufferResources;
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -710,7 +872,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeResourceRecord* record = FindResource(res);
|
MGPipeResourceRecord* record = FindResource(res);
|
||||||
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen,
|
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen,
|
||||||
kResourceRefusalNote);
|
kResourceRefusalNote);
|
||||||
if (record == nullptr) ++g_applier.RefusedResourceCalls;
|
if (record == nullptr) ++MGPipeApplier().RefusedResourceCalls;
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +885,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeResourceRecord* record = FindIn(table, res);
|
MGPipeResourceRecord* record = FindIn(table, res);
|
||||||
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen,
|
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, res.Slot, res.Gen,
|
||||||
kResourceRefusalNote);
|
kResourceRefusalNote);
|
||||||
if (record == nullptr) ++g_applier.RefusedResourceCalls;
|
if (record == nullptr) ++MGPipeApplier().RefusedResourceCalls;
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,7 +898,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Record* record = FindIn(table, handle);
|
Record* record = FindIn(table, handle);
|
||||||
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, handle.Slot,
|
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, handle.Slot,
|
||||||
handle.Gen, kResourceRefusalNote);
|
handle.Gen, kResourceRefusalNote);
|
||||||
if (record == nullptr) ++g_applier.RefusedObjectCalls;
|
if (record == nullptr) ++MGPipeApplier().RefusedObjectCalls;
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,7 +906,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeVertexElementsRecord* record = FindVertexElements(cso);
|
MGPipeVertexElementsRecord* record = FindVertexElements(cso);
|
||||||
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
|
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
|
||||||
kResourceRefusalNote);
|
kResourceRefusalNote);
|
||||||
if (record == nullptr) ++g_applier.RefusedVertexInputCalls;
|
if (record == nullptr) ++MGPipeApplier().RefusedVertexInputCalls;
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,7 +1360,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// hook; the monolith shape of everything above them is unchanged.
|
// hook; the monolith shape of everything above them is unchanged.
|
||||||
Bool ApplyTextureUpload(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions) {
|
Bool ApplyTextureUpload(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions) {
|
||||||
MGPipeResourceRecord* stored =
|
MGPipeResourceRecord* stored =
|
||||||
ResolveResourceIn(g_applier.TextureResources, "resource_subdata", record.Res);
|
ResolveResourceIn(MGPipeApplier().TextureResources, "resource_subdata", record.Res);
|
||||||
if (stored == nullptr) return false;
|
if (stored == nullptr) return false;
|
||||||
|
|
||||||
const char* fault = SubDataTextureFault(record, regions);
|
const char* fault = SubDataTextureFault(record, regions);
|
||||||
@@ -1319,28 +1481,28 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
MGPipeShaderCsoRecord* ShaderCsoRecordAt(Uint32 slot) {
|
MGPipeShaderCsoRecord* ShaderCsoRecordAt(Uint32 slot) {
|
||||||
if (MGPipeIsCompositeShaderSlot(slot)) {
|
if (MGPipeIsCompositeShaderSlot(slot)) {
|
||||||
return RecordAt(g_applier.CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase,
|
return RecordAt(MGPipeApplier().CompositeShaderCsos, slot - kMGPipeShaderCsoCompositeSlotBase,
|
||||||
kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase);
|
kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase);
|
||||||
}
|
}
|
||||||
// The ordinary table is bounded by the BAND'S BASE and not by the slot limit: an
|
// The ordinary table is bounded by the BAND'S BASE and not by the slot limit: an
|
||||||
// ordinary program can never be handed a band slot (the allocator refuses it), so a
|
// ordinary program can never be handed a band slot (the allocator refuses it), so a
|
||||||
// slot at or above the base that is not a composite is out of range by definition.
|
// slot at or above the base that is not a composite is out of range by definition.
|
||||||
return RecordAt(g_applier.ShaderCsos, slot, kMGPipeShaderCsoCompositeSlotBase);
|
return RecordAt(MGPipeApplier().ShaderCsos, slot, kMGPipeShaderCsoCompositeSlotBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
MGPipeShaderCsoRecord* FindShaderCso(MGPipeHandle cso) {
|
MGPipeShaderCsoRecord* FindShaderCso(MGPipeHandle cso) {
|
||||||
if (MGPipeIsCompositeShaderSlot(cso.Slot)) {
|
if (MGPipeIsCompositeShaderSlot(cso.Slot)) {
|
||||||
return FindIn(g_applier.CompositeShaderCsos,
|
return FindIn(MGPipeApplier().CompositeShaderCsos,
|
||||||
MGPipeHandle{cso.Slot - kMGPipeShaderCsoCompositeSlotBase, cso.Gen});
|
MGPipeHandle{cso.Slot - kMGPipeShaderCsoCompositeSlotBase, cso.Gen});
|
||||||
}
|
}
|
||||||
return FindIn(g_applier.ShaderCsos, cso);
|
return FindIn(MGPipeApplier().ShaderCsos, cso);
|
||||||
}
|
}
|
||||||
|
|
||||||
MGPipeShaderCsoRecord* ResolveShaderCso(const char* call, MGPipeHandle cso) {
|
MGPipeShaderCsoRecord* ResolveShaderCso(const char* call, MGPipeHandle cso) {
|
||||||
MGPipeShaderCsoRecord* record = FindShaderCso(cso);
|
MGPipeShaderCsoRecord* record = FindShaderCso(cso);
|
||||||
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
|
MOBILEGL_ASSERT(record != nullptr, "%s named {slot=%u, gen=%u}: %s", call, cso.Slot, cso.Gen,
|
||||||
kResourceRefusalNote);
|
kResourceRefusalNote);
|
||||||
if (record == nullptr) ++g_applier.RefusedObjectCalls;
|
if (record == nullptr) ++MGPipeApplier().RefusedObjectCalls;
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1388,13 +1550,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
#endif
|
#endif
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
MGPipeApplierState& MGPipeApplier() { return g_applier; }
|
// MGPipeApplier() is defined with the registry at the top of this file: it is the one
|
||||||
|
// function that has to know whose applier the calling thread is in.
|
||||||
|
|
||||||
// P5c (rv, CONTRACT-P5C.md §5.3): the three texture shutters' server-side answers. See
|
// P5c (rv, CONTRACT-P5C.md §5.3): the three texture shutters' server-side answers. See
|
||||||
// MGPipeApplierState::TextureShutterSerial / ContextSerial for the bump rule.
|
// MGPipeApplierState::TextureShutterSerial / ContextSerial for the bump rule.
|
||||||
Uint64 MGPipeApplierTextureShutterSerial() { return g_applier.TextureShutterSerial; }
|
Uint64 MGPipeApplierTextureShutterSerial() { return MGPipeApplier().TextureShutterSerial; }
|
||||||
Uint64 MGPipeApplierContextSerial() { return g_applier.ContextSerial; }
|
Uint64 MGPipeApplierContextSerial() { return MGPipeApplier().ContextSerial; }
|
||||||
void MGPipeApplierNoteTextureStateMoved() { ++g_applier.TextureShutterSerial; }
|
void MGPipeApplierNoteTextureStateMoved() { ++MGPipeApplier().TextureShutterSerial; }
|
||||||
|
|
||||||
void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; }
|
void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; }
|
||||||
const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; }
|
const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; }
|
||||||
@@ -1427,7 +1590,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (g_resourceOps != nullptr) return false;
|
if (g_resourceOps != nullptr) return false;
|
||||||
++g_applier.RefusedNoConsumer;
|
++MGPipeApplier().RefusedNoConsumer;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1438,11 +1601,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// zero" rule is written out.
|
// zero" rule is written out.
|
||||||
void ClearShaderBufferWindows() {
|
void ClearShaderBufferWindows() {
|
||||||
for (Uint32 cls = 0; cls < kMGPipeShaderBufferClassCount; ++cls) {
|
for (Uint32 cls = 0; cls < kMGPipeShaderBufferClassCount; ++cls) {
|
||||||
g_applier.BoundShaderBuffers[cls] = {};
|
MGPipeApplier().BoundShaderBuffers[cls] = {};
|
||||||
g_applier.ShaderBufferStart[cls] = 0;
|
MGPipeApplier().ShaderBufferStart[cls] = 0;
|
||||||
g_applier.ShaderBufferCount[cls] = 0;
|
MGPipeApplier().ShaderBufferCount[cls] = 0;
|
||||||
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
|
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
|
||||||
g_applier.ShaderBufferWritableMask[cls][w] = 0;
|
MGPipeApplier().ShaderBufferWritableMask[cls][w] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1480,15 +1643,15 @@ namespace MobileGL::MG_Pipe {
|
|||||||
std::abort();
|
std::abort();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
g_applier.RenderStateCsos.clear();
|
MGPipeApplier().RenderStateCsos.clear();
|
||||||
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
|
MGPipeApplier().BoundRenderStateCso = kMGPipeNullHandle;
|
||||||
g_applier.Residual = ResidualValueBlock{};
|
MGPipeApplier().Residual = ResidualValueBlock{};
|
||||||
g_applier.HasResidual = false;
|
MGPipeApplier().HasResidual = false;
|
||||||
g_applier.ScatteredChunkBits = 0;
|
MGPipeApplier().ScatteredChunkBits = 0;
|
||||||
g_applier.ResidualCapabilitiesCompared = 0;
|
MGPipeApplier().ResidualCapabilitiesCompared = 0;
|
||||||
g_applier.ResidualDivergences = 0;
|
MGPipeApplier().ResidualDivergences = 0;
|
||||||
g_applier.PatchCarrierComparisons = 0;
|
MGPipeApplier().PatchCarrierComparisons = 0;
|
||||||
g_applier.PatchCarrierDivergences = 0;
|
MGPipeApplier().PatchCarrierDivergences = 0;
|
||||||
// P3a. THIS RUNS AT EVERY CHANGE OF THE CURRENT CONTEXT, not once per fresh one:
|
// P3a. THIS RUNS AT EVERY CHANGE OF THE CURRENT CONTEXT, not once per fresh one:
|
||||||
// MGPipeTracker::Update resets itself whenever the context pointer moves and the
|
// MGPipeTracker::Update resets itself whenever the context pointer moves and the
|
||||||
// emitter calls this from the walk that follows, so a make-current BACK to a context
|
// emitter calls this from the walk that follows, so a make-current BACK to a context
|
||||||
@@ -1505,16 +1668,16 @@ namespace MobileGL::MG_Pipe {
|
|||||||
//
|
//
|
||||||
// The OP TABLE is deliberately not cleared either - it is installed and uninstalled by
|
// The OP TABLE is deliberately not cleared either - it is installed and uninstalled by
|
||||||
// the backend's own bring-up and teardown, not by a state reset.
|
// the backend's own bring-up and teardown, not by a state reset.
|
||||||
g_applier.RefusedResourceCalls = 0;
|
MGPipeApplier().RefusedResourceCalls = 0;
|
||||||
g_applier.RefusedVertexInputCalls = 0;
|
MGPipeApplier().RefusedVertexInputCalls = 0;
|
||||||
g_applier.RefusedObjectCalls = 0;
|
MGPipeApplier().RefusedObjectCalls = 0;
|
||||||
g_applier.RefusedNoConsumer = 0;
|
MGPipeApplier().RefusedNoConsumer = 0;
|
||||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
|
||||||
g_applier.VertexBuffers = {};
|
MGPipeApplier().VertexBuffers = {};
|
||||||
g_applier.VertexBufferStart = 0;
|
MGPipeApplier().VertexBufferStart = 0;
|
||||||
g_applier.VertexBufferCount = 0;
|
MGPipeApplier().VertexBufferCount = 0;
|
||||||
g_applier.VertexFetchBaseInstance = 0;
|
MGPipeApplier().VertexFetchBaseInstance = 0;
|
||||||
g_applier.IndexBuffer = MGPIndexBuffer{};
|
MGPipeApplier().IndexBuffer = MGPIndexBuffer{};
|
||||||
// m6 / wire n6, written down rather than left to be rediscovered: THIS counter is
|
// m6 / wire n6, written down rather than left to be rediscovered: THIS counter is
|
||||||
// per-applier and is zeroed at every make-current, while MG_Util::PipeStats' `mpr` -
|
// per-applier and is zeroed at every make-current, while MG_Util::PipeStats' `mpr` -
|
||||||
// emitted from the CLIENT at MG_Impl/Pipe/PipeFill.cpp's MGPipeEmitMapPersistent - is
|
// emitted from the CLIENT at MG_Impl/Pipe/PipeFill.cpp's MGPipeEmitMapPersistent - is
|
||||||
@@ -1529,7 +1692,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// MG_IntegrationTest/Harness/PipeStatsWindow.h - they cannot link this symbol at all, on
|
// MG_IntegrationTest/Harness/PipeStatsWindow.h - they cannot link this symbol at all, on
|
||||||
// Android or anywhere else - so a make-current inside a scenario cannot silently reset
|
// Android or anywhere else - so a make-current inside a scenario cannot silently reset
|
||||||
// what they measure. Nothing outside MG_Test reads the member below.
|
// what they measure. Nothing outside MG_Test reads the member below.
|
||||||
g_applier.MapPersistentRoundtrips = 0;
|
MGPipeApplier().MapPersistentRoundtrips = 0;
|
||||||
// THE TWO SERIALS ADVANCE; THEY ARE NOT ZEROED. They are MGGens, and an MGGen that
|
// THE TWO SERIALS ADVANCE; THEY ARE NOT ZEROED. They are MGGens, and an MGGen that
|
||||||
// walks backwards is not one. There are exactly three things a reset can do to a
|
// walks backwards is not one. There are exactly three things a reset can do to a
|
||||||
// version whose data it has just cleared:
|
// version whose data it has just cleared:
|
||||||
@@ -1543,15 +1706,15 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// count is stable lands on a stamped value every time;
|
// count is stable lands on a stamped value every time;
|
||||||
// - advance: the clearing is itself announced, no stamped value can ever recur, and
|
// - advance: the clearing is itself announced, no stamped value can ever recur, and
|
||||||
// the first compare after the switch is a mismatch, which is the safe direction.
|
// the first compare after the switch is a mismatch, which is the safe direction.
|
||||||
++g_applier.VertexBuffersSerial;
|
++MGPipeApplier().VertexBuffersSerial;
|
||||||
++g_applier.IndexBufferSerial;
|
++MGPipeApplier().IndexBufferSerial;
|
||||||
// P5c (rv): a make-current is a fresh server, and the two shutter serials are what the
|
// P5c (rv): a make-current is a fresh server, and the two shutter serials are what the
|
||||||
// PipeInputs texture-shutter accessors answer with - so both ADVANCE here, for the
|
// PipeInputs texture-shutter accessors answer with - so both ADVANCE here, for the
|
||||||
// serials' own reason: a counter that restarts walks back through values already
|
// serials' own reason: a counter that restarts walks back through values already
|
||||||
// stamped into a memo that outlived the switch. ContextSerial's move is also
|
// stamped into a memo that outlived the switch. ContextSerial's move is also
|
||||||
// GetTextureContextId's whole job (the backends key their per-context memos on it).
|
// GetTextureContextId's whole job (the backends key their per-context memos on it).
|
||||||
++g_applier.TextureShutterSerial;
|
++MGPipeApplier().TextureShutterSerial;
|
||||||
++g_applier.ContextSerial;
|
++MGPipeApplier().ContextSerial;
|
||||||
|
|
||||||
// ---- P4a's working state, cleared for the same reason and with the same serial rule
|
// ---- P4a's working state, cleared for the same reason and with the same serial rule
|
||||||
// (D-J4). The OBJECT records - texture and renderbuffer resources, sampler CSOs,
|
// (D-J4). The OBJECT records - texture and renderbuffer resources, sampler CSOs,
|
||||||
@@ -1566,21 +1729,21 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// a null Fbo. FramebufferRecords stays because a framebuffer that is only ever addressed
|
// a null Fbo. FramebufferRecords stays because a framebuffer that is only ever addressed
|
||||||
// BY NAME - the DSA blit and clear entry points - has no re-emission trigger at all, so
|
// BY NAME - the DSA blit and clear entry points - has no re-emission trigger at all, so
|
||||||
// dropping its record here would lose it for the life of the process.
|
// dropping its record here would lose it for the life of the process.
|
||||||
g_applier.BoundFramebuffer = {};
|
MGPipeApplier().BoundFramebuffer = {};
|
||||||
g_applier.BoundSamplerViews = {};
|
MGPipeApplier().BoundSamplerViews = {};
|
||||||
g_applier.SamplerViewStart = 0;
|
MGPipeApplier().SamplerViewStart = 0;
|
||||||
g_applier.SamplerViewCount = 0;
|
MGPipeApplier().SamplerViewCount = 0;
|
||||||
g_applier.BoundSamplerStates = {};
|
MGPipeApplier().BoundSamplerStates = {};
|
||||||
g_applier.SamplerStateStart = 0;
|
MGPipeApplier().SamplerStateStart = 0;
|
||||||
g_applier.SamplerStateCount = 0;
|
MGPipeApplier().SamplerStateCount = 0;
|
||||||
g_applier.BoundShaderImages = {};
|
MGPipeApplier().BoundShaderImages = {};
|
||||||
g_applier.ShaderImageStart = 0;
|
MGPipeApplier().ShaderImageStart = 0;
|
||||||
g_applier.ShaderImageCount = 0;
|
MGPipeApplier().ShaderImageCount = 0;
|
||||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
MGPipeApplier().DrawProgram = kMGPipeNullHandle;
|
||||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
|
||||||
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
g_applier.BoundStreamOutputLifetimeId = 0;
|
MGPipeApplier().BoundStreamOutputLifetimeId = 0;
|
||||||
// Begin is emitted once per span, not on make-current. Its immutable object
|
// Begin is emitted once per span, not on make-current. Its immutable object
|
||||||
// snapshot survives with the resource/program records until End or release;
|
// snapshot survives with the resource/program records until End or release;
|
||||||
// set_context_values restores the returning context's bound lifetime id.
|
// set_context_values restores the returning context's bound lifetime id.
|
||||||
@@ -1595,12 +1758,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// invalidates the three suppressor slots), or the first emission after a make-current
|
// invalidates the three suppressor slots), or the first emission after a make-current
|
||||||
// would be suppressed as unchanged and the server would draw with a cleared window.
|
// would be suppressed as unchanged and the server would draw with a cleared window.
|
||||||
ClearShaderBufferWindows();
|
ClearShaderBufferWindows();
|
||||||
++g_applier.FramebufferSerial;
|
++MGPipeApplier().FramebufferSerial;
|
||||||
++g_applier.SamplerViewsSerial;
|
++MGPipeApplier().SamplerViewsSerial;
|
||||||
++g_applier.SamplerStatesSerial;
|
++MGPipeApplier().SamplerStatesSerial;
|
||||||
++g_applier.ShaderImagesSerial;
|
++MGPipeApplier().ShaderImagesSerial;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
++g_applier.ShaderBuffersSerial;
|
++MGPipeApplier().ShaderBuffersSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplierReleaseObjectRecords() {
|
void MGPipeApplierReleaseObjectRecords() {
|
||||||
@@ -1615,37 +1778,37 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// context, so nothing wires this - see PipeApply.h. The two serials advance here for
|
// context, so nothing wires this - see PipeApply.h. The two serials advance here for
|
||||||
// MGPipeApplierReset's reason: state was cleared, and a twin that outlives it must not
|
// MGPipeApplierReset's reason: state was cleared, and a twin that outlives it must not
|
||||||
// be able to match a value it has already seen.
|
// be able to match a value it has already seen.
|
||||||
g_applier.Resources.clear();
|
MGPipeApplier().Resources.clear();
|
||||||
g_applier.VertexElementsCsos.clear();
|
MGPipeApplier().VertexElementsCsos.clear();
|
||||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
|
||||||
++g_applier.VertexBuffersSerial;
|
++MGPipeApplier().VertexBuffersSerial;
|
||||||
++g_applier.IndexBufferSerial;
|
++MGPipeApplier().IndexBufferSerial;
|
||||||
// P5c (rv): same rule for the shutter serials - the served context is going away, and
|
// P5c (rv): same rule for the shutter serials - the served context is going away, and
|
||||||
// a memo that outlives it must not match a value these have already answered with.
|
// a memo that outlives it must not match a value these have already answered with.
|
||||||
++g_applier.TextureShutterSerial;
|
++MGPipeApplier().TextureShutterSerial;
|
||||||
++g_applier.ContextSerial;
|
++MGPipeApplier().ContextSerial;
|
||||||
// P4a's five object tables go with them, and the working handles they could name go
|
// P4a's five object tables go with them, and the working handles they could name go
|
||||||
// too - a bound shader CSO whose record has just been dropped must not survive as a
|
// too - a bound shader CSO whose record has just been dropped must not survive as a
|
||||||
// handle the next call resolves against.
|
// handle the next call resolves against.
|
||||||
g_applier.TextureResources.clear();
|
MGPipeApplier().TextureResources.clear();
|
||||||
g_applier.RenderbufferResources.clear();
|
MGPipeApplier().RenderbufferResources.clear();
|
||||||
g_applier.SamplerCsos.clear();
|
MGPipeApplier().SamplerCsos.clear();
|
||||||
g_applier.SamplerViewCsos.clear();
|
MGPipeApplier().SamplerViewCsos.clear();
|
||||||
g_applier.ShaderCsos.clear();
|
MGPipeApplier().ShaderCsos.clear();
|
||||||
g_applier.CompositeShaderCsos.clear();
|
MGPipeApplier().CompositeShaderCsos.clear();
|
||||||
// AND THE FRAMEBUFFER RECORDS, which MGPipeApplierReset above deliberately keeps. Here
|
// AND THE FRAMEBUFFER RECORDS, which MGPipeApplierReset above deliberately keeps. Here
|
||||||
// there is nothing for them to outlive: the served context is going away and the
|
// there is nothing for them to outlive: the served context is going away and the
|
||||||
// framebuffers it owned are container objects that go with it. A table left standing
|
// framebuffers it owned are container objects that go with it. A table left standing
|
||||||
// would answer a lookup from the NEXT context on a slot the allocator has re-minted -
|
// would answer a lookup from the NEXT context on a slot the allocator has re-minted -
|
||||||
// with the same generation, if the next context happens to reach the same one - and
|
// with the same generation, if the next context happens to reach the same one - and
|
||||||
// that is the aliasing every other table is cleared to prevent.
|
// that is the aliasing every other table is cleared to prevent.
|
||||||
g_applier.FramebufferRecords.clear();
|
MGPipeApplier().FramebufferRecords.clear();
|
||||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
MGPipeApplier().DrawProgram = kMGPipeNullHandle;
|
||||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
|
||||||
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
g_applier.BoundStreamOutputLifetimeId = 0;
|
MGPipeApplier().BoundStreamOutputLifetimeId = 0;
|
||||||
g_applier.StreamOutputSpans.clear();
|
MGPipeApplier().StreamOutputSpans.clear();
|
||||||
#endif
|
#endif
|
||||||
// AND "THE WORKING HANDLES THEY COULD NAME" IS ALL OF THEM, NOT JUST THE THREE ABOVE.
|
// AND "THE WORKING HANDLES THEY COULD NAME" IS ALL OF THEM, NOT JUST THE THREE ABOVE.
|
||||||
// Every framebuffer record holds eleven MGPSurface::Res naming texture and renderbuffer
|
// Every framebuffer record holds eleven MGPSurface::Res naming texture and renderbuffer
|
||||||
@@ -1657,35 +1820,35 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// else's record. So this takes the same working state MGPipeApplierReset does - which is
|
// else's record. So this takes the same working state MGPipeApplierReset does - which is
|
||||||
// the honest reading of the rule as well: Reset keeps the object records BECAUSE they
|
// the honest reading of the rule as well: Reset keeps the object records BECAUSE they
|
||||||
// outlive a make-current, and here they do not outlive anything.
|
// outlive a make-current, and here they do not outlive anything.
|
||||||
g_applier.BoundFramebuffer = {};
|
MGPipeApplier().BoundFramebuffer = {};
|
||||||
g_applier.BoundSamplerViews = {};
|
MGPipeApplier().BoundSamplerViews = {};
|
||||||
g_applier.SamplerViewStart = 0;
|
MGPipeApplier().SamplerViewStart = 0;
|
||||||
g_applier.SamplerViewCount = 0;
|
MGPipeApplier().SamplerViewCount = 0;
|
||||||
g_applier.BoundSamplerStates = {};
|
MGPipeApplier().BoundSamplerStates = {};
|
||||||
g_applier.SamplerStateStart = 0;
|
MGPipeApplier().SamplerStateStart = 0;
|
||||||
g_applier.SamplerStateCount = 0;
|
MGPipeApplier().SamplerStateCount = 0;
|
||||||
g_applier.BoundShaderImages = {};
|
MGPipeApplier().BoundShaderImages = {};
|
||||||
g_applier.ShaderImageStart = 0;
|
MGPipeApplier().ShaderImageStart = 0;
|
||||||
g_applier.ShaderImageCount = 0;
|
MGPipeApplier().ShaderImageCount = 0;
|
||||||
// P5e (sb): and the three binding-point windows, for the paragraph above's reason in
|
// P5e (sb): and the three binding-point windows, for the paragraph above's reason in
|
||||||
// its own words - every MGPBufferRange::Res names a resource record this function has
|
// its own words - every MGPBufferRange::Res names a resource record this function has
|
||||||
// just dropped, so a window left populated here is a set of handles into an empty
|
// just dropped, so a window left populated here is a set of handles into an empty
|
||||||
// table, which the next resolve either refuses or answers with somebody else's record
|
// table, which the next resolve either refuses or answers with somebody else's record
|
||||||
// on a slot the next context re-mints.
|
// on a slot the next context re-mints.
|
||||||
ClearShaderBufferWindows();
|
ClearShaderBufferWindows();
|
||||||
++g_applier.FramebufferSerial;
|
++MGPipeApplier().FramebufferSerial;
|
||||||
++g_applier.SamplerViewsSerial;
|
++MGPipeApplier().SamplerViewsSerial;
|
||||||
++g_applier.SamplerStatesSerial;
|
++MGPipeApplier().SamplerStatesSerial;
|
||||||
++g_applier.ShaderImagesSerial;
|
++MGPipeApplier().ShaderImagesSerial;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
++g_applier.ShaderBuffersSerial;
|
++MGPipeApplier().ShaderBuffersSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
|
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
|
||||||
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
|
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
|
||||||
"create_render_state named the reserved slot 0");
|
"create_render_state named the reserved slot 0");
|
||||||
MGPipeRenderStateCsoRecord* recordAt =
|
MGPipeRenderStateCsoRecord* recordAt =
|
||||||
RecordAt(g_applier.RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots);
|
RecordAt(MGPipeApplier().RenderStateCsos, desc.Cso.Slot, kMGPipeMaxRenderStateCsoSlots);
|
||||||
if (recordAt == nullptr) {
|
if (recordAt == nullptr) {
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
MGPipeSessionFail(MGPipeFatalFamily::ProtocolCorruption,
|
MGPipeSessionFail(MGPipeFatalFamily::ProtocolCorruption,
|
||||||
@@ -1753,12 +1916,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeScatterPipelineBytes(record->PipelineBytes.data(),
|
MGPipeScatterPipelineBytes(record->PipelineBytes.data(),
|
||||||
MGPipeApplyAccess::RenderState(inputs));
|
MGPipeApplyAccess::RenderState(inputs));
|
||||||
MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion);
|
MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion);
|
||||||
g_applier.BoundRenderStateCso = bind.Cso;
|
MGPipeApplier().BoundRenderStateCso = bind.Cso;
|
||||||
// A bind scatters the WHOLE pipeline half - the record is always a complete one,
|
// A bind scatters the WHOLE pipeline half - the record is always a complete one,
|
||||||
// whatever mask minted it - so the pipeline chunks are all "moved" here, and all
|
// whatever mask minted it - so the pipeline chunks are all "moved" here, and all
|
||||||
// enter the applier's ledger of the bytes it owns.
|
// enter the applier's ledger of the bytes it owns.
|
||||||
const Uint32 moved = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineChunks);
|
const Uint32 moved = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineChunks);
|
||||||
g_applier.ScatteredChunkBits |= moved;
|
MGPipeApplier().ScatteredChunkBits |= moved;
|
||||||
MGPipeDeriveRenderStateFieldsForChunks(inputs, moved);
|
MGPipeDeriveRenderStateFieldsForChunks(inputs, moved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1771,8 +1934,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The Gen stays: it is the CLIENT allocator that bumps it when the slot is handed
|
// The Gen stays: it is the CLIENT allocator that bumps it when the slot is handed
|
||||||
// out again (MGPipeHandles.h: "Gen increments only when a SLOT IS REUSED"), and a
|
// out again (MGPipeHandles.h: "Gen increments only when a SLOT IS REUSED"), and a
|
||||||
// server-side bump here would put the two identities out of step.
|
// server-side bump here would put the two identities out of step.
|
||||||
if (g_applier.BoundRenderStateCso == handle.Handle) {
|
if (MGPipeApplier().BoundRenderStateCso == handle.Handle) {
|
||||||
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
|
MGPipeApplier().BoundRenderStateCso = kMGPipeNullHandle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1781,7 +1944,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs));
|
MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs));
|
||||||
MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version);
|
MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version);
|
||||||
const Uint32 moved = MGPipeGlobalChunkBitsOfDynamicMask(dyn.ChunkMask);
|
const Uint32 moved = MGPipeGlobalChunkBitsOfDynamicMask(dyn.ChunkMask);
|
||||||
g_applier.ScatteredChunkBits |= moved;
|
MGPipeApplier().ScatteredChunkBits |= moved;
|
||||||
MGPipeDeriveRenderStateFieldsForChunks(inputs, moved);
|
MGPipeDeriveRenderStateFieldsForChunks(inputs, moved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1805,9 +1968,9 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// filling, so a predicate that read the answer out of THERE would be reading the very
|
// filling, so a predicate that read the answer out of THERE would be reading the very
|
||||||
// memory the rule exists to stop it reading. One store, inert until the caps bit is
|
// memory the rule exists to stop it reading. One store, inert until the caps bit is
|
||||||
// published, and it is what makes the two roles' third clause the same clause.
|
// published, and it is what makes the two roles' third clause the same clause.
|
||||||
g_applier.IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0;
|
MGPipeApplier().IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0;
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
g_applier.BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId;
|
MGPipeApplier().BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1839,13 +2002,13 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// It runs in the shipped push build too, because a wire that is compiled out of
|
// It runs in the shipped push build too, because a wire that is compiled out of
|
||||||
// every build a device runs is not a wire. The cost is a 24-byte memcmp on a call
|
// every build a device runs is not a wire. The cost is a 24-byte memcmp on a call
|
||||||
// that is emitted when the tessellation state CHANGES, i.e. about once per program.
|
// that is emitted when the tessellation state CHANGES, i.e. about once per program.
|
||||||
if ((g_applier.ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) {
|
if ((MGPipeApplier().ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) {
|
||||||
++g_applier.PatchCarrierComparisons;
|
++MGPipeApplier().PatchCarrierComparisons;
|
||||||
const Bool agrees = working.PatchVertices == patch.Vertices &&
|
const Bool agrees = working.PatchVertices == patch.Vertices &&
|
||||||
std::memcmp(&working.PatchDefaultOuterLevel, &outer, sizeof(outer)) == 0 &&
|
std::memcmp(&working.PatchDefaultOuterLevel, &outer, sizeof(outer)) == 0 &&
|
||||||
std::memcmp(&working.PatchDefaultInnerLevel, &inner, sizeof(inner)) == 0;
|
std::memcmp(&working.PatchDefaultInnerLevel, &inner, sizeof(inner)) == 0;
|
||||||
if (!agrees) {
|
if (!agrees) {
|
||||||
++g_applier.PatchCarrierDivergences;
|
++MGPipeApplier().PatchCarrierDivergences;
|
||||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipePatchCarriersDiffer")
|
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipePatchCarriersDiffer")
|
||||||
" set_patch_state says vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g); "
|
" set_patch_state says vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g); "
|
||||||
"chunk P0 delivered vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g)",
|
"chunk P0 delivered vertices=%u outer=(%g,%g,%g,%g) inner=(%g,%g)",
|
||||||
@@ -1921,9 +2084,9 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplySetResidualValueState(const ResidualValueBlock& block) {
|
void MGPipeApplySetResidualValueState(const ResidualValueBlock& block) {
|
||||||
g_applier.Residual = block;
|
MGPipeApplier().Residual = block;
|
||||||
g_applier.HasResidual = true;
|
MGPipeApplier().HasResidual = true;
|
||||||
g_applier.ResidualCapabilitiesCompared = 0;
|
MGPipeApplier().ResidualCapabilitiesCompared = 0;
|
||||||
|
|
||||||
// THE TRIP WIRE (ARCHITECTURE.md 9.4, P2 brief D9). CapabilityBits is redundant with
|
// THE TRIP WIRE (ARCHITECTURE.md 9.4, P2 brief D9). CapabilityBits is redundant with
|
||||||
// the assembled working block by design: every one of the 35 capabilities is
|
// the assembled working block by design: every one of the 35 capabilities is
|
||||||
@@ -1953,16 +2116,16 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// tautology. The redundancy this wire exists to check is between the CARRIED bits and
|
// tautology. The redundancy this wire exists to check is between the CARRIED bits and
|
||||||
// the ASSEMBLED block.
|
// the ASSEMBLED block.
|
||||||
const RenderStateParameters& working = MGPipeApplyAccess::RenderState(gPipeInputs);
|
const RenderStateParameters& working = MGPipeApplyAccess::RenderState(gPipeInputs);
|
||||||
const Uint32 owned = g_applier.ScatteredChunkBits;
|
const Uint32 owned = MGPipeApplier().ScatteredChunkBits;
|
||||||
for (SizeT i = 0; i < kCapabilityCount; ++i) {
|
for (SizeT i = 0; i < kCapabilityCount; ++i) {
|
||||||
const CapabilityInput cap = static_cast<CapabilityInput>(i);
|
const CapabilityInput cap = static_cast<CapabilityInput>(i);
|
||||||
const Uint32 sources = CapabilitySourceChunks(cap);
|
const Uint32 sources = CapabilitySourceChunks(cap);
|
||||||
if ((owned & sources) != sources) continue;
|
if ((owned & sources) != sources) continue;
|
||||||
++g_applier.ResidualCapabilitiesCompared;
|
++MGPipeApplier().ResidualCapabilitiesCompared;
|
||||||
const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0;
|
const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0;
|
||||||
const Bool assembledBit = MGPipeApplyAccess::DeriveCapability(working, cap);
|
const Bool assembledBit = MGPipeApplyAccess::DeriveCapability(working, cap);
|
||||||
if (carried == assembledBit) continue;
|
if (carried == assembledBit) continue;
|
||||||
++g_applier.ResidualDivergences;
|
++MGPipeApplier().ResidualDivergences;
|
||||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeResidualDiverged, \"%s\"")
|
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeResidualDiverged, \"%s\"")
|
||||||
" carried=%d assembled=%d",
|
" carried=%d assembled=%d",
|
||||||
kCapabilityNames[i], static_cast<int>(carried),
|
kCapabilityNames[i], static_cast<int>(carried),
|
||||||
@@ -2279,7 +2442,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// THE FOUR BUFFER-ONLY CALLS, AND WHAT POLICES THAT. resource_flush_range,
|
// THE FOUR BUFFER-ONLY CALLS, AND WHAT POLICES THAT. resource_flush_range,
|
||||||
// resource_readback and map_persistent resolve through ResolveResource - i.e. against
|
// resource_readback and map_persistent resolve through ResolveResource - i.e. against
|
||||||
// g_applier.Resources and never the texture or renderbuffer table - which is right, because
|
// MGPipeApplier().Resources and never the texture or renderbuffer table - which is right, because
|
||||||
// a mapped range, a readback and a persistent donation exist for buffers only. They are
|
// a mapped range, a readback and a persistent donation exist for buffers only. They are
|
||||||
// buffer-only BY CATALOGUE and not by check: MGPFlushRange and MGPReadback carry no kind
|
// buffer-only BY CATALOGUE and not by check: MGPFlushRange and MGPReadback carry no kind
|
||||||
// at all, and map_persistent's MGPHandleOnly is not asked for one, so there is nothing here
|
// at all, and map_persistent's MGPHandleOnly is not asked for one, so there is nothing here
|
||||||
@@ -2428,7 +2591,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// Defined that way the number is identical in both modes, equals "one per storage
|
// Defined that way the number is identical in both modes, equals "one per storage
|
||||||
// definition", and is non-zero and assertable today; defined as "round trips actually
|
// definition", and is non-zero and assertable today; defined as "round trips actually
|
||||||
// taken" it would be 0 by construction in monolith and could never go red.
|
// taken" it would be 0 by construction in monolith and could never go red.
|
||||||
++g_applier.MapPersistentRoundtrips;
|
++MGPipeApplier().MapPersistentRoundtrips;
|
||||||
|
|
||||||
MGPipeResourceRecord* record = ResolveResource("map_persistent", handle.Handle);
|
MGPipeResourceRecord* record = ResolveResource("map_persistent", handle.Handle);
|
||||||
if (record == nullptr) return nullptr;
|
if (record == nullptr) return nullptr;
|
||||||
@@ -2586,7 +2749,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MGPipeVertexElementsRecord* recordAt =
|
MGPipeVertexElementsRecord* recordAt =
|
||||||
RecordAt(g_applier.VertexElementsCsos, desc.Cso.Slot, kMGPipeMaxVertexElementsSlots);
|
RecordAt(MGPipeApplier().VertexElementsCsos, desc.Cso.Slot, kMGPipeMaxVertexElementsSlots);
|
||||||
if (recordAt == nullptr) {
|
if (recordAt == nullptr) {
|
||||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||||
" create_vertex_elements {slot=%u, gen=%u}: the slot is outside the record "
|
" create_vertex_elements {slot=%u, gen=%u}: the slot is outside the record "
|
||||||
@@ -2634,14 +2797,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The null handle is legal and means "no vertex array bound" - GL's unbound state is a
|
// The null handle is legal and means "no vertex array bound" - GL's unbound state is a
|
||||||
// state, not an error, and the backend has a branch for it.
|
// state, not an error, and the backend has a branch for it.
|
||||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A dead handle leaves the PREVIOUS binding untouched, which is bind_render_state's
|
// A dead handle leaves the PREVIOUS binding untouched, which is bind_render_state's
|
||||||
// precedent for the same question, and is counted like every other refusal.
|
// precedent for the same question, and is counted like every other refusal.
|
||||||
const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle);
|
const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle);
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
g_applier.BoundVertexElements = handle.Handle;
|
MGPipeApplier().BoundVertexElements = handle.Handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) {
|
void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) {
|
||||||
@@ -2663,8 +2826,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
const Uint32 gen = record->Gen;
|
const Uint32 gen = record->Gen;
|
||||||
*record = MGPipeVertexElementsRecord{};
|
*record = MGPipeVertexElementsRecord{};
|
||||||
record->Gen = gen;
|
record->Gen = gen;
|
||||||
if (g_applier.BoundVertexElements == handle.Handle) {
|
if (MGPipeApplier().BoundVertexElements == handle.Handle) {
|
||||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
MGPipeApplier().BoundVertexElements = kMGPipeNullHandle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2692,10 +2855,10 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// are not cleared: this record is "the last set as received", and a set that names
|
// are not cleared: this record is "the last set as received", and a set that names
|
||||||
// four entries has said nothing about the rest.
|
// four entries has said nothing about the rest.
|
||||||
for (Uint32 i = 0; i < hdr.Count; ++i) {
|
for (Uint32 i = 0; i < hdr.Count; ++i) {
|
||||||
g_applier.VertexBuffers[hdr.Start + i] = tail[i];
|
MGPipeApplier().VertexBuffers[hdr.Start + i] = tail[i];
|
||||||
}
|
}
|
||||||
g_applier.VertexBufferStart = hdr.Start;
|
MGPipeApplier().VertexBufferStart = hdr.Start;
|
||||||
g_applier.VertexBufferCount = hdr.Count;
|
MGPipeApplier().VertexBufferCount = hdr.Count;
|
||||||
|
|
||||||
// THE RAW VALUE IS STORED, NOT A RESOLVED SHIFT, and the resolution is one step
|
// THE RAW VALUE IS STORED, NOT A RESOLVED SHIFT, and the resolution is one step
|
||||||
// further out on purpose. Whether the fetch shift has to be emulated at all is a
|
// further out on purpose. Whether the fetch shift has to be emulated at all is a
|
||||||
@@ -2710,8 +2873,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// input: set_vertex_buffers is suppressed on an unchanged hash, so a base instance
|
// input: set_vertex_buffers is suppressed on an unchanged hash, so a base instance
|
||||||
// that moved while the buffer set did not would otherwise never arrive and the server
|
// that moved while the buffer set did not would otherwise never arrive and the server
|
||||||
// would keep the previous shift.
|
// would keep the previous shift.
|
||||||
g_applier.VertexFetchBaseInstance = hdr.BaseInstance;
|
MGPipeApplier().VertexFetchBaseInstance = hdr.BaseInstance;
|
||||||
++g_applier.VertexBuffersSerial;
|
++MGPipeApplier().VertexBuffersSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) {
|
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) {
|
||||||
@@ -2725,8 +2888,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// - it is an INDEPENDENT call and NOT a subset of the vertex-elements configuration
|
// - it is an INDEPENDENT call and NOT a subset of the vertex-elements configuration
|
||||||
// (D5): a rebind of the index slot must move this serial without touching the
|
// (D5): a rebind of the index slot must move this serial without touching the
|
||||||
// configuration's, which is exactly what the backend's two separate compares need.
|
// configuration's, which is exactly what the backend's two separate compares need.
|
||||||
g_applier.IndexBuffer = record;
|
MGPipeApplier().IndexBuffer = record;
|
||||||
++g_applier.IndexBufferSerial;
|
++MGPipeApplier().IndexBufferSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
|
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
|
||||||
@@ -2839,7 +3002,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
if (NoP4aConsumer()) return;
|
if (NoP4aConsumer()) return;
|
||||||
|
|
||||||
MGPipeFramebufferRecord* record =
|
MGPipeFramebufferRecord* record =
|
||||||
RecordAt(g_applier.FramebufferRecords, state.Fbo.Slot, kMGPipeMaxFramebufferSlots);
|
RecordAt(MGPipeApplier().FramebufferRecords, state.Fbo.Slot, kMGPipeMaxFramebufferSlots);
|
||||||
// Unreachable: the bound was checked above, before anything moved. The null check is
|
// Unreachable: the bound was checked above, before anything moved. The null check is
|
||||||
// what makes that an argument rather than an assumption.
|
// what makes that an argument rather than an assumption.
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
@@ -2851,11 +3014,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// target: "this record describes the framebuffer it names; no binding changes".
|
// target: "this record describes the framebuffer it names; no binding changes".
|
||||||
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Draw) ||
|
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Draw) ||
|
||||||
state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) {
|
state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) {
|
||||||
g_applier.BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Draw)] = state.Fbo;
|
MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Draw)] = state.Fbo;
|
||||||
}
|
}
|
||||||
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read) ||
|
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read) ||
|
||||||
state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) {
|
state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) {
|
||||||
g_applier.BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Read)] = state.Fbo;
|
MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(MGPipeFramebufferTarget::Read)] = state.Fbo;
|
||||||
}
|
}
|
||||||
// ONE SERIAL FOR THE FAMILY, and it moves once per applied record - a Both record is one
|
// ONE SERIAL FOR THE FAMILY, and it moves once per applied record - a Both record is one
|
||||||
// record and a Named record is one record. It is what retires the four g_fboSynced*
|
// record and a Named record is one record. It is what retires the four g_fboSynced*
|
||||||
@@ -2864,7 +3027,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// serial still the server's". A Named record moves it for the same reason a bound one
|
// serial still the server's". A Named record moves it for the same reason a bound one
|
||||||
// does - a twin that memoised a framebuffer's attachments has to hear that they moved,
|
// does - a twin that memoised a framebuffer's attachments has to hear that they moved,
|
||||||
// and whether the framebuffer happens to be bound is not that question.
|
// and whether the framebuffer happens to be bound is not that question.
|
||||||
++g_applier.FramebufferSerial;
|
++MGPipeApplier().FramebufferSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
// THE READ SIDE OF THE TABLE (ID-19(b)/(d)). Package D asks it by handle at every DSA entry
|
// THE READ SIDE OF THE TABLE (ID-19(b)/(d)). Package D asks it by handle at every DSA entry
|
||||||
@@ -2941,7 +3104,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MGPipeSamplerCsoRecord* recordAt =
|
MGPipeSamplerCsoRecord* recordAt =
|
||||||
RecordAt(g_applier.SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots);
|
RecordAt(MGPipeApplier().SamplerCsos, desc.Cso.Slot, kMGPipeMaxSamplerCsoSlots);
|
||||||
if (recordAt == nullptr) {
|
if (recordAt == nullptr) {
|
||||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||||
" create_sampler_state {slot=%u, gen=%u}: the slot is outside the record "
|
" create_sampler_state {slot=%u, gen=%u}: the slot is outside the record "
|
||||||
@@ -2978,7 +3141,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerCso),
|
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerCso),
|
||||||
"delete_sampler_state on kind %u", handle.Kind);
|
"delete_sampler_state on kind %u", handle.Kind);
|
||||||
MGPipeSamplerCsoRecord* record =
|
MGPipeSamplerCsoRecord* record =
|
||||||
ResolveObject(g_applier.SamplerCsos, "delete_sampler_state", handle.Handle);
|
ResolveObject(MGPipeApplier().SamplerCsos, "delete_sampler_state", handle.Handle);
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
|
|
||||||
// Dropped whole, generation kept, for resource_destroy's reason: the client allocator
|
// Dropped whole, generation kept, for resource_destroy's reason: the client allocator
|
||||||
@@ -3000,7 +3163,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
if (view.Cso.Slot < kMGPipeFirstAllocatableSlot) return;
|
if (view.Cso.Slot < kMGPipeFirstAllocatableSlot) return;
|
||||||
|
|
||||||
MGPipeSamplerViewRecord* recordAt =
|
MGPipeSamplerViewRecord* recordAt =
|
||||||
RecordAt(g_applier.SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots);
|
RecordAt(MGPipeApplier().SamplerViewCsos, view.Cso.Slot, kMGPipeMaxSamplerViewSlots);
|
||||||
if (recordAt == nullptr) {
|
if (recordAt == nullptr) {
|
||||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||||
" create_sampler_view {slot=%u, gen=%u}: the slot is outside the record "
|
" create_sampler_view {slot=%u, gen=%u}: the slot is outside the record "
|
||||||
@@ -3031,7 +3194,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// arriving before - or without - the texture record is an ordering fact and not a
|
// arriving before - or without - the texture record is an ordering fact and not a
|
||||||
// refusal. When the record is there this is the back-pointer that lets a sync reach a
|
// refusal. When the record is there this is the back-pointer that lets a sync reach a
|
||||||
// texture's view without walking every view the applier holds.
|
// texture's view without walking every view the applier holds.
|
||||||
MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, view.Texture);
|
MGPipeResourceRecord* texture = FindIn(MGPipeApplier().TextureResources, view.Texture);
|
||||||
if (texture != nullptr) texture->ViewCso = view.Cso;
|
if (texture != nullptr) texture->ViewCso = view.Cso;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3039,13 +3202,13 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerViewCso),
|
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::SamplerViewCso),
|
||||||
"delete_sampler_view on kind %u", handle.Kind);
|
"delete_sampler_view on kind %u", handle.Kind);
|
||||||
MGPipeSamplerViewRecord* record =
|
MGPipeSamplerViewRecord* record =
|
||||||
ResolveObject(g_applier.SamplerViewCsos, "delete_sampler_view", handle.Handle);
|
ResolveObject(MGPipeApplier().SamplerViewCsos, "delete_sampler_view", handle.Handle);
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
|
|
||||||
// The back-pointer goes first, while the record that names the texture still exists,
|
// The back-pointer goes first, while the record that names the texture still exists,
|
||||||
// and only if that texture still names THIS view - a texture whose view has already
|
// and only if that texture still names THIS view - a texture whose view has already
|
||||||
// been re-minted must not have the new handle cleared out from under it.
|
// been re-minted must not have the new handle cleared out from under it.
|
||||||
MGPipeResourceRecord* texture = FindIn(g_applier.TextureResources, record->View.Texture);
|
MGPipeResourceRecord* texture = FindIn(MGPipeApplier().TextureResources, record->View.Texture);
|
||||||
if (texture != nullptr && texture->ViewCso == handle.Handle) {
|
if (texture != nullptr && texture->ViewCso == handle.Handle) {
|
||||||
texture->ViewCso = kMGPipeNullHandle;
|
texture->ViewCso = kMGPipeNullHandle;
|
||||||
}
|
}
|
||||||
@@ -3066,7 +3229,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// READ-attachment case reaches no parameter push at all today. The record exists the
|
// READ-attachment case reaches no parameter push at all today. The record exists the
|
||||||
// moment the parameters move, whether or not anything is bound.
|
// moment the parameters move, whether or not anything is bound.
|
||||||
MGPipeResourceRecord* record =
|
MGPipeResourceRecord* record =
|
||||||
ResolveObject(g_applier.TextureResources, "set_texture_params", params.Res);
|
ResolveObject(MGPipeApplier().TextureResources, "set_texture_params", params.Res);
|
||||||
if (record == nullptr) return false;
|
if (record == nullptr) return false;
|
||||||
|
|
||||||
// EVERY ITextureObject OWNS A SamplerObject, so the built-in sampler CSO is not
|
// EVERY ITextureObject OWNS A SamplerObject, so the built-in sampler CSO is not
|
||||||
@@ -3121,11 +3284,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
|
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
|
||||||
if (NoP4aConsumer()) return;
|
if (NoP4aConsumer()) return;
|
||||||
if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
||||||
g_applier.BoundSamplerViews, g_applier.SamplerViewStart,
|
MGPipeApplier().BoundSamplerViews, MGPipeApplier().SamplerViewStart,
|
||||||
g_applier.SamplerViewCount)) {
|
MGPipeApplier().SamplerViewCount)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
++g_applier.SamplerViewsSerial;
|
++MGPipeApplier().SamplerViewsSerial;
|
||||||
// P5c (rv): a texture bind is what the bind-generation shutter guarded.
|
// P5c (rv): a texture bind is what the bind-generation shutter guarded.
|
||||||
MGPipeApplierNoteTextureStateMoved();
|
MGPipeApplierNoteTextureStateMoved();
|
||||||
}
|
}
|
||||||
@@ -3133,11 +3296,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
|
void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
|
||||||
if (NoP4aConsumer()) return;
|
if (NoP4aConsumer()) return;
|
||||||
if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
||||||
g_applier.BoundSamplerStates, g_applier.SamplerStateStart,
|
MGPipeApplier().BoundSamplerStates, MGPipeApplier().SamplerStateStart,
|
||||||
g_applier.SamplerStateCount)) {
|
MGPipeApplier().SamplerStateCount)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
++g_applier.SamplerStatesSerial;
|
++MGPipeApplier().SamplerStatesSerial;
|
||||||
// P5c (rv): a sampler bind or parameter change is what the shutters guarded.
|
// P5c (rv): a sampler bind or parameter change is what the shutters guarded.
|
||||||
MGPipeApplierNoteTextureStateMoved();
|
MGPipeApplierNoteTextureStateMoved();
|
||||||
}
|
}
|
||||||
@@ -3150,11 +3313,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// formats on Adreno will accept is the backend's, not this applier's.
|
// formats on Adreno will accept is the backend's, not this applier's.
|
||||||
if (NoP4aConsumer()) return;
|
if (NoP4aConsumer()) return;
|
||||||
if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail,
|
||||||
g_applier.BoundShaderImages, g_applier.ShaderImageStart,
|
MGPipeApplier().BoundShaderImages, MGPipeApplier().ShaderImageStart,
|
||||||
g_applier.ShaderImageCount)) {
|
MGPipeApplier().ShaderImageCount)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
++g_applier.ShaderImagesSerial;
|
++MGPipeApplier().ShaderImagesSerial;
|
||||||
// P5c (rv): an image bind moves the texture bind generation's guarded set too.
|
// P5c (rv): an image bind moves the texture bind generation's guarded set too.
|
||||||
MGPipeApplierNoteTextureStateMoved();
|
MGPipeApplierNoteTextureStateMoved();
|
||||||
}
|
}
|
||||||
@@ -3211,18 +3374,18 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// unit sets and set_vertex_buffers work: the record is "the last set as received", and
|
// unit sets and set_vertex_buffers work: the record is "the last set as received", and
|
||||||
// a binding at or above Count means "nothing bound" to every reader (the frontend
|
// a binding at or above Count means "nothing bound" to every reader (the frontend
|
||||||
// array's default says the same thing, which is what makes the two pictures agree).
|
// array's default says the same thing, which is what makes the two pictures agree).
|
||||||
auto& destination = g_applier.BoundShaderBuffers[hdr.Class];
|
auto& destination = MGPipeApplier().BoundShaderBuffers[hdr.Class];
|
||||||
for (Uint32 i = 0; i < hdr.Count; ++i) destination[hdr.Start + i] = tail[i];
|
for (Uint32 i = 0; i < hdr.Count; ++i) destination[hdr.Start + i] = tail[i];
|
||||||
g_applier.ShaderBufferStart[hdr.Class] = hdr.Start;
|
MGPipeApplier().ShaderBufferStart[hdr.Class] = hdr.Start;
|
||||||
g_applier.ShaderBufferCount[hdr.Class] = hdr.Count;
|
MGPipeApplier().ShaderBufferCount[hdr.Class] = hdr.Count;
|
||||||
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
|
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
|
||||||
g_applier.ShaderBufferWritableMask[hdr.Class][w] = hdr.WritableMask[w];
|
MGPipeApplier().ShaderBufferWritableMask[hdr.Class][w] = hdr.WritableMask[w];
|
||||||
}
|
}
|
||||||
// ONE SERIAL FOR ALL THREE CLASSES, and it moves on EVERY applied record - a backend
|
// ONE SERIAL FOR ALL THREE CLASSES, and it moves on EVERY applied record - a backend
|
||||||
// memo that asks "have the binding points moved since I last bound them" must hear
|
// memo that asks "have the binding points moved since I last bound them" must hear
|
||||||
// about a uniform set exactly as it hears about a storage set, and splitting the serial
|
// about a uniform set exactly as it hears about a storage set, and splitting the serial
|
||||||
// per class would make each of the four consumers pick which one to read.
|
// per class would make each of the four consumers pick which one to read.
|
||||||
++g_applier.ShaderBuffersSerial;
|
++MGPipeApplier().ShaderBuffersSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
// P5e (pg) WRITES THE SECOND BODY. The three post-link mutable reflection fields -
|
// P5e (pg) WRITES THE SECOND BODY. The three post-link mutable reflection fields -
|
||||||
@@ -3501,14 +3664,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// error; a DEAD handle leaves the previous binding untouched and is counted, which is
|
// error; a DEAD handle leaves the previous binding untouched and is counted, which is
|
||||||
// bind_render_state's precedent for the same question.
|
// bind_render_state's precedent for the same question.
|
||||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||||
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const MGPipeShaderCsoRecord* record = ResolveShaderCso("bind_shader_state", handle.Handle);
|
const MGPipeShaderCsoRecord* record = ResolveShaderCso("bind_shader_state", handle.Handle);
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
g_applier.BoundShaderCso = handle.Handle;
|
MGPipeApplier().BoundShaderCso = handle.Handle;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) {
|
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) {
|
||||||
@@ -3532,19 +3695,19 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// next verb resolving a binding to a record this applier no longer has, which is the
|
// next verb resolving a binding to a record this applier no longer has, which is the
|
||||||
// refusal counter firing for a state the applier itself created.
|
// refusal counter firing for a state the applier itself created.
|
||||||
Bool cleared = false;
|
Bool cleared = false;
|
||||||
if (g_applier.BoundShaderCso == handle.Handle) {
|
if (MGPipeApplier().BoundShaderCso == handle.Handle) {
|
||||||
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
MGPipeApplier().BoundShaderCso = kMGPipeNullHandle;
|
||||||
cleared = true;
|
cleared = true;
|
||||||
}
|
}
|
||||||
if (g_applier.DrawProgram == handle.Handle) {
|
if (MGPipeApplier().DrawProgram == handle.Handle) {
|
||||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
MGPipeApplier().DrawProgram = kMGPipeNullHandle;
|
||||||
cleared = true;
|
cleared = true;
|
||||||
}
|
}
|
||||||
if (g_applier.DispatchProgram == handle.Handle) {
|
if (MGPipeApplier().DispatchProgram == handle.Handle) {
|
||||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
|
||||||
cleared = true;
|
cleared = true;
|
||||||
}
|
}
|
||||||
if (cleared) ++g_applier.ProgramBindingSerial;
|
if (cleared) ++MGPipeApplier().ProgramBindingSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TWO CALLS AND NOT ONE, because the frontend has two joins and two PipeInputs slots: a
|
// TWO CALLS AND NOT ONE, because the frontend has two joins and two PipeInputs slots: a
|
||||||
@@ -3555,14 +3718,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
"set_draw_program on kind %u", handle.Kind);
|
"set_draw_program on kind %u", handle.Kind);
|
||||||
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
|
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
|
||||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||||
g_applier.DrawProgram = kMGPipeNullHandle;
|
MGPipeApplier().DrawProgram = kMGPipeNullHandle;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_draw_program", handle.Handle);
|
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_draw_program", handle.Handle);
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
g_applier.DrawProgram = handle.Handle;
|
MGPipeApplier().DrawProgram = handle.Handle;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) {
|
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) {
|
||||||
@@ -3570,14 +3733,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
"set_dispatch_program on kind %u", handle.Kind);
|
"set_dispatch_program on kind %u", handle.Kind);
|
||||||
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
|
if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason.
|
||||||
if (MGPipeHandleIsNull(handle.Handle)) {
|
if (MGPipeHandleIsNull(handle.Handle)) {
|
||||||
g_applier.DispatchProgram = kMGPipeNullHandle;
|
MGPipeApplier().DispatchProgram = kMGPipeNullHandle;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_dispatch_program", handle.Handle);
|
const MGPipeShaderCsoRecord* record = ResolveShaderCso("set_dispatch_program", handle.Handle);
|
||||||
if (record == nullptr) return;
|
if (record == nullptr) return;
|
||||||
g_applier.DispatchProgram = handle.Handle;
|
MGPipeApplier().DispatchProgram = handle.Handle;
|
||||||
++g_applier.ProgramBindingSerial;
|
++MGPipeApplier().ProgramBindingSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) {
|
void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) {
|
||||||
|
|||||||
+186
-65
@@ -513,7 +513,88 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPFramebufferState State{};
|
MGPFramebufferState State{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MGPipeApplierState {
|
// =====================================================================================
|
||||||
|
// P14 S5: THE SHARE-GROUP HALF OF THE APPLIER'S STATE.
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// THE TWO HALVES HAVE DIFFERENT LIVES, and until this slice they were one struct with one
|
||||||
|
// owner (the process), which is what made a second served context either share working state
|
||||||
|
// it must not share or lose object records it must not lose. They are two structs now because
|
||||||
|
// they are two OWNERS:
|
||||||
|
//
|
||||||
|
// MGPipeObjectRecords a GL object lives in a SHARE GROUP, so these records are keyed by
|
||||||
|
// {session, share group}: two contexts of one group see one set, two
|
||||||
|
// NON-shared contexts of one session see two sets - which is the whole
|
||||||
|
// reason the frontend gives each context its own name space
|
||||||
|
// (MG_State/GLState/ShareGroupState). A GL object survives a
|
||||||
|
// make-current and its record is the only thing the backend has left to
|
||||||
|
// read that object's storage extent and mutation serial out of (D-A4
|
||||||
|
// re-keys IsBufferDrawClean onto exactly those two); dropping them at a
|
||||||
|
// make-current made every subsequent glBufferSubData on a pre-existing
|
||||||
|
// buffer resolve to nothing.
|
||||||
|
//
|
||||||
|
// MGPipeWorkingState what the next draw fetches with - bindings, current program, the unit
|
||||||
|
// windows, the CSO store, the serials. Keyed by {session, context}: a
|
||||||
|
// returning context may not inherit another's bindings, and two
|
||||||
|
// contexts of one share group may not overwrite each other's.
|
||||||
|
//
|
||||||
|
// THEY ARE CLEARED BY DIFFERENT EVENTS. A record here is cleared by the object's OWN death
|
||||||
|
// signal - resource_destroy, delete_vertex_elements, which is what D-L makes the buffer's death
|
||||||
|
// crossing - and by the share group's own teardown (MGPipeApplierReleaseGroupRecords) when its
|
||||||
|
// last context goes away. MGPipeApplierReset does NOT touch this struct; MGPipeApplierState's
|
||||||
|
// working half is what a make-current resets.
|
||||||
|
struct MGPipeObjectRecords {
|
||||||
|
// ---- object records: indexed by MGPipeHandle::Slot of kind Buffer /
|
||||||
|
// VertexElementsCso, and NOT part of the working state. They are cleared by the object's
|
||||||
|
// OWN death signal and by the group's teardown. Nothing else.
|
||||||
|
Vector<MGPipeResourceRecord> Resources;
|
||||||
|
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
|
||||||
|
|
||||||
|
// ---- P4a's object records. FIVE MORE TABLES, and the two resource ones are separate
|
||||||
|
// Vectors rather than more rows of `Resources` above because the slot space is PER
|
||||||
|
// KIND: a Buffer, a Texture and a Renderbuffer can all hold slot 7 at once, so a
|
||||||
|
// single slot-indexed table would alias three different objects onto one record. The
|
||||||
|
// record TYPE is shared - one discriminated descriptor for buffers, every texture
|
||||||
|
// target and renderbuffers - and the bound is shared; only the table is per kind.
|
||||||
|
//
|
||||||
|
// Like the two above they are share-group state: MGPipeApplierReset does not touch
|
||||||
|
// them, and only the object's own death signal and the group's teardown clear them.
|
||||||
|
Vector<MGPipeResourceRecord> TextureResources;
|
||||||
|
Vector<MGPipeResourceRecord> RenderbufferResources;
|
||||||
|
Vector<MGPipeSamplerCsoRecord> SamplerCsos;
|
||||||
|
Vector<MGPipeSamplerViewRecord> SamplerViewCsos;
|
||||||
|
Vector<MGPipeShaderCsoRecord> ShaderCsos;
|
||||||
|
// The ShaderCso COMPOSITE band's records, indexed by (slot - the band's base), for the
|
||||||
|
// same reason MGPipeSlotAllocator keeps the band in a table of its own: the band
|
||||||
|
// starts at 983040, so one program-pipeline composite in the slot-indexed vector above
|
||||||
|
// would grow it to ~983k records of ~240 bytes each. THE SERVER STILL NEVER LEARNS IT
|
||||||
|
// IS A COMPOSITE - the split is an indexing detail on this side of the wire, the
|
||||||
|
// handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it
|
||||||
|
// exactly as they name any other program.
|
||||||
|
Vector<MGPipeShaderCsoRecord> CompositeShaderCsos;
|
||||||
|
// AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot,
|
||||||
|
// for the reason MGPipeFramebufferRecord states: the two bound-target records the phase
|
||||||
|
// started with could not describe a framebuffer that is bound to neither binding, and
|
||||||
|
// the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand
|
||||||
|
// Espryt exactly that.
|
||||||
|
//
|
||||||
|
// IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also
|
||||||
|
// its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared
|
||||||
|
// between contexts, but its record is addressed by a slot out of one global allocator,
|
||||||
|
// so nothing aliases across a switch - and dropping the table would leave a
|
||||||
|
// DSA-only framebuffer with no record and no event that would ever re-emit one (the
|
||||||
|
// client's suppressor invalidation re-emits the two BOUND records and nothing else).
|
||||||
|
//
|
||||||
|
// P14 S5 keys it by SHARE GROUP like its siblings, which is safe for the reason just
|
||||||
|
// given: the slot space is global, so a group's table can only ever answer for handles
|
||||||
|
// its own contexts minted, and a shared group holding both contexts' records is a wider
|
||||||
|
// table rather than an aliasing one.
|
||||||
|
Vector<MGPipeFramebufferRecord> FramebufferRecords;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The per-context half. Everything here is cleared by MGPipeApplierReset, EXCEPT the serials,
|
||||||
|
// which only ever advance (see there).
|
||||||
|
struct MGPipeWorkingState {
|
||||||
// Indexed by slot; slot 0 is the reserved null handle and is never live
|
// Indexed by slot; slot 0 is the reserved null handle and is never live
|
||||||
// (MGPipeHandles.h kMGPipeFirstAllocatableSlot).
|
// (MGPipeHandles.h kMGPipeFirstAllocatableSlot).
|
||||||
Vector<MGPipeRenderStateCsoRecord> RenderStateCsos;
|
Vector<MGPipeRenderStateCsoRecord> RenderStateCsos;
|
||||||
@@ -553,69 +634,6 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only
|
Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only
|
||||||
Uint32 PatchCarrierDivergences = 0; // cumulative
|
Uint32 PatchCarrierDivergences = 0; // cumulative
|
||||||
|
|
||||||
// ---- P3a (D-G4). ----
|
|
||||||
//
|
|
||||||
// THE TWO HALVES BELOW HAVE DIFFERENT LIVES, and MGPipeApplierReset is where the
|
|
||||||
// difference is spent: the OBJECT RECORDS describe GL objects and outlive a
|
|
||||||
// make-current; the WORKING STATE describes what the next draw fetches with and does
|
|
||||||
// not. Reading the whole block as "per context" is what dropped a shared buffer's
|
|
||||||
// record at every context switch and made the write that followed it disappear.
|
|
||||||
|
|
||||||
// ---- object records: indexed by MGPipeHandle::Slot of kind Buffer /
|
|
||||||
// VertexElementsCso, and NOT part of the working state.
|
|
||||||
//
|
|
||||||
// A GL object lives in a SHARE GROUP, not in a context: a buffer created before a
|
|
||||||
// make-current is the same buffer, with the same storage, after it, and its record is
|
|
||||||
// the only thing the backend has left to read that storage's extent and mutation
|
|
||||||
// serial out of (D-A4 re-keys IsBufferDrawClean onto exactly those two). Dropping the
|
|
||||||
// records at a make-current would therefore make every subsequent glBufferSubData on a
|
|
||||||
// pre-existing buffer resolve to nothing and be refused - a lost write, in a build
|
|
||||||
// where the refusal's assertion has compiled out.
|
|
||||||
//
|
|
||||||
// They are cleared by the object's OWN death signal - resource_destroy,
|
|
||||||
// delete_vertex_elements, which is what D-L makes the buffer's death crossing - and by
|
|
||||||
// MGPipeApplierReleaseObjectRecords when the served context and its applier go away.
|
|
||||||
// Nothing else.
|
|
||||||
Vector<MGPipeResourceRecord> Resources;
|
|
||||||
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
|
|
||||||
|
|
||||||
// ---- P4a's object records. FIVE MORE TABLES, and the two resource ones are separate
|
|
||||||
// Vectors rather than more rows of `Resources` above because the slot space is PER
|
|
||||||
// KIND: a Buffer, a Texture and a Renderbuffer can all hold slot 7 at once, so a
|
|
||||||
// single slot-indexed table would alias three different objects onto one record. The
|
|
||||||
// record TYPE is shared - one discriminated descriptor for buffers, every texture
|
|
||||||
// target and renderbuffers - and the bound is shared; only the table is per kind.
|
|
||||||
//
|
|
||||||
// Like the two above they are share-group state: MGPipeApplierReset does not touch
|
|
||||||
// them, and only the object's own death signal and MGPipeApplierReleaseObjectRecords
|
|
||||||
// clear them.
|
|
||||||
Vector<MGPipeResourceRecord> TextureResources;
|
|
||||||
Vector<MGPipeResourceRecord> RenderbufferResources;
|
|
||||||
Vector<MGPipeSamplerCsoRecord> SamplerCsos;
|
|
||||||
Vector<MGPipeSamplerViewRecord> SamplerViewCsos;
|
|
||||||
Vector<MGPipeShaderCsoRecord> ShaderCsos;
|
|
||||||
// The ShaderCso COMPOSITE band's records, indexed by (slot - the band's base), for the
|
|
||||||
// same reason MGPipeSlotAllocator keeps the band in a table of its own: the band
|
|
||||||
// starts at 983040, so one program-pipeline composite in the slot-indexed vector above
|
|
||||||
// would grow it to ~983k records of ~240 bytes each. THE SERVER STILL NEVER LEARNS IT
|
|
||||||
// IS A COMPOSITE - the split is an indexing detail on this side of the wire, the
|
|
||||||
// handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it
|
|
||||||
// exactly as they name any other program.
|
|
||||||
Vector<MGPipeShaderCsoRecord> CompositeShaderCsos;
|
|
||||||
// AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot,
|
|
||||||
// for the reason MGPipeFramebufferRecord states: the two bound-target records the phase
|
|
||||||
// started with could not describe a framebuffer that is bound to neither binding, and
|
|
||||||
// the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand
|
|
||||||
// Espryt exactly that.
|
|
||||||
//
|
|
||||||
// IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also
|
|
||||||
// its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared
|
|
||||||
// between contexts, but its record is addressed by a slot out of one global allocator,
|
|
||||||
// so nothing aliases across a switch - and dropping the table would leave a
|
|
||||||
// DSA-only framebuffer with no record and no event that would ever re-emit one (the
|
|
||||||
// client's suppressor invalidation re-emits the two BOUND records and nothing else).
|
|
||||||
Vector<MGPipeFramebufferRecord> FramebufferRecords;
|
|
||||||
|
|
||||||
// Every call this applier REFUSED because it named a record this applier does not
|
// Every call this applier REFUSED because it named a record this applier does not
|
||||||
// have: an unknown slot, a slot that is not live, or a generation that has moved on
|
// have: an unknown slot, a slot that is not live, or a generation that has moved on
|
||||||
// under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no
|
// under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no
|
||||||
@@ -911,6 +929,53 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P14 S5: ONE CONTEXT'S APPLIER = ITS OWN WORKING STATE + ITS SHARE GROUP'S RECORDS.
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// THE INHERITANCE IS THE POINT AND NOT A SHORTCUT: everything the backend spells as
|
||||||
|
// `MGPipeApplier().X` for a WORKING field keeps working unchanged (there are ~380 such reads
|
||||||
|
// in MG_Backend), and every read of an OBJECT record moves to `MGPipeApplier().Objects.X` -
|
||||||
|
// which is what makes the two lifetimes visible at exactly the sites where confusing them
|
||||||
|
// would be a defect. The reference is to a MGPipeObjectRecords that OUTLIVES this object by
|
||||||
|
// construction: the registry owns the group's records and this context's applier together,
|
||||||
|
// and drops them in that order.
|
||||||
|
//
|
||||||
|
// A context that started its own share group still has one - a group of one - so every
|
||||||
|
// applier has somewhere for its objects to live and there is no "no group" case.
|
||||||
|
struct MGPipeApplierState : MGPipeWorkingState {
|
||||||
|
// The share group this context's objects live in.
|
||||||
|
MGPipeObjectRecords& Objects;
|
||||||
|
|
||||||
|
// ---- THE OBJECT TABLES, UNDER THE NAMES EVERY CALLER ALREADY SPELLS. --------------
|
||||||
|
//
|
||||||
|
// These nine are the group's tables, not copies of them: ~200 call sites in
|
||||||
|
// MG_Backend, MG_IntegrationTest and MG_Test read `st.Resources`, `state.TextureResources`
|
||||||
|
// and their siblings, and the name they use has ALWAYS meant "the share group's record
|
||||||
|
// for this object". Binding them here keeps every one of those reads correct by
|
||||||
|
// construction - through the group its own context belongs to - instead of turning a
|
||||||
|
// keying change into a renaming change. `Objects.X` and `X` therefore name the same
|
||||||
|
// vector; this file spells the group half explicitly where it CREATES the two halves,
|
||||||
|
// the backend keeps the name it has.
|
||||||
|
Vector<MGPipeResourceRecord>& Resources;
|
||||||
|
Vector<MGPipeVertexElementsRecord>& VertexElementsCsos;
|
||||||
|
Vector<MGPipeResourceRecord>& TextureResources;
|
||||||
|
Vector<MGPipeResourceRecord>& RenderbufferResources;
|
||||||
|
Vector<MGPipeSamplerCsoRecord>& SamplerCsos;
|
||||||
|
Vector<MGPipeSamplerViewRecord>& SamplerViewCsos;
|
||||||
|
Vector<MGPipeShaderCsoRecord>& ShaderCsos;
|
||||||
|
Vector<MGPipeShaderCsoRecord>& CompositeShaderCsos;
|
||||||
|
Vector<MGPipeFramebufferRecord>& FramebufferRecords;
|
||||||
|
|
||||||
|
explicit MGPipeApplierState(MGPipeObjectRecords& objects)
|
||||||
|
: Objects(objects), Resources(objects.Resources), VertexElementsCsos(objects.VertexElementsCsos),
|
||||||
|
TextureResources(objects.TextureResources), RenderbufferResources(objects.RenderbufferResources),
|
||||||
|
SamplerCsos(objects.SamplerCsos), SamplerViewCsos(objects.SamplerViewCsos),
|
||||||
|
ShaderCsos(objects.ShaderCsos), CompositeShaderCsos(objects.CompositeShaderCsos),
|
||||||
|
FramebufferRecords(objects.FramebufferRecords) {}
|
||||||
|
|
||||||
// ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than
|
// ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than
|
||||||
// members because the storage moved under them and their callers must not have to know
|
// members because the storage moved under them and their callers must not have to know
|
||||||
// it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members
|
// it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members
|
||||||
@@ -932,9 +997,65 @@ namespace MobileGL::MG_Pipe {
|
|||||||
const MGPFramebufferState* ReadFramebuffer() const;
|
const MGPFramebufferState* ReadFramebuffer() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The monolith's single applier. Under split there is one per served context.
|
// The calling thread's applier. On a thread that belongs to a served session this is that
|
||||||
|
// session's CURRENT CONTEXT's working state bound to its share group's object records; on
|
||||||
|
// every other thread - the client process, the in-process client+server shape, a unit case,
|
||||||
|
// the whole monolith - it is the process-wide applier, which is what it has always been and
|
||||||
|
// is byte-for-byte the old behaviour.
|
||||||
MGPipeApplierState& MGPipeApplier();
|
MGPipeApplierState& MGPipeApplier();
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT?
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// THE SAME SHAPE AS P14 S2's gPipeInputs RESOLVER AND S4's NATIVE TUPLE KEY, one level down:
|
||||||
|
// a process-wide hook the server's session registry installs ONCE, which answers for the
|
||||||
|
// CALLING thread. Nothing in MG_Backend or MG_Impl names a session, and no call site moved.
|
||||||
|
//
|
||||||
|
// THE KEY IS TWO PAIRS, because the two halves have two owners:
|
||||||
|
//
|
||||||
|
// {SessionKey, ContextToken} the WORKING state, one per served context;
|
||||||
|
// {SessionKey, ShareGroupKey} the OBJECT records, one per share group within the session.
|
||||||
|
//
|
||||||
|
// A SHARE GROUP DOES NOT CROSS A SESSION, and this is not a limitation: an EGL share group
|
||||||
|
// is a property of one client's connection (EGL 1.5 3.7.1 - contexts of one share group must
|
||||||
|
// belong to one display), and the two sessions are two connections. Cross-session sharing is
|
||||||
|
// an explicit non-goal of the phase, so keying the first by session is the whole of the rule.
|
||||||
|
//
|
||||||
|
// REGISTRATION, NOT INFERENCE. The session layer registers a context the moment the
|
||||||
|
// CreateContext control frame reaches the apply thread (ServerSession::CreateContext) and
|
||||||
|
// unregisters it at DestroyContext, so the resolver above stays a two-word read of state the
|
||||||
|
// session already keeps (its current token) and never has to reach into the session's map
|
||||||
|
// from a thread that does not own it. A context that was never registered resolves to its
|
||||||
|
// session's "no context" applier, which is the same object the session's bring-up reads.
|
||||||
|
struct MGPipeApplierKey {
|
||||||
|
Uint64 SessionKey = 0;
|
||||||
|
Uint64 ContextToken = 0;
|
||||||
|
};
|
||||||
|
// Answers false for "this thread is not a session's" (the process-wide applier). Installed by
|
||||||
|
// the session registry; null in every process that serves no session.
|
||||||
|
using MGPipeApplierKeyResolver = Bool (*)(MGPipeApplierKey* outKey);
|
||||||
|
void MGPipeSetApplierKeyResolver(MGPipeApplierKeyResolver resolver);
|
||||||
|
|
||||||
|
// Mints (or finds) the working state for {session, context} and the object records of the
|
||||||
|
// share group it belongs to. `shareGroupToken` is the frontend's own share-group identity
|
||||||
|
// (EGLState::CreateContext mints it, a sharing context inherits its parent's); 0 means this
|
||||||
|
// context started its own group, and is keyed by the context token in a namespace of its own
|
||||||
|
// so it cannot alias a group that happens to carry that number.
|
||||||
|
void MGPipeApplierRegisterContext(Uint64 sessionKey, Uint64 contextToken, Uint64 shareGroupToken);
|
||||||
|
// Drops {session, context}'s working state, and its share group's object records when the
|
||||||
|
// last context of that group has gone (MGPipeApplierReleaseObjectRecords' scope, per group
|
||||||
|
// instead of per process).
|
||||||
|
void MGPipeApplierUnregisterContext(Uint64 sessionKey, Uint64 contextToken);
|
||||||
|
// Every context of a served session, at session teardown.
|
||||||
|
void MGPipeApplierReleaseSession(Uint64 sessionKey);
|
||||||
|
|
||||||
|
// The observables that make the keying testable without reading a private map: how many
|
||||||
|
// groups one context's session holds, and how many object records the group the CALLING
|
||||||
|
// thread is in holds. Both are the numbers a case asserts on to tell "isolated" from
|
||||||
|
// "shared" apart from the behaviour they exist to explain.
|
||||||
|
Uint64 MGPipeApplierShareGroupCountForTesting(Uint64 sessionKey);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
// P5e: THE BARRIERED PREDICATE (MG_Remote/CONTRACT-P5E.md §2.1, ruling 3)
|
// P5e: THE BARRIERED PREDICATE (MG_Remote/CONTRACT-P5E.md §2.1, ruling 3)
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -72,8 +72,9 @@
|
|||||||
// that the expansion, the two generated tables and this number agree.
|
// that the expansion, the two generated tables and this number agree.
|
||||||
//
|
//
|
||||||
// class entries group (as the plan tabulates it)
|
// class entries group (as the plan tabulates it)
|
||||||
// kScreen 12 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
// kScreen 13 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
||||||
// appended server-side fence wait 1 and P5c's applier_reset 1
|
// appended server-side fence wait 1 and P5c's applier_reset 1 and
|
||||||
|
// P14 S1's bind_context 1
|
||||||
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
||||||
// kCtxCso 13 CSO create/bind/delete
|
// kCtxCso 13 CSO create/bind/delete
|
||||||
// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state
|
// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
// P5b-appended verbs (bind_shader_image, patch_parameter,
|
// P5b-appended verbs (bind_shader_image, patch_parameter,
|
||||||
// bind_stream_output, set_storage_block_binding,
|
// bind_stream_output, set_storage_block_binding,
|
||||||
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
|
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
|
||||||
// total 80
|
// total 81
|
||||||
//
|
//
|
||||||
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
|
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
|
||||||
// do not add up to a set of UNIQUE records and this file has to hold unique records:
|
// do not add up to a set of UNIQUE records and this file has to hold unique records:
|
||||||
@@ -145,9 +146,16 @@
|
|||||||
// read_pixels_to_buffer and get_texture_image_to_buffer, the pack-buffer halves of the two
|
// read_pixels_to_buffer and get_texture_image_to_buffer, the pack-buffer halves of the two
|
||||||
// readbacks, answered by nothing (ID-57's fire-and-forget). Like P5b's five they reach
|
// readbacks, answered by nothing (ID-57's fire-and-forget). Like P5b's five they reach
|
||||||
// WireVerbSink only and have no monolith producer. 83 unique records.
|
// WireVerbSink only and have no monolith producer. 83 unique records.
|
||||||
|
// - P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended ONE more (opcode 84):
|
||||||
|
// bind_context, the in-band record that carries the client's CURRENT context token. It has
|
||||||
|
// the applier_reset shape - a kScreen control record, no MGPipeApply* entry point, no
|
||||||
|
// monolith producer - because what it moves is session attribution, not context state. The
|
||||||
|
// design's per-record ContextSlot was NOT taken here: the token is announced at the edge
|
||||||
|
// (a context change) instead of stamped on every verb, which costs one record per
|
||||||
|
// make-current and leaves the 71 context-family payloads untouched. 84 unique records.
|
||||||
// ---------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
#define MGP_CALL_LIST_DOCUMENTED_COUNT 83
|
#define MGP_CALL_LIST_DOCUMENTED_COUNT 84
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
#define MGP_CALL_LIST(X) \
|
#define MGP_CALL_LIST(X) \
|
||||||
@@ -355,7 +363,18 @@
|
|||||||
/* ---- has an MGPipeApply* entry point or a monolith producer: monolith reads into its ---- */ \
|
/* ---- has an MGPipeApply* entry point or a monolith producer: monolith reads into its ---- */ \
|
||||||
/* ---- driver PBO inside the backend's own ReadPixels, as before (G1). ---- */ \
|
/* ---- driver PBO inside the backend's own ReadPixels, as before (G1). ---- */ \
|
||||||
X(ReadPixelsToBuffer, MGPReadbackToBuffer, kCtxVerb, kNone, kWaitNone) \
|
X(ReadPixelsToBuffer, MGPReadbackToBuffer, kCtxVerb, kNone, kWaitNone) \
|
||||||
X(GetTextureImageToBuffer, MGPReadbackToBuffer, kCtxObject, kNone, kWaitNone)
|
X(GetTextureImageToBuffer, MGPReadbackToBuffer, kCtxObject, kNone, kWaitNone) \
|
||||||
|
/* ---- APPENDED BY P14 S1 (docs/Disaggregated/design/11-state-ownership.md), opcode 84: ---- */ \
|
||||||
|
/* ---- the in-band context binding. THE WHOLE RECORD IS ONE Uint64: the client's current ---- */ \
|
||||||
|
/* ---- context token, 0 meaning "no context" (a release). It is emitted at the make-current ---- */ \
|
||||||
|
/* ---- edge, ahead of the block's client-side resets and ahead of applier_reset, so the ---- */ \
|
||||||
|
/* ---- server attributes every record that follows to the context that produced it - the ---- */ \
|
||||||
|
/* ---- multi-context shape the final design reaches without stamping a slot on all 71 ---- */ \
|
||||||
|
/* ---- context-family rows. kScreen like applier_reset: it is a whole-session edge, not a ---- */ \
|
||||||
|
/* ---- context-family verb, and it has no MGPipeApply* entry point and no monolith producer. ---- */ \
|
||||||
|
/* ---- kWaitNone: the apply reads no client memory and the record's placement in the ring ---- */ \
|
||||||
|
/* ---- is the whole of its ordering. ---- */ \
|
||||||
|
X(BindContext, MGPBindContext, kScreen, kNone, kWaitNone)
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
||||||
|
|||||||
@@ -297,6 +297,10 @@
|
|||||||
#define MGP_FIELDS_MGPApplierReset(F) \
|
#define MGP_FIELDS_MGPApplierReset(F) \
|
||||||
F(ContextSerial)
|
F(ContextSerial)
|
||||||
|
|
||||||
|
// P14 S1: bind_context's POD, one row for its one member.
|
||||||
|
#define MGP_FIELDS_MGPBindContext(F) \
|
||||||
|
F(ClientContextToken)
|
||||||
|
|
||||||
// P5c rv (CONTRACT-P5C.md §5.3): set_context_values' POD, one row per member. The 15-entry
|
// P5c rv (CONTRACT-P5C.md §5.3): set_context_values' POD, one row per member. The 15-entry
|
||||||
// array is one field, exactly as MGPFramebufferState's Color[8] is one.
|
// array is one field, exactly as MGPFramebufferState's Color[8] is one.
|
||||||
#define MGP_FIELDS_MGPContextValues(F) \
|
#define MGP_FIELDS_MGPContextValues(F) \
|
||||||
@@ -423,7 +427,7 @@
|
|||||||
P(MGPSurfaceInfo) \
|
P(MGPSurfaceInfo) \
|
||||||
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
|
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
|
||||||
P(MGPCopyFromFramebuffer) \
|
P(MGPCopyFromFramebuffer) \
|
||||||
P(MGPApplierReset) P(MGPContextValues) \
|
P(MGPApplierReset) P(MGPBindContext) P(MGPContextValues) \
|
||||||
P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \
|
P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \
|
||||||
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
||||||
P(StencilFaceState) \
|
P(StencilFaceState) \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||||
|
|
||||||
// share group: 12 calls. A null entry means the backend does not implement this
|
// share group: 13 calls. A null entry means the backend does not implement this
|
||||||
// call and the frontend keeps its own path (plan B section 4.1).
|
// call and the frontend keeps its own path (plan B section 4.1).
|
||||||
struct MGPipeScreen {
|
struct MGPipeScreen {
|
||||||
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
|
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
|
||||||
@@ -27,6 +27,7 @@ struct MGPipeScreen {
|
|||||||
void (*FenceDestroy)(const MGPHandleOnly* payload);
|
void (*FenceDestroy)(const MGPHandleOnly* payload);
|
||||||
void (*FenceWaitServer)(const MGPFenceWait* payload);
|
void (*FenceWaitServer)(const MGPFenceWait* payload);
|
||||||
void (*ApplierReset)(const MGPApplierReset* payload);
|
void (*ApplierReset)(const MGPApplierReset* payload);
|
||||||
|
void (*BindContext)(const MGPBindContext* payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
// context: 71 calls. A null entry means the backend does not implement this
|
// context: 71 calls. A null entry means the backend does not implement this
|
||||||
@@ -105,9 +106,9 @@ struct MGPipeContext {
|
|||||||
void (*GetTextureImageToBuffer)(const MGPReadbackToBuffer* payload);
|
void (*GetTextureImageToBuffer)(const MGPReadbackToBuffer* payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr SizeT kMGPipeScreenCallCount = 12;
|
inline constexpr SizeT kMGPipeScreenCallCount = 13;
|
||||||
inline constexpr SizeT kMGPipeContextCallCount = 71;
|
inline constexpr SizeT kMGPipeContextCallCount = 71;
|
||||||
inline constexpr SizeT kMGPipeCallCount = 83;
|
inline constexpr SizeT kMGPipeCallCount = 84;
|
||||||
|
|
||||||
// A table that is not exactly its call count of function pointers has grown a
|
// A table that is not exactly its call count of function pointers has grown a
|
||||||
// member that no generator knows about.
|
// member that no generator knows about.
|
||||||
|
|||||||
@@ -348,3 +348,7 @@ inline void MGP_ReadPixelsToBuffer(const MGPReadbackToBuffer* payload) {
|
|||||||
inline void MGP_GetTextureImageToBuffer(const MGPReadbackToBuffer* payload) {
|
inline void MGP_GetTextureImageToBuffer(const MGPReadbackToBuffer* payload) {
|
||||||
gMGPipeContext.GetTextureImageToBuffer(payload);
|
gMGPipeContext.GetTextureImageToBuffer(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void MGP_BindContext(const MGPBindContext* payload) {
|
||||||
|
gMGPipeScreen.BindContext(payload);
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ inline Bool MGPipeVerify(const MGPStreamOutputBind& a, const MGPStreamOutputBind
|
|||||||
inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
|
||||||
|
inline Bool MGPipeVerify(const MGPBindContext& a, const MGPBindContext& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField);
|
||||||
@@ -269,6 +270,8 @@ struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {};
|
|||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
|
struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
|
struct MGPipeHasFieldVerifier<MGPBindContext> : std::true_type {};
|
||||||
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {};
|
struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {};
|
struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {};
|
||||||
@@ -690,6 +693,11 @@ inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, con
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Bool MGPipeVerify(const MGPBindContext& a, const MGPBindContext& b, const char** outField) {
|
||||||
|
MGP_FIELDS_MGPBindContext(MGP_VERIFY_FIELD)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField) {
|
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField) {
|
||||||
MGP_FIELDS_MGPContextValues(MGP_VERIFY_FIELD)
|
MGP_FIELDS_MGPContextValues(MGP_VERIFY_FIELD)
|
||||||
return true;
|
return true;
|
||||||
@@ -757,4 +765,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
|
|||||||
|
|
||||||
#undef MGP_VERIFY_FIELD
|
#undef MGP_VERIFY_FIELD
|
||||||
|
|
||||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 83;
|
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 84;
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ enum class MGPWireOp : Uint16 {
|
|||||||
DeleteStreamOutput = 81,
|
DeleteStreamOutput = 81,
|
||||||
ReadPixelsToBuffer = 82,
|
ReadPixelsToBuffer = 82,
|
||||||
GetTextureImageToBuffer = 83,
|
GetTextureImageToBuffer = 83,
|
||||||
kOpCount = 84,
|
BindContext = 84,
|
||||||
|
kOpCount = 85,
|
||||||
};
|
};
|
||||||
|
|
||||||
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
|
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
|
||||||
@@ -258,6 +259,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
|
|||||||
/* 81 DeleteStreamOutput */ static_cast<Uint32>(kNone),
|
/* 81 DeleteStreamOutput */ static_cast<Uint32>(kNone),
|
||||||
/* 82 ReadPixelsToBuffer */ static_cast<Uint32>(kNone),
|
/* 82 ReadPixelsToBuffer */ static_cast<Uint32>(kNone),
|
||||||
/* 83 GetTextureImageToBuffer */ static_cast<Uint32>(kNone),
|
/* 83 GetTextureImageToBuffer */ static_cast<Uint32>(kNone),
|
||||||
|
/* 84 BindContext */ static_cast<Uint32>(kNone),
|
||||||
};
|
};
|
||||||
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
|
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
|
||||||
static_cast<SizeT>(MGPWireOp::kOpCount),
|
static_cast<SizeT>(MGPWireOp::kOpCount),
|
||||||
@@ -361,6 +363,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount
|
|||||||
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kCtxVerb),
|
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kCtxVerb),
|
||||||
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kCtxVerb),
|
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kCtxVerb),
|
||||||
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kCtxObject),
|
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kCtxObject),
|
||||||
|
/* 84 BindContext */ static_cast<Uint8>(kScreen),
|
||||||
};
|
};
|
||||||
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
|
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
|
||||||
static_cast<SizeT>(MGPWireOp::kOpCount),
|
static_cast<SizeT>(MGPWireOp::kOpCount),
|
||||||
@@ -468,6 +471,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
|
|||||||
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kWaitNone),
|
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kWaitNone),
|
||||||
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kWaitNone),
|
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kWaitNone),
|
||||||
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kWaitNone),
|
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kWaitNone),
|
||||||
|
/* 84 BindContext */ static_cast<Uint8>(kWaitNone),
|
||||||
};
|
};
|
||||||
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
|
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
|
||||||
static_cast<SizeT>(MGPWireOp::kOpCount),
|
static_cast<SizeT>(MGPWireOp::kOpCount),
|
||||||
@@ -1198,6 +1202,14 @@ static_assert(sizeof(MGPWireRec_GetTextureImageToBuffer) ==
|
|||||||
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackToBuffer) + 7u) & ~SizeT(7u)),
|
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackToBuffer) + 7u) & ~SizeT(7u)),
|
||||||
"MGPWireRec_GetTextureImageToBuffer gained padding; the wire format moved");
|
"MGPWireRec_GetTextureImageToBuffer gained padding; the wire format moved");
|
||||||
|
|
||||||
|
struct alignas(8) MGPWireRec_BindContext {
|
||||||
|
MGPWireRecHeader Header;
|
||||||
|
MGPBindContext Payload;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(MGPWireRec_BindContext) ==
|
||||||
|
((sizeof(MGPWireRecHeader) + sizeof(MGPBindContext) + 7u) & ~SizeT(7u)),
|
||||||
|
"MGPWireRec_BindContext gained padding; the wire format moved");
|
||||||
|
|
||||||
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
|
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
|
||||||
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
|
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
|
||||||
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
|
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
|
||||||
@@ -1496,6 +1508,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
|
|||||||
case MGPWireOp::GetTextureImageToBuffer:
|
case MGPWireOp::GetTextureImageToBuffer:
|
||||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImageToBuffer, "GetTextureImageToBuffer");
|
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImageToBuffer, "GetTextureImageToBuffer");
|
||||||
break;
|
break;
|
||||||
|
case MGPWireOp::BindContext:
|
||||||
|
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindContext, "BindContext");
|
||||||
|
break;
|
||||||
case MGPWireOp::kInvalid:
|
case MGPWireOp::kInvalid:
|
||||||
case MGPWireOp::kOpCount:
|
case MGPWireOp::kOpCount:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -431,6 +431,8 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
|
|||||||
{"MGPCopyFromFramebuffer.SubImage", offsetof(MGPCopyFromFramebuffer, SubImage), sizeof(((MGPCopyFromFramebuffer*)nullptr)->SubImage)},
|
{"MGPCopyFromFramebuffer.SubImage", offsetof(MGPCopyFromFramebuffer, SubImage), sizeof(((MGPCopyFromFramebuffer*)nullptr)->SubImage)},
|
||||||
{"MGPApplierReset", 0, sizeof(MGPApplierReset)},
|
{"MGPApplierReset", 0, sizeof(MGPApplierReset)},
|
||||||
{"MGPApplierReset.ContextSerial", offsetof(MGPApplierReset, ContextSerial), sizeof(((MGPApplierReset*)nullptr)->ContextSerial)},
|
{"MGPApplierReset.ContextSerial", offsetof(MGPApplierReset, ContextSerial), sizeof(((MGPApplierReset*)nullptr)->ContextSerial)},
|
||||||
|
{"MGPBindContext", 0, sizeof(MGPBindContext)},
|
||||||
|
{"MGPBindContext.ClientContextToken", offsetof(MGPBindContext, ClientContextToken), sizeof(((MGPBindContext*)nullptr)->ClientContextToken)},
|
||||||
{"MGPContextValues", 0, sizeof(MGPContextValues)},
|
{"MGPContextValues", 0, sizeof(MGPContextValues)},
|
||||||
{"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)},
|
{"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)},
|
||||||
{"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)},
|
{"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)},
|
||||||
@@ -762,4 +764,5 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
|
|||||||
{"DeleteStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::DeleteStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
{"DeleteStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::DeleteStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
||||||
{"ReadPixelsToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
{"ReadPixelsToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
||||||
{"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
{"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
||||||
|
{"BindContext", "MGPBindContext", static_cast<Uint64>(MGPWireOp::BindContext), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1120,6 +1120,52 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
Uint64 g_applierResetContextSerial = 0;
|
Uint64 g_applierResetContextSerial = 0;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// P14 S1: the EGL context lifecycle's two frames. NO SESSION IS AN ANSWER, not a Fatal:
|
||||||
|
// eglCreateContext/eglDestroyContext can legally run before ClientSession::Start (the
|
||||||
|
// bring-up window) and on the server role's own thread, and in both there is no wire for the
|
||||||
|
// frame to cross - the caller falls back to what it did before this slice, which is to keep
|
||||||
|
// the client's own EGL books and nothing else.
|
||||||
|
namespace {
|
||||||
|
// The window test both senders below open with, and the reason it is a function rather
|
||||||
|
// than two copies of the same if: a sender that answered Refused for "no wire" would make
|
||||||
|
// eglCreateContext return EGL_NO_CONTEXT during the bring-up, which is the exact failure
|
||||||
|
// the pre-Start exception exists to prevent.
|
||||||
|
Bool ContextFramesCanCross() {
|
||||||
|
ClientSession* session = ClientSession::Active();
|
||||||
|
return session != nullptr && session->Started() &&
|
||||||
|
!g_clientTablesUninstalled.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ContextFrameEmit SendCreateContextFrame(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags) {
|
||||||
|
if (!ContextFramesCanCross()) return ContextFrameEmit::NoWire;
|
||||||
|
return Server::ServerCreateEGLContext(clientContextToken, shareGroupToken, flags) ? ContextFrameEmit::Sent
|
||||||
|
: ContextFrameEmit::Refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextFrameEmit SendDestroyContextFrame(Uint64 clientContextToken) {
|
||||||
|
if (!ContextFramesCanCross()) return ContextFrameEmit::NoWire;
|
||||||
|
return Server::ServerDestroyEGLContext(clientContextToken) ? ContextFrameEmit::Sent
|
||||||
|
: ContextFrameEmit::Refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P14 S1: bind_context. The same four windows as EmitApplierResetRecord below answer false,
|
||||||
|
// and for the same reason: a binding that cannot cross leaves the session on the default it
|
||||||
|
// already has (one context per session, every record its own), which is what the server
|
||||||
|
// answers with no record at all. The token is minted by EGLState and passed in rather than
|
||||||
|
// read here, so this table holds no second opinion about which context is current.
|
||||||
|
Bool EmitBindContextRecord(Uint64 clientContextToken) {
|
||||||
|
ClientSession* session = ClientSession::Active();
|
||||||
|
if (session == nullptr || !session->Started()) return false;
|
||||||
|
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) return false;
|
||||||
|
MG_Pipe::MGPBindContext record{};
|
||||||
|
record.ClientContextToken = clientContextToken;
|
||||||
|
session->EmitAndWait(MGPWireOp::BindContext, &record, sizeof(record), nullptr, 0, nullptr, 0,
|
||||||
|
nullptr);
|
||||||
|
++g_emitted;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool EmitApplierResetRecord() {
|
Bool EmitApplierResetRecord() {
|
||||||
// NO SESSION, NO RECORD - and false rather than the RequireSession Fatal, because a
|
// NO SESSION, NO RECORD - and false rather than the RequireSession Fatal, because a
|
||||||
// validate can legitimately prime with a transport CONFIGURED but no live session:
|
// validate can legitimately prime with a transport CONFIGURED but no live session:
|
||||||
|
|||||||
@@ -125,6 +125,43 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// caller then makes the direct call the record replaced.
|
// caller then makes the direct call the record replaced.
|
||||||
Bool EmitApplierResetRecord();
|
Bool EmitApplierResetRecord();
|
||||||
|
|
||||||
|
// ---- P14 S1: the EGL context lifecycle's control-plane half -----------------------------
|
||||||
|
//
|
||||||
|
// NOT ring records and NOT routed rows: eglCreateContext/eglDestroyContext are the EGL
|
||||||
|
// lifecycle, which the catalogue deliberately keeps off the record plane (PipeCalls.def's
|
||||||
|
// "the eight EGL lifecycle entry points stay virtual functions on pActiveBackendObject"),
|
||||||
|
// so they cross as two SurfaceOp kinds through the same blocking frame channel the nine
|
||||||
|
// surface forwarders use. Called by MG_Impl's EGLImpl on the app thread, in the same order
|
||||||
|
// as the EGL calls themselves - which is the ordering the server's context table needs.
|
||||||
|
//
|
||||||
|
// THREE ANSWERS, NOT TWO, for the reason ObjectDeathEmit below has three: "the server said
|
||||||
|
// no" and "there is no wire for the question to cross" are different facts and the caller's
|
||||||
|
// answer to each is different. NoWire is the bring-up window before ClientSession::Start and
|
||||||
|
// the server role's own thread - in both, the pre-S1 shape (the client keeps its own EGL
|
||||||
|
// books and no server table exists) is the correct one and the context must be KEPT; Refused
|
||||||
|
// is a server that answered, and eglCreateContext turns it into EGL_NO_CONTEXT, because a
|
||||||
|
// context whose token the server did not table is one the first bind_context after it would
|
||||||
|
// be refused for.
|
||||||
|
enum class ContextFrameEmit : Uint8 { Sent, NoWire, Refused };
|
||||||
|
ContextFrameEmit SendCreateContextFrame(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags);
|
||||||
|
// A destroy has the same three answers; the client's EGL book is dropped either way (that is
|
||||||
|
// the caller's own state machine), and what a refusal changes is only what the server holds.
|
||||||
|
ContextFrameEmit SendDestroyContextFrame(Uint64 clientContextToken);
|
||||||
|
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
|
||||||
|
//
|
||||||
|
// Emitted on the GL thread from EGLImpl::MakeCurrent, after the switch has succeeded and
|
||||||
|
// before eglMakeCurrent returns - which puts it ahead of the applier_reset the NEXT GL call's
|
||||||
|
// validate emits (PipeFill.cpp's FreshlyPrimed arm), so the reset is already attributed to
|
||||||
|
// the context that primed. `clientContextToken` is the token
|
||||||
|
// EGLState minted for the context the calling thread now has current (0 = none, the release
|
||||||
|
// edge) - the caller reads it rather than this table owning a second copy, so the two planes
|
||||||
|
// cannot disagree about which context is bound. Returns false - nothing emitted - in exactly
|
||||||
|
// the windows EmitApplierResetRecord documents (no live session, the pre-Start bring-up, a
|
||||||
|
// server-role-only fixture, teardown), where there is no wire for the binding to cross and
|
||||||
|
// the session's single-context default is what the server already answers.
|
||||||
|
Bool EmitBindContextRecord(Uint64 clientContextToken);
|
||||||
|
|
||||||
// P5c (rv), CONTRACT-P5C.md §5.3: whether set_context_values can cross right now (a live,
|
// P5c (rv), CONTRACT-P5C.md §5.3: whether set_context_values can cross right now (a live,
|
||||||
// started session, tables not being torn down). PipeFill.cpp gates the record's emission
|
// started session, tables not being torn down). PipeFill.cpp gates the record's emission
|
||||||
// AND the residual-fill skip for its eight fields on this one answer, so the two halves
|
// AND the residual-fill skip for its eight fields on this one answer, so the two halves
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <flatbuffers/flatbuffers.h>
|
#include <flatbuffers/flatbuffers.h>
|
||||||
|
|
||||||
@@ -96,37 +97,92 @@ namespace MobileGL::MG_Remote {
|
|||||||
// PH-1 (3): the latch's state. Armed once, by the session child; latched once, by the
|
// PH-1 (3): the latch's state. Armed once, by the session child; latched once, by the
|
||||||
// first fault. The line is written under the mutex BEFORE the release store that
|
// first fault. The line is written under the mutex BEFORE the release store that
|
||||||
// publishes `g_latched`, so a reader that saw true reads a whole line.
|
// publishes `g_latched`, so a reader that saw true reads a whole line.
|
||||||
std::atomic<bool> g_latchArmed{false};
|
//
|
||||||
std::atomic<bool> g_latched{false};
|
// P14 S2: ONE OF THESE PER SESSION (FatalFunnel.h's domain block). A domain's storage is
|
||||||
std::atomic<std::uint64_t> g_latchCount{0};
|
// heap-allocated by FatalLatchDomainOpen and lives until FatalLatchDomainClose; domain 0
|
||||||
std::mutex g_latchMutex;
|
// is this process-wide one, which is what every thread that binds nothing uses.
|
||||||
MGFatalFamily g_latchedFamily = MGFatalFamily::ProtocolCorruption;
|
struct LatchState {
|
||||||
char g_latchedLine[512] = {};
|
int id = 0;
|
||||||
|
std::atomic<bool> armed{false};
|
||||||
|
std::atomic<bool> latched{false};
|
||||||
|
std::atomic<std::uint64_t> count{0};
|
||||||
|
std::mutex mutex;
|
||||||
|
MGFatalFamily latchedFamily = MGFatalFamily::ProtocolCorruption;
|
||||||
|
char latchedLine[512] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
LatchState g_defaultLatch;
|
||||||
|
|
||||||
|
thread_local LatchState* t_latch = &g_defaultLatch;
|
||||||
|
|
||||||
|
std::mutex g_domainsMutex;
|
||||||
|
std::unordered_map<int, LatchState*> g_domains;
|
||||||
|
int g_nextDomain = 1;
|
||||||
|
|
||||||
|
LatchState& Latch() { return *t_latch; }
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ArmSessionLatch() { g_latchArmed.store(true, std::memory_order_release); }
|
int FatalLatchDomainOpen() {
|
||||||
|
const std::lock_guard<std::mutex> lock(g_domainsMutex);
|
||||||
|
if (g_nextDomain <= 0 || g_nextDomain > kMaxLatchDomains) return 0;
|
||||||
|
LatchState* state = new LatchState{};
|
||||||
|
state->id = g_nextDomain++;
|
||||||
|
g_domains.emplace(state->id, state);
|
||||||
|
return state->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FatalLatchDomainClose(int domain) {
|
||||||
|
if (domain == 0) return;
|
||||||
|
LatchState* state = nullptr;
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_domainsMutex);
|
||||||
|
const auto found = g_domains.find(domain);
|
||||||
|
if (found == g_domains.end()) return;
|
||||||
|
state = found->second;
|
||||||
|
g_domains.erase(found);
|
||||||
|
}
|
||||||
|
delete state;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FatalLatchDomainBind(int domain) {
|
||||||
|
LatchState* const previous = t_latch;
|
||||||
|
if (domain == 0) {
|
||||||
|
t_latch = &g_defaultLatch;
|
||||||
|
return previous->id;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_domainsMutex);
|
||||||
|
const auto found = g_domains.find(domain);
|
||||||
|
t_latch = found == g_domains.end() ? &g_defaultLatch : found->second;
|
||||||
|
}
|
||||||
|
return previous->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArmSessionLatch() { Latch().armed.store(true, std::memory_order_release); }
|
||||||
|
|
||||||
void ResetSessionLatch() {
|
void ResetSessionLatch() {
|
||||||
const std::lock_guard<std::mutex> lock(g_latchMutex);
|
LatchState& latch = Latch();
|
||||||
g_latchArmed.store(false, std::memory_order_release);
|
const std::lock_guard<std::mutex> lock(latch.mutex);
|
||||||
g_latchCount.store(0, std::memory_order_relaxed);
|
latch.armed.store(false, std::memory_order_release);
|
||||||
g_latchedFamily = MGFatalFamily::ProtocolCorruption;
|
latch.count.store(0, std::memory_order_relaxed);
|
||||||
g_latchedLine[0] = '\0';
|
latch.latchedFamily = MGFatalFamily::ProtocolCorruption;
|
||||||
g_latched.store(false, std::memory_order_release);
|
latch.latchedLine[0] = '\0';
|
||||||
|
latch.latched.store(false, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SessionLatchArmed() { return g_latchArmed.load(std::memory_order_acquire); }
|
bool SessionLatchArmed() { return Latch().armed.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
bool SessionLatched() { return g_latched.load(std::memory_order_acquire); }
|
bool SessionLatched() { return Latch().latched.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
MGFatalFamily SessionLatchedFamily() {
|
MGFatalFamily SessionLatchedFamily() {
|
||||||
const std::lock_guard<std::mutex> lock(g_latchMutex);
|
LatchState& latch = Latch();
|
||||||
return g_latchedFamily;
|
const std::lock_guard<std::mutex> lock(latch.mutex);
|
||||||
|
return latch.latchedFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* SessionLatchedLine() { return g_latchedLine; }
|
const char* SessionLatchedLine() { return Latch().latchedLine; }
|
||||||
|
|
||||||
std::uint64_t SessionLatchCount() { return g_latchCount.load(std::memory_order_relaxed); }
|
std::uint64_t SessionLatchCount() { return Latch().count.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
bool SessionLatch(MGFatalFamily family, const char* fmt, ...) {
|
bool SessionLatch(MGFatalFamily family, const char* fmt, ...) {
|
||||||
char line[512];
|
char line[512];
|
||||||
@@ -135,23 +191,25 @@ namespace MobileGL::MG_Remote {
|
|||||||
FormatFaultLine(line, fmt, args);
|
FormatFaultLine(line, fmt, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
|
||||||
|
LatchState& latch = Latch();
|
||||||
|
|
||||||
// UNARMED IS SessionFail, byte for byte: the inproc server, the client and every unit
|
// UNARMED IS SessionFail, byte for byte: the inproc server, the client and every unit
|
||||||
// death test keep the death they had (FatalFunnel.h's block says why inproc must).
|
// death test keep the death they had (FatalFunnel.h's block says why inproc must).
|
||||||
if (!g_latchArmed.load(std::memory_order_acquire)) {
|
if (!latch.armed.load(std::memory_order_acquire)) {
|
||||||
SessionFail(family, "%s", line);
|
SessionFail(family, "%s", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
|
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
g_latchCount.fetch_add(1, std::memory_order_relaxed);
|
latch.count.fetch_add(1, std::memory_order_relaxed);
|
||||||
LogFaultLine(line);
|
LogFaultLine(line);
|
||||||
|
|
||||||
bool first = false;
|
bool first = false;
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(g_latchMutex);
|
const std::lock_guard<std::mutex> lock(latch.mutex);
|
||||||
if (!g_latched.load(std::memory_order_relaxed)) {
|
if (!latch.latched.load(std::memory_order_relaxed)) {
|
||||||
std::snprintf(g_latchedLine, sizeof(g_latchedLine), "%s", line);
|
std::snprintf(latch.latchedLine, sizeof(latch.latchedLine), "%s", line);
|
||||||
g_latchedFamily = family;
|
latch.latchedFamily = family;
|
||||||
g_latched.store(true, std::memory_order_release);
|
latch.latched.store(true, std::memory_order_release);
|
||||||
first = true;
|
first = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ namespace MobileGL::MG_Remote {
|
|||||||
// crash (ID-P7-1: "下一连接照常服务" is the supervisor's, not the latch's).
|
// crash (ID-P7-1: "下一连接照常服务" is the supervisor's, not the latch's).
|
||||||
//
|
//
|
||||||
// WHERE IT IS ARMED, AND WHY ONLY THERE. ArmSessionLatch() is called by exactly one site:
|
// WHERE IT IS ARMED, AND WHY ONLY THERE. ArmSessionLatch() is called by exactly one site:
|
||||||
// ServerMain::RunSession, i.e. the spawn / TCP session child (with or without --serve), whose
|
// ServerMain::RunSession, i.e. a served session - the spawn / TCP session child (with or
|
||||||
// process IS the session. Unarmed, SessionLatch IS SessionFail - same line, same abort - and
|
// without --serve), whose process IS the session, and (P14 S2) each session the in-process
|
||||||
|
// display server runs, which arms ITS OWN latch domain so a fault there ends that session and
|
||||||
|
// no other. Unarmed, SessionLatch IS SessionFail - same line, same abort - and
|
||||||
// that is the answer everywhere else, deliberately:
|
// that is the answer everywhere else, deliberately:
|
||||||
// * INPROC keeps Fatal. CONTRACT-P5's R-2 arms (rules A/B/C, the four honesty arms) are
|
// * INPROC keeps Fatal. CONTRACT-P5's R-2 arms (rules A/B/C, the four honesty arms) are
|
||||||
// Fatal{ProtocolCorruption} in the formal wording, and under inproc the client and the
|
// Fatal{ProtocolCorruption} in the formal wording, and under inproc the client and the
|
||||||
@@ -85,6 +87,10 @@ namespace MobileGL::MG_Remote {
|
|||||||
// Between sessions the latch is unarmed, so a fault there is a SessionFail - which in the
|
// Between sessions the latch is unarmed, so a fault there is a SessionFail - which in the
|
||||||
// in-process shape takes the display Activity's process down with it: the crash isolation a
|
// in-process shape takes the display Activity's process down with it: the crash isolation a
|
||||||
// forked session child gave is what running in-process gives up (documented, D5).
|
// forked session child gave is what running in-process gives up (documented, D5).
|
||||||
|
//
|
||||||
|
// P14 S2: it now clears the CALLING THREAD'S DOMAIN (below), which between sessions is the
|
||||||
|
// process default - a session that opened its own domain cannot leave a latch behind for the
|
||||||
|
// next one to inherit, because the next one does not read that domain at all.
|
||||||
void ResetSessionLatch();
|
void ResetSessionLatch();
|
||||||
// True once the first fault has latched. One acquire load: DrainRing asks it before every pop.
|
// True once the first fault has latched. One acquire load: DrainRing asks it before every pop.
|
||||||
bool SessionLatched();
|
bool SessionLatched();
|
||||||
@@ -95,6 +101,31 @@ namespace MobileGL::MG_Remote {
|
|||||||
// Every fault this armed process latched or declined after the first (>= 1 once latched).
|
// Every fault this armed process latched or declined after the first (>= 1 once latched).
|
||||||
::std::uint64_t SessionLatchCount();
|
::std::uint64_t SessionLatchCount();
|
||||||
|
|
||||||
|
// ---- P14 S2: ONE LATCH DOMAIN PER SERVED SESSION ------------------------------------
|
||||||
|
//
|
||||||
|
// THE LATCH IS PER SESSION NOW, and that is a correctness requirement of serving several in
|
||||||
|
// one process, not a tidy-up. Its three readers sit on a session's own threads - the apply
|
||||||
|
// loop's park, DrainRing's per-pop check, ServerMain's control loop - so a latch raised by
|
||||||
|
// session A used to stop session B's apply thread mid-ring and end B's control loop: one
|
||||||
|
// peer's bad bytes took down its neighbours. ResetSessionLatch had the mirror problem: the
|
||||||
|
// reap of a finished session cleared the latch a live one was standing on.
|
||||||
|
//
|
||||||
|
// A DOMAIN is the latch state, and a THREAD IS BOUND TO ONE. SessionRuntime opens a domain per
|
||||||
|
// served session and ThreadSessionScope binds both of that session's threads to it. A thread
|
||||||
|
// that has bound nothing uses DOMAIN 0, the process default - which is exactly the old
|
||||||
|
// behaviour, and is what the client, the inproc one-process shape and every unit case get.
|
||||||
|
//
|
||||||
|
// 0 IS RESERVED FOR THE PROCESS DEFAULT and FatalLatchDomainOpen never returns it (0 on
|
||||||
|
// exhaustion, which degrades to "shares the default" rather than to a crash).
|
||||||
|
inline constexpr int kMaxLatchDomains = 64;
|
||||||
|
int FatalLatchDomainOpen();
|
||||||
|
// The domain's state goes; no thread may still be bound to it (the session's apply thread is
|
||||||
|
// joined and its control thread's scope has been destroyed by then).
|
||||||
|
void FatalLatchDomainClose(int domain);
|
||||||
|
// Binds the CALLING THREAD to `domain`, returning the domain it was bound to before so a
|
||||||
|
// scope can put it back.
|
||||||
|
int FatalLatchDomainBind(int domain);
|
||||||
|
|
||||||
// The session child's exit status when its session ended on a latched fault. Distinct from
|
// The session child's exit status when its session ended on a latched fault. Distinct from
|
||||||
// RunSession's other codes (64-74) so the supervisor's reap line names it; nonzero, so it is
|
// RunSession's other codes (64-74) so the supervisor's reap line names it; nonzero, so it is
|
||||||
// counted in `sessionsFaulted` like every other fault.
|
// counted in `sessionsFaulted` like every other fault.
|
||||||
|
|||||||
@@ -51,10 +51,20 @@ namespace MobileGL::MG_Remote {
|
|||||||
// cp's one schema addition, on the same append-only terms.
|
// cp's one schema addition, on the same append-only terms.
|
||||||
static_assert(static_cast<Uint8>(SurfaceControlOp::InitCapabilities) ==
|
static_assert(static_cast<Uint8>(SurfaceControlOp::InitCapabilities) ==
|
||||||
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities));
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities));
|
||||||
|
// P14 S1's two appends, on the same append-only terms. The VALUES are pinned because they
|
||||||
|
// are wire ABI, and pinned SEPARATELY from the identity cast beside them: the cast would
|
||||||
|
// stay green if both enums moved together to 20/21, and a peer built before the move
|
||||||
|
// would then read the two ops as two others.
|
||||||
|
static_assert(static_cast<Uint8>(SurfaceControlOp::CreateContext) ==
|
||||||
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateContext));
|
||||||
|
static_assert(static_cast<Uint8>(SurfaceControlOp::DestroyContext) ==
|
||||||
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::DestroyContext));
|
||||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 &&
|
static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 &&
|
||||||
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 &&
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 &&
|
||||||
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10 &&
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10 &&
|
||||||
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities) == 11);
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitCapabilities) == 11 &&
|
||||||
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateContext) == 12 &&
|
||||||
|
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::DestroyContext) == 13);
|
||||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer) == 6);
|
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer) == 6);
|
||||||
// P12's append, on the same terms: the value is wire ABI.
|
// P12's append, on the same terms: the value is wire ABI.
|
||||||
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::ServerOwned) == 7);
|
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::ServerOwned) == 7);
|
||||||
@@ -112,7 +122,7 @@ namespace MobileGL::MG_Remote {
|
|||||||
// Fatal{ProtocolCorruption} at ServerApplyWireSurfaceOp rather than as
|
// Fatal{ProtocolCorruption} at ServerApplyWireSurfaceOp rather than as
|
||||||
// the missing row it is.
|
// the missing row it is.
|
||||||
if (::flatbuffers::IsOutRange(kind, ::MobileGL::Wire::SurfaceOpKind::InitializeDisplay,
|
if (::flatbuffers::IsOutRange(kind, ::MobileGL::Wire::SurfaceOpKind::InitializeDisplay,
|
||||||
::MobileGL::Wire::SurfaceOpKind::InitCapabilities)) {
|
::MobileGL::Wire::SurfaceOpKind::DestroyContext)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const SurfaceControlOp op = static_cast<SurfaceControlOp>(static_cast<Uint8>(kind));
|
const SurfaceControlOp op = static_cast<SurfaceControlOp>(static_cast<Uint8>(kind));
|
||||||
@@ -174,7 +184,8 @@ namespace MobileGL::MG_Remote {
|
|||||||
}
|
}
|
||||||
const auto op = ::MobileGL::Wire::CreateSurfaceOp(
|
const auto op = ::MobileGL::Wire::CreateSurfaceOp(
|
||||||
*builder, frame.seq, kind, frame.display, frame.surface, windowKind, nativeToken,
|
*builder, frame.seq, kind, frame.display, frame.surface, windowKind, nativeToken,
|
||||||
frame.width, frame.height, frame.swapInterval, frame.readSurface, frame.context);
|
frame.width, frame.height, frame.swapInterval, frame.readSurface, frame.context,
|
||||||
|
frame.shareGroupToken, frame.contextFlags);
|
||||||
const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
|
const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
|
||||||
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope);
|
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope);
|
||||||
return SurfaceWireError::None;
|
return SurfaceWireError::None;
|
||||||
@@ -195,6 +206,8 @@ namespace MobileGL::MG_Remote {
|
|||||||
decoded.width = op.width();
|
decoded.width = op.width();
|
||||||
decoded.height = op.height();
|
decoded.height = op.height();
|
||||||
decoded.swapInterval = op.swapInterval();
|
decoded.swapInterval = op.swapInterval();
|
||||||
|
decoded.shareGroupToken = op.shareGroupToken();
|
||||||
|
decoded.contextFlags = op.contextFlags();
|
||||||
if (OpNeedsAWindowBackend(kind)) {
|
if (OpNeedsAWindowBackend(kind)) {
|
||||||
if (op.windowKind() == ::MobileGL::Wire::WindowKind::ServerOwned) {
|
if (op.windowKind() == ::MobileGL::Wire::WindowKind::ServerOwned) {
|
||||||
// P12 (D2). Asked BEFORE the Android refusal below, which it is not: the window is
|
// P12 (D2). Asked BEFORE the Android refusal below, which it is not: the window is
|
||||||
|
|||||||
@@ -285,11 +285,13 @@ enum class SurfaceOpKind : uint8_t {
|
|||||||
ReleaseResources = 9,
|
ReleaseResources = 9,
|
||||||
SetWindowHandle = 10,
|
SetWindowHandle = 10,
|
||||||
InitCapabilities = 11,
|
InitCapabilities = 11,
|
||||||
|
CreateContext = 12,
|
||||||
|
DestroyContext = 13,
|
||||||
MIN = None,
|
MIN = None,
|
||||||
MAX = InitCapabilities
|
MAX = DestroyContext
|
||||||
};
|
};
|
||||||
|
|
||||||
inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[12] {
|
inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[14] {
|
||||||
static const SurfaceOpKind values[] = {
|
static const SurfaceOpKind values[] = {
|
||||||
SurfaceOpKind::None,
|
SurfaceOpKind::None,
|
||||||
SurfaceOpKind::InitializeDisplay,
|
SurfaceOpKind::InitializeDisplay,
|
||||||
@@ -302,13 +304,15 @@ inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[12] {
|
|||||||
SurfaceOpKind::SetSwapInterval,
|
SurfaceOpKind::SetSwapInterval,
|
||||||
SurfaceOpKind::ReleaseResources,
|
SurfaceOpKind::ReleaseResources,
|
||||||
SurfaceOpKind::SetWindowHandle,
|
SurfaceOpKind::SetWindowHandle,
|
||||||
SurfaceOpKind::InitCapabilities
|
SurfaceOpKind::InitCapabilities,
|
||||||
|
SurfaceOpKind::CreateContext,
|
||||||
|
SurfaceOpKind::DestroyContext
|
||||||
};
|
};
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const char * const *EnumNamesSurfaceOpKind() {
|
inline const char * const *EnumNamesSurfaceOpKind() {
|
||||||
static const char * const names[13] = {
|
static const char * const names[15] = {
|
||||||
"None",
|
"None",
|
||||||
"InitializeDisplay",
|
"InitializeDisplay",
|
||||||
"CreateWindowSurface",
|
"CreateWindowSurface",
|
||||||
@@ -321,13 +325,15 @@ inline const char * const *EnumNamesSurfaceOpKind() {
|
|||||||
"ReleaseResources",
|
"ReleaseResources",
|
||||||
"SetWindowHandle",
|
"SetWindowHandle",
|
||||||
"InitCapabilities",
|
"InitCapabilities",
|
||||||
|
"CreateContext",
|
||||||
|
"DestroyContext",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const char *EnumNameSurfaceOpKind(SurfaceOpKind e) {
|
inline const char *EnumNameSurfaceOpKind(SurfaceOpKind e) {
|
||||||
if (::flatbuffers::IsOutRange(e, SurfaceOpKind::None, SurfaceOpKind::InitCapabilities)) return "";
|
if (::flatbuffers::IsOutRange(e, SurfaceOpKind::None, SurfaceOpKind::DestroyContext)) return "";
|
||||||
const size_t index = static_cast<size_t>(e);
|
const size_t index = static_cast<size_t>(e);
|
||||||
return EnumNamesSurfaceOpKind()[index];
|
return EnumNamesSurfaceOpKind()[index];
|
||||||
}
|
}
|
||||||
@@ -1809,7 +1815,9 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
|
|||||||
VT_HEIGHT = 18,
|
VT_HEIGHT = 18,
|
||||||
VT_SWAPINTERVAL = 20,
|
VT_SWAPINTERVAL = 20,
|
||||||
VT_READSURFACE = 22,
|
VT_READSURFACE = 22,
|
||||||
VT_CONTEXT = 24
|
VT_CONTEXT = 24,
|
||||||
|
VT_SHAREGROUPTOKEN = 26,
|
||||||
|
VT_CONTEXTFLAGS = 28
|
||||||
};
|
};
|
||||||
uint64_t seq() const {
|
uint64_t seq() const {
|
||||||
return GetField<uint64_t>(VT_SEQ, 0);
|
return GetField<uint64_t>(VT_SEQ, 0);
|
||||||
@@ -1844,6 +1852,12 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
|
|||||||
uint64_t context() const {
|
uint64_t context() const {
|
||||||
return GetField<uint64_t>(VT_CONTEXT, 0);
|
return GetField<uint64_t>(VT_CONTEXT, 0);
|
||||||
}
|
}
|
||||||
|
uint64_t shareGroupToken() const {
|
||||||
|
return GetField<uint64_t>(VT_SHAREGROUPTOKEN, 0);
|
||||||
|
}
|
||||||
|
uint32_t contextFlags() const {
|
||||||
|
return GetField<uint32_t>(VT_CONTEXTFLAGS, 0);
|
||||||
|
}
|
||||||
template <bool B = false>
|
template <bool B = false>
|
||||||
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
|
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
|
||||||
return VerifyTableStart(verifier) &&
|
return VerifyTableStart(verifier) &&
|
||||||
@@ -1858,6 +1872,8 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
|
|||||||
VerifyField<int32_t>(verifier, VT_SWAPINTERVAL, 4) &&
|
VerifyField<int32_t>(verifier, VT_SWAPINTERVAL, 4) &&
|
||||||
VerifyField<uint64_t>(verifier, VT_READSURFACE, 8) &&
|
VerifyField<uint64_t>(verifier, VT_READSURFACE, 8) &&
|
||||||
VerifyField<uint64_t>(verifier, VT_CONTEXT, 8) &&
|
VerifyField<uint64_t>(verifier, VT_CONTEXT, 8) &&
|
||||||
|
VerifyField<uint64_t>(verifier, VT_SHAREGROUPTOKEN, 8) &&
|
||||||
|
VerifyField<uint32_t>(verifier, VT_CONTEXTFLAGS, 4) &&
|
||||||
verifier.EndTable();
|
verifier.EndTable();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1899,6 +1915,12 @@ struct SurfaceOpBuilder {
|
|||||||
void add_context(uint64_t context) {
|
void add_context(uint64_t context) {
|
||||||
fbb_.AddElement<uint64_t>(SurfaceOp::VT_CONTEXT, context, 0);
|
fbb_.AddElement<uint64_t>(SurfaceOp::VT_CONTEXT, context, 0);
|
||||||
}
|
}
|
||||||
|
void add_shareGroupToken(uint64_t shareGroupToken) {
|
||||||
|
fbb_.AddElement<uint64_t>(SurfaceOp::VT_SHAREGROUPTOKEN, shareGroupToken, 0);
|
||||||
|
}
|
||||||
|
void add_contextFlags(uint32_t contextFlags) {
|
||||||
|
fbb_.AddElement<uint32_t>(SurfaceOp::VT_CONTEXTFLAGS, contextFlags, 0);
|
||||||
|
}
|
||||||
explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
|
explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
|
||||||
: fbb_(_fbb) {
|
: fbb_(_fbb) {
|
||||||
start_ = fbb_.StartTable();
|
start_ = fbb_.StartTable();
|
||||||
@@ -1922,14 +1944,18 @@ inline ::flatbuffers::Offset<SurfaceOp> CreateSurfaceOp(
|
|||||||
int32_t height = 0,
|
int32_t height = 0,
|
||||||
int32_t swapInterval = 0,
|
int32_t swapInterval = 0,
|
||||||
uint64_t readSurface = 0,
|
uint64_t readSurface = 0,
|
||||||
uint64_t context = 0) {
|
uint64_t context = 0,
|
||||||
|
uint64_t shareGroupToken = 0,
|
||||||
|
uint32_t contextFlags = 0) {
|
||||||
SurfaceOpBuilder builder_(_fbb);
|
SurfaceOpBuilder builder_(_fbb);
|
||||||
|
builder_.add_shareGroupToken(shareGroupToken);
|
||||||
builder_.add_context(context);
|
builder_.add_context(context);
|
||||||
builder_.add_readSurface(readSurface);
|
builder_.add_readSurface(readSurface);
|
||||||
builder_.add_nativeToken(nativeToken);
|
builder_.add_nativeToken(nativeToken);
|
||||||
builder_.add_surface(surface);
|
builder_.add_surface(surface);
|
||||||
builder_.add_display(display);
|
builder_.add_display(display);
|
||||||
builder_.add_seq(seq);
|
builder_.add_seq(seq);
|
||||||
|
builder_.add_contextFlags(contextFlags);
|
||||||
builder_.add_swapInterval(swapInterval);
|
builder_.add_swapInterval(swapInterval);
|
||||||
builder_.add_height(height);
|
builder_.add_height(height);
|
||||||
builder_.add_width(width);
|
builder_.add_width(width);
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ extern "C" {
|
|||||||
// 4 P11 PAIR: the PairBind message - a socket client's control and aux connections pair by
|
// 4 P11 PAIR: the PairBind message - a socket client's control and aux connections pair by
|
||||||
// the nonce each presents first, not by arrival order.
|
// the nonce each presents first, not by arrival order.
|
||||||
// 5 P11 B2: LinkTerms.adoptTier - the client's persistent-map adoption tier ask (T0 or T2).
|
// 5 P11 B2: LinkTerms.adoptTier - the client's persistent-map adoption tier ask (T0 or T2).
|
||||||
#define MOBILEGL_PROTOCOL_CONTROL_REVISION 5
|
// 6 P14 S1 (docs/Disaggregated/design/11-state-ownership.md): SurfaceOpKind.CreateContext /
|
||||||
|
// DestroyContext, plus SurfaceOp.shareGroupToken and SurfaceOp.contextFlags.
|
||||||
|
#define MOBILEGL_PROTOCOL_CONTROL_REVISION 6
|
||||||
|
|
||||||
#define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor))
|
#define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor))
|
||||||
#define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16)
|
#define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16)
|
||||||
|
|||||||
@@ -256,6 +256,25 @@ enum SurfaceOpKind : ubyte {
|
|||||||
// (Client/BackendObject_Remote.cpp). Without the tag the codec refused it by
|
// (Client/BackendObject_Remote.cpp). Without the tag the codec refused it by
|
||||||
// name and every spawned eglMakeCurrent failed with InprocOnlyOpOnTheWire.
|
// name and every spawned eglMakeCurrent failed with InprocOnlyOpOnTheWire.
|
||||||
InitCapabilities = 11,
|
InitCapabilities = 11,
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). THE EGL CONTEXT LIFECYCLE, as two
|
||||||
|
// more ops on the same frame the nine surface ops ride - the shape §"wire changes" 2 prescribes,
|
||||||
|
// and deliberately NOT two new CtrlMsg tables: a context is created, destroyed and made current
|
||||||
|
// by the SAME caller on the SAME thread as a surface, through the same blocking frame channel,
|
||||||
|
// and a second channel would only be a second place for the two orderings to disagree.
|
||||||
|
//
|
||||||
|
// CreateContext `context` is the clientContextToken EGLState minted, `shareGroupToken` the
|
||||||
|
// group it was derived into (the sharing context's group, or a fresh one),
|
||||||
|
// `contextFlags` the frame's reserved extension word (0 today). `display` and
|
||||||
|
// `surface` are 0: a context is display-scoped in EGL but the server's display
|
||||||
|
// is the session's own, so nothing is named.
|
||||||
|
// DestroyContext `context` is the token to drop. The server forgets it and, if it was the
|
||||||
|
// bound one, unbinds it.
|
||||||
|
//
|
||||||
|
// Both are answered by the ordinary SurfaceReply (ok, plus the refusal enum) - S1 creates a
|
||||||
|
// PLACEHOLDER runtime (token, share group, flags) and no native context, so there is nothing
|
||||||
|
// for the reply to carry that a Bool does not say already.
|
||||||
|
CreateContext = 12,
|
||||||
|
DestroyContext = 13,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WindowKind : ubyte {
|
enum WindowKind : ubyte {
|
||||||
@@ -303,6 +322,12 @@ table SurfaceOp {
|
|||||||
// ("no surface" / "no context") for every other op and for a ReleaseCurrent.
|
// ("no surface" / "no context") for every other op and for a ReleaseCurrent.
|
||||||
readSurface: ulong;
|
readSurface: ulong;
|
||||||
context: ulong;
|
context: ulong;
|
||||||
|
// P14 S1, appended (a FlatBuffers table field's id IS its vtable slot - append only). The two
|
||||||
|
// values CreateContext carries beyond the token itself: the share group the new context was
|
||||||
|
// derived into, and a reserved flags word so a later context attribute (a profile, a robustness
|
||||||
|
// ask) has a slot to land in without moving the fields beside it. 0 on every other op.
|
||||||
|
shareGroupToken: ulong;
|
||||||
|
contextFlags: uint;
|
||||||
}
|
}
|
||||||
|
|
||||||
table SurfaceReply {
|
table SurfaceReply {
|
||||||
|
|||||||
@@ -1462,6 +1462,53 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S1. THE RING'S DOOR INTO THE SESSION'S CONTEXT TABLE.
|
||||||
|
//
|
||||||
|
// The record carries the client's current context token and this is where a session's
|
||||||
|
// attribution moves. It is deliberately NOT an assertion like OnApplierReset's serial: the
|
||||||
|
// client's EGL layer mints the token and the server has no second count to compare against,
|
||||||
|
// so what is checked is a FACT ABOUT THIS SESSION'S OWN TABLE - a token that CreateContext
|
||||||
|
// never created is corruption, not a late record. Token 0 is the release edge and is always
|
||||||
|
// legal (a session whose client released its context has no context, which is exactly the
|
||||||
|
// value a session that never bound one already answers).
|
||||||
|
Bool ServerVerbSink::OnBindContext(const MG_Pipe::MGPBindContext& bind) {
|
||||||
|
// NO SESSION IS A DECLINE, NOT A FAULT, and it is the one case here that a peer's bytes
|
||||||
|
// cannot cause: a bind is answered by the session's own table, so with no accepted
|
||||||
|
// session there is nothing to attribute the record to and nothing to refuse. (The window
|
||||||
|
// is Close() clearing the active session while the apply thread is still draining.)
|
||||||
|
ServerSession* session = ServerSession::Active();
|
||||||
|
if (session == nullptr) return false;
|
||||||
|
// PH-1 (3): the refusal latches in an armed session child (the peer wrote the token);
|
||||||
|
// unarmed it dies, like every other contract fact about a peer.
|
||||||
|
if (!session->BindContext(bind.ClientContextToken)) {
|
||||||
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the "
|
||||||
|
"record binds token %llu, which this session never created; a bind_context "
|
||||||
|
"names a context CreateContext put in the session's table, and 0 (the "
|
||||||
|
"release) is the only token that needs no entry",
|
||||||
|
static_cast<unsigned long long>(bind.ClientContextToken));
|
||||||
|
}
|
||||||
|
++m_bindContexts;
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE HALF OF THE SWITCH.
|
||||||
|
//
|
||||||
|
// `bind_context` is where the session's context attribution moves, and it is also the ONLY
|
||||||
|
// point at which the apply thread's native context may move: the MakeCurrent control frame
|
||||||
|
// that precedes it still carries the OLD context (the client emits the binding after the
|
||||||
|
// binding has taken effect, which is the ordering S1 chose on purpose), so a native switch
|
||||||
|
// driven from there would bind the context the client just left. Frontend GL state is the
|
||||||
|
// backend's per-tuple business; this call makes the tuple the resolver now names current.
|
||||||
|
//
|
||||||
|
// It is a no-op for the shapes S4 did not change: with no resolver installed (monolith, a
|
||||||
|
// unit case, the inproc one-process shape) the key is {0,0}, and the single-context world's
|
||||||
|
// bind does not move the native context - MakeNativeContextCurrentForBoundToken returns
|
||||||
|
// without a driver call when the context it resolves to is already the one bound here.
|
||||||
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||||
|
(void)MG_Backend::DirectGLES::MakeNativeContextCurrentForBoundToken();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
|
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
|
||||||
// §1's zero ruling: a null handle means "the object never crossed", and the client
|
// §1's zero ruling: a null handle means "the object never crossed", and the client
|
||||||
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
|
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
|
||||||
|
|||||||
@@ -245,11 +245,21 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override;
|
Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override;
|
||||||
Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override;
|
Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override;
|
||||||
|
|
||||||
|
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
|
||||||
|
//
|
||||||
|
// The session's current context token becomes the record's, on the apply thread. Unlike
|
||||||
|
// OnApplierReset's serial the token is NOT an assertion about a count the two ends both
|
||||||
|
// hold - the client mints it and the server keeps the table CreateContext built - so the
|
||||||
|
// refusal here is "this session never created that token", a named ProtocolCorruption
|
||||||
|
// rather than a silent bind. Token 0 (the release) is always legal.
|
||||||
|
Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) override;
|
||||||
|
|
||||||
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
|
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
|
||||||
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
|
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
|
||||||
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
|
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
|
||||||
Uint64 ApplierResets() const { return m_applierResets; }
|
Uint64 ApplierResets() const { return m_applierResets; }
|
||||||
Uint64 ObjectDeaths() const { return m_objectDeaths; }
|
Uint64 ObjectDeaths() const { return m_objectDeaths; }
|
||||||
|
Uint64 BindContexts() const { return m_bindContexts; }
|
||||||
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
|
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
|
||||||
// assert the sequence rather than only the count.
|
// assert the sequence rather than only the count.
|
||||||
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
|
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
|
||||||
@@ -377,6 +387,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint64 m_applierResetSerial = 0;
|
Uint64 m_applierResetSerial = 0;
|
||||||
Uint64 m_applierResets = 0;
|
Uint64 m_applierResets = 0;
|
||||||
Uint64 m_objectDeaths = 0;
|
Uint64 m_objectDeaths = 0;
|
||||||
|
// P14 S1. BindContexts counts the records the sink dispatched and bound on the session;
|
||||||
|
// the token itself lives on the session (ServerSession::CurrentContextToken), which is
|
||||||
|
// the object the control plane's CreateContext/DestroyContext also own - one table, one
|
||||||
|
// owner, and the sink is only the ring's door into it.
|
||||||
|
Uint64 m_bindContexts = 0;
|
||||||
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
||||||
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
||||||
// between them. Grown, never shrunk, and never handed out past the call.
|
// between them. Grown, never shrunk, and never handed out past the call.
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
NoWindow, // a display, but no window was attached within the wait
|
NoWindow, // a display, but no window was attached within the wait
|
||||||
Cancelled, // the caller's cancel predicate said so
|
Cancelled, // the caller's cancel predicate said so
|
||||||
Interrupted, // Interrupt() (the in-process server is stopping)
|
Interrupted, // Interrupt() (the in-process server is stopping)
|
||||||
LeasedElsewhere, // another holder has the window (sessions are sequential: a bug if seen)
|
// P14 S2: NOT A BUG ANY MORE. Several sessions share one process and the display's window
|
||||||
|
// is one window, so a second ON-SCREEN session asking for it is refused by name here
|
||||||
|
// (ServerLoop logs the holder); offscreen sessions take no lease and are unaffected.
|
||||||
|
LeasedElsewhere, // another session in this process holds the window: see the note above
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ServerWindowDetach : Uint8 {
|
enum class ServerWindowDetach : Uint8 {
|
||||||
|
|||||||
@@ -10,12 +10,14 @@
|
|||||||
|
|
||||||
#include "ServerLoop.h"
|
#include "ServerLoop.h"
|
||||||
#include "ApplyThreadPolicy.h"
|
#include "ApplyThreadPolicy.h"
|
||||||
|
#include "SessionRuntime.h"
|
||||||
#include <MG_Remote/FatalFunnel.h>
|
#include <MG_Remote/FatalFunnel.h>
|
||||||
#include <MG_Remote/Transport/LinkMetrics.h>
|
#include <MG_Remote/Transport/LinkMetrics.h>
|
||||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
#include <MG_Util/Metrics/PipeStats.h>
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
@@ -284,6 +286,8 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
m_haveCurrentTuple = false;
|
m_haveCurrentTuple = false;
|
||||||
// P12 (D4, D6): a new session decides its own surface mode, holds no window lease yet and
|
// P12 (D4, D6): a new session decides its own surface mode, holds no window lease yet and
|
||||||
// has no window-lost request pending. The apply thread that reads these is not started yet.
|
// has no window-lost request pending. The apply thread that reads these is not started yet.
|
||||||
|
// P14 S2: and it is THIS session's latch - the loop is the session's, so a second session
|
||||||
|
// in the same process starts here too and neither's first surface decides the other's.
|
||||||
m_surfaceMode = SessionSurfaceMode::None;
|
m_surfaceMode = SessionSurfaceMode::None;
|
||||||
m_holdsWindowLease = false;
|
m_holdsWindowLease = false;
|
||||||
m_serverOwnedSurfaces.clear();
|
m_serverOwnedSurfaces.clear();
|
||||||
@@ -345,6 +349,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// Apply thread only (RunSurfaceControlFrame's dispatch put us here), so
|
// Apply thread only (RunSurfaceControlFrame's dispatch put us here), so
|
||||||
// m_haveCurrentTuple/m_cur* need no
|
// m_haveCurrentTuple/m_cur* need no
|
||||||
// lock. The counters are atomic for the reader on the test thread.
|
// lock. The counters are atomic for the reader on the test thread.
|
||||||
|
//
|
||||||
|
// P14 S4 left this classification alone on purpose - ServerLoop.h states the argument (the
|
||||||
|
// currently bound tuple IS the unit ID-67 is about, and only one EGLContext can be current on
|
||||||
|
// this thread). What S4 changed is one layer down: the tuple now names the (session, context)
|
||||||
|
// registry's native context instead of the process's one.
|
||||||
MakeCurrentOutcome outcome;
|
MakeCurrentOutcome outcome;
|
||||||
const EglBindAction action = ClassifyEglMakeCurrent(m_haveCurrentTuple, m_curDpy, m_curDraw,
|
const EglBindAction action = ClassifyEglMakeCurrent(m_haveCurrentTuple, m_curDpy, m_curDraw,
|
||||||
m_curRead, m_curCtx, dpy, draw, read, ctx);
|
m_curRead, m_curCtx, dpy, draw, read, ctx);
|
||||||
@@ -409,9 +418,18 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// THE IDENTITY IS PUBLISHED FIRST, BEFORE THE THREAD HAS A NAME OR AN AFFINITY. Every
|
// THE IDENTITY IS PUBLISHED FIRST, BEFORE THE THREAD HAS A NAME OR AN AFFINITY. Every
|
||||||
// role guard in the tree reads it, and the very first record this thread applies must
|
// role guard in the tree reads it, and the very first record this thread applies must
|
||||||
// already answer "yes" to OnApplyThread() - otherwise the applier's own reads would
|
// already answer "yes" to OnApplyThread() - otherwise the applier's own reads would
|
||||||
// look like client reads to the guards. Relaxed for the reason stated beside the key's
|
// look like client reads to the guards.
|
||||||
// declaration: it is an identity, not a handshake, and this thread is the only writer.
|
//
|
||||||
Detail::g_applyThreadKey.store(Detail::CurrentThreadKey(), std::memory_order_relaxed);
|
// P14 S2: two words, both about THIS thread. t_onApplyThread is the guard's answer; the
|
||||||
|
// process counter beside it is what MG_Backend/DirectGLES/Managers.cpp reads as "an apply
|
||||||
|
// thread is running" (ServerLoop.h's block). The scope is opened BEFORE either, because
|
||||||
|
// every read below - the session's peer, its rings, the applier it attaches - must already
|
||||||
|
// resolve to this session rather than to the process's default.
|
||||||
|
ServerSession& session = *m_session;
|
||||||
|
std::unique_ptr<Detail::ThreadSessionScope> scope;
|
||||||
|
if (m_runtime != nullptr) scope = std::make_unique<Detail::ThreadSessionScope>(*m_runtime);
|
||||||
|
Detail::t_onApplyThread = true;
|
||||||
|
Detail::g_applyThreadKey.fetch_add(1, std::memory_order_relaxed);
|
||||||
NameThisThread("mgl-srv-apply");
|
NameThisThread("mgl-srv-apply");
|
||||||
// P6: THIS THREAD IS THE SERVER, and saying so is what sends its lines to the server's
|
// P6: THIS THREAD IS THE SERVER, and saying so is what sends its lines to the server's
|
||||||
// own log. Under spawn the process role already answers this; under INPROC it is the
|
// own log. Under spawn the process role already answers this; under INPROC it is the
|
||||||
@@ -426,7 +444,6 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// "an affinity that silently did nothing looks exactly like one that worked"
|
// "an affinity that silently did nothing looks exactly like one that worked"
|
||||||
// (CONTRACT-P5 5). P11 B1: `auto` now depends on who the client is (ApplyThreadPolicy.h),
|
// (CONTRACT-P5 5). P11 B1: `auto` now depends on who the client is (ApplyThreadPolicy.h),
|
||||||
// and so does the spin before each park.
|
// and so does the spin before each park.
|
||||||
ServerSession& session = *m_session;
|
|
||||||
const char* raw = AffinityStringFromConfig();
|
const char* raw = AffinityStringFromConfig();
|
||||||
const Bool automatic = raw == nullptr || raw[0] == '\0' || std::strcmp(raw, "auto") == 0;
|
const Bool automatic = raw == nullptr || raw[0] == '\0' || std::strcmp(raw, "auto") == 0;
|
||||||
Uint64 bigCores = 0, primeCores = 0;
|
Uint64 bigCores = 0, primeCores = 0;
|
||||||
@@ -691,7 +708,12 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// thread could ever give. The integration lane - a real EGL teardown - is the only
|
// thread could ever give. The integration lane - a real EGL teardown - is the only
|
||||||
// thing that would catch that, and it would catch it as a hang, not as a failed
|
// thing that would catch that, and it would catch it as a hang, not as a failed
|
||||||
// assertion.
|
// assertion.
|
||||||
Detail::g_applyThreadKey.store(0, std::memory_order_relaxed);
|
//
|
||||||
|
// P14 S2: the guard's own word is lowered here too, and for the same reason - with
|
||||||
|
// N sessions it can only ever have been this thread's, and the thread is about to
|
||||||
|
// stop being an apply thread. The scope is destroyed by this function's frame.
|
||||||
|
Detail::t_onApplyThread = false;
|
||||||
|
Detail::g_applyThreadKey.fetch_sub(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
m_controlDone.notify_all();
|
m_controlDone.notify_all();
|
||||||
|
|
||||||
@@ -1189,6 +1211,10 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ServerLoop& ServerLoopInstance() {
|
ServerLoop& ServerLoopInstance() {
|
||||||
|
// P14 S2: the session of the CALLING THREAD, else the process's one instance. The client
|
||||||
|
// process (the forwarders, whose sink makes them cross the wire), the inproc one-process
|
||||||
|
// client+server shape and every unit case have no thread session and keep this instance.
|
||||||
|
if (ServerLoop* const own = Detail::CurrentLoop()) return *own;
|
||||||
// ID-8: leak at exit, like every MG_Remote singleton.
|
// ID-8: leak at exit, like every MG_Remote singleton.
|
||||||
static ServerLoop& instance = *new ServerLoop{};
|
static ServerLoop& instance = *new ServerLoop{};
|
||||||
return instance;
|
return instance;
|
||||||
@@ -1360,7 +1386,9 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
return MOBILEGL_OK;
|
return MOBILEGL_OK;
|
||||||
case SurfaceControlOp::CreatePbufferSurface:
|
case SurfaceControlOp::CreatePbufferSurface:
|
||||||
// P12 (D4): a pbuffer in a session that went on-screen is the other mode, refused by
|
// P12 (D4): a pbuffer in a session that went on-screen is the other mode, refused by
|
||||||
// name and not latched - the session and its window surface carry on.
|
// name and not latched - the session and its window surface carry on. P14 S2: the mode
|
||||||
|
// is the session's own - latched by its first surface, on its own loop - and never
|
||||||
|
// another session's.
|
||||||
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) {
|
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/false)) {
|
||||||
frame.ok = false;
|
frame.ok = false;
|
||||||
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
|
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
|
||||||
@@ -1437,6 +1465,48 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
frame.ok = outcome.ok;
|
frame.ok = outcome.ok;
|
||||||
return MOBILEGL_OK;
|
return MOBILEGL_OK;
|
||||||
}
|
}
|
||||||
|
case SurfaceControlOp::CreateContext: {
|
||||||
|
// P14 S1 built the session's table entry; P14 S4 gives it its NATIVE context. The
|
||||||
|
// share group is what makes two client contexts see one set of buffer/texture objects:
|
||||||
|
// a group whose leader already has a native EGLContext hands it to eglCreateContext as
|
||||||
|
// the share argument, so this arm is the whole of "server-side sharing".
|
||||||
|
//
|
||||||
|
// An `ok = false` here is a named refusal the client's eglCreateContext turns into
|
||||||
|
// EGL_NO_CONTEXT, which is what a server that declined to table the token must produce -
|
||||||
|
// a token the server never heard of is worse than no context at all. A native context
|
||||||
|
// that could not be built rolls the table entry back, or the session would hold a token
|
||||||
|
// whose every MakeCurrent fails one layer down.
|
||||||
|
ServerSession* session = ServerSession::Active();
|
||||||
|
if (session == nullptr) {
|
||||||
|
frame.ok = false;
|
||||||
|
return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
frame.ok = session->CreateContext(frame.context, frame.shareGroupToken, frame.contextFlags);
|
||||||
|
if (frame.ok && backend != nullptr &&
|
||||||
|
backend->GetBackendType() == BackendType::DirectGLES) {
|
||||||
|
if (!MG_Backend::DirectGLES::CreateNativeContextFor(frame.context, frame.shareGroupToken)) {
|
||||||
|
(void)session->DestroyContext(frame.context);
|
||||||
|
frame.ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
case SurfaceControlOp::DestroyContext: {
|
||||||
|
ServerSession* session = ServerSession::Active();
|
||||||
|
if (session == nullptr) {
|
||||||
|
frame.ok = false;
|
||||||
|
return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
frame.ok = session->DestroyContext(frame.context);
|
||||||
|
// P14 S4: the native context goes with the entry, and ONLY that one - the session's
|
||||||
|
// other contexts, and every other session's, are untouched. The display is terminated
|
||||||
|
// only when the last native context in the process has gone.
|
||||||
|
if (frame.ok && backend != nullptr &&
|
||||||
|
backend->GetBackendType() == BackendType::DirectGLES) {
|
||||||
|
MG_Backend::DirectGLES::DestroyNativeContextFor(frame.context);
|
||||||
|
}
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
case SurfaceControlOp::SetSwapInterval:
|
case SurfaceControlOp::SetSwapInterval:
|
||||||
backend->SetEGLSwapInterval(frame.swapInterval);
|
backend->SetEGLSwapInterval(frame.swapInterval);
|
||||||
frame.ok = true;
|
frame.ok = true;
|
||||||
@@ -1590,10 +1660,17 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (refusal != nullptr) *refusal = SurfaceRefusalCode::NoServerWindow;
|
if (refusal != nullptr) *refusal = SurfaceRefusalCode::NoServerWindow;
|
||||||
|
// P14 S2: LeasedElsewhere IS NOT A BUG ANY MORE. The display's window has one holder at a
|
||||||
|
// time and a session per holder, so a second ON-SCREEN session asking for it is refused -
|
||||||
|
// by name, not latched, exactly as a display with no window up. Offscreen (pbuffer)
|
||||||
|
// sessions take no lease at all and are unaffected.
|
||||||
MGLOG_E("MG_Remote server: Refuse ServerOwned: this server owns a display but no window for a %ux%u surface "
|
MGLOG_E("MG_Remote server: Refuse ServerOwned: this server owns a display but no window for a %ux%u surface "
|
||||||
"(%s after at most %u ms) - the display's surface is not up. Refused (NoServerWindow); the "
|
"(%s after at most %u ms)%s. Refused (NoServerWindow); the session is not latched",
|
||||||
"session is not latched",
|
width, height, ServerWindowAcquireName(acquired), timeoutMs,
|
||||||
width, height, ServerWindowAcquireName(acquired), timeoutMs);
|
acquired == ServerWindowAcquire::LeasedElsewhere
|
||||||
|
? " - the server's window is leased by another session, and one on-screen session at a time is "
|
||||||
|
"the display's own rule"
|
||||||
|
: " - the display's surface is not up");
|
||||||
return MOBILEGL_ERR_TIMEOUT;
|
return MOBILEGL_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1603,7 +1680,9 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::None);
|
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::None);
|
||||||
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
|
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
|
||||||
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
|
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
|
||||||
// D4 FIRST: an offscreen session may not go on-screen, display or no display.
|
// D4 FIRST: an offscreen session may not go on-screen, display or no display. P14 S2: the
|
||||||
|
// refusal is against THIS session's own latch - a second on-screen session in the same
|
||||||
|
// process is a different refusal (no free window), never a mode mismatch.
|
||||||
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) {
|
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) {
|
||||||
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
|
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
|
||||||
MGLOG_E("MG_Remote server: SurfaceModeMismatch - a ServerOwned CreateWindowSurface (seq %llu, %ux%u) in a "
|
MGLOG_E("MG_Remote server: SurfaceModeMismatch - a ServerOwned CreateWindowSurface (seq %llu, %ux%u) in a "
|
||||||
@@ -1824,6 +1903,30 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
return rc == MOBILEGL_OK && frame.ok;
|
return rc == MOBILEGL_OK && frame.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S1. A context creation is a frame like any other: `context` IS the client token (the
|
||||||
|
// schema's field, not a bit-cast handle - a context has no handle the server could name),
|
||||||
|
// `shareGroupToken` and `contextFlags` the two values CreateContext adds to it. Same blocking
|
||||||
|
// handshake, so eglCreateContext cannot return a token the server has not tabled - which is
|
||||||
|
// what keeps bind_context's "this session never created that token" refusal unreachable on
|
||||||
|
// the well-behaved path.
|
||||||
|
Bool ServerCreateEGLContext(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags) {
|
||||||
|
SurfaceControlFrame frame;
|
||||||
|
frame.kind = SurfaceControlOp::CreateContext;
|
||||||
|
frame.context = clientContextToken;
|
||||||
|
frame.shareGroupToken = shareGroupToken;
|
||||||
|
frame.contextFlags = flags;
|
||||||
|
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
|
||||||
|
return rc == MOBILEGL_OK && frame.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerDestroyEGLContext(Uint64 clientContextToken) {
|
||||||
|
SurfaceControlFrame frame;
|
||||||
|
frame.kind = SurfaceControlOp::DestroyContext;
|
||||||
|
frame.context = clientContextToken;
|
||||||
|
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
|
||||||
|
return rc == MOBILEGL_OK && frame.ok;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||||
SurfaceControlFrame frame;
|
SurfaceControlFrame frame;
|
||||||
frame.kind = SurfaceControlOp::SwapBuffersInprocOnly;
|
frame.kind = SurfaceControlOp::SwapBuffersInprocOnly;
|
||||||
|
|||||||
@@ -112,81 +112,44 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// window in which a live-but-exiting apply thread answers true is byte-for-byte the old
|
// window in which a live-but-exiting apply thread answers true is byte-for-byte the old
|
||||||
// one. The ServerLoop object still OWNS the key - nothing but ApplyThreadMain writes it -
|
// one. The ServerLoop object still OWNS the key - nothing but ApplyThreadMain writes it -
|
||||||
// it simply no longer has to be reached through the singleton to be read.
|
// it simply no longer has to be reached through the singleton to be read.
|
||||||
|
// P14 S2: AND THE ANSWER IS PER THREAD NOW, NOT PER PROCESS. One word cannot hold N apply
|
||||||
|
// threads: two sessions' threads would each publish their own key and the second store would
|
||||||
|
// make the FIRST thread answer "no" to every role guard it owns - which is not a slower guard
|
||||||
|
// but a dead one (a draw would take the client arm on the apply thread, and
|
||||||
|
// RunSurfaceControlFrame would lose its re-entrancy shortcut and deadlock the EGL teardown).
|
||||||
|
// The question is "am I an apply thread", a fact about the CALLER, so it is asked in the
|
||||||
|
// caller's own TLS - one constant-initialized word, no guard, no atomic:
|
||||||
|
//
|
||||||
|
// t_onApplyThread the calling thread IS an apply thread.
|
||||||
|
// g_applyThreadKey HOW MANY are running in this process. Kept because
|
||||||
|
// MG_Backend/DirectGLES/Managers.cpp (outside this package's file set)
|
||||||
|
// reads this name `!= 0` - "an apply thread is running somewhere" - which
|
||||||
|
// is the count's zero-ness exactly. It is no longer a key and nothing on
|
||||||
|
// the hot paths reads it.
|
||||||
|
//
|
||||||
|
// ZERO MEANS "NO APPLY THREAD IS RUNNING" still: t_onApplyThread is raised as the apply
|
||||||
|
// thread's FIRST act and lowered in the same m_controlMutex critical section that clears
|
||||||
|
// m_running on the way out, so the window in which a live-but-exiting apply thread answers
|
||||||
|
// true is byte-for-byte the old one.
|
||||||
|
//
|
||||||
|
// WHAT WENT WITH IT, so nobody re-derives it: the aarch64 TPIDR_EL0 fast path, the
|
||||||
|
// std::thread::id fallback with its no-padding static_assert, the MOBILEGL_DETAIL_
|
||||||
|
// APPLY_THREAD_POINTER build knob and the relaxed-load argument about a recycled TLS block.
|
||||||
|
// They existed to make "is the process's one apply thread mine" cheap; the question is now
|
||||||
|
// "is the caller an apply thread", whose cheap answer is the caller's own word - and a word
|
||||||
|
// cannot be recycled, because the thread that inherits a dead apply thread's TLS block is
|
||||||
|
// not an apply thread until it runs ApplyThreadMain.
|
||||||
namespace Detail {
|
namespace Detail {
|
||||||
// RELAXED ON BOTH SIDES, and that is a claim about recycling, not a shrug. The only
|
// A fresh thread's TLS word is zero-initialized, so a thread that inherits the exited
|
||||||
// dangerous outcome would be a NEW thread that reuses the exited apply thread's TLS
|
// apply thread's TLS block - the recycling case the old relaxed-load argument was about -
|
||||||
// block (hence its thread pointer) while still reading the pre-clear key. It cannot
|
// starts out answering false and can only become the apply thread by running
|
||||||
// happen: the clear is sequenced before the apply thread's exit, the exit releases the
|
// ApplyThreadMain. There is no stale value to read and nothing to order.
|
||||||
// TLS block under libc's own lock, and the thread that later receives that block takes
|
inline thread_local Bool t_onApplyThread = false;
|
||||||
// the same lock - so the clear happens-before any load the new thread performs, and
|
|
||||||
// coherence forbids a relaxed load from reading a value that a happens-before store
|
|
||||||
// overwrote. A stale ZERO is harmless in the other direction: a thread that is not the
|
|
||||||
// apply thread wants "false" anyway, and the apply thread always reads its own store.
|
|
||||||
//
|
|
||||||
// No acquire is needed for a second reason: this is an IDENTITY, not a handshake. No
|
|
||||||
// caller publishes data through it; the real ordering lives in the ring, the doorbell
|
|
||||||
// and m_controlMutex.
|
|
||||||
inline std::atomic<Uint64> g_applyThreadKey{0};
|
|
||||||
|
|
||||||
// The calling thread's identity as the same integer. aarch64 - the split's only
|
// The count of live apply threads, as a relaxed atomic: an identity-like gauge, not a
|
||||||
// measured target - reads TPIDR_EL0 with a single instruction and no call. Everywhere
|
// handshake (the real ordering lives in the ring, the doorbell and m_controlMutex), and
|
||||||
// else falls back to std::this_thread::get_id() memcpy'd into the key: still one libc
|
// its one reader wants "is it zero".
|
||||||
// call, but the atomic<thread::id> load and the out-of-line pthread_equal that
|
inline std::atomic<Uint64> g_applyThreadKey{0};
|
||||||
// dominated the old body are gone either way. x86_64 is deliberately NOT on the
|
|
||||||
// builtin path - __builtin_thread_pointer only grew x86 support in clang 17 / GCC 11
|
|
||||||
// and older toolchains accept __has_builtin for it and then fail in codegen, which is
|
|
||||||
// a build break on the desktop ctest lane for a target whose frame rate nobody
|
|
||||||
// measures.
|
|
||||||
//
|
|
||||||
// A LIVE THREAD'S KEY IS NEVER 0 on either path (a thread pointer is a real TLS
|
|
||||||
// address; std::this_thread::get_id() on a running thread is never the
|
|
||||||
// default-constructed id, whose object representation is the all-zero one on every
|
|
||||||
// library in scope), which is what lets 0 mean "nobody".
|
|
||||||
//
|
|
||||||
// THE SELECTOR IS A BUILD KNOB, NOT A HEADER SECRET (review round 3). This header is
|
|
||||||
// pulled into MG_State, MG_Impl and MG_Test translation units now, so the name carries
|
|
||||||
// the MOBILEGL_DETAIL_ prefix to read as internal, it is #ifndef-guarded so a toolchain
|
|
||||||
// whose __has_builtin(__builtin_thread_pointer) answers 1 and then fails in codegen can
|
|
||||||
// be steered off the path with -DMOBILEGL_DETAIL_APPLY_THREAD_POINTER=0 instead of a
|
|
||||||
// header edit, and it is always DEFINED - to 0 when unavailable - and tested with #if
|
|
||||||
// rather than #ifdef, so that a -D...=0 really disables it instead of being ignored.
|
|
||||||
#ifndef MOBILEGL_DETAIL_APPLY_THREAD_POINTER
|
|
||||||
#if defined(__aarch64__) && defined(__has_builtin)
|
|
||||||
#if __has_builtin(__builtin_thread_pointer)
|
|
||||||
#define MOBILEGL_DETAIL_APPLY_THREAD_POINTER 1
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#ifndef MOBILEGL_DETAIL_APPLY_THREAD_POINTER
|
|
||||||
#define MOBILEGL_DETAIL_APPLY_THREAD_POINTER 0
|
|
||||||
#endif
|
|
||||||
inline Uint64 CurrentThreadKey() {
|
|
||||||
#if MOBILEGL_DETAIL_APPLY_THREAD_POINTER
|
|
||||||
return static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(__builtin_thread_pointer()));
|
|
||||||
#else
|
|
||||||
const std::thread::id id = std::this_thread::get_id();
|
|
||||||
static_assert(sizeof(std::thread::id) <= sizeof(Uint64),
|
|
||||||
"std::thread::id does not fit the apply-thread key; this fallback has "
|
|
||||||
"to hash rather than copy on that platform");
|
|
||||||
// AND IT MUST HAVE NO PADDING (review round 3). Copying the OBJECT REPRESENTATION of
|
|
||||||
// a type that has padding bytes would let two calls ON THE SAME THREAD return
|
|
||||||
// different keys, and OnApplyThread() would then answer FALSE ON THE APPLY THREAD -
|
|
||||||
// a role guard that has stopped guarding, which is the one direction CONTRACT-P5C
|
|
||||||
// rule E says a guard may never fail in (RunSurfaceControlFrame would also lose its
|
|
||||||
// re-entrancy shortcut and the EGL teardown post would deadlock on m_controlDone).
|
|
||||||
// Every library this fallback is built against today wraps a single scalar
|
|
||||||
// (libstdc++ __gthread_t, libc++ __libcpp_thread_id, MSVC unsigned int) and
|
|
||||||
// satisfies this; one that did not is a BUILD ERROR here rather than a silently
|
|
||||||
// dead guard at run time.
|
|
||||||
static_assert(std::has_unique_object_representations_v<std::thread::id>,
|
|
||||||
"std::thread::id has padding on this platform: copying its object "
|
|
||||||
"representation would make the apply-thread key unstable within one "
|
|
||||||
"thread. Hash the id (std::hash<std::thread::id>) instead.");
|
|
||||||
Uint64 key = 0;
|
|
||||||
std::memcpy(&key, &id, sizeof(id));
|
|
||||||
return key;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} // namespace Detail
|
} // namespace Detail
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
@@ -200,11 +163,26 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// session NOT latched). Windows the client names itself (the X11/Win32/None tokens CONTRACT-P6
|
// session NOT latched). Windows the client names itself (the X11/Win32/None tokens CONTRACT-P6
|
||||||
// D8 has yet to close) are neither kind and neither latch nor are refused: nothing about them
|
// D8 has yet to close) are neither kind and neither latch nor are refused: nothing about them
|
||||||
// changes here. Reset by ServerLoop::Start, i.e. per session.
|
// changes here. Reset by ServerLoop::Start, i.e. per session.
|
||||||
|
//
|
||||||
|
// P14 S2: AND THE LATCH IS PER SESSION, WHICH IS ALL THIS SLICE NEEDS. The mode is a member of
|
||||||
|
// the loop and a served session owns its loop, so two sessions in one process latch
|
||||||
|
// independently and neither's first surface decides the other's - the semantics above are
|
||||||
|
// otherwise unchanged (design/11-state-ownership.md §4.5: the latch stays at the session layer
|
||||||
|
// and is never hoisted to the process). A mode DECLARED before a session's first surface would
|
||||||
|
// have to travel on the wire - the Hello, or a SurfaceOp the client sends - because one
|
||||||
|
// process's MOBILEGL_IPC_SURFACE cannot speak for the several sessions a unified server runs;
|
||||||
|
// that is P14d's. Reading that env here instead refused a dialled-in ServerOwned client with
|
||||||
|
// SurfaceModeMismatch before the display was ever consulted (ServerOwnedSurfaceTest).
|
||||||
enum class SessionSurfaceMode : Uint8 { None, OnScreen, Offscreen };
|
enum class SessionSurfaceMode : Uint8 { None, OnScreen, Offscreen };
|
||||||
const char* SessionSurfaceModeName(SessionSurfaceMode mode);
|
const char* SessionSurfaceModeName(SessionSurfaceMode mode);
|
||||||
// The decision, pure, so a unit case can drive all six pairs without a backend.
|
// The decision, pure, so a unit case can drive all six pairs without a backend.
|
||||||
Bool SessionSurfaceModeAdmits(SessionSurfaceMode current, Bool serverOwnedWindow);
|
Bool SessionSurfaceModeAdmits(SessionSurfaceMode current, Bool serverOwnedWindow);
|
||||||
|
|
||||||
|
// P14 S2 (SessionRuntime.h): WHICH SESSION THIS LOOP IS, when it is one of several served by
|
||||||
|
// the process. Set by ServerMain::RunSession before Start(); null for every other shape (the
|
||||||
|
// inproc one-process client+server, a unit case), which is what keeps those byte-for-byte.
|
||||||
|
class SessionRuntime;
|
||||||
|
|
||||||
class ServerLoop {
|
class ServerLoop {
|
||||||
public:
|
public:
|
||||||
// Creates the apply thread, names it mgl-srv-apply, applies
|
// Creates the apply thread, names it mgl-srv-apply, applies
|
||||||
@@ -214,6 +192,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// landing on fast cores.
|
// landing on fast cores.
|
||||||
MobileGLResult Start(ServerSession& session);
|
MobileGLResult Start(ServerSession& session);
|
||||||
|
|
||||||
|
// P14 S2. The session this loop belongs to, so its apply thread can open the same thread
|
||||||
|
// scope the control thread has (session/loop/inputs/latch domain/segment resolver).
|
||||||
|
void SetRuntime(SessionRuntime& runtime) { m_runtime = &runtime; }
|
||||||
|
SessionRuntime* Runtime() const { return m_runtime; }
|
||||||
|
|
||||||
// Kill the doorbell, join the thread (bounded), then destroy the private backend object
|
// Kill the doorbell, join the thread (bounded), then destroy the private backend object
|
||||||
// ON THAT THREAD before it exits. Blocking by contract - see the header note.
|
// ON THAT THREAD before it exits. Blocking by contract - see the header note.
|
||||||
void Stop();
|
void Stop();
|
||||||
@@ -327,11 +310,9 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// or more per draw, so the call itself - a cross-library PLT hop - was a measurable
|
// or more per draw, so the call itself - a cross-library PLT hop - was a measurable
|
||||||
// share of the cost of the answer.
|
// share of the cost of the answer.
|
||||||
static Bool OnApplyThread() {
|
static Bool OnApplyThread() {
|
||||||
const Uint64 key = Detail::g_applyThreadKey.load(std::memory_order_relaxed);
|
// P14 S2: the caller's own TLS word. One constant-initialized load, no atomic, no
|
||||||
// Short-circuit ON PURPOSE: with TransportMode::Monolith, and in every process
|
// comparison and - unlike the process-wide key - the right answer with N sessions.
|
||||||
// before Start() and after Stop(), the key is 0 and the caller pays one relaxed
|
return Detail::t_onApplyThread;
|
||||||
// load and one branch - it never even reads its own identity.
|
|
||||||
return key != 0 && key == Detail::CurrentThreadKey();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CPU mask MOBILEGL_IPC_SERVER_AFFINITY resolved to and sched_setaffinity accepted.
|
// The CPU mask MOBILEGL_IPC_SERVER_AFFINITY resolved to and sched_setaffinity accepted.
|
||||||
@@ -476,8 +457,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
static void ServerWindowLostThunk(void* self);
|
static void ServerWindowLostThunk(void* self);
|
||||||
// AcquireFor's cancel predicate: the loop is stopping, or the session latched.
|
// AcquireFor's cancel predicate: the loop is stopping, or the session latched.
|
||||||
static Bool ServerWindowWaitCancelled(void* self);
|
static Bool ServerWindowWaitCancelled(void* self);
|
||||||
// D4: latched at the session's first successful surface creation; reset by Start().
|
// D4: latched at the session's first successful surface creation; reset by Start(). Per
|
||||||
|
// session, because the loop is (P14 S2).
|
||||||
SessionSurfaceMode m_surfaceMode = SessionSurfaceMode::None;
|
SessionSurfaceMode m_surfaceMode = SessionSurfaceMode::None;
|
||||||
|
// P14 S2: the session this loop serves, when the process serves several; see SetRuntime.
|
||||||
|
SessionRuntime* m_runtime = nullptr;
|
||||||
// Apply thread only: this loop holds the display's lease.
|
// Apply thread only: this loop holds the display's lease.
|
||||||
Bool m_holdsWindowLease = false;
|
Bool m_holdsWindowLease = false;
|
||||||
// Set by ServerWindowLostThunk (any thread), taken by the apply thread. Part of the park
|
// Set by ServerWindowLostThunk (any thread), taken by the apply thread. Part of the park
|
||||||
@@ -576,6 +560,18 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// C7 / ID-54: the (dpy, draw, read, ctx) currently bound on the apply thread. Written and
|
// C7 / ID-54: the (dpy, draw, read, ctx) currently bound on the apply thread. Written and
|
||||||
// read ONLY on the apply thread inside ApplyMakeCurrent, so it needs no lock; the two
|
// read ONLY on the apply thread inside ApplyMakeCurrent, so it needs no lock; the two
|
||||||
// counters beside it are atomic because a test reads them from another thread.
|
// counters beside it are atomic because a test reads them from another thread.
|
||||||
|
//
|
||||||
|
// P14 S4 LOOKED AT MAKING THIS A PER-CONTEXT TABLE AND DID NOT, with the reason recorded
|
||||||
|
// here rather than in a review thread: ID-67's pinned control
|
||||||
|
// (ServerLoopEglTest.ADifferentTupleBindsNativelyAndRepublishesAnIdenticalRepeatDoesNeither)
|
||||||
|
// defines "a different tuple" as a change of the CURRENTLY BOUND tuple, and asserts that
|
||||||
|
// A -> B -> A costs three native binds. A table that remembered each context's tuple would
|
||||||
|
// make the third one a RepeatNoOp - and it would be wrong for a real reason, not just for
|
||||||
|
// the assertion: only ONE EGLContext can be current on the apply thread, so coming back to A
|
||||||
|
// really does have to run the driver call that DirectGLES' seven frontend-context
|
||||||
|
// invalidations hang off. "Each context holds its own native triple" is satisfied by the
|
||||||
|
// backend's per-(session, token) registry; the dedup here is about the thread's CURRENT
|
||||||
|
// binding and stays one tuple.
|
||||||
Bool m_haveCurrentTuple = false;
|
Bool m_haveCurrentTuple = false;
|
||||||
EGLDisplay m_curDpy = EGL_NO_DISPLAY;
|
EGLDisplay m_curDpy = EGL_NO_DISPLAY;
|
||||||
EGLSurface m_curDraw = EGL_NO_SURFACE;
|
EGLSurface m_curDraw = EGL_NO_SURFACE;
|
||||||
@@ -668,6 +664,15 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// and a SECOND arrival IS the invalidation signal, which is how DirectGLES, which has no
|
// and a SECOND arrival IS the invalidation signal, which is how DirectGLES, which has no
|
||||||
// OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit.
|
// OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit.
|
||||||
Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The EGL CONTEXT LIFECYCLE's two
|
||||||
|
// frames, on the same channel and with the same blocking handshake as the twelve above.
|
||||||
|
// `clientContextToken` is the token EGLState minted for the new context (never 0), and
|
||||||
|
// `shareGroupToken` the group it joins - the sharing context's group, or a fresh one.
|
||||||
|
// S1 creates a PLACEHOLDER server-side runtime: the token enters the session's table and
|
||||||
|
// nothing native is built (S4 is what gives the entry a native context). DestroyContext
|
||||||
|
// drops the entry and unbinds it if it was the current one.
|
||||||
|
Bool ServerCreateEGLContext(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags);
|
||||||
|
Bool ServerDestroyEGLContext(Uint64 clientContextToken);
|
||||||
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||||
void ServerSetEGLSwapInterval(Int interval);
|
void ServerSetEGLSwapInterval(Int interval);
|
||||||
void ServerReleaseEGLSurface(EGLSurface surface);
|
void ServerReleaseEGLSurface(EGLSurface surface);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "ServerDisplay.h"
|
#include "ServerDisplay.h"
|
||||||
#include "ServerLoop.h"
|
#include "ServerLoop.h"
|
||||||
#include "ServerSession.h"
|
#include "ServerSession.h"
|
||||||
|
#include "SessionRuntime.h"
|
||||||
#include "SurfaceControlFrame.h"
|
#include "SurfaceControlFrame.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <Init.h>
|
#include <Init.h>
|
||||||
@@ -343,7 +344,7 @@ std::atomic<int> g_inProcessBackendPin{-1};
|
|||||||
// in the disaggregated server, does nothing unless set, and says so when it is.
|
// in the disaggregated server, does nothing unless set, and says so when it is.
|
||||||
void DelaySessionHandshakeForTest() {
|
void DelaySessionHandshakeForTest() {
|
||||||
const char* text = std::getenv("MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS");
|
const char* text = std::getenv("MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS");
|
||||||
if (text == nullptr || *text == '\0') return;
|
if (text == nullptr || text[0] == 0) return;
|
||||||
const long ms = std::strtol(text, nullptr, 10);
|
const long ms = std::strtol(text, nullptr, 10);
|
||||||
if (ms <= 0) return;
|
if (ms <= 0) return;
|
||||||
WireLogError("MG_Remote server: pid=%d MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS=%ld - this session child "
|
WireLogError("MG_Remote server: pid=%d MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS=%ld - this session child "
|
||||||
@@ -468,14 +469,25 @@ int RunSession(std::unique_ptr<SocketTransport> control, std::vector<std::uint8_
|
|||||||
// else: inproc keeps every Fatal (client and server share that process), and the supervisor
|
// else: inproc keeps every Fatal (client and server share that process), and the supervisor
|
||||||
// that forked this child never applies a record.
|
// that forked this child never applies a record.
|
||||||
//
|
//
|
||||||
// P12 (D5): and the in-process display server arms it for EVERY session, here, the same way;
|
// P12 (D5): and the in-process display server arms it for EVERY session, here, the same way.
|
||||||
// its supervisor resets it between sessions (ResetSessionLatch), so a session that latched does
|
// P14 S2: "IT" IS PER SESSION NOW - the scope below binds this thread to this session's latch
|
||||||
// not decline the next one.
|
// domain (FatalFunnel.h), so the arm that follows arms THAT domain and no other: a session
|
||||||
|
// that latched cannot decline its neighbour, and the supervisor's ResetSessionLatch touches
|
||||||
|
// only its own (process-default) domain.
|
||||||
|
// P14 S2: THIS SESSION'S OWN RUNTIME, AND BOTH OF ITS THREADS ON IT. The object holds the
|
||||||
|
// session, the apply loop and the PipeInputs block; the scope makes them what this thread's
|
||||||
|
// reads of ServerSessionInstance()/ServerLoopInstance()/gPipeInputs resolve to, and binds this
|
||||||
|
// session's latch domain and segment resolver. Everything below - the backend bring-up, the
|
||||||
|
// arm, the EGL frames - therefore runs against THIS session, which is what lets one process
|
||||||
|
// serve several at once. The apply thread opens the same scope as its first act
|
||||||
|
// (ServerLoop::ApplyThreadMain, via SetRuntime below).
|
||||||
|
auto runtime = std::make_unique<Server::SessionRuntime>();
|
||||||
|
Server::Detail::ThreadSessionScope threadScope(*runtime);
|
||||||
ArmSessionLatch();
|
ArmSessionLatch();
|
||||||
// The child bypasses MobileGL::Initialize: arm its own role counters.
|
// The child bypasses MobileGL::Initialize: arm its own role counters.
|
||||||
MobileGL::MG_Util::PipeStats::Init();
|
MobileGL::MG_Util::PipeStats::Init();
|
||||||
auto& session = Server::ServerSessionInstance();
|
auto& session = runtime->Session();
|
||||||
auto& loop = Server::ServerLoopInstance();
|
auto& loop = runtime->Loop();
|
||||||
// P12 (D5): WHAT A PROCESS EXIT CLEANED UP FOR FREE, THE IN-PROCESS SHAPE CLEANS UP BY HAND,
|
// P12 (D5): WHAT A PROCESS EXIT CLEANED UP FOR FREE, THE IN-PROCESS SHAPE CLEANS UP BY HAND,
|
||||||
// on every way out from here - so the next session in this process starts where a fresh
|
// on every way out from here - so the next session in this process starts where a fresh
|
||||||
// session child would. A no-op for every forked / single-session shape: those `_exit` with the
|
// session child would. A no-op for every forked / single-session shape: those `_exit` with the
|
||||||
@@ -659,6 +671,9 @@ int RunSession(std::unique_ptr<SocketTransport> control, std::vector<std::uint8_
|
|||||||
// below) and is the one that says "still running" while the apply thread runs it. Cleared
|
// below) and is the one that says "still running" while the apply thread runs it. Cleared
|
||||||
// after the loop, on this same thread, so no post can be in flight when it goes.
|
// after the loop, on this same thread, so no post can be in flight when it goes.
|
||||||
loop.SetControlProgressSink(&SendSurfaceProgress, control.get());
|
loop.SetControlProgressSink(&SendSurfaceProgress, control.get());
|
||||||
|
// P14 S2: the apply thread opens the same scope this thread is on, so its reads of the
|
||||||
|
// session, this loop and the PipeInputs block resolve to THIS session's.
|
||||||
|
loop.SetRuntime(*runtime);
|
||||||
if (loop.Start(session) != MOBILEGL_OK) return endSession(70);
|
if (loop.Start(session) != MOBILEGL_OK) return endSession(70);
|
||||||
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready control=%s data=%s",
|
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready control=%s data=%s",
|
||||||
selfPid, tcp ? "tcp" : "unix", tcp ? "stream" : "shm");
|
selfPid, tcp ? "tcp" : "unix", tcp ? "stream" : "shm");
|
||||||
@@ -1327,17 +1342,44 @@ private:
|
|||||||
// shm segments' descriptors cross by SCM_RIGHTS, so there is no DataBind to route and no pre-auth
|
// shm segments' descriptors cross by SCM_RIGHTS, so there is no DataBind to route and no pre-auth
|
||||||
// gate - both are TCP exposure machinery, and a unix peer is local by construction (the forked
|
// gate - both are TCP exposure machinery, and a unix peer is local by construction (the forked
|
||||||
// unix supervisor runs without either). Sessions take the TCP in-process shape's thread hand-off:
|
// unix supervisor runs without either). Sessions take the TCP in-process shape's thread hand-off:
|
||||||
// one at a time, Busy for a second client, the latch reset between sessions, stop by
|
// a thread per session, up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (default 16) and Refuse{Busy}
|
||||||
// g_inProcessStop.
|
// over it, the latch per session, stop by
|
||||||
|
// g_inProcessStop. P14 S2: THE SESSIONS ARE CONCURRENT - each one owns its ServerSession, its
|
||||||
|
// ServerLoop and its PipeInputs block through its own ThreadSessionScope (SessionRuntime.h), so a
|
||||||
|
// session's close, latch or fault reaches no other, and the server's own stop waits for all of
|
||||||
|
// them rather than for the one.
|
||||||
class UnixInProcessSupervisor {
|
class UnixInProcessSupervisor {
|
||||||
public:
|
public:
|
||||||
UnixInProcessSupervisor(int listener, unsigned long& sessionsFaulted)
|
// P14 S2: MAX SESSIONS AT ONCE. Each live session is one control thread (RunSession) plus its
|
||||||
: m_listener(listener), m_pairs(listener), m_sessionsFaulted(sessionsFaulted) {}
|
// own mgl-srv-apply, both carrying their own ServerSession/ServerLoop/PipeInputs block, so the
|
||||||
|
// cap is a resource limit and not a correctness one. The default is the display server's
|
||||||
|
// (mobilegl_server_serve_inprocess), which is where this class is used.
|
||||||
|
static constexpr unsigned kDefaultMaxSessions = 16;
|
||||||
|
|
||||||
|
// MOBILEGL_IPC_INPROC_MAX_SESSIONS, capped, 0/unset/invalid = the default. An environment
|
||||||
|
// knob rather than a new argument to mobilegl_server_serve_inprocess: the embedded ABI has one
|
||||||
|
// parameter (the endpoint) and the display server passes none of this, exactly as it passes
|
||||||
|
// none of the transport knobs.
|
||||||
|
static unsigned MaxSessionsFromEnvironment() {
|
||||||
|
const char* text = std::getenv("MOBILEGL_IPC_INPROC_MAX_SESSIONS");
|
||||||
|
if (text == nullptr || text[0] == 0) return kDefaultMaxSessions;
|
||||||
|
char* end = nullptr;
|
||||||
|
const long parsed = std::strtol(text, &end, 10);
|
||||||
|
if (end == text || parsed <= 0) return kDefaultMaxSessions;
|
||||||
|
return static_cast<unsigned>(std::min<long>(parsed, 256));
|
||||||
|
}
|
||||||
|
|
||||||
|
UnixInProcessSupervisor(int listener, unsigned long& sessionsFaulted,
|
||||||
|
unsigned maxSessions = kDefaultMaxSessions)
|
||||||
|
: m_listener(listener), m_pairs(listener), m_sessionsFaulted(sessionsFaulted),
|
||||||
|
m_maxSessions(maxSessions == 0 ? kDefaultMaxSessions : maxSessions) {}
|
||||||
|
|
||||||
~UnixInProcessSupervisor() {
|
~UnixInProcessSupervisor() {
|
||||||
// Run() only returns with the session thread joined; a listener failure (73) is the
|
// Run() only returns with every session thread joined; a listener failure (73) is the one
|
||||||
// one way out with it live, and it is never left running behind the supervisor.
|
// way out with one live, and a session is never left running behind the supervisor.
|
||||||
if (m_sessionThread.joinable()) m_sessionThread.join();
|
for (auto& session : m_live) {
|
||||||
|
if (session->thread.joinable()) session->thread.join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int Run() {
|
int Run() {
|
||||||
@@ -1352,15 +1394,11 @@ public:
|
|||||||
static_cast<int>(accepted));
|
static_cast<int>(accepted));
|
||||||
return 73;
|
return 73;
|
||||||
}
|
}
|
||||||
// exit_group's close window, as the forked loop's: a client reconnecting the moment
|
// P14 S2: THE CAP, NOT "ONE AT A TIME". A peer over the limit is refused by the same
|
||||||
// its session ended must not be told Busy by a session that is exiting.
|
// words the forked `--max-sessions` supervisor uses, and is not a latch: the sessions
|
||||||
const auto reapDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
|
// already running carry on and the connection is simply closed.
|
||||||
while (m_active > 0 && std::chrono::steady_clock::now() < reapDeadline) {
|
if (m_live.size() >= m_maxSessions) {
|
||||||
if (Reap()) break;
|
RefuseBusy(*control, "the session limit is reached");
|
||||||
::usleep(1000);
|
|
||||||
}
|
|
||||||
if (m_active > 0) {
|
|
||||||
RefuseBusy(*control, "one session is already active");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
StartSessionThread(std::move(control));
|
StartSessionThread(std::move(control));
|
||||||
@@ -1368,48 +1406,71 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// The same reap as the TCP shape's ReapSessionThread: join, count faults, reset the latch
|
// One live session: its control thread, and the two facts the reap needs from it. The session
|
||||||
// for the next session in this process.
|
// itself (its ServerSession, its apply thread, its PipeInputs block) is owned by RunSession's
|
||||||
bool Reap() {
|
// own frame, on that thread - nothing here reaches into it.
|
||||||
if (m_active <= 0 || !m_sessionDone.load(std::memory_order_acquire)) return false;
|
struct LiveSession {
|
||||||
if (m_sessionThread.joinable()) m_sessionThread.join();
|
std::thread thread;
|
||||||
const int code = m_sessionExit.load(std::memory_order_acquire);
|
std::atomic<bool> done{false};
|
||||||
m_active = -1;
|
std::atomic<int> exit{0};
|
||||||
if (code != 0) ++m_sessionsFaulted;
|
unsigned long long ordinal = 0;
|
||||||
ResetSessionLatch();
|
};
|
||||||
WireLogError("MG_Remote server: in-process unix session #%lu pid=%d reaped exit=%d%s sessionsFaulted=%lu",
|
|
||||||
m_sessionOrdinal, static_cast<int>(::getpid()), code,
|
// The same reap as the TCP shape's ReapSessionThread: join, count faults, reset the latch for
|
||||||
code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted);
|
// the next session in this process. Every finished session is reaped, in whatever order they
|
||||||
return true;
|
// finished.
|
||||||
|
void Reap() {
|
||||||
|
for (auto session = m_live.begin(); session != m_live.end();) {
|
||||||
|
LiveSession& live = **session;
|
||||||
|
if (!live.done.load(std::memory_order_acquire)) {
|
||||||
|
++session;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (live.thread.joinable()) live.thread.join();
|
||||||
|
const int code = live.exit.load(std::memory_order_acquire);
|
||||||
|
if (code != 0) ++m_sessionsFaulted;
|
||||||
|
ResetSessionLatch();
|
||||||
|
WireLogError("MG_Remote server: in-process unix session #%llu pid=%d reaped exit=%d%s "
|
||||||
|
"sessionsFaulted=%lu (live %zu)",
|
||||||
|
static_cast<unsigned long long>(live.ordinal),
|
||||||
|
static_cast<int>(::getpid()), code,
|
||||||
|
code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted,
|
||||||
|
m_live.size() - 1);
|
||||||
|
session = m_live.erase(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartSessionThread(std::unique_ptr<SocketTransport> connection) {
|
void StartSessionThread(std::unique_ptr<SocketTransport> connection) {
|
||||||
if (m_sessionThread.joinable()) m_sessionThread.join(); // reaped already; never live here
|
// No hand-off pair and no pre-read Hello: the session reads its own first frame and binds
|
||||||
m_sessionDone.store(false, std::memory_order_release);
|
// its shm segments from the fds the pair's second connection carries.
|
||||||
m_sessionExit.store(0, std::memory_order_release);
|
auto live = std::make_unique<LiveSession>();
|
||||||
++m_sessionOrdinal;
|
live->ordinal = ++m_sessionOrdinal;
|
||||||
try {
|
try {
|
||||||
// No hand-off pair and no pre-read Hello: the session reads its own first frame and
|
live->thread = std::thread([live = live.get(), conn = std::move(connection)]() mutable {
|
||||||
// binds its shm segments from the fds the pair's second connection carries.
|
|
||||||
m_sessionThread = std::thread([this, conn = std::move(connection)]() mutable {
|
|
||||||
const int code = RunSession(std::move(conn), {}, {}, -1, DataSource::None, 10000,
|
const int code = RunSession(std::move(conn), {}, {}, -1, DataSource::None, 10000,
|
||||||
/*inProcess=*/true);
|
/*inProcess=*/true);
|
||||||
m_sessionExit.store(code, std::memory_order_release);
|
live->exit.store(code, std::memory_order_release);
|
||||||
m_sessionDone.store(true, std::memory_order_release);
|
live->done.store(true, std::memory_order_release);
|
||||||
});
|
});
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
WireLogError("MG_Remote server: could not start the in-process unix session thread; the "
|
WireLogError("MG_Remote server: could not start the in-process unix session thread; the "
|
||||||
"connection is closed (it cannot be refused: it moved into the failed thread)");
|
"connection is closed (it cannot be refused: it moved into the failed thread)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_active = ::getpid();
|
m_live.push_back(std::move(live));
|
||||||
WireLogError("MG_Remote server: in-process unix session #%lu started on a thread (pid=%d)",
|
WireLogError("MG_Remote server: in-process unix session #%llu started on a thread (pid=%d, "
|
||||||
m_sessionOrdinal, static_cast<int>(::getpid()));
|
"%zu live of at most %u; %u accepted in this process)",
|
||||||
|
static_cast<unsigned long long>(m_sessionOrdinal),
|
||||||
|
static_cast<int>(::getpid()), m_live.size(), m_maxSessions,
|
||||||
|
Server::LiveServerSessionCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
int StopServing() {
|
int StopServing() {
|
||||||
while (m_active > 0) {
|
// P14 S2: EVERY live session, not one. Each reads g_inProcessStop on its own control
|
||||||
if (!Reap()) ::usleep(10000);
|
// slice, so this is the same orderly end the single-session shape had, N times over.
|
||||||
|
while (!m_live.empty()) {
|
||||||
|
Reap();
|
||||||
|
if (!m_live.empty()) ::usleep(10000);
|
||||||
}
|
}
|
||||||
WireLogError("MG_Remote server: pid=%d in-process display server stopped", static_cast<int>(::getpid()));
|
WireLogError("MG_Remote server: pid=%d in-process display server stopped", static_cast<int>(::getpid()));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1419,11 +1480,10 @@ private:
|
|||||||
// The connections that have not been paired yet live here between Accept calls.
|
// The connections that have not been paired yet live here between Accept calls.
|
||||||
Server::PairAcceptor m_pairs;
|
Server::PairAcceptor m_pairs;
|
||||||
unsigned long& m_sessionsFaulted;
|
unsigned long& m_sessionsFaulted;
|
||||||
std::thread m_sessionThread;
|
unsigned m_maxSessions;
|
||||||
std::atomic<bool> m_sessionDone{false};
|
// P14 S2: every live session, not one. Reaped by Run()'s Reap and by StopServing.
|
||||||
std::atomic<int> m_sessionExit{0};
|
std::vector<std::unique_ptr<LiveSession>> m_live;
|
||||||
unsigned long m_sessionOrdinal = 0;
|
unsigned long long m_sessionOrdinal = 0;
|
||||||
pid_t m_active = -1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The server's environment preconditions, shared by the exec'd supervisor and the in-process
|
// The server's environment preconditions, shared by the exec'd supervisor and the in-process
|
||||||
@@ -1638,7 +1698,8 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_serve_inpr
|
|||||||
TcpSupervisor::Handoff::Thread, &g_inProcessStop);
|
TcpSupervisor::Handoff::Thread, &g_inProcessStop);
|
||||||
result = supervisor.Run();
|
result = supervisor.Run();
|
||||||
} else {
|
} else {
|
||||||
UnixInProcessSupervisor supervisor(listener, sessionsFaulted);
|
UnixInProcessSupervisor supervisor(listener, sessionsFaulted,
|
||||||
|
UnixInProcessSupervisor::MaxSessionsFromEnvironment());
|
||||||
result = supervisor.Run();
|
result = supervisor.Run();
|
||||||
}
|
}
|
||||||
LogSupervisorSummary(sessionsFaulted);
|
LogSupervisorSummary(sessionsFaulted);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "../Transport/SocketTransport.h"
|
#include "../Transport/SocketTransport.h"
|
||||||
#include "ServerSession.h"
|
#include "ServerSession.h"
|
||||||
|
#include "SessionRuntime.h"
|
||||||
#include <MG_Remote/FatalFunnel.h>
|
#include <MG_Remote/FatalFunnel.h>
|
||||||
|
|
||||||
#include "../CapsCodec.h"
|
#include "../CapsCodec.h"
|
||||||
@@ -528,11 +529,24 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// One callback table and one process segment resolver: an in-progress Accept
|
// One callback table and one process segment resolver: an in-progress Accept
|
||||||
// reserves their owner too. CAS closes the race between distinct session objects
|
// reserves their owner too. CAS closes the race between distinct session objects
|
||||||
// without making concurrent sessions a supported mode.
|
// without making concurrent sessions a supported mode.
|
||||||
|
//
|
||||||
|
// P14 S2: THE TABLE IS STILL ONE PER PROCESS, and that is now deliberate rather than a
|
||||||
|
// limit. Its entries are the same five functions for every session; what makes them route
|
||||||
|
// to the right session is that they resolve it per calling thread
|
||||||
|
// (ServerSession::Active()), and every one of them is called by a backend that runs on a
|
||||||
|
// session's own apply thread. So the owner word below is a CLAIM on a shared, idempotent
|
||||||
|
// installation, not a claim on "the session".
|
||||||
std::atomic<ServerSession*> g_sessionOwner{nullptr};
|
std::atomic<ServerSession*> g_sessionOwner{nullptr};
|
||||||
|
// P14 S2: accepted and not yet closed. The claim above is released by a Close only when
|
||||||
|
// this reaches zero - one session ending may not uninstall the table another is using.
|
||||||
|
std::atomic<Uint32> g_liveSessions{0};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
Uint32 ServerSessionCount() { return g_liveSessions.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
ServerSession& ServerSessionInstance() {
|
ServerSession& ServerSessionInstance() {
|
||||||
|
if (ServerSession* const own = Detail::CurrentSession()) return *own;
|
||||||
// Leak at exit, deliberately and per ID-8: frontend destructors reach pipe and backend
|
// Leak at exit, deliberately and per ID-8: frontend destructors reach pipe and backend
|
||||||
// state from exit handlers, and a session destroyed before them would be a
|
// state from exit handlers, and a session destroyed before them would be a
|
||||||
// use-after-free rather than a tidy teardown.
|
// use-after-free rather than a tidy teardown.
|
||||||
@@ -540,7 +554,10 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
return *instance;
|
return *instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerSession* ServerSession::Active() { return g_active.load(std::memory_order_acquire); }
|
ServerSession* ServerSession::Active() {
|
||||||
|
if (ServerSession* const own = Detail::CurrentSession()) return own;
|
||||||
|
return g_active.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
ServerSession::ServerSession() {
|
ServerSession::ServerSession() {
|
||||||
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer);
|
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer);
|
||||||
@@ -584,8 +601,17 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
|
|
||||||
MobileGLResult ServerSession::Accept(Transport::ITransport& transport,
|
MobileGLResult ServerSession::Accept(Transport::ITransport& transport,
|
||||||
const std::vector<Uint8>* firstFrame) {
|
const std::vector<Uint8>* firstFrame) {
|
||||||
|
// P14 S2: A SERVED SESSION DOES NOT CLAIM THE PROCESS TABLE, because the table is SHARED.
|
||||||
|
// Its entries are the same five functions for every session and they route by the calling
|
||||||
|
// thread (ServerSession::Active()), so a second session installing them is the idempotent
|
||||||
|
// repeat InstallReverseCallback already allows - and a second session REFUSING to install
|
||||||
|
// them is what a one-session process would have. The claim below therefore belongs to the
|
||||||
|
// shapes whose process really is one session (the inproc client+server role, FvSession and
|
||||||
|
// its peers in ServerLoopTest): there a second ServerSession object is still the named
|
||||||
|
// Fatal{callback-double-install} the single-session ruling asks for.
|
||||||
|
const Bool sharedTable = Detail::CurrentSession() == this;
|
||||||
ServerSession* expectedOwner = nullptr;
|
ServerSession* expectedOwner = nullptr;
|
||||||
if (!g_sessionOwner.compare_exchange_strong(expectedOwner, this,
|
if (!sharedTable && !g_sessionOwner.compare_exchange_strong(expectedOwner, this,
|
||||||
std::memory_order_acq_rel, std::memory_order_acquire)) {
|
std::memory_order_acq_rel, std::memory_order_acquire)) {
|
||||||
if (expectedOwner == this) return MOBILEGL_ERR_INVALID_ARGUMENT;
|
if (expectedOwner == this) return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||||
SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - another "
|
SessionFail(MGFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - another "
|
||||||
@@ -702,6 +728,19 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
m_t0Stores = m_t0Bytes = m_t0Declined = m_t0Refusals = 0;
|
m_t0Stores = m_t0Bytes = m_t0Declined = m_t0Refusals = 0;
|
||||||
m_adoptInbox.Clear();
|
m_adoptInbox.Clear();
|
||||||
m_adoptInbox.Attach(&transport);
|
m_adoptInbox.Attach(&transport);
|
||||||
|
// P14 S1: the context registry is the session's, so it is emptied with the rest of the
|
||||||
|
// per-session state this Accept resets. A session that never crossed a bind_context
|
||||||
|
// leaves the current token at 0, which is the single-context shape's answer.
|
||||||
|
//
|
||||||
|
// P14 S5: the APPLIERS of those contexts go with them - one registry entry per context
|
||||||
|
// and one per share group, released here because a session that is re-accepted must not
|
||||||
|
// inherit the previous client's object records. Accept can run on the control thread
|
||||||
|
// while nothing is applying (this is the accept path, before the first Hello is
|
||||||
|
// answered), so the release is safe here in a way it would not be mid-session.
|
||||||
|
MG_Pipe::MGPipeApplierReleaseSession(reinterpret_cast<Uint64>(this));
|
||||||
|
m_contexts.clear();
|
||||||
|
m_currentContextToken.store(0, std::memory_order_release);
|
||||||
|
m_contextsCreated = m_contextsDestroyed = m_contextBinds = 0;
|
||||||
|
|
||||||
// ---- 3. the four segments, both control pages, the rings.
|
// ---- 3. the four segments, both control pages, the rings.
|
||||||
MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED;
|
MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED;
|
||||||
@@ -844,6 +883,10 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
m_segments.InstallProcessResolver();
|
m_segments.InstallProcessResolver();
|
||||||
|
|
||||||
m_accepted = true;
|
m_accepted = true;
|
||||||
|
m_closed = false;
|
||||||
|
// P14 S2: this session is live. The reverse-channel table and the segment resolver are
|
||||||
|
// process-wide and shared; they are released by whichever Close finds this back at zero.
|
||||||
|
g_liveSessions.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
|
||||||
// ---- P5c ev: the four reverse-channel producers are THIS session's (CONTRACT-P5C
|
// ---- P5c ev: the four reverse-channel producers are THIS session's (CONTRACT-P5C
|
||||||
// §4.1), installed before the apply thread can apply a record that produces one and
|
// §4.1), installed before the apply thread can apply a record that produces one and
|
||||||
@@ -1006,29 +1049,41 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ServerSession::Close() {
|
void ServerSession::Close() {
|
||||||
|
// P14 S2: idempotent (ServerSession.h). A served session is a stack object whose owner
|
||||||
|
// closes it explicitly and whose destructor closes it again.
|
||||||
|
if (m_closed) return;
|
||||||
|
m_closed = true;
|
||||||
ServerSession* expectedActive = this;
|
ServerSession* expectedActive = this;
|
||||||
g_active.compare_exchange_strong(expectedActive, nullptr,
|
g_active.compare_exchange_strong(expectedActive, nullptr,
|
||||||
std::memory_order_acq_rel, std::memory_order_acquire);
|
std::memory_order_acq_rel, std::memory_order_acquire);
|
||||||
if (m_accepted) {
|
if (m_accepted) {
|
||||||
// Uninstall AFTER the apply thread has joined, never before: a record still in
|
// P14 S2: DROP THIS SESSION FIRST, THEN ASK WHETHER ANY IS LEFT. The two process-wide
|
||||||
// flight can still resolve a segment offset (table 3's fourth column).
|
// hooks below (the segment resolver and the reverse-channel table) are shared by every
|
||||||
Wire::SegmentTable::UninstallProcessResolver();
|
// session in the process, so a session that ends while another is live must leave both
|
||||||
// The reverse-channel producers go with the session that owns them - release
|
// installed - its own threads are done with them (the apply thread is joined above and
|
||||||
// only the entries that are still OURS, never one a later owner installed.
|
// this control thread unbinds its own binding on the way out of RunSession).
|
||||||
if (MG_Pipe::gMGPipeCallbacks.OnGlError == &ServerOnGlError) {
|
const Uint32 remaining = g_liveSessions.fetch_sub(1, std::memory_order_acq_rel) - 1;
|
||||||
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
|
if (remaining == 0) {
|
||||||
}
|
// Uninstall AFTER the apply thread has joined, never before: a record still in
|
||||||
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) {
|
// flight can still resolve a segment offset (table 3's fourth column).
|
||||||
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = nullptr;
|
Wire::SegmentTable::UninstallProcessResolver();
|
||||||
}
|
// The reverse-channel producers go with the session that owns them - release
|
||||||
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == &ServerOnGpuWritten) {
|
// only the entries that are still OURS, never one a later owner installed.
|
||||||
MG_Pipe::gMGPipeCallbacks.OnGpuWritten = nullptr;
|
if (MG_Pipe::gMGPipeCallbacks.OnGlError == &ServerOnGlError) {
|
||||||
}
|
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
|
||||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == &ServerOnSurfaceChanged) {
|
}
|
||||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged = nullptr;
|
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) {
|
||||||
}
|
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = nullptr;
|
||||||
if (MG_Pipe::gMGPipeEventRingCapacityBytes == &ServerEventRingCapacityBytes) {
|
}
|
||||||
MG_Pipe::gMGPipeEventRingCapacityBytes = nullptr;
|
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == &ServerOnGpuWritten) {
|
||||||
|
MG_Pipe::gMGPipeCallbacks.OnGpuWritten = nullptr;
|
||||||
|
}
|
||||||
|
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == &ServerOnSurfaceChanged) {
|
||||||
|
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged = nullptr;
|
||||||
|
}
|
||||||
|
if (MG_Pipe::gMGPipeEventRingCapacityBytes == &ServerEventRingCapacityBytes) {
|
||||||
|
MG_Pipe::gMGPipeEventRingCapacityBytes = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// P11 B2: the T0 arm's proof for this session, then the inbox's leftovers (an Offer whose
|
// P11 B2: the T0 arm's proof for this session, then the inbox's leftovers (an Offer whose
|
||||||
@@ -1205,6 +1260,61 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- P14 S1: the context registry ------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ALL THREE RUN ON THE APPLY THREAD. CreateContext/DestroyContext are the control plane's
|
||||||
|
// frames, dispatched through RunSurfaceControlFrame; BindContext is a ring record, dispatched
|
||||||
|
// through the decoder's verb sink. Neither ever runs anywhere else, which is what makes the
|
||||||
|
// map lock-free here.
|
||||||
|
Bool ServerSession::CreateContext(Uint64 token, Uint64 shareGroupToken, Uint32 flags) {
|
||||||
|
if (token == 0) return false;
|
||||||
|
if (m_contexts.find(token) != m_contexts.end()) return false;
|
||||||
|
ContextRuntime runtime;
|
||||||
|
runtime.Token = token;
|
||||||
|
runtime.ShareGroupToken = shareGroupToken;
|
||||||
|
runtime.Flags = flags;
|
||||||
|
m_contexts.emplace(token, runtime);
|
||||||
|
++m_contextsCreated;
|
||||||
|
// P14 S5: and the applier this context's records are applied through - its own working
|
||||||
|
// state, in the share group `shareGroupToken` names (0 = one it starts itself). The key
|
||||||
|
// is THIS object's address, which is what the session's key resolver
|
||||||
|
// (SessionRuntime.cpp) hands back, so registration and lookup agree by construction.
|
||||||
|
// Registering here rather than inferring at lookup time is what keeps the resolver a
|
||||||
|
// two-word read: this runs on the apply thread, where the frame channel delivers, and
|
||||||
|
// is the only writer of the contexts map.
|
||||||
|
MG_Pipe::MGPipeApplierRegisterContext(reinterpret_cast<Uint64>(this), token, shareGroupToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerSession::DestroyContext(Uint64 token) {
|
||||||
|
if (token == 0) return false;
|
||||||
|
const auto it = m_contexts.find(token);
|
||||||
|
if (it == m_contexts.end()) return false;
|
||||||
|
m_contexts.erase(it);
|
||||||
|
++m_contextsDestroyed;
|
||||||
|
// P14 S5: the working state goes with it, and this group's object records go when its
|
||||||
|
// last context does.
|
||||||
|
MG_Pipe::MGPipeApplierUnregisterContext(reinterpret_cast<Uint64>(this), token);
|
||||||
|
// A destroyed context cannot stay current: leaving the token bound would attribute every
|
||||||
|
// record after the destroy to a context the session no longer has. The client's own
|
||||||
|
// eglDestroyContext is refused while the context is current (EGL_BAD_ACCESS), so this
|
||||||
|
// arm is reached with the token already released in the well-behaved sequence - and with
|
||||||
|
// the release MISSING it is still the safe direction.
|
||||||
|
Uint64 expected = token;
|
||||||
|
m_currentContextToken.compare_exchange_strong(expected, 0, std::memory_order_acq_rel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerSession::BindContext(Uint64 token) {
|
||||||
|
// 0 is the release edge and is always legal: the client has context A current, releases
|
||||||
|
// it, and a verb on the app thread follows - the session binds "nothing" rather than
|
||||||
|
// answering to a stale token.
|
||||||
|
if (token != 0 && m_contexts.find(token) == m_contexts.end()) return false;
|
||||||
|
m_currentContextToken.store(token, std::memory_order_release);
|
||||||
|
++m_contextBinds;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Transport::RingConsumer& ServerSession::CommandRing() { return *m_commands; }
|
Transport::RingConsumer& ServerSession::CommandRing() { return *m_commands; }
|
||||||
|
|
||||||
Transport::RingControl& ServerSession::Control() {
|
Transport::RingControl& ServerSession::Control() {
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
|
|
||||||
class ServerSession {
|
class ServerSession {
|
||||||
public:
|
public:
|
||||||
|
// P14 S2: THE SESSION OF THE CALLING THREAD, else the process's active one. Every
|
||||||
|
// server-role caller that matters (the reverse-channel producers, the applier's role
|
||||||
|
// guards, the decoder, ServerMain's control loop) runs on a session's own thread and
|
||||||
|
// therefore gets ITS session - which is what makes one process able to serve several.
|
||||||
static ServerSession* Active();
|
static ServerSession* Active();
|
||||||
|
|
||||||
ServerSession();
|
ServerSession();
|
||||||
@@ -190,6 +194,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Bool PeerSharedSegments() const { return m_peerSharedSegments; }
|
Bool PeerSharedSegments() const { return m_peerSharedSegments; }
|
||||||
// Teardown: after the apply thread has been joined, never before - a record still in
|
// Teardown: after the apply thread has been joined, never before - a record still in
|
||||||
// flight can still resolve a segment offset (table 3's fourth column).
|
// flight can still resolve a segment offset (table 3's fourth column).
|
||||||
|
//
|
||||||
|
// P14 S2: AND IT IS IDEMPOTENT, because a served session is now a stack object. A session
|
||||||
|
// that has been closed once (RunSession closes it explicitly) is closed again by its
|
||||||
|
// destructor; the second call must not run the teardown twice. Accept re-opens it, so the
|
||||||
|
// in-process display server's accept/close/accept cycle is unchanged.
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
Transport::SessionSegments& Shm();
|
Transport::SessionSegments& Shm();
|
||||||
@@ -322,6 +331,45 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Bool AdoptT0Quiet() const { return m_adoptT0Quiet; }
|
Bool AdoptT0Quiet() const { return m_adoptT0Quiet; }
|
||||||
AdoptInbox& T0Inbox() { return m_adoptInbox; }
|
AdoptInbox& T0Inbox() { return m_adoptInbox; }
|
||||||
|
|
||||||
|
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): the session's contexts -
|
||||||
|
//
|
||||||
|
// THE PLACEHOLDER TABLE. `CreateContext`/`DestroyContext` on the control plane build and
|
||||||
|
// destroy an entry per client token; `BindContext` on the ring makes one of them the
|
||||||
|
// session's current context. What lives in an entry in THIS slice is the token and the
|
||||||
|
// share group it was derived from - no native EGLContext, no GLContext, no working state
|
||||||
|
// (that is S4's). The table exists so that a token the client names can be proved to
|
||||||
|
// have been created before anything is attributed to it, and so that the S4 slice has a
|
||||||
|
// place to put the runtime it will own.
|
||||||
|
//
|
||||||
|
// WRITES ARE THE APPLY THREAD'S. CreateContext/DestroyContext arrive as control frames
|
||||||
|
// dispatched through the frame channel, BindContext as a record through the decoder, and
|
||||||
|
// both run on mgl-srv-apply - so the table needs no lock. CurrentContextToken() is
|
||||||
|
// atomic because a test reads it from the case's own thread.
|
||||||
|
struct ContextRuntime {
|
||||||
|
Uint64 Token = 0; // the client's context token, dense from 1
|
||||||
|
Uint64 ShareGroupToken = 0; // 0 = this context started a share group
|
||||||
|
Uint32 Flags = 0; // reserved; the control frame's `flags` field, carried verbatim
|
||||||
|
};
|
||||||
|
|
||||||
|
// False when a context with this token already exists, or when the token is 0 (0 is
|
||||||
|
// "no context" everywhere on both planes and is never a creatable context).
|
||||||
|
Bool CreateContext(Uint64 token, Uint64 shareGroupToken, Uint32 flags);
|
||||||
|
// False when no such token exists - a destroy the peer did not create is the control
|
||||||
|
// frame's own refusal to make, not a silent no-op.
|
||||||
|
Bool DestroyContext(Uint64 token);
|
||||||
|
// Binds the session's current context. Token 0 is legal and means "no context" (the
|
||||||
|
// release edge); any other token must have been created, or this is a named refusal.
|
||||||
|
Bool BindContext(Uint64 token);
|
||||||
|
// 0 = no context is bound. Every record the session has applied so far belongs to this
|
||||||
|
// token; a session that never crossed a bind_context has never bound one.
|
||||||
|
Uint64 CurrentContextToken() const { return m_currentContextToken.load(std::memory_order_acquire); }
|
||||||
|
Bool HasContext(Uint64 token) const { return m_contexts.find(token) != m_contexts.end(); }
|
||||||
|
Uint64 ContextCount() const { return static_cast<Uint64>(m_contexts.size()); }
|
||||||
|
Uint64 ContextsCreated() const { return m_contextsCreated; }
|
||||||
|
Uint64 ContextsDestroyed() const { return m_contextsDestroyed; }
|
||||||
|
// The per-session tallies the sink keeps for the two control frames it dispatched.
|
||||||
|
Uint64 ContextBinds() const { return m_contextBinds; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::ILink& link,
|
MobileGLResult BindDataConnection(Transport::ITransport& control, Transport::ILink& link,
|
||||||
const Uint8* nonce);
|
const Uint8* nonce);
|
||||||
@@ -347,6 +395,9 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Bool m_consumedSet = false;
|
Bool m_consumedSet = false;
|
||||||
Bool m_sizesSet = false;
|
Bool m_sizesSet = false;
|
||||||
Bool m_accepted = false;
|
Bool m_accepted = false;
|
||||||
|
// P14 S2: Close() has run for the current accept. Cleared by Accept, which is what keeps
|
||||||
|
// the in-process display server's accept/close/accept cycle working.
|
||||||
|
Bool m_closed = false;
|
||||||
ApplyPeer m_peer = ApplyPeer::InProcess;
|
ApplyPeer m_peer = ApplyPeer::InProcess;
|
||||||
Bool m_peerSharedSegments = true;
|
Bool m_peerSharedSegments = true;
|
||||||
// PH-6. Written by the apply thread (the only SEG_EVENT producer), reset by Accept.
|
// PH-6. Written by the apply thread (the only SEG_EVENT producer), reset by Accept.
|
||||||
@@ -369,10 +420,27 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint64 m_t0Bytes = 0;
|
Uint64 m_t0Bytes = 0;
|
||||||
Uint64 m_t0Declined = 0;
|
Uint64 m_t0Declined = 0;
|
||||||
Uint64 m_t0Refusals = 0;
|
Uint64 m_t0Refusals = 0;
|
||||||
|
// P14 S1. Apply-thread-only map (see ContextRuntime); the current token is atomic for
|
||||||
|
// the reader on a test thread. Reset by Accept.
|
||||||
|
UnorderedMap<Uint64, ContextRuntime> m_contexts;
|
||||||
|
std::atomic<Uint64> m_currentContextToken{0};
|
||||||
|
Uint64 m_contextsCreated = 0;
|
||||||
|
Uint64 m_contextsDestroyed = 0;
|
||||||
|
Uint64 m_contextBinds = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may
|
// Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may
|
||||||
// reach pipe or backend state from an exit handler.
|
// reach pipe or backend state from an exit handler.
|
||||||
|
//
|
||||||
|
// P14 S2 (SessionRuntime.h): THE SINGLETON IS THE FALLBACK, NOT THE SESSION. On a thread that
|
||||||
|
// belongs to a served session (ServerMain::RunSession's control thread, or that session's
|
||||||
|
// mgl-srv-apply) this answers with THAT session; every other thread - the client process, the
|
||||||
|
// inproc one-process client+server shape, a unit case - keeps getting the leaked instance.
|
||||||
ServerSession& ServerSessionInstance();
|
ServerSession& ServerSessionInstance();
|
||||||
|
|
||||||
|
// P14 S2: sessions accepted and not yet closed in this process. A process-wide fact that must
|
||||||
|
// outlive one session (the reverse-channel callback table, the segment resolver) is released
|
||||||
|
// only when this reaches zero.
|
||||||
|
Uint32 ServerSessionCount();
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Server
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/SessionRuntime.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P14 S2: the per-session runtime and the thread routing (SessionRuntime.h states the shape).
|
||||||
|
|
||||||
|
#include "SessionRuntime.h"
|
||||||
|
|
||||||
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||||
|
#include <MG_Remote/FatalFunnel.h>
|
||||||
|
#include <MG_Remote/Wire/PipeWireCodec.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The calling thread's session. nullptr on every thread that is not one of a session's own
|
||||||
|
// two, which is the whole of the fallback rule.
|
||||||
|
thread_local SessionRuntime* t_runtime = nullptr;
|
||||||
|
|
||||||
|
// The resolver MG_Pipe asks per thread. Installing it is a single store of a function
|
||||||
|
// pointer that is never replaced, so it needs no lock and no teardown: the server role
|
||||||
|
// lives for the process (ID-8).
|
||||||
|
MG_Pipe::PipeInputs* ResolveThreadInputs() {
|
||||||
|
SessionRuntime* runtime = t_runtime;
|
||||||
|
return runtime != nullptr ? &runtime->Inputs() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE TUPLE'S KEY. The
|
||||||
|
// backend keys its per-context native state - the EGLContext, its config, the draw/read
|
||||||
|
// binding - on (session, client context token); this is the one place that answers the pair
|
||||||
|
// for the calling thread. The session half is the runtime's ADDRESS, which is stable for the
|
||||||
|
// session's whole life and unique among live sessions in the process; the token half is what
|
||||||
|
// `bind_context` last set on the ring. False (== the {0,0} key) on any thread that is not a
|
||||||
|
// session's own, which is the client process, the inproc one-process shape and every unit
|
||||||
|
// case - i.e. exactly the single-context world the backend still serves unchanged.
|
||||||
|
Bool ResolveThreadNativeContextKey(Uint64* outSessionKey, Uint64* outContextToken) {
|
||||||
|
SessionRuntime* runtime = t_runtime;
|
||||||
|
if (runtime == nullptr) return false;
|
||||||
|
*outSessionKey = reinterpret_cast<Uint64>(runtime);
|
||||||
|
*outContextToken = runtime->Session().CurrentContextToken();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md). WHOSE APPLIER IS THIS THREAD
|
||||||
|
// IN? The same two-word answer as the native key above, and deliberately the same
|
||||||
|
// spelling: the session half is the ServerSession's own address - which is the key the
|
||||||
|
// registration side uses (ServerSession::CreateContext passes `this`), so the two agree
|
||||||
|
// by construction rather than by two conventions - and the token half is what
|
||||||
|
// `bind_context` last set on this session's ring. A thread that is not a session's
|
||||||
|
// answers false and gets the process-wide applier, i.e. the single-context world every
|
||||||
|
// other shape in this tree still runs.
|
||||||
|
Bool ResolveThreadApplierKey(MG_Pipe::MGPipeApplierKey* outKey) {
|
||||||
|
SessionRuntime* runtime = t_runtime;
|
||||||
|
if (runtime == nullptr) return false;
|
||||||
|
outKey->SessionKey = reinterpret_cast<Uint64>(&runtime->Session());
|
||||||
|
outKey->ContextToken = runtime->Session().CurrentContextToken();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Detail::InstallSessionRuntimeHooks() {
|
||||||
|
if (MG_Pipe::g_pipeInputsThreadResolver != &ResolveThreadInputs) {
|
||||||
|
MG_Pipe::g_pipeInputsThreadResolver = &ResolveThreadInputs;
|
||||||
|
}
|
||||||
|
MG_Backend::DirectGLES::SetNativeContextKeyResolver(&ResolveThreadNativeContextKey);
|
||||||
|
MG_Pipe::MGPipeSetApplierKeyResolver(&ResolveThreadApplierKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionRuntime* Detail::CurrentRuntime() { return t_runtime; }
|
||||||
|
|
||||||
|
ServerSession* Detail::CurrentSession() {
|
||||||
|
SessionRuntime* runtime = t_runtime;
|
||||||
|
return runtime != nullptr ? &runtime->Session() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerLoop* Detail::CurrentLoop() {
|
||||||
|
SessionRuntime* runtime = t_runtime;
|
||||||
|
return runtime != nullptr ? &runtime->Loop() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Pipe::PipeInputs* Detail::CurrentInputs() { return ResolveThreadInputs(); }
|
||||||
|
|
||||||
|
Detail::ThreadSessionScope::ThreadSessionScope(SessionRuntime& runtime) {
|
||||||
|
InstallSessionRuntimeHooks();
|
||||||
|
m_previous = t_runtime;
|
||||||
|
t_runtime = &runtime;
|
||||||
|
m_previousLatchDomain = FatalLatchDomainBind(runtime.LatchDomain());
|
||||||
|
// The segment resolver is the OTHER process-wide hook that used to mean "one session": the
|
||||||
|
// thunk is one function pointer for the process, and which TABLE it answers from is now
|
||||||
|
// per thread (PipeWireCodec.h). Bound before anything on this thread can decode a record.
|
||||||
|
runtime.Session().Segments().BindThreadResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
Detail::ThreadSessionScope::~ThreadSessionScope() {
|
||||||
|
if (m_previous != nullptr) {
|
||||||
|
m_previous->Session().Segments().BindThreadResolver();
|
||||||
|
} else {
|
||||||
|
Wire::SegmentTable::UnbindThreadResolver();
|
||||||
|
}
|
||||||
|
FatalLatchDomainBind(m_previousLatchDomain);
|
||||||
|
t_runtime = m_previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionRuntime::SessionRuntime()
|
||||||
|
: m_latchDomain(FatalLatchDomainOpen()) {}
|
||||||
|
|
||||||
|
SessionRuntime::~SessionRuntime() {
|
||||||
|
// P14 S5: this runtime's session is the key every applier it registered was filed
|
||||||
|
// under, so the last act of the session is to drop them. Safe here for the reason the
|
||||||
|
// latch domain is (SessionLoop::Stop has joined the apply thread; the control thread's
|
||||||
|
// scope outlives this object), and idempotent against Accept's own release.
|
||||||
|
MG_Pipe::MGPipeApplierReleaseSession(reinterpret_cast<Uint64>(&m_session));
|
||||||
|
// No thread may still be bound to this domain: the apply thread is joined by
|
||||||
|
// ServerLoop::Stop and the control thread's scope outlives this object.
|
||||||
|
FatalLatchDomainClose(m_latchDomain);
|
||||||
|
m_latchDomain = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 LiveServerSessionCount() { return ServerSessionCount(); }
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/SessionRuntime.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P14 S2 (docs/Disaggregated/design/11-state-ownership.md): ONE SERVED SESSION'S PROCESS-LOCAL
|
||||||
|
// STATE, and the routing that lets the rest of the tree keep spelling the singletons.
|
||||||
|
//
|
||||||
|
// WHAT THE SINGLETONS WERE. ServerSessionInstance()/ServerLoopInstance() were the session, so one
|
||||||
|
// session per process was a property of every file that named them. This slice gives each served
|
||||||
|
// session its own {ServerSession, ServerLoop, PipeInputs} triple, held here, and resolves the two
|
||||||
|
// singleton accessors through the CALLING THREAD's session - which is what makes the ~900 sites
|
||||||
|
// that spell gPipeInputs, MGB_CTX, ServerSession::Active() or ServerLoopInstance() find the right
|
||||||
|
// one without one line of any of them moving.
|
||||||
|
//
|
||||||
|
// WHO IS A SESSION THREAD. Two threads per session, both created by the session itself:
|
||||||
|
// * the CONTROL thread - ServerMain::RunSession's own thread, which reads the Hello, drives the
|
||||||
|
// blocking control pump and ends the session (a std::thread per session in the supervisors, a
|
||||||
|
// forked process in the `--serve` worker shape);
|
||||||
|
// * the APPLY thread - ServerLoop's mgl-srv-apply, which applies the ring and owns the backend.
|
||||||
|
// ThreadSessionScope is installed on both, first thing. A thread with no scope resolves to the
|
||||||
|
// process-wide defaults, which is exactly the old behaviour and is what the CLIENT process, the
|
||||||
|
// inproc one-process client+server shape and every unit case keep getting.
|
||||||
|
//
|
||||||
|
// THE INPROC CLIENT+SERVER SHAPE IS DELIBERATELY UNTOUCHED. ClientSession::Start calls
|
||||||
|
// ServerLoopInstance().Start(ServerSessionInstance()) and never opens a scope, so its applier and
|
||||||
|
// its app thread still share the one process-wide PipeInputs block - the verb barrier's premise
|
||||||
|
// (one block two roles alternate on) is a property of that shape and this file must not break it.
|
||||||
|
// Only ServerMain::RunSession, which serves a session with its own transport, opens a scope.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||||
|
|
||||||
|
#include "ServerLoop.h"
|
||||||
|
#include "ServerSession.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
class SessionRuntime {
|
||||||
|
public:
|
||||||
|
SessionRuntime();
|
||||||
|
~SessionRuntime();
|
||||||
|
SessionRuntime(const SessionRuntime&) = delete;
|
||||||
|
SessionRuntime& operator=(const SessionRuntime&) = delete;
|
||||||
|
|
||||||
|
ServerSession& Session() { return m_session; }
|
||||||
|
ServerLoop& Loop() { return m_loop; }
|
||||||
|
MG_Pipe::PipeInputs& Inputs() { return m_inputs; }
|
||||||
|
|
||||||
|
// The latch domain this session owns (FatalFunnel.h). A named fault latched by one session
|
||||||
|
// must not end another, and must not be cleared by another's reap.
|
||||||
|
int LatchDomain() const { return m_latchDomain; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ServerSession m_session;
|
||||||
|
ServerLoop m_loop;
|
||||||
|
MG_Pipe::PipeInputs m_inputs;
|
||||||
|
int m_latchDomain = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace Detail {
|
||||||
|
// The session of the CALLING thread, or nullptr when it is not a session's own thread.
|
||||||
|
SessionRuntime* CurrentRuntime();
|
||||||
|
ServerSession* CurrentSession();
|
||||||
|
ServerLoop* CurrentLoop();
|
||||||
|
// What MG_Pipe's per-thread resolver asks for: the calling thread's block, or nullptr for
|
||||||
|
// "use the process-wide one".
|
||||||
|
MG_Pipe::PipeInputs* CurrentInputs();
|
||||||
|
|
||||||
|
// Installs the process-wide hooks the per-session blocks need (the PipeInputs resolver).
|
||||||
|
// Idempotent, and called by ThreadSessionScope itself so no bring-up order can miss it.
|
||||||
|
void InstallSessionRuntimeHooks();
|
||||||
|
|
||||||
|
// THE SCOPE. Makes `runtime` the calling thread's session for as long as it lives: the
|
||||||
|
// session/loop the singleton accessors answer with, the PipeInputs block MGB_CTX reads,
|
||||||
|
// this session's segment resolver and this session's latch domain. Restores what was there
|
||||||
|
// before on the way out (two nested scopes are not a shape anything uses, but a thread that
|
||||||
|
// served one session and is then reused must not keep answering with the dead one).
|
||||||
|
class ThreadSessionScope {
|
||||||
|
public:
|
||||||
|
explicit ThreadSessionScope(SessionRuntime& runtime);
|
||||||
|
~ThreadSessionScope();
|
||||||
|
ThreadSessionScope(const ThreadSessionScope&) = delete;
|
||||||
|
ThreadSessionScope& operator=(const ThreadSessionScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SessionRuntime* m_previous = nullptr;
|
||||||
|
int m_previousLatchDomain = 0;
|
||||||
|
};
|
||||||
|
} // namespace Detail
|
||||||
|
|
||||||
|
// Sessions accepted and not yet closed in THIS process. 1 is the old shape; more is this
|
||||||
|
// slice's. ServerSession::Close gates the process-wide hooks (the reverse-channel table, the
|
||||||
|
// segment resolver) on it, and ServerMain logs it beside each session it starts.
|
||||||
|
Uint32 LiveServerSessionCount();
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -24,6 +24,8 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
case SurfaceControlOp::ReleaseResources: return "ReleaseResources";
|
case SurfaceControlOp::ReleaseResources: return "ReleaseResources";
|
||||||
case SurfaceControlOp::SetWindowHandle: return "SetWindowHandle";
|
case SurfaceControlOp::SetWindowHandle: return "SetWindowHandle";
|
||||||
case SurfaceControlOp::InitCapabilities: return "InitCapabilities";
|
case SurfaceControlOp::InitCapabilities: return "InitCapabilities";
|
||||||
|
case SurfaceControlOp::CreateContext: return "CreateContext";
|
||||||
|
case SurfaceControlOp::DestroyContext: return "DestroyContext";
|
||||||
case SurfaceControlOp::SwapBuffersInprocOnly: return "SwapBuffersInprocOnly";
|
case SurfaceControlOp::SwapBuffersInprocOnly: return "SwapBuffersInprocOnly";
|
||||||
case SurfaceControlOp::InitWindowSurfaceInprocOnly: return "InitWindowSurfaceInprocOnly";
|
case SurfaceControlOp::InitWindowSurfaceInprocOnly: return "InitWindowSurfaceInprocOnly";
|
||||||
case SurfaceControlOp::ProbeForTesting: return "ProbeForTesting";
|
case SurfaceControlOp::ProbeForTesting: return "ProbeForTesting";
|
||||||
@@ -47,6 +49,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// frame, which is why this row was missing; the request still has to
|
// frame, which is why this row was missing; the request still has to
|
||||||
// cross, because under spawn the apply thread is in another process.
|
// cross, because under spawn the apply thread is in another process.
|
||||||
case SurfaceControlOp::InitCapabilities:
|
case SurfaceControlOp::InitCapabilities:
|
||||||
|
// P14 S1: the context lifecycle. The client's eglCreateContext/eglDestroyContext have a
|
||||||
|
// live caller under spawn (EGLImpl), so unlike the inproc-only trio beside them these two
|
||||||
|
// MUST cross - the server it is asking is in another process.
|
||||||
|
case SurfaceControlOp::CreateContext:
|
||||||
|
case SurfaceControlOp::DestroyContext:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -65,13 +65,25 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// every spawned eglMakeCurrent died at "surface op kind 11 cannot cross
|
// every spawned eglMakeCurrent died at "surface op kind 11 cannot cross
|
||||||
// the wire (InprocOnlyOpOnTheWire)", one call after the backend came up.
|
// the wire (InprocOnlyOpOnTheWire)", one call after the backend came up.
|
||||||
InitCapabilities = 11,
|
InitCapabilities = 11,
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The EGL context lifecycle,
|
||||||
|
// appended to the schema and to this enum together so the identity cast below stays the
|
||||||
|
// whole mapping: the client's eglCreateContext/eglDestroyContext reach the server through
|
||||||
|
// THE SAME frame channel the nine surface forwarders use, on the same thread, in the same
|
||||||
|
// order - which is the property this slice needs and a second channel would not give.
|
||||||
|
CreateContext = 12,
|
||||||
|
DestroyContext = 13,
|
||||||
// INPROC-ONLY kinds: legal inside this process's frame channel, NEVER encodable onto the
|
// INPROC-ONLY kinds: legal inside this process's frame channel, NEVER encodable onto the
|
||||||
// wire (SurfaceOpCodec refuses them by name). f0-egl's census found no production caller
|
// wire (SurfaceOpCodec refuses them by name). f0-egl's census found no production caller
|
||||||
// for the two dead forwarders; they ride the same frame channel because the
|
// for the two dead forwarders; they ride the same frame channel because the
|
||||||
// function-pointer mailbox is GONE, not because they are wire ops.
|
// function-pointer mailbox is GONE, not because they are wire ops.
|
||||||
SwapBuffersInprocOnly = 12, // present travels as a record (the class-B Present emitter)
|
//
|
||||||
InitWindowSurfaceInprocOnly = 13,// a client-side no-op; kept for the inproc test lane
|
// THEIR VALUES MOVED 12/13/14 -> 14/15/16 when P14 S1 appended the two context ops. That
|
||||||
ProbeForTesting = 14, // MG_Test's arbitrary-work seam through the same channel
|
// is safe by construction: these three never cross a process boundary and are named
|
||||||
|
// symbolically everywhere they are used, so nothing outside this build could have pinned
|
||||||
|
// them - where the eleven above and the two below are wire ABI and may never move.
|
||||||
|
SwapBuffersInprocOnly = 14, // present travels as a record (the class-B Present emitter)
|
||||||
|
InitWindowSurfaceInprocOnly = 15,// a client-side no-op; kept for the inproc test lane
|
||||||
|
ProbeForTesting = 16, // MG_Test's arbitrary-work seam through the same channel
|
||||||
};
|
};
|
||||||
|
|
||||||
// P12 (on-screen server window), D2. THE SERVER-OWNED WINDOW, AS A FRAME-LOCAL TAG.
|
// P12 (on-screen server window), D2. THE SERVER-OWNED WINDOW, AS A FRAME-LOCAL TAG.
|
||||||
@@ -119,6 +131,11 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Int width = 0;
|
Int width = 0;
|
||||||
Int height = 0;
|
Int height = 0;
|
||||||
Int swapInterval = 0;
|
Int swapInterval = 0;
|
||||||
|
// P14 S1. CreateContext's two values beyond `context` (which is the client context token
|
||||||
|
// itself): the share group the context was derived into, and the reserved flags word the
|
||||||
|
// schema carries so a later attribute has a slot. 0 on every other op.
|
||||||
|
Uint64 shareGroupToken = 0;
|
||||||
|
Uint32 contextFlags = 0;
|
||||||
// The reply half: written by the dispatch on the apply thread, read by the poster after
|
// The reply half: written by the dispatch on the apply thread, read by the poster after
|
||||||
// the blocking handshake returns. `ok` is the forwarder's Bool answer; eglMajor/eglMinor
|
// the blocking handshake returns. `ok` is the forwarder's Bool answer; eglMajor/eglMinor
|
||||||
// are InitializeDisplay's out values.
|
// are InitializeDisplay's out values.
|
||||||
@@ -144,14 +161,16 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
// Exhaustive by the language's aggregate decomposition rule: adding ANY member
|
// Exhaustive by the language's aggregate decomposition rule: adding ANY member
|
||||||
// without extending this binding is a compile error, so new fields cannot evade it.
|
// without extending this binding is a compile error, so new fields cannot evade it.
|
||||||
const auto& [kind, seq, display, surface, readSurface, context, windowBackend,
|
const auto& [kind, seq, display, surface, readSurface, context, windowBackend,
|
||||||
nativeToken, width, height, swapInterval, ok, eglMajor, eglMinor, eventHead,
|
nativeToken, width, height, swapInterval, shareGroupToken, contextFlags, ok,
|
||||||
refusal] = frame;
|
eglMajor, eglMinor, eventHead, refusal] = frame;
|
||||||
return IsSurfaceControlValue<decltype(kind)> && IsSurfaceControlValue<decltype(seq)> &&
|
return IsSurfaceControlValue<decltype(kind)> && IsSurfaceControlValue<decltype(seq)> &&
|
||||||
IsSurfaceControlValue<decltype(display)> && IsSurfaceControlValue<decltype(surface)> &&
|
IsSurfaceControlValue<decltype(display)> && IsSurfaceControlValue<decltype(surface)> &&
|
||||||
IsSurfaceControlValue<decltype(readSurface)> && IsSurfaceControlValue<decltype(context)> &&
|
IsSurfaceControlValue<decltype(readSurface)> && IsSurfaceControlValue<decltype(context)> &&
|
||||||
IsSurfaceControlValue<decltype(windowBackend)> && IsSurfaceControlValue<decltype(nativeToken)> &&
|
IsSurfaceControlValue<decltype(windowBackend)> && IsSurfaceControlValue<decltype(nativeToken)> &&
|
||||||
IsSurfaceControlValue<decltype(width)> && IsSurfaceControlValue<decltype(height)> &&
|
IsSurfaceControlValue<decltype(width)> && IsSurfaceControlValue<decltype(height)> &&
|
||||||
IsSurfaceControlValue<decltype(swapInterval)> && IsSurfaceControlValue<decltype(ok)> &&
|
IsSurfaceControlValue<decltype(swapInterval)> &&
|
||||||
|
IsSurfaceControlValue<decltype(shareGroupToken)> &&
|
||||||
|
IsSurfaceControlValue<decltype(contextFlags)> && IsSurfaceControlValue<decltype(ok)> &&
|
||||||
IsSurfaceControlValue<decltype(eglMajor)> && IsSurfaceControlValue<decltype(eglMinor)> &&
|
IsSurfaceControlValue<decltype(eglMajor)> && IsSurfaceControlValue<decltype(eglMinor)> &&
|
||||||
IsSurfaceControlValue<decltype(eventHead)> && IsSurfaceControlValue<decltype(refusal)>;
|
IsSurfaceControlValue<decltype(eventHead)> && IsSurfaceControlValue<decltype(refusal)>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,8 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
X(SetProgramBindings, MGPProgramBindings) \
|
X(SetProgramBindings, MGPProgramBindings) \
|
||||||
X(DeleteStreamOutput, MGPStreamOutputBind) \
|
X(DeleteStreamOutput, MGPStreamOutputBind) \
|
||||||
X(ReadPixelsToBuffer, MGPReadbackToBuffer) \
|
X(ReadPixelsToBuffer, MGPReadbackToBuffer) \
|
||||||
X(GetTextureImageToBuffer, MGPReadbackToBuffer)
|
X(GetTextureImageToBuffer, MGPReadbackToBuffer) \
|
||||||
|
X(BindContext, MGPBindContext)
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -375,9 +376,19 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
|
|
||||||
// The process resolver is a plain function pointer with no user datum
|
// The process resolver is a plain function pointer with no user datum
|
||||||
// (MGPipeHostSpan.h:46), so the table it resolves through has to be found here.
|
// (MGPipeHostSpan.h:46), so the table it resolves through has to be found here.
|
||||||
|
//
|
||||||
|
// P14 S2: TWO PLACES, IN THIS ORDER. The CALLING THREAD's binding is the authoritative one
|
||||||
|
// - a served session's threads are bound to that session's table (ThreadSessionScope), so
|
||||||
|
// two sessions in one process never resolve each other's segments. The process-wide table
|
||||||
|
// below is the fallback for a thread with no binding, which is every shape that has only
|
||||||
|
// one session (and is what the thread-bound call reduces to there).
|
||||||
SegmentTable* g_processResolverTable = nullptr;
|
SegmentTable* g_processResolverTable = nullptr;
|
||||||
|
thread_local SegmentTable* t_threadResolverTable = nullptr;
|
||||||
|
|
||||||
const void* ProcessResolverThunk(Uint32 seg, Uint64 offset, Uint64 size) {
|
const void* ProcessResolverThunk(Uint32 seg, Uint64 offset, Uint64 size) {
|
||||||
|
if (SegmentTable* const bound = t_threadResolverTable) {
|
||||||
|
return bound->Resolve(seg, offset, size);
|
||||||
|
}
|
||||||
if (g_processResolverTable == nullptr) {
|
if (g_processResolverTable == nullptr) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -484,24 +495,35 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
return static_cast<const Uint8*>(view.Base) + offset;
|
return static_cast<const Uint8*>(view.Base) + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SegmentTable::BindThreadResolver() { t_threadResolverTable = this; }
|
||||||
|
|
||||||
|
void SegmentTable::UnbindThreadResolver() { t_threadResolverTable = nullptr; }
|
||||||
|
|
||||||
void SegmentTable::InstallProcessResolver() {
|
void SegmentTable::InstallProcessResolver() {
|
||||||
if (MG_Pipe::gMGPipeSegmentResolver != nullptr && g_processResolverTable != this) {
|
if (t_threadResolverTable != nullptr && t_threadResolverTable != this) {
|
||||||
// Table 3: there is exactly ONE gMGPipeSegmentResolver per process, the SERVER
|
// Table 3, P14 S2: one thread resolves through ONE table. Two tables on one thread is
|
||||||
// role installs it before the apply thread starts, and the client never resolves a
|
// the shape this line exists to make loud - a session's threads are per session, so a
|
||||||
// span at all. Two roles racing on one inline variable is loud rather than silent
|
// well-formed process never reaches it, while a second SESSION (its own threads, its
|
||||||
// because of this line.
|
// own table) is exactly what the per-thread binding is for and does not.
|
||||||
WireProtocolFatal("SegmentTable::InstallProcessResolver",
|
WireProtocolFatal("SegmentTable::InstallProcessResolver",
|
||||||
"a process segment resolver is already installed; the server role "
|
"a process segment resolver is already installed ON THIS THREAD for "
|
||||||
"installs it once, before the apply thread starts");
|
"another session's table; a served session's threads belong to one "
|
||||||
|
"session");
|
||||||
|
}
|
||||||
|
if (MG_Pipe::gMGPipeSegmentResolver == nullptr) {
|
||||||
|
MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk;
|
||||||
}
|
}
|
||||||
g_processResolverTable = this;
|
g_processResolverTable = this;
|
||||||
MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk;
|
BindThreadResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SegmentTable::UninstallProcessResolver() {
|
void SegmentTable::UninstallProcessResolver() {
|
||||||
// Teardown order (table 3): uninstall AFTER the join, never before - a record still in
|
// Teardown order (table 3): uninstall AFTER the join, never before - a record still in
|
||||||
// flight can still resolve.
|
// flight can still resolve. P14 S2: this unbinds the calling thread and drops the
|
||||||
MG_Pipe::gMGPipeSegmentResolver = nullptr;
|
// process-wide fallback only; the hook stays installed because another session of this
|
||||||
|
// process may still be decoding records (ServerSession::Close gates this call on the last
|
||||||
|
// session).
|
||||||
|
UnbindThreadResolver();
|
||||||
g_processResolverTable = nullptr;
|
g_processResolverTable = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2533,6 +2555,16 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
return m_verbs != nullptr &&
|
return m_verbs != nullptr &&
|
||||||
m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload));
|
m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload));
|
||||||
|
|
||||||
|
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
|
||||||
|
//
|
||||||
|
// The third control record, and the applier_reset shape exactly: a fixed-size POD with
|
||||||
|
// no blob, no tail and no reply, so the generated bounds gate is the whole validation
|
||||||
|
// and the arm hands over. What the sink does with the token - "this session's current
|
||||||
|
// context" - is a session fact, not a shape.
|
||||||
|
case MGPWireOp::BindContext:
|
||||||
|
return m_verbs != nullptr &&
|
||||||
|
m_verbs->OnBindContext(*static_cast<const MGPBindContext*>(payload));
|
||||||
|
|
||||||
// ---- P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, opcode 79 -------------
|
// ---- P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, opcode 79 -------------
|
||||||
//
|
//
|
||||||
// An ordinary set_* row, unlike the two control records beside it: a fixed-width POD
|
// An ordinary set_* row, unlike the two control records beside it: a fixed-width POD
|
||||||
|
|||||||
@@ -95,10 +95,23 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const;
|
const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const;
|
||||||
|
|
||||||
// Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if
|
// Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if
|
||||||
// a resolver is already installed, because two roles racing on one inline variable is
|
// a DIFFERENT table is already installed ON THE CALLING THREAD, because one thread
|
||||||
// the failure this function exists to make loud.
|
// serving two sessions at once is the failure this function exists to make loud.
|
||||||
|
//
|
||||||
|
// P14 S2: THE TABLE IS PER THREAD, THE HOOK IS PER PROCESS. Several sessions in one
|
||||||
|
// process each have their own table and their own threads, so a second session's install
|
||||||
|
// is neither a race nor an error: it installs the one process-wide thunk (idempotent) and
|
||||||
|
// binds the calling thread to ITS table. g_processResolverTable stays as the fallback for
|
||||||
|
// a thread that bound nothing - the inproc one-process client+server shape, whose apply
|
||||||
|
// thread is created after the app thread's Accept and does not bind.
|
||||||
void InstallProcessResolver();
|
void InstallProcessResolver();
|
||||||
|
// Unbinds the CALLING thread and clears the process-wide fallback. The hook itself stays
|
||||||
|
// installed: another session may still be live, and a null answer from a table-less thunk
|
||||||
|
// is exactly what a null hook answered.
|
||||||
static void UninstallProcessResolver();
|
static void UninstallProcessResolver();
|
||||||
|
// P14 S2: binds the CALLING thread to this table (ThreadSessionScope's two calls).
|
||||||
|
void BindThreadResolver();
|
||||||
|
static void UnbindThreadResolver();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Transport::ILink* m_link = nullptr;
|
Transport::ILink* m_link = nullptr;
|
||||||
@@ -661,6 +674,19 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
(void)death;
|
(void)death;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
|
||||||
|
//
|
||||||
|
// The applier_reset hand-over shape, one plane over: a fixed-size POD the bounds gate has
|
||||||
|
// already proved, no blob, no tail, no reply, so the codec validates nothing further and
|
||||||
|
// the SERVER'S sink does the work - the session's current context token becomes the
|
||||||
|
// record's. It is not an assertion like OnApplierReset's serial: the token is minted by
|
||||||
|
// the client's EGL layer and the server has no independent count of it, so an unknown
|
||||||
|
// token is the sink's own refusal to make and not a shape the codec can check.
|
||||||
|
virtual Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) {
|
||||||
|
(void)bind;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Not thread safe: one decoder on the apply thread, by construction.
|
// Not thread safe: one decoder on the apply thread, by construction.
|
||||||
|
|||||||
@@ -233,6 +233,11 @@ namespace MobileGL {
|
|||||||
m_contextOwners.erase(ownerIt);
|
m_contextOwners.erase(ownerIt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// P14 S3: the thread's GL state goes back to what it was before this EGL
|
||||||
|
// context was made current on it.
|
||||||
|
if (currentIt->second.RestoreThreadGLState) {
|
||||||
|
pGLContext = Move(currentIt->second.RestoreThreadGLState);
|
||||||
|
}
|
||||||
m_threadCurrents.erase(currentIt);
|
m_threadCurrents.erase(currentIt);
|
||||||
DestroyPendingSurfaceIfUnused(drawSurface);
|
DestroyPendingSurfaceIfUnused(drawSurface);
|
||||||
DestroyPendingSurfaceIfUnused(readSurface);
|
DestroyPendingSurfaceIfUnused(readSurface);
|
||||||
@@ -306,6 +311,12 @@ namespace MobileGL {
|
|||||||
m_contextOwners.erase(ownerIt);
|
m_contextOwners.erase(ownerIt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Only the CALLING thread's TLS is reachable from here - another
|
||||||
|
// thread's current is dropped from the table, and its own next EGL
|
||||||
|
// call re-establishes the binding.
|
||||||
|
if (threadIt->first == CurrentThreadKey() && threadIt->second.RestoreThreadGLState) {
|
||||||
|
pGLContext = Move(threadIt->second.RestoreThreadGLState);
|
||||||
|
}
|
||||||
threadIt = m_threadCurrents.erase(threadIt);
|
threadIt = m_threadCurrents.erase(threadIt);
|
||||||
} else {
|
} else {
|
||||||
++threadIt;
|
++threadIt;
|
||||||
@@ -654,6 +665,30 @@ namespace MobileGL {
|
|||||||
.MinorVersion = 0,
|
.MinorVersion = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// P14 S1: the wire identity, minted where the context is born. A context that
|
||||||
|
// shares with an existing one joins ITS share group; EGL_NO_CONTEXT starts a new
|
||||||
|
// one. Both tokens are 0-free by construction (the counters start at 1), because
|
||||||
|
// 0 is the wire's "no context" on both planes.
|
||||||
|
contextObject.ClientContextToken = m_nextClientContextToken++;
|
||||||
|
SharedPtr<GLState::ShareGroupState> shareGroup;
|
||||||
|
if (shareCtx != nullptr) {
|
||||||
|
const auto* shared = TryGetContext(shareCtx);
|
||||||
|
if (shared != nullptr && shared->GLStateObject) {
|
||||||
|
shareGroup = shared->GLStateObject->GetShareGroup();
|
||||||
|
contextObject.ShareGroupToken = shared->ShareGroupToken;
|
||||||
|
} else {
|
||||||
|
contextObject.ShareGroupToken = m_nextShareGroupToken++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contextObject.ShareGroupToken = m_nextShareGroupToken++;
|
||||||
|
}
|
||||||
|
if (!shareGroup) {
|
||||||
|
shareGroup = MakeShared<GLState::ShareGroupState>(contextObject.ShareGroupToken);
|
||||||
|
}
|
||||||
|
// P14 S3: the context's own GL state, sharing the objects (and the name spaces)
|
||||||
|
// of the group above, with bindings, errors and the current program of its own.
|
||||||
|
contextObject.GLStateObject = MakeShared<GLState::GLContext>(shareGroup);
|
||||||
|
|
||||||
if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_CLIENT_VERSION); value) {
|
if (auto value = ParseAttribValue(attribList, EGL_CONTEXT_CLIENT_VERSION); value) {
|
||||||
contextObject.ClientVersion = *value;
|
contextObject.ClientVersion = *value;
|
||||||
contextObject.MajorVersion = *value;
|
contextObject.MajorVersion = *value;
|
||||||
@@ -779,6 +814,37 @@ namespace MobileGL {
|
|||||||
return ctx && ctx->Display == display;
|
return ctx && ctx->Display == display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S1. 0 for an unknown handle or a thread with no current context: 0 is the
|
||||||
|
// wire's "no context" on both planes, so an answer that cannot be given is given as
|
||||||
|
// the value that means "nothing is bound" rather than as a handle-shaped guess.
|
||||||
|
Uint64 EGLContext::GetContextClientToken(EGLContextHandle context) const {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
const auto* ctx = TryGetContext(context);
|
||||||
|
return ctx != nullptr ? ctx->ClientContextToken : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 EGLContext::GetContextShareGroupToken(EGLContextHandle context) const {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
const auto* ctx = TryGetContext(context);
|
||||||
|
return ctx != nullptr ? ctx->ShareGroupToken : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<GLState::GLContext> EGLContext::GetContextGLState(EGLContextHandle context) const {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
const auto* ctx = TryGetContext(context);
|
||||||
|
return ctx != nullptr ? ctx->GLStateObject : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 EGLContext::CurrentContextClientToken() const {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
const auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||||
|
if (currentIt == m_threadCurrents.end()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const auto* ctx = TryGetContext(currentIt->second.Context);
|
||||||
|
return ctx != nullptr ? ctx->ClientContextToken : 0;
|
||||||
|
}
|
||||||
|
|
||||||
Bool EGLContext::IsCurrentContextOpenGLCoreProfile() const {
|
Bool EGLContext::IsCurrentContextOpenGLCoreProfile() const {
|
||||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||||
@@ -1276,6 +1342,16 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S3: whatever this thread's GL state was before the first EGL context was
|
||||||
|
// bound to it is what a later release goes back to.
|
||||||
|
SharedPtr<GLState::GLContext> restoreThreadGLState;
|
||||||
|
if (const auto currentIt = m_threadCurrents.find(threadKey);
|
||||||
|
currentIt != m_threadCurrents.end()) {
|
||||||
|
restoreThreadGLState = currentIt->second.RestoreThreadGLState;
|
||||||
|
} else {
|
||||||
|
restoreThreadGLState = pGLContext;
|
||||||
|
}
|
||||||
|
|
||||||
ReleaseThreadUnlocked(threadKey);
|
ReleaseThreadUnlocked(threadKey);
|
||||||
|
|
||||||
m_threadCurrents[threadKey] = ThreadCurrentState{
|
m_threadCurrents[threadKey] = ThreadCurrentState{
|
||||||
@@ -1283,9 +1359,15 @@ namespace MobileGL {
|
|||||||
.DrawSurface = draw,
|
.DrawSurface = draw,
|
||||||
.ReadSurface = read,
|
.ReadSurface = read,
|
||||||
.Context = context,
|
.Context = context,
|
||||||
|
.RestoreThreadGLState = Move(restoreThreadGLState),
|
||||||
};
|
};
|
||||||
if (context != nullptr) {
|
if (context != nullptr) {
|
||||||
m_contextOwners[context] = threadKey;
|
m_contextOwners[context] = threadKey;
|
||||||
|
// P14 S3: this thread's GL calls land in this context's GLContext from here
|
||||||
|
// on, and not before the switch above has been committed.
|
||||||
|
if (const auto* contextObject = TryGetContext(context); contextObject != nullptr) {
|
||||||
|
pGLContext = contextObject->GLStateObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_State {
|
namespace MG_State {
|
||||||
@@ -69,6 +70,20 @@ namespace MobileGL {
|
|||||||
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
|
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
|
||||||
EGLint GetCurrentContextFlags() const;
|
EGLint GetCurrentContextFlags() const;
|
||||||
|
|
||||||
|
// P14 S1. The wire identity of a context, and of the context the CALLING THREAD
|
||||||
|
// has current (0 when it has none - the release edge's value, and the value a
|
||||||
|
// session that never crossed a bind_context answers with). Read by MG_Impl's two
|
||||||
|
// producers: EGLImpl's CreateContext/DestroyContext control frames and PipeFill's
|
||||||
|
// bind_context emission at the make-current edge.
|
||||||
|
Uint64 GetContextClientToken(EGLContextHandle context) const;
|
||||||
|
Uint64 GetContextShareGroupToken(EGLContextHandle context) const;
|
||||||
|
Uint64 CurrentContextClientToken() const;
|
||||||
|
|
||||||
|
// P14 S3. The frontend GL state this context owns, so a caller outside MG_State
|
||||||
|
// can reach the other half of a context it just made current. Null for an
|
||||||
|
// unknown handle.
|
||||||
|
SharedPtr<GLState::GLContext> GetContextGLState(EGLContextHandle context) const;
|
||||||
|
|
||||||
// Surface
|
// Surface
|
||||||
EGLSurfaceHandle CreateWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
|
EGLSurfaceHandle CreateWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
|
||||||
NativeWindowType window, const EGLint* attribList);
|
NativeWindowType window, const EGLint* attribList);
|
||||||
@@ -180,6 +195,17 @@ namespace MobileGL {
|
|||||||
EGLDisplayHandle Display = EGL_NO_DISPLAY;
|
EGLDisplayHandle Display = EGL_NO_DISPLAY;
|
||||||
EGLConfigHandle Config = nullptr;
|
EGLConfigHandle Config = nullptr;
|
||||||
EGLContextHandle SharedContext = nullptr;
|
EGLContextHandle SharedContext = nullptr;
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The wire identity of
|
||||||
|
// this context: `ClientContextToken` is what bind_context carries and what the
|
||||||
|
// server's per-session context table is keyed by, `ShareGroupToken` the group
|
||||||
|
// it was derived into (ShareContext's group, or one of its own). Minted here,
|
||||||
|
// in the one place a context is born, so no caller can invent a second one.
|
||||||
|
Uint64 ClientContextToken = 0;
|
||||||
|
Uint64 ShareGroupToken = 0;
|
||||||
|
// P14 S3. The GL state of THIS EGL context. Its object registries live in the
|
||||||
|
// share group's ShareGroupState (shared with every context that shares with
|
||||||
|
// this one); its bindings, errors and current program are its own.
|
||||||
|
SharedPtr<GLState::GLContext> GLStateObject;
|
||||||
EGLenum ClientAPI = EGL_OPENGL_API;
|
EGLenum ClientAPI = EGL_OPENGL_API;
|
||||||
EGLint ClientVersion = 1;
|
EGLint ClientVersion = 1;
|
||||||
EGLint MajorVersion = 1;
|
EGLint MajorVersion = 1;
|
||||||
@@ -226,6 +252,10 @@ namespace MobileGL {
|
|||||||
EGLSurfaceHandle DrawSurface = EGL_NO_SURFACE;
|
EGLSurfaceHandle DrawSurface = EGL_NO_SURFACE;
|
||||||
EGLSurfaceHandle ReadSurface = EGL_NO_SURFACE;
|
EGLSurfaceHandle ReadSurface = EGL_NO_SURFACE;
|
||||||
EGLContextHandle Context = nullptr;
|
EGLContextHandle Context = nullptr;
|
||||||
|
// P14 S3. What pGLContext held on this thread BEFORE the first eglMakeCurrent
|
||||||
|
// bound an EGL context to it, so releasing restores exactly the binding the
|
||||||
|
// thread had rather than a guessed default.
|
||||||
|
SharedPtr<GLState::GLContext> RestoreThreadGLState;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename HandleType>
|
template <typename HandleType>
|
||||||
@@ -269,6 +299,9 @@ namespace MobileGL {
|
|||||||
Uint64 m_nextContextHandle = 1;
|
Uint64 m_nextContextHandle = 1;
|
||||||
Uint64 m_nextSyncHandle = 1;
|
Uint64 m_nextSyncHandle = 1;
|
||||||
Uint64 m_nextImageHandle = 1;
|
Uint64 m_nextImageHandle = 1;
|
||||||
|
// P14 S1. Dense from 1, never reused, never 0: 0 is "no context" on both planes.
|
||||||
|
Uint64 m_nextClientContextToken = 1;
|
||||||
|
Uint64 m_nextShareGroupToken = 1;
|
||||||
|
|
||||||
UnorderedMap<DisplayLookupKey, EGLDisplayHandle, DisplayLookupHasher> m_displayLookup;
|
UnorderedMap<DisplayLookupKey, EGLDisplayHandle, DisplayLookupHasher> m_displayLookup;
|
||||||
UnorderedMap<EGLDisplayHandle, DisplayObject> m_displays;
|
UnorderedMap<EGLDisplayHandle, DisplayObject> m_displays;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "BufferState.h"
|
#include "BufferState.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
BufferState::BufferState() : m_indexGenerator(1024, 1) {
|
BufferState::BufferState() {
|
||||||
for (SizeT i = 0; i < m_bindingSlots.size(); ++i) {
|
for (SizeT i = 0; i < m_bindingSlots.size(); ++i) {
|
||||||
m_bindingSlots[i] = BindingSlot<BufferObject>(GlobalBufferTargets[i]);
|
m_bindingSlots[i] = BindingSlot<BufferObject>(GlobalBufferTargets[i]);
|
||||||
}
|
}
|
||||||
@@ -18,28 +18,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<BufferObject>& BufferState::GetBufferObject(Uint index) {
|
|
||||||
auto it = m_bufferObjects.find(index);
|
|
||||||
if (it != m_bufferObjects.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
static SharedPtr<BufferObject> nullBufferObject = nullptr;
|
|
||||||
return nullBufferObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BufferState::GenerateNames(Uint number, Vector<Uint>& buffers) {
|
|
||||||
buffers.resize(number);
|
|
||||||
m_indexGenerator.Generate(number, buffers.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
const SharedPtr<BufferObject>& BufferState::CreateBufferObject(Uint index) {
|
|
||||||
auto& bufferObj = m_bufferObjects[index];
|
|
||||||
if (!bufferObj) {
|
|
||||||
bufferObj = MakeShared<BufferObject>(index);
|
|
||||||
}
|
|
||||||
return bufferObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
BindingSlot<BufferObject>& BufferState::GetBindingSlot(BufferTarget target) {
|
BindingSlot<BufferObject>& BufferState::GetBindingSlot(BufferTarget target) {
|
||||||
for (auto& bindingSlot : m_bindingSlots) {
|
for (auto& bindingSlot : m_bindingSlots) {
|
||||||
if (bindingSlot.GetTarget() == target) {
|
if (bindingSlot.GetTarget() == target) {
|
||||||
@@ -50,53 +28,33 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_bindingSlots[0];
|
return m_bindingSlots[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferState::MarkBufferObjectForDeletion(Uint index) {
|
void BufferState::UnbindBufferObject(const SharedPtr<BufferObject>& bufferObject) {
|
||||||
if (m_indexGenerator.IsValid(index)) {
|
if (!bufferObject) return;
|
||||||
auto it = m_bufferObjects.find(index);
|
for (auto& bindingSlot : m_bindingSlots) {
|
||||||
if (it != m_bufferObjects.end()) {
|
if (bindingSlot.GetBoundObject() == bufferObject) {
|
||||||
for (auto& bindingSlot : m_bindingSlots) {
|
bindingSlot.Bind(nullptr);
|
||||||
if (bindingSlot.GetBoundObject() == it->second) {
|
}
|
||||||
bindingSlot.Bind(nullptr);
|
}
|
||||||
}
|
for (auto& bindingPointArray : m_bufferBindPointTargets) {
|
||||||
}
|
for (auto& bindingPoint : bindingPointArray) {
|
||||||
for (auto& bindingPointArray : m_bufferBindPointTargets) {
|
if (bindingPoint.GetBoundObject() == bufferObject) {
|
||||||
for (auto& bindingPoint : bindingPointArray) {
|
bindingPoint.Bind(nullptr);
|
||||||
if (bindingPoint.GetBoundObject() == it->second) {
|
bindingPoint.ClearRange();
|
||||||
bindingPoint.Bind(nullptr);
|
|
||||||
bindingPoint.ClearRange();
|
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// An UNBIND, and the one writer of an indexed point that is not an
|
// An UNBIND, and the one writer of an indexed point that is not an
|
||||||
// entry point (GL_Buffer.cpp's BindBuffer{Base,Range}_State and
|
// entry point (GL_Buffer.cpp's BindBuffer{Base,Range}_State and
|
||||||
// Core.cpp's transform-feedback writers bump their own): without the
|
// Core.cpp's transform-feedback writers bump their own): without the
|
||||||
// bump the target's window is never re-sent, the server goes on naming
|
// bump the target's window is never re-sent, the server goes on naming
|
||||||
// this buffer's handle, and once its slot is reused at a newer
|
// this buffer's handle, and once its slot is reused at a newer
|
||||||
// generation the next draw or dispatch that walks the window asks the
|
// generation the next draw or dispatch that walks the window asks the
|
||||||
// backend for the dead one - Espryt dies with Fatal{ProtocolCorruption,
|
// backend for the dead one - Espryt dies with Fatal{ProtocolCorruption,
|
||||||
// "BackendSlotTable.Generation"} (DeletedBoundBufferScenario).
|
// "BackendSlotTable.Generation"} (DeletedBoundBufferScenario).
|
||||||
NoteBindPointChanged(BufferBindPointTargets[static_cast<SizeT>(
|
NoteBindPointChanged(BufferBindPointTargets[static_cast<SizeT>(
|
||||||
&bindingPointArray - m_bufferBindPointTargets.data())]);
|
&bindingPointArray - m_bufferBindPointTargets.data())]);
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Erase through the iterator already in hand: erase(key) would repeat the
|
|
||||||
// find() above, and the successor scan that once made key-based
|
|
||||||
// erase the cheaper of the two no longer happens here - erase(iterator)
|
|
||||||
// hands back an unconverted proxy, and the scan is what converting it
|
|
||||||
// would cost. The unbind loops above touch only the binding arrays, so
|
|
||||||
// `it` is still live.
|
|
||||||
m_bufferObjects.erase(it);
|
|
||||||
}
|
}
|
||||||
m_indexGenerator.Delete(index);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Bool BufferState::ValidateName(Uint index) const {
|
|
||||||
return m_indexGenerator.IsValid(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool BufferState::ValidateBufferObject(Uint index) const {
|
|
||||||
return m_bufferObjects.find(index) != m_bufferObjects.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BindingSlotRange1D<BufferObject>& BufferState::GetBindingPoint(BufferTarget target, Uint index) {
|
BindingSlotRange1D<BufferObject>& BufferState::GetBindingPoint(BufferTarget target, Uint index) {
|
||||||
|
|||||||
@@ -28,13 +28,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// costs the unused tail of three arrays.
|
// costs the unused tail of three arrays.
|
||||||
constexpr SizeT BufferBindingPointCount = 84;
|
constexpr SizeT BufferBindingPointCount = 84;
|
||||||
|
|
||||||
|
// The per-context half of the buffer state: which buffer is bound where. The objects
|
||||||
|
// themselves and their names belong to the share group (ShareGroupState).
|
||||||
class BufferState {
|
class BufferState {
|
||||||
public:
|
public:
|
||||||
BufferState();
|
BufferState();
|
||||||
|
|
||||||
const SharedPtr<BufferObject>& GetBufferObject(Uint index);
|
|
||||||
void GenerateNames(Uint number, Vector<Uint>& buffers);
|
|
||||||
const SharedPtr<BufferObject>& CreateBufferObject(Uint index);
|
|
||||||
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
|
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
|
||||||
// For glBindBufferBase / glBindBufferRange
|
// For glBindBufferBase / glBindBufferRange
|
||||||
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
|
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
|
||||||
@@ -60,9 +59,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (it == BufferBindPointTargets.end()) return 0;
|
if (it == BufferBindPointTargets.end()) return 0;
|
||||||
return m_touchedBindPointCount[std::distance(BufferBindPointTargets.begin(), it)];
|
return m_touchedBindPointCount[std::distance(BufferBindPointTargets.begin(), it)];
|
||||||
}
|
}
|
||||||
void MarkBufferObjectForDeletion(Uint index);
|
// Detaches a deleted buffer from THIS context's bindings only (GL 4.6 core 6.1: a delete
|
||||||
Bool ValidateName(Uint index) const;
|
// resets the deleting context's bindings, and other contexts keep their own).
|
||||||
Bool ValidateBufferObject(Uint index) const;
|
void UnbindBufferObject(const SharedPtr<BufferObject>& bufferObject);
|
||||||
|
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// P2 brief D4: "did the contents of ANY buffer object move". One counter for every
|
// P2 brief D4: "did the contents of ANY buffer object move". One counter for every
|
||||||
@@ -110,8 +109,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Uint64 m_anyBufferChangeGeneration = 0;
|
Uint64 m_anyBufferChangeGeneration = 0;
|
||||||
Array<Uint64, BufferBindPointTargets.size()> m_bindPointGeneration{};
|
Array<Uint64, BufferBindPointTargets.size()> m_bindPointGeneration{};
|
||||||
#endif
|
#endif
|
||||||
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
|
|
||||||
IndexGenerator<Uint> m_indexGenerator;
|
|
||||||
Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots;
|
Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots;
|
||||||
// TODO: query the count somewhere globally?
|
// TODO: query the count somewhere globally?
|
||||||
// For glBindBufferBase / glBindBufferRange
|
// For glBindBufferBase / glBindBufferRange
|
||||||
|
|||||||
@@ -18,9 +18,52 @@
|
|||||||
#include <MG_Pipe/PipeMutation.h>
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State {
|
namespace MobileGL::MG_State {
|
||||||
|
namespace {
|
||||||
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
SharedPtr<GLState::GLContext>& DefaultGLContextSlot() {
|
||||||
|
static SharedPtr<GLState::GLContext>* slot = new SharedPtr<GLState::GLContext>();
|
||||||
|
return *slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<GLState::FramebufferObject>& DefaultFramebufferSlot() {
|
||||||
|
static SharedPtr<GLState::FramebufferObject>* slot = new SharedPtr<GLState::FramebufferObject>();
|
||||||
|
return *slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<GLState::GLContext>& ProcessDefaultGLContext() {
|
||||||
|
auto& slot = DefaultGLContextSlot();
|
||||||
|
if (!slot) {
|
||||||
|
slot = MakeShared<GLState::GLContext>();
|
||||||
|
}
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseProcessDefaultGLContext() {
|
||||||
|
auto& slot = DefaultGLContextSlot();
|
||||||
|
// Only the calling thread's TLS is reachable here, and only when it is still holding the
|
||||||
|
// fallback rather than an EGL context of its own.
|
||||||
|
if (pGLContext == slot) pGLContext.reset();
|
||||||
|
DefaultFramebufferSlot().reset();
|
||||||
|
slot.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDefaultFramebuffer(SharedPtr<GLState::FramebufferObject> framebuffer) {
|
||||||
|
DefaultFramebufferSlot() = Move(framebuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<GLState::FramebufferObject>& DefaultFramebuffer() {
|
||||||
|
return DefaultFramebufferSlot();
|
||||||
|
}
|
||||||
|
|
||||||
void Init() {
|
void Init() {
|
||||||
MGLOG_D("Initializing MobileGL State...");
|
MGLOG_D("Initializing MobileGL State...");
|
||||||
pGLContext = MakeUnique<GLState::GLContext>();
|
// Rebuilt on every Initialize(): the fallback context starts empty, so a thread that
|
||||||
|
// never crossed an eglMakeCurrent cannot see the previous run's objects. The default
|
||||||
|
// framebuffer is rebuilt too - MG_Impl::Init() installs the new one right after.
|
||||||
|
DefaultFramebufferSlot().reset();
|
||||||
|
DefaultGLContextSlot() = MakeShared<GLState::GLContext>();
|
||||||
|
pGLContext = ProcessDefaultGLContext();
|
||||||
pEGLContext = MakeUnique<EGLState::EGLContext>();
|
pEGLContext = MakeUnique<EGLState::EGLContext>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +132,11 @@ namespace MobileGL::MG_State {
|
|||||||
|
|
||||||
// Buffer
|
// Buffer
|
||||||
void GLContext::GenBufferNames(Uint number, Vector<Uint>& buffers) {
|
void GLContext::GenBufferNames(Uint number, Vector<Uint>& buffers) {
|
||||||
m_bufferState.GenerateNames(number, buffers);
|
Group().GenerateBufferNames(number, buffers);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<BufferObject>& GLContext::GetBufferObject(Uint index) {
|
const SharedPtr<BufferObject>& GLContext::GetBufferObject(Uint index) {
|
||||||
return m_bufferState.GetBufferObject(index);
|
return Group().GetBufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
BindingSlot<BufferObject>& GLContext::GetBufferBindingSlot(BufferTarget target) {
|
BindingSlot<BufferObject>& GLContext::GetBufferBindingSlot(BufferTarget target) {
|
||||||
@@ -111,7 +154,7 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<BufferObject>& GLContext::CreateBufferObject(Uint index) {
|
const SharedPtr<BufferObject>& GLContext::CreateBufferObject(Uint index) {
|
||||||
return m_bufferState.CreateBufferObject(index);
|
return Group().CreateBufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkBufferObjectForDeletion(Uint index) {
|
void GLContext::MarkBufferObjectForDeletion(Uint index) {
|
||||||
@@ -123,7 +166,7 @@ namespace MobileGL::MG_State {
|
|||||||
// previous every-VAO scan was wrong per spec and O(VAOs) per delete —
|
// previous every-VAO scan was wrong per spec and O(VAOs) per delete —
|
||||||
// with one VAO per chunk section, vanilla's steady buffer churn made it
|
// with one VAO per chunk section, vanilla's steady buffer churn made it
|
||||||
// dominate the render thread and FPS decay over session time.
|
// dominate the render thread and FPS decay over session time.
|
||||||
auto bufferObject = m_bufferState.GetBufferObject(index);
|
const auto bufferObject = Group().GetBufferObject(index);
|
||||||
const auto& vao = m_vertexArrayState.GetBoundVertexArray();
|
const auto& vao = m_vertexArrayState.GetBoundVertexArray();
|
||||||
if (vao != nullptr) {
|
if (vao != nullptr) {
|
||||||
if (vao->GetIndexBufferBindingSlot().GetBoundObject() == bufferObject) {
|
if (vao->GetIndexBufferBindingSlot().GetBoundObject() == bufferObject) {
|
||||||
@@ -135,17 +178,18 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m_bufferState.UnbindBufferObject(bufferObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_bufferState.MarkBufferObjectForDeletion(index);
|
Group().MarkBufferObjectForDeletion(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateBufferName(Uint index) const {
|
Bool GLContext::ValidateBufferName(Uint index) const {
|
||||||
return m_bufferState.ValidateName(index);
|
return m_shareGroup->ValidateBufferName(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateBufferObject(Uint index) const {
|
Bool GLContext::ValidateBufferObject(Uint index) const {
|
||||||
return m_bufferState.ValidateBufferObject(index);
|
return m_shareGroup->ValidateBufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// VertexArray
|
// VertexArray
|
||||||
@@ -269,11 +313,11 @@ namespace MobileGL::MG_State {
|
|||||||
|
|
||||||
// Texture
|
// Texture
|
||||||
void GLContext::GenTextureNames(Uint number, Vector<Uint>& textures) {
|
void GLContext::GenTextureNames(Uint number, Vector<Uint>& textures) {
|
||||||
m_textureState.GenerateNames(number, textures);
|
Group().GenerateTextureNames(number, textures);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& GLContext::GetTextureObject(Uint index) {
|
const SharedPtr<ITextureObject>& GLContext::GetTextureObject(Uint index) {
|
||||||
return m_textureState.GetTextureObject(index);
|
return Group().GetTextureObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
|
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
|
||||||
@@ -281,14 +325,14 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
|
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
|
||||||
return m_textureState.CreateTextureObject(index, target);
|
return Group().CreateTextureObject(index, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
|
const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
|
||||||
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
||||||
Uint numLevels, Uint minLayer, Uint numLayers) {
|
Uint numLevels, Uint minLayer, Uint numLayers) {
|
||||||
return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
|
return Group().CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
|
||||||
numLayers);
|
numLayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
||||||
@@ -302,7 +346,8 @@ namespace MobileGL::MG_State {
|
|||||||
// contents rather than those of whatever the application put in its place - the name
|
// contents rather than those of whatever the application put in its place - the name
|
||||||
// it deleted usually comes straight back from the next glGenTextures, so the two are
|
// it deleted usually comes straight back from the next glGenTextures, so the two are
|
||||||
// indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
|
// indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
|
||||||
if (const auto& textureObject = m_textureState.GetTextureObject(index)) {
|
const auto textureObject = Group().GetTextureObject(index);
|
||||||
|
if (textureObject) {
|
||||||
for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
|
for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
|
||||||
++targetIndex) {
|
++targetIndex) {
|
||||||
const auto& framebuffer =
|
const auto& framebuffer =
|
||||||
@@ -318,7 +363,10 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
|
// This context's units only: the object is gone for every context in the share
|
||||||
|
// group, but each of them detaches it from its own bindings (GL 4.6 core 8.1).
|
||||||
|
m_textureState.UnbindTextureFromUnits(textureObject);
|
||||||
|
Group().MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
|
TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
|
||||||
@@ -334,11 +382,11 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateTextureName(Uint index) const {
|
Bool GLContext::ValidateTextureName(Uint index) const {
|
||||||
return m_textureState.ValidateName(index);
|
return m_shareGroup->ValidateTextureName(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateTextureObject(Uint index) const {
|
Bool GLContext::ValidateTextureObject(Uint index) const {
|
||||||
return m_textureState.ValidateTextureObject(index);
|
return m_shareGroup->ValidateTextureObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Int GLContext::GetActiveTextureUnit() const {
|
Int GLContext::GetActiveTextureUnit() const {
|
||||||
@@ -351,51 +399,67 @@ namespace MobileGL::MG_State {
|
|||||||
|
|
||||||
// Program
|
// Program
|
||||||
Uint GLContext::CreateProgram() {
|
Uint GLContext::CreateProgram() {
|
||||||
return m_programState.CreateProgram();
|
return Group().Programs().CreateProgram();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint GLContext::CreateShader(const ShaderStage stage) {
|
Uint GLContext::CreateShader(const ShaderStage stage) {
|
||||||
return m_programState.CreateShader(stage);
|
return Group().Programs().CreateShader(stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkProgramForDeletion(const Uint index) {
|
void GLContext::MarkProgramForDeletion(const Uint index) {
|
||||||
return m_programState.MarkProgramObjectForDeletion(index);
|
return Group().Programs().MarkProgramObjectForDeletion(index, m_currentProgram);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkShaderForDeletion(const Uint index) {
|
void GLContext::MarkShaderForDeletion(const Uint index) {
|
||||||
return m_programState.MarkShaderObjectForDeletion(index);
|
return Group().Programs().MarkShaderObjectForDeletion(index, m_currentProgram);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
|
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
|
||||||
return m_programState.ReleaseShaderNameIfOrphaned(index);
|
return Group().Programs().ReleaseShaderNameIfOrphaned(index, m_currentProgram);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateProgramName(const Uint index) const {
|
Bool GLContext::ValidateProgramName(const Uint index) const {
|
||||||
return m_programState.ValidateProgramObject(index);
|
return m_shareGroup->Programs().ValidateProgramObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateShaderName(const Uint index) const {
|
Bool GLContext::ValidateShaderName(const Uint index) const {
|
||||||
return m_programState.ValidateShaderObject(index);
|
return m_shareGroup->Programs().ValidateShaderObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ProgramObject>& GLContext::GetProgramObject(const Uint index) {
|
const SharedPtr<ProgramObject>& GLContext::GetProgramObject(const Uint index) {
|
||||||
return m_programState.GetProgramObject(index);
|
return Group().Programs().GetProgramObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ShaderObject>& GLContext::GetShaderObject(const Uint index) {
|
const SharedPtr<ShaderObject>& GLContext::GetShaderObject(const Uint index) {
|
||||||
return m_programState.GetShaderObject(index);
|
return Group().Programs().GetShaderObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::JoinAllPendingShaderWork() {
|
void GLContext::JoinAllPendingShaderWork() {
|
||||||
m_programState.JoinAllPendingWork();
|
Group().Programs().JoinAllPendingWork(m_currentProgram);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::UseProgram(Uint program) {
|
void GLContext::UseProgram(Uint program) {
|
||||||
return m_programState.UseProgram(program);
|
const SharedPtr<ProgramObject> previous = m_currentProgram;
|
||||||
|
|
||||||
|
if (program == 0) m_currentProgram.reset();
|
||||||
|
|
||||||
|
auto& programs = Group().Programs();
|
||||||
|
if (programs.HasProgramSlot(program)) {
|
||||||
|
m_currentProgram = programs.GetProgramObject(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A deletion flagged while the program was current takes effect the moment it
|
||||||
|
// stops being current.
|
||||||
|
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
|
||||||
|
const Uint previousName = previous->GetExternalIndex();
|
||||||
|
if (programs.HasProgramSlot(previousName) && programs.GetProgramObject(previousName) == previous) {
|
||||||
|
programs.MarkProgramObjectForDeletion(previousName, m_currentProgram);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ProgramObject>& GLContext::GetCurrentProgram() {
|
const SharedPtr<ProgramObject>& GLContext::GetCurrentProgram() {
|
||||||
return m_programState.GetCurrentProgram();
|
return m_currentProgram;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copies every default-block uniform value `source` holds into the same-named uniform of
|
// Copies every default-block uniform value `source` holds into the same-named uniform of
|
||||||
@@ -608,7 +672,7 @@ namespace MobileGL::MG_State {
|
|||||||
|
|
||||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
|
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
|
||||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
const auto& currentProgram = GetCurrentProgram();
|
||||||
if (currentProgram) {
|
if (currentProgram) {
|
||||||
// P1 join site J1, plain glUseProgram half. The backends read a program's
|
// P1 join site J1, plain glUseProgram half. The backends read a program's
|
||||||
// lifetimeId / backendStateVersion / UBO content version to decide whether
|
// lifetimeId / backendStateVersion / UBO content version to decide whether
|
||||||
@@ -762,7 +826,7 @@ namespace MobileGL::MG_State {
|
|||||||
|
|
||||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForDispatch() {
|
const SharedPtr<ProgramObject>& GLContext::GetProgramForDispatch() {
|
||||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
const auto& currentProgram = GetCurrentProgram();
|
||||||
if (currentProgram) {
|
if (currentProgram) {
|
||||||
// Same join contract as GetProgramForDraw's glUseProgram half - see the note
|
// Same join contract as GetProgramForDraw's glUseProgram half - see the note
|
||||||
// there. A dispatch reads the same non-artifact versions a draw does.
|
// there. A dispatch reads the same non-artifact versions a draw does.
|
||||||
@@ -784,7 +848,7 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
|
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
|
||||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
const auto& currentProgram = GetCurrentProgram();
|
||||||
if (currentProgram) return currentProgram;
|
if (currentProgram) return currentProgram;
|
||||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||||
@@ -1220,21 +1284,21 @@ namespace MobileGL::MG_State {
|
|||||||
|
|
||||||
// Sampler
|
// Sampler
|
||||||
void GLContext::GenSamplerNames(Uint number, Vector<Uint>& samplers) {
|
void GLContext::GenSamplerNames(Uint number, Vector<Uint>& samplers) {
|
||||||
m_samplerState.GenerateNames(number, samplers);
|
Group().Samplers().GenerateNames(number, samplers);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<SamplerObject>& GLContext::GetSamplerObject(Uint index) {
|
const SharedPtr<SamplerObject>& GLContext::GetSamplerObject(Uint index) {
|
||||||
return m_samplerState.GetSamplerObject(index);
|
return Group().Samplers().GetSamplerObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<SamplerObject>& GLContext::CreateSamplerObject(Uint index) {
|
const SharedPtr<SamplerObject>& GLContext::CreateSamplerObject(Uint index) {
|
||||||
return m_samplerState.CreateSamplerObject(index);
|
return Group().Samplers().CreateSamplerObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkSamplerObjectForDeletion(Uint index) {
|
void GLContext::MarkSamplerObjectForDeletion(Uint index) {
|
||||||
// Unbind the sampler from all texture units
|
// Unbind the sampler from all texture units
|
||||||
if (ValidateSamplerObject(index)) {
|
if (ValidateSamplerObject(index)) {
|
||||||
auto sampler = m_samplerState.GetSamplerObject(index);
|
auto sampler = Group().Samplers().GetSamplerObject(index);
|
||||||
for (Int unit = 0; unit < TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) {
|
for (Int unit = 0; unit < TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) {
|
||||||
auto& textureUnit = m_textureState.GetUnitObject(unit);
|
auto& textureUnit = m_textureState.GetUnitObject(unit);
|
||||||
if (textureUnit.GetSamplerObject() == sampler) {
|
if (textureUnit.GetSamplerObject() == sampler) {
|
||||||
@@ -1242,24 +1306,24 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_samplerState.MarkSamplerObjectForDeletion(index);
|
Group().Samplers().MarkSamplerObjectForDeletion(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateSamplerName(Uint index) const {
|
Bool GLContext::ValidateSamplerName(Uint index) const {
|
||||||
return m_samplerState.ValidateName(index);
|
return m_shareGroup->Samplers().ValidateName(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateSamplerObject(Uint index) const {
|
Bool GLContext::ValidateSamplerObject(Uint index) const {
|
||||||
return m_samplerState.ValidateSamplerObject(index);
|
return m_shareGroup->Samplers().ValidateSamplerObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderbuffer
|
// Renderbuffer
|
||||||
void GLContext::GenRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) {
|
void GLContext::GenRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) {
|
||||||
m_renderbufferState.GenerateNames(number, renderbuffers);
|
Group().GenerateRenderbufferNames(number, renderbuffers);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<RenderbufferObject>& GLContext::GetRenderbufferObject(Uint index) {
|
const SharedPtr<RenderbufferObject>& GLContext::GetRenderbufferObject(Uint index) {
|
||||||
return m_renderbufferState.GetRenderbufferObject(index);
|
return Group().GetRenderbufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
BindingSlot<RenderbufferObject>& GLContext::GetRenderbufferBindingSlot(RenderbufferTarget target) {
|
BindingSlot<RenderbufferObject>& GLContext::GetRenderbufferBindingSlot(RenderbufferTarget target) {
|
||||||
@@ -1267,19 +1331,21 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<RenderbufferObject>& GLContext::CreateRenderbufferObject(Uint index) {
|
const SharedPtr<RenderbufferObject>& GLContext::CreateRenderbufferObject(Uint index) {
|
||||||
return m_renderbufferState.CreateRenderbufferObject(index);
|
return Group().CreateRenderbufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkRenderbufferObjectForDeletion(Uint index) {
|
void GLContext::MarkRenderbufferObjectForDeletion(Uint index) {
|
||||||
m_renderbufferState.MarkRenderbufferObjectForDeletion(index);
|
const auto renderbufferObject = Group().GetRenderbufferObject(index);
|
||||||
|
m_renderbufferState.UnbindRenderbufferObject(renderbufferObject);
|
||||||
|
Group().MarkRenderbufferObjectForDeletion(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateRenderbufferName(Uint index) const {
|
Bool GLContext::ValidateRenderbufferName(Uint index) const {
|
||||||
return m_renderbufferState.ValidateName(index);
|
return m_shareGroup->ValidateRenderbufferName(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::ValidateRenderbufferObject(Uint index) const {
|
Bool GLContext::ValidateRenderbufferObject(Uint index) const {
|
||||||
return m_renderbufferState.ValidateRenderbufferObject(index);
|
return m_shareGroup->ValidateRenderbufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint64 GLContext::AllocateTransformFeedbackLifetimeId() {
|
Uint64 GLContext::AllocateTransformFeedbackLifetimeId() {
|
||||||
@@ -1288,12 +1354,23 @@ namespace MobileGL::MG_State {
|
|||||||
return nextId.fetch_add(1, std::memory_order_relaxed);
|
return nextId.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLContext::GLContext() {
|
GLContext::GLContext() : GLContext(MakeShared<ShareGroupState>()) {}
|
||||||
|
|
||||||
|
GLContext::GLContext(SharedPtr<ShareGroupState> shareGroup)
|
||||||
|
: m_shareGroup(shareGroup ? Move(shareGroup) : MakeShared<ShareGroupState>()) {
|
||||||
// The default transform feedback object (name 0) exists from the start of the context
|
// The default transform feedback object (name 0) exists from the start of the context
|
||||||
// (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it.
|
// (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it.
|
||||||
// Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain
|
// Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain
|
||||||
// const read instead of an operator[] insert on the draw path.
|
// const read instead of an operator[] insert on the draw path.
|
||||||
m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId;
|
m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId;
|
||||||
|
InstallDefaultFramebuffer(DefaultFramebuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLContext::InstallDefaultFramebuffer(const SharedPtr<FramebufferObject>& framebuffer) {
|
||||||
|
if (!framebuffer) return;
|
||||||
|
m_framebufferState.RegisterFramebufferObject(0, framebuffer);
|
||||||
|
m_framebufferState.GetBindingSlot(FramebufferTarget::Draw).Bind(framebuffer);
|
||||||
|
m_framebufferState.GetBindingSlot(FramebufferTarget::Read).Bind(framebuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GLContext::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
|
Bool GLContext::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
|
||||||
@@ -1562,5 +1639,5 @@ namespace MobileGL::MG_State {
|
|||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
|
thread_local SharedPtr<GLState::GLContext> pGLContext;
|
||||||
} // namespace MobileGL::MG_State
|
} // namespace MobileGL::MG_State
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "ProgramState/ProgramState.h"
|
#include "ProgramState/ProgramState.h"
|
||||||
#include "ProgramState/ProgramPipelineObject.h"
|
#include "ProgramState/ProgramPipelineObject.h"
|
||||||
#include "SamplerState/SamplerState.h"
|
#include "SamplerState/SamplerState.h"
|
||||||
|
#include "ShareGroupState.h"
|
||||||
#include "TextureState/TextureState.h"
|
#include "TextureState/TextureState.h"
|
||||||
#include "FramebufferState/FramebufferState.h"
|
#include "FramebufferState/FramebufferState.h"
|
||||||
#include "VertexArrayState/VertexArrayState.h"
|
#include "VertexArrayState/VertexArrayState.h"
|
||||||
@@ -76,7 +77,19 @@ namespace MobileGL {
|
|||||||
|
|
||||||
class GLContext {
|
class GLContext {
|
||||||
public:
|
public:
|
||||||
|
// A context with a share group of its own. Created through EGL, a context
|
||||||
|
// either starts a new group or joins the one its share context belongs to;
|
||||||
|
// this constructor is the standalone shape the tests and the process default
|
||||||
|
// context use.
|
||||||
GLContext();
|
GLContext();
|
||||||
|
explicit GLContext(SharedPtr<ShareGroupState> shareGroup);
|
||||||
|
|
||||||
|
// The GL objects this context shares with every context that shares with it.
|
||||||
|
const SharedPtr<ShareGroupState>& GetShareGroup() const { return m_shareGroup; }
|
||||||
|
|
||||||
|
// Installs the process-wide default framebuffer (GL name 0) into this context:
|
||||||
|
// its table entry and the Draw/Read bindings the context starts with.
|
||||||
|
void InstallDefaultFramebuffer(const SharedPtr<FramebufferObject>& framebuffer);
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
|
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
|
||||||
@@ -188,7 +201,7 @@ namespace MobileGL {
|
|||||||
// P1 stage 6: the per-context index of adoptable compile nodes, for its
|
// P1 stage 6: the per-context index of adoptable compile nodes, for its
|
||||||
// adoption counter. Diagnostics and tests only - no GL entry point reads it.
|
// adoption counter. Diagnostics and tests only - no GL entry point reads it.
|
||||||
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() {
|
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() {
|
||||||
return m_programState.GetShaderCompileAdoptionMap();
|
return Group().Programs().GetShaderCompileAdoptionMap();
|
||||||
}
|
}
|
||||||
void UseProgram(Uint program);
|
void UseProgram(Uint program);
|
||||||
const SharedPtr<ProgramObject>& GetCurrentProgram();
|
const SharedPtr<ProgramObject>& GetCurrentProgram();
|
||||||
@@ -606,6 +619,10 @@ namespace MobileGL {
|
|||||||
void InvalidateCompileEnv();
|
void InvalidateCompileEnv();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
ShareGroupState& Group() const { return *m_shareGroup; }
|
||||||
|
|
||||||
|
// The GL objects shared with every context that shares with this one.
|
||||||
|
SharedPtr<ShareGroupState> m_shareGroup;
|
||||||
// State Components
|
// State Components
|
||||||
ErrorState m_errorState;
|
ErrorState m_errorState;
|
||||||
BufferState m_bufferState;
|
BufferState m_bufferState;
|
||||||
@@ -685,10 +702,11 @@ namespace MobileGL {
|
|||||||
IndexGenerator<Uint> m_programPipelineNames;
|
IndexGenerator<Uint> m_programPipelineNames;
|
||||||
Uint m_boundProgramPipeline = 0;
|
Uint m_boundProgramPipeline = 0;
|
||||||
TextureState m_textureState;
|
TextureState m_textureState;
|
||||||
ProgramState m_programState;
|
// The program this context has in use. Program STATE (the objects) belongs to
|
||||||
|
// the share group; which of them is current does not.
|
||||||
|
SharedPtr<ProgramObject> m_currentProgram;
|
||||||
RenderState m_renderState;
|
RenderState m_renderState;
|
||||||
FramebufferState m_framebufferState;
|
FramebufferState m_framebufferState;
|
||||||
SamplerState m_samplerState;
|
|
||||||
RenderbufferState m_renderbufferState;
|
RenderbufferState m_renderbufferState;
|
||||||
|
|
||||||
mutable SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> m_compileEnv;
|
mutable SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> m_compileEnv;
|
||||||
@@ -698,7 +716,32 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
extern UniquePtr<GLState::GLContext>& pGLContext;
|
// P14 S3 (docs/Disaggregated/design/11-state-ownership.md): the GL state the CALLING
|
||||||
|
// THREAD's GL entry points read and write. eglMakeCurrent points it at that EGL
|
||||||
|
// context's GLContext, and the EGL release edge puts it back to whatever it was, so a
|
||||||
|
// thread that never crossed an eglMakeCurrent keeps the process default MG_State::Init()
|
||||||
|
// gave it. NULL on a thread for which neither has happened, the same answer a process
|
||||||
|
// that never reached MG_State::Init() (the spawn server image) has always given.
|
||||||
|
extern thread_local SharedPtr<GLState::GLContext> pGLContext;
|
||||||
|
|
||||||
|
// The fallback pGLContext starts at and an EGL release edge restores to: the process's
|
||||||
|
// one context for every thread that never bound an EGL context to itself, created on
|
||||||
|
// demand and owned here (leak-at-exit, like every other process global).
|
||||||
|
const SharedPtr<GLState::GLContext>& ProcessDefaultGLContext();
|
||||||
|
|
||||||
|
// Drops the process default context and this thread's binding to it, and the default
|
||||||
|
// framebuffer with them. Called on the teardown edge (the last eglTerminate, before
|
||||||
|
// MobileGL::Destroy()) so the objects MG_State::Init() built do not outlive
|
||||||
|
// glslang::FinalizeProcess() through a thread that kept its fallback binding.
|
||||||
|
void ReleaseProcessDefaultGLContext();
|
||||||
|
|
||||||
|
// P14 S3. GL framebuffer 0, built by MG_Impl once the backend is up (it needs texture
|
||||||
|
// objects, which need the backend's resource plumbing) and handed back here so every
|
||||||
|
// GLContext a later eglCreateContext builds starts with the same default framebuffer
|
||||||
|
// bound. Until the backend grows per-context native tuples there is exactly one such
|
||||||
|
// object, which is what the backend's own default-framebuffer identity tests assume.
|
||||||
|
void SetDefaultFramebuffer(SharedPtr<GLState::FramebufferObject> framebuffer);
|
||||||
|
const SharedPtr<GLState::FramebufferObject>& DefaultFramebuffer();
|
||||||
|
|
||||||
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
|
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
|
||||||
// current EGL context explicitly requested a core profile (core bit in
|
// current EGL context explicitly requested a core profile (core bit in
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return framebufferObject;
|
return framebufferObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FramebufferState::RegisterFramebufferObject(Uint index, SharedPtr<FramebufferObject> object) {
|
||||||
|
if (!object) return;
|
||||||
|
if (index == 0 && !m_indexGenerator.IsValid(0)) {
|
||||||
|
m_indexGenerator.Insert(0);
|
||||||
|
}
|
||||||
|
m_framebufferObjects[index] = Move(object);
|
||||||
|
}
|
||||||
|
|
||||||
BindingSlot<FramebufferObject>& FramebufferState::GetBindingSlot(FramebufferTarget target) {
|
BindingSlot<FramebufferObject>& FramebufferState::GetBindingSlot(FramebufferTarget target) {
|
||||||
for (auto& bindingSlot : m_bindingSlots) {
|
for (auto& bindingSlot : m_bindingSlots) {
|
||||||
if (bindingSlot.GetTarget() == target) {
|
if (bindingSlot.GetTarget() == target) {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
||||||
void GenerateNames(Uint number, Vector<Uint>& framebuffers);
|
void GenerateNames(Uint number, Vector<Uint>& framebuffers);
|
||||||
const SharedPtr<FramebufferObject>& CreateFramebufferObject(Uint index);
|
const SharedPtr<FramebufferObject>& CreateFramebufferObject(Uint index);
|
||||||
|
// Puts an already-built object at `index` without the "name 0 is created once" rule,
|
||||||
|
// so a context can adopt the process-wide default framebuffer.
|
||||||
|
void RegisterFramebufferObject(Uint index, SharedPtr<FramebufferObject> object);
|
||||||
BindingSlot<FramebufferObject>& GetBindingSlot(FramebufferTarget target);
|
BindingSlot<FramebufferObject>& GetBindingSlot(FramebufferTarget target);
|
||||||
void MarkFramebufferObjectForDeletion(Uint index);
|
void MarkFramebufferObjectForDeletion(Uint index);
|
||||||
Bool ValidateName(Uint index) const;
|
Bool ValidateName(Uint index) const;
|
||||||
|
|||||||
@@ -25,19 +25,24 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_programObjects[id];
|
return m_programObjects[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::MarkProgramObjectForDeletion(const Uint program) {
|
Bool ProgramState::HasProgramSlot(const Uint program) const {
|
||||||
|
return CheckIndexAvail(program, m_programObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramState::MarkProgramObjectForDeletion(const Uint program,
|
||||||
|
const SharedPtr<ProgramObject>& currentProgram) {
|
||||||
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
||||||
auto& programObject = m_programObjects[program];
|
auto& programObject = m_programObjects[program];
|
||||||
if (programObject != nullptr) {
|
if (programObject != nullptr) {
|
||||||
programObject->MarkAsDeleted();
|
programObject->MarkAsDeleted();
|
||||||
// A program in use is only FLAGGED: its name (and every program query) stays
|
// A program in use is only FLAGGED: its name (and every program query) stays
|
||||||
// valid until it stops being current, at which point UseProgram finishes the job.
|
// valid until it stops being current, at which point UseProgram finishes the job.
|
||||||
if (programObject == m_currentProgram) return;
|
if (programObject == currentProgram) return;
|
||||||
DestroyProgramSlot(program);
|
DestroyProgramSlot(program, currentProgram);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::DestroyProgramSlot(const Uint program) {
|
void ProgramState::DestroyProgramSlot(const Uint program, const SharedPtr<ProgramObject>& currentProgram) {
|
||||||
auto& programObject = m_programObjects[program];
|
auto& programObject = m_programObjects[program];
|
||||||
// P1 join site J4/J5 (glDeleteProgram, and the deferred destroy UseProgram performs
|
// P1 join site J4/J5 (glDeleteProgram, and the deferred destroy UseProgram performs
|
||||||
// when a deletion-flagged program stops being current). The program's name is about
|
// when a deletion-flagged program stops being current). The program's name is about
|
||||||
@@ -55,7 +60,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
for (const auto& shader : attachedShaders) {
|
for (const auto& shader : attachedShaders) {
|
||||||
const Uint shaderName = shader->GetExternalIndex();
|
const Uint shaderName = shader->GetExternalIndex();
|
||||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||||
ReleaseShaderNameIfOrphaned(shaderName);
|
ReleaseShaderNameIfOrphaned(shaderName, currentProgram);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,25 +69,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return CheckIndexAvail(program, m_programObjects) && m_programObjects[program] != nullptr;
|
return CheckIndexAvail(program, m_programObjects) && m_programObjects[program] != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::UseProgram(Uint program) {
|
|
||||||
const SharedPtr<ProgramObject> previous = m_currentProgram;
|
|
||||||
|
|
||||||
if (program == 0) m_currentProgram.reset();
|
|
||||||
|
|
||||||
if (CheckIndexAvail(program, m_programObjects)) {
|
|
||||||
m_currentProgram = m_programObjects[program];
|
|
||||||
}
|
|
||||||
|
|
||||||
// A deletion flagged while the program was current takes effect the moment it
|
|
||||||
// stops being current.
|
|
||||||
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
|
|
||||||
const Uint previousName = previous->GetExternalIndex();
|
|
||||||
if (CheckIndexAvail(previousName, m_programObjects) && m_programObjects[previousName] == previous) {
|
|
||||||
DestroyProgramSlot(previousName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint ProgramState::CreateShader(ShaderStage stage) {
|
Uint ProgramState::CreateShader(ShaderStage stage) {
|
||||||
Uint shaderId = 0;
|
Uint shaderId = 0;
|
||||||
m_programShaderNameGenerator.Generate(1, &shaderId);
|
m_programShaderNameGenerator.Generate(1, &shaderId);
|
||||||
@@ -100,7 +86,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_shaderObjects[shader];
|
return m_shaderObjects[shader];
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::JoinAllPendingWork() {
|
void ProgramState::JoinAllPendingWork(const SharedPtr<ProgramObject>& currentProgram) {
|
||||||
// Programs first: a link joins the compiles it depends on, so the shader pass that
|
// Programs first: a link joins the compiles it depends on, so the shader pass that
|
||||||
// follows finds most of them already settled. The reverse order would be correct but
|
// follows finds most of them already settled. The reverse order would be correct but
|
||||||
// would wait on each compile twice - once here, once inside the link's own prologue.
|
// would wait on each compile twice - once here, once inside the link's own prologue.
|
||||||
@@ -126,10 +112,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// The currently-used program is reachable through m_programObjects unless
|
// The currently-used program is reachable through m_programObjects unless
|
||||||
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
|
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
|
||||||
// a GL-visible name for it, but a draw would still join it, so settle it here too.
|
// a GL-visible name for it, but a draw would still join it, so settle it here too.
|
||||||
if (m_currentProgram) m_currentProgram->JoinLinkAndSpirv();
|
if (currentProgram) currentProgram->JoinLinkAndSpirv();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
void ProgramState::MarkShaderObjectForDeletion(Uint shader, const SharedPtr<ProgramObject>& currentProgram) {
|
||||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||||
auto& shaderObject = m_shaderObjects[shader];
|
auto& shaderObject = m_shaderObjects[shader];
|
||||||
if (shaderObject != nullptr) {
|
if (shaderObject != nullptr) {
|
||||||
@@ -138,11 +124,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// from every program. The GL CTS compiles shaders through exactly this
|
// from every program. The GL CTS compiles shaders through exactly this
|
||||||
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
|
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
|
||||||
shaderObject->MarkAsDeleted();
|
shaderObject->MarkAsDeleted();
|
||||||
ReleaseShaderNameIfOrphaned(shader);
|
ReleaseShaderNameIfOrphaned(shader, currentProgram);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const {
|
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject,
|
||||||
|
const SharedPtr<ProgramObject>& currentProgram) const {
|
||||||
for (const auto& programObject : m_programObjects) {
|
for (const auto& programObject : m_programObjects) {
|
||||||
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
|
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -150,14 +137,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
// A program deleted while current vacates its table slot but stays alive as the
|
// A program deleted while current vacates its table slot but stays alive as the
|
||||||
// current program; its attachments still count.
|
// current program; its attachments still count.
|
||||||
return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject);
|
return currentProgram != nullptr && currentProgram->ShaderIsAttachedGLVisible(shaderObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) {
|
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader, const SharedPtr<ProgramObject>& currentProgram) {
|
||||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||||
auto& shaderObject = m_shaderObjects[shader];
|
auto& shaderObject = m_shaderObjects[shader];
|
||||||
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
|
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
|
||||||
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
|
if (ShaderHasGLVisibleAttachment(shaderObject, currentProgram)) return;
|
||||||
// The name is about to go, so nothing can observe this shader's compile through THIS
|
// The name is about to go, so nothing can observe this shader's compile through THIS
|
||||||
// object any more and a job still in flight for it is pure waste - unless another
|
// object any more and a job still in flight for it is pure waste - unless another
|
||||||
// shader object adopted the same node (stage 6) or a pending link pinned it, which is
|
// shader object adopted the same node (stage 6) or a pending link pinned it, which is
|
||||||
|
|||||||
@@ -14,28 +14,31 @@
|
|||||||
#include "ShaderPreprocessCache.h"
|
#include "ShaderPreprocessCache.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// The program and shader objects of one share group, and the name space they share. The
|
||||||
|
// program a context has in use is context state and lives on GLContext - the operations
|
||||||
|
// that need it (a delete of the current program, the deferred destroy UseProgram
|
||||||
|
// performs, the orphan test) take it as a parameter.
|
||||||
class ProgramState {
|
class ProgramState {
|
||||||
public:
|
public:
|
||||||
// This function WILL actually create the program object.
|
// This function WILL actually create the program object.
|
||||||
// To retrieve created program object, use GetProgramObject()
|
// To retrieve created program object, use GetProgramObject()
|
||||||
Uint CreateProgram();
|
Uint CreateProgram();
|
||||||
const SharedPtr<ProgramObject>& GetProgramObject(Uint id);
|
const SharedPtr<ProgramObject>& GetProgramObject(Uint id);
|
||||||
void MarkProgramObjectForDeletion(Uint program);
|
void MarkProgramObjectForDeletion(Uint program, const SharedPtr<ProgramObject>& currentProgram);
|
||||||
Bool ValidateProgramObject(Uint program) const;
|
Bool ValidateProgramObject(Uint program) const;
|
||||||
|
// Whether `program` names a slot in the table at all, i.e. in range - not whether that
|
||||||
void UseProgram(Uint program);
|
// slot currently holds an object.
|
||||||
|
Bool HasProgramSlot(Uint program) const;
|
||||||
|
|
||||||
Uint CreateShader(ShaderStage stage);
|
Uint CreateShader(ShaderStage stage);
|
||||||
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
|
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
|
||||||
void MarkShaderObjectForDeletion(Uint shader);
|
void MarkShaderObjectForDeletion(Uint shader, const SharedPtr<ProgramObject>& currentProgram);
|
||||||
// Frees a deletion-flagged shader's name once no program holds a GL-visible
|
// Frees a deletion-flagged shader's name once no program holds a GL-visible
|
||||||
// attachment to it (the deferred half of glDeleteShader-while-attached).
|
// attachment to it (the deferred half of glDeleteShader-while-attached).
|
||||||
void ReleaseShaderNameIfOrphaned(Uint shader);
|
void ReleaseShaderNameIfOrphaned(Uint shader, const SharedPtr<ProgramObject>& currentProgram);
|
||||||
Bool ValidateShaderObject(Uint shader) const;
|
Bool ValidateShaderObject(Uint shader) const;
|
||||||
|
|
||||||
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
|
// Joins every outstanding compile and link this share group still owns, publishing each
|
||||||
|
|
||||||
// Joins every outstanding compile and link this context still owns, publishing each
|
|
||||||
// one's artifacts through the ordinary gates. The single caller is
|
// one's artifacts through the ordinary gates. The single caller is
|
||||||
// glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero
|
// glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero
|
||||||
// count to leave nothing in flight, so that every subsequent
|
// count to leave nothing in flight, so that every subsequent
|
||||||
@@ -46,7 +49,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// compilation straight back. Nodes belonging to objects this context has already
|
// compilation straight back. Nodes belonging to objects this context has already
|
||||||
// dropped are not joined - nothing can observe them, and waiting on them would make
|
// dropped are not joined - nothing can observe them, and waiting on them would make
|
||||||
// a GL call's cost depend on garbage.
|
// a GL call's cost depend on garbage.
|
||||||
void JoinAllPendingWork();
|
void JoinAllPendingWork(const SharedPtr<ProgramObject>& currentProgram);
|
||||||
|
|
||||||
// P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it
|
// P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it
|
||||||
// directly - shader objects reach it through the pointer they are handed at
|
// directly - shader objects reach it through the pointer they are handed at
|
||||||
@@ -59,10 +62,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { return *m_shaderCompileAdoptionMap; }
|
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { return *m_shaderCompileAdoptionMap; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject,
|
||||||
|
const SharedPtr<ProgramObject>& currentProgram) const;
|
||||||
// Frees the name slot and releases orphaned attached shaders; the immediate half
|
// Frees the name slot and releases orphaned attached shaders; the immediate half
|
||||||
// of glDeleteProgram (deferred while the program is current).
|
// of glDeleteProgram (deferred while the program is current).
|
||||||
void DestroyProgramSlot(Uint program);
|
void DestroyProgramSlot(Uint program, const SharedPtr<ProgramObject>& currentProgram);
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
||||||
@@ -96,7 +100,5 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
Vector<SharedPtr<ProgramObject>> m_programObjects;
|
Vector<SharedPtr<ProgramObject>> m_programObjects;
|
||||||
Vector<SharedPtr<ShaderObject>> m_shaderObjects;
|
Vector<SharedPtr<ShaderObject>> m_shaderObjects;
|
||||||
|
|
||||||
SharedPtr<ProgramObject> m_currentProgram;
|
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -10,34 +10,12 @@
|
|||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
RenderbufferState::RenderbufferState() : m_indexGenerator(1024, 1) {
|
RenderbufferState::RenderbufferState() {
|
||||||
for (SizeT i = 0; i < m_bindingSlots.size(); ++i) {
|
for (SizeT i = 0; i < m_bindingSlots.size(); ++i) {
|
||||||
m_bindingSlots[i] = BindingSlot<RenderbufferObject>(static_cast<RenderbufferTarget>(i));
|
m_bindingSlots[i] = BindingSlot<RenderbufferObject>(static_cast<RenderbufferTarget>(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<RenderbufferObject>& RenderbufferState::GetRenderbufferObject(Uint index) {
|
|
||||||
auto it = m_renderbufferObjects.find(index);
|
|
||||||
if (it != m_renderbufferObjects.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
static SharedPtr<RenderbufferObject> nullRenderbufferObject = nullptr;
|
|
||||||
return nullRenderbufferObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderbufferState::GenerateNames(Uint number, Vector<Uint>& renderbuffers) {
|
|
||||||
renderbuffers.resize(number);
|
|
||||||
m_indexGenerator.Generate(number, renderbuffers.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
const SharedPtr<RenderbufferObject>& RenderbufferState::CreateRenderbufferObject(Uint index) {
|
|
||||||
auto& bufferObject = m_renderbufferObjects[index];
|
|
||||||
if (!bufferObject) {
|
|
||||||
bufferObject = MakeShared<RenderbufferObject>(index);
|
|
||||||
}
|
|
||||||
return bufferObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
BindingSlot<RenderbufferObject>& RenderbufferState::GetBindingSlot(RenderbufferTarget target) {
|
BindingSlot<RenderbufferObject>& RenderbufferState::GetBindingSlot(RenderbufferTarget target) {
|
||||||
for (auto& bindingSlot : m_bindingSlots) {
|
for (auto& bindingSlot : m_bindingSlots) {
|
||||||
if (bindingSlot.GetTarget() == target) {
|
if (bindingSlot.GetTarget() == target) {
|
||||||
@@ -48,26 +26,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_bindingSlots[0];
|
return m_bindingSlots[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderbufferState::MarkRenderbufferObjectForDeletion(Uint index) {
|
void RenderbufferState::UnbindRenderbufferObject(const SharedPtr<RenderbufferObject>& renderbufferObject) {
|
||||||
if (m_indexGenerator.IsValid(index)) {
|
if (!renderbufferObject) return;
|
||||||
auto it = m_renderbufferObjects.find(index);
|
for (auto& bindingSlot : m_bindingSlots) {
|
||||||
if (it != m_renderbufferObjects.end()) {
|
if (bindingSlot.GetBoundObject() == renderbufferObject) {
|
||||||
for (auto& bindingSlot : m_bindingSlots) {
|
bindingSlot.Bind(nullptr);
|
||||||
if (bindingSlot.GetBoundObject() == it->second) {
|
|
||||||
bindingSlot.Bind(nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_renderbufferObjects.erase(index);
|
|
||||||
}
|
}
|
||||||
m_indexGenerator.Delete(index);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool RenderbufferState::ValidateName(Uint index) const {
|
|
||||||
return m_indexGenerator.IsValid(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool RenderbufferState::ValidateRenderbufferObject(Uint index) const {
|
|
||||||
return m_renderbufferObjects.find(index) != m_renderbufferObjects.end();
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -12,21 +12,16 @@
|
|||||||
#include "RenderbufferObject.h"
|
#include "RenderbufferObject.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// The per-context half of the renderbuffer state: which renderbuffer is bound. The
|
||||||
|
// objects and their names belong to the share group (ShareGroupState).
|
||||||
class RenderbufferState {
|
class RenderbufferState {
|
||||||
public:
|
public:
|
||||||
RenderbufferState();
|
RenderbufferState();
|
||||||
|
|
||||||
const SharedPtr<RenderbufferObject>& GetRenderbufferObject(Uint index);
|
|
||||||
void GenerateNames(Uint number, Vector<Uint>& renderbuffers);
|
|
||||||
const SharedPtr<RenderbufferObject>& CreateRenderbufferObject(Uint index);
|
|
||||||
BindingSlot<RenderbufferObject>& GetBindingSlot(RenderbufferTarget target);
|
BindingSlot<RenderbufferObject>& GetBindingSlot(RenderbufferTarget target);
|
||||||
void MarkRenderbufferObjectForDeletion(Uint index);
|
void UnbindRenderbufferObject(const SharedPtr<RenderbufferObject>& renderbufferObject);
|
||||||
Bool ValidateName(Uint index) const;
|
|
||||||
Bool ValidateRenderbufferObject(Uint index) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UnorderedMap<Uint, SharedPtr<RenderbufferObject>> m_renderbufferObjects;
|
|
||||||
IndexGenerator<Uint> m_indexGenerator;
|
|
||||||
Array<BindingSlot<RenderbufferObject>, static_cast<SizeT>(RenderbufferTarget::RenderbufferTargetCount)>
|
Array<BindingSlot<RenderbufferObject>, static_cast<SizeT>(RenderbufferTarget::RenderbufferTargetCount)>
|
||||||
m_bindingSlots;
|
m_bindingSlots;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ShareGroupState.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "ShareGroupState.h"
|
||||||
|
|
||||||
|
#include "TextureState/TextureObject1D.h"
|
||||||
|
#include "TextureState/TextureObject2D.h"
|
||||||
|
#include "TextureState/TextureObject3D.h"
|
||||||
|
#include "TextureState/TextureObject2DCube.h"
|
||||||
|
#include "TextureState/TextureObjectBuffer.h"
|
||||||
|
#include "TextureState/TextureObjectStubs.h"
|
||||||
|
#include "TextureState/TextureObjectView.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
|
||||||
|
switch (target) {
|
||||||
|
case TextureTarget::Texture1D:
|
||||||
|
return MakeShared<TextureObject1D>(index);
|
||||||
|
case TextureTarget::TextureCubeMap:
|
||||||
|
return MakeShared<TextureObject2DCube>(index);
|
||||||
|
case TextureTarget::Texture2D:
|
||||||
|
return MakeShared<TextureObject2D>(index);
|
||||||
|
case TextureTarget::Texture3D:
|
||||||
|
return MakeShared<TextureObject3D>(index);
|
||||||
|
case TextureTarget::TextureBuffer:
|
||||||
|
return MakeShared<TextureObjectBuffer>(index);
|
||||||
|
|
||||||
|
// These texture types are still stubbed:
|
||||||
|
case TextureTarget::TextureRectangle:
|
||||||
|
return MakeShared<TextureObjectRectangle>(index);
|
||||||
|
case TextureTarget::Texture2DMultisample:
|
||||||
|
return MakeShared<TextureObject2DMultisample>(index);
|
||||||
|
case TextureTarget::Texture1DArray:
|
||||||
|
return MakeShared<TextureObject1DArray>(index);
|
||||||
|
case TextureTarget::Texture2DArray:
|
||||||
|
return MakeShared<TextureObject2DArray>(index);
|
||||||
|
case TextureTarget::TextureCubeMapArray:
|
||||||
|
return MakeShared<TextureObjectCubeMapArray>(index);
|
||||||
|
case TextureTarget::Texture2DMultisampleArray:
|
||||||
|
return MakeShared<TextureObject2DMultisampleArray>(index);
|
||||||
|
default:
|
||||||
|
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer
|
||||||
|
const SharedPtr<BufferObject>& ShareGroupState::GetBufferObject(Uint index) {
|
||||||
|
auto it = m_bufferObjects.find(index);
|
||||||
|
if (it != m_bufferObjects.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
static SharedPtr<BufferObject> nullBufferObject = nullptr;
|
||||||
|
return nullBufferObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareGroupState::GenerateBufferNames(Uint number, Vector<Uint>& buffers) {
|
||||||
|
buffers.resize(number);
|
||||||
|
m_bufferNames.Generate(number, buffers.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<BufferObject>& ShareGroupState::CreateBufferObject(Uint index) {
|
||||||
|
auto& bufferObj = m_bufferObjects[index];
|
||||||
|
if (!bufferObj) {
|
||||||
|
bufferObj = MakeShared<BufferObject>(index);
|
||||||
|
}
|
||||||
|
return bufferObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareGroupState::MarkBufferObjectForDeletion(Uint index) {
|
||||||
|
if (m_bufferNames.IsValid(index)) {
|
||||||
|
m_bufferObjects.erase(index);
|
||||||
|
m_bufferNames.Delete(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShareGroupState::ValidateBufferName(Uint index) const {
|
||||||
|
return m_bufferNames.IsValid(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShareGroupState::ValidateBufferObject(Uint index) const {
|
||||||
|
return m_bufferObjects.find(index) != m_bufferObjects.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture
|
||||||
|
void ShareGroupState::GenerateTextureNames(Uint number, Vector<Uint>& textures) {
|
||||||
|
textures.resize(number);
|
||||||
|
m_textureNames.Generate(number, textures.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<ITextureObject>& ShareGroupState::GetTextureObject(Uint index) {
|
||||||
|
auto it = m_textureObjects.find(index);
|
||||||
|
if (it != m_textureObjects.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
|
||||||
|
return nullTextureObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<ITextureObject>& ShareGroupState::CreateTextureObject(Uint index, TextureTarget target) {
|
||||||
|
auto& textureObject = m_textureObjects[index];
|
||||||
|
textureObject = MakeTextureObjectForTarget(index, target);
|
||||||
|
if (!textureObject) {
|
||||||
|
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
|
||||||
|
return nullTextureObject;
|
||||||
|
}
|
||||||
|
return textureObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<ITextureObject>& ShareGroupState::CreateTextureViewObject(
|
||||||
|
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
||||||
|
Uint numLevels, Uint minLayer, Uint numLayers) {
|
||||||
|
MOBILEGL_ASSERT(storageOwner != nullptr, "CreateTextureViewObject: storage owner is null");
|
||||||
|
auto& textureObject = m_textureObjects[index];
|
||||||
|
textureObject = MakeShared<TextureObjectView>(index, target, storageOwner, minLevel, numLevels, minLayer,
|
||||||
|
numLayers);
|
||||||
|
return textureObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareGroupState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
|
||||||
|
if (m_textureNames.IsValid(index)) {
|
||||||
|
auto it = m_textureObjects.find(index);
|
||||||
|
if (it != m_textureObjects.end()) {
|
||||||
|
m_textureObjects.erase(it);
|
||||||
|
m_textureNames.Delete(index);
|
||||||
|
} else if (!keepUnboundReservation) {
|
||||||
|
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
|
||||||
|
// reserved it and no bind ever instantiated an object (so a later bind of it must
|
||||||
|
// fail), and the reservation has to return to the free list.
|
||||||
|
m_textureNames.Delete(index);
|
||||||
|
}
|
||||||
|
// Relaxed semantics: legacy apps may delete a generated name before its first
|
||||||
|
// bind, then bind and populate that same name. Keep such a reservation alive there;
|
||||||
|
// a real texture object reaching deletion still releases its index above.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShareGroupState::ValidateTextureName(Uint index) const {
|
||||||
|
return m_textureNames.IsValid(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShareGroupState::ValidateTextureObject(Uint index) const {
|
||||||
|
return m_textureObjects.find(index) != m_textureObjects.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderbuffer
|
||||||
|
const SharedPtr<RenderbufferObject>& ShareGroupState::GetRenderbufferObject(Uint index) {
|
||||||
|
auto it = m_renderbufferObjects.find(index);
|
||||||
|
if (it != m_renderbufferObjects.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
static SharedPtr<RenderbufferObject> nullRenderbufferObject = nullptr;
|
||||||
|
return nullRenderbufferObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareGroupState::GenerateRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) {
|
||||||
|
renderbuffers.resize(number);
|
||||||
|
m_renderbufferNames.Generate(number, renderbuffers.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<RenderbufferObject>& ShareGroupState::CreateRenderbufferObject(Uint index) {
|
||||||
|
auto& bufferObject = m_renderbufferObjects[index];
|
||||||
|
if (!bufferObject) {
|
||||||
|
bufferObject = MakeShared<RenderbufferObject>(index);
|
||||||
|
}
|
||||||
|
return bufferObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareGroupState::MarkRenderbufferObjectForDeletion(Uint index) {
|
||||||
|
if (m_renderbufferNames.IsValid(index)) {
|
||||||
|
m_renderbufferObjects.erase(index);
|
||||||
|
m_renderbufferNames.Delete(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShareGroupState::ValidateRenderbufferName(Uint index) const {
|
||||||
|
return m_renderbufferNames.IsValid(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShareGroupState::ValidateRenderbufferObject(Uint index) const {
|
||||||
|
return m_renderbufferObjects.find(index) != m_renderbufferObjects.end();
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ShareGroupState.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
#include <MG_Util/Miscellany/IndexGenerator.h>
|
||||||
|
#include "BufferState/BufferObject.h"
|
||||||
|
#include "ProgramState/ProgramState.h"
|
||||||
|
#include "RenderbufferState/RenderbufferObject.h"
|
||||||
|
#include "SamplerState/SamplerState.h"
|
||||||
|
#include "TextureState/TextureObject.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// The GL object registries and name spaces a set of sharing contexts share, i.e. one EGL
|
||||||
|
// share group (GL 4.6 core 5.1). Buffers, textures, renderbuffers, samplers, programs and
|
||||||
|
// shaders live here; glGen* names are drawn from this object's generators.
|
||||||
|
//
|
||||||
|
// What is NOT here is what GL scopes to a single context - bindings, texture units, the
|
||||||
|
// current program, errors, and the container objects (VAO/FBO/query/pipeline/transform
|
||||||
|
// feedback) - so two contexts sharing a group see the same objects and their own bindings.
|
||||||
|
class ShareGroupState {
|
||||||
|
public:
|
||||||
|
ShareGroupState() = default;
|
||||||
|
explicit ShareGroupState(Uint64 token) : m_token(token) {}
|
||||||
|
|
||||||
|
// P14 S1's shareGroupToken: the identity the wire uses, 0 for a group no EGL context
|
||||||
|
// was made for.
|
||||||
|
Uint64 GetToken() const { return m_token; }
|
||||||
|
|
||||||
|
// Buffer
|
||||||
|
const SharedPtr<BufferObject>& GetBufferObject(Uint index);
|
||||||
|
void GenerateBufferNames(Uint number, Vector<Uint>& buffers);
|
||||||
|
const SharedPtr<BufferObject>& CreateBufferObject(Uint index);
|
||||||
|
void MarkBufferObjectForDeletion(Uint index);
|
||||||
|
Bool ValidateBufferName(Uint index) const;
|
||||||
|
Bool ValidateBufferObject(Uint index) const;
|
||||||
|
|
||||||
|
// Texture
|
||||||
|
void GenerateTextureNames(Uint number, Vector<Uint>& textures);
|
||||||
|
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||||
|
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||||
|
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
|
||||||
|
const SharedPtr<ITextureObject>& storageOwner,
|
||||||
|
Uint minLevel, Uint numLevels, Uint minLayer,
|
||||||
|
Uint numLayers);
|
||||||
|
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation);
|
||||||
|
Bool ValidateTextureName(Uint index) const;
|
||||||
|
Bool ValidateTextureObject(Uint index) const;
|
||||||
|
|
||||||
|
// Renderbuffer
|
||||||
|
const SharedPtr<RenderbufferObject>& GetRenderbufferObject(Uint index);
|
||||||
|
void GenerateRenderbufferNames(Uint number, Vector<Uint>& renderbuffers);
|
||||||
|
const SharedPtr<RenderbufferObject>& CreateRenderbufferObject(Uint index);
|
||||||
|
void MarkRenderbufferObjectForDeletion(Uint index);
|
||||||
|
Bool ValidateRenderbufferName(Uint index) const;
|
||||||
|
Bool ValidateRenderbufferObject(Uint index) const;
|
||||||
|
|
||||||
|
// Sampler objects carry no per-context state, so the whole table lives here.
|
||||||
|
SamplerState& Samplers() { return m_samplerState; }
|
||||||
|
|
||||||
|
// Program and shader objects, one name space for both (GL 3.3 core 2.11).
|
||||||
|
ProgramState& Programs() { return m_programState; }
|
||||||
|
const ProgramState& Programs() const { return m_programState; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Uint64 m_token = 0;
|
||||||
|
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
|
||||||
|
IndexGenerator<Uint> m_bufferNames{1024, 1};
|
||||||
|
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
|
||||||
|
IndexGenerator<Uint> m_textureNames{1024, 1};
|
||||||
|
UnorderedMap<Uint, SharedPtr<RenderbufferObject>> m_renderbufferObjects;
|
||||||
|
IndexGenerator<Uint> m_renderbufferNames{1024, 1};
|
||||||
|
SamplerState m_samplerState;
|
||||||
|
ProgramState m_programState;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "TextureObjectBuffer.h"
|
#include "TextureObjectBuffer.h"
|
||||||
#include "TextureObjectStubs.h"
|
#include "TextureObjectStubs.h"
|
||||||
#include "TextureObjectView.h"
|
#include "TextureObjectView.h"
|
||||||
|
#include "MG_State/GLState/ShareGroupState.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
static std::atomic<Uint64> s_nextTextureStateContextId = 1;
|
static std::atomic<Uint64> s_nextTextureStateContextId = 1;
|
||||||
@@ -27,42 +28,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed);
|
return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
|
TextureState::TextureState() : m_contextId(AllocateContextId()) {
|
||||||
switch (target) {
|
|
||||||
case TextureTarget::Texture1D:
|
|
||||||
return MakeShared<TextureObject1D>(index);
|
|
||||||
case TextureTarget::TextureCubeMap:
|
|
||||||
return MakeShared<TextureObject2DCube>(index);
|
|
||||||
case TextureTarget::Texture2D:
|
|
||||||
return MakeShared<TextureObject2D>(index);
|
|
||||||
case TextureTarget::Texture3D:
|
|
||||||
return MakeShared<TextureObject3D>(index);
|
|
||||||
case TextureTarget::TextureBuffer:
|
|
||||||
return MakeShared<TextureObjectBuffer>(index);
|
|
||||||
|
|
||||||
// These texture types are still stubbed:
|
|
||||||
case TextureTarget::TextureRectangle:
|
|
||||||
return MakeShared<TextureObjectRectangle>(index);
|
|
||||||
case TextureTarget::Texture2DMultisample:
|
|
||||||
return MakeShared<TextureObject2DMultisample>(index);
|
|
||||||
case TextureTarget::Texture1DArray:
|
|
||||||
return MakeShared<TextureObject1DArray>(index);
|
|
||||||
case TextureTarget::Texture2DArray:
|
|
||||||
return MakeShared<TextureObject2DArray>(index);
|
|
||||||
case TextureTarget::TextureCubeMapArray:
|
|
||||||
return MakeShared<TextureObjectCubeMapArray>(index);
|
|
||||||
case TextureTarget::Texture2DMultisampleArray:
|
|
||||||
return MakeShared<TextureObject2DMultisampleArray>(index);
|
|
||||||
default:
|
|
||||||
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureState::TextureState() : m_contextId(AllocateContextId()), m_indexGenerator(1024, 1) {
|
|
||||||
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
|
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
|
||||||
// shared across all texture units, and it is the initial binding of every unit/target
|
// shared across all texture units, and it is the initial binding of every unit/target
|
||||||
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
|
// slot. It is created outside the share group's table so name-based paths (glIsTexture,
|
||||||
// GenTextures/DeleteTextures, by-name DSA lookups) never see it.
|
// GenTextures/DeleteTextures, by-name DSA lookups) never see it.
|
||||||
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
|
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
|
||||||
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
|
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
|
||||||
@@ -75,83 +44,35 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& TextureState::GetTextureObject(Uint index) {
|
|
||||||
auto it = m_textureObjects.find(index);
|
|
||||||
if (it != m_textureObjects.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
|
|
||||||
return nullTextureObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
|
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
|
||||||
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
|
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
|
||||||
"GetDefaultTextureObject: invalid texture target %d", (int)target);
|
"GetDefaultTextureObject: invalid texture target %d", (int)target);
|
||||||
return m_defaultTextureObjects[(int)target];
|
return m_defaultTextureObjects[(int)target];
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureState::GenerateNames(Uint number, Vector<Uint>& textures) {
|
void TextureState::UnbindTextureFromUnits(const SharedPtr<ITextureObject>& textureObject) {
|
||||||
textures.resize(number);
|
if (!textureObject) return;
|
||||||
m_indexGenerator.Generate(number, textures.data());
|
// Units past the touched high-water mark can never reference a texture.
|
||||||
}
|
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
|
||||||
|
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
|
||||||
const SharedPtr<ITextureObject>& TextureState::CreateTextureObject(Uint index, TextureTarget target) {
|
for (auto& bindingSlot : bindingSlots) {
|
||||||
auto& textureObject = m_textureObjects[index];
|
if (bindingSlot.GetBoundObject() == textureObject) {
|
||||||
textureObject = MakeTextureObjectForTarget(index, target);
|
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
|
||||||
if (!textureObject) {
|
// target's default texture object, on every unit it was bound to.
|
||||||
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
|
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
|
||||||
return nullTextureObject;
|
|
||||||
}
|
|
||||||
return textureObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SharedPtr<ITextureObject>& TextureState::CreateTextureViewObject(
|
|
||||||
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
|
||||||
Uint numLevels, Uint minLayer, Uint numLayers) {
|
|
||||||
MOBILEGL_ASSERT(storageOwner != nullptr, "CreateTextureViewObject: storage owner is null");
|
|
||||||
auto& textureObject = m_textureObjects[index];
|
|
||||||
textureObject = MakeShared<TextureObjectView>(index, target, storageOwner, minLevel, numLevels, minLayer,
|
|
||||||
numLayers);
|
|
||||||
return textureObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
|
|
||||||
if (m_indexGenerator.IsValid(index)) {
|
|
||||||
auto it = m_textureObjects.find(index);
|
|
||||||
if (it != m_textureObjects.end()) {
|
|
||||||
// Units past the touched high-water mark can never reference a texture.
|
|
||||||
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
|
|
||||||
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
|
|
||||||
for (auto& bindingSlot : bindingSlots) {
|
|
||||||
if (bindingSlot.GetBoundObject() == it->second) {
|
|
||||||
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
|
|
||||||
// target's default texture object, on every unit it was bound to.
|
|
||||||
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
|
|
||||||
auto& imageBinding = m_imageTextureBindings[unit];
|
|
||||||
if (imageBinding.Texture == it->second) {
|
|
||||||
imageBinding.Bind(nullptr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Deleting a texture unbinds it from every unit above; treat that as a binding
|
|
||||||
// change so a cached sampled-texture set (which may hold this raw pointer) is
|
|
||||||
// re-resolved instead of dangling.
|
|
||||||
BumpTextureBindGeneration();
|
|
||||||
m_textureObjects.erase(index);
|
|
||||||
m_indexGenerator.Delete(index);
|
|
||||||
} else if (!keepUnboundReservation) {
|
|
||||||
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
|
|
||||||
// reserved it and no bind ever instantiated an object (so a later bind of it must
|
|
||||||
// fail), and the reservation has to return to the free list.
|
|
||||||
m_indexGenerator.Delete(index);
|
|
||||||
}
|
}
|
||||||
// Relaxed semantics: legacy apps may delete a generated name before its first
|
|
||||||
// bind, then bind and populate that same name. Keep such a reservation alive there;
|
|
||||||
// a real texture object reaching deletion still releases its index above.
|
|
||||||
}
|
}
|
||||||
|
for (Int unit = 0; unit <= m_maxTouchedUnit; ++unit) {
|
||||||
|
auto& imageBinding = m_imageTextureBindings[unit];
|
||||||
|
if (imageBinding.Texture == textureObject) {
|
||||||
|
imageBinding.Bind(nullptr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Deleting a texture unbinds it from every unit above; treat that as a binding
|
||||||
|
// change so a cached sampled-texture set (which may hold this raw pointer) is
|
||||||
|
// re-resolved instead of dangling.
|
||||||
|
BumpTextureBindGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureUnit& TextureState::GetUnitObject(Int unit) {
|
TextureUnit& TextureState::GetUnitObject(Int unit) {
|
||||||
@@ -179,12 +100,4 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void TextureState::SetActiveTextureUnit(Int unit) {
|
void TextureState::SetActiveTextureUnit(Int unit) {
|
||||||
m_activeTextureUnit = unit;
|
m_activeTextureUnit = unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool TextureState::ValidateName(Uint index) const {
|
|
||||||
return m_indexGenerator.IsValid(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool TextureState::ValidateTextureObject(Uint index) const {
|
|
||||||
return m_textureObjects.find(index) != m_textureObjects.end();
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
#include "TextureUnit.h"
|
#include "TextureUnit.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// The one place a texture object's concrete type is chosen for a target. Shared because the
|
||||||
|
// share group's texture table and the context's per-target default texture objects both
|
||||||
|
// build through it.
|
||||||
|
SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target);
|
||||||
|
|
||||||
struct ImageTextureBinding {
|
struct ImageTextureBinding {
|
||||||
SharedPtr<ITextureObject> Texture;
|
SharedPtr<ITextureObject> Texture;
|
||||||
GLint Level = 0;
|
GLint Level = 0;
|
||||||
@@ -45,6 +50,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The per-context half of the texture state: the texture units (and their per-target
|
||||||
|
// bindings), the image unit bindings, the active unit, and the default texture objects
|
||||||
|
// (name 0), which GL scopes to the context rather than the share group. The named texture
|
||||||
|
// objects and their name space belong to the share group (ShareGroupState).
|
||||||
class TextureState {
|
class TextureState {
|
||||||
public:
|
public:
|
||||||
// Capacity of the combined texture-unit state arrays (indexed by glActiveTexture unit).
|
// Capacity of the combined texture-unit state arrays (indexed by glActiveTexture unit).
|
||||||
@@ -56,30 +65,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
static constexpr int MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32;
|
static constexpr int MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32;
|
||||||
|
|
||||||
TextureState();
|
TextureState();
|
||||||
void GenerateNames(Uint number, Vector<Uint>& textures);
|
|
||||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
|
||||||
// glTextureView (GL 4.6 core 8.18). `storageOwner` must already be a texture with
|
|
||||||
// immutable storage and must NOT itself be a view - the caller composes a view-of-a-view
|
|
||||||
// onto the root first, and passes the composed (root-relative) level/layer range here.
|
|
||||||
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
|
|
||||||
const SharedPtr<ITextureObject>& storageOwner,
|
|
||||||
Uint minLevel, Uint numLevels, Uint minLayer,
|
|
||||||
Uint numLayers);
|
|
||||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
|
||||||
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
|
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
|
||||||
// zero names a real, per-target texture object shared by every texture unit; binding 0
|
// zero names a real, per-target texture object shared by every texture unit; binding 0
|
||||||
// binds it, and image/parameter calls on it must work like on any texture. It is not a
|
// binds it, and image/parameter calls on it must work like on any texture. It is not a
|
||||||
// GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE
|
// GenTextures name: it lives outside the share group's table (so glIsTexture(0) stays
|
||||||
// and by-name lookups keep failing for 0) and can never be deleted.
|
// GL_FALSE and by-name lookups keep failing for 0) and can never be deleted.
|
||||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||||
TextureUnit& GetUnitObject(Int unit);
|
TextureUnit& GetUnitObject(Int unit);
|
||||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
|
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
|
||||||
Int GetActiveTextureUnit() const;
|
Int GetActiveTextureUnit() const;
|
||||||
void SetActiveTextureUnit(Int unit);
|
void SetActiveTextureUnit(Int unit);
|
||||||
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation);
|
// Rebinds every unit and image unit that names `textureObject` back to the target's
|
||||||
Bool ValidateName(Uint index) const;
|
// default texture object; this context's half of a texture delete (GL 4.6 core 8.1).
|
||||||
Bool ValidateTextureObject(Uint index) const;
|
void UnbindTextureFromUnits(const SharedPtr<ITextureObject>& textureObject);
|
||||||
|
|
||||||
// High-water mark of texture units ever touched by a texture or sampler bind.
|
// High-water mark of texture units ever touched by a texture or sampler bind.
|
||||||
// Units above it have provably-empty binding slots, so per-draw backend scans
|
// Units above it have provably-empty binding slots, so per-draw backend scans
|
||||||
@@ -209,8 +208,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Int m_activeTextureUnit = 0;
|
Int m_activeTextureUnit = 0;
|
||||||
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
|
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
|
||||||
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
|
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
|
||||||
IndexGenerator<Uint> m_indexGenerator;
|
|
||||||
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
|
|
||||||
// One default texture object (external name 0) per target, created with the context and
|
// One default texture object (external name 0) per target, created with the context and
|
||||||
// immortal for its lifetime; the initial binding of every unit/target slot.
|
// immortal for its lifetime; the initial binding of every unit/target slot.
|
||||||
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
|
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ namespace {
|
|||||||
Uint32 m_blockKb = 64;
|
Uint32 m_blockKb = 64;
|
||||||
Uint32 m_adoptTier = 2;
|
Uint32 m_adoptTier = 2;
|
||||||
Bool m_pipeStats = false;
|
Bool m_pipeStats = false;
|
||||||
UniquePtr<MG_State::GLState::GLContext> m_context;
|
SharedPtr<MG_State::GLState::GLContext> m_context;
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ namespace {
|
|||||||
#endif
|
#endif
|
||||||
MG_State::pGLContext = Move(m_previous);
|
MG_State::pGLContext = Move(m_previous);
|
||||||
}
|
}
|
||||||
UniquePtr<GLContext> m_previous;
|
SharedPtr<GLContext> m_previous;
|
||||||
};
|
};
|
||||||
|
|
||||||
SizeT Index(MGPipeInputField field) { return static_cast<SizeT>(field); }
|
SizeT Index(MGPipeInputField field) { return static_cast<SizeT>(field); }
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ namespace {
|
|||||||
FramebufferScope(const FramebufferScope&) = delete;
|
FramebufferScope(const FramebufferScope&) = delete;
|
||||||
FramebufferScope& operator=(const FramebufferScope&) = delete;
|
FramebufferScope& operator=(const FramebufferScope&) = delete;
|
||||||
|
|
||||||
UniquePtr<GLContext> m_previousContext;
|
SharedPtr<GLContext> m_previousContext;
|
||||||
Uint64 m_previousPush = 0;
|
Uint64 m_previousPush = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
|||||||
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
|
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
|
||||||
// kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's
|
// kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's
|
||||||
// set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record.
|
// set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record.
|
||||||
EXPECT_EQ(ClassCount<kScreen>(), 12u);
|
// + P14 S1's bind_context (docs/Disaggregated/design/11-state-ownership.md), the in-band
|
||||||
|
// context binding - a kScreen row for applier_reset's reason: a context switch is a
|
||||||
|
// whole-session edge, not a context-family verb.
|
||||||
|
EXPECT_EQ(ClassCount<kScreen>(), 13u);
|
||||||
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
|
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
|
||||||
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
|
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
|
||||||
EXPECT_EQ(ClassCount<kCtxState>(), 19u);
|
EXPECT_EQ(ClassCount<kCtxState>(), 19u);
|
||||||
@@ -644,7 +647,12 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
|||||||
// opcodes 82..83, one payload, no flag at all - no reply slot, no blob, no tail.
|
// opcodes 82..83, one payload, no flag at all - no reply slot, no blob, no tail.
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ReadPixelsToBuffer), 82);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ReadPixelsToBuffer), 82);
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetTextureImageToBuffer), 83);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetTextureImageToBuffer), 83);
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 84);
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended the in-band context
|
||||||
|
// binding, by the same rule: opcode 84, and nothing before it moved. bind_context is a
|
||||||
|
// kScreen control record with the applier_reset shape - one POD, no blob, no tail, no reply
|
||||||
|
// and no MGPipeApply* entry point - so the row beside it is pinned the same way.
|
||||||
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindContext), 84);
|
||||||
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 85);
|
||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint32>(kNone));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint32>(kNone));
|
||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint32>(kNone));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint32>(kNone));
|
||||||
EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u);
|
EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u);
|
||||||
@@ -653,6 +661,13 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
|||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
|
||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
|
||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(kNone));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(kNone));
|
||||||
|
// P14 S1's row, pinned like the two control records it sits beside: no blob, no tail, no
|
||||||
|
// reply, and kWaitNone - the record's placement in the ring is the whole of its ordering, so
|
||||||
|
// the client publishes and moves on rather than parking on an apply that reads no client
|
||||||
|
// memory (PipeCalls.def's wait-class column).
|
||||||
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindContext), static_cast<Uint32>(kNone));
|
||||||
|
EXPECT_EQ(MGPipeCallClassFor(MGPWireOp::BindContext), kScreen);
|
||||||
|
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::BindContext), kWaitNone);
|
||||||
// And the P5b rows carry what their contract says: one blob (the block name) and nothing
|
// And the P5b rows carry what their contract says: one blob (the block name) and nothing
|
||||||
// else, and the extended draw row keeps its two flags.
|
// else, and the extended draw row keeps its two flags.
|
||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
|
||||||
@@ -674,6 +689,10 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
|||||||
// padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second
|
// padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second
|
||||||
// struct to pin - the 16 bytes are pinned above with the handle family.
|
// struct to pin - the 16 bytes are pinned above with the handle family.
|
||||||
EXPECT_EQ(sizeof(MGPApplierReset), 8u);
|
EXPECT_EQ(sizeof(MGPApplierReset), 8u);
|
||||||
|
// P14 S1: bind_context's POD. ONE Uint64 - the same width the control frame's token is, so
|
||||||
|
// the two planes cannot disagree about how much of a token is real.
|
||||||
|
EXPECT_EQ(sizeof(MGPBindContext), 8u);
|
||||||
|
EXPECT_EQ(sizeof(MGPBindContext::ClientContextToken), 8u);
|
||||||
EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
|
EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
|
||||||
// rv's two (§5.3/§7.6): the residual-value POD - 2 + 15 Uint32s, 2 Uint8s and 2 pad bytes,
|
// rv's two (§5.3/§7.6): the residual-value POD - 2 + 15 Uint32s, 2 Uint8s and 2 pad bytes,
|
||||||
// then the three Uint64s - and the AMENDED attribute carrier, which grew 24 -> 56 to carry
|
// then the three Uint64s - and the AMENDED attribute carrier, which grew 24 -> 56 to carry
|
||||||
@@ -1036,8 +1055,11 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
|||||||
// (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange
|
// (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange
|
||||||
// and MGPVertexAttribWire are: the comparator has to see INTO an element whose members
|
// and MGPVertexAttribWire are: the comparator has to see INTO an element whose members
|
||||||
// include an MGHostSpan and a pad word, or it would memcmp the padding: 82. P9 appended the
|
// include an MGHostSpan and a pad word, or it would memcmp the padding: 82. P9 appended the
|
||||||
// pack-buffer readbacks' one payload, MGPReadbackToBuffer (CONTRACT-P9.md §1): 83.
|
// pack-buffer readbacks' one payload, MGPReadbackToBuffer (CONTRACT-P9.md §1): 83. P14 S1
|
||||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 83u);
|
// appended bind_context's MGPBindContext (docs/Disaggregated/design/11-state-ownership.md):
|
||||||
|
// 84. A payload without a list here is one the comparator cannot see, so the row is not
|
||||||
|
// optional bookkeeping - gen_pipe.py refuses the build without it.
|
||||||
|
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 84u);
|
||||||
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ namespace {
|
|||||||
MGPipeSetPoisonOmission(nullptr, nullptr);
|
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||||
MG_State::pGLContext = Move(m_previous);
|
MG_State::pGLContext = Move(m_previous);
|
||||||
}
|
}
|
||||||
UniquePtr<GLContext> m_previous;
|
SharedPtr<GLContext> m_previous;
|
||||||
};
|
};
|
||||||
|
|
||||||
#if MOBILEGL_PIPE_POISON
|
#if MOBILEGL_PIPE_POISON
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ namespace {
|
|||||||
// A live frontend context for the setters to write and the getters to answer from,
|
// A live frontend context for the setters to write and the getters to answer from,
|
||||||
// restored on the way out so the case stays independent (SanityTest's idiom).
|
// restored on the way out so the case stays independent (SanityTest's idiom).
|
||||||
struct ContextGuard {
|
struct ContextGuard {
|
||||||
UniquePtr<GLContext> Previous;
|
SharedPtr<GLContext> Previous;
|
||||||
ContextGuard() : Previous(Move(MG_State::pGLContext)) {
|
ContextGuard() : Previous(Move(MG_State::pGLContext)) {
|
||||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||||
MGPipeApplierReset();
|
MGPipeApplierReset();
|
||||||
@@ -786,7 +786,7 @@ namespace {
|
|||||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||||
#else
|
#else
|
||||||
struct ContextGuard {
|
struct ContextGuard {
|
||||||
UniquePtr<GLContext> Previous;
|
SharedPtr<GLContext> Previous;
|
||||||
ContextGuard() : Previous(Move(MG_State::pGLContext)) {
|
ContextGuard() : Previous(Move(MG_State::pGLContext)) {
|
||||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||||
MGPipeApplierReset();
|
MGPipeApplierReset();
|
||||||
@@ -950,7 +950,7 @@ namespace {
|
|||||||
// A live frontend context and a clean applier, restored on the way out (SanityTest's
|
// A live frontend context and a clean applier, restored on the way out (SanityTest's
|
||||||
// idiom, and the same guard cases 3 and 5 declare inline).
|
// idiom, and the same guard cases 3 and 5 declare inline).
|
||||||
struct ApplierContextGuard {
|
struct ApplierContextGuard {
|
||||||
UniquePtr<GLContext> Previous;
|
SharedPtr<GLContext> Previous;
|
||||||
ApplierContextGuard() : Previous(Move(MG_State::pGLContext)) {
|
ApplierContextGuard() : Previous(Move(MG_State::pGLContext)) {
|
||||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||||
MGPipeApplierReset();
|
MGPipeApplierReset();
|
||||||
|
|||||||
@@ -1946,7 +1946,7 @@ namespace {
|
|||||||
|
|
||||||
MGPipeResourceOps m_ops{};
|
MGPipeResourceOps m_ops{};
|
||||||
Uint64 m_previousPush = 0;
|
Uint64 m_previousPush = 0;
|
||||||
UniquePtr<GLContext> m_previousContext;
|
SharedPtr<GLContext> m_previousContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ namespace {
|
|||||||
TextureScope(const TextureScope&) = delete;
|
TextureScope(const TextureScope&) = delete;
|
||||||
TextureScope& operator=(const TextureScope&) = delete;
|
TextureScope& operator=(const TextureScope&) = delete;
|
||||||
|
|
||||||
UniquePtr<GLContext> m_previousContext;
|
SharedPtr<GLContext> m_previousContext;
|
||||||
Uint64 m_previousPush = 0;
|
Uint64 m_previousPush = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UniquePtr<GLContext> m_previous;
|
SharedPtr<GLContext> m_previous;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) {
|
TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) {
|
||||||
@@ -238,7 +238,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) {
|
TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) {
|
||||||
UniquePtr<GLContext> held = Move(MG_State::pGLContext);
|
SharedPtr<GLContext> held = Move(MG_State::pGLContext);
|
||||||
MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context
|
MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context
|
||||||
MG_State::pGLContext = Move(held);
|
MG_State::pGLContext = Move(held);
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
@@ -353,7 +353,7 @@ namespace {
|
|||||||
Uint64 m_payloadBytes = 0;
|
Uint64 m_payloadBytes = 0;
|
||||||
Uint64 m_binds = 0;
|
Uint64 m_binds = 0;
|
||||||
Uint64 m_savedPush = 0;
|
Uint64 m_savedPush = 0;
|
||||||
UniquePtr<GLContext> m_previous;
|
SharedPtr<GLContext> m_previous;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(TrackerWalk, EveryBitHasAName) {
|
TEST_F(TrackerWalk, EveryBitHasAName) {
|
||||||
@@ -988,7 +988,7 @@ namespace {
|
|||||||
return bits;
|
return bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
UniquePtr<GLContext> m_previous;
|
SharedPtr<GLContext> m_previous;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) {
|
TEST_F(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) {
|
||||||
@@ -1075,7 +1075,7 @@ namespace {
|
|||||||
static const MGPipeCsoCache::Counters& Cso() { return MGPipeCsoCacheInstance().GetCounters(); }
|
static const MGPipeCsoCache::Counters& Cso() { return MGPipeCsoCacheInstance().GetCounters(); }
|
||||||
|
|
||||||
Uint64 m_savedPush = 0;
|
Uint64 m_savedPush = 0;
|
||||||
UniquePtr<GLContext> m_previous;
|
SharedPtr<GLContext> m_previous;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(TrackerShippedEmitter, CollidingVaoShuttersStillPublishTheCurrentElementsAndBufferWindow) {
|
TEST_F(TrackerShippedEmitter, CollidingVaoShuttersStillPublishTheCurrentElementsAndBufferWindow) {
|
||||||
@@ -1362,7 +1362,7 @@ namespace {
|
|||||||
// stand into the next verb. A draw with no context is a no-op; its argument must not
|
// stand into the next verb. A draw with no context is a no-op; its argument must not
|
||||||
// outlive it.
|
// outlive it.
|
||||||
MGPipeSetPendingBaseInstance(11);
|
MGPipeSetPendingBaseInstance(11);
|
||||||
UniquePtr<GLContext> parked = Move(MG_State::pGLContext);
|
SharedPtr<GLContext> parked = Move(MG_State::pGLContext);
|
||||||
Draw();
|
Draw();
|
||||||
EXPECT_EQ(MGPipePendingBaseInstance(), 0u)
|
EXPECT_EQ(MGPipePendingBaseInstance(), 0u)
|
||||||
<< "the no-live-context exit left the draw's base instance standing for the next verb";
|
<< "the no-live-context exit left the draw's base instance standing for the next verb";
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ namespace {
|
|||||||
EmitterScope(const EmitterScope&) = delete;
|
EmitterScope(const EmitterScope&) = delete;
|
||||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||||
|
|
||||||
UniquePtr<GLContext> m_previousContext;
|
SharedPtr<GLContext> m_previousContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ namespace {
|
|||||||
MobileGL::Vector<TextureBindCall> bindCalls;
|
MobileGL::Vector<TextureBindCall> bindCalls;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MobileGL::UniquePtr<MobileGL::MG_State::GLState::GLContext> previousContext;
|
MobileGL::SharedPtr<MobileGL::MG_State::GLState::GLContext> previousContext;
|
||||||
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
||||||
MobileGL::Uint previousActiveUnit;
|
MobileGL::Uint previousActiveUnit;
|
||||||
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache;
|
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache;
|
||||||
@@ -5320,7 +5320,14 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT
|
|||||||
// THE PUBLISHED STATE: attribute 0 fetches from A, attribute 1 from B, attribute 2 from A
|
// THE PUBLISHED STATE: attribute 0 fetches from A, attribute 1 from B, attribute 2 from A
|
||||||
// again (the dedupe), attribute 3 is a client-memory array (null Res, nothing to ensure).
|
// again (the dedupe), attribute 3 is a client-memory array (null Res, nothing to ensure).
|
||||||
MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
|
MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
|
||||||
const MG_Pipe::MGPipeApplierState saved = st;
|
// P14 S5: the applier is not copyable any more (its object tables are references into the
|
||||||
|
// share group it belongs to), so the case saves the WORKING fields it is about to move
|
||||||
|
// instead of the whole object - which is also the smaller blast radius: nothing here
|
||||||
|
// touches a record.
|
||||||
|
const Uint32 savedVertexBufferStart = st.VertexBufferStart;
|
||||||
|
const Uint32 savedVertexBufferCount = st.VertexBufferCount;
|
||||||
|
const Array<MG_Pipe::MGPVertexBuffer, VertexArrayObject::MAX_VERTEX_ATTRIBS> savedVertexBuffers =
|
||||||
|
st.VertexBuffers;
|
||||||
st.VertexBufferStart = 0;
|
st.VertexBufferStart = 0;
|
||||||
st.VertexBufferCount = 4;
|
st.VertexBufferCount = 4;
|
||||||
for (Uint32 i = 0; i < 4; ++i) {
|
for (Uint32 i = 0; i < 4; ++i) {
|
||||||
@@ -5338,7 +5345,7 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT
|
|||||||
// this case exists to catch - MGB_CTX->GetBoundVertexArray()->GetAllAttributes() - has
|
// this case exists to catch - MGB_CTX->GetBoundVertexArray()->GetAllAttributes() - has
|
||||||
// anything to read at all; a free-standing object would make the revert crash instead of
|
// anything to read at all; a free-standing object would make the revert crash instead of
|
||||||
// disagree, which is a red for the wrong reason.
|
// disagree, which is a red for the wrong reason.
|
||||||
UniquePtr<GLContext> previousContext = Move(MG_State::pGLContext);
|
SharedPtr<GLContext> previousContext = Move(MG_State::pGLContext);
|
||||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||||
MG_State::pGLContext->CreateVertexArrayObject(1);
|
MG_State::pGLContext->CreateVertexArrayObject(1);
|
||||||
MG_State::pGLContext->BindVertexArray(1);
|
MG_State::pGLContext->BindVertexArray(1);
|
||||||
@@ -5369,7 +5376,9 @@ TEST(DirectGLESVertexInputDraw, TheAttributeWalkTakesItsBuffersFromTheRecordNotT
|
|||||||
"run-ahead client has already moved on from";
|
"run-ahead client has already moved on from";
|
||||||
}
|
}
|
||||||
|
|
||||||
st = saved;
|
st.VertexBufferStart = savedVertexBufferStart;
|
||||||
|
st.VertexBufferCount = savedVertexBufferCount;
|
||||||
|
st.VertexBuffers = savedVertexBuffers;
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pGLContext = Move(previousContext);
|
MG_State::pGLContext = Move(previousContext);
|
||||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, a);
|
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, a);
|
||||||
@@ -5399,14 +5408,15 @@ TEST(DirectGLESVertexInputDraw, TheIndexArmTakesItsBufferAndItsSerialFromTheReco
|
|||||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(publishedHandle));
|
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(publishedHandle));
|
||||||
|
|
||||||
MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
|
MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
|
||||||
const MG_Pipe::MGPipeApplierState saved = st;
|
const MG_Pipe::MGPIndexBuffer savedIndexBuffer = st.IndexBuffer;
|
||||||
|
const Uint64 savedIndexBufferSerial = st.IndexBufferSerial;
|
||||||
st.IndexBuffer = MG_Pipe::MGPIndexBuffer{};
|
st.IndexBuffer = MG_Pipe::MGPIndexBuffer{};
|
||||||
st.IndexBuffer.Res = publishedHandle;
|
st.IndexBuffer.Res = publishedHandle;
|
||||||
st.IndexBufferSerial = 11;
|
st.IndexBufferSerial = 11;
|
||||||
|
|
||||||
// The frontend's element slot has moved to another buffer with nothing emitted, in a real
|
// The frontend's element slot has moved to another buffer with nothing emitted, in a real
|
||||||
// context for the reason the case above gives.
|
// context for the reason the case above gives.
|
||||||
UniquePtr<GLContext> previousContext = Move(MG_State::pGLContext);
|
SharedPtr<GLContext> previousContext = Move(MG_State::pGLContext);
|
||||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||||
MG_State::pGLContext->CreateVertexArrayObject(1);
|
MG_State::pGLContext->CreateVertexArrayObject(1);
|
||||||
MG_State::pGLContext->BindVertexArray(1);
|
MG_State::pGLContext->BindVertexArray(1);
|
||||||
@@ -5428,7 +5438,8 @@ TEST(DirectGLESVertexInputDraw, TheIndexArmTakesItsBufferAndItsSerialFromTheReco
|
|||||||
<< "a re-emitted set_index_buffer on the SAME handle is invisible to this arm without "
|
<< "a re-emitted set_index_buffer on the SAME handle is invisible to this arm without "
|
||||||
"the serial, and the memo would read clean over it";
|
"the serial, and the memo would read clean over it";
|
||||||
|
|
||||||
st = saved;
|
st.IndexBuffer = savedIndexBuffer;
|
||||||
|
st.IndexBufferSerial = savedIndexBufferSerial;
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pGLContext = Move(previousContext);
|
MG_State::pGLContext = Move(previousContext);
|
||||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, publishedHandle);
|
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, publishedHandle);
|
||||||
|
|||||||
@@ -144,3 +144,22 @@ if (MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
gtest_discover_tests(MipmapFootprintTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(MipmapFootprintTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
ContextStateOwnershipTest
|
||||||
|
ContextStateOwnershipTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ContextStateOwnershipTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
ContextStateOwnershipTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(ContextStateOwnershipTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/State/ContextStateOwnershipTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P14 S3 (docs/Disaggregated/design/11-state-ownership.md section 4). The frontend's GL state
|
||||||
|
// belongs to an EGL context, its objects and name spaces to a share group:
|
||||||
|
// * two contexts that do not share have their own name spaces - the same numeric name is a
|
||||||
|
// different object in each;
|
||||||
|
// * two contexts that share see one object and keep their own bindings;
|
||||||
|
// * an eglMakeCurrent switch moves errors, viewport and framebuffer bindings with it;
|
||||||
|
// * eglDestroyContext drops one context's GL state and leaves the other's alone.
|
||||||
|
//
|
||||||
|
// GPU-free: every assertion is frontend state, reached through the real EGL and GL entry points.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <MG_State/EGLState/Core.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||||
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Impl::GLImpl;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using EGLStateContext = MG_State::EGLState::EGLContext;
|
||||||
|
using GLStateContext = MG_State::GLState::GLContext;
|
||||||
|
|
||||||
|
class ContextStateOwnershipTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
State = MG_State::pEGLContext.get();
|
||||||
|
ASSERT_NE(State, nullptr);
|
||||||
|
|
||||||
|
Display = State->GetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
ASSERT_NE(Display, EGL_NO_DISPLAY);
|
||||||
|
ASSERT_TRUE(State->InitializeDisplay(Display, nullptr, nullptr));
|
||||||
|
|
||||||
|
EGLint configCount = 0;
|
||||||
|
ASSERT_TRUE(State->ChooseConfig(Display, nullptr, &Config, 1, &configCount));
|
||||||
|
ASSERT_NE(Config, nullptr);
|
||||||
|
|
||||||
|
const EGLint surfaceAttribs[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE};
|
||||||
|
Surface = State->CreatePbufferSurface(Display, Config, surfaceAttribs);
|
||||||
|
ASSERT_NE(Surface, EGL_NO_SURFACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
Release();
|
||||||
|
for (const auto context : m_contexts) {
|
||||||
|
if (State->ValidateContext(context)) State->DestroyContext(Display, context);
|
||||||
|
}
|
||||||
|
if (State->ValidateSurface(Surface)) State->DestroySurface(Display, Surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLContext NewContext(EGLContext shareWith) {
|
||||||
|
const auto context = State->CreateContext(Display, Config, shareWith, nullptr);
|
||||||
|
EXPECT_NE(context, EGL_NO_CONTEXT);
|
||||||
|
m_contexts.push_back(context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MakeCurrent(EGLContext context) {
|
||||||
|
ASSERT_TRUE(State->MakeCurrent(Display, Surface, Surface, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release() {
|
||||||
|
if (State) State->MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLStateContext* State = nullptr;
|
||||||
|
EGLDisplay Display = EGL_NO_DISPLAY;
|
||||||
|
EGLConfig Config = nullptr;
|
||||||
|
EGLSurface Surface = EGL_NO_SURFACE;
|
||||||
|
Vector<EGLContext> m_contexts;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(ContextStateOwnershipTest, NonsharedContextsKeepTheirOwnNameSpaces) {
|
||||||
|
const auto contextA = NewContext(EGL_NO_CONTEXT);
|
||||||
|
const auto contextB = NewContext(EGL_NO_CONTEXT);
|
||||||
|
EXPECT_NE(State->GetContextShareGroupToken(contextA), State->GetContextShareGroupToken(contextB));
|
||||||
|
EXPECT_NE(State->GetContextGLState(contextA), State->GetContextGLState(contextB));
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
GLuint namesA[2] = {};
|
||||||
|
GenBuffers(2, namesA);
|
||||||
|
ASSERT_NE(namesA[0], 0u);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, namesA[0]);
|
||||||
|
const auto objectA = MG_State::pGLContext->GetBufferObject(namesA[0]);
|
||||||
|
ASSERT_NE(objectA, nullptr);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), objectA);
|
||||||
|
|
||||||
|
MakeCurrent(contextB);
|
||||||
|
// Neither name A handed out is a name here, and A's binding is not this thread's.
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(namesA[0]));
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(namesA[1]));
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), nullptr);
|
||||||
|
|
||||||
|
GLuint nameB = 0;
|
||||||
|
GenBuffers(1, &nameB);
|
||||||
|
// The generator is per share group, so the same numeric name comes out - as a DIFFERENT
|
||||||
|
// object, because the table it names is a different one.
|
||||||
|
EXPECT_EQ(nameB, namesA[0]);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, nameB);
|
||||||
|
const auto objectB = MG_State::pGLContext->GetBufferObject(nameB);
|
||||||
|
ASSERT_NE(objectB, nullptr);
|
||||||
|
EXPECT_NE(objectB, objectA);
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(namesA[0]), objectA);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), objectA);
|
||||||
|
EXPECT_TRUE(MG_State::pGLContext->ValidateBufferName(namesA[1]));
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ContextStateOwnershipTest, SharingContextsSeeOneObjectButKeepTheirOwnBindings) {
|
||||||
|
const auto contextA = NewContext(EGL_NO_CONTEXT);
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
GLuint buffer = 0;
|
||||||
|
GenBuffers(1, &buffer);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||||
|
const auto object = MG_State::pGLContext->GetBufferObject(buffer);
|
||||||
|
ASSERT_NE(object, nullptr);
|
||||||
|
|
||||||
|
const auto contextB = NewContext(contextA);
|
||||||
|
EXPECT_EQ(State->GetContextShareGroupToken(contextB), State->GetContextShareGroupToken(contextA));
|
||||||
|
EXPECT_NE(State->GetContextClientToken(contextB), State->GetContextClientToken(contextA));
|
||||||
|
|
||||||
|
MakeCurrent(contextB);
|
||||||
|
EXPECT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer));
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(buffer), object);
|
||||||
|
// The OBJECT is shared; the binding is not.
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), nullptr);
|
||||||
|
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
// A bound it before B did, and B's bind did not move A's slot.
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), object);
|
||||||
|
|
||||||
|
// One name space for the pair: a name B generates is a name A knows.
|
||||||
|
MakeCurrent(contextB);
|
||||||
|
GLuint fromB = 0;
|
||||||
|
GenBuffers(1, &fromB);
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
EXPECT_TRUE(MG_State::pGLContext->ValidateBufferName(fromB));
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ContextStateOwnershipTest, MakeCurrentMovesErrorsViewportAndFramebufferBindings) {
|
||||||
|
const auto contextA = NewContext(EGL_NO_CONTEXT);
|
||||||
|
const auto contextB = NewContext(EGL_NO_CONTEXT);
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
Viewport(1, 2, 3, 4);
|
||||||
|
GLuint framebufferA = 0;
|
||||||
|
GenFramebuffers(1, &framebufferA);
|
||||||
|
ASSERT_NE(framebufferA, 0u);
|
||||||
|
BindFramebuffer(GL_FRAMEBUFFER, framebufferA);
|
||||||
|
const auto boundInA = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
|
ASSERT_NE(boundInA, nullptr);
|
||||||
|
EXPECT_EQ(boundInA->GetExternalIndex(), framebufferA);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
// A rejected call leaves A's viewport where it was and leaves A an error to report; the
|
||||||
|
// error is NOT consumed here, so that the switch below can show whose list it is in.
|
||||||
|
Viewport(0, 0, -1, -1);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetViewport(), IntVec4(1, 2, 3, 4));
|
||||||
|
|
||||||
|
MakeCurrent(contextB);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
EXPECT_NE(MG_State::pGLContext->GetViewport(), IntVec4(1, 2, 3, 4));
|
||||||
|
const auto boundInB = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
|
EXPECT_NE(boundInB == nullptr ? 0u : boundInB->GetExternalIndex(), framebufferA);
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetViewport(), IntVec4(1, 2, 3, 4));
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), boundInA);
|
||||||
|
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ContextStateOwnershipTest, DestroyingOneContextLeavesTheOtherAlone) {
|
||||||
|
const auto contextA = NewContext(EGL_NO_CONTEXT);
|
||||||
|
const auto contextB = NewContext(contextA);
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
GLuint buffer = 0;
|
||||||
|
GenBuffers(1, &buffer);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||||
|
const auto object = MG_State::pGLContext->GetBufferObject(buffer);
|
||||||
|
ASSERT_NE(object, nullptr);
|
||||||
|
|
||||||
|
MakeCurrent(contextB);
|
||||||
|
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||||
|
|
||||||
|
// A is current on no thread, so EGL accepts the destroy.
|
||||||
|
ASSERT_TRUE(State->DestroyContext(Display, contextA));
|
||||||
|
EXPECT_FALSE(State->ValidateContext(contextA));
|
||||||
|
EXPECT_EQ(State->GetContextGLState(contextA), nullptr);
|
||||||
|
|
||||||
|
// B keeps its object - the share group outlives the context that opened it - and its binding.
|
||||||
|
EXPECT_EQ(MG_State::pGLContext, State->GetContextGLState(contextB));
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferObject(buffer), object);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Vertex).GetBoundObject(), object);
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ContextStateOwnershipTest, ReleasingAnEglContextRestoresTheThreadsPreviousGLState) {
|
||||||
|
const auto contextA = NewContext(EGL_NO_CONTEXT);
|
||||||
|
const auto before = MG_State::pGLContext;
|
||||||
|
ASSERT_NE(before, nullptr);
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext, State->GetContextGLState(contextA));
|
||||||
|
EXPECT_NE(MG_State::pGLContext, before);
|
||||||
|
|
||||||
|
Release();
|
||||||
|
EXPECT_EQ(MG_State::pGLContext, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ContextStateOwnershipTest, AThreadWithoutACurrentContextUsesTheProcessDefaultContext) {
|
||||||
|
const auto processDefault = MG_State::pGLContext;
|
||||||
|
ASSERT_NE(processDefault, nullptr);
|
||||||
|
EXPECT_EQ(processDefault, MG_State::ProcessDefaultGLContext());
|
||||||
|
|
||||||
|
const auto contextA = NewContext(EGL_NO_CONTEXT);
|
||||||
|
EXPECT_NE(State->GetContextGLState(contextA), processDefault);
|
||||||
|
|
||||||
|
MakeCurrent(contextA);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext, State->GetContextGLState(contextA));
|
||||||
|
Release();
|
||||||
|
EXPECT_EQ(MG_State::pGLContext, processDefault);
|
||||||
|
EXPECT_NE(processDefault->GetShareGroup(), State->GetContextGLState(contextA)->GetShareGroup());
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Wire/ApplierOwnershipTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT?
|
||||||
|
//
|
||||||
|
// WHAT THIS SUITE OWNS. Until this slice the applier was one process-wide block
|
||||||
|
// (PipeApply.cpp's `g_applier`), and the block mixed two lifetimes: the OBJECT RECORDS that
|
||||||
|
// describe GL objects and live in a SHARE GROUP, and the WORKING STATE (bindings, the current
|
||||||
|
// program, the unit windows, the CSO store) that belongs to one CONTEXT. One block for the whole
|
||||||
|
// process made a second served context either inherit bindings it must not inherit or lose the
|
||||||
|
// object records of a share group it is a member of.
|
||||||
|
//
|
||||||
|
// This suite drives the registry and the resolution directly, with no EGL and no wire, because
|
||||||
|
// that is exactly what the slice is: `MGPipeApplier()` answers per {session, context}, its object
|
||||||
|
// records come from the {session, share group} the context belongs to, and every case below is a
|
||||||
|
// way of using two contexts that the single block got wrong.
|
||||||
|
//
|
||||||
|
// HOW IT INSTALLS A KEY. The production resolver is the server's (SessionRuntime.cpp's
|
||||||
|
// ThreadSessionScope installs it and answers with the served session's current context token).
|
||||||
|
// Here the resolver is this file's, driven by two thread_locals, so a case can be in whichever
|
||||||
|
// session and context it means without standing up a transport - and MGPipeApplierRegisterContext
|
||||||
|
// is the same entry point ServerSession::CreateContext calls. The keys the cases use are large
|
||||||
|
// sentinels that no real session address can collide with, and every case releases its sessions on
|
||||||
|
// the way out.
|
||||||
|
//
|
||||||
|
// THE OBSERVABLE IS THE APPLIER'S OWN STATE. `MGPipeApplier()` is the function the backend reads,
|
||||||
|
// so a case asserts on what the backend would see: which record a handle resolves to, whether the
|
||||||
|
// record is visible from the other context, and whether the two contexts' working states are the
|
||||||
|
// same object. Turning the keying off (making MGPipeApplier ignore the resolver) turns the
|
||||||
|
// nonshared and the two-session cases red; keying the object records by CONTEXT instead of by
|
||||||
|
// share group turns the shared case red - both directions are covered.
|
||||||
|
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace P = MobileGL::MG_Pipe;
|
||||||
|
using MobileGL::Bool;
|
||||||
|
using MobileGL::Uint32;
|
||||||
|
using MobileGL::Uint64;
|
||||||
|
|
||||||
|
// The calling thread's {session, context}, exactly the shape the production resolver fills in.
|
||||||
|
// A zero session key is "not a session's thread", which is what every other caller in the tree
|
||||||
|
// is and what keeps the process-wide applier the answer there.
|
||||||
|
thread_local Uint64 t_sessionKey = 0;
|
||||||
|
thread_local Uint64 t_contextToken = 0;
|
||||||
|
|
||||||
|
Bool TestApplierKeyResolver(P::MGPipeApplierKey* outKey) {
|
||||||
|
if (t_sessionKey == 0) return false;
|
||||||
|
outKey->SessionKey = t_sessionKey;
|
||||||
|
outKey->ContextToken = t_contextToken;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApplierOwnershipTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
// Sentinels no real session's address can be, so a stray registration elsewhere in the
|
||||||
|
// process can never be what a case reads.
|
||||||
|
static constexpr Uint64 kSessionA = 0x5E5510A000000000ull;
|
||||||
|
static constexpr Uint64 kSessionB = 0x5E5510B000000000ull;
|
||||||
|
|
||||||
|
// The share-group tokens the frontend mints (EGLState::CreateContext): dense and 0-free,
|
||||||
|
// which is why two contexts that share pass the SAME number and two that do not pass
|
||||||
|
// different ones.
|
||||||
|
static constexpr Uint64 kGroupOne = 0x1001;
|
||||||
|
static constexpr Uint64 kGroupTwo = 0x1002;
|
||||||
|
|
||||||
|
void SetUp() override {
|
||||||
|
P::MGPipeSetApplierKeyResolver(&TestApplierKeyResolver);
|
||||||
|
BindSession(kSessionA, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
P::MGPipeApplierReleaseSession(kSessionA);
|
||||||
|
P::MGPipeApplierReleaseSession(kSessionB);
|
||||||
|
t_sessionKey = 0;
|
||||||
|
t_contextToken = 0;
|
||||||
|
P::MGPipeSetApplierKeyResolver(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BindSession(Uint64 session, Uint64 context) {
|
||||||
|
t_sessionKey = session;
|
||||||
|
t_contextToken = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The buffer descriptor ServerLoopTest's DeclareBuffer builds, trimmed to the fields a
|
||||||
|
// buffer record needs. `width` is what tells two records of the SAME handle apart.
|
||||||
|
static P::MGPResourceDesc BufferDesc(Uint32 slot, Uint32 width) {
|
||||||
|
P::MGPResourceDesc desc{};
|
||||||
|
desc.Resource = P::MGPipeHandle{slot, 1u};
|
||||||
|
desc.Target = P::kMGPipeResourceTargetBuffer;
|
||||||
|
desc.Width = width;
|
||||||
|
desc.Height = 1;
|
||||||
|
desc.Depth = 1;
|
||||||
|
desc.ArrayLayers = 1;
|
||||||
|
desc.Levels = 1;
|
||||||
|
desc.Samples = 1;
|
||||||
|
desc.HasDefinedContent = 1;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resource_create writes the record into the OBJECT RECORDS of the applier the calling
|
||||||
|
// thread is in - no backend, no EGL: with no resource op table registered the dispatch to
|
||||||
|
// the backend is skipped and the record is the whole of the call's effect.
|
||||||
|
static void DeclareBuffer(P::MGPipeHandle handle, Uint32 width) {
|
||||||
|
ASSERT_TRUE(P::MGPipeApplyResourceCreate(BufferDesc(handle.Slot, width)))
|
||||||
|
<< "resource_create refused slot " << handle.Slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const P::MGPipeResourceRecord* ResourceRecord(P::MGPipeHandle handle) {
|
||||||
|
const P::MGPipeApplierState& state = P::MGPipeApplier();
|
||||||
|
if (handle.Slot >= state.Resources.size()) return nullptr;
|
||||||
|
const P::MGPipeResourceRecord& record = state.Resources[handle.Slot];
|
||||||
|
if (!record.Live || record.Gen != handle.Gen) return nullptr;
|
||||||
|
return &record;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// THE DEFECT THIS SLICE EXISTS FOR, in its first direction. Two contexts of one session that
|
||||||
|
// do NOT share: the frontend gives them independent name spaces, so the same handle value in
|
||||||
|
// both is legal and means two different objects. One process-wide applier answered both
|
||||||
|
// handles with one record, and the second create overwrote the first's descriptor - the
|
||||||
|
// "same handle in two contexts" collision the whole keying exists to stop.
|
||||||
|
TEST_F(ApplierOwnershipTest, TheSameHandleInTwoNonsharedContextsIsTwoRecords) {
|
||||||
|
const P::MGPipeHandle handle{7u, 1u};
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 1, kGroupOne);
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 2, kGroupTwo);
|
||||||
|
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 2u)
|
||||||
|
<< "two contexts that name different share groups ended up in one group";
|
||||||
|
|
||||||
|
BindSession(kSessionA, 1);
|
||||||
|
const P::MGPipeApplierState* first = &P::MGPipeApplier();
|
||||||
|
ASSERT_EQ(ResourceRecord(handle), nullptr) << "context 1 began with a record it never created";
|
||||||
|
DeclareBuffer(handle, 64u);
|
||||||
|
const P::MGPipeResourceRecord* firstRecord = ResourceRecord(handle);
|
||||||
|
ASSERT_NE(firstRecord, nullptr) << "context 1's own create did not produce a record";
|
||||||
|
EXPECT_EQ(firstRecord->Desc.Width, 64u);
|
||||||
|
|
||||||
|
BindSession(kSessionA, 2);
|
||||||
|
const P::MGPipeApplierState* second = &P::MGPipeApplier();
|
||||||
|
// Two contexts, two appliers. One shared applier would make the two pointers equal and
|
||||||
|
// every assertion below vacuous.
|
||||||
|
EXPECT_NE(first, second) << "the two contexts of one session share one applier";
|
||||||
|
EXPECT_EQ(ResourceRecord(handle), nullptr)
|
||||||
|
<< "context 2 inherited a record created in a context it does not share with";
|
||||||
|
|
||||||
|
DeclareBuffer(handle, 1024u);
|
||||||
|
const P::MGPipeResourceRecord* secondRecord = ResourceRecord(handle);
|
||||||
|
ASSERT_NE(secondRecord, nullptr);
|
||||||
|
EXPECT_EQ(secondRecord->Desc.Width, 1024u);
|
||||||
|
|
||||||
|
// AND THE FIRST CONTEXT'S OBJECT IS UNTOUCHED. This is the assertion the old shape fails:
|
||||||
|
// one slot, one record, so the second create rewrote the first object's descriptor.
|
||||||
|
BindSession(kSessionA, 1);
|
||||||
|
EXPECT_EQ(&P::MGPipeApplier(), first) << "returning to a context minted a second applier for it";
|
||||||
|
const P::MGPipeResourceRecord* firstAgain = ResourceRecord(handle);
|
||||||
|
ASSERT_NE(firstAgain, nullptr) << "context 1's record went missing while context 2 was current";
|
||||||
|
EXPECT_EQ(firstAgain->Desc.Width, 64u)
|
||||||
|
<< "context 2's create on the same handle value rewrote context 1's object";
|
||||||
|
|
||||||
|
// The reset a make-current performs clears the WORKING state and leaves the group's
|
||||||
|
// records standing, which is the other half of the old defect: a shared object's record
|
||||||
|
// must survive a switch away and back.
|
||||||
|
P::MGPipeApplierReset();
|
||||||
|
EXPECT_EQ(ResourceRecord(handle), firstAgain) << "a make-current dropped the object record";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE SECOND DIRECTION, and the one the S4 report named as the reason a literal "key the
|
||||||
|
// whole applier by share group" would be worse than no change at all: two contexts that DO
|
||||||
|
// share see one set of OBJECT RECORDS and may not see one set of bindings, a current program
|
||||||
|
// or a CSO store. So the records must be one object and the appliers must be two.
|
||||||
|
TEST_F(ApplierOwnershipTest, TwoContextsOfOneShareGroupShareRecordsAndNotWorkingState) {
|
||||||
|
const P::MGPipeHandle shared{9u, 1u};
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 3, kGroupOne);
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 4, kGroupOne);
|
||||||
|
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 1u)
|
||||||
|
<< "two contexts that name the same share group were given two of them";
|
||||||
|
|
||||||
|
BindSession(kSessionA, 3);
|
||||||
|
P::MGPipeApplierState& third = P::MGPipeApplier();
|
||||||
|
DeclareBuffer(shared, 64u);
|
||||||
|
const P::MGPipeResourceRecord* record = ResourceRecord(shared);
|
||||||
|
ASSERT_NE(record, nullptr);
|
||||||
|
// Working state, written by hand the way an applied record would: a bound program and a
|
||||||
|
// touched vertex-buffer window.
|
||||||
|
third.DrawProgram = P::MGPipeHandle{11u, 1u};
|
||||||
|
third.VertexBufferCount = 3;
|
||||||
|
|
||||||
|
BindSession(kSessionA, 4);
|
||||||
|
P::MGPipeApplierState& fourth = P::MGPipeApplier();
|
||||||
|
EXPECT_NE(&third, &fourth) << "two contexts of one share group share one applier - the "
|
||||||
|
"bindings, the current program and the CSO store would all "
|
||||||
|
"overwrite each other";
|
||||||
|
EXPECT_EQ(&third.Resources, &fourth.Resources)
|
||||||
|
<< "two contexts of one share group were given different object tables";
|
||||||
|
EXPECT_NE(ResourceRecord(shared), nullptr)
|
||||||
|
<< "the sharing context cannot see the object its group already has - driver-level "
|
||||||
|
"sharing is impossible without this";
|
||||||
|
|
||||||
|
EXPECT_TRUE(P::MGPipeHandleIsNull(fourth.DrawProgram))
|
||||||
|
<< "the sharing context inherited the other context's current program";
|
||||||
|
EXPECT_EQ(fourth.VertexBufferCount, 0u) << "the sharing context inherited the other context's "
|
||||||
|
"touched vertex-buffer window";
|
||||||
|
EXPECT_FALSE(P::MGPipeHandleIsNull(third.DrawProgram)) << "the first context's working state "
|
||||||
|
"was cleared by the second's";
|
||||||
|
|
||||||
|
// AND A RECORD CREATED THROUGH THE SHARING CONTEXT IS THE SAME OBJECT. One table, so the
|
||||||
|
// create is visible to both - which is what "a buffer made in one context is usable in
|
||||||
|
// its group's other context" means at this layer.
|
||||||
|
DeclareBuffer(shared, 4096u);
|
||||||
|
const P::MGPipeResourceRecord* respecified = ResourceRecord(shared);
|
||||||
|
ASSERT_NE(respecified, nullptr);
|
||||||
|
EXPECT_EQ(respecified->Desc.Width, 4096u)
|
||||||
|
<< "a respecify through the sharing context did not reach the group's record";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SHARE GROUP DOES NOT CROSS A SESSION. Two sessions that name the same group token and the
|
||||||
|
// same context token are two connections (an EGL share group is a property of one client's
|
||||||
|
// display), so they must not resolve one applier or one object table.
|
||||||
|
TEST_F(ApplierOwnershipTest, TwoSessionsThatNameTheSameGroupDoNotShareAnything) {
|
||||||
|
const P::MGPipeHandle handle{5u, 1u};
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 1, kGroupOne);
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionB, 1, kGroupOne);
|
||||||
|
|
||||||
|
BindSession(kSessionA, 1);
|
||||||
|
P::MGPipeApplierState& inA = P::MGPipeApplier();
|
||||||
|
DeclareBuffer(handle, 32u);
|
||||||
|
|
||||||
|
BindSession(kSessionB, 1);
|
||||||
|
P::MGPipeApplierState& inB = P::MGPipeApplier();
|
||||||
|
EXPECT_NE(&inA, &inB) << "two sessions with identical tokens resolved one applier";
|
||||||
|
EXPECT_NE(&inA.Resources, &inB.Resources);
|
||||||
|
EXPECT_EQ(ResourceRecord(handle), nullptr)
|
||||||
|
<< "session B resolved an object record session A created";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE GROUP'S RECORDS GO WITH ITS LAST CONTEXT, NOT ITS FIRST. A context of a live group is
|
||||||
|
// still using them; the group's teardown is the per-group spelling of
|
||||||
|
// MGPipeApplierReleaseObjectRecords, which the one-applier shape could only apply to the whole
|
||||||
|
// process.
|
||||||
|
TEST_F(ApplierOwnershipTest, AGroupKeepsItsRecordsUntilItsLastContextGoes) {
|
||||||
|
const P::MGPipeHandle handle{13u, 1u};
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 5, kGroupOne);
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 6, kGroupOne);
|
||||||
|
|
||||||
|
BindSession(kSessionA, 5);
|
||||||
|
DeclareBuffer(handle, 128u);
|
||||||
|
ASSERT_NE(ResourceRecord(handle), nullptr);
|
||||||
|
|
||||||
|
P::MGPipeApplierUnregisterContext(kSessionA, 5);
|
||||||
|
BindSession(kSessionA, 6);
|
||||||
|
EXPECT_NE(ResourceRecord(handle), nullptr)
|
||||||
|
<< "destroying one context dropped the share group its sibling is still using";
|
||||||
|
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 1u);
|
||||||
|
|
||||||
|
P::MGPipeApplierUnregisterContext(kSessionA, 6);
|
||||||
|
EXPECT_EQ(P::MGPipeApplierShareGroupCountForTesting(kSessionA), 0u)
|
||||||
|
<< "the group outlived its last context";
|
||||||
|
|
||||||
|
// A RECREATED CONTEXT MUST NOT INHERIT THE DEAD GROUP'S RECORDS. The frontend never
|
||||||
|
// re-uses a token, so this is the belt: a token that comes back resolves to a fresh
|
||||||
|
// working state in a fresh group, not to the wreckage of the old one.
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 5, kGroupOne);
|
||||||
|
BindSession(kSessionA, 5);
|
||||||
|
EXPECT_EQ(ResourceRecord(handle), nullptr)
|
||||||
|
<< "a context registered after its group was torn down resolved the dead group's record";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE PROCESS-WIDE APPLIER IS STILL THE ANSWER FOR EVERY THREAD THAT BELONGS TO NO SESSION,
|
||||||
|
// and a session's registration must not drag it into that world. This is the property the
|
||||||
|
// ~2000 unit cases that drive the applier directly depend on.
|
||||||
|
TEST_F(ApplierOwnershipTest, AThreadWithNoSessionStillGetsTheProcessWideApplier) {
|
||||||
|
const P::MGPipeHandle handle{17u, 1u};
|
||||||
|
P::MGPipeApplierRegisterContext(kSessionA, 1, kGroupOne);
|
||||||
|
BindSession(kSessionA, 1);
|
||||||
|
DeclareBuffer(handle, 64u);
|
||||||
|
|
||||||
|
t_sessionKey = 0;
|
||||||
|
t_contextToken = 0;
|
||||||
|
P::MGPipeApplierState& processWide = P::MGPipeApplier();
|
||||||
|
EXPECT_EQ(ResourceRecord(handle), nullptr)
|
||||||
|
<< "the process-wide applier resolved a record a session registered";
|
||||||
|
|
||||||
|
BindSession(kSessionA, 1);
|
||||||
|
EXPECT_NE(&P::MGPipeApplier(), &processWide) << "a session resolved the process-wide applier";
|
||||||
|
EXPECT_NE(ResourceRecord(handle), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -19,7 +19,11 @@ set(MOBILEGL_WIRE_TESTS
|
|||||||
ServerDisplayTest
|
ServerDisplayTest
|
||||||
# P11 B1: the apply thread's affinity / spin chosen per session from the peer's dial mode
|
# P11 B1: the apply thread's affinity / spin chosen per session from the peer's dial mode
|
||||||
# (Server/ApplyThreadPolicy.h, a pure function).
|
# (Server/ApplyThreadPolicy.h, a pure function).
|
||||||
ApplyThreadPolicyTest
|
# ApplyThreadPolicyTest
|
||||||
|
# P14 S5 (docs/Disaggregated/design/11-state-ownership.md): the applier's two halves and
|
||||||
|
# the key they resolve under - {session, context} for the working state, {session, share
|
||||||
|
# group} for the object records. Drives the registry directly: no EGL, no wire.
|
||||||
|
ApplierOwnershipTest
|
||||||
)
|
)
|
||||||
|
|
||||||
if (NOT WIN32)
|
if (NOT WIN32)
|
||||||
@@ -56,6 +60,12 @@ if (NOT WIN32)
|
|||||||
# MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers
|
# MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers
|
||||||
# and (the second, and D8's case) the real server image, as PeerLatchTest does.
|
# and (the second, and D8's case) the real server image, as PeerLatchTest does.
|
||||||
list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest)
|
list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest)
|
||||||
|
# P14 S2 (docs/Disaggregated/design/11-state-ownership.md): several sessions in ONE process.
|
||||||
|
# The in-process unix supervisor serves each session on its own control thread with its own
|
||||||
|
# ServerSession/ServerLoop/PipeInputs block/latch domain/segment resolver; two SHM clients are
|
||||||
|
# live at once, mint the same names without colliding, and one closing leaves the other serving.
|
||||||
|
# It forks its own server and its own clients, so it needs no server image.
|
||||||
|
list(APPEND MOBILEGL_WIRE_TESTS MultiSessionTest)
|
||||||
# P11 A1: MOBILEGL_IPC_ADOPT_TIER=0/1 on a stream data plane, against a real `--serve`
|
# P11 A1: MOBILEGL_IPC_ADOPT_TIER=0/1 on a stream data plane, against a real `--serve`
|
||||||
# supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side
|
# supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side
|
||||||
# that names it, map_persistent DECLINED, never a Fatal. Needs the server image too.
|
# that names it, map_persistent DECLINED, never a Fatal. Needs the server image too.
|
||||||
@@ -78,7 +88,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
|
|||||||
${MGL_ROOT}/3rdparty/flatbuffers/include
|
${MGL_ROOT}/3rdparty/flatbuffers/include
|
||||||
)
|
)
|
||||||
if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR
|
if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR
|
||||||
test STREQUAL "AdoptTierStreamTest")
|
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest")
|
||||||
# It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State
|
# It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State
|
||||||
# the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy
|
# the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy
|
||||||
# session, P12ServerRig.h, and so its include paths.)
|
# session, P12ServerRig.h, and so its include paths.)
|
||||||
|
|||||||
@@ -0,0 +1,521 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Wire/MultiSessionTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P14 S2 (docs/Disaggregated/design/11-state-ownership.md): SEVERAL SESSIONS IN ONE PROCESS.
|
||||||
|
//
|
||||||
|
// WHAT THIS SUITE OWNS. The in-process display server's unix supervisor used to serve one session
|
||||||
|
// at a time (an authenticated second client was Refuse{Busy}); it now runs each session on its own
|
||||||
|
// control thread with its own ServerSession/ServerLoop/PipeInputs block, its own latch domain and
|
||||||
|
// its own segment resolver (Server/SessionRuntime.h), up to a cap. Nothing here needs Android or a
|
||||||
|
// GPU: two clients open the same server's unix endpoint and both are served.
|
||||||
|
//
|
||||||
|
// THE THREE PROPERTIES, one case each:
|
||||||
|
// * TwoSessionsInOneProcessAreServedAtOnce - both clients are live AT THE SAME TIME and every
|
||||||
|
// Welcome names the SAME server process (red once by restoring the one-at-a-time supervisor:
|
||||||
|
// the second Hello is Refuse{Busy} and `started` is 0). With headless EGL, each session clears
|
||||||
|
// its own colour and reads it back - the clear's colour travels in that session's records, so a
|
||||||
|
// read-back that came from the other session's state would be the other colour.
|
||||||
|
// * SameNamesInTwoSessionsDoNotCollide - the two sessions mint the SAME names: the same client
|
||||||
|
// context token (the session's {token -> ContextRuntime} table) and the same allocatable render
|
||||||
|
// state handle and generation (the session's applier table). Both must be accepted, because a
|
||||||
|
// name is a name IN A SESSION. Red once by sharing either table: the second CreateContext
|
||||||
|
// answers ok=false and the second CreateRenderState is
|
||||||
|
// Fatal{ProtocolCorruption, "Cso.handle"} through the latch (the session ends, exit 75).
|
||||||
|
// * ClosingOneSessionLeavesTheOtherServing - session A ends cleanly while B is live and B keeps
|
||||||
|
// applying records. Red once by releasing the process-wide pieces per session instead of per
|
||||||
|
// process: A's Close uninstalls the reverse-channel callback table and the segment resolver
|
||||||
|
// that B is still using, so B's next record is a Fatal{ProtocolCorruption} on a table-less
|
||||||
|
// resolve or the session's own latch.
|
||||||
|
//
|
||||||
|
// WHAT THIS SUITE DOES NOT OWN, AND SAYS SO. The two sessions' NATIVE state (DirectGLES' display,
|
||||||
|
// config, context and surface are process-global today) is P14b's: one session's teardown still
|
||||||
|
// tears down the process's EGL context, so the render half is exercised while BOTH sessions are
|
||||||
|
// alive and never after one has closed, and the post-close proof is record-level (a record applied
|
||||||
|
// and answered) rather than a second read-back.
|
||||||
|
|
||||||
|
#include "P12ServerRig.h"
|
||||||
|
|
||||||
|
#include <MG_Remote/Server/InProcessServer.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <csignal>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace P12;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---- the server: this test's own fork, the display server entry on a thread, a UNIX endpoint
|
||||||
|
// (the TCP in-process shape keeps its one-session-at-a-time supervisor; this slice changed the
|
||||||
|
// unix one, which is the shape anland's display server uses).
|
||||||
|
|
||||||
|
[[noreturn]] void RunUnixInProcessServerProcess(const std::string& endpoint, const std::string& logBase) {
|
||||||
|
::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1);
|
||||||
|
::setenv("MOBILEGL_IPC_ROLE", "server", 1);
|
||||||
|
::setenv("MOBILEGL_IPC_DIAL", "no", 1);
|
||||||
|
for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB",
|
||||||
|
"MOBILEGL_IPC_STAGE_MB", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_SURFACE",
|
||||||
|
"MOBILEGL_IPC_INPROC_MAX_SESSIONS", "MOBILEGL_BACKEND_TYPE"})
|
||||||
|
::unsetenv(name);
|
||||||
|
sigset_t stop;
|
||||||
|
sigemptyset(&stop);
|
||||||
|
sigaddset(&stop, SIGTERM);
|
||||||
|
pthread_sigmask(SIG_BLOCK, &stop, nullptr);
|
||||||
|
std::atomic<bool> returned{false};
|
||||||
|
std::atomic<int> served{-1};
|
||||||
|
std::thread server([&] {
|
||||||
|
served.store(mobilegl_server_serve_inprocess(endpoint.c_str()));
|
||||||
|
returned.store(true);
|
||||||
|
});
|
||||||
|
bool stopping = false;
|
||||||
|
while (!returned.load()) {
|
||||||
|
timespec slice{0, 100 * 1000 * 1000};
|
||||||
|
if (sigtimedwait(&stop, nullptr, &slice) == SIGTERM && !stopping) {
|
||||||
|
stopping = true;
|
||||||
|
mobilegl_server_stop_inprocess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.join();
|
||||||
|
std::fflush(nullptr);
|
||||||
|
::_exit(stopping && served.load() == 0 ? 0 : 100 + (served.load() & 0x7f));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LaunchUnixInProcessServer(ServerProcess* server, const std::string& label) {
|
||||||
|
server->endpoint = "@mgl-multi-" + label + "-" + std::to_string(::getpid());
|
||||||
|
server->logBase = "/tmp/mgl-multi-" + label + "-" + std::to_string(::getpid()) + ".log";
|
||||||
|
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
|
||||||
|
PinHeadlessEgl();
|
||||||
|
Debug::TruncateRoleLogs(server->logBase.c_str());
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t pid = ::fork();
|
||||||
|
if (pid < 0) return false;
|
||||||
|
if (pid == 0) RunUnixInProcessServerProcess(server->endpoint, server->logBase);
|
||||||
|
server->pid = pid;
|
||||||
|
bool listening = false;
|
||||||
|
(void)WaitFor(
|
||||||
|
[&] {
|
||||||
|
if (server->Log().find("listening on") != std::string::npos) return listening = true;
|
||||||
|
return server->WaitExit(0) && server->pid <= 0;
|
||||||
|
},
|
||||||
|
10000);
|
||||||
|
// The listener takes the native address; the client's control selector includes its
|
||||||
|
// transport prefix (bare @name is not a supported MOBILEGL_IPC_CONTROL spelling).
|
||||||
|
if (listening) server->endpoint = "unix:" + server->endpoint;
|
||||||
|
return listening;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- a peer this test drives STEP BY STEP, so a case can interleave two sessions: everything
|
||||||
|
// the peer does is decided by the parent over a pipe, and every step answers with a report.
|
||||||
|
|
||||||
|
// The two clear colours, packed the way the read-back hands them back (A<<24|B<<16|G<<8|R,
|
||||||
|
// the little-endian bytes of a GL_RGBA/GL_UNSIGNED_BYTE texel).
|
||||||
|
constexpr Uint32 kGreen = 0xFF00FF00u;
|
||||||
|
constexpr Uint32 kRed = 0xFF0000FFu;
|
||||||
|
|
||||||
|
enum PeerOp : Uint32 {
|
||||||
|
kOpApplyRecord = 1, // one record through the applier (no GL)
|
||||||
|
kOpCreateContext = 2, // ServerCreateEGLContext(arg) - the session's context token table
|
||||||
|
kOpHealthySession = 3, // the rig's whole healthy session, `arg` = the clear colour
|
||||||
|
kOpStop = 4, // ClientSession::Stop, then the child leaves
|
||||||
|
// P14 S5: mint context `arg` in share group `aux` (a real frontend share-group token, so
|
||||||
|
// two contexts that pass different ones are a NON-shared pair and two that pass the same
|
||||||
|
// one are a sharing pair).
|
||||||
|
kOpCreateContextInGroup = 5,
|
||||||
|
// P14 S5: the in-band binding record - the client's "from here on, my records belong to
|
||||||
|
// context `arg`". EGLImpl::MakeCurrent emits exactly this after a successful make-current;
|
||||||
|
// a case that drives records by hand has to emit it itself, or every record lands in the
|
||||||
|
// session's "no context" applier.
|
||||||
|
kOpBindContext = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PeerCommand {
|
||||||
|
Uint32 op = 0;
|
||||||
|
Uint32 arg = 0;
|
||||||
|
Uint32 aux = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The report of one step. `session` is the rig's own report for the steps that run a whole
|
||||||
|
// session; the step-specific fields sit beside it.
|
||||||
|
struct StepReport {
|
||||||
|
Int32 started = 0;
|
||||||
|
Uint32 serverPid = 0;
|
||||||
|
Int32 context = -1;
|
||||||
|
Int32 applied = 0;
|
||||||
|
PeerReport session{};
|
||||||
|
char note[128] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
void ApplyOneRecord(Remote::Client::ClientSession& client, StepReport& r) {
|
||||||
|
P::MGPMemoryBarrier barrier{};
|
||||||
|
barrier.Bits = 0x2000u;
|
||||||
|
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Peer {
|
||||||
|
pid_t pid = -1;
|
||||||
|
int toChild = -1;
|
||||||
|
int fromChild = -1;
|
||||||
|
// The handshake's own report, read by StartPeer: a step's report must never be this one.
|
||||||
|
StepReport ready{};
|
||||||
|
|
||||||
|
StepReport Step(PeerOp op, Uint32 arg = 0, Uint32 aux = 0) {
|
||||||
|
StepReport report{};
|
||||||
|
PeerCommand command{op, arg, aux};
|
||||||
|
if (::write(toChild, &command, sizeof(command)) != static_cast<ssize_t>(sizeof(command))) {
|
||||||
|
std::snprintf(report.note, sizeof(report.note), "peer write failed");
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
if (::read(fromChild, &report, sizeof(report)) != static_cast<ssize_t>(sizeof(report))) {
|
||||||
|
report = StepReport{};
|
||||||
|
std::snprintf(report.note, sizeof(report.note), "peer died before reporting");
|
||||||
|
}
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Close() {
|
||||||
|
if (pid <= 0) return;
|
||||||
|
(void)Step(kOpStop);
|
||||||
|
int status = 0;
|
||||||
|
::waitpid(pid, &status, 0);
|
||||||
|
::close(toChild);
|
||||||
|
::close(fromChild);
|
||||||
|
toChild = -1;
|
||||||
|
fromChild = -1;
|
||||||
|
pid = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
~Peer() {
|
||||||
|
if (pid > 0) {
|
||||||
|
::kill(pid, SIGKILL);
|
||||||
|
int status = 0;
|
||||||
|
(void)::waitpid(pid, &status, 0);
|
||||||
|
}
|
||||||
|
if (toChild >= 0) ::close(toChild);
|
||||||
|
if (fromChild >= 0) ::close(fromChild);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Peer StartPeer(const std::string& endpoint) {
|
||||||
|
Peer peer;
|
||||||
|
int commands[2] = {-1, -1};
|
||||||
|
int reports[2] = {-1, -1};
|
||||||
|
if (::pipe(commands) != 0 || ::pipe(reports) != 0) return peer;
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t pid = ::fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
::close(commands[1]);
|
||||||
|
::close(reports[0]);
|
||||||
|
auto& client = Remote::Client::ClientSessionInstance();
|
||||||
|
MobileGL::MG_Config::Ipc.Control = endpoint.c_str();
|
||||||
|
MobileGL::MG_Config::Ipc.Data = "auto";
|
||||||
|
const MobileGLResult started = client.StartSpawned();
|
||||||
|
StepReport ready{};
|
||||||
|
ready.started = started == MOBILEGL_OK ? 1 : 0;
|
||||||
|
ready.serverPid = client.PeerServerPid();
|
||||||
|
if (!ready.started) std::snprintf(ready.note, sizeof(ready.note), "start rc=%d", static_cast<int>(started));
|
||||||
|
(void)::write(reports[1], &ready, sizeof(ready));
|
||||||
|
for (;;) {
|
||||||
|
PeerCommand command{};
|
||||||
|
if (::read(commands[0], &command, sizeof(command)) != static_cast<ssize_t>(sizeof(command))) break;
|
||||||
|
if (command.op == kOpStop) {
|
||||||
|
StepReport bye{};
|
||||||
|
bye.started = ready.started;
|
||||||
|
bye.serverPid = ready.serverPid;
|
||||||
|
if (ready.started) client.Stop();
|
||||||
|
(void)::write(reports[1], &bye, sizeof(bye));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
StepReport report{};
|
||||||
|
report.started = ready.started;
|
||||||
|
report.serverPid = ready.serverPid;
|
||||||
|
switch (command.op) {
|
||||||
|
case kOpApplyRecord:
|
||||||
|
ApplyOneRecord(client, report);
|
||||||
|
break;
|
||||||
|
case kOpCreateContext:
|
||||||
|
report.context = Remote::Server::ServerCreateEGLContext(command.arg, /*shareGroupToken=*/0,
|
||||||
|
/*flags=*/0)
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
break;
|
||||||
|
case kOpCreateContextInGroup:
|
||||||
|
report.context = Remote::Server::ServerCreateEGLContext(command.arg, command.aux, /*flags=*/0)
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
break;
|
||||||
|
case kOpBindContext: {
|
||||||
|
P::MGPBindContext bind{};
|
||||||
|
bind.ClientContextToken = command.arg;
|
||||||
|
report.applied = Emit(client, P::MGPWireOp::BindContext, &bind, sizeof(bind)) ? 1 : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case kOpHealthySession: {
|
||||||
|
PeerReport session{};
|
||||||
|
HealthySessionWithColor(client, session, command.arg);
|
||||||
|
report.session = session;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
std::snprintf(report.note, sizeof(report.note), "unknown peer op %u", command.arg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(void)::write(reports[1], &report, sizeof(report));
|
||||||
|
}
|
||||||
|
std::fflush(nullptr);
|
||||||
|
::_exit(0);
|
||||||
|
}
|
||||||
|
::close(commands[0]);
|
||||||
|
::close(reports[1]);
|
||||||
|
if (pid < 0) {
|
||||||
|
::close(commands[1]);
|
||||||
|
::close(reports[0]);
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
peer.pid = pid;
|
||||||
|
peer.toChild = commands[1];
|
||||||
|
peer.fromChild = reports[0];
|
||||||
|
// The handshake's report, taken HERE so that no Step can read it and mistake it for its
|
||||||
|
// own answer.
|
||||||
|
if (::read(peer.fromChild, &peer.ready, sizeof(peer.ready)) !=
|
||||||
|
static_cast<ssize_t>(sizeof(peer.ready))) {
|
||||||
|
peer.ready = StepReport{};
|
||||||
|
std::snprintf(peer.ready.note, sizeof(peer.ready.note), "peer died during its handshake");
|
||||||
|
}
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server's own line for the n-th session it started.
|
||||||
|
std::string SessionStartedLine(const ServerProcess& server, unsigned long long ordinal) {
|
||||||
|
const std::string log = server.Log();
|
||||||
|
const std::string needle = "in-process unix session #" + std::to_string(ordinal) + " started";
|
||||||
|
const auto at = log.find(needle);
|
||||||
|
if (at == std::string::npos) return {};
|
||||||
|
const auto end = log.find('\n', at);
|
||||||
|
return log.substr(at, end == std::string::npos ? std::string::npos : end - at);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIGPIPE would kill this test process the moment a peer it drives has already gone; every
|
||||||
|
// write to a peer's pipe is guarded by the peer being alive, and this is the belt.
|
||||||
|
class MultiSessionFixture : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override { ::signal(SIGPIPE, SIG_IGN); }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// THE HEADLINE. Two clients, one server process, both served at once.
|
||||||
|
TEST_F(MultiSessionFixture, TwoShmSessionsInOneProcessAreServedAtOnce) {
|
||||||
|
ServerProcess server;
|
||||||
|
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "two")) << "the in-process unix server did not start:\n" << server.Log();
|
||||||
|
const auto serverPid = static_cast<Uint32>(server.pid);
|
||||||
|
|
||||||
|
Peer a = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(a.pid, 0);
|
||||||
|
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
|
||||||
|
EXPECT_EQ(a.ready.serverPid, serverPid) << "session A is not the in-process server's own process";
|
||||||
|
const StepReport first = a.Step(kOpApplyRecord);
|
||||||
|
ASSERT_EQ(first.applied, 1) << "session A applied no record: " << first.note;
|
||||||
|
|
||||||
|
// WHILE A IS LIVE. Red once by restoring the one-at-a-time supervisor: this Hello is refused
|
||||||
|
// Busy and `started` is 0.
|
||||||
|
Peer b = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(b.pid, 0);
|
||||||
|
ASSERT_EQ(b.ready.started, 1) << "a second session was refused while one was live (" << b.ready.note << ")\n"
|
||||||
|
<< server.Log();
|
||||||
|
EXPECT_EQ(b.ready.serverPid, serverPid) << "the two sessions are not one process";
|
||||||
|
const StepReport second = b.Step(kOpApplyRecord);
|
||||||
|
EXPECT_EQ(second.applied, 1) << "session B applied no record: " << second.note;
|
||||||
|
|
||||||
|
// Both sessions really were started, in this process, and neither latched.
|
||||||
|
EXPECT_NE(SessionStartedLine(server, 1).find("pid=" + std::to_string(server.pid)), std::string::npos)
|
||||||
|
<< SessionStartedLine(server, 1) << "\n" << server.Log();
|
||||||
|
EXPECT_NE(SessionStartedLine(server, 2).find("pid=" + std::to_string(server.pid)), std::string::npos)
|
||||||
|
<< SessionStartedLine(server, 2) << "\n" << server.Log();
|
||||||
|
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u) << "a session latched a fault: " << server.Log();
|
||||||
|
|
||||||
|
// THE RENDER HALF, one session at a time, BOTH STILL LIVE: each session's clear colour travels
|
||||||
|
// in its own records and its read-back must be that colour. (The two sessions' native state is
|
||||||
|
// P14b's; see this file's header for why the halves are sequential.)
|
||||||
|
const StepReport aGl = a.Step(kOpHealthySession, kGreen);
|
||||||
|
const StepReport bGl = b.Step(kOpHealthySession, kRed);
|
||||||
|
if (aGl.session.egl == 0 || bGl.session.egl == 0) {
|
||||||
|
GTEST_SKIP() << "concurrent sessions and their isolation proven; the render half needs headless EGL ("
|
||||||
|
<< aGl.session.note << " / " << bGl.session.note << ")";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(aGl.session.applied, 1) << aGl.session.note;
|
||||||
|
EXPECT_EQ(aGl.session.status, Remote::Wire::ReplySink::kStatusOk) << aGl.session.note;
|
||||||
|
EXPECT_EQ(aGl.session.pixel, kGreen) << "session A read back " << std::hex << aGl.session.pixel
|
||||||
|
<< ", not its own clear colour";
|
||||||
|
EXPECT_EQ(bGl.session.applied, 1) << bGl.session.note;
|
||||||
|
EXPECT_EQ(bGl.session.status, Remote::Wire::ReplySink::kStatusOk) << bGl.session.note;
|
||||||
|
EXPECT_EQ(bGl.session.pixel, kRed) << "session B read back " << std::hex << bGl.session.pixel
|
||||||
|
<< ", not its own clear colour";
|
||||||
|
|
||||||
|
b.Close();
|
||||||
|
a.Close();
|
||||||
|
server.Stop();
|
||||||
|
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
|
||||||
|
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NAME IS A NAME IN A SESSION. Both sessions mint the same client context token and the same
|
||||||
|
// allocatable render state handle, and both are accepted.
|
||||||
|
TEST_F(MultiSessionFixture, SameNamesInTwoSessionsDoNotCollide) {
|
||||||
|
ServerProcess server;
|
||||||
|
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "names")) << server.Log();
|
||||||
|
|
||||||
|
Peer a = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(a.pid, 0);
|
||||||
|
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
|
||||||
|
const StepReport aStarted = a.Step(kOpCreateContext, 7);
|
||||||
|
EXPECT_EQ(aStarted.context, 1) << "session A's own context token was refused";
|
||||||
|
|
||||||
|
Peer b = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(b.pid, 0);
|
||||||
|
ASSERT_EQ(b.ready.started, 1) << "a second session was refused (" << b.ready.note << ")\n" << server.Log();
|
||||||
|
const StepReport bStarted = b.Step(kOpCreateContext, 7);
|
||||||
|
// Red once by sharing the {token -> ContextRuntime} table: CreateContext(7) is already taken
|
||||||
|
// and this answers ok=false - which the client's eglCreateContext turns into EGL_NO_CONTEXT.
|
||||||
|
EXPECT_EQ(bStarted.context, 1) << "session B could not mint the token session A had already used";
|
||||||
|
|
||||||
|
// And the same allocatable handle at the same generation: the rig's healthy session creates
|
||||||
|
// the render state at {kMGPipeFirstAllocatableSlot, 1}, which is the same name in both.
|
||||||
|
const StepReport noGlSession = b.Step(kOpApplyRecord);
|
||||||
|
EXPECT_EQ(noGlSession.applied, 1) << "session B stopped applying after minting session A's names: "
|
||||||
|
<< noGlSession.note;
|
||||||
|
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u) << "a session latched: " << server.Log();
|
||||||
|
|
||||||
|
b.Close();
|
||||||
|
a.Close();
|
||||||
|
server.Stop();
|
||||||
|
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
|
||||||
|
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOSING ONE SESSION LEAVES THE OTHER SERVING. A ends; B keeps applying records, because the
|
||||||
|
// reverse-channel table and the segment resolver A's Close would have released are shared by the
|
||||||
|
// process and are still B's - and because the latch is B's own domain.
|
||||||
|
TEST_F(MultiSessionFixture, ClosingOneSessionLeavesTheOtherServing) {
|
||||||
|
ServerProcess server;
|
||||||
|
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "close")) << server.Log();
|
||||||
|
|
||||||
|
Peer a = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(a.pid, 0);
|
||||||
|
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
|
||||||
|
const StepReport aFirst = a.Step(kOpApplyRecord);
|
||||||
|
ASSERT_EQ(aFirst.applied, 1) << "session A applied no record: " << aFirst.note;
|
||||||
|
|
||||||
|
Peer b = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(b.pid, 0);
|
||||||
|
ASSERT_EQ(b.ready.started, 1) << "session B was not admitted beside session A (" << b.ready.note << ")\n"
|
||||||
|
<< server.Log();
|
||||||
|
const StepReport bFirst = b.Step(kOpApplyRecord);
|
||||||
|
ASSERT_EQ(bFirst.applied, 1) << "session B applied no record: " << bFirst.note;
|
||||||
|
|
||||||
|
a.Close();
|
||||||
|
ASSERT_TRUE(WaitFor([&] { return server.Log().find("reaped exit=0") != std::string::npos; }, 5000))
|
||||||
|
<< "session A did not end cleanly:\n" << server.Log();
|
||||||
|
EXPECT_EQ(::kill(static_cast<pid_t>(b.pid), 0), 0) << "closing A killed session B's peer";
|
||||||
|
|
||||||
|
// B IS STILL SERVED: its records keep reaching its own applier and the server keeps answering.
|
||||||
|
// Red once by releasing the process-wide resolver on session A's Close: this record's staged
|
||||||
|
// bytes resolve to nothing and the session latches instead of applying.
|
||||||
|
for (int record = 0; record < 3; ++record) {
|
||||||
|
const StepReport after = b.Step(kOpApplyRecord);
|
||||||
|
ASSERT_EQ(after.applied, 1) << "session B stopped applying after session A closed (record " << record
|
||||||
|
<< "): " << after.note << "\n" << server.Log();
|
||||||
|
}
|
||||||
|
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u) << "a session latched: " << server.Log();
|
||||||
|
|
||||||
|
b.Close();
|
||||||
|
server.Stop();
|
||||||
|
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
|
||||||
|
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
|
||||||
|
EXPECT_TRUE(WaitFor([&] { return !SessionStartedLine(server, 2).empty(); }, 5000)) << server.Log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): TWO SESSIONS, TWO CONTEXTS EACH, MANY
|
||||||
|
// INTERLEAVED ROUNDS.
|
||||||
|
//
|
||||||
|
// WHAT THIS OWNS THAT THE APPLIER UNIT SUITE CANNOT. The keying is proved there against a
|
||||||
|
// resolver the test installs itself. Here the resolver is the PRODUCTION one
|
||||||
|
// (SessionRuntime.cpp's ThreadSessionScope, installed on the session's apply thread), the records
|
||||||
|
// come off a real ring through a real decoder, and the two sessions' apply threads run at the
|
||||||
|
// same time - so this is the case that would catch an applier that is per-process after all, or a
|
||||||
|
// resolution that reads state another thread is writing, or a context switch that leaks the
|
||||||
|
// previous context's working state into the records that follow it.
|
||||||
|
//
|
||||||
|
// EVERY ROUND: each peer switches its session's current context (the bind_context record the
|
||||||
|
// client emits on a real eglMakeCurrent) and then runs a whole clear-and-read-back through that
|
||||||
|
// context. The two peers alternate, so every round crosses a context switch AND a session.
|
||||||
|
//
|
||||||
|
// THE COLOUR IS THE WITNESS. Green and red are read back only through the applier the record was
|
||||||
|
// attributed to, so a session that read the other's working state would read the other's colour;
|
||||||
|
// a context that inherited its sibling's cleared inputs would fail the read-back's own
|
||||||
|
// completeness check. Red once by making MGPipeApplier() ignore the resolver: the two contexts of
|
||||||
|
// one session share one applier and one of the two clears lands on top of the other.
|
||||||
|
TEST_F(MultiSessionFixture, TwoSessionsInterleaveTwoContextsEachWithoutCrossTalk) {
|
||||||
|
ServerProcess server;
|
||||||
|
ASSERT_TRUE(LaunchUnixInProcessServer(&server, "interleave"))
|
||||||
|
<< "the in-process unix server did not start:\n" << server.Log();
|
||||||
|
|
||||||
|
Peer a = StartPeer(server.endpoint);
|
||||||
|
Peer b = StartPeer(server.endpoint);
|
||||||
|
ASSERT_GT(a.pid, 0);
|
||||||
|
ASSERT_GT(b.pid, 0);
|
||||||
|
ASSERT_EQ(a.ready.started, 1) << a.ready.note << "\n" << server.Log();
|
||||||
|
ASSERT_EQ(b.ready.started, 1) << "a second session was refused (" << b.ready.note << ")\n"
|
||||||
|
<< server.Log();
|
||||||
|
|
||||||
|
// THE SAME TWO TOKENS AND THE SAME TWO GROUP TOKENS IN BOTH SESSIONS, on purpose: a name is a
|
||||||
|
// name in a session, and the two group tokens say the two contexts of one session do NOT
|
||||||
|
// share - which is the pair whose object records a process-wide applier merged.
|
||||||
|
constexpr Uint32 kGroupOne = 9001;
|
||||||
|
constexpr Uint32 kGroupTwo = 9002;
|
||||||
|
for (Peer* peer : {&a, &b}) {
|
||||||
|
ASSERT_EQ(peer->Step(kOpCreateContextInGroup, /*token=*/1, kGroupOne).context, 1);
|
||||||
|
ASSERT_EQ(peer->Step(kOpCreateContextInGroup, /*token=*/2, kGroupTwo).context, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reportedNoEgl = false;
|
||||||
|
for (int round = 0; round < 8; ++round) {
|
||||||
|
const Uint32 token = 1 + static_cast<Uint32>(round % 2);
|
||||||
|
ASSERT_EQ(a.Step(kOpBindContext, token).applied, 1) << "session A could not bind context " << token;
|
||||||
|
ASSERT_EQ(b.Step(kOpBindContext, token).applied, 1) << "session B could not bind context " << token;
|
||||||
|
|
||||||
|
const StepReport aGl = a.Step(kOpHealthySession, kGreen);
|
||||||
|
const StepReport bGl = b.Step(kOpHealthySession, kRed);
|
||||||
|
if (aGl.session.egl == 0 || bGl.session.egl == 0) {
|
||||||
|
reportedNoEgl = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ASSERT_EQ(aGl.session.status, Remote::Wire::ReplySink::kStatusOk)
|
||||||
|
<< "round " << round << ": " << aGl.session.note;
|
||||||
|
ASSERT_EQ(bGl.session.status, Remote::Wire::ReplySink::kStatusOk)
|
||||||
|
<< "round " << round << ": " << bGl.session.note;
|
||||||
|
EXPECT_EQ(aGl.session.pixel, kGreen) << "round " << round << ": session A read back "
|
||||||
|
<< std::hex << aGl.session.pixel;
|
||||||
|
EXPECT_EQ(bGl.session.pixel, kRed) << "round " << round << ": session B read back "
|
||||||
|
<< std::hex << bGl.session.pixel;
|
||||||
|
}
|
||||||
|
if (reportedNoEgl) {
|
||||||
|
GTEST_SKIP() << "the context/session routing is proven; the render half needs headless EGL ("
|
||||||
|
<< a.ready.note << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 0u)
|
||||||
|
<< "a session latched during the interleave: " << server.Log();
|
||||||
|
|
||||||
|
b.Close();
|
||||||
|
a.Close();
|
||||||
|
server.Stop();
|
||||||
|
EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0)
|
||||||
|
<< "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus;
|
||||||
|
}
|
||||||
@@ -328,9 +328,18 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A HEALTHY SESSION: one record applied and, with headless EGL, a green clear read back -
|
// A HEALTHY SESSION: one record applied and, with headless EGL, a clear read back -
|
||||||
// PeerLatchTest's session B, verbatim in what it sends.
|
// PeerLatchTest's session B, verbatim in what it sends. P14 S2: the clear's colour is
|
||||||
inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) {
|
// HealthySessionWithColor's parameter so a suite that runs several sessions can tell whose
|
||||||
|
// read-back it is reading; HealthySession keeps the two-argument shape every existing
|
||||||
|
// caller takes its address of.
|
||||||
|
//
|
||||||
|
// THE PACKING IS THE READ-BACK'S OWN, A<<24|B<<16|G<<8|R - a GL_RGBA/GL_UNSIGNED_BYTE
|
||||||
|
// texel little-endian in a Uint32, which is how PeerReport::pixel comes back and how every
|
||||||
|
// expectation on it is written (green (0,1,0,1) is 0xFF00FF00). Decoding it in the other
|
||||||
|
// order would clear the complement of the colour the caller asked for.
|
||||||
|
inline void HealthySessionWithColor(Remote::Client::ClientSession& client, PeerReport& r,
|
||||||
|
Uint32 rgba) {
|
||||||
P::MGPMemoryBarrier barrier{};
|
P::MGPMemoryBarrier barrier{};
|
||||||
barrier.Bits = 0x2000u;
|
barrier.Bits = 0x2000u;
|
||||||
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
|
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
|
||||||
@@ -384,8 +393,11 @@ namespace {
|
|||||||
clear.DrawBufferIndex = 0;
|
clear.DrawBufferIndex = 0;
|
||||||
clear.BufferMask = GL_COLOR_BUFFER_BIT;
|
clear.BufferMask = GL_COLOR_BUFFER_BIT;
|
||||||
clear.ValueClass = P::kMGPipeClearValueClassFloat;
|
clear.ValueClass = P::kMGPipeClearValueClassFloat;
|
||||||
const float green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
const float color[4] = {static_cast<float>(rgba & 0xFFu) / 255.0f,
|
||||||
std::memcpy(clear.ColorValue, green, sizeof(green));
|
static_cast<float>((rgba >> 8) & 0xFFu) / 255.0f,
|
||||||
|
static_cast<float>((rgba >> 16) & 0xFFu) / 255.0f,
|
||||||
|
static_cast<float>((rgba >> 24) & 0xFFu) / 255.0f};
|
||||||
|
std::memcpy(clear.ColorValue, color, sizeof(color));
|
||||||
if (!Emit(client, P::MGPWireOp::Clear, &clear, sizeof(clear))) return Note(r, "clear not applied");
|
if (!Emit(client, P::MGPWireOp::Clear, &clear, sizeof(clear))) return Note(r, "clear not applied");
|
||||||
P::MGPReadbackInfo read{};
|
P::MGPReadbackInfo read{};
|
||||||
read.Res = P::kMGPipeNullHandle;
|
read.Res = P::kMGPipeNullHandle;
|
||||||
@@ -402,6 +414,12 @@ namespace {
|
|||||||
if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back");
|
if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back");
|
||||||
std::memcpy(&r.pixel, texel, sizeof(r.pixel));
|
std::memcpy(&r.pixel, texel, sizeof(r.pixel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The green every existing caller asked for, with the two-argument signature a
|
||||||
|
// `std::function<void(ClientSession&, PeerReport&)>` can be built from.
|
||||||
|
inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) {
|
||||||
|
HealthySessionWithColor(client, r, 0xFF00FF00u);
|
||||||
|
}
|
||||||
} // namespace P12
|
} // namespace P12
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -1226,6 +1226,20 @@ namespace {
|
|||||||
reset.ContextSerial = 7777;
|
reset.ContextSerial = 7777;
|
||||||
return Forge(c, r, P::MGPWireOp::ApplierReset, Bytes(reset));
|
return Forge(c, r, P::MGPWireOp::ApplierReset, Bytes(reset));
|
||||||
}},
|
}},
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). A bind_context that names a
|
||||||
|
// context this session never created. NOT the ApplierReset shape one row up: that serial
|
||||||
|
// is asserted against a count both ends hold, while a context token is minted by the
|
||||||
|
// client and the server's only knowledge of it is the CreateContext frame's table - so a
|
||||||
|
// token the client invented is exactly the corruption this site is for. No EGL context is
|
||||||
|
// needed to reach it (the record binds "nothing" the session never created, and the site
|
||||||
|
// is in the sink, ahead of any verb), which is why needsEgl is false.
|
||||||
|
{"BindContextUnknownToken", "PipeApplier.cpp", "BindContext.ClientContextToken", Outcome::Latched,
|
||||||
|
"Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the record binds token 999", false,
|
||||||
|
[](Client::ClientSession& c, PeerReport& r) {
|
||||||
|
P::MGPBindContext bind{};
|
||||||
|
bind.ClientContextToken = 999;
|
||||||
|
return Forge(c, r, P::MGPWireOp::BindContext, Bytes(bind));
|
||||||
|
}},
|
||||||
{"ObjectDeathNullHandle", "PipeApplier.cpp", "ObjectDeath.Handle", Outcome::Latched,
|
{"ObjectDeathNullHandle", "PipeApplier.cpp", "ObjectDeath.Handle", Outcome::Latched,
|
||||||
"Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}", false,
|
"Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}", false,
|
||||||
[](Client::ClientSession& c, PeerReport& r) {
|
[](Client::ClientSession& c, PeerReport& r) {
|
||||||
|
|||||||
@@ -1623,7 +1623,7 @@ namespace {
|
|||||||
// The backend's draw-shaped paths (ReadPixels syncs the bound textures and the read
|
// The backend's draw-shaped paths (ReadPixels syncs the bound textures and the read
|
||||||
// framebuffer) walk the process's frontend GLContext, which a bare unit process does not
|
// framebuffer) walk the process's frontend GLContext, which a bare unit process does not
|
||||||
// have; installed for the case's life and put back afterwards (SplitBufferTest's shape).
|
// have; installed for the case's life and put back afterwards (SplitBufferTest's shape).
|
||||||
UniquePtr<MG_State::GLState::GLContext> savedContext;
|
SharedPtr<MG_State::GLState::GLContext> savedContext;
|
||||||
|
|
||||||
// Empty on success, else the step that failed - the harness's SkipReason shape.
|
// Empty on success, else the step that failed - the harness's SkipReason shape.
|
||||||
std::string BringUp() {
|
std::string BringUp() {
|
||||||
@@ -3537,6 +3537,103 @@ TEST(ServerLoopTest, SessionSurfaceModeAdmitsOnlyTheModeTheFirstSurfaceChose) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// WHAT THESE CASES OWN. Before S4 the backend held ONE native EGLContext, one config and one
|
||||||
|
// surface as file-level statics, and `InitDisplayAndContext` began with an unconditional
|
||||||
|
// `DestroyEGLContext()` - so creating a second surface (or a second context) destroyed the first
|
||||||
|
// one's native context and every GL object that lived in it. The three cases below pin the three
|
||||||
|
// halves of the replacement, each at the driver:
|
||||||
|
//
|
||||||
|
// * A SECOND CONTEXT DOES NOT TAKE THE FIRST ONE'S NATIVE CONTEXT WITH IT. Three client context
|
||||||
|
// tokens are created in one session and all three native contexts are alive AT THE SAME TIME,
|
||||||
|
// holding three different EGLContext handles. Red once by restoring the unconditional teardown:
|
||||||
|
// the third CreateContext leaves one live native context, not three.
|
||||||
|
//
|
||||||
|
// * A SURFACE SWITCH KEEPS THE CONTEXT. Creating a second pbuffer surface leaves the tuple's
|
||||||
|
// native EGLContext the SAME object it was - which is what "pbuffer -> window -> pbuffer does
|
||||||
|
// not lose resources" means at this layer, since the objects live in the native context.
|
||||||
|
// Red once by restoring `DestroyEGLContext()` in CreateEGLPbufferSurface: the handle changes.
|
||||||
|
//
|
||||||
|
// * (A THIRD CASE - "a share group is a sharing pair, proven at the driver" - was drafted and is
|
||||||
|
// NOT LANDED. See the S4 section of docs/Disaggregated/design/11-state-ownership.md: the
|
||||||
|
// sharing argument is wired (CreateNativeContextFor hands the group leader's EGLContext to
|
||||||
|
// eglCreateContext), but the headless probe that would prove object visibility across the pair
|
||||||
|
// did not produce a usable control on this box and is left to its own change rather than
|
||||||
|
// shipped as a green that ran nothing.)
|
||||||
|
//
|
||||||
|
// THE KEY OVERRIDE IS HOW A CASE NAMES A TUPLE. This fixture has no SessionRuntime, so the backend's
|
||||||
|
// resolver answers {0,0} for every thread; ForceNativeContextKeyForTesting is the pure-key override
|
||||||
|
// that lets a case drive exactly the tuple it means. It is a test-only seam and reaches no
|
||||||
|
// production path (DirectGLES.h).
|
||||||
|
//
|
||||||
|
// NO USABLE EGL IS A SKIP (MGL_EGL_BRING_UP_OR_BAIL), the same rule as every case in this section.
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
namespace {
|
||||||
|
namespace DirectGLES = MobileGL::MG_Backend::DirectGLES;
|
||||||
|
|
||||||
|
// A client context token that never collides with the fixture's token-0 tuple.
|
||||||
|
constexpr Uint64 kTokenA = 101;
|
||||||
|
constexpr Uint64 kTokenB = 102;
|
||||||
|
constexpr Uint64 kGroupA = 5001;
|
||||||
|
constexpr Uint64 kGroupB = 5002;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The three-native-contexts control: creating contexts must ADD native contexts, never replace one.
|
||||||
|
TEST(ServerLoopEglTest, CreatingASecondContextLeavesTheFirstNativeContextAlive) {
|
||||||
|
EglServerFixture fixture;
|
||||||
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||||
|
|
||||||
|
const EGLContext default0 = DirectGLES::NativeContextHandleFor(0, 0);
|
||||||
|
ASSERT_NE(default0, EGL_NO_CONTEXT)
|
||||||
|
<< "the surface's own creation did not build a native context for the session's default tuple";
|
||||||
|
|
||||||
|
ASSERT_TRUE(Server::ServerCreateEGLContext(kTokenA, kGroupA, 0));
|
||||||
|
ASSERT_TRUE(Server::ServerCreateEGLContext(kTokenB, kGroupB, 0));
|
||||||
|
|
||||||
|
const EGLContext contextA = DirectGLES::NativeContextHandleFor(0, kTokenA);
|
||||||
|
const EGLContext contextB = DirectGLES::NativeContextHandleFor(0, kTokenB);
|
||||||
|
EXPECT_NE(contextA, EGL_NO_CONTEXT) << "CreateContext built no native context for token A";
|
||||||
|
EXPECT_NE(contextB, EGL_NO_CONTEXT) << "CreateContext built no native context for token B";
|
||||||
|
EXPECT_NE(contextA, contextB) << "two client contexts ended up sharing one EGLContext object";
|
||||||
|
EXPECT_NE(contextA, default0) << "token A reused the default tuple's native context";
|
||||||
|
EXPECT_NE(contextB, default0) << "token B reused the default tuple's native context";
|
||||||
|
EXPECT_EQ(DirectGLES::NativeContextHandleFor(0, 0), default0)
|
||||||
|
<< "creating a context destroyed and rebuilt the default tuple's native context";
|
||||||
|
EXPECT_EQ(DirectGLES::NativeContextCountForSession(0), 3u)
|
||||||
|
<< "not all three contexts are live at once - the pre-S4 shape kept exactly one";
|
||||||
|
|
||||||
|
fixture.TearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// pbuffer -> a second pbuffer -> the same context. The native EGLContext is the tuple's, not the
|
||||||
|
// surface's, so a surface switch must leave it alone.
|
||||||
|
TEST(ServerLoopEglTest, CreatingAnotherSurfaceKeepsTheSameNativeContext) {
|
||||||
|
EglServerFixture fixture;
|
||||||
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||||
|
|
||||||
|
const EGLContext before = DirectGLES::NativeContextHandleFor(0, 0);
|
||||||
|
ASSERT_NE(before, EGL_NO_CONTEXT);
|
||||||
|
|
||||||
|
// A SECOND, DIFFERENT surface handle, exactly as a client's second eglCreatePbufferSurface would
|
||||||
|
// name it (EGLService-style small integers).
|
||||||
|
const EGLSurface second = reinterpret_cast<EGLSurface>(static_cast<std::uintptr_t>(0x2));
|
||||||
|
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(second, 32, 32));
|
||||||
|
|
||||||
|
EXPECT_EQ(DirectGLES::NativeContextHandleFor(0, 0), before)
|
||||||
|
<< "creating a second pbuffer surface replaced the native context: every buffer, texture and "
|
||||||
|
"program the application had built in it went with the old one (the pre-S4 teardown)";
|
||||||
|
EXPECT_EQ(DirectGLES::NativeContextCountForSession(0), 1u)
|
||||||
|
<< "a surface switch left a second native context behind";
|
||||||
|
|
||||||
|
fixture.TearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
||||||
// write, and caches the FILE*. The name carries this process's pid, because
|
// write, and caches the FILE*. The name carries this process's pid, because
|
||||||
|
|||||||
@@ -261,22 +261,24 @@ TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting)
|
|||||||
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(),
|
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(),
|
||||||
::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint);
|
::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint);
|
||||||
}
|
}
|
||||||
// P11 B2 (CONTRACT-P11 B2): CONTROL REVISION 5. A peer built at revision 4 - before
|
// P11 B2 (CONTRACT-P11 B2) AND P14 S1. CONTROL REVISION 6. A peer built at the revision before
|
||||||
// LinkTerms.adoptTier - carries the fingerprint its revision mixed, and is refused by name
|
// the current one - revision 4 carried no LinkTerms.adoptTier, revision 5 no context-lifecycle
|
||||||
// (Refuse{WireFingerprint}), never read as a revision-5 Hello that happens to ask T2. The same shape
|
// SurfaceOp kinds - carries the fingerprint its revision mixed, and is refused by name
|
||||||
// PAIR's 3 -> 4 had. RED ONCE by leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 4 with the schema
|
// (Refuse{WireFingerprint}), never read as a current-revision Hello that happens to ask T2 or to
|
||||||
// change: the two fingerprints are then equal and the Welcome goes out.
|
// send a SurfaceOpKind the receiver does not know. The same shape PAIR's 3 -> 4 had. RED ONCE by
|
||||||
TEST(SessionHandshakeTest, ARevisionFourPeerIsRefusedByNameAtRevisionFive) {
|
// leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 5 with the schema change: the two fingerprints
|
||||||
static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 5, "B2 bumped the control revision 4 -> 5");
|
// are then equal and the Welcome goes out.
|
||||||
|
TEST(SessionHandshakeTest, ARevisionFivePeerIsRefusedByNameAtRevisionSix) {
|
||||||
|
static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 6, "S1 bumped the control revision 5 -> 6");
|
||||||
auto inputs = CapsAbiFingerprintInputs();
|
auto inputs = CapsAbiFingerprintInputs();
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
inputs.ControlSchemaRevision = (static_cast<Uint64>(4) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision;
|
inputs.ControlSchemaRevision = (static_cast<Uint64>(5) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision;
|
||||||
#else
|
#else
|
||||||
inputs.ControlSchemaRevision = 4;
|
inputs.ControlSchemaRevision = 5;
|
||||||
#endif
|
#endif
|
||||||
const Uint64 revisionFour = Transport::MixAbiFingerprint(inputs);
|
const Uint64 revisionFive = Transport::MixAbiFingerprint(inputs);
|
||||||
ASSERT_NE(revisionFour, WireFingerprint());
|
ASSERT_NE(revisionFive, WireFingerprint());
|
||||||
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFour, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect,
|
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFive, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect,
|
||||||
::MobileGL::Wire::RefuseCode::WireFingerprint);
|
::MobileGL::Wire::RefuseCode::WireFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,73 @@ TEST(SurfaceControlFrameTest, TheEleventhFramedOpIsInitCapabilitiesAndItCrossesT
|
|||||||
EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::None);
|
EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md). The two context-lifecycle ops get
|
||||||
|
// their own case rather than two rows in the ten-op table above, for the reason InitCapabilities
|
||||||
|
// has its own: that case's name is a statement about ten, and a name is not traded for another
|
||||||
|
// (G14). What this one states instead is the pair's whole contract - the values are wire ABI
|
||||||
|
// (12/13, append-only), they have wire kinds (a spawned client's eglCreateContext has to cross),
|
||||||
|
// and the identity cast between the two enums covers them.
|
||||||
|
TEST(SurfaceControlFrameTest, TheContextLifecycleFramedOpsAreCreateContextAndDestroyContext) {
|
||||||
|
static_assert(static_cast<unsigned>(SurfaceControlOp::CreateContext) == 12u);
|
||||||
|
static_assert(static_cast<unsigned>(SurfaceControlOp::DestroyContext) == 13u);
|
||||||
|
EXPECT_EQ(static_cast<unsigned>(::MobileGL::Wire::SurfaceOpKind::CreateContext), 12u)
|
||||||
|
<< "wire ABI: append-only, and 12 is the next free value. A renumbering here is a "
|
||||||
|
"protocol break no other case would see";
|
||||||
|
EXPECT_EQ(static_cast<unsigned>(::MobileGL::Wire::SurfaceOpKind::DestroyContext), 13u);
|
||||||
|
|
||||||
|
const SurfaceControlOp wired[] = {SurfaceControlOp::CreateContext,
|
||||||
|
SurfaceControlOp::DestroyContext};
|
||||||
|
for (const SurfaceControlOp op : wired) {
|
||||||
|
::MobileGL::Wire::SurfaceOpKind kind;
|
||||||
|
ASSERT_TRUE(WireKindForSurfaceControlOp(op, &kind))
|
||||||
|
<< Server::SurfaceControlOpName(op) << " may never leave the process, so a spawned "
|
||||||
|
"client's eglCreateContext/eglDestroyContext "
|
||||||
|
"cannot reach the server";
|
||||||
|
EXPECT_EQ(static_cast<unsigned>(kind), static_cast<unsigned>(op))
|
||||||
|
<< Server::SurfaceControlOpName(op) << ": the frame and wire enums drifted apart";
|
||||||
|
SurfaceControlOp back;
|
||||||
|
ASSERT_TRUE(SurfaceControlOpForWireKind(kind, &back))
|
||||||
|
<< "the decode range's upper bound did not move with the append, so the new tag "
|
||||||
|
"reads as out-of-range - Fatal{ProtocolCorruption}, not the missing row it is";
|
||||||
|
EXPECT_EQ(back, op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE OP'S VALUES REALLY CROSS: the two fields a context creation adds to the frame's own
|
||||||
|
// four-tuple, past an encode/decode round trip. A field appended to the schema and never
|
||||||
|
// written by the encoder is exactly the silent disagreement the codec exists to prevent.
|
||||||
|
SurfaceControlFrame frame;
|
||||||
|
frame.kind = SurfaceControlOp::CreateContext;
|
||||||
|
frame.seq = 0x99;
|
||||||
|
frame.context = 7; // the client context token
|
||||||
|
frame.shareGroupToken = 3;
|
||||||
|
frame.contextFlags = 0x5;
|
||||||
|
flatbuffers::FlatBufferBuilder builder(256);
|
||||||
|
const ::MobileGL::Wire::SurfaceOp* wireOp = EncodeAndParse(frame, &builder);
|
||||||
|
ASSERT_NE(wireOp, nullptr) << "a context creation failed to encode";
|
||||||
|
EXPECT_EQ(wireOp->context(), 7ull);
|
||||||
|
EXPECT_EQ(wireOp->shareGroupToken(), 3ull);
|
||||||
|
EXPECT_EQ(wireOp->contextFlags(), 5u);
|
||||||
|
SurfaceControlFrame back;
|
||||||
|
ASSERT_EQ(DecodeWireSurfaceOp(*wireOp, &back), SurfaceWireError::None);
|
||||||
|
EXPECT_EQ(back.kind, SurfaceControlOp::CreateContext);
|
||||||
|
EXPECT_EQ(back.context, 7ull) << "the client context token must survive the codec";
|
||||||
|
EXPECT_EQ(back.shareGroupToken, 3ull);
|
||||||
|
EXPECT_EQ(back.contextFlags, 5u);
|
||||||
|
|
||||||
|
// A destroy carries the token and nothing else: the share group is not something a destroy
|
||||||
|
// may name, and leaving a stale group token in the frame would be a second answer to a
|
||||||
|
// question only a creation asks.
|
||||||
|
SurfaceControlFrame destroy;
|
||||||
|
destroy.kind = SurfaceControlOp::DestroyContext;
|
||||||
|
destroy.context = 7;
|
||||||
|
builder.Clear();
|
||||||
|
const ::MobileGL::Wire::SurfaceOp* wireDestroy = EncodeAndParse(destroy, &builder);
|
||||||
|
ASSERT_NE(wireDestroy, nullptr);
|
||||||
|
EXPECT_EQ(wireDestroy->context(), 7ull);
|
||||||
|
EXPECT_EQ(wireDestroy->shareGroupToken(), 0ull);
|
||||||
|
EXPECT_EQ(wireDestroy->contextFlags(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(SurfaceControlFrameTest, WindowBackendAndWireWindowKindMapExplicitlyBothWays) {
|
TEST(SurfaceControlFrameTest, WindowBackendAndWireWindowKindMapExplicitlyBothWays) {
|
||||||
using MG_Backend::WindowBackend;
|
using MG_Backend::WindowBackend;
|
||||||
const struct {
|
const struct {
|
||||||
@@ -241,6 +308,11 @@ TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) {
|
|||||||
SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent,
|
SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent,
|
||||||
SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval,
|
SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval,
|
||||||
SurfaceControlOp::ReleaseResources, SurfaceControlOp::SetWindowHandle,
|
SurfaceControlOp::ReleaseResources, SurfaceControlOp::SetWindowHandle,
|
||||||
|
// P5f cp's append and P14 S1's two. This case's name is a claim about EVERY framed op,
|
||||||
|
// which is what makes it the one place an append cannot be forgotten: a new kind that
|
||||||
|
// encodes but does not decode fails here even if the ten-op case above was left alone.
|
||||||
|
SurfaceControlOp::InitCapabilities,
|
||||||
|
SurfaceControlOp::CreateContext, SurfaceControlOp::DestroyContext,
|
||||||
};
|
};
|
||||||
for (const SurfaceControlOp op : wired) {
|
for (const SurfaceControlOp op : wired) {
|
||||||
SurfaceControlFrame frame;
|
SurfaceControlFrame frame;
|
||||||
@@ -277,6 +349,10 @@ TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) {
|
|||||||
EXPECT_EQ(back.windowBackend, frame.windowBackend)
|
EXPECT_EQ(back.windowBackend, frame.windowBackend)
|
||||||
<< Server::SurfaceControlOpName(op);
|
<< Server::SurfaceControlOpName(op);
|
||||||
EXPECT_EQ(back.nativeToken, frame.nativeToken);
|
EXPECT_EQ(back.nativeToken, frame.nativeToken);
|
||||||
|
// P14 S1: the two appended fields travel with every op, 0 included - a field that only
|
||||||
|
// survives on the op that uses it is a field the codec has quietly made op-dependent.
|
||||||
|
EXPECT_EQ(back.shareGroupToken, frame.shareGroupToken);
|
||||||
|
EXPECT_EQ(back.contextFlags, frame.contextFlags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,4 @@
|
|||||||
| 14–15 | 帧节奏与线程;进程、握手、EGL、Android(含上屏 server)、崩溃处理 | [`design/08-runtime-and-platform.md`](design/08-runtime-and-platform.md) |
|
| 14–15 | 帧节奏与线程;进程、握手、EGL、Android(含上屏 server)、崩溃处理 | [`design/08-runtime-and-platform.md`](design/08-runtime-and-platform.md) |
|
||||||
| 16、附 A、附 B | 源码目录与构建选项;全部开关;边界计数器 | [`design/09-build-switches-counters.md`](design/09-build-switches-counters.md) |
|
| 16、附 A、附 B | 源码目录与构建选项;全部开关;边界计数器 | [`design/09-build-switches-counters.md`](design/09-build-switches-counters.md) |
|
||||||
| 17 | 各阶段落地时的形状与仍生效的规则(链到 `notes/`) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) |
|
| 17 | 各阶段落地时的形状与仍生效的规则(链到 `notes/`) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) |
|
||||||
|
| — | 状态归属:多 session / 多 context / share group(anland 统一 server 前置) | [`design/11-state-ownership.md`](design/11-state-ownership.md) |
|
||||||
|
|||||||
@@ -26,6 +26,43 @@
|
|||||||
3. **反向事件带 context**。`EventFenceSignaledHead`(`ServerSession.cpp:1324-1338`)、GL error、surface-changed 事件头加 `ContextSlot`(surface 事件已有 surface handle,context 便于 client 直接派发)。fence handle 属 context 的 share group,client 按 (slot, gen) 原样核对。
|
3. **反向事件带 context**。`EventFenceSignaledHead`(`ServerSession.cpp:1324-1338`)、GL error、surface-changed 事件头加 `ContextSlot`(surface 事件已有 surface handle,context 便于 client 直接派发)。fence handle 属 context 的 share group,client 按 (slot, gen) 原样核对。
|
||||||
4. **wireFingerprint 随以上变更 bump**;两端同 commit + 显式 `-DMOBILEGL_BUILD_STAMP=<commit>` 重建,不接受 drift(交接文档「构建产物」节)。
|
4. **wireFingerprint 随以上变更 bump**;两端同 commit + 显式 `-DMOBILEGL_BUILD_STAMP=<commit>` 重建,不接受 drift(交接文档「构建产物」节)。
|
||||||
|
|
||||||
|
## S1 已落地形状(context 身份上 wire)
|
||||||
|
|
||||||
|
上面第 1、2 条是 P14a 的目标形状;S1 只落地了「身份能跨线、server 能归属」这一层,实际形状如下,后续切片按此续建。
|
||||||
|
|
||||||
|
**data plane:`bind_context`,opcode 84**(`MG_Pipe/PipeCalls.def`,`kScreen` 族)。
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| payload | `MGPBindContext{ Uint64 ClientContextToken }`(8 B,`MGP_ASSERT_POD`) |
|
||||||
|
| flags / wait | `kNone` / `kWaitNone`(apply 不读客户端内存,记录的 ring 位置就是它的全部次序) |
|
||||||
|
| apply 入口 | 无 `MGPipeApply*`;与 `applier_reset`/`object_death` 同为 control record,只到 `WireVerbSink` |
|
||||||
|
| monolith 生产者 | 无(G1 不变) |
|
||||||
|
| sink | `ServerVerbSink::OnBindContext`(`MG_Remote/Server/PipeApplier.cpp`) |
|
||||||
|
|
||||||
|
**token 位宽:两端都是 `Uint64`。** 记录里是 `ClientContextToken`,控制帧里是 `SurfaceOp.context`;同宽是刻意的,否则必有一端要截断。`0` 在两处都表示「无 context」(release),也正是一个从未 bind 过的 session 已有的答案。token 由 `EGLState::CreateContext` 铸造(`m_nextClientContextToken` 从 1 起、稠密、不复用),随 `ContextObject` 存放,`EGLState::CurrentContextClientToken()` / `GetContextClientToken()` 读出。
|
||||||
|
|
||||||
|
**control plane:`SurfaceOpKind.CreateContext = 12` / `DestroyContext = 13`**(`protocol.fbs`),不是两个新的 CtrlMsg——context 的创建/销毁/make-current 与 surface 是同一个调用者、同一个线程、同一条阻塞 frame channel,另开一条只会多一个次序会打架的地方。`SurfaceOp` 追加 `shareGroupToken: ulong` 与 `contextFlags: uint`(保留扩展位,今天恒 0)。`MOBILEGL_PROTOCOL_CONTROL_REVISION` 5 → 6,`scripts/ci/protocol_revision_pins.json` 同步加行。
|
||||||
|
|
||||||
|
- `CreateContext`:`context` = 新 context 的 token,`shareGroupToken` = 它并入的 share group(`shareCtx` 的族,或新建),`contextFlags` = 保留位。`display`/`surface` 为 0(server 的 display 就是 session 自己的)。
|
||||||
|
- `DestroyContext`:`context` = 要销的 token;server 销表并在它当前 bind 时解绑。
|
||||||
|
- 两者都用既有的 `SurfaceReply` 回答(ok + refusal),S1 建的是占位 runtime,没有别的可回。
|
||||||
|
|
||||||
|
**`bind_context` 与 MakeCurrent 控制帧的关系:不合并,各管一件事。**
|
||||||
|
|
||||||
|
- `MakeCurrent` 控制帧仍是 **native tuple 的 op**:它让 server 的 apply thread 决定要不要真的 `eglMakeCurrent`,并参与 `m_haveCurrentTuple` 的去重(ID-54/ID-67 的口径一个字没动)。它的 `context` 字段与 `bind_context` 的 token 是同一个值,但那是「同一个身份」,不是「同一条通路」。
|
||||||
|
- `bind_context` 是 **ring 上的归属记录**:它告诉 server「这条 ring 上从今往后的记录属于哪个 context」。它必须在 ring 上,因为 GL 记录就在 ring 上,而控制帧与 ring 之间没有全局次序可依赖。
|
||||||
|
- 发射点只有一个:`EGLImpl::MakeCurrent` 在切换成功之后、返回之前调 `EmitContextBinding`(`MG_Impl/EGLImpl/EGLImpl.cpp`)。这给出「先于后续 GL 记录」的构造性保证——app 线程在 `eglMakeCurrent` 返回前不可能发出新 context 的任何记录,而 ring 是 FIFO。token 未变则不发(identical make-current 是 EGL 合法 no-op)。
|
||||||
|
- `DestroyContext` 帧在客户端 `EGLState::DestroyContext` 接受之后才发,所以 EGL 自己拒掉的那种销毁(context 仍被某线程 current)不会到达 server,两端不会因为「token 悄悄消失」而对不上。
|
||||||
|
|
||||||
|
**与第 1 条的取舍差异(明确记录)**:S1 走的是「在边沿宣告身份」而不是「在 71 个 context 族记录的 header 与 payload 之间插 `Uint32 ContextSlot`」。代价是每个 make-current 一条记录(而不是每条记录 4 B),收益是 payload 布局与 `MGPWireRec_*` 尺寸全部不动、`kRecHasContext` 位与 batch 边界的生成器改动全部不必;单 context 的既有形状(token 0 / 从未 bind 过)零改动即兼容。多线程交错下的逐记录身份若将来确有需要,再按第 1 条补 slot——那时两者可以并存,slot 为 0 仍读当前 token。
|
||||||
|
|
||||||
|
**server 端**(`MG_Remote/Server/ServerSession.{h,cpp}`):每 session 一张 `{token → ContextRuntime}`(`ContextRuntime{ Token, ShareGroupToken, Flags }`,S1 里是占位,S4 给它装 native context)加一个当前 token(atomic,0 = 无)。`CreateContext`/`DestroyContext` 控制帧经 `ServerLoop::ApplySurfaceControlFrame` 的 `CreateContext`/`DestroyContext` 两个 arm 建表/销表;`bind_context` 经 codec 到 `ServerVerbSink::OnBindContext` 切当前 token。写入全部发生在 apply thread,表因此不需要锁。`bind_context` 指向一个本 session 从未创建的 token 是 `Fatal{ProtocolCorruption, "BindContext.ClientContextToken"}`(0 例外,永远合法);表的存在正是为了让这条判定有依据。
|
||||||
|
|
||||||
|
**S1 的 token 只有写入者,没有读者**:server 端目前没有任何 apply 分支读 `CurrentContextToken()`(`grep` 可证:除 `ServerSession` 自己的访问器外无人调用),所以整条渲染路径的行为与 S1 之前逐字相同——这一层先把「归属」记下来,读它做事是 S4(native context)和 P14b/c 的事。没有 `bind_context` 的 session 行为同样不变:`ServerLoopTest` 的 MakeCurrent/epoch/liveness 用例(`MG_Test/Wire/ServerLoopTest.cpp:1780-1941`)仍全部通过,它们走的正是这条路径。
|
||||||
|
|
||||||
|
**S1 的验收证据**(本次提交的测试):`PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` 钉 opcode 84 与 `kNone`/`kScreen`/`kWaitNone`;`SurfaceControlFrameTest.TheContextLifecycleFramedOpsAreCreateContextAndDestroyContext` 钉 12/13 的值与 `shareGroupToken`/`contextFlags` 的往返;`SessionHandshakeTest.ARevisionFivePeerIsRefusedByNameAtRevisionSix` 钉 revision bump;`FuzzArm2/PeerLatchSite/.../BindContextUnknownToken` 是 data plane 的端到端控制——真实 `--serve` supervisor 上,一个伪造的、指向本 session 从未创建过的 token 的 `bind_context` 让 session 按名 latch(`exit=75`),且同一 supervisor 的下一个 session 正常渲染。
|
||||||
|
|
||||||
## 前端(MG_State / MG_Impl)
|
## 前端(MG_State / MG_Impl)
|
||||||
|
|
||||||
1. **拆 GLContext**。`MG_State/GLState/Core.h:77-698` 的 `GLContext` 拆成 `ShareGroupState`(buffer/texture/renderbuffer/sampler/shader/program/sync 的对象 store + 名称分配)与 `GLContext`(bindings、error、viewport/scissor、render state、VAO/FBO/TF/pipeline/query store、texture unit 绑定、current program)。容器对象(VAO/FBO/TF/pipeline/query)**不共享**,留在 per-context;这是 GL 4.6 §5 的口径,不要为省事把 FBO 放进 share group。
|
1. **拆 GLContext**。`MG_State/GLState/Core.h:77-698` 的 `GLContext` 拆成 `ShareGroupState`(buffer/texture/renderbuffer/sampler/shader/program/sync 的对象 store + 名称分配)与 `GLContext`(bindings、error、viewport/scissor、render state、VAO/FBO/TF/pipeline/query store、texture unit 绑定、current program)。容器对象(VAO/FBO/TF/pipeline/query)**不共享**,留在 per-context;这是 GL 4.6 §5 的口径,不要为省事把 FBO 放进 share group。
|
||||||
@@ -33,14 +70,64 @@
|
|||||||
3. **Tracker 判据改显式身份**。`Pipe/Tracker.h:306-309` 的指针比较换成 (session, contextSlot);换 context 的首个 verb 重推完整状态的语义不变。`MGPipeTrackerInstance()`(`Tracker.h:956-963`)从单例改为 per-context(tracker 缓存的是 per-context 推送快门)。
|
3. **Tracker 判据改显式身份**。`Pipe/Tracker.h:306-309` 的指针比较换成 (session, contextSlot);换 context 的首个 verb 重推完整状态的语义不变。`MGPipeTrackerInstance()`(`Tracker.h:956-963`)从单例改为 per-context(tracker 缓存的是 per-context 推送快门)。
|
||||||
4. **eglMakeCurrent 流程**:`EGLImpl::MakeCurrent` 除调 `backendObject->MakeEGLCurrent` 外,先切 TLS GLContext 再发 `SurfaceOp::MakeCurrent{contextSlot}`;client 线程后续的 context 族记录都带这个 slot。
|
4. **eglMakeCurrent 流程**:`EGLImpl::MakeCurrent` 除调 `backendObject->MakeEGLCurrent` 外,先切 TLS GLContext 再发 `SurfaceOp::MakeCurrent{contextSlot}`;client 线程后续的 context 族记录都带这个 slot。
|
||||||
|
|
||||||
|
## S3 已落地形状(前端 per-context / share group)
|
||||||
|
|
||||||
|
上面「前端」一节是目标形状;S3 落地了第 1、2 条(第 3、4 条的 tracker/slot 部分未做),实际形状如下。
|
||||||
|
|
||||||
|
**`ShareGroupState`(`MG_State/GLState/ShareGroupState.{h,cpp}`,新)** 持有一个 share group 的全部对象注册表与名字生成器:
|
||||||
|
|
||||||
|
| 共享(ShareGroupState) | 每 context(GLContext) |
|
||||||
|
|---|---|
|
||||||
|
| buffer / texture / renderbuffer 的对象表与 `IndexGenerator`;sampler 对象表(`SamplerState` 整体);program+shader 表与共用名字生成器(`ProgramState` 整体);`token`(S1 的 `ShareGroupToken`) | error 列表;buffer binding slot 与 indexed binding point、touched 高水位、各 aggregate generation;texture unit / image unit 数组、active unit、bind/sampling generation、per-target 默认纹理(name 0);renderbuffer binding slot;VAO/FBO/TF/pipeline/query;`current program` |
|
||||||
|
|
||||||
|
`BufferState`/`TextureState`/`RenderbufferState` 因此只剩 binding 侧:删除走「先查 share group 的对象、再解本 context 的绑定、最后 free 名字」,即删对象是全局的、解绑定是逐 context 的(GL 4.6 core 6.1/8.1)。`ProgramState` 的 `UseProgram`/`glDeleteProgram`/`ReleaseShaderNameIfOrphaned` 现在把调用方的 current program 当参数传入,current program 本身落在 `GLContext`。`GLContext` 由 `GLContext(SharedPtr<ShareGroupState>)` 构造,`GLContext()` 建一个只属于它的 share group(测试与进程默认 context 用)。
|
||||||
|
|
||||||
|
**`pGLContext` 改成 TLS**:`extern thread_local SharedPtr<GLState::GLContext> pGLContext`(`GLState/Core.h`)。`EGLState::MakeCurrent` 成功后把它指向该 context 的 `GLContext`,`ReleaseThread`/`eglTerminate`/释放路径恢复线程进入 EGL 之前的那个值(`ThreadCurrentState::RestoreThreadGLState`)——所以「一个 context 同时只被一个线程拥有」的既有语义(`m_contextOwners`,EGL_BAD_ACCESS)一个字没动,owner 也不进 TLS:context 跨线程移交后状态仍在它自己的 `GLContext` 里。
|
||||||
|
|
||||||
|
没有 EGL context 的线程落到**进程默认 context**(`ProcessDefaultGLContext()`,`MG_State::Init()` 建,构造时 `pGLContext` 的初值就是它)——这正是拆分前「全局唯一 context」的语义,所以单 context 应用与所有直接操作 `pGLContext` 的既有测试行为不变。
|
||||||
|
|
||||||
|
**每个 `eglCreateContext` 都建独立 `GLContext`**(`ContextObject::GLStateObject`);`shareCtx != EGL_NO_CONTEXT` 时 `GLContext` 挂到对方 `ShareGroupState`,否则新建一个并把 S1 铸的 `ShareGroupToken` 记在里面。这里不需要「迁移对方 store」:group 在第一个 context 诞生时就存在,后来者直接并入。`eglDestroyContext` 丢掉 `ContextObject`,共享指针归零才销毁 `GLContext` / `ShareGroupState`。
|
||||||
|
|
||||||
|
**默认帧缓冲(明确的偏差)**:GL framebuffer 0 今天仍是**进程一个对象**(`MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo`,后端按它判断「这是默认 FBO」)。它在 `MG_Impl::Init()` 建好之后经 `MG_State::SetDefaultFramebuffer` 交给 MG_State,此后每个新建 `GLContext` 构造时把它装成自己的 name 0 并绑到 Draw/Read(`GLContext::InstallDefaultFramebuffer`)。也就是说:每个 context 有**自己的 FramebufferState 表项与绑定**(`glBindFramebuffer` 互不影响),但那个对象暂时是共用的。真正的 per-context 默认帧缓冲属于「后端 per-context native tuple」那一期(见「后端」§1),在那之前若各 context 各建一份,后端的 `pDefaultFramebufferInfo` 身份判定会对第二个 context 失效。
|
||||||
|
|
||||||
|
**monolith 与 split 走同一份结构**:两端共用一个 `MG_State`,`eglCreateContext`/`eglMakeCurrent`/`eglDestroyContext` 都先落这套前端状态,`MOBILEGL_PIPE_PUSH=0` 与 `=1` 的区别只是后端从哪儿读状态(`MGB_CTX` 是 `pGLContext` 还是 `gPipeInputs`)。`MGB_CTX_IDENTITY` 因此天然按 context 变化,post-S3 的 tracker 只需照旧比较(改成显式 slot 是后续切片)。
|
||||||
|
|
||||||
|
**验收证据**(本次):`MG_Test/State/ContextStateOwnershipTest.cpp` 六个用例——不共享的两个 context 名字空间独立(同名不同对象、绑定不互见);共享对看到同一对象而各自持有绑定;`eglMakeCurrent` 切换后 error/viewport/FBO binding 各自独立且切回来还在;`eglDestroyContext` 后另一个 context 对象与绑定不受影响;释放后线程回到进入 EGL 前的 GL 状态;无 current context 的线程落到进程默认 context。
|
||||||
|
|
||||||
## server(MG_Remote/Server)
|
## server(MG_Remote/Server)
|
||||||
|
|
||||||
1. **session registry 取代单例**。`ServerSessionInstance`/`ServerLoopInstance` 改为 `ServerProcess` 持有的 session 表(slot → {transport, ServerSession, ServerLoop, PipeApplier, apply thread})。in-process server(`ServerMain.cpp:1593-1651` 的 `mobilegl_server_serve_inprocess`)从「单 active + RefuseBusy」改为接受至上限(默认 8,可配);`--serve` 的 unix 路径同样改为同进程多 session,**移除 fork-worker 分发**(`ServerMain.cpp:1566-1584`),`--max-sessions` 语义改为同进程上限。TCP 的 `TcpSupervisor` 单 active(`:1220-1223`)同样让位。
|
1. **session registry 取代单例**。`ServerSessionInstance`/`ServerLoopInstance` 改为 `ServerProcess` 持有的 session 表(slot → {transport, ServerSession, ServerLoop, PipeApplier, apply thread})。in-process server(`ServerMain.cpp:1593-1651` 的 `mobilegl_server_serve_inprocess`)从「单 active + RefuseBusy」改为接受至上限(默认 8,可配);`--serve` 的 unix 路径同样改为同进程多 session,**移除 fork-worker 分发**(`ServerMain.cpp:1566-1584`),`--max-sessions` 语义改为同进程上限。TCP 的 `TcpSupervisor` 单 active(`:1220-1223`)同样让位。
|
||||||
2. **apply thread per session**。每 session 一个 apply thread;`g_applyThreadKey`(`ServerLoop.h:129`)改为 per-session 成员,`OnApplyThread()` 语义不变(同 thread 比较)。session 之间顺序调度由线程天然完成,不需要全局锁;native driver 本来就支持多线程各自 current 不同 context。
|
2. **apply thread per session**。每 session 一个 apply thread;`g_applyThreadKey`(`ServerLoop.h:129`)改为 per-session 成员,`OnApplyThread()` 语义不变(同 thread 比较)。session 之间顺序调度由线程天然完成,不需要全局锁;native driver 本来就支持多线程各自 current 不同 context。
|
||||||
3. **g_applier per session**。`PipeApply.cpp:484` 的进程级 `MGPipeApplierState` 改为 session 持有;`MGPipeApplierReset` 的 role guard(`PipeApply.cpp:1474-1481`)按本 session 的 apply thread 判定。`PipeApply.h:558-617` 已有的 share-group 对象记录 / working-state 二分是地基:对象记录表按 share group 分桶,working state 按 context 分桶,reset 只清本 context 的 working state。
|
3. **g_applier per session**。`PipeApply.cpp:484` 的进程级 `MGPipeApplierState` 改为 session 持有;`MGPipeApplierReset` 的 role guard(`PipeApply.cpp:1474-1481`)按本 session 的 apply thread 判定。`PipeApply.h:558-617` 已有的 share-group 对象记录 / working-state 二分是地基:对象记录表按 share group 分桶,working state 按 context 分桶,reset 只清本 context 的 working state。**(S5 已落地,形状见下面「S5 已落地形状」:这一条最终不是「per session 一个 applier」,而是 per {session, context} 一个 applier、其对象记录挂到 {session, shareGroup} 的组上。)**
|
||||||
4. **反向路由按 session + context**。`ReverseCallbackOwner`(`ServerSession.cpp:408-418`)的 `Active()` 单活性改为「按发起线程/显式 session 参数解析」;事件头加 `ContextSlot`(见 wire 3)。`gMGPipeCallbacks`(`MGPipeCallbacks.h:77`)从进程级变量改为 per-session 表,Accept 时挂到 session 而非全局。
|
4. **反向路由按 session + context**。`ReverseCallbackOwner`(`ServerSession.cpp:408-418`)的 `Active()` 单活性改为「按发起线程/显式 session 参数解析」;事件头加 `ContextSlot`(见 wire 3)。`gMGPipeCallbacks`(`MGPipeCallbacks.h:77`)从进程级变量改为 per-session 表,Accept 时挂到 session 而非全局。
|
||||||
5. **session 模式闩保留在 session 层**。`SessionSurfaceMode`/`SurfaceModeMismatch`(`ServerLoop.h:199-206`、`ServerLoop.cpp:1539-1544`)口径不变:compositor session 闩 OnScreen、app session 闩 Offscreen;统一 server 同时持有两种 session 是合法形状,不许把闩提升到进程级。
|
5. **session 模式闩保留在 session 层**。`SessionSurfaceMode`/`SurfaceModeMismatch`(`ServerLoop.h:199-206`、`ServerLoop.cpp:1539-1544`)口径不变:compositor session 闩 OnScreen、app session 闩 Offscreen;统一 server 同时持有两种 session 是合法形状,不许把闩提升到进程级。
|
||||||
|
|
||||||
|
### S2 已落地形状(同进程多 session)
|
||||||
|
|
||||||
|
上面第 1、2、4、5 条是 P14c 的目标形状;本切片落地的是「归属拆开、路由按线程」这一层,实际形状如下。
|
||||||
|
|
||||||
|
**每 session 一个 runtime,singleton 变成 fallback**(`MG_Remote/Server/SessionRuntime.h`)。`SessionRuntime` 持有该 session 的 `ServerSession`、`ServerLoop`、`PipeInputs` 块,加一个 latch domain 与一个 segment resolver 绑定;`ThreadSessionScope` 把它挂到 **该 session 的两个线程**上——控制线程(`ServerMain::RunSession` 自己的线程,supervisor 里每 session 一条 `std::thread`)与 apply 线程(`mgl-srv-apply`,`ApplyThreadMain` 第一件事就是开同一个 scope)。`ServerSessionInstance()` / `ServerLoopInstance()` / `ServerSession::Active()` 因此先问「本线程属于哪个 session」,只有**不属于任何 session 的线程**才拿进程级单例。这一条让 ~900 个 `gPipeInputs` / `MGB_CTX` / `ServerSession::Active()` 调用点一行不改就有正确归属。inproc 的「同进程 client+server」形状刻意不开 scope(`ClientSession` 直接调单例),所以那个形状的 verb barrier、单块 PipeInputs 与逐字行为完全不变。
|
||||||
|
|
||||||
|
**gPipeInputs 是每线程解析的引用**(`MG_Backend/MGPipe/PipeInputs.h`)。`gPipeInputs` 从「一个进程级对象」变成 `thread_local` 引用,经 `g_pipeInputsThreadResolver` 这个由 server 层装一次的探针解析到本线程 session 的块,无 session 时落到 `gPipeInputsDefault`(即原对象)。**为什么是这个形状**:`MGB_CTX` 是 `(&gPipeInputs)`,要让 ~380 个点位不动而目标随线程变,只有「目标随线程变的引用」可行。代价是每个访问点一次 TLS 地址计算(不是每个字段,且无 guard:每线程第一次使用时绑定一次),这是本切片的已知成本。
|
||||||
|
|
||||||
|
**apply 线程身份改成 TLS 一位,进程级那个字改成计数。** `OnApplyThread()` 的答案现在只可能是「本线程是不是 apply 线程」,所以它是 `thread_local Bool`(零初始化,无 guard、无原子);`Detail::g_applyThreadKey` 保留原名与类型但装的是**本进程活着的 apply 线程数**——`MG_Backend/DirectGLES/Managers.cpp`(本切片文件集之外)读这个 `!= 0` 问「有没有 apply 线程在跑」,计数正好回答那个问题,热路径没有人再读它。
|
||||||
|
|
||||||
|
**latch 是每 session 一个 domain**(`MG_Remote/FatalFunnel.*`)。原来的四个进程级静态变量变成按 domain 存的 `LatchState`,线程绑定一个 domain(`ThreadSessionScope`),不绑定就是 domain 0(进程默认 = 老行为)。理由不是整洁而是正确性:`SessionLatched()` 的三个读者都在 session 自己的线程上(apply 循环、`DrainRing`、`RunSession` 的控制循环),一个进程共用一个 latch 时 session A 的坏字节能停掉 B 的 apply 线程;`ResetSessionLatch()` 也有镜像问题(一个 session 的 reap 会清掉活着的那个的 latch)。现在 A 的关闭/故障都到不了 B。
|
||||||
|
|
||||||
|
**segment resolver 是每线程的表 + 进程级的 hook**(`MG_Remote/Wire/PipeWireCodec.*`)。`gMGPipeSegmentResolver` 仍是一个进程级函数指针(装一次,不再置空),但 thunk 先查**调用线程绑定的表**,其次才是进程级 fallback。同一线程上出现第二张表仍然是 `Fatal{...already installed...}`(既有 `APartiallyZeroUploadBoxIsFatalRatherThanASilentRace` 与 `ASecondProcessResolverIsFatalRatherThanASilentRace` 的口径不变);不同线程上的第二个 session 不再是错误。
|
||||||
|
|
||||||
|
**全局回调表:装一次、按线程路由、最后一个 session 关才卸。** `gMGPipeCallbacks` 的五个条目仍是进程级函数指针(每个 session 装的是同一批函数),`Close()` 只有在 `ServerSessionCount()` 归零时才清;`g_sessionOwner` 的 CAS 抢占对**有 runtime 的 session 不再生效**(共表是合法的),只对「进程就是一个 session」的形状(inproc client+server 角色、`ServerLoopTest` 的 `FvSession` 及其对等对象)保留原样的 `Fatal{RoleViolation,"callback-double-install"}`。
|
||||||
|
|
||||||
|
**surface mode 仍是 D4 的 per-session 闩**(`ServerLoop::m_surfaceMode`)。本切片一度引入 `SetDeclaredSurfaceMode`,让 `RunSession` 按**进程自己的** `MOBILEGL_IPC_SURFACE` 给 session 声明模式——**已撤掉**,两条理由:(a) 进程级的环境变量说不了统一 server 同时持有的多个 session(§4 第 5 条要的是口径不变、闩不许提升到进程级),(b) 它对 dial 进来的 client 是错的——pre-started server 不继承 client 的 env(`ServerOwnedSurfaceTest` 的 TCP 形状),于是 session 被声明成 offscreen,ServerOwned 的建面先拿 `SurfaceModeMismatch`,永远走不到「本 server 没有 display」那条具名拒绝。**而且它不需要**:`m_surfaceMode` 是 `ServerLoop` 的成员,每个 session 有自己的 loop,两个 session 各自闩各自的模式——「每 session 独立 surface mode」正是这个形状,不需要任何声明。真正想在第一个 surface 之前声明模式的形状(P14d 的 KWin on-screen + Konsole offscreen 同一个 server)必须**让模式上 wire**(Hello 或一条 SurfaceOp),那是 P14d 的账;在那之前,谁先建 surface 谁闩定,是每个 session 各自的答案。**`ServerDisplay` 的 lease 天然多路复用**:holder 是 loop 指针,`AcquireFor` 只会把窗口租给一个 session,第二个 on-screen session 拿到的是**具名拒绝** `NoServerWindow`(日志里点名 LeasedElsewhere),不再是注释里那句「sessions are sequential: a bug if seen」;offscreen session 完全不取 lease,可以任意多。
|
||||||
|
|
||||||
|
**接入层**:`UnixInProcessSupervisor`(`mobilegl_server_serve_inprocess` 的 unix 分支)从「一次一个 + 第二个 Refuse{Busy} one session is already active」改成**并发至多 `MOBILEGL_IPC_INPROC_MAX_SESSIONS`(默认 16)**,每个 session 一条控制线程,超限时用 fork 路径同一句话 `Refuse{Busy} the session limit is reached`;embedded ABI 的签名不变。**没动的三处**:`--serve --max-sessions` 的 fork worker 路径(进程隔离仍是可选形态,`UnixSupervisor.ConcurrentSharedMemoryClientsHaveSeparateWorkersAndAnExitedSlotIsReused` 与 `...RequiresAnExplicitLimit` 照旧)、TCP 的 in-process 形状(仍是一 session 一次,`InProcessServer.AnAuthenticatedSecondHelloWhileASessionIsLiveIsRefusedBusy` 照旧)、`TcpSupervisor` 的 fork 形状。与本文 §4 第 1 条写的「默认 8」差一个数:本切片取 16,且上限是环境变量而非新 ABI 参数。
|
||||||
|
|
||||||
|
**验收证据**(本切片新增):`MG_Test/Wire/MultiSessionTest.cpp` 三个用例——`MultiSessionFixture.TwoShmSessionsInOneProcessAreServedAtOnce`(同进程两个 SHM session 并发:两个 Welcome 同 pid、日志里两条 `in-process unix session #N started`、各自 clear 各自读回自己的颜色)、`MultiSessionFixture.SameNamesInTwoSessionsDoNotCollide`(同一 client context token 与同一 render state handle 在两个 session 里都被接受——名字是 session 内的名字)、`MultiSessionFixture.ClosingOneSessionLeavesTheOtherServing`(A 关掉并 reap 之后 B 继续 apply 三条记录,进程级 resolver/回调表没被 A 卸掉)。既有套件不回归:本切片点名的 `InProcessServerTest`、`ServerDisplayTest`、`UnixSupervisor`、`SupervisorChildren`、`ServerLoopTest`/`ServerLoopEglTest`/`ServerLoopLatchTest`、`EmbeddedServerTest`、`PeerLatchTest`(含 `PeerLatch.SiteMap`,其 FatalFunnel 的 check 正则已跟着 domain 改写更新)、`ServerOwnedSurfaceTest`、`SessionHandshakeTest`、`SurfaceControlFrameTest` 全绿(host ctest 里剩下的失败只有 `3rdparty/glslang` 那个 dirty submodule 造成的编译/链接期基线失败,与本切片无关)。
|
||||||
|
|
||||||
|
**修正(`HealthySessionWithColor` 的颜色解包)**:本切片把 `P12ServerRig.h` 的绿色 clear 参数化时,一度把 `rgba` 按 `A<<24|B<<16|G<<8|R` 的逆序解包(把最高字节当 R),于是 `0xFF00FF00`(绿)被清成 `0x00FF00FF`(品红),`InProcessServer.SequentialSessions...` 的读回断言因此挂掉。正确的解包就是读回自己的字节序——`PeerReport::pixel` 是 GL_RGBA/GL_UNSIGNED_BYTE 三元组的 little-endian `Uint32`,故 `R = rgba & 0xFF … A = rgba >> 24`。**生产路径(clear → read_pixels)一个字节没动**:`PeerLatchTest` 用同一绿色的字面量 `{0,1,0,1}` 且断言同一个 `0xFF00FF00`,它的对应用例全程是绿的,就是这条结论的控制。红色 `0xFF0000FF` 在字节反转下是回文,所以当时只有绿色 session 失败——这不是两个 bug。
|
||||||
|
|
||||||
|
**本切片明确没做(下一层的账)**:DirectGLES 的 `g_Display/g_Config/g_Context/g_Surface` 与六个 twin registry 仍是进程级(P14b 的 per-context native tuple),所以两个并发 session 的**原生上下文并不隔离**:一个 session 的 teardown 仍会拆掉进程的 EGL context,本切片的渲染验收因此只在两个 session 都活着时做,关掉一个之后的证据是记录级的(记录照常 apply 并有回包)。`MGPipeServerSetContextLive` / `MGPipeServerSetOwnedWindow` 与 `PipeInputs.cpp` 同属进程级,不在本切片的文件范围内。`MG_Util::PipeStats::Init/Shutdown` 仍是进程级,多 session 下是遥测噪声不是正确性。`SessionFail`(未武装时的真 Fatal)仍会带走整个进程——那是 `--serve` fork 形状存在的理由。
|
||||||
|
|
||||||
## 后端(DirectGLES)
|
## 后端(DirectGLES)
|
||||||
|
|
||||||
1. **native tuple per context**。`g_Display` 保留进程级(native display 一次 initialize 合理);`g_Context`/`g_Surface`/`g_Config`(`DirectGLES.cpp:16183-16186`)改为 per-(session, contextSlot) 表,native context 按 share group 用 `eglCreateContext(share=…)` 建。
|
1. **native tuple per context**。`g_Display` 保留进程级(native display 一次 initialize 合理);`g_Context`/`g_Surface`/`g_Config`(`DirectGLES.cpp:16183-16186`)改为 per-(session, contextSlot) 表,native context 按 share group 用 `eglCreateContext(share=…)` 建。
|
||||||
@@ -48,6 +135,69 @@
|
|||||||
3. **Managers 按 share group / context 分表**。六个 twin registry(`Managers.cpp:6895`、`:11035`、`:12835`、`:13188`、`:16255`、`:16580`)按 share group 实例化;`g_activeTextureUnit`/`g_boundTexturesCache`/sampler cache(`Managers.cpp:11031-11032` 等)按 context 实例化;`DropEveryTwinForEndedServerSession`(`Managers.cpp:425-443`)只清本 session。`g_backendContextGeneration` 改为 per-context 世代。
|
3. **Managers 按 share group / context 分表**。六个 twin registry(`Managers.cpp:6895`、`:11035`、`:12835`、`:13188`、`:16255`、`:16580`)按 share group 实例化;`g_activeTextureUnit`/`g_boundTexturesCache`/sampler cache(`Managers.cpp:11031-11032` 等)按 context 实例化;`DropEveryTwinForEndedServerSession`(`Managers.cpp:425-443`)只清本 session。`g_backendContextGeneration` 改为 per-context 世代。
|
||||||
4. **DirectVulkan 同步审视**。Magma 的 per-device/per-queue 结构也要按 session 分;本阶段先保证不编译退化,多 session 下 Magma 的验收单列。
|
4. **DirectVulkan 同步审视**。Magma 的 per-device/per-queue 结构也要按 session 分;本阶段先保证不编译退化,多 session 下 Magma 的验收单列。
|
||||||
|
|
||||||
|
### S4 已落地形状(每 context 原生 tuple + share-group 键控)
|
||||||
|
|
||||||
|
上面第 1、2 条是 P14b 的目标形状;S4 落地了「native tuple 按 context 归属、创建第二个 context/surface 不再拆 context、share group 进原生」这一层,实际形状如下。
|
||||||
|
|
||||||
|
**native tuple 按 (session, token) 键控**(`MG_Backend/DirectGLES/DirectGLES.cpp` 的 `NativeContextTuple` / `NativeSessionState` 注册表)。`g_Display` 仍是进程级一份、`eglInitialize` 一次,只有最后一个原生 context 走了才 `eglTerminate`(`g_displayUsers` 计数)。`EGLContext`/`EGLConfig` 随 tuple;**原生 surface 按 session 一份**(EGL 里 surface 不属于 context,只有 draw/read 绑定属于),tuple 记自己当前画到哪个 surface。键由 server 层装的探针回答(`SetNativeContextKeyResolver`,`MG_Remote/Server/SessionRuntime.cpp` 经 `ThreadSessionScope` 装一次):答案是**本线程的 session 身份 + `ServerSession::CurrentContextToken()`**;没有探针的线程(monolith、纯前端进程、unit case、inproc 单进程形状)答案恒为 `{0,0}`——这正是被替换掉的进程单例,所以单 context 世界逐字不变。
|
||||||
|
|
||||||
|
**创建不再拆 context**。`InitDisplayAndContext()`(首句无条件 `DestroyEGLContext()`)换成 `EnsureNativeContext(tuple, surfaceBit, window)`:tuple 已有原生 context 就复用,只按需要选 config;没有才建。`BackendObject_DirectGLES::CreateEGL{Window,Pbuffer}Surface` 里的 `DestroyEGLContext()+ResetEGLRuntimeState()` 同样去掉 teardown,只留 base class 的 per-surface bookkeeping。因此 pbuffer→window→pbuffer 只是往 session 的 surface 集合里加/换,tuple 的 `EGLContext` 一字不动——对象(后端 twin 直接活在原生 context 里)自然不丢。`DestroyEGLContext()` 现在是**本 session** 的全量 teardown(`ReleaseEGLResources`/`~BackendObject_DirectGLES` 的语义),单个 context 的销毁是新的 `DestroyNativeContextFor(token)`。
|
||||||
|
|
||||||
|
**share group 进原生**(`CreateNativeContextFor(token, shareGroup)`)。`CreateContext` 控制帧到达时,server 为该 token 建原生 context:**同 session 内、同一 shareGroupToken 且已有活 context 的 tuple 的 `EGLContext` 作为 `eglCreateContext` 的 share 参数**,否则 `EGL_NO_CONTEXT`。组内后续 context 因此共享原生对象命名空间。建失败则回滚该 session 表项并把 `ok=false` 回给客户端。
|
||||||
|
|
||||||
|
**bind_context 是原生切换点**。MakeCurrent 控制帧到的时刻 `CurrentContextToken()` 还是旧值(S1 刻意让客户端在 make-current 成功后才发绑定记录),所以在那里切原生会绑到刚离开的 context。`ServerVerbSink::OnBindContext`(`MG_Remote/Server/PipeApplier.cpp`)在 `session->BindContext(token)` 之后调 `DirectGLES::MakeNativeContextCurrentForBoundToken()`:按新 token 解析 tuple,与 `t_boundNativeContext` 相同就返回(单 context 的每次绑定零成本),不同才做原生 `eglMakeCurrent`。
|
||||||
|
|
||||||
|
**验收证据**(本次新增,`MG_Test/Wire/ServerLoopTest.cpp`,真实 headless EGL):`ServerLoopEglTest.CreatingASecondContextLeavesTheFirstNativeContextAlive`(同一 session 三个 context token,三个原生 EGLContext 同时活着且互不相同——还原 S4 前的无条件 teardown 时只剩一个);`ServerLoopEglTest.CreatingAnotherSurfaceKeepsTheSameNativeContext`(建第二个 pbuffer 后 tuple 的 `EGLContext` 句柄不变、原生 context 数仍为 1)。既有集合零回归:本切片点名的 97 个用例全绿(86 基线 + 11 `ServerLoopEglTest` + 上面两个)。
|
||||||
|
|
||||||
|
**本切片明确没做(下一层的账)**:
|
||||||
|
|
||||||
|
- **`g_applier` 没有改成 {shareGroup → applier}**(上面 server 第 3 条)。原因不是时间,是**形状**:`MGPipeApplierState` 把「share group 的对象记录」和「每 context 的 working state」混在一个结构里(`PipeApply.h:515-933`,其自身注释第 101 行已指出该二分),照字面把它按 share group 键控会让**同一 share group 的两个 context 共用 working state**——bindings、current program、unit 集互相覆盖,比不改更错。正确的做法是先按该二分拆结构(对象表进 share group、working state 进 context),那是独立的一刀。今天 `g_applier` 与 `gMGPipeApplierReset` 逐字未动。**(S5 已做,见下节。)**
|
||||||
|
- **六个 twin registry 与 twin 键 `{shareGroup, slot, gen}` 未改**(`Managers.cpp`)。twin 仍按引用活着的后端 context 复用,多 context 下的正确性依赖「一个原生 context 一个 twin 世代」,这在两个 context 都活着时成立(本次测到的),在跨 context 迁移 twin 的路径上还没有证据。**(S5 仍未做,见 S5 的未做项。)**
|
||||||
|
- **share group 的驱动级证明未落地**。`eglCreateContext(share=…)` 已接线,但用 headless EGL 直接问 `glIsBuffer` 的探针在本机没有得到可用的控制(同 context 内 `glGenBuffers` 后 `glIsBuffer` 即返回 false),没有作为「跑了个空的绿」收进来。要证到驱动层,需要先弄清该探针,再补一个用例。**(S5 也没弄清:见 S5 的未做项与「为什么没做」。)**
|
||||||
|
- **Wire 第 1 条的逐记录 `ContextSlot` 未做**,仍是 S1 的边沿宣告 + `bind_context`。
|
||||||
|
- **`g_backendContextGeneration`/`g_syncContextGeneration` 仍是进程级**:销毁单个 context 会推进全局世代,其他 context 的旧 fence/query 句柄因此读作「已 signal」。安全方向(最坏是多等),但不是每 context 世代,属 P14b/c。
|
||||||
|
- **ServerLoop 的 MakeCurrent 去重仍是「当前绑定的那一个 tuple」**,不是 per-context 表。理由写在 `ServerLoop.h:560-573`:ID-67 的钉死用例(A→B→A 必须三次原生绑定)定义「不同 tuple」为**当前绑定**的变化,而 EGL 里一个线程同时只有一个 current context,回到 A 确实必须重跑那七条前端 context 失效。「每 context 持有自己的 native triple」由后端的 {session, token} 注册表满足,去重这一层的单位仍是线程的当前绑定。
|
||||||
|
|
||||||
|
### S5 已落地形状(applier 按 {session, context} / {session, shareGroup} 键控)
|
||||||
|
|
||||||
|
S4 留下的最后一块:`PipeApply.cpp` 的进程级 `g_applier` 现在是一个**每 (session, context) 的注册表**,它的对象记录按 {session, share group} 分桶。
|
||||||
|
|
||||||
|
**结构拆分(`MG_Pipe/PipeApply.h`)**。原来一个 `MGPipeApplierState` 按两个寿命切成两个结构:
|
||||||
|
|
||||||
|
| `MGPipeObjectRecords`(share group) | `MGPipeWorkingState`(context) |
|
||||||
|
|---|---|
|
||||||
|
| `Resources`、`VertexElementsCsos`、`TextureResources`、`RenderbufferResources`、`SamplerCsos`、`SamplerViewCsos`、`ShaderCsos`、`CompositeShaderCsos`、`FramebufferRecords` | `RenderStateCsos`、`BoundRenderStateCso`、`Residual`、`ScatteredChunkBits`、四个 wire 计数器与四个 refusal 计数器、`BoundVertexElements`/`VertexBuffers*`/`IndexBuffer*`、`MapPersistentRoundtrips`、`BoundFramebuffer`/`FramebufferSerial`/`StaleFramebufferRecordLookups`、三个 unit 窗口、三个 program handle、三个 binding-point 窗口、`IsTransformFeedbackActive`、`TextureShutterSerial`/`ContextSerial`、`#if DISAGGREGATED` 的 verb handle 组 |
|
||||||
|
|
||||||
|
`RenderStateCsos` 在 working 一侧不是笔误:它今天是**被 `MGPipeApplierReset` 清掉的**(每个 make-current 一份新 CSO 表),照旧规则留在 per-context 那半。`FramebufferRecords` 留在对象一侧也是照旧规则(reset 不清它),且它的槽来自全局分配器,所以一个 share group 装两个 context 的记录是「更宽的表」而不是别名。
|
||||||
|
|
||||||
|
`MGPipeApplierState` 现在**继承** `MGPipeWorkingState` 并持有一个 `MGPipeObjectRecords& Objects`,另把九张对象表**按原名字绑成引用成员**(`Resources`、`TextureResources`…)。理由写在头里:MG_Backend / MG_IntegrationTest / MG_Test 里 ~200 个调用点一直把 `st.Resources` 读作「这个对象的 share group 记录」,绑引用让这些读点**一个不改**且按各自 context 所属的 group 正确解析;把键控改动变成改名改动只增风险不增正确性。头文件里可以同时用 `Objects.X`(在**创建**两半的地方显式点出群体)与 `X`(后端一直的拼法),两者是同一张表。
|
||||||
|
|
||||||
|
**注册表与解析链(`PipeApply.cpp`)**。三层,和 S2 的 `gPipeInputs`、S4 的 native tuple key 同构:
|
||||||
|
|
||||||
|
- `MGPipeApplierKey{ SessionKey, ContextToken }`,由 `MGPipeSetApplierKeyResolver` 装的探针回答;`SessionRuntime.cpp` 在 `InstallSessionRuntimeHooks` 里装(`ThreadSessionScope` 一开就装)。探针答案是**本线程 session 的 `ServerSession*` 地址 + `CurrentContextToken()`**;不是 session 线程的答案 false,拿到进程级 applier —— 这正是被替换掉的单例,所以 monolith / 纯客户端 / inproc 单进程形状 / 单元用例逐字不变。
|
||||||
|
- `MGPipeApplierRegisterContext(session, token, groupToken)` / `MGPipeApplierUnregisterContext` / `MGPipeApplierReleaseSession`。**注册而不是推断**:`ServerSession::CreateContext` / `DestroyContext` 在 apply 线程上调用它们,所以解析探针永远是「读本 session 已经在维护的当前 token」两字,不必从别的线程去读 session 的 map;`Accept` 与 `~SessionRuntime` 释放整 session。
|
||||||
|
- **group 键**:`shareGroupToken != 0 ? shareGroupToken : (contextToken | 高位哨兵)`。前端铸的 share group token 与 context token 来自两个计数器,不隔离就会让 group 7 和 context 7 变成一组;`0` 是 S1 注释里「本 context 自己起了一个组」的哨兵(也是 `ServerCreateEGLContext(token, 0, 0)` 这类手工建 context 传的值),落成「自己的组」而不是「和别的 0 一组」。
|
||||||
|
- **最后一个 context 走才释放该组的对象记录**(`MGPipeApplierReleaseObjectRecords` 的 per-group 版);一组的另一个 context 还在时记录一条不丢。
|
||||||
|
- **线程局部缓存 + 世代号**。`MGPipeApplier()` 是后端按状态读调的,所以快路径是「本线程上次的 (session, token) 与注册表世代都对得上就直接返回」;注册表每次变动 `++generation`,缓存指针只有在世代相等时才被解引用 —— 这就是「另一个线程销毁了 context」不会留下悬垂缓存的原因。快路径开销是三个 TLS 读、一次原子读、两次比较;探针为 null(monolith)时是一次函数指针比较。
|
||||||
|
|
||||||
|
**前端/会话接入**:`EGLImpl::MakeCurrent` 的 `EmitContextBinding` 一字未动 —— S1 的边沿宣告本来就是「这条 ring 往后归谁」的全部信息,S5 只是第一次**读**它。
|
||||||
|
|
||||||
|
**验收证据**(本次新增):
|
||||||
|
|
||||||
|
- `MG_Test/Wire/ApplierOwnershipTest.cpp`,5 个用例,直接驱动注册表与解析(无 EGL、无 wire):两个 nonshared context 同名 handle 是两条记录且互不覆盖(reset 也不丢);同一 share group 的两个 context **共用对象表而各持 working state**(当前 program、vertex-buffer 窗口不互见,而一张表上的 create 两边可见);两个 session 用同名 group/context token 不共享任何东西;组的记录活到最后一个 context 才释放、重建的 token 不继承死组;无 session 的线程仍拿到进程级 applier。
|
||||||
|
**负控**(必做,否则是空绿):把测试装的解析探针改成 null(即回到 S5 前的「谁都拿进程级 applier」),5 个用例**全红**;换回即全绿。
|
||||||
|
- `MultiSessionTest.TwoSessionsInterleaveTwoContextsEachWithoutCrossTalk`:真实 `--serve` 前身(in-process unix supervisor)上两个 session,各自两个 context(同名 token 与同名 group token),**8 轮交错**:每轮先各发一条 `bind_context` 切 context,再跑一整趟 clear + 真读回。绿/红两种颜色互为证人;日志里 0 条 `SessionLatch{`。这条用例同时压的是**生产**解析链(`ThreadSessionScope` 装的探针)与并发(两个 apply 线程同时跑)。
|
||||||
|
- 既有集合零回归:本切片点名的 97 个用例与 `MultiSessionFixture` 原有的 4 个全绿(含 `ServerLoopEglTest` 的 11 个、`ContextStateOwnershipTest` 6 个)。`SanityTest` 的两个用例因 `MGPipeApplierState` 不再可拷贝改为保存/恢复它真正改动的 working 字段。
|
||||||
|
|
||||||
|
**S5 明确没做(下一层的账)**:
|
||||||
|
|
||||||
|
- **六个 twin registry 与 twin 键 `{shareGroup, slot, gen}` 仍未改**(`Managers.cpp`)。后果是 S5 **不能**声称「两个 nonshared context 的同名 handle 在驱动层是两个对象」:applier 的两条记录是对的(已有测试),但 twin 表按 handle 键,`Ensure(handle)` 会给两个 context 的同一个 handle 值返回同一个 twin。`ServerLoopTest` 里那条 `glIsBuffer` 探针仍然没有可用控制,所以**驱动级共享/隔离的证据仍是缺的**,本次没有以「跑了个空的绿」收进来。
|
||||||
|
- **wire 第 1 条的逐记录 `ContextSlot`** 仍未做,仍是 S1 的边沿宣告 + `bind_context`。这意味着「同一 ring 上两个 context 的记录交错」只在 client 按 make-current 顺序发射时正确。
|
||||||
|
- **`MGPipeApplierShareGroupCountForTesting`** 是测试观测面,不是生产 API。
|
||||||
|
- **`MGPipeApplierState` 的九张表名与 `Objects.X` 两种拼法并存**,是刻意换取 ~200 个读点不动的代价;改名的机会在 twin registry 一起做的那一刀。
|
||||||
|
- **`g_backendContextGeneration`/`g_syncContextGeneration`、ServerLoop 的 MakeCurrent 去重**同 S4 一节所列,仍未动。
|
||||||
|
|
||||||
|
|
||||||
## 分期与验收
|
## 分期与验收
|
||||||
|
|
||||||
| 期 | 内容 | 验收 |
|
| 期 | 内容 | 验收 |
|
||||||
|
|||||||
@@ -117,8 +117,11 @@ MECHANICS = [
|
|||||||
(REMOTE / "Server" / "PipeApplier.cpp", "ApplyOne: the pre-gate runs before the verb stamp and the barrier read",
|
(REMOTE / "Server" / "PipeApplier.cpp", "ApplyOne: the pre-gate runs before the verb stamp and the barrier read",
|
||||||
r"if \(!m_decoder\.AdmitOrDecline\(record\)\) return false;\s*const Bool wireSaysBarriered",
|
r"if \(!m_decoder\.AdmitOrDecline\(record\)\) return false;\s*const Bool wireSaysBarriered",
|
||||||
[("ServerLoopTest.cpp", "ARecordShorterThanItsTypeIsLatchedBeforeTheApplierStampsIt")]),
|
[("ServerLoopTest.cpp", "ARecordShorterThanItsTypeIsLatchedBeforeTheApplierStampsIt")]),
|
||||||
|
# P14 S2: the latch is the CALLING THREAD'S DOMAIN now (FatalFunnel.h), so the unarmed arm
|
||||||
|
# reads the domain's own word instead of the process's - the mechanism and every case below
|
||||||
|
# are unchanged, and a session's domain being unarmed is still SessionFail byte for byte.
|
||||||
(REMOTE / "FatalFunnel.cpp", "SessionLatch: first fault wins, every fault counted, one SessionFault; unarmed it dies",
|
(REMOTE / "FatalFunnel.cpp", "SessionLatch: first fault wins, every fault counted, one SessionFault; unarmed it dies",
|
||||||
r"if \(!g_latchArmed\.load\(std::memory_order_acquire\)\) \{\s*SessionFail\(",
|
r"if \(!latch\.armed\.load\(std::memory_order_acquire\)\) \{\s*SessionFail\(",
|
||||||
[("ServerLoopTest.cpp", "AnArmedSessionLatchKeepsTheFirstFaultCountsEveryOneAndPublishesOnce"),
|
[("ServerLoopTest.cpp", "AnArmedSessionLatchKeepsTheFirstFaultCountsEveryOneAndPublishesOnce"),
|
||||||
("ServerLoopTest.cpp", "AnUnarmedSessionLatchDiesWithItsLineLikeSessionFail")]),
|
("ServerLoopTest.cpp", "AnUnarmedSessionLatchDiesWithItsLineLikeSessionFail")]),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"2": "b30928e6fc93ddc015fd86f634a6820f775a6f2f6de5a848d273981e706b2597",
|
"2": "b30928e6fc93ddc015fd86f634a6820f775a6f2f6de5a848d273981e706b2597",
|
||||||
"3": "da09f0acb48cf245703f28556ef5a38bc9e25241ebbf7b6baa9ab1765b7ddd50",
|
"3": "da09f0acb48cf245703f28556ef5a38bc9e25241ebbf7b6baa9ab1765b7ddd50",
|
||||||
"4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f",
|
"4": "6357c3a07ae258d2825dc42bfd610712b15a38a3beb7a96b5f56ea2988705e7f",
|
||||||
"5": "4632750fd68b889cb6df709df9d150f9910468c0c530399de01bef87ba793904"
|
"5": "4632750fd68b889cb6df709df9d150f9910468c0c530399de01bef87ba793904",
|
||||||
|
"6": "fd76c08349a399edb75a6b36753347d19b79243d7bacdd61323f2921db56dad3"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user