From 3dd0a4a6f1fb79e0404d2d9854f27f9f36697003 Mon Sep 17 00:00:00 2001 From: Patedam Date: Sun, 25 Jan 2026 12:14:06 -0500 Subject: [PATCH] feat: Implement a memory arena system with an ImGui-based visual debugger for allocation visualization. Made using Gemini --- .agent/rules/coding-guidelines.md | 2 + Juliet/include/Core/Memory/MemoryArena.h | 21 ++- Juliet/src/Core/Memory/MemoryArena.cpp | 56 ++++++ Juliet/src/Engine/Debug/MemoryDebugger.cpp | 199 ++++++++++++++++----- log_direct.txt | 3 + misc/agent_build.bat | 2 +- 6 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 log_direct.txt diff --git a/.agent/rules/coding-guidelines.md b/.agent/rules/coding-guidelines.md index 01eaaa7..f8fa913 100644 --- a/.agent/rules/coding-guidelines.md +++ b/.agent/rules/coding-guidelines.md @@ -2,6 +2,8 @@ trigger: always_on --- +Code compiles with all warning active and warning as errors. +use static_cast or reinterpret_cast but not parenthesis for casting. No exceptions Use [[nodiscard]] auto is allowed but when its a pointer add the * and when reference adds the & diff --git a/Juliet/include/Core/Memory/MemoryArena.h b/Juliet/include/Core/Memory/MemoryArena.h index 8e499ff..b5f005b 100644 --- a/Juliet/include/Core/Memory/MemoryArena.h +++ b/Juliet/include/Core/Memory/MemoryArena.h @@ -10,14 +10,27 @@ namespace Juliet { // --- Paged Memory Architecture --- - // [Verifying Rebuild] + struct ArenaAllocation + { + size_t Offset; + size_t Size; + String Tag; + ArenaAllocation* Next; + }; + struct MemoryBlock { static constexpr uint32 kMagic = 0xAA55AA55; uint32 Magic; - MemoryBlock* Next; // Next block in the chain (Arena) or FreeList (Pool) - size_t TotalSize; // Total size of this block (including header) - size_t Used; // Offset relative to the start of Data + MemoryBlock* Next; // Next block in the chain (Arena) or FreeList (Pool) + size_t TotalSize; // Total size of this block (including header) + size_t Used; // Offset relative to the start of Data + +#if JULIET_DEBUG + ArenaAllocation* FirstAllocation = nullptr; + uint64 Pad; // Ensure 16-byte alignment (Size 40 -> 48) +#endif + // Data follows immediately. // We use a helper to access it to avoid C++ flexible array warning issues if strict uint8* GetData() { return reinterpret_cast(this + 1); } diff --git a/Juliet/src/Core/Memory/MemoryArena.cpp b/Juliet/src/Core/Memory/MemoryArena.cpp index 665d77b..174d460 100644 --- a/Juliet/src/Core/Memory/MemoryArena.cpp +++ b/Juliet/src/Core/Memory/MemoryArena.cpp @@ -15,6 +15,21 @@ 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) { @@ -57,6 +72,9 @@ namespace Juliet curr->Next = nullptr; curr->Used = 0; curr->Magic = MemoryBlock::kMagic; +#if JULIET_DEBUG + curr->FirstAllocation = nullptr; +#endif #if JULIET_DEBUG if (curr->TotalSize > sizeof(MemoryBlock)) @@ -87,6 +105,7 @@ namespace Juliet // Poison Header and Data in Debug #if JULIET_DEBUG + FreeDebugAllocations(block); // 0xDD = Dead Data MemSet(block->GetData(), 0xDD, block->TotalSize - sizeof(MemoryBlock)); block->Magic = 0xDEADBEEF; @@ -169,6 +188,23 @@ namespace Juliet // Commit 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 + blk->Used += size; return ptr; @@ -195,6 +231,13 @@ namespace Juliet { // Yes, expand in place blk->Used += (newSize - oldSize); +#if JULIET_DEBUG + { + ArenaAllocation* t = blk->FirstAllocation; + while (t && t->Next) t = t->Next; + if (t) t->Size += (newSize - oldSize); + } +#endif return oldPtr; } } @@ -223,6 +266,18 @@ 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 return true; } @@ -249,6 +304,7 @@ namespace Juliet #if JULIET_DEBUG // Poison First Block + FreeDebugAllocations(arena->FirstBlock); MemSet(arena->FirstBlock->GetData(), 0xCD, arena->FirstBlock->TotalSize - sizeof(MemoryBlock)); #endif } diff --git a/Juliet/src/Engine/Debug/MemoryDebugger.cpp b/Juliet/src/Engine/Debug/MemoryDebugger.cpp index 8d1f83f..58d61dc 100644 --- a/Juliet/src/Engine/Debug/MemoryDebugger.cpp +++ b/Juliet/src/Engine/Debug/MemoryDebugger.cpp @@ -1,21 +1,43 @@ #include #include -#include #include #include +#include + namespace Juliet::Debug { namespace { - void DrawMemoryArena(String name, const MemoryArena& arena) +#if JULIET_DEBUG + // Generate a stable color from a string tag + uint32 GetColorForTag(const String& tag) + { + uint32 hash = 0; + size_t len = tag.Size; + const char* s = tag.Data; + // Simple FNV-1a style hash + for (size_t i = 0; i < len; ++i) + { + hash = hash * 65599 + (uint8)s[i]; + } + // Use hash to pick a Hue + float h = static_cast(hash % 360) / 360.0f; + return ImColor::HSV(h, 0.7f, 0.8f); + } +#endif + + void DrawMemoryArena(String name, const MemoryArena& arena, [[maybe_unused]] ArenaAllocation* currentHighlight, + [[maybe_unused]] ArenaAllocation*& outNewHighlight) { if (ImGui::CollapsingHeader(CStr(name), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::PushID(CStr(name)); + // Calculate Stats size_t totalCapacity = 0; - size_t totalUsed = 0; - size_t blockCount = 0; + size_t totalUsed = 0; + size_t blockCount = 0; MemoryBlock* curr = arena.FirstBlock; while (curr) @@ -26,47 +48,131 @@ namespace Juliet::Debug curr = curr->Next; } - float progress = 0.0f; - if (totalCapacity > 0) - { - progress = (float)totalUsed / (float)totalCapacity; - } + ImGui::Text("Used: %zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount); - char overlay[64]; - sprintf_s(overlay, "%zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount); - ImGui::ProgressBar(progress, ImVec2(0.0f, 0.0f), overlay); - - ImGui::PushID(CStr(name)); - if (ImGui::TreeNode("Blocks")) +#if JULIET_DEBUG + // --- Tree View --- + if (ImGui::TreeNode("Allocations List")) { - if (ImGui::BeginTable("BlocksTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + MemoryBlock* blk = arena.FirstBlock; + int blkIdx = 0; + while (blk) { - ImGui::TableSetupColumn("ID"); - ImGui::TableSetupColumn("Used"); - ImGui::TableSetupColumn("Capacity"); - ImGui::TableHeadersRow(); - - size_t idx = 0; - curr = arena.FirstBlock; - while (curr) + if (ImGui::TreeNode((void*)blk, "Block %d (%zu bytes)", blkIdx, blk->TotalSize)) { - ImGui::TableNextRow(); - - ImGui::TableSetColumnIndex(0); - ImGui::Text("%zu", idx++); - - ImGui::TableSetColumnIndex(1); - ImGui::Text("%zu", curr->Used); - - ImGui::TableSetColumnIndex(2); - ImGui::Text("%zu", curr->TotalSize); - - curr = curr->Next; + ArenaAllocation* alloc = blk->FirstAllocation; + while (alloc) + { + bool isHovered = (currentHighlight == alloc); + if (isHovered) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 0, 1)); + } + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (isHovered) + { + flags |= ImGuiTreeNodeFlags_Selected; + } + + // Use implicit string length from String struct + ImGui::TreeNodeEx(alloc, flags, "[%zu] %.*s (%zu bytes)", alloc->Offset, + (int)alloc->Tag.Size, alloc->Tag.Data, alloc->Size); + + if (ImGui::IsItemHovered()) + { + outNewHighlight = alloc; + } + if (isHovered) + { + ImGui::PopStyleColor(); + } + + alloc = alloc->Next; + } + ImGui::TreePop(); } - ImGui::EndTable(); + blk = blk->Next; + blkIdx++; } ImGui::TreePop(); } + + // --- Visual View --- + ImGui::Spacing(); + + MemoryBlock* blk = arena.FirstBlock; + ImDrawList* dl = ImGui::GetWindowDrawList(); + float availWidth = ImGui::GetContentRegionAvail().x; + float blockHeight = 24.0f; + ImVec2 startPos = ImGui::GetCursorScreenPos(); + ImVec2 pos = startPos; + + int bIdx = 0; + while (blk) + { + // Draw Block Frame + ImVec2 rectMin = pos; + auto rectMax = ImVec2(pos.x + availWidth, pos.y + blockHeight); + + // Border Color + ImU32 borderColor = (uint32)ImColor::HSV(((float)bIdx * 0.1f), 0.0f, 0.7f); + dl->AddRect(rectMin, rectMax, borderColor); + + size_t dataSize = blk->TotalSize - sizeof(MemoryBlock); + if (dataSize > 0) + { + double scale = (double)availWidth / (double)dataSize; + + ArenaAllocation* alloc = blk->FirstAllocation; + while (alloc) + { + float xStart = pos.x + (float)((double)alloc->Offset * scale); + float width = (float)((double)alloc->Size * scale); + width = std::max(width, 1.0f); + + auto aMin = ImVec2(xStart, pos.y + 1); + auto aMax = ImVec2(xStart + width, pos.y + blockHeight - 1); + + ImU32 color = GetColorForTag(alloc->Tag); + + if (currentHighlight == alloc) + { + // Highlight + dl->AddRectFilled(aMin, aMax, IM_COL32_WHITE); + dl->AddRect(aMin, aMax, IM_COL32_BLACK); + } + else + { + dl->AddRectFilled(aMin, aMax, color); + } + + // Hit Test + if (ImGui::IsMouseHoveringRect(aMin, aMax)) + { + outNewHighlight = alloc; + ImGui::BeginTooltip(); + ImGui::Text("Tag: %.*s", (int)alloc->Tag.Size, alloc->Tag.Data); + ImGui::Text("Size: %zu bytes", alloc->Size); + ImGui::Text("Offset: %zu", alloc->Offset); + ImGui::EndTooltip(); + } + + alloc = alloc->Next; + } + } + + pos.y += blockHeight + 4; + blk = blk->Next; + bIdx++; + } + + // Reserve space for the custom drawing + ImGui::Dummy(ImVec2(0, (pos.y - startPos.y))); +#else + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Compile with JULIET_DEBUG for Memory Visualization"); +#endif + ImGui::PopID(); } } @@ -74,12 +180,25 @@ namespace Juliet::Debug void DebugDrawMemoryArena() { +#if JULIET_DEBUG + // Cross-frame hover state + static ArenaAllocation* s_ConfirmedHovered = nullptr; + ArenaAllocation* frameHovered = nullptr; +#else + ArenaAllocation* s_ConfirmedHovered = nullptr; + ArenaAllocation* frameHovered = nullptr; +#endif + if (ImGui::Begin("Memory Debugger")) { - DrawMemoryArena(ConstString("Game Arena"), *GetGameArena()); - DrawMemoryArena(ConstString("Engine Arena"), *GetEngineArena()); - DrawMemoryArena(ConstString("Scratch Arena"), *GetScratchArena()); + DrawMemoryArena(ConstString("Game Arena"), *GetGameArena(), s_ConfirmedHovered, frameHovered); + DrawMemoryArena(ConstString("Engine Arena"), *GetEngineArena(), s_ConfirmedHovered, frameHovered); + DrawMemoryArena(ConstString("Scratch Arena"), *GetScratchArena(), s_ConfirmedHovered, frameHovered); } ImGui::End(); + +#if JULIET_DEBUG + s_ConfirmedHovered = frameHovered; +#endif } } // namespace Juliet::Debug diff --git a/log_direct.txt b/log_direct.txt new file mode 100644 index 0000000..cc7f6a5 --- /dev/null +++ b/log_direct.txt @@ -0,0 +1,3 @@ +2> Exe: W:\Classified\Juliet\bin\x64Clang-Debug\JulietApp.exe +FBuild: OK: clang-Debug +Time: 0.213s diff --git a/misc/agent_build.bat b/misc/agent_build.bat index 5e05f80..a304978 100644 --- a/misc/agent_build.bat +++ b/misc/agent_build.bat @@ -1,3 +1,3 @@ @echo off -rem call misc\shell.bat +call misc\shell.bat fbuild %* -cache \ No newline at end of file