From 2f9b1696cd0d3d883967a7226ddd9d14cf9dad43 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 18:37:11 +0800 Subject: [PATCH] Close shader authoring usepass regressions --- .../XCEngine/Resources/BuiltinResources.h | 4 + engine/src/Resources/BuiltinResources.cpp | 148 ++- .../ShaderAuthoringDirectiveUtils.cpp | 282 ++++- .../Shader/Internal/ShaderAuthoringLoader.cpp | 102 +- .../Internal/ShaderAuthoringParserCore.cpp | 406 ++++--- .../Internal/ShaderRuntimeBuildUtils.cpp | 444 +++++-- .../Shader/Internal/ShaderRuntimeBuildUtils.h | 8 +- engine/src/Resources/Shader/ShaderIR.h | 3 + tests/Rendering/unit/CMakeLists.txt | 1 + tests/Resources/Material/CMakeLists.txt | 1 + tests/Resources/Shader/CMakeLists.txt | 1 + tests/Resources/Shader/test_shader_loader.cpp | 1055 +++++++++++++---- 12 files changed, 1908 insertions(+), 547 deletions(-) diff --git a/engine/include/XCEngine/Resources/BuiltinResources.h b/engine/include/XCEngine/Resources/BuiltinResources.h index 4e121450..00813f0d 100644 --- a/engine/include/XCEngine/Resources/BuiltinResources.h +++ b/engine/include/XCEngine/Resources/BuiltinResources.h @@ -19,6 +19,9 @@ bool IsBuiltinMeshPath(const Containers::String& path); bool IsBuiltinMaterialPath(const Containers::String& path); bool IsBuiltinShaderPath(const Containers::String& path); bool IsBuiltinTexturePath(const Containers::String& path); +bool TryGetBuiltinShaderPathByShaderName( + const Containers::String& shaderName, + Containers::String& outPath); const char* GetBuiltinPrimitiveDisplayName(BuiltinPrimitiveType primitiveType); Containers::String GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType primitiveType); @@ -28,6 +31,7 @@ Containers::String GetBuiltinUnlitShaderPath(); Containers::String GetBuiltinDepthOnlyShaderPath(); Containers::String GetBuiltinShadowCasterShaderPath(); Containers::String GetBuiltinObjectIdShaderPath(); +Containers::String GetBuiltinObjectIdOutlineShaderPath(); Containers::String GetBuiltinSkyboxShaderPath(); Containers::String GetBuiltinColorScalePostProcessShaderPath(); Containers::String GetBuiltinFinalColorShaderPath(); diff --git a/engine/src/Resources/BuiltinResources.cpp b/engine/src/Resources/BuiltinResources.cpp index 8823a18e..66e49447 100644 --- a/engine/src/Resources/BuiltinResources.cpp +++ b/engine/src/Resources/BuiltinResources.cpp @@ -32,6 +32,7 @@ constexpr const char* kBuiltinUnlitShaderPath = "builtin://shaders/unlit"; constexpr const char* kBuiltinDepthOnlyShaderPath = "builtin://shaders/depth-only"; constexpr const char* kBuiltinShadowCasterShaderPath = "builtin://shaders/shadow-caster"; constexpr const char* kBuiltinObjectIdShaderPath = "builtin://shaders/object-id"; +constexpr const char* kBuiltinObjectIdOutlineShaderPath = "builtin://shaders/object-id-outline"; constexpr const char* kBuiltinSkyboxShaderPath = "builtin://shaders/skybox"; constexpr const char* kBuiltinColorScalePostProcessShaderPath = "builtin://shaders/color-scale-post-process"; @@ -46,22 +47,24 @@ struct MeshBuffers { size_t CalculateBuiltinShaderMemorySize(const Shader& shader); -constexpr const char* kBuiltinForwardLitShaderManifestRelativePath = - "engine/assets/builtin/shaders/forward-lit/forward-lit.shader"; -constexpr const char* kBuiltinUnlitShaderManifestRelativePath = - "engine/assets/builtin/shaders/unlit/unlit.shader"; -constexpr const char* kBuiltinDepthOnlyShaderManifestRelativePath = - "engine/assets/builtin/shaders/depth-only/depth-only.shader"; -constexpr const char* kBuiltinShadowCasterShaderManifestRelativePath = - "engine/assets/builtin/shaders/shadow-caster/shadow-caster.shader"; -constexpr const char* kBuiltinObjectIdShaderManifestRelativePath = - "engine/assets/builtin/shaders/object-id/object-id.shader"; -constexpr const char* kBuiltinSkyboxShaderManifestRelativePath = - "engine/assets/builtin/shaders/skybox/skybox.shader"; -constexpr const char* kBuiltinColorScalePostProcessShaderManifestRelativePath = - "engine/assets/builtin/shaders/color-scale-post-process/color-scale-post-process.shader"; -constexpr const char* kBuiltinFinalColorShaderManifestRelativePath = - "engine/assets/builtin/shaders/final-color/final-color.shader"; +constexpr const char* kBuiltinForwardLitShaderAssetRelativePath = + "engine/assets/builtin/shaders/forward-lit.shader"; +constexpr const char* kBuiltinUnlitShaderAssetRelativePath = + "engine/assets/builtin/shaders/unlit.shader"; +constexpr const char* kBuiltinDepthOnlyShaderAssetRelativePath = + "engine/assets/builtin/shaders/depth-only.shader"; +constexpr const char* kBuiltinShadowCasterShaderAssetRelativePath = + "engine/assets/builtin/shaders/shadow-caster.shader"; +constexpr const char* kBuiltinObjectIdShaderAssetRelativePath = + "engine/assets/builtin/shaders/object-id.shader"; +constexpr const char* kBuiltinObjectIdOutlineShaderAssetRelativePath = + "engine/assets/builtin/shaders/object-id-outline.shader"; +constexpr const char* kBuiltinSkyboxShaderAssetRelativePath = + "engine/assets/builtin/shaders/skybox.shader"; +constexpr const char* kBuiltinColorScalePostProcessShaderAssetRelativePath = + "engine/assets/builtin/shaders/color-scale-post-process.shader"; +constexpr const char* kBuiltinFinalColorShaderAssetRelativePath = + "engine/assets/builtin/shaders/final-color.shader"; Containers::String NormalizeBuiltinAssetPath(const std::filesystem::path& path) { return Containers::String(path.lexically_normal().generic_string().c_str()); @@ -98,7 +101,7 @@ bool TryResolveBuiltinAssetPathFromAnchor( return false; } -bool TryResolveBuiltinShaderManifestPath( +bool TryResolveBuiltinShaderAssetPath( const std::filesystem::path& relativePath, Containers::String& outPath) { std::filesystem::path resolvedPath; @@ -124,51 +127,54 @@ bool TryResolveBuiltinShaderManifestPath( return false; } -const char* GetBuiltinShaderManifestRelativePath(const Containers::String& builtinShaderPath) { +const char* GetBuiltinShaderAssetRelativePath(const Containers::String& builtinShaderPath) { if (builtinShaderPath == Containers::String(kBuiltinForwardLitShaderPath)) { - return kBuiltinForwardLitShaderManifestRelativePath; + return kBuiltinForwardLitShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinUnlitShaderPath)) { - return kBuiltinUnlitShaderManifestRelativePath; + return kBuiltinUnlitShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinDepthOnlyShaderPath)) { - return kBuiltinDepthOnlyShaderManifestRelativePath; + return kBuiltinDepthOnlyShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinShadowCasterShaderPath)) { - return kBuiltinShadowCasterShaderManifestRelativePath; + return kBuiltinShadowCasterShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinObjectIdShaderPath)) { - return kBuiltinObjectIdShaderManifestRelativePath; + return kBuiltinObjectIdShaderAssetRelativePath; + } + if (builtinShaderPath == Containers::String(kBuiltinObjectIdOutlineShaderPath)) { + return kBuiltinObjectIdOutlineShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinSkyboxShaderPath)) { - return kBuiltinSkyboxShaderManifestRelativePath; + return kBuiltinSkyboxShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinColorScalePostProcessShaderPath)) { - return kBuiltinColorScalePostProcessShaderManifestRelativePath; + return kBuiltinColorScalePostProcessShaderAssetRelativePath; } if (builtinShaderPath == Containers::String(kBuiltinFinalColorShaderPath)) { - return kBuiltinFinalColorShaderManifestRelativePath; + return kBuiltinFinalColorShaderAssetRelativePath; } return nullptr; } -bool TryResolveBuiltinShaderManifestPath( +bool TryResolveBuiltinShaderAssetPath( const Containers::String& builtinShaderPath, Containers::String& outPath) { - const char* relativePath = GetBuiltinShaderManifestRelativePath(builtinShaderPath); + const char* relativePath = GetBuiltinShaderAssetRelativePath(builtinShaderPath); if (relativePath == nullptr) { return false; } - return TryResolveBuiltinShaderManifestPath(std::filesystem::path(relativePath), outPath); + return TryResolveBuiltinShaderAssetPath(std::filesystem::path(relativePath), outPath); } -Shader* LoadBuiltinShaderFromManifest( +Shader* LoadBuiltinShaderFromAsset( const Containers::String& builtinPath, - const Containers::String& manifestPath) { + const Containers::String& assetPath) { ShaderLoader shaderLoader; - LoadResult result = shaderLoader.Load(manifestPath); + LoadResult result = shaderLoader.Load(assetPath); if (!result || result.resource == nullptr) { return nullptr; } @@ -180,13 +186,13 @@ Shader* LoadBuiltinShaderFromManifest( return shader; } -Shader* TryLoadBuiltinShaderFromManifest(const Containers::String& builtinPath) { - Containers::String manifestPath; - if (!TryResolveBuiltinShaderManifestPath(builtinPath, manifestPath)) { +Shader* TryLoadBuiltinShaderFromAsset(const Containers::String& builtinPath) { + Containers::String assetPath; + if (!TryResolveBuiltinShaderAssetPath(builtinPath, assetPath)) { return nullptr; } - return LoadBuiltinShaderFromManifest(builtinPath, manifestPath); + return LoadBuiltinShaderFromAsset(builtinPath, assetPath); } Math::Bounds ComputeBounds(const std::vector& vertices) { @@ -675,35 +681,39 @@ size_t CalculateBuiltinShaderMemorySize(const Shader& shader) { } Shader* BuildBuiltinForwardLitShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinUnlitShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinDepthOnlyShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinShadowCasterShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinObjectIdShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); +} + +Shader* BuildBuiltinObjectIdOutlineShader(const Containers::String& path) { + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinSkyboxShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinColorScalePostProcessShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Shader* BuildBuiltinFinalColorShader(const Containers::String& path) { - return TryLoadBuiltinShaderFromManifest(path); + return TryLoadBuiltinShaderFromAsset(path); } Material* BuildDefaultPrimitiveMaterial(const Containers::String& path) { @@ -776,6 +786,50 @@ bool IsBuiltinTexturePath(const Containers::String& path) { return path.StartsWith(kBuiltinTexturePrefix); } +bool TryGetBuiltinShaderPathByShaderName( + const Containers::String& shaderName, + Containers::String& outPath) { + if (shaderName == "Builtin Forward Lit") { + outPath = GetBuiltinForwardLitShaderPath(); + return true; + } + if (shaderName == "Builtin Unlit") { + outPath = GetBuiltinUnlitShaderPath(); + return true; + } + if (shaderName == "Builtin Depth Only") { + outPath = GetBuiltinDepthOnlyShaderPath(); + return true; + } + if (shaderName == "Builtin Shadow Caster") { + outPath = GetBuiltinShadowCasterShaderPath(); + return true; + } + if (shaderName == "Builtin Object Id") { + outPath = GetBuiltinObjectIdShaderPath(); + return true; + } + if (shaderName == "Builtin Object Id Outline") { + outPath = GetBuiltinObjectIdOutlineShaderPath(); + return true; + } + if (shaderName == "Builtin Skybox") { + outPath = GetBuiltinSkyboxShaderPath(); + return true; + } + if (shaderName == "Builtin Color Scale Post Process") { + outPath = GetBuiltinColorScalePostProcessShaderPath(); + return true; + } + if (shaderName == "Builtin Final Color") { + outPath = GetBuiltinFinalColorShaderPath(); + return true; + } + + outPath.Clear(); + return false; +} + const char* GetBuiltinPrimitiveDisplayName(BuiltinPrimitiveType primitiveType) { switch (primitiveType) { case BuiltinPrimitiveType::Cube: return "Cube"; @@ -824,6 +878,10 @@ Containers::String GetBuiltinObjectIdShaderPath() { return Containers::String(kBuiltinObjectIdShaderPath); } +Containers::String GetBuiltinObjectIdOutlineShaderPath() { + return Containers::String(kBuiltinObjectIdOutlineShaderPath); +} + Containers::String GetBuiltinSkyboxShaderPath() { return Containers::String(kBuiltinSkyboxShaderPath); } @@ -934,6 +992,8 @@ LoadResult CreateBuiltinShaderResource(const Containers::String& path) { shader = BuildBuiltinShadowCasterShader(path); } else if (path == GetBuiltinObjectIdShaderPath()) { shader = BuildBuiltinObjectIdShader(path); + } else if (path == GetBuiltinObjectIdOutlineShaderPath()) { + shader = BuildBuiltinObjectIdOutlineShader(path); } else if (path == GetBuiltinSkyboxShaderPath()) { shader = BuildBuiltinSkyboxShader(path); } else if (path == GetBuiltinColorScalePostProcessShaderPath()) { diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringDirectiveUtils.cpp b/engine/src/Resources/Shader/Internal/ShaderAuthoringDirectiveUtils.cpp index 07e55fb4..25e1343a 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringDirectiveUtils.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringDirectiveUtils.cpp @@ -4,6 +4,47 @@ namespace XCEngine { namespace Resources { namespace Internal { +namespace { + +std::vector NormalizeDirectiveTokens( + const std::vector& tokens, + size_t startIndex) { + std::vector normalizedTokens; + normalizedTokens.reserve(tokens.size() > startIndex ? tokens.size() - startIndex : 0u); + for (size_t tokenIndex = startIndex; tokenIndex < tokens.size(); ++tokenIndex) { + if (tokens[tokenIndex] == ",") { + continue; + } + + std::string normalizedToken = tokens[tokenIndex]; + while (!normalizedToken.empty() && normalizedToken.back() == ',') { + normalizedToken.pop_back(); + } + + if (!normalizedToken.empty()) { + normalizedTokens.push_back(std::move(normalizedToken)); + } + } + + return normalizedTokens; +} + +bool TryParseUInt8Value(const std::string& token, Core::uint8& outValue) { + try { + const unsigned long value = std::stoul(token, nullptr, 0); + if (value > 0xFFul) { + return false; + } + + outValue = static_cast(value); + return true; + } catch (...) { + return false; + } +} + +} // namespace + MaterialRenderState BuildDefaultAuthoringFixedFunctionState() { MaterialRenderState state = {}; state.blendEnable = false; @@ -146,6 +187,68 @@ bool TryParseAuthoringBlendFactor(const std::string& token, MaterialBlendFactor& return false; } +bool TryParseAuthoringBlendOp(const std::string& token, MaterialBlendOp& outOp) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "add") { + outOp = MaterialBlendOp::Add; + return true; + } + if (normalized == "sub" || normalized == "subtract") { + outOp = MaterialBlendOp::Subtract; + return true; + } + if (normalized == "revsub" || normalized == "reverse_subtract" || normalized == "reversesubtract") { + outOp = MaterialBlendOp::ReverseSubtract; + return true; + } + if (normalized == "min") { + outOp = MaterialBlendOp::Min; + return true; + } + if (normalized == "max") { + outOp = MaterialBlendOp::Max; + return true; + } + return false; +} + +bool TryParseAuthoringStencilOp(const std::string& token, MaterialStencilOp& outOp) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "keep") { + outOp = MaterialStencilOp::Keep; + return true; + } + if (normalized == "zero") { + outOp = MaterialStencilOp::Zero; + return true; + } + if (normalized == "replace") { + outOp = MaterialStencilOp::Replace; + return true; + } + if (normalized == "incrsat" || normalized == "incr_sat") { + outOp = MaterialStencilOp::IncrSat; + return true; + } + if (normalized == "decrsat" || normalized == "decr_sat") { + outOp = MaterialStencilOp::DecrSat; + return true; + } + if (normalized == "invert") { + outOp = MaterialStencilOp::Invert; + return true; + } + if (normalized == "incrwrap" || normalized == "incr_wrap" || normalized == "incr") { + outOp = MaterialStencilOp::IncrWrap; + return true; + } + if (normalized == "decrwrap" || normalized == "decr_wrap" || normalized == "decr") { + outOp = MaterialStencilOp::DecrWrap; + return true; + } + return false; +} + bool TryParseAuthoringColorMask(const std::string& token, Core::uint8& outMask) { const Containers::String normalized = Containers::String(token.c_str()).Trim().ToUpper(); if (normalized == "0") { @@ -180,21 +283,7 @@ bool TryParseAuthoringColorMask(const std::string& token, Core::uint8& outMask) bool TryParseAuthoringBlendDirective( const std::vector& tokens, MaterialRenderState& outState) { - std::vector normalizedTokens; - normalizedTokens.reserve(tokens.size()); - for (const std::string& token : tokens) { - if (token == ",") { - continue; - } - - std::string normalizedToken = token; - while (!normalizedToken.empty() && normalizedToken.back() == ',') { - normalizedToken.pop_back(); - } - if (!normalizedToken.empty()) { - normalizedTokens.push_back(std::move(normalizedToken)); - } - } + const std::vector normalizedTokens = NormalizeDirectiveTokens(tokens, 0u); if (normalizedTokens.size() != 2u && normalizedTokens.size() != 3u && @@ -242,6 +331,169 @@ bool TryParseAuthoringBlendDirective( return true; } +bool TryParseAuthoringBlendOpDirective( + const std::vector& tokens, + MaterialRenderState& outState) { + const std::vector normalizedTokens = NormalizeDirectiveTokens(tokens, 1u); + if (normalizedTokens.size() != 1u && normalizedTokens.size() != 2u) { + return false; + } + + if (!TryParseAuthoringBlendOp(normalizedTokens[0], outState.blendOp)) { + return false; + } + + if (normalizedTokens.size() == 2u) { + return TryParseAuthoringBlendOp(normalizedTokens[1], outState.blendOpAlpha); + } + + outState.blendOpAlpha = outState.blendOp; + return true; +} + +bool TryParseAuthoringOffsetDirective( + const std::vector& tokens, + MaterialRenderState& outState) { + const std::vector normalizedTokens = NormalizeDirectiveTokens(tokens, 1u); + if (normalizedTokens.size() != 2u) { + return false; + } + + try { + outState.depthBiasFactor = std::stof(normalizedTokens[0]); + outState.depthBiasUnits = static_cast(std::stol(normalizedTokens[1])); + return true; + } catch (...) { + return false; + } +} + +bool TryParseAuthoringStencilDirective( + const std::vector& tokens, + MaterialStencilState& outState) { + if (tokens.size() != 2u && tokens.size() != 3u) { + return false; + } + + const Containers::String directive = Containers::String(tokens[0].c_str()).Trim().ToLower(); + if (directive == "ref") { + Core::uint8 reference = 0; + if (!TryParseUInt8Value(tokens[1], reference)) { + return false; + } + outState.reference = reference; + outState.enabled = true; + return true; + } + + if (directive == "readmask") { + Core::uint8 readMask = 0; + if (!TryParseUInt8Value(tokens[1], readMask)) { + return false; + } + outState.readMask = readMask; + outState.enabled = true; + return true; + } + + if (directive == "writemask") { + Core::uint8 writeMask = 0; + if (!TryParseUInt8Value(tokens[1], writeMask)) { + return false; + } + outState.writeMask = writeMask; + outState.enabled = true; + return true; + } + + auto applyComparison = [&](MaterialStencilFaceState& faceState) -> bool { + return TryParseAuthoringComparisonFunc(tokens[1], faceState.func); + }; + auto applyOperation = [&](MaterialStencilFaceState& faceState, MaterialStencilOp MaterialStencilFaceState::* member) -> bool { + MaterialStencilOp op = MaterialStencilOp::Keep; + if (!TryParseAuthoringStencilOp(tokens[1], op)) { + return false; + } + + faceState.*member = op; + return true; + }; + + if (directive == "comp") { + outState.enabled = true; + return applyComparison(outState.front) && applyComparison(outState.back); + } + if (directive == "pass") { + outState.enabled = true; + return applyOperation(outState.front, &MaterialStencilFaceState::passOp) && + applyOperation(outState.back, &MaterialStencilFaceState::passOp); + } + if (directive == "fail") { + outState.enabled = true; + return applyOperation(outState.front, &MaterialStencilFaceState::failOp) && + applyOperation(outState.back, &MaterialStencilFaceState::failOp); + } + if (directive == "zfail") { + outState.enabled = true; + return applyOperation(outState.front, &MaterialStencilFaceState::depthFailOp) && + applyOperation(outState.back, &MaterialStencilFaceState::depthFailOp); + } + if (directive == "compfront") { + outState.enabled = true; + return applyComparison(outState.front); + } + if (directive == "passfront") { + outState.enabled = true; + return applyOperation(outState.front, &MaterialStencilFaceState::passOp); + } + if (directive == "failfront") { + outState.enabled = true; + return applyOperation(outState.front, &MaterialStencilFaceState::failOp); + } + if (directive == "zfailfront") { + outState.enabled = true; + return applyOperation(outState.front, &MaterialStencilFaceState::depthFailOp); + } + if (directive == "compback") { + outState.enabled = true; + return applyComparison(outState.back); + } + if (directive == "passback") { + outState.enabled = true; + return applyOperation(outState.back, &MaterialStencilFaceState::passOp); + } + if (directive == "failback") { + outState.enabled = true; + return applyOperation(outState.back, &MaterialStencilFaceState::failOp); + } + if (directive == "zfailback") { + outState.enabled = true; + return applyOperation(outState.back, &MaterialStencilFaceState::depthFailOp); + } + + return false; +} + +bool TryParseAuthoringUsePassReference( + const Containers::String& reference, + Containers::String& outShaderName, + Containers::String& outPassName) { + outShaderName.Clear(); + outPassName.Clear(); + + const std::string referenceText = reference.Trim().CStr(); + const size_t slashPos = referenceText.rfind('/'); + if (slashPos == std::string::npos || + slashPos == 0u || + slashPos + 1u >= referenceText.size()) { + return false; + } + + outShaderName = Containers::String(referenceText.substr(0, slashPos).c_str()); + outPassName = Containers::String(referenceText.substr(slashPos + 1u).c_str()); + return !outShaderName.Empty() && !outPassName.Empty(); +} + void SetOrReplaceAuthoringTag( std::vector& tags, const Containers::String& name, diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp index 493d1f95..cddd29e8 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringLoader.cpp @@ -1,19 +1,33 @@ #include "ShaderAuthoringLoader.h" #include "../ShaderAuthoringParser.h" -#include "ShaderRuntimeBuildUtils.h" #include "../ShaderSourceUtils.h" +#include "ShaderFileUtils.h" +#include "ShaderRuntimeBuildUtils.h" +#include + +#include #include namespace XCEngine { namespace Resources { -bool CollectShaderAuthoringDependencyPaths( +namespace { + +namespace fs = std::filesystem; + +bool CollectShaderAuthoringDependencyPathsRecursive( const Containers::String& path, const std::string& sourceText, + std::unordered_set& seenShaderPaths, + std::unordered_set& seenDependencyPaths, Containers::Array& outDependencies) { - outDependencies.Clear(); + const fs::path normalizedShaderPath = fs::path(path.CStr()).lexically_normal(); + const std::string shaderKey = normalizedShaderPath.generic_string(); + if (!shaderKey.empty() && !seenShaderPaths.insert(shaderKey).second) { + return true; + } ShaderIR shaderIR = {}; Containers::String parseError; @@ -21,19 +35,91 @@ bool CollectShaderAuthoringDependencyPaths( return false; } - std::unordered_set seenPaths; - CollectQuotedIncludeDependencyPaths(path, shaderIR.sharedProgramSource, seenPaths, outDependencies); + CollectQuotedIncludeDependencyPaths(path, shaderIR.sharedProgramSource, seenDependencyPaths, outDependencies); for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { - CollectQuotedIncludeDependencyPaths(path, subShader.sharedProgramSource, seenPaths, outDependencies); + CollectQuotedIncludeDependencyPaths(path, subShader.sharedProgramSource, seenDependencyPaths, outDependencies); for (const ShaderPassIR& pass : subShader.passes) { - CollectQuotedIncludeDependencyPaths(path, pass.sharedProgramSource, seenPaths, outDependencies); - CollectQuotedIncludeDependencyPaths(path, pass.programSource, seenPaths, outDependencies); + if (!pass.isUsePass) { + CollectQuotedIncludeDependencyPaths(path, pass.sharedProgramSource, seenDependencyPaths, outDependencies); + CollectQuotedIncludeDependencyPaths(path, pass.programSource, seenDependencyPaths, outDependencies); + continue; + } + + Containers::String resolvedUsePassPath; + if (!ResolveShaderUsePassPath( + path, + shaderIR.name, + pass.usePassShaderName, + resolvedUsePassPath)) { + Containers::String builtinShaderPath; + if (!TryGetBuiltinShaderPathByShaderName(pass.usePassShaderName, builtinShaderPath)) { + return false; + } + + if (!builtinShaderPath.Empty() && + seenDependencyPaths.insert(ToStdString(builtinShaderPath)).second) { + outDependencies.PushBack(builtinShaderPath); + } + continue; + } + + if (resolvedUsePassPath.Empty() || resolvedUsePassPath == path) { + continue; + } + + if (IsBuiltinShaderPath(resolvedUsePassPath)) { + if (seenDependencyPaths.insert(ToStdString(resolvedUsePassPath)).second) { + outDependencies.PushBack(resolvedUsePassPath); + } + continue; + } + + const fs::path normalizedDependencyPath = + fs::path(resolvedUsePassPath.CStr()).lexically_normal(); + const Containers::String normalizedDependency = + normalizedDependencyPath.generic_string().c_str(); + if (!normalizedDependency.Empty() && + seenDependencyPaths.insert(ToStdString(normalizedDependency)).second) { + outDependencies.PushBack(normalizedDependency); + } + + Containers::String referencedSourceText; + if (!ReadShaderTextFile(resolvedUsePassPath, referencedSourceText)) { + return false; + } + + if (!CollectShaderAuthoringDependencyPathsRecursive( + resolvedUsePassPath, + referencedSourceText.CStr(), + seenShaderPaths, + seenDependencyPaths, + outDependencies)) { + return false; + } } } return true; } +} // namespace + +bool CollectShaderAuthoringDependencyPaths( + const Containers::String& path, + const std::string& sourceText, + Containers::Array& outDependencies) { + outDependencies.Clear(); + + std::unordered_set seenShaderPaths; + std::unordered_set seenDependencyPaths; + return CollectShaderAuthoringDependencyPathsRecursive( + path, + sourceText, + seenShaderPaths, + seenDependencyPaths, + outDependencies); +} + LoadResult LoadShaderAuthoring( const Containers::String& path, const std::string& sourceText) { diff --git a/engine/src/Resources/Shader/Internal/ShaderAuthoringParserCore.cpp b/engine/src/Resources/Shader/Internal/ShaderAuthoringParserCore.cpp index 8f1272ce..14b290c8 100644 --- a/engine/src/Resources/Shader/Internal/ShaderAuthoringParserCore.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderAuthoringParserCore.cpp @@ -17,7 +17,8 @@ bool ParseShaderAuthoring( Shader, Properties, SubShader, - Pass + Pass, + Stencil }; auto fail = [&outError](const std::string& message, size_t lineNumber) -> bool { @@ -48,6 +49,7 @@ bool ParseShaderAuthoring( BlockKind pendingBlock = BlockKind::None; ShaderSubShaderIR* currentSubShader = nullptr; ShaderPassIR* currentPass = nullptr; + MaterialRenderState* currentStencilRenderState = nullptr; bool inProgramBlock = false; bool inSharedIncludeBlock = false; @@ -76,9 +78,193 @@ bool ParseShaderAuthoring( return true; }; + auto tryParseFixedFunctionDirective = + [&](const std::string& line, bool& outMatched, std::string& outErrorMessage) -> bool { + outMatched = false; + + MaterialRenderState* targetState = nullptr; + bool* hasFixedFunctionState = nullptr; + if (currentBlock() == BlockKind::Pass && currentPass != nullptr) { + targetState = ¤tPass->fixedFunctionState; + hasFixedFunctionState = ¤tPass->hasFixedFunctionState; + } else if (currentBlock() == BlockKind::SubShader && currentSubShader != nullptr) { + targetState = ¤tSubShader->fixedFunctionState; + hasFixedFunctionState = ¤tSubShader->hasFixedFunctionState; + } else { + return true; + } + + std::vector tokens; + + if (StartsWithKeyword(line, "Cull")) { + outMatched = true; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + outErrorMessage = "Cull directive must use Front, Back, or Off"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + if (!TryParseAuthoringCullMode(tokens[1], targetState->cullMode)) { + outErrorMessage = "Cull directive must use Front, Back, or Off"; + return false; + } + return true; + } + + if (StartsWithKeyword(line, "ZWrite")) { + outMatched = true; + bool enabled = false; + if (!TryTokenizeQuotedArguments(line, tokens) || + tokens.size() != 2u || + !TryParseAuthoringBoolDirectiveToken(tokens[1], enabled)) { + outErrorMessage = "ZWrite directive must use On or Off"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + targetState->depthWriteEnable = enabled; + return true; + } + + if (StartsWithKeyword(line, "ZTest")) { + outMatched = true; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + outErrorMessage = "ZTest directive uses an unsupported compare function"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + if (!TryParseAuthoringComparisonFunc(tokens[1], targetState->depthFunc)) { + outErrorMessage = "ZTest directive uses an unsupported compare function"; + return false; + } + targetState->depthTestEnable = true; + return true; + } + + if (StartsWithKeyword(line, "BlendOp")) { + outMatched = true; + if (!TryTokenizeQuotedArguments(line, tokens)) { + outErrorMessage = "BlendOp directive could not be tokenized"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + if (!TryParseAuthoringBlendOpDirective(tokens, *targetState)) { + outErrorMessage = "BlendOp directive uses an unsupported blend operation"; + return false; + } + return true; + } + + if (StartsWithKeyword(line, "Blend")) { + outMatched = true; + if (!TryTokenizeQuotedArguments(line, tokens)) { + outErrorMessage = "Blend directive could not be tokenized"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + if (!TryParseAuthoringBlendDirective(tokens, *targetState)) { + outErrorMessage = "Blend directive uses an unsupported factor combination"; + return false; + } + return true; + } + + if (StartsWithKeyword(line, "ColorMask")) { + outMatched = true; + if (!TryTokenizeQuotedArguments(line, tokens) || + (tokens.size() != 2u && tokens.size() != 3u)) { + outErrorMessage = "ColorMask directive uses an unsupported channel mask"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + if (!TryParseAuthoringColorMask(tokens[1], targetState->colorWriteMask)) { + outErrorMessage = "ColorMask directive uses an unsupported channel mask"; + return false; + } + return true; + } + + if (StartsWithKeyword(line, "Offset")) { + outMatched = true; + if (!TryTokenizeQuotedArguments(line, tokens)) { + outErrorMessage = "Offset directive could not be tokenized"; + return false; + } + + EnsureAuthoringFixedFunctionStateInitialized(*hasFixedFunctionState, *targetState); + if (!TryParseAuthoringOffsetDirective(tokens, *targetState)) { + outErrorMessage = "Offset directive must provide factor and units"; + return false; + } + return true; + } + + return true; + }; + + auto tryParseStencilDirective = + [&](const std::string& line, bool& outMatched, std::string& outErrorMessage) -> bool { + outMatched = false; + if (currentBlock() != BlockKind::Stencil || currentStencilRenderState == nullptr) { + return true; + } + + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens)) { + outMatched = true; + outErrorMessage = "Stencil directive could not be tokenized"; + return false; + } + + outMatched = true; + if (!TryParseAuthoringStencilDirective(tokens, currentStencilRenderState->stencil)) { + outErrorMessage = "Stencil block contains an unsupported directive"; + return false; + } + + return true; + }; + + auto tryParseUsePassDirective = + [&](const std::string& line, bool& outMatched, std::string& outErrorMessage) -> bool { + outMatched = false; + if (currentBlock() != BlockKind::SubShader || currentSubShader == nullptr) { + return true; + } + + if (!StartsWithKeyword(line, "UsePass")) { + return true; + } + + outMatched = true; + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + outErrorMessage = "UsePass directive must reference \"ShaderName/PassName\""; + return false; + } + + ShaderPassIR usePass = {}; + usePass.isUsePass = true; + if (!TryParseAuthoringUsePassReference( + tokens[1].c_str(), + usePass.usePassShaderName, + usePass.usePassPassName)) { + outErrorMessage = "UsePass directive must reference \"ShaderName/PassName\""; + return false; + } + + usePass.name = usePass.usePassPassName; + currentSubShader->passes.push_back(std::move(usePass)); + return true; + }; + for (size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex) { const std::string& line = lines[lineIndex]; - const size_t humanLine = lineIndex + 1; + const size_t humanLine = lineIndex + 1u; if (inSharedIncludeBlock || inProgramBlock) { if (line == "ENDHLSL" || line == "ENDCG") { @@ -159,6 +345,24 @@ bool ParseShaderAuthoring( } blockStack.push_back(BlockKind::Pass); break; + case BlockKind::Stencil: + if (currentPass != nullptr) { + EnsureAuthoringFixedFunctionStateInitialized( + currentPass->hasFixedFunctionState, + currentPass->fixedFunctionState); + currentStencilRenderState = ¤tPass->fixedFunctionState; + } else if (currentSubShader != nullptr) { + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + currentStencilRenderState = ¤tSubShader->fixedFunctionState; + } else { + return fail("Stencil block must be inside a SubShader or Pass", humanLine); + } + + currentStencilRenderState->stencil.enabled = true; + blockStack.push_back(BlockKind::Stencil); + break; case BlockKind::None: default: return fail("unexpected opening brace", humanLine); @@ -175,7 +379,9 @@ bool ParseShaderAuthoring( const BlockKind closingBlock = blockStack.back(); blockStack.pop_back(); - if (closingBlock == BlockKind::Pass) { + if (closingBlock == BlockKind::Stencil) { + currentStencilRenderState = nullptr; + } else if (closingBlock == BlockKind::Pass) { currentPass = nullptr; } else if (closingBlock == BlockKind::SubShader) { currentSubShader = nullptr; @@ -212,7 +418,7 @@ bool ParseShaderAuthoring( continue; } - if (StartsWithKeyword(line, "Pass")) { + if (line == "Pass") { pendingBlock = BlockKind::Pass; continue; } @@ -288,88 +494,41 @@ bool ParseShaderAuthoring( continue; } - if (currentBlock() == BlockKind::SubShader && currentSubShader != nullptr) { - if (StartsWithKeyword(line, "Cull")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } + if ((currentBlock() == BlockKind::SubShader || currentBlock() == BlockKind::Pass) && + line == "Stencil") { + pendingBlock = BlockKind::Stencil; + continue; + } - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseAuthoringCullMode(tokens[1], currentSubShader->fixedFunctionState.cullMode)) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } + if (currentBlock() == BlockKind::Stencil) { + bool matched = false; + std::string errorMessage; + if (!tryParseStencilDirective(line, matched, errorMessage)) { + return fail(errorMessage, humanLine); + } + if (matched) { continue; } + } - if (StartsWithKeyword(line, "ZWrite")) { - std::vector tokens; - bool enabled = false; - if (!TryTokenizeQuotedArguments(line, tokens) || - tokens.size() != 2u || - !TryParseAuthoringBoolDirectiveToken(tokens[1], enabled)) { - return fail("ZWrite directive must use On or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - currentSubShader->fixedFunctionState.depthWriteEnable = enabled; + if (currentBlock() == BlockKind::SubShader) { + bool matched = false; + std::string errorMessage; + if (!tryParseUsePassDirective(line, matched, errorMessage)) { + return fail(errorMessage, humanLine); + } + if (matched) { continue; } + } - if (StartsWithKeyword(line, "ZTest")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseAuthoringComparisonFunc(tokens[1], currentSubShader->fixedFunctionState.depthFunc)) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - currentSubShader->fixedFunctionState.depthTestEnable = true; - continue; + if (currentBlock() == BlockKind::SubShader || currentBlock() == BlockKind::Pass) { + bool matched = false; + std::string errorMessage; + if (!tryParseFixedFunctionDirective(line, matched, errorMessage)) { + return fail(errorMessage, humanLine); } - - if (StartsWithKeyword(line, "Blend")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens)) { - return fail("Blend directive could not be tokenized", humanLine); - } - - for (size_t tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex) { - if (!tokens[tokenIndex].empty() && tokens[tokenIndex].back() == ',') { - tokens[tokenIndex].pop_back(); - } - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseAuthoringBlendDirective(tokens, currentSubShader->fixedFunctionState)) { - return fail("Blend directive uses an unsupported factor combination", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ColorMask")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || - (tokens.size() != 2u && tokens.size() != 3u)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentSubShader->hasFixedFunctionState, - currentSubShader->fixedFunctionState); - if (!TryParseAuthoringColorMask(tokens[1], currentSubShader->fixedFunctionState.colorWriteMask)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } + if (matched) { continue; } } @@ -384,90 +543,6 @@ bool ParseShaderAuthoring( continue; } - if (StartsWithKeyword(line, "Cull")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseAuthoringCullMode(tokens[1], currentPass->fixedFunctionState.cullMode)) { - return fail("Cull directive must use Front, Back, or Off", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ZWrite")) { - std::vector tokens; - bool enabled = false; - if (!TryTokenizeQuotedArguments(line, tokens) || - tokens.size() != 2u || - !TryParseAuthoringBoolDirectiveToken(tokens[1], enabled)) { - return fail("ZWrite directive must use On or Off", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - currentPass->fixedFunctionState.depthWriteEnable = enabled; - continue; - } - - if (StartsWithKeyword(line, "ZTest")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseAuthoringComparisonFunc(tokens[1], currentPass->fixedFunctionState.depthFunc)) { - return fail("ZTest directive uses an unsupported compare function", humanLine); - } - currentPass->fixedFunctionState.depthTestEnable = true; - continue; - } - - if (StartsWithKeyword(line, "Blend")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens)) { - return fail("Blend directive could not be tokenized", humanLine); - } - - for (size_t tokenIndex = 1; tokenIndex < tokens.size(); ++tokenIndex) { - if (!tokens[tokenIndex].empty() && tokens[tokenIndex].back() == ',') { - tokens[tokenIndex].pop_back(); - } - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseAuthoringBlendDirective(tokens, currentPass->fixedFunctionState)) { - return fail("Blend directive uses an unsupported factor combination", humanLine); - } - continue; - } - - if (StartsWithKeyword(line, "ColorMask")) { - std::vector tokens; - if (!TryTokenizeQuotedArguments(line, tokens) || - (tokens.size() != 2u && tokens.size() != 3u)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - - EnsureAuthoringFixedFunctionStateInitialized( - currentPass->hasFixedFunctionState, - currentPass->fixedFunctionState); - if (!TryParseAuthoringColorMask(tokens[1], currentPass->fixedFunctionState.colorWriteMask)) { - return fail("ColorMask directive uses an unsupported channel mask", humanLine); - } - continue; - } - if (line == "HLSLPROGRAM" || line == "CGPROGRAM") { inProgramBlock = true; if (!consumeExtractedBlock( @@ -503,6 +578,13 @@ bool ParseShaderAuthoring( } for (ShaderPassIR& pass : subShader.passes) { + if (pass.isUsePass) { + if (pass.usePassShaderName.Empty() || pass.usePassPassName.Empty()) { + return fail("a UsePass directive is missing a valid target", 0); + } + continue; + } + if (pass.name.Empty()) { return fail("a Pass is missing a Name directive", 0); } diff --git a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp index 5eca0e37..a7c6481e 100644 --- a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp +++ b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.cpp @@ -1,14 +1,300 @@ #include "ShaderRuntimeBuildUtils.h" #include "../ShaderAuthoringParser.h" -#include "ShaderFileUtils.h" #include "../ShaderSourceUtils.h" +#include "ShaderAuthoringInternal.h" +#include "ShaderFileUtils.h" +#include +#include +#include + +#include #include +#include +#include +#include namespace XCEngine { namespace Resources { +namespace { + +namespace fs = std::filesystem; + +Containers::String ResolveBuiltinShaderPathByAuthoringName(const Containers::String& shaderName) { + Containers::String builtinShaderPath; + if (TryGetBuiltinShaderPathByShaderName(shaderName, builtinShaderPath)) { + return builtinShaderPath; + } + + return Containers::String(); +} + +void AddUniqueSearchRoot( + const fs::path& candidate, + std::vector& searchRoots, + std::unordered_set& seenRoots) { + if (candidate.empty()) { + return; + } + + const fs::path normalized = candidate.lexically_normal(); + const std::string key = normalized.generic_string(); + if (key.empty()) { + return; + } + + if (seenRoots.insert(key).second) { + searchRoots.push_back(normalized); + } +} + +bool TryReadDeclaredAuthoringShaderName( + const Containers::String& shaderPath, + Containers::String& outShaderName) { + outShaderName.Clear(); + + const Containers::Array data = ReadShaderFileData(shaderPath); + if (data.Empty()) { + return false; + } + + const std::string sourceText = ToStdStringFromBytes(data); + std::vector lines; + Internal::SplitShaderAuthoringLines(sourceText, lines); + if (lines.empty() || !Internal::StartsWithKeyword(lines.front(), "Shader")) { + return false; + } + + std::vector tokens; + if (!Internal::TryTokenizeQuotedArguments(lines.front(), tokens) || tokens.size() < 2u) { + return false; + } + + outShaderName = tokens[1].c_str(); + return !outShaderName.Empty(); +} + +bool TryResolveProjectShaderPathByAuthoringName( + const Containers::String& currentShaderPath, + const Containers::String& shaderName, + Containers::String& outResolvedPath) { + outResolvedPath.Clear(); + + fs::path currentPath(currentShaderPath.CStr()); + if (!currentPath.is_absolute()) { + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + currentPath = fs::path(resourceRoot.CStr()) / currentPath; + } + } + + if (currentPath.empty()) { + return false; + } + + std::vector searchRoots; + std::unordered_set seenRoots; + + AddUniqueSearchRoot(currentPath.parent_path(), searchRoots, seenRoots); + + for (fs::path ancestor = currentPath.parent_path(); + !ancestor.empty(); + ancestor = ancestor.parent_path()) { + std::error_code ec; + const fs::path assetsRoot = ancestor / "Assets"; + if (fs::exists(assetsRoot, ec) && fs::is_directory(assetsRoot, ec)) { + AddUniqueSearchRoot(assetsRoot, searchRoots, seenRoots); + } + + const fs::path parent = ancestor.parent_path(); + if (parent == ancestor) { + break; + } + } + + std::vector matches; + std::unordered_set seenMatches; + + for (const fs::path& root : searchRoots) { + std::error_code ec; + if (!fs::exists(root, ec) || !fs::is_directory(root, ec)) { + continue; + } + + for (fs::recursive_directory_iterator it(root, ec), end; !ec && it != end; it.increment(ec)) { + if (!it->is_regular_file()) { + continue; + } + + const fs::path candidatePath = it->path(); + if (candidatePath.extension() != ".shader") { + continue; + } + + Containers::String declaredShaderName; + if (!TryReadDeclaredAuthoringShaderName( + Containers::String(candidatePath.generic_string().c_str()), + declaredShaderName) || + declaredShaderName != shaderName) { + continue; + } + + const std::string matchKey = candidatePath.lexically_normal().generic_string(); + if (seenMatches.insert(matchKey).second) { + matches.push_back(candidatePath.lexically_normal()); + } + } + } + + if (matches.size() != 1u) { + return false; + } + + outResolvedPath = matches.front().generic_string().c_str(); + return true; +} + +void ImportConcretePass(Shader& shader, const ShaderPass& sourcePass) { + if (ShaderPass* existingPass = shader.FindPass(sourcePass.name)) { + *existingPass = sourcePass; + return; + } + + shader.AddPass(sourcePass); +} + +ShaderPass BuildConcretePass( + const ShaderIR& shaderIR, + const ShaderSubShaderIR& subShader, + const ShaderPassIR& pass) { + ShaderPass shaderPass = {}; + shaderPass.name = pass.name; + shaderPass.hasFixedFunctionState = pass.hasFixedFunctionState; + shaderPass.fixedFunctionState = pass.fixedFunctionState; + + for (const ShaderTagIR& subShaderTag : subShader.tags) { + shaderPass.tags.PushBack({ subShaderTag.name, subShaderTag.value }); + } + for (const ShaderTagIR& passTag : pass.tags) { + shaderPass.tags.PushBack({ passTag.name, passTag.value }); + } + for (const ShaderResourceBindingDesc& resourceBinding : pass.resources) { + shaderPass.resources.PushBack(resourceBinding); + } + for (const ShaderKeywordDeclaration& keywordDeclaration : pass.keywordDeclarations) { + shaderPass.keywordDeclarations.PushBack(keywordDeclaration); + } + + if (pass.programSource.Empty()) { + return shaderPass; + } + + Containers::String combinedSource; + AppendAuthoringSourceBlock(combinedSource, shaderIR.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, subShader.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, pass.sharedProgramSource); + AppendAuthoringSourceBlock(combinedSource, pass.programSource); + + const Containers::String strippedCombinedSource = StripShaderAuthoringPragmas(combinedSource); + const std::vector keywordSets = + BuildShaderKeywordVariantSets(pass.keywordDeclarations); + + for (const ShaderKeywordSet& keywordSet : keywordSets) { + const Containers::String variantSource = + BuildKeywordVariantSource(strippedCombinedSource, keywordSet); + + ShaderStageVariant vertexVariant = {}; + vertexVariant.stage = ShaderType::Vertex; + vertexVariant.backend = ShaderBackend::Generic; + vertexVariant.language = ShaderLanguage::HLSL; + vertexVariant.requiredKeywords = keywordSet; + vertexVariant.entryPoint = + !pass.vertexEntryPoint.Empty() + ? pass.vertexEntryPoint + : GetDefaultEntryPoint(ShaderLanguage::HLSL, ShaderType::Vertex); + vertexVariant.profile = GetDefaultProfile( + ShaderLanguage::HLSL, + ShaderBackend::Generic, + ShaderType::Vertex); + vertexVariant.sourceCode = variantSource; + shaderPass.variants.PushBack(vertexVariant); + + ShaderStageVariant fragmentVariant = {}; + fragmentVariant.stage = ShaderType::Fragment; + fragmentVariant.backend = ShaderBackend::Generic; + fragmentVariant.language = ShaderLanguage::HLSL; + fragmentVariant.requiredKeywords = keywordSet; + fragmentVariant.entryPoint = + !pass.fragmentEntryPoint.Empty() + ? pass.fragmentEntryPoint + : GetDefaultEntryPoint(ShaderLanguage::HLSL, ShaderType::Fragment); + fragmentVariant.profile = GetDefaultProfile( + ShaderLanguage::HLSL, + ShaderBackend::Generic, + ShaderType::Fragment); + fragmentVariant.sourceCode = variantSource; + shaderPass.variants.PushBack(fragmentVariant); + } + + return shaderPass; +} + +bool TryResolveUsePass( + const Containers::String& currentShaderPath, + const Containers::String& currentShaderName, + const Containers::String& referencedShaderName, + const Containers::String& referencedPassName, + const std::unordered_map& localConcretePasses, + ShaderPass& outPass, + Containers::String& outError) { + if (referencedShaderName == currentShaderName) { + const auto passIt = localConcretePasses.find(ToStdString(referencedPassName)); + if (passIt == localConcretePasses.end()) { + outError = + Containers::String("UsePass could not resolve local pass: ") + referencedPassName; + return false; + } + + outPass = passIt->second; + return true; + } + + Containers::String referencedShaderPath; + if (!ResolveShaderUsePassPath( + currentShaderPath, + currentShaderName, + referencedShaderName, + referencedShaderPath)) { + outError = + Containers::String("UsePass could not resolve referenced shader: ") + + referencedShaderName; + return false; + } + + ShaderLoader loader; + LoadResult referencedShaderResult = loader.Load(referencedShaderPath); + if (!referencedShaderResult || referencedShaderResult.resource == nullptr) { + outError = + Containers::String("UsePass failed to load referenced shader: ") + referencedShaderName; + return false; + } + + std::unique_ptr referencedShader(static_cast(referencedShaderResult.resource)); + const ShaderPass* referencedPass = referencedShader->FindPass(referencedPassName); + if (referencedPass == nullptr) { + outError = + Containers::String("UsePass could not find referenced pass: ") + referencedPassName; + return false; + } + + outPass = *referencedPass; + return true; +} + +} // namespace + Containers::String GetDefaultEntryPoint(ShaderLanguage language, ShaderType stage) { if (language == ShaderLanguage::HLSL) { switch (stage) { @@ -98,6 +384,33 @@ size_t CalculateShaderMemorySize(const Shader& shader) { return memorySize; } +bool ResolveShaderUsePassPath( + const Containers::String& currentShaderPath, + const Containers::String& currentShaderName, + const Containers::String& targetShaderName, + Containers::String& outResolvedPath) { + outResolvedPath.Clear(); + + if (targetShaderName.Empty()) { + return false; + } + + if (targetShaderName == currentShaderName) { + outResolvedPath = currentShaderPath; + return true; + } + + outResolvedPath = ResolveBuiltinShaderPathByAuthoringName(targetShaderName); + if (!outResolvedPath.Empty()) { + return true; + } + + return TryResolveProjectShaderPathByAuthoringName( + currentShaderPath, + targetShaderName, + outResolvedPath); +} + LoadResult BuildShaderFromIR( const Containers::String& path, const ShaderIR& shaderIR) { @@ -113,76 +426,56 @@ LoadResult BuildShaderFromIR( shader->AddProperty(property); } + std::unordered_map localConcretePasses; for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { for (const ShaderPassIR& pass : subShader.passes) { - ShaderPass shaderPass = {}; - shaderPass.name = pass.name; - shaderPass.hasFixedFunctionState = pass.hasFixedFunctionState; - shaderPass.fixedFunctionState = pass.fixedFunctionState; - shader->AddPass(shaderPass); - - for (const ShaderTagIR& subShaderTag : subShader.tags) { - shader->SetPassTag(pass.name, subShaderTag.name, subShaderTag.value); - } - for (const ShaderTagIR& passTag : pass.tags) { - shader->SetPassTag(pass.name, passTag.name, passTag.value); + if (pass.isUsePass) { + continue; } - for (const ShaderResourceBindingDesc& resourceBinding : pass.resources) { - shader->AddPassResourceBinding(pass.name, resourceBinding); - } - for (const ShaderKeywordDeclaration& keywordDeclaration : pass.keywordDeclarations) { - shader->AddPassKeywordDeclaration(pass.name, keywordDeclaration); + ShaderPass concretePass = BuildConcretePass(shaderIR, subShader, pass); + const std::string passKey = ToStdString(concretePass.name); + if (passKey.empty()) { + return LoadResult("Shader authoring produced a pass with an empty name"); } - if (!pass.programSource.Empty()) { - Containers::String combinedSource; - AppendAuthoringSourceBlock(combinedSource, shaderIR.sharedProgramSource); - AppendAuthoringSourceBlock(combinedSource, subShader.sharedProgramSource); - AppendAuthoringSourceBlock(combinedSource, pass.sharedProgramSource); - AppendAuthoringSourceBlock(combinedSource, pass.programSource); - const Containers::String strippedCombinedSource = - StripShaderAuthoringPragmas(combinedSource); - const std::vector keywordSets = - BuildShaderKeywordVariantSets(pass.keywordDeclarations); + if (!localConcretePasses.emplace(passKey, std::move(concretePass)).second) { + return LoadResult( + Containers::String("Shader authoring produced duplicate pass name: ") + pass.name); + } + } + } - for (const ShaderKeywordSet& keywordSet : keywordSets) { - const Containers::String variantSource = - BuildKeywordVariantSource(strippedCombinedSource, keywordSet); - - ShaderStageVariant vertexVariant = {}; - vertexVariant.stage = ShaderType::Vertex; - vertexVariant.backend = ShaderBackend::Generic; - vertexVariant.language = ShaderLanguage::HLSL; - vertexVariant.requiredKeywords = keywordSet; - vertexVariant.entryPoint = - !pass.vertexEntryPoint.Empty() - ? pass.vertexEntryPoint - : GetDefaultEntryPoint(ShaderLanguage::HLSL, ShaderType::Vertex); - vertexVariant.profile = GetDefaultProfile( - ShaderLanguage::HLSL, - ShaderBackend::Generic, - ShaderType::Vertex); - vertexVariant.sourceCode = variantSource; - shader->AddPassVariant(pass.name, vertexVariant); - - ShaderStageVariant fragmentVariant = {}; - fragmentVariant.stage = ShaderType::Fragment; - fragmentVariant.backend = ShaderBackend::Generic; - fragmentVariant.language = ShaderLanguage::HLSL; - fragmentVariant.requiredKeywords = keywordSet; - fragmentVariant.entryPoint = - !pass.fragmentEntryPoint.Empty() - ? pass.fragmentEntryPoint - : GetDefaultEntryPoint(ShaderLanguage::HLSL, ShaderType::Fragment); - fragmentVariant.profile = GetDefaultProfile( - ShaderLanguage::HLSL, - ShaderBackend::Generic, - ShaderType::Fragment); - fragmentVariant.sourceCode = variantSource; - shader->AddPassVariant(pass.name, fragmentVariant); + for (const ShaderSubShaderIR& subShader : shaderIR.subShaders) { + for (const ShaderPassIR& pass : subShader.passes) { + if (pass.isUsePass) { + ShaderPass importedPass = {}; + Containers::String importError; + if (!TryResolveUsePass( + path, + shaderIR.name, + pass.usePassShaderName, + pass.usePassPassName, + localConcretePasses, + importedPass, + importError)) { + return LoadResult(importError); } + + ImportConcretePass(*shader, importedPass); + for (const ShaderTagIR& subShaderTag : subShader.tags) { + shader->SetPassTag(importedPass.name, subShaderTag.name, subShaderTag.value); + } + continue; } + + const auto passIt = localConcretePasses.find(ToStdString(pass.name)); + if (passIt == localConcretePasses.end()) { + return LoadResult( + Containers::String("Shader authoring lost concrete pass during build: ") + pass.name); + } + + ImportConcretePass(*shader, passIt->second); } } @@ -190,32 +483,5 @@ LoadResult BuildShaderFromIR( return LoadResult(shader.release()); } -LoadResult LoadLegacySingleStageShader( - const Containers::String& path, - const std::string& sourceText) { - auto shader = std::make_unique(); - shader->m_path = path; - shader->m_name = path; - shader->m_guid = ResourceGUID::Generate(path); - - const Containers::String ext = GetShaderPathExtension(path).ToLower(); - if (ext == "hlsl") { - shader->SetShaderLanguage(ShaderLanguage::HLSL); - } else { - shader->SetShaderLanguage(ShaderLanguage::GLSL); - } - - shader->SetShaderType(DetectShaderTypeFromPath(path)); - shader->SetSourceCode(sourceText.c_str()); - shader->m_isValid = true; - shader->m_memorySize = - sizeof(Shader) + - shader->m_name.Length() + - shader->m_path.Length() + - shader->GetSourceCode().Length(); - - return LoadResult(shader.release()); -} - } // namespace Resources } // namespace XCEngine diff --git a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.h b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.h index 7c92893b..a37b346c 100644 --- a/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.h +++ b/engine/src/Resources/Shader/Internal/ShaderRuntimeBuildUtils.h @@ -22,9 +22,11 @@ LoadResult BuildShaderFromIR( const Containers::String& path, const ShaderIR& shaderIR); -LoadResult LoadLegacySingleStageShader( - const Containers::String& path, - const std::string& sourceText); +bool ResolveShaderUsePassPath( + const Containers::String& currentShaderPath, + const Containers::String& currentShaderName, + const Containers::String& targetShaderName, + Containers::String& outResolvedPath); } // namespace Resources } // namespace XCEngine diff --git a/engine/src/Resources/Shader/ShaderIR.h b/engine/src/Resources/Shader/ShaderIR.h index c6623fb6..d6aafb3e 100644 --- a/engine/src/Resources/Shader/ShaderIR.h +++ b/engine/src/Resources/Shader/ShaderIR.h @@ -16,6 +16,9 @@ struct ShaderTagIR { struct ShaderPassIR { Containers::String name; + bool isUsePass = false; + Containers::String usePassShaderName; + Containers::String usePassPassName; bool hasFixedFunctionState = false; MaterialRenderState fixedFunctionState = {}; std::vector tags; diff --git a/tests/Rendering/unit/CMakeLists.txt b/tests/Rendering/unit/CMakeLists.txt index 9a64b790..536b84b9 100644 --- a/tests/Rendering/unit/CMakeLists.txt +++ b/tests/Rendering/unit/CMakeLists.txt @@ -19,6 +19,7 @@ if(MSVC) set_target_properties(rendering_unit_tests PROPERTIES LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" ) + target_compile_options(rendering_unit_tests PRIVATE /FS) endif() target_link_libraries(rendering_unit_tests PRIVATE diff --git a/tests/Resources/Material/CMakeLists.txt b/tests/Resources/Material/CMakeLists.txt index 56091c0f..c701a015 100644 --- a/tests/Resources/Material/CMakeLists.txt +++ b/tests/Resources/Material/CMakeLists.txt @@ -13,6 +13,7 @@ if(MSVC) set_target_properties(material_tests PROPERTIES LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" ) + target_compile_options(material_tests PRIVATE /FS) endif() target_link_libraries(material_tests diff --git a/tests/Resources/Shader/CMakeLists.txt b/tests/Resources/Shader/CMakeLists.txt index 61c81baf..4e4accba 100644 --- a/tests/Resources/Shader/CMakeLists.txt +++ b/tests/Resources/Shader/CMakeLists.txt @@ -13,6 +13,7 @@ if(MSVC) set_target_properties(shader_tests PROPERTIES LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" ) + target_compile_options(shader_tests PRIVATE /FS) endif() target_link_libraries(shader_tests diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index b7814af4..6d0c420e 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -51,25 +51,27 @@ TEST(ShaderLoader, GetSupportedExtensions) { 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.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_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.glsl"); + LoadResult result = loader.Load("invalid/path/shader.shader"); EXPECT_FALSE(result); } -TEST(ShaderLoader, LoadLegacySingleStageShaderBuildsDefaultPassVariant) { +TEST(ShaderLoader, RejectsLegacySingleStageShaderSourceFiles) { namespace fs = std::filesystem; const fs::path shaderPath = fs::temp_directory_path() / "xc_shader_loader_stage.vert"; @@ -81,107 +83,83 @@ TEST(ShaderLoader, LoadLegacySingleStageShaderBuildsDefaultPassVariant) { ShaderLoader loader; LoadResult result = loader.Load(shaderPath.string().c_str()); - ASSERT_TRUE(result); - ASSERT_NE(result.resource, nullptr); + EXPECT_FALSE(result); + EXPECT_NE( + std::string(result.errorMessage.CStr()).find("Unsupported shader source format"), + std::string::npos); - 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) { +TEST(ShaderLoader, LoadShaderAuthoringBuildsMultiPassGenericVariants) { 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"; + 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(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"); - + fs::create_directories(shaderRoot); + WriteTextFile( + shaderPath, + R"(Shader "TestLitShader" +{ + Properties { - std::ofstream manifest(manifestPath); - ASSERT_TRUE(manifest.is_open()); - manifest << "{\n"; - manifest << " \"name\": \"TestLitShader\",\n"; - manifest << " \"properties\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"_BaseColor\",\n"; - manifest << " \"displayName\": \"Base Color\",\n"; - manifest << " \"type\": \"Color\",\n"; - manifest << " \"defaultValue\": \"(1,1,1,1)\",\n"; - manifest << " \"semantic\": \"BaseColor\"\n"; - manifest << " },\n"; - manifest << " {\n"; - manifest << " \"name\": \"_MainTex\",\n"; - manifest << " \"displayName\": \"Base Map\",\n"; - manifest << " \"type\": \"2D\",\n"; - manifest << " \"defaultValue\": \"white\",\n"; - manifest << " \"semantic\": \"BaseColorTexture\"\n"; - manifest << " }\n"; - manifest << " ],\n"; - manifest << " \"passes\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"ForwardLit\",\n"; - manifest << " \"tags\": {\n"; - manifest << " \"LightMode\": \"ForwardBase\",\n"; - manifest << " \"Queue\": \"Geometry\"\n"; - manifest << " },\n"; - manifest << " \"resources\": [\n"; - manifest << " { \"name\": \"PerObjectConstants\", \"type\": \"ConstantBuffer\", \"set\": 1, \"binding\": 0, \"semantic\": \"PerObject\" },\n"; - manifest << " { \"name\": \"LightingConstants\", \"type\": \"ConstantBuffer\", \"set\": 2, \"binding\": 0, \"semantic\": \"Lighting\" },\n"; - manifest << " { \"name\": \"MaterialConstants\", \"type\": \"ConstantBuffer\", \"set\": 3, \"binding\": 0, \"semantic\": \"Material\" },\n"; - manifest << " { \"name\": \"ShadowReceiverConstants\", \"type\": \"ConstantBuffer\", \"set\": 4, \"binding\": 0, \"semantic\": \"ShadowReceiver\" },\n"; - manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 5, \"binding\": 0, \"semantic\": \"BaseColorTexture\" },\n"; - manifest << " { \"name\": \"LinearClampSampler\", \"type\": \"Sampler\", \"set\": 6, \"binding\": 0, \"semantic\": \"LinearClampSampler\" },\n"; - manifest << " { \"name\": \"ShadowMapTexture\", \"type\": \"Texture2D\", \"set\": 7, \"binding\": 0, \"semantic\": \"ShadowMapTexture\" },\n"; - manifest << " { \"name\": \"ShadowMapSampler\", \"type\": \"Sampler\", \"set\": 8, \"binding\": 0, \"semantic\": \"ShadowMapSampler\" }\n"; - manifest << " ],\n"; - manifest << " \"variants\": [\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.vs.hlsl\", \"entryPoint\": \"MainVS\", \"profile\": \"vs_5_0\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/forward_lit.ps.hlsl\", \"entryPoint\": \"MainPS\", \"profile\": \"ps_5_0\" },\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.vert.glsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.frag.glsl\" },\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"Vulkan\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.vert.vk.glsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"Vulkan\", \"language\": \"GLSL\", \"source\": \"stages/forward_lit.frag.vk.glsl\" }\n"; - manifest << " ]\n"; - manifest << " },\n"; - manifest << " {\n"; - manifest << " \"name\": \"DepthOnly\",\n"; - manifest << " \"tags\": {\n"; - manifest << " \"LightMode\": \"DepthOnly\"\n"; - manifest << " },\n"; - manifest << " \"variants\": [\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/depth_only.vs.hlsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"D3D12\", \"language\": \"HLSL\", \"source\": \"stages/depth_only.ps.hlsl\" }\n"; - manifest << " ]\n"; - manifest << " }\n"; - manifest << " ]\n"; - manifest << "}\n"; + _BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)] + _MainTex ("Base Map", 2D) = "white" [Semantic(BaseColorTexture)] } + SubShader + { + Pass + { + Name "ForwardLit" + Tags { "LightMode" = "ForwardBase", "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(manifestPath.string().c_str()); + LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); @@ -208,78 +186,101 @@ TEST(ShaderLoader, LoadShaderManifestBuildsMultiPassBackendVariants) { const ShaderPass* forwardLitPass = shader->FindPass("ForwardLit"); ASSERT_NE(forwardLitPass, nullptr); ASSERT_EQ(forwardLitPass->tags.Size(), 2u); - ASSERT_EQ(forwardLitPass->resources.Size(), 8u); - EXPECT_EQ(forwardLitPass->tags[0].name, "LightMode"); - EXPECT_EQ(forwardLitPass->tags[0].value, "ForwardBase"); - EXPECT_EQ(forwardLitPass->tags[1].name, "Queue"); - EXPECT_EQ(forwardLitPass->tags[1].value, "Geometry"); - - const ShaderResourceBindingDesc* baseTextureBinding = - shader->FindPassResourceBinding("ForwardLit", "BaseColorTexture"); - ASSERT_NE(baseTextureBinding, nullptr); - EXPECT_EQ(baseTextureBinding->type, ShaderResourceType::Texture2D); - EXPECT_EQ(baseTextureBinding->set, 5u); - EXPECT_EQ(baseTextureBinding->binding, 0u); - EXPECT_EQ(baseTextureBinding->semantic, "BaseColorTexture"); + EXPECT_TRUE(forwardLitPass->resources.Empty()); + ASSERT_NE(FindPassTag(forwardLitPass, "LightMode"), nullptr); + ASSERT_NE(FindPassTag(forwardLitPass, "Queue"), nullptr); + EXPECT_EQ(FindPassTag(forwardLitPass, "LightMode")->value, "ForwardBase"); + 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_0"); - EXPECT_NE(std::string(d3d12Vertex->sourceCode.CStr()).find("FORWARD_LIT_D3D12_VS"), std::string::npos); + 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, "main"); - EXPECT_EQ(openglFragment->profile, "fs_4_30"); - EXPECT_NE(std::string(openglFragment->sourceCode.CStr()).find("FORWARD_LIT_GL_PS"), std::string::npos); + EXPECT_EQ(openglFragment->entryPoint, "MainPS"); + EXPECT_EQ(openglFragment->profile, "ps_5_0"); + 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, "fs_4_50"); - EXPECT_NE(std::string(vulkanFragment->sourceCode.CStr()).find("FORWARD_LIT_VK_PS"), std::string::npos); + EXPECT_EQ(vulkanFragment->profile, "ps_5_0"); + 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); - EXPECT_EQ(shader->FindVariant("DepthOnly", ShaderType::Fragment, ShaderBackend::OpenGL), 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, LoadShaderManifestParsesVariantKeywordsAndKeywordAwareLookup) { +TEST(ShaderLoader, LoadShaderAuthoringRejectsNonAuthoringShaderFiles) { namespace fs = std::filesystem; - const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_manifest_variant_keywords"; - const fs::path manifestPath = shaderRoot / "variant_keywords.shader"; + 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 { - std::ofstream manifest(manifestPath); - ASSERT_TRUE(manifest.is_open()); - manifest << "{\n"; - manifest << " \"name\": \"VariantKeywordShader\",\n"; - manifest << " \"passes\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"ForwardLit\",\n"; - manifest << " \"variants\": [\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"sourceCode\": \"#version 430\\nvoid main() {}\\n\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"sourceCode\": \"#version 430\\n// BASE_FRAGMENT\\nvoid main() {}\\n\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"keywords\": [\"XC_ALPHA_TEST\", \"_\", \"XC_FOG\", \"XC_ALPHA_TEST\"], \"sourceCode\": \"#version 430\\n// KEYWORD_FRAGMENT\\nvoid main() {}\\n\" }\n"; - manifest << " ]\n"; - manifest << " }\n"; - manifest << " ]\n"; - manifest << "}\n"; + 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(manifestPath.string().c_str()); + LoadResult result = loader.Load(shaderPath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); @@ -289,7 +290,8 @@ TEST(ShaderLoader, LoadShaderManifestParsesVariantKeywordsAndKeywordAwareLookup) const ShaderStageVariant* baseFragment = shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL); ASSERT_NE(baseFragment, nullptr); - EXPECT_NE(std::string(baseFragment->sourceCode.CStr()).find("BASE_FRAGMENT"), std::string::npos); + 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"); @@ -302,7 +304,9 @@ TEST(ShaderLoader, LoadShaderManifestParsesVariantKeywordsAndKeywordAwareLookup) ShaderBackend::OpenGL, enabledKeywords); ASSERT_NE(keywordFragment, nullptr); - EXPECT_NE(std::string(keywordFragment->sourceCode.CStr()).find("KEYWORD_FRAGMENT"), std::string::npos); + 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"); @@ -477,7 +481,23 @@ TEST(ShaderLoader, LoadShaderAuthoringParsesPassStateAndFallback) { 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 @@ -510,7 +530,82 @@ TEST(ShaderLoader, LoadShaderAuthoringParsesPassStateAndFallback) { 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); @@ -539,7 +634,19 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringPreservesPassS 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 @@ -575,7 +682,23 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringPreservesPassS 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(); @@ -664,6 +787,18 @@ TEST(ShaderLoader, LoadShaderAuthoringParsesFallbackAndFixedFunctionStateInherit 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" @@ -702,7 +837,23 @@ TEST(ShaderLoader, LoadShaderAuthoringParsesFallbackAndFixedFunctionStateInherit 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); @@ -720,6 +871,328 @@ TEST(ShaderLoader, LoadShaderAuthoringParsesFallbackAndFixedFunctionStateInherit 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, LoadShaderAuthoringRejectsLegacyBackendPragma) { namespace fs = std::filesystem; @@ -806,40 +1279,45 @@ TEST(ShaderLoader, LoadShaderAuthoringRejectsLegacyResourcesBlock) { fs::remove_all(shaderRoot); } -TEST(ShaderLoader, ResourceManagerLoadsShaderManifestRelativeToResourceRoot) { +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_manifest_resource_root"; + const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_authoring_resource_root"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; - const fs::path manifestPath = shaderDir / "simple.shader"; + const fs::path shaderPath = 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"); - + WriteTextFile( + shaderPath, + R"(Shader "SimpleShader" +{ + SubShader { - 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"; + 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()); @@ -852,7 +1330,7 @@ TEST(ShaderLoader, ResourceManagerLoadsShaderManifestRelativeToResourceRoot) { 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); + EXPECT_NE(std::string(vertexVariant->sourceCode.CStr()).find("SIMPLE_AUTHORING_VS"), std::string::npos); } fs::current_path(previousPath); @@ -861,50 +1339,113 @@ TEST(ShaderLoader, ResourceManagerLoadsShaderManifestRelativeToResourceRoot) { fs::remove_all(projectRoot); } -TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { +TEST(ShaderLoader, ResourceManagerLoadsProjectUsePassRelativeToResourceRoot) { namespace fs = std::filesystem; - const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_artifact_test"; + 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 manifestPath = shaderDir / "lit.shader"; + const fs::path sharedShaderPath = shaderDir / "shared.shader"; + const fs::path mainShaderPath = shaderDir / "main.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"); + 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()); { - std::ofstream manifest(manifestPath); - ASSERT_TRUE(manifest.is_open()); - manifest << "{\n"; - manifest << " \"name\": \"ArtifactShader\",\n"; - manifest << " \"properties\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"_MainTex\",\n"; - manifest << " \"displayName\": \"Main Tex\",\n"; - manifest << " \"type\": \"2D\",\n"; - manifest << " \"defaultValue\": \"white\",\n"; - manifest << " \"semantic\": \"BaseColorTexture\"\n"; - manifest << " }\n"; - manifest << " ],\n"; - manifest << " \"passes\": [\n"; - manifest << " {\n"; - manifest << " \"name\": \"ForwardLit\",\n"; - manifest << " \"tags\": { \"LightMode\": \"ForwardBase\" },\n"; - manifest << " \"resources\": [\n"; - manifest << " { \"name\": \"BaseColorTexture\", \"type\": \"Texture2D\", \"set\": 3, \"binding\": 0, \"semantic\": \"BaseColorTexture\" }\n"; - manifest << " ],\n"; - manifest << " \"variants\": [\n"; - manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.vert.glsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"lit.frag.glsl\" },\n"; - manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"keywords\": [\"XC_ALPHA_TEST\", \"XC_FOG\"], \"sourceCode\": \"#version 430\\n// ARTIFACT_GL_PS_KEYWORD\\nvoid main() {}\\n\" }\n"; - manifest << " ]\n"; - manifest << " }\n"; - manifest << " ]\n"; - manifest << "}\n"; + 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" = "ForwardBase" } + 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()); @@ -925,20 +1466,19 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { EXPECT_EQ(shader->GetName(), "ArtifactShader"); EXPECT_EQ( fs::path(shader->GetPath().CStr()).lexically_normal().generic_string(), - manifestPath.lexically_normal().generic_string()); + shaderPath.lexically_normal().generic_string()); const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); - ASSERT_EQ(pass->variants.Size(), 3u); - ASSERT_EQ(pass->resources.Size(), 1u); + ASSERT_EQ(pass->variants.Size(), 4u); + EXPECT_TRUE(pass->resources.Empty()); 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); + EXPECT_NE(std::string(fragmentVariant->sourceCode.CStr()).find("ARTIFACT_AUTHORING_PS"), std::string::npos); ShaderKeywordSet enabledKeywords = {}; - enabledKeywords.enabledKeywords.PushBack("XC_FOG"); enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST"); const ShaderStageVariant* keywordVariant = @@ -948,10 +1488,10 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactAndLoaderReadsItBack) { ShaderBackend::OpenGL, enabledKeywords); ASSERT_NE(keywordVariant, nullptr); - EXPECT_NE(std::string(keywordVariant->sourceCode.CStr()).find("ARTIFACT_GL_PS_KEYWORD"), std::string::npos); - ASSERT_EQ(keywordVariant->requiredKeywords.enabledKeywords.Size(), 2u); + EXPECT_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"); - EXPECT_EQ(keywordVariant->requiredKeywords.enabledKeywords[1], "XC_FOG"); delete shader; database.Shutdown(); @@ -1134,37 +1674,35 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringAndTracksInclu fs::remove_all(projectRoot); } -TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenStageDependencyChanges) { +TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenAuthoringFileChanges) { 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 projectRoot = fs::temp_directory_path() / "xc_shader_authoring_reimport_test"; const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; - const fs::path manifestPath = shaderDir / "lit.shader"; - const fs::path fragmentPath = shaderDir / "lit.frag.glsl"; + const fs::path shaderPath = shaderDir / "lit.shader"; 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"); - + WriteTextFile( + shaderPath, + R"(Shader "DependencyShader" +{ + SubShader { - 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"; + 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()); @@ -1177,10 +1715,10 @@ TEST(ShaderLoader, AssetDatabaseReimportsShaderWhenStageDependencyChanges) { 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)); + 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()); @@ -1918,6 +2456,65 @@ TEST(ShaderLoader, LoadBuiltinColorScalePostProcessShaderBuildsAuthoringVariants 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_0"); + EXPECT_NE( + std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_OUTLINE_D3D12_VS"), + 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_0"); + 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(); @@ -1990,6 +2587,12 @@ TEST(ShaderLoader, ResourceManagerLoadsBuiltinShadersOutsideProjectWorkingDirect 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);