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
|
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 &
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ 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;
|
||||||
@@ -18,6 +25,12 @@ namespace Juliet
|
|||||||
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); }
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
#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;
|
||||||
@@ -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)
|
|
||||||
|
#if JULIET_DEBUG
|
||||||
|
// --- Tree View ---
|
||||||
|
if (ImGui::TreeNode("Allocations List"))
|
||||||
{
|
{
|
||||||
progress = (float)totalUsed / (float)totalCapacity;
|
MemoryBlock* blk = arena.FirstBlock;
|
||||||
|
int blkIdx = 0;
|
||||||
|
while (blk)
|
||||||
|
{
|
||||||
|
if (ImGui::TreeNode((void*)blk, "Block %d (%zu bytes)", blkIdx, blk->TotalSize))
|
||||||
|
{
|
||||||
|
ArenaAllocation* alloc = blk->FirstAllocation;
|
||||||
|
while (alloc)
|
||||||
|
{
|
||||||
|
bool isHovered = (currentHighlight == alloc);
|
||||||
|
if (isHovered)
|
||||||
|
{
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
char overlay[64];
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||||
sprintf_s(overlay, "%zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount);
|
if (isHovered)
|
||||||
ImGui::ProgressBar(progress, ImVec2(0.0f, 0.0f), overlay);
|
|
||||||
|
|
||||||
ImGui::PushID(CStr(name));
|
|
||||||
if (ImGui::TreeNode("Blocks"))
|
|
||||||
{
|
{
|
||||||
if (ImGui::BeginTable("BlocksTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
|
flags |= ImGuiTreeNodeFlags_Selected;
|
||||||
{
|
|
||||||
ImGui::TableSetupColumn("ID");
|
|
||||||
ImGui::TableSetupColumn("Used");
|
|
||||||
ImGui::TableSetupColumn("Capacity");
|
|
||||||
ImGui::TableHeadersRow();
|
|
||||||
|
|
||||||
size_t idx = 0;
|
|
||||||
curr = arena.FirstBlock;
|
|
||||||
while (curr)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
ImGui::EndTable();
|
|
||||||
|
// 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::TreePop();
|
||||||
}
|
}
|
||||||
|
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();
|
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
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
|
@echo off
|
||||||
rem call misc\shell.bat
|
call misc\shell.bat
|
||||||
fbuild %* -cache
|
fbuild %* -cache
|
||||||
Reference in New Issue
Block a user