Converted Descriptor heap to memory arena. Used Gemini with antigravity.

Heap pool code is a mess right now (lot of useless comments)
Will revisit later.
This commit is contained in:
2026-01-18 14:33:52 -05:00
parent f95ba51c13
commit 7328d02d3d
18 changed files with 446 additions and 45 deletions

View File

@@ -58,6 +58,7 @@
<CustomBuild Include="include\Core\Math\Vector.h" />
<CustomBuild Include="include\Core\Memory\Allocator.h" />
<CustomBuild Include="include\Core\Memory\MemoryArena.h" />
<CustomBuild Include="include\Core\Memory\ScratchArena.h" />
<CustomBuild Include="include\Core\Memory\Utils.h" />
<CustomBuild Include="include\Core\Networking\IPAddress.h" />
<CustomBuild Include="include\Core\Networking\NetworkPacket.h" />
@@ -122,6 +123,7 @@
<CustomBuild Include="src\Core\Memory\EngineArena.h" />
<CustomBuild Include="src\Core\Memory\MemoryArena.cpp" />
<CustomBuild Include="src\Core\Memory\MemoryArenaTests.cpp" />
<CustomBuild Include="src\Core\Memory\ScratchArena.cpp" />
<CustomBuild Include="src\Core\Networking\NetworkPacket.cpp" />
<CustomBuild Include="src\Core\Networking\Socket.cpp" />
<CustomBuild Include="src\Core\Networking\SocketPlatformImpl.h" />
@@ -162,6 +164,8 @@
<CustomBuild Include="src\Graphics\D3D12\D3D12GraphicsPipeline.cpp" />
<CustomBuild Include="src\Graphics\D3D12\D3D12GraphicsPipeline.h" />
<CustomBuild Include="src\Graphics\D3D12\D3D12Includes.h" />
<CustomBuild Include="src\Graphics\D3D12\D3D12InternalTests.cpp" />
<CustomBuild Include="src\Graphics\D3D12\D3D12InternalTests.h" />
<CustomBuild Include="src\Graphics\D3D12\D3D12RenderPass.cpp" />
<CustomBuild Include="src\Graphics\D3D12\D3D12RenderPass.h" />
<CustomBuild Include="src\Graphics\D3D12\D3D12Shader.cpp" />

View File

@@ -94,6 +94,9 @@
<CustomBuild Include="include\Core\Memory\MemoryArena.h">
<Filter>include\Core\Memory</Filter>
</CustomBuild>
<CustomBuild Include="include\Core\Memory\ScratchArena.h">
<Filter>include\Core\Memory</Filter>
</CustomBuild>
<CustomBuild Include="include\Core\Memory\Utils.h">
<Filter>include\Core\Memory</Filter>
</CustomBuild>
@@ -285,6 +288,9 @@
<CustomBuild Include="src\Core\Memory\MemoryArenaTests.cpp">
<Filter>src\Core\Memory</Filter>
</CustomBuild>
<CustomBuild Include="src\Core\Memory\ScratchArena.cpp">
<Filter>src\Core\Memory</Filter>
</CustomBuild>
<CustomBuild Include="src\Core\Networking\NetworkPacket.cpp">
<Filter>src\Core\Networking</Filter>
</CustomBuild>
@@ -405,6 +411,12 @@
<CustomBuild Include="src\Graphics\D3D12\D3D12Includes.h">
<Filter>src\Graphics\D3D12</Filter>
</CustomBuild>
<CustomBuild Include="src\Graphics\D3D12\D3D12InternalTests.cpp">
<Filter>src\Graphics\D3D12</Filter>
</CustomBuild>
<CustomBuild Include="src\Graphics\D3D12\D3D12InternalTests.h">
<Filter>src\Graphics\D3D12</Filter>
</CustomBuild>
<CustomBuild Include="src\Graphics\D3D12\D3D12RenderPass.cpp">
<Filter>src\Graphics\D3D12</Filter>
</CustomBuild>

View File

