diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index e216ec759..cd2d2e80a 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -27,6 +27,10 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/tilecachedrecastmeshmanager.cpp settings/parser.cpp + + shader/parsedefines.cpp + shader/parsefors.cpp + shader/shadermanager.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/shader/parsedefines.cpp b/apps/openmw_test_suite/shader/parsedefines.cpp new file mode 100644 index 000000000..65b4380a7 --- /dev/null +++ b/apps/openmw_test_suite/shader/parsedefines.cpp @@ -0,0 +1,191 @@ +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace Shader; + + using DefineMap = ShaderManager::DefineMap; + + struct ShaderParseDefinesTest : Test + { + std::string mSource; + const std::string mName = "shader"; + DefineMap mDefines; + DefineMap mGlobalDefines; + }; + + TEST_F(ShaderParseDefinesTest, empty_should_succeed) + { + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, ""); + } + + TEST_F(ShaderParseDefinesTest, should_fail_for_absent_define) + { + mSource = "@foo\n"; + ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "@foo\n"); + } + + TEST_F(ShaderParseDefinesTest, should_replace_by_existing_define) + { + mDefines["foo"] = "42"; + mSource = "@foo\n"; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "42\n"); + } + + TEST_F(ShaderParseDefinesTest, should_replace_by_existing_global_define) + { + mGlobalDefines["foo"] = "42"; + mSource = "@foo\n"; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "42\n"); + } + + TEST_F(ShaderParseDefinesTest, should_prefer_define_over_global_define) + { + mDefines["foo"] = "13"; + mGlobalDefines["foo"] = "42"; + mSource = "@foo\n"; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "13\n"); + } + + namespace SupportedTerminals + { + struct ShaderParseDefinesTest : ::ShaderParseDefinesTest, WithParamInterface {}; + + TEST_P(ShaderParseDefinesTest, support_defines_terminated_by) + { + mDefines["foo"] = "13"; + mSource = "@foo" + std::string(1, GetParam()); + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "13" + std::string(1, GetParam())); + } + + INSTANTIATE_TEST_SUITE_P( + SupportedTerminals, + ShaderParseDefinesTest, + Values(' ', '\n', '\r', '(', ')', '[', ']', '.', ';', ',') + ); + } + + TEST_F(ShaderParseDefinesTest, should_not_support_define_ending_with_source) + { + mDefines["foo"] = "42"; + mSource = "@foo"; + ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "@foo"); + } + + TEST_F(ShaderParseDefinesTest, should_replace_all_matched_values) + { + mDefines["foo"] = "42"; + mSource = "@foo @foo "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "42 42 "); + } + + TEST_F(ShaderParseDefinesTest, should_support_define_with_empty_name) + { + mDefines[""] = "42"; + mSource = "@ "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "42 "); + } + + TEST_F(ShaderParseDefinesTest, should_replace_all_found_defines) + { + mDefines["foo"] = "42"; + mDefines["bar"] = "13"; + mDefines["baz"] = "55"; + mSource = "@foo @bar "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "42 13 "); + } + + TEST_F(ShaderParseDefinesTest, should_fail_on_foreach_without_endforeach) + { + mSource = "@foreach "; + ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach "); + } + + TEST_F(ShaderParseDefinesTest, should_fail_on_endforeach_without_foreach) + { + mSource = "@endforeach "; + ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_replace_at_sign_by_dollar_for_foreach_endforeach) + { + mSource = "@foreach @endforeach "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_succeed_on_unmatched_nested_foreach) + { + mSource = "@foreach @foreach @endforeach "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach $foreach $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_fail_on_unmatched_nested_endforeach) + { + mSource = "@foreach @endforeach @endforeach "; + ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach $endforeach $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_support_nested_foreach) + { + mSource = "@foreach @foreach @endforeach @endforeach "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach $foreach $endforeach $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_support_foreach_variable) + { + mSource = "@foreach foo @foo @endforeach "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach foo $foo $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_not_replace_foreach_variable_by_define) + { + mDefines["foo"] = "42"; + mSource = "@foreach foo @foo @endforeach "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach foo $foo $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_support_nested_foreach_with_variable) + { + mSource = "@foreach foo @foo @foreach bar @bar @endforeach @endforeach "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "$foreach foo $foo $foreach bar $bar $endforeach $endforeach "); + } + + TEST_F(ShaderParseDefinesTest, should_not_support_single_line_comments_for_defines) + { + mDefines["foo"] = "42"; + mSource = "@foo // @foo\n"; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "42 // 42\n"); + } + + TEST_F(ShaderParseDefinesTest, should_not_support_multiline_comments_for_defines) + { + mDefines["foo"] = "42"; + mSource = "/* @foo */ @foo "; + ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); + EXPECT_EQ(mSource, "/* 42 */ 42 "); + } +} diff --git a/apps/openmw_test_suite/shader/parsefors.cpp b/apps/openmw_test_suite/shader/parsefors.cpp new file mode 100644 index 000000000..330feb172 --- /dev/null +++ b/apps/openmw_test_suite/shader/parsefors.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace Shader; + + using DefineMap = ShaderManager::DefineMap; + + struct ShaderParseForsTest : Test + { + std::string mSource; + const std::string mName = "shader"; + }; + + TEST_F(ShaderParseForsTest, empty_should_succeed) + { + ASSERT_TRUE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, ""); + } + + TEST_F(ShaderParseForsTest, should_fail_for_single_escape_symbol) + { + mSource = "$"; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "$"); + } + + TEST_F(ShaderParseForsTest, should_fail_on_first_found_escaped_not_foreach) + { + mSource = "$foo "; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "$foo "); + } + + TEST_F(ShaderParseForsTest, should_fail_on_absent_foreach_variable) + { + mSource = "$foreach "; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "$foreach "); + } + + TEST_F(ShaderParseForsTest, should_fail_on_unmatched_after_variable) + { + mSource = "$foreach foo "; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "$foreach foo "); + } + + TEST_F(ShaderParseForsTest, should_fail_on_absent_newline_after_foreach_list) + { + mSource = "$foreach foo 1,2,3 "; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "$foreach foo 1,2,3 "); + } + + TEST_F(ShaderParseForsTest, should_fail_on_absent_endforeach_after_newline) + { + mSource = "$foreach foo 1,2,3\n"; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "$foreach foo 1,2,3\n"); + } + + TEST_F(ShaderParseForsTest, should_replace_complete_foreach_by_line_number) + { + mSource = "$foreach foo 1,2,3\n$endforeach"; + ASSERT_TRUE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "\n#line 3"); + } + + TEST_F(ShaderParseForsTest, should_replace_loop_variable) + { + mSource = "$foreach foo 1,2,3\n$foo\n$endforeach"; + ASSERT_TRUE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "1\n2\n3\n\n#line 4"); + } + + TEST_F(ShaderParseForsTest, should_count_line_number_from_existing) + { + mSource = "$foreach foo 1,2,3\n#line 10\n$foo\n$endforeach"; + ASSERT_TRUE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "#line 10\n1\n#line 10\n2\n#line 10\n3\n\n#line 12"); + } + + TEST_F(ShaderParseForsTest, should_not_support_nested_loops) + { + mSource = "$foreach foo 1,2\n$foo\n$foreach bar 1,2\n$bar\n$endforeach\n$endforeach"; + ASSERT_FALSE(parseFors(mSource, mName)); + EXPECT_EQ(mSource, "1\n1\n2\n$foreach bar 1,2\n1\n\n#line 6\n2\n2\n$foreach bar 1,2\n2\n\n#line 6\n\n#line 7"); + } +} diff --git a/apps/openmw_test_suite/shader/shadermanager.cpp b/apps/openmw_test_suite/shader/shadermanager.cpp new file mode 100644 index 000000000..e823d5fe2 --- /dev/null +++ b/apps/openmw_test_suite/shader/shadermanager.cpp @@ -0,0 +1,240 @@ +#include + +#include + +#include + +namespace +{ + using namespace testing; + using namespace Shader; + + struct ShaderManagerTest : Test + { + ShaderManager mManager; + ShaderManager::DefineMap mDefines; + + ShaderManagerTest() + { + mManager.setShaderPath("."); + } + + template + void withShaderFile(const std::string& content, F&& f) + { + withShaderFile("", content, std::forward(f)); + } + + template + void withShaderFile(const std::string& suffix, const std::string& content, F&& f) + { + const auto path = UnitTest::GetInstance()->current_test_info()->name() + suffix + ".glsl"; + + { + boost::filesystem::ofstream stream; + stream.open(path); + stream << content; + stream.close(); + } + + f(path); + } + }; + + TEST_F(ShaderManagerTest, get_shader_with_empty_content_should_succeed) + { + const std::string content; + + withShaderFile(content, [this] (const std::string& templateName) { + EXPECT_TRUE(mManager.getShader(templateName, {}, osg::Shader::VERTEX)); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_not_change_source_without_template_parameters) + { + const std::string content = + "#version 120\n" + "void main() {}\n"; + + withShaderFile(content, [&] (const std::string& templateName) { + const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX); + ASSERT_TRUE(shader); + EXPECT_EQ(shader->getShaderSource(), content); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_replace_includes_with_content) + { + const std::string content0 = + "void foo() {}\n"; + + withShaderFile("_0", content0, [&] (const std::string& templateName0) { + const std::string content1 = + "#include \"" + templateName0 + "\"\n" + "void bar() { foo() }\n"; + + withShaderFile("_1", content1, [&] (const std::string& templateName1) { + const std::string content2 = + "#version 120\n" + "#include \"" + templateName1 + "\"\n" + "void main() { bar() }\n"; + + withShaderFile(content2, [&] (const std::string& templateName2) { + const auto shader = mManager.getShader(templateName2, mDefines, osg::Shader::VERTEX); + ASSERT_TRUE(shader); + const std::string expected = + "#version 120\n" + "#line 0 1\n" + "#line 0 2\n" + "void foo() {}\n" + "\n" + "#line 0 0\n" + "\n" + "void bar() { foo() }\n" + "\n" + "#line 2 0\n" + "\n" + "void main() { bar() }\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + }); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_replace_defines) + { + const std::string content = + "#version 120\n" + "#define FLAG @flag\n" + "void main() {}\n" + ; + + withShaderFile(content, [&] (const std::string& templateName) { + mDefines["flag"] = "1"; + const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX); + ASSERT_TRUE(shader); + const std::string expected = + "#version 120\n" + "#define FLAG 1\n" + "void main() {}\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_expand_loop) + { + const std::string content = + "#version 120\n" + "@foreach index @list\n" + " varying vec4 foo@index;\n" + "@endforeach\n" + "void main() {}\n" + ; + + withShaderFile(content, [&] (const std::string& templateName) { + mDefines["list"] = "1,2,3"; + const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX); + ASSERT_TRUE(shader); + const std::string expected = + "#version 120\n" + " varying vec4 foo1;\n" + " varying vec4 foo2;\n" + " varying vec4 foo3;\n" + "\n" + "#line 5\n" + "void main() {}\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_replace_loops_with_conditions) + { + const std::string content = + "#version 120\n" + "@foreach index @list\n" + " varying vec4 foo@index;\n" + "@endforeach\n" + "void main()\n" + "{\n" + "#ifdef BAR\n" + "@foreach index @list\n" + " foo@index = vec4(1.0);\n" + "@endforeach\n" + "#elif BAZ\n" + "@foreach index @list\n" + " foo@index = vec4(2.0);\n" + "@endforeach\n" + "#else\n" + "@foreach index @list\n" + " foo@index = vec4(3.0);\n" + "@endforeach\n" + "#endif\n" + "}\n" + ; + + withShaderFile(content, [&] (const std::string& templateName) { + mDefines["list"] = "1,2,3"; + const auto shader = mManager.getShader(templateName, mDefines, osg::Shader::VERTEX); + ASSERT_TRUE(shader); + const std::string expected = + "#version 120\n" + " varying vec4 foo1;\n" + " varying vec4 foo2;\n" + " varying vec4 foo3;\n" + "\n" + "#line 5\n" + "void main()\n" + "{\n" + "#ifdef BAR\n" + " foo1 = vec4(1.0);\n" + " foo2 = vec4(1.0);\n" + " foo3 = vec4(1.0);\n" + "\n" + "#line 11\n" + "#elif BAZ\n" + "#line 12\n" + " foo1 = vec4(2.0);\n" + " foo2 = vec4(2.0);\n" + " foo3 = vec4(2.0);\n" + "\n" + "#line 15\n" + "#else\n" + "#line 16\n" + " foo1 = vec4(3.0);\n" + " foo2 = vec4(3.0);\n" + " foo3 = vec4(3.0);\n" + "\n" + "#line 19\n" + "#endif\n" + "#line 20\n" + "}\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_fail_on_absent_template_parameters_in_single_line_comments) + { + const std::string content = + "#version 120\n" + "// #define FLAG @flag\n" + "void main() {}\n" + ; + + withShaderFile(content, [&] (const std::string& templateName) { + EXPECT_FALSE(mManager.getShader(templateName, mDefines, osg::Shader::VERTEX)); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_fail_on_absent_template_parameter_in_multi_line_comments) + { + const std::string content = + "#version 120\n" + "/* #define FLAG @flag */\n" + "void main() {}\n" + ; + + withShaderFile(content, [&] (const std::string& templateName) { + EXPECT_FALSE(mManager.getShader(templateName, mDefines, osg::Shader::VERTEX)); + }); + } +} diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index dbe989476..c602ac62b 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -63,6 +63,10 @@ namespace Shader OpenThreads::Mutex mMutex; }; + bool parseFors(std::string& source, const std::string& templateName); + + bool parseDefines(std::string& source, const ShaderManager::DefineMap& defines, + const ShaderManager::DefineMap& globalDefines, const std::string& templateName); } #endif