From 9a5c187abc5928048b789554d97a807783ca3bbf Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 27 Mar 2026 12:18:04 +0800 Subject: [PATCH] Add material render state and pipeline caching --- .../Pipelines/BuiltinForwardPipeline.h | 8 +- .../Rendering/RenderMaterialUtility.h | 169 ++++++++++ .../XCEngine/Resources/Material/Material.h | 85 +++++ .../Pipelines/BuiltinForwardPipeline.cpp | 76 +++-- engine/src/Resources/Material/Material.cpp | 7 + .../src/Resources/Material/MaterialLoader.cpp | 290 ++++++++++++++++++ .../unit/test_render_scene_extractor.cpp | 38 +++ tests/Resources/Material/test_material.cpp | 39 +++ .../Material/test_material_loader.cpp | 31 ++ 9 files changed, 717 insertions(+), 26 deletions(-) diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index ef120353..6d4733a9 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -11,6 +12,8 @@ #include #include +#include + namespace XCEngine { namespace Resources { class Material; @@ -44,6 +47,9 @@ private: bool EnsureInitialized(const RenderContext& context); bool CreatePipelineResources(const RenderContext& context); void DestroyPipelineResources(); + RHI::RHIPipelineState* GetOrCreatePipelineState( + const RenderContext& context, + const Resources::Material* material); const Resources::Texture* ResolveTexture(const Resources::Material* material) const; RHI::RHIResourceView* ResolveTextureView(const VisibleRenderItem& visibleItem); @@ -65,7 +71,7 @@ private: RHI::RHIDescriptorPool* m_samplerPool = nullptr; RHI::RHIDescriptorSet* m_samplerSet = nullptr; RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; - RHI::RHIPipelineState* m_pipelineState = nullptr; + std::unordered_map m_pipelineStates; RHI::RHISampler* m_sampler = nullptr; RHI::RHITexture* m_fallbackTexture = nullptr; RHI::RHIResourceView* m_fallbackTextureView = nullptr; diff --git a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h index 0437ce61..3670cb5d 100644 --- a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h +++ b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -93,5 +94,173 @@ inline bool MatchesBuiltinPass(const Resources::Material* material, BuiltinMater } } +inline RHI::CullMode ToRHICullMode(Resources::MaterialCullMode mode) { + switch (mode) { + case Resources::MaterialCullMode::Front: + return RHI::CullMode::Front; + case Resources::MaterialCullMode::Back: + return RHI::CullMode::Back; + case Resources::MaterialCullMode::None: + default: + return RHI::CullMode::None; + } +} + +inline RHI::ComparisonFunc ToRHIComparisonFunc(Resources::MaterialComparisonFunc func) { + switch (func) { + case Resources::MaterialComparisonFunc::Never: + return RHI::ComparisonFunc::Never; + case Resources::MaterialComparisonFunc::Equal: + return RHI::ComparisonFunc::Equal; + case Resources::MaterialComparisonFunc::LessEqual: + return RHI::ComparisonFunc::LessEqual; + case Resources::MaterialComparisonFunc::Greater: + return RHI::ComparisonFunc::Greater; + case Resources::MaterialComparisonFunc::NotEqual: + return RHI::ComparisonFunc::NotEqual; + case Resources::MaterialComparisonFunc::GreaterEqual: + return RHI::ComparisonFunc::GreaterEqual; + case Resources::MaterialComparisonFunc::Always: + return RHI::ComparisonFunc::Always; + case Resources::MaterialComparisonFunc::Less: + default: + return RHI::ComparisonFunc::Less; + } +} + +inline RHI::BlendFactor ToRHIBlendFactor(Resources::MaterialBlendFactor factor) { + switch (factor) { + case Resources::MaterialBlendFactor::Zero: + return RHI::BlendFactor::Zero; + case Resources::MaterialBlendFactor::SrcColor: + return RHI::BlendFactor::SrcColor; + case Resources::MaterialBlendFactor::InvSrcColor: + return RHI::BlendFactor::InvSrcColor; + case Resources::MaterialBlendFactor::SrcAlpha: + return RHI::BlendFactor::SrcAlpha; + case Resources::MaterialBlendFactor::InvSrcAlpha: + return RHI::BlendFactor::InvSrcAlpha; + case Resources::MaterialBlendFactor::DstAlpha: + return RHI::BlendFactor::DstAlpha; + case Resources::MaterialBlendFactor::InvDstAlpha: + return RHI::BlendFactor::InvDstAlpha; + case Resources::MaterialBlendFactor::DstColor: + return RHI::BlendFactor::DstColor; + case Resources::MaterialBlendFactor::InvDstColor: + return RHI::BlendFactor::InvDstColor; + case Resources::MaterialBlendFactor::SrcAlphaSat: + return RHI::BlendFactor::SrcAlphaSat; + case Resources::MaterialBlendFactor::BlendFactor: + return RHI::BlendFactor::BlendFactor; + case Resources::MaterialBlendFactor::InvBlendFactor: + return RHI::BlendFactor::InvBlendFactor; + case Resources::MaterialBlendFactor::Src1Color: + return RHI::BlendFactor::Src1Color; + case Resources::MaterialBlendFactor::InvSrc1Color: + return RHI::BlendFactor::InvSrc1Color; + case Resources::MaterialBlendFactor::Src1Alpha: + return RHI::BlendFactor::Src1Alpha; + case Resources::MaterialBlendFactor::InvSrc1Alpha: + return RHI::BlendFactor::InvSrc1Alpha; + case Resources::MaterialBlendFactor::One: + default: + return RHI::BlendFactor::One; + } +} + +inline RHI::BlendOp ToRHIBlendOp(Resources::MaterialBlendOp op) { + switch (op) { + case Resources::MaterialBlendOp::Subtract: + return RHI::BlendOp::Subtract; + case Resources::MaterialBlendOp::ReverseSubtract: + return RHI::BlendOp::ReverseSubtract; + case Resources::MaterialBlendOp::Min: + return RHI::BlendOp::Min; + case Resources::MaterialBlendOp::Max: + return RHI::BlendOp::Max; + case Resources::MaterialBlendOp::Add: + default: + return RHI::BlendOp::Add; + } +} + +inline RHI::RasterizerDesc BuildRasterizerState(const Resources::Material* material) { + 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)); + } + + return desc; +} + +inline RHI::BlendDesc BuildBlendState(const Resources::Material* material) { + 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; + } + + return desc; +} + +inline RHI::DepthStencilStateDesc BuildDepthStencilState(const Resources::Material* material) { + RHI::DepthStencilStateDesc desc = {}; + desc.depthTestEnable = true; + desc.depthWriteEnable = true; + desc.depthFunc = static_cast(RHI::ComparisonFunc::Less); + 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 ApplyMaterialRenderState(const Resources::Material* material, RHI::GraphicsPipelineDesc& pipelineDesc) { + pipelineDesc.rasterizerState = BuildRasterizerState(material); + pipelineDesc.blendState = BuildBlendState(material); + pipelineDesc.depthStencilState = BuildDepthStencilState(material); +} + +struct MaterialRenderStateHash { + size_t operator()(const Resources::MaterialRenderState& state) const noexcept { + size_t hash = 2166136261u; + auto combine = [&hash](size_t value) { + hash ^= value + 0x9e3779b9u + (hash << 6) + (hash >> 2); + }; + + combine(static_cast(state.blendEnable)); + combine(static_cast(state.srcBlend)); + combine(static_cast(state.dstBlend)); + combine(static_cast(state.srcBlendAlpha)); + combine(static_cast(state.dstBlendAlpha)); + combine(static_cast(state.blendOp)); + combine(static_cast(state.blendOpAlpha)); + combine(static_cast(state.colorWriteMask)); + combine(static_cast(state.depthTestEnable)); + combine(static_cast(state.depthWriteEnable)); + combine(static_cast(state.depthFunc)); + combine(static_cast(state.cullMode)); + return hash; + } +}; + } // namespace Rendering } // namespace XCEngine diff --git a/engine/include/XCEngine/Resources/Material/Material.h b/engine/include/XCEngine/Resources/Material/Material.h index bf6f3dc6..b37dfd5d 100644 --- a/engine/include/XCEngine/Resources/Material/Material.h +++ b/engine/include/XCEngine/Resources/Material/Material.h @@ -22,6 +22,87 @@ 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, @@ -65,6 +146,9 @@ public: void SetRenderQueue(MaterialRenderQueue renderQueue); Core::int32 GetRenderQueue() const { return m_renderQueue; } + void SetRenderState(const MaterialRenderState& renderState); + const MaterialRenderState& GetRenderState() const { return m_renderState; } + void SetShaderPass(const Containers::String& shaderPass); const Containers::String& GetShaderPass() const { return m_shaderPass; } @@ -105,6 +189,7 @@ private: ResourceHandle m_shader; Core::int32 m_renderQueue = static_cast(MaterialRenderQueue::Geometry); + MaterialRenderState m_renderState; Containers::String m_shaderPass; struct TagEntry { Containers::String name; diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index d30a18fc..894d72f3 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -86,7 +86,10 @@ void main() { } )"; -RHI::GraphicsPipelineDesc CreatePipelineDesc(RHI::RHIType backendType, RHI::RHIPipelineLayout* pipelineLayout) { +RHI::GraphicsPipelineDesc CreatePipelineDesc( + RHI::RHIType backendType, + RHI::RHIPipelineLayout* pipelineLayout, + const Resources::Material* material) { RHI::GraphicsPipelineDesc pipelineDesc = {}; pipelineDesc.pipelineLayout = pipelineLayout; pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); @@ -94,16 +97,7 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc(RHI::RHIType backendType, RHI::RHIP pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); pipelineDesc.sampleCount = 1; - - pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); - pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); - pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); - pipelineDesc.rasterizerState.depthClipEnable = true; - - pipelineDesc.depthStencilState.depthTestEnable = true; - pipelineDesc.depthStencilState.depthWriteEnable = true; - pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Less); - pipelineDesc.depthStencilState.stencilEnable = false; + ApplyMaterialRenderState(material, pipelineDesc); RHI::InputElementDesc position = {}; position.semanticName = "POSITION"; @@ -248,14 +242,24 @@ bool BuiltinForwardPipeline::Render( commandList->ClearDepthStencil(surface.GetDepthAttachment(), 1.0f, 0); } - commandList->SetPipelineState(m_pipelineState); commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + RHI::RHIPipelineState* currentPipelineState = nullptr; for (const VisibleRenderItem& visibleItem : sceneData.visibleItems) { - if (!MatchesBuiltinPass(ResolveMaterial(visibleItem), BuiltinMaterialPass::Forward)) { + const Resources::Material* material = ResolveMaterial(visibleItem); + if (!MatchesBuiltinPass(material, BuiltinMaterialPass::Forward)) { continue; } + RHI::RHIPipelineState* pipelineState = GetOrCreatePipelineState(context, material); + if (pipelineState == nullptr) { + continue; + } + if (pipelineState != currentPipelineState) { + commandList->SetPipelineState(pipelineState); + currentPipelineState = pipelineState; + } + DrawVisibleItem(context, sceneData, visibleItem); } @@ -387,12 +391,6 @@ bool BuiltinForwardPipeline::CreatePipelineResources(const RenderContext& contex return false; } - const RHI::GraphicsPipelineDesc pipelineDesc = CreatePipelineDesc(context.backendType, m_pipelineLayout); - m_pipelineState = context.device->CreatePipelineState(pipelineDesc); - if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { - return false; - } - RHI::SamplerDesc samplerDesc = {}; samplerDesc.filter = static_cast(RHI::FilterMode::Linear); samplerDesc.addressU = static_cast(RHI::TextureAddressMode::Clamp); @@ -441,6 +439,14 @@ bool BuiltinForwardPipeline::CreatePipelineResources(const RenderContext& contex void BuiltinForwardPipeline::DestroyPipelineResources() { m_resourceCache.Shutdown(); + for (auto& pipelinePair : m_pipelineStates) { + if (pipelinePair.second != nullptr) { + pipelinePair.second->Shutdown(); + delete pipelinePair.second; + } + } + m_pipelineStates.clear(); + if (m_fallbackTextureView != nullptr) { m_fallbackTextureView->Shutdown(); delete m_fallbackTextureView; @@ -459,12 +465,6 @@ void BuiltinForwardPipeline::DestroyPipelineResources() { m_sampler = nullptr; } - if (m_pipelineState != nullptr) { - m_pipelineState->Shutdown(); - delete m_pipelineState; - m_pipelineState = nullptr; - } - if (m_pipelineLayout != nullptr) { m_pipelineLayout->Shutdown(); delete m_pipelineLayout; @@ -511,6 +511,32 @@ void BuiltinForwardPipeline::DestroyPipelineResources() { m_initialized = false; } +RHI::RHIPipelineState* BuiltinForwardPipeline::GetOrCreatePipelineState( + const RenderContext& context, + const Resources::Material* material) { + const Resources::MaterialRenderState renderState = + material != nullptr ? material->GetRenderState() : Resources::MaterialRenderState(); + + const auto existing = m_pipelineStates.find(renderState); + if (existing != m_pipelineStates.end()) { + return existing->second; + } + + const RHI::GraphicsPipelineDesc pipelineDesc = + CreatePipelineDesc(context.backendType, m_pipelineLayout, material); + RHI::RHIPipelineState* pipelineState = context.device->CreatePipelineState(pipelineDesc); + if (pipelineState == nullptr || !pipelineState->IsValid()) { + if (pipelineState != nullptr) { + pipelineState->Shutdown(); + delete pipelineState; + } + return nullptr; + } + + m_pipelineStates.emplace(renderState, pipelineState); + return pipelineState; +} + const Resources::Texture* BuiltinForwardPipeline::ResolveTexture(const Resources::Material* material) const { return material != nullptr ? FindMaterialTexture(*material) : nullptr; } diff --git a/engine/src/Resources/Material/Material.cpp b/engine/src/Resources/Material/Material.cpp index 53d3b93a..da1350df 100644 --- a/engine/src/Resources/Material/Material.cpp +++ b/engine/src/Resources/Material/Material.cpp @@ -12,6 +12,7 @@ Material::~Material() = default; void Material::Release() { m_shader.Reset(); m_renderQueue = static_cast(MaterialRenderQueue::Geometry); + m_renderState = MaterialRenderState(); m_shaderPass.Clear(); m_tags.Clear(); m_properties.Clear(); @@ -34,6 +35,11 @@ void Material::SetRenderQueue(MaterialRenderQueue renderQueue) { SetRenderQueue(static_cast(renderQueue)); } +void Material::SetRenderState(const MaterialRenderState& renderState) { + m_renderState = renderState; + UpdateMemorySize(); +} + void Material::SetShaderPass(const Containers::String& shaderPass) { m_shaderPass = shaderPass; UpdateMemorySize(); @@ -267,6 +273,7 @@ void Material::ClearAllProperties() { void Material::UpdateMemorySize() { m_memorySize = m_constantBufferData.Size() + + sizeof(MaterialRenderState) + m_shaderPass.Length() + m_tags.Size() * sizeof(TagEntry) + m_textureBindings.Size() * sizeof(TextureBinding) + diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index adca7e6d..2a7c235f 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -109,6 +109,24 @@ bool TryParseIntValue(const std::string& json, const char* key, Core::int32& out return true; } +bool TryParseBoolValue(const std::string& json, const char* key, bool& outValue) { + size_t valuePos = 0; + if (!FindValueStart(json, key, valuePos)) { + return false; + } + + if (json.compare(valuePos, 4, "true") == 0) { + outValue = true; + return true; + } + if (json.compare(valuePos, 5, "false") == 0) { + outValue = false; + return true; + } + + return false; +} + bool TryExtractObject(const std::string& json, const char* key, std::string& outObject) { size_t valuePos = 0; if (!FindValueStart(json, key, valuePos) || valuePos >= json.size() || json[valuePos] != '{') { @@ -234,6 +252,270 @@ bool TryParseTagMap(const std::string& objectText, Material* material) { return false; } +bool TryParseCullMode(const Containers::String& value, MaterialCullMode& outMode) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "none" || normalized == "off") { + outMode = MaterialCullMode::None; + return true; + } + if (normalized == "front") { + outMode = MaterialCullMode::Front; + return true; + } + if (normalized == "back") { + outMode = MaterialCullMode::Back; + return true; + } + + return false; +} + +bool TryParseComparisonFunc(const Containers::String& value, MaterialComparisonFunc& outFunc) { + const Containers::String normalized = value.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 == "lessequal" || normalized == "less_equal" || normalized == "lequal") { + 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 == "greaterequal" || normalized == "greater_equal" || normalized == "gequal") { + outFunc = MaterialComparisonFunc::GreaterEqual; + return true; + } + if (normalized == "always") { + outFunc = MaterialComparisonFunc::Always; + return true; + } + + return false; +} + +bool TryParseBlendFactor(const Containers::String& value, MaterialBlendFactor& outFactor) { + const Containers::String normalized = value.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 == "invsrccolor" || normalized == "inv_src_color" || normalized == "one_minus_src_color") { + outFactor = MaterialBlendFactor::InvSrcColor; + return true; + } + if (normalized == "srcalpha" || normalized == "src_alpha") { + outFactor = MaterialBlendFactor::SrcAlpha; + return true; + } + if (normalized == "invsrcalpha" || normalized == "inv_src_alpha" || normalized == "one_minus_src_alpha") { + outFactor = MaterialBlendFactor::InvSrcAlpha; + return true; + } + if (normalized == "dstalpha" || normalized == "dst_alpha") { + outFactor = MaterialBlendFactor::DstAlpha; + return true; + } + if (normalized == "invdstalpha" || normalized == "inv_dst_alpha" || normalized == "one_minus_dst_alpha") { + outFactor = MaterialBlendFactor::InvDstAlpha; + return true; + } + if (normalized == "dstcolor" || normalized == "dst_color") { + outFactor = MaterialBlendFactor::DstColor; + return true; + } + if (normalized == "invdstcolor" || normalized == "inv_dst_color" || normalized == "one_minus_dst_color") { + outFactor = MaterialBlendFactor::InvDstColor; + return true; + } + if (normalized == "srcalphasat" || normalized == "src_alpha_sat") { + outFactor = MaterialBlendFactor::SrcAlphaSat; + return true; + } + if (normalized == "blendfactor" || normalized == "blend_factor") { + outFactor = MaterialBlendFactor::BlendFactor; + return true; + } + if (normalized == "invblendfactor" || normalized == "inv_blend_factor" || normalized == "one_minus_blend_factor") { + outFactor = MaterialBlendFactor::InvBlendFactor; + return true; + } + if (normalized == "src1color" || normalized == "src1_color") { + outFactor = MaterialBlendFactor::Src1Color; + return true; + } + if (normalized == "invsrc1color" || normalized == "inv_src1_color" || normalized == "one_minus_src1_color") { + outFactor = MaterialBlendFactor::InvSrc1Color; + return true; + } + if (normalized == "src1alpha" || normalized == "src1_alpha") { + outFactor = MaterialBlendFactor::Src1Alpha; + return true; + } + if (normalized == "invsrc1alpha" || normalized == "inv_src1_alpha" || normalized == "one_minus_src1_alpha") { + outFactor = MaterialBlendFactor::InvSrc1Alpha; + return true; + } + + return false; +} + +bool TryParseBlendOp(const Containers::String& value, MaterialBlendOp& outOp) { + const Containers::String normalized = value.Trim().ToLower(); + if (normalized == "add") { + outOp = MaterialBlendOp::Add; + return true; + } + if (normalized == "subtract") { + outOp = MaterialBlendOp::Subtract; + return true; + } + if (normalized == "reversesubtract" || normalized == "reverse_subtract") { + outOp = MaterialBlendOp::ReverseSubtract; + return true; + } + if (normalized == "min") { + outOp = MaterialBlendOp::Min; + return true; + } + if (normalized == "max") { + outOp = MaterialBlendOp::Max; + return true; + } + + return false; +} + +bool TryParseRenderStateObject(const std::string& objectText, Material* material) { + if (material == nullptr || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { + return false; + } + + MaterialRenderState renderState = material->GetRenderState(); + + Containers::String enumValue; + if (HasKey(objectText, "cull")) { + if (!TryParseStringValue(objectText, "cull", enumValue) || + !TryParseCullMode(enumValue, renderState.cullMode)) { + return false; + } + } + + bool boolValue = false; + if (HasKey(objectText, "depthTest")) { + if (!TryParseBoolValue(objectText, "depthTest", boolValue)) { + return false; + } + renderState.depthTestEnable = boolValue; + } + if (HasKey(objectText, "depthWrite")) { + if (!TryParseBoolValue(objectText, "depthWrite", boolValue)) { + return false; + } + renderState.depthWriteEnable = boolValue; + } + if (HasKey(objectText, "zWrite")) { + if (!TryParseBoolValue(objectText, "zWrite", boolValue)) { + return false; + } + renderState.depthWriteEnable = boolValue; + } + if (HasKey(objectText, "blend")) { + if (!TryParseBoolValue(objectText, "blend", boolValue)) { + return false; + } + renderState.blendEnable = boolValue; + } + if (HasKey(objectText, "blendEnable")) { + if (!TryParseBoolValue(objectText, "blendEnable", boolValue)) { + return false; + } + renderState.blendEnable = boolValue; + } + + if (HasKey(objectText, "depthFunc")) { + if (!TryParseStringValue(objectText, "depthFunc", enumValue) || + !TryParseComparisonFunc(enumValue, renderState.depthFunc)) { + return false; + } + } + if (HasKey(objectText, "zTest")) { + if (!TryParseStringValue(objectText, "zTest", enumValue) || + !TryParseComparisonFunc(enumValue, renderState.depthFunc)) { + return false; + } + } + if (HasKey(objectText, "colorWriteMask")) { + Core::int32 colorWriteMask = 0; + if (!TryParseIntValue(objectText, "colorWriteMask", colorWriteMask) || + colorWriteMask < 0 || colorWriteMask > 0xF) { + return false; + } + renderState.colorWriteMask = static_cast(colorWriteMask); + } + if (HasKey(objectText, "srcBlend")) { + if (!TryParseStringValue(objectText, "srcBlend", enumValue) || + !TryParseBlendFactor(enumValue, renderState.srcBlend)) { + return false; + } + } + if (HasKey(objectText, "dstBlend")) { + if (!TryParseStringValue(objectText, "dstBlend", enumValue) || + !TryParseBlendFactor(enumValue, renderState.dstBlend)) { + return false; + } + } + if (HasKey(objectText, "srcBlendAlpha")) { + if (!TryParseStringValue(objectText, "srcBlendAlpha", enumValue) || + !TryParseBlendFactor(enumValue, renderState.srcBlendAlpha)) { + return false; + } + } + if (HasKey(objectText, "dstBlendAlpha")) { + if (!TryParseStringValue(objectText, "dstBlendAlpha", enumValue) || + !TryParseBlendFactor(enumValue, renderState.dstBlendAlpha)) { + return false; + } + } + if (HasKey(objectText, "blendOp")) { + if (!TryParseStringValue(objectText, "blendOp", enumValue) || + !TryParseBlendOp(enumValue, renderState.blendOp)) { + return false; + } + } + if (HasKey(objectText, "blendOpAlpha")) { + if (!TryParseStringValue(objectText, "blendOpAlpha", enumValue) || + !TryParseBlendOp(enumValue, renderState.blendOpAlpha)) { + return false; + } + } + + material->SetRenderState(renderState); + return true; +} + ResourceHandle LoadShaderHandle(const Containers::String& shaderPath) { ResourceHandle shader = ResourceManager::Get().Load(shaderPath); if (shader.IsValid()) { @@ -349,6 +631,14 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array& dat } } + if (HasKey(jsonText, "renderState")) { + std::string renderStateObject; + if (!TryExtractObject(jsonText, "renderState", renderStateObject) || + !TryParseRenderStateObject(renderStateObject, material)) { + return false; + } + } + return true; } diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index b1f4623e..6f9b83e6 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -265,4 +265,42 @@ TEST(RenderMaterialUtility_Test, MatchesBuiltinForwardPassMetadata) { EXPECT_FALSE(MatchesBuiltinPass(&depthOnlyMaterial, BuiltinMaterialPass::Forward)); } +TEST(RenderMaterialUtility_Test, MapsMaterialRenderStateToRhiDescriptors) { + Material material; + MaterialRenderState renderState; + renderState.cullMode = MaterialCullMode::Back; + renderState.blendEnable = true; + renderState.srcBlend = MaterialBlendFactor::SrcAlpha; + renderState.dstBlend = MaterialBlendFactor::InvSrcAlpha; + renderState.srcBlendAlpha = MaterialBlendFactor::One; + renderState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha; + renderState.blendOp = MaterialBlendOp::Add; + renderState.blendOpAlpha = MaterialBlendOp::Subtract; + renderState.colorWriteMask = 0x7; + renderState.depthTestEnable = true; + renderState.depthWriteEnable = false; + renderState.depthFunc = MaterialComparisonFunc::LessEqual; + material.SetRenderState(renderState); + + const XCEngine::RHI::RasterizerDesc rasterizerState = BuildRasterizerState(&material); + const XCEngine::RHI::BlendDesc blendState = BuildBlendState(&material); + const XCEngine::RHI::DepthStencilStateDesc depthStencilState = BuildDepthStencilState(&material); + + EXPECT_EQ(rasterizerState.cullMode, static_cast(XCEngine::RHI::CullMode::Back)); + EXPECT_EQ(rasterizerState.frontFace, static_cast(XCEngine::RHI::FrontFace::CounterClockwise)); + + EXPECT_TRUE(blendState.blendEnable); + EXPECT_EQ(blendState.srcBlend, static_cast(XCEngine::RHI::BlendFactor::SrcAlpha)); + EXPECT_EQ(blendState.dstBlend, static_cast(XCEngine::RHI::BlendFactor::InvSrcAlpha)); + EXPECT_EQ(blendState.srcBlendAlpha, static_cast(XCEngine::RHI::BlendFactor::One)); + EXPECT_EQ(blendState.dstBlendAlpha, static_cast(XCEngine::RHI::BlendFactor::InvSrcAlpha)); + EXPECT_EQ(blendState.blendOp, static_cast(XCEngine::RHI::BlendOp::Add)); + EXPECT_EQ(blendState.blendOpAlpha, static_cast(XCEngine::RHI::BlendOp::Subtract)); + EXPECT_EQ(blendState.colorWriteMask, 0x7); + + EXPECT_TRUE(depthStencilState.depthTestEnable); + EXPECT_FALSE(depthStencilState.depthWriteEnable); + EXPECT_EQ(depthStencilState.depthFunc, static_cast(XCEngine::RHI::ComparisonFunc::LessEqual)); +} + } // namespace diff --git a/tests/Resources/Material/test_material.cpp b/tests/Resources/Material/test_material.cpp index ec56aec2..cc75f869 100644 --- a/tests/Resources/Material/test_material.cpp +++ b/tests/Resources/Material/test_material.cpp @@ -37,6 +37,11 @@ TEST(Material, SetGetShader) { TEST(Material, DefaultRenderMetadata) { Material material; EXPECT_EQ(material.GetRenderQueue(), static_cast(MaterialRenderQueue::Geometry)); + EXPECT_EQ(material.GetRenderState().cullMode, MaterialCullMode::None); + EXPECT_FALSE(material.GetRenderState().blendEnable); + EXPECT_TRUE(material.GetRenderState().depthTestEnable); + EXPECT_TRUE(material.GetRenderState().depthWriteEnable); + EXPECT_EQ(material.GetRenderState().depthFunc, MaterialComparisonFunc::Less); EXPECT_TRUE(material.GetShaderPass().Empty()); EXPECT_EQ(material.GetTagCount(), 0u); } @@ -51,6 +56,40 @@ TEST(Material, SetGetRenderQueue) { EXPECT_EQ(material.GetRenderQueue(), 2600); } +TEST(Material, SetGetRenderState) { + Material material; + + MaterialRenderState renderState; + renderState.cullMode = MaterialCullMode::Back; + renderState.blendEnable = true; + renderState.srcBlend = MaterialBlendFactor::SrcAlpha; + renderState.dstBlend = MaterialBlendFactor::InvSrcAlpha; + renderState.srcBlendAlpha = MaterialBlendFactor::One; + renderState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha; + renderState.blendOp = MaterialBlendOp::Add; + renderState.blendOpAlpha = MaterialBlendOp::Add; + renderState.depthTestEnable = true; + renderState.depthWriteEnable = false; + renderState.depthFunc = MaterialComparisonFunc::LessEqual; + renderState.colorWriteMask = 0x7; + + material.SetRenderState(renderState); + + const MaterialRenderState& result = material.GetRenderState(); + EXPECT_EQ(result.cullMode, MaterialCullMode::Back); + EXPECT_TRUE(result.blendEnable); + EXPECT_EQ(result.srcBlend, MaterialBlendFactor::SrcAlpha); + EXPECT_EQ(result.dstBlend, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(result.srcBlendAlpha, MaterialBlendFactor::One); + EXPECT_EQ(result.dstBlendAlpha, MaterialBlendFactor::InvSrcAlpha); + EXPECT_EQ(result.blendOp, MaterialBlendOp::Add); + EXPECT_EQ(result.blendOpAlpha, MaterialBlendOp::Add); + EXPECT_TRUE(result.depthTestEnable); + EXPECT_FALSE(result.depthWriteEnable); + EXPECT_EQ(result.depthFunc, MaterialComparisonFunc::LessEqual); + EXPECT_EQ(result.colorWriteMask, 0x7); +} + TEST(Material, SetGetShaderPass) { Material material; diff --git a/tests/Resources/Material/test_material_loader.cpp b/tests/Resources/Material/test_material_loader.cpp index 8ecb3a18..667eefd1 100644 --- a/tests/Resources/Material/test_material_loader.cpp +++ b/tests/Resources/Material/test_material_loader.cpp @@ -70,6 +70,14 @@ TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { materialFile << " \"tags\": {\n"; materialFile << " \"LightMode\": \"ForwardBase\",\n"; materialFile << " \"RenderType\": \"Transparent\"\n"; + materialFile << " },\n"; + materialFile << " \"renderState\": {\n"; + materialFile << " \"cull\": \"Back\",\n"; + materialFile << " \"blend\": true,\n"; + materialFile << " \"srcBlend\": \"SrcAlpha\",\n"; + materialFile << " \"dstBlend\": \"InvSrcAlpha\",\n"; + materialFile << " \"depthWrite\": false,\n"; + materialFile << " \"depthFunc\": \"LessEqual\"\n"; materialFile << " }\n"; materialFile << "}"; } @@ -87,6 +95,12 @@ TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { EXPECT_EQ(material->GetShaderPass(), "ForwardLit"); EXPECT_EQ(material->GetTag("LightMode"), "ForwardBase"); EXPECT_EQ(material->GetTag("RenderType"), "Transparent"); + EXPECT_EQ(material->GetRenderState().cullMode, MaterialCullMode::Back); + EXPECT_TRUE(material->GetRenderState().blendEnable); + EXPECT_EQ(material->GetRenderState().srcBlend, MaterialBlendFactor::SrcAlpha); + EXPECT_EQ(material->GetRenderState().dstBlend, MaterialBlendFactor::InvSrcAlpha); + EXPECT_FALSE(material->GetRenderState().depthWriteEnable); + EXPECT_EQ(material->GetRenderState().depthFunc, MaterialComparisonFunc::LessEqual); delete material; std::remove(materialPath.string().c_str()); @@ -110,4 +124,21 @@ TEST(MaterialLoader, RejectsUnknownRenderQueueName) { std::remove(materialPath.string().c_str()); } +TEST(MaterialLoader, RejectsUnknownRenderStateEnum) { + const std::filesystem::path materialPath = + std::filesystem::current_path() / "material_loader_invalid_render_state.material"; + + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{ \"renderState\": { \"cull\": \"Sideways\" } }"; + } + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.string().c_str()); + EXPECT_FALSE(result); + + std::remove(materialPath.string().c_str()); +} + } // namespace