@@ -16,6 +16,7 @@ namespace Juliet
JULIET_API void MemoryArenaCreate(MemoryArena* arena, void* backingMemory, size_t size);
JULIET_API void* ArenaPush(MemoryArena* arena, size_t size, size_t alignment = 16);
JULIET_API void* ArenaRealloc(MemoryArena* arena, void* oldPtr, size_t oldSize, size_t newSize, size_t alignment = 16);
JULIET_API void ArenaReset(MemoryArena* arena);
JULIET_API size_t ArenaGetMarker(MemoryArena* arena);
JULIET_API void ArenaResetToMarker(MemoryArena* arena, size_t marker);
@@ -58,4 +59,10 @@ namespace Juliet
}
return result;
}
template <typename T>
inline T* ArenaRealloc(MemoryArena* arena, T* oldPtr, size_t oldCount, size_t newCount)
{
return static_cast<T*>(Juliet::ArenaRealloc(arena, static_cast<void*>(oldPtr), sizeof(T) * oldCount, sizeof(T) * newCount, alignof(T)));
}
} // namespace Juliet

View File

@@ -2,6 +2,8 @@
#include <Core/Memory/MemoryArena.h>
#include <Core/Memory/Utils.h>
#include <cstring>
namespace Juliet
{
void MemoryArenaCreate(MemoryArena* arena, void* backingMemory, size_t size)
@@ -36,6 +38,66 @@ namespace Juliet
return result;
}
void* ArenaRealloc(MemoryArena* arena, void* oldPtr, size_t oldSize, size_t newSize, size_t alignment)
{
Assert(arena);
// Alignment must be power of 2
Assert((alignment & (alignment - 1)) == 0);
if (oldPtr == nullptr)
{
return ArenaPush(arena, newSize, alignment);
}
if (newSize == 0)
{
return nullptr;
}
// Check if the old allocation is at the top of the stack
// We need to verify if (oldPtr + oldSize) == (Data + Offset)
// Note: usage of reinterpret_cast is to be careful with pointer arithmetic on void*
uint8* oldPtrBytes = static_cast<uint8*>(oldPtr);
uint8* arenaEnd = arena->Data + arena->Offset;
if (oldPtrBytes + oldSize == arenaEnd)
{
// It is the last allocation! We can reuse the space.
// We just need to check if we can expand it (if growing)
// Re-calculate the offset start for this block (to ensure nothing weird with padding)
// Ideally oldPtr was aligned.
// Current Offset corresponds to oldPtrBytes + oldSize.
// We want to move Offset to oldPtrBytes + newSize.
size_t oldPtrOffset = static_cast<size_t>(oldPtrBytes - arena->Data);
size_t newOffset = oldPtrOffset + newSize;
if (newOffset > arena->Size)
{
// Cannot expand in place, not enough space.
// Fallthrough to Alloc + Copy
}
else
{
// In-place Resize success
arena->Offset = newOffset;
return oldPtr;
}
}
// Fallback: Alloc + Copy
void* newPtr = ArenaPush(arena, newSize, alignment);
if (newPtr)
{
size_t copySize = oldSize < newSize ? oldSize : newSize;
std::memcpy(newPtr, oldPtr, copySize);
}
return newPtr;
}
void ArenaReset(MemoryArena* arena)
{
Assert(arena);

View File

@@ -1,4 +1,5 @@
#include <Core/Memory/Allocator.h>
#include <Core/Memory/EngineArena.h>
#include <Graphics/D3D12/D3D12DescriptorHeap.h>
#include <Graphics/D3D12/D3D12GraphicsDevice.h>
#include <Graphics/D3D12/D3D12Utils.h>
@@ -91,7 +92,10 @@ namespace Juliet::D3D12::Internal
void ReleaseDescriptor(const D3D12Descriptor& descriptor)
{
if (descriptor.Index == UINT32_MAX || descriptor.Heap == nullptr) return;
if (descriptor.Index == UINT32_MAX || descriptor.Heap == nullptr)
{
return;
}
D3D12DescriptorHeap* heap = descriptor.Heap;
@@ -105,23 +109,64 @@ namespace Juliet::D3D12::Internal
heap->FreeIndicesCount++;
}
D3D12DescriptorHeap* AcquireSamplerHeapFromPool(NonNullPtr<D3D12Driver> d3d12Driver)
D3D12DescriptorHeap* AcquireSamplerHeapFromPool(NonNullPtr<D3D12Driver> d3d12Driver, DescriptorHeapCreator creator)
{
D3D12DescriptorHeapPool* pool = &d3d12Driver->SamplerHeapPool;
uint32 count = GPUDriver::kSampler_HeapDescriptorCount;
D3D12DescriptorHeap* result = nullptr;
if (pool->Count > 0)
{
result = pool->Heaps[pool->Count - 1];
pool->Count -= 1;
}
else
{
result = CreateDescriptorHeap(d3d12Driver, D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, count, false);
return pool->Heaps[pool->Count];
}
return result;
// Pool is exhausted (all heaps are in use). We need to create a new one.
// The pool array might be full of "Used" heaps, or it might have empty slots (nullptr) if we expanded before.
// We look for a free slot to store the new heap.
size_t freeSlotIndex = SIZE_MAX;
// Scan for nullptr in the entire array.
// Optimization: active heaps are likely at the beginning, but since Count=0, "Used" heaps are 0..Capacity?
// Actually, if Count=0, ALL slots 0..Capacity-1 are effectively "Used" (unless some are nullptr).
for (size_t i = 0; i < pool->Capacity; ++i)
{
if (pool->Heaps[i] == nullptr)
{
freeSlotIndex = i;
break;
}
}
if (freeSlotIndex == SIZE_MAX)
{
// No free slot, expand
size_t oldCapacity = pool->Capacity;
pool->Capacity = pool->Capacity == 0 ? 1 : pool->Capacity * 2;
// Using ArenaRealloc (Template):
pool->Heaps = ArenaRealloc<D3D12DescriptorHeap*>(GetEngineArena(), pool->Heaps, oldCapacity, pool->Capacity);
// Initialize new slots to nullptr
for (size_t i = oldCapacity; i < pool->Capacity; ++i)
{
pool->Heaps[i] = nullptr;
}
freeSlotIndex = oldCapacity;
}
// Create the new heap at the free slot
pool->Heaps[freeSlotIndex] = creator(d3d12Driver, D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, count, false);
// Now we have a new heap at freeSlotIndex. Ideally we want to return it.
// But to maintain the "Stack" invariant (Available are at 0..Count-1),
// and "Used" are at "Count..Capacity-1".
// Currently Count=0. So "Used" is 0..Capacity-1.
// freeSlotIndex is inside "Used" range.
// We can just return it. It is already in the "Used" partition.
// However, we want to ensure it doesn't get lost or overwritten.
// Since we didn't increment Count, it stays in "Used".
return pool->Heaps[freeSlotIndex];
}
void ReturnSamplerHeapToPool(NonNullPtr<D3D12Driver> d3d12Driver, D3D12DescriptorHeap* heap)
@@ -136,14 +181,35 @@ namespace Juliet::D3D12::Internal
heap->CurrentDescriptorIndex = 0;
if (pool->Count >= pool->Capacity)
// The heap is currently in the "Used" partition (index >= Count).
// We want to move it to the "Available" partition (index < Count).
// Strategy: Swap heap with the element at pool->Heaps[pool->Count], then increment Count.
// First, find the heap index.
size_t heapIndex = SIZE_MAX;
for (size_t i = pool->Count; i < pool->Capacity; ++i)
{
pool->Capacity = pool->Capacity == 0 ? 1 : pool->Capacity * 2;
pool->Heaps =
static_cast<D3D12DescriptorHeap**>(Realloc(pool->Heaps, pool->Capacity * sizeof(D3D12DescriptorHeap*)));
if (pool->Heaps[i] == heap)
{
heapIndex = i;
break;
}
}
pool->Heaps[pool->Count] = heap;
pool->Count += 1;
if (heapIndex != SIZE_MAX)
{
D3D12DescriptorHeap* temp = pool->Heaps[pool->Count];
pool->Heaps[pool->Count] = pool->Heaps[heapIndex];
pool->Heaps[heapIndex] = temp;
pool->Count += 1;
}
else
{
// Should not happen if the logic is correct.
// Maybe it was not in the pool? (e.g. error scenario).
// In that case, we should probably add it?
// But per constraints, we assume it was allocated via pool.
Assert(false, "Returning a heap that was not found in the pool!");
}
}
} // namespace Juliet::D3D12::Internal

