Fix: MilkdropShader - ignore sampler keywords in shader comments (#958)

This commit is contained in:
Michael Baetgen
2026-03-04 23:32:30 -06:00
committed by Kai Blaschke
parent 23cb45ba9c
commit 55fb0345ca
5 changed files with 1018 additions and 19 deletions

View File

@ -341,20 +341,23 @@ void MilkdropShader::PreprocessPresetShader(std::string& program)
// Find "sampler_state" overrides and remove them first, as they're not supported by GLSL.
// The logic isn't totally fool-proof, but should work in general.
found = program.find("sampler_state");
// Use a comment-stripped copy for searching so commented-out sampler_state blocks are skipped.
// StripComments preserves string length, so positions map 1:1 to the original.
std::string stripped = Utils::StripComments(program);
found = stripped.find("sampler_state");
while (found != std::string::npos)
{
// Now go backwards and find the assignment
found = program.rfind('=', found);
found = stripped.rfind('=', found);
auto startPos = found;
// Find closing brace and semicolon
found = program.find('}', found);
found = program.find(';', found);
found = stripped.find('}', found);
found = stripped.find(';', found);
if (found != std::string::npos)
{
program.replace(startPos, found - startPos, "");
stripped.replace(startPos, found - startPos, "");
}
else
{
@ -362,11 +365,12 @@ void MilkdropShader::PreprocessPresetShader(std::string& program)
break;
}
found = program.find("sampler_state");
found = stripped.find("sampler_state");
}
// replace shader_body with entry point function
found = program.find("shader_body");
// Use the stripped copy so a commented-out shader_body is not matched.
found = stripped.find("shader_body");
if (found != std::string::npos)
{
if (m_type == ShaderType::WarpShader)
@ -430,7 +434,7 @@ void PS(float4 _vDiffuse : COLOR,
switch (program.at(pos))
{
case '/':
// Skip comments until EoL to prevent false counting
// Skip line comments until EoL to prevent false counting
if (pos < program.length() - 1 && program.at(pos + 1) == '/')
{
for (; pos < program.length(); ++pos)
@ -441,6 +445,19 @@ void PS(float4 _vDiffuse : COLOR,
}
}
}
// Skip block comments to prevent false counting
else if (pos < program.length() - 1 && program.at(pos + 1) == '*')
{
pos += 2;
for (; pos < program.length() - 1; ++pos)
{
if (program.at(pos) == '*' && program.at(pos + 1) == '/')
{
++pos; // skip past '/'
break;
}
}
}
continue;
case '{':
@ -492,16 +509,19 @@ void MilkdropShader::GetReferencedSamplers(const std::string& program)
// "main" should always be present.
m_samplerNames.insert("main");
// Strip comments so that commented-out sampler/texsize declarations are not matched.
std::string const stripped = Utils::StripComments(program);
// Search for sampler usage
auto found = program.find("sampler_", 0);
auto found = stripped.find("sampler_", 0);
while (found != std::string::npos)
{
found += 8;
size_t const end = program.find_first_of(" ;,\n\r)", found);
size_t const end = stripped.find_first_of(" ;,\n\r)", found);
if (end != std::string::npos)
{
std::string const sampler = program.substr(static_cast<int>(found), static_cast<int>(end - found));
std::string const sampler = stripped.substr(static_cast<int>(found), static_cast<int>(end - found));
// Skip "sampler_state", as it's a reserved word and not a sampler.
if (sampler != "state")
{
@ -509,23 +529,23 @@ void MilkdropShader::GetReferencedSamplers(const std::string& program)
}
}
found = program.find("sampler_", found);
found = stripped.find("sampler_", found);
}
// Also search for texsize usage, some presets don't reference the sampler.
found = program.find("texsize_", 0);
found = stripped.find("texsize_", 0);
while (found != std::string::npos)
{
found += 8;
size_t const end = program.find_first_of(" ;,.\n\r)", found);
size_t const end = stripped.find_first_of(" ;,.\n\r)", found);
if (end != std::string::npos)
{
std::string const sampler = program.substr(static_cast<int>(found), static_cast<int>(end - found));
std::string const sampler = stripped.substr(static_cast<int>(found), static_cast<int>(end - found));
m_samplerNames.insert(sampler);
}
found = program.find("texsize_", found);
found = stripped.find("texsize_", found);
}
{
@ -555,15 +575,15 @@ void MilkdropShader::GetReferencedSamplers(const std::string& program)
}
}
if (program.find("GetBlur3") != std::string::npos)
if (stripped.find("GetBlur3") != std::string::npos)
{
UpdateMaxBlurLevel(BlurTexture::BlurLevel::Blur3);
}
else if (program.find("GetBlur2") != std::string::npos)
else if (stripped.find("GetBlur2") != std::string::npos)
{
UpdateMaxBlurLevel(BlurTexture::BlurLevel::Blur2);
}
else if (program.find("GetBlur1") != std::string::npos)
else if (stripped.find("GetBlur1") != std::string::npos)
{
UpdateMaxBlurLevel(BlurTexture::BlurLevel::Blur1);
}

View File

@ -29,5 +29,53 @@ void ToUpperInPlace(std::string& str)
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
}
auto StripComments(const std::string& source) -> std::string
{
std::string result = source;
size_t i = 0;
while (i < result.size())
{
if (i + 1 < result.size() && result.at(i) == '/' && result.at(i + 1) == '/')
{
// Line comment: replace until end of line
while (i < result.size() && result.at(i) != '\n' && result.at(i) != '\r')
{
result.at(i) = ' ';
i++;
}
}
else if (i + 1 < result.size() && result.at(i) == '/' && result.at(i + 1) == '*')
{
// Block comment: replace until closing */
result.at(i) = ' ';
result.at(i + 1) = ' ';
i += 2;
while (i < result.size())
{
if (i + 1 < result.size() && result.at(i) == '*' && result.at(i + 1) == '/')
{
result.at(i) = ' ';
result.at(i + 1) = ' ';
i += 2;
break;
}
// Preserve newlines to keep line structure intact
if (result.at(i) != '\n' && result.at(i) != '\r')
{
result.at(i) = ' ';
}
i++;
}
}
else
{
i++;
}
}
return result;
}
} // namespace Utils
} // namespace libprojectM

