mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
1152 lines
55 KiB
C++
1152 lines
55 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 <initializer_list>
|
|
#include <utility>
|
|
#include <Config.h>
|
|
#include <MG_Backend/BackendObjects.h>
|
|
|
|
namespace {
|
|
using MobileGL::SizeT;
|
|
using MobileGL::String;
|
|
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 == '_';
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (ch != '\n' && ch != '\r') {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
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);
|
|
if (profile == "es" || profile == "ES") {
|
|
info.profile = MobileGL::ShaderProfile::ES;
|
|
} else if (profile == "compatibility") {
|
|
info.profile = MobileGL::ShaderProfile::Compatibility;
|
|
} else {
|
|
info.profile = MobileGL::ShaderProfile::Core;
|
|
}
|
|
}
|
|
} 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;
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
const bool useLegacyDesktopVersion =
|
|
info.version < 400 && !info.enablesGpuShader5;
|
|
return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n";
|
|
}
|
|
|
|
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
|
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);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (info.hasUtf8Bom) {
|
|
source.erase(0, 3);
|
|
}
|
|
source.insert(0, replacement);
|
|
}
|
|
|
|
bool HasSingleLineFunctionDefinition(const MobileGL::String& source, const MobileGL::String& functionName) {
|
|
SizeT lineStart = 0;
|
|
while (lineStart < source.size()) {
|
|
SizeT lineEnd = source.find('\n', lineStart);
|
|
if (lineEnd == MobileGL::String::npos) {
|
|
lineEnd = source.size();
|
|
}
|
|
|
|
SizeT functionPos = source.find(functionName, lineStart);
|
|
while (functionPos != MobileGL::String::npos && functionPos < lineEnd) {
|
|
const bool hasLeftBoundary = functionPos == 0 || !IsIdentifierChar(source[functionPos - 1]);
|
|
const SizeT functionEnd = functionPos + functionName.size();
|
|
const bool hasRightBoundary = functionEnd >= source.size() || !IsIdentifierChar(source[functionEnd]);
|
|
if (hasLeftBoundary && hasRightBoundary) {
|
|
SizeT probe = functionEnd;
|
|
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
probe++;
|
|
}
|
|
if (probe < lineEnd && source[probe] == '(') {
|
|
const SizeT closingParen = source.find(')', probe);
|
|
if (closingParen != MobileGL::String::npos && closingParen < lineEnd) {
|
|
probe = closingParen + 1;
|
|
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
probe++;
|
|
}
|
|
if (probe < lineEnd && source[probe] == '{') {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
functionPos = source.find(functionName, functionPos + functionName.size());
|
|
}
|
|
|
|
lineStart = lineEnd + 1;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RenameFunctionInvocations(MobileGL::String& source, const MobileGL::String& from, const MobileGL::String& to) {
|
|
SizeT pos = 0;
|
|
while ((pos = source.find(from, pos)) != MobileGL::String::npos) {
|
|
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]);
|
|
const SizeT end = pos + from.size();
|
|
const bool hasRightBoundary = end >= source.size() || !IsIdentifierChar(source[end]);
|
|
|
|
SizeT probe = end;
|
|
while (probe < source.size() && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
probe++;
|
|
}
|
|
|
|
if (hasLeftBoundary && hasRightBoundary && probe < source.size() && source[probe] == '(') {
|
|
source.replace(pos, from.size(), to);
|
|
pos += to.size();
|
|
continue;
|
|
}
|
|
|
|
pos = end;
|
|
}
|
|
}
|
|
|
|
void RenameBuiltinShadowingFunction(MobileGL::String& source, const char* from, const char* to) {
|
|
const MobileGL::String fromName = from;
|
|
if (!HasSingleLineFunctionDefinition(source, fromName)) {
|
|
return;
|
|
}
|
|
|
|
RenameFunctionInvocations(source, fromName, to);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoveDefineForIdentifier(MobileGL::String& source, const MobileGL::String& identifier) {
|
|
SizeT lineStart = 0;
|
|
while (lineStart < source.size()) {
|
|
SizeT lineEnd = source.find('\n', lineStart);
|
|
const bool hasLineBreak = lineEnd != MobileGL::String::npos;
|
|
if (!hasLineBreak) {
|
|
lineEnd = source.size();
|
|
}
|
|
|
|
SizeT probe = lineStart;
|
|
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
probe++;
|
|
}
|
|
if (probe < lineEnd && source[probe] == '#') {
|
|
probe++;
|
|
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
probe++;
|
|
}
|
|
|
|
constexpr const char* defineToken = "define";
|
|
constexpr SizeT defineLen = 6;
|
|
const bool hasDefine = probe + defineLen <= lineEnd &&
|
|
source.compare(probe, defineLen, defineToken) == 0 &&
|
|
(probe + defineLen == lineEnd ||
|
|
!IsIdentifierChar(source[probe + defineLen]));
|
|
if (hasDefine) {
|
|
probe += defineLen;
|
|
while (probe < lineEnd && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
|
probe++;
|
|
}
|
|
|
|
const bool hasIdentifier = probe + identifier.size() <= lineEnd &&
|
|
source.compare(probe, identifier.size(), identifier) == 0 &&
|
|
(probe + identifier.size() == lineEnd ||
|
|
!IsIdentifierChar(source[probe + identifier.size()]));
|
|
if (hasIdentifier) {
|
|
source.erase(lineStart, lineEnd - lineStart + (hasLineBreak ? 1 : 0));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
SizeT FindAfterVersionDirective(const MobileGL::String& source) {
|
|
const ShaderLanguageInfo info = InspectShaderLanguage(source);
|
|
return info.HasVersionDirective() ? info.versionDirectiveEnd : 0;
|
|
}
|
|
|
|
bool IsExtensionAdvertised(MobileGL::GLExtension extension) {
|
|
const auto& activeBackendObject = MobileGL::MG_Backend::pActiveBackendObject;
|
|
if (!activeBackendObject) {
|
|
return true;
|
|
}
|
|
|
|
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
|
|
return std::find(extensions.begin(), extensions.end(), extension) != extensions.end();
|
|
}
|
|
|
|
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(MobileGL::String& source) {
|
|
if (IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) {
|
|
return;
|
|
}
|
|
|
|
SizeT lineStart = 0;
|
|
while (lineStart < source.size()) {
|
|
SizeT lineEnd = source.find('\n', lineStart);
|
|
const bool hasLineBreak = lineEnd != MobileGL::String::npos;
|
|
if (!hasLineBreak) {
|
|
lineEnd = source.size();
|
|
}
|
|
|
|
const MobileGL::String line = source.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") {
|
|
const MobileGL::String replacement =
|
|
"#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n";
|
|
source.replace(lineStart, replaceLen, replacement);
|
|
lineStart += replacement.size();
|
|
} else if (behavior == "enable" || behavior == "warn") {
|
|
source.replace(lineStart, replaceLen, "\n");
|
|
lineStart++;
|
|
} else {
|
|
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) {
|
|
// 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(FindAfterVersionDirective(source), "out vec4 mg_FragColor;\n");
|
|
}
|
|
if (usesFragData) {
|
|
ReplaceIdentifier(source, "gl_FragData", "mg_FragData");
|
|
source.insert(FindAfterVersionDirective(source), "layout(location = 0) out vec4 mg_FragData[8];\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
void InjectDepthRangeBuiltinShim(MobileGL::ShaderStage stage, MobileGL::String& source) {
|
|
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(FindAfterVersionDirective(source), shim);
|
|
}
|
|
} // namespace
|
|
|
|
namespace MobileGL {
|
|
namespace MG_Util {
|
|
namespace ShaderTranspiler {
|
|
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
|
|
String& source) {
|
|
constexpr Uint32 capturedSubgroupSize = 32;
|
|
if (stage != ShaderStage::Compute || nativeSubgroupSize <= capturedSubgroupSize ||
|
|
nativeSubgroupSize % capturedSubgroupSize != 0) {
|
|
return false;
|
|
}
|
|
|
|
// Vulkan subgroup widths are powers of two. Keep the workaround restricted to
|
|
// wider widths which are a power-of-two multiple of the captured 32-lane model.
|
|
const Uint32 subgroupScale = nativeSubgroupSize / capturedSubgroupSize;
|
|
if ((subgroupScale & (subgroupScale - 1u)) != 0u) {
|
|
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("%s: subgroupInclusiveAdd present but the linear prefix-scan template "
|
|
"did not match; the wide-subgroup 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;
|
|
}
|
|
|
|
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;
|
|
MG_Config::QuirkOverride (*GetOverride)();
|
|
Bool (*DeviceApplies)(const ShaderSourceQuirkContext&);
|
|
Bool (*Apply)(const ShaderSourceQuirkContext&, String&);
|
|
};
|
|
|
|
constexpr ShaderSourceQuirk kShaderSourceQuirks[] = {
|
|
{
|
|
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN
|
|
"subgroup-prefix-scan-rewrite",
|
|
[] { return MG_Config::Features.SubgroupPrefixScanQuirk; },
|
|
[](const ShaderSourceQuirkContext& ctx) {
|
|
// Qualcomm's Vulkan driver miscompiles the recognized float
|
|
// InclusiveScan pattern for native subgroups wider than the
|
|
// captured 32 lanes; other vendors compile it correctly and
|
|
// should keep their native scan.
|
|
return ctx.backend == BackendType::DirectVulkan &&
|
|
ctx.vendor == MG_Backend::GpuVendorKind::Qualcomm;
|
|
},
|
|
[](const ShaderSourceQuirkContext& ctx, String& source) {
|
|
return RewriteLinearSubgroupPrefixScanForVulkan(ctx.stage, ctx.subgroupSize,
|
|
source);
|
|
},
|
|
},
|
|
};
|
|
|
|
void ApplyShaderSourceQuirks(ShaderStage stage, String& source) {
|
|
const auto& activeBackend = MG_Backend::pActiveBackendObject;
|
|
if (!activeBackend) {
|
|
return;
|
|
}
|
|
const auto& dynamicParameters = activeBackend->GetDynamicParameters();
|
|
const ShaderSourceQuirkContext quirkContext{
|
|
stage,
|
|
activeBackend->GetBackendType(),
|
|
dynamicParameters.GpuVendor,
|
|
dynamicParameters.SubgroupSize,
|
|
};
|
|
for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) {
|
|
const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride();
|
|
if (quirkOverride == MG_Config::QuirkOverride::ForceOff) {
|
|
continue;
|
|
}
|
|
if (quirkOverride == MG_Config::QuirkOverride::Auto &&
|
|
!quirk.DeviceApplies(quirkContext)) {
|
|
continue;
|
|
}
|
|
if (quirk.Apply(quirkContext, source)) {
|
|
MGLOG_I("ApplyShaderSourceQuirks: applied '%s'%s", quirk.name,
|
|
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void PreprocessShaderSource(ShaderStage stage, String& source) {
|
|
// Normalize while the inspector's source span still refers to the untouched input. Later passes
|
|
// remove comments and directives, so any subsequent insertion re-inspects the current source.
|
|
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
|
|
NormalizeVersionDirective(source, originalLanguage);
|
|
|
|
// remove multi-line comment
|
|
size_t commentStartPos = source.find("/*");
|
|
while (commentStartPos != String::npos) {
|
|
size_t commentEndPos = source.find("*/", commentStartPos);
|
|
if (commentEndPos == String::npos) {
|
|
source.erase(commentStartPos);
|
|
break;
|
|
}
|
|
// + length of "*/"
|
|
source = source.replace(commentStartPos, commentEndPos - commentStartPos + 2, "");
|
|
commentStartPos = source.find("/*", commentStartPos);
|
|
}
|
|
|
|
// remove #line directives
|
|
SizeT linedirPos = source.find("#line");
|
|
while (linedirPos != String::npos) {
|
|
SizeT newlinePos = source.find('\n', linedirPos);
|
|
if (newlinePos == String::npos) {
|
|
source.erase(linedirPos);
|
|
break;
|
|
}
|
|
|
|
// Preserve a line break so adjacent preprocessor directives do not merge.
|
|
source = source.replace(linedirPos, newlinePos - linedirPos + 1, "\n");
|
|
linedirPos = source.find("#line", linedirPos);
|
|
}
|
|
|
|
// remove "noperspective"
|
|
const char* str_np = "noperspective";
|
|
const SizeT len_np = strlen(str_np);
|
|
SizeT noperspectivePos = source.find(str_np);
|
|
while (noperspectivePos != String::npos) {
|
|
// + length of "\n"
|
|
source = source.replace(noperspectivePos, len_np, "");
|
|
noperspectivePos = source.find(str_np);
|
|
}
|
|
|
|
FilterUnsupportedGpuShaderInt64(source);
|
|
CoerceUniformBlockPackingToStd140(source);
|
|
|
|
// Some shader packs define helpers with built-in GLSL names such as round(), tanh(), or fma().
|
|
// These may pass OpenGL-style validation but fail when recompiled for Vulkan/SPIR-V generation.
|
|
RenameBuiltinShadowingFunction(source, "round", "mg_round");
|
|
RenameBuiltinShadowingFunction(source, "tanh", "mg_tanh");
|
|
RenameBuiltinShadowingFunction(source, "fma", "mg_fma");
|
|
RenameBuiltinShadowingFunction(source, "min3", "mg_min3");
|
|
RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
|
|
ModernizeLegacyGLSL(stage, source);
|
|
InjectDepthRangeBuiltinShim(stage, source);
|
|
|
|
ApplyShaderSourceQuirks(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;
|
|
// 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;
|
|
|
|
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
|
"#version 460 core\n");
|
|
return true;
|
|
}
|
|
|
|
} // namespace ShaderTranspiler
|
|
} // namespace MG_Util
|
|
} // namespace MobileGL
|