/* * 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 #define PRINTF(...) do { UnitTest::ColoredPrintf(UnitTest::COLOR_GREEN, "[ ] "); UnitTest::ColoredPrintf(UnitTest::COLOR_YELLOW, __VA_ARGS__); } while(0) namespace UnitTest { using namespace AZ; class AllocatorTest : public RHITestFixture { public: AllocatorTest() : RHITestFixture() {} static size_t GetBytesToBlocks(size_t bytes, size_t alignment) { return size_t((bytes + alignment - 1) / alignment); } struct TestDescriptor { bool m_useVisualizer = false; RHI::Allocator* m_allocator = nullptr; size_t m_byteCountMax = 0; size_t m_byteAlignmentBase = 256; size_t m_gcLatency = 3; size_t m_iterations = 10000; size_t m_allocationSizeMin = 1; size_t m_allocationSizeMax = 8 * 1024; AZ::u64 m_addressBase = 0; }; void run(const TestDescriptor& descriptor) { struct Block { Block() : m_used{}, m_gcIteration{} {} AZ::u16 m_used; AZ::u16 m_gcIteration; }; struct Allocation { RHI::VirtualAddress address; size_t size; }; const size_t BlockCount = descriptor.m_byteCountMax / descriptor.m_byteAlignmentBase; AZStd::vector m_usedBlocks(BlockCount); AZStd::vector currentAllocations; AZStd::vector retiredAllocationsCurrent; AZStd::vector retiredAllocationsPrevious; const size_t AllocationSizeRange = descriptor.m_allocationSizeMax - descriptor.m_allocationSizeMin; AZStd::string outputString; outputString.resize(BlockCount); AZ::SimpleLcgRandom random(AZStd::GetTimeNowMicroSecond()); /** * Does a bunch of random add / remove iterations, tracking garbage collection * and block usage. It will assert if the allocator attempts to stomp on another * allocation that is marked used. */ for (size_t iteration = 0; iteration < descriptor.m_iterations; ++iteration) { bool doPrint = false; // 51% chance of doing an add. Biased towards adds so we fill up the allocator. if ((random.GetRandom() % 100) <= 51) { const size_t AllocationSize = (AllocationSizeRange ? (random.GetRandom() % AllocationSizeRange) : 0) + descriptor.m_allocationSizeMin; const size_t AllocationBlockCount = GetBytesToBlocks(AllocationSize, descriptor.m_byteAlignmentBase); RHI::VirtualAddress address = descriptor.m_allocator->Allocate(AllocationSize, descriptor.m_byteAlignmentBase); // Allocator has space. Record the allocation. if (address.IsValid()) { ASSERT_TRUE(address.m_ptr % descriptor.m_byteAlignmentBase == 0); size_t addressOffset = address.m_ptr - descriptor.m_addressBase; for (size_t currentBlock = 0; currentBlock < AllocationBlockCount; ++currentBlock) { size_t blockIndex = GetBytesToBlocks(addressOffset, descriptor.m_byteAlignmentBase) + currentBlock; ASSERT_TRUE(m_usedBlocks[blockIndex].m_used == 0); m_usedBlocks[blockIndex].m_used = 1; } currentAllocations.push_back(Allocation{ address, AllocationSize }); doPrint = true; } else { ASSERT_TRUE(currentAllocations.size() || retiredAllocationsCurrent.size()); } } else if (currentAllocations.size()) { // pick a random allocation to remove size_t allocationIndex = random.GetRandom() % currentAllocations.size(); Allocation allocation = currentAllocations[allocationIndex]; currentAllocations.erase(currentAllocations.begin() + allocationIndex); descriptor.m_allocator->DeAllocate(allocation.address); retiredAllocationsCurrent.push_back(allocation); doPrint = true; } if ((random.GetRandom() % 4) == 0) { descriptor.m_allocator->GarbageCollect(); doPrint = true; retiredAllocationsPrevious.clear(); for (Allocation retiredAllocation : retiredAllocationsCurrent) { size_t addressOffset = retiredAllocation.address.m_ptr - descriptor.m_addressBase; size_t blockIndex = addressOffset / descriptor.m_byteAlignmentBase; const size_t AllocationBlockCount = GetBytesToBlocks(retiredAllocation.size, descriptor.m_byteAlignmentBase); // Just use the root block as the bookmarked item. ASSERT_TRUE(m_usedBlocks[blockIndex].m_used == 1); m_usedBlocks[blockIndex].m_gcIteration++; if (m_usedBlocks[blockIndex].m_gcIteration > descriptor.m_gcLatency) { for (size_t currentBlock = 0; currentBlock < AllocationBlockCount; ++currentBlock) { size_t subBlockIndex = GetBytesToBlocks(addressOffset, descriptor.m_byteAlignmentBase) + currentBlock; m_usedBlocks[subBlockIndex].m_used = 0; m_usedBlocks[subBlockIndex].m_gcIteration = 0; } } else { retiredAllocationsPrevious.push_back(retiredAllocation); } } retiredAllocationsCurrent.swap(retiredAllocationsPrevious); if (descriptor.m_useVisualizer) { PRINTF("GC...\n"); } } ASSERT_TRUE((retiredAllocationsCurrent.size() + currentAllocations.size()) == descriptor.m_allocator->GetAllocationCount()); if (doPrint) { for (size_t i = 0; i < BlockCount; ++i) { outputString[i] = m_usedBlocks[i].m_used ? 'x' : '-'; } if (descriptor.m_useVisualizer) { PRINTF("%s\n", outputString.c_str()); } } } } }; TEST_F(AllocatorTest, PoolAllocator) { RHI::PoolAllocator::Descriptor descriptor; descriptor.m_elementSize = 128; descriptor.m_capacityInBytes = 128 * descriptor.m_elementSize; descriptor.m_garbageCollectLatency = 2; descriptor.m_addressBase = RHI::VirtualAddress::CreateFromPointer((void*)0xffffffffeeee0100); RHI::PoolAllocator allocator; allocator.Init(descriptor); AllocatorTest::TestDescriptor testDescriptor; testDescriptor.m_byteCountMax = descriptor.m_capacityInBytes; testDescriptor.m_byteAlignmentBase = 128; testDescriptor.m_gcLatency = descriptor.m_garbageCollectLatency; testDescriptor.m_allocator = &allocator; testDescriptor.m_useVisualizer = false; testDescriptor.m_iterations = 10000; testDescriptor.m_allocationSizeMin = descriptor.m_elementSize; testDescriptor.m_allocationSizeMax = descriptor.m_elementSize; testDescriptor.m_addressBase = descriptor.m_addressBase.m_ptr; run(testDescriptor); } TEST_F(AllocatorTest, FirstFitAllocator) { RHI::FreeListAllocator::Descriptor descriptor; descriptor.m_capacityInBytes = 64 * 1024; descriptor.m_alignmentInBytes = 256; descriptor.m_garbageCollectLatency = 2; descriptor.m_addressBase = RHI::VirtualAddress::CreateFromPointer((void*)0xffffffffeeee0100); descriptor.m_policy = RHI::FreeListAllocatorPolicy::FirstFit; RHI::FreeListAllocator allocator; allocator.Init(descriptor); AllocatorTest::TestDescriptor testDescriptor; testDescriptor.m_byteCountMax = descriptor.m_capacityInBytes; testDescriptor.m_byteAlignmentBase = descriptor.m_alignmentInBytes; testDescriptor.m_gcLatency = descriptor.m_garbageCollectLatency; testDescriptor.m_allocator = &allocator; testDescriptor.m_useVisualizer = false; testDescriptor.m_iterations = 10000; testDescriptor.m_addressBase = descriptor.m_addressBase.m_ptr; run(testDescriptor); descriptor.m_garbageCollectLatency = 0; descriptor.m_addressBase = RHI::VirtualAddress::CreateFromOffset(1024); allocator.Init(descriptor); testDescriptor.m_gcLatency = descriptor.m_garbageCollectLatency; testDescriptor.m_addressBase = descriptor.m_addressBase.m_ptr; run(testDescriptor); } TEST_F(AllocatorTest, BestFitAllocator) { RHI::FreeListAllocator::Descriptor descriptor; descriptor.m_capacityInBytes = 64 * 1024; descriptor.m_alignmentInBytes = 256; descriptor.m_garbageCollectLatency = 2; descriptor.m_addressBase = RHI::VirtualAddress::CreateFromPointer((void*)0xffffffffeeee0100); descriptor.m_policy = RHI::FreeListAllocatorPolicy::BestFit; RHI::FreeListAllocator allocator; allocator.Init(descriptor); AllocatorTest::TestDescriptor testDescriptor; testDescriptor.m_byteCountMax = descriptor.m_capacityInBytes; testDescriptor.m_byteAlignmentBase = descriptor.m_alignmentInBytes; testDescriptor.m_gcLatency = descriptor.m_garbageCollectLatency; testDescriptor.m_allocator = &allocator; testDescriptor.m_useVisualizer = false; testDescriptor.m_iterations = 10000; testDescriptor.m_addressBase = descriptor.m_addressBase.m_ptr; run(testDescriptor); descriptor.m_garbageCollectLatency = 0; descriptor.m_addressBase = RHI::VirtualAddress::CreateFromOffset(1024); allocator.Init(descriptor); testDescriptor.m_gcLatency = descriptor.m_garbageCollectLatency; testDescriptor.m_addressBase = descriptor.m_addressBase.m_ptr; run(testDescriptor); } }