[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
@@ -107,6 +107,25 @@ namespace MobileGL::MG_Util::Async {
return kAsyncShaderCompileDefault;
}
namespace {
// Written only by glMaxShaderCompilerThreadsKHR/ARB, i.e. only on the GL thread, but
// read by every enqueue decision, so it is atomic rather than plain: a worker never
// reads it, but a second GL thread in another context shares this process-wide pool.
std::atomic<Bool> g_asyncSuspendedByApplication{false};
} // namespace
void SetAsyncShaderCompileSuspended(const Bool suspended) {
g_asyncSuspendedByApplication.store(suspended, std::memory_order_release);
}
Bool IsAsyncShaderCompileSuspended() {
return g_asyncSuspendedByApplication.load(std::memory_order_acquire);
}
Bool AsyncShaderCompileActive() {
return AsyncShaderCompileEnabled() && !IsAsyncShaderCompileSuspended();
}
Uint DetectShaderCompileThreadCount() {
if (const Uint32 configured = MG_Config::Features.AsyncShaderCompileThreads; configured > 0) {
// An explicit request is honoured as given - it is the escape hatch for measuring
+29 -3
View File
@@ -27,11 +27,37 @@ namespace MobileGL::MG_Util::Async {
inline constexpr Bool kAsyncShaderCompileDefault = false;
// MOBILEGL_ASYNC_SHADER_COMPILE forces the answer either way; unset keeps the built-in
// default above. Falsy is a complete kill switch: it reverts the threading *and* (from
// the extension stage on) withdraws GL_KHR_parallel_shader_compile, so the application
// behaviour change goes with it.
// default above. Falsy is a complete kill switch: it reverts the threading *and*
// withdraws GL_KHR_parallel_shader_compile, so the application behaviour change goes
// with it.
//
// This is the pure CONFIGURATION answer, and it is deliberately not affected by
// glMaxShaderCompilerThreadsKHR: it is what decides whether the extension is advertised
// at all, and an application that switched threading off through the extension has not
// made the extension go away. Code deciding whether to enqueue asks
// AsyncShaderCompileActive() instead.
Bool AsyncShaderCompileEnabled();
// ---- GL_KHR_parallel_shader_compile: glMaxShaderCompilerThreadsKHR(count) ----
// The extension defines count == 0 as "no compiler threads": compilation must happen on
// the application's thread. That is a mode switch, not a concurrency budget of one, so it
// is a latch of its own rather than SetMaxConcurrency(1) - a budget of one would still
// move the work off-thread and still report GL_COMPLETION_STATUS_KHR = GL_FALSE, both of
// which the extension forbids after a zero count.
//
// The latch is process-wide, matching the pool it suspends. It is released by the next
// nonzero glMaxShaderCompilerThreadsKHR/ARB, which is the only thing that releases it:
// no implicit re-arm on eglInitialize, on a context switch or at any join, because an
// application that asked for serial compilation gets to keep it until it asks otherwise.
void SetAsyncShaderCompileSuspended(Bool suspended);
Bool IsAsyncShaderCompileSuspended();
// What every enqueue site branches on: the configuration flag AND the absence of a
// glMaxShaderCompilerThreadsKHR(0). False makes glCompileShader/glLinkProgram run their
// bodies inline, exactly as the flag-off path does, which is what makes a subsequent
// GL_COMPLETION_STATUS_KHR read immediately GL_TRUE.
Bool AsyncShaderCompileActive();
// min(4, big cores), where a big core is one whose cpufreq ceiling is within 15% of the
// machine maximum; the whole CPU count where that sysfs tree is absent. Clamped to [1, 4]
// because peak RSS scales as workers x largest glslang arena, and four
@@ -858,6 +858,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
caps.SupportsDisjointTimerQuery = true;
}
if (std::strcmp(extension, "GL_KHR_parallel_shader_compile") == 0) {
caps.SupportsParallelShaderCompile = true;
}
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
caps.SupportsDualSourceBlend = true;
}
@@ -1220,6 +1223,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidSamplerMipmapMinFilter;
MGLOG_I(" GL_EXT_disjoint_timer_query supported: %s",
caps.SupportsDisjointTimerQuery ? "true" : "false");
MGLOG_I(" GL_KHR_parallel_shader_compile supported: %s",
caps.SupportsParallelShaderCompile ? "true" : "false");
MGLOG_I(" ANGLE renderer: %s", caps.IsAngleRenderer ? "true" : "false");
MGLOG_I(" ANGLE llvmpipe renderer: %s", caps.IsAngleLlvmpipeRenderer ? "true" : "false");
MGLOG_I(" Avoid sampler mipmap min filter: %s",
@@ -1053,6 +1053,16 @@ namespace MobileGL {
Bool SupportsBaseInstance = false;
// GL_EXT_disjoint_timer_query is present in the extension string.
Bool SupportsDisjointTimerQuery = false;
// GL_KHR_parallel_shader_compile is present in the HOST driver's extension string,
// i.e. the device driver can compile its own (ESSL) shaders on its own threads.
//
// Purely informational today, and NOT what gates MobileGL's advertisement of the
// same extension: MobileGL's parallelism is its own compile pool turning GLSL into
// SPIR-V, which is where the shaderpack time goes, and it works on a driver that
// has never heard of the extension. This flag becomes load-bearing only if the
// driver-side glCompileShader of the translated ESSL is parallelised too, at which
// point it decides whether that half can overlap.
Bool SupportsParallelShaderCompile = false;
// glPolygonModeNV/ANGLE loaded (GL_NV_polygon_mode / GL_ANGLE_polygon_mode). GLES core
// has no glPolygonMode, so without this the mode stays FILL.
Bool SupportsPolygonMode = false;
+47
View File
@@ -17,6 +17,7 @@
// MG_State code: it runs standalone, before MG_State::Init().
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <chrono>
#include <thread>
@@ -122,6 +123,37 @@ namespace MobileGL::MG_Util::SelfTest {
return result;
}
// ---- Asynchronous shader compilation ------------------------------------
// MobileGL's OWN capability row, appended for both backends: nothing about it comes
// from the device driver, so it is the same fact on Espryt and on Magma. The POST
// rule ("every new capability gets a row") applies to frontend capabilities too -
// and this one especially, because it is the capability that changes what
// applications DO, not just what they can do: with the extension advertised, Iris
// and Sodium batch their pipeline compiles and poll GL_COMPLETION_STATUS_KHR.
//
// PASS when it is on (the intended configuration once the default flips), INFO when
// it is off - "off" is a supported configuration, not a degradation, so it must not
// colour the verdict. Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a
// user reading a POST page can tell which side of the switch they are on and how to
// change it.
void AppendAsyncShaderCompileRow(ReportBuilder& builder) {
constexpr const char* rowName = "Asynchronous shader compilation";
if (!MG_Util::Async::AsyncShaderCompileEnabled()) {
builder.Info(rowName,
"off; glCompileShader and glLinkProgram run on the calling thread and "
"GL_KHR_parallel_shader_compile is not advertised (set environment variable "
"MOBILEGL_ASYNC_SHADER_COMPILE=1 to enable it)");
return;
}
const Uint threads = MG_Util::Async::DetectShaderCompileThreadCount();
builder.Pass(rowName,
format("on with {} compiler thread{}; GL_KHR_parallel_shader_compile is advertised "
"and GL_MAX_SHADER_COMPILER_THREADS_KHR = {} (set environment variable "
"MOBILEGL_ASYNC_SHADER_COMPILE=0 to disable it, or "
"MOBILEGL_ASYNC_SHADER_COMPILE_THREADS=n to change the count)",
threads, threads == 1 ? "" : "s", threads));
}
// Appends the four "MobileGL reported ..." rows for one backend section.
// GL_VENDOR and GL_VERSION only depend on the backend's static identity, so
// they are always concrete; GL_RENDERER and GL_EXTENSIONS need data from the
@@ -130,6 +162,9 @@ namespace MobileGL::MG_Util::SelfTest {
const Optional<String>& backendApiVersionString,
const Optional<String>& advertisedExtensions) {
static const String Unavailable = "unavailable (backend probe failed)";
// Frontend capability, not a probe result, so it is appended on every path -
// including one where the device probe failed outright.
AppendAsyncShaderCompileRow(builder);
builder.MobileGLReported("MobileGL reported GL_VENDOR", BuildReportedGLVendor(identity));
builder.MobileGLReported("MobileGL reported GL_VERSION", BuildReportedGLVersion(identity));
builder.MobileGLReported("MobileGL reported GL_RENDERER",
@@ -416,6 +451,18 @@ namespace MobileGL::MG_Util::SelfTest {
"not supported; 16-bit normalized texture formats need emulation");
}
// INFO, never WARN: this is the HOST driver's ability to compile its own ESSL on
// its own threads, and MobileGL's asynchronous compilation does not depend on it
// in the slightest - the pool parallelises GLSL -> SPIR-V -> ESSL translation,
// which is where a shaderpack load actually spends its time, and it does that on
// a driver that has never heard of the extension. The row exists so that the day
// the driver-side half is overlapped too, the POST already says which devices can.
builder.Info("Driver GL_KHR_parallel_shader_compile",
caps.SupportsParallelShaderCompile
? "supported; the device driver can also compile the translated ESSL off-thread"
: "not supported; the device driver compiles the translated ESSL on the calling "
"thread (MobileGL's own compile pool is unaffected)");
builder.Info("Indirect gl_InstanceID semantics",
caps.IndirectDrawInstanceIdIncludesBaseInstance
? "includes baseInstance (ANGLE-style; MobileGL's shader rewrite keeps gl_InstanceID "