[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
@@ -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