diff --git a/MobileGL/Config.h b/MobileGL/Config.h index a8a60ab6..772bb5ed 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -323,6 +323,11 @@ namespace MobileGL::MG_Config { // MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means // auto, which is min(4, big cores); an explicit value is honoured as given. Uint32 AsyncShaderCompileThreads = 0; + // MOBILEGL_DISABLE_SHADER_PREWARM: skip the glslang built-in table prewarm in + // Initialize(), which moves ~20-100 ms of table building from the first shader compiles + // into eglInitialize. For short-lived processes that would never repay it, such as the + // CI test runners; applications keep the default. + Bool DisableShaderPrewarm = false; // MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight, // glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads // empty, WITHOUT joining the job (latched per compile - see diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 546e99f7..3a03a2a7 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -417,6 +417,7 @@ namespace MobileGL::MG_ConfigLoader { features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE"); features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE"); features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64); + features.DisableShaderPrewarm = QueryEnvFlag("MOBILEGL_DISABLE_SHADER_PREWARM"); features.AsyncOptimisticShaderStatus = QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS"); features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE"); features.EsprytViewportArrayEmulation = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION"); diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index bf8dd9e9..474bd742 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -205,7 +205,8 @@ namespace MobileGL { // three throwaway parses buy nothing - they just add to every eglInitialize. Read the // flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency // on the async subsystem (ProgramUtilTest compiles that file without it). - if (MG_Util::Async::AsyncShaderCompileEnabled()) { + // MOBILEGL_DISABLE_SHADER_PREWARM skips it for processes too short-lived to repay it. + if (MG_Util::Async::AsyncShaderCompileEnabled() && !MG_Config::Features.DisableShaderPrewarm) { MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins(); } MGLOG_D("glslang initialized"); diff --git a/MobileGL/MG_Test/Program/CMakeLists.txt b/MobileGL/MG_Test/Program/CMakeLists.txt index 6a9033de..ce34cd2e 100644 --- a/MobileGL/MG_Test/Program/CMakeLists.txt +++ b/MobileGL/MG_Test/Program/CMakeLists.txt @@ -183,6 +183,23 @@ target_link_libraries( ${LINK_LIBRARIES} ) +# Its own binary for the same reason: every case Initialize()s and Destroy()s the library. +add_executable( + ShaderPrewarmTest + ShaderPrewarmTest.cpp +) + +target_include_directories(ShaderPrewarmTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL +) + +target_link_libraries( + ShaderPrewarmTest PRIVATE + MobileGLTestMain + ${LINK_LIBRARIES} +) + add_executable( XfbBlockVaryingTest XfbBlockVaryingTest.cpp @@ -281,6 +298,7 @@ gtest_discover_tests(ParallelShaderCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES L # in-flight compile, and the two-phase replay links 48 programs across both flag states. gtest_discover_tests(OptimisticStatusTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300) gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300) +gtest_discover_tests(ShaderPrewarmTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) # Same reason again: several cases leave A links outstanding while B compiles and links. gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300) diff --git a/MobileGL/MG_Test/Program/ShaderPrewarmTest.cpp b/MobileGL/MG_Test/Program/ShaderPrewarmTest.cpp new file mode 100644 index 00000000..6ed10daf --- /dev/null +++ b/MobileGL/MG_Test/Program/ShaderPrewarmTest.cpp @@ -0,0 +1,111 @@ +// MobileGL - MobileGL/MG_Test/Program/ShaderPrewarmTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// MOBILEGL_DISABLE_SHADER_PREWARM. Initialize() builds glslang's built-in tables ahead of the +// first compile unless the variable is truthy; the CI test lanes set it, so these cases set or +// clear it themselves. MG_ConfigLoader::Init() rereads the environment on every Initialize(), +// so the variable, not the feature block, is the input. Its own binary: every case calls +// Destroy(), and ShaderCompilePool's stop is a one-way latch (see AsyncTeardownTest). + +#include + +#include +#include + +#include "Config.h" +#include "Includes.h" +#include "Init.h" +#include "MG_Impl/GLImpl/Getter/GL_Getter.h" +#include "MG_Impl/GLImpl/Program/GL_Program.h" +#include "MG_Util/ShaderTranspiler/ShaderCompiler.h" + +using namespace MobileGL; +using namespace MobileGL::MG_Impl::GLImpl; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr const char* kPrewarmKnob = "MOBILEGL_DISABLE_SHADER_PREWARM"; + constexpr const char* kAsyncKnob = "MOBILEGL_ASYNC_SHADER_COMPILE"; + + // Sets (or, with nullptr, clears) one environment variable for a scope. + class ScopedEnv { + public: + ScopedEnv(const char* name, const char* value) : m_name(name) { + if (const char* old = std::getenv(name)) { + m_previous = old; + m_had = true; + } + Apply(value); + } + ~ScopedEnv() { Apply(m_had ? m_previous.c_str() : nullptr); } + ScopedEnv(const ScopedEnv&) = delete; + ScopedEnv& operator=(const ScopedEnv&) = delete; + + private: + void Apply(const char* value) const { +#if defined(_WIN32) + _putenv_s(m_name.c_str(), value != nullptr ? value : ""); +#else + if (value != nullptr) + setenv(m_name.c_str(), value, 1); + else + unsetenv(m_name.c_str()); +#endif + } + + std::string m_name, m_previous; + bool m_had = false; + }; + + GLint CompileStatusOf(const GLenum type, const char* source) { + const GLuint shader = CreateShader(type); + ShaderSource(shader, 1, &source, nullptr); + CompileShader(shader); + GLint status = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &status); + DeleteShader(shader); + return status; + } + + constexpr const char* kFragment460 = + "#version 460\nlayout(location = 0) out vec4 c;\nvoid main() { c = vec4(1.0); }\n"; +} // namespace + +TEST(ShaderPrewarm, InitializePrewarmsByDefaultAndAgainAfterDestroy) { + const ScopedEnv knob(kPrewarmKnob, nullptr); + const ScopedEnv async(kAsyncKnob, "1"); + MobileGL::Initialize(); + EXPECT_FALSE(MG_Config::Features.DisableShaderPrewarm); + EXPECT_TRUE(ShaderCompiler::BuiltinsPrewarmed()); + MobileGL::Destroy(); + // Destroy finalizes glslang, which frees the tables the latch stands for. + EXPECT_FALSE(ShaderCompiler::BuiltinsPrewarmed()); + MobileGL::Initialize(); + EXPECT_TRUE(ShaderCompiler::BuiltinsPrewarmed()); + MobileGL::Destroy(); +} + +TEST(ShaderPrewarm, ATruthyKnobSkipsThePrewarmAndCompilesStillWork) { + const ScopedEnv knob(kPrewarmKnob, "1"); + const ScopedEnv async(kAsyncKnob, "1"); + MobileGL::Initialize(); + EXPECT_TRUE(MG_Config::Features.DisableShaderPrewarm); + EXPECT_FALSE(ShaderCompiler::BuiltinsPrewarmed()); + EXPECT_EQ(CompileStatusOf(GL_FRAGMENT_SHADER, kFragment460), GL_TRUE); + EXPECT_FALSE(ShaderCompiler::BuiltinsPrewarmed()); + MobileGL::Destroy(); +} + +TEST(ShaderPrewarm, AFalsyKnobKeepsThePrewarm) { + const ScopedEnv knob(kPrewarmKnob, "0"); + const ScopedEnv async(kAsyncKnob, "1"); + MobileGL::Initialize(); + EXPECT_FALSE(MG_Config::Features.DisableShaderPrewarm); + EXPECT_TRUE(ShaderCompiler::BuiltinsPrewarmed()); + MobileGL::Destroy(); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index d0006113..788c1731 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -456,16 +456,20 @@ namespace MobileGL { g_builtinsPrewarmed = false; } + Bool ShaderCompiler::BuiltinsPrewarmed() { + return g_builtinsPrewarmed; + } + void ShaderCompiler::PrewarmBuiltins() { if (g_builtinsPrewarmed) return; g_builtinsPrewarmed = true; - // One vertex and one fragment shader is enough: the built-in table is cached - // per (version, spvVersion, profile, source), not per stage language, and - // both configurations CompileShader can reach - the declared-460 path and - // the retargeted-legacy path - resolve to the same combination here because - // ParseShaderSource always passes 460/ECoreProfile as the default. Parsing - // both anyway costs microseconds and keeps this honest if that ever changes. + // glslang caches one table per (version, spvVersion, profile, source), shared + // by every stage, and picks it from each shader's own #version: the 460 / + // ECoreProfile that ParseShaderSource passes is only the default for a source + // without one. So these parses build two tables, 460 core and 330 core, which + // the legacy #version 110-150 shaders normalise to; a shader declaring any + // other version builds its own on first use. static constexpr const char* kPrewarmVertexSource = "#version 460\nvoid main() { gl_Position = vec4(0.0); }\n"; static constexpr const char* kPrewarmFragmentSource = diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 6dfdcd5d..627f3145 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -510,9 +510,9 @@ namespace MobileGL { // caught: past it the words reach SPIRV-Cross, which is not a validator. static Result ValidateSpirvModule(const Vector& spirv); - // Parses one trivial shader in each configuration the production path can - // reach, on the calling thread, so the built-in symbol tables those - // configurations need are already cached before any worker asks for one. + // Parses trivial #version 460 and #version 330 core shaders on the calling + // thread, so those two built-in symbol tables are already cached before any + // worker asks for one. Every other declared version builds its own on first use. // // Why it matters: glslang builds a built-in TSymbolTable per distinct // (version, spvVersion, profile, source) combination, and does it under a @@ -533,6 +533,8 @@ namespace MobileGL { // it, the second eglInitialize of a process comes back up unwarmed and with // no way left to warm it. static void ResetPrewarmLatch(); + // Whether PrewarmBuiltins has run since the last ResetPrewarmLatch. + static Bool BuiltinsPrewarmed(); // Validation is an explicit immutable option of each compiler operation. The // program-link task snapshots MOBILEGL_ENABLE_SPIRV_VALIDATION before it can run