feat: Implement a memory arena system with an ImGui-based visual debugger for allocation visualization.

Made using Gemini
This commit is contained in:
2026-01-25 12:14:06 -05:00
parent 0788fdeb98
commit 3dd0a4a6f1
6 changed files with 238 additions and 45 deletions

View File

@@ -2,6 +2,8 @@
trigger: always_on 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 No exceptions
Use [[nodiscard]] Use [[nodiscard]]
auto is allowed but when its a pointer add the * and when reference adds the & auto is allowed but when its a pointer add the * and when reference adds the &

View File

@@ -10,14 +10,27 @@ namespace Juliet
{ {
// --- Paged Memory Architecture --- // --- Paged Memory Architecture ---
// [Verifying Rebuild] struct ArenaAllocation
{
size_t Offset;
size_t Size;
String Tag;
ArenaAllocation* Next;
};
struct MemoryBlock struct MemoryBlock
{ {
static constexpr uint32 kMagic = 0xAA55AA55; static constexpr uint32 kMagic = 0xAA55AA55;
uint32 Magic; uint32 Magic;
MemoryBlock* Next; // Next block in the chain (Arena) or FreeList (Pool) MemoryBlock* Next; // Next block in the chain (Arena) or FreeList (Pool)
size_t TotalSize; // Total size of this block (including header) size_t TotalSize; // Total size of this block (including header)
size_t Used; // Offset relative to the start of Data 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. // Data follows immediately.
// We use a helper to access it to avoid C++ flexible array warning issues if strict // We use a helper to access it to avoid C++ flexible array warning issues if strict
uint8* GetData() { return reinterpret_cast<uint8*>(this + 1); } uint8* GetData() { return reinterpret_cast<uint8*>(this + 1); }

View File

@@ -15,6 +15,21 @@ namespace Juliet
// --- MemoryPool Implementation --- // --- 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 // Simple First-Fit Allocator
MemoryBlock* MemoryPool::AllocateBlock(size_t minCapacity) MemoryBlock* MemoryPool::AllocateBlock(size_t minCapacity)
{ {
@@ -57,6 +72,9 @@ namespace Juliet
curr->Next = nullptr; curr->Next = nullptr;
curr->Used = 0; curr->Used = 0;
curr->Magic = MemoryBlock::kMagic; curr->Magic = MemoryBlock::kMagic;
#if JULIET_DEBUG
curr->FirstAllocation = nullptr;
#endif
#if JULIET_DEBUG #if JULIET_DEBUG
if (curr->TotalSize > sizeof(MemoryBlock)) if (curr->TotalSize > sizeof(MemoryBlock))
@@ -87,6 +105,7 @@ namespace Juliet
// Poison Header and Data in Debug // Poison Header and Data in Debug
#if JULIET_DEBUG #if JULIET_DEBUG
FreeDebugAllocations(block);
// 0xDD = Dead Data // 0xDD = Dead Data
MemSet(block->GetData(), 0xDD, block->TotalSize - sizeof(MemoryBlock)); MemSet(block->GetData(), 0xDD, block->TotalSize - sizeof(MemoryBlock));
block->Magic = 0xDEADBEEF; block->Magic = 0xDEADBEEF;
@@ -169,6 +188,23 @@ namespace Juliet
// Commit // Commit
blk->Used += alignmentOffset; blk->Used += alignmentOffset;
void* ptr = blk->GetData() + blk->Used; 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; blk->Used += size;
return ptr; return ptr;
@@ -195,6 +231,13 @@ namespace Juliet
{ {
// Yes, expand in place // Yes, expand in place
blk->Used += (newSize - oldSize); 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; return oldPtr;
} }
} }
@@ -223,6 +266,18 @@ namespace Juliet
{ {
// Yes, we can just rewind the Used pointer // Yes, we can just rewind the Used pointer
blk->Used -= size; 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; return true;
} }
@@ -249,6 +304,7 @@ namespace Juliet
#if JULIET_DEBUG #if JULIET_DEBUG
// Poison First Block // Poison First Block
FreeDebugAllocations(arena->FirstBlock);
MemSet(arena->FirstBlock->GetData(), 0xCD, arena->FirstBlock->TotalSize - sizeof(MemoryBlock)); MemSet(arena->FirstBlock->GetData(), 0xCD, arena->FirstBlock->TotalSize - sizeof(MemoryBlock));
#endif #endif
} }

View File

@@ -1,21 +1,43 @@
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Core/Memory/EngineArena.h> #include <Core/Memory/EngineArena.h>
#include <cstdio>
#include <Engine/Debug/MemoryDebugger.h> #include <Engine/Debug/MemoryDebugger.h>
#include <imgui.h> #include <imgui.h>
#include <algorithm>
namespace Juliet::Debug namespace Juliet::Debug
{ {
namespace 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<float>(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)) if (ImGui::CollapsingHeader(CStr(name), ImGuiTreeNodeFlags_DefaultOpen))
{ {
ImGui::PushID(CStr(name));
// Calculate Stats // Calculate Stats
size_t totalCapacity = 0; size_t totalCapacity = 0;
size_t totalUsed = 0; size_t totalUsed = 0;
size_t blockCount = 0; size_t blockCount = 0;
MemoryBlock* curr = arena.FirstBlock; MemoryBlock* curr = arena.FirstBlock;
while (curr) while (curr)
@@ -26,47 +48,131 @@ namespace Juliet::Debug
curr = curr->Next; curr = curr->Next;
} }
float progress = 0.0f; ImGui::Text("Used: %zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount);
if (totalCapacity > 0)
{
progress = (float)totalUsed / (float)totalCapacity;
}
char overlay[64]; #if JULIET_DEBUG
sprintf_s(overlay, "%zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount); // --- Tree View ---
ImGui::ProgressBar(progress, ImVec2(0.0f, 0.0f), overlay); if (ImGui::TreeNode("Allocations List"))
ImGui::PushID(CStr(name));
if (ImGui::TreeNode("Blocks"))
{ {
if (ImGui::BeginTable("BlocksTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) MemoryBlock* blk = arena.FirstBlock;
int blkIdx = 0;
while (blk)
{ {
ImGui::TableSetupColumn("ID"); if (ImGui::TreeNode((void*)blk, "Block %d (%zu bytes)", blkIdx, blk->TotalSize))
ImGui::TableSetupColumn("Used");
ImGui::TableSetupColumn("Capacity");
ImGui::TableHeadersRow();
size_t idx = 0;
curr = arena.FirstBlock;
while (curr)
{ {
ImGui::TableNextRow(); ArenaAllocation* alloc = blk->FirstAllocation;
while (alloc)
{
bool isHovered = (currentHighlight == alloc);
if (isHovered)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 0, 1));
}
ImGui::TableSetColumnIndex(0); ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
ImGui::Text("%zu", idx++); if (isHovered)
{
flags |= ImGuiTreeNodeFlags_Selected;
}
ImGui::TableSetColumnIndex(1); // Use implicit string length from String struct
ImGui::Text("%zu", curr->Used); ImGui::TreeNodeEx(alloc, flags, "[%zu] %.*s (%zu bytes)", alloc->Offset,
(int)alloc->Tag.Size, alloc->Tag.Data, alloc->Size);
ImGui::TableSetColumnIndex(2); if (ImGui::IsItemHovered())
ImGui::Text("%zu", curr->TotalSize); {
outNewHighlight = alloc;
}
if (isHovered)
{
ImGui::PopStyleColor();
}
curr = curr->Next; alloc = alloc->Next;
}
ImGui::TreePop();
} }
ImGui::EndTable(); blk = blk->Next;
blkIdx++;
} }
ImGui::TreePop(); 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(); ImGui::PopID();
} }
} }
@@ -74,12 +180,25 @@ namespace Juliet::Debug
void DebugDrawMemoryArena() 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")) if (ImGui::Begin("Memory Debugger"))
{ {
DrawMemoryArena(ConstString("Game Arena"), *GetGameArena()); DrawMemoryArena(ConstString("Game Arena"), *GetGameArena(), s_ConfirmedHovered, frameHovered);
DrawMemoryArena(ConstString("Engine Arena"), *GetEngineArena()); DrawMemoryArena(ConstString("Engine Arena"), *GetEngineArena(), s_ConfirmedHovered, frameHovered);
DrawMemoryArena(ConstString("Scratch Arena"), *GetScratchArena()); DrawMemoryArena(ConstString("Scratch Arena"), *GetScratchArena(), s_ConfirmedHovered, frameHovered);
} }
ImGui::End(); ImGui::End();
#if JULIET_DEBUG
s_ConfirmedHovered = frameHovered;
#endif
} }
} // namespace Juliet::Debug } // namespace Juliet::Debug

3
log_direct.txt Normal file
View File

@@ -0,0 +1,3 @@
2> Exe: W:\Classified\Juliet\bin\x64Clang-Debug\JulietApp.exe
FBuild: OK: clang-Debug
Time: 0.213s

View File

@@ -1,3 +1,3 @@
@echo off @echo off
rem call misc\shell.bat call misc\shell.bat
fbuild %* -cache fbuild %* -cache