From 27014e613e454b63e51a5755d6e60fdcc90dfc4e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 17:05:38 +0800 Subject: [PATCH] Adopt binding plan for builtin object-id pass --- docs/plan/Shader与Material系统下一阶段计划.md | 9 +++ .../shaders/object-id/object-id.shader | 9 +++ .../Rendering/Passes/BuiltinObjectIdPass.h | 3 + .../Rendering/RenderMaterialUtility.h | 13 ++++ .../Rendering/Passes/BuiltinObjectIdPass.cpp | 62 ++++++++++++++++--- .../unit/test_builtin_forward_pipeline.cpp | 40 ++++++++++++ tests/Resources/Shader/test_shader_loader.cpp | 5 ++ 7 files changed, 132 insertions(+), 9 deletions(-) diff --git a/docs/plan/Shader与Material系统下一阶段计划.md b/docs/plan/Shader与Material系统下一阶段计划.md index 48ba2185..46d47149 100644 --- a/docs/plan/Shader与Material系统下一阶段计划.md +++ b/docs/plan/Shader与Material系统下一阶段计划.md @@ -336,6 +336,15 @@ Unity-like Shader Authoring (.shader) - 至少 `ForwardLit + Unlit + ObjectId` 共用同一套 shader/material 执行边界 - 新增 pass 不再默认要求先写一套新的硬编码 binding 路径 +当前进展(`2026-04-03`): + +- 已完成:builtin `ObjectId` pass 接入通用 pass binding plan + - builtin object-id shader 已显式声明 `PerObject` 资源合约 + - `BuiltinObjectIdPass` 已改为消费通用 binding plan,不再硬编码 `set0/binding0` 常量布局 + - 显式 shader `resources` 与 legacy object-id fallback 现在走同一套解析与校验路径 +- 已验证:`rendering_unit_tests` 59/59,`shader_tests` 26/26 +- 下一步:把同一套共享执行边界继续推到 `Unlit`,并评估是否抽出 forward/object-id 共用的 pass layout 构建与 descriptor set 组装骨架 + ### 阶段 D:扩展 AssetDatabase / Library Artifact 能力 目标: diff --git a/engine/assets/builtin/shaders/object-id/object-id.shader b/engine/assets/builtin/shaders/object-id/object-id.shader index 713be6f8..2f272a7c 100644 --- a/engine/assets/builtin/shaders/object-id/object-id.shader +++ b/engine/assets/builtin/shaders/object-id/object-id.shader @@ -6,6 +6,15 @@ "tags": { "LightMode": "ObjectId" }, + "resources": [ + { + "name": "PerObjectConstants", + "type": "ConstantBuffer", + "set": 0, + "binding": 0, + "semantic": "PerObject" + } + ], "variants": [ { "stage": "Vertex", diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h index a629fce0..fb42762c 100644 --- a/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -54,6 +55,8 @@ private: RHI::RHIType m_backendType = RHI::RHIType::D3D12; RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; RHI::RHIPipelineState* m_pipelineState = nullptr; + PassResourceBindingLocation m_perObjectBinding = {}; + Core::uint32 m_firstDescriptorSet = 0; Resources::ResourceHandle m_builtinObjectIdShader; RenderResourceCache m_resourceCache; std::unordered_map m_perObjectSets; diff --git a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h index d740d5ba..cb301288 100644 --- a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h +++ b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h @@ -229,6 +229,19 @@ inline Containers::Array BuildLegacyBuilti return bindings; } +inline Containers::Array BuildLegacyBuiltinObjectIdPassResourceBindings() { + Containers::Array bindings; + bindings.Resize(1); + + bindings[0].name = "PerObjectConstants"; + bindings[0].type = Resources::ShaderResourceType::ConstantBuffer; + bindings[0].set = 0; + bindings[0].binding = 0; + bindings[0].semantic = "PerObject"; + + return bindings; +} + inline bool IsBuiltinPassResourceTypeCompatible( BuiltinPassResourceSemantic semantic, Resources::ShaderResourceType type) { diff --git a/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp b/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp index ad199585..5a91b0b4 100644 --- a/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp +++ b/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp @@ -12,6 +12,7 @@ #include "Resources/Mesh/Mesh.h" #include +#include namespace XCEngine { namespace Rendering { @@ -222,18 +223,55 @@ bool BuiltinObjectIdPass::CreateResources(const RenderContext& context) { return false; } + Containers::Array resourceBindings = objectIdPass->resources; + if (resourceBindings.Empty()) { + resourceBindings = BuildLegacyBuiltinObjectIdPassResourceBindings(); + } + + BuiltinPassResourceBindingPlan bindingPlan = {}; + Containers::String bindingPlanError; + if (!TryBuildBuiltinPassResourceBindingPlan(resourceBindings, bindingPlan, &bindingPlanError)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + (Containers::String("BuiltinObjectIdPass failed to resolve pass resource bindings: ") + bindingPlanError).CStr()); + DestroyResources(); + return false; + } + + if (!bindingPlan.perObject.IsValid() || + bindingPlan.bindings.Size() != 1u || + bindingPlan.descriptorSetCount != 1u || + !bindingPlan.usesConstantBuffers || + bindingPlan.usesTextures || + bindingPlan.usesSamplers) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinObjectIdPass requires exactly one PerObject constant-buffer binding"); + DestroyResources(); + return false; + } + + m_perObjectBinding = bindingPlan.perObject; + m_firstDescriptorSet = bindingPlan.firstDescriptorSet; + + std::vector> setBindingStorage( + static_cast(bindingPlan.maxSetIndex) + 1u); RHI::DescriptorSetLayoutBinding constantBinding = {}; - constantBinding.binding = 0; + constantBinding.binding = m_perObjectBinding.binding; constantBinding.type = static_cast(RHI::DescriptorType::CBV); constantBinding.count = 1; + setBindingStorage[m_perObjectBinding.set].push_back(constantBinding); - RHI::DescriptorSetLayoutDesc constantLayout = {}; - constantLayout.bindings = &constantBinding; - constantLayout.bindingCount = 1; + std::vector setLayouts(setBindingStorage.size()); + for (size_t setIndex = 0; setIndex < setBindingStorage.size(); ++setIndex) { + setLayouts[setIndex].bindings = + setBindingStorage[setIndex].empty() ? nullptr : setBindingStorage[setIndex].data(); + setLayouts[setIndex].bindingCount = static_cast(setBindingStorage[setIndex].size()); + } RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; - pipelineLayoutDesc.setLayouts = &constantLayout; - pipelineLayoutDesc.setLayoutCount = 1; + pipelineLayoutDesc.setLayouts = setLayouts.empty() ? nullptr : setLayouts.data(); + pipelineLayoutDesc.setLayoutCount = static_cast(setLayouts.size()); m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); if (m_pipelineLayout == nullptr) { DestroyResources(); @@ -281,10 +319,16 @@ void BuiltinObjectIdPass::DestroyResources() { m_device = nullptr; m_backendType = RHI::RHIType::D3D12; + m_perObjectBinding = {}; + m_firstDescriptorSet = 0; m_builtinObjectIdShader.Reset(); } RHI::RHIDescriptorSet* BuiltinObjectIdPass::GetOrCreatePerObjectSet(uint64_t objectId) { + if (m_perObjectBinding.IsValid() == false) { + return nullptr; + } + const auto existing = m_perObjectSets.find(objectId); if (existing != m_perObjectSets.end()) { return existing->second.set; @@ -302,7 +346,7 @@ RHI::RHIDescriptorSet* BuiltinObjectIdPass::GetOrCreatePerObjectSet(uint64_t obj } RHI::DescriptorSetLayoutBinding binding = {}; - binding.binding = 0; + binding.binding = m_perObjectBinding.binding; binding.type = static_cast(RHI::DescriptorType::CBV); binding.count = 1; @@ -369,10 +413,10 @@ bool BuiltinObjectIdPass::DrawVisibleItem( visibleItem.localToWorld.Transpose(), EncodeObjectIdToColor(objectId) }; - constantSet->WriteConstant(0, &constants, sizeof(constants)); + constantSet->WriteConstant(m_perObjectBinding.binding, &constants, sizeof(constants)); RHI::RHIDescriptorSet* descriptorSets[] = { constantSet }; - commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_pipelineLayout); + commandList->SetGraphicsDescriptorSets(m_firstDescriptorSet, 1, descriptorSets, m_pipelineLayout); if (visibleItem.hasSection) { const Containers::Array& sections = visibleItem.mesh->GetSections(); diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp index 1adc849b..9e098c60 100644 --- a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -120,6 +120,46 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLegacy EXPECT_EQ(plan.linearClampSampler.set, 4u); } +TEST(BuiltinObjectIdPass_Test, BuiltinObjectIdShaderDeclaresExplicitPerObjectResourceContract) { + ShaderLoader loader; + LoadResult result = loader.Load(GetBuiltinObjectIdShaderPath()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("ObjectId"); + ASSERT_NE(pass, nullptr); + ASSERT_EQ(pass->resources.Size(), 1u); + + EXPECT_EQ(pass->resources[0].semantic, "PerObject"); + EXPECT_EQ(pass->resources[0].type, ShaderResourceType::ConstantBuffer); + EXPECT_EQ(pass->resources[0].set, 0u); + EXPECT_EQ(pass->resources[0].binding, 0u); + + delete shader; +} + +TEST(BuiltinObjectIdPass_Test, BuildsBuiltinPassResourceBindingPlanFromLegacyFallbackResources) { + const Array bindings = BuildLegacyBuiltinObjectIdPassResourceBindings(); + ASSERT_EQ(bindings.Size(), 1u); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(bindings, plan, &error)) << error.CStr(); + ASSERT_EQ(plan.bindings.Size(), 1u); + EXPECT_TRUE(plan.perObject.IsValid()); + EXPECT_FALSE(plan.material.IsValid()); + EXPECT_FALSE(plan.baseColorTexture.IsValid()); + EXPECT_FALSE(plan.linearClampSampler.IsValid()); + EXPECT_EQ(plan.firstDescriptorSet, 0u); + EXPECT_EQ(plan.descriptorSetCount, 1u); + EXPECT_TRUE(plan.usesConstantBuffers); + EXPECT_FALSE(plan.usesTextures); + EXPECT_FALSE(plan.usesSamplers); +} + 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 3e5198cf..c3cf9de1 100644 --- a/tests/Resources/Shader/test_shader_loader.cpp +++ b/tests/Resources/Shader/test_shader_loader.cpp @@ -507,6 +507,11 @@ TEST(ShaderLoader, LoadBuiltinObjectIdShaderBuildsBackendVariants) { const ShaderPass* pass = shader->FindPass("ObjectId"); ASSERT_NE(pass, nullptr); + ASSERT_EQ(pass->resources.Size(), 1u); + EXPECT_EQ(pass->resources[0].semantic, "PerObject"); + EXPECT_EQ(pass->resources[0].type, ShaderResourceType::ConstantBuffer); + EXPECT_EQ(pass->resources[0].set, 0u); + EXPECT_EQ(pass->resources[0].binding, 0u); ASSERT_EQ(pass->variants.Size(), 6u); ASSERT_EQ(pass->tags.Size(), 1u); EXPECT_EQ(pass->tags[0].name, "LightMode");