You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/Atom/RPI/Code/Tests/Shader/ShaderTests.cpp

1831 lines
87 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzTest/AzTest.h>
#include <Atom/RHI.Reflect/RenderAttachmentLayoutBuilder.h>
#include <Atom/RHI.Reflect/ShaderStageFunction.h>
#include <Atom/RPI.Reflect/Shader/ShaderAsset.h>
#include <Atom/RPI.Reflect/Shader/ShaderAssetCreator.h>
#include <Atom/RPI.Reflect/Shader/ShaderOptionGroup.h>
#include <Atom/RPI.Edit/Shader/ShaderVariantTreeAssetCreator.h>
#include <Atom/RPI.Edit/Shader/ShaderVariantAssetCreator.h>
#include <Atom/RHI/RHISystemInterface.h>
#include <Atom/RPI.Public/Shader/Shader.h>
#include <Common/RPITestFixture.h>
#include <Common/ErrorMessageFinder.h>
#include <Common/SerializeTester.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Utils/TypeHash.h>
#include <AzCore/Math/Random.h>
#include <AzCore/std/string/conversions.h>
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<ShaderAsset>
{
using Base = UnitTest::SerializeTester<ShaderAsset>;
public:
ShaderAssetTester(AZ::SerializeContext* serializeContext)
: Base(serializeContext)
{}
AZ::Data::Asset<ShaderAsset> SerializeInHelper(const AZ::Data::AssetId& assetId)
{
AZ::Data::Asset<ShaderAsset> asset = Base::SerializeIn(assetId);
asset->SelectShaderApiData();
asset->SetReady();
return asset;
}
};
}
}
namespace UnitTest
{
using namespace AZ;
using ShaderByteCode = AZStd::vector<uint8_t>;
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<AZ::SerializeContext*>(context))
{
serializeContext->Class<TestPipelineLayoutDescriptor, AZ::RHI::PipelineLayoutDescriptor>()
->Version(1)
;
}
}
static AZ::RHI::Ptr<TestPipelineLayoutDescriptor> 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<AZ::SerializeContext*>(context))
{
serializeContext->Class<TestShaderStageFunction, AZ::RHI::ShaderStageFunction>()
->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<const uint8_t*>(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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RHI::ShaderResourceGroupLayout> 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<AZ::RPI::ShaderOptionGroupLayout> CreateShaderOptionLayout(AZ::RHI::Handle<size_t> indexToOmit = {})
{
using namespace AZ;
RPI::Ptr<RPI::ShaderOptionGroupLayout> 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<RHI::ShaderResourceGroupLayout> 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<RHI::ShaderResourceGroupLayout> 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<uint32_t>(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<uint32_t>(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<AZStd::string> 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<Name> 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<RPI::ShaderVariantAsset> CreateTestShaderVariantAsset(RPI::ShaderVariantId id, RPI::ShaderVariantStableId stableId,
bool isFullyBaked,
const AZStd::vector<RHI::ShaderStage>& 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<RHI::ShaderStageFunction> vertexStageFunction = aznew TestShaderStageFunction(rhiStage);
shaderVariantAssetCreator.SetShaderFunction(rhiStage, vertexStageFunction);
}
Data::Asset<RPI::ShaderVariantAsset> shaderVariantAsset;
shaderVariantAssetCreator.End(shaderVariantAsset);
return shaderVariantAsset;
}
void BeginCreatingTestShaderAsset(AZ::RPI::ShaderAssetCreator& creator,
const AZStd::vector<RHI::ShaderStage>& 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<RPI::ShaderVariantAsset> 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<AZ::RPI::ShaderAsset> EndCreatingTestShaderAsset(RPI::ShaderAssetCreator& creator)
{
Data::Asset<RPI::ShaderAsset> shaderAsset;
if (creator.EndAPI())
{
creator.End(shaderAsset);
}
return shaderAsset;
}
AZ::Data::Asset<AZ::RPI::ShaderAsset> CreateShaderAsset()
{
using namespace AZ;
RPI::ShaderAssetCreator creator;
BeginCreatingTestShaderAsset(creator);
Data::Asset<RPI::ShaderAsset> shaderAsset = EndCreatingTestShaderAsset(creator);
return shaderAsset;
}
//! The tree will only contain the root variant.
AZ::Data::Asset<AZ::RPI::ShaderVariantTreeAsset> CreateEmptyShaderVariantTreeAsset(Data::Asset<RPI::ShaderAsset> shaderAsset)
{
using namespace AZ;
AZStd::vector<RPI::ShaderVariantListSourceData::VariantInfo> shaderVariantList;
RPI::ShaderVariantTreeAssetCreator creator;
creator.Begin(Uuid::CreateRandom());
creator.SetShaderOptionGroupLayout(*shaderAsset->GetShaderOptionGroupLayout());
creator.SetVariantInfos(shaderVariantList);
Data::Asset<RPI::ShaderVariantTreeAsset> shaderVariantTreeAsset;
if (!creator.End(shaderVariantTreeAsset))
{
return {};
}
return shaderVariantTreeAsset;
}
AZ::Data::Asset<AZ::RPI::ShaderVariantTreeAsset> CreateShaderVariantTreeAssetForSearch(Data::Asset<RPI::ShaderAsset> shaderAsset)
{
using namespace AZ;
AZStd::vector<RPI::ShaderVariantListSourceData::VariantInfo> 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<RPI::ShaderVariantTreeAsset> shaderVariantTreeAsset;
if (!creator.End(shaderVariantTreeAsset))
{
return {};
}
return shaderVariantTreeAsset;
}
void ValidateShaderAsset(const AZ::Data::Asset<AZ::RPI::ShaderAsset>& 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<AZ::RPI::Shader>& 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<AZ::RPI::ShaderOptionDescriptor, 4> m_bindings;
AZ::Name m_name;
AZ::Name m_drawListName;
AZ::RHI::Ptr<AZ::RHI::PipelineLayoutDescriptor> m_pipelineLayoutDescriptor;
AZ::RPI::Ptr<AZ::RPI::ShaderOptionGroupLayout> m_shaderOptionGroupLayoutForAsset;
AZ::RPI::Ptr<AZ::RPI::ShaderOptionGroupLayout> m_shaderOptionGroupLayoutForVariants;
AZ::RHI::RenderStates m_renderStates;
AZStd::fixed_vector<AZ::RHI::Ptr<AZ::RHI::ShaderResourceGroupLayout>, 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<RPI::ShaderOptionGroupLayout> 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<RPI::ShaderOptionGroupLayout> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionGroupLayout> CreateOptionsLayoutWithAllBools()
{
AZStd::vector<RPI::ShaderOptionValuePair> boolIdList;
boolIdList.push_back({Name("Off"), RPI::ShaderOptionValue(0)});
boolIdList.push_back({Name("On"), RPI::ShaderOptionValue(1)});
RPI::Ptr<RPI::ShaderOptionGroupLayout> 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<AZ::RPI::ShaderVariantKeyBitCount> 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<AZ::RPI::ShaderVariantKeyBitCount> 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<AZ::RPI::ShaderVariantKeyBitCount> 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<AZ::RPI::ShaderVariantKeyBitCount> 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<ShaderOptionGroupLayout> 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<AZ::RPI::ShaderAsset> 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<AZ::RPI::ShaderAsset> 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<AZ::RPI::ShaderAsset> 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<AZ::RPI::ShaderAsset> 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<AZ::RPI::ShaderAsset> shaderAsset = EndCreatingTestShaderAsset(creator);
messageFinder.CheckExpectedErrorsFound();
EXPECT_FALSE(shaderAsset);
}
TEST_F(ShaderTests, ShaderAsset_Serialize_Test)
{
using namespace AZ;
Data::Asset<RPI::ShaderAsset> shaderAsset = CreateShaderAsset();
ValidateShaderAsset(shaderAsset);
RPI::ShaderAssetTester tester(GetSerializeContext());
tester.SerializeOut(shaderAsset.Get());
Data::Asset<RPI::ShaderAsset> 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<RPI::ShaderAsset> 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<size_t>(indexToOmit));
AZ_TEST_START_TRACE_SUPPRESSION;
Data::Asset<RPI::ShaderAsset> shaderAsset = CreateShaderAsset();
Data::Asset<RPI::ShaderVariantTreeAsset> shaderVariantTreeAsset = CreateShaderVariantTreeAssetForSearch(shaderAsset);
AZ_TEST_STOP_TRACE_SUPPRESSION_NO_COUNT;
EXPECT_FALSE(shaderVariantTreeAsset);
}
TEST_F(ShaderTests, Shader_Baseline_Test)
{
using namespace AZ;
Data::Instance<RPI::Shader> 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<RPI::ShaderOptionValuePair> 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<RPI::ShaderOptionGroupLayout> 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;
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());
}
}