mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
glCompileShader with MOBILEGL_ASYNC_SHADER_COMPILE=1 snapshots its inputs on the GL thread (source SharedPtr, CompileEnv, cache handle) and runs the whole pure pipeline - preprocess, validators, extractors, glslang parse - as a ShaderCompileTask on the worker pool, returning immediately. Every read of compile-produced state joins through the single Compiled() gate; links stay synchronous this stage and join their attached shaders at the top of the body. Flag off, the path is the same code run inline. Mechanics: the job node owns all its inputs (no back-pointer, no lifetime tie to the shader object), so re-sourcing or deleting a pending shader is cancel-and-drop, never a wait; glslang worker hygiene is a TLS-allocator scope guard plus GL-thread builtin prewarm (gated on the flag, latch reset on Destroy so re-initialization re-warms); worker-side diagnostics defer through the job and replay on the GL thread at the join, enforced by IsPoolThread asserts in RecordError and an empty-deferred-errors tripwire. A body that throws publishes a COMPLETE failed compile (status false, real info log) rather than an abandoned node, and never memoizes away the retry; a failed enqueue (OOM) cancels the node instead of stranding the joiner - including inside the dispatch loop, where the in-flight slot is repaid. The pool StopAndDrains from an atexit sentinel too: workers still inside glslang parse while exit() ran static destructors was a real 2-in-5 SIGSEGV, reproduced and fixed (15/15 clean after). Backend-internal shader objects (default FS, DirectVulkan blit/mipmap) are cache-less and always compile inline - compile-and-read-in-one-breath needs no round trip. Gates: unit suite 488/488 with the flag off AND on (x5); AsyncCompileTest (12 e2e cases: pending re-source/delete/recompile, byte-identical failure logs across modes, 48-compile cache stress) x10 repeats clean both modes; full NVIDIA DirectGLES retrace identical result sets flag off/on (zero new deltas); compile-phase timing flat as designed (links still serial - the parallel win arrives with stage 4's async link + stage 5's KHR_parallel_shader_compile).
148 lines
6.5 KiB
C++
148 lines
6.5 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_Util/Async/ShaderCompilePool.h>
|
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.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...");
|
|
}
|
|
// 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();
|
|
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();
|
|
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");
|
|
MG_State::Init();
|
|
MGLOG_D("MG_State initialized");
|
|
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
|