diff --git a/docs/plan/Shader与Material系统下一阶段计划.md b/docs/plan/Shader与Material系统下一阶段计划.md index de169861..48ba2185 100644 --- a/docs/plan/Shader与Material系统下一阶段计划.md +++ b/docs/plan/Shader与Material系统下一阶段计划.md @@ -316,7 +316,12 @@ Unity-like Shader Authoring (.shader) - `Material` 现在会生成正式的 constant layout 元数据 - layout 字段包含 `name / type / offset / size / alignedSize` - renderer 读取的已不再只是裸字节 payload,而是 `layout + payload` 组合 -- 下一步:把 texture / sampler / constant resource mapping 从 builtin forward 特判继续推进到更通用的 pass contract +- 已完成:builtin pass resource mapping / material binding plan runtime contract + - `RenderMaterialUtility` 现在统一提供 `PassResourceBindingLocation / BuiltinPassResourceBindingPlan` + - 显式 shader pass `resources` 与 legacy builtin forward fallback 已走同一套解析与校验路径 + - `BuiltinForwardPipeline` 已改为消费通用 binding plan,而不是继续内联 forward 特判绑定逻辑 +- 已验证:`rendering_unit_tests` 57/57,`material_tests` 51/51 +- 下一步:把这套 pass binding plan 继续推到 `Unlit / ObjectId` 等 pass,收敛成真正共享的 shader/material 执行边界 ### 阶段 C:把 Pass Binding 扩展为正式材质执行链路 diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index 773baeb9..324ebccd 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -68,19 +68,10 @@ private: Math::Vector4 mainLightColorAndFlags = Math::Vector4::Zero(); }; - struct PerMaterialConstants { + struct FallbackPerMaterialConstants { Math::Vector4 baseColorFactor = Math::Vector4::One(); }; - struct DescriptorBindingLocation { - Core::uint32 set = UINT32_MAX; - Core::uint32 binding = UINT32_MAX; - - bool IsValid() const { - return set != UINT32_MAX && binding != UINT32_MAX; - } - }; - struct PassLayoutKey { const Resources::Shader* shader = nullptr; Containers::String passName; @@ -115,10 +106,10 @@ private: Core::uint32 descriptorSetCount = 0; std::vector setLayouts; std::vector staticDescriptorSets; - DescriptorBindingLocation perObject = {}; - DescriptorBindingLocation material = {}; - DescriptorBindingLocation baseColorTexture = {}; - DescriptorBindingLocation linearClampSampler = {}; + PassResourceBindingLocation perObject = {}; + PassResourceBindingLocation material = {}; + PassResourceBindingLocation baseColorTexture = {}; + PassResourceBindingLocation linearClampSampler = {}; }; struct DynamicDescriptorSetKey { @@ -209,7 +200,7 @@ private: Core::uint32 setIndex, Core::uint64 objectId, const Resources::Material* material, - const PerMaterialConstants& materialConstants, + const MaterialConstantPayloadView& materialConstants, RHI::RHIResourceView* textureView); void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet); void DestroyPassResourceLayout(PassResourceLayout& passLayout); diff --git a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h index 6beba568..d740d5ba 100644 --- a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h +++ b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h @@ -7,7 +7,9 @@ #include #include +#include #include +#include namespace XCEngine { namespace Rendering { @@ -21,6 +23,53 @@ enum class BuiltinMaterialPass : Core::uint32 { Forward = ForwardLit }; +struct PassResourceBindingLocation { + Core::uint32 set = UINT32_MAX; + Core::uint32 binding = UINT32_MAX; + + bool IsValid() const { + return set != UINT32_MAX && binding != UINT32_MAX; + } +}; + +enum class BuiltinPassResourceSemantic : Core::uint8 { + Unknown = 0, + PerObject, + Material, + BaseColorTexture, + LinearClampSampler +}; + +struct BuiltinPassResourceBindingDesc { + BuiltinPassResourceSemantic semantic = BuiltinPassResourceSemantic::Unknown; + Resources::ShaderResourceType resourceType = Resources::ShaderResourceType::ConstantBuffer; + PassResourceBindingLocation location = {}; +}; + +struct BuiltinPassResourceBindingPlan { + Containers::Array bindings; + Core::uint32 maxSetIndex = 0; + Core::uint32 firstDescriptorSet = 0; + Core::uint32 descriptorSetCount = 0; + bool usesConstantBuffers = false; + bool usesTextures = false; + bool usesSamplers = false; + PassResourceBindingLocation perObject = {}; + PassResourceBindingLocation material = {}; + PassResourceBindingLocation baseColorTexture = {}; + PassResourceBindingLocation linearClampSampler = {}; + + const BuiltinPassResourceBindingDesc* FindBinding(BuiltinPassResourceSemantic semantic) const { + for (const BuiltinPassResourceBindingDesc& binding : bindings) { + if (binding.semantic == semantic) { + return &binding; + } + } + + return nullptr; + } +}; + inline Containers::String NormalizeBuiltinPassMetadataValue(const Containers::String& value) { return value.Trim().ToLower(); } @@ -120,6 +169,180 @@ inline bool ShaderPassMatchesBuiltinPass( return hasMetadata; } +inline BuiltinPassResourceSemantic ResolveBuiltinPassResourceSemantic( + const Resources::ShaderResourceBindingDesc& binding) { + Containers::String semantic = NormalizeBuiltinPassMetadataValue(binding.semantic); + if (semantic.Empty()) { + semantic = NormalizeBuiltinPassMetadataValue(binding.name); + } + + if (semantic == Containers::String("perobject") || + semantic == Containers::String("perobjectconstants")) { + return BuiltinPassResourceSemantic::PerObject; + } + + if (semantic == Containers::String("material") || + semantic == Containers::String("materialconstants")) { + return BuiltinPassResourceSemantic::Material; + } + + if (semantic == Containers::String("basecolortexture") || + semantic == Containers::String("maintex")) { + return BuiltinPassResourceSemantic::BaseColorTexture; + } + + if (semantic == Containers::String("linearclampsampler")) { + return BuiltinPassResourceSemantic::LinearClampSampler; + } + + return BuiltinPassResourceSemantic::Unknown; +} + +inline Containers::Array BuildLegacyBuiltinForwardPassResourceBindings() { + Containers::Array bindings; + bindings.Resize(4); + + bindings[0].name = "PerObjectConstants"; + bindings[0].type = Resources::ShaderResourceType::ConstantBuffer; + bindings[0].set = 1; + bindings[0].binding = 0; + bindings[0].semantic = "PerObject"; + + bindings[1].name = "MaterialConstants"; + bindings[1].type = Resources::ShaderResourceType::ConstantBuffer; + bindings[1].set = 2; + bindings[1].binding = 0; + bindings[1].semantic = "Material"; + + bindings[2].name = "BaseColorTexture"; + bindings[2].type = Resources::ShaderResourceType::Texture2D; + bindings[2].set = 3; + bindings[2].binding = 0; + bindings[2].semantic = "BaseColorTexture"; + + bindings[3].name = "LinearClampSampler"; + bindings[3].type = Resources::ShaderResourceType::Sampler; + bindings[3].set = 4; + bindings[3].binding = 0; + bindings[3].semantic = "LinearClampSampler"; + + return bindings; +} + +inline bool IsBuiltinPassResourceTypeCompatible( + BuiltinPassResourceSemantic semantic, + Resources::ShaderResourceType type) { + switch (semantic) { + case BuiltinPassResourceSemantic::PerObject: + case BuiltinPassResourceSemantic::Material: + return type == Resources::ShaderResourceType::ConstantBuffer; + case BuiltinPassResourceSemantic::BaseColorTexture: + return type == Resources::ShaderResourceType::Texture2D || + type == Resources::ShaderResourceType::TextureCube; + case BuiltinPassResourceSemantic::LinearClampSampler: + return type == Resources::ShaderResourceType::Sampler; + case BuiltinPassResourceSemantic::Unknown: + default: + return false; + } +} + +inline bool TryBuildBuiltinPassResourceBindingPlan( + const Containers::Array& bindings, + BuiltinPassResourceBindingPlan& outPlan, + Containers::String* outError = nullptr) { + outPlan = {}; + + auto fail = [&outError](const char* message) { + if (outError != nullptr) { + *outError = message; + } + return false; + }; + + if (bindings.Empty()) { + return true; + } + + outPlan.bindings.Reserve(bindings.Size()); + Core::uint32 minBoundSet = UINT32_MAX; + Core::uint32 maxBoundSet = 0; + + for (const Resources::ShaderResourceBindingDesc& binding : bindings) { + const BuiltinPassResourceSemantic semantic = ResolveBuiltinPassResourceSemantic(binding); + if (semantic == BuiltinPassResourceSemantic::Unknown) { + return fail("Unsupported builtin pass resource semantic"); + } + if (!IsBuiltinPassResourceTypeCompatible(semantic, binding.type)) { + return fail("Builtin pass resource semantic/type combination is invalid"); + } + + PassResourceBindingLocation* location = nullptr; + switch (semantic) { + case BuiltinPassResourceSemantic::PerObject: + location = &outPlan.perObject; + break; + case BuiltinPassResourceSemantic::Material: + location = &outPlan.material; + break; + case BuiltinPassResourceSemantic::BaseColorTexture: + location = &outPlan.baseColorTexture; + break; + case BuiltinPassResourceSemantic::LinearClampSampler: + location = &outPlan.linearClampSampler; + break; + case BuiltinPassResourceSemantic::Unknown: + default: + break; + } + + if (location == nullptr) { + return fail("Builtin pass resource semantic could not be mapped"); + } + if (location->IsValid()) { + return fail("Builtin pass resource semantic appears more than once"); + } + + for (const BuiltinPassResourceBindingDesc& existingBinding : outPlan.bindings) { + if (existingBinding.location.set == binding.set && + existingBinding.location.binding == binding.binding) { + return fail("Builtin pass resource set/binding pair appears more than once"); + } + } + + *location = { binding.set, binding.binding }; + + BuiltinPassResourceBindingDesc resolvedBinding = {}; + resolvedBinding.semantic = semantic; + resolvedBinding.resourceType = binding.type; + resolvedBinding.location = *location; + outPlan.bindings.PushBack(resolvedBinding); + + outPlan.maxSetIndex = std::max(outPlan.maxSetIndex, binding.set); + minBoundSet = std::min(minBoundSet, binding.set); + maxBoundSet = std::max(maxBoundSet, binding.set); + + switch (binding.type) { + case Resources::ShaderResourceType::ConstantBuffer: + outPlan.usesConstantBuffers = true; + break; + case Resources::ShaderResourceType::Texture2D: + case Resources::ShaderResourceType::TextureCube: + outPlan.usesTextures = true; + break; + case Resources::ShaderResourceType::Sampler: + outPlan.usesSamplers = true; + break; + default: + break; + } + } + + outPlan.firstDescriptorSet = minBoundSet; + outPlan.descriptorSetCount = maxBoundSet - minBoundSet + 1u; + return true; +} + struct BuiltinForwardMaterialData { Math::Vector4 baseColorFactor = Math::Vector4::One(); }; diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index 85914eef..b806c7de 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -52,42 +52,6 @@ private: namespace { -enum class ForwardPassSemantic : uint8_t { - Unknown = 0, - PerObject, - Material, - BaseColorTexture, - LinearClampSampler -}; - -ForwardPassSemantic ResolveForwardPassSemantic(const Resources::ShaderResourceBindingDesc& binding) { - Containers::String semantic = NormalizeBuiltinPassMetadataValue(binding.semantic); - if (semantic.Empty()) { - semantic = NormalizeBuiltinPassMetadataValue(binding.name); - } - - if (semantic == Containers::String("perobject") || - semantic == Containers::String("perobjectconstants")) { - return ForwardPassSemantic::PerObject; - } - - if (semantic == Containers::String("material") || - semantic == Containers::String("materialconstants")) { - return ForwardPassSemantic::Material; - } - - if (semantic == Containers::String("basecolortexture") || - semantic == Containers::String("maintex")) { - return ForwardPassSemantic::BaseColorTexture; - } - - if (semantic == Containers::String("linearclampsampler")) { - return ForwardPassSemantic::LinearClampSampler; - } - - return ForwardPassSemantic::Unknown; -} - RHI::DescriptorType ToDescriptorType(Resources::ShaderResourceType type) { switch (type) { case Resources::ShaderResourceType::ConstantBuffer: @@ -154,36 +118,6 @@ bool BindingNumberExists( return false; } -std::vector BuildLegacyForwardResourceBindings() { - std::vector bindings(4); - - bindings[0].name = "PerObjectConstants"; - bindings[0].type = Resources::ShaderResourceType::ConstantBuffer; - bindings[0].set = 1; - bindings[0].binding = 0; - bindings[0].semantic = "PerObject"; - - bindings[1].name = "MaterialConstants"; - bindings[1].type = Resources::ShaderResourceType::ConstantBuffer; - bindings[1].set = 2; - bindings[1].binding = 0; - bindings[1].semantic = "Material"; - - bindings[2].name = "BaseColorTexture"; - bindings[2].type = Resources::ShaderResourceType::Texture2D; - bindings[2].set = 3; - bindings[2].binding = 0; - bindings[2].semantic = "BaseColorTexture"; - - bindings[3].name = "LinearClampSampler"; - bindings[3].type = Resources::ShaderResourceType::Sampler; - bindings[3].set = 4; - bindings[3].binding = 0; - bindings[3].semantic = "LinearClampSampler"; - - return bindings; -} - const Resources::ShaderPass* FindForwardCompatiblePass( const Resources::Shader& shader, const Resources::Material* material, @@ -588,16 +522,6 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP return &existing->second; } - std::vector resourceBindings; - if (resolvedShaderPass.pass->resources.Empty()) { - resourceBindings = BuildLegacyForwardResourceBindings(); - } else { - resourceBindings.reserve(resolvedShaderPass.pass->resources.Size()); - for (const Resources::ShaderResourceBindingDesc& binding : resolvedShaderPass.pass->resources) { - resourceBindings.push_back(binding); - } - } - PassResourceLayout passLayout = {}; auto failLayout = [this, &passLayout](const char* message) -> PassResourceLayout* { Debug::Logger::Get().Error(Debug::LogCategory::Rendering, message); @@ -605,61 +529,44 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP return nullptr; }; - Core::uint32 minBoundSet = UINT32_MAX; - Core::uint32 maxBoundSet = 0; - Core::uint32 maxSetIndex = 0; - bool hasAnyResource = false; - bool usesConstantBuffers = false; - bool usesTextures = false; - bool usesSamplers = false; - for (const Resources::ShaderResourceBindingDesc& binding : resourceBindings) { - maxSetIndex = std::max(maxSetIndex, binding.set); - hasAnyResource = true; - minBoundSet = std::min(minBoundSet, binding.set); - maxBoundSet = std::max(maxBoundSet, binding.set); - - switch (binding.type) { - case Resources::ShaderResourceType::ConstantBuffer: - usesConstantBuffers = true; - break; - case Resources::ShaderResourceType::Texture2D: - case Resources::ShaderResourceType::TextureCube: - usesTextures = true; - break; - case Resources::ShaderResourceType::Sampler: - usesSamplers = true; - break; - default: - break; - } + Containers::Array resourceBindings = resolvedShaderPass.pass->resources; + if (resourceBindings.Empty()) { + resourceBindings = BuildLegacyBuiltinForwardPassResourceBindings(); } + BuiltinPassResourceBindingPlan bindingPlan = {}; + Containers::String bindingPlanError; + if (!TryBuildBuiltinPassResourceBindingPlan(resourceBindings, bindingPlan, &bindingPlanError)) { + return failLayout(bindingPlanError.CStr()); + } + + const bool hasAnyResource = !bindingPlan.bindings.Empty(); if (hasAnyResource) { - passLayout.setLayouts.resize(static_cast(maxSetIndex) + 1u); + passLayout.setLayouts.resize(static_cast(bindingPlan.maxSetIndex) + 1u); passLayout.staticDescriptorSets.resize(passLayout.setLayouts.size()); - passLayout.firstDescriptorSet = minBoundSet; - passLayout.descriptorSetCount = maxBoundSet - minBoundSet + 1u; + passLayout.firstDescriptorSet = bindingPlan.firstDescriptorSet; + passLayout.descriptorSetCount = bindingPlan.descriptorSetCount; } - for (const Resources::ShaderResourceBindingDesc& binding : resourceBindings) { - ForwardPassSemantic semantic = ResolveForwardPassSemantic(binding); - if (semantic == ForwardPassSemantic::Unknown) { - return failLayout("BuiltinForwardPipeline encountered an unsupported forward shader resource semantic"); - } + passLayout.perObject = bindingPlan.perObject; + passLayout.material = bindingPlan.material; + passLayout.baseColorTexture = bindingPlan.baseColorTexture; + passLayout.linearClampSampler = bindingPlan.linearClampSampler; - if (binding.set >= passLayout.setLayouts.size()) { + for (const BuiltinPassResourceBindingDesc& binding : bindingPlan.bindings) { + if (binding.location.set >= passLayout.setLayouts.size()) { return failLayout("BuiltinForwardPipeline encountered an invalid forward shader resource set"); } - const RHI::DescriptorType descriptorType = ToDescriptorType(binding.type); - const RHI::DescriptorHeapType heapType = ResolveDescriptorHeapType(binding.type); - PassSetLayoutMetadata& setLayout = passLayout.setLayouts[binding.set]; + const RHI::DescriptorType descriptorType = ToDescriptorType(binding.resourceType); + const RHI::DescriptorHeapType heapType = ResolveDescriptorHeapType(binding.resourceType); + PassSetLayoutMetadata& setLayout = passLayout.setLayouts[binding.location.set]; if (!setLayout.bindings.empty() && setLayout.heapType != heapType) { return failLayout("BuiltinForwardPipeline does not support mixing sampler and non-sampler bindings in one set"); } - if (BindingNumberExists(setLayout.bindings, binding.binding)) { + if (BindingNumberExists(setLayout.bindings, binding.location.binding)) { return failLayout("BuiltinForwardPipeline encountered duplicate bindings inside one descriptor set"); } @@ -668,40 +575,22 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP } RHI::DescriptorSetLayoutBinding layoutBinding = {}; - layoutBinding.binding = binding.binding; + layoutBinding.binding = binding.location.binding; layoutBinding.type = static_cast(descriptorType); layoutBinding.count = 1; setLayout.bindings.push_back(layoutBinding); - switch (semantic) { - case ForwardPassSemantic::PerObject: - if (binding.type != Resources::ShaderResourceType::ConstantBuffer || passLayout.perObject.IsValid()) { - return failLayout("BuiltinForwardPipeline requires a single constant-buffer PerObject resource"); - } - passLayout.perObject = { binding.set, binding.binding }; + switch (binding.semantic) { + case BuiltinPassResourceSemantic::PerObject: setLayout.usesPerObject = true; break; - case ForwardPassSemantic::Material: - if (binding.type != Resources::ShaderResourceType::ConstantBuffer || passLayout.material.IsValid()) { - return failLayout("BuiltinForwardPipeline requires a single constant-buffer Material resource"); - } - passLayout.material = { binding.set, binding.binding }; + case BuiltinPassResourceSemantic::Material: setLayout.usesMaterial = true; break; - case ForwardPassSemantic::BaseColorTexture: - if ((binding.type != Resources::ShaderResourceType::Texture2D && - binding.type != Resources::ShaderResourceType::TextureCube) || - passLayout.baseColorTexture.IsValid()) { - return failLayout("BuiltinForwardPipeline requires a single texture BaseColorTexture resource"); - } - passLayout.baseColorTexture = { binding.set, binding.binding }; + case BuiltinPassResourceSemantic::BaseColorTexture: setLayout.usesTexture = true; break; - case ForwardPassSemantic::LinearClampSampler: - if (binding.type != Resources::ShaderResourceType::Sampler || passLayout.linearClampSampler.IsValid()) { - return failLayout("BuiltinForwardPipeline requires a single sampler LinearClampSampler resource"); - } - passLayout.linearClampSampler = { binding.set, binding.binding }; + case BuiltinPassResourceSemantic::LinearClampSampler: setLayout.usesSampler = true; break; default: @@ -718,7 +607,7 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP !passLayout.setLayouts.empty() && passLayout.setLayouts[0].bindings.empty()) { PassSetLayoutMetadata& compatibilitySet = passLayout.setLayouts[0]; - if (usesConstantBuffers) { + if (bindingPlan.usesConstantBuffers) { compatibilitySet.bindings.push_back({ 0, static_cast(RHI::DescriptorType::CBV), @@ -726,7 +615,7 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP 0 }); } - if (usesTextures) { + if (bindingPlan.usesTextures) { compatibilitySet.bindings.push_back({ 0, static_cast(RHI::DescriptorType::SRV), @@ -734,7 +623,7 @@ BuiltinForwardPipeline::PassResourceLayout* BuiltinForwardPipeline::GetOrCreateP 0 }); } - if (usesSamplers) { + if (bindingPlan.usesSamplers) { compatibilitySet.bindings.push_back({ 0, static_cast(RHI::DescriptorType::Sampler), diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp index 1ff0f62a..1adc849b 100644 --- a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -10,6 +10,8 @@ using namespace XCEngine::Rendering::Pipelines; using namespace XCEngine::Rendering::Passes; +using namespace XCEngine::Rendering; +using namespace XCEngine::Containers; using namespace XCEngine::Resources; using namespace XCEngine::RHI; @@ -76,6 +78,48 @@ TEST(BuiltinForwardPipeline_Test, BuiltinForwardShaderDeclaresExplicitForwardRes delete shader; } +TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromExplicitForwardResources) { + ShaderLoader loader; + LoadResult result = loader.Load(GetBuiltinForwardLitShaderPath()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("ForwardLit"); + ASSERT_NE(pass, nullptr); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(pass->resources, 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, 1u); + EXPECT_EQ(plan.descriptorSetCount, 4u); + EXPECT_TRUE(plan.usesConstantBuffers); + EXPECT_TRUE(plan.usesTextures); + EXPECT_TRUE(plan.usesSamplers); + + delete shader; +} + +TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLegacyFallbackResources) { + const Array bindings = BuildLegacyBuiltinForwardPassResourceBindings(); + ASSERT_EQ(bindings.Size(), 4u); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(bindings, plan, &error)) << error.CStr(); + ASSERT_EQ(plan.bindings.Size(), 4u); + EXPECT_EQ(plan.perObject.set, 1u); + EXPECT_EQ(plan.material.set, 2u); + EXPECT_EQ(plan.baseColorTexture.set, 3u); + EXPECT_EQ(plan.linearClampSampler.set, 4u); +} + TEST(BuiltinObjectIdPass_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) { const InputLayoutDesc inputLayout = BuiltinObjectIdPass::BuildInputLayout();