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

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

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

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

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

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

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

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

Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
This commit is contained in:
2026-10-01 19:16:41 -04:00
parent ebc552a048
commit 4ed1c9cef2
80 changed files with 4677 additions and 949 deletions
+47
View File
@@ -1462,6 +1462,53 @@ namespace MobileGL::MG_Remote::Server {
return true;
}
// P14 S1. THE RING'S DOOR INTO THE SESSION'S CONTEXT TABLE.
//
// The record carries the client's current context token and this is where a session's
// attribution moves. It is deliberately NOT an assertion like OnApplierReset's serial: the
// client's EGL layer mints the token and the server has no second count to compare against,
// so what is checked is a FACT ABOUT THIS SESSION'S OWN TABLE - a token that CreateContext
// never created is corruption, not a late record. Token 0 is the release edge and is always
// legal (a session whose client released its context has no context, which is exactly the
// value a session that never bound one already answers).
Bool ServerVerbSink::OnBindContext(const MG_Pipe::MGPBindContext& bind) {
// NO SESSION IS A DECLINE, NOT A FAULT, and it is the one case here that a peer's bytes
// cannot cause: a bind is answered by the session's own table, so with no accepted
// session there is nothing to attribute the record to and nothing to refuse. (The window
// is Close() clearing the active session while the apply thread is still draining.)
ServerSession* session = ServerSession::Active();
if (session == nullptr) return false;
// PH-1 (3): the refusal latches in an armed session child (the peer wrote the token);
// unarmed it dies, like every other contract fact about a peer.
if (!session->BindContext(bind.ClientContextToken)) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the "
"record binds token %llu, which this session never created; a bind_context "
"names a context CreateContext put in the session's table, and 0 (the "
"release) is the only token that needs no entry",
static_cast<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) {
// §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,