#include #include #include #include #include #include #include #include #include "Rendering/Internal/ShaderVariantUtils.h" #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)); } ShaderArtifactFileHeader ReadShaderArtifactFileHeader(const std::filesystem::path& path) { ShaderArtifactFileHeader header = {}; std::ifstream input(path, std::ios::binary); EXPECT_TRUE(input.is_open()); input.read(reinterpret_cast(&header), sizeof(header)); EXPECT_TRUE(static_cast(input)); return header; } 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.shader")); EXPECT_TRUE(loader.CanLoad("test.xcshader")); EXPECT_TRUE(loader.CanLoad(GetBuiltinForwardLitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinUnlitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdOutlineShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinSelectionMaskShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinSelectionOutlineShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinGaussianSplatUtilitiesShaderPath())); EXPECT_FALSE(loader.CanLoad("test.vert")); EXPECT_FALSE(loader.CanLoad("test.frag")); EXPECT_FALSE(loader.CanLoad("test.glsl")); EXPECT_FALSE(loader.CanLoad("test.hlsl")); EXPECT_FALSE(loader.CanLoad("test.txt")); EXPECT_FALSE(loader.CanLoad("test.png")); } TEST(ShaderLoader, LoadInvalidPath) { ShaderLoader loader; LoadResult result = loader.Load("invalid/path/shader.shader"); EXPECT_FALSE(result); } TEST(ShaderLoader, RejectsLegacyShaderArtifactSchemaHeaders) { namespace fs = std::filesystem; const fs::path artifactPath = fs::temp_directory_path() / "xc_shader_loader_legacy_schema.xcshader"; fs::remove(artifactPath); ShaderArtifactFileHeader header = {}; std::memcpy(header.magic, "XCSHD04", 7); header.magic[7] = '\0'; header.schemaVersion = 4u; { std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); ASSERT_TRUE(output.is_open()); output.write(reinterpret_cast(&header), sizeof(header)); ASSERT_TRUE(static_cast(output)); } ShaderLoader loader; LoadResult result = loader.Load(artifactPath.string().c_str()); EXPECT_FALSE(result); EXPECT_NE( std::string(result.errorMessage.CStr()).find("Invalid shader artifact header"), std::string::npos); fs::remove(artifactPath); } TEST(ShaderLoader, RejectsLegacySingleStageShaderSourceFiles) { 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()); EXPECT_FALSE(result); EXPECT_NE( std::string(result.errorMessage.CStr()).find("Unsupported shader source format"), std::string::npos); std::remove(shaderPath.string().c_str()); } TEST(ShaderLoader, LoadShaderAuthoringBuildsMultiPassGenericVariants) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_multi_pass_test"; const fs::path shaderPath = shaderRoot / "multi_pass.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "TestLitShader" { Properties { _BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)] _MainTex ("Base Map", 2D) = "white" [Semantic(BaseColorTexture)] } SubShader { Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardLit", "Queue" = "Geometry" } HLSLPROGRAM #pragma target 4.5 #pragma vertex MainVS #pragma fragment MainPS struct VSInput { float3 positionOS : POSITION; }; float4 MainVS(VSInput input) : SV_POSITION { return float4(input.positionOS, 1.0); // FORWARD_LIT_GENERIC_VS } float4 MainPS() : SV_TARGET { return float4(1.0, 0.0, 0.0, 1.0); // FORWARD_LIT_GENERIC_PS } ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } HLSLPROGRAM #pragma target 4.5 #pragma vertex DepthVS #pragma fragment DepthPS struct DepthInput { float3 positionOS : POSITION; }; float4 DepthVS(DepthInput input) : SV_POSITION { return float4(input.positionOS, 1.0); // DEPTH_ONLY_GENERIC_VS } float4 DepthPS() : SV_TARGET { return 1.0; // DEPTH_ONLY_GENERIC_PS } 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(), "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); EXPECT_EQ(forwardLitPass->resources.Size(), 8u); ASSERT_NE(FindPassTag(forwardLitPass, "LightMode"), nullptr); ASSERT_NE(FindPassTag(forwardLitPass, "Queue"), nullptr); EXPECT_EQ(FindPassTag(forwardLitPass, "LightMode")->value, "ForwardLit"); EXPECT_EQ(FindPassTag(forwardLitPass, "Queue")->value, "Geometry"); 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_1"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("FORWARD_LIT_GENERIC_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "ps_5_1"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("FORWARD_LIT_GENERIC_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->profile, "ps_5_1"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("FORWARD_LIT_GENERIC_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].name, "LightMode"); 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); const ShaderStageVariant* depthOnlyOpenGLFragment = shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(depthOnlyOpenGLFragment, nullptr); EXPECT_NE(std::string(depthOnlyOpenGLFragment->sourceCode.CStr()).find("DEPTH_ONLY_GENERIC_PS"), std::string::npos); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringRejectsNonAuthoringShaderFiles) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_invalid_authoring_test"; const fs::path shaderPath = shaderRoot / "invalid.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile(shaderPath, "{\n \"passes\": []\n}\n"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); EXPECT_FALSE(result); EXPECT_NE( std::string(result.errorMessage.CStr()).find("must start with a Shader declaration"), std::string::npos); fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringParsesVariantKeywordsAndKeywordAwareLookup) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_variant_keywords"; const fs::path shaderPath = shaderRoot / "variant_keywords.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "VariantKeywordShader" { SubShader { Pass { Name "ForwardLit" HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #pragma shader_feature_local _ XC_ALPHA_TEST #pragma shader_feature_local _ XC_FOG float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return float4(1.0, 0.0, 0.0, 1.0); // KEYWORD_AWARE_FRAGMENT } 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); const ShaderStageVariant* baseFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(baseFragment, nullptr); EXPECT_NE(std::string(baseFragment->sourceCode.CStr()).find("KEYWORD_AWARE_FRAGMENT"), std::string::npos); EXPECT_EQ(baseFragment->requiredKeywords.enabledKeywords.Size(), 0u); 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_AWARE_FRAGMENT"), std::string::npos); EXPECT_NE(std::string(keywordFragment->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); EXPECT_NE(std::string(keywordFragment->sourceCode.CStr()).find("#define XC_FOG 1"), 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_EQ(pass->resources.Size(), 8u); 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_1"); 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_1"); 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, LoadShaderAuthoringBuildsComputeOnlyPassVariant) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_compute_only"; const fs::path shaderPath = shaderRoot / "compute_only.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "ComputeOnlyShader" { SubShader { Pass { Name "SortKeys" HLSLPROGRAM #pragma target 4.5 #pragma compute SortKeysCS StructuredBuffer InputData; RWStructuredBuffer OutputOrder; [numthreads(64, 1, 1)] void SortKeysCS(uint3 dispatchThreadId : SV_DispatchThreadID) { OutputOrder[dispatchThreadId.x] = (uint)InputData[dispatchThreadId.x].x; // COMPUTE_ONLY_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()); const ShaderPass* pass = shader->FindPass("SortKeys"); ASSERT_NE(pass, nullptr); EXPECT_EQ(pass->resources.Size(), 2u); const ShaderResourceBindingDesc* inputData = shader->FindPassResourceBinding("SortKeys", "InputData"); ASSERT_NE(inputData, nullptr); EXPECT_EQ(inputData->type, ShaderResourceType::StructuredBuffer); EXPECT_EQ(inputData->set, 2u); EXPECT_EQ(inputData->binding, 0u); const ShaderResourceBindingDesc* outputOrder = shader->FindPassResourceBinding("SortKeys", "OutputOrder"); ASSERT_NE(outputOrder, nullptr); EXPECT_EQ(outputOrder->type, ShaderResourceType::RWStructuredBuffer); EXPECT_EQ(outputOrder->set, 4u); EXPECT_EQ(outputOrder->binding, 0u); EXPECT_EQ(shader->FindVariant("SortKeys", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_EQ(shader->FindVariant("SortKeys", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); const ShaderStageVariant* computeVariant = shader->FindVariant("SortKeys", ShaderType::Compute, ShaderBackend::D3D12); ASSERT_NE(computeVariant, nullptr); EXPECT_EQ(computeVariant->backend, ShaderBackend::Generic); EXPECT_EQ(computeVariant->language, ShaderLanguage::HLSL); EXPECT_EQ(computeVariant->entryPoint, "SortKeysCS"); EXPECT_EQ(computeVariant->profile, "cs_5_1"); EXPECT_NE( std::string(computeVariant->sourceCode.CStr()).find("COMPUTE_ONLY_BODY"), std::string::npos); XCEngine::RHI::ShaderCompileDesc d3d12CompileDesc = {}; ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, ShaderBackend::D3D12, *computeVariant, d3d12CompileDesc); const std::string d3d12Source( reinterpret_cast(d3d12CompileDesc.source.data()), d3d12CompileDesc.source.size()); EXPECT_NE(d3d12Source.find("StructuredBuffer InputData: register(t0);"), std::string::npos); EXPECT_NE(d3d12Source.find("RWStructuredBuffer OutputOrder: register(u0);"), std::string::npos); XCEngine::RHI::ShaderCompileDesc vulkanCompileDesc = {}; ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, ShaderBackend::Vulkan, *computeVariant, vulkanCompileDesc); const std::string vulkanSource( reinterpret_cast(vulkanCompileDesc.source.data()), vulkanCompileDesc.source.size()); EXPECT_NE(vulkanSource.find("StructuredBuffer InputData: register(t0, space2);"), std::string::npos); EXPECT_NE(vulkanSource.find("RWStructuredBuffer OutputOrder: register(u0, space4);"), std::string::npos); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringBuildsComputeOnlyPassConstantBufferBindings) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_compute_constants"; const fs::path shaderPath = shaderRoot / "compute_constants.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "ComputeConstantsShader" { SubShader { Pass { Name "Prepare" HLSLPROGRAM #pragma target 4.5 #pragma compute PrepareCS cbuffer PerObjectConstants { float4x4 ObjectToWorld; }; StructuredBuffer InputData; RWStructuredBuffer OutputOrder; [numthreads(64, 1, 1)] void PrepareCS(uint3 dispatchThreadId : SV_DispatchThreadID) { OutputOrder[dispatchThreadId.x] = (uint)mul(ObjectToWorld, InputData[dispatchThreadId.x]).x; } 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); const ShaderPass* pass = shader->FindPass("Prepare"); ASSERT_NE(pass, nullptr); EXPECT_EQ(pass->resources.Size(), 3u); const ShaderResourceBindingDesc* perObject = shader->FindPassResourceBinding("Prepare", "PerObjectConstants"); ASSERT_NE(perObject, nullptr); EXPECT_EQ(perObject->type, ShaderResourceType::ConstantBuffer); EXPECT_EQ(perObject->set, 0u); EXPECT_EQ(perObject->binding, 0u); const ShaderStageVariant* computeVariant = shader->FindVariant("Prepare", ShaderType::Compute, ShaderBackend::D3D12); ASSERT_NE(computeVariant, nullptr); XCEngine::RHI::ShaderCompileDesc d3d12CompileDesc = {}; ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, ShaderBackend::D3D12, *computeVariant, d3d12CompileDesc); const std::string d3d12Source( reinterpret_cast(d3d12CompileDesc.source.data()), d3d12CompileDesc.source.size()); EXPECT_TRUE(std::regex_search( d3d12Source, std::regex(R"(cbuffer\s+PerObjectConstants\s*:\s*register\(b0\))", std::regex::ECMAScript))); XCEngine::RHI::ShaderCompileDesc vulkanCompileDesc = {}; ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, ShaderBackend::Vulkan, *computeVariant, vulkanCompileDesc); const std::string vulkanSource( reinterpret_cast(vulkanCompileDesc.source.data()), vulkanCompileDesc.source.size()); EXPECT_TRUE(std::regex_search( vulkanSource, std::regex( R"(cbuffer\s+PerObjectConstants\s*:\s*register\(b0,\s*space0\))", std::regex::ECMAScript))); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringRejectsPassMixingComputeAndGraphicsPragmas) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_mixed_compute_graphics"; const fs::path shaderPath = shaderRoot / "mixed_compute_graphics.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "MixedComputeGraphicsShader" { SubShader { Pass { Name "InvalidPass" HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #pragma compute ComputeMain float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1.0; } [numthreads(1, 1, 1)] void ComputeMain(uint3 dispatchThreadId : SV_DispatchThreadID) {} ENDHLSL } } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); EXPECT_FALSE(result); EXPECT_NE( std::string(result.errorMessage.CStr()).find("must not mix #pragma compute with graphics stage pragmas"), std::string::npos); fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringCollectsUnityStyleBufferResources) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_buffer_resources_test"; const fs::path shaderPath = shaderRoot / "buffer_resources.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "Test/BufferResources" { SubShader { Pass { Name "Volume" HLSLPROGRAM #pragma target 4.5 #pragma vertex MainVS #pragma fragment MainPS StructuredBuffer InputBuffer; ByteAddressBuffer RawInput; RWStructuredBuffer OutputBuffer; RWByteAddressBuffer OutputRaw; struct VSInput { float3 positionOS : POSITION; }; float4 MainVS(VSInput input) : SV_POSITION { return float4(input.positionOS, 1.0); } float4 MainPS() : SV_TARGET { return float4(1.0, 1.0, 1.0, 1.0); } 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); const ShaderPass* pass = shader->FindPass("Volume"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->resources.Size(), 4u); const ShaderResourceBindingDesc* inputBuffer = shader->FindPassResourceBinding("Volume", "InputBuffer"); ASSERT_NE(inputBuffer, nullptr); EXPECT_EQ(inputBuffer->type, ShaderResourceType::StructuredBuffer); EXPECT_EQ(inputBuffer->set, 2u); EXPECT_EQ(inputBuffer->binding, 0u); const ShaderResourceBindingDesc* rawInput = shader->FindPassResourceBinding("Volume", "RawInput"); ASSERT_NE(rawInput, nullptr); EXPECT_EQ(rawInput->type, ShaderResourceType::RawBuffer); EXPECT_EQ(rawInput->set, 2u); EXPECT_EQ(rawInput->binding, 1u); const ShaderResourceBindingDesc* outputBuffer = shader->FindPassResourceBinding("Volume", "OutputBuffer"); ASSERT_NE(outputBuffer, nullptr); EXPECT_EQ(outputBuffer->type, ShaderResourceType::RWStructuredBuffer); EXPECT_EQ(outputBuffer->set, 4u); EXPECT_EQ(outputBuffer->binding, 0u); const ShaderResourceBindingDesc* outputRaw = shader->FindPassResourceBinding("Volume", "OutputRaw"); ASSERT_NE(outputRaw, nullptr); EXPECT_EQ(outputRaw->type, ShaderResourceType::RWRawBuffer); EXPECT_EQ(outputRaw->set, 4u); EXPECT_EQ(outputRaw->binding, 1u); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, RuntimeShaderSourceRewritesUnityStyleBufferRegisters) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_buffer_runtime_test"; const fs::path shaderPath = shaderRoot / "buffer_runtime.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "Test/BufferRuntime" { SubShader { Pass { Name "Volume" HLSLPROGRAM #pragma target 4.5 #pragma vertex MainVS #pragma fragment MainPS StructuredBuffer InputBuffer; ByteAddressBuffer RawInput; RWStructuredBuffer OutputBuffer; RWByteAddressBuffer OutputRaw; struct VSInput { float3 positionOS : POSITION; }; float4 MainVS(VSInput input) : SV_POSITION { return float4(input.positionOS, 1.0); } float4 MainPS() : SV_TARGET { return float4(1.0, 0.5, 0.25, 1.0); } 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); const ShaderPass* pass = shader->FindPass("Volume"); ASSERT_NE(pass, nullptr); const ShaderStageVariant* fragmentVariant = shader->FindVariant("Volume", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(fragmentVariant, nullptr); const std::string d3d12Source = ::XCEngine::Rendering::Internal::BuildRuntimeShaderSource( *pass, ShaderBackend::D3D12, *fragmentVariant); EXPECT_NE( d3d12Source.find("StructuredBuffer InputBuffer: register(t0);"), std::string::npos); EXPECT_NE( d3d12Source.find("ByteAddressBuffer RawInput: register(t1);"), std::string::npos); EXPECT_NE( d3d12Source.find("RWStructuredBuffer OutputBuffer: register(u0);"), std::string::npos); EXPECT_NE( d3d12Source.find("RWByteAddressBuffer OutputRaw: register(u1);"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant("Volume", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); const std::string vulkanSource = ::XCEngine::Rendering::Internal::BuildRuntimeShaderSource( *pass, ShaderBackend::Vulkan, *vulkanFragment); EXPECT_NE( vulkanSource.find("StructuredBuffer InputBuffer: register(t0, space2);"), std::string::npos); EXPECT_NE( vulkanSource.find("ByteAddressBuffer RawInput: register(t1, space2);"), std::string::npos); EXPECT_NE( vulkanSource.find("RWStructuredBuffer OutputBuffer: register(u0, space4);"), std::string::npos); EXPECT_NE( vulkanSource.find("RWByteAddressBuffer OutputRaw: register(u1, space4);"), std::string::npos); 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 BlendOp RevSub, Max ColorMask RGB Offset -1, 2 Stencil { Ref 7 ReadMask 63 WriteMask 31 CompFront Equal PassFront Replace FailFront Zero ZFailFront IncrSat CompBack NotEqual PassBack IncrWrap FailBack Invert ZFailBack DecrWrap } 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.blendOp, MaterialBlendOp::ReverseSubtract); EXPECT_EQ(pass->fixedFunctionState.blendOpAlpha, MaterialBlendOp::Max); EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7u); EXPECT_FLOAT_EQ(pass->fixedFunctionState.depthBiasFactor, -1.0f); EXPECT_EQ(pass->fixedFunctionState.depthBiasUnits, 2); EXPECT_TRUE(pass->fixedFunctionState.stencil.enabled); EXPECT_EQ(pass->fixedFunctionState.stencil.reference, 7u); EXPECT_EQ(pass->fixedFunctionState.stencil.readMask, 63u); EXPECT_EQ(pass->fixedFunctionState.stencil.writeMask, 31u); EXPECT_EQ(pass->fixedFunctionState.stencil.front.func, MaterialComparisonFunc::Equal); EXPECT_EQ(pass->fixedFunctionState.stencil.front.passOp, MaterialStencilOp::Replace); EXPECT_EQ(pass->fixedFunctionState.stencil.front.failOp, MaterialStencilOp::Zero); EXPECT_EQ(pass->fixedFunctionState.stencil.front.depthFailOp, MaterialStencilOp::IncrSat); EXPECT_EQ(pass->fixedFunctionState.stencil.back.func, MaterialComparisonFunc::NotEqual); EXPECT_EQ(pass->fixedFunctionState.stencil.back.passOp, MaterialStencilOp::IncrWrap); EXPECT_EQ(pass->fixedFunctionState.stencil.back.failOp, MaterialStencilOp::Invert); EXPECT_EQ(pass->fixedFunctionState.stencil.back.depthFailOp, MaterialStencilOp::DecrWrap); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringParsesHexStencilMasks) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_hex_stencil"; const fs::path shaderPath = shaderRoot / "hex_stencil.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "HexStencilShader" { SubShader { Pass { Name "ForwardLit" Stencil { Ref 0x07 ReadMask 0x3F WriteMask 0x1F Comp Always Pass Replace } 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); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_TRUE(pass->fixedFunctionState.stencil.enabled); EXPECT_EQ(pass->fixedFunctionState.stencil.reference, 7u); EXPECT_EQ(pass->fixedFunctionState.stencil.readMask, 63u); EXPECT_EQ(pass->fixedFunctionState.stencil.writeMask, 31u); EXPECT_EQ(pass->fixedFunctionState.stencil.front.passOp, MaterialStencilOp::Replace); EXPECT_EQ(pass->fixedFunctionState.stencil.back.passOp, MaterialStencilOp::Replace); 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 BlendOp Max, Sub ColorMask RGBA Offset 1, 3 Stencil { Ref 5 ReadMask 127 WriteMask 63 Comp Always Pass Replace Fail Keep ZFail IncrWrap } 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.blendOp, MaterialBlendOp::Max); EXPECT_EQ(pass->fixedFunctionState.blendOpAlpha, MaterialBlendOp::Subtract); EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0xFu); EXPECT_FLOAT_EQ(pass->fixedFunctionState.depthBiasFactor, 1.0f); EXPECT_EQ(pass->fixedFunctionState.depthBiasUnits, 3); EXPECT_TRUE(pass->fixedFunctionState.stencil.enabled); EXPECT_EQ(pass->fixedFunctionState.stencil.reference, 5u); EXPECT_EQ(pass->fixedFunctionState.stencil.readMask, 127u); EXPECT_EQ(pass->fixedFunctionState.stencil.writeMask, 63u); EXPECT_EQ(pass->fixedFunctionState.stencil.front.func, MaterialComparisonFunc::Always); EXPECT_EQ(pass->fixedFunctionState.stencil.front.passOp, MaterialStencilOp::Replace); EXPECT_EQ(pass->fixedFunctionState.stencil.front.failOp, MaterialStencilOp::Keep); EXPECT_EQ(pass->fixedFunctionState.stencil.front.depthFailOp, MaterialStencilOp::IncrWrap); EXPECT_EQ(pass->fixedFunctionState.stencil.back.func, MaterialComparisonFunc::Always); EXPECT_EQ(pass->fixedFunctionState.stencil.back.passOp, MaterialStencilOp::Replace); EXPECT_EQ(pass->fixedFunctionState.stencil.back.failOp, MaterialStencilOp::Keep); EXPECT_EQ(pass->fixedFunctionState.stencil.back.depthFailOp, MaterialStencilOp::IncrWrap); 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 BlendOp Max Offset 2, 4 Stencil { Ref 9 ReadMask 255 WriteMask 15 Comp Equal Pass Replace Fail Keep ZFail DecrWrap } 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.blendOp, MaterialBlendOp::Max); EXPECT_EQ(pass->fixedFunctionState.blendOpAlpha, MaterialBlendOp::Max); EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7); EXPECT_FLOAT_EQ(pass->fixedFunctionState.depthBiasFactor, 2.0f); EXPECT_EQ(pass->fixedFunctionState.depthBiasUnits, 4); EXPECT_TRUE(pass->fixedFunctionState.stencil.enabled); EXPECT_EQ(pass->fixedFunctionState.stencil.reference, 9u); EXPECT_EQ(pass->fixedFunctionState.stencil.readMask, 255u); EXPECT_EQ(pass->fixedFunctionState.stencil.writeMask, 15u); EXPECT_EQ(pass->fixedFunctionState.stencil.front.func, MaterialComparisonFunc::Equal); EXPECT_EQ(pass->fixedFunctionState.stencil.front.passOp, MaterialStencilOp::Replace); EXPECT_EQ(pass->fixedFunctionState.stencil.front.failOp, MaterialStencilOp::Keep); EXPECT_EQ(pass->fixedFunctionState.stencil.front.depthFailOp, MaterialStencilOp::DecrWrap); EXPECT_EQ(pass->fixedFunctionState.stencil.back.func, MaterialComparisonFunc::Equal); EXPECT_EQ(pass->fixedFunctionState.stencil.back.passOp, MaterialStencilOp::Replace); EXPECT_EQ(pass->fixedFunctionState.stencil.back.failOp, MaterialStencilOp::Keep); EXPECT_EQ(pass->fixedFunctionState.stencil.back.depthFailOp, MaterialStencilOp::DecrWrap); 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, LoadShaderAuthoringExpandsBuiltinUsePass) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_builtin"; const fs::path shaderPath = shaderRoot / "usepass.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "BuiltinUsePassShader" { SubShader { UsePass "Builtin Shadow Caster/ShadowCaster" } } )"); 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("ShadowCaster"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); EXPECT_TRUE(shader->PassDeclaresKeyword("ShadowCaster", "XC_ALPHA_TEST")); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadShaderAuthoringBuiltinUsePassMergesTargetSubShaderTags) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_builtin_tags"; const fs::path shaderPath = shaderRoot / "usepass_tags.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderPath, R"(Shader "BuiltinUsePassTaggedShader" { SubShader { Tags { "Queue" = "Transparent" } LOD 250 UsePass "Builtin Shadow Caster/ShadowCaster" } } )"); 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("ShadowCaster"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->tags.Size(), 3u); 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, "250"); const ShaderPassTagEntry* lightModeTag = FindPassTag(pass, "LightMode"); ASSERT_NE(lightModeTag, nullptr); EXPECT_EQ(lightModeTag->value, "ShadowCaster"); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringExpandsBuiltinUsePass) { namespace fs = std::filesystem; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_artifact"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path shaderPath = shaderDir / "usepass.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "BuiltinUsePassArtifactShader" { SubShader { UsePass "Builtin Shadow Caster/ShadowCaster" } } )"); AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset resolvedAsset; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/usepass.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); const ShaderPass* pass = shader->FindPass("ShadowCaster"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_TRUE(pass->fixedFunctionState.depthWriteEnable); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); delete shader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, LoadShaderAuthoringExpandsProjectUsePass) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_project"; const fs::path sharedShaderPath = shaderRoot / "shared.shader"; const fs::path mainShaderPath = shaderRoot / "main.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( sharedShaderPath, R"(Shader "Project Shared Shader" { SubShader { Pass { Name "SharedForward" Tags { "LightMode" = "ForwardLit" } Cull Front ZWrite Off Blend SrcAlpha OneMinusSrcAlpha BlendOp Max Offset 2, 5 HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return float4(1.0, 0.0, 0.0, 1.0); } ENDHLSL } } } )"); WriteTextFile( mainShaderPath, R"(Shader "Project UsePass Shader" { SubShader { UsePass "Project Shared Shader/SharedForward" } } )"); ShaderLoader loader; LoadResult result = loader.Load(mainShaderPath.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("SharedForward"); ASSERT_NE(pass, nullptr); EXPECT_TRUE(pass->hasFixedFunctionState); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front); EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); EXPECT_TRUE(pass->fixedFunctionState.blendEnable); EXPECT_EQ(pass->fixedFunctionState.blendOp, MaterialBlendOp::Max); EXPECT_FLOAT_EQ(pass->fixedFunctionState.depthBiasFactor, 2.0f); EXPECT_EQ(pass->fixedFunctionState.depthBiasUnits, 5); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ForwardLit"); EXPECT_NE(shader->FindVariant("SharedForward", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("SharedForward", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); Array dependencies; ASSERT_TRUE(loader.CollectSourceDependencies(mainShaderPath.string().c_str(), dependencies)); ASSERT_EQ(dependencies.Size(), 1u); EXPECT_EQ( fs::path(dependencies[0].CStr()).lexically_normal(), sharedShaderPath.lexically_normal()); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenUsePassDependencyChanges) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_project_artifact"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path sharedShaderPath = shaderDir / "shared.shader"; const fs::path mainShaderPath = shaderDir / "main.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); auto writeSharedShader = [&](const char* cullMode, const char* marker) { WriteTextFile( sharedShaderPath, std::string(R"(Shader "Project Shared Shader" { SubShader { Pass { Name "SharedForward" Tags { "LightMode" = "ForwardLit" } Cull )") + cullMode + R"( HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return )" + marker + R"(; } ENDHLSL } } } )"); }; writeSharedShader("Back", "float4(1.0, 0.0, 0.0, 1.0)"); WriteTextFile( mainShaderPath, R"(Shader "Project UsePass Shader" { SubShader { UsePass "Project Shared Shader/SharedForward" } } )"); AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset firstResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/main.shader", ResourceType::Shader, firstResolve)); ASSERT_TRUE(firstResolve.artifactReady); ShaderLoader loader; LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr()); ASSERT_TRUE(firstLoad); auto* firstShader = static_cast(firstLoad.resource); ASSERT_NE(firstShader, nullptr); const ShaderPass* firstPass = firstShader->FindPass("SharedForward"); ASSERT_NE(firstPass, nullptr); EXPECT_EQ(firstPass->fixedFunctionState.cullMode, MaterialCullMode::Back); delete firstShader; const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); writeSharedShader("Front", "float4(0.0, 1.0, 0.0, 1.0)"); database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/main.shader", ResourceType::Shader, secondResolve)); ASSERT_TRUE(secondResolve.artifactReady); EXPECT_NE(secondResolve.artifactMainPath, firstArtifactPath); LoadResult secondLoad = loader.Load(secondResolve.artifactMainPath.CStr()); ASSERT_TRUE(secondLoad); auto* secondShader = static_cast(secondLoad.resource); ASSERT_NE(secondShader, nullptr); const ShaderPass* secondPass = secondShader->FindPass("SharedForward"); ASSERT_NE(secondPass, nullptr); EXPECT_EQ(secondPass->fixedFunctionState.cullMode, MaterialCullMode::Front); const ShaderStageVariant* fragmentVariant = secondShader->FindVariant("SharedForward", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(fragmentVariant, nullptr); EXPECT_NE( std::string(fragmentVariant->sourceCode.CStr()).find("float4(0.0, 1.0, 0.0, 1.0)"), std::string::npos); delete secondShader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenBuiltinUsePassDependencyChanges) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path sandboxRoot = fs::temp_directory_path() / "xc_shader_authoring_builtin_usepass_reimport"; const fs::path projectRoot = sandboxRoot / "Project"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path mainShaderPath = shaderDir / "main.shader"; const fs::path builtinShaderAssetPath = sandboxRoot / "engine" / "assets" / "builtin" / "shaders" / "shadow-caster.shader"; const fs::path previousPath = fs::current_path(); fs::remove_all(sandboxRoot); fs::create_directories(shaderDir); fs::create_directories(builtinShaderAssetPath.parent_path()); auto writeBuiltinShader = [&](const char* cullMode, const char* marker) { WriteTextFile( builtinShaderAssetPath, std::string(R"(Shader "Builtin Shadow Caster" { SubShader { Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } Cull )") + cullMode + R"( HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS float4 MainVS() : SV_POSITION { return 0; } float4 MainPS() : SV_TARGET { return )" + marker + R"(; } ENDHLSL } } } )"); }; writeBuiltinShader("Back", "float4(1.0, 0.0, 0.0, 1.0)"); WriteTextFile( mainShaderPath, R"(Shader "Builtin UsePass Dependency Shader" { SubShader { UsePass "Builtin Shadow Caster/ShadowCaster" } } )"); fs::current_path(projectRoot); ShaderLoader dependencyLoader; Array firstDependencies; ASSERT_TRUE(dependencyLoader.CollectSourceDependencies(mainShaderPath.string().c_str(), firstDependencies)); ASSERT_EQ(firstDependencies.Size(), 1u); EXPECT_EQ( fs::path(firstDependencies[0].CStr()).lexically_normal(), builtinShaderAssetPath.lexically_normal()); AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset firstResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/main.shader", ResourceType::Shader, firstResolve)); ASSERT_TRUE(firstResolve.artifactReady); ShaderLoader loader; LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr()); ASSERT_TRUE(firstLoad); auto* firstShader = static_cast(firstLoad.resource); ASSERT_NE(firstShader, nullptr); const ShaderPass* firstPass = firstShader->FindPass("ShadowCaster"); ASSERT_NE(firstPass, nullptr); EXPECT_EQ(firstPass->fixedFunctionState.cullMode, MaterialCullMode::Back); const ShaderStageVariant* firstFragmentVariant = firstShader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(firstFragmentVariant, nullptr); EXPECT_NE( std::string(firstFragmentVariant->sourceCode.CStr()).find("float4(1.0, 0.0, 0.0, 1.0)"), std::string::npos); delete firstShader; const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); writeBuiltinShader("Front", "float4(0.0, 1.0, 0.0, 1.0)"); database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/main.shader", ResourceType::Shader, secondResolve)); ASSERT_TRUE(secondResolve.artifactReady); EXPECT_NE(secondResolve.artifactMainPath, firstArtifactPath); LoadResult secondLoad = loader.Load(secondResolve.artifactMainPath.CStr()); ASSERT_TRUE(secondLoad); auto* secondShader = static_cast(secondLoad.resource); ASSERT_NE(secondShader, nullptr); const ShaderPass* secondPass = secondShader->FindPass("ShadowCaster"); ASSERT_NE(secondPass, nullptr); EXPECT_EQ(secondPass->fixedFunctionState.cullMode, MaterialCullMode::Front); const ShaderStageVariant* secondFragmentVariant = secondShader->FindVariant("ShadowCaster", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(secondFragmentVariant, nullptr); EXPECT_NE( std::string(secondFragmentVariant->sourceCode.CStr()).find("float4(0.0, 1.0, 0.0, 1.0)"), std::string::npos); delete secondShader; database.Shutdown(); fs::current_path(previousPath); fs::remove_all(sandboxRoot); } TEST(ShaderLoader, LoadShaderAuthoringRejectsCyclicProjectUsePass) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_usepass_cycle"; const fs::path shaderAPath = shaderRoot / "a.shader"; const fs::path shaderBPath = shaderRoot / "b.shader"; fs::remove_all(shaderRoot); fs::create_directories(shaderRoot); WriteTextFile( shaderAPath, R"(Shader "Cycle Shader A" { SubShader { UsePass "Cycle Shader B/PassB" } } )"); WriteTextFile( shaderBPath, R"(Shader "Cycle Shader B" { SubShader { UsePass "Cycle Shader A/PassA" } } )"); ShaderLoader loader; LoadResult result = loader.Load(shaderAPath.string().c_str()); EXPECT_FALSE(result.success); EXPECT_EQ(result.resource, nullptr); EXPECT_NE( std::string(result.errorMessage.CStr()).find("cyclic shader reference"), std::string::npos); 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, ResourceManagerLoadsShaderAuthoringRelativeToResourceRoot) { 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_authoring_resource_root"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path shaderPath = shaderDir / "simple.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "SimpleShader" { SubShader { Pass { Name "Unlit" Tags { "LightMode" = "Unlit" } HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS float4 MainVS() : SV_POSITION { return 0; // SIMPLE_AUTHORING_VS } float4 MainPS() : SV_TARGET { return 1; // SIMPLE_AUTHORING_PS } ENDHLSL } } } )"); 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_AUTHORING_VS"), std::string::npos); } fs::current_path(previousPath); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, ResourceManagerLoadsProjectUsePassRelativeToResourceRoot) { 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_usepass_resource_root"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path sharedShaderPath = shaderDir / "shared.shader"; const fs::path mainShaderPath = shaderDir / "main.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( sharedShaderPath, R"(Shader "Project Shared Shader" { SubShader { Pass { Name "SharedForward" Cull Front HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag float4 Vert() : SV_POSITION { return 0; } float4 Frag() : SV_TARGET { return 1; } ENDHLSL } } } )"); WriteTextFile( mainShaderPath, R"(Shader "Project Main Shader" { SubShader { UsePass "Project Shared Shader/SharedForward" } } )"); manager.SetResourceRoot(projectRoot.string().c_str()); fs::current_path(projectRoot.parent_path()); { const ResourceHandle shaderHandle = manager.Load("Assets/Shaders/main.shader"); ASSERT_TRUE(shaderHandle.IsValid()); const ShaderPass* pass = shaderHandle->FindPass("SharedForward"); ASSERT_NE(pass, nullptr); EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front); EXPECT_NE(shaderHandle->FindVariant("SharedForward", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); } fs::current_path(previousPath); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringAndLoaderReadsItBack) { 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 shaderPath = shaderDir / "lit.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "ArtifactShader" { Properties { _MainTex ("Main Tex", 2D) = "white" [Semantic(BaseColorTexture)] } SubShader { Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardLit" } HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS #pragma shader_feature_local _ XC_ALPHA_TEST float4 MainVS() : SV_POSITION { return 0; // ARTIFACT_AUTHORING_VS } float4 MainPS() : SV_TARGET { return 1; // ARTIFACT_AUTHORING_PS } ENDHLSL } } } )"); 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(), shaderPath.lexically_normal().generic_string()); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->variants.Size(), 4u); EXPECT_EQ(pass->resources.Size(), 8u); const ShaderStageVariant* fragmentVariant = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(fragmentVariant, nullptr); EXPECT_NE(std::string(fragmentVariant->sourceCode.CStr()).find("ARTIFACT_AUTHORING_PS"), std::string::npos); ShaderKeywordSet enabledKeywords = {}; 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_AUTHORING_PS"), std::string::npos); EXPECT_NE(std::string(keywordVariant->sourceCode.CStr()).find("#define XC_ALPHA_TEST 1"), std::string::npos); ASSERT_EQ(keywordVariant->requiredKeywords.enabledKeywords.Size(), 1u); EXPECT_EQ(keywordVariant->requiredKeywords.enabledKeywords[0], "XC_ALPHA_TEST"); delete shader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseReimportsLegacyShaderArtifactHeaderBeforeLoad) { namespace fs = std::filesystem; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_legacy_artifact_reimport"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path shaderPath = shaderDir / "lit.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "ArtifactSchemaReimportShader" { SubShader { Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardLit" } HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS float4 MainVS() : SV_POSITION { return 0; } float4 MainPS() : SV_TARGET { return 1; } 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); ASSERT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr())); ShaderArtifactFileHeader legacyHeader = ReadShaderArtifactFileHeader(firstResolve.artifactMainPath.CStr()); std::memcpy(legacyHeader.magic, "XCSHD04", 7); legacyHeader.magic[7] = '\0'; legacyHeader.schemaVersion = 4u; { std::fstream output(firstResolve.artifactMainPath.CStr(), std::ios::binary | std::ios::in | std::ios::out); ASSERT_TRUE(output.is_open()); output.write(reinterpret_cast(&legacyHeader), sizeof(legacyHeader)); ASSERT_TRUE(static_cast(output)); } AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, secondResolve)); EXPECT_TRUE(secondResolve.imported); ASSERT_TRUE(secondResolve.artifactReady); const ShaderArtifactFileHeader currentHeader = ReadShaderArtifactFileHeader(secondResolve.artifactMainPath.CStr()); EXPECT_EQ(std::string(currentHeader.magic, currentHeader.magic + 7), std::string("XCSHD05")); EXPECT_EQ(currentHeader.schemaVersion, kShaderArtifactSchemaVersion); ShaderLoader loader; LoadResult result = loader.Load(secondResolve.artifactMainPath.CStr()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); delete static_cast(result.resource); 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" = "ForwardLit" } 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, AssetDatabaseReimportsShaderWhenAuthoringFileChanges) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_reimport_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path shaderPath = shaderDir / "lit.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile( shaderPath, R"(Shader "DependencyShader" { SubShader { Pass { Name "ForwardLit" HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS float4 MainVS() : SV_POSITION { return 0; } float4 MainPS() : SV_TARGET { return 1; } 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); const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); { std::ofstream shaderFile(shaderPath, std::ios::app); ASSERT_TRUE(shaderFile.is_open()); shaderFile << "\n// force authoring reimport\n"; ASSERT_TRUE(static_cast(shaderFile)); } 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_EQ(pass->resources.Size(), 8u); 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, "ForwardLit"); 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_NE(shader->FindPassResourceBinding("ForwardLit", "PerObjectConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "LightingConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "ShadowReceiverConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"), nullptr); EXPECT_NE(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_1"); 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_1"); 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_1"); 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_1"); 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_1"); 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_1"); 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_EQ(pass->resources.Size(), 4u); 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_NE(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_1"); 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_1"); 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_1"); 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_EQ(pass->resources.Size(), 1u); 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_1"); 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_1"); 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_1"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gModelMatrix"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinSelectionMaskShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinSelectionMaskShaderPath()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); const ShaderPass* alphaTestPass = shader->FindPass("SelectionMask"); ASSERT_NE(alphaTestPass, nullptr); EXPECT_EQ(alphaTestPass->resources.Size(), 4u); ASSERT_EQ(alphaTestPass->keywordDeclarations.Size(), 1u); EXPECT_EQ(alphaTestPass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::ShaderFeatureLocal); ASSERT_EQ(alphaTestPass->keywordDeclarations[0].options.Size(), 2u); EXPECT_EQ(alphaTestPass->keywordDeclarations[0].options[1], "XC_ALPHA_TEST"); 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_EQ(pass->resources.Size(), 4u); 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_NE(shader->FindPassResourceBinding("DepthOnly", "PerObjectConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("DepthOnly", "MaterialConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("DepthOnly", "BaseColorTexture"), nullptr); EXPECT_NE(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_1"); 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_1"); 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_1"); 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_1"); 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_1"); 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_EQ(pass->resources.Size(), 4u); 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_NE(shader->FindPassResourceBinding("ShadowCaster", "PerObjectConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("ShadowCaster", "MaterialConstants"), nullptr); EXPECT_NE(shader->FindPassResourceBinding("ShadowCaster", "BaseColorTexture"), nullptr); EXPECT_NE(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_1"); 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_1"); 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_1"); 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_1"); 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_1"); 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_EQ(pass->resources.Size(), 3u); 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, "FinalColor"); 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_1"); 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_1"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("SourceColorTexture.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_1"); 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_EQ(pass->resources.Size(), 3u); 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, "ColorScale"); 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_1"); 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_1"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("SourceColorTexture.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_1"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("gColorScale"), std::string::npos); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("SourceColorTexture.Sample"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinObjectIdOutlineShaderBuildsAuthoringVariants) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinObjectIdOutlineShaderPath()); 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("ObjectIdOutline"); ASSERT_NE(pass, nullptr); 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_TRUE(pass->fixedFunctionState.blendEnable); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ObjectIdOutline"); EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Vertex, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Vertex, ShaderBackend::Vulkan), nullptr); EXPECT_NE(shader->FindVariant("ObjectIdOutline", ShaderType::Fragment, ShaderBackend::Vulkan), nullptr); const ShaderStageVariant* d3d12Vertex = shader->FindVariant( "ObjectIdOutline", 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_1"); EXPECT_NE( std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_OUTLINE_D3D12_VS"), std::string::npos); const ShaderStageVariant* d3d12Fragment = shader->FindVariant( "ObjectIdOutline", ShaderType::Fragment, ShaderBackend::D3D12); ASSERT_NE(d3d12Fragment, nullptr); const std::string d3d12FragmentSource = d3d12Fragment->sourceCode.CStr(); EXPECT_EQ(d3d12FragmentSource.find("objectIdColor.a <= 0.0"), std::string::npos); EXPECT_NE(d3d12FragmentSource.find("all(abs(objectIdColor) <= float4("), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ObjectIdOutline", 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_1"); EXPECT_NE( std::string(vulkanFragment->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_OUTLINE_D3D12_PS"), 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;"); expectBuiltinShader( GetBuiltinObjectIdOutlineShaderPath(), "ObjectIdOutline", ShaderType::Fragment, ShaderBackend::OpenGL, "XC_BUILTIN_OBJECT_ID_OUTLINE_D3D12_PS"); fs::current_path(previousPath); manager.SetResourceRoot(previousResourceRoot); manager.Shutdown(); fs::remove_all(isolatedWorkingDirectory); } } // namespace