mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
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.
156 lines
9.8 KiB
C++
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
|