/* * Copyright (c) Contributors to the Open 3D Engine Project * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace AZ { namespace RPI { /// This length represents the up-aligned shader variant key length in respect to the shader register space /// AZSLc aligns all keys up to a register length and this constant emulates that requirement static constexpr uint32_t ShaderVariantKeyAlignedBitCount = (ShaderVariantKeyBitCount % ShaderRegisterBitSize == 0) ? ShaderVariantKeyBitCount : ShaderVariantKeyBitCount + (ShaderRegisterBitSize - ShaderVariantKeyBitCount % ShaderRegisterBitSize); class ShaderAssetTester : public UnitTest::SerializeTester { using Base = UnitTest::SerializeTester; public: ShaderAssetTester(AZ::SerializeContext* serializeContext) : Base(serializeContext) {} AZ::Data::Asset SerializeInHelper(const AZ::Data::AssetId& assetId) { AZ::Data::Asset asset = Base::SerializeIn(assetId); asset->SelectShaderApiData(); asset->SetReady(); return asset; } }; } } namespace UnitTest { using namespace AZ; using ShaderByteCode = AZStd::vector; class TestPipelineLayoutDescriptor : public AZ::RHI::PipelineLayoutDescriptor { public: AZ_RTTI(TestPipelineLayoutDescriptor, "{B226636F-7C85-4500-B499-26C112D1128B}", AZ::RHI::PipelineLayoutDescriptor); static void Reflect(AZ::ReflectContext* context) { if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(1) ; } } static AZ::RHI::Ptr Create() { return aznew TestPipelineLayoutDescriptor; } }; class TestShaderStageFunction : public AZ::RHI::ShaderStageFunction { public: AZ_RTTI(TestShaderStageFunction, "{1BAEE536-96CA-4AEB-BA73-D5D72EE35B45}", AZ::RHI::ShaderStageFunction); AZ_CLASS_ALLOCATOR(ShaderStageFunction, AZ::SystemAllocator, 0); static void Reflect(AZ::ReflectContext* context) { if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(1) ->Field("m_byteCode", &TestShaderStageFunction::m_byteCode) ->Field("m_index", &TestShaderStageFunction::m_index) ; } } TestShaderStageFunction() = default; explicit TestShaderStageFunction(AZ::RHI::ShaderStage shaderStage) : AZ::RHI::ShaderStageFunction(shaderStage) {} void SetIndex(size_t index) { m_index = index; } size_t m_index; ShaderByteCode m_byteCode; private: AZ::RHI::ResultCode FinalizeInternal() override { SetHash(AZ::TypeHash64(reinterpret_cast(m_byteCode.data()), m_byteCode.size())); return AZ::RHI::ResultCode::Success; } }; class ShaderTests : public RPITestFixture { protected: void SetUp() override { using namespace AZ; RPITestFixture::SetUp(); auto* serializeContext = GetSerializeContext(); TestPipelineLayoutDescriptor::Reflect(serializeContext); TestShaderStageFunction::Reflect(serializeContext); // Example of unscoped enum AZStd::vector idList0; idList0.push_back({ Name("Black"), RPI::ShaderOptionValue(0) }); // 1+ bit idList0.push_back({ Name("Maroon"), RPI::ShaderOptionValue(1) }); // ... idList0.push_back({ Name("Green"), RPI::ShaderOptionValue(2) }); // 2+ bits idList0.push_back({ Name("Olive"), RPI::ShaderOptionValue(3) }); // ... idList0.push_back({ Name("Navy"), RPI::ShaderOptionValue(4) }); // 3+ bits idList0.push_back({ Name("Purple"), RPI::ShaderOptionValue(5) }); // ... idList0.push_back({ Name("Teal"), RPI::ShaderOptionValue(6) }); // ... idList0.push_back({ Name("Silver"), RPI::ShaderOptionValue(7) }); // ... idList0.push_back({ Name("Gray"), RPI::ShaderOptionValue(8) }); // 4+ bits idList0.push_back({ Name("Red"), RPI::ShaderOptionValue(9) }); // ... idList0.push_back({ Name("Lime"), RPI::ShaderOptionValue(10) }); // ... idList0.push_back({ Name("Yellow"), RPI::ShaderOptionValue(11) }); // ... idList0.push_back({ Name("Blue"), RPI::ShaderOptionValue(12) }); // ... idList0.push_back({ Name("Fuchsia"), RPI::ShaderOptionValue(13) }); // ... idList0.push_back({ Name("Cyan"), RPI::ShaderOptionValue(14) }); // ... idList0.push_back({ Name("White"), RPI::ShaderOptionValue(15) }); // ... uint32_t bitOffset = 0; uint32_t order = 0; m_bindings[0] = RPI::ShaderOptionDescriptor{ Name("Color"), RPI::ShaderOptionType::Enumeration, bitOffset, order++, idList0, Name("Fuchsia") }; bitOffset = m_bindings[0].GetBitOffset() + m_bindings[0].GetBitCount(); // Example of scoped enum - the only difference is that enumerators are qualified AZStd::vector idList1; idList1.push_back({ Name("Quality::Auto"), RPI::ShaderOptionValue(0) }); // 1+ bit idList1.push_back({ Name("Quality::Poor"), RPI::ShaderOptionValue(1) }); // ... idList1.push_back({ Name("Quality::Low"), RPI::ShaderOptionValue(2) }); // 2+ bits idList1.push_back({ Name("Quality::Average"), RPI::ShaderOptionValue(3) }); // ... idList1.push_back({ Name("Quality::Good"), RPI::ShaderOptionValue(4) }); // 3+ bits idList1.push_back({ Name("Quality::High"), RPI::ShaderOptionValue(5) }); // ... idList1.push_back({ Name("Quality::Ultra"), RPI::ShaderOptionValue(6) }); // ... idList1.push_back({ Name("Quality::Sublime"), RPI::ShaderOptionValue(7) }); // ... m_bindings[1] = RPI::ShaderOptionDescriptor{ Name("Quality"), RPI::ShaderOptionType::Enumeration, bitOffset, order++, idList1, Name("Quality::Auto") }; bitOffset = m_bindings[1].GetBitOffset() + m_bindings[1].GetBitCount(); // Example of integer range. It only requires two values, min and max. The name id-s are expected to match the numericla value. AZStd::vector idList2; idList2.push_back({ Name("5"), RPI::ShaderOptionValue(5) }); // 1+ bit idList2.push_back({ Name("200"), RPI::ShaderOptionValue(200) }); // 8+ bits idList2.push_back({ Name("10"), RPI::ShaderOptionValue(10) }); // It doesn't really matter whether there are extra numbers; the shader option will take the min and max m_bindings[2] = RPI::ShaderOptionDescriptor{ Name("NumberSamples"), RPI::ShaderOptionType::IntegerRange, bitOffset, order++, idList2, Name("50") }; bitOffset = m_bindings[2].GetBitOffset() + m_bindings[2].GetBitCount(); // Example of boolean. By standard, the first value should be false (0). AZStd::vector idList3; idList3.push_back({ Name("Off"), RPI::ShaderOptionValue(0) }); // 1+ bit idList3.push_back({ Name("On"), RPI::ShaderOptionValue(1) }); // ... m_bindings[3] = RPI::ShaderOptionDescriptor{ Name("Raytracing"), RPI::ShaderOptionType::Boolean, bitOffset, order++, idList3, Name("Off") }; bitOffset = m_bindings[3].GetBitOffset() + m_bindings[3].GetBitCount(); m_name = Name("TestName"); m_drawListName = Name("DrawListTagName"); m_pipelineLayoutDescriptor = TestPipelineLayoutDescriptor::Create(); m_shaderOptionGroupLayoutForAsset = CreateShaderOptionLayout(); m_shaderOptionGroupLayoutForVariants = m_shaderOptionGroupLayoutForAsset; // Just set up a couple values, not the whole struct, for some basic checking later that the struct is copied. m_renderStates.m_rasterState.m_fillMode = RHI::FillMode::Wireframe; m_renderStates.m_multisampleState.m_samples = 4; m_renderStates.m_depthStencilState.m_depth.m_func = RHI::ComparisonFunc::Equal; m_renderStates.m_depthStencilState.m_stencil.m_enable = 1; m_renderStates.m_blendState.m_targets[0].m_blendOp = RHI::BlendOp::SubtractReverse; for (size_t i = 0; i < RHI::Limits::Pipeline::ShaderResourceGroupCountMax; ++i) { RHI::Ptr srgLayout = CreateShaderResourceGroupLayout(i); AZ::RHI::ShaderResourceGroupBindingInfo bindingInfo = CreateShaderResouceGroupBindingInfo(i); m_pipelineLayoutDescriptor->AddShaderResourceGroupLayoutInfo(*srgLayout.get(), bindingInfo); m_srgLayouts.push_back(srgLayout); } m_pipelineLayoutDescriptor->Finalize(); } void TearDown() override { m_name = Name{}; m_drawListName = Name{}; for (size_t i = 0; i < m_bindings.size(); ++i) { m_bindings[i] = {}; } m_srgLayouts.clear(); m_pipelineLayoutDescriptor = nullptr; m_shaderOptionGroupLayoutForAsset = nullptr; m_shaderOptionGroupLayoutForVariants = nullptr; RPITestFixture::TearDown(); } AZ::RPI::Ptr CreateShaderOptionLayout(AZ::RHI::Handle indexToOmit = {}) { using namespace AZ; RPI::Ptr layout = RPI::ShaderOptionGroupLayout::Create(); for (size_t i = 0; i < m_bindings.size(); ++i) { // Allows omitting a single option to test for missing options. if (indexToOmit.GetIndex() != i) { layout->AddShaderOption(m_bindings[i]); } } layout->Finalize(); return layout; } AZ::Name CreateShaderResourceGroupId(size_t index) { using namespace AZ; return Name{ AZStd::to_string(index) }; } RHI::Ptr CreateShaderResourceGroupLayout(size_t index) { using namespace AZ; Name srgId = CreateShaderResourceGroupId(index); // Creates a simple SRG asset with a unique SRG layout hash (based on the index). RHI::Ptr srgLayout = RHI::ShaderResourceGroupLayout::Create(); srgLayout->SetName(srgId); srgLayout->SetBindingSlot(aznumeric_caster(index)); srgLayout->AddShaderInput(RHI::ShaderInputBufferDescriptor{ srgId, RHI::ShaderInputBufferAccess::Read, RHI::ShaderInputBufferType::Raw, 1, 4, static_cast(index) }); EXPECT_TRUE(srgLayout->Finalize()); return srgLayout; } AZ::RHI::ShaderResourceGroupBindingInfo CreateShaderResouceGroupBindingInfo(size_t index) { Name srgId = CreateShaderResourceGroupId(index); AZ::RHI::ShaderResourceGroupBindingInfo bindingInfo; bindingInfo.m_resourcesRegisterMap.insert({ srgId, RHI::ResourceBindingInfo{RHI::ShaderStageMask::Vertex, static_cast(index)} }); return bindingInfo; } AZ::RPI::ShaderInputContract CreateSimpleShaderInputContract() { AZ::RPI::ShaderInputContract contract; AZ::RPI::ShaderInputContract::StreamChannelInfo channel; channel.m_semantic = AZ::RHI::ShaderSemantic{ AZ::Name{"POSITION"} }; contract.m_streamChannels.push_back(channel); return contract; } AZ::RPI::ShaderOutputContract CreateSimpleShaderOutputContract() { AZ::RPI::ShaderOutputContract contract; AZ::RPI::ShaderOutputContract::ColorAttachmentInfo attachment; attachment.m_componentCount = 4; contract.m_requiredColorAttachments.push_back(attachment); return contract; } RPI::ShaderVariantListSourceData::VariantInfo CreateVariantInfo(uint32_t stableId, AZStd::vector optionValues) { RPI::ShaderVariantListSourceData::VariantInfo variantInfo; variantInfo.m_stableId = stableId; auto nextValue = optionValues.begin(); auto nextOption = m_shaderOptionGroupLayoutForVariants->GetShaderOptions().begin(); while (nextValue != optionValues.end() && nextOption != m_shaderOptionGroupLayoutForVariants->GetShaderOptions().end()) { AZStd::string optionNameStr(nextOption->GetName().GetCStr()); if (nextValue->empty()) { // TODO (To consider) If we decide to support gaps (unqualified options) in the lookup key // we can actually remove this check variantInfo.m_options[optionNameStr] = nextOption->GetDefaultValue().GetCStr(); } else { variantInfo.m_options[optionNameStr] = *nextValue; } nextValue++; nextOption++; } return variantInfo; } // Creates and returns a shader option group with the specified option values. RPI::ShaderOptionGroup CreateShaderOptionGroup(AZStd::vector optionValues) { RPI::ShaderOptionGroup shaderOptionGroup(m_shaderOptionGroupLayoutForVariants); auto nextValue = optionValues.begin(); auto nextOption = m_shaderOptionGroupLayoutForVariants->GetShaderOptions().begin(); while (nextValue != optionValues.end() && nextOption != m_shaderOptionGroupLayoutForVariants->GetShaderOptions().end()) { if (nextValue->IsEmpty()) { // TODO (To consider) If we decide to support gaps (unqualified options) in the lookup key // we can actually remove this check shaderOptionGroup.SetValue(nextOption->GetName(), nextOption->GetDefaultValue()); } else { shaderOptionGroup.SetValue(nextOption->GetName(), *nextValue); } nextValue++; nextOption++; } return shaderOptionGroup; } Data::Asset CreateTestShaderVariantAsset(RPI::ShaderVariantId id, RPI::ShaderVariantStableId stableId, bool isFullyBaked, const AZStd::vector& stagesToActivate = {RHI::ShaderStage::Vertex, RHI::ShaderStage::Fragment}) { RPI::ShaderVariantAssetCreator shaderVariantAssetCreator; shaderVariantAssetCreator.Begin(Uuid::CreateRandom(), id, stableId, isFullyBaked); shaderVariantAssetCreator.SetBuildTimestamp(AZStd::sys_time_t(1)); //Make non-zero for (RHI::ShaderStage rhiStage : stagesToActivate) { RHI::Ptr vertexStageFunction = aznew TestShaderStageFunction(rhiStage); shaderVariantAssetCreator.SetShaderFunction(rhiStage, vertexStageFunction); } Data::Asset shaderVariantAsset; shaderVariantAssetCreator.End(shaderVariantAsset); return shaderVariantAsset; } void BeginCreatingTestShaderAsset(AZ::RPI::ShaderAssetCreator& creator, const AZStd::vector& stagesToActivate = {RHI::ShaderStage::Vertex, RHI::ShaderStage::Fragment} ) { using namespace AZ; creator.Begin(Uuid::CreateRandom()); creator.SetName(m_name); creator.SetDrawListName(m_drawListName); creator.SetShaderOptionGroupLayout(m_shaderOptionGroupLayoutForAsset); creator.BeginAPI(RHI::Factory::Get().GetType()); creator.BeginSupervariant(AZ::Name{}); // The default (first) supervariant MUST be nameless. creator.SetSrgLayoutList(m_srgLayouts); creator.SetPipelineLayout(m_pipelineLayoutDescriptor); creator.SetRenderStates(m_renderStates); creator.SetInputContract(CreateSimpleShaderInputContract()); creator.SetOutputContract(CreateSimpleShaderOutputContract()); RHI::ShaderStageAttributeMapList attributeMaps; attributeMaps.resize(RHI::ShaderStageCount); creator.SetShaderStageAttributeMapList(attributeMaps); Data::Asset shaderVariantAsset = CreateTestShaderVariantAsset(RPI::ShaderVariantId{}, RPI::ShaderVariantStableId{0}, false, stagesToActivate); creator.SetRootShaderVariantAsset(shaderVariantAsset); creator.EndSupervariant(); } //! Used to finish creating a shader that began with BeginCreatingTestShaderAsset(). Call this after adding all the desired shader variants. AZ::Data::Asset EndCreatingTestShaderAsset(RPI::ShaderAssetCreator& creator) { Data::Asset shaderAsset; if (creator.EndAPI()) { creator.End(shaderAsset); } return shaderAsset; } AZ::Data::Asset CreateShaderAsset() { using namespace AZ; RPI::ShaderAssetCreator creator; BeginCreatingTestShaderAsset(creator); Data::Asset shaderAsset = EndCreatingTestShaderAsset(creator); return shaderAsset; } //! The tree will only contain the root variant. AZ::Data::Asset CreateEmptyShaderVariantTreeAsset(Data::Asset shaderAsset) { using namespace AZ; AZStd::vector shaderVariantList; RPI::ShaderVariantTreeAssetCreator creator; creator.Begin(Uuid::CreateRandom()); creator.SetShaderOptionGroupLayout(*shaderAsset->GetShaderOptionGroupLayout()); creator.SetVariantInfos(shaderVariantList); Data::Asset shaderVariantTreeAsset; if (!creator.End(shaderVariantTreeAsset)) { return {}; } return shaderVariantTreeAsset; } AZ::Data::Asset CreateShaderVariantTreeAssetForSearch(Data::Asset shaderAsset) { using namespace AZ; AZStd::vector shaderVariantList; shaderVariantList.push_back(CreateVariantInfo(1, { AZStd::string{"Fuchsia"} })); shaderVariantList.push_back(CreateVariantInfo(2, { AZStd::string{"Fuchsia"}, AZStd::string{"Quality::Auto"} })); shaderVariantList.push_back(CreateVariantInfo(3, { AZStd::string{"Fuchsia"}, AZStd::string{"Quality::Auto"}, AZStd::string{"50"} })); shaderVariantList.push_back(CreateVariantInfo(4, { AZStd::string{"Fuchsia"}, AZStd::string{"Quality::Auto"}, AZStd::string{"50"}, AZStd::string{"Off"} })); shaderVariantList.push_back(CreateVariantInfo(5, { AZStd::string{"Fuchsia"}, AZStd::string{"Quality::Auto"}, AZStd::string{"50"}, AZStd::string{"On"} })); shaderVariantList.push_back(CreateVariantInfo(6, { AZStd::string{"Teal"} })); shaderVariantList.push_back(CreateVariantInfo(7, { AZStd::string{"Teal"}, AZStd::string{"Quality::Sublime"} })); RPI::ShaderVariantTreeAssetCreator creator; creator.Begin(Uuid::CreateRandom()) ; creator.SetShaderOptionGroupLayout(*shaderAsset->GetShaderOptionGroupLayout()); creator.SetVariantInfos(shaderVariantList); Data::Asset shaderVariantTreeAsset; if (!creator.End(shaderVariantTreeAsset)) { return {}; } return shaderVariantTreeAsset; } void ValidateShaderAsset(const AZ::Data::Asset& shaderAsset) { using namespace AZ; EXPECT_TRUE(shaderAsset); EXPECT_EQ(shaderAsset->GetName(), m_name); EXPECT_EQ(shaderAsset->GetDrawListName(), m_drawListName); EXPECT_EQ(shaderAsset->GetShaderOptionGroupLayout()->GetHash(), m_shaderOptionGroupLayoutForAsset->GetHash()); EXPECT_EQ(shaderAsset->GetPipelineLayoutDescriptor()->GetHash(), m_pipelineLayoutDescriptor->GetHash()); for (size_t i = 0; i < shaderAsset->GetShaderResourceGroupLayouts().size(); ++i) { auto& srgLayout = shaderAsset->GetShaderResourceGroupLayouts()[i]; EXPECT_EQ(srgLayout->GetHash(), m_srgLayouts[i]->GetHash()); EXPECT_EQ(shaderAsset->FindShaderResourceGroupLayout(CreateShaderResourceGroupId(i))->GetHash(), srgLayout->GetHash()); } } void ValidateShader(const AZ::Data::Instance& shader) { using namespace AZ; EXPECT_TRUE(shader); EXPECT_TRUE(shader->GetAsset()); auto shaderAsset = shader->GetAsset(); EXPECT_EQ(shader->GetPipelineStateType(), shaderAsset->GetPipelineStateType()); EXPECT_EQ(shader->GetShaderResourceGroupLayouts(), shaderAsset->GetShaderResourceGroupLayouts()); const RPI::ShaderVariant& rootShaderVariant = shader->GetVariant( RPI::ShaderVariantStableId{0} ); RHI::PipelineStateDescriptorForDraw descriptorForDraw; rootShaderVariant.ConfigurePipelineState(descriptorForDraw); EXPECT_EQ(descriptorForDraw.m_pipelineLayoutDescriptor->GetHash(), m_pipelineLayoutDescriptor->GetHash()); EXPECT_NE(descriptorForDraw.m_vertexFunction, nullptr); EXPECT_NE(descriptorForDraw.m_fragmentFunction, nullptr); EXPECT_EQ(descriptorForDraw.m_renderStates.GetHash(), m_renderStates.GetHash()); EXPECT_EQ(descriptorForDraw.m_inputStreamLayout.GetHash(), HashValue64{ 0 }); // ConfigurePipelineState shouldn't touch descriptorForDraw.m_inputStreamLayout EXPECT_EQ(descriptorForDraw.m_renderAttachmentConfiguration.GetHash(), RHI::RenderAttachmentConfiguration().GetHash()); // ConfigurePipelineState shouldn't touch descriptorForDraw.m_outputAttachmentLayout // Actual layout content doesn't matter for this test, it just needs to be set up to pass validation inside AcquirePipelineState(). descriptorForDraw.m_inputStreamLayout.SetTopology(RHI::PrimitiveTopology::TriangleList); descriptorForDraw.m_inputStreamLayout.Finalize(); RHI::RenderAttachmentLayoutBuilder builder; builder.AddSubpass() ->RenderTargetAttachment(RHI::Format::R8G8B8A8_SNORM) ->DepthStencilAttachment(RHI::Format::R32_FLOAT); builder.End(descriptorForDraw.m_renderAttachmentConfiguration.m_renderAttachmentLayout); const RHI::PipelineState* pipelineState = shader->AcquirePipelineState(descriptorForDraw); EXPECT_NE(pipelineState, nullptr); } AZStd::array m_bindings; AZ::Name m_name; AZ::Name m_drawListName; AZ::RHI::Ptr m_pipelineLayoutDescriptor; AZ::RPI::Ptr m_shaderOptionGroupLayoutForAsset; AZ::RPI::Ptr m_shaderOptionGroupLayoutForVariants; AZ::RHI::RenderStates m_renderStates; AZStd::fixed_vector, AZ::RHI::Limits::Pipeline::ShaderResourceGroupCountMax> m_srgLayouts; }; TEST_F(ShaderTests, ShaderOptionBindingTest) { using namespace AZ; EXPECT_EQ(m_bindings[0].GetBitMask(), RPI::ShaderVariantKey{ AZ_BIT_MASK_OFFSET(4, 0) }); EXPECT_EQ(m_bindings[1].GetBitMask(), RPI::ShaderVariantKey{ AZ_BIT_MASK_OFFSET(3, 4) }); EXPECT_EQ(m_bindings[2].GetBitMask(), RPI::ShaderVariantKey{ AZ_BIT_MASK_OFFSET(8, 7) }); EXPECT_EQ(m_bindings[3].GetBitMask(), RPI::ShaderVariantKey{ AZ_BIT_MASK_OFFSET(1, 15) }); EXPECT_TRUE(m_bindings[0].FindValue(Name("Navy")).IsValid()); EXPECT_FALSE(m_bindings[0].FindValue(Name("Color::Navy")).IsValid()); // Not found - Color is unscoped EXPECT_TRUE(m_bindings[1].FindValue(Name("Quality::Average")).IsValid()); EXPECT_FALSE(m_bindings[1].FindValue(Name("Average")).IsValid()); // Not found - Quality is scoped EXPECT_FALSE(m_bindings[1].FindValue(Name("Cake")).IsValid()); // Not found - Cake is not on the list EXPECT_FALSE(m_bindings[1].FindValue(Name("Quality::Cake")).IsValid()); // Not found - still not on the list EXPECT_TRUE(m_bindings[2].FindValue(Name("5")).IsValid()); EXPECT_TRUE(m_bindings[2].FindValue(Name("200")).IsValid()); EXPECT_TRUE(m_bindings[2].FindValue(Name("42")).IsValid()); EXPECT_FALSE(m_bindings[2].FindValue(Name("-1")).IsValid()); // Not found - less than MinValue EXPECT_FALSE(m_bindings[2].FindValue(Name("1001")).IsValid()); // Not found - more than MaxValue EXPECT_TRUE(m_bindings[3].FindValue(Name("Off")).IsValid()); EXPECT_TRUE(m_bindings[3].FindValue(Name("On")).IsValid()); EXPECT_FALSE(m_bindings[3].FindValue(Name("False")).IsValid()); // Not found - the correct user-defined id is Off EXPECT_FALSE(m_bindings[3].FindValue(Name("True")).IsValid()); // Not found - the correct user-defined id is On EXPECT_EQ(m_bindings[0].GetValueName(RPI::ShaderOptionValue(4)), Name("Navy")); EXPECT_EQ(m_bindings[1].GetValueName(RPI::ShaderOptionValue(3)), Name("Quality::Average")); EXPECT_EQ(m_bindings[2].GetValueName(RPI::ShaderOptionValue(200)), Name("200")); EXPECT_EQ(m_bindings[3].GetValueName(RPI::ShaderOptionValue(0)), Name("Off")); EXPECT_EQ(m_bindings[3].GetValueName(RPI::ShaderOptionValue(1)), Name("On")); EXPECT_TRUE(m_bindings[2].GetValueName(RPI::ShaderOptionValue(-1)).IsEmpty()); // No matching value EXPECT_TRUE(m_bindings[2].GetValueName(RPI::ShaderOptionValue(1001)).IsEmpty()); // No matching value RPI::Ptr shaderOptionGroupLayout = RPI::ShaderOptionGroupLayout::Create(); bool success = shaderOptionGroupLayout->AddShaderOption(m_bindings[0]); EXPECT_TRUE(success); success = shaderOptionGroupLayout->AddShaderOption(m_bindings[1]); EXPECT_TRUE(success); success = shaderOptionGroupLayout->AddShaderOption(m_bindings[2]); EXPECT_TRUE(success); success = shaderOptionGroupLayout->AddShaderOption(m_bindings[3]); EXPECT_TRUE(success); shaderOptionGroupLayout->Finalize(); EXPECT_TRUE(shaderOptionGroupLayout->IsFinalized()); RPI::ShaderOptionGroup testGroup(shaderOptionGroupLayout); m_bindings[0].Set(testGroup, m_bindings[0].FindValue(Name("Gray"))); EXPECT_EQ(m_bindings[0].Get(testGroup).GetIndex(), RPI::ShaderOptionValue(8).GetIndex()); m_bindings[0].Set(testGroup, RPI::ShaderOptionValue(1)); EXPECT_EQ(m_bindings[0].Get(testGroup).GetIndex(), RPI::ShaderOptionValue(1).GetIndex()); testGroup.SetValue(Name("Color"), Name("Olive")); EXPECT_EQ(testGroup.GetValue(Name("Color")).GetIndex(), RPI::ShaderOptionValue(3).GetIndex()); testGroup.SetValue(Name("Color"), RPI::ShaderOptionValue(5)); EXPECT_EQ(testGroup.GetValue(Name("Color")).GetIndex(), RPI::ShaderOptionValue(5).GetIndex()); testGroup.SetValue(RPI::ShaderOptionIndex(0), Name("Lime")); EXPECT_EQ(testGroup.GetValue(RPI::ShaderOptionIndex(0)).GetIndex(), RPI::ShaderOptionValue(10).GetIndex()); testGroup.SetValue(RPI::ShaderOptionIndex(0), RPI::ShaderOptionValue(0)); EXPECT_EQ(testGroup.GetValue(RPI::ShaderOptionIndex(0)).GetIndex(), RPI::ShaderOptionValue(0).GetIndex()); m_bindings[1].Set(testGroup, m_bindings[1].FindValue(Name("Quality::Average"))); EXPECT_EQ(m_bindings[1].Get(testGroup).GetIndex(), RPI::ShaderOptionValue(3).GetIndex()); m_bindings[1].Set(testGroup, RPI::ShaderOptionValue(1)); EXPECT_EQ(m_bindings[1].Get(testGroup).GetIndex(), RPI::ShaderOptionValue(1).GetIndex()); testGroup.SetValue(Name("Quality"), Name("Quality::Ultra")); EXPECT_EQ(testGroup.GetValue(Name("Quality")).GetIndex(), RPI::ShaderOptionValue(6).GetIndex()); testGroup.SetValue(Name("Quality"), RPI::ShaderOptionValue(5)); EXPECT_EQ(testGroup.GetValue(Name("Quality")).GetIndex(), RPI::ShaderOptionValue(5).GetIndex()); testGroup.SetValue(RPI::ShaderOptionIndex(1), Name("Quality::Auto")); EXPECT_EQ(testGroup.GetValue(RPI::ShaderOptionIndex(1)).GetIndex(), RPI::ShaderOptionValue(0).GetIndex()); testGroup.SetValue(RPI::ShaderOptionIndex(1), RPI::ShaderOptionValue(2)); EXPECT_EQ(testGroup.GetValue(RPI::ShaderOptionIndex(1)).GetIndex(), RPI::ShaderOptionValue(2).GetIndex()); m_bindings[2].Set(testGroup, m_bindings[2].FindValue(Name("150"))); EXPECT_EQ(m_bindings[2].Get(testGroup).GetIndex(), RPI::ShaderOptionValue(150).GetIndex()); m_bindings[2].Set(testGroup, RPI::ShaderOptionValue(120)); EXPECT_EQ(m_bindings[2].Get(testGroup).GetIndex(), RPI::ShaderOptionValue(120).GetIndex()); testGroup.SetValue(Name("NumberSamples"), Name("101")); EXPECT_EQ(testGroup.GetValue(Name("NumberSamples")).GetIndex(), RPI::ShaderOptionValue(101).GetIndex()); testGroup.SetValue(Name("NumberSamples"), RPI::ShaderOptionValue(102)); EXPECT_EQ(testGroup.GetValue(Name("NumberSamples")).GetIndex(), RPI::ShaderOptionValue(102).GetIndex()); testGroup.SetValue(RPI::ShaderOptionIndex(2), Name("103")); EXPECT_EQ(testGroup.GetValue(RPI::ShaderOptionIndex(2)).GetIndex(), RPI::ShaderOptionValue(103).GetIndex()); testGroup.SetValue(RPI::ShaderOptionIndex(2), RPI::ShaderOptionValue(104)); EXPECT_EQ(testGroup.GetValue(RPI::ShaderOptionIndex(2)).GetIndex(), RPI::ShaderOptionValue(104).GetIndex()); // Tests for invalid or Null value id // Setting a valid value id changes the key testGroup.SetValue(Name("Quality"), Name("Quality::Sublime")); EXPECT_EQ(testGroup.GetValue(Name("Quality")).GetIndex(), RPI::ShaderOptionValue(7).GetIndex()); // "Cake" is delicious, but it's not a valid option for "Quality" // Setting an invalid value id does nothing - it's ignored, so the key remains the same AZ_TEST_START_TRACE_SUPPRESSION; testGroup.SetValue(Name("Quality"), Name("Cake")); AZ_TEST_STOP_TRACE_SUPPRESSION(1); EXPECT_EQ(testGroup.GetValue(Name("Quality")).GetIndex(), RPI::ShaderOptionValue(7).GetIndex()); // ClearValue clears the mask testGroup.ClearValue(Name("Quality")); EXPECT_EQ(testGroup.GetValue(Name("Quality")).IsNull(), true); } TEST_F(ShaderTests, ShaderOptionGroupLayoutTest) { using namespace AZ; RPI::Ptr shaderOptionGroupLayout = RPI::ShaderOptionGroupLayout::Create(); bool success = shaderOptionGroupLayout->AddShaderOption(m_bindings[0]); EXPECT_TRUE(success); success = shaderOptionGroupLayout->AddShaderOption(m_bindings[1]); EXPECT_TRUE(success); success = shaderOptionGroupLayout->AddShaderOption(m_bindings[2]); EXPECT_TRUE(success); success = shaderOptionGroupLayout->AddShaderOption(m_bindings[3]); EXPECT_TRUE(success); auto intRangeType = RPI::ShaderOptionType::IntegerRange; uint32_t order = m_bindings[3].GetOrder() + 1; // The tests below will fail anyway, but still ErrorMessageFinder errorMessageFinder; // Overlaps previous mask. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("mask overlaps with previously added masks"); AZStd::vector list0; list0.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit list0.push_back({ Name("1"), RPI::ShaderOptionValue(1) }); // ... success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, 6, order++, list0, Name("0") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add shader option that extends past end of bit mask. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("exceeds size of mask"); AZStd::vector list1; list1.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit list1.push_back({ Name("255"), RPI::ShaderOptionValue(255) }); // 8+ bit success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, RPI::ShaderVariantKeyBitCount - 4, order++, list1, Name("0") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add shader option with empty name. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("empty name"); AZStd::vector list2; list2.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit list2.push_back({ Name("1"), RPI::ShaderOptionValue(1) }); // ... success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{}, intRangeType, 16, order++, list2, Name("0") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add shader option with empty bits. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("has zero bits"); AZStd::vector list3; success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, 16, order++, list3, Name("0") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // An integer range option must have at least two values defining the range errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("has zero bits"); AZStd::vector list3b; list3b.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, 16, order++, list3b, Name("0") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add a shader option with an order that collides with an existing shader option errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("has the same order"); uint32_t bitOffset = m_bindings[3].GetBitOffset() + m_bindings[3].GetBitCount(); AZStd::vector list4; list4.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit list4.push_back({ Name("1"), RPI::ShaderOptionValue(1) }); // ... success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, bitOffset, 0, list4, Name("0") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add shader option with an empty default value. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("invalid default value"); AZStd::vector list5; list5.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit list5.push_back({ Name("1"), RPI::ShaderOptionValue(1) }); // ... success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, 16, order++, list5, Name() }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add shader option with an invalid default int value. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("invalid default value"); AZStd::vector list6; list6.push_back({ Name("0"), RPI::ShaderOptionValue(0) }); // 1+ bit list6.push_back({ Name("1"), RPI::ShaderOptionValue(1) }); // ... success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, intRangeType, 16, order++, list6, Name("3") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Add shader option with an invalid default enum value. errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("invalid default value"); AZStd::vector list7; list7.push_back({ Name("TypeA"), RPI::ShaderOptionValue(0) }); list7.push_back({ Name("TypeB"), RPI::ShaderOptionValue(1) }); list7.push_back({ Name("TypeC"), RPI::ShaderOptionValue(2) }); success = shaderOptionGroupLayout->AddShaderOption(AZ::RPI::ShaderOptionDescriptor{ Name{"Invalid"}, RPI::ShaderOptionType::Enumeration, 16, order++, list7, Name("TypeO") }); EXPECT_FALSE(success); errorMessageFinder.CheckExpectedErrorsFound(); // Test access before finalize. EXPECT_FALSE(shaderOptionGroupLayout->IsFinalized()); errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("ShaderOptionGroupLayout is not finalized", 4); EXPECT_EQ(shaderOptionGroupLayout->FindShaderOptionIndex(m_bindings[0].GetName()), RPI::ShaderOptionIndex()); EXPECT_EQ(shaderOptionGroupLayout->FindShaderOptionIndex(m_bindings[1].GetName()), RPI::ShaderOptionIndex()); EXPECT_EQ(shaderOptionGroupLayout->FindShaderOptionIndex(m_bindings[2].GetName()), RPI::ShaderOptionIndex()); EXPECT_EQ(shaderOptionGroupLayout->FindShaderOptionIndex(m_bindings[3].GetName()), RPI::ShaderOptionIndex()); errorMessageFinder.CheckExpectedErrorsFound(); { errorMessageFinder.Reset(); errorMessageFinder.AddExpectedErrorMessage("ShaderOptionGroupLayout is not finalized"); RPI::ShaderVariantKey testKey = 1; EXPECT_FALSE(shaderOptionGroupLayout->IsValidShaderVariantKey(testKey)); errorMessageFinder.CheckExpectedErrorsFound(); } errorMessageFinder.Disable(); shaderOptionGroupLayout->Finalize(); EXPECT_TRUE(shaderOptionGroupLayout->IsFinalized()); EXPECT_EQ(shaderOptionGroupLayout->GetShaderOptionCount(), 4); EXPECT_EQ(shaderOptionGroupLayout->GetShaderOption(RPI::ShaderOptionIndex(0)), m_bindings[0]); EXPECT_EQ(shaderOptionGroupLayout->GetShaderOption(RPI::ShaderOptionIndex(1)), m_bindings[1]); EXPECT_EQ(shaderOptionGroupLayout->GetShaderOption(RPI::ShaderOptionIndex(2)), m_bindings[2]); EXPECT_EQ(shaderOptionGroupLayout->GetShaderOption(RPI::ShaderOptionIndex(3)), m_bindings[3]); RPI::ShaderVariantKey unionMask; for (const auto& binding : m_bindings) { unionMask |= binding.GetBitMask(); } EXPECT_EQ(unionMask, shaderOptionGroupLayout->GetBitMask()); EXPECT_TRUE(shaderOptionGroupLayout->IsValidShaderVariantKey(m_bindings[0].GetBitMask())); EXPECT_TRUE(shaderOptionGroupLayout->IsValidShaderVariantKey(m_bindings[1].GetBitMask())); EXPECT_TRUE(shaderOptionGroupLayout->IsValidShaderVariantKey(m_bindings[2].GetBitMask())); EXPECT_TRUE(shaderOptionGroupLayout->IsValidShaderVariantKey(m_bindings[3].GetBitMask())); // Test value-lookup functions auto& colorOption = shaderOptionGroupLayout->GetShaderOption(RPI::ShaderOptionIndex{ 0 }); EXPECT_EQ(colorOption.FindValue(Name{ "Navy" }).GetIndex(), 4); EXPECT_EQ(colorOption.FindValue(Name{ "Purple" }).GetIndex(), 5); EXPECT_FALSE(colorOption.FindValue(Name{ "Blah" }).IsValid()); EXPECT_EQ(shaderOptionGroupLayout->FindValue(RPI::ShaderOptionIndex{ 0 }, Name{ "Navy" }).GetIndex(), 4); EXPECT_EQ(shaderOptionGroupLayout->FindValue(RPI::ShaderOptionIndex{ 0 }, Name{ "Purple" }).GetIndex(), 5); EXPECT_EQ(shaderOptionGroupLayout->FindValue(Name{ "Color" }, Name{ "Navy" }).GetIndex(), 4); EXPECT_EQ(shaderOptionGroupLayout->FindValue(Name{ "Color" }, Name{ "Purple" }).GetIndex(), 5); EXPECT_FALSE(shaderOptionGroupLayout->FindValue(RPI::ShaderOptionIndex{ 0 }, Name{ "Blah" }).IsValid()); EXPECT_FALSE(shaderOptionGroupLayout->FindValue(Name{ "Color" }, Name{ "Blah" }).IsValid()); EXPECT_FALSE(shaderOptionGroupLayout->FindValue(RPI::ShaderOptionIndex{}, Name{ "Navy" }).IsValid()); EXPECT_FALSE(shaderOptionGroupLayout->FindValue(RPI::ShaderOptionIndex{ 100 }, Name{ "Navy" }).IsValid()); EXPECT_FALSE(shaderOptionGroupLayout->FindValue(Name{ "Blah" }, Name{ "Navy" }).IsValid()); EXPECT_FALSE(shaderOptionGroupLayout->FindShaderOptionIndex(Name{ "Invalid" }).IsValid()); } TEST_F(ShaderTests, ShaderOptionGroupTest) { using namespace AZ; RPI::ShaderOptionGroup group(m_shaderOptionGroupLayoutForAsset); EXPECT_TRUE(group.GetShaderVariantId().IsEmpty()); group.SetValue(RPI::ShaderOptionIndex(0), RPI::ShaderOptionValue(7)); group.SetValue(RPI::ShaderOptionIndex(1), RPI::ShaderOptionValue(6)); group.SetValue(RPI::ShaderOptionIndex(2), RPI::ShaderOptionValue(5)); group.SetValue(RPI::ShaderOptionIndex(3), RPI::ShaderOptionValue(1)); group.SetValue(group.FindShaderOptionIndex(m_bindings[0].GetName()), RPI::ShaderOptionValue(7)); group.SetValue(group.FindShaderOptionIndex(m_bindings[1].GetName()), RPI::ShaderOptionValue(6)); group.SetValue(group.FindShaderOptionIndex(m_bindings[2].GetName()), RPI::ShaderOptionValue(5)); group.SetValue(group.FindShaderOptionIndex(m_bindings[3].GetName()), RPI::ShaderOptionValue(1)); EXPECT_FALSE(group.GetShaderVariantId().IsEmpty()); EXPECT_EQ(group.GetValue(group.FindShaderOptionIndex(m_bindings[0].GetName())).GetIndex(), 7); EXPECT_EQ(group.GetValue(group.FindShaderOptionIndex(m_bindings[1].GetName())).GetIndex(), 6); EXPECT_EQ(group.GetValue(group.FindShaderOptionIndex(m_bindings[2].GetName())).GetIndex(), 5); EXPECT_EQ(group.GetValue(group.FindShaderOptionIndex(m_bindings[3].GetName())).GetIndex(), 1); EXPECT_EQ(group.FindShaderOptionIndex(Name{}), RPI::ShaderOptionIndex{}); EXPECT_EQ(group.FindShaderOptionIndex(Name{ "Invalid" }), RPI::ShaderOptionIndex{}); // Helper methods - these are suboptimal since because they fetch index from id // The intended use for these methods is in prototypes and simple sample code group.SetValue(Name("Color"), Name("Fuchsia")); // 13 group.SetValue(Name("Quality"), Name("Quality::Sublime")); // 7 group.SetValue(Name("NumberSamples"), Name("190")); // 190 group.SetValue(Name("Raytracing"), Name("On")); // 1 EXPECT_EQ(group.GetValue(Name("Color")).GetIndex(), 13); EXPECT_EQ(group.GetValue(Name("Quality")).GetIndex(), 7); EXPECT_EQ(group.GetValue(Name("NumberSamples")).GetIndex(), 190); EXPECT_EQ(group.GetValue(Name("Raytracing")).GetIndex(), 1); } RPI::Ptr CreateOptionsLayoutWithAllBools() { AZStd::vector boolIdList; boolIdList.push_back({Name("Off"), RPI::ShaderOptionValue(0)}); boolIdList.push_back({Name("On"), RPI::ShaderOptionValue(1)}); RPI::Ptr layout = RPI::ShaderOptionGroupLayout::Create(); for (uint32_t i = 0; i < AZ::RPI::ShaderVariantKeyBitCount; ++i) { RPI::ShaderOptionDescriptor option{ Name{AZStd::string::format("option%d", i)}, RPI::ShaderOptionType::Boolean, i, i, boolIdList, Name{"Off"}}; EXPECT_TRUE(layout->AddShaderOption(option)); } layout->Finalize(); return layout; } TEST_F(ShaderTests, ShaderOptionGroup_AccessEachBit_AllOtherOptionsUnspecified) { AZStd::bitset allBitsOff; for (size_t i = 0; i < AZ::RPI::ShaderVariantKeyBitCount; ++i) { // Verify the assumption that bitset is initialized to all false EXPECT_FALSE(allBitsOff[i]); } for (size_t targetBit = 0; targetBit < AZ::RPI::ShaderVariantKeyBitCount; ++targetBit) { RPI::ShaderOptionGroup group(CreateOptionsLayoutWithAllBools()); // Set target bit on, all other bits are unspecified group.SetValue(AZ::RPI::ShaderOptionIndex{targetBit}, AZ::RPI::ShaderOptionValue{1}); for (int j = 0; j < AZ::RPI::ShaderVariantKeyBitCount; ++j) { if (j == targetBit) { EXPECT_TRUE(group.GetValue(AZ::RPI::ShaderOptionIndex{j}).IsValid()); EXPECT_EQ(1, group.GetValue(AZ::RPI::ShaderOptionIndex{j}).GetIndex()); } else { EXPECT_FALSE(group.GetValue(AZ::RPI::ShaderOptionIndex{j}).IsValid()); } } AZStd::bitset expected = allBitsOff; expected[targetBit] = true; EXPECT_EQ(expected, group.GetShaderVariantId().m_key); EXPECT_EQ(expected, group.GetShaderVariantId().m_mask); } } TEST_F(ShaderTests, ShaderOptionGroup_AccessEachBit_AllOtherOptionsTrue) { AZStd::bitset allBitsOn; allBitsOn.set(); for (size_t i = 0; i < AZ::RPI::ShaderVariantKeyBitCount; ++i) { EXPECT_TRUE(allBitsOn[i]); } for (size_t targetBit = 0; targetBit < AZ::RPI::ShaderVariantKeyBitCount; ++targetBit) { RPI::ShaderOptionGroup group(CreateOptionsLayoutWithAllBools()); // Set all other bits on for (int j = 0; j < AZ::RPI::ShaderVariantKeyBitCount; ++j) { group.SetValue(AZ::RPI::ShaderOptionIndex{j}, AZ::RPI::ShaderOptionValue{1}); } // Set the target bit off group.SetValue(AZ::RPI::ShaderOptionIndex{targetBit}, AZ::RPI::ShaderOptionValue{0}); for (int j = 0; j < AZ::RPI::ShaderVariantKeyBitCount; ++j) { if (j == targetBit) { EXPECT_TRUE(group.GetValue(AZ::RPI::ShaderOptionIndex{j}).IsValid()); EXPECT_EQ(0, group.GetValue(AZ::RPI::ShaderOptionIndex{j}).GetIndex()); } else { EXPECT_TRUE(group.GetValue(AZ::RPI::ShaderOptionIndex{j}).IsValid()); EXPECT_EQ(1, group.GetValue(AZ::RPI::ShaderOptionIndex{j}).GetIndex()); } } AZStd::bitset expected = allBitsOn; expected[targetBit] = false; EXPECT_EQ(expected, group.GetShaderVariantId().m_key); EXPECT_EQ(allBitsOn, group.GetShaderVariantId().m_mask); } } TEST_F(ShaderTests, ShaderOptionGroup_SetAllToDefaultValues) { using namespace AZ; RPI::ShaderOptionGroup group(m_shaderOptionGroupLayoutForAsset); EXPECT_FALSE(group.GetValue(Name{"Color"}).IsValid()); EXPECT_FALSE(group.GetValue(Name{"Quality"}).IsValid()); EXPECT_FALSE(group.GetValue(Name{"NumberSamples"}).IsValid()); EXPECT_FALSE(group.GetValue(Name{"Raytracing"}).IsValid()); group.SetAllToDefaultValues(); EXPECT_EQ(13, group.GetValue(Name{"Color"}).GetIndex()); EXPECT_EQ(0, group.GetValue(Name{"Quality"}).GetIndex()); EXPECT_EQ(50, group.GetValue(Name{"NumberSamples"}).GetIndex()); EXPECT_EQ(0, group.GetValue(Name{"Raytracing"}).GetIndex()); } TEST_F(ShaderTests, ShaderOptionGroup_SetUnspecifiedToDefaultValues) { using namespace AZ; RPI::ShaderOptionGroup group(m_shaderOptionGroupLayoutForAsset); EXPECT_FALSE(group.GetValue(Name{"Color"}).IsValid()); EXPECT_FALSE(group.GetValue(Name{"Quality"}).IsValid()); EXPECT_FALSE(group.GetValue(Name{"NumberSamples"}).IsValid()); EXPECT_FALSE(group.GetValue(Name{"Raytracing"}).IsValid()); group.SetValue(Name{"Color"}, Name("Yellow")); group.SetValue(Name{"Raytracing"}, Name("On")); group.SetUnspecifiedToDefaultValues(); EXPECT_EQ(11, group.GetValue(Name{"Color"}).GetIndex()); EXPECT_EQ(0, group.GetValue(Name{"Quality"}).GetIndex()); EXPECT_EQ(50, group.GetValue(Name{"NumberSamples"}).GetIndex()); EXPECT_EQ(1, group.GetValue(Name{"Raytracing"}).GetIndex()); } TEST_F(ShaderTests, ShaderOptionGroup_ToString) { using namespace AZ; RPI::ShaderOptionGroup group(m_shaderOptionGroupLayoutForAsset); group.SetValue(Name("Color"), Name("Silver")); // 7 group.SetValue(Name("NumberSamples"), Name("50")); // 50 group.SetValue(Name("Raytracing"), Name("On")); // 1 EXPECT_STREQ("Color=7, Quality=?, NumberSamples=50, Raytracing=1", group.ToString().c_str()); } TEST_F(ShaderTests, ShaderOptionGroupTest_Errors) { using namespace AZ::RPI; Ptr layout = CreateShaderOptionLayout(); ShaderOptionIndex colorIndex = layout->FindShaderOptionIndex(Name{ "Color" }); ShaderOptionValue redValue = layout->GetShaderOption(colorIndex).FindValue(Name("Red")); RPI::ShaderOptionGroup group(layout); // Setting by option index and value index... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.SetValue(ShaderOptionIndex{}, ShaderOptionValue{})); EXPECT_FALSE(group.SetValue(ShaderOptionIndex{}, redValue)); EXPECT_FALSE(group.SetValue(colorIndex, ShaderOptionValue{})); AZ_TEST_STOP_TRACE_SUPPRESSION(3); EXPECT_TRUE(group.SetValue(colorIndex, redValue)); // Setting by option name and value index... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.SetValue(Name{ "DoesNotExist" }, ShaderOptionValue{})); EXPECT_FALSE(group.SetValue(Name{ "DoesNotExist" }, redValue)); EXPECT_FALSE(group.SetValue(Name{ "Color" }, ShaderOptionValue{})); AZ_TEST_STOP_TRACE_SUPPRESSION(3); EXPECT_TRUE(group.SetValue(Name{ "Color" }, redValue)); // Setting by option index and value name... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.SetValue(ShaderOptionIndex{}, Name{ "DoesNotExist" })); EXPECT_FALSE(group.SetValue(ShaderOptionIndex{}, Name{ "Red" })); EXPECT_FALSE(group.SetValue(colorIndex, Name{ "DoesNotExist" })); AZ_TEST_STOP_TRACE_SUPPRESSION(3); EXPECT_TRUE(group.SetValue(colorIndex, Name{ "Red" })); // Setting by option name and value name... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.SetValue(Name{ "DoesNotExist" }, Name{ "DoesNotExist" })); EXPECT_FALSE(group.SetValue(Name{ "DoesNotExist" }, Name{ "Red" })); EXPECT_FALSE(group.SetValue(Name{ "Color" }, Name{ "DoesNotExist" })); AZ_TEST_STOP_TRACE_SUPPRESSION(3); EXPECT_TRUE(group.SetValue(Name{ "Color" }, Name{ "Red" })); // GetValue by option index... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.GetValue(ShaderOptionIndex{}).IsValid()); AZ_TEST_STOP_TRACE_SUPPRESSION(1); EXPECT_TRUE(group.GetValue(colorIndex).IsValid()); // GetValue by option name... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.GetValue(Name{ "DoesNotExist" }).IsValid()); AZ_TEST_STOP_TRACE_SUPPRESSION(1); EXPECT_TRUE(group.GetValue(Name{ "Color" }).IsValid()); // Clearing by option index... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.ClearValue(ShaderOptionIndex{})); AZ_TEST_STOP_TRACE_SUPPRESSION(1); EXPECT_TRUE(group.ClearValue(colorIndex)); // Clearing by option name... AZ_TEST_START_TRACE_SUPPRESSION; EXPECT_FALSE(group.ClearValue(Name{ "DoesNotExist" })); AZ_TEST_STOP_TRACE_SUPPRESSION(1); EXPECT_TRUE(group.ClearValue(Name{ "Color" })); } TEST_F(ShaderTests, ShaderAsset_Baseline_Test) { using namespace AZ; ValidateShaderAsset(CreateShaderAsset()); } TEST_F(ShaderTests, ShaderAsset_PipelineStateType_VertexImpliesDraw) { AZ::RPI::ShaderAssetCreator creator; BeginCreatingTestShaderAsset(creator, {RHI::ShaderStage::Vertex}); AZ::Data::Asset shaderAsset = EndCreatingTestShaderAsset(creator); EXPECT_TRUE(shaderAsset); EXPECT_EQ(shaderAsset->GetPipelineStateType(), RHI::PipelineStateType::Draw); } TEST_F(ShaderTests, ShaderAsset_PipelineStateType_ComputeImpliesDispatch) { AZ::RPI::ShaderAssetCreator creator; BeginCreatingTestShaderAsset(creator, {AZ::RHI::ShaderStage::Compute}); AZ::Data::Asset shaderAsset = EndCreatingTestShaderAsset(creator); EXPECT_TRUE(shaderAsset); EXPECT_EQ(shaderAsset->GetPipelineStateType(), RHI::PipelineStateType::Dispatch); } TEST_F(ShaderTests, ShaderAsset_PipelineStateType_Error_DrawAndDispatch) { ErrorMessageFinder messageFinder("both Draw functions and Dispatch functions"); messageFinder.AddExpectedErrorMessage("Invalid root variant"); messageFinder.AddExpectedErrorMessage("Cannot continue building ShaderAsset because 1 error(s) reported"); AZ::RPI::ShaderAssetCreator creator; BeginCreatingTestShaderAsset(creator, {AZ::RHI::ShaderStage::Vertex, AZ::RHI::ShaderStage::Fragment, AZ::RHI::ShaderStage::Compute}); AZ::Data::Asset shaderAsset = EndCreatingTestShaderAsset(creator); EXPECT_FALSE(shaderAsset); } TEST_F(ShaderTests, ShaderAsset_Error_FragmentFunctionRequiresVertexFunction) { ErrorMessageFinder messageFinder("fragment function but no vertex function"); messageFinder.AddExpectedErrorMessage("Invalid root variant"); messageFinder.AddExpectedErrorMessage("Cannot continue building ShaderAsset because 1 error(s) reported"); AZ::RPI::ShaderAssetCreator creator; BeginCreatingTestShaderAsset(creator, {AZ::RHI::ShaderStage::Fragment}); AZ::Data::Asset shaderAsset = EndCreatingTestShaderAsset(creator); messageFinder.CheckExpectedErrorsFound(); EXPECT_FALSE(shaderAsset); } TEST_F(ShaderTests, ShaderAsset_Error_TessellationFunctionRequiresVertexFunction) { ErrorMessageFinder messageFinder("tessellation function but no vertex function"); messageFinder.AddExpectedErrorMessage("Invalid root variant"); messageFinder.AddExpectedErrorMessage("Cannot continue building ShaderAsset because 1 error(s) reported"); AZ::RPI::ShaderAssetCreator creator; BeginCreatingTestShaderAsset(creator, { AZ::RHI::ShaderStage::Tessellation }); AZ::Data::Asset shaderAsset = EndCreatingTestShaderAsset(creator); messageFinder.CheckExpectedErrorsFound(); EXPECT_FALSE(shaderAsset); } TEST_F(ShaderTests, ShaderAsset_Serialize_Test) { using namespace AZ; Data::Asset shaderAsset = CreateShaderAsset(); ValidateShaderAsset(shaderAsset); RPI::ShaderAssetTester tester(GetSerializeContext()); tester.SerializeOut(shaderAsset.Get()); Data::Asset serializedShaderAsset = tester.SerializeInHelper(Uuid::CreateRandom()); ValidateShaderAsset(serializedShaderAsset); } TEST_F(ShaderTests, ShaderAsset_PipelineLayout_Missing_Test) { using namespace AZ; m_pipelineLayoutDescriptor = nullptr; AZ_TEST_START_TRACE_SUPPRESSION; Data::Asset shaderAsset = CreateShaderAsset(); AZ_TEST_STOP_TRACE_SUPPRESSION(2); EXPECT_FALSE(shaderAsset); } TEST_F(ShaderTests, ShaderAsset_ShaderOptionGroupLayout_Mismatch_Test) { using namespace AZ; const size_t indexToOmit = 0; // Creates a shader option group layout assigned to the asset which doesn't match the // one assigned to the the variants. m_shaderOptionGroupLayoutForAsset = CreateShaderOptionLayout(RHI::Handle(indexToOmit)); AZ_TEST_START_TRACE_SUPPRESSION; Data::Asset shaderAsset = CreateShaderAsset(); Data::Asset shaderVariantTreeAsset = CreateShaderVariantTreeAssetForSearch(shaderAsset); AZ_TEST_STOP_TRACE_SUPPRESSION_NO_COUNT; EXPECT_FALSE(shaderVariantTreeAsset); } TEST_F(ShaderTests, Shader_Baseline_Test) { using namespace AZ; Data::Instance shader = RPI::Shader::FindOrCreate(CreateShaderAsset()); ValidateShader(shader); } TEST_F(ShaderTests, ValidateShaderVariantIdMath) { RPI::ShaderVariantId idSmall; RPI::ShaderVariantId idLarge; RPI::ShaderVariantIdComparator idComparator; idSmall.m_mask = RPI::ShaderVariantKey(15); idLarge.m_mask = RPI::ShaderVariantKey(31); idSmall.m_key = RPI::ShaderVariantKey(15); idLarge.m_key = RPI::ShaderVariantKey(31); EXPECT_TRUE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), -1); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 1); // The mask has precedence so the evaluation is the same as above idSmall.m_key = RPI::ShaderVariantKey(31); idLarge.m_key = RPI::ShaderVariantKey(15); EXPECT_TRUE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), -1); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 1); // The mask has precedence so the evaluation is the same as above idSmall.m_key = RPI::ShaderVariantKey(0); idLarge.m_key = RPI::ShaderVariantKey(0); EXPECT_TRUE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), -1); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 1); // The mask has precedence so the evaluation is the same as above idSmall.m_key = RPI::ShaderVariantKey(63); idLarge.m_key = RPI::ShaderVariantKey(63); EXPECT_TRUE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), -1); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 1); // In the case where the mask are equal, the id's key should be used idSmall.m_mask = RPI::ShaderVariantKey(31); idLarge.m_mask = RPI::ShaderVariantKey(31); idSmall.m_key = RPI::ShaderVariantKey(6); idLarge.m_key = RPI::ShaderVariantKey(20); EXPECT_TRUE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), -1); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 1); // The variant id is the same idSmall.m_mask = RPI::ShaderVariantKey(31); idLarge.m_mask = RPI::ShaderVariantKey(31); idSmall.m_key = RPI::ShaderVariantKey(15); idLarge.m_key = RPI::ShaderVariantKey(15); EXPECT_FALSE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), 0); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 0); // The variant id is the same idSmall.m_mask = RPI::ShaderVariantKey(0); idLarge.m_mask = RPI::ShaderVariantKey(0); EXPECT_FALSE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), 0); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 0); // If the mask is 0, the key has insignificant bits, the variant id is the same idSmall.m_mask = RPI::ShaderVariantKey(0); idLarge.m_mask = RPI::ShaderVariantKey(0); idSmall.m_key = RPI::ShaderVariantKey(31); idLarge.m_key = RPI::ShaderVariantKey(15); EXPECT_FALSE(idComparator(idSmall, idLarge)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idSmall, idLarge), 0); EXPECT_FALSE(idComparator(idLarge, idSmall)); EXPECT_EQ(RPI::ShaderVariantIdComparator::Compare(idLarge, idSmall), 0); } TEST_F(ShaderTests, ValidateShaderVariantKeyFallbackPacking) { AZStd::vector idList0; idList0.push_back({ Name("Black"), RPI::ShaderOptionValue(0) }); // 1+ bit idList0.push_back({ Name("Maroon"), RPI::ShaderOptionValue(1) }); // ... idList0.push_back({ Name("Green"), RPI::ShaderOptionValue(2) }); // 2+ bits idList0.push_back({ Name("Olive"), RPI::ShaderOptionValue(3) }); // ... idList0.push_back({ Name("Navy"), RPI::ShaderOptionValue(4) }); // 3+ bits idList0.push_back({ Name("Purple"), RPI::ShaderOptionValue(5) }); // ... idList0.push_back({ Name("Teal"), RPI::ShaderOptionValue(6) }); // ... idList0.push_back({ Name("Silver"), RPI::ShaderOptionValue(7) }); // ... idList0.push_back({ Name("Gray"), RPI::ShaderOptionValue(8) }); // 4+ bits idList0.push_back({ Name("Red"), RPI::ShaderOptionValue(9) }); // ... idList0.push_back({ Name("Lime"), RPI::ShaderOptionValue(10) }); // ... idList0.push_back({ Name("Yellow"), RPI::ShaderOptionValue(11) }); // ... idList0.push_back({ Name("Blue"), RPI::ShaderOptionValue(12) }); // ... idList0.push_back({ Name("Fuchsia"), RPI::ShaderOptionValue(13) }); // ... idList0.push_back({ Name("Cyan"), RPI::ShaderOptionValue(14) }); // ... idList0.push_back({ Name("White"), RPI::ShaderOptionValue(15) }); // ... idList0.push_back({ Name("Beige"), RPI::ShaderOptionValue(16) }); // 5 bits!! // Six descriptors with 5 bits each are 30 bits, but AZSLc will pack them within 32-bit boundaries, so // every six descriptors will end up wasting 2 bits of register space. // This test checks for values up to 256 bits uint32_t bitOffset = 0; uint32_t order = 0; constexpr uint32_t descriptorsPerElement = 6; constexpr uint32_t numberOfElements = RPI::ShaderVariantKeyBitCount / RPI::ShaderElementBitSize; RPI::ShaderOptionDescriptor descriptor[numberOfElements * descriptorsPerElement]; RPI::Ptr shaderOptionGroupLayout = RPI::ShaderOptionGroupLayout::Create(); for (int i = 0; i < numberOfElements * descriptorsPerElement; i++) { std::stringstream ss; ss << "Color" << i; descriptor[i] = RPI::ShaderOptionDescriptor{ Name(ss.str().c_str()), RPI::ShaderOptionType::Enumeration, bitOffset, order++, idList0, Name("Fuchsia") }; shaderOptionGroupLayout->AddShaderOption(descriptor[i]); EXPECT_EQ(descriptor[i].GetBitCount(), 5); bitOffset = descriptor[i].GetBitOffset() + descriptor[i].GetBitCount(); // This hack up-aligns the bit offset to match the AZSLc behavior // (AZSLc respects a 32-bit boundary for any options used) // It doesn't matter for the test itself since we read raw data if (i % descriptorsPerElement == (descriptorsPerElement - 1)) { bitOffset += 2; } } shaderOptionGroupLayout->Finalize(); // Create and test a few ShaderOptionGroup-s // This simple test matches the expected padding for AZSLc and should only be updated // if AZSLc.exe changes the shader variant key fallback mask. auto shaderOptionGroup = RPI::ShaderOptionGroup(shaderOptionGroupLayout); // ShaderVariantKey is 32 or more bits if constexpr (numberOfElements >= 1) { shaderOptionGroup.SetValue(AZ::Name("Color0"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color1"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color2"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color3"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color4"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color5"), AZ::Name("Fuchsia")); // 13 } // ShaderVariantKey is 64 or more bits if constexpr (numberOfElements >= 2) { shaderOptionGroup.SetValue(AZ::Name("Color6"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color7"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color8"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color9"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color10"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color11"), AZ::Name("Fuchsia")); // 13 } // ShaderVariantKey is 96 or more bits if constexpr (numberOfElements >= 3) { shaderOptionGroup.SetValue(AZ::Name("Color12"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color13"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color14"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color15"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color16"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color17"), AZ::Name("Fuchsia")); // 13 } // ShaderVariantKey is 128 or more bits if constexpr (numberOfElements >= 4) { shaderOptionGroup.SetValue(AZ::Name("Color18"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color19"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color20"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color21"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color22"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color23"), AZ::Name("Fuchsia")); // 13 } // ShaderVariantKey is 160 or more bits if constexpr (numberOfElements >= 5) { shaderOptionGroup.SetValue(AZ::Name("Color24"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color25"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color26"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color27"), AZ::Name("Fuchsia")); // 13 shaderOptionGroup.SetValue(AZ::Name("Color28"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color29"), AZ::Name("Olive")); // 3 } // ShaderVariantKey is 192 or more bits if constexpr (numberOfElements >= 6) { shaderOptionGroup.SetValue(AZ::Name("Color30"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color31"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color32"), AZ::Name("Fuchsia")); // 13 shaderOptionGroup.SetValue(AZ::Name("Color33"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color34"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color35"), AZ::Name("Navy")); // 4 } // ShaderVariantKey is 224 or more bits if constexpr (numberOfElements >= 7) { shaderOptionGroup.SetValue(AZ::Name("Color36"), AZ::Name("Lime")); // 10 shaderOptionGroup.SetValue(AZ::Name("Color37"), AZ::Name("Fuchsia")); // 13 shaderOptionGroup.SetValue(AZ::Name("Color38"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color39"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color40"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color41"), AZ::Name("Teal")); // 6 } // ShaderVariantKey is 256 or more bits if constexpr (numberOfElements >= 8) { shaderOptionGroup.SetValue(AZ::Name("Color42"), AZ::Name("Fuchsia")); // 13 shaderOptionGroup.SetValue(AZ::Name("Color43"), AZ::Name("Beige")); // 16 shaderOptionGroup.SetValue(AZ::Name("Color44"), AZ::Name("Olive")); // 3 shaderOptionGroup.SetValue(AZ::Name("Color45"), AZ::Name("Navy")); // 4 shaderOptionGroup.SetValue(AZ::Name("Color46"), AZ::Name("Teal")); // 6 shaderOptionGroup.SetValue(AZ::Name("Color47"), AZ::Name("Lime")); // 10 } uint32_t fallbackValue[RPI::ShaderVariantKeyAlignedBitCount / RPI::ShaderElementBitSize]; memcpy(fallbackValue, shaderOptionGroup.GetShaderVariantId().m_key.data(), RPI::ShaderVariantKeyBitCount / 8); if constexpr (numberOfElements > 0) { EXPECT_EQ(fallbackValue[0], 0x1aa31070); } if constexpr (numberOfElements > 1) { EXPECT_EQ(fallbackValue[1], 0x1aa31203); } if constexpr (numberOfElements > 2) { EXPECT_EQ(fallbackValue[2], 0x1aa30e04); } if constexpr (numberOfElements > 3) { EXPECT_EQ(fallbackValue[3], 0x1aa20e06); } if constexpr (numberOfElements > 4) { EXPECT_EQ(fallbackValue[4], 0x0706a8c4); } if constexpr (numberOfElements > 5) { EXPECT_EQ(fallbackValue[5], 0x08383546); } if constexpr (numberOfElements > 6) { EXPECT_EQ(fallbackValue[6], 0x0c41c1aa); } if constexpr (numberOfElements > 7) { EXPECT_EQ(fallbackValue[7], 0x14620e0d); } } TEST_F(ShaderTests, ShaderAsset_ValidateSearch) { using namespace AZ; using namespace AZ::RPI; auto shaderAsset = CreateShaderAsset(); auto shaderVariantTreeAsset = CreateShaderVariantTreeAssetForSearch(shaderAsset); // We expect the following composition: // Index 0 - [] // Index 1 - [Fuchsia] // Index 2 - [Fuchsia, Quality::Auto] // Index 3 - [Fuchsia, Quality::Auto, 50] // Index 4 - [Fuchsia, Quality::Auto, 50, Off] // Index 5 - [Fuchsia, Quality::Auto, 50, On] // Index 6 - [Teal] // Index 7 - [Teal, Quality::Sublime] // Let's search it! RPI::ShaderOptionGroup shaderOptionGroup(m_shaderOptionGroupLayoutForVariants); const uint32_t stableId0 = 0; const uint32_t stableId1 = 1; const uint32_t stableId2 = 2; const uint32_t stableId3 = 3; const uint32_t stableId4 = 4; const uint32_t stableId5 = 5; const uint32_t stableId6 = 6; const uint32_t stableId7 = 7; // Index 0 - [] const auto& result0 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_TRUE(result0.IsRoot()); EXPECT_FALSE(result0.IsFullyBaked()); EXPECT_EQ(result0.GetStableId().GetIndex(), stableId0); // Index 1 - [Fuchsia] shaderOptionGroup.SetValue(Name("Color"), Name("Fuchsia")); const auto& result1 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result1.IsRoot()); EXPECT_FALSE(result1.IsFullyBaked()); EXPECT_EQ(result1.GetStableId().GetIndex(), stableId1); // Index 2 - [Fuchsia, Quality::Auto] shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Auto")); const auto& result2 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result2.IsRoot()); EXPECT_FALSE(result2.IsFullyBaked()); EXPECT_EQ(result2.GetStableId().GetIndex(), stableId2); // Index 3 - [Fuchsia, Quality::Auto, 50] shaderOptionGroup.SetValue(Name("NumberSamples"), Name("50")); const auto& result3 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result3.IsRoot()); EXPECT_FALSE(result3.IsFullyBaked()); EXPECT_EQ(result3.GetStableId().GetIndex(), stableId3); // Index 4 - [Fuchsia, Quality::Auto, 50, Off] shaderOptionGroup.SetValue(Name("Raytracing"), Name("Off")); const auto& result4 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result4.IsRoot()); EXPECT_TRUE(result4.IsFullyBaked()); EXPECT_EQ(result4.GetStableId().GetIndex(), stableId4); // Index 5 - [Fuchsia, Quality::Auto, 50, On] shaderOptionGroup.SetValue(Name("Raytracing"), Name("On")); const auto& result5 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result5.IsRoot()); EXPECT_TRUE(result5.IsFullyBaked()); EXPECT_EQ(result5.GetStableId().GetIndex(), stableId5); shaderOptionGroup.Clear(); // Index 6 - [Teal] shaderOptionGroup.SetValue(Name("Color"), Name("Teal")); const auto& result6 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result6.IsRoot()); EXPECT_FALSE(result6.IsFullyBaked()); EXPECT_EQ(result6.GetStableId().GetIndex(), stableId6); // Index 7 - [Teal, Quality::Sublime] shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Sublime")); const auto& result7 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result7.IsRoot()); EXPECT_FALSE(result7.IsFullyBaked()); EXPECT_EQ(result7.GetStableId().GetIndex(), stableId7); // All searches so far found exactly the node we were looking for // The next couple of searches will not find the requested node // and will instead default to its parent, up the tree to the root // // [] [Root] // / \ // [Color] [Teal] [Fuchsia] // / \ // [Quality] [Sublime] [Auto] // / // [NumberSamples] [50] // / \ // [Raytracing] [On] [Off] // ---------------------------------------- // [Quality::Poor] shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Color"), Name("Fuchsia")); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Poor")); // This node doesn't exist, but setting the quality forced Color to its default value, so we expect to get: // Index 1 - [Fuchsia] const auto& result8 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result8.IsRoot()); EXPECT_FALSE(result8.IsFullyBaked()); EXPECT_EQ(result8.GetStableId().GetIndex(), stableId1); // ---------------------------------------- // [Teal, Quality::Poor] shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Poor")); shaderOptionGroup.SetValue(Name("Color"), Name("Teal")); // This node doesn't exist, but we have set both Color and Quality so we expect to get: // Index 6 - [Teal] const auto& result9 = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(result9.IsRoot()); EXPECT_FALSE(result9.IsFullyBaked()); EXPECT_EQ(result9.GetStableId().GetIndex(), stableId6); // ---------------------------------------- // [Navy, Quality::Good] shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Good")); shaderOptionGroup.SetValue(Name("Color"), Name("Navy")); // This node doesn't exist (Good Navy), its parent (Navy) doesn't exist either so we expect to get the root: // Index 0 - [] const auto& resultA = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_TRUE(resultA.IsRoot()); EXPECT_FALSE(resultA.IsFullyBaked()); EXPECT_EQ(resultA.GetStableId().GetIndex(), stableId0); // ---------------------------------------- // [Teal, Quality::Sublime, 50, Off] - Test 1/3 shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Color"), Name("Teal")); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Sublime")); shaderOptionGroup.SetValue(Name("NumberSamples"), Name("50")); shaderOptionGroup.SetValue(Name("Raytracing"), Name("Off")); // No specialized nodes exist under (Teal, Sublime) so we expect to get that: // Index 7 - [Teal, Quality::Sublime] const auto& resultB = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(resultB.IsRoot()); EXPECT_FALSE(resultB.IsFullyBaked()); EXPECT_EQ(resultB.GetStableId().GetIndex(), stableId7); // ---------------------------------------- // [Teal, Quality::Sublime, 50, On] - Test 2/3 shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Color"), Name("Teal")); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Sublime")); shaderOptionGroup.SetValue(Name("NumberSamples"), Name("50")); shaderOptionGroup.SetValue(Name("Raytracing"), Name("On")); // No specialized nodes exist under (Teal, Sublime) so we expect to get that: // Index 7 - [Teal, Quality::Sublime] const auto& resultC = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(resultC.IsRoot()); EXPECT_FALSE(resultC.IsFullyBaked()); EXPECT_EQ(resultC.GetStableId().GetIndex(), stableId7); // ---------------------------------------- // [Teal, Quality::Sublime, 150] - Test 3/3 shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Color"), Name("Teal")); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Sublime")); shaderOptionGroup.SetValue(Name("NumberSamples"), Name("150")); // No specialized nodes exist under (Teal, Sublime) so we expect to get that: // Index 7 - [Teal, Quality::Sublime] const auto& resultD = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(resultD.IsRoot()); EXPECT_FALSE(resultD.IsFullyBaked()); EXPECT_EQ(resultD.GetStableId().GetIndex(), stableId7); // ---------------------------------------- // [120] shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Color"), Name("Fuchsia")); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Auto")); shaderOptionGroup.SetValue(Name("NumberSamples"), Name("120")); // The node (Fuchsia, Auto, 120) doesn't exist - note that the higher order options assume their default values. We get: // Index 2 - [Fuchsia, Quality::Auto] const auto& resultE = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(resultE.IsRoot()); EXPECT_FALSE(resultE.IsFullyBaked()); EXPECT_EQ(resultE.GetStableId().GetIndex(), stableId2); // ---------------------------------------- // [50] shaderOptionGroup.Clear(); shaderOptionGroup.SetValue(Name("Color"), Name("Fuchsia")); shaderOptionGroup.SetValue(Name("Quality"), Name("Quality::Auto")); shaderOptionGroup.SetValue(Name("NumberSamples"), Name("50")); // ---------------------------------------- shaderOptionGroup.SetValue(Name("Raytracing"), Name("Off")); // Index 4 - [Fuchsia, Quality::Auto, 50, Off] const auto& resultF = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(resultF.IsRoot()); EXPECT_TRUE(resultF.IsFullyBaked()); EXPECT_EQ(resultF.GetStableId().GetIndex(), stableId4); shaderOptionGroup.SetValue(Name("Raytracing"), Name("On")); // Index 5 - [Fuchsia, Quality::Auto, 50, On] const auto& resultG = shaderVariantTreeAsset->FindVariantStableId(shaderAsset->GetShaderOptionGroupLayout(), shaderOptionGroup.GetShaderVariantId()); EXPECT_FALSE(resultG.IsRoot()); EXPECT_TRUE(resultG.IsFullyBaked()); EXPECT_EQ(resultG.GetStableId().GetIndex(), stableId5); } TEST_F(ShaderTests, ShaderVariantAsset_IsFullyBaked) { using namespace AZ; using namespace AZ::RPI; ShaderOptionGroup shaderOptions{m_shaderOptionGroupLayoutForAsset}; Data::Asset shaderVariantAsset; shaderVariantAsset = CreateTestShaderVariantAsset(shaderOptions.GetShaderVariantId(), RPI::ShaderVariantStableId{0}, false); EXPECT_FALSE(shaderVariantAsset->IsFullyBaked()); EXPECT_FALSE(ShaderOptionGroup(m_shaderOptionGroupLayoutForAsset, shaderVariantAsset->GetShaderVariantId()).IsFullySpecified()); shaderOptions.SetValue(AZ::Name{"Color"}, AZ::Name{"Yellow"}); shaderOptions.SetValue(AZ::Name{"Quality"}, AZ::Name{"Quality::Average"}); shaderOptions.SetValue(AZ::Name{"NumberSamples"}, AZ::Name{"100"}); shaderOptions.SetValue(AZ::Name{"Raytracing"}, AZ::Name{"On"}); shaderVariantAsset = CreateTestShaderVariantAsset(shaderOptions.GetShaderVariantId(), RPI::ShaderVariantStableId{0}, true); EXPECT_TRUE(shaderVariantAsset->IsFullyBaked()); EXPECT_TRUE(ShaderOptionGroup(m_shaderOptionGroupLayoutForAsset, shaderVariantAsset->GetShaderVariantId()).IsFullySpecified()); shaderOptions.ClearValue(AZ::Name{"NumberSamples"}); shaderVariantAsset = CreateTestShaderVariantAsset(shaderOptions.GetShaderVariantId(), RPI::ShaderVariantStableId{0}, false); EXPECT_FALSE(shaderVariantAsset->IsFullyBaked()); EXPECT_FALSE(ShaderOptionGroup(m_shaderOptionGroupLayoutForAsset, shaderVariantAsset->GetShaderVariantId()).IsFullySpecified()); } }