#include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Resources; using namespace XCEngine::Containers; namespace { void WriteTextFile(const std::filesystem::path& path, const std::string& contents) { std::ofstream output(path, std::ios::binary | std::ios::trunc); ASSERT_TRUE(output.is_open()); output << contents; ASSERT_TRUE(static_cast(output)); } const ShaderPassTagEntry* FindPassTag(const ShaderPass* pass, const char* name) { if (pass == nullptr || name == nullptr) { return nullptr; } for (const ShaderPassTagEntry& tag : pass->tags) { if (tag.name == name) { return &tag; } } return nullptr; } TEST(ShaderLoader, GetResourceType) { ShaderLoader loader; EXPECT_EQ(loader.GetResourceType(), ResourceType::Shader); } TEST(ShaderLoader, GetSupportedExtensions) { ShaderLoader loader; auto extensions = loader.GetSupportedExtensions(); EXPECT_GE(extensions.Size(), 1u); } TEST(ShaderLoader, CanLoad) { ShaderLoader loader; EXPECT_TRUE(loader.CanLoad("test.vert")); EXPECT_TRUE(loader.CanLoad("test.frag")); EXPECT_TRUE(loader.CanLoad("test.glsl")); EXPECT_TRUE(loader.CanLoad("test.hlsl")); EXPECT_TRUE(loader.CanLoad("test.xcshader")); EXPECT_TRUE(loader.CanLoad(GetBuiltinForwardLitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinUnlitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdShaderPath())); EXPECT_FALSE(loader.CanLoad("test.txt")); EXPECT_FALSE(loader.CanLoad("test.png")); } TEST(ShaderLoader, LoadInvalidPath) { ShaderLoader loader; LoadResult result = loader.Load("invalid/path/shader.glsl"); EXPECT_FALSE(result); } TEST(ShaderLoader, LoadLegacySingleStageShaderBuildsDefaultPassVariant) { namespace fs = std::filesystem; const fs::path shaderPath = fs::temp_directory_path() / "xc_shader_loader_stage.vert"; { std::ofstream shaderFile(shaderPath); ASSERT_TRUE(shaderFile.is_open()); shaderFile << "#version 430\nvoid main() {}"; } ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_EQ(shader->GetShaderType(), ShaderType::Vertex); ASSERT_EQ(shader->GetPassCount(), 1u); const ShaderPass* pass = shader->FindPass("Default"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->variants.Size(), 1u); EXPECT_EQ(pass->variants[0].stage, ShaderType::Vertex); EXPECT_EQ(pass->variants[0].backend, ShaderBackend::Generic); EXPECT_EQ(pass->variants[0].sourceCode, shader->GetSourceCode()); delete shader; std::remove(shaderPath.string().c_str()); } TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_manifest_test"; const fs::path stageRoot = shaderRoot / "stages"; const fs::path manifestPath = shaderRoot / "multi_pass.shader"; fs::remove_all(shaderRoot); fs::create_directories(stageRoot); WriteTextFile(stageRoot / "forward_lit.vs.hlsl", "float4 MainVS() : SV_POSITION { return 0; } // FORWARD_LIT_D3D12_VS\n"); WriteTextFile(stageRoot / "forward_lit.ps.hlsl", "float4 MainPS() : SV_TARGET { return 1; } // FORWARD_LIT_D3D12_PS\n"); WriteTextFile(stageRoot / "forward_lit.vert.glsl", "#version 430\n// FORWARD_LIT_GL_VS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.frag.glsl", "#version 430\n// FORWARD_LIT_GL_PS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.vert.vk.glsl", "#version 450\n// FORWARD_LIT_VK_VS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.frag.vk.glsl", "#version 450\n// FORWARD_LIT_VK_PS\nvoid main() {}\n"); WriteTextFile(stageRoot / "depth_only.vs.hlsl", "float4 MainVS() : SV_POSITION { return 0; } // DEPTH_ONLY_D3D12_VS\n"); WriteTextFile(stageRoot / "depth_only.ps.hlsl", "float4 MainPS() : SV_TARGET { return 1; } // DEPTH_ONLY_D3D12_PS\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"TestLitShader\",\n"; manifest << " \"properties\": [\n"; manifest << " {\n"; manifest << " \"name\": \"_BaseColor\",\n"; manifest << " \"displayName\": \"Base Color\",\n"; manifest << " \"type\": \"Color\",\n"; manifest << " \"defaultValue\": \"(1,1,1,1)\",\n"; manifest << " \"semantic\": \"BaseColor\"\n"; manifest << " },\n"; manifest << " {\n"; manifest << " \"name\": \"_MainTex\",\n"; manifest << " \"displayName\": \"Base Map\",\n"; manifest << " \"type\": \"2D\",\n"; manifest << " \"defaultValue\": \"white\",\n"; manifest << " \"semantic\": \"BaseColorTexture\"\n"; manifest << " }\n"; manifest << " ],\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"tags\": {\n"; manifest << " \"LightMode\": \"ForwardBase\",\n"; manifest << " \"Queue\": \"Geometry\"\n"; manifest << " },\n"; manifest << " \"resources\": [\n"; manifest << " { \"name\": \"PerObjectConstants\", \"type\": \"ConstantBuffer\", \"set\": 1, \"binding\": 0, \"semantic\": \"PerObject\" },\n"; manifest << " { \"name\": \"LightingConstants\", \"type\": \"ConstantBuffer\", \"set\": 2, \"binding\": 0, \"semantic\": \"Lighting\" },\n"; manifest << " { \"name\": \"MaterialConstants\", \"type\": \"ConstantBuffer\", \"set\": 3, \"binding\": 0, \"semantic\": \"Material\" },\n"; manifest << " { \"name\": \"ShadowReceiverConstants\", \"type\": \"ConstantBuffer\", \"set\": 4, \"binding\": 0, \"semantic\": \"ShadowReceiver\" },\n"; manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 5, \"binding\": 0, \"semantic\": \"BaseColorTexture\" },\n"; manifest << " { \"name\": \"LinearClampSampler\", \"type\": \"Sampler\", \"set\": 6, \"binding\": 0, \"semantic\": \"LinearClampSampler\" },\n"; manifest << " { \"name\": \"ShadowMapTexture\", \"type\": \"Texture2D\", \"set\": 7, \"binding\": 0, \"semantic\": \"ShadowMapTexture\" },\n"; manifest << " { \"name\": \"ShadowMapSampler\", \"type\": \"Sampler\", \"set\": 8, \"binding\": 0, \"semantic\": \"ShadowMapSampler\" }\n"; manifest << " ],\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.vs.hlsl\", \"entryPoint\": \"MainVS\", \"profile\": \"vs_5_0\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.ps.hlsl\", \"entryPoint\": \"MainPS\", \"profile\": \"ps_5_0\" },\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.frag.glsl\" },\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"Vulkan\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.vert.vk.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"Vulkan\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.frag.vk.glsl\" }\n"; manifest << " ]\n"; manifest << " },\n"; manifest << " {\n"; manifest << " \"name\": \"DepthOnly\",\n"; manifest << " \"tags\": {\n"; manifest << " \"LightMode\": \"DepthOnly\"\n"; manifest << " },\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/depth_only.vs.hlsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/depth_only.ps.hlsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } ShaderLoader loader; LoadResult result = loader.Load(manifestPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); EXPECT_EQ(shader->GetName(), "TestLitShader"); ASSERT_EQ(shader->GetProperties().Size(), 2u); ASSERT_EQ(shader->GetPassCount(), 2u); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->displayName, "Base Color"); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->defaultValue, "(1,1,1,1)"); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->defaultValue, "white"); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); const ShaderPass* forwardLitPass = shader->FindPass("ForwardLit"); ASSERT_NE(forwardLitPass, nullptr); ASSERT_EQ(forwardLitPass->tags.Size(), 2u); ASSERT_EQ(forwardLitPass->resources.Size(), 8u); EXPECT_EQ(forwardLitPass->tags[0].name, "LightMode"); EXPECT_EQ(forwardLitPass->tags[0].value, "ForwardBase"); EXPECT_EQ(forwardLitPass->tags[1].name, "Queue"); EXPECT_EQ(forwardLitPass->tags[1].value, "Geometry"); const ShaderResourceBindingDesc* baseTextureBinding = shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"); ASSERT_NE(baseTextureBinding, nullptr); EXPECT_EQ(baseTextureBinding->type, ShaderResourceType::Texture2D); EXPECT_EQ(baseTextureBinding->set, 5u); EXPECT_EQ(baseTextureBinding->binding, 0u); EXPECT_EQ(baseTextureBinding->semantic, "BaseColorTexture"); const ShaderStageVariant* d3d12Vertex = shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("FORWARD_LIT_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->entryPoint, "main"); EXPECT_EQ(openglFragment->profile, "fs_4_30"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("FORWARD_LIT_GL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->profile, "fs_4_50"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("FORWARD_LIT_VK_PS"), std::string::npos); const ShaderPass* depthOnlyPass = shader->FindPass("DepthOnly"); ASSERT_NE(depthOnlyPass, nullptr); ASSERT_EQ(depthOnlyPass->tags.Size(), 1u); EXPECT_EQ(depthOnlyPass->tags[0].value, "DepthOnly"); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_EQ(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderManifestParsesVariantKeywordsAndKeywordAwareLookup) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_manifest_variant_keywords"; const fs::path manifestPath = shaderRoot / "variant_keywords.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"VariantKeywordShader\",\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"sourceCode\": \"#version 430\\nvoid main() {}\\n\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"sourceCode\": \"#version 430\\n// BASE_FRAGMENT\\nvoid main() {}\\n\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"keywords\": [\"XC_ALPHA_TEST\", \"_\", \"XC_FOG\", \"XC_ALPHA_TEST\"], \"sourceCode\": \"#version 430\\n// KEYWORD_FRAGMENT\\nvoid main() {}\\n\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } ShaderLoader loader; LoadResult result = loader.Load(manifestPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); const ShaderStageVariant* baseFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(baseFragment, nullptr); EXPECT_NE(std::string(baseFragment->sourceCode.CStr()).find("BASE_FRAGMENT"), std::string::npos); ShaderKeywordSet enabledKeywords = {}; enabledKeywords.enabledKeywords.PushBack("XC_FOG"); enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); const ShaderStageVariant* keywordFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL, enabledKeywords); ASSERT_NE(keywordFragment, nullptr); EXPECT_NE(std::string(keywordFragment->sourceCode.CStr()).find("KEYWORD_FRAGMENT"), std::string::npos); ASSERT_EQ(keywordFragment->requiredKeywords.enabledKeywords.Size(), 2u); EXPECT_EQ(keywordFragment->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); EXPECT_EQ(keywordFragment->requiredKeywords.enabledKeywords[1], "XC_FOG"); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringBuildsGenericHlslVariants) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_test"; const fs::path includeRoot = shaderRoot / "shaderlib"; const fs::path shaderPath = shaderRoot / "authoring.shader"; fs::remove_all(shaderRoot); fs::create_directories(includeRoot); WriteTextFile( includeRoot / "shared.hlsl", R"(// XC_SINGLE_SOURCE_SHARED_INCLUDE #define XC_SINGLE_SOURCE_SHARED_VALUE 1 )"); WriteTextFile( shaderPath, R"(Shader "AuthoringLit" { Properties { _BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)] } HLSLINCLUDE #include "shaderlib/shared.hlsl" struct VSInput { float3 positionOS : POSITION; }; ENDHLSL SubShader { Tags { "Queue" = "Geometry" } LOD 200 Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardLit" } HLSLPROGRAM #pragma target 4.5 #pragma vertex Vert #pragma fragment Frag #pragma multi_compile _ XC_MAIN_LIGHT_SHADOWS #pragma shader_feature_local _ XC_ALPHA_TEST float4 Vert(VSInput input) : SV_POSITION { return float4(input.positionOS, 1.0); } float4 Frag() : SV_TARGET { return float4(1.0, 0.0, 0.0, 1.0); // XC_SINGLE_SOURCE_FRAG_BODY } ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); EXPECT_EQ(shader->GetName(), "AuthoringLit"); ASSERT_EQ(shader->GetProperties().Size(), 1u); ASSERT_EQ(shader->GetPassCount(), 1u); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->tags.Size(), 3u); EXPECT_EQ(pass->tags[0].name, "Queue"); EXPECT_EQ(pass->tags[0].value, "Geometry"); EXPECT_EQ(pass->tags[1].name, "LOD"); EXPECT_EQ(pass->tags[1].value, "200"); EXPECT_EQ(pass->tags[2].name, "LightMode"); EXPECT_EQ(pass->tags[2].value, "ForwardLit"); EXPECT_TRUE(pass->resources.Empty()); ASSERT_EQ(pass->keywordDeclarations.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::MultiCompile); ASSERT_EQ(pass->keywordDeclarations[0].options.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].options[0], "_"); EXPECT_EQ(pass->keywordDeclarations[0].options[1], "XC_MAIN_LIGHT_SHADOWS"); EXPECT_EQ(pass->keywordDeclarations[1].type, ShaderKeywordDeclarationType::ShaderFeatureLocal); ASSERT_EQ(pass->keywordDeclarations[1].options.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[1].options[1], "XC_ALPHA_TEST"); EXPECT_TRUE(shader->PassDeclaresKeyword("ForwardLit", "XC_MAIN_LIGHT_SHADOWS")); EXPECT_TRUE(shader->DeclaresKeyword("XC_ALPHA_TEST")); EXPECT_FALSE(shader->DeclaresKeyword("_")); ASSERT_EQ(pass->variants.Size(), 8u); const ShaderStageVariant* vertexVariant = shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(vertexVariant, nullptr); EXPECT_EQ(vertexVariant->backend, ShaderBackend::Generic); EXPECT_EQ(vertexVariant->language, ShaderLanguage::HLSL); EXPECT_EQ(vertexVariant->entryPoint, "Vert"); EXPECT_EQ(vertexVariant->profile, "vs_5_0"); EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("#include \"shaderlib/shared.hlsl\""), std::string::npos); EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("XC_SINGLE_SOURCE_FRAG_BODY"), std::string::npos); EXPECT_EQ(vertexVariant->requiredKeywords.enabledKeywords.Size(), 0u); EXPECT_EQ(std::string(vertexVariant->sourceCode.CStr()).find("#pragma multi_compile"), std::string::npos); const ShaderStageVariant* fragmentVariant = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(fragmentVariant, nullptr); EXPECT_EQ(fragmentVariant->backend, ShaderBackend::Generic); EXPECT_EQ(fragmentVariant->language, ShaderLanguage::HLSL); EXPECT_EQ(fragmentVariant->entryPoint, "Frag"); EXPECT_EQ(fragmentVariant->profile, "ps_5_0"); ShaderKeywordSet enabledKeywords = {}; enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); enabledKeywords.enabledKeywords.PushBack("XC_MAIN_LIGHT_SHADOWS"); const ShaderStageVariant* keywordFragmentVariant = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12, enabledKeywords); ASSERT_NE(keywordFragmentVariant, nullptr); ASSERT_EQ(keywordFragmentVariant->requiredKeywords.enabledKeywords.Size(), 2u); EXPECT_EQ(keywordFragmentVariant->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); EXPECT_EQ(keywordFragmentVariant->requiredKeywords.enabledKeywords[1], "XC_MAIN_LIGHT_SHADOWS"); EXPECT_NE(std::string(keywordFragmentVariant->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE(std::string(keywordFragmentVariant->sourceCode.CStr()).find("#define XC_MAIN_LIGHT_SHADOWS 1"), std::string::npos); EXPECT_EQ(std::string(keywordFragmentVariant->sourceCode.CStr()).find("#pragma shader_feature_local"), std::string::npos); Array dependencies; ASSERT_TRUE(loader.CollectSourceDependencies(shaderPath.string().c_str(), dependencies)); ASSERT_EQ(dependencies.Size(), 1u); EXPECT_EQ( fs::path(dependencies[0].CStr()).lexically_normal(), (includeRoot / "shared.hlsl").lexically_normal()); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringParsesPassStateAndFallback) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_pass_state"; const fs::path shaderPath = shaderRoot / "authoring_state.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "AuthoringStateful" { Fallback "Legacy/Diffuse" SubShader { Pass { Name "ForwardLit" Cull Front ZWrite Off ZTest LEqual Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha ColorMask RGB HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_EQ(shader->GetFallback(), "Legacy/Diffuse"); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); EXPECT_TRUE(pass->fixedFunctionState.depthTestEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); EXPECT_TRUE(pass->fixedFunctionState.blendEnable); EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha); EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha); EXPECT_EQ(pass->fixedFunctionState.srcBlendAlpha, MaterialBlendFactor::One); EXPECT_EQ(pass->fixedFunctionState.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha); EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7u); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringPreservesPassStateAndFallback) { namespace fs = std::filesystem; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_artifact_pass_state"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path shaderPath = shaderDir / "authoring_state.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "ArtifactAuthoringStateful" { Fallback "Legacy/Cutout" SubShader { Pass { Name "ForwardLit" Cull Off ZWrite Off Blend SrcAlpha OneMinusSrcAlpha ColorMask RGBA HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset resolvedAsset; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/authoring_state.shader", ResourceType::Shader, resolvedAsset)); ASSERT_TRUE(resolvedAsset.artifactReady); ShaderLoader loader; LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_EQ(shader->GetFallback(), "Legacy/Cutout"); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); EXPECT_TRUE(pass->fixedFunctionState.blendEnable); EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha); EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha); EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0xFu); delete shader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, LoadShaderAuthoringParsesMultiCompileLocalKeywords) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_multi_compile_local"; const fs::path shaderPath = shaderRoot / "authoring_multi_compile_local.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "AuthoringLocalKeywords" { SubShader { Pass { Name "ForwardLit" HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #pragma multi_compile_local _ XC_LOCAL_FOG float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->keywordDeclarations.Size(), 1u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::MultiCompileLocal); EXPECT_TRUE(pass->keywordDeclarations[0].IsLocal()); ASSERT_EQ(pass->variants.Size(), 4u); ShaderKeywordSet enabledKeywords = {}; enabledKeywords.enabledKeywords.PushBack("XC_LOCAL_FOG"); const ShaderStageVariant* keywordFragmentVariant = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12, enabledKeywords); ASSERT_NE(keywordFragmentVariant, nullptr); EXPECT_NE( std::string(keywordFragmentVariant->sourceCode.CStr()).find("#define XC_LOCAL_FOG 1"), std::string::npos); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringParsesFallbackAndFixedFunctionStateInheritance) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_fixed_state"; const fs::path shaderPath = shaderRoot / "authoring_fixed_state.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "AuthoringFixedState" { Fallback "Legacy Shaders/Diffuse" SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } LOD 310 Cull Front ZWrite Off Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardLit" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_EQ(shader->GetFallback(), "Legacy Shaders/Diffuse"); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); EXPECT_TRUE(pass->fixedFunctionState.depthTestEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); EXPECT_TRUE(pass->fixedFunctionState.blendEnable); EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha); EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha); EXPECT_EQ(pass->fixedFunctionState.srcBlendAlpha, MaterialBlendFactor::SrcAlpha); EXPECT_EQ(pass->fixedFunctionState.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha); EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7); const ShaderPassTagEntry* queueTag = FindPassTag(pass, "Queue"); ASSERT_NE(queueTag, nullptr); EXPECT_EQ(queueTag->value, "Transparent"); const ShaderPassTagEntry* lodTag = FindPassTag(pass, "LOD"); ASSERT_NE(lodTag, nullptr); EXPECT_EQ(lodTag->value, "310"); const ShaderPassTagEntry* lightModeTag = FindPassTag(pass, "LightMode"); ASSERT_NE(lightModeTag, nullptr); EXPECT_EQ(lightModeTag->value, "ForwardLit"); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringRejectsLegacyBackendPragma) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_reject_backend"; const fs::path shaderPath = shaderRoot / "invalid_backend.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "InvalidBackend" { SubShader { Pass { Name "ForwardLit" HLSLPROGRAM #pragma target 4.5 #pragma vertex Vert #pragma fragment Frag #pragma backend D3D12 HLSL "forward.vs.hlsl" "forward.ps.hlsl" float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); EXPECT_FALSE(result); const std::string errorMessage = result.errorMessage.CStr(); EXPECT_FALSE(errorMessage.empty()); EXPECT_EQ(errorMessage.find("Failed to read shader authoring"), std::string::npos); EXPECT_TRUE( errorMessage.find("backend") != std::string::npos || errorMessage.find("unsupported pragma") != std::string::npos); fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringRejectsLegacyResourcesBlock) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_reject_resources"; const fs::path shaderPath = shaderRoot / "invalid_resources.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "InvalidResources" { SubShader { Pass { Name "ForwardLit" Resources { MaterialConstants (ConstantBuffer, 0, 0) } HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); EXPECT_FALSE(result); EXPECT_NE(std::string(result.errorMessage.CStr()).find("must not declare Resources blocks"), std::string::npos); fs::remove_all(shaderRoot); } TEST(ShaderLoader, ResourceManagerLoadsShaderManifestRelativeToResourceRoot) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path previousPath = fs::current_path(); const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_manifest_resource_root"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path manifestPath = shaderDir / "simple.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile(shaderDir / "simple.vert.glsl", "#version 430\n// SIMPLE_GL_VS\nvoid main() {}\n"); WriteTextFile(shaderDir / "simple.frag.glsl", "#version 430\n// SIMPLE_GL_PS\nvoid main() {}\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"SimpleShader\",\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"Unlit\",\n"; manifest << " \"tags\": { \"LightMode\": \"Unlit\" },\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"simple.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"simple.frag.glsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } manager.SetResourceRoot(projectRoot.string().c_str()); fs::current_path(projectRoot.parent_path()); { const ResourceHandle shaderHandle = manager.Load("Assets/Shaders/simple.shader"); ASSERT_TRUE(shaderHandle.IsValid()); EXPECT_EQ(shaderHandle->GetName(), "SimpleShader"); const ShaderStageVariant* vertexVariant = shaderHandle->FindVariant("Unlit", ShaderType::Vertex, ShaderBackend::OpenGL); ASSERT_NE(vertexVariant, nullptr); EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("SIMPLE_GL_VS"), std::string::npos); } fs::current_path(previousPath); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { namespace fs = std::filesystem; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_artifact_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path manifestPath = shaderDir / "lit.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile(shaderDir / "lit.vert.glsl", "#version 430\n// ARTIFACT_GL_VS\nvoid main() {}\n"); WriteTextFile(shaderDir / "lit.frag.glsl", "#version 430\n// ARTIFACT_GL_PS\nvoid main() {}\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"ArtifactShader\",\n"; manifest << " \"properties\": [\n"; manifest << " {\n"; manifest << " \"name\": \"_MainTex\",\n"; manifest << " \"displayName\": \"Main Tex\",\n"; manifest << " \"type\": \"2D\",\n"; manifest << " \"defaultValue\": \"white\",\n"; manifest << " \"semantic\": \"BaseColorTexture\"\n"; manifest << " }\n"; manifest << " ],\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"tags\": { \"LightMode\": \"ForwardBase\" },\n"; manifest << " \"resources\": [\n"; manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 3, \"binding\": 0, \"semantic\": \"BaseColorTexture\" }\n"; manifest << " ],\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"keywords\": [\"XC_ALPHA_TEST\", \"XC_FOG\"], \"sourceCode\": \"#version 430\\n// ARTIFACT_GL_PS_KEYWORD\\nvoid main() {}\\n\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset resolvedAsset; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, resolvedAsset)); ASSERT_TRUE(resolvedAsset.artifactReady); EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcshader"); EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr())); ShaderLoader loader; LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_TRUE(shader->IsValid()); EXPECT_EQ(shader->GetName(), "ArtifactShader"); EXPECT_EQ( fs::path(shader->GetPath().CStr()).lexically_normal().generic_string(), manifestPath.lexically_normal().generic_string()); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->variants.Size(), 3u); ASSERT_EQ(pass->resources.Size(), 1u); const ShaderStageVariant* fragmentVariant = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(fragmentVariant, nullptr); EXPECT_NE(std::string(fragmentVariant->sourceCode.CStr()).find("ARTIFACT_GL_PS"), std::string::npos); ShaderKeywordSet enabledKeywords = {}; enabledKeywords.enabledKeywords.PushBack("XC_FOG"); enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); const ShaderStageVariant* keywordVariant = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL, enabledKeywords); ASSERT_NE(keywordVariant, nullptr); EXPECT_NE(std::string(keywordVariant->sourceCode.CStr()).find("ARTIFACT_GL_PS_KEYWORD"), std::string::npos); ASSERT_EQ(keywordVariant->requiredKeywords.enabledKeywords.Size(), 2u); EXPECT_EQ(keywordVariant->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); EXPECT_EQ(keywordVariant->requiredKeywords.enabledKeywords[1], "XC_FOG"); delete shader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringPreservesKeywords) { namespace fs = std::filesystem; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_artifact_keywords"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path shaderPath = shaderDir / "authoring.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "ArtifactAuthoringKeywords" { SubShader { Pass { Name "ForwardLit" HLSLPROGRAM #pragma target 4.5 #pragma vertex Vert #pragma fragment Frag #pragma multi_compile _ XC_MAIN_LIGHT_SHADOWS #pragma shader_feature_local _ XC_ALPHA_TEST float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return float4(1.0, 0.0, 0.0, 1.0); } ENDHLSL } } } )"); AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset resolvedAsset; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/authoring.shader", ResourceType::Shader, resolvedAsset)); ASSERT_TRUE(resolvedAsset.artifactReady); EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr())); ShaderLoader loader; LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->DeclaresKeyword("XC_MAIN_LIGHT_SHADOWS")); ASSERT_TRUE(shader->DeclaresKeyword("XC_ALPHA_TEST")); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->keywordDeclarations.Size(), 2u); ASSERT_EQ(pass->variants.Size(), 8u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::MultiCompile); EXPECT_EQ(pass->keywordDeclarations[1].type, ShaderKeywordDeclarationType::ShaderFeatureLocal); ShaderKeywordSet enabledKeywords = {}; enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); enabledKeywords.enabledKeywords.PushBack("XC_MAIN_LIGHT_SHADOWS"); const ShaderStageVariant* keywordFragmentVariant = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12, enabledKeywords); ASSERT_NE(keywordFragmentVariant, nullptr); ASSERT_EQ(keywordFragmentVariant->requiredKeywords.enabledKeywords.Size(), 2u); EXPECT_NE( std::string(keywordFragmentVariant->sourceCode.CStr()).find("#define XC_MAIN_LIGHT_SHADOWS 1"), std::string::npos); EXPECT_NE( std::string(keywordFragmentVariant->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); delete shader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringAndTracksIncludeDependencies) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_artifact_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path includeDir = shaderDir / "shaderlib"; const fs::path shaderPath = shaderDir / "lit.shader"; const fs::path includePath = includeDir / "shared.hlsl"; fs::remove_all(projectRoot); fs::create_directories(includeDir); WriteTextFile(includePath, "// AUTHORING_ARTIFACT_SHARED_INCLUDE\n"); WriteTextFile( shaderPath, R"(Shader "ArtifactAuthoringShader" { HLSLINCLUDE #include "shaderlib/shared.hlsl" struct VSInput { float3 positionOS : POSITION; }; ENDHLSL SubShader { Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardBase" } HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert(VSInput input) : SV_POSITION { return float4(input.positionOS, 1.0); } float4 Frag() : SV_TARGET { return float4(1.0, 0.0, 0.0, 1.0); } ENDHLSL } } } )"); AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset firstResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, firstResolve)); ASSERT_TRUE(firstResolve.artifactReady); EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().string(), ".xcshader"); EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr())); ShaderLoader loader; LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr()); ASSERT_TRUE(firstLoad); ASSERT_NE(firstLoad.resource, nullptr); auto* firstShader = static_cast(firstLoad.resource); ASSERT_NE(firstShader, nullptr); EXPECT_EQ(firstShader->GetName(), "ArtifactAuthoringShader"); const ShaderStageVariant* firstFragment = firstShader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(firstFragment, nullptr); EXPECT_NE( std::string(firstFragment->sourceCode.CStr()).find("#include \"shaderlib/shared.hlsl\""), std::string::npos); delete firstShader; const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); { std::ofstream includeFile(includePath, std::ios::app); ASSERT_TRUE(includeFile.is_open()); includeFile << "\n// force authoring dependency reimport\n"; ASSERT_TRUE(static_cast(includeFile)); } database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, secondResolve)); ASSERT_TRUE(secondResolve.artifactReady); EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath); EXPECT_TRUE(fs::exists(secondResolve.artifactMainPath.CStr())); database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenStageDependencyChanges) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_dependency_reimport_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path manifestPath = shaderDir / "lit.shader"; const fs::path fragmentPath = shaderDir / "lit.frag.glsl"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile(shaderDir / "lit.vert.glsl", "#version 430\nvoid main() {}\n"); WriteTextFile(fragmentPath, "#version 430\nvoid main() {}\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"DependencyShader\",\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset firstResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, firstResolve)); ASSERT_TRUE(firstResolve.artifactReady); const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); { std::ofstream fragmentFile(fragmentPath, std::ios::app); ASSERT_TRUE(fragmentFile.is_open()); fragmentFile << "\n// force dependency reimport\n"; ASSERT_TRUE(static_cast(fragmentFile)); } database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, secondResolve)); ASSERT_TRUE(secondResolve.artifactReady); EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath); EXPECT_TRUE(fs::exists(secondResolve.artifactMainPath.CStr())); database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, LoadBuiltinForwardLitShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(shader->GetProperties().Size(), 3u); ASSERT_EQ(pass->variants.Size(), 8u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ForwardBase"); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); const ShaderPropertyDesc* cutoffProperty = shader->FindProperty("_Cutoff"); ASSERT_NE(cutoffProperty, nullptr); EXPECT_EQ(cutoffProperty->type, ShaderPropertyType::Range); EXPECT_EQ(cutoffProperty->semantic, "AlphaCutoff"); EXPECT_EQ(shader->FindPassResourceBinding("ForwardLit", "PerObjectConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ForwardLit", "LightingConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ForwardLit", "ShadowReceiverConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ForwardLit", "ShadowMapTexture"), nullptr); ASSERT_EQ(pass->keywordDeclarations.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::MultiCompile); ASSERT_EQ(pass->keywordDeclarations[0].options.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].options[0], "_"); EXPECT_EQ(pass->keywordDeclarations[0].options[1], "XC_MAIN_LIGHT_SHADOWS"); EXPECT_EQ(pass->keywordDeclarations[1].type, ShaderKeywordDeclarationType::ShaderFeatureLocal); ASSERT_EQ(pass->keywordDeclarations[1].options.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[1].options[0], "_"); EXPECT_EQ(pass->keywordDeclarations[1].options[1], "XC_ALPHA_TEST"); EXPECT_TRUE(shader->PassDeclaresKeyword("ForwardLit", "XC_MAIN_LIGHT_SHADOWS")); EXPECT_TRUE(shader->PassDeclaresKeyword("ForwardLit", "XC_ALPHA_TEST")); ASSERT_EQ(pass->variants.Size(), 8u); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("cbuffer LightingConstants"), std::string::npos); const ShaderStageVariant* d3d12Fragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(d3d12Fragment, nullptr); EXPECT_EQ(d3d12Fragment->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Fragment->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Fragment->entryPoint, "MainPS"); EXPECT_EQ(d3d12Fragment->profile, "ps_5_0"); EXPECT_NE(std::string(d3d12Fragment->sourceCode.CStr()).find("gAdditionalLights"), std::string::npos); EXPECT_NE(std::string(d3d12Fragment->sourceCode.CStr()).find("gLightingParams"), std::string::npos); EXPECT_EQ(std::string(d3d12Fragment->sourceCode.CStr()).find("#define XC_MAIN_LIGHT_SHADOWS 1"), std::string::npos); ShaderKeywordSet shadowKeywords = {}; shadowKeywords.enabledKeywords.PushBack("XC_MAIN_LIGHT_SHADOWS"); const ShaderStageVariant* shadowD3D12Fragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12, shadowKeywords); ASSERT_NE(shadowD3D12Fragment, nullptr); EXPECT_NE( std::string(shadowD3D12Fragment->sourceCode.CStr()).find("#define XC_MAIN_LIGHT_SHADOWS 1"), std::string::npos); EXPECT_NE( std::string(shadowD3D12Fragment->sourceCode.CStr()).find("ShadowMapTexture.Sample"), std::string::npos); ShaderKeywordSet alphaShadowKeywords = {}; alphaShadowKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); alphaShadowKeywords.enabledKeywords.PushBack("XC_MAIN_LIGHT_SHADOWS"); const ShaderStageVariant* alphaShadowD3D12Fragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::D3D12, alphaShadowKeywords); ASSERT_NE(alphaShadowD3D12Fragment, nullptr); EXPECT_NE( std::string(alphaShadowD3D12Fragment->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE( std::string(alphaShadowD3D12Fragment->sourceCode.CStr()).find("gAlphaCutoffParams"), std::string::npos); const ShaderStageVariant* alphaShadowOpenGLFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL, alphaShadowKeywords); ASSERT_NE(alphaShadowOpenGLFragment, nullptr); EXPECT_EQ(alphaShadowOpenGLFragment->backend, ShaderBackend::Generic); EXPECT_EQ(alphaShadowOpenGLFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(alphaShadowOpenGLFragment->entryPoint, "MainPS"); EXPECT_EQ(alphaShadowOpenGLFragment->profile, "ps_5_0"); const std::string alphaShadowOpenGLSource = alphaShadowOpenGLFragment->sourceCode.CStr(); EXPECT_NE(alphaShadowOpenGLSource.find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE(alphaShadowOpenGLSource.find("clip(baseColor.a - gAlphaCutoffParams.x);"), std::string::npos); const ShaderStageVariant* alphaShadowVulkanFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan, alphaShadowKeywords); ASSERT_NE(alphaShadowVulkanFragment, nullptr); EXPECT_EQ(alphaShadowVulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(alphaShadowVulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(alphaShadowVulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(alphaShadowVulkanFragment->profile, "ps_5_0"); const std::string alphaShadowVulkanSource = alphaShadowVulkanFragment->sourceCode.CStr(); EXPECT_NE(alphaShadowVulkanSource.find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE(alphaShadowVulkanSource.find("ShadowMapTexture.Sample"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("gAdditionalLights"), std::string::npos); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("gLightingParams"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gAdditionalLights"), std::string::npos); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gLightingParams"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinUnlitShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinUnlitShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("Unlit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(shader->GetProperties().Size(), 2u); ASSERT_EQ(pass->variants.Size(), 2u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "Unlit"); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); EXPECT_EQ(shader->FindPassResourceBinding("Unlit", "PerObjectConstants"), nullptr); EXPECT_NE(shader->FindVariant("Unlit", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("Unlit", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("Unlit", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("Unlit", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("Unlit", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("Unlit", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "Unlit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("gProjectionMatrix"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "Unlit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("BaseColorTexture.Sample"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "Unlit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gBaseColorFactor"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinObjectIdShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinObjectIdShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("ObjectId"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); ASSERT_EQ(pass->variants.Size(), 2u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ObjectId"); EXPECT_NE(shader->FindVariant("ObjectId", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ObjectId", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ObjectId", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ObjectId", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ObjectId", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("ObjectId", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "ObjectId", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("gObjectIdColor"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ObjectId", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("return gObjectIdColor;"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ObjectId", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gModelMatrix"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinDepthOnlyShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinDepthOnlyShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("DepthOnly"); ASSERT_NE(pass, nullptr); ASSERT_EQ(shader->GetProperties().Size(), 3u); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); ASSERT_EQ(pass->variants.Size(), 4u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "DepthOnly"); ASSERT_EQ(pass->keywordDeclarations.Size(), 1u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::ShaderFeatureLocal); ASSERT_EQ(pass->keywordDeclarations[0].options.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].options[0], "_"); EXPECT_EQ(pass->keywordDeclarations[0].options[1], "XC_ALPHA_TEST"); EXPECT_TRUE(shader->PassDeclaresKeyword("DepthOnly", "XC_ALPHA_TEST")); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* cutoffProperty = shader->FindProperty("_Cutoff"); ASSERT_NE(cutoffProperty, nullptr); EXPECT_EQ(cutoffProperty->type, ShaderPropertyType::Range); EXPECT_EQ(cutoffProperty->semantic, "AlphaCutoff"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); EXPECT_EQ(shader->FindPassResourceBinding("DepthOnly", "PerObjectConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("DepthOnly", "MaterialConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("DepthOnly", "BaseColorTexture"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("DepthOnly", "LinearClampSampler"), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "DepthOnly", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("gProjectionMatrix"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("BaseColorTexture.Sample"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "DepthOnly", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gAlphaCutoffParams"), std::string::npos); ShaderKeywordSet alphaKeywords = {}; alphaKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); const ShaderStageVariant* alphaD3D12Fragment = shader->FindVariant( "DepthOnly", ShaderType::Fragment, ShaderBackend::D3D12, alphaKeywords); ASSERT_NE(alphaD3D12Fragment, nullptr); EXPECT_EQ(alphaD3D12Fragment->backend, ShaderBackend::Generic); EXPECT_EQ(alphaD3D12Fragment->language, ShaderLanguage::HLSL); EXPECT_EQ(alphaD3D12Fragment->entryPoint, "MainPS"); EXPECT_EQ(alphaD3D12Fragment->profile, "ps_5_0"); EXPECT_NE( std::string(alphaD3D12Fragment->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE( std::string(alphaD3D12Fragment->sourceCode.CStr()).find("gAlphaCutoffParams"), std::string::npos); const ShaderStageVariant* alphaOpenGLFragment = shader->FindVariant( "DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL, alphaKeywords); ASSERT_NE(alphaOpenGLFragment, nullptr); EXPECT_EQ(alphaOpenGLFragment->backend, ShaderBackend::Generic); EXPECT_EQ(alphaOpenGLFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(alphaOpenGLFragment->entryPoint, "MainPS"); EXPECT_EQ(alphaOpenGLFragment->profile, "ps_5_0"); const std::string alphaOpenGLSource = alphaOpenGLFragment->sourceCode.CStr(); EXPECT_NE(alphaOpenGLSource.find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE(alphaOpenGLSource.find("clip(baseColor.a - gAlphaCutoffParams.x);"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinShadowCasterShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinShadowCasterShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("ShadowCaster"); ASSERT_NE(pass, nullptr); ASSERT_EQ(shader->GetProperties().Size(), 3u); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); ASSERT_EQ(pass->variants.Size(), 4u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ShadowCaster"); ASSERT_EQ(pass->keywordDeclarations.Size(), 1u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::ShaderFeatureLocal); ASSERT_EQ(pass->keywordDeclarations[0].options.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].options[0], "_"); EXPECT_EQ(pass->keywordDeclarations[0].options[1], "XC_ALPHA_TEST"); EXPECT_TRUE(shader->PassDeclaresKeyword("ShadowCaster", "XC_ALPHA_TEST")); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* cutoffProperty = shader->FindProperty("_Cutoff"); ASSERT_NE(cutoffProperty, nullptr); EXPECT_EQ(cutoffProperty->type, ShaderPropertyType::Range); EXPECT_EQ(cutoffProperty->semantic, "AlphaCutoff"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); EXPECT_EQ(shader->FindPassResourceBinding("ShadowCaster", "PerObjectConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ShadowCaster", "MaterialConstants"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ShadowCaster", "BaseColorTexture"), nullptr); EXPECT_EQ(shader->FindPassResourceBinding("ShadowCaster", "LinearClampSampler"), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "ShadowCaster", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("gProjectionMatrix"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ShadowCaster", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("return input.position.z;"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ShadowCaster", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gAlphaCutoffParams"), std::string::npos); ShaderKeywordSet alphaKeywords = {}; alphaKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); const ShaderStageVariant* alphaD3D12Fragment = shader->FindVariant( "ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12, alphaKeywords); ASSERT_NE(alphaD3D12Fragment, nullptr); EXPECT_EQ(alphaD3D12Fragment->backend, ShaderBackend::Generic); EXPECT_EQ(alphaD3D12Fragment->language, ShaderLanguage::HLSL); EXPECT_EQ(alphaD3D12Fragment->entryPoint, "MainPS"); EXPECT_EQ(alphaD3D12Fragment->profile, "ps_5_0"); EXPECT_NE( std::string(alphaD3D12Fragment->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE( std::string(alphaD3D12Fragment->sourceCode.CStr()).find("gAlphaCutoffParams"), std::string::npos); const ShaderStageVariant* alphaOpenGLFragment = shader->FindVariant( "ShadowCaster", ShaderType::Fragment, ShaderBackend::OpenGL, alphaKeywords); ASSERT_NE(alphaOpenGLFragment, nullptr); EXPECT_EQ(alphaOpenGLFragment->backend, ShaderBackend::Generic); EXPECT_EQ(alphaOpenGLFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(alphaOpenGLFragment->entryPoint, "MainPS"); EXPECT_EQ(alphaOpenGLFragment->profile, "ps_5_0"); const std::string alphaOpenGLSource = alphaOpenGLFragment->sourceCode.CStr(); EXPECT_NE(alphaOpenGLSource.find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE(alphaOpenGLSource.find("clip(baseColor.a - gAlphaCutoffParams.x);"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinFinalColorShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinFinalColorShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("FinalColor"); ASSERT_NE(pass, nullptr); ASSERT_EQ(shader->GetProperties().Size(), 4u); ASSERT_EQ(pass->variants.Size(), 2u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::Always); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "FinalOutput"); const ShaderPropertyDesc* colorScaleProperty = shader->FindProperty("_ColorScale"); ASSERT_NE(colorScaleProperty, nullptr); EXPECT_EQ(colorScaleProperty->type, ShaderPropertyType::Color); const ShaderPropertyDesc* exposureProperty = shader->FindProperty("_Exposure"); ASSERT_NE(exposureProperty, nullptr); EXPECT_EQ(exposureProperty->type, ShaderPropertyType::Float); const ShaderPropertyDesc* outputTransferProperty = shader->FindProperty("_OutputTransferMode"); ASSERT_NE(outputTransferProperty, nullptr); EXPECT_EQ(outputTransferProperty->type, ShaderPropertyType::Float); const ShaderPropertyDesc* toneMappingProperty = shader->FindProperty("_ToneMappingMode"); ASSERT_NE(toneMappingProperty, nullptr); EXPECT_EQ(toneMappingProperty->type, ShaderPropertyType::Float); EXPECT_NE(shader->FindVariant("FinalColor", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("FinalColor", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("FinalColor", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("FinalColor", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("FinalColor", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("FinalColor", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "FinalColor", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("UNITY_UV_STARTS_AT_TOP"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "FinalColor", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("gSourceColorTexture.Sample"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "FinalColor", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("ApplyToneMapping"), std::string::npos); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gFinalColorParams"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinColorScalePostProcessShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinColorScalePostProcessShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* pass = shader->FindPass("ColorScale"); ASSERT_NE(pass, nullptr); ASSERT_EQ(shader->GetProperties().Size(), 1u); ASSERT_EQ(pass->variants.Size(), 2u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_TRUE(pass->resources.Empty()); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::Always); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "PostProcess"); const ShaderPropertyDesc* colorScaleProperty = shader->FindProperty("_ColorScale"); ASSERT_NE(colorScaleProperty, nullptr); EXPECT_EQ(colorScaleProperty->type, ShaderPropertyType::Color); EXPECT_NE(shader->FindVariant("ColorScale", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ColorScale", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ColorScale", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ColorScale", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ColorScale", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("ColorScale", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "ColorScale", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->backend, ShaderBackend::Generic); EXPECT_EQ(d3d12Vertex->language, ShaderLanguage::HLSL); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("UNITY_UV_STARTS_AT_TOP"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ColorScale", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->backend, ShaderBackend::Generic); EXPECT_EQ(openglFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_0"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("gSourceColorTexture.Sample"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ColorScale", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->backend, ShaderBackend::Generic); EXPECT_EQ(vulkanFragment->language, ShaderLanguage::HLSL); EXPECT_EQ(vulkanFragment->entryPoint, "MainPS"); EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gColorScale"), std::string::npos); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gSourceColorTexture.Sample"), std::string::npos); delete shader; } TEST(ShaderLoader, ResourceManagerLazilyLoadsBuiltinForwardLitShader) { ResourceManager& manager = ResourceManager::Get(); manager.Shutdown(); manager.UnregisterLoader(ResourceType::Shader); ResourceHandle shaderHandle = manager.Load(GetBuiltinForwardLitShaderPath()); ASSERT_TRUE(shaderHandle.IsValid()); ASSERT_NE(shaderHandle->FindPass("ForwardLit"), nullptr); ResourceHandle unlitShaderHandle = manager.Load(GetBuiltinUnlitShaderPath()); ASSERT_TRUE(unlitShaderHandle.IsValid()); ASSERT_NE(unlitShaderHandle->FindPass("Unlit"), nullptr); ResourceHandle objectIdShaderHandle = manager.Load(GetBuiltinObjectIdShaderPath()); ASSERT_TRUE(objectIdShaderHandle.IsValid()); ASSERT_NE(objectIdShaderHandle->FindPass("ObjectId"), nullptr); manager.Shutdown(); } TEST(ShaderLoader, ResourceManagerLoadsBuiltinShadersOutsideProjectWorkingDirectory) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); const fs::path previousPath = fs::current_path(); const String previousResourceRoot = manager.GetResourceRoot(); const fs::path isolatedWorkingDirectory = fs::temp_directory_path() / "xc_builtin_shader_out_of_tree"; fs::remove_all(isolatedWorkingDirectory); fs::create_directories(isolatedWorkingDirectory); manager.Shutdown(); manager.SetResourceRoot(""); fs::current_path(isolatedWorkingDirectory); auto expectBuiltinShader = [&](const String& path, const char* passName, ShaderType stage, ShaderBackend backend, const char* marker) { const ResourceHandle shaderHandle = manager.Load(path); EXPECT_TRUE(shaderHandle.IsValid()) << path.CStr(); if (!shaderHandle.IsValid()) { return; } EXPECT_NE(shaderHandle->FindPass(passName), nullptr) << path.CStr(); const ShaderStageVariant* variant = shaderHandle->FindVariant(passName, stage, backend); EXPECT_NE(variant, nullptr) << path.CStr(); if (variant != nullptr) { EXPECT_NE(std::string(variant->sourceCode.CStr()).find(marker), std::string::npos) << path.CStr(); } }; expectBuiltinShader( GetBuiltinForwardLitShaderPath(), "ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12, "cbuffer LightingConstants"); expectBuiltinShader( GetBuiltinUnlitShaderPath(), "Unlit", ShaderType::Fragment, ShaderBackend::OpenGL, "BaseColorTexture"); expectBuiltinShader( GetBuiltinObjectIdShaderPath(), "ObjectId", ShaderType::Fragment, ShaderBackend::Vulkan, "return gObjectIdColor;"); fs::current_path(previousPath); manager.SetResourceRoot(previousResourceRoot); manager.Shutdown(); fs::remove_all(isolatedWorkingDirectory); } } // namespace