From 87533e08f6384f46e18ee1d66761af388f24465a Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 00:34:28 +0800 Subject: [PATCH] rendering: formalize unity-style shader pass contracts --- .../XCEngine/Core/Asset/ArtifactFormats.h | 27 +- .../Builtin/BuiltinPassLayoutUtils.h | 149 ++++++ .../Materials/RenderMaterialStateUtils.h | 80 +-- .../XCEngine/Resources/Material/Material.h | 85 +-- .../Resources/Material/MaterialRenderState.h | 90 ++++ .../XCEngine/Resources/Shader/Shader.h | 6 + engine/src/Core/Asset/AssetDatabase.cpp | 6 +- .../BuiltinDepthStylePassBaseResources.cpp | 32 +- .../Passes/BuiltinObjectIdPassResources.cpp | 12 +- .../BuiltinForwardPipelineResources.cpp | 16 +- engine/src/Resources/Shader/ShaderLoader.cpp | 505 +++++++++++++++++- .../unit/test_builtin_forward_pipeline.cpp | 63 +++ tests/Resources/Shader/test_shader_loader.cpp | 226 +++++++- 13 files changed, 1133 insertions(+), 164 deletions(-) create mode 100644 engine/include/XCEngine/Resources/Material/MaterialRenderState.h diff --git a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h index 0bd8aa59..48583656 100644 --- a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h +++ b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h @@ -11,9 +11,9 @@ namespace XCEngine { namespace Resources { constexpr Core::uint32 kTextureArtifactSchemaVersion = 1; -constexpr Core::uint32 kMaterialArtifactSchemaVersion = 3; +constexpr Core::uint32 kMaterialArtifactSchemaVersion = 4; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; -constexpr Core::uint32 kShaderArtifactSchemaVersion = 3; +constexpr Core::uint32 kShaderArtifactSchemaVersion = 4; constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2; struct TextureArtifactHeader { @@ -46,7 +46,7 @@ struct MeshArtifactHeader { }; struct MaterialArtifactFileHeader { - char magic[8] = { 'X', 'C', 'M', 'A', 'T', '0', '3', '\0' }; + char magic[8] = { 'X', 'C', 'M', 'A', 'T', '0', '4', '\0' }; Core::uint32 schemaVersion = kMaterialArtifactSchemaVersion; }; @@ -58,10 +58,20 @@ struct MaterialArtifactHeaderV2 { Core::uint32 textureBindingCount = 0; }; +struct MaterialArtifactHeaderV3 { + Core::int32 renderQueue = static_cast(MaterialRenderQueue::Geometry); + MaterialRenderState renderState = {}; + Core::uint32 tagCount = 0; + Core::uint32 keywordCount = 0; + Core::uint32 propertyCount = 0; + Core::uint32 textureBindingCount = 0; +}; + struct MaterialArtifactHeader { Core::int32 renderQueue = static_cast(MaterialRenderQueue::Geometry); MaterialRenderState renderState = {}; Core::uint32 tagCount = 0; + Core::uint32 hasRenderStateOverride = 0; Core::uint32 keywordCount = 0; Core::uint32 propertyCount = 0; Core::uint32 textureBindingCount = 0; @@ -73,7 +83,7 @@ struct MaterialPropertyArtifact { }; struct ShaderArtifactFileHeader { - char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '3', '\0' }; + char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '4', '\0' }; Core::uint32 schemaVersion = kShaderArtifactSchemaVersion; }; @@ -95,6 +105,15 @@ struct ShaderPassArtifactHeader { Core::uint32 variantCount = 0; }; +struct ShaderPassArtifactHeaderV4 { + Core::uint32 tagCount = 0; + Core::uint32 resourceCount = 0; + Core::uint32 keywordDeclarationCount = 0; + Core::uint32 variantCount = 0; + Core::uint32 hasFixedFunctionState = 0; + MaterialRenderState fixedFunctionState = {}; +}; + struct ShaderPropertyArtifact { Core::uint32 propertyType = 0; }; diff --git a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h index 93435cb1..c4e45d8d 100644 --- a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h +++ b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h @@ -115,6 +115,155 @@ inline bool TryBuildBuiltinPassResourceBindingPlan( return true; } +inline void AppendBuiltinPassResourceBinding( + Containers::Array& bindings, + const char* name, + Resources::ShaderResourceType type, + Core::uint32 set, + Core::uint32 binding, + const char* semantic) { + Resources::ShaderResourceBindingDesc desc = {}; + desc.name = name; + desc.type = type; + desc.set = set; + desc.binding = binding; + desc.semantic = semantic; + bindings.PushBack(desc); +} + +inline bool TryBuildImplicitBuiltinPassResourceBindings( + const Resources::ShaderPass& shaderPass, + Containers::Array& outBindings) { + outBindings.Clear(); + + if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::ForwardLit)) { + AppendBuiltinPassResourceBinding( + outBindings, + "PerObjectConstants", + Resources::ShaderResourceType::ConstantBuffer, + 0u, + 0u, + "PerObject"); + AppendBuiltinPassResourceBinding( + outBindings, + "LightingConstants", + Resources::ShaderResourceType::ConstantBuffer, + 1u, + 0u, + "Lighting"); + AppendBuiltinPassResourceBinding( + outBindings, + "MaterialConstants", + Resources::ShaderResourceType::ConstantBuffer, + 2u, + 0u, + "Material"); + AppendBuiltinPassResourceBinding( + outBindings, + "ShadowReceiverConstants", + Resources::ShaderResourceType::ConstantBuffer, + 3u, + 0u, + "ShadowReceiver"); + AppendBuiltinPassResourceBinding( + outBindings, + "BaseColorTexture", + Resources::ShaderResourceType::Texture2D, + 4u, + 0u, + "BaseColorTexture"); + AppendBuiltinPassResourceBinding( + outBindings, + "LinearClampSampler", + Resources::ShaderResourceType::Sampler, + 5u, + 0u, + "LinearClampSampler"); + AppendBuiltinPassResourceBinding( + outBindings, + "ShadowMapTexture", + Resources::ShaderResourceType::Texture2D, + 6u, + 0u, + "ShadowMapTexture"); + AppendBuiltinPassResourceBinding( + outBindings, + "ShadowMapSampler", + Resources::ShaderResourceType::Sampler, + 7u, + 0u, + "ShadowMapSampler"); + return true; + } + + if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::Unlit) || + ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::DepthOnly) || + ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::ShadowCaster)) { + AppendBuiltinPassResourceBinding( + outBindings, + "PerObjectConstants", + Resources::ShaderResourceType::ConstantBuffer, + 0u, + 0u, + "PerObject"); + AppendBuiltinPassResourceBinding( + outBindings, + "MaterialConstants", + Resources::ShaderResourceType::ConstantBuffer, + 1u, + 0u, + "Material"); + AppendBuiltinPassResourceBinding( + outBindings, + "BaseColorTexture", + Resources::ShaderResourceType::Texture2D, + 2u, + 0u, + "BaseColorTexture"); + AppendBuiltinPassResourceBinding( + outBindings, + "LinearClampSampler", + Resources::ShaderResourceType::Sampler, + 3u, + 0u, + "LinearClampSampler"); + return true; + } + + if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::ObjectId)) { + AppendBuiltinPassResourceBinding( + outBindings, + "PerObjectConstants", + Resources::ShaderResourceType::ConstantBuffer, + 0u, + 0u, + "PerObject"); + return true; + } + + return false; +} + +inline bool TryBuildBuiltinPassResourceBindingPlan( + const Resources::ShaderPass& shaderPass, + BuiltinPassResourceBindingPlan& outPlan, + Containers::String* outError = nullptr) { + if (!shaderPass.resources.Empty()) { + return TryBuildBuiltinPassResourceBindingPlan(shaderPass.resources, outPlan, outError); + } + + Containers::Array implicitBindings; + if (!TryBuildImplicitBuiltinPassResourceBindings(shaderPass, implicitBindings)) { + if (outError != nullptr) { + *outError = "Builtin pass does not declare explicit bindings and no implicit contract is available"; + } + outPlan = {}; + return false; + } + + return TryBuildBuiltinPassResourceBindingPlan(implicitBindings, outPlan, outError); +} + inline RHI::DescriptorType ToBuiltinPassDescriptorType(Resources::ShaderResourceType type) { switch (type) { case Resources::ShaderResourceType::ConstantBuffer: diff --git a/engine/include/XCEngine/Rendering/Materials/RenderMaterialStateUtils.h b/engine/include/XCEngine/Rendering/Materials/RenderMaterialStateUtils.h index 9bb215ff..717e0f6d 100644 --- a/engine/include/XCEngine/Rendering/Materials/RenderMaterialStateUtils.h +++ b/engine/include/XCEngine/Rendering/Materials/RenderMaterialStateUtils.h @@ -2,6 +2,7 @@ #include #include +#include namespace XCEngine { namespace Rendering { @@ -96,59 +97,74 @@ inline RHI::BlendOp ToRHIBlendOp(Resources::MaterialBlendOp op) { } } -inline RHI::RasterizerDesc BuildRasterizerState(const Resources::Material* material) { +inline Resources::MaterialRenderState ResolveEffectiveRenderState( + const Resources::ShaderPass* shaderPass, + const Resources::Material* material) { + Resources::MaterialRenderState renderState = {}; + + if (shaderPass != nullptr && shaderPass->hasFixedFunctionState) { + renderState = shaderPass->fixedFunctionState; + } else if (material != nullptr) { + renderState = material->GetRenderState(); + } + + if (material != nullptr && material->HasRenderStateOverride()) { + renderState = material->GetRenderState(); + } + + return renderState; +} + +inline RHI::RasterizerDesc BuildRasterizerState(const Resources::MaterialRenderState& renderState) { RHI::RasterizerDesc desc = {}; desc.fillMode = static_cast(RHI::FillMode::Solid); desc.cullMode = static_cast(RHI::CullMode::None); desc.frontFace = static_cast(RHI::FrontFace::CounterClockwise); desc.depthClipEnable = true; - - if (material != nullptr) { - const Resources::MaterialRenderState& renderState = material->GetRenderState(); - desc.cullMode = static_cast(ToRHICullMode(renderState.cullMode)); - } + desc.cullMode = static_cast(ToRHICullMode(renderState.cullMode)); return desc; } -inline RHI::BlendDesc BuildBlendState(const Resources::Material* material) { +inline RHI::BlendDesc BuildBlendState(const Resources::MaterialRenderState& renderState) { RHI::BlendDesc desc = {}; - if (material != nullptr) { - const Resources::MaterialRenderState& renderState = material->GetRenderState(); - desc.blendEnable = renderState.blendEnable; - desc.srcBlend = static_cast(ToRHIBlendFactor(renderState.srcBlend)); - desc.dstBlend = static_cast(ToRHIBlendFactor(renderState.dstBlend)); - desc.srcBlendAlpha = static_cast(ToRHIBlendFactor(renderState.srcBlendAlpha)); - desc.dstBlendAlpha = static_cast(ToRHIBlendFactor(renderState.dstBlendAlpha)); - desc.blendOp = static_cast(ToRHIBlendOp(renderState.blendOp)); - desc.blendOpAlpha = static_cast(ToRHIBlendOp(renderState.blendOpAlpha)); - desc.colorWriteMask = renderState.colorWriteMask; - } + desc.blendEnable = renderState.blendEnable; + desc.srcBlend = static_cast(ToRHIBlendFactor(renderState.srcBlend)); + desc.dstBlend = static_cast(ToRHIBlendFactor(renderState.dstBlend)); + desc.srcBlendAlpha = static_cast(ToRHIBlendFactor(renderState.srcBlendAlpha)); + desc.dstBlendAlpha = static_cast(ToRHIBlendFactor(renderState.dstBlendAlpha)); + desc.blendOp = static_cast(ToRHIBlendOp(renderState.blendOp)); + desc.blendOpAlpha = static_cast(ToRHIBlendOp(renderState.blendOpAlpha)); + desc.colorWriteMask = renderState.colorWriteMask; return desc; } -inline RHI::DepthStencilStateDesc BuildDepthStencilState(const Resources::Material* material) { +inline RHI::DepthStencilStateDesc BuildDepthStencilState(const Resources::MaterialRenderState& renderState) { RHI::DepthStencilStateDesc desc = {}; - desc.depthTestEnable = true; - desc.depthWriteEnable = true; - desc.depthFunc = static_cast(RHI::ComparisonFunc::Less); + desc.depthTestEnable = renderState.depthTestEnable; + desc.depthWriteEnable = renderState.depthWriteEnable; + desc.depthFunc = static_cast(ToRHIComparisonFunc(renderState.depthFunc)); desc.stencilEnable = false; - if (material != nullptr) { - const Resources::MaterialRenderState& renderState = material->GetRenderState(); - desc.depthTestEnable = renderState.depthTestEnable; - desc.depthWriteEnable = renderState.depthWriteEnable; - desc.depthFunc = static_cast(ToRHIComparisonFunc(renderState.depthFunc)); - } - return desc; } +inline void ApplyRenderState(const Resources::MaterialRenderState& renderState, RHI::GraphicsPipelineDesc& pipelineDesc) { + pipelineDesc.rasterizerState = BuildRasterizerState(renderState); + pipelineDesc.blendState = BuildBlendState(renderState); + pipelineDesc.depthStencilState = BuildDepthStencilState(renderState); +} + inline void ApplyMaterialRenderState(const Resources::Material* material, RHI::GraphicsPipelineDesc& pipelineDesc) { - pipelineDesc.rasterizerState = BuildRasterizerState(material); - pipelineDesc.blendState = BuildBlendState(material); - pipelineDesc.depthStencilState = BuildDepthStencilState(material); + ApplyRenderState(ResolveEffectiveRenderState(nullptr, material), pipelineDesc); +} + +inline void ApplyResolvedRenderState( + const Resources::ShaderPass* shaderPass, + const Resources::Material* material, + RHI::GraphicsPipelineDesc& pipelineDesc) { + ApplyRenderState(ResolveEffectiveRenderState(shaderPass, material), pipelineDesc); } struct MaterialRenderStateHash { diff --git a/engine/include/XCEngine/Resources/Material/Material.h b/engine/include/XCEngine/Resources/Material/Material.h index 677899bd..0960e068 100644 --- a/engine/include/XCEngine/Resources/Material/Material.h +++ b/engine/include/XCEngine/Resources/Material/Material.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -25,87 +26,6 @@ enum class MaterialRenderQueue : Core::int32 { Overlay = 4000 }; -enum class MaterialCullMode : Core::uint8 { - None = 0, - Front = 1, - Back = 2 -}; - -enum class MaterialComparisonFunc : Core::uint8 { - Never = 0, - Less = 1, - Equal = 2, - LessEqual = 3, - Greater = 4, - NotEqual = 5, - GreaterEqual = 6, - Always = 7 -}; - -enum class MaterialBlendOp : Core::uint8 { - Add = 0, - Subtract = 1, - ReverseSubtract = 2, - Min = 3, - Max = 4 -}; - -enum class MaterialBlendFactor : Core::uint8 { - Zero = 0, - One = 1, - SrcColor = 2, - InvSrcColor = 3, - SrcAlpha = 4, - InvSrcAlpha = 5, - DstAlpha = 6, - InvDstAlpha = 7, - DstColor = 8, - InvDstColor = 9, - SrcAlphaSat = 10, - BlendFactor = 11, - InvBlendFactor = 12, - Src1Color = 13, - InvSrc1Color = 14, - Src1Alpha = 15, - InvSrc1Alpha = 16 -}; - -struct MaterialRenderState { - bool blendEnable = false; - MaterialBlendFactor srcBlend = MaterialBlendFactor::One; - MaterialBlendFactor dstBlend = MaterialBlendFactor::Zero; - MaterialBlendFactor srcBlendAlpha = MaterialBlendFactor::One; - MaterialBlendFactor dstBlendAlpha = MaterialBlendFactor::Zero; - MaterialBlendOp blendOp = MaterialBlendOp::Add; - MaterialBlendOp blendOpAlpha = MaterialBlendOp::Add; - Core::uint8 colorWriteMask = 0xF; - - bool depthTestEnable = true; - bool depthWriteEnable = true; - MaterialComparisonFunc depthFunc = MaterialComparisonFunc::Less; - - MaterialCullMode cullMode = MaterialCullMode::None; - - bool operator==(const MaterialRenderState& other) const { - return blendEnable == other.blendEnable && - srcBlend == other.srcBlend && - dstBlend == other.dstBlend && - srcBlendAlpha == other.srcBlendAlpha && - dstBlendAlpha == other.dstBlendAlpha && - blendOp == other.blendOp && - blendOpAlpha == other.blendOpAlpha && - colorWriteMask == other.colorWriteMask && - depthTestEnable == other.depthTestEnable && - depthWriteEnable == other.depthWriteEnable && - depthFunc == other.depthFunc && - cullMode == other.cullMode; - } - - bool operator!=(const MaterialRenderState& other) const { - return !(*this == other); - } -}; - enum class MaterialPropertyType { Float, Float2, Float3, Float4, Int, Int2, Int3, Int4, @@ -179,6 +99,8 @@ public: void SetRenderState(const MaterialRenderState& renderState); const MaterialRenderState& GetRenderState() const { return m_renderState; } + bool HasRenderStateOverride() const { return m_hasRenderStateOverride; } + void SetRenderStateOverrideEnabled(bool enabled); void SetShaderPass(const Containers::String& shaderPass); const Containers::String& GetShaderPass() const { return m_shaderPass; } @@ -256,6 +178,7 @@ private: ResourceHandle m_shader; Core::int32 m_renderQueue = static_cast(MaterialRenderQueue::Geometry); MaterialRenderState m_renderState; + bool m_hasRenderStateOverride = false; Containers::String m_shaderPass; Containers::Array m_tags; ShaderKeywordSet m_keywordSet; diff --git a/engine/include/XCEngine/Resources/Material/MaterialRenderState.h b/engine/include/XCEngine/Resources/Material/MaterialRenderState.h new file mode 100644 index 00000000..0f9417a7 --- /dev/null +++ b/engine/include/XCEngine/Resources/Material/MaterialRenderState.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Resources { + +enum class MaterialCullMode : Core::uint8 { + None = 0, + Front = 1, + Back = 2 +}; + +enum class MaterialComparisonFunc : Core::uint8 { + Never = 0, + Less = 1, + Equal = 2, + LessEqual = 3, + Greater = 4, + NotEqual = 5, + GreaterEqual = 6, + Always = 7 +}; + +enum class MaterialBlendOp : Core::uint8 { + Add = 0, + Subtract = 1, + ReverseSubtract = 2, + Min = 3, + Max = 4 +}; + +enum class MaterialBlendFactor : Core::uint8 { + Zero = 0, + One = 1, + SrcColor = 2, + InvSrcColor = 3, + SrcAlpha = 4, + InvSrcAlpha = 5, + DstAlpha = 6, + InvDstAlpha = 7, + DstColor = 8, + InvDstColor = 9, + SrcAlphaSat = 10, + BlendFactor = 11, + InvBlendFactor = 12, + Src1Color = 13, + InvSrc1Color = 14, + Src1Alpha = 15, + InvSrc1Alpha = 16 +}; + +struct MaterialRenderState { + bool blendEnable = false; + MaterialBlendFactor srcBlend = MaterialBlendFactor::One; + MaterialBlendFactor dstBlend = MaterialBlendFactor::Zero; + MaterialBlendFactor srcBlendAlpha = MaterialBlendFactor::One; + MaterialBlendFactor dstBlendAlpha = MaterialBlendFactor::Zero; + MaterialBlendOp blendOp = MaterialBlendOp::Add; + MaterialBlendOp blendOpAlpha = MaterialBlendOp::Add; + Core::uint8 colorWriteMask = 0xF; + + bool depthTestEnable = true; + bool depthWriteEnable = true; + MaterialComparisonFunc depthFunc = MaterialComparisonFunc::Less; + + MaterialCullMode cullMode = MaterialCullMode::None; + + bool operator==(const MaterialRenderState& other) const { + return blendEnable == other.blendEnable && + srcBlend == other.srcBlend && + dstBlend == other.dstBlend && + srcBlendAlpha == other.srcBlendAlpha && + dstBlendAlpha == other.dstBlendAlpha && + blendOp == other.blendOp && + blendOpAlpha == other.blendOpAlpha && + colorWriteMask == other.colorWriteMask && + depthTestEnable == other.depthTestEnable && + depthWriteEnable == other.depthWriteEnable && + depthFunc == other.depthFunc && + cullMode == other.cullMode; + } + + bool operator!=(const MaterialRenderState& other) const { + return !(*this == other); + } +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/include/XCEngine/Resources/Shader/Shader.h b/engine/include/XCEngine/Resources/Shader/Shader.h index de55c526..4586fc1c 100644 --- a/engine/include/XCEngine/Resources/Shader/Shader.h +++ b/engine/include/XCEngine/Resources/Shader/Shader.h @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace XCEngine { @@ -97,6 +98,8 @@ struct ShaderStageVariant { struct ShaderPass { Containers::String name; + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState; Containers::Array tags; Containers::Array resources; Containers::Array keywordDeclarations; @@ -138,6 +141,8 @@ public: void ClearProperties(); const Containers::Array& GetProperties() const { return m_properties; } const ShaderPropertyDesc* FindProperty(const Containers::String& propertyName) const; + void SetFallback(const Containers::String& fallback); + const Containers::String& GetFallback() const { return m_fallback; } void AddPass(const ShaderPass& pass); void ClearPasses(); @@ -189,6 +194,7 @@ private: Containers::Array m_attributes; Containers::Array m_properties; Containers::Array m_passes; + Containers::String m_fallback; class IRHIShader* m_rhiResource = nullptr; }; diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index 4f274625..a02902df 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -451,6 +451,7 @@ bool WriteMaterialArtifactFile( header.renderQueue = material.GetRenderQueue(); header.renderState = material.GetRenderState(); header.tagCount = material.GetTagCount(); + header.hasRenderStateOverride = material.HasRenderStateOverride() ? 1u : 0u; header.keywordCount = material.GetKeywordCount(); const std::vector properties = GatherMaterialProperties(material); @@ -510,6 +511,7 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) WriteString(output, shader.GetName()); WriteString(output, NormalizeArtifactPathString(shader.GetPath())); + WriteString(output, shader.GetFallback()); ShaderArtifactHeader header; header.propertyCount = static_cast(shader.GetProperties().Size()); @@ -530,11 +532,13 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) for (const ShaderPass& pass : shader.GetPasses()) { WriteString(output, pass.name); - ShaderPassArtifactHeader passHeader; + ShaderPassArtifactHeaderV4 passHeader; passHeader.tagCount = static_cast(pass.tags.Size()); passHeader.resourceCount = static_cast(pass.resources.Size()); passHeader.keywordDeclarationCount = static_cast(pass.keywordDeclarations.Size()); passHeader.variantCount = static_cast(pass.variants.Size()); + passHeader.hasFixedFunctionState = pass.hasFixedFunctionState ? 1u : 0u; + passHeader.fixedFunctionState = pass.fixedFunctionState; output.write(reinterpret_cast(&passHeader), sizeof(passHeader)); for (const ShaderPassTagEntry& tag : pass.tags) { diff --git a/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp b/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp index 0c308fa4..4e786d4d 100644 --- a/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp +++ b/engine/src/Rendering/Passes/BuiltinDepthStylePassBaseResources.cpp @@ -100,6 +100,7 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( RHI::RHIType backendType, RHI::RHIPipelineLayout* pipelineLayout, const Resources::Shader& shader, + const Resources::ShaderPass& shaderPass, const Containers::String& passName, const Resources::ShaderKeywordSet& keywordSet, const Resources::Material* material, @@ -117,13 +118,17 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( static_cast(ResolveSurfaceDepthFormat(surface)); pipelineDesc.sampleCount = 1; pipelineDesc.inputLayout = inputLayout; - ApplyMaterialRenderState(material, pipelineDesc); + ApplyResolvedRenderState(&shaderPass, material, pipelineDesc); - pipelineDesc.blendState.blendEnable = false; - pipelineDesc.blendState.colorWriteMask = pipelineDesc.renderTargetCount > 0 ? 0xF : 0; - pipelineDesc.depthStencilState.depthTestEnable = true; - pipelineDesc.depthStencilState.depthWriteEnable = true; - pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + if (!shaderPass.hasFixedFunctionState) { + pipelineDesc.blendState.blendEnable = false; + pipelineDesc.blendState.colorWriteMask = pipelineDesc.renderTargetCount > 0 ? 0xF : 0; + pipelineDesc.depthStencilState.depthTestEnable = true; + pipelineDesc.depthStencilState.depthWriteEnable = true; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + } else if (pipelineDesc.renderTargetCount == 0) { + pipelineDesc.blendState.colorWriteMask = 0; + } const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(backendType); if (const Resources::ShaderStageVariant* vertexVariant = @@ -350,16 +355,7 @@ bool BuiltinDepthStylePassBase::TryBuildSupportedBindingPlan( const Resources::ShaderPass& shaderPass, BuiltinPassResourceBindingPlan& outPlan, Containers::String* outError) const { - if (shaderPass.resources.Empty()) { - if (outError != nullptr) { - *outError = - Containers::String("Builtin depth-style pass requires explicit resource bindings on shader pass: ") + - shaderPass.name; - } - return false; - } - - if (!TryBuildBuiltinPassResourceBindingPlan(shaderPass.resources, outPlan, outError)) { + if (!TryBuildBuiltinPassResourceBindingPlan(shaderPass, outPlan, outError)) { return false; } @@ -470,8 +466,7 @@ RHI::RHIPipelineState* BuiltinDepthStylePassBase::GetOrCreatePipelineState( } PipelineStateKey pipelineKey = {}; - pipelineKey.renderState = - material != nullptr ? material->GetRenderState() : Resources::MaterialRenderState(); + pipelineKey.renderState = ResolveEffectiveRenderState(resolvedShaderPass.pass, material); pipelineKey.shader = resolvedShaderPass.shader; pipelineKey.passName = resolvedShaderPass.passName; pipelineKey.keywordSignature = ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature(keywordSet); @@ -488,6 +483,7 @@ RHI::RHIPipelineState* BuiltinDepthStylePassBase::GetOrCreatePipelineState( context.backendType, passLayout->pipelineLayout, *resolvedShaderPass.shader, + *resolvedShaderPass.pass, resolvedShaderPass.passName, keywordSet, material, diff --git a/engine/src/Rendering/Passes/BuiltinObjectIdPassResources.cpp b/engine/src/Rendering/Passes/BuiltinObjectIdPassResources.cpp index c2c64f20..d0db32f1 100644 --- a/engine/src/Rendering/Passes/BuiltinObjectIdPassResources.cpp +++ b/engine/src/Rendering/Passes/BuiltinObjectIdPassResources.cpp @@ -132,19 +132,9 @@ bool BuiltinObjectIdPass::CreateResources(const RenderContext& context) { return false; } - const Containers::Array& resourceBindings = objectIdPass->resources; - if (resourceBindings.Empty()) { - Debug::Logger::Get().Error( - Debug::LogCategory::Rendering, - (Containers::String("BuiltinObjectIdPass requires explicit resource bindings on shader pass: ") + - objectIdPass->name).CStr()); - DestroyResources(); - return false; - } - BuiltinPassResourceBindingPlan bindingPlan = {}; Containers::String bindingPlanError; - if (!TryBuildBuiltinPassResourceBindingPlan(resourceBindings, bindingPlan, &bindingPlanError)) { + if (!TryBuildBuiltinPassResourceBindingPlan(*objectIdPass, bindingPlan, &bindingPlanError)) { Debug::Logger::Get().Error( Debug::LogCategory::Rendering, (Containers::String("BuiltinObjectIdPass failed to resolve pass resource bindings: ") + bindingPlanError).CStr()); diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp index d622bf4e..c731699a 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipelineResources.cpp @@ -109,6 +109,7 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( RHI::RHIType backendType, RHI::RHIPipelineLayout* pipelineLayout, const Resources::Shader& shader, + const Resources::ShaderPass& shaderPass, const Containers::String& passName, const Resources::ShaderKeywordSet& keywordSet, const Resources::Material* material) { @@ -119,7 +120,7 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); pipelineDesc.sampleCount = 1; - ApplyMaterialRenderState(material, pipelineDesc); + ApplyResolvedRenderState(&shaderPass, material, pipelineDesc); pipelineDesc.inputLayout = BuiltinForwardPipeline::BuildInputLayout(); @@ -216,20 +217,15 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP return nullptr; }; - const Containers::Array& resourceBindings = resolvedShaderPass.pass->resources; - if (resourceBindings.Empty()) { - return failLayout("BuiltinForwardPipeline requires explicit resource bindings on the resolved shader pass"); - } - BuiltinPassResourceBindingPlan bindingPlan = {}; Containers::String bindingPlanError; - if (!TryBuildBuiltinPassResourceBindingPlan(resourceBindings, bindingPlan, &bindingPlanError)) { + if (!TryBuildBuiltinPassResourceBindingPlan(*resolvedShaderPass.pass, bindingPlan, &bindingPlanError)) { const Containers::String contextualError = Containers::String("BuiltinForwardPipeline failed to resolve pass resource bindings for shader='") + resolvedShaderPass.shader->GetPath() + "', pass='" + resolvedShaderPass.passName + "': " + bindingPlanError + - ". Bindings: " + DescribeShaderResourceBindings(resourceBindings); + ". Bindings: " + DescribeShaderResourceBindings(resolvedShaderPass.pass->resources); return failLayout(contextualError.CStr()); } @@ -299,8 +295,7 @@ RHI::RHIPipelineState* BuiltinForwardPipeline::GetOrCreatePipelineState( } PipelineStateKey pipelineKey = {}; - pipelineKey.renderState = - material != nullptr ? material->GetRenderState() : Resources::MaterialRenderState(); + pipelineKey.renderState = ResolveEffectiveRenderState(resolvedShaderPass.pass, material); pipelineKey.shader = resolvedShaderPass.shader; pipelineKey.passName = resolvedShaderPass.passName; pipelineKey.keywordSignature = ::XCEngine::Rendering::Detail::BuildShaderKeywordSignature(keywordSet); @@ -315,6 +310,7 @@ RHI::RHIPipelineState* BuiltinForwardPipeline::GetOrCreatePipelineState( context.backendType, passLayout->pipelineLayout, *resolvedShaderPass.shader, + *resolvedShaderPass.pass, resolvedShaderPass.passName, keywordSet, material); diff --git a/engine/src/Resources/Shader/ShaderLoader.cpp b/engine/src/Resources/Shader/ShaderLoader.cpp index b787465c..88a135e7 100644 --- a/engine/src/Resources/Shader/ShaderLoader.cpp +++ b/engine/src/Resources/Shader/ShaderLoader.cpp @@ -615,6 +615,10 @@ bool ReadTextFile(const Containers::String& path, Containers::String& outText) { size_t CalculateShaderMemorySize(const Shader& shader); bool TryTokenizeQuotedArguments(const std::string& line, std::vector& outTokens); +MaterialRenderState BuildUnityDefaultFixedFunctionState(); +void EnsureAuthoringFixedFunctionStateInitialized( + bool& hasFixedFunctionState, + MaterialRenderState& fixedFunctionState); enum class ShaderAuthoringStyle { NotShaderAuthoring = 0, @@ -638,6 +642,8 @@ struct AuthoringBackendVariantEntry { struct AuthoringPassEntry { Containers::String name; + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState = {}; std::vector tags; Containers::Array resources; Containers::Array keywordDeclarations; @@ -650,6 +656,8 @@ struct AuthoringPassEntry { }; struct AuthoringSubShaderEntry { + bool hasFixedFunctionState = false; + MaterialRenderState fixedFunctionState = {}; std::vector tags; Containers::String sharedProgramSource; std::vector passes; @@ -657,6 +665,7 @@ struct AuthoringSubShaderEntry { struct AuthoringShaderDesc { Containers::String name; + Containers::String fallback; Containers::String sharedProgramSource; Containers::Array properties; std::vector subShaders; @@ -845,6 +854,258 @@ bool ContainsSingleSourceAuthoringConstructs(const std::vector& lin return false; } +MaterialRenderState BuildUnityDefaultFixedFunctionState() { + MaterialRenderState state = {}; + state.blendEnable = false; + state.srcBlend = MaterialBlendFactor::One; + state.dstBlend = MaterialBlendFactor::Zero; + state.srcBlendAlpha = MaterialBlendFactor::One; + state.dstBlendAlpha = MaterialBlendFactor::Zero; + state.blendOp = MaterialBlendOp::Add; + state.blendOpAlpha = MaterialBlendOp::Add; + state.colorWriteMask = 0xF; + state.depthTestEnable = true; + state.depthWriteEnable = true; + state.depthFunc = MaterialComparisonFunc::LessEqual; + state.cullMode = MaterialCullMode::Back; + return state; +} + +void EnsureAuthoringFixedFunctionStateInitialized( + bool& hasFixedFunctionState, + MaterialRenderState& fixedFunctionState) { + if (!hasFixedFunctionState) { + hasFixedFunctionState = true; + fixedFunctionState = BuildUnityDefaultFixedFunctionState(); + } +} + +bool TryParseUnityStyleBoolDirectiveToken(const std::string& token, bool& outValue) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "on") { + outValue = true; + return true; + } + if (normalized == "off") { + outValue = false; + return true; + } + return false; +} + +bool TryParseUnityStyleCullMode(const std::string& token, MaterialCullMode& outMode) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "back") { + outMode = MaterialCullMode::Back; + return true; + } + if (normalized == "front") { + outMode = MaterialCullMode::Front; + return true; + } + if (normalized == "off") { + outMode = MaterialCullMode::None; + return true; + } + return false; +} + +bool TryParseUnityStyleComparisonFunc(const std::string& token, MaterialComparisonFunc& outFunc) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "never") { + outFunc = MaterialComparisonFunc::Never; + return true; + } + if (normalized == "less") { + outFunc = MaterialComparisonFunc::Less; + return true; + } + if (normalized == "equal") { + outFunc = MaterialComparisonFunc::Equal; + return true; + } + if (normalized == "lequal" || normalized == "lessequal" || normalized == "less_equal") { + outFunc = MaterialComparisonFunc::LessEqual; + return true; + } + if (normalized == "greater") { + outFunc = MaterialComparisonFunc::Greater; + return true; + } + if (normalized == "notequal" || normalized == "not_equal") { + outFunc = MaterialComparisonFunc::NotEqual; + return true; + } + if (normalized == "gequal" || normalized == "greaterequal" || normalized == "greater_equal") { + outFunc = MaterialComparisonFunc::GreaterEqual; + return true; + } + if (normalized == "always") { + outFunc = MaterialComparisonFunc::Always; + return true; + } + return false; +} + +bool TryParseUnityStyleBlendFactor(const std::string& token, MaterialBlendFactor& outFactor) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToLower(); + if (normalized == "zero") { + outFactor = MaterialBlendFactor::Zero; + return true; + } + if (normalized == "one") { + outFactor = MaterialBlendFactor::One; + return true; + } + if (normalized == "srccolor" || normalized == "src_color") { + outFactor = MaterialBlendFactor::SrcColor; + return true; + } + if (normalized == "oneminussrccolor" || normalized == "one_minus_src_color" || normalized == "invsrccolor") { + outFactor = MaterialBlendFactor::InvSrcColor; + return true; + } + if (normalized == "srcalpha" || normalized == "src_alpha") { + outFactor = MaterialBlendFactor::SrcAlpha; + return true; + } + if (normalized == "oneminussrcalpha" || normalized == "one_minus_src_alpha" || normalized == "invsrcalpha") { + outFactor = MaterialBlendFactor::InvSrcAlpha; + return true; + } + if (normalized == "dstalpha" || normalized == "dst_alpha") { + outFactor = MaterialBlendFactor::DstAlpha; + return true; + } + if (normalized == "oneminusdstalpha" || normalized == "one_minus_dst_alpha" || normalized == "invdstalpha") { + outFactor = MaterialBlendFactor::InvDstAlpha; + return true; + } + if (normalized == "dstcolor" || normalized == "dst_color") { + outFactor = MaterialBlendFactor::DstColor; + return true; + } + if (normalized == "oneminusdstcolor" || normalized == "one_minus_dst_color" || normalized == "invdstcolor") { + outFactor = MaterialBlendFactor::InvDstColor; + return true; + } + if (normalized == "srcalphasaturate" || normalized == "src_alpha_saturate" || normalized == "srcalphasat") { + outFactor = MaterialBlendFactor::SrcAlphaSat; + return true; + } + return false; +} + +bool TryParseUnityStyleColorMask(const std::string& token, Core::uint8& outMask) { + const Containers::String normalized = Containers::String(token.c_str()).Trim().ToUpper(); + if (normalized == "0") { + outMask = 0u; + return true; + } + + Core::uint8 mask = 0u; + for (size_t index = 0; index < normalized.Length(); ++index) { + switch (normalized[index]) { + case 'R': + mask |= 0x1u; + break; + case 'G': + mask |= 0x2u; + break; + case 'B': + mask |= 0x4u; + break; + case 'A': + mask |= 0x8u; + break; + default: + return false; + } + } + + outMask = mask; + return true; +} + +bool TryParseUnityStyleBlendDirective( + 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)); + } + } + + if (normalizedTokens.size() != 2u && + normalizedTokens.size() != 3u && + normalizedTokens.size() != 5u) { + return false; + } + + if (normalizedTokens.size() == 2u) { + bool enabled = false; + if (!TryParseUnityStyleBoolDirectiveToken(normalizedTokens[1], enabled)) { + return false; + } + + outState.blendEnable = enabled; + if (!enabled) { + outState.srcBlend = MaterialBlendFactor::One; + outState.dstBlend = MaterialBlendFactor::Zero; + outState.srcBlendAlpha = MaterialBlendFactor::One; + outState.dstBlendAlpha = MaterialBlendFactor::Zero; + } + return true; + } + + MaterialBlendFactor srcBlend = MaterialBlendFactor::One; + MaterialBlendFactor dstBlend = MaterialBlendFactor::Zero; + if (!TryParseUnityStyleBlendFactor(normalizedTokens[1], srcBlend) || + !TryParseUnityStyleBlendFactor(normalizedTokens[2], dstBlend)) { + return false; + } + + outState.blendEnable = true; + outState.srcBlend = srcBlend; + outState.dstBlend = dstBlend; + + if (normalizedTokens.size() == 5u) { + if (!TryParseUnityStyleBlendFactor(normalizedTokens[3], outState.srcBlendAlpha) || + !TryParseUnityStyleBlendFactor(normalizedTokens[4], outState.dstBlendAlpha)) { + return false; + } + } else { + outState.srcBlendAlpha = srcBlend; + outState.dstBlendAlpha = dstBlend; + } + + return true; +} + +void SetOrReplaceAuthoringTag( + std::vector& tags, + const Containers::String& name, + const Containers::String& value) { + for (AuthoringTagEntry& tag : tags) { + if (tag.name == name) { + tag.value = value; + return; + } + } + + tags.push_back({ name, value }); +} + ShaderAuthoringStyle DetectShaderAuthoringStyle(const std::string& sourceText) { std::vector lines; SplitShaderAuthoringLines(sourceText, lines); @@ -1684,6 +1945,15 @@ bool ParseLegacyBackendSplitShaderAuthoring( continue; } + if (currentBlock() == BlockKind::Shader && StartsWithKeyword(line, "Fallback")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { + return fail("Fallback directive is missing a value", humanLine); + } + outDesc.fallback = tokens[1].c_str(); + continue; + } + if (StartsWithKeyword(line, "SubShader")) { pendingBlock = BlockKind::SubShader; continue; @@ -1931,6 +2201,11 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( } currentSubShader->passes.emplace_back(); currentPass = ¤tSubShader->passes.back(); + currentPass->hasFixedFunctionState = true; + currentPass->fixedFunctionState = BuildUnityDefaultFixedFunctionState(); + if (currentSubShader->hasFixedFunctionState) { + currentPass->fixedFunctionState = currentSubShader->fixedFunctionState; + } blockStack.push_back(BlockKind::Pass); break; case BlockKind::None: @@ -1972,6 +2247,15 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( continue; } + if (currentBlock() == BlockKind::Shader && StartsWithKeyword(line, "Fallback")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() < 2u) { + return fail("Fallback directive is missing a value", humanLine); + } + outDesc.fallback = tokens[1].c_str(); + continue; + } + if (StartsWithKeyword(line, "SubShader")) { pendingBlock = BlockKind::SubShader; continue; @@ -2039,9 +2323,106 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( } if (currentBlock() == BlockKind::SubShader && StartsWithKeyword(line, "LOD")) { + std::vector tokens; + if (!TryTokenizeQuotedArguments(line, tokens) || tokens.size() != 2u) { + return fail("LOD directive must provide a numeric value", humanLine); + } + + try { + const Core::uint32 lodValue = static_cast(std::stoul(tokens[1])); + SetOrReplaceAuthoringTag(currentSubShader->tags, "LOD", std::to_string(lodValue).c_str()); + } catch (...) { + return fail("LOD directive must provide a numeric value", humanLine); + } 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); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleCullMode(tokens[1], currentSubShader->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 || + !TryParseUnityStyleBoolDirectiveToken(tokens[1], enabled)) { + return fail("ZWrite directive must use On or Off", humanLine); + } + + EnsureAuthoringFixedFunctionStateInitialized( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + currentSubShader->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( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleComparisonFunc(tokens[1], currentSubShader->fixedFunctionState.depthFunc)) { + return fail("ZTest directive uses an unsupported compare function", humanLine); + } + currentSubShader->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( + currentSubShader->hasFixedFunctionState, + currentSubShader->fixedFunctionState); + if (!TryParseUnityStyleBlendDirective(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 (!TryParseUnityStyleColorMask(tokens[1], currentSubShader->fixedFunctionState.colorWriteMask)) { + return fail("ColorMask directive uses an unsupported channel mask", humanLine); + } + continue; + } + } + if (currentBlock() == BlockKind::Pass && currentPass != nullptr) { if (StartsWithKeyword(line, "Name")) { std::vector tokens; @@ -2052,6 +2433,90 @@ bool ParseUnityStyleSingleSourceShaderAuthoring( 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 (!TryParseUnityStyleCullMode(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 || + !TryParseUnityStyleBoolDirectiveToken(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 (!TryParseUnityStyleComparisonFunc(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 (!TryParseUnityStyleBlendDirective(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 (!TryParseUnityStyleColorMask(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( @@ -2114,6 +2579,7 @@ LoadResult BuildShaderFromAuthoringDesc( params.guid = ResourceGUID::Generate(path); params.name = authoringDesc.name; shader->Initialize(params); + shader->SetFallback(authoringDesc.fallback); for (const ShaderPropertyDesc& property : authoringDesc.properties) { shader->AddProperty(property); @@ -2123,6 +2589,8 @@ LoadResult BuildShaderFromAuthoringDesc( for (const AuthoringPassEntry& pass : subShader.passes) { ShaderPass shaderPass = {}; shaderPass.name = pass.name; + shaderPass.hasFixedFunctionState = pass.hasFixedFunctionState; + shaderPass.fixedFunctionState = pass.fixedFunctionState; shader->AddPass(shaderPass); for (const AuthoringTagEntry& subShaderTag : subShader.tags) { @@ -2393,7 +2861,8 @@ bool TryParseUnsignedValue(const std::string& json, const char* key, Core::uint3 } size_t CalculateShaderMemorySize(const Shader& shader) { - size_t memorySize = sizeof(Shader) + shader.GetName().Length() + shader.GetPath().Length(); + size_t memorySize = + sizeof(Shader) + shader.GetName().Length() + shader.GetPath().Length() + shader.GetFallback().Length(); for (const ShaderPropertyDesc& property : shader.GetProperties()) { memorySize += property.name.Length(); memorySize += property.displayName.Length(); @@ -2518,6 +2987,11 @@ LoadResult LoadShaderManifest(const Containers::String& path, const std::string& shader->Initialize(params); + Containers::String fallback; + if (TryParseStringValue(jsonText, "fallback", fallback)) { + shader->SetFallback(fallback); + } + std::string propertiesArray; if (TryExtractArray(jsonText, "properties", propertiesArray)) { std::vector propertyObjects; @@ -2683,9 +3157,10 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { const std::string magic(fileHeader.magic, fileHeader.magic + 7); const bool isLegacySchema = magic == "XCSHD01" && fileHeader.schemaVersion == 1u; const bool isSchemaV2 = magic == "XCSHD02" && fileHeader.schemaVersion == 2u; + const bool isSchemaV3 = magic == "XCSHD03" && fileHeader.schemaVersion == 3u; const bool isCurrentSchema = - magic == "XCSHD03" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; - if (!isLegacySchema && !isSchemaV2 && !isCurrentSchema) { + magic == "XCSHD04" && fileHeader.schemaVersion == kShaderArtifactSchemaVersion; + if (!isLegacySchema && !isSchemaV2 && !isSchemaV3 && !isCurrentSchema) { return LoadResult("Invalid shader artifact header: " + path); } @@ -2693,14 +3168,20 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { Containers::String shaderName; Containers::String shaderSourcePath; + Containers::String shaderFallback; if (!ReadShaderArtifactString(data, offset, shaderName) || !ReadShaderArtifactString(data, offset, shaderSourcePath)) { return LoadResult("Failed to parse shader artifact strings: " + path); } + if (isCurrentSchema && + !ReadShaderArtifactString(data, offset, shaderFallback)) { + return LoadResult("Failed to parse shader artifact strings: " + path); + } shader->m_name = shaderName.Empty() ? path : shaderName; shader->m_path = shaderSourcePath.Empty() ? path : shaderSourcePath; shader->m_guid = ResourceGUID::Generate(shader->m_path); + shader->SetFallback(shaderFallback); ShaderArtifactHeader shaderHeader; if (!ReadShaderArtifactValue(data, offset, shaderHeader)) { @@ -2728,6 +3209,8 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { Core::uint32 resourceCount = 0; Core::uint32 keywordDeclarationCount = 0; Core::uint32 variantCount = 0; + Core::uint32 hasFixedFunctionState = 0; + MaterialRenderState fixedFunctionState = {}; if (!ReadShaderArtifactString(data, offset, passName)) { return LoadResult("Failed to read shader artifact passes: " + path); } @@ -2741,7 +3224,7 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { tagCount = passHeader.tagCount; resourceCount = passHeader.resourceCount; variantCount = passHeader.variantCount; - } else { + } else if (isSchemaV2 || isSchemaV3) { ShaderPassArtifactHeader passHeader = {}; if (!ReadShaderArtifactValue(data, offset, passHeader)) { return LoadResult("Failed to read shader artifact passes: " + path); @@ -2751,10 +3234,24 @@ LoadResult LoadShaderArtifact(const Containers::String& path) { resourceCount = passHeader.resourceCount; keywordDeclarationCount = passHeader.keywordDeclarationCount; variantCount = passHeader.variantCount; + } else { + ShaderPassArtifactHeaderV4 passHeader = {}; + if (!ReadShaderArtifactValue(data, offset, passHeader)) { + return LoadResult("Failed to read shader artifact passes: " + path); + } + + tagCount = passHeader.tagCount; + resourceCount = passHeader.resourceCount; + keywordDeclarationCount = passHeader.keywordDeclarationCount; + variantCount = passHeader.variantCount; + hasFixedFunctionState = passHeader.hasFixedFunctionState; + fixedFunctionState = passHeader.fixedFunctionState; } ShaderPass pass = {}; pass.name = passName; + pass.hasFixedFunctionState = hasFixedFunctionState != 0u; + pass.fixedFunctionState = fixedFunctionState; shader->AddPass(pass); for (Core::uint32 tagIndex = 0; tagIndex < tagCount; ++tagIndex) { diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp index e6ced0c6..232ad960 100644 --- a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -295,6 +295,30 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromExplic delete shader; } +TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromImplicitForwardContract) { + ShaderPass pass = {}; + pass.name = "ForwardLit"; + + ShaderPassTagEntry tag = {}; + tag.name = "LightMode"; + tag.value = "ForwardBase"; + pass.tags.PushBack(tag); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass, plan, &error)) << error.CStr(); + EXPECT_TRUE(plan.perObject.IsValid()); + EXPECT_TRUE(plan.lighting.IsValid()); + EXPECT_TRUE(plan.material.IsValid()); + EXPECT_TRUE(plan.shadowReceiver.IsValid()); + EXPECT_TRUE(plan.baseColorTexture.IsValid()); + EXPECT_TRUE(plan.linearClampSampler.IsValid()); + EXPECT_TRUE(plan.shadowMapTexture.IsValid()); + EXPECT_TRUE(plan.shadowMapSampler.IsValid()); + EXPECT_EQ(plan.firstDescriptorSet, 0u); + EXPECT_EQ(plan.descriptorSetCount, 8u); +} + TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromExplicitUnlitResources) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinUnlitShaderPath()); @@ -529,6 +553,45 @@ TEST(BuiltinObjectIdPass_Test, BuildsBuiltinPassResourceBindingPlanFromExplicitO delete shader; } +TEST(BuiltinObjectIdPass_Test, BuildsBuiltinPassResourceBindingPlanFromImplicitObjectIdContract) { + ShaderPass pass = {}; + pass.name = "ObjectId"; + + ShaderPassTagEntry tag = {}; + tag.name = "LightMode"; + tag.value = "ObjectId"; + pass.tags.PushBack(tag); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass, plan, &error)) << error.CStr(); + ASSERT_EQ(plan.bindings.Size(), 1u); + EXPECT_TRUE(plan.perObject.IsValid()); + EXPECT_FALSE(plan.material.IsValid()); + EXPECT_EQ(plan.firstDescriptorSet, 0u); + EXPECT_EQ(plan.descriptorSetCount, 1u); +} + +TEST(BuiltinDepthStylePass_Test, BuildsBuiltinPassResourceBindingPlanFromImplicitDepthOnlyContract) { + ShaderPass pass = {}; + pass.name = "DepthOnly"; + + ShaderPassTagEntry tag = {}; + tag.name = "LightMode"; + tag.value = "DepthOnly"; + pass.tags.PushBack(tag); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass, plan, &error)) << error.CStr(); + EXPECT_TRUE(plan.perObject.IsValid()); + EXPECT_TRUE(plan.material.IsValid()); + EXPECT_TRUE(plan.baseColorTexture.IsValid()); + EXPECT_TRUE(plan.linearClampSampler.IsValid()); + EXPECT_EQ(plan.firstDescriptorSet, 0u); + EXPECT_EQ(plan.descriptorSetCount, 4u); +} + TEST(BuiltinObjectIdPass_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) { const InputLayoutDesc inputLayout = BuiltinObjectIdPass::BuildInputLayout(); diff --git a/tests/Resources/Shader/test_shader_loader.cpp b/tests/Resources/Shader/test_shader_loader.cpp index 4efb6ee2..26e253ed 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -24,6 +24,20 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content ASSERT_TRUE(static_cast(output)); } +const ShaderPassTagEntry* FindPassTag(const ShaderPass* pass, const char* name) { + if (pass == nullptr || name == nullptr) { + return nullptr; + } + + for (const ShaderPassTagEntry& tag : pass->tags) { + if (tag.name == name) { + return &tag; + } + } + + return nullptr; +} + TEST(ShaderLoader, GetResourceType) { ShaderLoader loader; EXPECT_EQ(loader.GetResourceType(), ResourceType::Shader); @@ -640,11 +654,13 @@ TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringBuildsGenericHlslVar const ShaderPass* pass = shader->FindPass("ForwardLit"); ASSERT_NE(pass, nullptr); - ASSERT_EQ(pass->tags.Size(), 2u); + ASSERT_EQ(pass->tags.Size(), 3u); EXPECT_EQ(pass->tags[0].name, "Queue"); EXPECT_EQ(pass->tags[0].value, "Geometry"); - EXPECT_EQ(pass->tags[1].name, "LightMode"); - EXPECT_EQ(pass->tags[1].value, "ForwardLit"); + EXPECT_EQ(pass->tags[1].name, "LOD"); + EXPECT_EQ(pass->tags[1].value, "200"); + EXPECT_EQ(pass->tags[2].name, "LightMode"); + EXPECT_EQ(pass->tags[2].value, "ForwardLit"); EXPECT_TRUE(pass->resources.Empty()); ASSERT_EQ(pass->keywordDeclarations.Size(), 2u); EXPECT_EQ(pass->keywordDeclarations[0].type, ShaderKeywordDeclarationType::MultiCompile); @@ -708,6 +724,134 @@ TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringBuildsGenericHlslVar fs::remove_all(shaderRoot); } +TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesPassStateAndFallback) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_pass_state"; + const fs::path shaderPath = shaderRoot / "single_source_state.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + + WriteTextFile( + shaderPath, + R"(Shader "SingleSourceStateful" +{ + Fallback "Legacy/Diffuse" + SubShader + { + Pass + { + Name "ForwardLit" + Cull Front + ZWrite Off + ZTest LEqual + Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha + ColorMask RGB + HLSLPROGRAM + #pragma vertex Vert + #pragma fragment Frag + float4 Vert() : SV_POSITION { return 0; } + float4 Frag() : SV_TARGET { return 1; } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + EXPECT_EQ(shader->GetFallback(), "Legacy/Diffuse"); + + const ShaderPass* pass = shader->FindPass("ForwardLit"); + ASSERT_NE(pass, nullptr); + EXPECT_TRUE(pass->hasFixedFunctionState); + EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front); + EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); + EXPECT_TRUE(pass->fixedFunctionState.depthTestEnable); + EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); + EXPECT_TRUE(pass->fixedFunctionState.blendEnable); + EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.srcBlendAlpha, MaterialBlendFactor::One); + EXPECT_EQ(pass->fixedFunctionState.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7u); + + delete shader; + fs::remove_all(shaderRoot); +} + +TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromSingleSourceAuthoringPreservesPassStateAndFallback) { + namespace fs = std::filesystem; + + const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_single_source_artifact_pass_state"; + const fs::path shaderDir = projectRoot / "Assets" / "Shaders"; + const fs::path shaderPath = shaderDir / "single_source_state.shader"; + + fs::remove_all(projectRoot); + fs::create_directories(shaderDir); + + WriteTextFile( + shaderPath, + R"(Shader "ArtifactSingleSourceStateful" +{ + Fallback "Legacy/Cutout" + SubShader + { + Pass + { + Name "ForwardLit" + Cull Off + ZWrite Off + Blend SrcAlpha OneMinusSrcAlpha + ColorMask RGBA + HLSLPROGRAM + #pragma vertex Vert + #pragma fragment Frag + float4 Vert() : SV_POSITION { return 0; } + float4 Frag() : SV_TARGET { return 1; } + ENDHLSL + } + } +} +)"); + + AssetDatabase database; + database.Initialize(projectRoot.string().c_str()); + + AssetDatabase::ResolvedAsset resolvedAsset; + ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/single_source_state.shader", ResourceType::Shader, resolvedAsset)); + ASSERT_TRUE(resolvedAsset.artifactReady); + + ShaderLoader loader; + LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + EXPECT_EQ(shader->GetFallback(), "Legacy/Cutout"); + + const ShaderPass* pass = shader->FindPass("ForwardLit"); + ASSERT_NE(pass, nullptr); + EXPECT_TRUE(pass->hasFixedFunctionState); + EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None); + EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); + EXPECT_TRUE(pass->fixedFunctionState.blendEnable); + EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0xFu); + + delete shader; + database.Shutdown(); + fs::remove_all(projectRoot); +} + TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesMultiCompileLocalKeywords) { namespace fs = std::filesystem; @@ -770,6 +914,82 @@ TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesMultiCompileLo fs::remove_all(shaderRoot); } +TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringParsesFallbackAndFixedFunctionStateInheritance) { + namespace fs = std::filesystem; + + const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_single_source_fixed_state"; + const fs::path shaderPath = shaderRoot / "single_source_fixed_state.shader"; + + fs::remove_all(shaderRoot); + fs::create_directories(shaderRoot); + + WriteTextFile( + shaderPath, + R"(Shader "SingleSourceFixedState" +{ + Fallback "Legacy Shaders/Diffuse" + SubShader + { + Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } + LOD 310 + Cull Front + ZWrite Off + Pass + { + Name "ForwardLit" + Tags { "LightMode" = "ForwardLit" } + Blend SrcAlpha OneMinusSrcAlpha + ColorMask RGB + HLSLPROGRAM + #pragma vertex Vert + #pragma fragment Frag + float4 Vert() : SV_POSITION { return 0; } + float4 Frag() : SV_TARGET { return 1; } + ENDHLSL + } + } +} +)"); + + ShaderLoader loader; + LoadResult result = loader.Load(shaderPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + EXPECT_EQ(shader->GetFallback(), "Legacy Shaders/Diffuse"); + + const ShaderPass* pass = shader->FindPass("ForwardLit"); + ASSERT_NE(pass, nullptr); + EXPECT_TRUE(pass->hasFixedFunctionState); + EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::Front); + EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); + EXPECT_TRUE(pass->fixedFunctionState.depthTestEnable); + EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); + EXPECT_TRUE(pass->fixedFunctionState.blendEnable); + EXPECT_EQ(pass->fixedFunctionState.srcBlend, MaterialBlendFactor::SrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.dstBlend, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.srcBlendAlpha, MaterialBlendFactor::SrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(pass->fixedFunctionState.colorWriteMask, 0x7); + + const ShaderPassTagEntry* queueTag = FindPassTag(pass, "Queue"); + ASSERT_NE(queueTag, nullptr); + EXPECT_EQ(queueTag->value, "Transparent"); + + const ShaderPassTagEntry* lodTag = FindPassTag(pass, "LOD"); + ASSERT_NE(lodTag, nullptr); + EXPECT_EQ(lodTag->value, "310"); + + const ShaderPassTagEntry* lightModeTag = FindPassTag(pass, "LightMode"); + ASSERT_NE(lightModeTag, nullptr); + EXPECT_EQ(lightModeTag->value, "ForwardLit"); + + delete shader; + fs::remove_all(shaderRoot); +} + TEST(ShaderLoader, LoadUnityStyleSingleSourceShaderAuthoringRejectsBackendPragma) { namespace fs = std::filesystem;