mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (MobileGL, MG_Test/Program, MG_Util/ShaderTranspiler): add MOBILEGL_DISABLE_SHADER_PREWARM
- Initialize() still prewarms glslang's built-in tables by default; a truthy MOBILEGL_DISABLE_SHADER_PREWARM skips it, for short-lived processes that never repay the ~100 ms - ShaderPrewarmTest covers the default, a truthy and a falsy value, and the latch Destroy clears; red with the gate removed and with the reset removed - the prewarm comments said both parses share one table; glslang picks it from each shader's own #version, so they build 460 core and 330 core
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
+2
-1
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <gtest/gtest.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#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();
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -510,9 +510,9 @@ namespace MobileGL {
|
||||
// caught: past it the words reach SPIRV-Cross, which is not a validator.
|
||||
static Result<void> ValidateSpirvModule(const Vector<Uint32>& 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
|
||||
|
||||
Reference in New Issue
Block a user