[Perf] (MG_State): adopt in-flight compile jobs across shader objects (P1 stage 6)

~21% of a shaderpack's glCompileShader calls hand different shader objects
byte-identical source; the P0b cache only helps after one finishes, so under
async two workers would run the whole pipeline twice. Now the GL thread
consults a per-context (stage, hash, length, envFingerprint) -> weak-node
map at enqueue and ADOPTS the in-flight (or completed) node instead of
posting a duplicate - a hit is honored only after a full byte comparison
(the hash never decides), a cancel-requested or settled-cancelled node is
never adopted, and no worker ever waits.

Sharing a node makes the unconditional cancel wrong, so release is now
adopter-counted: a plain GL-thread Int (every mutation site is a GL entry
point; the single-threadedness argument and the terminal-early-out that
keeps the count exact are in the header), and the cancel fires only at
count zero AND with no pending link pinning the node (the stage-4
MarkLinkReferenced precedence). Adoption also re-points the object's source
at the node's snapshot so the layer-1 memo's pointer compare stays armed -
without that, an adopter's next glCompileShader would re-enqueue the very
duplicate this stage removes. Both guards are negative-control-proven: each
removed guard fails exactly its own tests. Count discipline was proven with
a temporary hard-abort on underflow/leak across the full suite and retrace
corpus - zero hits.

18 new tests (13 GL-surface incl. shared-node re-source/delete/orphan-sweep
isolation, shared failure logs, 48-over-6 stress with a deterministic
adoption count, flag-off and KHR-suspended zero-adoption guards; 5 direct
map cases incl. fingerprint mismatch and cancelled/expired pruning).
Gates: 538/538 unit both flag states, async suites x5 no flakes, NVIDIA
DirectGLES retrace identical sets both states. Timing: 2-worker
(Android-shaped) 1-3% faster consistently on complementary and BSL;
4-worker unchanged - the win this stage exists for lands where CPU is
scarce.
This commit is contained in:
BZLZHH
2026-08-08 20:17:51 -04:00
parent d98f72447d
commit dcf918b9ee
14 changed files with 1431 additions and 68 deletions
+14 -2
View File
@@ -20,6 +20,7 @@
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ShaderPreprocessCache.h"
#include "MG_Util/Async/ShaderCompilePool.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
using namespace MobileGL;
@@ -3006,8 +3007,19 @@ TEST_F(ProgramTest, TwoShaderObjectsWithIdenticalSourceLinkIndependently) {
ASSERT_NE(objectA, nullptr);
ASSERT_NE(objectB, nullptr);
EXPECT_EQ(objectA->GetShaderSource(), objectB->GetShaderSource());
// Independent parses despite the shared preprocess.
EXPECT_NE(objectA->GetCompiledShader(), objectB->GetCompiledShader());
// P0b's layer 2 shares the PREPROCESS and never the parse: glslang's TShader is
// consume-once, so a memo hit still has to parse for itself.
//
// P1 stage 6 shares something stronger when it is active - the whole compile JOB, and
// therefore the single parse that job produced - and that sharing is made safe by
// ShaderCompileTask::ClaimParsedShader's CAS instead, exactly as it already was for one
// shader object attached to two programs. ShaderCompileAdoptionTest is where that is
// pinned down (it links both objects and compares the generated SPIR-V). So the
// one-parse-per-object assertion belongs to the non-adopting path; the two independent
// LINKS below are what both modes have to agree on, and they are the point of this case.
if (!MG_Util::Async::AsyncShaderCompileActive()) {
EXPECT_NE(objectA->GetCompiledShader(), objectB->GetCompiledShader());
}
EXPECT_NE(objectA->GetCompiledShader(), nullptr);
EXPECT_NE(objectB->GetCompiledShader(), nullptr);