// MobileGL - MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.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 "ShaderSourceProcessor.h" #include #include #include #include #include #include #include #include #include #include "EsslBuiltinFunctionNames.h" namespace { using MobileGL::SizeT; using MobileGL::String; using MobileGL::Uint32; using MobileGL::Vector; bool IsIdentifierChar(char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; } bool IsIdentifierStart(char ch) { return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; } // Return a copy of `source` with every comment and string-literal interior blanked to spaces. // // The passes that follow answer lexical questions ("is this identifier real code?", "where does // the #version line end?"), so comment and literal text has to stop being visible to them - but // it must not be *deleted*: replacing the bytes with spaces keeps every offset 1:1 with the // original, so an edit collected against the mask applies verbatim to the source, and keeping // newlines means glslang's diagnostics still point at the line the application wrote. // // It also has to be lexically stateful. A banner line such as // // //*** lighting pass *** // // contains "/*" one byte in, and a naive search for that opener treats the rest of the file as // an unterminated comment. MobileGL::String MaskCommentsAndQuotedText(const MobileGL::String& source) { enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText }; MobileGL::String masked = source; Region region = Region::Code; char quote = '\0'; bool escaped = false; for (SizeT pos = 0; pos < source.size(); pos++) { const char ch = source[pos]; const char next = pos + 1 < source.size() ? source[pos + 1] : '\0'; if (region == Region::Code) { if (ch == '/' && next == '/') { masked[pos] = ' '; masked[pos + 1] = ' '; pos++; region = Region::SingleLineComment; } else if (ch == '/' && next == '*') { masked[pos] = ' '; masked[pos + 1] = ' '; pos++; region = Region::MultiLineComment; } else if (ch == '"' || ch == '\'') { masked[pos] = ' '; quote = ch; escaped = false; region = Region::QuotedText; } continue; } if (region == Region::SingleLineComment) { if (ch == '\n' || ch == '\r') { region = Region::Code; } else { masked[pos] = ' '; } continue; } if (region == Region::MultiLineComment) { if (ch == '*' && next == '/') { masked[pos] = ' '; masked[pos + 1] = ' '; pos++; region = Region::Code; } else if (ch != '\n' && ch != '\r') { masked[pos] = ' '; } continue; } // GLSL has no multi-line string literals, so a quote that reaches end of line was never // a literal to begin with - most likely an apostrophe in a #error or #pragma message. // Ending the region here keeps one stray apostrophe from swallowing the rest of the file // for every consumer of this mask: the tokenizer, the #version inspection, and the // explicit-location / opaque-binding extractors all go blind past that point otherwise. if (ch == '\n' || ch == '\r') { region = Region::Code; continue; } masked[pos] = ' '; if (escaped) { escaped = false; } else if (ch == '\\') { escaped = true; } else if (ch == quote) { region = Region::Code; } } return masked; } struct CodeToken { String text; SizeT begin = 0; SizeT end = 0; }; Vector TokenizeCode(const String& source) { const String masked = MaskCommentsAndQuotedText(source); Vector tokens; tokens.reserve(source.size() / 4); SizeT pos = 0; while (pos < masked.size()) { const char ch = masked[pos]; if (std::isspace(static_cast(ch))) { ++pos; continue; } const SizeT begin = pos; if (IsIdentifierStart(ch)) { ++pos; while (pos < masked.size() && IsIdentifierChar(masked[pos])) { ++pos; } } else if (std::isdigit(static_cast(ch))) { ++pos; while (pos < masked.size()) { const char numberChar = masked[pos]; if (!IsIdentifierChar(numberChar) && numberChar != '.') { break; } ++pos; } } else { ++pos; if (pos < masked.size()) { const String twoChars = masked.substr(begin, 2); if (twoChars == "==" || twoChars == "!=" || twoChars == "<=" || twoChars == ">=" || twoChars == "+=" || twoChars == "-=" || twoChars == "<<" || twoChars == ">>" || twoChars == "++" || twoChars == "--" || twoChars == "&&" || twoChars == "||") { ++pos; } } } tokens.push_back(CodeToken{source.substr(begin, pos - begin), begin, pos}); } return tokens; } bool IsIdentifierToken(const CodeToken& token) { if (token.text.empty() || !IsIdentifierStart(token.text.front())) { return false; } return std::all_of(token.text.begin() + 1, token.text.end(), IsIdentifierChar); } class TokenCursor { public: TokenCursor(const Vector& tokens, SizeT position) : m_tokens(tokens), m_position(position) {} bool Consume(const char* expected) { if (m_position >= m_tokens.size() || m_tokens[m_position].text != expected) { return false; } ++m_position; return true; } bool ConsumeAnyIdentifier(String& identifier) { if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) { return false; } identifier = m_tokens[m_position++].text; return true; } bool ConsumeAnyIdentifier() { if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) { return false; } ++m_position; return true; } bool ConsumeIdentifier(const String& expected) { if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position]) || m_tokens[m_position].text != expected) { return false; } ++m_position; return true; } SizeT Position() const { return m_position; } private: const Vector& m_tokens; SizeT m_position; }; SizeT CountToken(const Vector& tokens, const String& tokenText) { return static_cast(std::count_if(tokens.begin(), tokens.end(), [&](const CodeToken& token) { return token.text == tokenText; })); } bool HasIdentifierWithPrefixOutsideAllowed(const Vector& tokens, const String& prefix, std::initializer_list allowedIdentifiers) { return std::any_of(tokens.begin(), tokens.end(), [&](const CodeToken& token) { if (!IsIdentifierToken(token) || !token.text.starts_with(prefix)) { return false; } return std::none_of(allowedIdentifiers.begin(), allowedIdentifiers.end(), [&](const char* allowed) { return token.text == allowed; }); }); } bool MatchTokenSequence(const Vector& tokens, SizeT position, std::initializer_list expected) { if (position + expected.size() > tokens.size()) { return false; } for (const char* token : expected) { if (tokens[position++].text != token) { return false; } } return true; } SizeT CountTokenSequence(const Vector& tokens, std::initializer_list expected) { SizeT count = 0; for (SizeT position = 0; position < tokens.size(); ++position) { if (MatchTokenSequence(tokens, position, expected)) { ++count; } } return count; } bool FindUniqueTokenSequence(const Vector& tokens, const Vector& expected, SizeT& sourceBegin, SizeT& sourceEnd) { if (expected.empty() || expected.size() > tokens.size()) { return false; } SizeT matchCount = 0; for (SizeT position = 0; position + expected.size() <= tokens.size(); ++position) { bool matches = true; for (SizeT expectedIndex = 0; expectedIndex < expected.size(); ++expectedIndex) { if (tokens[position + expectedIndex].text != expected[expectedIndex].text) { matches = false; break; } } if (!matches) { continue; } ++matchCount; sourceBegin = tokens[position].begin; sourceEnd = tokens[position + expected.size() - 1].end; } return matchCount == 1; } bool IsPowerOfTwo(Uint32 value) { return value != 0u && (value & (value - 1u)) == 0u; } struct LinearPrefixScanMatch { SizeT sharedArraySizeBegin = 0; SizeT sharedArraySizeEnd = 0; SizeT scanBegin = 0; SizeT scanEnd = 0; String cache; String importance; String prefixSum; String loopLength; String loopIndex; String sum; }; bool ParseLinearPrefixScanTemplate(const Vector& tokens, LinearPrefixScanMatch& match) { // The workaround deliberately recognizes one complete algorithm, not merely the // subgroupInclusiveAdd token. Changing scratch storage is only safe when that storage is // private to this scan and the workgroup has exactly 1024 X invocations. SizeT localSizeDeclarationCount = 0; for (SizeT i = 0; i < tokens.size(); ++i) { if (MatchTokenSequence(tokens, i, {"layout", "(", "local_size_x", "=", "1024", ")", "in", ";"})) { ++localSizeDeclarationCount; } } if (localSizeDeclarationCount != 1) { return false; } SizeT sharedDeclarationIndex = String::npos; SizeT sharedDeclarationCount = 0; String cacheName; for (SizeT i = 0; i + 6 < tokens.size(); ++i) { if (tokens[i].text != "shared" || tokens[i + 1].text != "float" || !IsIdentifierToken(tokens[i + 2]) || tokens[i + 3].text != "[" || tokens[i + 4].text != "64" || tokens[i + 5].text != "]" || tokens[i + 6].text != ";") { continue; } ++sharedDeclarationCount; sharedDeclarationIndex = i; cacheName = tokens[i + 2].text; } if (sharedDeclarationCount != 1) { return false; } SizeT scanTokenIndex = String::npos; SizeT scanCount = 0; for (SizeT i = 0; i + 7 < tokens.size(); ++i) { if (tokens[i].text == "float" && IsIdentifierToken(tokens[i + 1]) && tokens[i + 2].text == "=" && tokens[i + 3].text == "subgroupInclusiveAdd" && tokens[i + 4].text == "(" && IsIdentifierToken(tokens[i + 5]) && tokens[i + 6].text == ")" && tokens[i + 7].text == ";") { ++scanCount; scanTokenIndex = i; } } if (scanCount != 1 || sharedDeclarationIndex >= scanTokenIndex) { return false; } TokenCursor cursor(tokens, scanTokenIndex); String prefixSum; String importance; String loopLength; String loopIndex; String sum; if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier(prefixSum) || !cursor.Consume("=") || !cursor.Consume("subgroupInclusiveAdd") || !cursor.Consume("(") || !cursor.ConsumeAnyIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupInvocationID") || !cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") || !cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("uint") || !cursor.ConsumeAnyIdentifier(loopLength) || !cursor.Consume("=") || !cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("findMSB") || !cursor.Consume("(") || !cursor.Consume("gl_NumSubgroups") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("+=") || !cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("gl_NumSubgroups") || !cursor.Consume("-") || !cursor.Consume("(") || !cursor.Consume("1u") || !cursor.Consume("<<") || !cursor.Consume("(") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") || !cursor.Consume("0u") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("for") || !cursor.Consume("(") || !cursor.Consume("uint") || !cursor.ConsumeAnyIdentifier(loopIndex) || !cursor.Consume("=") || !cursor.Consume("0") || !cursor.Consume(";") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume(";") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("++") || !cursor.Consume(")") || !cursor.Consume("{") || !cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("&") || !cursor.Consume("(") || !cursor.Consume("1u") || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") || !cursor.Consume("0u") || !cursor.Consume(")") || !cursor.Consume("{") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume("+=") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume(">>") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume(")") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume("]") || !cursor.Consume(";") || !cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupInvocationID") || !cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("barrier") || !cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") || !cursor.Consume("x") || !cursor.Consume("==") || !cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("1024") || !cursor.Consume("-") || !cursor.Consume("1") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") || !cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("float") || !cursor.ConsumeAnyIdentifier(sum) || !cursor.Consume("=") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume(";")) { return false; } const SizeT scanEndToken = cursor.Position() - 1; // Require the scan's immediate consumer as well. This makes the match specific to a // linear distribution warp, and avoids changing unrelated prefix scans which may rely on // the implementation's native subgroup partitioning. if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier() || !cursor.Consume("=") || !cursor.Consume("(") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume("-") || !cursor.ConsumeIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume("/") || !cursor.ConsumeIdentifier(sum) || !cursor.Consume("-") || !cursor.Consume("float") || !cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") || !cursor.Consume("x") || !cursor.Consume("+") || !cursor.Consume("1u") || !cursor.Consume(")") || !cursor.Consume("/") || !cursor.Consume("float") || !cursor.Consume("(") || !cursor.Consume("1024") || !cursor.Consume(")") || !cursor.Consume(";")) { return false; } // No other use may share the scratch array, and no additional subgroup operation or // builtin may silently retain native-64 semantics after this module becomes virtual-32. if (CountToken(tokens, cacheName) != 6 || CountToken(tokens, "subgroupInclusiveAdd") != 1 || CountToken(tokens, "gl_SubgroupInvocationID") != 2 || CountToken(tokens, "gl_SubgroupSize") != 2 || CountToken(tokens, "gl_SubgroupID") != 4 || CountToken(tokens, "gl_NumSubgroups") != 2 || CountToken(tokens, "gl_LocalInvocationID") != 2 || CountToken(tokens, "barrier") != 3 || CountToken(tokens, "findMSB") != 1 || HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) || HasIdentifierWithPrefixOutsideAllowed( tokens, "gl_Subgroup", {"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"}) || // ARB/NV spellings of lane-width-sensitive builtins and functions // (gl_SubGroupSizeARB, ballotARB, gl_WarpSizeNV, shuffleNV, ...) must block the // rewrite just like their KHR counterparts: they would silently keep native-width // semantics in a module rewritten to the virtual 32-lane model. HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SubGroup", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Warp", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Thread", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SMID", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "ballot", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "shuffle", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "readInvocation", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "readFirstInvocation", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "anyInvocation", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "allInvocations", {})) { return false; } // The scan must be at the top level of the sole main() body. Its existing barriers already // require uniform control flow; this check prevents us from introducing extra barriers in // a nested branch or loop. SizeT mainOpenBrace = String::npos; SizeT mainCloseBrace = String::npos; SizeT mainCount = 0; for (SizeT i = 0; i + 4 < tokens.size(); ++i) { if (!MatchTokenSequence(tokens, i, {"void", "main", "(", ")", "{"})) { continue; } ++mainCount; mainOpenBrace = i + 4; int depth = 1; for (SizeT j = mainOpenBrace + 1; j < tokens.size(); ++j) { if (tokens[j].text == "{") ++depth; else if (tokens[j].text == "}" && --depth == 0) { mainCloseBrace = j; break; } } } if (mainCount != 1 || mainCloseBrace == String::npos || scanTokenIndex <= mainOpenBrace || scanEndToken >= mainCloseBrace) { return false; } int depthAtScan = 1; for (SizeT i = mainOpenBrace + 1; i < scanTokenIndex; ++i) { if (tokens[i].text == "{") ++depthAtScan; else if (tokens[i].text == "}") --depthAtScan; } if (depthAtScan != 1) { return false; } constexpr const char* injectedNames[] = {"mglPrefixScanLane", "mglVirtualSubgroupInvocation", "mglVirtualSubgroup", "mglVirtualSubgroupBase", "mglPrefixLane", "mglVirtualSubgroupCount"}; for (const char* injectedName : injectedNames) { if (CountToken(tokens, injectedName) != 0) { return false; } } match.sharedArraySizeBegin = tokens[sharedDeclarationIndex + 4].begin; match.sharedArraySizeEnd = tokens[sharedDeclarationIndex + 4].end; match.scanBegin = tokens[scanTokenIndex].begin; match.scanEnd = tokens[scanEndToken].end; match.cache = std::move(cacheName); match.importance = std::move(importance); match.prefixSum = std::move(prefixSum); match.loopLength = std::move(loopLength); match.loopIndex = std::move(loopIndex); match.sum = std::move(sum); return true; } String BuildLinearPrefixScanReplacement(const LinearPrefixScanMatch& match) { String replacement; replacement.reserve(1800); replacement += "uint mglPrefixScanLane = gl_LocalInvocationID.x;\n"; replacement += "uint mglVirtualSubgroupInvocation = mglPrefixScanLane & 31u;\n"; replacement += "uint mglVirtualSubgroup = mglPrefixScanLane >> 5u;\n"; replacement += "const uint mglVirtualSubgroupCount = 32u;\n"; replacement += match.cache + "[mglPrefixScanLane] = " + match.importance + ";\n"; replacement += "barrier();\n"; replacement += "float " + match.prefixSum + " = 0.0f;\n"; replacement += "uint mglVirtualSubgroupBase = mglVirtualSubgroup << 5u;\n"; replacement += "for (uint mglPrefixLane = mglVirtualSubgroupBase; " "mglPrefixLane <= mglPrefixScanLane; ++mglPrefixLane) {\n"; replacement += match.prefixSum + " += " + match.cache + "[mglPrefixLane];\n"; replacement += "}\n"; replacement += "barrier();\n"; replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache + "[mglVirtualSubgroup] = " + match.prefixSum + ";\n"; replacement += "barrier();\n"; replacement += "uint " + match.loopLength + " = uint(findMSB(mglVirtualSubgroupCount));\n"; replacement += match.loopLength + " += uint(mglVirtualSubgroupCount - (1u << (" + match.loopLength + " - 1u)) > 0u);\n"; replacement += "for (uint " + match.loopIndex + " = 0u; " + match.loopIndex + " < " + match.loopLength + "; ++" + match.loopIndex + ") {\n"; replacement += "if ((mglVirtualSubgroup & (1u << " + match.loopIndex + ")) > 0u) {\n"; replacement += match.prefixSum + " += " + match.cache + "[(mglVirtualSubgroup >> " + match.loopIndex + " << " + match.loopIndex + ") - 1u];\n"; replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache + "[mglVirtualSubgroup] = " + match.prefixSum + ";\n"; replacement += "}\nbarrier();\n}\n"; replacement += "if (mglPrefixScanLane == 1023u) " + match.cache + "[0] = " + match.prefixSum + ";\n"; replacement += "barrier();\n"; replacement += "float " + match.sum + " = " + match.cache + "[0];"; return replacement; } struct WeightedExposureReductionMatch { SizeT mainBegin = 0; SizeT mainEnd = 0; }; bool ParseWeightedExposureReductionTemplate(const Vector& tokens, WeightedExposureReductionMatch& match) { // IterationRP's exposure pass is a complete, stable shader-pack template. Match the // whole main body before replacing it: a partial match would be unsafe because the // replacement deliberately replays the 32x16 sample grid from one invocation. static const Vector expectedMain = TokenizeCode(R"glsl( void main() { vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f); vec2 sampleCoord = texCoord * (1.0f / 64.0f); sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f; float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb, vec3(0.2125f, 0.7154f, 0.0721f)); vec2 sampleLuminance = vec2(tileExposure, 0.0f); sampleLuminance = subgroupInclusiveAdd(sampleLuminance); if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = sampleLuminance; barrier(); uint loopLength = uint(findMSB(gl_NumSubgroups)); loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u); for (uint i = 0; i < loopLength; i++) { if ((gl_SubgroupID & (1u << i)) > 0u) { sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u]; if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = sampleLuminance; } barrier(); } if (gl_LocalInvocationIndex == 511u) prefixSumCache[0] = sampleLuminance / 512.0f; ; barrier(); float avg = prefixSumCache[0].x; vec2 tileDistance = texCoord * 2.0f - 1.0f; tileDistance.y /= aspectRatio; float centerDistance = length(tileDistance); float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f); tileExposure = max(7.0E-7f, tileExposure); float lumaWeight = avg / tileExposure; lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f); tileWeight *= lumaWeight; vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight); sampleExposure = subgroupInclusiveAdd(sampleExposure); if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = sampleExposure; barrier(); for (uint i = 0; i < loopLength; i++) { if ((gl_SubgroupID & (1u << i)) > 0u) { sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u]; if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = sampleExposure; } barrier(); } if (gl_LocalInvocationIndex == 511u) { float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f); avgExposure = log2(avgExposure); float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x); float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f; float exposureTime = clamp(frameTimeFixed * (2.0f / 1.0f), 0.0f, 1.0f); avgExposure = mix(prevAvgExposure, avgExposure, exposureTime); avgExposure = max(exp2(avgExposure), 1.0E-5f); float exposure = GetExposureValue(avgExposure); imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f)); } } )glsl"); if (!FindUniqueTokenSequence(tokens, expectedMain, match.mainBegin, match.mainEnd) || CountTokenSequence(tokens, {"layout", "(", "local_size_x", "=", "32", ",", "local_size_y", "=", "16", ")", "in", ";"}) != 1 || CountTokenSequence(tokens, {"shared", "vec2", "prefixSumCache", "[", "32", "]", ";"}) != 1 || CountTokenSequence(tokens, {"float", "GetExposureValue", "(", "float", "luminance", ")", "{"}) != 1 || CountTokenSequence(tokens, {"uniform", "int", "frameCounter", ";"}) != 1 || CountTokenSequence(tokens, {"uniform", "float", "frameTime", ";"}) != 1 || CountTokenSequence(tokens, {"uniform", "float", "aspectRatio", ";"}) != 1 || CountTokenSequence(tokens, {"uniform", "vec2", "pixelSize", ";"}) != 1 || CountTokenSequence(tokens, {"uniform", "sampler2D", "colortex2", ";"}) != 1 || CountTokenSequence(tokens, {"uniform", "sampler2D", "pixelData2D", ";"}) != 1 || CountTokenSequence(tokens, {"layout", "(", "rg16f", ")", "uniform", "image2D", "img_pixelData2D", ";"}) != 1) { return false; } // No second user of the scratch array or lane-width-sensitive builtin may survive the // rewrite. These counts describe the fully matched main body plus its one declaration. if (CountToken(tokens, "prefixSumCache") != 9 || CountToken(tokens, "GetExposureValue") != 2 || CountToken(tokens, "subgroupInclusiveAdd") != 2 || CountToken(tokens, "gl_SubgroupInvocationID") != 4 || CountToken(tokens, "gl_SubgroupSize") != 4 || CountToken(tokens, "gl_SubgroupID") != 8 || CountToken(tokens, "gl_NumSubgroups") != 2 || CountToken(tokens, "gl_LocalInvocationIndex") != 2 || CountToken(tokens, "barrier") != 5 || CountToken(tokens, "findMSB") != 1 || HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) || HasIdentifierWithPrefixOutsideAllowed( tokens, "gl_Subgroup", {"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SubGroup", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Warp", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Thread", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SMID", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "ballot", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "shuffle", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "readInvocation", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "readFirstInvocation", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "anyInvocation", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "allInvocations", {}) || HasIdentifierWithPrefixOutsideAllowed(tokens, "mglExposure", {})) { return false; } return true; } String BuildWeightedExposureReductionReplacement() { return R"glsl(void main() { if (gl_LocalInvocationIndex != 0u) return; float mglExposureAverage = 0.0f; for (uint mglExposureY = 0u; mglExposureY < 16u; ++mglExposureY) { for (uint mglExposureX = 0u; mglExposureX < 32u; ++mglExposureX) { vec2 mglExposureTexCoord = (vec2(mglExposureX, mglExposureY) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f); vec2 mglExposureSampleCoord = mglExposureTexCoord * (1.0f / 64.0f); mglExposureSampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f; mglExposureAverage += dot(textureLod(colortex2, mglExposureSampleCoord, 0.0f).rgb, vec3(0.2125f, 0.7154f, 0.0721f)); } } mglExposureAverage /= 512.0f; vec2 mglExposureWeightedSum = vec2(0.0f); for (uint mglExposureY = 0u; mglExposureY < 16u; ++mglExposureY) { for (uint mglExposureX = 0u; mglExposureX < 32u; ++mglExposureX) { vec2 mglExposureTexCoord = (vec2(mglExposureX, mglExposureY) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f); vec2 mglExposureSampleCoord = mglExposureTexCoord * (1.0f / 64.0f); mglExposureSampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f; float mglExposureTile = dot(textureLod(colortex2, mglExposureSampleCoord, 0.0f).rgb, vec3(0.2125f, 0.7154f, 0.0721f)); vec2 mglExposureDistance = mglExposureTexCoord * 2.0f - 1.0f; mglExposureDistance.y /= aspectRatio; float mglExposureWeight = remapSaturate(length(mglExposureDistance), 0.6f, 0.4f); mglExposureTile = max(7.0E-7f, mglExposureTile); float mglExposureLumaWeight = mglExposureAverage / mglExposureTile; mglExposureLumaWeight = pow(mglExposureLumaWeight, remapSaturate(mglExposureAverage, 0.02f, 0.001f) * 0.4f + 0.2f); mglExposureWeight *= mglExposureLumaWeight; mglExposureWeightedSum += vec2(mglExposureTile * mglExposureWeight, mglExposureWeight); } } float avgExposure = max(mglExposureWeightedSum.x / mglExposureWeightedSum.y * 29.3f, 1.0E-10f); avgExposure = log2(avgExposure); float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x); float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f; float exposureTime = clamp(frameTimeFixed * (2.0f / 1.0f), 0.0f, 1.0f); avgExposure = mix(prevAvgExposure, avgExposure, exposureTime); avgExposure = max(exp2(avgExposure), 1.0E-5f); float exposure = GetExposureValue(avgExposure); imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f)); })glsl"; } void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { while (pos < lineEnd && std::isspace(static_cast(source[pos]))) { pos++; } } MobileGL::String ReadDirectiveIdentifier(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { if (pos >= lineEnd || !IsIdentifierStart(source[pos])) { return {}; } const SizeT start = pos++; while (pos < lineEnd && IsIdentifierChar(source[pos])) { pos++; } return source.substr(start, pos - start); } bool HasUtf8Bom(const MobileGL::String& source) { return source.size() >= 3 && static_cast(source[0]) == 0xef && static_cast(source[1]) == 0xbb && static_cast(source[2]) == 0xbf; } // The GLSL versions MobileGL is willing to normalize. Anything else in a #version line - a number // that is not a real language version (329, 331), a bad profile keyword, a float/identifier where // the integer belongs, or trailing tokens - is left untouched so glslang rejects it, matching // KHR-GL33.shaders.preprocessor.directive.version_*. The set is deliberately generous (every real // desktop and ES version) so the normalizer never starts rejecting a form it used to accept. bool IsRecognizedGlslVersion(unsigned version) { switch (version) { case 100: case 110: case 120: case 130: case 140: case 150: case 300: case 310: case 320: case 330: case 400: case 410: case 420: case 430: case 440: case 450: case 460: return true; default: return false; } } struct ShaderLanguageInfo { unsigned version = 110; MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core; SizeT versionDirectiveStart = MobileGL::String::npos; SizeT versionDirectiveEnd = MobileGL::String::npos; bool hasUtf8Bom = false; bool enablesGpuShader5 = false; // Whether the parsed #version directive is a well-formed one MobileGL should rewrite. A // malformed directive (see IsRecognizedGlslVersion) is left alone for glslang to reject. bool hasValidVersionDirective = false; bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; } }; ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) { const MobileGL::String code = MaskCommentsAndQuotedText(source); ShaderLanguageInfo info; info.hasUtf8Bom = HasUtf8Bom(source); SizeT lineStart = 0; while (lineStart < code.size()) { SizeT lineEnd = code.find('\n', lineStart); const bool hasLineBreak = lineEnd != MobileGL::String::npos; if (!hasLineBreak) { lineEnd = code.size(); } SizeT probe = lineStart; if (lineStart == 0 && info.hasUtf8Bom) { probe = 3; } SkipDirectiveWhitespace(code, probe, lineEnd); if (probe < lineEnd && code[probe] == '#') { const SizeT directiveStart = probe; probe++; SkipDirectiveWhitespace(code, probe, lineEnd); const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd); if (directive == "version" && !info.HasVersionDirective()) { SkipDirectiveWhitespace(code, probe, lineEnd); unsigned version = 0; bool hasVersionDigits = false; while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') { hasVersionDigits = true; version = version * 10 + static_cast(code[probe] - '0'); probe++; } if (hasVersionDigits) { info.version = version; info.versionDirectiveStart = directiveStart; info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0); SkipDirectiveWhitespace(code, probe, lineEnd); const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd); bool profileTokenValid = true; if (profile.empty() || profile == "core") { info.profile = MobileGL::ShaderProfile::Core; } else if (profile == "es" || profile == "ES") { info.profile = MobileGL::ShaderProfile::ES; } else if (profile == "compatibility") { info.profile = MobileGL::ShaderProfile::Compatibility; } else { // "#version 330 foo": an unrecognized profile keyword. Keep Core for any // downstream routing, but mark the directive malformed. info.profile = MobileGL::ShaderProfile::Core; profileTokenValid = false; } // Comments are already masked to spaces, so anything non-blank left on the // line is real trailing garbage: "#version 330 foobar" / "#version 330.0". SkipDirectiveWhitespace(code, probe, lineEnd); const bool hasTrailingTokens = probe < lineEnd; info.hasValidVersionDirective = IsRecognizedGlslVersion(info.version) && profileTokenValid && !hasTrailingTokens; } } else if (directive == "extension") { SkipDirectiveWhitespace(code, probe, lineEnd); const MobileGL::String extension = ReadDirectiveIdentifier(code, probe, lineEnd); SkipDirectiveWhitespace(code, probe, lineEnd); if (probe < lineEnd && code[probe] == ':') { probe++; SkipDirectiveWhitespace(code, probe, lineEnd); const MobileGL::String behavior = ReadDirectiveIdentifier(code, probe, lineEnd); const bool isGpuShader5 = extension == "GL_ARB_gpu_shader5" || extension == "GL_NV_gpu_shader5"; const bool enablesExtension = behavior == "enable" || behavior == "require" || behavior == "warn"; // Gate the whole source if it ever opts into either extension. This is deliberately // conservative around conditional directives and keeps legal sample qualifiers intact. info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension); } } } lineStart = lineEnd + (hasLineBreak ? 1 : 0); } return info; } // Stamped onto the normalized directive when a legacy (or absent) desktop // version was rewritten to 330; consumed by RetargetLegacyVersionDirectiveTo460. constexpr const char* kNormalizedLegacyMarker = "/*mobilegl-normalized-legacy*/"; MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) { if (info.profile == MobileGL::ShaderProfile::ES) { // Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan // glslang resource table cannot parse its ESSL built-ins today, even at ESSL 310, whereas the same // source is accepted through the normalized desktop core path. return "#version 460 core\n"; } // Keep compatibility-profile handling on its pre-existing 460 path. Vulkan glslang does not accept that // profile today, and this legacy-sample fix must not broaden or otherwise alter that separate limitation. if (info.profile == MobileGL::ShaderProfile::Compatibility) { return "#version 460 compatibility\n"; } // An explicitly declared modern core version keeps its number: the GL CTS // negative-compile cases (reserved names, layout-qualifier forms, missing // overloads) rely on the declared version's rules, and raising it would // silently legalize them. gpu_shader5 opt-ins keep the 460 escalation - // Vulkan glslang's ARB_gpu_shader5 support is not complete enough alone. if (info.hasValidVersionDirective && info.version >= 330 && !info.enablesGpuShader5) { return "#version " + std::to_string(info.version) + " core\n"; } const bool useLegacyDesktopVersion = info.version < 400 && !info.enablesGpuShader5; // The trailing marker records that this 330 came from a legacy declaration // (or none at all), so the compile-failure retry may re-raise it to 460. // An application's own "#version 330" never carries it and keeps strict // 3.30 semantics. return useLegacyDesktopVersion ? MobileGL::String("#version 330 core ") + kNormalizedLegacyMarker + "\n" : "#version 460 core\n"; } // Rewrites the #version directive and returns the offset just past it in the rewritten source - // the anchor every later injection inserts at. // // The offset is returned rather than rediscovered because this function is the only place that // knows it for free; recovering it costs a whole-source mask plus a line scan // (FindAfterVersionDirective -> InspectShaderLanguage). Each branch below leaves the bytes // ahead of the directive untouched apart from the BOM erase, and each replacement text is // exactly one newline-terminated line, so the arithmetic is exact in all three cases. SizeT NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) { const SizeT bomBytes = info.hasUtf8Bom ? 3 : 0; // A malformed #version (329, 331, bad profile, float/trailing tokens) is left exactly as the // application wrote it so glslang rejects it - rewriting it to "#version 330 core" would // silently legalize the CTS directive.version_* rejection cases. Still drop a leading BOM so // the reported error is the bad version rather than a stray byte-order mark. if (info.HasVersionDirective() && !info.hasValidVersionDirective) { if (info.hasUtf8Bom) { source.erase(0, 3); } // The directive keeps its text and only slides left by the erased BOM. return info.versionDirectiveEnd - bomBytes; } const MobileGL::String replacement = GetNormalizedVersionDirective(info); if (info.HasVersionDirective()) { source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart, replacement); if (info.hasUtf8Bom) { source.erase(0, 3); } // Only whitespace can precede the directive on its own line, so the replacement occupies // the whole rest of that line and ends it. return info.versionDirectiveStart - bomBytes + replacement.size(); } if (info.hasUtf8Bom) { source.erase(0, 3); } source.insert(0, replacement); return replacement.size(); } // 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> FindDirectiveLineRanges(const MobileGL::String& source) { Vector> ranges; SizeT lineStart = 0; while (lineStart < source.size()) { SizeT lineEnd = source.find('\n', lineStart); if (lineEnd == MobileGL::String::npos) { lineEnd = source.size(); } SizeT probe = lineStart; while (probe < lineEnd && std::isspace(static_cast(source[probe]))) { probe++; } if (probe >= lineEnd || source[probe] != '#') { lineStart = lineEnd + 1; continue; } 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(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; } bool IsInDirectiveLine(const Vector>& ranges, SizeT offset) { // Ranges are disjoint and sorted, so the only candidate is the last one starting at or // before the offset. const auto next = std::upper_bound(ranges.begin(), ranges.end(), offset, [](SizeT value, const std::pair& range) { return value < range.first; }); return next != ranges.begin() && offset < std::prev(next)->second; } // No GLSL type name is a statement keyword, so " (" 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& 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_ 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 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> directiveRanges = FindDirectiveLineRanges(source); // Pass A - collect the shadowed names. A definition or prototype at brace depth 0 reads // as " (", 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 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; } // 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 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) { 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]); if (hasLeftBoundary && hasRightBoundary) { source.replace(pos, from.size(), to); pos += to.size(); } else { pos = end; } } } SizeT FindAfterVersionDirective(const MobileGL::String& source) { const ShaderLanguageInfo info = InspectShaderLanguage(source); return info.HasVersionDirective() ? info.versionDirectiveEnd : 0; } // Holds the offset just past the #version directive - the anchor every injected declaration is // inserted at - across the passes of one PreprocessShaderSource call. // // Four consumers want that one number, and each used to buy it with its own // FindAfterVersionDirective, i.e. its own whole-source mask plus line scan. Taking it once and // handing it down turns up to five InspectShaderLanguage sweeps per compile into one. // // It stays EXACT rather than merely cached. The memo is handed out only while the bytes ahead // of the anchor are byte-for-byte what they were when it was taken, and that is precisely the // condition under which a fresh FindAfterVersionDirective returns the same answer: the whole // version line, and every line the scan looks at before reaching it, lies inside that prefix, // so an unchanged prefix means the same directive is still found ending at the same offset. // The guard is load-bearing, not decoration - passes really do rewrite ahead of the anchor. // NormalizeLineDirectives deletes #line directives that precede the version line, and // ModernizeLegacyGLSL's ReplaceIdentifier is raw text and so rewrites inside a leading comment // banner. When the guard trips the offset is simply recomputed, which is the pre-memo behavior. // // The one-argument constructor is that pre-memo behavior in full, for any caller that has a // source but no anchor to hand. class AfterVersionAnchor { public: explicit AfterVersionAnchor(const MobileGL::String& source) { Recompute(source); } AfterVersionAnchor(const MobileGL::String& source, SizeT offset) { Adopt(source, offset); } SizeT Get(const MobileGL::String& source) { if (source.size() < m_offset || source.compare(0, m_offset, m_prefix) != 0) { Recompute(source); } return m_offset; } private: void Recompute(const MobileGL::String& source) { Adopt(source, FindAfterVersionDirective(source)); } void Adopt(const MobileGL::String& source, SizeT offset) { m_offset = offset; m_prefix.assign(source, 0, offset); } SizeT m_offset = 0; MobileGL::String m_prefix; }; // GLSL's #line takes integer expressions only, but plenty of shader-pack preprocessors emit the // C form with a quoted filename. Deleting every #line outright made those harmless - at the cost // of __LINE__ reporting the position in MobileGL's rewritten text rather than the one the pack // author wrote, and of every later diagnostic pointing at the wrong line. Dropping just the // quoted operand keeps the directive doing its job and still hands glslang something it accepts. // // `versionEnd` is the after-version anchor for the current `source` (AfterVersionAnchor::Get); // this pass only reads the source ahead of its own rewrites, so the plain offset is enough. void NormalizeLineDirectives(MobileGL::String& source, SizeT versionEnd) { const MobileGL::String masked = MaskCommentsAndQuotedText(source); MobileGL::String result; result.reserve(source.size()); SizeT lineStart = 0; while (lineStart <= source.size()) { SizeT lineEnd = source.find('\n', lineStart); const bool lastLine = lineEnd == MobileGL::String::npos; if (lastLine) lineEnd = source.size(); SizeT probe = lineStart; while (probe < lineEnd && (source[probe] == ' ' || source[probe] == '\t')) probe++; const bool isLineDirective = masked.compare(probe, 5, "#line") == 0 && (probe + 5 >= lineEnd || !IsIdentifierChar(source[probe + 5])); if (isLineDirective && lineStart < versionEnd) { // #version has to be the first token in the shader, so a #line ahead of it could // never have taken effect. Drop it rather than hand glslang a source it must reject // - some pack preprocessors emit their directives before the version line. } else if (isLineDirective) { // Keep everything up to the first quote that the masker identified as string text. SizeT quotePos = MobileGL::String::npos; for (SizeT i = probe + 5; i < lineEnd; i++) { if (source[i] == '"' || source[i] == '\'') { quotePos = i; break; } } if (quotePos != MobileGL::String::npos) { result.append(source, lineStart, quotePos - lineStart); } else { result.append(source, lineStart, lineEnd - lineStart); } } else { result.append(source, lineStart, lineEnd - lineStart); } if (lastLine) break; result.push_back('\n'); lineStart = lineEnd + 1; } source = std::move(result); } MobileGL::String TrimDirectiveToken(const MobileGL::String& token) { SizeT start = 0; while (start < token.size() && std::isspace(static_cast(token[start]))) { start++; } SizeT end = token.size(); while (end > start && std::isspace(static_cast(token[end - 1]))) { end--; } return token.substr(start, end - start); } void FilterUnsupportedGpuShaderInt64(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env, MobileGL::String& source) { if (env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) { return; } // Detect the directive on a comment/string-masked copy so a commented-out // "#extension GL_ARB_gpu_shader_int64" is never turned into a synthesized #error. Comments are // no longer blanked in the delivered source (glslang handles them), so this pass must mask // locally like its siblings. Masking preserves offsets, so edits collected against the scan // apply verbatim to `source`; they are applied back-to-front to keep earlier offsets valid. const MobileGL::String scan = MaskCommentsAndQuotedText(source); struct DirectiveEdit { SizeT pos; SizeT len; MobileGL::String replacement; }; Vector edits; SizeT lineStart = 0; while (lineStart < scan.size()) { SizeT lineEnd = scan.find('\n', lineStart); const bool hasLineBreak = lineEnd != MobileGL::String::npos; if (!hasLineBreak) { lineEnd = scan.size(); } const MobileGL::String line = scan.substr(lineStart, lineEnd - lineStart); SizeT probe = 0; while (probe < line.size() && std::isspace(static_cast(line[probe]))) { probe++; } if (probe < line.size() && line[probe] == '#') { probe++; while (probe < line.size() && std::isspace(static_cast(line[probe]))) { probe++; } constexpr const char* extensionToken = "extension"; constexpr SizeT extensionLen = 9; const bool hasExtensionDirective = probe + extensionLen <= line.size() && line.compare(probe, extensionLen, extensionToken) == 0 && (probe + extensionLen == line.size() || !IsIdentifierChar(line[probe + extensionLen])); if (hasExtensionDirective) { probe += extensionLen; while (probe < line.size() && std::isspace(static_cast(line[probe]))) { probe++; } constexpr const char* int64Extension = "GL_ARB_gpu_shader_int64"; constexpr SizeT int64ExtensionLen = 23; const bool hasInt64Extension = probe + int64ExtensionLen <= line.size() && line.compare(probe, int64ExtensionLen, int64Extension) == 0 && (probe + int64ExtensionLen == line.size() || !IsIdentifierChar(line[probe + int64ExtensionLen])); if (hasInt64Extension) { probe += int64ExtensionLen; while (probe < line.size() && std::isspace(static_cast(line[probe]))) { probe++; } if (probe < line.size() && line[probe] == ':') { probe++; const MobileGL::String behavior = TrimDirectiveToken(line.substr(probe)); const SizeT replaceLen = lineEnd - lineStart + (hasLineBreak ? 1 : 0); if (behavior == "require") { edits.push_back({lineStart, replaceLen, "#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n"}); } else if (behavior == "enable" || behavior == "warn") { edits.push_back({lineStart, replaceLen, "\n"}); } lineStart = lineEnd + (hasLineBreak ? 1 : 0); continue; } } } } lineStart = lineEnd + (hasLineBreak ? 1 : 0); } for (auto it = edits.rbegin(); it != edits.rend(); ++it) { source.replace(it->pos, it->len, it->replacement); } ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64"); } // Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to // `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the // app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as // std140 (glslang under a SPIR-V target rejects `packed`/`shared` outright and SPIRV-Cross has // no other packing for UBOs), so std140 IS this implementation's chosen layout. Rewriting at // the source level keeps the validation compile, the reflection the app queries, and the // generated SPIR-V all agreeing on that choice. Both replacement tokens are 6 characters, so // the rewrite is done in place. void CoerceUniformBlockPackingToStd140(MobileGL::String& source) { constexpr const char* layoutToken = "layout"; constexpr SizeT layoutLen = 6; SizeT pos = 0; while ((pos = source.find(layoutToken, pos)) != MobileGL::String::npos) { const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]); SizeT probe = pos + layoutLen; const bool hasRightBoundary = probe >= source.size() || !IsIdentifierChar(source[probe]); if (!hasLeftBoundary || !hasRightBoundary) { pos = probe; continue; } while (probe < source.size() && std::isspace(static_cast(source[probe]))) { probe++; } if (probe >= source.size() || source[probe] != '(') { pos = probe; continue; } // Scan the qualifier list; layout qualifier values may contain parenthesized // constant expressions, so track nesting until the matching ')'. SizeT cursor = probe + 1; int depth = 1; while (cursor < source.size() && depth > 0) { const char ch = source[cursor]; if (ch == '(') { depth++; } else if (ch == ')') { depth--; } else if (IsIdentifierChar(ch) && (cursor == 0 || !IsIdentifierChar(source[cursor - 1]))) { SizeT identifierEnd = cursor; while (identifierEnd < source.size() && IsIdentifierChar(source[identifierEnd])) { identifierEnd++; } const SizeT identifierLen = identifierEnd - cursor; if (identifierLen == 6 && (source.compare(cursor, 6, "packed") == 0 || source.compare(cursor, 6, "shared") == 0)) { source.replace(cursor, 6, "std140"); } cursor = identifierEnd; continue; } cursor++; } pos = cursor; } } // `afterVersion` tracks the anchor the two injections below insert at. It is passed as the // tracker rather than a bare offset because this pass rewrites identifiers first, and those // rewrites are raw text: a leading comment banner mentioning `varying` or `texture2D` moves the // anchor, and the tracker notices. void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source, AfterVersionAnchor& afterVersion) { // Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and // ignored in the normalized desktop core profiles, so glslang handles them natively. ReplaceIdentifier(source, "texture2D", "texture"); ReplaceIdentifier(source, "texture2DProj", "textureProj"); ReplaceIdentifier(source, "textureCube", "texture"); ReplaceIdentifier(source, "texture3D", "texture"); if (stage == MobileGL::ShaderStage::Vertex) { ReplaceIdentifier(source, "attribute", "in"); ReplaceIdentifier(source, "varying", "out"); return; } if (stage == MobileGL::ShaderStage::Fragment) { ReplaceIdentifier(source, "varying", "in"); const bool usesFragColor = source.find("gl_FragColor") != MobileGL::String::npos; const bool usesFragData = source.find("gl_FragData") != MobileGL::String::npos; if (usesFragColor) { ReplaceIdentifier(source, "gl_FragColor", "mg_FragColor"); source.insert(afterVersion.Get(source), "out vec4 mg_FragColor;\n"); } if (usesFragData) { ReplaceIdentifier(source, "gl_FragData", "mg_FragData"); source.insert(afterVersion.Get(source), "layout(location = 0) out vec4 mg_FragData[8];\n"); } } } void InjectDepthRangeBuiltinShim(MobileGL::ShaderStage stage, MobileGL::String& source, AfterVersionAnchor& afterVersion) { if (stage != MobileGL::ShaderStage::Fragment) return; if (source.find("gl_DepthRange") == MobileGL::String::npos) return; if (source.find("mg_DepthRangeParameters") != MobileGL::String::npos) return; constexpr const char* shim = "struct mg_DepthRangeParameters { float near; float far; float diff; };\n" "const mg_DepthRangeParameters mg_DepthRange = mg_DepthRangeParameters(0.0, 1.0, 1.0);\n" "#define gl_DepthRange mg_DepthRange\n"; source.insert(afterVersion.Get(source), shim); } } // namespace namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source) { constexpr Uint32 capturedSubgroupSize = 32; const Bool narrowSubgroup = nativeSubgroupSize != 0u && nativeSubgroupSize < 16u && capturedSubgroupSize % nativeSubgroupSize == 0u; const Bool wideSubgroup = nativeSubgroupSize > capturedSubgroupSize && nativeSubgroupSize % capturedSubgroupSize == 0u; if (stage != ShaderStage::Compute || !IsPowerOfTwo(nativeSubgroupSize) || (!narrowSubgroup && !wideSubgroup)) { return false; } const Vector tokens = TokenizeCode(source); LinearPrefixScanMatch match; if (!ParseLinearPrefixScanTemplate(tokens, match)) { // Diagnosability: when the trigger op is present but the template no longer // matches (e.g. the pack shipped a new shader revision), the affected device // silently falls back to the driver's miscompiled path. Make that visible. if (CountToken(tokens, "subgroupInclusiveAdd") > 0) { MGLOG_W_ONCE("%s: subgroupInclusiveAdd present but the linear prefix-scan template " "did not match; the subgroup-compatibility rewrite was NOT applied", __func__); } return false; } const String replacement = BuildLinearPrefixScanReplacement(match); source.replace(match.scanBegin, match.scanEnd - match.scanBegin, replacement); // The declaration occurs before the replaced scan, so its original offsets remain // valid after the first replacement. source.replace(match.sharedArraySizeBegin, match.sharedArraySizeEnd - match.sharedArraySizeBegin, "1024"); return true; } Bool RewriteWeightedExposureSubgroupReductionForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source) { if (stage != ShaderStage::Compute || !IsPowerOfTwo(nativeSubgroupSize) || nativeSubgroupSize >= 16u) { return false; } const Vector tokens = TokenizeCode(source); WeightedExposureReductionMatch match; if (!ParseWeightedExposureReductionTemplate(tokens, match)) { if (CountToken(tokens, "subgroupInclusiveAdd") == 2 && CountToken(tokens, "GetExposureValue") > 0) { MGLOG_W_ONCE("%s: weighted exposure subgroup reductions were present but the complete " "template did not match; the narrow-subgroup rewrite was NOT applied", __func__); } return false; } source.replace(match.mainBegin, match.mainEnd - match.mainBegin, BuildWeightedExposureReductionReplacement()); return true; } namespace { struct ShaderSourceQuirkContext { ShaderStage stage = ShaderStage::Unknown; BackendType backend = BackendType::Unknown; MG_Backend::GpuVendorKind vendor = MG_Backend::GpuVendorKind::Unknown; Uint32 subgroupSize = 0; }; // Device-quirk registry. Every entry is a narrowly scoped source rewrite that // works around a specific driver defect. A quirk runs when its env override // forces it on, or when the override is Auto and DeviceApplies matches the // detected device. ForceOn bypasses only the device gate - each Apply keeps // its own structural safety checks. Add new per-device workarounds here // instead of open-coding them in PreprocessShaderSource. struct ShaderSourceQuirk { const char* name; // Reads the override out of the captured env, never out of the live // MG_Config table: a worker must see the same config the GL thread saw. MG_Config::QuirkOverride (*GetOverride)(const CompileEnv&); Bool (*DeviceApplies)(const ShaderSourceQuirkContext&); Bool (*Apply)(const ShaderSourceQuirkContext&, String&); }; constexpr ShaderSourceQuirk kShaderSourceQuirks[] = { { // MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN "subgroup-prefix-scan-rewrite", [](const CompileEnv& env) { return env.subgroupPrefixScanQuirk; }, [](const ShaderSourceQuirkContext& ctx) { // Narrow subgroups overflow the pack's fixed subgroup-result scratch // arrays. Qualcomm also miscompiles the recognized float InclusiveScan // pattern when its native subgroup is wider than the captured 32 lanes. return ctx.backend == BackendType::DirectVulkan && ((ctx.subgroupSize != 0u && ctx.subgroupSize < 16u) || (ctx.vendor == MG_Backend::GpuVendorKind::Qualcomm && ctx.subgroupSize > 32u)); }, [](const ShaderSourceQuirkContext& ctx, String& source) { const Bool exposureRewritten = RewriteWeightedExposureSubgroupReductionForVulkan( ctx.stage, ctx.subgroupSize, source); const Bool prefixScanRewritten = RewriteLinearSubgroupPrefixScanForVulkan( ctx.stage, ctx.subgroupSize, source); return exposureRewritten || prefixScanRewritten; }, }, }; void ApplyShaderSourceQuirks(const CompileEnv& env, ShaderStage stage, String& source) { // No backend at capture time means no device to match a quirk against, // and (as before) no quirk can fire - not even a forced one, because // every Apply reads device parameters that do not exist yet. if (!env.HasBackend()) { return; } const ShaderSourceQuirkContext quirkContext{ stage, env.backend, env.params.GpuVendor, env.params.SubgroupSize, }; for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) { const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride(env); if (quirkOverride == MG_Config::QuirkOverride::ForceOff) { continue; } if (quirkOverride == MG_Config::QuirkOverride::Auto && !quirk.DeviceApplies(quirkContext)) { continue; } if (quirk.Apply(quirkContext, source)) { MGLOG_D("ApplyShaderSourceQuirks: applied '%s'%s", quirk.name, quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : ""); } } } } // namespace void PreprocessShaderSource(ShaderStage stage, String& source) { PreprocessShaderSource(stage, source, *GetCurrentCompileEnv()); } void PreprocessShaderSource(ShaderStage stage, String& source, const CompileEnv& env) { // Normalize while the inspector's source span still refers to the untouched input. const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source); // Four passes below inject just past the #version directive, and each of them used // to locate that anchor for itself - a whole-source mask plus line scan apiece, up // to five per compile for one offset. NormalizeVersionDirective hands back the // anchor it just created and the tracker keeps it honest from there. AfterVersionAnchor afterVersion(source, NormalizeVersionDirective(source, originalLanguage)); // Comments are left intact for glslang's own preprocessor: a block comment is a single // preprocessing token that collapses to one space even across newlines and inside a // directive, so blanking it here (which preserved the interior newlines) truncated // multi-line #define bodies and broke otherwise-valid shaders (KHR-GL3x.shaders. // preprocessor multiline_comment_define / redefine_object / function_redefinition). // Every MobileGL pass that must ignore comment/string text already masks them locally // via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them. NormalizeLineDirectives(source, afterVersion.Get(source)); // noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+) // and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders // natively and SPIRV-Cross turns into ESSL `noperspective` + the // GL_NV_shader_noperspective_interpolation extension. The old naked substring erase // both discarded that interpolation (shader packs need it) and corrupted any // identifier that merely contained the word. The GLES fallback for devices without // the extension lives in the backend, where device capabilities are known. FilterUnsupportedGpuShaderInt64(env, source); CoerceUniformBlockPackingToStd140(source); RenameBuiltinShadowingFunctions(source); ModernizeLegacyGLSL(stage, source, afterVersion); InjectDepthRangeBuiltinShim(stage, source, afterVersion); ApplyShaderSourceQuirks(env, stage, source); } Bool RetargetLegacyVersionDirectiveTo460(String& source) { // Re-inspect rather than searching for the literal directive: it is not necessarily at // offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere // must not be mistaken for the real one. const ShaderLanguageInfo info = InspectShaderLanguage(source); if (!info.HasVersionDirective()) return false; // Never rescue a malformed directive to 460: that is precisely what re-legalized the // CTS directive.version_* rejection cases after the first compile failed. The shader- // pack retry this exists for only ever sees a valid low version (a real "#version 330"). if (!info.hasValidVersionDirective) return false; // Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and // compatibility shaders keep whatever they declared. if (info.profile != ShaderProfile::Core || info.version >= 400) return false; // Only rescue MobileGL's own legacy normalization (marked on the directive line). // An application-declared "#version 330" keeps strict 3.30 semantics: raising it // would re-legalize the CTS negative-compile cases (reserved names, arrays of // arrays, missing overloads). SizeT lineEnd = source.find('\n', info.versionDirectiveStart); if (lineEnd == MobileGL::String::npos) { lineEnd = source.size(); } const SizeT markerPos = source.find(kNormalizedLegacyMarker, info.versionDirectiveStart); if (markerPos == MobileGL::String::npos || markerPos > lineEnd) { return false; } source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart, "#version 460 core\n"); return true; } std::optional FindReservedIdentifierViolation(const String& source) { // Reserved anywhere; glslang accepts them as plain identifiers. static constexpr const char* kAlwaysReserved[] = { "image1DShadow", "image2DShadow", "image1DArrayShadow", "image2DArrayShadow", }; // Keywords legal only inside a layout(...) qualifier list. static constexpr const char* kLayoutOnlyKeywords[] = { "packed", "row_major", }; const auto isIdentChar = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; }; const SizeT length = source.size(); SizeT i = 0; Int layoutParenDepth = 0; // >0 while inside layout(...) Bool pendingLayoutParen = false; // saw "layout", awaiting its '(' while (i < length) { const char c = source[i]; // Comments. if (c == '/' && i + 1 < length && source[i + 1] == '/') { while (i < length && source[i] != '\n') ++i; continue; } if (c == '/' && i + 1 < length && source[i + 1] == '*') { i += 2; while (i + 1 < length && !(source[i] == '*' && source[i + 1] == '/')) ++i; i = (i + 1 < length) ? i + 2 : length; continue; } // Preprocessor lines stay out of scope (macro names may shadow anything). if (c == '#' && (i == 0 || source[i - 1] == '\n' || source.find_last_not_of(" \t", i - 1) == MobileGL::String::npos || source[source.find_last_not_of(" \t", i - 1)] == '\n')) { while (i < length && source[i] != '\n') { if (source[i] == '\\' && i + 1 < length && source[i + 1] == '\n') ++i; ++i; } continue; } if (c == '(') { if (pendingLayoutParen) { layoutParenDepth = 1; pendingLayoutParen = false; } else if (layoutParenDepth > 0) { ++layoutParenDepth; } ++i; continue; } if (c == ')') { if (layoutParenDepth > 0) --layoutParenDepth; ++i; continue; } if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { ++i; continue; } if (isIdentChar(c) && !(c >= '0' && c <= '9')) { const SizeT start = i; while (i < length && isIdentChar(source[i])) ++i; const StringView word(source.data() + start, i - start); if (word == "layout") { pendingLayoutParen = true; continue; } pendingLayoutParen = false; for (const char* reserved : kAlwaysReserved) { if (word == reserved) { return String("ERROR: reserved identifier '") + reserved + "' may not be used."; } } if (layoutParenDepth == 0) { for (const char* keyword : kLayoutOnlyKeywords) { if (word == keyword) { return String("ERROR: '") + keyword + "' is a keyword and may not be used as an identifier."; } } } continue; } if (isIdentChar(c)) { // digit-led token: skip the whole number/identifier tail while (i < length && isIdentChar(source[i])) ++i; pendingLayoutParen = false; continue; } pendingLayoutParen = false; ++i; } return std::nullopt; } namespace { bool IsNonLayoutQualifierKeyword(const String& text) { static const char* kQualifiers[] = { "highp", "mediump", "lowp", "precise", "const", "flat", "noperspective", "smooth", "centroid", "sample", "patch", "invariant", "coherent", "volatile", "restrict", "readonly", "writeonly", "subroutine", }; for (const char* qualifier : kQualifiers) { if (text == qualifier) return true; } return false; } bool IsDecimalIntegerToken(const String& text) { if (text.empty()) return false; return std::all_of(text.begin(), text.end(), [](char ch) { return ch >= '0' && ch <= '9'; }); } // Parses one brace-free depth-0 statement [begin, end) and records its // declarators when it is a uniform declaration carrying an integral // layout(location = N). Multi-declarator statements assign consecutive // locations, each declarator advancing by its array element count // (ARB_explicit_uniform_location rules). Anything the narrow grammar does // not recognize is skipped, never guessed at. void RecordUniformDeclarationLocations(const Vector& tokens, SizeT begin, SizeT end, MobileGL::UnorderedMap& locations) { using MobileGL::Int; long long location = -1; bool sawUniform = false; SizeT declaratorBegin = end; for (SizeT k = begin; k < end;) { const String& text = tokens[k].text; if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") { SizeT j = k + 2; Int parenDepth = 1; while (j < end && parenDepth > 0) { const String& layoutToken = tokens[j].text; if (layoutToken == "(") { ++parenDepth; } else if (layoutToken == ")") { --parenDepth; } else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end && tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) { location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10), static_cast(INT_MAX / 2)); j += 2; } ++j; } k = j; continue; } if (text == "uniform") { sawUniform = true; ++k; continue; } if (sawUniform && location >= 0 && IsIdentifierToken(tokens[k]) && !IsNonLayoutQualifierKeyword(text)) { declaratorBegin = k + 1; // 'text' is the type; declarators follow break; } ++k; } if (!sawUniform || location < 0 || declaratorBegin >= end) return; long long nextLocation = location; for (SizeT k = declaratorBegin; k < end;) { if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further const String& name = tokens[k].text; ++k; long long span = 1; while (k < end && tokens[k].text == "[") { ++k; long long dimension = 1; if (k < end && IsDecimalIntegerToken(tokens[k].text)) { dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10); ++k; } if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out ++k; span *= std::max(1ll, std::min(dimension, static_cast(INT_MAX / 2))); } // Keep the first sighting: a duplicate can only come from alternative // preprocessor branches declaring the same name. locations.emplace(name, static_cast(std::min( nextLocation, static_cast(INT_MAX / 2)))); nextLocation += span; if (k >= end) break; if (tokens[k].text == "=") { // skip an initializer up to the declarator comma Int nestingDepth = 0; ++k; while (k < end) { const String& initializerToken = tokens[k].text; if (initializerToken == "(" || initializerToken == "[") { ++nestingDepth; } else if (initializerToken == ")" || initializerToken == "]") { --nestingDepth; } else if (initializerToken == "," && nestingDepth == 0) { break; } ++k; } } if (k >= end) break; if (tokens[k].text != ",") return; ++k; } } // Parses one brace-free depth-0 statement [begin, end) and records its // declarators when it is a sampler/image uniform declaration carrying an // integral layout(binding = N). Such a binding is a GL texture/image unit, // which the Vulkan-client relaxed parse strips before mapIO can observe it // (it is not a valid descriptor binding there), so it is extracted lexically // and restored as the uniform's initial unit. Every declarator in the // statement shares the qualifier's binding, matching what the GL-client // mapIO used to capture from the shared type qualifier. Anything the narrow // grammar does not recognize is skipped, never guessed at. void RecordOpaqueDeclarationBindings(const Vector& tokens, SizeT begin, SizeT end, MobileGL::UnorderedMap& bindings) { using MobileGL::Int; long long binding = -1; bool sawUniform = false; SizeT declaratorBegin = end; for (SizeT k = begin; k < end;) { const String& text = tokens[k].text; if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") { SizeT j = k + 2; Int parenDepth = 1; while (j < end && parenDepth > 0) { const String& layoutToken = tokens[j].text; if (layoutToken == "(") { ++parenDepth; } else if (layoutToken == ")") { --parenDepth; } else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end && tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) { binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10), static_cast(INT_MAX / 2)); j += 2; } ++j; } k = j; continue; } if (text == "uniform") { sawUniform = true; ++k; continue; } if (sawUniform && binding >= 0 && IsIdentifierToken(tokens[k]) && !IsNonLayoutQualifierKeyword(text)) { // 'text' is the type. Only sampler/image opaques carry unit // bindings; on anything else (e.g. atomic_uint, whose binding // is a counter-buffer index) record nothing. if (text.find("sampler") == String::npos && text.find("image") == String::npos) return; declaratorBegin = k + 1; break; } ++k; } if (!sawUniform || binding < 0 || declaratorBegin >= end) return; for (SizeT k = declaratorBegin; k < end;) { if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further const String& name = tokens[k].text; ++k; while (k < end && tokens[k].text == "[") { ++k; if (k < end && IsDecimalIntegerToken(tokens[k].text)) ++k; if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out ++k; } bindings[name] = static_cast(binding); if (k >= end) break; if (tokens[k].text != ",") return; // opaque declarators cannot take initializers ++k; } } } // namespace UnorderedMap ExtractExplicitOpaqueBindings(const String& source) { UnorderedMap bindings; // Fast path: without the qualifier keyword there is nothing to extract. if (source.find("binding") == String::npos) return bindings; const Vector tokens = TokenizeCode(source); const SizeT count = tokens.size(); Int braceDepth = 0; SizeT pos = 0; while (pos < count) { const String& text = tokens[pos].text; if (text == "{") { ++braceDepth; ++pos; continue; } if (text == "}") { if (braceDepth > 0) --braceDepth; ++pos; continue; } if (braceDepth != 0 || text == ";") { ++pos; continue; } // A depth-0 statement runs to its ';'. One that opens a brace instead is // a function definition or an interface/uniform block: a block's binding // is a buffer binding point, not a texture unit, so skip both alike. SizeT statementEnd = pos; while (statementEnd < count && tokens[statementEnd].text != ";" && tokens[statementEnd].text != "{") { ++statementEnd; } if (statementEnd >= count || tokens[statementEnd].text == "{") { pos = statementEnd; continue; } RecordOpaqueDeclarationBindings(tokens, pos, statementEnd, bindings); pos = statementEnd + 1; } return bindings; } UnorderedMap ExtractExplicitUniformLocations(const String& source) { UnorderedMap locations; // Fast path: without the qualifier keyword there is nothing to extract. if (source.find("location") == String::npos) return locations; const Vector tokens = TokenizeCode(source); const SizeT count = tokens.size(); Int braceDepth = 0; SizeT pos = 0; while (pos < count) { const String& text = tokens[pos].text; if (text == "{") { ++braceDepth; ++pos; continue; } if (text == "}") { if (braceDepth > 0) --braceDepth; ++pos; continue; } if (braceDepth != 0 || text == ";") { ++pos; continue; } // A depth-0 statement runs to its ';'. One that opens a brace instead is a // function definition or an interface/uniform block: neither can declare a // default-block uniform location, so hand the '{' back to the depth tracker. SizeT statementEnd = pos; while (statementEnd < count && tokens[statementEnd].text != ";" && tokens[statementEnd].text != "{") { ++statementEnd; } if (statementEnd >= count || tokens[statementEnd].text == "{") { pos = statementEnd; continue; } RecordUniformDeclarationLocations(tokens, pos, statementEnd, locations); pos = statementEnd + 1; } return locations; } } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL