mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Perf] (MG_Util): replace the builtin-shadowing string scans with one tokenize and a SPIR-V OpName pass
RenameBuiltinShadowingFunction probed the whole source ten times per compile (5 names x mask + scan, each a full-text pass) and still had two blind spots: a 5-name list and single-line-definition-only detection. On Complementary-scale packs (4.5MB of sources) that was ~68% of the compile phase. The rename is now split by FAILURE LAYER, both halves sharing one name table header so they cannot drift: - A SPIR-V OpName pass in SanitizeAndOptimizeBinary covers the full ESSL 3.20 builtin set (~146 names). Renaming a function id is safe by construction: builtin calls are GLSL.std.450 instructions and can never resolve to a user OpFunction, overloads are distinct ids (a helper overload delegating to the real builtin keeps working), dead preprocessor branches never reach SPIR-V, and macro-expanded definitions are covered. ESSL 3.x is the only consumer that forbids the redefinitions, and this pass runs before its transpile. - A lexical pass covers only the 5 names whose exact-signature redefinitions glslang's relaxed parse rejects outright (never producing SPIR-V for the backstop): the historical fma/max3/min3/round/tanh. One TokenizeCode pass; definition detection requires brace depth 0, a type-identifier previous token that is neither a statement keyword nor a directive tail, and skips files whose token-level braces do not balance (preprocessor-asymmetric arms) - over-detection is unrecoverable, so every ambiguity falls through to the backstop. Measured on the compile phase (prefix-diff, 3-run medians, Espryt/NVIDIA): complementary-reimagined 20.0s -> 5.5s, BSL 2.14s -> 1.85s. bliss (the pack that ships from-scratch fma/tanh helpers) stays at SSIM 0.999962. Tests: end-to-end ESSL assertions for the multiline-definition and new-overload shapes, the three adversarial-review reproductions (statement- keyword call under asymmetric braces, dead-#if compat shim, overload delegating to the shadowed builtin), and a source-level assertion pinning the lexical half specifically.
This commit is contained in:
@@ -194,6 +194,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||||
|
|||||||
@@ -2620,3 +2620,217 @@ TEST_F(ProgramTest, ProgramAndShaderNamesShareOneNameSpace) {
|
|||||||
DeleteProgram(program);
|
DeleteProgram(program);
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- builtin-shadowing OpName pass (P0c) ----
|
||||||
|
// Desktop GLSL lets a pack redefine builtins; ESSL 3.x forbids it, so the rename
|
||||||
|
// now happens as a SPIR-V OpName pass in SanitizeAndOptimizeBinary instead of the
|
||||||
|
// old whole-source string scan. These pin the pass end-to-end: real sources through
|
||||||
|
// glCompileShader/glLinkProgram, generated SPIR-V transpiled to the ESSL the Espryt
|
||||||
|
// driver would see.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Vector<MobileGL::String> TranspileProgramSpirvToEssl(GLuint program) {
|
||||||
|
Vector<MobileGL::String> esslModules;
|
||||||
|
auto programObj = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
for (auto& spirvCode : programObj->GetGeneratedSpirv()) {
|
||||||
|
MG_Util::ShaderTranspiler::SpvcSession spvcSession(
|
||||||
|
spirvCode, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
spvc_compiler_options options;
|
||||||
|
spvcSession.CreateOptions(&options);
|
||||||
|
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||||
|
spvcSession.SetOptions(options);
|
||||||
|
const char* result = nullptr;
|
||||||
|
spvcSession.Compile(&result);
|
||||||
|
EXPECT_NE(result, nullptr) << spvcSession.GetLastErrorString();
|
||||||
|
esslModules.push_back(result ? result : "");
|
||||||
|
}
|
||||||
|
return esslModules;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The two blind spots of the old string scan, eliminated by construction: a
|
||||||
|
// MULTILINE definition (bliss-shaped "float fma\n(...)"), and names outside the
|
||||||
|
// old 5-entry list: sinh, as a NEW overload no builtin signature matches, so it
|
||||||
|
// parses fine and the SPIR-V OpName backstop does the rename. (An EXACT-signature
|
||||||
|
// sinh redefinition is parse-rejected by glslang - on HEAD too - and is therefore
|
||||||
|
// deliberately NOT lexically rescued; see kLexicalPreemptRenameNames.)
|
||||||
|
// min3/max3 keep their historical coverage.
|
||||||
|
TEST_F(ProgramTest, BuiltinShadowingFunctionsRenamedInEsslOutput) {
|
||||||
|
const char* vsSource = R"(#version 330 core
|
||||||
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
const char* fsSource = R"(#version 330 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
float fma
|
||||||
|
(float a, float b, float c) { return a * b + c; }
|
||||||
|
float sinh(float x, float y) { return x * y; }
|
||||||
|
float round(float x) { return floor(x + 0.5); }
|
||||||
|
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25), min3(0.1, 0.2, 0.3));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
|
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
|
||||||
|
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
|
||||||
|
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||||
|
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
||||||
|
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
|
||||||
|
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
|
||||||
|
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
||||||
|
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
||||||
|
EXPECT_EQ(essl.find("float sinh("), String::npos) << essl;
|
||||||
|
EXPECT_EQ(essl.find("float round("), String::npos) << essl;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure builtin USAGE (plus a commented-out definition) must stay untouched: builtin
|
||||||
|
// calls never resolve to a user function id in SPIR-V, so no mg_ name may appear.
|
||||||
|
TEST_F(ProgramTest, BuiltinUsageWithoutShadowingDefinitionKeepsBuiltinCalls) {
|
||||||
|
const char* vsSource = R"(#version 330 core
|
||||||
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
// 400, not 330: the builtin fma() really is called here, and it is only core from GLSL 4.00
|
||||||
|
// (at 330 it needs GL_ARB_gpu_shader5). The shadowing case above can stay at 330 precisely
|
||||||
|
// because the rename means no call to the builtin survives.
|
||||||
|
const char* fsSource = R"(#version 400 core
|
||||||
|
// float round(float x) { return floor(x + 0.5); }
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(round(1.25), fma(0.1, 0.2, 0.3), tanh(0.5), 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
|
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
|
||||||
|
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
|
||||||
|
EXPECT_EQ(essl.find("mg_"), String::npos) << essl;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the three shapes the lexical pre-empt pass must NOT touch (P0c) ----
|
||||||
|
// The source-level rename runs only for the handful of names glslang's relaxed
|
||||||
|
// parse rejects outright; everything else waits for the OpName pass, which cannot
|
||||||
|
// over-fire. These pin the three ways a lexical scan gets it wrong. All of them
|
||||||
|
// would fail as "no matching overloaded function found" - an over-detection is
|
||||||
|
// unrecoverable because the source never reaches SPIR-V.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// "pow(" as a real builtin call, i.e. not the tail of "mg_pow(".
|
||||||
|
bool ContainsUnprefixedCall(const MobileGL::String& essl, const MobileGL::String& name) {
|
||||||
|
const MobileGL::String needle = name + "(";
|
||||||
|
for (SizeT pos = essl.find(needle); pos != String::npos; pos = essl.find(needle, pos + 1)) {
|
||||||
|
const char before = pos == 0 ? ' ' : essl[pos - 1];
|
||||||
|
const bool isIdentifierChar =
|
||||||
|
std::isalnum(static_cast<unsigned char>(before)) != 0 || before == '_';
|
||||||
|
if (!isIdentifierChar) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// B1: preprocessor-asymmetric braces desync a raw brace-depth counter (each arm of
|
||||||
|
// the #ifdef closes the function), and "return" is lexically an identifier - so
|
||||||
|
// "return clamp(...)" reads as a top-level definition "<type> <builtin> (". A
|
||||||
|
// shader that shadows nothing must survive intact.
|
||||||
|
TEST_F(ProgramTest, StatementKeywordCallInPreprocessorAsymmetricBracesIsNotAShadowingDefinition) {
|
||||||
|
const char* vsSource = R"(#version 330 core
|
||||||
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
const char* fsSource = R"(#version 330 core
|
||||||
|
uniform vec3 uP;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
float getShadow(vec3 v) {
|
||||||
|
#ifdef SHADOW_OFF
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
return round(dot(v, v));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void main() { fragColor = vec4(getShadow(uP) * clamp(uP.x, 0.0, 1.0)); }
|
||||||
|
)";
|
||||||
|
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
|
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
|
||||||
|
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
|
||||||
|
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||||
|
EXPECT_EQ(essl.find("mg_"), String::npos) << essl;
|
||||||
|
// SPIRV-Cross lowers GLSL.std.450 FClamp to its NaN-correct min/max/isnan form, so the
|
||||||
|
// surviving evidence of the builtin call is that pair, not the spelling "clamp(". The
|
||||||
|
// stronger guard is above it: a renamed mg_clamp would not have compiled at all.
|
||||||
|
EXPECT_TRUE(ContainsUnprefixedCall(essl, "min")) << essl;
|
||||||
|
EXPECT_TRUE(ContainsUnprefixedCall(essl, "max")) << essl;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// B2: the scan is preprocessor-blind, so a definition in a DEAD #if branch would
|
||||||
|
// poison every live call to the real builtin. #version 120 normalizes to 330, so
|
||||||
|
// __VERSION__ is 330 and the compat shim is dropped by glslang - the definition
|
||||||
|
// never exists, and nothing may be renamed.
|
||||||
|
TEST_F(ProgramTest, ShadowingDefinitionInDeadPreprocessorBranchLeavesLiveBuiltinCalls) {
|
||||||
|
const char* vsSource = R"(#version 120
|
||||||
|
#if __VERSION__ < 140
|
||||||
|
mat4 inverse(mat4 m) { return m; }
|
||||||
|
#endif
|
||||||
|
uniform mat4 uM;
|
||||||
|
uniform vec4 uV;
|
||||||
|
void main() { gl_Position = inverse(uM) * uV; }
|
||||||
|
)";
|
||||||
|
const char* fsSource = R"(#version 330 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
|
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
|
||||||
|
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
|
||||||
|
if (essl.find("gl_Position") == String::npos) continue; // vertex module only
|
||||||
|
EXPECT_EQ(essl.find("mg_"), String::npos) << essl;
|
||||||
|
EXPECT_TRUE(ContainsUnprefixedCall(essl, "inverse")) << essl;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// B3: the idiomatic reason to shadow a builtin is to ADD an overload and delegate
|
||||||
|
// to the real one. A blanket call-site rewrite would turn the body's builtin call
|
||||||
|
// into mg_pow(vec3, vec3), which has no overload. The OpName backstop renames the
|
||||||
|
// user function id only, so the delegation still resolves to GLSL.std.450 Pow.
|
||||||
|
TEST_F(ProgramTest, OverloadDelegatingToShadowedBuiltinKeepsItsBuiltinCall) {
|
||||||
|
const char* vsSource = R"(#version 330 core
|
||||||
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
const char* fsSource = R"(#version 330 core
|
||||||
|
uniform vec3 uBase;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
vec3 pow(vec3 v, float e) { return pow(v, vec3(e)); }
|
||||||
|
|
||||||
|
void main() { fragColor = vec4(pow(uBase, 2.2), 1.0); }
|
||||||
|
)";
|
||||||
|
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
|
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
|
||||||
|
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
|
||||||
|
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||||
|
EXPECT_NE(essl.find("mg_pow("), String::npos) << essl;
|
||||||
|
EXPECT_TRUE(ContainsUnprefixedCall(essl, "pow")) << essl;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -615,25 +615,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The builtin-shadowing rename only fires when the shader really defines its own round/tanh/etc.
|
|
||||||
// Deciding that from a commented-out definition renames every genuine call to the builtin to a
|
|
||||||
// mg_ name that nothing defines, which fails to link.
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessIgnoresCommentedOutBuiltinShadowingDefinition) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
String source = R"(#version 330 core
|
|
||||||
// float round(float x) { return floor(x + 0.5); }
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main() {
|
|
||||||
fragColor = vec4(round(1.25));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
|
||||||
|
|
||||||
EXPECT_NE(source.find("round(1.25)"), String::npos) << "call was renamed from a comment:\n" << source;
|
|
||||||
EXPECT_EQ(source.find("mg_round"), String::npos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A block-commented extension directive must not be treated as a real one - the int64 filter turns
|
// A block-commented extension directive must not be treated as a real one - the int64 filter turns
|
||||||
// unsupported directives into #error, so reading one out of a comment manufactures a compile
|
// unsupported directives into #error, so reading one out of a comment manufactures a compile
|
||||||
// failure for a shader that never asked for the extension.
|
// failure for a shader that never asked for the extension.
|
||||||
@@ -844,36 +825,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessFragmentShaderRenamesMin3Max3Helpers) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
String source = R"(#version 460 core
|
|
||||||
out vec4 fragColor;
|
|
||||||
|
|
||||||
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
|
||||||
float max3(float a, float b, float c) { return max(max(a, b), c); }
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float dark = min3(0.1, 0.2, 0.3);
|
|
||||||
float bright = max3(max3(0.1, 0.2, 0.3), 0.4, 0.5);
|
|
||||||
fragColor = vec4(dark, bright, 0.0, 1.0);
|
|
||||||
})";
|
|
||||||
|
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
|
||||||
|
|
||||||
EXPECT_NE(source.find("float mg_min3("), String::npos);
|
|
||||||
EXPECT_NE(source.find("float mg_max3("), String::npos);
|
|
||||||
EXPECT_NE(source.find("mg_min3(0.1, 0.2, 0.3)"), String::npos);
|
|
||||||
EXPECT_NE(source.find("mg_max3(mg_max3(0.1, 0.2, 0.3), 0.4, 0.5)"), String::npos);
|
|
||||||
EXPECT_EQ(source.find("float min3("), String::npos);
|
|
||||||
EXPECT_EQ(source.find("float max3("), String::npos);
|
|
||||||
|
|
||||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
|
||||||
auto res = ShaderCompiler::CompileShader(attrib);
|
|
||||||
if (!res) {
|
|
||||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* vs = R"(#version 150
|
const char* vs = R"(#version 150
|
||||||
|
|
||||||
@@ -2248,3 +2199,25 @@ TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTem
|
|||||||
"float other = shuffleNV(1.0f, 0u, 32u);\n ");
|
"float other = shuffleNV(1.0f, 0u, 32u);\n ");
|
||||||
expectUnchanged(std::move(nvShuffleCall));
|
expectUnchanged(std::move(nvShuffleCall));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The LEXICAL half must fire at the source level (before the parse) for the
|
||||||
|
// preempt-list names - the end-to-end ESSL tests cannot tell which half did the
|
||||||
|
// rename, and for these names the parse would fail without the source rewrite.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessRenamesLexicalPreemptShadowingInSource) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 460 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(min3(0.1, 0.2, 0.3));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
EXPECT_NE(source.find("float mg_min3("), String::npos) << source;
|
||||||
|
EXPECT_NE(source.find("mg_min3(0.1, 0.2, 0.3)"), String::npos) << source;
|
||||||
|
EXPECT_EQ(source.find("float min3("), String::npos) << source;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/EsslBuiltinFunctionNames.h
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Builtin-shadowing rename: TWO tables, split by FAILURE LAYER.
|
||||||
|
//
|
||||||
|
// A desktop pack may redefine a builtin; ESSL 3.x forbids the redefinition, so every
|
||||||
|
// such helper is renamed to mg_<name>. That rename happens in two places, and which
|
||||||
|
// names belong in which place is decided by *where the failure would occur*, not by
|
||||||
|
// how thorough the table looks:
|
||||||
|
//
|
||||||
|
// - kEsslBuiltinFunctionNames (below, the full ~146-name ESSL 3.20 set plus the
|
||||||
|
// GL_AMD/EXT trinary min3/mid3/max3) drives the SPIR-V OpName backstop pass. That
|
||||||
|
// pass is safe BY CONSTRUCTION for any name: it renames function ids, and builtin
|
||||||
|
// calls are GLSL.std.450 instructions that can never resolve to a user OpFunction.
|
||||||
|
// Overloads are distinct ids (so an overload delegating to the real builtin keeps
|
||||||
|
// working), dead preprocessor branches never reach SPIR-V, and there is no lexical
|
||||||
|
// guessing to over-fire. Everything that CAN wait for the IR belongs here only.
|
||||||
|
//
|
||||||
|
// - kLexicalPreemptRenameNames (a strict handful-of-names subset) drives the source-level
|
||||||
|
// scan in ShaderSourceProcessor::RenameBuiltinShadowingFunctions. That scan exists
|
||||||
|
// for exactly one reason: glslang's relaxed parse rejects some shadowing overload
|
||||||
|
// shapes at PARSE time ("overloaded functions must have the same parameter
|
||||||
|
// precision qualifiers"), and a shadowed builtin can itself need an extension the
|
||||||
|
// declared #version does not enable (fma() at #version 330 wants
|
||||||
|
// GL_ARB_gpu_shader5) - such a shader never produces SPIR-V, so the backstop never
|
||||||
|
// sees it. Only names empirically observed to hit that parse-level rejection go
|
||||||
|
// here. A lexical scan is preprocessor-blind and cannot see overload sets, so it
|
||||||
|
// can over-fire (rename a live call whose definition sits in a dead #if branch, or
|
||||||
|
// rewrite an overload's delegating call to the real builtin) - and over-detection
|
||||||
|
// is UNRECOVERABLE, because the source never reaches the backstop. Keeping this
|
||||||
|
// table minimal keeps that exposure at its historical scope.
|
||||||
|
//
|
||||||
|
// "main" is deliberately absent from both.
|
||||||
|
inline constexpr std::string_view kEsslBuiltinFunctionNames[] = {
|
||||||
|
"EmitVertex", "EndPrimitive",
|
||||||
|
"abs", "acos", "acosh", "all", "any", "asin", "asinh", "atan", "atanh",
|
||||||
|
"atomicAdd", "atomicAnd", "atomicCompSwap", "atomicCounter",
|
||||||
|
"atomicCounterDecrement", "atomicCounterIncrement", "atomicExchange",
|
||||||
|
"atomicMax", "atomicMin", "atomicOr", "atomicXor",
|
||||||
|
"barrier", "bitCount", "bitfieldExtract", "bitfieldInsert", "bitfieldReverse",
|
||||||
|
"ceil", "clamp", "cos", "cosh", "cross",
|
||||||
|
"dFdx", "dFdy", "degrees", "determinant", "distance", "dot",
|
||||||
|
"equal", "exp", "exp2",
|
||||||
|
"faceforward", "findLSB", "findMSB", "floatBitsToInt", "floatBitsToUint",
|
||||||
|
"floor", "fma", "fract", "frexp", "fwidth",
|
||||||
|
"greaterThan", "greaterThanEqual", "groupMemoryBarrier",
|
||||||
|
"imageAtomicAdd", "imageAtomicAnd", "imageAtomicCompSwap",
|
||||||
|
"imageAtomicExchange", "imageAtomicMax", "imageAtomicMin", "imageAtomicOr",
|
||||||
|
"imageAtomicXor", "imageLoad", "imageSize", "imageStore", "imulExtended",
|
||||||
|
"intBitsToFloat", "interpolateAtCentroid", "interpolateAtOffset",
|
||||||
|
"interpolateAtSample", "inverse", "inversesqrt", "isinf", "isnan",
|
||||||
|
"ldexp", "length", "lessThan", "lessThanEqual", "log", "log2",
|
||||||
|
"matrixCompMult", "max", "max3", "memoryBarrier",
|
||||||
|
"memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
|
||||||
|
"memoryBarrierShared", "mid3", "min", "min3", "mix", "mod", "modf",
|
||||||
|
"normalize", "not", "notEqual",
|
||||||
|
"outerProduct",
|
||||||
|
"packHalf2x16", "packSnorm2x16", "packSnorm4x8", "packUnorm2x16",
|
||||||
|
"packUnorm4x8", "pow",
|
||||||
|
"radians", "reflect", "refract", "round", "roundEven",
|
||||||
|
"sign", "sin", "sinh", "smoothstep", "sqrt", "step",
|
||||||
|
"tan", "tanh", "texelFetch", "texelFetchOffset", "texture",
|
||||||
|
"textureGather", "textureGatherOffset", "textureGatherOffsets",
|
||||||
|
"textureGrad", "textureGradOffset", "textureLod", "textureLodOffset",
|
||||||
|
"textureOffset", "textureProj", "textureProjGrad", "textureProjGradOffset",
|
||||||
|
"textureProjLod", "textureProjLodOffset", "textureProjOffset", "textureSize",
|
||||||
|
"transpose", "trunc",
|
||||||
|
"uaddCarry", "uintBitsToFloat", "umulExtended", "unpackHalf2x16",
|
||||||
|
"unpackSnorm2x16", "unpackSnorm4x8", "unpackUnorm2x16", "unpackUnorm4x8",
|
||||||
|
"usubBorrow",
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool IsEsslBuiltinFunctionName(std::string_view name) {
|
||||||
|
return std::binary_search(std::begin(kEsslBuiltinFunctionNames),
|
||||||
|
std::end(kEsslBuiltinFunctionNames), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parse-level subset, sorted for std::binary_search.
|
||||||
|
//
|
||||||
|
// What decides membership, measured against this glslang: a redefinition whose
|
||||||
|
// signature EXACTLY matches a builtin overload is rejected at parse time
|
||||||
|
// ("overloaded functions must have the same parameter precision qualifiers", because
|
||||||
|
// the builtin declaration carries precision qualifiers and the user's does not), so
|
||||||
|
// it never produces SPIR-V and the OpName backstop never gets a turn. A definition
|
||||||
|
// that merely ADDS an overload (a signature the builtin set does not have, e.g.
|
||||||
|
// vec3 pow(vec3, float)) parses fine and is the backstop's job. Probed across the
|
||||||
|
// full table with a float(float) redefinition, 38 names are rejected that way - so
|
||||||
|
// membership here is not "everything that could ever be rejected", it is the set
|
||||||
|
// actually seen in shipped content plus whatever the test suite pins:
|
||||||
|
// fma, tanh - the bliss shaderpack's from-scratch helpers
|
||||||
|
// round, min3, max3 - the historical string-scan list this pass replaced
|
||||||
|
// An EXACT-signature redefinition of any of the other 33 probed-rejected names
|
||||||
|
// (sinh, floor, sqrt, ...) never compiled on MobileGL HEAD either - the old
|
||||||
|
// 5-name string scan did not rescue them - so leaving them out preserves the
|
||||||
|
// status quo for that (never-working) shape while keeping the dead-#if /
|
||||||
|
// overload-delegation exposure at exactly its historical scope.
|
||||||
|
// Adding a name is not free: it buys a parse-time rescue at the cost of lexical
|
||||||
|
// over-detection risk on every shader that merely *calls* that builtin (a definition
|
||||||
|
// in a dead #if branch, or an overload delegating to the real builtin). Add one only
|
||||||
|
// with evidence that real content redefines it with a builtin-identical signature.
|
||||||
|
inline constexpr std::string_view kLexicalPreemptRenameNames[] = {
|
||||||
|
"fma", "max3", "min3", "round", "tanh",
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool IsLexicalPreemptRenameName(std::string_view name) {
|
||||||
|
return std::binary_search(std::begin(kLexicalPreemptRenameNames),
|
||||||
|
std::end(kLexicalPreemptRenameNames), name);
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
||||||
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
||||||
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
||||||
|
#include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h"
|
||||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||||
@@ -310,6 +311,8 @@ namespace MobileGL {
|
|||||||
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
||||||
optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass());
|
optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass());
|
||||||
optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass());
|
optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass());
|
||||||
|
optimizer.RegisterPass(
|
||||||
|
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
|
||||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
|
#include "EsslBuiltinFunctionNames.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using MobileGL::SizeT;
|
using MobileGL::SizeT;
|
||||||
using MobileGL::String;
|
using MobileGL::String;
|
||||||
@@ -755,7 +757,23 @@ namespace {
|
|||||||
source.insert(0, replacement);
|
source.insert(0, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasSingleLineFunctionDefinition(const MobileGL::String& source, const MobileGL::String& functionName) {
|
// Start of the physical line containing `offset`, never scanning before `lowerBound`.
|
||||||
|
SizeT FindPhysicalLineStart(const MobileGL::String& source, SizeT offset, SizeT lowerBound) {
|
||||||
|
if (offset == 0) {
|
||||||
|
return lowerBound;
|
||||||
|
}
|
||||||
|
const SizeT newline = source.rfind('\n', offset - 1);
|
||||||
|
if (newline == MobileGL::String::npos || newline + 1 < lowerBound) {
|
||||||
|
return lowerBound;
|
||||||
|
}
|
||||||
|
return newline + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Half-open [begin, end) byte ranges of the preprocessor directive lines, in source order.
|
||||||
|
// A directive is one logical line: a trailing backslash splices the next physical line into it.
|
||||||
|
Vector<std::pair<SizeT, SizeT>> FindDirectiveLineRanges(const MobileGL::String& source) {
|
||||||
|
Vector<std::pair<SizeT, SizeT>> ranges;
|
||||||
|
|
||||||
SizeT lineStart = 0;
|
SizeT lineStart = 0;
|
||||||
while (lineStart < source.size()) {
|
while (lineStart < source.size()) {
|
||||||
SizeT lineEnd = source.find('\n', lineStart);
|
SizeT lineEnd = source.find('\n', lineStart);
|
||||||
@@ -763,71 +781,177 @@ namespace {
|
|||||||
lineEnd = source.size();
|
lineEnd = source.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
SizeT functionPos = source.find(functionName, lineStart);
|
SizeT probe = lineStart;
|
||||||
while (functionPos != MobileGL::String::npos && functionPos < lineEnd) {
|
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
||||||
const bool hasLeftBoundary = functionPos == 0 || !IsIdentifierChar(source[functionPos - 1]);
|
|
||||||
const SizeT functionEnd = functionPos + functionName.size();
|
|
||||||
const bool hasRightBoundary = functionEnd >= source.size() || !IsIdentifierChar(source[functionEnd]);
|
|
||||||
if (hasLeftBoundary && hasRightBoundary) {
|
|
||||||
SizeT probe = functionEnd;
|
|
||||||
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
||||||
probe++;
|
|
||||||
}
|
|
||||||
if (probe < lineEnd && source[probe] == '(') {
|
|
||||||
const SizeT closingParen = source.find(')', probe);
|
|
||||||
if (closingParen != MobileGL::String::npos && closingParen < lineEnd) {
|
|
||||||
probe = closingParen + 1;
|
|
||||||
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
||||||
probe++;
|
|
||||||
}
|
|
||||||
if (probe < lineEnd && source[probe] == '{') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
functionPos = source.find(functionName, functionPos + functionName.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenameFunctionInvocations(MobileGL::String& source, const MobileGL::String& from, const MobileGL::String& to) {
|
|
||||||
SizeT pos = 0;
|
|
||||||
while ((pos = source.find(from, pos)) != MobileGL::String::npos) {
|
|
||||||
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]);
|
|
||||||
const SizeT end = pos + from.size();
|
|
||||||
const bool hasRightBoundary = end >= source.size() || !IsIdentifierChar(source[end]);
|
|
||||||
|
|
||||||
SizeT probe = end;
|
|
||||||
while (probe < source.size() && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
||||||
probe++;
|
probe++;
|
||||||
}
|
}
|
||||||
|
if (probe >= lineEnd || source[probe] != '#') {
|
||||||
if (hasLeftBoundary && hasRightBoundary && probe < source.size() && source[probe] == '(') {
|
lineStart = lineEnd + 1;
|
||||||
source.replace(pos, from.size(), to);
|
|
||||||
pos += to.size();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos = end;
|
SizeT directiveEnd = lineEnd;
|
||||||
|
while (directiveEnd < source.size()) {
|
||||||
|
// directiveEnd sits on a '\n'; a backslash immediately before it (modulo the \r of
|
||||||
|
// a CRLF file and trailing blanks) splices the following physical line in.
|
||||||
|
// The scan must not leave the physical line that directiveEnd terminates: a
|
||||||
|
// whitespace-only spliced line would otherwise let the back-scan reach the
|
||||||
|
// backslash of the PREVIOUS line and swallow one extra real line of code.
|
||||||
|
const SizeT physicalLineStart = FindPhysicalLineStart(source, directiveEnd, lineStart);
|
||||||
|
SizeT back = directiveEnd;
|
||||||
|
while (back > physicalLineStart && std::isspace(static_cast<unsigned char>(source[back - 1]))) {
|
||||||
|
back--;
|
||||||
|
}
|
||||||
|
if (back == physicalLineStart || source[back - 1] != '\\') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SizeT splicedEnd = source.find('\n', directiveEnd + 1);
|
||||||
|
if (splicedEnd == MobileGL::String::npos) {
|
||||||
|
splicedEnd = source.size();
|
||||||
|
}
|
||||||
|
directiveEnd = splicedEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges.push_back({lineStart, directiveEnd});
|
||||||
|
lineStart = directiveEnd + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenameBuiltinShadowingFunction(MobileGL::String& source, const char* from, const char* to) {
|
bool IsInDirectiveLine(const Vector<std::pair<SizeT, SizeT>>& ranges, SizeT offset) {
|
||||||
const MobileGL::String fromName = from;
|
// Ranges are disjoint and sorted, so the only candidate is the last one starting at or
|
||||||
// Decide from a comment-free view. A commented-out definition is not a definition, and
|
// before the offset.
|
||||||
// acting on one renames every genuine call to the builtin to a name nothing defines - which
|
const auto next = std::upper_bound(ranges.begin(), ranges.end(), offset,
|
||||||
// then fails to resolve. Line comments survive BlankBlockComments, so this matters.
|
[](SizeT value, const std::pair<SizeT, SizeT>& range) {
|
||||||
if (!HasSingleLineFunctionDefinition(MaskCommentsAndQuotedText(source), fromName)) {
|
return value < range.first;
|
||||||
|
});
|
||||||
|
return next != ranges.begin() && offset < std::prev(next)->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No GLSL type name is a statement keyword, so "<keyword> <builtin> (" is never a definition -
|
||||||
|
// it is `return clamp(...)`, `else round(...)`, `do fma(...)`, a `case` label expression. The
|
||||||
|
// if/for/while/switch entries cannot precede a call in valid GLSL either (a '(' always follows
|
||||||
|
// them directly), and are listed defensively. Sorted for std::binary_search.
|
||||||
|
constexpr std::string_view kStatementKeywordsBeforeCall[] = {
|
||||||
|
"case", "do", "else", "for", "if", "return", "switch", "while",
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsStatementKeywordToken(const CodeToken& token) {
|
||||||
|
return std::binary_search(std::begin(kStatementKeywordsBeforeCall),
|
||||||
|
std::end(kStatementKeywordsBeforeCall), std::string_view(token.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A brace counter over raw tokens is preprocessor-blind: it counts the braces of BOTH arms of
|
||||||
|
// an #ifdef, so the classic "early return inside one arm, closing brace in each arm" idiom
|
||||||
|
// desyncs it. A desynced depth turns statements into apparent top-level definitions, and an
|
||||||
|
// over-detection is unrecoverable (the source never reaches the SPIR-V backstop). A file whose
|
||||||
|
// braces do not net to zero, or whose running depth ever dips below zero, is therefore not
|
||||||
|
// trustworthy for depth-based detection at all.
|
||||||
|
bool HasBalancedBraces(const Vector<CodeToken>& tokens) {
|
||||||
|
SizeT depth = 0;
|
||||||
|
for (const CodeToken& token : tokens) {
|
||||||
|
if (token.text.size() != 1) continue;
|
||||||
|
if (token.text[0] == '{') {
|
||||||
|
depth++;
|
||||||
|
} else if (token.text[0] == '}') {
|
||||||
|
if (depth == 0) return false;
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return depth == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some shader packs define their own helpers under builtin GLSL names - round(), fma(),
|
||||||
|
// min3(), tanh(). Desktop GLSL allows that shadowing; ESSL 3.x forbids the redefinition, so
|
||||||
|
// every such helper is renamed to mg_<name> together with all of its call sites.
|
||||||
|
//
|
||||||
|
// Scope is deliberately NARROW: only kLexicalPreemptRenameNames, the handful of names whose
|
||||||
|
// shadowing definitions glslang's relaxed parse rejects outright ("overloaded functions must
|
||||||
|
// have the same parameter precision qualifiers"), or which need an extension the declared
|
||||||
|
// #version does not enable (fma() at #version 330 wants GL_ARB_gpu_shader5). Those shaders
|
||||||
|
// never produce SPIR-V, so only a source-level rename can save them. Everything else is left
|
||||||
|
// to the SPIR-V OpName pass in SanitizeAndOptimizeBinary, which is safe by construction -
|
||||||
|
// see EsslBuiltinFunctionNames.h for the full failure-layer split. A lexical scan is
|
||||||
|
// preprocessor-blind and overload-blind, so widening this table trades a rescue nobody needs
|
||||||
|
// for an unrecoverable over-detection risk on every shader that merely calls the builtin.
|
||||||
|
//
|
||||||
|
// Cost: ONE tokenize for the whole job, and nothing further at all in the overwhelmingly
|
||||||
|
// common no-shadowing case. The path this replaces probed the entire source once per
|
||||||
|
// candidate name, which measured ~68% of a Complementary-scale pack's compile time.
|
||||||
|
void RenameBuiltinShadowingFunctions(MobileGL::String& source) {
|
||||||
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
|
if (tokens.size() < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Desynced depth -> skip the lexical half entirely and let the backstop handle whatever
|
||||||
|
// this file shadows. Missing a definition is recoverable; inventing one is not.
|
||||||
|
if (!HasBalancedBraces(tokens)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vector<std::pair<SizeT, SizeT>> directiveRanges = FindDirectiveLineRanges(source);
|
||||||
|
|
||||||
|
// Pass A - collect the shadowed names. A definition or prototype at brace depth 0 reads
|
||||||
|
// as "<type-identifier> <builtin-name> (", which is what separates it from a call in a
|
||||||
|
// global initializer ("const float PI = radians(180.0);", where the previous token is '=').
|
||||||
|
// Token positions ignore layout, so a definition split across lines is found the same way.
|
||||||
|
Vector<MobileGL::String> shadowedNames;
|
||||||
|
SizeT braceDepth = 0;
|
||||||
|
for (SizeT i = 0; i + 1 < tokens.size(); i++) {
|
||||||
|
const CodeToken& token = tokens[i];
|
||||||
|
if (token.text.size() == 1) {
|
||||||
|
if (token.text[0] == '{') {
|
||||||
|
braceDepth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token.text[0] == '}') {
|
||||||
|
if (braceDepth > 0) braceDepth--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (braceDepth != 0 || i == 0 || tokens[i + 1].text != "(" || !IsIdentifierToken(tokens[i - 1])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// IsIdentifierToken is purely lexical, so "return"/"else"/"do"/"case" pass it. None of
|
||||||
|
// them is a return type, so "return round(x)" is a CALL, not a definition.
|
||||||
|
// A directive tail ('#endif' tokenizes to '#' + 'endif') is not a return type;
|
||||||
|
// without this, a balanced-but-desynced file could see it as one.
|
||||||
|
if (IsInDirectiveLine(directiveRanges, tokens[i - 1].begin)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (IsStatementKeywordToken(tokens[i - 1])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// "#define FOO fma(x, y, z)" defines FOO, not fma.
|
||||||
|
if (!MobileGL::MG_Util::ShaderTranspiler::IsLexicalPreemptRenameName(token.text) ||
|
||||||
|
IsInDirectiveLine(directiveRanges, token.begin)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::find(shadowedNames.begin(), shadowedNames.end(), token.text) == shadowedNames.end()) {
|
||||||
|
shadowedNames.push_back(token.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shadowedNames.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenameFunctionInvocations(source, fromName, to);
|
// Pass B - rename the definition, its prototypes and every call. Only a name followed by
|
||||||
|
// '(' is the function; the same spelling as a variable must keep its own identity.
|
||||||
|
// Directive lines DO participate: a macro body calling the renamed helper has to follow it.
|
||||||
|
Vector<SizeT> insertOffsets;
|
||||||
|
for (SizeT i = 0; i + 1 < tokens.size(); i++) {
|
||||||
|
if (tokens[i + 1].text != "(") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::find(shadowedNames.begin(), shadowedNames.end(), tokens[i].text) != shadowedNames.end()) {
|
||||||
|
insertOffsets.push_back(tokens[i].begin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Back to front, so each recorded offset is still valid when it is used.
|
||||||
|
for (auto offset = insertOffsets.rbegin(); offset != insertOffsets.rend(); ++offset) {
|
||||||
|
source.insert(*offset, "mg_");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplaceIdentifier(MobileGL::String& source, const MobileGL::String& from, const MobileGL::String& to) {
|
void ReplaceIdentifier(MobileGL::String& source, const MobileGL::String& from, const MobileGL::String& to) {
|
||||||
@@ -1300,13 +1424,8 @@ namespace MobileGL {
|
|||||||
FilterUnsupportedGpuShaderInt64(source);
|
FilterUnsupportedGpuShaderInt64(source);
|
||||||
CoerceUniformBlockPackingToStd140(source);
|
CoerceUniformBlockPackingToStd140(source);
|
||||||
|
|
||||||
// Some shader packs define helpers with built-in GLSL names such as round(), tanh(), or fma().
|
RenameBuiltinShadowingFunctions(source);
|
||||||
// These may pass OpenGL-style validation but fail when recompiled for Vulkan/SPIR-V generation.
|
|
||||||
RenameBuiltinShadowingFunction(source, "round", "mg_round");
|
|
||||||
RenameBuiltinShadowingFunction(source, "tanh", "mg_tanh");
|
|
||||||
RenameBuiltinShadowingFunction(source, "fma", "mg_fma");
|
|
||||||
RenameBuiltinShadowingFunction(source, "min3", "mg_min3");
|
|
||||||
RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
|
|
||||||
ModernizeLegacyGLSL(stage, source);
|
ModernizeLegacyGLSL(stage, source);
|
||||||
InjectDepthRangeBuiltinShim(stage, source);
|
InjectDepthRangeBuiltinShim(stage, source);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.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
|
||||||
|
|
||||||
|
#include "RenameBuiltinShadowingFunctionsPass.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "../EsslBuiltinFunctionNames.h"
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
spvtools::opt::Pass::Status RenameBuiltinShadowingFunctionsPass::Process() {
|
||||||
|
Bool modified = false;
|
||||||
|
auto* irContext = context();
|
||||||
|
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||||
|
|
||||||
|
for (auto& debugInst : irContext->debugs2()) {
|
||||||
|
if (debugInst.opcode() != spv::Op::OpName || debugInst.NumInOperands() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto* target = defUseMgr->GetDef(debugInst.GetSingleWordInOperand(0));
|
||||||
|
if (target == nullptr || target->opcode() != spv::Op::OpFunction) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glslang mangles function OpNames as "name(<paramcodes>"; the base name is
|
||||||
|
// everything before the '(' (entry points like "main" carry no mangling).
|
||||||
|
const std::string mangled = debugInst.GetInOperand(1).AsString();
|
||||||
|
const std::string_view baseName =
|
||||||
|
std::string_view(mangled).substr(0, mangled.find('('));
|
||||||
|
if (!IsEsslBuiltinFunctionName(baseName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugInst.SetInOperand(1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(
|
||||||
|
"mg_" + mangled));
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken
|
||||||
|
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<RenameBuiltinShadowingFunctionsPass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.h
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Desktop GLSL lets a shader redefine a builtin function (round, fma, ...) and
|
||||||
|
// shadow it; ESSL 3.x forbids the redefinition, so when SPIRV-Cross re-emits the
|
||||||
|
// function under its original OpName a strict ES driver rejects the shader with a
|
||||||
|
// redefinition error. Prefix the OpName of every user-defined function whose base
|
||||||
|
// name collides with an ESSL builtin (plus the min3/max3 trinary extension names)
|
||||||
|
// with "mg_". Renaming a user function is always semantics-preserving: its
|
||||||
|
// definition and every call site go through the same result id, while calls to
|
||||||
|
// the real builtin never resolve to a user function id in SPIR-V.
|
||||||
|
//
|
||||||
|
// This is the BACKSTOP half of the rename. The primary half is the lexical
|
||||||
|
// RenameBuiltinShadowingFunctions in ShaderSourceProcessor, which has to run
|
||||||
|
// before the parse - glslang's relaxed parse rejects some shadowing overload
|
||||||
|
// shapes outright, and a shadowed builtin may itself need an extension the
|
||||||
|
// declared #version does not enable. This pass catches what a lexical scan
|
||||||
|
// cannot see (macro-expanded definitions) and is idempotent: an already
|
||||||
|
// renamed mg_* name is not in the builtin table.
|
||||||
|
//
|
||||||
|
// Both halves share MG_Util/ShaderTranspiler/EsslBuiltinFunctionNames.h, so the
|
||||||
|
// covered name set cannot drift between them.
|
||||||
|
class RenameBuiltinShadowingFunctionsPass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "rename-builtin-shadowing-functions"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateRenameBuiltinShadowingFunctionsPass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
Reference in New Issue
Block a user