From d623118485b8d464d941367ba04559839a5400cc Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 05:45:15 -0400 Subject: [PATCH] [Feat] (MG_Backend, MobileGL/Init): the single split hook in MG_Backend::Init - server backend, CallMask halves, ClientSession::Start, remote object - and ShutdownSplitRoles at the top of Destroy --- MobileGL/Init.cpp | 5 + MobileGL/MG_Backend/BackendObjects.h | 23 ++++ MobileGL/MG_Backend/Init.cpp | 161 +++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index 48028625..dde56bdd 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -43,6 +43,11 @@ namespace MobileGL { if (logLifecycle) { MGLOG_I("MobileGL closing..."); } + // P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole + // of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op + // in a monolith build and in a monolith run; see BackendObjects.h for why the + // position rather than the call is the load-bearing part. + MG_Backend::ShutdownSplitRoles(); // Before any subsystem the counters name goes away, and before the last frame's // numbers can be lost: emits the final summary line and, when // MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are diff --git a/MobileGL/MG_Backend/BackendObjects.h b/MobileGL/MG_Backend/BackendObjects.h index 1a148824..fcff0086 100644 --- a/MobileGL/MG_Backend/BackendObjects.h +++ b/MobileGL/MG_Backend/BackendObjects.h @@ -15,4 +15,27 @@ namespace MobileGL::MG_Backend { extern UniquePtr& pActiveBackendObject; extern GlobalBackendFunctionsTable gBackendFunctionsTable; + + // P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every build and + // every run that is not split - which is why MobileGL/Init.cpp can call it unconditionally + // without a second #if in a file no package owns. + // + // IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is + // publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close + // the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the + // sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has + // to happen at its top. Two consequences are load-bearing rather than tidy: + // + // * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing + // a counter while the final line is produced; + // * the apply thread is joined before pActiveBackendObject.reset(), so the server's own + // BackendObject - which is NOT that global (table 3) - is destroyed on the thread that + // owns the context, by ServerLoop::Stop, rather than on the app thread. + // + // The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67 + // BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after + // MobileGL::Destroy(); the two are only reconcilable if a split sync handle is + // client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a + // KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order. + void ShutdownSplitRoles(); } // namespace MobileGL::MG_Backend diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index fec43324..6e09bbe9 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -11,6 +11,26 @@ #include #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#include +#include +#include +#endif + +#if MOBILEGL_BUILD_DISAGGREGATED +namespace MobileGL::MG_Remote::Client { + // PACKAGE c1's, DECLARED HERE RATHER THAN INCLUDED. The client's BackendObject_Remote is + // c1's file and does not exist while v1 is written, so v1 ships a WEAK definition of this + // beside ServerLoop that aborts by name. c1's strong definition displaces it at link time + // with no edit to this file - and until then the hook cannot silently succeed, which is the + // only property that matters: a split lane that installed a WORKING monolith backend object + // here would render correctly for entirely the wrong reason (ARCHITECTURE.md 10.3). + UniquePtr CreateRemoteBackendObject(); +} // namespace MobileGL::MG_Remote::Client +#endif + namespace MobileGL::MG_Backend { void LogBackendInfo() { if (!pActiveBackendObject) { @@ -45,9 +65,150 @@ namespace MobileGL::MG_Backend { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + namespace { + // WHICH MGPipe SUBSYSTEMS THIS SERVER HAS A CONSUMER FOR - CallMask bits 32..47 (R-8 / + // C-4). It is stated from what the server's own backend IS, and NOT derived from + // MGPipeGetResourceOps(): that is a PROCESS-WIDE global, so under inproc a derivation + // would answer with whatever the client half of the same process registered and under + // spawn it would collapse to P2's 0x7f. Either way CapsMirror::ServerConsumes would + // then answer a client-side liveness gate with a guess, the client would stop emitting + // whole record families, clear its dirty flags on acceptance anyway, and the lane would + // go green with the uploads lost - ID-39's 66 lost uploads, reflected (ServerSession.h). + // + // DirectGLES consumes all thirteen migrated families (P2's 0..6, P3a's 7..8, P4a's + // 9..12): it registers the resource op table in Initialize() + // (BackendObject_DirectGLES.cpp:849) and reads every other family out of gPipeInputs. + // DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller + // in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is + // the same fact ObjectSubsystemControlScenario already pins from the client side. + Uint64 ConsumedSubsystemsFor(BackendType type) { + switch (type) { + case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a; + case BackendType::DirectVulkan: + return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a & ~MG_Pipe::kMGPipeSubsystemResources; + default: return 0; + } + } + + // THE CROSS-CHECK THAT MAKES A WRONG ANSWER LOUD. Claiming bit 7 while no resource op + // table is registered is the exact failure the mask exists to prevent, one level down: + // the client would keep emitting the resource family and the server would drop every + // record of it. The check runs AFTER Initialize(), which is where DirectGLES registers + // the table, so it can see the real answer rather than a promise. + void AssertConsumerMaskIsHonest(Uint64 mask) { + const Bool claimsResources = (mask & MG_Pipe::kMGPipeSubsystemResources) != 0; + const Bool hasResourceOps = MG_Pipe::MGPipeGetResourceOps() != nullptr; + if (claimsResources && !hasResourceOps) { + MGLOG_F("MGPipe: Fatal{ConsumerMaskLie, \"kMGPipeSubsystemResources\"} - the " + "server published a consumer bit for the resource family while " + "MGPipeGetResourceOps() is null. The client's R-8 liveness gate would " + "keep emitting resource_create / resource_subdata records that this " + "server drops on the floor, and the client clears its dirty flags on " + "acceptance anyway (ID-39). A mask is a statement about this backend, " + "not a hope"); + std::abort(); + } + if (!claimsResources && hasResourceOps) { + // The safe direction: the legacy pull path keeps running. Said out loud anyway, + // because it silently costs the whole P3a family its migration. + MGLOG_W("MG_Remote server: a resource op table is registered but the consumer " + "mask withholds kMGPipeSubsystemResources; the buffer family will fall " + "back to the legacy path for this session"); + } + } + + // The single hook (ARCHITECTURE.md:29). Returns false when the split could not be + // brought up, and the caller then REFUSES TO CONTINUE rather than falling back to the + // switch below - a fallback here is "the split lane ran monolith and went green". + Bool InitSplitRoles() { + using namespace MobileGL::MG_Remote; + + // 1. the SERVER role's private backend object, on the app thread, with no GL and no + // EGL. The context is created and made current later, on mgl-srv-apply, when the + // client's first eglMakeCurrent crosses as a blocking control request. + Server::ServerLoop& loop = Server::ServerLoopInstance(); + const MobileGLResult created = loop.CreateBackend(MG_Config::ActiveBackendType); + if (created != MOBILEGL_OK) return false; + + // 2. the two CallMask halves. NEITHER HAS A DEFAULT and CallMask() is a named Fatal + // on an unset one (s1's BLOCKER fix), so this is the "somebody" that block names. + Server::ServerSession& session = Server::ServerSessionInstance(); + const Uint64 consumed = ConsumedSubsystemsFor(MG_Config::ActiveBackendType); + AssertConsumerMaskIsHonest(consumed); + session.SetConsumedSubsystems(consumed); + // ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block): + // every optional capability bit belongs to the package that owns its question, and + // withholding one leaves the legacy path running, which is the safe direction. + // kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5 + // by ruling - they are the only two things that ask for an MGHostSpan, and 0 is + // what keeps every one of them out of the first IPC frame (contract table 0). + session.SetCapabilityBits(0); + session.SetBackend(loop.Backend()); + + // 3. the handshake, the four segments, and - at its end - the apply thread. + const MobileGLResult started = + Client::ClientSessionInstance().Start(MG_Config::Transport, MG_Config::TransportEndpoint); + if (started != MOBILEGL_OK) { + MGLOG_E("MG_Remote: the split session failed to start (rc=%d); MobileGL will not " + "fall back to monolith - a lane named split that ran monolith is the one " + "failure this phase is built to make impossible", + static_cast(started)); + return false; + } + + // 4. and only now the CLIENT's backend object in the one global that holds it. + // Table 3: pActiveBackendObject holds BackendObject_Remote and the server's + // BackendObject_DirectGLES stays private to ServerLoop. + pActiveBackendObject = MG_Remote::Client::CreateRemoteBackendObject(); + if (!pActiveBackendObject) { + MGLOG_E("MG_Remote: CreateRemoteBackendObject returned null"); + return false; + } + return true; + } + } // namespace +#endif + + void ShutdownSplitRoles() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; + // ClientSession::Stop IS table 3's whole order and it is idempotent: publish and wait + // for the server to drain (bounded - a lost record must be a red lane, not a hung + // exit), Doorbell::Kill through the transport's Shutdown, ServerLoop::Stop's bounded + // join - which also destroys the server's private BackendObject ON the apply thread + // while it still owns the context - the transport, and only THEN anything an emitter + // owns. A var-tail still named by an unapplied record is a use-after-free the join is + // what prevents, which is why the order is not a preference. + MG_Remote::Client::ClientSessionInstance().Stop(); +#endif + } + void Init() { MGLOG_D("Initializing MobileGL Backend..."); +#if MOBILEGL_BUILD_DISAGGREGATED + // THE SINGLE HOOK. In a build without MOBILEGL_BUILD_DISAGGREGATED, MG_Config::Transport + // is a `constexpr Monolith` (Config.h) and this whole statement is discarded, so the + // pull build gains no symbol, no branch and no byte - which is what G1 measures. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + if (!InitSplitRoles()) { + // NOT a fallback to the switch. pActiveBackendObject stays null and the next GL + // call fails loudly, which is the only honest outcome: the operator asked for a + // transport this process could not bring up. + pActiveBackendObject = nullptr; + return; + } + Bool remoteResult = InitSpecificBackendLibs(); + if (!remoteResult) { + MGLOG_W("Failed to initialize MobileGL backend libraries for the remote object"); + return; + } + LogBackendInfo(); + return; + } +#endif + switch (MG_Config::ActiveBackendType) { case BackendType::DirectGLES: pActiveBackendObject = MakeUnique();