mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
2043 lines
104 KiB
C++
2043 lines
104 KiB
C++
// 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 <algorithm>
|
|
#include <cctype>
|
|
#include <climits>
|
|
#include <cstdlib>
|
|
#include <initializer_list>
|
|
#include <utility>
|
|
#include <Config.h>
|
|
#include <MG_Backend/BackendObjects.h>
|
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
|
|
|
#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<CodeToken> TokenizeCode(const String& source) {
|
|
const String masked = MaskCommentsAndQuotedText(source);
|
|
Vector<CodeToken> tokens;
|
|
tokens.reserve(source.size() / 4);
|
|
|
|
SizeT pos = 0;
|
|
while (pos < masked.size()) {
|
|
const char ch = masked[pos];
|
|
if (std::isspace(static_cast<unsigned char>(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<unsigned char>(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<CodeToken>& 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<CodeToken>& m_tokens;
|
|
SizeT m_position;
|
|
};
|
|
|
|
SizeT CountToken(const Vector<CodeToken>& tokens, const String& tokenText) {
|
|
return static_cast<SizeT>(std::count_if(tokens.begin(), tokens.end(),
|
|
[&](const CodeToken& token) { return token.text == tokenText; }));
|
|
}
|
|
|
|
bool HasIdentifierWithPrefixOutsideAllowed(const Vector<CodeToken>& tokens, const String& prefix,
|
|
std::initializer_list<const char*> 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<CodeToken>& tokens, SizeT position,
|
|
std::initializer_list<const char*> 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<CodeToken>& tokens,
|
|
std::initializer_list<const char*> expected) {
|
|
SizeT count = 0;
|
|
for (SizeT position = 0; position < tokens.size(); ++position) {
|
|
if (MatchTokenSequence(tokens, position, expected)) {
|
|
++count;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool FindUniqueTokenSequence(const Vector<CodeToken>& tokens, const Vector<CodeToken>& 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<CodeToken>& 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<CodeToken>& 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<CodeToken> 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<unsigned char>(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<unsigned char>(source[0]) == 0xef &&
|
|
static_cast<unsigned char>(source[1]) == 0xbb && static_cast<unsigned char>(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<unsigned>(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<std::pair<SizeT, SizeT>> FindDirectiveLineRanges(const MobileGL::String& source) {
|
|
Vector<std::pair<SizeT, SizeT>> 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<unsigned char>(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<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;
|
|
}
|
|
|
|
bool IsInDirectiveLine(const Vector<std::pair<SizeT, SizeT>>& 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<SizeT, SizeT>& range) {
|
|
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;
|
|
}
|
|
|
|
// 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) {
|
|
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<unsigned char>(token[start]))) {
|
|
start++;
|
|
}
|
|
|
|
SizeT end = token.size();
|
|
while (end > start && std::isspace(static_cast<unsigned char>(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<DirectiveEdit> 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<unsigned char>(line[probe]))) {
|
|
probe++;
|
|
}
|
|
|
|
if (probe < line.size() && line[probe] == '#') {
|
|
probe++;
|
|
while (probe < line.size() && std::isspace(static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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<unsigned char>(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<CodeToken> 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<CodeToken> 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<String> 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<CodeToken>& tokens, SizeT begin, SizeT end,
|
|
MobileGL::UnorderedMap<String, MobileGL::Int>& 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<long long>(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<long long>(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<Int>(std::min(
|
|
nextLocation, static_cast<long long>(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<CodeToken>& tokens, SizeT begin, SizeT end,
|
|
MobileGL::UnorderedMap<String, MobileGL::Uint>& 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<long long>(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<MobileGL::Uint>(binding);
|
|
if (k >= end) break;
|
|
if (tokens[k].text != ",") return; // opaque declarators cannot take initializers
|
|
++k;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source) {
|
|
UnorderedMap<String, Uint> bindings;
|
|
// Fast path: without the qualifier keyword there is nothing to extract.
|
|
if (source.find("binding") == String::npos) return bindings;
|
|
|
|
const Vector<CodeToken> 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<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
|
UnorderedMap<String, Int> locations;
|
|
// Fast path: without the qualifier keyword there is nothing to extract.
|
|
if (source.find("location") == String::npos) return locations;
|
|
|
|
const Vector<CodeToken> 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
|