Files
MobileGL/MobileGL/MG_Remote/FatalFunnel.h
T
swung0x48 4ed1c9cef2 [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.
2026-10-01 19:16:41 -04:00

156 lines
9.8 KiB
C++

// MobileGL - MobileGL/MG_Remote/FatalFunnel.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
// P6 `dl` (CONTRACT-P6 5.2). THE ONE PLACE A SESSION DIES.
//
// Every `Fatal{...}` under MG_Remote/ used to be an MGLOG_F followed by its own std::abort().
// a6 counted ~92 of them. Scattered, they gave the funnel's three benefits to nobody: there was
// no single place to publish a SessionFault to the peer before dying, no single telemetry point,
// and no way for a census to be sure a new death was named. This is that place.
//
// THE MESSAGE STRING IS PASSED VERBATIM, family word and all. The contract requires the log
// lines, the CI greps and every red-once to be byte-identical, so SessionFail does not compose
// the line from the family - it takes the SAME string the site used to hand MGLOG_F and forwards
// it unchanged. The family enum rides ALONGSIDE, redundantly, so the funnel knows the coarse wire
// FatalCode without parsing the string; a test asserts the enum's name appears in the string, so
// the redundancy cannot rot into disagreement.
//
// NOT in WireLog.h: that header is deliberately dependency-light (ITransport.h's rule, the
// include-graph purity gate), and this needs FatalFamily.h, which pulls the protocol header. The
// header-layer sites that must stay pure keep using WireLogFatal, whose own abort routes through
// the same publisher in WireLog.cpp.
#pragma once
#include <MG_Remote/FatalFamily.h>
#include <cstdint>
namespace MobileGL::MG_Remote {
// Logs the line (byte-identical to the old MGLOG_F), echoes it to stderr so a death test can
// see it, publishes a SessionFault{family, ...} to the peer if one is connected, bumps the
// telemetry counter, and aborts. `fmt` is the FULL original message, e.g.
// "MGPipe: Fatal{RingOverrun, \"SEG_CMD\"} - %s ...".
[[noreturn]]
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
#endif
void SessionFail(MGFatalFamily family, const char* fmt, ...);
// How many times a session has ended through the funnel this process. Exit gate S8 asserts
// this is zero over a whole good run; a test reads it to prove the funnel was reached. A
// latched fault (SessionLatch below) counts here too: it ends the session just as surely.
::std::uint64_t SessionFaultCount();
// ---- PH-1 (3), ID-P7-1: THE PER-SESSION LATCH ---------------------------------------
//
// WHAT IT IS. At a site whose input bytes the PEER wrote, and which already sits in a
// function that can say "no" (Bool / MobileGLResult), a named fault no longer has to kill the
// process: SessionLatch logs the SAME line SessionFail would (family word and all, so every
// `Fatal{` grep, the census and the SessionFault frame read exactly as before), publishes the
// SessionFault to the peer, records the FIRST fault (family + line), and RETURNS false. The
// site returns failure, the decoder declines, ServerLoop::DrainRing stops applying at its
// next check, the apply thread leaves its loop, and ServerMain::RunSession closes the session
// cleanly and exits kSessionLatchedExitCode - a named, counted end the supervisor reaps and
// then serves the next connection, exactly as P6.5's fork-per-session already does for a
// 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:
// ServerMain::RunSession, i.e. a served session - the spawn / TCP session child (with or
// 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:
// * 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
// server share one process: a latched-but-alive server would leave the client's GL thread
// parked on a barrier in the SAME process with no EOF to wake it, and there is no
// supervisor to serve a "next connection" - the process is the session. So the latch
// changes nothing inproc and every death test stays a death test.
// * THE CLIENT never arms it: a client-side arm of a shared helper (the encoder's honesty
// pass) is a local bug, not a peer's bytes.
// The census (scripts/ci/fatal_census.py rule 2) checks SessionLatch calls the way it checks
// SessionFail calls: the string must carry its `Fatal{Word` and the word must have a .def row.
void ArmSessionLatch();
bool SessionLatchArmed();
// P12 (D5): THE IN-PROCESS DISPLAY SERVER RUNS SESSIONS ONE AFTER ANOTHER IN ONE PROCESS, so
// the latch that a session child took to its _exit has to be put back for the next session:
// disarmed, unlatched, the count and the first fault's line cleared. Called between sessions
// (the in-process supervisor, after it has joined the session thread) and by nothing else; the
// next RunSession arms it again. SessionFaultCount() is the process's telemetry and is kept.
// 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
// 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();
// True once the first fault has latched. One acquire load: DrainRing asks it before every pop.
bool SessionLatched();
// The first latched fault - its family and its line, verbatim. Meaningful only once
// SessionLatched() is true; the line's storage lives for the process.
MGFatalFamily SessionLatchedFamily();
const char* SessionLatchedLine();
// Every fault this armed process latched or declined after the first (>= 1 once latched).
::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
// 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.
inline constexpr int kSessionLatchedExitCode = 75;
// LATCH-OR-DIE. Armed: log + stderr echo + SessionFault (first fault only) + count, then
// return false, so a Bool site reads `return SessionLatch(...);`. Unarmed: SessionFail with
// the identical line (does not return).
bool
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
#endif
SessionLatch(MGFatalFamily family, const char* fmt, ...);
// P7 wave 0. Points MG_Pipe's MGPipeSessionFail seam (MG_Pipe/PipeSessionFail.h) at
// SessionFail, which is what makes the three Magma wire funnels in MG_Backend's renderer
// publish a SessionFault, bump SessionFaultCount() and appear in the census like every other
// death - without MG_Backend naming a single MG_Remote symbol.
//
// Called from MG_Backend/Init.cpp's InitServerRoleCommon, which BOTH the inproc server role
// and the spawn/TCP session child run, so there is one install point rather than one per
// transport. Installing it in the client process would be wrong and is not done: the hook's
// families are the SERVER's renderer refusing a wire verb, and a client that installed it
// would publish a SessionFault for a death the server never had.
void InstallPipeSessionFailHook();
} // namespace MobileGL::MG_Remote