Created
July 22, 2026 03:08
-
-
Save bazhenovc/4b527b1912288184ca7f35c0aefb259a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "Base/Render/HandleAllocator.h" | |
| #include "Base/Math/MathRandom.h" | |
| #include "Base/Time/Timers.h" | |
| #include <iostream> | |
| #include <iomanip> | |
| using namespace EE; | |
| //------------------------------------------------------------------------- | |
| static int gNumTestFailures = 0; | |
| #define TEST_ASSERT(cond, msg) \ | |
| if ( !(cond) ) \ | |
| { \ | |
| std::cout << " FAIL [" << __LINE__ << "]: " << msg << std::endl; \ | |
| ++gNumTestFailures; \ | |
| return; \ | |
| } | |
| #define TEST_PRINT(msg) std::cout << msg << std::endl; | |
| // Test: Basic allocate and deallocate cycle | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_BasicAllocFree() | |
| { | |
| TEST_PRINT( "Test_BasicAllocFree..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 1 ); | |
| auto h = allocator.Allocate( 1 ); | |
| TEST_ASSERT( h.IsValid(), "Allocate(1) should return valid handle" ); | |
| TEST_ASSERT( h.m_offset == 0, "First allocation should be at offset 0" ); | |
| TEST_ASSERT( h.m_size == 1, "Size should be 1" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == 1ULL, "Bit 0 should be set" ); | |
| allocator.Deallocate( eastl::move( h ) ); | |
| TEST_ASSERT( !h.IsValid(), "Handle should be invalid after deallocate" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == 0ULL, "Bit 0 should be cleared" ); | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Sequential allocations fill pages in order, lowest offset first | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_SequentialAlloc() | |
| { | |
| TEST_PRINT( "Test_SequentialAlloc..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 2 ); | |
| typename Render::HandleAllocator<OffsetType>::Handle handles[10]; | |
| OffsetType expectedOffset = 0; | |
| for ( uint32_t i = 0; i < 10; ++i ) | |
| { | |
| OffsetType size = static_cast<OffsetType>( ( i % 5 ) + 1 ); | |
| handles[i] = allocator.Allocate( size ); | |
| TEST_ASSERT( handles[i].IsValid(), "Allocate should succeed" ); | |
| TEST_ASSERT( handles[i].m_offset == expectedOffset, "Offset should be sequential and minimal" ); | |
| TEST_ASSERT( handles[i].m_size == size, "Size should match request" ); | |
| expectedOffset = static_cast<OffsetType>( expectedOffset + size ); | |
| } | |
| uint64_t const* pData = allocator.GetPageData(); | |
| for ( uint64_t slot = 0; slot < static_cast<uint64_t>( expectedOffset ); ++slot ) | |
| { | |
| uint32_t page = static_cast<uint32_t>( slot / 64 ); | |
| uint32_t bit = static_cast<uint32_t>( slot % 64 ); | |
| TEST_ASSERT( ( pData[page] >> bit ) & 1ULL, "Allocated slot should be set in bitmask" ); | |
| } | |
| for ( uint32_t i = 0; i < 10; ++i ) | |
| { | |
| allocator.Deallocate( eastl::move( handles[i] ) ); | |
| TEST_ASSERT( !handles[i].IsValid(), "Handle should be invalidated" ); | |
| } | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( pData[p] == 0ULL, "Page should be empty after freeing all" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Offset minimization | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_OffsetMinimization() | |
| { | |
| TEST_PRINT( "Test_OffsetMinimization..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 4 ); | |
| typename Render::HandleAllocator<OffsetType>::Handle handles[5]; | |
| for ( uint32_t i = 0; i < 5; ++i ) | |
| { | |
| handles[i] = allocator.Allocate( 10 ); | |
| TEST_ASSERT( handles[i].m_offset == static_cast<OffsetType>( i * 10 ), "Sequential allocation offset" ); | |
| } | |
| allocator.Deallocate( eastl::move( handles[1] ) ); | |
| auto hNew = allocator.Allocate( 3 ); | |
| TEST_ASSERT( hNew.m_offset == 10, "Should reuse freed low-offset slot" ); | |
| allocator.Deallocate( eastl::move( handles[3] ) ); | |
| auto hNew2 = allocator.Allocate( 8 ); | |
| TEST_ASSERT( hNew2.m_offset == 30, "Should reuse next lowest freed slot" ); | |
| allocator.Deallocate( eastl::move( handles[0] ) ); | |
| allocator.Deallocate( eastl::move( handles[2] ) ); | |
| allocator.Deallocate( eastl::move( handles[4] ) ); | |
| allocator.Deallocate( eastl::move( hNew ) ); | |
| allocator.Deallocate( eastl::move( hNew2 ) ); | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Multi-page allocation (>64 slots) | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_MultiPageAlloc() | |
| { | |
| TEST_PRINT( "Test_MultiPageAlloc..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 4 ); | |
| auto hLarge = allocator.Allocate( 70 ); | |
| TEST_ASSERT( hLarge.IsValid(), "Large alloc should succeed" ); | |
| TEST_ASSERT( hLarge.m_offset == 0, "Should start at offset 0" ); | |
| TEST_ASSERT( hLarge.m_size == 70, "Size should be 70" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 should be full" ); | |
| TEST_ASSERT( allocator.GetPageData()[1] == 0x3FULL, "Page 1 should have first 6 bits set" ); | |
| allocator.Deallocate( eastl::move( hLarge ) ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == 0ULL, "Page 0 should be clear" ); | |
| TEST_ASSERT( allocator.GetPageData()[1] == 0ULL, "Page 1 should be clear" ); | |
| typename Render::HandleAllocator<OffsetType>::Handle smallHandles[64]; | |
| for ( uint32_t i = 0; i < 64; ++i ) | |
| { | |
| smallHandles[i] = allocator.Allocate( 1 ); | |
| TEST_ASSERT( smallHandles[i].m_offset == static_cast<OffsetType>( i ), "Should fill sequentially" ); | |
| } | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 should be full after 64 single allocs" ); | |
| for ( uint32_t i = 0; i < 64; ++i ) | |
| { | |
| allocator.Deallocate( eastl::move( smallHandles[i] ) ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Large sequential allocations (65-200 range) | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_LargeSequential() | |
| { | |
| TEST_PRINT( "Test_LargeSequential..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 32 ); | |
| OffsetType const sizes[] = { 70, 65, 90, 150, 200, 120, 180, 130, 170, 110, 80, 140 }; | |
| static constexpr uint32_t c_numHandles = sizeof( sizes ) / sizeof( sizes[0] ); | |
| typename Render::HandleAllocator<OffsetType>::Handle handles[c_numHandles]; | |
| OffsetType nextExpectedOffset = 0; | |
| for ( uint32_t i = 0; i < c_numHandles; ++i ) | |
| { | |
| handles[i] = allocator.Allocate( sizes[i] ); | |
| TEST_ASSERT( handles[i].IsValid(), "Alloc should succeed" ); | |
| TEST_ASSERT( handles[i].m_size == sizes[i], "Size must match" ); | |
| TEST_ASSERT( handles[i].m_offset == nextExpectedOffset, "Should pack sequentially at minimal offset" ); | |
| nextExpectedOffset = static_cast<OffsetType>( nextExpectedOffset + sizes[i] ); | |
| } | |
| for ( uint32_t i = 0; i < c_numHandles; ++i ) | |
| { | |
| OffsetType iEnd = static_cast<OffsetType>( handles[i].m_offset + handles[i].m_size ); | |
| for ( uint32_t j = i + 1; j < c_numHandles; ++j ) | |
| { | |
| TEST_ASSERT( iEnd <= handles[j].m_offset, "Ranges must not overlap" ); | |
| } | |
| } | |
| uint64_t const* pData = allocator.GetPageData(); | |
| for ( uint32_t i = 0; i < c_numHandles; ++i ) | |
| { | |
| uint64_t startSlot = handles[i].m_offset; | |
| uint64_t endSlot = startSlot + handles[i].m_size; | |
| uint32_t firstPage = static_cast<uint32_t>( startSlot / 64 ); | |
| uint32_t firstBit = static_cast<uint32_t>( startSlot % 64 ); | |
| TEST_ASSERT( ( pData[firstPage] >> firstBit ) & 1ULL, "First slot must be set" ); | |
| uint64_t lastSlot = endSlot - 1; | |
| uint32_t lastPage = static_cast<uint32_t>( lastSlot / 64 ); | |
| uint32_t lastBit = static_cast<uint32_t>( lastSlot % 64 ); | |
| TEST_ASSERT( ( pData[lastPage] >> lastBit ) & 1ULL, "Last slot must be set" ); | |
| for ( uint64_t p = startSlot / 64 + 1; p <= lastSlot / 64; ++p ) | |
| { | |
| uint64_t boundarySlot = p * 64; | |
| if ( boundarySlot < endSlot ) | |
| { | |
| uint32_t bp = static_cast<uint32_t>( boundarySlot / 64 ); | |
| uint32_t bb = static_cast<uint32_t>( boundarySlot % 64 ); | |
| TEST_ASSERT( ( pData[bp] >> bb ) & 1ULL, "Page boundary slot must be set" ); | |
| } | |
| } | |
| } | |
| for ( uint32_t i = 1; i < c_numHandles; i += 2 ) | |
| { | |
| allocator.Deallocate( eastl::move( handles[i] ) ); | |
| } | |
| OffsetType const reallocSizes[] = { 60, 140, 80, 190, 55, 100 }; | |
| typename Render::HandleAllocator<OffsetType>::Handle reallocHandles[6]; | |
| for ( uint32_t i = 0; i < 6; ++i ) | |
| { | |
| reallocHandles[i] = allocator.Allocate( reallocSizes[i] ); | |
| TEST_ASSERT( reallocHandles[i].IsValid(), "Realloc should succeed" ); | |
| } | |
| for ( uint32_t i = 0; i < c_numHandles; i += 2 ) | |
| { | |
| allocator.Deallocate( eastl::move( handles[i] ) ); | |
| } | |
| for ( uint32_t i = 0; i < 6; ++i ) | |
| { | |
| allocator.Deallocate( eastl::move( reallocHandles[i] ) ); | |
| } | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( pData[p] == 0ULL, "Page must be zero after freeing all" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Cross-page gap fill | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_CrossPageGapFill() | |
| { | |
| TEST_PRINT( "Test_CrossPageGapFill..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 4 ); | |
| auto h0 = allocator.Allocate( 32 ); | |
| auto h1 = allocator.Allocate( 32 ); | |
| auto h2 = allocator.Allocate( 32 ); | |
| auto h3 = allocator.Allocate( 32 ); | |
| TEST_ASSERT( h0.m_offset == 0, "First alloc at offset 0" ); | |
| TEST_ASSERT( h1.m_offset == 32, "Second alloc at offset 32" ); | |
| TEST_ASSERT( h2.m_offset == 64, "Third alloc at offset 64" ); | |
| TEST_ASSERT( h3.m_offset == 96, "Fourth alloc at offset 96" ); | |
| allocator.Deallocate( eastl::move( h1 ) ); | |
| allocator.Deallocate( eastl::move( h2 ) ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == 0x00000000FFFFFFFFULL, "Page 0: low 32 set, high 32 clear" ); | |
| TEST_ASSERT( allocator.GetPageData()[1] == 0xFFFFFFFF00000000ULL, "Page 1: low 32 clear, high 32 set" ); | |
| // Test A: Allocate 64 — must fill cross-page gap at offset 32, not new page | |
| auto hGap64 = allocator.Allocate( 64 ); | |
| TEST_ASSERT( hGap64.IsValid(), "64-slot alloc should succeed" ); | |
| TEST_ASSERT( hGap64.m_offset == 32, "Must fill cross-page gap at offset 32" ); | |
| TEST_ASSERT( hGap64.m_size == 64, "Size must be 64" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 should be full" ); | |
| TEST_ASSERT( allocator.GetPageData()[1] == ~0ULL, "Page 1 should be full" ); | |
| allocator.Deallocate( eastl::move( hGap64 ) ); | |
| // Test B: Allocate 40 then 24 — both must fill the gap | |
| auto h40 = allocator.Allocate( 40 ); | |
| TEST_ASSERT( h40.IsValid(), "40-slot alloc should succeed" ); | |
| TEST_ASSERT( h40.m_offset == 32, "40-slot must go to start of gap at offset 32" ); | |
| TEST_ASSERT( h40.m_size == 40, "Size must be 40" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 should be full after 40" ); | |
| TEST_ASSERT( ( allocator.GetPageData()[1] & 0xFFULL ) == 0xFFULL, "Page 1 bits 0-7 should be set" ); | |
| auto h24 = allocator.Allocate( 24 ); | |
| TEST_ASSERT( h24.IsValid(), "24-slot alloc should succeed" ); | |
| TEST_ASSERT( h24.m_offset == 72, "24-slot must fill remainder of gap at offset 72" ); | |
| TEST_ASSERT( h24.m_size == 24, "Size must be 24" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 should be full" ); | |
| TEST_ASSERT( allocator.GetPageData()[1] == ~0ULL, "Page 1 should be full" ); | |
| TEST_ASSERT( allocator.GetPageData()[2] == 0ULL, "Page 2 should be clean (no spill)" ); | |
| allocator.Deallocate( eastl::move( h0 ) ); | |
| allocator.Deallocate( eastl::move( h3 ) ); | |
| allocator.Deallocate( eastl::move( h40 ) ); | |
| allocator.Deallocate( eastl::move( h24 ) ); | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( allocator.GetPageData()[p] == 0ULL, "All pages must be zero" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Exact page-multiple allocations (128, 192) — verify bitmask at | |
| // every page boundary within the allocation. | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_ExactPageMultipleAlloc() | |
| { | |
| TEST_PRINT( "Test_ExactPageMultipleAlloc..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 8 ); | |
| // Alloc 128 slots — exactly 2 pages | |
| auto h128 = allocator.Allocate( 128 ); | |
| TEST_ASSERT( h128.IsValid(), "128-slot alloc should succeed" ); | |
| TEST_ASSERT( h128.m_offset == 0, "Offset 0" ); | |
| TEST_ASSERT( h128.m_size == 128, "Size 128" ); | |
| // Pages 0 and 1 should be full, page 2 should be clean | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 full" ); | |
| TEST_ASSERT( allocator.GetPageData()[1] == ~0ULL, "Page 1 full" ); | |
| TEST_ASSERT( allocator.GetPageData()[2] == 0ULL, "Page 2 clean" ); | |
| // Alloc 192 slots — exactly 3 pages, should pack right after h128 | |
| auto h192 = allocator.Allocate( 192 ); | |
| TEST_ASSERT( h192.IsValid(), "192-slot alloc should succeed" ); | |
| TEST_ASSERT( h192.m_offset == 128, "Offset 128 (right after first alloc)" ); | |
| TEST_ASSERT( h192.m_size == 192, "Size 192" ); | |
| TEST_ASSERT( allocator.GetPageData()[2] == ~0ULL, "Page 2 full" ); | |
| TEST_ASSERT( allocator.GetPageData()[3] == ~0ULL, "Page 3 full" ); | |
| TEST_ASSERT( allocator.GetPageData()[4] == ~0ULL, "Page 4 full" ); | |
| TEST_ASSERT( allocator.GetPageData()[5] == 0ULL, "Page 5 clean" ); | |
| // Free and verify clean | |
| allocator.Deallocate( eastl::move( h128 ) ); | |
| allocator.Deallocate( eastl::move( h192 ) ); | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( allocator.GetPageData()[p] == 0ULL, "All pages must be zero" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Growth across L1 word boundary (64 pages per L1 word). | |
| // Growing from ≤64 pages to >64 pages must create a new L1 word. | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_L1WordBoundaryGrowth() | |
| { | |
| TEST_PRINT( "Test_L1WordBoundaryGrowth..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 32 ); // Start with 32 pages | |
| // Fill all 32 pages (2048 slots) | |
| auto hFill = allocator.Allocate( 2048 ); | |
| TEST_ASSERT( hFill.IsValid(), "Should fill 32 pages" ); | |
| // Allocate one more — must grow. 32 pages → 33 pages is still within | |
| // L1 word 0 (pages 0-63), so no new L1 word needed. | |
| auto hGrow1 = allocator.Allocate( 1 ); | |
| TEST_ASSERT( hGrow1.IsValid(), "Growth within L1 word should succeed" ); | |
| TEST_ASSERT( hGrow1.m_offset == 2048, "Offset should be after fill" ); | |
| allocator.Deallocate( eastl::move( hGrow1 ) ); | |
| // Now fill pages 0-63 (4096 slots total). First free the big block. | |
| allocator.Deallocate( eastl::move( hFill ) ); | |
| // Refill: 64 pages exactly | |
| auto hFill64 = allocator.Allocate( 4096 ); | |
| TEST_ASSERT( hFill64.IsValid(), "Should fill 64 pages" ); | |
| // Grow to page 64 (the 65th page) — crosses L1 word boundary | |
| auto hCrossL1 = allocator.Allocate( 10 ); | |
| TEST_ASSERT( hCrossL1.IsValid(), "Growth across L1 word boundary should succeed" ); | |
| TEST_ASSERT( hCrossL1.m_offset == 4096, "Offset should be after 64 pages" ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() >= 65, "Pool should have at least 65 pages" ); | |
| allocator.Deallocate( eastl::move( hCrossL1 ) ); | |
| allocator.Deallocate( eastl::move( hFill64 ) ); | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( allocator.GetPageData()[p] == 0ULL, "All pages must be zero" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Pool growth | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_PoolGrowth() | |
| { | |
| TEST_PRINT( "Test_PoolGrowth..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 1 ); | |
| auto hFull = allocator.Allocate( 64 ); | |
| TEST_ASSERT( hFull.IsValid(), "Should fill first page" ); | |
| TEST_ASSERT( allocator.GetPageData()[0] == ~0ULL, "Page 0 should be full" ); | |
| auto hGrow = allocator.Allocate( 10 ); | |
| TEST_ASSERT( hGrow.IsValid(), "Should trigger growth and succeed" ); | |
| TEST_ASSERT( hGrow.m_offset == 64, "Should be at start of new page" ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() >= 2, "Pool should have grown" ); | |
| allocator.Deallocate( eastl::move( hGrow ) ); | |
| allocator.Deallocate( eastl::move( hFull ) ); | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Random alloc/dealloc stress — realistic workload | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_RandomStress() | |
| { | |
| TEST_PRINT( "Test_RandomStress..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 64 ); | |
| Math::RNG random( 12345 ); | |
| TVector<typename Render::HandleAllocator<OffsetType>::Handle> liveHandles; | |
| liveHandles.reserve( 2000 ); | |
| // Scale iterations to avoid exhausting uint16_t address space | |
| static constexpr uint32_t NumIterations = sizeof( OffsetType ) == 2 ? 3000 : 30000; | |
| for ( uint32_t iter = 0; iter < NumIterations; ++iter ) | |
| { | |
| if ( liveHandles.empty() || random.GetUInt( 0, 99 ) < 65 ) | |
| { | |
| OffsetType size; | |
| uint32_t const cat = random.GetUInt( 0, 99 ); | |
| if ( cat < 60 ) | |
| { | |
| size = static_cast<OffsetType>( random.GetUInt( 1, 8 ) ); | |
| } | |
| else if ( cat < 85 ) | |
| { | |
| size = static_cast<OffsetType>( random.GetUInt( 9, 40 ) ); | |
| } | |
| else if ( cat < 95 ) | |
| { | |
| size = static_cast<OffsetType>( random.GetUInt( 41, 120 ) ); | |
| } | |
| else | |
| { | |
| size = static_cast<OffsetType>( random.GetUInt( 121, 200 ) ); | |
| } | |
| auto h = allocator.Allocate( size ); | |
| // Allocate asserts in Debug but returns invalid in Release — handle both | |
| if ( h.IsValid() ) | |
| { | |
| for ( auto const& existing : liveHandles ) | |
| { | |
| OffsetType aEnd = static_cast<OffsetType>( h.m_offset + h.m_size ); | |
| OffsetType bEnd = static_cast<OffsetType>( existing.m_offset + existing.m_size ); | |
| TEST_ASSERT( aEnd <= existing.m_offset || bEnd <= h.m_offset, "Handles must not overlap" ); | |
| } | |
| liveHandles.push_back( h ); | |
| } | |
| } | |
| else | |
| { | |
| uint32_t const n = static_cast<uint32_t>( liveHandles.size() ); | |
| uint32_t const idx = ( n == 1 ) ? 0 : random.GetUInt( 0, n - 1 ); | |
| allocator.Deallocate( eastl::move( liveHandles[idx] ) ); | |
| TEST_ASSERT( !liveHandles[idx].IsValid(), "Deallocated handle must be invalidated" ); | |
| if ( idx != liveHandles.size() - 1 ) | |
| { | |
| liveHandles[idx] = eastl::move( liveHandles.back() ); | |
| } | |
| liveHandles.pop_back(); | |
| } | |
| } | |
| for ( auto& h : liveHandles ) | |
| { | |
| allocator.Deallocate( eastl::move( h ) ); | |
| } | |
| liveHandles.clear(); | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( allocator.GetPageData()[p] == 0ULL, "All pages must be zero" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Fragmentation — alloc/free/alloc with scattered pattern | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_Fragmentation() | |
| { | |
| TEST_PRINT( "Test_Fragmentation..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 4 ); | |
| typename Render::HandleAllocator<OffsetType>::Handle handles[10]; | |
| for ( uint32_t i = 0; i < 10; ++i ) | |
| { | |
| handles[i] = allocator.Allocate( 5 ); | |
| } | |
| for ( uint32_t i = 1; i < 10; i += 2 ) | |
| { | |
| allocator.Deallocate( eastl::move( handles[i] ) ); | |
| } | |
| auto h1 = allocator.Allocate( 3 ); | |
| TEST_ASSERT( h1.m_offset == 5, "Should use first hole (offset 5)" ); | |
| auto h2 = allocator.Allocate( 2 ); | |
| TEST_ASSERT( h2.m_offset == 8, "Should use remainder of first hole (offset 8)" ); | |
| auto h3 = allocator.Allocate( 1 ); | |
| TEST_ASSERT( h3.m_offset == 15, "Should use second hole (offset 15)" ); | |
| for ( uint32_t i = 0; i < 10; i += 2 ) | |
| { | |
| allocator.Deallocate( eastl::move( handles[i] ) ); | |
| } | |
| allocator.Deallocate( eastl::move( h1 ) ); | |
| allocator.Deallocate( eastl::move( h2 ) ); | |
| allocator.Deallocate( eastl::move( h3 ) ); | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: GetPageData / GetCapacityInPages | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_PageData() | |
| { | |
| TEST_PRINT( "Test_PageData..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 3 ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 3, "Initial capacity 3 pages" ); | |
| auto h1 = allocator.Allocate( 65 ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 3, "Capacity unchanged" ); | |
| auto h2 = allocator.Allocate( 1 ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 3, "Still 3 pages" ); | |
| allocator.Deallocate( eastl::move( h2 ) ); | |
| allocator.Deallocate( eastl::move( h1 ) ); | |
| for ( uint32_t p = 0; p < allocator.GetCapacityInPages(); ++p ) | |
| { | |
| TEST_ASSERT( allocator.GetPageData()[p] == 0ULL, "All pages should be zero" ); | |
| } | |
| allocator.Shutdown(); | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test: Shrink — deallocating the last live handles should shrink the pool | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void Test_Shrink() | |
| { | |
| TEST_PRINT( "Test_Shrink..." ); | |
| Render::HandleAllocator<OffsetType> allocator; | |
| allocator.Initialize( 4 ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 4, "Initial capacity 4 pages" ); | |
| // Allocate handles that fill into the last page | |
| auto h1 = allocator.Allocate( 64 ); // Page 0 full | |
| TEST_ASSERT( h1.IsValid(), "h1 valid" ); | |
| auto h2 = allocator.Allocate( 64 ); // Page 1 full | |
| TEST_ASSERT( h2.IsValid(), "h2 valid" ); | |
| auto h3 = allocator.Allocate( 64 ); // Page 2 full | |
| TEST_ASSERT( h3.IsValid(), "h3 valid" ); | |
| auto h4 = allocator.Allocate( 32 ); // Page 3 partial | |
| TEST_ASSERT( h4.IsValid(), "h4 valid" ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 4, "Still 4 pages after alloc" ); | |
| // Deallocate h4 (last page becomes empty) — should shrink to 3 | |
| allocator.Deallocate( eastl::move( h4 ) ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 3, "Shrink to 3 pages after freeing last page's only handle" ); | |
| // Deallocate h3 — should shrink to 2 | |
| allocator.Deallocate( eastl::move( h3 ) ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 2, "Shrink to 2 pages" ); | |
| // Deallocate h2 — should shrink to 1 (keeps at least 1 page) | |
| allocator.Deallocate( eastl::move( h2 ) ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 1, "Shrink to 1 page (minimum)" ); | |
| // Deallocate h1 — stays at 1 page | |
| allocator.Deallocate( eastl::move( h1 ) ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 1, "Stays at 1 page" ); | |
| // Verify all pages are clean | |
| TEST_ASSERT( allocator.GetPageData()[0] == 0ULL, "Page 0 clean" ); | |
| // Allocate again — should succeed without growth (page exists) | |
| auto h5 = allocator.Allocate( 32 ); | |
| TEST_ASSERT( h5.IsValid(), "Allocate after shrink succeeds" ); | |
| TEST_ASSERT( allocator.GetCapacityInPages() == 1, "No growth needed" ); | |
| allocator.Deallocate( eastl::move( h5 ) ); | |
| allocator.Shutdown(); | |
| // Large handles spanning multiple pages: verify shrink removes all tail pages | |
| struct LargeCase { OffsetType m_size; uint32_t m_pagesToFill; uint32_t m_expectedAfterFree; }; | |
| LargeCase const cases[] = { | |
| { 128, 3, 3 }, // 2 pages | |
| { 192, 2, 2 }, // 3 pages | |
| { 256, 1, 1 }, // 4 pages | |
| }; | |
| for ( auto const& c : cases ) | |
| { | |
| uint32_t const totalPages = c.m_pagesToFill + ( static_cast<uint32_t>( c.m_size ) + 63 ) / 64; | |
| Render::HandleAllocator<OffsetType> alloc; | |
| alloc.Initialize( totalPages ); | |
| TEST_ASSERT( alloc.GetCapacityInPages() == totalPages, "Initial capacity" ); | |
| // Fill leading pages with full-page allocs to push the large handle to the tail | |
| TVector<typename Render::HandleAllocator<OffsetType>::Handle> small; | |
| small.reserve( c.m_pagesToFill ); | |
| for ( uint32_t i = 0; i < c.m_pagesToFill; ++i ) | |
| { | |
| auto h = alloc.Allocate( 64 ); | |
| TEST_ASSERT( h.IsValid(), "Small alloc valid" ); | |
| small.push_back( eastl::move( h ) ); | |
| } | |
| // Allocate the large handle — spans the remaining pages at the tail | |
| auto big = alloc.Allocate( c.m_size ); | |
| TEST_ASSERT( big.IsValid(), "Large handle valid" ); | |
| TEST_ASSERT( alloc.GetCapacityInPages() == totalPages, "Capacity unchanged after alloc" ); | |
| // Free the large handle — tail pages should shrink | |
| alloc.Deallocate( eastl::move( big ) ); | |
| TEST_ASSERT( alloc.GetCapacityInPages() == c.m_expectedAfterFree, | |
| "Shrink after freeing large handle" ); | |
| // Free small allocs and verify shrink to 1 | |
| while ( !small.empty() ) | |
| { | |
| alloc.Deallocate( eastl::move( small.back() ) ); | |
| small.pop_back(); | |
| } | |
| TEST_ASSERT( alloc.GetCapacityInPages() == 1, "Shrink to 1 after freeing all" ); | |
| alloc.Shutdown(); | |
| } | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Minimal D3D12MA::VirtualBlock wrapper for benchmark comparison. | |
| // Include implementation directly since symbols aren't exported from Esoterica.Base. | |
| //------------------------------------------------------------------------- | |
| #define D3D12MA_EXPORTS | |
| #include "Base/ThirdParty/D3D12MemoryAllocator/D3D12MemAlloc.h" | |
| #include "Base/ThirdParty/D3D12MemoryAllocator/D3D12MemAlloc.cpp" | |
| #include "Base/Memory/Memory.h" | |
| struct D3D12MAHandleAllocator | |
| { | |
| struct Handle | |
| { | |
| uint64_t m_offset = ~0ULL; | |
| uint64_t m_size = 0; | |
| D3D12MA::VirtualAllocation m_d3d12Allocation = {}; | |
| bool IsValid() const { return m_offset != ~0ULL; } | |
| }; | |
| void Initialize( uint32_t numPages ) | |
| { | |
| uint64_t const poolSize = static_cast<uint64_t>( numPages ) * 64; | |
| D3D12MA::ALLOCATION_CALLBACKS callbacks = {}; | |
| callbacks.pAllocate = [] ( size_t size, size_t alignment, void* ) | |
| { | |
| return EE::Alloc( size, alignment ); | |
| }; | |
| callbacks.pFree = [] ( void* pPtr, void* ) | |
| { | |
| if ( pPtr ) | |
| { | |
| uint8_t* pActual = static_cast<uint8_t*>( pPtr ); | |
| EE::Free( pActual ); | |
| } | |
| }; | |
| D3D12MA::VIRTUAL_BLOCK_DESC desc = {}; | |
| desc.Size = poolSize; | |
| desc.pAllocationCallbacks = &callbacks; | |
| HRESULT hr = D3D12MA::CreateVirtualBlock( &desc, &m_pVirtualBlock ); | |
| EE_ASSERT( SUCCEEDED( hr ) ); | |
| } | |
| void Shutdown() | |
| { | |
| if ( m_pVirtualBlock ) | |
| { | |
| m_pVirtualBlock->Release(); | |
| m_pVirtualBlock = nullptr; | |
| } | |
| } | |
| Handle Allocate( uint64_t size ) | |
| { | |
| D3D12MA::VIRTUAL_ALLOCATION_DESC allocDesc = {}; | |
| allocDesc.Size = size; | |
| allocDesc.Flags = D3D12MA::VIRTUAL_ALLOCATION_FLAG_STRATEGY_MIN_OFFSET; | |
| D3D12MA::VirtualAllocation allocation = {}; | |
| UINT64 offset = ~0ULL; | |
| HRESULT hr = m_pVirtualBlock->Allocate( &allocDesc, &allocation, &offset ); | |
| if ( SUCCEEDED( hr ) ) | |
| { | |
| return { offset, size, allocation }; | |
| } | |
| return {}; | |
| } | |
| void Deallocate( Handle&& handle ) | |
| { | |
| m_pVirtualBlock->FreeAllocation( handle.m_d3d12Allocation ); | |
| handle = {}; | |
| } | |
| uint32_t GetCapacityInPages() const { return 0; } | |
| D3D12MA::VirtualBlock* m_pVirtualBlock = nullptr; | |
| }; | |
| // Same as above but uses D3D12MA's MIN_TIME strategy (no offset minimization). | |
| struct D3D12MAHandleAllocator_MinTime | |
| { | |
| struct Handle | |
| { | |
| uint64_t m_offset = ~0ULL; | |
| uint64_t m_size = 0; | |
| D3D12MA::VirtualAllocation m_d3d12Allocation = {}; | |
| bool IsValid() const { return m_offset != ~0ULL; } | |
| }; | |
| void Initialize( uint32_t numPages ) | |
| { | |
| uint64_t const poolSize = static_cast<uint64_t>( numPages ) * 64; | |
| D3D12MA::ALLOCATION_CALLBACKS callbacks = {}; | |
| callbacks.pAllocate = [] ( size_t size, size_t alignment, void* ) | |
| { | |
| return EE::Alloc( size, alignment ); | |
| }; | |
| callbacks.pFree = [] ( void* pPtr, void* ) | |
| { | |
| if ( pPtr ) | |
| { | |
| uint8_t* pActual = static_cast<uint8_t*>( pPtr ); | |
| EE::Free( pActual ); | |
| } | |
| }; | |
| D3D12MA::VIRTUAL_BLOCK_DESC desc = {}; | |
| desc.Size = poolSize; | |
| desc.pAllocationCallbacks = &callbacks; | |
| HRESULT hr = D3D12MA::CreateVirtualBlock( &desc, &m_pVirtualBlock ); | |
| EE_ASSERT( SUCCEEDED( hr ) ); | |
| } | |
| void Shutdown() | |
| { | |
| if ( m_pVirtualBlock ) | |
| { | |
| m_pVirtualBlock->Release(); | |
| m_pVirtualBlock = nullptr; | |
| } | |
| } | |
| Handle Allocate( uint64_t size ) | |
| { | |
| D3D12MA::VIRTUAL_ALLOCATION_DESC allocDesc = {}; | |
| allocDesc.Size = size; | |
| allocDesc.Flags = D3D12MA::VIRTUAL_ALLOCATION_FLAG_STRATEGY_MIN_TIME; | |
| D3D12MA::VirtualAllocation allocation = {}; | |
| UINT64 offset = ~0ULL; | |
| HRESULT hr = m_pVirtualBlock->Allocate( &allocDesc, &allocation, &offset ); | |
| if ( SUCCEEDED( hr ) ) | |
| { | |
| return { offset, size, allocation }; | |
| } | |
| return {}; | |
| } | |
| void Deallocate( Handle&& handle ) | |
| { | |
| m_pVirtualBlock->FreeAllocation( handle.m_d3d12Allocation ); | |
| handle = {}; | |
| } | |
| uint32_t GetCapacityInPages() const { return 0; } | |
| D3D12MA::VirtualBlock* m_pVirtualBlock = nullptr; | |
| }; | |
| // Benchmark: HandleAllocator vs D3D12MA::VirtualBlock directly | |
| //------------------------------------------------------------------------- | |
| struct FragBenchResult | |
| { | |
| float m_phase1Ms; | |
| float m_phase2Ms; | |
| float m_totalMs; | |
| }; | |
| template <typename Allocator, typename Handle> | |
| static FragBenchResult RunFragBenchmark | |
| ( | |
| char const* pName, Allocator& allocator, | |
| uint32_t numInitialAllocs, uint32_t numSmallAllocs, | |
| uint32_t numLargeAllocs, | |
| TVector<Handle>& outHandles | |
| ) | |
| { | |
| Math::RNG random( 42 ); | |
| // Setup: populate the pool, then free every 3rd to create fragmentation baseline | |
| { | |
| for ( uint32_t i = 0; i < numInitialAllocs; ++i ) | |
| { | |
| uint32_t size = random.GetUInt( 1, 15 ); | |
| auto h = allocator.Allocate( static_cast<uint64_t>( size ) ); | |
| if ( h.IsValid() ) | |
| { | |
| outHandles.push_back( h ); | |
| } | |
| } | |
| uint32_t const numLive = static_cast<uint32_t>( outHandles.size() ); | |
| for ( uint32_t i = 2; i < numLive; i += 3 ) | |
| { | |
| allocator.Deallocate( eastl::move( outHandles[i] ) ); | |
| } | |
| // Compact: remove invalidated handles | |
| uint32_t writeIdx = 0; | |
| for ( uint32_t i = 0; i < numLive; ++i ) | |
| { | |
| if ( outHandles[i].IsValid() ) | |
| { | |
| if ( writeIdx != i ) { outHandles[writeIdx] = eastl::move( outHandles[i] ); } | |
| ++writeIdx; | |
| } | |
| } | |
| outHandles.resize( writeIdx ); | |
| } | |
| Milliseconds phase1Time, phase2Time; | |
| // Phase 1: Small allocs into fragmented pool (alloc + free, timed together) | |
| { | |
| Timer<PlatformClock> timer; | |
| timer.Start(); | |
| uint32_t const countBefore = static_cast<uint32_t>( outHandles.size() ); | |
| for ( uint32_t i = 0; i < numSmallAllocs; ++i ) | |
| { | |
| uint32_t size = random.GetUInt( 1, 15 ); | |
| auto h = allocator.Allocate( static_cast<uint64_t>( size ) ); | |
| if ( h.IsValid() ) | |
| { | |
| outHandles.push_back( h ); | |
| } | |
| } | |
| // Free only what this phase allocated | |
| for ( uint32_t i = countBefore; i < static_cast<uint32_t>( outHandles.size() ); ++i ) | |
| { | |
| allocator.Deallocate( eastl::move( outHandles[i] ) ); | |
| } | |
| outHandles.resize( countBefore ); | |
| phase1Time = timer.GetElapsedTimeMilliseconds(); | |
| } | |
| // Phase 2: Large allocs into fragmented pool (alloc + free, timed together) | |
| { | |
| Timer<PlatformClock> timer; | |
| timer.Start(); | |
| uint32_t const countBefore = static_cast<uint32_t>( outHandles.size() ); | |
| for ( uint32_t i = 0; i < numLargeAllocs; ++i ) | |
| { | |
| uint32_t size; | |
| uint32_t const cat = random.GetUInt( 0, 99 ); | |
| if ( cat < 10 ) { size = random.GetUInt( 1, 8 ); } | |
| else if ( cat < 50 ) { size = random.GetUInt( 9, 30 ); } | |
| else if ( cat < 90 ) { size = random.GetUInt( 31, 60 ); } | |
| else { size = random.GetUInt( 61, 100 ); } | |
| auto h = allocator.Allocate( static_cast<uint64_t>( size ) ); | |
| if ( h.IsValid() ) | |
| { | |
| outHandles.push_back( h ); | |
| } | |
| } | |
| // Free only what this phase allocated | |
| for ( uint32_t i = countBefore; i < static_cast<uint32_t>( outHandles.size() ); ++i ) | |
| { | |
| allocator.Deallocate( eastl::move( outHandles[i] ) ); | |
| } | |
| outHandles.resize( countBefore ); | |
| phase2Time = timer.GetElapsedTimeMilliseconds(); | |
| } | |
| // Free the initial fragmented baseline (untimed cleanup) | |
| for ( auto& h : outHandles ) | |
| { | |
| allocator.Deallocate( eastl::move( h ) ); | |
| } | |
| outHandles.clear(); | |
| FragBenchResult result; | |
| result.m_phase1Ms = phase1Time.ToFloat(); | |
| result.m_phase2Ms = phase2Time.ToFloat(); | |
| result.m_totalMs = result.m_phase1Ms + result.m_phase2Ms; | |
| std::cout << " " << pName << ":" << std::endl; | |
| std::cout << " Phase 1 (small allocs, fragmented): " << result.m_phase1Ms << " ms" << std::endl; | |
| std::cout << " Phase 2 (large allocs, fragmented): " << result.m_phase2Ms << " ms" << std::endl; | |
| std::cout << " Total: " << result.m_totalMs << " ms" << std::endl; | |
| return result; | |
| } | |
| static void Benchmark_Fragmentation() | |
| { | |
| TEST_PRINT( "Benchmark_Fragmentation (HandleAllocator vs D3D12MA::VirtualBlock)..." ); | |
| static constexpr uint32_t NumInitialAllocs = 4000; | |
| static constexpr uint32_t NumSmallAllocs = 2000; | |
| static constexpr uint32_t NumLargeAllocs = 1000; | |
| static constexpr uint32_t NumTotalSlots = 131072; | |
| static constexpr uint32_t NumTotalPages = NumTotalSlots / 64; | |
| float h2Total = 0.0f; | |
| float d3d12OffsetTotal = 0.0f; | |
| float d3d12TimeTotal = 0.0f; | |
| // HandleAllocator<uint32_t> | |
| { | |
| Render::HandleAllocator<uint32_t> allocator; | |
| allocator.Initialize( NumTotalPages ); | |
| TVector<Render::HandleAllocator<uint32_t>::Handle> handles; | |
| handles.reserve( NumInitialAllocs + NumSmallAllocs + NumLargeAllocs ); | |
| FragBenchResult const r = RunFragBenchmark( "HandleAllocator (32-bit)", allocator, NumInitialAllocs, NumSmallAllocs, NumLargeAllocs, handles ); | |
| h2Total = r.m_totalMs; | |
| allocator.Shutdown(); | |
| } | |
| // D3D12MA::VirtualBlock — MIN_OFFSET strategy (same goal as HandleAllocator) | |
| { | |
| D3D12MAHandleAllocator allocator; | |
| allocator.Initialize( NumTotalPages ); | |
| TVector<D3D12MAHandleAllocator::Handle> handles; | |
| handles.reserve( NumInitialAllocs + NumSmallAllocs + NumLargeAllocs ); | |
| FragBenchResult const r = RunFragBenchmark( "D3D12MA::VirtualBlock (MIN_OFFSET)", allocator, NumInitialAllocs, NumSmallAllocs, NumLargeAllocs, handles ); | |
| d3d12OffsetTotal = r.m_totalMs; | |
| allocator.Shutdown(); | |
| } | |
| // D3D12MA::VirtualBlock — MIN_TIME strategy (no offset minimization) | |
| { | |
| D3D12MAHandleAllocator_MinTime allocator; | |
| allocator.Initialize( NumTotalPages ); | |
| TVector<D3D12MAHandleAllocator_MinTime::Handle> handles; | |
| handles.reserve( NumInitialAllocs + NumSmallAllocs + NumLargeAllocs ); | |
| FragBenchResult const r = RunFragBenchmark( "D3D12MA::VirtualBlock (MIN_TIME)", allocator, NumInitialAllocs, NumSmallAllocs, NumLargeAllocs, handles ); | |
| d3d12TimeTotal = r.m_totalMs; | |
| allocator.Shutdown(); | |
| } | |
| // Print comparison factors | |
| std::cout << std::fixed << std::setprecision( 1 ); | |
| if ( d3d12OffsetTotal > 0.0f ) | |
| { | |
| std::cout << " ---" << std::endl; | |
| std::cout << " HandleAllocator vs D3D12MA MIN_OFFSET: " | |
| << ( d3d12OffsetTotal / h2Total ) << "x faster" << std::endl; | |
| } | |
| if ( d3d12TimeTotal > 0.0f ) | |
| { | |
| std::cout << " HandleAllocator vs D3D12MA MIN_TIME: " | |
| << ( h2Total / d3d12TimeTotal ) << "x slower" << std::endl; | |
| } | |
| if ( d3d12OffsetTotal > 0.0f && d3d12TimeTotal > 0.0f ) | |
| { | |
| std::cout << " D3D12MA MIN_OFFSET vs D3D12MA MIN_TIME: " | |
| << ( d3d12OffsetTotal / d3d12TimeTotal ) << "x penalty for offset minimization" << std::endl; | |
| } | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Benchmark: Offset quality — measure mean/max offset and fragmentation | |
| //------------------------------------------------------------------------- | |
| struct OffsetQualityResult | |
| { | |
| uint64_t m_meanOffset; | |
| uint64_t m_maxOffset; | |
| float m_packingRatio; | |
| }; | |
| template <typename Allocator, typename Handle> | |
| static OffsetQualityResult MeasureOffsetQuality | |
| ( | |
| char const* pName, Allocator& allocator, | |
| uint32_t numSmallAllocs, uint32_t maxSmallSize, | |
| uint32_t numLargeAllocs, | |
| TVector<Handle>& outHandles | |
| ) | |
| { | |
| Math::RNG random( 42 ); | |
| // Phase 1: Many allocations with wide size variance (1 to maxSmallSize) | |
| uint64_t totalAllocated = 0; | |
| for ( uint32_t i = 0; i < numSmallAllocs; ++i ) | |
| { | |
| uint32_t size = random.GetUInt( 1, maxSmallSize ); | |
| auto h = allocator.Allocate( size ); | |
| if ( h.IsValid() ) | |
| { | |
| totalAllocated += size; | |
| outHandles.push_back( h ); | |
| } | |
| } | |
| // Phase 2: Free every 3rd to create fragmentation | |
| for ( uint32_t i = 2; i < static_cast<uint32_t>( outHandles.size() ); i += 3 ) | |
| { | |
| allocator.Deallocate( eastl::move( outHandles[i] ) ); | |
| } | |
| // Compact | |
| { | |
| uint32_t writeIdx = 0; | |
| for ( uint32_t i = 0; i < static_cast<uint32_t>( outHandles.size() ); ++i ) | |
| { | |
| if ( outHandles[i].IsValid() ) | |
| { | |
| if ( writeIdx != i ) { outHandles[writeIdx] = eastl::move( outHandles[i] ); } | |
| ++writeIdx; | |
| } | |
| } | |
| outHandles.resize( writeIdx ); | |
| } | |
| // Phase 3: Mixed-size allocs into fragmented pool — measure offsets | |
| uint64_t maxOffset = 0; | |
| uint64_t sumOffset = 0; | |
| uint32_t numLarge = 0; | |
| for ( uint32_t i = 0; i < numLargeAllocs; ++i ) | |
| { | |
| uint32_t size; | |
| uint32_t const cat = random.GetUInt( 0, 99 ); | |
| if ( cat < 10 ) | |
| { | |
| size = random.GetUInt( 1, 4 ); | |
| } | |
| else if ( cat < 40 ) | |
| { | |
| size = random.GetUInt( 5, 20 ); | |
| } | |
| else if ( cat < 80 ) | |
| { | |
| size = random.GetUInt( 21, 60 ); | |
| } | |
| else | |
| { | |
| size = random.GetUInt( 61, 120 ); | |
| } | |
| auto h = allocator.Allocate( size ); | |
| if ( h.IsValid() ) | |
| { | |
| if ( h.m_offset > maxOffset ) { maxOffset = h.m_offset; } | |
| sumOffset += h.m_offset; | |
| ++numLarge; | |
| outHandles.push_back( h ); | |
| } | |
| } | |
| OffsetQualityResult result; | |
| result.m_meanOffset = numLarge > 0 ? sumOffset / numLarge : 0; | |
| result.m_maxOffset = maxOffset; | |
| result.m_packingRatio = maxOffset > 0 ? static_cast<float>( totalAllocated ) / static_cast<float>( maxOffset ) : 0.0f; | |
| std::cout << " " << pName << ":" << std::endl; | |
| std::cout << " Mean offset: " << result.m_meanOffset << std::endl; | |
| std::cout << " Max offset: " << result.m_maxOffset << std::endl; | |
| std::cout << " Packing ratio: " << result.m_packingRatio << std::endl; | |
| // Cleanup | |
| for ( auto& h : outHandles ) | |
| { | |
| if ( h.IsValid() ) | |
| { | |
| allocator.Deallocate( eastl::move( h ) ); | |
| } | |
| } | |
| outHandles.clear(); | |
| return result; | |
| } | |
| static void Benchmark_OffsetQuality() | |
| { | |
| TEST_PRINT( "Benchmark_OffsetQuality (offset minimization + fragmentation)..." ); | |
| static constexpr uint32_t NumSmallAllocs = 2000; | |
| static constexpr uint32_t MaxSmallSize = 80; | |
| static constexpr uint32_t NumLargeAllocs = 300; | |
| static constexpr uint32_t NumTotalSlots = 131072; | |
| static constexpr uint32_t NumTotalPages = NumTotalSlots / 64; | |
| OffsetQualityResult h2Result, d3d12OffsetResult, d3d12TimeResult; | |
| { | |
| Render::HandleAllocator<uint32_t> allocator; | |
| allocator.Initialize( NumTotalPages ); | |
| TVector<Render::HandleAllocator<uint32_t>::Handle> handles; | |
| handles.reserve( NumSmallAllocs + NumLargeAllocs ); | |
| h2Result = MeasureOffsetQuality( "HandleAllocator (32-bit)", allocator, NumSmallAllocs, MaxSmallSize, NumLargeAllocs, handles ); | |
| allocator.Shutdown(); | |
| } | |
| { | |
| D3D12MAHandleAllocator allocator; | |
| allocator.Initialize( NumTotalPages ); | |
| TVector<D3D12MAHandleAllocator::Handle> handles; | |
| handles.reserve( NumSmallAllocs + NumLargeAllocs ); | |
| d3d12OffsetResult = MeasureOffsetQuality( "D3D12MA (MIN_OFFSET)", allocator, NumSmallAllocs, MaxSmallSize, NumLargeAllocs, handles ); | |
| allocator.Shutdown(); | |
| } | |
| { | |
| D3D12MAHandleAllocator_MinTime allocator; | |
| allocator.Initialize( NumTotalPages ); | |
| TVector<D3D12MAHandleAllocator_MinTime::Handle> handles; | |
| handles.reserve( NumSmallAllocs + NumLargeAllocs ); | |
| d3d12TimeResult = MeasureOffsetQuality( "D3D12MA (MIN_TIME)", allocator, NumSmallAllocs, MaxSmallSize, NumLargeAllocs, handles ); | |
| allocator.Shutdown(); | |
| } | |
| // Print comparison factors | |
| std::cout << " ---" << std::endl; | |
| std::cout << std::fixed << std::setprecision( 1 ); | |
| if ( h2Result.m_meanOffset > 0 && d3d12TimeResult.m_meanOffset > 0 ) | |
| { | |
| float const meanOffsetRatio = static_cast<float>( d3d12TimeResult.m_meanOffset ) / static_cast<float>( h2Result.m_meanOffset ); | |
| std::cout << " MIN_TIME mean offset vs HandleAllocator: " << meanOffsetRatio << "x higher" << std::endl; | |
| } | |
| std::cout << std::fixed << std::setprecision( 1 ); | |
| if ( h2Result.m_packingRatio > 0.0f && d3d12TimeResult.m_packingRatio > 0.0f ) | |
| { | |
| float const packingDelta = ( h2Result.m_packingRatio - d3d12TimeResult.m_packingRatio ) / h2Result.m_packingRatio * 100.0f; | |
| std::cout << " MIN_TIME packing ratio vs HandleAllocator: " << packingDelta << "% lower" << std::endl; | |
| } | |
| TEST_PRINT( " PASSED" ); | |
| } | |
| // Test runner — instantiates all tests for both OffsetType variants | |
| //------------------------------------------------------------------------- | |
| template <typename OffsetType> | |
| static void RunTestsForType( char const* pTypeName ) | |
| { | |
| std::cout << "--- HandleAllocator<" << pTypeName << "> ---" << std::endl; | |
| Test_BasicAllocFree<OffsetType>(); | |
| Test_SequentialAlloc<OffsetType>(); | |
| Test_OffsetMinimization<OffsetType>(); | |
| Test_MultiPageAlloc<OffsetType>(); | |
| Test_LargeSequential<OffsetType>(); | |
| Test_CrossPageGapFill<OffsetType>(); | |
| Test_ExactPageMultipleAlloc<OffsetType>(); | |
| Test_L1WordBoundaryGrowth<OffsetType>(); | |
| Test_PoolGrowth<OffsetType>(); | |
| Test_RandomStress<OffsetType>(); | |
| Test_Fragmentation<OffsetType>(); | |
| Test_PageData<OffsetType>(); | |
| Test_Shrink<OffsetType>(); | |
| } | |
| int RunHandleAllocatorTests() | |
| { | |
| RunTestsForType<uint16_t>( "uint16_t" ); | |
| RunTestsForType<uint32_t>( "uint32_t" ); | |
| Benchmark_Fragmentation(); | |
| Benchmark_OffsetQuality(); | |
| if ( gNumTestFailures > 0 ) | |
| { | |
| std::cout << "\n" << gNumTestFailures << " TEST(S) FAILED!" << std::endl; | |
| } | |
| else | |
| { | |
| std::cout << "\nAll tests PASSED." << std::endl; | |
| } | |
| return gNumTestFailures; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment