// MobileGL - MobileGL/Init.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 #include "Init.h" #include "Config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #if MOBILEGL_PIPE_VERIFY #include // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl #endif #include #if MOBILEGL_BUILD_DISAGGREGATED #include #endif #include #include namespace MobileGL { namespace { std::atomic g_isInitialized = false; thread_local Bool tl_initializing = false; std::mutex& InitMutex() { static std::mutex mutex; return mutex; } void DestroyImpl(Bool logLifecycle) { if (!g_isInitialized) { return; } if (logLifecycle) { MGLOG_I("MobileGL closing..."); } #if MOBILEGL_BUILD_DISAGGREGATED // 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 RUN; absent from a monolith BUILD, because G1 admits no new pull // symbol and no resized one (the first version called it unconditionally and moved // DestroyImpl by 32 bytes). See BackendObjects.h for why the position rather than // the call is the load-bearing part. MG_Backend::ShutdownSplitRoles(); #endif // 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 // off, and idempotent. MG_Util::PipeStats::Shutdown(); // First, before anything else is torn down. In-flight compile/link jobs own // their own inputs and are safe against everything below EXCEPT glslang's // process globals and the TShader/TProgram objects hanging off pGLContext, // both of which this function is about to destroy. This is the one // cancellation path in the whole design that waits. MG_Util::Async::ShaderCompilePool::Get().StopAndDrain(); // GL syncs die with their contexts, and every context is gone by the // time full teardown runs: drain the live-sync registry while the // backend function table can still release the backend handles (and // before a re-initialized library could pair them with the wrong // backend's DeleteSync). MG_Impl::GLImpl::DestroyAllSyncObjects(); // Queries die with their contexts for the same reason, and their registry // is the same shape of process-global map: drain it here too, while the // function table can still pair each backend handle with the backend that // minted it. MG_Impl::GLImpl::DestroyAllQueryObjects(); MG_Backend::pActiveBackendObject.reset(); MG_State::pGLContext.reset(); MG_State::pEGLContext.reset(); MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset(); MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset(); // Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes // glslang's process-wide pool allocator and every cached built-in symbol table, // while the TShader/TProgram objects owned by the shader and program objects // still reference levels adopted from those tables. Finalizing first left live // glslang objects pointing at freed memory for the rest of the teardown. glslang::FinalizeProcess(); // Immediately after, and never apart from it: FinalizeProcess just deleted the // built-in symbol tables the prewarm latch stands for, so leaving it set would // make the next Initialize() skip a prewarm it genuinely needs. MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch(); // The two-level translation memo. Nothing in it references a glslang object - // both levels hold plain bytes - so this is RSS hygiene rather than a lifetime // requirement, and it is safe either side of FinalizeProcess. Stats first: an // fordebug build gets one line per level saying how the run went. MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats(); MG_Util::ShaderTranspiler::ClearShaderTranslationCaches(); MG_State::GLState::LogProgramTranslationCacheStats(); MG_State::GLState::ClearProgramTranslationCache(); MG_Backend::gBackendFunctionsTable = {}; g_isInitialized = false; #if MOBILEGL_BUILD_DISAGGREGATED // A later re-initialization asks whether the server is there again. MG_Remote::Client::ResetServerProbeCache(); #endif #if MOBILEGL_PIPE_VERIFY // BEFORE Close, and from here rather than from a static destructor (V1 fix round 2): // Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the // FATAL=0 summary the comparator used to write from __run_exit_handlers truncated the // client half of every such run to that one line. Verify builds only - the guard is // the compile definition, not a runtime knob, and the pull build gains no byte (G1). MG_Pipe::MGPipeVerifyFlushSummary(); #endif if (logLifecycle) { MG_Util::Debug::Close(); } // TODO: add and use Destroy functions for other subsystems } } void Initialize() { if (g_isInitialized) { MGLOG_D("MobileGL already initialized; skipping duplicate Initialize()"); return; } MG_Util::Debug::InitFile(); MGLOG_I("Initializing MobileGL..."); MG_ConfigLoader::Init(); MGLOG_I("Config loaded"); // Immediately after the config load and before anything can count: the MGPipe // boundary counters latch their enable flag here, so every counting site in the // two backends is a load of an already-settled global for the rest of the run. MG_Util::PipeStats::Init(); #if MOBILEGL_BUILD_DISAGGREGATED // F1 (P7 wave 2). THE WIRE COMES UP BEFORE THE FIRST FRONTEND OBJECT IS BORN, and the // ORDER is the fix - not a wait, and not a friendlier default mask. // // MG_State::Init() constructs GLContext, and GLContext's TextureState constructor // materialises one default texture object per target (GL 3.3 core 3.8, // TextureState.cpp:62-71). TextureObjectBase's constructor emits resource_create for // every one of them (TextureState/TextureObject.cpp:168-169), and that emission is // gated, through PipeFill's FamilyIsLive, on // CapsMirror::ServerConsumes(kMGPipeSubsystemTextureResources). In the old order the // mirror was still GENERATION 0 when it was asked - it had no snapshot because the // session had not been started yet, MG_Backend::Init() being what starts it - so it // answered from the placeholder, every one of those creates was withheld, and the log // said so once and then never again: // // MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this // family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0, // caps generation 0 // // That is not a race and it never was one: the line reproduces byte-for-byte on // lavapipe and on an Adreno 830, because the two Init calls are two statements in one // thread. And "the legacy pull path runs for it" is false under split - the client has // no pull path to fall back to - so the records are simply lost, and only a later // respecify's self-healing create (TextureEmit.h:872) ever brings one back. // // A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during // MG_State::Init() no handshake is in flight, and the one that will land is started // LATER BY THIS SAME THREAD. The only correct answer is to have already brought it up, // which is what this hook does. ServerConsumes now REFUSES to answer from a // placeholder (CapsMirror.cpp), so a regression of this order is a named Fatal rather // than a whole family of silently dropped records. // // In a build without MOBILEGL_BUILD_DISAGGREGATED this statement does not exist, so // the pull build gains no symbol, no branch and no byte - which is what G1 measures. const Bool splitBackendIsUp = MG_Backend::InitSplitRolesBeforeState(); #endif MG_State::Init(); MGLOG_D("MG_State initialized"); #if MOBILEGL_BUILD_DISAGGREGATED if (!splitBackendIsUp) #endif { MG_Backend::Init(); // P13 W4: monolith's record arm reads the verb's own state, which only a verb sink // writes; the port routes the ported verbs through one (MG_Impl/Pipe/Verb/VerbPort.cpp). // Installed HERE, by the library's role-neutral init, rather than by MG_Backend's: the // port is client code, and the server image must not need it to link. if (MG_Config::RecordArmAliasesFrontend() && MG_Backend::pActiveBackendObject) { MG_Record::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable); } } MGLOG_D("MG_Backend initialized"); MG_Impl::Init(); MGLOG_D("MG_Impl initialized"); glslang::InitializeProcess(); // On the GL thread, before any worker can exist. glslang builds its built-in symbol // tables lazily under a process-wide lock held for the whole build, so without this // the first concurrent compiles of a shaderpack all serialize behind the very first // parse and asynchronous compilation looks like it is doing nothing. // // Gated on the flag, because the problem it solves only exists when there are // workers: with compilation synchronous, nothing ever contends for that lock and the // three throwaway parses buy nothing - they just add to every eglInitialize. Read the // flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency // on the async subsystem (ProgramUtilTest compiles that file without it). if (MG_Util::Async::AsyncShaderCompileEnabled()) { MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins(); } MGLOG_D("glslang initialized"); g_isInitialized = true; MGLOG_I("MobileGL initialized"); } void EnsureInitialized() { if (g_isInitialized.load(std::memory_order_acquire)) { return; } // Re-entrant call while this thread is already inside Initialize() // (e.g. an init step routing back through a public entry point). if (tl_initializing) { return; } const std::lock_guard lock(InitMutex()); if (g_isInitialized.load(std::memory_order_acquire)) { return; } tl_initializing = true; Initialize(); tl_initializing = false; } void Destroy() { DestroyImpl(true); } Bool ImplementationAvailable() { if (g_isInitialized.load(std::memory_order_acquire)) { #if MOBILEGL_BUILD_DISAGGREGATED // Initialized: under a split transport the session either came up (a backend object // exists) or it did not, and a display on a session that never came up cannot draw. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { return MG_Backend::pActiveBackendObject != nullptr; } #endif return true; } #if MOBILEGL_BUILD_DISAGGREGATED // Not yet: the bring-up would dial the configured server and wait for it. Ask first. return MG_Remote::Client::ConfiguredServerAvailable(); #else return true; #endif } // MobileGL's lifecycle is owned entirely by the host-API layers // (EGL/WGL/CGL): initialization happens lazily on the first entry point // via EnsureInitialized(), and full teardown happens deterministically // when the last EGL display is terminated with nothing current (EGLImpl // calls Destroy()). There is intentionally no backend-initializing static // constructor, no static destructor, and no DllMain: the global singletons // use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits // without eglTerminate simply leaks them to the OS instead of running // backend destructors during static teardown. macOS has a lightweight // dyld constructor that installs NSOpenGL dispatch hooks only; full backend // initialization still enters here from the first hooked CGL context. } // namespace MobileGL