From 6260d9aacf09fa1832d5538476b0d7b2fd018001 Mon Sep 17 00:00:00 2001 From: Patedam Date: Sat, 14 Feb 2026 16:48:54 -0500 Subject: [PATCH] Squashed commit of the following: commit c138fe98ce41a7ee9f2d208f6e38c1ed2f2aad0e Author: Patedam Date: Sat Feb 14 16:47:51 2026 -0500 Updated the memory viewer to have better naming, tooltip, zoom, etc. Made by claude opus commit 16120dd8651f44187192afd7943c651dd18db8f4 Author: Patedam Date: Sat Feb 14 15:57:01 2026 -0500 Made memory arena debugger show the new arenas. Made by gemini --- Juliet/include/Core/Container/Vector.h | 7 +- Juliet/include/Core/Memory/MemoryArena.h | 42 +- Juliet/include/Core/Memory/MemoryArenaDebug.h | 50 ++ Juliet/include/Juliet.h | 2 +- Juliet/src/Core/Common/String.cpp | 2 +- Juliet/src/Core/HAL/Display/Display.cpp | 4 +- .../HAL/Display/Win32/Win32DisplayDevice.cpp | 2 +- Juliet/src/Core/HotReload/HotReload.cpp | 6 +- Juliet/src/Core/ImGui/ImGuiService.cpp | 4 +- Juliet/src/Core/Logging/LogManager.cpp | 2 +- Juliet/src/Core/Memory/MemoryArena.cpp | 119 ++-- Juliet/src/Core/Memory/MemoryArenaDebug.cpp | 242 ++++++++ Juliet/src/Core/Memory/MemoryArenaTests.cpp | 12 +- Juliet/src/Engine/Debug/MemoryDebugger.cpp | 581 +++++++++++++++++- Juliet/src/Graphics/D3D12/D3D12Common.cpp | 2 +- .../Graphics/D3D12/D3D12DescriptorHeap.cpp | 4 +- .../Graphics/D3D12/D3D12GraphicsDevice.cpp | 2 +- .../src/UnitTest/Container/VectorUnitTest.cpp | 15 +- 18 files changed, 977 insertions(+), 121 deletions(-) create mode 100644 Juliet/include/Core/Memory/MemoryArenaDebug.h create mode 100644 Juliet/src/Core/Memory/MemoryArenaDebug.cpp diff --git a/Juliet/include/Core/Container/Vector.h b/Juliet/include/Core/Container/Vector.h index f35e46c..8508559 100644 --- a/Juliet/include/Core/Container/Vector.h +++ b/Juliet/include/Core/Container/Vector.h @@ -9,7 +9,7 @@ namespace Juliet template struct VectorArena { - void Create() + void Create(JULIET_DEBUG_ONLY(const char* name = "VectorArena")) { Assert(!Arena); static_assert(AllowRealloc == false); @@ -17,7 +17,7 @@ namespace Juliet DataFirst = DataLast = nullptr; Count = 0; ArenaParams params{ .AllowRealloc = AllowRealloc JULIET_DEBUG_ONLY(, .CanReserveMore = false) }; - Arena = ArenaAllocate(params); + Arena = ArenaAllocate(params JULIET_DEBUG_ONLY(, name)); InternalArena = true; Reserve(ReserveSize); @@ -58,7 +58,8 @@ namespace Juliet else if (AllowRealloc && !InternalArena) { DataFirst = Data = static_cast(ArenaReallocate(Arena, Data, Capacity * sizeof(Type), - newCapacity * sizeof(Type), AlignOf(Type), true)); + newCapacity * sizeof(Type), AlignOf(Type), true + JULIET_DEBUG_ONLY(, "VectorRealloc"))); DataLast = Data + Count - 1; } Capacity = newCapacity; diff --git a/Juliet/include/Core/Memory/MemoryArena.h b/Juliet/include/Core/Memory/MemoryArena.h index 43ac87e..77f47b8 100644 --- a/Juliet/include/Core/Memory/MemoryArena.h +++ b/Juliet/include/Core/Memory/MemoryArena.h @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include // Added for typeid namespace Juliet { @@ -15,6 +17,10 @@ namespace Juliet struct ArenaFreeNode; +#if JULIET_DEBUG + struct ArenaDebugInfo; +#endif + struct Arena { Arena* Previous; @@ -36,8 +42,13 @@ namespace Juliet bool AllowRealloc : 1; - JULIET_DEBUG_ONLY(uint16 LostNodeCount); + JULIET_DEBUG_ONLY(uint16 LostNodeCount;) JULIET_DEBUG_ONLY(bool CanReserveMore : 1;) + + JULIET_DEBUG_ONLY(Arena* GlobalNext;) + JULIET_DEBUG_ONLY(Arena* GlobalPrev;) + JULIET_DEBUG_ONLY(ArenaDebugInfo* FirstDebugInfo;) + JULIET_DEBUG_ONLY(const char* Name;) }; static_assert(sizeof(Arena) <= k_ArenaHeaderSize); @@ -54,14 +65,17 @@ namespace Juliet JULIET_DEBUG_ONLY(bool CanReserveMore : 1 = true;) }; - [[nodiscard]] Arena* ArenaAllocate(const ArenaParams& params = {}, + [[nodiscard]] Arena* ArenaAllocate(const ArenaParams& params + JULIET_DEBUG_ONLY(, const char* name), const std::source_location& loc = std::source_location::current()); void ArenaRelease(NonNullPtr arena); // Raw Push, can be used but templated helpers exists below - [[nodiscard]] void* ArenaPush(NonNullPtr arena, size_t size, size_t align, bool shouldBeZeroed); - [[nodiscard]] void* ArenaReallocate(NonNullPtr arena, void* oldPtr, size_t oldSize, size_t newSize, - size_t align, bool shouldBeZeroed); + // Raw Push, can be used but templated helpers exists below + [[nodiscard]] void* ArenaPush(NonNullPtr arena, size_t size, size_t align, bool shouldBeZeroed + JULIET_DEBUG_ONLY(, const char* tag)); + [[nodiscard]] void* ArenaReallocate(NonNullPtr arena, void* oldPtr, size_t oldSize, size_t newSize, + size_t align, bool shouldBeZeroed JULIET_DEBUG_ONLY(, const char* tag)); void ArenaPopTo(NonNullPtr arena, size_t position); void ArenaPop(NonNullPtr arena, size_t amount); void ArenaClear(NonNullPtr arena); @@ -70,7 +84,7 @@ namespace Juliet template [[nodiscard]] Type* ArenaPushStruct(NonNullPtr arena) { - return static_cast(ArenaPush(arena, sizeof(Type) * 1, AlignOf(Type), true)); + return static_cast(ArenaPush(arena, sizeof(Type) * 1, AlignOf(Type), true JULIET_DEBUG_ONLY(, typeid(Type).name()))); } // #define push_array_no_zero_aligned(a, T, c, align) (T *)arena_push((a), sizeof(T)*(c), (align), (0)) @@ -81,17 +95,11 @@ namespace Juliet template [[nodiscard]] Type* ArenaPushArray(NonNullPtr arena, size_t count) { - return static_cast(ArenaPush(arena, sizeof(Type) * count, Max(8ull, AlignOf(Type)), true)); + return static_cast(ArenaPush(arena, sizeof(Type) * count, Max(8ull, AlignOf(Type)), true JULIET_DEBUG_ONLY(, typeid(Type).name()))); } // --- Paged Memory Architecture --- - struct ArenaAllocation - { - size_t Offset; - size_t Size; - String Tag; - ArenaAllocation* Next; - }; + struct ArenaAllocation; struct MemoryBlock { @@ -159,9 +167,15 @@ namespace Juliet // Internal engine function to initialize memory arenas. void MemoryArenasInit(); + // Internal engine function to shutdown memory arenas. // Internal engine function to shutdown memory arenas. void MemoryArenasShutdown(); +#if JULIET_DEBUG + JULIET_API Arena* GetGlobalArenaListHead(); +#endif + + template inline T* ArenaPushType(MemoryArena* arena, String tag) { diff --git a/Juliet/include/Core/Memory/MemoryArenaDebug.h b/Juliet/include/Core/Memory/MemoryArenaDebug.h new file mode 100644 index 0000000..c61e72c --- /dev/null +++ b/Juliet/include/Core/Memory/MemoryArenaDebug.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +#if JULIET_DEBUG + +namespace Juliet +{ + struct Arena; + struct MemoryBlock; + + // Arena (Struct) + struct ArenaDebugInfo + { + const char* Tag; + size_t Offset; + size_t Size; + ArenaDebugInfo* Next; + }; + + // MemoryArena (Pool-based) + struct ArenaAllocation + { + size_t Offset; + size_t Size; + String Tag; + ArenaAllocation* Next; + }; + + // Arena (Struct) + void DebugRegisterArena(NonNullPtr arena); + void DebugUnregisterArena(NonNullPtr arena); + void DebugArenaSetDebugName(NonNullPtr arena, const char* name); + bool IsDebugInfoArena(const Arena* arena); // To prevent recursion + void DebugArenaFreeBlock(Arena* block); // To clear all debug infos in a block + void DebugArenaRemoveAllocation(Arena* block, size_t oldOffset); + void DebugArenaPopTo(Arena* block, size_t newPosition); + void DebugArenaAddDebugInfo(Arena* block, size_t size, size_t offset, const char* tag); + + // MemoryArena (Pool-based) + void DebugFreeArenaAllocations(MemoryBlock* blk); + void DebugArenaAddAllocation(MemoryBlock* blk, size_t size, size_t offset, String tag); + void DebugArenaRemoveLastAllocation(MemoryBlock* blk); + +} // namespace Juliet + +#endif diff --git a/Juliet/include/Juliet.h b/Juliet/include/Juliet.h index e200816..7f9f266 100644 --- a/Juliet/include/Juliet.h +++ b/Juliet/include/Juliet.h @@ -24,7 +24,7 @@ #define JULIET_DEBUG_ONLY(...) __VA_ARGS__ #else #define JULIET_DEBUG 0 -#define JULIET_DEBUG_ONLY(expr) +#define JULIET_DEBUG_ONLY(...) #endif // Manual override to disable ImGui diff --git a/Juliet/src/Core/Common/String.cpp b/Juliet/src/Core/Common/String.cpp index 1df1983..e20391d 100644 --- a/Juliet/src/Core/Common/String.cpp +++ b/Juliet/src/Core/Common/String.cpp @@ -493,7 +493,7 @@ namespace Juliet { String result; result.Size = str.Size; - result.Data = ArenaPushArray(arena, str.Size + 1); + result.Data = static_cast(ArenaPush(arena, str.Size + 1, alignof(char), true JULIET_DEBUG_ONLY(, "String"))); MemCopy(result.Data, str.Data, str.Size); result.Data[result.Size] = 0; return result; diff --git a/Juliet/src/Core/HAL/Display/Display.cpp b/Juliet/src/Core/HAL/Display/Display.cpp index db66949..2142fc2 100644 --- a/Juliet/src/Core/HAL/Display/Display.cpp +++ b/Juliet/src/Core/HAL/Display/Display.cpp @@ -23,7 +23,7 @@ namespace Juliet { Assert(!g_CurrentDisplayDevice); - Arena* arena = ArenaAllocate(); + Arena* arena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Display System")); DisplayDevice* candidateDevice = nullptr; DisplayDeviceFactory* candidateFactory = nullptr; @@ -80,7 +80,7 @@ namespace Juliet Assert(g_CurrentDisplayDevice->CreatePlatformWindow); Window window = {}; - window.Arena = ArenaAllocate(); + window.Arena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Window")); window.Width = width; window.Height = height; diff --git a/Juliet/src/Core/HAL/Display/Win32/Win32DisplayDevice.cpp b/Juliet/src/Core/HAL/Display/Win32/Win32DisplayDevice.cpp index 241ea09..29a3ffa 100644 --- a/Juliet/src/Core/HAL/Display/Win32/Win32DisplayDevice.cpp +++ b/Juliet/src/Core/HAL/Display/Win32/Win32DisplayDevice.cpp @@ -36,7 +36,7 @@ namespace Juliet::Win32 device->PumpEvents = PumpEvents; - device->Windows.Create(); + device->Windows.Create(JULIET_DEBUG_ONLY("Display Windows")); return device; } diff --git a/Juliet/src/Core/HotReload/HotReload.cpp b/Juliet/src/Core/HotReload/HotReload.cpp index 9d41a68..8e24d89 100644 --- a/Juliet/src/Core/HotReload/HotReload.cpp +++ b/Juliet/src/Core/HotReload/HotReload.cpp @@ -11,7 +11,7 @@ namespace Juliet { void InitHotReloadCode(HotReloadCode& code, String dllName, String transientDllName, String lockFilename) { - code.Arena = ArenaAllocate(); + code.Arena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Hot Reload")); // Get the app base path and build the dll path from there. String basePath = GetBasePath(); @@ -25,7 +25,7 @@ namespace Juliet const size_t dllFullPathLength = basePathLength + StringLength(dllName) + 1; // Need +1 because snprintf needs 0 terminated strings - code.DLLFullPath.Data = ArenaPushArray(code.Arena, dllFullPathLength); + code.DLLFullPath.Data = static_cast(ArenaPush(code.Arena, dllFullPathLength, alignof(char), true JULIET_DEBUG_ONLY(, "DLL Path"))); int writtenSize = snprintf(CStr(code.DLLFullPath), dllFullPathLength, "%s%s", CStr(basePath), CStr(dllName)); if (writtenSize < static_cast(dllFullPathLength) - 1) { @@ -38,7 +38,7 @@ namespace Juliet // Lock filename path const size_t lockPathLength = basePathLength + StringLength(lockFilename) + 1; // Need +1 because snprintf needs 0 terminated strings - code.LockFullPath.Data = ArenaPushArray(code.Arena, lockPathLength); + code.LockFullPath.Data = static_cast(ArenaPush(code.Arena, lockPathLength, alignof(char), true JULIET_DEBUG_ONLY(, "Lock File Path"))); writtenSize = snprintf(CStr(code.LockFullPath), lockPathLength, "%s%s", CStr(basePath), CStr(lockFilename)); if (writtenSize < static_cast(lockPathLength) - 1) { diff --git a/Juliet/src/Core/ImGui/ImGuiService.cpp b/Juliet/src/Core/ImGui/ImGuiService.cpp index 667632b..0629010 100644 --- a/Juliet/src/Core/ImGui/ImGuiService.cpp +++ b/Juliet/src/Core/ImGui/ImGuiService.cpp @@ -27,7 +27,7 @@ namespace Juliet::ImGuiService void* ImGuiAllocWrapper(size_t size, void* /*user_data*/) { - return ArenaPush(g_ImGuiArena, size, 8, false); + return ArenaPush(g_ImGuiArena, size, 8, false JULIET_DEBUG_ONLY(, "ImGuiAlloc")); } void ImGuiFreeWrapper(void* /*ptr*/, void* /*user_data*/) @@ -41,7 +41,7 @@ namespace Juliet::ImGuiService Assert(!g_Initialized); // Initialize ImGui Arena using Engine Pool - g_ImGuiArena = ArenaAllocate(); + g_ImGuiArena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "ImGui")); // Setup Allocator ImGui::SetAllocatorFunctions(ImGuiAllocWrapper, ImGuiFreeWrapper, nullptr); diff --git a/Juliet/src/Core/Logging/LogManager.cpp b/Juliet/src/Core/Logging/LogManager.cpp index 7a021b2..7645ba5 100644 --- a/Juliet/src/Core/Logging/LogManager.cpp +++ b/Juliet/src/Core/Logging/LogManager.cpp @@ -48,7 +48,7 @@ namespace Juliet Logs* LogAllocate() { - Arena* arena = ArenaAllocate(); + Arena* arena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Log Manager")); Logs* logs = ArenaPushStruct(arena); logs->Arena = arena; return logs; diff --git a/Juliet/src/Core/Memory/MemoryArena.cpp b/Juliet/src/Core/Memory/MemoryArena.cpp index e2d534d..9aa2bfd 100644 --- a/Juliet/src/Core/Memory/MemoryArena.cpp +++ b/Juliet/src/Core/Memory/MemoryArena.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include // For std::max @@ -33,7 +34,7 @@ namespace Juliet // https://github.com/EpicGamesExt/raddebugger/blob/master/src/base/base_arena.c - Arena* ArenaAllocate(const ArenaParams& params, const std::source_location& loc) + Arena* ArenaAllocate(const ArenaParams& params JULIET_DEBUG_ONLY(, const char* name), const std::source_location& loc) { Log(LogLevel::Message, LogCategory::Core, "Allocating from %s : %ul", loc.file_name(), loc.line()); @@ -61,22 +62,34 @@ namespace Juliet arena->FreeNodes = nullptr; arena->AllowRealloc = params.AllowRealloc; +#if JULIET_DEBUG arena->CanReserveMore = params.CanReserveMore; + arena->FirstDebugInfo = nullptr; + + DebugArenaSetDebugName(arena, name); + DebugRegisterArena(arena); +#endif return arena; } void ArenaRelease(NonNullPtr arena) { + JULIET_DEBUG_ONLY(DebugUnregisterArena(arena);) + for (Arena *node = arena->Current, *previous = nullptr; node != nullptr; node = previous) { previous = node->Previous; + + JULIET_DEBUG_ONLY(DebugArenaFreeBlock(node);) + Memory::OS_Release(node, node->Reserved); } } - void* ArenaPush(NonNullPtr arena, size_t size, size_t align, bool shouldBeZeroed) + void* ArenaPush(NonNullPtr arena, size_t size, size_t align, bool shouldBeZeroed JULIET_DEBUG_ONLY(, const char* tag)) { + // Assert(IsPowerOfTwo(align)); Arena* current = arena->Current; size_t positionPrePush = AlignPow2(current->Position, align); size_t positionPostPush = positionPrePush + size; @@ -104,7 +117,12 @@ namespace Juliet next->Previous = previous; } - JULIET_DEBUG_ONLY(remainingSize > 0 ? ++current->LostNodeCount : current->LostNodeCount); +#if JULIET_DEBUG + if (remainingSize > 0) + { + ++current->LostNodeCount; + } +#endif } else { @@ -158,7 +176,8 @@ namespace Juliet reserveSize = AlignPow2(size + k_ArenaHeaderSize, align); commitSize = AlignPow2(size + k_ArenaHeaderSize, align); } - newBlock = ArenaAllocate({ .ReserveSize = reserveSize, .CommitSize = commitSize }); + + newBlock = ArenaAllocate({ .ReserveSize = reserveSize, .CommitSize = commitSize } JULIET_DEBUG_ONLY(, arena->Name)); } newBlock->BasePosition = current->BasePosition + current->Reserved; @@ -201,19 +220,22 @@ namespace Juliet MemoryZero(result, sizeToZero); } } - - // If alloc failed, log and assert + if (result == nullptr) [[unlikely]] { + Log(LogLevel::Error, LogCategory::Core, "Fatal Allocation Failure - Unexpected allocation failure"); Assert(false, "Fatal Allocation Failure - Unexpected allocation failure"); } + JULIET_DEBUG_ONLY(if (!IsDebugInfoArena(arena)) { DebugArenaAddDebugInfo(current, size, positionPrePush, tag); }) + return result; } - void* ArenaReallocate(NonNullPtr arena, void* oldPtr, size_t oldSize, size_t newSize, size_t align, bool shouldBeZeroed) + void* ArenaReallocate(NonNullPtr arena, void* oldPtr, size_t oldSize, size_t newSize, size_t align, + bool shouldBeZeroed JULIET_DEBUG_ONLY(, const char* tag)) { - void* result = ArenaPush(arena, newSize, align, shouldBeZeroed); + void* result = ArenaPush(arena, newSize, align, shouldBeZeroed JULIET_DEBUG_ONLY(, tag)); // Find the correct block to release Arena* block = nullptr; @@ -249,6 +271,12 @@ namespace Juliet block->FreeNodes->Previous = freeNode; } block->FreeNodes = freeNode; + +#if JULIET_DEBUG + // Remove the debug info for the old allocation (since it's now free) + size_t oldOffset = static_cast(static_cast(oldPtr) - reinterpret_cast(block)); + DebugArenaRemoveAllocation(block, oldOffset); +#endif } return result; } @@ -262,20 +290,18 @@ namespace Juliet { previous = current->Previous; current->Position = k_ArenaHeaderSize; + + JULIET_DEBUG_ONLY(DebugArenaFreeBlock(current);) + SingleLinkedListPushPrevious(arena->FreeBlockLast, current); } - // No freelist : - // for (Arena* previous = nullptr; current->BasePosition >= clampedPosition; current = previous) - // { - // previous = current->Previous; - // Memory::OS_Release(current, current->Reserved); - // } - arena->Current = current; size_t newPosition = clampedPosition - current->BasePosition; Assert(newPosition <= current->Position); current->Position = newPosition; + + JULIET_DEBUG_ONLY(DebugArenaPopTo(current, newPosition);) } void ArenaPop(NonNullPtr arena, size_t amount) @@ -308,23 +334,6 @@ namespace Juliet // --- MemoryPool Implementation --- -#if JULIET_DEBUG - static void FreeDebugAllocations(MemoryBlock* blk) - { - if (!blk) - { - return; - } - ArenaAllocation* curr = blk->FirstAllocation; - while (curr) - { - ArenaAllocation* next = curr->Next; - SafeFree(curr); - curr = next; - } - blk->FirstAllocation = nullptr; - } -#endif // Simple First-Fit Allocator MemoryBlock* MemoryPool::AllocateBlock(size_t minCapacity) @@ -401,7 +410,7 @@ namespace Juliet // Poison Header and Data in Debug #if JULIET_DEBUG - FreeDebugAllocations(block); + DebugFreeArenaAllocations(block); // 0xDD = Dead Data MemSet(block->GetData(), 0xDD, block->TotalSize - sizeof(MemoryBlock)); block->Magic = 0xDEADBEEF; @@ -485,23 +494,7 @@ namespace Juliet blk->Used += alignmentOffset; void* ptr = blk->GetData() + blk->Used; -#if JULIET_DEBUG - ArenaAllocation* node = (ArenaAllocation*)Malloc(sizeof(ArenaAllocation)); - node->Offset = blk->Used; - node->Size = size; - node->Tag = tag; - node->Next = nullptr; - - if (!blk->FirstAllocation) - blk->FirstAllocation = node; - else - { - ArenaAllocation* t = blk->FirstAllocation; - while (t->Next) - t = t->Next; - t->Next = node; - } -#endif + JULIET_DEBUG_ONLY(DebugArenaAddAllocation(blk, size, blk->Used, tag);) blk->Used += size; @@ -565,29 +558,7 @@ namespace Juliet { // Yes, we can just rewind the Used pointer blk->Used -= size; -#if JULIET_DEBUG - { - ArenaAllocation* t = blk->FirstAllocation; - ArenaAllocation* prev = nullptr; - while (t && t->Next) - { - prev = t; - t = t->Next; - } - if (t) - { - SafeFree(t); - if (prev) - { - prev->Next = nullptr; - } - else - { - blk->FirstAllocation = nullptr; - } - } - } -#endif + JULIET_DEBUG_ONLY(DebugArenaRemoveLastAllocation(blk);) return true; } @@ -614,7 +585,7 @@ namespace Juliet #if JULIET_DEBUG // Poison First Block - FreeDebugAllocations(arena->FirstBlock); + DebugFreeArenaAllocations(arena->FirstBlock); MemSet(arena->FirstBlock->GetData(), 0xCD, arena->FirstBlock->TotalSize - sizeof(MemoryBlock)); #endif } diff --git a/Juliet/src/Core/Memory/MemoryArenaDebug.cpp b/Juliet/src/Core/Memory/MemoryArenaDebug.cpp new file mode 100644 index 0000000..38a66fe --- /dev/null +++ b/Juliet/src/Core/Memory/MemoryArenaDebug.cpp @@ -0,0 +1,242 @@ +#include +#include // For Arena definition + +#if JULIET_DEBUG + +namespace Juliet +{ + Arena* g_ArenaGlobalHead = nullptr; + Arena* g_DebugInfoArena = nullptr; + ArenaDebugInfo* g_DebugInfoFreeList = nullptr; + ArenaAllocation* g_ArenaAllocationFreeList = nullptr; + + // --- Internal Helpers --- + static ArenaDebugInfo* AllocDebugInfo() + { + if (g_DebugInfoFreeList) + { + ArenaDebugInfo* info = g_DebugInfoFreeList; + g_DebugInfoFreeList = info->Next; + info->Next = nullptr; + return info; + } + + if (!g_DebugInfoArena) + { + // Create a dedicated arena for debug info + g_DebugInfoArena = ArenaAllocate({ .ReserveSize = Megabytes(16), .CommitSize = Kilobytes(64) } JULIET_DEBUG_ONLY(, "Debug Info Arena")); + } + + return ArenaPushStruct(g_DebugInfoArena); + } + + static void FreeDebugInfo(ArenaDebugInfo* info) + { + if (info) + { + info->Next = g_DebugInfoFreeList; + g_DebugInfoFreeList = info; + } + } + + static ArenaAllocation* AllocArenaAllocation() + { + if (g_ArenaAllocationFreeList) + { + ArenaAllocation* info = g_ArenaAllocationFreeList; + g_ArenaAllocationFreeList = info->Next; + info->Next = nullptr; + return info; + } + + if (!g_DebugInfoArena) + { + g_DebugInfoArena = ArenaAllocate({ .ReserveSize = Megabytes(16), .CommitSize = Kilobytes(64) } JULIET_DEBUG_ONLY(, "Debug Info Arena")); + } + + return ArenaPushStruct(g_DebugInfoArena); + } + + static void FreeArenaAllocation(ArenaAllocation* info) + { + if (info) + { + info->Next = g_ArenaAllocationFreeList; + g_ArenaAllocationFreeList = info; + } + } + // ------------------------ + + void DebugRegisterArena(NonNullPtr arena) + { + // Add to Global List + // Note: Not thread safe, assuming single thread for now as per context + if (g_ArenaGlobalHead) + { + g_ArenaGlobalHead->GlobalPrev = arena; + } + arena->GlobalNext = g_ArenaGlobalHead; + arena->GlobalPrev = nullptr; + g_ArenaGlobalHead = arena; + } + + void DebugUnregisterArena(NonNullPtr arena) + { + // Remove from Global List + if (arena->GlobalPrev) + { + arena->GlobalPrev->GlobalNext = arena->GlobalNext; + } + else + { + g_ArenaGlobalHead = arena->GlobalNext; + } + if (arena->GlobalNext) + { + arena->GlobalNext->GlobalPrev = arena->GlobalPrev; + } + } + + void DebugArenaSetDebugName(NonNullPtr arena, const char* name) + { + arena->Name = name; + } + + bool IsDebugInfoArena(const Arena* arena) + { + return arena == g_DebugInfoArena; + } + + void DebugArenaFreeBlock(Arena* block) + { + ArenaDebugInfo* info = block->FirstDebugInfo; + while (info) + { + ArenaDebugInfo* nextInfo = info->Next; + FreeDebugInfo(info); + info = nextInfo; + } + block->FirstDebugInfo = nullptr; + } + + void DebugArenaRemoveAllocation(Arena* block, size_t oldOffset) + { + ArenaDebugInfo** prevInfo = &block->FirstDebugInfo; + ArenaDebugInfo* info = block->FirstDebugInfo; + + while (info) + { + if (info->Offset == oldOffset) // Found it + { + *prevInfo = info->Next; + FreeDebugInfo(info); + break; + } + prevInfo = &info->Next; + info = info->Next; + } + } + + void DebugArenaPopTo(Arena* block, size_t newPosition) + { + ArenaDebugInfo** prevInfo = &block->FirstDebugInfo; + ArenaDebugInfo* info = block->FirstDebugInfo; + while (info) + { + if (info->Offset >= newPosition) + { + ArenaDebugInfo* toFree = info; + *prevInfo = info->Next; // Unlink + info = info->Next; // Advance + FreeDebugInfo(toFree); + } + else + { + prevInfo = &info->Next; + info = info->Next; + } + } + } + + void DebugArenaAddDebugInfo(Arena* block, size_t size, size_t offset, const char* tag) + { + ArenaDebugInfo* info = AllocDebugInfo(); + if (info) + { + info->Tag = tag ? tag : "Untagged"; + info->Size = size; + info->Offset = offset; + info->Next = block->FirstDebugInfo; + block->FirstDebugInfo = info; + } + } + + // Moved to top + + void DebugFreeArenaAllocations(MemoryBlock* blk) + { + if (!blk) + { + return; + } + ArenaAllocation* curr = blk->FirstAllocation; + while (curr) + { + ArenaAllocation* next = curr->Next; + FreeArenaAllocation(curr); + curr = next; + } + blk->FirstAllocation = nullptr; + } + + void DebugArenaAddAllocation(MemoryBlock* blk, size_t size, size_t offset, String tag) + { + ArenaAllocation* node = AllocArenaAllocation(); + node->Offset = offset; + node->Size = size; + node->Tag = tag; + node->Next = nullptr; + + if (!blk->FirstAllocation) + blk->FirstAllocation = node; + else + { + ArenaAllocation* t = blk->FirstAllocation; + while (t->Next) + t = t->Next; + t->Next = node; + } + } + + void DebugArenaRemoveLastAllocation(MemoryBlock* blk) + { + ArenaAllocation* t = blk->FirstAllocation; + ArenaAllocation* prev = nullptr; + while (t && t->Next) + { + prev = t; + t = t->Next; + } + if (t) + { + FreeArenaAllocation(t); + if (prev) + { + prev->Next = nullptr; + } + else + { + blk->FirstAllocation = nullptr; + } + } + } + + // Accessor for global list head (if needed by other files) + Arena* GetGlobalArenaListHead() + { + return g_ArenaGlobalHead; + } + +} // namespace Juliet + +#endif diff --git a/Juliet/src/Core/Memory/MemoryArenaTests.cpp b/Juliet/src/Core/Memory/MemoryArenaTests.cpp index 1f1083f..73811f3 100644 --- a/Juliet/src/Core/Memory/MemoryArenaTests.cpp +++ b/Juliet/src/Core/Memory/MemoryArenaTests.cpp @@ -20,7 +20,7 @@ namespace Juliet::UnitTest // New Arena! ArenaParams param{ .ReserveSize = Megabytes(64llu), .CommitSize = Kilobytes(64llu) }; - Arena* testArena = ArenaAllocate(param); + Arena* testArena = ArenaAllocate(param JULIET_DEBUG_ONLY(, "Test Arena")); size_t pos = ArenaPos(testArena); Assert(pos == k_ArenaHeaderSize); @@ -119,12 +119,12 @@ namespace Juliet::UnitTest { // Test reallocate - Arena* arena = ArenaAllocate({ .AllowRealloc = true }); + Arena* arena = ArenaAllocate({ .AllowRealloc = true } JULIET_DEBUG_ONLY(, "Test Realloc")); char* charArray = ArenaPushArray(arena, 128); char* secondCharArray = ArenaPushArray(arena, 128); char* thirdCharArray = ArenaPushArray(arena, 128); - secondCharArray = static_cast(ArenaReallocate(arena, secondCharArray, 128, 256, alignof(char), true)); + secondCharArray = static_cast(ArenaReallocate(arena, secondCharArray, 128, 256, alignof(char), true JULIET_DEBUG_ONLY(, "ReallocChar"))); char* fourthCharArray = ArenaPushArray(arena, 128); @@ -138,14 +138,14 @@ namespace Juliet::UnitTest { // Test Reallocate Shrink (Buffer Overflow Bug Check) - Arena* arena = ArenaAllocate({ .AllowRealloc = true }); + Arena* arena = ArenaAllocate({ .AllowRealloc = true } JULIET_DEBUG_ONLY(, "Test Shrink")); size_t largeSize = 100; char* large = ArenaPushArray(arena, largeSize); MemSet(large, 'A', largeSize); size_t smallSize = 50; - char* smallData = static_cast(ArenaReallocate(arena, large, largeSize, smallSize, alignof(char), true)); + char* smallData = static_cast(ArenaReallocate(arena, large, largeSize, smallSize, alignof(char), true JULIET_DEBUG_ONLY(, "ResizeSmall"))); for (size_t i = 0; i < smallSize; ++i) { @@ -154,7 +154,7 @@ namespace Juliet::UnitTest // Allocate next block (should be immediately after 'small') // Don't zero it, if overflow happened it will contain 'A's - char* next = static_cast(ArenaPush(arena, 50, alignof(char), false)); + char* next = static_cast(ArenaPush(arena, 50, alignof(char), false JULIET_DEBUG_ONLY(, "OverflowCheck"))); bool corrupted = false; for(size_t i = 0; i < 50; ++i) diff --git a/Juliet/src/Engine/Debug/MemoryDebugger.cpp b/Juliet/src/Engine/Debug/MemoryDebugger.cpp index a519065..48b4c27 100644 --- a/Juliet/src/Engine/Debug/MemoryDebugger.cpp +++ b/Juliet/src/Engine/Debug/MemoryDebugger.cpp @@ -1,8 +1,10 @@ #include #include -#include +#include +#include // Added for Arena definition +#include +#include -#include namespace Juliet::Debug { @@ -16,6 +18,49 @@ namespace Juliet::Debug bool ScrollListToSelected = false; }; +#if JULIET_DEBUG + struct PagedArenaDebugState + { + float Zoom = 1.0f; + ArenaDebugInfo* SelectedAlloc = nullptr; + bool ScrollVisualToSelected = false; + bool ScrollListToSelected = false; + }; + + struct PagedArenaStateEntry + { + const Arena* Arena; + PagedArenaDebugState State; + }; + + // Static Vector for lookup + VectorArena s_PagedArenaStates; + + PagedArenaDebugState& GetPagedArenaState(const Arena* arena) + { + if (s_PagedArenaStates.Arena == nullptr) + { + s_PagedArenaStates.Create(JULIET_DEBUG_ONLY("DebugState States")); + } + + for (auto& entry : s_PagedArenaStates) + { + if (entry.Arena == arena) + { + return entry.State; + } + } + + PagedArenaStateEntry newEntry; + newEntry.Arena = arena; + // Default construct state + newEntry.State = PagedArenaDebugState(); + + s_PagedArenaStates.PushBack(newEntry); + return s_PagedArenaStates.Last()->State; + } +#endif + ArenaDebugState& GetState(const String& name) { // Simple static map-like storage using standard map would be better but we minimize deps. @@ -45,12 +90,37 @@ namespace Juliet::Debug // Simple FNV-1a style hash for (size_t i = 0; i < len; ++i) { - hash = hash * 65599 + (uint8)s[i]; + hash = hash * 65599 + static_cast(s[i]); } // Use hash to pick a Hue float h = static_cast(hash % 360) / 360.0f; return ImColor::HSV(h, 0.7f, 0.8f); } + + // Generate a unique color per allocation, using tag + offset + size + // so entries with same tag name get distinct colors + uint32 GetColorForAllocation(const char* tag, size_t offset, size_t size) + { + uint32 hash = 2166136261u; + if (tag) + { + for (const char* s = tag; *s; ++s) + { + hash ^= static_cast(*s); + hash *= 16777619u; + } + } + // Mix in offset and size to differentiate same-tag entries + hash ^= static_cast(offset); + hash *= 16777619u; + hash ^= static_cast(size); + hash *= 16777619u; + + float h = static_cast(hash % 360) / 360.0f; + float s = 0.5f + static_cast((hash >> 12) % 30) / 100.0f; + float v = 0.65f + static_cast((hash >> 20) % 25) / 100.0f; + return ImColor::HSV(h, s, v); + } #endif void DrawMemoryArena(String name, const MemoryArena& arena, [[maybe_unused]] ArenaAllocation* currentHighlight, @@ -403,6 +473,473 @@ namespace Juliet::Debug ImGui::PopID(); } } + +#if JULIET_DEBUG + void DrawPagedArena(const Arena* arena) + { + ImGui::PushID(arena); + const char* name = (arena->Name && arena->Name[0] != '\0') ? arena->Name : "Unnamed Arena"; + PagedArenaDebugState& state = GetPagedArenaState(arena); + + if (ImGui::CollapsingHeader(name)) + { + // Collect Blocks (Pages) in order (Oldest -> Newest) + // Arena->Previous chain goes Newest -> Oldest. + static VectorArena blocks; + if (blocks.Arena == nullptr) + { + blocks.Create(JULIET_DEBUG_ONLY("DebugState Blocks")); + } + blocks.Clear(); + + for (const Arena* curr = arena->Current; curr != nullptr; curr = curr->Previous) + { + blocks.PushBack(curr); + } + + // Reverse manually + size_t count = blocks.Count; + for (size_t i = 0; i < count / 2; ++i) + { + const Arena* temp = blocks[i]; + blocks[i] = blocks[count - 1 - i]; + blocks[count - 1 - i] = temp; + } + + // Calculate Stats + size_t totalCapacity = 0; + size_t totalUsed = 0; + + for (const Arena* blk : blocks) + { + totalCapacity += blk->Reserved; + totalUsed += blk->Position; + } + + ImGui::Text("Used: %zu / %zu bytes (%zu pages)", totalUsed, totalCapacity, blocks.Size()); + ImGui::SliderFloat("Zoom", &state.Zoom, 0.1f, 1000000.0f, "%.2f", ImGuiSliderFlags_Logarithmic); + + // --- Visual View --- + ImGui::Separator(); + ImGui::Text("Visual Map"); + + float blockHeight = 24.0f; + float blockSpacing = 4.0f; + float requiredVisHeight = static_cast(blocks.Size()) * (blockHeight + blockSpacing) + 30.0f; + // constrains + if (requiredVisHeight > 300.0f) + { + requiredVisHeight = 300.0f; + } + if (requiredVisHeight < 50.0f) + { + requiredVisHeight = 50.0f; + } + + if (ImGui::BeginChild("VisualMap", ImVec2(0, requiredVisHeight), true, + ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar)) + { + ImGui::SetScrollY(0.0f); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + float availWidth = ImGui::GetContentRegionAvail().x; + ImVec2 startPos = ImGui::GetCursorScreenPos(); + + float virtualWidth = availWidth * state.Zoom; + if (virtualWidth < availWidth) + { + virtualWidth = availWidth; + } + ImGui::Dummy(ImVec2(virtualWidth, requiredVisHeight + 1.0f)); + + bool isMapHovered = ImGui::IsWindowHovered(); + + // Zoom with pivot + if (isMapHovered) + { + float wheel = ImGui::GetIO().MouseWheel; + if (wheel != 0.0f) + { + float mouseXScreen = ImGui::GetMousePos().x; + float activeScrollX = ImGui::GetScrollX(); + float viewportLeft = ImGui::GetWindowPos().x; + float mouseRelViewport = mouseXScreen - viewportLeft; + + float virtualWidthOld = std::max(availWidth * state.Zoom, availWidth); + float mouseContentPos = activeScrollX + mouseRelViewport; + float ratio = mouseContentPos / virtualWidthOld; + + state.Zoom += wheel * 0.2f * state.Zoom; + if (state.Zoom < 0.1f) + { + state.Zoom = 0.1f; + } + if (state.Zoom > 1000000.0f) + { + state.Zoom = 1000000.0f; + } + + float virtualWidthNew = std::max(availWidth * state.Zoom, availWidth); + float desiredScrollX = (ratio * virtualWidthNew) - mouseRelViewport; + ImGui::SetScrollX(desiredScrollX); + } + } + + // Pan + if (isMapHovered && (ImGui::IsMouseDragging(ImGuiMouseButton_Left) || ImGui::IsMouseDragging(ImGuiMouseButton_Right) || + ImGui::IsMouseDragging(ImGuiMouseButton_Middle))) + { + ImGui::SetScrollX(ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x); + } + + // Jump to Selected (Sync from List) + if (state.ScrollVisualToSelected && state.SelectedAlloc) + { + for (const Arena* searchBlk : blocks) + { + ArenaDebugInfo* search = searchBlk->FirstDebugInfo; + bool found = false; + while (search) + { + if (search == state.SelectedAlloc) + { + found = true; + break; + } + search = search->Next; + } + + if (found) + { + float virtualWidthLocal = std::max(availWidth * state.Zoom, availWidth); + if (searchBlk->Reserved > 0) + { + double scale = static_cast(virtualWidthLocal) / static_cast(searchBlk->Reserved); + float xStart = static_cast(static_cast(state.SelectedAlloc->Offset) * scale); + float centerOffset = availWidth * 0.5f; + ImGui::SetScrollX(xStart - centerOffset); + } + state.ScrollVisualToSelected = false; + break; + } + } + } + + ImVec2 pos = startPos; + for (const Arena* blk : blocks) + { + ImGui::PushID(blk); + ImVec2 rectMin = pos; + ImVec2 rectMax = ImVec2(pos.x + virtualWidth, pos.y + blockHeight); + + dl->AddRect(rectMin, rectMax, IM_COL32(100, 100, 100, 255)); + + size_t dataSize = blk->Reserved - k_ArenaHeaderSize; + if (dataSize > 0) + { + double scale = static_cast(virtualWidth) / static_cast(blk->Reserved); + + // Draw Arena Header + { + float hdrWidth = static_cast(static_cast(k_ArenaHeaderSize) * scale); + hdrWidth = std::max(hdrWidth, 1.0f); + ImVec2 hMin(pos.x, pos.y + 1); + ImVec2 hMax(pos.x + hdrWidth, pos.y + blockHeight - 1); + dl->AddRectFilled(hMin, hMax, IM_COL32(40, 60, 120, 255)); + + if (hdrWidth > 30.0f) + { + const char* hdrLabel = "Header"; + ImVec2 textSize = ImGui::CalcTextSize(hdrLabel); + if (hdrWidth >= textSize.x + 4.0f) + { + float textX = hMin.x + (hdrWidth - textSize.x) * 0.5f; + float textY = hMin.y + (blockHeight - 2.0f - textSize.y) * 0.5f; + dl->AddText(ImVec2(textX, textY), IM_COL32(180, 200, 255, 255), hdrLabel); + } + } + + if (ImGui::IsMouseHoveringRect(hMin, hMax) && ImGui::IsWindowHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextColored(ImVec4(0.5f, 0.6f, 0.9f, 1.0f), "Arena Header"); + ImGui::Separator(); + ImGui::Text("Size: %zu bytes", k_ArenaHeaderSize); + ImGui::EndTooltip(); + } + } + + // Draw Allocations + ArenaDebugInfo* info = blk->FirstDebugInfo; + size_t expectedOffset = k_ArenaHeaderSize; + while (info) + { + // Draw alignment padding gap if present (only for small gaps that are actual alignment) + if (info->Offset > expectedOffset) + { + size_t padSize = info->Offset - expectedOffset; + constexpr size_t k_MaxAlignmentPadding = 256; + float padXStart = pos.x + static_cast(static_cast(expectedOffset) * scale); + float padWidth = static_cast(static_cast(padSize) * scale); + + if (padSize <= k_MaxAlignmentPadding && padWidth >= 1.0f) + { + ImVec2 pMin(padXStart, pos.y + 1); + ImVec2 pMax(padXStart + padWidth, pos.y + blockHeight - 1); + + // Clip + if (pMin.x < rectMin.x) { pMin.x = rectMin.x; } + if (pMax.x > rectMax.x) { pMax.x = rectMax.x; } + + // Draw hatched pattern (diagonal lines) - clipped to visible area + float visLeft = ImGui::GetWindowPos().x; + float visRight = visLeft + ImGui::GetWindowSize().x; + float clippedLeft = std::max(pMin.x, visLeft); + float clippedRight = std::min(pMax.x, visRight); + + dl->AddRectFilled(pMin, pMax, IM_COL32(30, 30, 30, 200)); + if (clippedLeft < clippedRight) + { + float step = 6.0f; + float startX = clippedLeft - (pMax.y - pMin.y); + startX = pMin.x + step * std::floor((startX - pMin.x) / step); + if (startX < pMin.x) { startX = pMin.x; } + + int lineCount = 0; + for (float lx = startX; lx < clippedRight && lineCount < 500; lx += step, ++lineCount) + { + float ly0 = pMin.y; + float lx1 = lx + (pMax.y - pMin.y); + float ly1 = pMax.y; + if (lx1 > pMax.x) { ly1 = pMin.y + (pMax.x - lx); lx1 = pMax.x; } + dl->AddLine(ImVec2(lx, ly0), ImVec2(lx1, ly1), IM_COL32(80, 80, 80, 180)); + } + } + + // Label when zoomed + if (padWidth > 30.0f) + { + const char* padLabel = "Pad"; + ImVec2 textSize = ImGui::CalcTextSize(padLabel); + if (padWidth >= textSize.x + 4.0f) + { + float textX = pMin.x + (padWidth - textSize.x) * 0.5f; + float textY = pMin.y + (blockHeight - 2.0f - textSize.y) * 0.5f; + dl->AddText(ImVec2(textX, textY), IM_COL32(120, 120, 120, 255), padLabel); + } + } + + if (ImGui::IsMouseHoveringRect(pMin, pMax) && ImGui::IsWindowHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Alignment Padding"); + ImGui::Separator(); + ImGui::Text("Size: %zu bytes", padSize); + ImGui::Text("Offset: %zu - %zu", expectedOffset, info->Offset); + ImGui::EndTooltip(); + } + } + } + expectedOffset = info->Offset + info->Size; + + ImGui::PushID(info); + float xStart = pos.x + static_cast(static_cast(info->Offset) * scale); + float width = static_cast(static_cast(info->Size) * scale); + width = std::max(width, 1.0f); + + ImVec2 aMin(xStart, pos.y + 1); + ImVec2 aMax(xStart + width, pos.y + blockHeight - 1); + + // Clip to block bounds + if (aMin.x < rectMin.x) + { + aMin.x = rectMin.x; + } + if (aMax.x > rectMax.x) + { + aMax.x = rectMax.x; + } + + ImU32 color = GetColorForAllocation(info->Tag, info->Offset, info->Size); + bool isSelected = (state.SelectedAlloc == info); + + if (isSelected) + { + dl->AddRectFilled(aMin, aMax, IM_COL32_WHITE); + dl->AddRect(aMin, aMax, IM_COL32_BLACK); + } + else + { + dl->AddRectFilled(aMin, aMax, color); + } + + // Draw text label when zoomed enough + if (width > 20.0f && info->Tag) + { + const char* tagStr = info->Tag; + size_t tagLen = 0; + while (tagStr[tagLen] != '\0') + { + ++tagLen; + } + ImVec2 textSize = ImGui::CalcTextSize(tagStr, tagStr + tagLen); + if (width >= textSize.x + 4.0f) + { + float textX = aMin.x + (width - textSize.x) * 0.5f; + float textY = aMin.y + (blockHeight - 2.0f - textSize.y) * 0.5f; + ImU32 textColor = IM_COL32_BLACK; + dl->AddText(ImVec2(textX, textY), textColor, tagStr, tagStr + tagLen); + } + } + + if (ImGui::IsMouseHoveringRect(aMin, aMax) && ImGui::IsWindowHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", info->Tag); + ImGui::Separator(); + ImGui::Text("Size: %zu bytes", info->Size); + ImGui::Text("Offset: %zu", info->Offset); + + // Hex data dump (first 16 bytes) + ImGui::Separator(); + uint8* dataPtr = reinterpret_cast(const_cast(blk)) + info->Offset; + ImGui::Text("Data: "); + for (int i = 0; i < 16 && i < static_cast(info->Size); ++i) + { + ImGui::SameLine(); + ImGui::Text("%02X", dataPtr[i]); + } + + ImGui::EndTooltip(); + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + state.SelectedAlloc = info; + state.ScrollListToSelected = true; + state.ScrollVisualToSelected = false; + } + } + + info = info->Next; + ImGui::PopID(); + } + + // Draw Free Nodes (Holes) if Realloc is enabled + if (arena->AllowRealloc && blk->FreeNodes) + { + ArenaFreeNode* freeNode = blk->FreeNodes; + while(freeNode) + { + float fxStart = pos.x + static_cast(static_cast(freeNode->Position) * scale); + float fWidth = static_cast(static_cast(freeNode->Size) * scale); + + ImVec2 fMin(fxStart, pos.y + 1); + ImVec2 fMax(fxStart + fWidth, pos.y + blockHeight - 1); + + dl->AddRectFilled(fMin, fMax, IM_COL32(50, 50, 50, 200)); + + freeNode = freeNode->Next; + } + } + } + + pos.y += blockHeight + blockSpacing; + ImGui::PopID(); + } + } + ImGui::EndChild(); + + // --- Tree View (Allocations List) --- + ImGui::Separator(); + if (ImGui::TreeNode("Allocations List")) + { + // Calculate item count for dynamic height + int totalAllocCount = 0; + for (const Arena* blk : blocks) + { + totalAllocCount++; + ArenaDebugInfo* countInfo = blk->FirstDebugInfo; + while (countInfo) + { + totalAllocCount++; + countInfo = countInfo->Next; + } + } + + float lineHeight = ImGui::GetTextLineHeightWithSpacing(); + float currentNeededHeight = static_cast(totalAllocCount) * lineHeight; + float maxHeight = lineHeight * 25.0f; + + float listHeight = currentNeededHeight; + if (listHeight > maxHeight) + { + listHeight = maxHeight; + } + if (listHeight < lineHeight) + { + listHeight = lineHeight; + } + + if (ImGui::BeginChild("AllocList", ImVec2(0, listHeight), true)) + { + int blkIdx = 0; + for (const Arena* blk : blocks) + { + if (ImGui::TreeNode(reinterpret_cast(blk), "Page %d (%zu bytes, pos %zu)", blkIdx, blk->Reserved, blk->Position)) + { + ArenaDebugInfo* info = blk->FirstDebugInfo; + if (!info) + { + ImGui::TextDisabled("No allocations"); + } + while (info) + { + bool isSelected = (state.SelectedAlloc == info); + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (isSelected) + { + flags |= ImGuiTreeNodeFlags_Selected; + } + + if (isSelected && state.ScrollListToSelected) + { + ImGui::SetScrollHereY(); + state.ScrollListToSelected = false; + } + + // Color Square + ImU32 color = GetColorForAllocation(info->Tag, info->Offset, info->Size); + ImGui::ColorButton("##color", ImColor(color), + ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, + ImVec2(12, 12)); + ImGui::SameLine(); + + ImGui::TreeNodeEx(info, flags, "[%zu] %s (%zu bytes)", info->Offset, info->Tag, info->Size); + + if (ImGui::IsItemClicked()) + { + state.SelectedAlloc = info; + state.ScrollVisualToSelected = true; + state.ScrollListToSelected = false; + } + + info = info->Next; + } + ImGui::TreePop(); + } + blkIdx++; + } + } + ImGui::EndChild(); + + ImGui::TreePop(); + } + } + ImGui::PopID(); + } +#endif + } // namespace void DebugDrawMemoryArena() @@ -418,7 +955,43 @@ namespace Juliet::Debug if (ImGui::Begin("Memory Debugger")) { - DrawMemoryArena(ConstString("Game Arena"), *GetGameArena(), s_ConfirmedHovered, frameHovered); + if (ImGui::BeginTabBar("ArenaTabs")) + { +#if JULIET_DEBUG + if (ImGui::BeginTabItem("Paged Arenas")) + { + Arena* head = GetGlobalArenaListHead(); + if (!head) + { + ImGui::Text("No active paged arenas."); + } + else + { + for (Arena* a = head; a != nullptr; a = a->GlobalNext) + { + // Skip test and internal debug arenas + if (a->Name) + { + bool isTest = (a->Name[0] == 'T' && a->Name[1] == 'e' && a->Name[2] == 's' && a->Name[3] == 't'); + bool isDebugInfo = (a->Name[0] == 'D' && a->Name[1] == 'e' && a->Name[2] == 'b' && a->Name[3] == 'u' && a->Name[4] == 'g' && a->Name[5] == ' ' && a->Name[6] == 'I'); + if (isTest || isDebugInfo) + { + continue; + } + } + DrawPagedArena(a); + } + } + ImGui::EndTabItem(); + } +#endif + if (ImGui::BeginTabItem("Legacy Arenas")) + { + DrawMemoryArena(ConstString("Game Arena"), *GetGameArena(), s_ConfirmedHovered, frameHovered); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } } ImGui::End(); diff --git a/Juliet/src/Graphics/D3D12/D3D12Common.cpp b/Juliet/src/Graphics/D3D12/D3D12Common.cpp index afe5656..26cab12 100644 --- a/Juliet/src/Graphics/D3D12/D3D12Common.cpp +++ b/Juliet/src/Graphics/D3D12/D3D12Common.cpp @@ -49,7 +49,7 @@ namespace Juliet::D3D12::Internal D3D12StagingDescriptorPool* CreateStagingDescriptorPool(NonNullPtr driver, D3D12_DESCRIPTOR_HEAP_TYPE type) { - Arena* arena = ArenaAllocate(); + Arena* arena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Staging Descriptors")); D3D12DescriptorHeap* heap = CreateDescriptorHeap(driver, arena, type, kStagingHeapDescriptorExpectedCount, true); if (!heap) diff --git a/Juliet/src/Graphics/D3D12/D3D12DescriptorHeap.cpp b/Juliet/src/Graphics/D3D12/D3D12DescriptorHeap.cpp index 2cd7c4a..338fdbb 100644 --- a/Juliet/src/Graphics/D3D12/D3D12DescriptorHeap.cpp +++ b/Juliet/src/Graphics/D3D12/D3D12DescriptorHeap.cpp @@ -12,7 +12,7 @@ namespace Juliet::D3D12::Internal { // Heap pool is just single linked list of free elements constexpr size_t kInitialCapacity = 4; - heapPool.Arena = ArenaAllocate(); + heapPool.Arena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "Descriptor Heap Pool")); heapPool.FirstFreeDescriptorHeap = nullptr; // Pre allocate 4 @@ -44,7 +44,7 @@ namespace Juliet::D3D12::Internal heap->CurrentDescriptorIndex = 0; - heap->FreeIndices.Create(); + heap->FreeIndices.Create(JULIET_DEBUG_ONLY("DescriptorHeap FreeIndices")); heap->FreeIndices.Resize(16); heap->CurrentFreeIndex = 0; diff --git a/Juliet/src/Graphics/D3D12/D3D12GraphicsDevice.cpp b/Juliet/src/Graphics/D3D12/D3D12GraphicsDevice.cpp index bbe2971..e574786 100644 --- a/Juliet/src/Graphics/D3D12/D3D12GraphicsDevice.cpp +++ b/Juliet/src/Graphics/D3D12/D3D12GraphicsDevice.cpp @@ -731,7 +731,7 @@ namespace Juliet::D3D12 auto driver = static_cast(Calloc(1, sizeof(D3D12Driver))); // TODO : Convert everything to arena - driver->DriverArena = ArenaAllocate(); + driver->DriverArena = ArenaAllocate({} JULIET_DEBUG_ONLY(, "D3D12 Driver")); #if JULIET_DEBUG #ifdef IDXGIINFOQUEUE_SUPPORTED diff --git a/Juliet/src/UnitTest/Container/VectorUnitTest.cpp b/Juliet/src/UnitTest/Container/VectorUnitTest.cpp index 19f9736..22f7795 100644 --- a/Juliet/src/UnitTest/Container/VectorUnitTest.cpp +++ b/Juliet/src/UnitTest/Container/VectorUnitTest.cpp @@ -149,7 +149,7 @@ namespace Juliet { // Initial capacity 4, allow realloc ArenaParams params{ .AllowRealloc = true }; - Arena* externalArena = ArenaAllocate(params); + Arena* externalArena = ArenaAllocate(params JULIET_DEBUG_ONLY(, "VectorTest")); VectorArena vec = {}; vec.Create(externalArena); @@ -174,6 +174,7 @@ namespace Juliet Assert(vec[4] == 5); vec.Destroy(); + ArenaRelease(externalArena); } // Test 6: Move Semantics @@ -208,7 +209,7 @@ namespace Juliet VectorArena vec = {}; ArenaParams params{ .AllowRealloc = true }; - Arena* externalArena = ArenaAllocate(params); + Arena* externalArena = ArenaAllocate(params JULIET_DEBUG_ONLY(, "VectorTest")); NonNullPtr validArena(externalArena); vec.Create(validArena); @@ -221,12 +222,13 @@ namespace Juliet Assert(!vec[0].MovedFrom); vec.Destroy(); + ArenaRelease(externalArena); } // Test 7: Resize Behavior { ArenaParams params{ .AllowRealloc = true }; - Arena* externalArena = ArenaAllocate(params); + Arena* externalArena = ArenaAllocate(params JULIET_DEBUG_ONLY(, "VectorTest")); NonNullPtr validArena(externalArena); VectorArena vec = {}; @@ -247,12 +249,13 @@ namespace Juliet Assert(vec[0] == 1); vec.Destroy(); + ArenaRelease(externalArena); } // Test 8: External Arena Usage { ArenaParams params{ .AllowRealloc = true }; - Arena* externalArena = ArenaAllocate(params); + Arena* externalArena = ArenaAllocate(params JULIET_DEBUG_ONLY(, "VectorTest")); NonNullPtr validArena(externalArena); VectorArena vec = {}; @@ -263,12 +266,13 @@ namespace Juliet Assert(vec[0] == 42); vec.Destroy(); + ArenaRelease(externalArena); } // Test 9: Volume Test (100 items with Realloc) { ArenaParams params{ .AllowRealloc = true }; - Arena* externalArena = ArenaAllocate(params); + Arena* externalArena = ArenaAllocate(params JULIET_DEBUG_ONLY(, "VectorTest")); NonNullPtr validArena(externalArena); VectorArena vec = {}; @@ -287,6 +291,7 @@ namespace Juliet } vec.Destroy(); + ArenaRelease(externalArena); } } } // namespace Juliet