/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include "RHITestFixture.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace UnitTest { using namespace AZ; class ShaderStageFunction : public RHI::ShaderStageFunction { public: AZ_CLASS_ALLOCATOR(ShaderStageFunction, SystemAllocator, 0); ShaderStageFunction(uint64_t hash, RHI::ShaderStage shaderStage) : RHI::ShaderStageFunction(shaderStage) , m_declaredHash{hash} {} RHI::ResultCode FinalizeInternal() override { SetHash(HashValue64(m_declaredHash)); return RHI::ResultCode::Success; } private: uint64_t m_declaredHash; }; struct BaselineStructWithPadding { BaselineStructWithPadding() { m_b = 1; m_c = reinterpret_cast(0x12345); } uint8_t m_b; // 7 bytes of padding. const uint32_t* m_c; HashValue64 GetHash() const { return TypeHash64(*this); } }; static_assert(sizeof(BaselineStructWithPadding) == 16, "Baseline struct does not exhibit expected padding."); /** * These tests validate that RHI structs which expose a 'GetHash()' method * will produce a reliable hash. Specifically, it tests the hash under conditions * where the underlying memory includes pad bytes which are uninitialized. */ class HashingTests : public RHITestFixture { public: HashingTests() : RHITestFixture() { } protected: enum class Expect { Success = 0, Failure }; void ScrambleMemory(void* memory, size_t size) { uint8_t* bytes = reinterpret_cast(memory); for (size_t i = 0; i < size; ++i) { bytes[i] = static_cast(m_random.GetRandom()); } } template void TestHash(AZStd::function initFunction = nullptr) { void* memory = azmalloc(sizeof(T)); T* type = nullptr; uint64_t hashPrev = 0; bool isFirstHash = true; const size_t IterationCount = 10; for (size_t i = 0; i < IterationCount; ++i) { ScrambleMemory(memory, sizeof(T)); type = new (memory) T(); if (initFunction) { initFunction(*type); } uint64_t hashCurr = static_cast(type->GetHash()); if (!isFirstHash) { if constexpr (expect == Expect::Success) { EXPECT_EQ(hashCurr, hashPrev); } else { EXPECT_NE(hashCurr, hashPrev); } } isFirstHash = false; hashPrev = hashCurr; type->~T(); } azfree(memory); } RHI::ConstPtr CreateShaderResourceGroupLayout() { RHI::Ptr layout = RHI::ShaderResourceGroupLayout::Create(); // The values here are just arbitrary. layout->SetBindingSlot(0); layout->AddShaderInput(RHI::ShaderInputConstantDescriptor{ Name("InputA"), 0, 12, 0}); layout->AddShaderInput(RHI::ShaderInputConstantDescriptor{ Name("InputB"), 12, 12, 0 }); layout->AddShaderInput(RHI::ShaderInputConstantDescriptor{ Name("InputC"), 24, 76, 0 }); layout->AddStaticSampler(RHI::ShaderInputStaticSamplerDescriptor{ Name("StaticSamplerA"), RHI::SamplerState::CreateAnisotropic(16, RHI::AddressMode::Wrap), 1 }); layout->Finalize(); return layout; } RHI::ShaderResourceGroupBindingInfo CreateShaderResourceGroupBindingInfo() { RHI::ShaderResourceGroupBindingInfo bindingInfo; bindingInfo.m_constantDataBindingInfo.m_registerId = 0; bindingInfo.m_resourcesRegisterMap.insert({ "StaticSamplerA", RHI::ResourceBindingInfo{RHI::ShaderStageMask::Vertex, 1} }); return bindingInfo; } void InitStreamLayout(RHI::InputStreamLayout& layout) { // The values here are just arbitrary (Except for buffer index which is validated in Finalize()) layout.SetTopology(RHI::PrimitiveTopology::TriangleList); layout.AddStreamBuffer(RHI::StreamBufferDescriptor{ RHI::StreamStepFunction::PerVertex, 1, 8 }); layout.AddStreamBuffer(RHI::StreamBufferDescriptor{ RHI::StreamStepFunction::PerInstance, 3, 16 }); layout.AddStreamChannel(RHI::StreamChannelDescriptor{ RHI::ShaderSemantic{Name("ChannelA"), 1}, RHI::Format::R8G8B8A8_UINT, 4, 0}); layout.AddStreamChannel(RHI::StreamChannelDescriptor{ RHI::ShaderSemantic{Name("ChannelB"), 1}, RHI::Format::R10G10B10A2_UINT, 7, 1}); layout.Finalize(); } void InitRenderAttachmentLayout(RHI::RenderAttachmentLayout& layout) { // The values here are just arbitrary layout.m_subpassCount = 2; layout.m_attachmentFormats[0] = RHI::Format::ASTC_10x6_UNORM; layout.m_attachmentFormats[1] = RHI::Format::ASTC_8x8_UNORM; layout.m_attachmentFormats[2] = RHI::Format::R32G32B32_FLOAT; layout.m_attachmentFormats[3] = RHI::Format::R16G16_UINT; { auto& subpassLayout = layout.m_subpassLayouts[0]; subpassLayout.m_rendertargetCount = 2; subpassLayout.m_subpassInputCount = 0; subpassLayout.m_depthStencilDescriptor = RHI::RenderAttachmentDescriptor{ 0, RHI::InvalidRenderAttachmentIndex, RHI::AttachmentLoadStoreAction() }; subpassLayout.m_rendertargetDescriptors[0] = RHI::RenderAttachmentDescriptor{ 1, RHI::InvalidRenderAttachmentIndex, RHI::AttachmentLoadStoreAction() }; subpassLayout.m_rendertargetDescriptors[1] = RHI::RenderAttachmentDescriptor{ 2, 3, RHI::AttachmentLoadStoreAction(RHI::ClearValue(), RHI::AttachmentLoadAction::DontCare) }; } { auto& subpassLayout = layout.m_subpassLayouts[1]; subpassLayout.m_rendertargetCount = 1; subpassLayout.m_subpassInputCount = 2; subpassLayout.m_rendertargetDescriptors[0] = RHI::RenderAttachmentDescriptor{ 0, 1, RHI::AttachmentLoadStoreAction() }; subpassLayout.m_subpassInputIndices[0] = 2; subpassLayout.m_subpassInputIndices[0] = 3; } } void InitRenderAttachmentConfiguration(RHI::RenderAttachmentConfiguration& configuration) { InitRenderAttachmentLayout(configuration.m_renderAttachmentLayout); configuration.m_subpassIndex = 1; } private: void SetUp() override { RHITestFixture::SetUp(); m_factory.reset(aznew Factory); } void TearDown() override { m_factory = nullptr; RHITestFixture::TearDown(); } SimpleLcgRandom m_random; AZStd::unique_ptr m_factory; }; TEST_F(HashingTests, BaselineTest) { // We expect this to fail, because padding exists in the baseline structure. TestHash(); } TEST_F(HashingTests, RenderStatesTest) { TestHash(); } TEST_F(HashingTests, ImageTest) { TestHash(); TestHash(); TestHash(); RHI::ClearValue clearValue; TestHash([&clearValue](RHI::TransientImageDescriptor& desc) { desc.m_attachmentId = RHI::AttachmentId{"ABC"}; desc.m_optimizedClearValue = &clearValue; }); } TEST_F(HashingTests, BufferTest) { TestHash(); TestHash(); TestHash([](RHI::TransientBufferDescriptor& desc) { desc.m_attachmentId = RHI::AttachmentId{"EFG"}; }); } TEST_F(HashingTests, SamplerStateTest) { TestHash(); } TEST_F(HashingTests, ShaderResourceGroupLayoutTest) { TestHash([](RHI::ShaderInputBufferDescriptor& input) { input.m_name = "InputA"; }); TestHash([](RHI::ShaderInputImageDescriptor& input) { input.m_name = "InputA"; }); TestHash([](RHI::ShaderInputSamplerDescriptor& input) { input.m_name = "InputA"; }); TestHash([](RHI::ShaderInputConstantDescriptor& input) { input.m_name = "InputA"; }); } TEST_F(HashingTests, StreamLayoutTest) { TestHash([](RHI::ShaderSemantic& semantic) { semantic.m_name = Name{"COLOR"}; semantic.m_index = 1; }); TestHash([](RHI::StreamChannelDescriptor& layout) { layout.m_semantic = RHI::ShaderSemantic{Name("UV"), 1}; }); TestHash(); TestHash([this](RHI::InputStreamLayout& layout) { InitStreamLayout(layout); }); } TEST_F(HashingTests, RenderAttachmentLayoutTest) { TestHash([this](RHI::RenderAttachmentLayout& layout) { InitRenderAttachmentLayout(layout); }); } TEST_F(HashingTests, RenderAttachmentConfigurationTest) { TestHash([this](RHI::RenderAttachmentConfiguration& configuration) { InitRenderAttachmentConfiguration(configuration); }); } TEST_F(HashingTests, PipelineStateTest) { // These are assigned by us, so just pick something arbitrary. const uint64_t vertFunctionHash = 0xABCDEF00; const uint64_t fragFunctionHash = 0xABCDEF01; RHI::Ptr vertFunction = aznew ShaderStageFunction(vertFunctionHash, RHI::ShaderStage::Vertex); vertFunction->Finalize(); RHI::Ptr fragFunction = aznew ShaderStageFunction(fragFunctionHash, RHI::ShaderStage::Fragment); fragFunction->Finalize(); RHI::InputStreamLayout inputStreamLayout; InitStreamLayout(inputStreamLayout); RHI::RenderAttachmentConfiguration renderAttachmentConfiguration; InitRenderAttachmentConfiguration(renderAttachmentConfiguration); RHI::ConstPtr srgLayout = CreateShaderResourceGroupLayout(); RHI::ShaderResourceGroupBindingInfo bindingInfo = CreateShaderResourceGroupBindingInfo(); RHI::Ptr pipelineLayoutDesc = RHI::PipelineLayoutDescriptor::Create(); pipelineLayoutDesc->AddShaderResourceGroupLayoutInfo(*srgLayout, bindingInfo); pipelineLayoutDesc->Finalize(); TestHash([&] (RHI::PipelineStateDescriptorForDraw& desc) { desc.m_vertexFunction = vertFunction; desc.m_fragmentFunction = fragFunction; desc.m_pipelineLayoutDescriptor = pipelineLayoutDesc; desc.m_inputStreamLayout = inputStreamLayout; desc.m_renderAttachmentConfiguration = renderAttachmentConfiguration; }); const uint64_t computeFunctionHash = 0xABCDEF02; RHI::Ptr computeFunction = aznew ShaderStageFunction(computeFunctionHash, RHI::ShaderStage::Compute); TestHash([&](RHI::PipelineStateDescriptorForDispatch& desc) { desc.m_pipelineLayoutDescriptor = pipelineLayoutDesc; desc.m_computeFunction = computeFunction; }); } }