feat: Implement a memory arena system with an ImGui-based visual debugger for allocation visualization.
Made using Gemini
This commit is contained in:
@@ -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 &
|
||||
|
||||
@@ -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<uint8*>(this + 1); }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/Memory/EngineArena.h>
|
||||
#include <cstdio>
|
||||
#include <Engine/Debug/MemoryDebugger.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<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))
|
||||
{
|
||||
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();
|
||||
ArenaAllocation* alloc = blk->FirstAllocation;
|
||||
while (alloc)
|
||||
{
|
||||
bool isHovered = (currentHighlight == alloc);
|
||||
if (isHovered)
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 0, 1));
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::Text("%zu", idx++);
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
if (isHovered)
|
||||
{
|
||||
flags |= ImGuiTreeNodeFlags_Selected;
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("%zu", curr->Used);
|
||||
// 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);
|
||||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%zu", curr->TotalSize);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
outNewHighlight = alloc;
|
||||
}
|
||||
if (isHovered)
|
||||
{
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
curr = curr->Next;
|
||||
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
|
||||
|
||||
3
log_direct.txt
Normal file
3
log_direct.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
2> Exe: W:\Classified\Juliet\bin\x64Clang-Debug\JulietApp.exe
|
||||
FBuild: OK: clang-Debug
|
||||
Time: 0.213s
|
||||
@@ -1,3 +1,3 @@
|
||||
@echo off
|
||||
rem call misc\shell.bat
|
||||
call misc\shell.bat
|
||||
fbuild %* -cache
|
||||
Reference in New Issue
Block a user