mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Perf, Fix] (MG_Util): preprocessing cleanups - dead scanners, quote-mask bug, one version inspection per compile
Three scoped changes to ShaderSourceProcessor, none altering any transform's
output (pinned by a byte-stability test across the legacy-shader anchor path):
- Delete BlankBlockComments and RemoveDefineForIdentifier - dead since their
callers left; the former's newline-terminated quote handling moves into
MaskCommentsAndQuotedText (below) together with its rationale comment.
- Fix MaskCommentsAndQuotedText treating a quote as running past end-of-line.
GLSL has no multi-line literals, but a stray apostrophe in a directive or
comment tail ("#pragma message can't") blanked the REST OF THE FILE for
every masked consumer - the tokenizer, the version inspection, and the P0a
explicit-location/binding extractors silently lost everything after it.
- Inspect the shader language once per PreprocessShaderSource run instead of
up to five times: NormalizeVersionDirective now takes the already-computed
ShaderLanguageInfo, and the two after-version injections share one
AfterVersionAnchor instead of re-running a full masked sweep each
(FindAfterVersionDirective -> InspectShaderLanguage) to find the same spot.
Compile-phase timings hold (BSL 1.848s, complementary-reimagined ~5.7s);
retraces and the 435-test unit suite unchanged.
This commit is contained in:
@@ -2221,3 +2221,157 @@ void main() {
|
|||||||
EXPECT_NE(source.find("mg_min3(0.1, 0.2, 0.3)"), String::npos) << source;
|
EXPECT_NE(source.find("mg_min3(0.1, 0.2, 0.3)"), String::npos) << source;
|
||||||
EXPECT_EQ(source.find("float min3("), String::npos) << source;
|
EXPECT_EQ(source.find("float min3("), String::npos) << source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GLSL has no multi-line string or character literal, so a lone apostrophe never opens one - it is
|
||||||
|
// an English contraction, in a comment or in a diagnostic directive. MaskCommentsAndQuotedText used
|
||||||
|
// to disagree: it entered its quoted-text region on the apostrophe and, having no end-of-line rule,
|
||||||
|
// stayed there to the end of the file, blanking everything after it for every consumer of the mask
|
||||||
|
// (the tokenizer, the #version inspection, the explicit-location and opaque-binding extractors).
|
||||||
|
//
|
||||||
|
// Apostrophes inside comments were never affected - the comment region claims them first - but that
|
||||||
|
// is exactly the property the fix must not break, so pin it.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessKeepsApostrophesInsideCommentsHarmless) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 460 core
|
||||||
|
// don't do this: the sampler isn't bound before the first frame
|
||||||
|
/* and here's a block comment whose apostrophes shouldn't matter either */
|
||||||
|
uniform sampler2D tex;
|
||||||
|
in vec2 uv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
fragColor = texture(tex, uv);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
EXPECT_NE(source.find("void main()"), String::npos) << "shader body was blanked:\n" << source;
|
||||||
|
EXPECT_NE(source.find("fragColor = texture(tex, uv);"), String::npos) << source;
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The case the old masker actually broke: an apostrophe in real (non-comment) text. Everything after
|
||||||
|
// it looked like string interior, so ExtractExplicitUniformLocations tokenized a blank source and
|
||||||
|
// handed the GL location assigner an empty map - the uniform silently lost its explicit location.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessApostropheInDirectiveKeepsLaterCodeVisibleToExtractors) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 460 core
|
||||||
|
#pragma MG_NOTE(this pack can't run without explicit locations)
|
||||||
|
layout(location = 7) uniform vec4 tint;
|
||||||
|
in vec2 uv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
fragColor = tint * uv.x;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||||
|
ASSERT_EQ(locations.count("tint"), 1u) << "extractor went blind past the apostrophe:\n" << source;
|
||||||
|
EXPECT_EQ(locations.at("tint"), 7);
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreprocessShaderSource used to rediscover "where does the #version directive end?" once per
|
||||||
|
// injection - up to five whole-source masks and line scans per compile for one offset. It now takes
|
||||||
|
// the anchor once, from the pass that creates it, and tracks it.
|
||||||
|
//
|
||||||
|
// These three sources drive every consumer of that anchor: NormalizeLineDirectives (both the
|
||||||
|
// keep branch and the drop-ahead-of-#version branch), ModernizeLegacyGLSL's gl_FragColor
|
||||||
|
// injection, and InjectDepthRangeBuiltinShim's. The expected texts are the byte-exact output of
|
||||||
|
// the pre-memo implementation, captured from it - the change is pure memoization and is allowed to
|
||||||
|
// move no byte at all.
|
||||||
|
//
|
||||||
|
// Case B and case C are the two ways the anchor moves out from under the memo, and are why it is
|
||||||
|
// tracked rather than simply cached: B deletes a #line that precedes the version directive, and C
|
||||||
|
// has ModernizeLegacyGLSL's raw ReplaceIdentifier rewrite "varying"/"texture2D" inside a comment
|
||||||
|
// banner ahead of it, pulling the anchor six bytes left. An offset cached blindly would put the
|
||||||
|
// injected declaration six bytes inside the version line.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderOutputIsByteStableAcrossTheVersionAnchor) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const char* kLegacyBody = R"(#line 30
|
||||||
|
varying vec2 uv;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float d = gl_DepthRange.diff;
|
||||||
|
gl_FragColor = texture2D(tex, uv) * d;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* kExpectedBody =
|
||||||
|
"#version 330 core /*mobilegl-normalized-legacy*/\n"
|
||||||
|
"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"
|
||||||
|
"out vec4 mg_FragColor;\n"
|
||||||
|
"#line 30\n"
|
||||||
|
"in vec2 uv;\n"
|
||||||
|
"uniform sampler2D tex;\n"
|
||||||
|
"\n"
|
||||||
|
"void main() {\n"
|
||||||
|
" float d = gl_DepthRange.diff;\n"
|
||||||
|
" mg_FragColor = texture(tex, uv) * d;\n"
|
||||||
|
"}\n";
|
||||||
|
|
||||||
|
{
|
||||||
|
SCOPED_TRACE("A: version directive at offset 0");
|
||||||
|
String source = String("#version 120\n") + kLegacyBody;
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
EXPECT_EQ(source, String(kExpectedBody));
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SCOPED_TRACE("B: a #line ahead of the version directive is dropped, shortening the prefix");
|
||||||
|
String source = String("// pack preamble\n#line 1 \"world.fsh\"\n#version 120\n") + kLegacyBody;
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
// The dropped directive leaves its newline behind, so line numbering is untouched.
|
||||||
|
EXPECT_EQ(source, String("// pack preamble\n\n") + kExpectedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SCOPED_TRACE("C: a comment banner ahead of the version directive is itself rewritten");
|
||||||
|
String source = R"(/* legacy varying / texture2D helpers */
|
||||||
|
#version 120
|
||||||
|
varying vec2 uv;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = texture2D(tex, uv);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
EXPECT_EQ(source, String("/* legacy in / texture helpers */\n"
|
||||||
|
"#version 330 core /*mobilegl-normalized-legacy*/\n"
|
||||||
|
"out vec4 mg_FragColor;\n"
|
||||||
|
"in vec2 uv;\n"
|
||||||
|
"uniform sampler2D tex;\n"
|
||||||
|
"void main() {\n"
|
||||||
|
" mg_FragColor = texture(tex, uv);\n"
|
||||||
|
"}\n"));
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ namespace {
|
|||||||
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || 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) {
|
MobileGL::String MaskCommentsAndQuotedText(const MobileGL::String& source) {
|
||||||
enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText };
|
enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText };
|
||||||
|
|
||||||
@@ -85,9 +99,17 @@ namespace {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ch != '\n' && ch != '\r') {
|
// GLSL has no multi-line string literals, so a quote that reaches end of line was never
|
||||||
masked[pos] = ' ';
|
// 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) {
|
if (escaped) {
|
||||||
escaped = false;
|
escaped = false;
|
||||||
} else if (ch == '\\') {
|
} else if (ch == '\\') {
|
||||||
@@ -100,77 +122,6 @@ namespace {
|
|||||||
return masked;
|
return masked;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blank out block comments in place, leaving line comments and every other byte where it is.
|
|
||||||
//
|
|
||||||
// The passes that follow scan the source as raw text, so block comments have to stop being
|
|
||||||
// visible to them - but they must not be *deleted*: replacing the bytes with spaces keeps every
|
|
||||||
// later offset valid and keeps newlines, so glslang's diagnostics still point at the line the
|
|
||||||
// application wrote. It also has to be lexically aware. 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.
|
|
||||||
void BlankBlockComments(MobileGL::String& source) {
|
|
||||||
enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText };
|
|
||||||
|
|
||||||
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 == '/') {
|
|
||||||
pos++;
|
|
||||||
region = Region::SingleLineComment;
|
|
||||||
} else if (ch == '/' && next == '*') {
|
|
||||||
source[pos] = ' ';
|
|
||||||
source[pos + 1] = ' ';
|
|
||||||
pos++;
|
|
||||||
region = Region::MultiLineComment;
|
|
||||||
} else if (ch == '"' || ch == '\'') {
|
|
||||||
quote = ch;
|
|
||||||
escaped = false;
|
|
||||||
region = Region::QuotedText;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (region == Region::SingleLineComment) {
|
|
||||||
if (ch == '\n' || ch == '\r') region = Region::Code;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (region == Region::MultiLineComment) {
|
|
||||||
if (ch == '*' && next == '/') {
|
|
||||||
source[pos] = ' ';
|
|
||||||
source[pos + 1] = ' ';
|
|
||||||
pos++;
|
|
||||||
region = Region::Code;
|
|
||||||
} else if (ch != '\n' && ch != '\r') {
|
|
||||||
source[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.
|
|
||||||
if (ch == '\n' || ch == '\r') {
|
|
||||||
region = Region::Code;
|
|
||||||
} else if (escaped) {
|
|
||||||
escaped = false;
|
|
||||||
} else if (ch == '\\') {
|
|
||||||
escaped = true;
|
|
||||||
} else if (ch == quote) {
|
|
||||||
region = Region::Code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CodeToken {
|
struct CodeToken {
|
||||||
String text;
|
String text;
|
||||||
SizeT begin = 0;
|
SizeT begin = 0;
|
||||||
@@ -729,7 +680,17 @@ namespace {
|
|||||||
: "#version 460 core\n";
|
: "#version 460 core\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
// 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
|
// 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
|
// 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
|
// silently legalize the CTS directive.version_* rejection cases. Still drop a leading BOM so
|
||||||
@@ -738,7 +699,8 @@ namespace {
|
|||||||
if (info.hasUtf8Bom) {
|
if (info.hasUtf8Bom) {
|
||||||
source.erase(0, 3);
|
source.erase(0, 3);
|
||||||
}
|
}
|
||||||
return;
|
// The directive keeps its text and only slides left by the erased BOM.
|
||||||
|
return info.versionDirectiveEnd - bomBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MobileGL::String replacement = GetNormalizedVersionDirective(info);
|
const MobileGL::String replacement = GetNormalizedVersionDirective(info);
|
||||||
@@ -748,13 +710,16 @@ namespace {
|
|||||||
if (info.hasUtf8Bom) {
|
if (info.hasUtf8Bom) {
|
||||||
source.erase(0, 3);
|
source.erase(0, 3);
|
||||||
}
|
}
|
||||||
return;
|
// 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) {
|
if (info.hasUtf8Bom) {
|
||||||
source.erase(0, 3);
|
source.erase(0, 3);
|
||||||
}
|
}
|
||||||
source.insert(0, replacement);
|
source.insert(0, replacement);
|
||||||
|
return replacement.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start of the physical line containing `offset`, never scanning before `lowerBound`.
|
// Start of the physical line containing `offset`, never scanning before `lowerBound`.
|
||||||
@@ -969,65 +934,64 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
SizeT FindAfterVersionDirective(const MobileGL::String& source) {
|
||||||
const ShaderLanguageInfo info = InspectShaderLanguage(source);
|
const ShaderLanguageInfo info = InspectShaderLanguage(source);
|
||||||
return info.HasVersionDirective() ? info.versionDirectiveEnd : 0;
|
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
|
// 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
|
// 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
|
// 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
|
// 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.
|
// quoted operand keeps the directive doing its job and still hands glslang something it accepts.
|
||||||
void NormalizeLineDirectives(MobileGL::String& source) {
|
//
|
||||||
|
// `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);
|
const MobileGL::String masked = MaskCommentsAndQuotedText(source);
|
||||||
const SizeT versionEnd = FindAfterVersionDirective(source);
|
|
||||||
MobileGL::String result;
|
MobileGL::String result;
|
||||||
result.reserve(source.size());
|
result.reserve(source.size());
|
||||||
|
|
||||||
@@ -1244,7 +1208,12 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) {
|
// `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
|
// Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and
|
||||||
// ignored in the normalized desktop core profiles, so glslang handles them natively.
|
// ignored in the normalized desktop core profiles, so glslang handles them natively.
|
||||||
|
|
||||||
@@ -1265,16 +1234,17 @@ namespace {
|
|||||||
const bool usesFragData = source.find("gl_FragData") != MobileGL::String::npos;
|
const bool usesFragData = source.find("gl_FragData") != MobileGL::String::npos;
|
||||||
if (usesFragColor) {
|
if (usesFragColor) {
|
||||||
ReplaceIdentifier(source, "gl_FragColor", "mg_FragColor");
|
ReplaceIdentifier(source, "gl_FragColor", "mg_FragColor");
|
||||||
source.insert(FindAfterVersionDirective(source), "out vec4 mg_FragColor;\n");
|
source.insert(afterVersion.Get(source), "out vec4 mg_FragColor;\n");
|
||||||
}
|
}
|
||||||
if (usesFragData) {
|
if (usesFragData) {
|
||||||
ReplaceIdentifier(source, "gl_FragData", "mg_FragData");
|
ReplaceIdentifier(source, "gl_FragData", "mg_FragData");
|
||||||
source.insert(FindAfterVersionDirective(source), "layout(location = 0) out vec4 mg_FragData[8];\n");
|
source.insert(afterVersion.Get(source), "layout(location = 0) out vec4 mg_FragData[8];\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InjectDepthRangeBuiltinShim(MobileGL::ShaderStage stage, MobileGL::String& source) {
|
void InjectDepthRangeBuiltinShim(MobileGL::ShaderStage stage, MobileGL::String& source,
|
||||||
|
AfterVersionAnchor& afterVersion) {
|
||||||
if (stage != MobileGL::ShaderStage::Fragment) return;
|
if (stage != MobileGL::ShaderStage::Fragment) return;
|
||||||
if (source.find("gl_DepthRange") == MobileGL::String::npos) return;
|
if (source.find("gl_DepthRange") == MobileGL::String::npos) return;
|
||||||
if (source.find("mg_DepthRangeParameters") != MobileGL::String::npos) return;
|
if (source.find("mg_DepthRangeParameters") != MobileGL::String::npos) return;
|
||||||
@@ -1283,7 +1253,7 @@ namespace {
|
|||||||
"struct mg_DepthRangeParameters { float near; float far; float diff; };\n"
|
"struct mg_DepthRangeParameters { float near; float far; float diff; };\n"
|
||||||
"const mg_DepthRangeParameters mg_DepthRange = mg_DepthRangeParameters(0.0, 1.0, 1.0);\n"
|
"const mg_DepthRangeParameters mg_DepthRange = mg_DepthRangeParameters(0.0, 1.0, 1.0);\n"
|
||||||
"#define gl_DepthRange mg_DepthRange\n";
|
"#define gl_DepthRange mg_DepthRange\n";
|
||||||
source.insert(FindAfterVersionDirective(source), shim);
|
source.insert(afterVersion.Get(source), shim);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1399,10 +1369,14 @@ namespace MobileGL {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void PreprocessShaderSource(ShaderStage stage, String& source) {
|
void PreprocessShaderSource(ShaderStage stage, String& source) {
|
||||||
// Normalize while the inspector's source span still refers to the untouched input. Later passes
|
// Normalize while the inspector's source span still refers to the untouched input.
|
||||||
// remove comments and directives, so any subsequent insertion re-inspects the current source.
|
|
||||||
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
|
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
|
||||||
NormalizeVersionDirective(source, originalLanguage);
|
|
||||||
|
// 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
|
// 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
|
// preprocessing token that collapses to one space even across newlines and inside a
|
||||||
@@ -1411,7 +1385,7 @@ namespace MobileGL {
|
|||||||
// preprocessor multiline_comment_define / redefine_object / function_redefinition).
|
// preprocessor multiline_comment_define / redefine_object / function_redefinition).
|
||||||
// Every MobileGL pass that must ignore comment/string text already masks them locally
|
// Every MobileGL pass that must ignore comment/string text already masks them locally
|
||||||
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
|
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
|
||||||
NormalizeLineDirectives(source);
|
NormalizeLineDirectives(source, afterVersion.Get(source));
|
||||||
|
|
||||||
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
|
// 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
|
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
|
||||||
@@ -1426,8 +1400,8 @@ namespace MobileGL {
|
|||||||
|
|
||||||
RenameBuiltinShadowingFunctions(source);
|
RenameBuiltinShadowingFunctions(source);
|
||||||
|
|
||||||
ModernizeLegacyGLSL(stage, source);
|
ModernizeLegacyGLSL(stage, source, afterVersion);
|
||||||
InjectDepthRangeBuiltinShim(stage, source);
|
InjectDepthRangeBuiltinShim(stage, source, afterVersion);
|
||||||
|
|
||||||
ApplyShaderSourceQuirks(stage, source);
|
ApplyShaderSourceQuirks(stage, source);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user