Skip to content

Instantly share code, notes, and snippets.

@bazhenovc
Created July 22, 2026 03:08
Show Gist options
  • Select an option

  • Save bazhenovc/4b527b1912288184ca7f35c0aefb259a to your computer and use it in GitHub Desktop.

Select an option

Save bazhenovc/4b527b1912288184ca7f35c0aefb259a to your computer and use it in GitHub Desktop.
#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