mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
F1. `MG_State::Init()` runs before `MG_Backend::Init()`, and the second is what
starts the session - so `GLContext`'s `TextureState` constructor materialised its
eleven default texture objects, each of them emitted `resource_create` through
`FamilyIsLive`, and `CapsMirror::ServerConsumes(0x400)` answered every one of them
from a generation-0 placeholder with `callMask=0`. All eleven records were dropped
and the whole event was one WARN line:
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
It is NOT a race. The line reproduces byte-for-byte on lavapipe and on the Adreno
830, because the two Init calls are two statements on one thread. And "the legacy
pull path runs for it" is false on the client under split: there is no pull path
on this side of the wire, so withholding is not a fallback, it is a drop that only
a later respecify's self-healing create ever undoes.
Two of the three options the brief offered do not apply, and both facts are worth
recording. (b) "extend the P6.5 fix to inproc" is already the tree: `FinishStartup`
step 7 is the one funnel every transport reaches and it will not return without a
real snapshot. (a) "a bounded wait in Consumes()" cannot work alone and would
deadlock: at `MG_State::Init()` no handshake is in flight, and the one that will
land is started later BY THE SAME THREAD.
So the fix is the ORDER, with the rule behind it as the gate:
* `MobileGL::Initialize` calls `MG_Backend::InitSplitRolesBeforeState()` before
`MG_State::Init()`. `Init()`'s split arm becomes one body, `InitSplitBackend()`,
shared with the direct callers that still reach `Init()`; the monolith arm keeps
its place after `MG_State::Init()`, because moving it would move pull `.text`.
* `CapsMirror::ServerConsumes()` refuses to answer from a placeholder. It waits,
bounded, when a bring-up is genuinely in flight on ANOTHER thread, and otherwise
dies as `Fatal{CapsBeforeFirstSnapshot, "<family>"}` through `SessionFail` -
a new family word, one row in `FatalFamilies.def`, no new abort site.
* Two arms are exempt and say why in place: the server arm (`MGPipeServerArm()` -
the apply thread and the spawn server, where the client's mirror is the wrong
object rather than a late one), and a process whose own configuration never
asked for a split transport (`MG_Config::SplitTransportRequestedByConfig`, set
by `MG_ConfigLoader::InitTransport`, which is what tells a real run apart from a
fixture that assigns `MG_Config::Transport` by hand).
The read accessors keep P5's ruling untouched: `LogBackendInfo` reads
`GetRendererInfo()` during `MG_Backend::Init()` and an abort there is a process that
cannot start. Only the DECISION changed. The warning was not made quieter and the
callMask was not defaulted to all-ones.
`MOBILEGL_TEST_DELAY_FIRST_CAPS_MS` (server side, disaggregated builds, inert unless
set) holds back the FIRST CapsSnapshot. On spawn/tcp that is a genuinely late
snapshot in another process and it exercises the client's step-7 wait; on inproc it
only makes `Start` slower, which is itself the proof that the only window a client
can read a placeholder in is the one before `Start` is called.
G1: `.text` 0xa52203 and the 30570-symbol list identical to p7-before/pull-syms.txt.
Census: 79 abort sites unchanged, 44 family words (was 43). Link ratchet: 186,
unchanged.
224 lines
12 KiB
C++
224 lines
12 KiB
C++
// 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 <MG_Backend/BackendObjects.h>
|
|
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
|
|
#include <MG_State/GLState/Core.h>
|
|
#include <MG_State/EGLState/Core.h>
|
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
|
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
|
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
|
#include <MG_Util/Metrics/PipeStats.h>
|
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
|
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
|
|
|
#include <atomic>
|
|
#include <mutex>
|
|
|
|
namespace MobileGL {
|
|
namespace {
|
|
std::atomic<Bool> 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 (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();
|
|
}
|
|
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<std::mutex> lock(InitMutex());
|
|
if (g_isInitialized.load(std::memory_order_acquire)) {
|
|
return;
|
|
}
|
|
tl_initializing = true;
|
|
Initialize();
|
|
tl_initializing = false;
|
|
}
|
|
|
|
void Destroy() {
|
|
DestroyImpl(true);
|
|
}
|
|
|
|
// 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
|