diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index ad198528..8c56b65d 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -1273,7 +1273,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4ivARB, GLenum target, const GL DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4sARB, GLenum target, GLshort s, GLshort t, GLshort r, GLshort q) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4sARB, target, s, t, r, q) DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4svARB, GLenum target, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4svARB, target, v) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectivARB, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectivARB, id, pname, params) -DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count) +DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count) DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfARB, pname, param) DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img) @@ -1381,7 +1381,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3ivARB, const GLint* v) DECLARE_GL_ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sARB, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sARB, x, y, z) DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3svARB, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3svARB, v) DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrierKHR, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrierKHR, ) -DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count) +DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count) DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bOES, GLenum texture, GLbyte s) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bOES, texture, s) DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bvOES, GLenum texture, const GLbyte* coords) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bvOES, texture, coords) DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord2bOES, GLenum texture, GLbyte s, GLbyte t) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord2bOES, texture, s, t) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 13b159e2..61d259d7 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace MobileGL::MG_Impl::GLImpl { @@ -1069,6 +1070,20 @@ namespace MobileGL::MG_Impl::GLImpl { *params = obj ? static_cast(obj->GetExternalIndex()) : 0; return; } + case GL_MAX_SHADER_COMPILER_THREADS_KHR: + // GL_KHR_parallel_shader_compile (GL_MAX_SHADER_COMPILER_THREADS_ARB is the same + // 0x91B0). The number of threads MobileGL's compile pool would actually use, so + // an application sizing its own submission batches gets a real answer. + // + // Zero when asynchronous compilation is off, which is the honest reply and the + // one the extension defines for an implementation with no compiler threads: the + // extension string is withdrawn in that configuration too, so a conforming + // application never reaches this query, and one that asks anyway is told there + // are none rather than being handed a thread count nothing will use. + *params = MG_Util::Async::AsyncShaderCompileEnabled() + ? static_cast(MG_Util::Async::ShaderCompilePool::Get().GetThreadCount()) + : 0; + return; case GL_MAX_DEBUG_GROUP_STACK_DEPTH: *params = 0; // debug-group entrypoints are stubbed return; diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 26fb242e..f2f989c6 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace MobileGL::MG_Impl::GLImpl { @@ -355,6 +356,54 @@ namespace MobileGL::MG_Impl::GLImpl { shaderObject->Compile(); } + // glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation, + // because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the + // same entry point with the same semantics and GetProcAddress.cpp maps both spellings. + // + // The three cases the extension defines, and what each means here: + // + // count == 0 "no compiler threads": compilation must happen on the + // application's thread. Everything already in flight is joined + // first, so that after this call returns NOTHING is outstanding + // and every GL_COMPLETION_STATUS_KHR reads GL_TRUE - which is + // the observable the extension actually specifies. The pool + // keeps its worker threads (this is not teardown); what changes + // is that AsyncShaderCompileActive() now says no, so + // glCompileShader/glLinkProgram run their bodies inline. + // count == 0xFFFFFFFF "implementation maximum": the pool's full thread count. + // otherwise a concurrency budget, clamped to the thread count - asking for + // more threads than exist cannot conjure any. + // + // A nonzero count is also what LIFTS a previous zero: the suspension lasts exactly until + // the application asks for threads again, and nothing else re-arms it (no implicit + // restore at eglInitialize, at a context switch or at a join). An application that turned + // compiler threads off keeps them off until it says otherwise. + // + // Legal - and a no-op beyond bookkeeping - while MOBILEGL_ASYNC_SHADER_COMPILE is off: + // compilation is already inline, and the call must not fail just because MobileGL had + // nothing to suspend. + void MaxShaderCompilerThreadsKHR_State(GLuint count) { + namespace Async = MG_Util::Async; + if (count == 0) { + MGLOG_D("%s: count = 0; joining all pending shader work and compiling inline", __func__); + Async::SetAsyncShaderCompileSuspended(true); + // Suspend BEFORE joining, not after. The post-condition this call owes the + // application is "nothing is in flight when I return", and only this order + // guarantees it: with the latch already set, anything the join itself causes to + // be compiled runs inline and is therefore already settled when the join ends. + // Joining first would leave a window in which a fresh enqueue is still legal. + if (MG_State::pGLContext) MG_State::pGLContext->JoinAllPendingShaderWork(); + return; + } + + Async::ShaderCompilePool& pool = Async::ShaderCompilePool::Get(); + const Uint threadCount = pool.GetThreadCount(); + const Uint requested = count == 0xFFFFFFFFu ? threadCount : std::min(count, threadCount); + pool.SetMaxConcurrency(requested); + Async::SetAsyncShaderCompileSuspended(false); + MGLOG_D("%s: count = %u; concurrency = %u of %u threads", __func__, count, requested, threadCount); + } + GLuint CreateProgram_State() { return MG_State::pGLContext->CreateProgram(); } @@ -693,6 +742,19 @@ namespace MobileGL::MG_Impl::GLImpl { break; } + // GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - it is the one program + // query whose entire purpose is to answer without waiting, and routing it through + // any of ProgramObject's Artifacts() accessors (the join gate, invariant I5) would + // block the caller and make the extension a lie: an application polling it would + // serialize itself on the very link it is trying to overlap. IsLinkComplete() is the + // node-direct reader that exists for exactly this. + // + // No link at all reads GL_TRUE, which is what the extension requires: the query + // means "is anything still outstanding", not "has this program ever been linked". + case GL_COMPLETION_STATUS_KHR: + *params = programObject->IsLinkComplete() ? GL_TRUE : GL_FALSE; + break; + case GL_PROGRAM_BINARY_LENGTH: // No program binary format is exposed, so a program never has a retrievable // binary and its length is zero (ARB_get_program_binary). @@ -746,6 +808,13 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_SHADER_SOURCE_LENGTH: *params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1; break; + // GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in + // GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has + // to: it reports the outcome); this one reports whether there is an outcome yet, and + // reading it through Compiled() would defeat the whole extension. + case GL_COMPLETION_STATUS_KHR: + *params = shaderObject->IsCompileComplete() ? GL_TRUE : GL_FALSE; + break; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -1832,6 +1901,16 @@ namespace MobileGL::MG_Impl::GLImpl { void CompileShader(GLuint shader) { CompileShader_State(shader); } + + void MaxShaderCompilerThreadsKHR(GLuint count) { + MaxShaderCompilerThreadsKHR_State(count); + } + + // GL_ARB_parallel_shader_compile's spelling of the same entry point. + void MaxShaderCompilerThreadsARB(GLuint count) { + MaxShaderCompilerThreadsKHR_State(count); + } + GLuint CreateProgram(void) { return CreateProgram_State(); } diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h index f8bf8361..437db49c 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h @@ -42,6 +42,10 @@ namespace MobileGL::MG_Impl::GLImpl { GLboolean IsProgram(GLuint program); GLboolean IsShader(GLuint shader); void LinkProgram(GLuint program); + // GL_KHR_parallel_shader_compile / GL_ARB_parallel_shader_compile. Both names are the + // same entry point; see MaxShaderCompilerThreadsKHR_State for the semantics of count. + void MaxShaderCompilerThreadsKHR(GLuint count); + void MaxShaderCompilerThreadsARB(GLuint count); void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length); void UseProgram(GLuint program); void Uniform1f(GLint location, GLfloat v0); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 706f1536..e22cc9c7 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -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); } diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 8e85d99a..02d1dba4 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -153,6 +153,9 @@ namespace MobileGL { Bool ValidateShaderName(Uint index) const; const SharedPtr& GetProgramObject(Uint index); const SharedPtr& 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& GetCurrentProgram(); // What a draw or dispatch actually executes: the program in use, or - when diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 0a0ca9db..1c76129a 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -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; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp index 2f529e5b..5a7d980c 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp @@ -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 program = m_programObjects[i]; + if (program) program->JoinLink(); + } + for (SizeT i = 0; i < m_shaderObjects.size(); ++i) { + const SharedPtr 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]; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h index e5270994..425acd5e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h @@ -34,6 +34,19 @@ namespace MobileGL::MG_State::GLState { const SharedPtr& 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(). diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp index 4ca44a55..2cbf55c3 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp @@ -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 diff --git a/MobileGL/MG_Util/Async/ShaderCompilePool.cpp b/MobileGL/MG_Util/Async/ShaderCompilePool.cpp index 4dd53bc7..c4953e57 100644 --- a/MobileGL/MG_Util/Async/ShaderCompilePool.cpp +++ b/MobileGL/MG_Util/Async/ShaderCompilePool.cpp @@ -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 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 diff --git a/MobileGL/MG_Util/Async/ShaderCompilePool.h b/MobileGL/MG_Util/Async/ShaderCompilePool.h index 56ff2c73..59500743 100644 --- a/MobileGL/MG_Util/Async/ShaderCompilePool.h +++ b/MobileGL/MG_Util/Async/ShaderCompilePool.h @@ -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 diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index d841e2be..a97a8de1 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -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", diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index e1897962..dea4eb24 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -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; diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 7bd7a39f..48990233 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -17,6 +17,7 @@ // MG_State code: it runs standalone, before MG_State::Init(). #include #include +#include #include #include @@ -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& backendApiVersionString, const Optional& 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 "