View File

@ -11,5 +11,16 @@ auto ToUpper(const std::string& str) -> std::string;
void ToLowerInPlace(std::string& str);
void ToUpperInPlace(std::string& str);
/**
* @brief Strips C and C++ style comments from source code.
*
* Replaces // line comments and block comments with spaces, preserving
* string length and newline positions so that character offsets remain valid.
*
* @param source The source code string to strip comments from.
* @return A copy of the source with all comment content replaced by spaces.
*/
auto StripComments(const std::string& source) -> std::string;
} // namespace Utils
} // namespace libprojectM

View File

@ -3,6 +3,7 @@ find_package(GTest 1.10 REQUIRED NO_MODULE)
add_executable(projectM-unittest
WaveformAlignerTest.cpp
PresetFileParserTest.cpp
MilkdropShaderCommentParsingTest.cpp
$<TARGET_OBJECTS:Audio>
$<TARGET_OBJECTS:MilkdropPreset>

View File

@ -0,0 +1,919 @@
#include <gtest/gtest.h>
#include <Utils.hpp>
#include <regex>
#include <set>
#include <string>
using libprojectM::Utils::StripComments;
// =====================================================================
// Test every comment-sensitive code path
// from MilkdropShader.cpp.
// =====================================================================
/**
* Mirrors MilkdropShader::GetReferencedSamplers() sampler_ extraction.
* (MilkdropShader.cpp ~lines 512-516)
*/
static auto ExtractSamplerNames(const std::string& program) -> std::set<std::string>
{
std::set<std::string> samplerNames;
samplerNames.insert("main");
std::string const stripped = StripComments(program);
auto found = stripped.find("sampler_", 0);
while (found != std::string::npos)
{
found += 8;
size_t const end = stripped.find_first_of(" ;,\n\r)", found);
if (end != std::string::npos)
{
std::string const sampler = stripped.substr(
static_cast<int>(found), static_cast<int>(end - found));
if (sampler != "state")
{
samplerNames.insert(sampler);
}
}
found = stripped.find("sampler_", found);
}
found = stripped.find("texsize_", 0);
while (found != std::string::npos)
{
found += 8;
size_t const end = stripped.find_first_of(" ;,.\n\r)", found);
if (end != std::string::npos)
{
std::string const sampler = stripped.substr(
static_cast<int>(found), static_cast<int>(end - found));
samplerNames.insert(sampler);
}
found = stripped.find("texsize_", found);
}
return samplerNames;
}
/**
* Mirrors MilkdropShader::GetReferencedSamplers() GetBlur detection.
* (MilkdropShader.cpp ~lines 561-576)
* Returns 0 for none, 1/2/3 for the highest blur level detected.
*/
static auto DetectBlurLevel(const std::string& program) -> int
{
std::string const stripped = StripComments(program);
if (stripped.find("GetBlur3") != std::string::npos)
{
return 3;
}
if (stripped.find("GetBlur2") != std::string::npos)
{
return 2;
}
if (stripped.find("GetBlur1") != std::string::npos)
{
return 1;
}
return 0;
}
/**
* Mirrors PreprocessPresetShader() sampler_state removal.
* (MilkdropShader.cpp ~lines 342-371)
* Removes "= sampler_state { ... };" blocks from program, using
* comment-stripped text for searching.
*/
static void RemoveSamplerStateBlocks(std::string& program)
{
std::string stripped = StripComments(program);
auto found = stripped.find("sampler_state");
while (found != std::string::npos)
{
found = stripped.rfind('=', found);
auto startPos = found;
found = stripped.find('}', found);
found = stripped.find(';', found);
if (found != std::string::npos)
{
program.replace(startPos, found - startPos, "");
stripped = StripComments(program);
}
else
{
break;
}
found = stripped.find("sampler_state");
}
}
/**
* Mirrors PreprocessPresetShader() shader_body finding.
* (MilkdropShader.cpp ~lines 373-376)
* Returns the position of "shader_body" in the original program,
* or std::string::npos if not found (ignoring commented instances).
*/
static auto FindShaderBody(const std::string& program) -> size_t
{
std::string const stripped = StripComments(program);
return stripped.find("shader_body");
}
/**
* Mirrors PreprocessPresetShader() brace-counting with comment skipping.
* (MilkdropShader.cpp ~lines 432-473)
* Given a string starting just after the opening brace, counts nested
* braces and returns the position of the matching closing brace,
* skipping braces inside // and block comments.
*/
static auto CountBracesToClose(const std::string& program, size_t startPos) -> size_t
{
int bracesOpen = 1;
size_t pos = startPos;
for (; pos < program.length() && bracesOpen > 0; ++pos)
{
switch (program.at(pos))
{
case '/':
// Skip line comments
if (pos < program.length() - 1 && program.at(pos + 1) == '/')
{
for (; pos < program.length(); ++pos)
{
if (program.at(pos) == '\n')
{
break;
}
}
}
// Skip block comments
else if (pos < program.length() - 1 && program.at(pos + 1) == '*')
{
pos += 2;
for (; pos < program.length() - 1; ++pos)
{
if (program.at(pos) == '*' && program.at(pos + 1) == '/')
{
++pos;
break;
}
}
}
continue;
case '{':
bracesOpen++;
continue;
case '}':
bracesOpen--;
}
}
return pos;
}
/**
* Mirrors TranspileHLSLShader() regex-based sampler declaration removal.
* (MilkdropShader.cpp ~lines 625-629)
*/
static void RegexRemoveSamplerDeclarations(std::string& source)
{
std::smatch matches;
while (std::regex_search(source, matches,
std::regex("sampler(2D|3D|)(\\s+|\\().*")))
{
source.replace(matches.position(), matches.length(), "");
}
}
/**
* Mirrors TranspileHLSLShader() regex-based texsize declaration removal.
* (MilkdropShader.cpp ~lines 632-635)
*/
static void RegexRemoveTexsizeDeclarations(std::string& source)
{
std::smatch matches;
while (std::regex_search(source, matches,
std::regex("float4\\s+texsize_.*")))
{
source.replace(matches.position(), matches.length(), "");
}
}
// =====================================================================
// StripComments unit tests
// =====================================================================
TEST(StripComments, PreservesCodeWithNoComments)
{
std::string input = "uniform sampler2D sampler_mytex;\n";
EXPECT_EQ(StripComments(input), input);
}
TEST(StripComments, StripsLineComment)
{
std::string result = StripComments("code // comment\nmore code\n");
EXPECT_EQ(result, "code \nmore code\n");
}
TEST(StripComments, StripsLineCommentNoSpace)
{
std::string result = StripComments("//sampler sampler_rand00;\n");
EXPECT_EQ(result.size(), std::string("//sampler sampler_rand00;\n").size());
EXPECT_EQ(result.back(), '\n');
EXPECT_EQ(result.find("sampler"), std::string::npos)
<< "All comment content should be replaced with spaces";
}
TEST(StripComments, StripsBlockComment)
{
std::string result = StripComments("before /* inside */ after\n");
EXPECT_EQ(result, "before after\n");
}
TEST(StripComments, StripsMultiLineBlockComment)
{
std::string input =
"before\n"
"/* line1\n"
" line2 */\n"
"after\n";
std::string result = StripComments(input);
EXPECT_NE(result.find("before"), std::string::npos);
EXPECT_NE(result.find("after"), std::string::npos);
EXPECT_EQ(result.find("line1"), std::string::npos);
EXPECT_EQ(result.find("line2"), std::string::npos);
EXPECT_EQ(result.size(), input.size()) << "String length must be preserved";
}
TEST(StripComments, PreservesNewlinesInBlockComment)
{
std::string input = "/*\nfoo\nbar\n*/\n";
std::string result = StripComments(input);
auto countNewlines = [](const std::string& s) {
return std::count(s.begin(), s.end(), '\n');
};
EXPECT_EQ(countNewlines(result), countNewlines(input));
}
TEST(StripComments, EmptyString)
{
EXPECT_EQ(StripComments(""), "");
}
TEST(StripComments, UnterminatedBlockComment)
{
std::string result = StripComments("code /* no close");
EXPECT_EQ(result.substr(0, 5), "code ");
EXPECT_EQ(result.find("no"), std::string::npos);
}
TEST(StripComments, PreservesStringLength)
{
// Every input should produce output of the same length
std::string inputs[] = {
"hello // world\n",
"a /* b */ c\n",
"// entire line\n",
"/* multi\nline\ncomment */\n",
"no comments here\n",
};
for (const auto& input : inputs)
{
EXPECT_EQ(StripComments(input).size(), input.size())
<< "Length mismatch for: " << input;
}
}
TEST(StripComments, LineCommentAtEndOfFileNoNewline)
{
std::string result = StripComments("code // trailing");
EXPECT_EQ(result, "code ");
}
TEST(StripComments, NestedSlashesInsideBlockComment)
{
// "//" inside a block comment should NOT start a line comment
std::string result = StripComments("/* // not a line comment */ real_code\n");
EXPECT_NE(result.find("real_code"), std::string::npos)
<< "Code after block comment close must survive";
}
TEST(StripComments, SlashStarInsideLineComment)
{
// "/*" inside a line comment should NOT start a block comment
std::string result = StripComments("// /* not block\nreal_code\n");
EXPECT_NE(result.find("real_code"), std::string::npos)
<< "Code on next line after // /* must survive";
}
TEST(StripComments, ConsecutiveComments)
{
std::string input = "// first\n// second\ncode;\n";
std::string result = StripComments(input);
EXPECT_EQ(result.find("first"), std::string::npos);
EXPECT_EQ(result.find("second"), std::string::npos);
EXPECT_NE(result.find("code"), std::string::npos);
}
TEST(StripComments, AdjacentBlockComments)
{
std::string input = "/* a *//* b */code;\n";
std::string result = StripComments(input);
EXPECT_EQ(result.find('a'), std::string::npos);
EXPECT_EQ(result.find('b'), std::string::npos);
EXPECT_NE(result.find("code"), std::string::npos);
}
TEST(StripComments, SingleSlashNotComment)
{
// A lone slash (e.g. division) should not be treated as a comment start
std::string input = "x = a / b;\n";
EXPECT_EQ(StripComments(input), input);
}
// =====================================================================
// GetReferencedSamplers: sampler_ / texsize_ extraction
// =====================================================================
TEST(SamplerParsing, FindsUncommentedSampler)
{
auto names = ExtractSamplerNames("uniform sampler2D sampler_mytex;\n");
EXPECT_TRUE(names.count("main"));
EXPECT_TRUE(names.count("mytex"));
}
TEST(SamplerParsing, IgnoresLineCommentedSampler)
{
auto names = ExtractSamplerNames("// sampler sampler_rand00;\n");
EXPECT_FALSE(names.count("rand00"))
<< "sampler_rand00 inside a // comment should not be extracted";
}
TEST(SamplerParsing, NoSpaceAfterSlashesStillCommented)
{
auto names = ExtractSamplerNames("//sampler sampler_rand00;\n");
EXPECT_FALSE(names.count("rand00"))
<< "sampler_rand00 after //sampler (no space) should not be extracted";
}
TEST(SamplerParsing, IgnoresBlockCommentedSampler)
{
auto names = ExtractSamplerNames("/* sampler_rand00 */\n");
EXPECT_FALSE(names.count("rand00"))
<< "sampler_rand00 inside /* */ should not be extracted";
}
TEST(SamplerParsing, BlockCommentSpansMultipleLines)
{
std::string program =
"/*\n"
" sampler_foo;\n"
" texsize_bar;\n"
"*/\n"
"uniform sampler2D sampler_real;\n";
auto names = ExtractSamplerNames(program);
EXPECT_FALSE(names.count("foo"));
EXPECT_FALSE(names.count("bar"));
EXPECT_TRUE(names.count("real"));
}
TEST(SamplerParsing, SamplerAfterBlockCommentCloseIsFound)
{
auto names = ExtractSamplerNames("/* comment */ sampler_visible;\n");
EXPECT_TRUE(names.count("visible"));
}
TEST(SamplerParsing, IgnoresLineCommentedTexsize)
{
auto names = ExtractSamplerNames("// texsize_rand00;\n");
EXPECT_FALSE(names.count("rand00"));
}
TEST(SamplerParsing, IgnoresBlockCommentedTexsize)
{
auto names = ExtractSamplerNames("/* texsize_rand00; */\n");
EXPECT_FALSE(names.count("rand00"));
}
TEST(SamplerParsing, TexsizeInBlockCommentSamplerOutside)
{
std::string program =
"/* texsize_hidden; */\n"
"float4 ts = texsize_visible;\n";
auto names = ExtractSamplerNames(program);
EXPECT_FALSE(names.count("hidden"));
EXPECT_TRUE(names.count("visible"));
}
TEST(SamplerParsing, MixedCommentedAndUncommented)
{
std::string program =
"//sampler sampler_rand00;\n"
"uniform sampler2D sampler_tex1;\n"
"/* sampler_hidden; */\n"
"uniform sampler2D sampler_tex2;\n";
auto names = ExtractSamplerNames(program);
EXPECT_FALSE(names.count("rand00"));
EXPECT_FALSE(names.count("hidden"));
EXPECT_TRUE(names.count("tex1"));
EXPECT_TRUE(names.count("tex2"));
}
TEST(SamplerParsing, SkipsSamplerState)
{
auto names = ExtractSamplerNames("sampler_state { Filter = LINEAR; };\n");
EXPECT_FALSE(names.count("state"))
<< "sampler_state is a reserved word and should be skipped";
}
TEST(SamplerParsing, CommentedSamplerStateNotExtracted)
{
auto names = ExtractSamplerNames("// sampler_state { Filter = LINEAR; };\n");
EXPECT_EQ(names.size(), 1u)
<< "Only 'main' should be present; entire line is a comment";
}
TEST(SamplerParsing, EmptyProgram)
{
auto names = ExtractSamplerNames("");
EXPECT_EQ(names.size(), 1u);
EXPECT_TRUE(names.count("main"));
}
TEST(SamplerParsing, ReproducesCrashPresetPattern)
{
std::string program =
"//sampler sampler_rand00; // this will choose a random texture from disk!\n";
auto names = ExtractSamplerNames(program);
EXPECT_EQ(names.size(), 1u);
EXPECT_FALSE(names.count("rand00"));
}
TEST(SamplerParsing, MultipleSamplersOnSameLinePartiallyCommented)
{
// sampler_a is real code, sampler_b is in a trailing comment
std::string program = "sampler_a; // sampler_b;\n";
auto names = ExtractSamplerNames(program);
EXPECT_TRUE(names.count("a"));
EXPECT_FALSE(names.count("b"))
<< "sampler_b after // on the same line should be ignored";
}
TEST(SamplerParsing, SamplerInsideBlockCommentWithCodeOnSameLine)
{
std::string program = "real /* sampler_fake; */ sampler_real;\n";
auto names = ExtractSamplerNames(program);
EXPECT_FALSE(names.count("fake"));
EXPECT_TRUE(names.count("real"));
}
// =====================================================================
// GetReferencedSamplers: GetBlur detection
// =====================================================================
TEST(BlurDetection, NoBlur)
{
EXPECT_EQ(DetectBlurLevel("ret = tex2D(sampler_main, uv);\n"), 0);
}
TEST(BlurDetection, DetectsBlur1)
{
EXPECT_EQ(DetectBlurLevel("ret = GetBlur1(uv);\n"), 1);
}
TEST(BlurDetection, DetectsBlur2)
{
EXPECT_EQ(DetectBlurLevel("ret = GetBlur2(uv);\n"), 2);
}
TEST(BlurDetection, DetectsBlur3)
{
EXPECT_EQ(DetectBlurLevel("ret = GetBlur3(uv);\n"), 3);
}
TEST(BlurDetection, Blur3TakesPrecedenceOverBlur1)
{
// GetBlur3 should be detected even if GetBlur1 also appears
EXPECT_EQ(DetectBlurLevel("a = GetBlur1(uv);\nb = GetBlur3(uv);\n"), 3);
}
TEST(BlurDetection, IgnoresBlur1InLineComment)
{
EXPECT_EQ(DetectBlurLevel("// ret = GetBlur1(uv);\n"), 0)
<< "GetBlur1 inside // comment should not be detected";
}
TEST(BlurDetection, IgnoresBlur2InLineComment)
{
EXPECT_EQ(DetectBlurLevel("// ret = GetBlur2(uv);\n"), 0)
<< "GetBlur2 inside // comment should not be detected";
}
TEST(BlurDetection, IgnoresBlur3InLineComment)
{
EXPECT_EQ(DetectBlurLevel("// ret = GetBlur3(uv);\n"), 0)
<< "GetBlur3 inside // comment should not be detected";
}
TEST(BlurDetection, IgnoresBlurInBlockComment)
{
EXPECT_EQ(DetectBlurLevel("/* GetBlur3(uv) */\n"), 0)
<< "GetBlur3 inside /* */ comment should not be detected";
}
TEST(BlurDetection, CommentedBlur3RealBlur1)
{
std::string program =
"// GetBlur3(uv);\n"
"ret = GetBlur1(uv);\n";
EXPECT_EQ(DetectBlurLevel(program), 1)
<< "Commented GetBlur3 should be ignored; only real GetBlur1 counts";
}
TEST(BlurDetection, BlockCommentedBlur2RealBlur1)
{
std::string program =
"/* GetBlur2(uv); */\n"
"ret = GetBlur1(uv);\n";
EXPECT_EQ(DetectBlurLevel(program), 1)
<< "Block-commented GetBlur2 should be ignored; only real GetBlur1 counts";
}
TEST(BlurDetection, AllBlurLevelsCommentedOut)
{
std::string program =
"// GetBlur1(uv);\n"
"/* GetBlur2(uv); */\n"
"// GetBlur3(uv);\n";
EXPECT_EQ(DetectBlurLevel(program), 0)
<< "All blur calls are commented; blur level should be 0";
}
// =====================================================================
// PreprocessPresetShader: sampler_state removal
// =====================================================================
TEST(SamplerStateRemoval, RemovesRealSamplerState)
{
std::string program =
"sampler mytex = sampler_state { Filter = LINEAR; };\n"
"shader_body { }\n";
RemoveSamplerStateBlocks(program);
EXPECT_EQ(program.find("sampler_state"), std::string::npos)
<< "Real sampler_state block should be removed";
EXPECT_NE(program.find("shader_body"), std::string::npos)
<< "shader_body should be preserved";
}
TEST(SamplerStateRemoval, SkipsLineCommentedSamplerState)
{
std::string program =
"float x = 1.0;\n"
"// sampler mytex = sampler_state { Filter = LINEAR; };\n"
"shader_body { }\n";
std::string original = program;
RemoveSamplerStateBlocks(program);
EXPECT_EQ(program, original)
<< "Commented sampler_state should not trigger any removal";
}
TEST(SamplerStateRemoval, SkipsBlockCommentedSamplerState)
{
std::string program =
"float x = 1.0;\n"
"/* sampler mytex = sampler_state { Filter = LINEAR; }; */\n"
"shader_body { }\n";
std::string original = program;
RemoveSamplerStateBlocks(program);
EXPECT_EQ(program, original)
<< "Block-commented sampler_state should not trigger any removal";
}
TEST(SamplerStateRemoval, RemovesRealButSkipsCommented)
{
std::string program =
"// sampler a = sampler_state { Filter = POINT; };\n"
"sampler b = sampler_state { Filter = LINEAR; };\n"
"shader_body { }\n";
RemoveSamplerStateBlocks(program);
EXPECT_EQ(program.find("Filter = LINEAR"), std::string::npos)
<< "The real sampler_state block should be removed";
EXPECT_NE(program.find("Filter = POINT"), std::string::npos)
<< "The commented sampler_state block should be untouched";
}
TEST(SamplerStateRemoval, MultipleRealSamplerStates)
{
std::string program =
"sampler a = sampler_state { Filter = POINT; };\n"
"sampler b = sampler_state { Filter = LINEAR; };\n"
"shader_body { }\n";
RemoveSamplerStateBlocks(program);
EXPECT_EQ(program.find("sampler_state"), std::string::npos)
<< "Both sampler_state blocks should be removed";
}
TEST(SamplerStateRemoval, NoSamplerState)
{
std::string program = "shader_body { }\n";
std::string original = program;
RemoveSamplerStateBlocks(program);
EXPECT_EQ(program, original);
}
// =====================================================================
// PreprocessPresetShader: shader_body finding
// =====================================================================
TEST(ShaderBodyFind, FindsRealShaderBody)
{
std::string program = "shader_body\n{\n}\n";
size_t pos = FindShaderBody(program);
EXPECT_EQ(pos, 0u);
}
TEST(ShaderBodyFind, SkipsLineCommentedShaderBody)
{
std::string program = "// shader_body\nshader_body\n{\n}\n";
size_t pos = FindShaderBody(program);
EXPECT_NE(pos, std::string::npos);
// Should find the one on the second line, not the first
EXPECT_GT(pos, program.find('\n'))
<< "Should skip the commented shader_body and find the real one";
}
TEST(ShaderBodyFind, SkipsBlockCommentedShaderBody)
{
std::string program = "/* shader_body */ shader_body\n{\n}\n";
size_t pos = FindShaderBody(program);
EXPECT_NE(pos, std::string::npos);
// The real one starts after the block comment
EXPECT_GT(pos, static_cast<size_t>(0));
}
TEST(ShaderBodyFind, AllShaderBodiesCommented)
{
std::string program = "// shader_body\n/* shader_body */\n{\n}\n";
size_t pos = FindShaderBody(program);
EXPECT_EQ(pos, std::string::npos)
<< "No real shader_body exists; should return npos";
}
TEST(ShaderBodyFind, ShaderBodyAfterCodeAndComments)
{
std::string program =
"// some commented shader_body\n"
"float4 x = 1.0;\n"
"/* another shader_body */\n"
"shader_body\n{\n}\n";
size_t pos = FindShaderBody(program);
EXPECT_NE(pos, std::string::npos);
// Should be on the last "shader_body" line
std::string before = program.substr(0, pos);
// Count newlines before the found position — should be 3
auto newlines = std::count(before.begin(), before.end(), '\n');
EXPECT_EQ(newlines, 3)
<< "Real shader_body should be on the 4th line (after 3 newlines)";
}
// =====================================================================
// PreprocessPresetShader: brace counting with comment skipping
// =====================================================================
TEST(BraceCounting, SimpleNesting)
{
// "{ a { b } c }" — start after first {, should stop after last }
std::string program = " a { b } c }";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}');
// After processing, bracesOpen should be 0 at position of last '}'
EXPECT_EQ(pos, program.length());
}
TEST(BraceCounting, SkipsBracesInLineComment)
{
// Braces inside // comment should not count
std::string program = " // { } \n}";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}')
<< "Should find the real closing brace after the comment";
}
TEST(BraceCounting, SkipsBracesInBlockComment)
{
// Braces inside /* */ should not count
std::string program = " /* { } */ }";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}')
<< "Should skip braces inside /* */ and find the real closing brace";
}
TEST(BraceCounting, NestedBracesWithBlockComment)
{
// Real nesting plus commented braces that should be ignored
std::string program = " { /* } */ } }";
size_t pos = CountBracesToClose(program, 0);
// bracesOpen starts at 1. Sees '{' (+1=2), skips '}' in comment,
// sees '}' (-1=1), sees '}' (-1=0) => stops
EXPECT_EQ(program.at(pos - 1), '}');
}
TEST(BraceCounting, MultipleLineComments)
{
std::string program =
" // }\n"
" // }\n"
" }\n";
size_t pos = CountBracesToClose(program, 0);
// Only the real } on the third line should count
EXPECT_EQ(program.at(pos - 1), '}');
}
TEST(BraceCounting, MixedCommentTypes)
{
std::string program =
" // { \n"
" /* } */ \n"
" { \n"
" } \n"
" }\n";
size_t pos = CountBracesToClose(program, 0);
// bracesOpen: 1, skip //{, skip /*}*/, see { (+1=2), see } (-1=1), see } (-1=0)
EXPECT_EQ(program.at(pos - 1), '}');
}
TEST(BraceCounting, BlockCommentSpanningMultipleLines)
{
std::string program =
" /*\n"
" {\n"
" }\n"
" */\n"
" }\n";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}')
<< "Braces inside multi-line block comment should be ignored";
}
TEST(BraceCounting, SlashThatIsNotComment)
{
// Division operator should not confuse the brace counter
std::string program = " float x = a / b; }";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}');
}
TEST(BraceCounting, LineCommentImmediatelyFollowedByBrace)
{
// Edge case: closing brace on line right after comment with no space
std::string program = "// comment\n}";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}');
}
TEST(BraceCounting, BlockCommentContainingSlashSlash)
{
// "//" inside block comment should NOT start a line comment
std::string program = " /* // } */ }";
size_t pos = CountBracesToClose(program, 0);
EXPECT_EQ(program.at(pos - 1), '}')
<< "// inside /* */ should not start a line comment; "
"the } after */ should close the brace";
}
// =====================================================================
// TranspileHLSLShader: regex-based sampler declaration removal
// =====================================================================
TEST(RegexSamplerRemoval, RemovesUncommentedDeclaration)
{
std::string source = "uniform sampler2D sampler_tex;\n";
RegexRemoveSamplerDeclarations(source);
EXPECT_EQ(source.find("sampler2D"), std::string::npos)
<< "sampler2D declaration should be removed";
}
TEST(RegexSamplerRemoval, Removes3DDeclaration)
{
std::string source = "uniform sampler3D sampler_vol;\n";
RegexRemoveSamplerDeclarations(source);
EXPECT_EQ(source.find("sampler3D"), std::string::npos);
}
TEST(RegexSamplerRemoval, RemovesBareSamplerDeclaration)
{
// "sampler" without 2D/3D suffix
std::string source = "sampler mytex;\n";
RegexRemoveSamplerDeclarations(source);
EXPECT_EQ(source.find("sampler"), std::string::npos);
}
TEST(RegexSamplerRemoval, MatchesCommentedDeclaration)
{
// The regex operates on HLSL preprocessor output which preserves
// comments. This test documents that commented declarations WILL
// be matched by the regex (known low-risk limitation).
std::string source = "// sampler2D sampler_tex;\nfloat x = 1.0;\n";
RegexRemoveSamplerDeclarations(source);
// The regex matches "sampler2D sampler_tex;" even inside the comment
// because the regex has no comment awareness. It will eat from
// "sampler2D" to end of line including everything after.
EXPECT_EQ(source.find("sampler2D"), std::string::npos)
<< "Regex matches commented declarations too (known limitation)";
}
TEST(RegexSamplerRemoval, PreservesUnrelatedCode)
{
std::string source = "float4 x = 1.0;\nsampler2D mytex;\nfloat4 y = 2.0;\n";
RegexRemoveSamplerDeclarations(source);
EXPECT_NE(source.find("float4 x"), std::string::npos)
<< "Unrelated code before the declaration should survive";
EXPECT_NE(source.find("float4 y"), std::string::npos)
<< "Unrelated code after the declaration should survive";
}
TEST(RegexSamplerRemoval, RemovesMultipleDeclarations)
{
std::string source =
"sampler2D tex1;\n"
"float x = 1.0;\n"
"sampler3D tex2;\n";
RegexRemoveSamplerDeclarations(source);
EXPECT_EQ(source.find("sampler2D"), std::string::npos);
EXPECT_EQ(source.find("sampler3D"), std::string::npos);
EXPECT_NE(source.find("float x"), std::string::npos);
}
// =====================================================================
// TranspileHLSLShader: regex-based texsize declaration removal
// =====================================================================
TEST(RegexTexsizeRemoval, RemovesUncommentedDeclaration)
{
std::string source = "float4 texsize_main;\n";
RegexRemoveTexsizeDeclarations(source);
EXPECT_EQ(source.find("texsize_main"), std::string::npos);
}
TEST(RegexTexsizeRemoval, MatchesCommentedDeclaration)
{
// Documents that commented texsize declarations are also removed
// (known low-risk limitation — regex has no comment awareness).
std::string source = "// float4 texsize_main;\nfloat x = 1.0;\n";
RegexRemoveTexsizeDeclarations(source);
EXPECT_EQ(source.find("texsize_main"), std::string::npos)
<< "Regex matches commented declarations too (known limitation)";
}
TEST(RegexTexsizeRemoval, PreservesUnrelatedCode)
{
std::string source = "float4 x = 1.0;\nfloat4 texsize_rand00;\nfloat4 y = 2.0;\n";
RegexRemoveTexsizeDeclarations(source);
EXPECT_NE(source.find("float4 x"), std::string::npos);
EXPECT_NE(source.find("float4 y"), std::string::npos);
}
TEST(RegexTexsizeRemoval, DoesNotMatchFloat3)
{
// "float3 texsize_..." should NOT match — only float4 does
std::string source = "float3 texsize_main;\n";
std::string original = source;
RegexRemoveTexsizeDeclarations(source);
EXPECT_EQ(source, original)
<< "float3 texsize_ should not be matched by the float4 regex";
}
TEST(RegexTexsizeRemoval, RemovesMultipleDeclarations)
{
std::string source =
"float4 texsize_rand00;\n"
"float x = 1.0;\n"
"float4 texsize_rand01;\n";
RegexRemoveTexsizeDeclarations(source);
EXPECT_EQ(source.find("texsize_rand00"), std::string::npos);
EXPECT_EQ(source.find("texsize_rand01"), std::string::npos);
EXPECT_NE(source.find("float x"), std::string::npos);
}