#include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Resources; using namespace XCEngine::Containers; namespace { void WriteTextFile(const std::filesystem::path& path, const std::string& contents) { std::ofstream output(path, std::ios::binary | std::ios::trunc); ASSERT_TRUE(output.is_open()); output << contents; ASSERT_TRUE(static_cast(output)); } TEST(ShaderLoader, GetResourceType) { ShaderLoader loader; EXPECT_EQ(loader.GetResourceType(), ResourceType::Shader); } TEST(ShaderLoader, GetSupportedExtensions) { ShaderLoader loader; auto extensions = loader.GetSupportedExtensions(); EXPECT_GE(extensions.Size(), 1u); } TEST(ShaderLoader, CanLoad) { ShaderLoader loader; EXPECT_TRUE(loader.CanLoad("test.vert")); EXPECT_TRUE(loader.CanLoad("test.frag")); EXPECT_TRUE(loader.CanLoad("test.glsl")); EXPECT_TRUE(loader.CanLoad("test.hlsl")); EXPECT_TRUE(loader.CanLoad("test.xcshader")); EXPECT_TRUE(loader.CanLoad(GetBuiltinForwardLitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinUnlitShaderPath())); EXPECT_TRUE(loader.CanLoad(GetBuiltinObjectIdShaderPath())); EXPECT_FALSE(loader.CanLoad("test.txt")); EXPECT_FALSE(loader.CanLoad("test.png")); } TEST(ShaderLoader, LoadInvalidPath) { ShaderLoader loader; LoadResult result = loader.Load("invalid/path/shader.glsl"); EXPECT_FALSE(result); } TEST(ShaderLoader, LoadLegacySingleStageShaderBuildsDefaultPassVariant) { namespace fs = std::filesystem; const fs::path shaderPath = fs::temp_directory_path() / "xc_shader_loader_stage.vert"; { std::ofstream shaderFile(shaderPath); ASSERT_TRUE(shaderFile.is_open()); shaderFile << "#version 430\nvoid main() {}"; } ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_EQ(shader->GetShaderType(), ShaderType::Vertex); ASSERT_EQ(shader->GetPassCount(), 1u); const ShaderPass* pass = shader->FindPass("Default"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->variants.Size(), 1u); EXPECT_EQ(pass->variants[0].stage, ShaderType::Vertex); EXPECT_EQ(pass->variants[0].backend, ShaderBackend::Generic); EXPECT_EQ(pass->variants[0].sourceCode, shader->GetSourceCode()); delete shader; std::remove(shaderPath.string().c_str()); } TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_manifest_test"; const fs::path stageRoot = shaderRoot / "stages"; const fs::path manifestPath = shaderRoot / "multi_pass.shader"; fs::remove_all(shaderRoot); fs::create_directories(stageRoot); WriteTextFile(stageRoot / "forward_lit.vs.hlsl", "float4 MainVS() : SV_POSITION { return 0; } // FORWARD_LIT_D3D12_VS\n"); WriteTextFile(stageRoot / "forward_lit.ps.hlsl", "float4 MainPS() : SV_TARGET { return 1; } // FORWARD_LIT_D3D12_PS\n"); WriteTextFile(stageRoot / "forward_lit.vert.glsl", "#version 430\n// FORWARD_LIT_GL_VS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.frag.glsl", "#version 430\n// FORWARD_LIT_GL_PS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.vert.vk.glsl", "#version 450\n// FORWARD_LIT_VK_VS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.frag.vk.glsl", "#version 450\n// FORWARD_LIT_VK_PS\nvoid main() {}\n"); WriteTextFile(stageRoot / "depth_only.vs.hlsl", "float4 MainVS() : SV_POSITION { return 0; } // DEPTH_ONLY_D3D12_VS\n"); WriteTextFile(stageRoot / "depth_only.ps.hlsl", "float4 MainPS() : SV_TARGET { return 1; } // DEPTH_ONLY_D3D12_PS\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"TestLitShader\",\n"; manifest << " \"properties\": [\n"; manifest << " {\n"; manifest << " \"name\": \"_BaseColor\",\n"; manifest << " \"displayName\": \"Base Color\",\n"; manifest << " \"type\": \"Color\",\n"; manifest << " \"defaultValue\": \"(1,1,1,1)\",\n"; manifest << " \"semantic\": \"BaseColor\"\n"; manifest << " },\n"; manifest << " {\n"; manifest << " \"name\": \"_MainTex\",\n"; manifest << " \"displayName\": \"Base Map\",\n"; manifest << " \"type\": \"2D\",\n"; manifest << " \"defaultValue\": \"white\",\n"; manifest << " \"semantic\": \"BaseColorTexture\"\n"; manifest << " }\n"; manifest << " ],\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"tags\": {\n"; manifest << " \"LightMode\": \"ForwardBase\",\n"; manifest << " \"Queue\": \"Geometry\"\n"; manifest << " },\n"; manifest << " \"resources\": [\n"; manifest << " { \"name\": \"PerObjectConstants\", \"type\": \"ConstantBuffer\", \"set\": 1, \"binding\": 0, \"semantic\": \"PerObject\" },\n"; manifest << " { \"name\": \"MaterialConstants\", \"type\": \"ConstantBuffer\", \"set\": 2, \"binding\": 0, \"semantic\": \"Material\" },\n"; manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 3, \"binding\": 0, \"semantic\": \"BaseColorTexture\" },\n"; manifest << " { \"name\": \"LinearClampSampler\", \"type\": \"Sampler\", \"set\": 4, \"binding\": 0, \"semantic\": \"LinearClampSampler\" }\n"; manifest << " ],\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.vs.hlsl\", \"entryPoint\": \"MainVS\", \"profile\": \"vs_5_0\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.ps.hlsl\", \"entryPoint\": \"MainPS\", \"profile\": \"ps_5_0\" },\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.frag.glsl\" },\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"Vulkan\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.vert.vk.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"Vulkan\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.frag.vk.glsl\" }\n"; manifest << " ]\n"; manifest << " },\n"; manifest << " {\n"; manifest << " \"name\": \"DepthOnly\",\n"; manifest << " \"tags\": {\n"; manifest << " \"LightMode\": \"DepthOnly\"\n"; manifest << " },\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/depth_only.vs.hlsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/depth_only.ps.hlsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } ShaderLoader loader; LoadResult result = loader.Load(manifestPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); Shader* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); ASSERT_TRUE(shader->IsValid()); EXPECT_EQ(shader->GetName(), "TestLitShader"); ASSERT_EQ(shader->GetProperties().Size(), 2u); ASSERT_EQ(shader->GetPassCount(), 2u); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->displayName, "Base Color"); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->defaultValue, "(1,1,1,1)"); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->defaultValue, "white"); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); const ShaderPass* forwardLitPass = shader->FindPass("ForwardLit"); ASSERT_NE(forwardLitPass, nullptr); ASSERT_EQ(forwardLitPass->tags.Size(), 2u); ASSERT_EQ(forwardLitPass->resources.Size(), 4u); EXPECT_EQ(forwardLitPass->tags[0].name, "LightMode"); EXPECT_EQ(forwardLitPass->tags[0].value, "ForwardBase"); EXPECT_EQ(forwardLitPass->tags[1].name, "Queue"); EXPECT_EQ(forwardLitPass->tags[1].value, "Geometry"); const ShaderResourceBindingDesc* baseTextureBinding = shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"); ASSERT_NE(baseTextureBinding, nullptr); EXPECT_EQ(baseTextureBinding->type, ShaderResourceType::Texture2D); EXPECT_EQ(baseTextureBinding->set, 3u); EXPECT_EQ(baseTextureBinding->binding, 0u); EXPECT_EQ(baseTextureBinding->semantic, "BaseColorTexture"); const ShaderStageVariant* d3d12Vertex = shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("FORWARD_LIT_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->entryPoint, "main"); EXPECT_EQ(openglFragment->profile, "fs_4_30"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("FORWARD_LIT_GL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->profile, "fs_4_50"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("FORWARD_LIT_VK_PS"), std::string::npos); const ShaderPass* depthOnlyPass = shader->FindPass("DepthOnly"); ASSERT_NE(depthOnlyPass, nullptr); ASSERT_EQ(depthOnlyPass->tags.Size(), 1u); EXPECT_EQ(depthOnlyPass->tags[0].value, "DepthOnly"); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_EQ(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL), nullptr); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, LoadUnityLikeShaderAuthoringBuildsRuntimeContract) { namespace fs = std::filesystem; const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_test"; const fs::path stageRoot = shaderRoot / "stages"; const fs::path shaderPath = shaderRoot / "multi_pass.shader"; fs::remove_all(shaderRoot); fs::create_directories(stageRoot); WriteTextFile(stageRoot / "forward_lit.vs.hlsl", "float4 MainVS() : SV_POSITION { return 0; } // AUTHORING_FORWARD_LIT_D3D12_VS\n"); WriteTextFile(stageRoot / "forward_lit.ps.hlsl", "float4 MainPS() : SV_TARGET { return 1; } // AUTHORING_FORWARD_LIT_D3D12_PS\n"); WriteTextFile(stageRoot / "forward_lit.vert.glsl", "#version 430\n// AUTHORING_FORWARD_LIT_GL_VS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.frag.glsl", "#version 430\n// AUTHORING_FORWARD_LIT_GL_PS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.vert.vk.glsl", "#version 450\n// AUTHORING_FORWARD_LIT_VK_VS\nvoid main() {}\n"); WriteTextFile(stageRoot / "forward_lit.frag.vk.glsl", "#version 450\n// AUTHORING_FORWARD_LIT_VK_PS\nvoid main() {}\n"); WriteTextFile(stageRoot / "depth_only.vs.hlsl", "float4 MainVS() : SV_POSITION { return 0; } // AUTHORING_DEPTH_ONLY_D3D12_VS\n"); WriteTextFile(stageRoot / "depth_only.ps.hlsl", "float4 MainPS() : SV_TARGET { return 1; } // AUTHORING_DEPTH_ONLY_D3D12_PS\n"); WriteTextFile( shaderPath, R"(Shader "AuthoringLit" { Properties { _BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)] _MainTex ("Base Map", 2D) = "white" [Semantic(BaseColorTexture)] } SubShader { Tags { "Queue" = "Geometry" } Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardBase" } Resources { PerObjectConstants (ConstantBuffer, 1, 0) [Semantic(PerObject)] MaterialConstants (ConstantBuffer, 2, 0) [Semantic(Material)] BaseColorTexture (Texture2D, 3, 0) [Semantic(BaseColorTexture)] LinearClampSampler (Sampler, 4, 0) [Semantic(LinearClampSampler)] } HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS #pragma backend D3D12 HLSL "stages/forward_lit.vs.hlsl" "stages/forward_lit.ps.hlsl" vs_5_0 ps_5_0 #pragma backend OpenGL GLSL "stages/forward_lit.vert.glsl" "stages/forward_lit.frag.glsl" #pragma backend Vulkan GLSL "stages/forward_lit.vert.vk.glsl" "stages/forward_lit.frag.vk.glsl" ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS #pragma backend D3D12 HLSL "stages/depth_only.vs.hlsl" "stages/depth_only.ps.hlsl" 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(), 2u); ASSERT_EQ(shader->GetPassCount(), 2u); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->displayName, "Base Color"); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->defaultValue, "(1,1,1,1)"); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->defaultValue, "white"); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); const ShaderPass* forwardLitPass = shader->FindPass("ForwardLit"); ASSERT_NE(forwardLitPass, nullptr); ASSERT_EQ(forwardLitPass->tags.Size(), 2u); ASSERT_EQ(forwardLitPass->resources.Size(), 4u); EXPECT_EQ(forwardLitPass->tags[0].name, "Queue"); EXPECT_EQ(forwardLitPass->tags[0].value, "Geometry"); EXPECT_EQ(forwardLitPass->tags[1].name, "LightMode"); EXPECT_EQ(forwardLitPass->tags[1].value, "ForwardBase"); const ShaderResourceBindingDesc* baseTextureBinding = shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"); ASSERT_NE(baseTextureBinding, nullptr); EXPECT_EQ(baseTextureBinding->type, ShaderResourceType::Texture2D); EXPECT_EQ(baseTextureBinding->set, 3u); EXPECT_EQ(baseTextureBinding->binding, 0u); EXPECT_EQ(baseTextureBinding->semantic, "BaseColorTexture"); const ShaderStageVariant* d3d12Vertex = shader->FindVariant("ForwardLit", ShaderType::Vertex, ShaderBackend::D3D12); ASSERT_NE(d3d12Vertex, nullptr); EXPECT_EQ(d3d12Vertex->entryPoint, "MainVS"); EXPECT_EQ(d3d12Vertex->profile, "vs_5_0"); EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("AUTHORING_FORWARD_LIT_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_EQ(openglFragment->entryPoint, "MainPS"); EXPECT_EQ(openglFragment->profile, "fs_4_30"); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("AUTHORING_FORWARD_LIT_GL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_EQ(vulkanFragment->profile, "fs_4_50"); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("AUTHORING_FORWARD_LIT_VK_PS"), std::string::npos); const ShaderPass* depthOnlyPass = shader->FindPass("DepthOnly"); ASSERT_NE(depthOnlyPass, nullptr); ASSERT_EQ(depthOnlyPass->tags.Size(), 2u); EXPECT_EQ(depthOnlyPass->tags[0].name, "Queue"); EXPECT_EQ(depthOnlyPass->tags[0].value, "Geometry"); EXPECT_EQ(depthOnlyPass->tags[1].name, "LightMode"); EXPECT_EQ(depthOnlyPass->tags[1].value, "DepthOnly"); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); delete shader; fs::remove_all(shaderRoot); } TEST(ShaderLoader, ResourceManagerLoadsShaderManifestRelativeToResourceRoot) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path previousPath = fs::current_path(); const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_manifest_resource_root"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path manifestPath = shaderDir / "simple.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile(shaderDir / "simple.vert.glsl", "#version 430\n// SIMPLE_GL_VS\nvoid main() {}\n"); WriteTextFile(shaderDir / "simple.frag.glsl", "#version 430\n// SIMPLE_GL_PS\nvoid main() {}\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"SimpleShader\",\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"Unlit\",\n"; manifest << " \"tags\": { \"LightMode\": \"Unlit\" },\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"simple.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"simple.frag.glsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } manager.SetResourceRoot(projectRoot.string().c_str()); fs::current_path(projectRoot.parent_path()); { const ResourceHandle shaderHandle = manager.Load("Assets/Shaders/simple.shader"); ASSERT_TRUE(shaderHandle.IsValid()); EXPECT_EQ(shaderHandle->GetName(), "SimpleShader"); const ShaderStageVariant* vertexVariant = shaderHandle->FindVariant("Unlit", ShaderType::Vertex, ShaderBackend::OpenGL); ASSERT_NE(vertexVariant, nullptr); EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("SIMPLE_GL_VS"), std::string::npos); } fs::current_path(previousPath); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { namespace fs = std::filesystem; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_artifact_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path manifestPath = shaderDir / "lit.shader"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile(shaderDir / "lit.vert.glsl", "#version 430\n// ARTIFACT_GL_VS\nvoid main() {}\n"); WriteTextFile(shaderDir / "lit.frag.glsl", "#version 430\n// ARTIFACT_GL_PS\nvoid main() {}\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"ArtifactShader\",\n"; manifest << " \"properties\": [\n"; manifest << " {\n"; manifest << " \"name\": \"_MainTex\",\n"; manifest << " \"displayName\": \"Main Tex\",\n"; manifest << " \"type\": \"2D\",\n"; manifest << " \"defaultValue\": \"white\",\n"; manifest << " \"semantic\": \"BaseColorTexture\"\n"; manifest << " }\n"; manifest << " ],\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"tags\": { \"LightMode\": \"ForwardBase\" },\n"; manifest << " \"resources\": [\n"; manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 3, \"binding\": 0, \"semantic\": \"BaseColorTexture\" }\n"; manifest << " ],\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset resolvedAsset; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, resolvedAsset)); ASSERT_TRUE(resolvedAsset.artifactReady); EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcshader"); EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr())); ShaderLoader loader; LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* shader = static_cast(result.resource); ASSERT_NE(shader, nullptr); EXPECT_TRUE(shader->IsValid()); EXPECT_EQ(shader->GetName(), "ArtifactShader"); EXPECT_EQ( fs::path(shader->GetPath().CStr()).lexically_normal().generic_string(), manifestPath.lexically_normal().generic_string()); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); ASSERT_EQ(pass->variants.Size(), 2u); ASSERT_EQ(pass->resources.Size(), 1u); const ShaderStageVariant* fragmentVariant = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(fragmentVariant, nullptr); EXPECT_NE(std::string(fragmentVariant->sourceCode.CStr()).find("ARTIFACT_GL_PS"), std::string::npos); delete shader; database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromUnityLikeAuthoringAndTracksStageDependencies) { 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 stageDir = shaderDir / "stages"; const fs::path shaderPath = shaderDir / "lit.shader"; const fs::path fragmentPath = stageDir / "lit.frag.glsl"; fs::remove_all(projectRoot); fs::create_directories(stageDir); WriteTextFile(stageDir / "lit.vert.glsl", "#version 430\n// AUTHORING_ARTIFACT_GL_VS\nvoid main() {}\n"); WriteTextFile(fragmentPath, "#version 430\n// AUTHORING_ARTIFACT_GL_PS\nvoid main() {}\n"); WriteTextFile( shaderPath, R"(Shader "ArtifactAuthoringShader" { Properties { _MainTex ("Main Tex", 2D) = "white" [Semantic(BaseColorTexture)] } SubShader { Pass { Name "ForwardLit" Tags { "LightMode" = "ForwardBase" } Resources { BaseColorTexture (Texture2D, 3, 0) [Semantic(BaseColorTexture)] } HLSLPROGRAM #pragma vertex main #pragma fragment main #pragma backend OpenGL GLSL "stages/lit.vert.glsl" "stages/lit.frag.glsl" 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::OpenGL); ASSERT_NE(firstFragment, nullptr); EXPECT_NE(std::string(firstFragment->sourceCode.CStr()).find("AUTHORING_ARTIFACT_GL_PS"), std::string::npos); delete firstShader; const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); { std::ofstream fragmentFile(fragmentPath, std::ios::app); ASSERT_TRUE(fragmentFile.is_open()); fragmentFile << "\n// force authoring dependency reimport\n"; ASSERT_TRUE(static_cast(fragmentFile)); } database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, secondResolve)); ASSERT_TRUE(secondResolve.artifactReady); EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath); EXPECT_TRUE(fs::exists(secondResolve.artifactMainPath.CStr())); database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenStageDependencyChanges) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_dependency_reimport_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; const fs::path manifestPath = shaderDir / "lit.shader"; const fs::path fragmentPath = shaderDir / "lit.frag.glsl"; fs::remove_all(projectRoot); fs::create_directories(shaderDir); WriteTextFile(shaderDir / "lit.vert.glsl", "#version 430\nvoid main() {}\n"); WriteTextFile(fragmentPath, "#version 430\nvoid main() {}\n"); { std::ofstream manifest(manifestPath); ASSERT_TRUE(manifest.is_open()); manifest << "{\n"; manifest << " \"name\": \"DependencyShader\",\n"; manifest << " \"passes\": [\n"; manifest << " {\n"; manifest << " \"name\": \"ForwardLit\",\n"; manifest << " \"variants\": [\n"; manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" }\n"; manifest << " ]\n"; manifest << " }\n"; manifest << " ]\n"; manifest << "}\n"; } AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset firstResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, firstResolve)); ASSERT_TRUE(firstResolve.artifactReady); const String firstArtifactPath = firstResolve.artifactMainPath; database.Shutdown(); std::this_thread::sleep_for(50ms); { std::ofstream fragmentFile(fragmentPath, std::ios::app); ASSERT_TRUE(fragmentFile.is_open()); fragmentFile << "\n// force dependency reimport\n"; ASSERT_TRUE(static_cast(fragmentFile)); } database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, secondResolve)); ASSERT_TRUE(secondResolve.artifactReady); EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath); EXPECT_TRUE(fs::exists(secondResolve.artifactMainPath.CStr())); database.Shutdown(); fs::remove_all(projectRoot); } TEST(ShaderLoader, LoadBuiltinForwardLitShaderBuildsBackendVariants) { 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(), 2u); ASSERT_EQ(pass->variants.Size(), 6u); ASSERT_EQ(pass->tags.Size(), 1u); ASSERT_EQ(pass->resources.Size(), 4u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ForwardBase"); const ShaderPropertyDesc* baseColorProperty = shader->FindProperty("_BaseColor"); ASSERT_NE(baseColorProperty, nullptr); EXPECT_EQ(baseColorProperty->type, ShaderPropertyType::Color); EXPECT_EQ(baseColorProperty->semantic, "BaseColor"); const ShaderPropertyDesc* baseMapProperty = shader->FindProperty("_MainTex"); ASSERT_NE(baseMapProperty, nullptr); EXPECT_EQ(baseMapProperty->type, ShaderPropertyType::Texture2D); EXPECT_EQ(baseMapProperty->semantic, "BaseColorTexture"); const ShaderResourceBindingDesc* perObjectBinding = shader->FindPassResourceBinding("ForwardLit", "PerObjectConstants"); ASSERT_NE(perObjectBinding, nullptr); EXPECT_EQ(perObjectBinding->type, ShaderResourceType::ConstantBuffer); EXPECT_EQ(perObjectBinding->set, 1u); EXPECT_EQ(perObjectBinding->binding, 0u); EXPECT_EQ(perObjectBinding->semantic, "PerObject"); 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_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_FORWARD_LIT_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("XC_BUILTIN_FORWARD_LIT_OPENGL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ForwardLit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("XC_BUILTIN_FORWARD_LIT_VULKAN_PS"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinUnlitShaderBuildsBackendVariants) { 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(), 6u); ASSERT_EQ(pass->tags.Size(), 1u); ASSERT_EQ(pass->resources.Size(), 4u); 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"); const ShaderResourceBindingDesc* perObjectBinding = shader->FindPassResourceBinding("Unlit", "PerObjectConstants"); ASSERT_NE(perObjectBinding, nullptr); EXPECT_EQ(perObjectBinding->type, ShaderResourceType::ConstantBuffer); EXPECT_EQ(perObjectBinding->set, 1u); EXPECT_EQ(perObjectBinding->binding, 0u); EXPECT_EQ(perObjectBinding->semantic, "PerObject"); 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_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_UNLIT_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "Unlit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("XC_BUILTIN_UNLIT_OPENGL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "Unlit", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("XC_BUILTIN_UNLIT_VULKAN_PS"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinObjectIdShaderBuildsBackendVariants) { 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); ASSERT_EQ(pass->resources.Size(), 1u); EXPECT_EQ(pass->resources[0].semantic, "PerObject"); EXPECT_EQ(pass->resources[0].type, ShaderResourceType::ConstantBuffer); EXPECT_EQ(pass->resources[0].set, 0u); EXPECT_EQ(pass->resources[0].binding, 0u); ASSERT_EQ(pass->variants.Size(), 6u); 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_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ObjectId", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_OPENGL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ObjectId", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_VULKAN_PS"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinDepthOnlyShaderBuildsBackendVariants) { 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(pass->resources.Size(), 1u); EXPECT_EQ(pass->resources[0].semantic, "PerObject"); EXPECT_EQ(pass->resources[0].type, ShaderResourceType::ConstantBuffer); EXPECT_EQ(pass->resources[0].set, 0u); EXPECT_EQ(pass->resources[0].binding, 0u); ASSERT_EQ(pass->variants.Size(), 6u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "DepthOnly"); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Vertex, ShaderBackend::D3D12), nullptr); EXPECT_NE(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::D3D12), nullptr); EXPECT_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_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_DEPTH_ONLY_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("XC_BUILTIN_DEPTH_ONLY_OPENGL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "DepthOnly", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("XC_BUILTIN_DEPTH_ONLY_VULKAN_PS"), std::string::npos); delete shader; } TEST(ShaderLoader, LoadBuiltinShadowCasterShaderBuildsBackendVariants) { 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(pass->resources.Size(), 1u); EXPECT_EQ(pass->resources[0].semantic, "PerObject"); EXPECT_EQ(pass->resources[0].type, ShaderResourceType::ConstantBuffer); EXPECT_EQ(pass->resources[0].set, 0u); EXPECT_EQ(pass->resources[0].binding, 0u); ASSERT_EQ(pass->variants.Size(), 6u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode"); EXPECT_EQ(pass->tags[0].value, "ShadowCaster"); 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_NE( std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_SHADOW_CASTER_D3D12_VS"), std::string::npos); const ShaderStageVariant* openglFragment = shader->FindVariant( "ShadowCaster", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(openglFragment, nullptr); EXPECT_NE( std::string(openglFragment->sourceCode.CStr()).find("XC_BUILTIN_SHADOW_CASTER_OPENGL_PS"), std::string::npos); const ShaderStageVariant* vulkanFragment = shader->FindVariant( "ShadowCaster", ShaderType::Fragment, ShaderBackend::Vulkan); ASSERT_NE(vulkanFragment, nullptr); EXPECT_NE( std::string(vulkanFragment->sourceCode.CStr()).find("XC_BUILTIN_SHADOW_CASTER_VULKAN_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, "XC_BUILTIN_FORWARD_LIT_D3D12_VS"); expectBuiltinShader( GetBuiltinUnlitShaderPath(), "Unlit", ShaderType::Fragment, ShaderBackend::OpenGL, "XC_BUILTIN_UNLIT_OPENGL_PS"); expectBuiltinShader( GetBuiltinObjectIdShaderPath(), "ObjectId", ShaderType::Fragment, ShaderBackend::Vulkan, "XC_BUILTIN_OBJECT_ID_VULKAN_PS"); fs::current_path(previousPath); manager.SetResourceRoot(previousResourceRoot); manager.Shutdown(); fs::remove_all(isolatedWorkingDirectory); } } // namespace