View File

@@ -44,11 +44,13 @@ namespace Juliet::D3D12::Internal
size_t Count;
};
using DescriptorHeapCreator = D3D12DescriptorHeap* (*)(NonNullPtr<D3D12Driver>, D3D12_DESCRIPTOR_HEAP_TYPE, uint32, bool);
extern D3D12DescriptorHeap* CreateDescriptorHeap(NonNullPtr<D3D12Driver> driver,
D3D12_DESCRIPTOR_HEAP_TYPE type, uint32 count, bool isStaging);
extern void DestroyDescriptorHeap(NonNullPtr<D3D12DescriptorHeap> heap);
extern D3D12DescriptorHeap* AcquireSamplerHeapFromPool(NonNullPtr<D3D12Driver> d3d12Driver);
extern D3D12DescriptorHeap* AcquireSamplerHeapFromPool(NonNullPtr<D3D12Driver> d3d12Driver, DescriptorHeapCreator creator = CreateDescriptorHeap);
extern void ReturnSamplerHeapToPool(NonNullPtr<D3D12Driver> d3d12Driver, D3D12DescriptorHeap* heap);
extern bool AssignDescriptor(D3D12DescriptorHeap* heap, D3D12Descriptor& outDescriptor);

View File

@@ -3,12 +3,14 @@
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Memory/Allocator.h>
#include <Core/Memory/EngineArena.h>
#include <Graphics/D3D12/D3D12Buffer.h>
#include <Graphics/D3D12/D3D12CommandList.h>
#include <Graphics/D3D12/D3D12DescriptorHeap.h>
#include <Graphics/D3D12/D3D12GraphicsDevice.h>
#include <Graphics/D3D12/D3D12GraphicsPipeline.h>
#include <Graphics/D3D12/D3D12Includes.h>
#include <Graphics/D3D12/D3D12InternalTests.h>
#include <Graphics/D3D12/D3D12RenderPass.h>
#include <Graphics/D3D12/D3D12Shader.h>
#include <Graphics/D3D12/D3D12SwapChain.h>
@@ -488,7 +490,7 @@ namespace Juliet::D3D12
heapPool.Heaps[i] = nullptr;
}
}
SafeFree(heapPool.Heaps);
// SafeFree(heapPool.Heaps); // Allocated with Arena, do not free.
}
};
DestroyDescriptorHeapPool(driver->SamplerHeapPool);
@@ -643,8 +645,13 @@ namespace Juliet::D3D12
Free(device.Get());
}
GraphicsDevice* CreateGraphicsDevice(bool enableDebug)
{
#if JULIET_DEBUG
// Unit Tests for D3D12 Logic
UnitTest::TestDescriptorHeapPool();
#endif
auto driver = static_cast<D3D12Driver*>(Calloc(1, sizeof(D3D12Driver)));
#if JULIET_DEBUG
@@ -929,8 +936,7 @@ namespace Juliet::D3D12
{
heapPool.Capacity = 4;
heapPool.Count = 4;
heapPool.Heaps = static_cast<Internal::D3D12DescriptorHeap**>(
Calloc(heapPool.Capacity, sizeof(Internal::D3D12DescriptorHeap*)));
heapPool.Heaps = ArenaPushArray<Internal::D3D12DescriptorHeap*>(GetEngineArena(), heapPool.Capacity);
for (uint32 i = 0; i < heapPool.Capacity; ++i)
{

View File

@@ -0,0 +1,141 @@
#include <Graphics/D3D12/D3D12InternalTests.h>
#include <Core/Memory/Allocator.h>
#include <Graphics/D3D12/D3D12DescriptorHeap.h>
#include <Graphics/D3D12/D3D12GraphicsDevice.h>
#if JULIET_DEBUG
namespace Juliet::D3D12::UnitTest
{
using namespace Juliet::D3D12;
using namespace Juliet::D3D12::Internal;
static D3D12DescriptorHeap* MockCreateDescriptorHeap(NonNullPtr<D3D12Driver>, D3D12_DESCRIPTOR_HEAP_TYPE type, uint32, bool)
{
D3D12DescriptorHeap* heap = static_cast<D3D12DescriptorHeap*>(Calloc(1, sizeof(D3D12DescriptorHeap)));
if (heap)
{
heap->HeapType = type;
heap->MaxDescriptors = 128;
// Mock other fields as needed to pass verification
}
return heap;
}
void TestDescriptorHeapPool()
{
D3D12Driver driver = {};
ZeroStruct(driver);
// Ensure pool is empty
driver.SamplerHeapPool.Count = 0;
driver.SamplerHeapPool.Capacity = 0;
driver.SamplerHeapPool.Heaps = nullptr;
NonNullPtr<D3D12Driver> driverPtr(&driver);
// 1. Acquire from empty -> Should create new
D3D12DescriptorHeap* heap1 = AcquireSamplerHeapFromPool(driverPtr, MockCreateDescriptorHeap);
Assert(heap1 != nullptr);
Assert(driver.SamplerHeapPool.Count == 0); // Used heaps are above Count
Assert(driver.SamplerHeapPool.Capacity >= 1);
Assert(driver.SamplerHeapPool.Heaps[0] == heap1);
// 2. Return -> Should become available
ReturnSamplerHeapToPool(driverPtr, heap1);
Assert(driver.SamplerHeapPool.Count == 1); // One available
Assert(driver.SamplerHeapPool.Heaps[0] == heap1); // Available is at index 0
// 3. Acquire again -> Should reuse
D3D12DescriptorHeap* heap2 = AcquireSamplerHeapFromPool(driverPtr, MockCreateDescriptorHeap);
Assert(heap2 == heap1); // Reused
Assert(driver.SamplerHeapPool.Count == 0); // No available left
// 4. Acquire another -> Should Expand and Create
D3D12DescriptorHeap* heap3 = AcquireSamplerHeapFromPool(driverPtr, MockCreateDescriptorHeap);
Assert(heap3 != nullptr);
Assert(heap3 != heap1);
Assert(driver.SamplerHeapPool.Count == 0);
Assert(driver.SamplerHeapPool.Capacity >= 2);
// Cleanup
(void)heap1; (void)heap2; (void)heap3;
Free(heap1);
Free(heap3);
// 5. Test Multiple Allocations (8 heaps)
// Reset pool for clean test or continue? Let's reset to be sure of state,
// effectively leaking previous pool array but that's fine for mock test.
// Actually, let's just continue using the pool, it has Capacity >= 2.
// Return everything first to have clean slate if possible, but we already freed heap1 and heap3 mentally/physically.
// The pool thinks heap3 is "Used" (outside Count).
// We freed it physically, but pool has a dangling pointer in Heaps[1].
// This is dangerous if we reuse it.
// So we MUST correct the pool state or reset it.
// Let's reset the pool state for the 8-loop test.
driver.SamplerHeapPool.Count = 0;
driver.SamplerHeapPool.Capacity = 0;
driver.SamplerHeapPool.Heaps = nullptr; // Leaks previous array
const int kNumHeaps = 32;
D3D12DescriptorHeap* heaps[kNumHeaps];
// 5.1 Acquire 8
for (int i = 0; i < kNumHeaps; ++i)
{
heaps[i] = AcquireSamplerHeapFromPool(driverPtr, MockCreateDescriptorHeap);
Assert(heaps[i] != nullptr);
// Verify uniqueness
for (int j = 0; j < i; ++j)
{
Assert(heaps[i] != heaps[j]);
}
}
Assert(driver.SamplerHeapPool.Capacity >= kNumHeaps);
Assert(driver.SamplerHeapPool.Count == 0);
// 5.2 Release 8
for (int i = 0; i < kNumHeaps; ++i)
{
ReturnSamplerHeapToPool(driverPtr, heaps[i]);
}
Assert(driver.SamplerHeapPool.Count == kNumHeaps);
// 5.3 Acquire 8 Again (Should Reuse)
D3D12DescriptorHeap* heapsAgain[kNumHeaps];
for (int i = 0; i < kNumHeaps; ++i)
{
heapsAgain[i] = AcquireSamplerHeapFromPool(driverPtr, MockCreateDescriptorHeap);
// Verify it matches one of the original heaps (order might reverse due to stack-like behavior)
bool found = false;
for (int j = 0; j < kNumHeaps; ++j)
{
if (heapsAgain[i] == heaps[j])
{
found = true;
break;
}
}
Assert(found);
// Verify uniqueness in the new set
for (int j = 0; j < i; ++j)
{
Assert(heapsAgain[i] != heapsAgain[j]);
}
}
Assert(driver.SamplerHeapPool.Count == 0);
// Capacity should not have increased if we reused
Assert(driver.SamplerHeapPool.Capacity == kNumHeaps); // Or >= 8
// Cleanup
for (int i = 0; i < kNumHeaps; ++i)
{
Free(heaps[i]);
}
// Note: heap2 is heap1.
// Pool array in driver leaked for test scope, acceptable.
}
}
#endif

View File

@@ -0,0 +1,9 @@
#pragma once
#include <Core/Common/CoreUtils.h>
#if JULIET_DEBUG
namespace Juliet::D3D12::UnitTest
{
void TestDescriptorHeapPool();
}
#endif