diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 93dfeabf..efc13c73 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -414,4 +414,103 @@ namespace MobileGL::MG_Config { String PipeStatsFile; }; extern FeaturesTable Features; + + // --------------------------------------------------------------------------------- + // P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A) + // --------------------------------------------------------------------------------- + // + // MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix: | pipe:. + // + // WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with + // MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook + // in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can + // take. A FeaturesTable member is a runtime field in every build, which is the opposite + // of that; it would also resize MG_Config::Features and break G1 (the pull build's + // symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit + // behind their own #if. + // + // ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option, + // MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the + // parser below does not exist to complain about it, and putting a complaint in the + // unconditional part of ConfigLoader would move a pull-build symbol. That is the exact + // shape of "the split lane ran monolith and went green", so the gate against it is a + // BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i + // MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job + // P5 adds beside build-linux-verify). + enum class TransportMode : Uint8 { + Monolith = 0, // today's in-library backend; no MG_Remote object is constructed + InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec + Spawn = 2, // P6: fork/exec MobileGLServer, socketpair + UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = ) + NamedPipe = 4, // P6: Windows named pipe (Endpoint = ) + }; + +#if MOBILEGL_BUILD_DISAGGREGATED + // Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the + // transport in is not the same as using it, and every existing lane of a build-split + // must keep running monolith unless it is asked for one. + extern TransportMode Transport; + // The of `unix:` / the of `pipe:`. Empty for the other three modes. + extern String TransportEndpoint; + + // The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members, + // for the G1 reason above and because every field here is meaningless without the + // transport: a build that cannot reach the MG_Remote code cannot honour one of them. + // + // P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE, + // through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds + // the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM, + // INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs + // to P6 or later. + struct IpcTable { + // MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5 + // lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's + // SPLIT variant already carry it, and an environment variable that nothing parses is + // indistinguishable from one that is parsed and ignored. + String ServerPath; + // MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS + // (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the + // codec publish a max-record-bytes counter rather than assume that is enough. + Uint32 RingMb = 8; + // MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live + // here (R-10: no chunking in P5, so nothing may exceed it). + Uint32 StageMb = 32; + // MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction. + Uint32 SpinUs = 50; + // MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push. + // 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and + // PersistentCoherentMapScenario must go RED under it (exit gate E3(a)). + Uint32 PersistentBlockKb = 64; + // MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which + // is the only tier P5 implements and the reason persistent-map-push can be non-zero + // at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the + // negative control has a spelling the day P11 writes it. + Uint32 AdoptTier = 2; + // MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until + // appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to + // be red, because 31 of the 63 PipeInputs fields are still pulled from a live + // GLContext by the client's residual fill and a free-running queue lets the server + // read a FUTURE value of them. + Uint32 VerbBarrier = 1; + // MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split + // build, the seven sticky forwards that are otherwise exempt - from "count it in + // rsp" to Fatal (R-7.3). + Bool StrictErrors = false; + // MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes + // it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an + // inproc implementation did not quietly keep using a pointer past its lifetime. + Bool Audit = false; + // MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed + // from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string + // because the resolved mask is logged by whoever starts the apply thread, and the + // string is what an operator typed. + String ServerAffinity = "auto"; + }; + extern IpcTable Ipc; +#else + // The whole point: in a build without MG_Remote this folds at compile time, so + // `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a + // discarded statement and the pull build gains no symbol, no branch and no byte. + inline constexpr TransportMode Transport = TransportMode::Monolith; +#endif } // namespace MobileGL::MG_Config diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 7e542e75..2ea22f02 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -25,6 +25,15 @@ namespace MobileGL::MG_Config { // Zero/default-initialized at static-init time (all fields have constexpr-friendly // defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet. FeaturesTable Features; +#if MOBILEGL_BUILD_DISAGGREGATED + // Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a + // build order that put it before MG_ConfigLoader::Init() must see Monolith rather than + // a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr + // and there is nothing to define. + TransportMode Transport = TransportMode::Monolith; + String TransportEndpoint; + IpcTable Ipc; +#endif } // namespace MobileGL::MG_Config namespace MobileGL::MG_ConfigLoader { @@ -297,12 +306,106 @@ namespace MobileGL::MG_ConfigLoader { #undef ENTRY } +#if MOBILEGL_BUILD_DISAGGREGATED + // MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix: | pipe: + // (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then + // one fallback that names what it did instead. The two prefixed forms are the only + // reason this is not literally that function's ENTRY macro. + // + // spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED + // rather than silent, because the failure this avoids is a P6 lane that set + // MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm. + // The mode is left at Monolith so nothing half-initializes. + inline void InitTransport() { + String value; + QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith"); + String lowered = value; + std::transform(lowered.begin(), lowered.end(), lowered.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + MG_Config::TransportEndpoint.clear(); + if (lowered.empty() || lowered == "monolith") { + MG_Config::Transport = MG_Config::TransportMode::Monolith; + return; + } + if (lowered == "inproc") { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real " + "ring to an apply thread"); + return; + } + // The three P6 forms. Recognised precisely, so the diagnostic can say "not yet" + // rather than "unknown", which are different bugs on the operator's side. + if (lowered == "spawn" || lowered.compare(0, 5, "unix:") == 0 || + lowered.compare(0, 5, "pipe:") == 0) { + MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 implements and P5 does " + "not; staying on monolith. This run is NOT a split run.", + value.c_str()); + MG_Config::Transport = MG_Config::TransportMode::Monolith; + return; + } + MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected " + "monolith|inproc|spawn|unix:|pipe:, using monolith", + value.c_str()); + MG_Config::Transport = MG_Config::TransportMode::Monolith; + } + + // The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only + // when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is + // how an A/B stops being one, and the ranges below are the diagnostics. + inline void InitIpc() { + auto& ipc = MG_Config::Ipc; + QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, ""); + // Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and + // the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a + // create_shader_state archive already needs far more than a toy ring. The ceilings + // are sanity, not policy. + ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024); + ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096); + ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000); + // 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns + // the persistent-map push OFF, and PersistentCoherentMapScenario must go red. + ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536); + // 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the + // point of use, which is where the "P11" in the message belongs. + ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2); + ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1); + ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS"); + ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT"); + QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto"); + + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; + // One line, on the arm where these numbers decide behaviour, because every one of + // them is a number a bug report has to quote. + MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB spin=%uus persistent-block=%uKiB " + "adopt-tier=%u verb-barrier=%u strict=%d audit=%d affinity='%s'", + ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier, + ipc.VerbBarrier, static_cast(ipc.StrictErrors), static_cast(ipc.Audit), + ipc.ServerAffinity.c_str()); + if (ipc.VerbBarrier == 0) { + MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is " + "expected to fail: the client still pulls 31 of 63 PipeInputs fields from a " + "live GLContext, so an unbarriered queue lets the server read future values"); + } + if (ipc.PersistentBlockKb == 0) { + MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: " + "the persistent-map push is OFF and a coherent-map scenario must go red"); + } + } +#endif + void Init() { MGLOG_D("Loading configuration from environment variables..."); InitializeAcceptedEnvVariables(); InitBackendType(); InitFeatures(); +#if MOBILEGL_BUILD_DISAGGREGATED + // After InitFeatures, so the one line InitIpc logs is the last word on this run's + // configuration, and before the accepted-env map is destroyed just below. + InitTransport(); + InitIpc(); +#endif // Destroy the map since we won't need it anymore acceptedEnvVariablesMap.reset();