[Feat] (MG_Impl, MG_State, MG_Util): the GL_KHR_parallel_shader_compile surface (P1 stage 5)

GL_COMPLETION_STATUS_KHR in both object getters, reading the non-joining
node-direct state - the one query that must never block is asserted never
to reach a join gate. glMaxShaderCompilerThreadsKHR/ARB share one
implementation: a zero count suspends async FIRST and then joins every
outstanding compile and link this context owns (suspend-before-join is the
only order whose post-condition is 'nothing in flight'), a nonzero count
restores; the suspension is a process latch the extension controls, kept
distinct from the configuration flag that gates the ADVERTISEMENT - an app
that turned threading off has not made the extension disappear.
GL_MAX_SHADER_COMPILER_THREADS_KHR reports the thread count. DriverPost
gains the MobileGL-side async row (PASS/INFO naming the env knob) and an
informational host-driver row backed by a new GLES capability probe.

The extension string itself lands per backend in the two follow-up
commits, keeping this one green stand-alone.
This commit is contained in:
BZLZHH
2026-08-08 13:17:58 -04:00
parent 6f8b7fbc40
commit bd0def6133
15 changed files with 265 additions and 10 deletions
+4
View File
@@ -357,6 +357,10 @@ namespace MobileGL::MG_State {
return m_programState.GetShaderObject(index);
}
void GLContext::JoinAllPendingShaderWork() {
m_programState.JoinAllPendingWork();
}
void GLContext::UseProgram(Uint program) {
return m_programState.UseProgram(program);
}
+3
View File
@@ -153,6 +153,9 @@ namespace MobileGL {
Bool ValidateShaderName(Uint index) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
const SharedPtr<ShaderObject>& GetShaderObject(Uint index);
// Settles every compile and link this context still owns; see
// ProgramState::JoinAllPendingWork. Called by glMaxShaderCompilerThreadsKHR(0).
void JoinAllPendingShaderWork();
void UseProgram(Uint program);
const SharedPtr<ProgramObject>& GetCurrentProgram();
// What a draw or dispatch actually executes: the program in use, or - when
@@ -292,10 +292,11 @@ namespace MobileGL::MG_State::GLState {
m_pendingLink = task;
// Flag off: byte-identical to the synchronous implementation. RunInline() executes
// the same body on this thread and the join below publishes through the same code, so
// the two modes differ only in WHICH thread ran RunBody().
if (!MG_Util::Async::AsyncShaderCompileEnabled()) {
// Flag off - or glMaxShaderCompilerThreadsKHR(0), see AsyncShaderCompileActive():
// byte-identical to the synchronous implementation. RunInline() executes the same
// body on this thread and the join below publishes through the same code, so the two
// modes differ only in WHICH thread ran RunBody().
if (!MG_Util::Async::AsyncShaderCompileActive()) {
task->RunInline();
EnsureLinkJoined();
return;
@@ -99,6 +99,31 @@ namespace MobileGL::MG_State::GLState {
return m_shaderObjects[shader];
}
void ProgramState::JoinAllPendingWork() {
// Programs first: a link joins the compiles it depends on, so the shader pass that
// follows finds most of them already settled. The reverse order would be correct but
// would wait on each compile twice - once here, once inside the link's own prologue.
//
// A copy of each slot rather than a reference into the vector, and an index rather
// than an iterator: publishing a link replays deferred diagnostics, which reach
// pGLContext->RecordError. That does not touch these tables today, but it is a sink
// that can grow, and a reallocation underneath this loop would be a use-after-free
// that only shows up on the one GL call that walks the whole table. The copy costs a
// refcount bump on a path a mode switch takes at most once.
for (SizeT i = 0; i < m_programObjects.size(); ++i) {
const SharedPtr<ProgramObject> program = m_programObjects[i];
if (program) program->JoinLink();
}
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
if (shader) shader->JoinCompile();
}
// The currently-used program is reachable through m_programObjects unless
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
// a GL-visible name for it, but a draw would still join it, so settle it here too.
if (m_currentProgram) m_currentProgram->JoinLink();
}
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader];
@@ -34,6 +34,19 @@ namespace MobileGL::MG_State::GLState {
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
// Joins every outstanding compile and link this context still owns, publishing each
// one's artifacts through the ordinary gates. The single caller is
// glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero
// count to leave nothing in flight, so that every subsequent
// GL_COMPLETION_STATUS_KHR reads GL_TRUE.
//
// NOT a teardown path and NOT ShaderCompilePool::StopAndDrain(): the pool keeps its
// threads and stays usable, because a later nonzero count has to bring asynchronous
// compilation straight back. Nodes belonging to objects this context has already
// dropped are not joined - nothing can observe them, and waiting on them would make
// a GL call's cost depend on garbage.
void JoinAllPendingWork();
// P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it
// directly - shader objects reach it through the pointer they are handed at
// CreateShader().
@@ -115,7 +115,11 @@ namespace MobileGL::MG_State::GLState {
// path must be byte-identical to the synchronous implementation, and a cache-less
// object is an internal shader that compiles and reads its status in the same
// breath (see the constructor comment) - a job would only add a round trip.
if (!m_preprocessCache || !MG_Util::Async::AsyncShaderCompileEnabled()) {
// AsyncShaderCompileActive(), not ...Enabled(): a glMaxShaderCompilerThreadsKHR(0)
// has to put compilation back on this thread even though the extension is still
// advertised, and that is exactly what makes the GL_COMPLETION_STATUS_KHR the
// extension mandates after a zero count (immediately GL_TRUE) fall out for free.
if (!m_preprocessCache || !MG_Util::Async::AsyncShaderCompileActive()) {
m_compiled->RunInline();
// Inline means the node is already terminal, so this join only replays
// diagnostics; it is here so the synchronous and asynchronous paths publish