mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
[Feat] (MG_State, MG_Util): async program linking on the job graph (P1 stage 4)
glLinkProgram with the flag on snapshots its inputs in a GL-thread prologue (stage-sorted shaders with their compile nodes taken without joining, env, explicit locations/fragdata/xfb, draw-buffer count), then runs the whole link body - glslang link/mapIO, SPIR-V, reflection, routing tables - as a ProgramLinkTask that auto-posts when its last compile dependency settles (+1-guarded countdown; no worker ever waits on another job). The publish is one move of the LinkArtifacts block at the join, with the second version bump so nothing memoized during the pending window survives. The consume-once TShader claim moved onto the shared compile node as a CAS: two link jobs racing for one shader resolve to winner-takes-the-parse, loser re-parses the preprocessed source against the node's own env - identical SPIR-V pinned by test for 2 and for 12 sharing programs. Two deliberate corrections to the design's cancel matrix, both test-proven: attach/detach do NOT cancel a pending link (the snapshot isolates it, and glCreateShaderProgramv's link-then-detach would otherwise discard its own result before anyone read it); and a compile node a pending link depends on is pinned against the orphan-name sweep - the ordinary LWJGL teardown compile/attach/link/detach/delete used to cancel the dependency and turn a must-pass link into GL_FALSE. Continuations are now throw-contained per-item (a stage-3 leftover made load-bearing by the first real continuation), and the review's deadlock find is fixed: the dispatch loop no longer cancels a node while holding the pool mutex, since that cancel can run OnDepSettled -> Post -> same mutex. Explicit joins: the draw path (GetProgramForDraw, both the pipeline stage loop and the plain-UseProgram half) and the composite-link site; destroy paths cancel-not-join; COMPLETION_STATUS readers stay non-joining. Gates: 506/506 unit both flag states; AsyncCompile/AsyncLink/AsyncTeardown suites x10 repeats clean both states (teardown with 128 jobs in flight, then re-Initialize); full NVIDIA DirectGLES retrace flag on twice - result sets identical to flag off, zero new deltas. Compile-phase prefix-diff, flag on vs off: complementary-reimagined 5.21s -> 2.16s, BSL 1.72s -> 0.90s - past the design's final acceptance targets before the KHR extension is even advertised. Default remains OFF until stage 5+7.
This commit is contained in:
@@ -185,24 +185,16 @@ namespace {
|
||||
return result;
|
||||
}
|
||||
|
||||
// glslang has no "detach this thread" API in the vendored revision (there is no
|
||||
// InitThread/DetachThread pair any more; thread attachment is implicit through
|
||||
// thread_local state, and glslang::InitializeProcess() is process-wide, refcounted and
|
||||
// mutex-guarded, so it needs no per-worker counterpart).
|
||||
//
|
||||
// What DOES need undoing is the thread pool allocator: TShader::parse sets the calling
|
||||
// thread's TLS allocator to the shader's own pool and never restores it. Left pointing
|
||||
// there, the next allocation this worker makes - in an unrelated job, or in glslang code
|
||||
// reached from a different object - would come out of a pool the GL thread may already
|
||||
// have deleted with the TShader. SetThreadPoolAllocator(nullptr) reverts the thread to
|
||||
// its own thread_local default and is the documented idiom. A scope guard, so it also
|
||||
// runs when a body throws.
|
||||
struct GlslangThreadAllocatorGuard {
|
||||
~GlslangThreadAllocatorGuard() { glslang::SetThreadPoolAllocator(nullptr); }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// glslang has no "detach this thread" API in the vendored revision (there is no
|
||||
// InitThread/DetachThread pair any more; thread attachment is implicit through
|
||||
// thread_local state, and glslang::InitializeProcess() is process-wide, refcounted and
|
||||
// mutex-guarded, so it needs no per-worker counterpart). The pool allocator is the part
|
||||
// that needs undoing; see the declaration in ShaderCompileTask.h.
|
||||
GlslangThreadAllocatorGuard::~GlslangThreadAllocatorGuard() { glslang::SetThreadPoolAllocator(nullptr); }
|
||||
|
||||
// Pure CPU work only. Everything this reads is either an input the node owns or a
|
||||
// process-wide constant; everything it writes is `artifacts`. Do not add a GL/EGL call,
|
||||
// a pActiveBackendObject read, or a pGLContext->RecordError() here - the first two are
|
||||
@@ -300,4 +292,47 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SharedPtr<glslang::TShader> ShaderCompileTask::ClaimParsedShader(String& outReparseLog) const {
|
||||
MOBILEGL_ASSERT(IsComplete(),
|
||||
"ShaderCompileTask::ClaimParsedShader() on a job that has not completed; its artifacts "
|
||||
"are still being written");
|
||||
|
||||
if (artifacts.shader) {
|
||||
// The whole race, in one instruction. Acquire-release because the winner is about
|
||||
// to hand the TShader to glslang's linker on a possibly different thread from the
|
||||
// one that parsed it - the node's terminal transition already published the
|
||||
// parse, and this orders the two claimants against each other.
|
||||
Bool expected = false;
|
||||
if (m_parseClaimed.compare_exchange_strong(expected, true, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire)) {
|
||||
return artifacts.shader;
|
||||
}
|
||||
}
|
||||
|
||||
// Either another link already consumed the stored parse (and mapIO mutated its
|
||||
// intermediate), or there never was one. Re-parse the preprocessed source through the
|
||||
// identical configuration; that costs one glslang parse, which is what GenerateBinary
|
||||
// used to spend here on EVERY link rather than only on reuse.
|
||||
//
|
||||
// The guard is not optional on this path: from stage 4 this runs on a pool worker,
|
||||
// and TShader::parse would leave that worker's TLS allocator pointing at a pool the
|
||||
// GL thread is about to free. (ProgramLinkTask::RunBody holds one too; they nest
|
||||
// harmlessly - both just reset the thread to its own default.)
|
||||
const GlslangThreadAllocatorGuard glslangGuard;
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(stage),
|
||||
.sourceStr = artifacts.preprocessedSource,
|
||||
.flags = 0,
|
||||
// Re-parse against the SAME environment the original parse used,
|
||||
// not against whatever the backend reports now.
|
||||
.env = artifacts.env.get()};
|
||||
auto result = ShaderCompiler::CompileShader(attrib);
|
||||
if (!result) {
|
||||
// Should be unreachable: the same source parsed successfully at Compile().
|
||||
outReparseLog = result.error().log;
|
||||
return nullptr;
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
Reference in New Issue
Block a user