mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
[Feat] (MG_State, MG_Util): async shader compilation behind the default-off flag (P1 stage 3)
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).
This commit is contained in:
@@ -249,6 +249,56 @@ namespace MobileGL {
|
||||
return retryResult;
|
||||
}
|
||||
|
||||
// Namespace-level rather than a function-local static, because it has to be
|
||||
// CLEARABLE: what PrewarmBuiltins latches is not a property of this process, it is
|
||||
// a property of the built-in symbol tables glslang currently holds, and
|
||||
// glslang::FinalizeProcess() deletes those. A function-local latch survived the
|
||||
// teardown that invalidated it, so an Initialize -> Destroy -> Initialize cycle
|
||||
// came back up with the tables gone and the prewarm skipped - which is exactly the
|
||||
// serialized-first-parse stall this function exists to prevent, only now
|
||||
// unfixable for the rest of the process. Reset it from DestroyImpl.
|
||||
namespace {
|
||||
Bool g_builtinsPrewarmed = false;
|
||||
} // namespace
|
||||
|
||||
void ShaderCompiler::ResetPrewarmLatch() { g_builtinsPrewarmed = false; }
|
||||
|
||||
void ShaderCompiler::PrewarmBuiltins() {
|
||||
if (g_builtinsPrewarmed) return;
|
||||
g_builtinsPrewarmed = true;
|
||||
|
||||
// One vertex and one fragment shader is enough: the built-in table is cached
|
||||
// per (version, spvVersion, profile, source), not per stage language, and
|
||||
// both configurations CompileShader can reach - the declared-460 path and
|
||||
// the retargeted-legacy path - resolve to the same combination here because
|
||||
// ParseShaderSource always passes 460/ECoreProfile as the default. Parsing
|
||||
// both anyway costs microseconds and keeps this honest if that ever changes.
|
||||
static constexpr const char* kPrewarmVertexSource =
|
||||
"#version 460\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
static constexpr const char* kPrewarmFragmentSource =
|
||||
"#version 460\nlayout(location = 0) out vec4 c;\nvoid main() { c = vec4(0.0); }\n";
|
||||
static constexpr const char* kPrewarmLegacyVertexSource =
|
||||
"#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
|
||||
const CompileEnv& env = *GetDefaultCompileEnv();
|
||||
for (const auto& [type, source] :
|
||||
{std::pair{GL_VERTEX_SHADER, kPrewarmVertexSource},
|
||||
std::pair{GL_FRAGMENT_SHADER, kPrewarmFragmentSource},
|
||||
std::pair{GL_VERTEX_SHADER, kPrewarmLegacyVertexSource}}) {
|
||||
ShaderAttrib attrib{.shaderType = static_cast<GLenum>(type),
|
||||
.sourceStr = source,
|
||||
.flags = 0,
|
||||
.env = &env};
|
||||
// The result is deliberately discarded: the value is the symbol table
|
||||
// glslang cached as a side effect. A failure here is not fatal - it just
|
||||
// means the first real compile pays for the table, exactly as before.
|
||||
(void)CompileShader(attrib);
|
||||
}
|
||||
// The parses above left this thread's glslang allocator pointing at the last
|
||||
// TShader's pool, and that TShader is about to be destroyed with it.
|
||||
glslang::SetThreadPoolAllocator(nullptr);
|
||||
}
|
||||
|
||||
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
|
||||
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
|
||||
for (auto& s : attrib.shaders) {
|
||||
|
||||
Reference in New Issue
Block a user