Updated memory debugger to have a better display of memory

Made with gemini
This commit is contained in:
2026-01-25 15:19:39 -05:00
parent 3dd0a4a6f1
commit 0a75554071

View File

@@ -9,6 +9,33 @@ namespace Juliet::Debug
{ {
namespace namespace
{ {
struct ArenaDebugState
{
float Zoom = 1.0f;
ArenaAllocation* SelectedAlloc = nullptr;
bool ScrollVisualToSelected = false;
bool ScrollListToSelected = false;
};
ArenaDebugState& GetState(const String& name)
{
// Simple static map-like storage using standard map would be better but we minimize deps.
// Just use a few static vars since we have known 3 arenas.
static ArenaDebugState s_GameState;
static ArenaDebugState s_EngineState;
static ArenaDebugState s_ScratchState;
if (StringCompare(name, ConstString("Game Arena")) == 0)
{
return s_GameState;
}
if (StringCompare(name, ConstString("Engine Arena")) == 0)
{
return s_EngineState;
}
return s_ScratchState;
}
#if JULIET_DEBUG #if JULIET_DEBUG
// Generate a stable color from a string tag // Generate a stable color from a string tag
uint32 GetColorForTag(const String& tag) uint32 GetColorForTag(const String& tag)
@@ -30,6 +57,8 @@ namespace Juliet::Debug
void DrawMemoryArena(String name, const MemoryArena& arena, [[maybe_unused]] ArenaAllocation* currentHighlight, void DrawMemoryArena(String name, const MemoryArena& arena, [[maybe_unused]] ArenaAllocation* currentHighlight,
[[maybe_unused]] ArenaAllocation*& outNewHighlight) [[maybe_unused]] ArenaAllocation*& outNewHighlight)
{ {
ArenaDebugState& state = GetState(name);
if (ImGui::CollapsingHeader(CStr(name), ImGuiTreeNodeFlags_DefaultOpen)) if (ImGui::CollapsingHeader(CStr(name), ImGuiTreeNodeFlags_DefaultOpen))
{ {
ImGui::PushID(CStr(name)); ImGui::PushID(CStr(name));
@@ -50,125 +79,324 @@ namespace Juliet::Debug
ImGui::Text("Used: %zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount); ImGui::Text("Used: %zu / %zu bytes (%zu blocks)", totalUsed, totalCapacity, blockCount);
// Zoom Control
ImGui::SliderFloat("Zoom", &state.Zoom, 0.1f, 1000.0f, "%.2f");
#if JULIET_DEBUG #if JULIET_DEBUG
// --- Tree View --- // --- Visual View (Scrollable + Zoom) ---
if (ImGui::TreeNode("Allocations List")) ImGui::Separator();
ImGui::Text("Visual Map");
// Calculate Dynamic Height
// Height = blockCount * (blockHeight + spacing) + padding
// Constrain between minHeight and maxHeight
float blockHeight = 24.0f;
float blockSpacing = 4.0f;
// Add extra padding to avoid vertical scrollbar triggering due to varying style padding
float requiredVisHeight = (float)blockCount * (blockHeight + blockSpacing) + 30.0f;
if (requiredVisHeight > 300.0f) requiredVisHeight = 300.0f;
if (requiredVisHeight < 50.0f) requiredVisHeight = 50.0f;
// Use ImGuiWindowFlags_NoScrollbar if we fit?
// No, we want horizontal scrollbar. If we provide just enough height, vertical shouldn't show.
if (ImGui::BeginChild("VisualMap", ImVec2(0, requiredVisHeight), true,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar))
{ {
MemoryBlock* blk = arena.FirstBlock; ImGui::SetScrollY(0.0f); // Lock Vertical Scroll to separate Zoom from Scroll
int blkIdx = 0;
ImDrawList* dl = ImGui::GetWindowDrawList();
float availWidth = ImGui::GetContentRegionAvail().x;
ImVec2 startPos = ImGui::GetCursorScreenPos();
// Reserve space for the virtual width FIRST to satisfy layout
// Also reserve explicit height > window height to ensure Child captures MouseWheel (stops bubbling)
float virtualWidth = availWidth * state.Zoom;
if (virtualWidth < availWidth) virtualWidth = availWidth;
ImGui::Dummy(ImVec2(virtualWidth, requiredVisHeight + 1.0f));
bool isMapHovered = ImGui::IsWindowHovered();
// Interaction Logic
// 1. Zoom with Pivot
if (isMapHovered)
{
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f)
{
// Pivot Logic
float mouseXScreen = ImGui::GetMousePos().x;
float activeScrollX = ImGui::GetScrollX();
// Viewport Left
float viewportLeft = ImGui::GetWindowPos().x;
float mouseRelViewport = mouseXScreen - viewportLeft;
// Current Ratio
float virtualWidthOld = std::max(availWidth * state.Zoom, availWidth);
float mouseContentPos = activeScrollX + mouseRelViewport;
float ratio = mouseContentPos / virtualWidthOld;
// Apply Zoom
state.Zoom += wheel * 0.2f * state.Zoom;
if (state.Zoom < 0.1f) state.Zoom = 0.1f;
if (state.Zoom > 1000.0f) state.Zoom = 1000.0f;
// New Ratio
float virtualWidthNew = std::max(availWidth * state.Zoom, availWidth);
// flNewScroll + MouseRel = Ratio * NewWidth
float desiredScrollX = (ratio * virtualWidthNew) - mouseRelViewport;
ImGui::SetScrollX(desiredScrollX);
}
}
// 2. Pan (Left, Right, or Middle Mouse)
if (isMapHovered && (ImGui::IsMouseDragging(ImGuiMouseButton_Left) || ImGui::IsMouseDragging(ImGuiMouseButton_Right) ||
ImGui::IsMouseDragging(ImGuiMouseButton_Middle)))
{
ImGui::SetScrollX(ImGui::GetScrollX() - ImGui::GetIO().MouseDelta.x);
}
// 3. Jump to Selected (Sync from List)
if (state.ScrollVisualToSelected && state.SelectedAlloc)
{
MemoryBlock* searchBlk = arena.FirstBlock;
while (searchBlk)
{
ArenaAllocation* search = searchBlk->FirstAllocation;
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);
size_t dataSize = searchBlk->TotalSize - sizeof(MemoryBlock);
if (dataSize > 0)
{
double scale = (double)virtualWidthLocal / (double)dataSize;
float xStart = (float)((double)state.SelectedAlloc->Offset * scale);
// Scroll to center xStart
float centerOffset = availWidth * 0.5f;
ImGui::SetScrollX(xStart - centerOffset);
}
state.ScrollVisualToSelected = false; // Consumed
break;
}
searchBlk = searchBlk->Next;
}
}
MemoryBlock* blk = arena.FirstBlock;
ImVec2 pos = startPos;
int bIdx = 0;
while (blk) while (blk)
{ {
if (ImGui::TreeNode((void*)blk, "Block %d (%zu bytes)", blkIdx, blk->TotalSize)) // Draw Block Frame
ImVec2 rectMin = pos;
auto rectMax = ImVec2(pos.x + virtualWidth, 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)virtualWidth / (double)dataSize;
ArenaAllocation* alloc = blk->FirstAllocation; ArenaAllocation* alloc = blk->FirstAllocation;
while (alloc) while (alloc)
{ {
bool isHovered = (currentHighlight == alloc); float xStart = pos.x + (float)((double)alloc->Offset * scale);
if (isHovered) 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);
// Clip visually to block bounds (simplistic)
if (aMin.x < rectMin.x) aMin.x = rectMin.x;
if (aMax.x > rectMax.x) aMax.x = rectMax.x;
ImU32 color = GetColorForTag(alloc->Tag);
bool isSelected = (state.SelectedAlloc == alloc);
bool isHovered = (currentHighlight == alloc);
if (isSelected || isHovered)
{ {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 0, 1)); // Highlight
dl->AddRectFilled(aMin, aMax, IM_COL32_WHITE);
dl->AddRect(aMin, aMax, IM_COL32_BLACK);
}
else
{
dl->AddRectFilled(aMin, aMax, color);
} }
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // Draw Text if zoomed enough and fits
if (isHovered) if (width > 20.0f) // Threshold width to attempt drawing text
{ {
flags |= ImGuiTreeNodeFlags_Selected; // Need to check actual text size
ImVec2 textSize = ImGui::CalcTextSize(alloc->Tag.Data, alloc->Tag.Data + alloc->Tag.Size);
if (width >= textSize.x + 4.0f)
{
// Center text
float textX = aMin.x + (width - textSize.x) * 0.5f;
float textY = aMin.y + (blockHeight - 2.0f - textSize.y) * 0.5f;
// Contrast color
ImU32 textColor = IM_COL32_BLACK;
dl->AddText(ImVec2(textX, textY), textColor, alloc->Tag.Data,
alloc->Tag.Data + alloc->Tag.Size);
}
} }
// Use implicit string length from String struct if (ImGui::IsMouseHoveringRect(aMin, aMax) && ImGui::IsWindowHovered())
ImGui::TreeNodeEx(alloc, flags, "[%zu] %.*s (%zu bytes)", alloc->Offset,
(int)alloc->Tag.Size, alloc->Tag.Data, alloc->Size);
if (ImGui::IsItemHovered())
{ {
outNewHighlight = alloc; outNewHighlight = alloc;
} if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
if (isHovered) {
{ state.SelectedAlloc = alloc;
ImGui::PopStyleColor(); state.ScrollListToSelected = true; // Sync to list
state.ScrollVisualToSelected = false;
}
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);
uint8* dataPtr = blk->GetData() + alloc->Offset;
ImGui::Text("Data: ");
for (int i = 0; i < 16 && i < (int)alloc->Size; ++i)
{
ImGui::SameLine();
ImGui::Text("%02X", dataPtr[i]);
}
ImGui::EndTooltip();
} }
alloc = alloc->Next; alloc = alloc->Next;
} }
ImGui::TreePop();
} }
pos.y += blockHeight + 4;
blk = blk->Next; blk = blk->Next;
blkIdx++; bIdx++;
} }
// Final spacing
ImGui::Dummy(ImVec2(0, pos.y - startPos.y));
}
ImGui::EndChild();
// --- Tree View (Scrollable) ---
ImGui::Separator();
if (ImGui::TreeNode("Allocations List"))
{
// Calculate item count for Dynamic Height
int totalAllocCount = 0;
MemoryBlock* countBlk = arena.FirstBlock;
while (countBlk)
{
// Add 1 for Block Node
totalAllocCount++;
// If Block Node is Open? We don't know if we don't query it.
// But actually we are inside a Child window inside the TreeNode.
// We want the Child Window to be sized appropriately.
// Simplification: Just count ALL allocations to assume "expanded" state or just use a max cap.
// User wants "Grow until 25 elements".
// This implies if we have 5 items, height is small. If 100, height is capped at 25.
// We'll just count total allocations + blocks as a rough estimate of potential height.
ArenaAllocation* countAlloc = countBlk->FirstAllocation;
while (countAlloc)
{
totalAllocCount++;
countAlloc = countAlloc->Next;
}
countBlk = countBlk->Next;
}
float lineHeight = ImGui::GetTextLineHeightWithSpacing();
float currentNeededHeight = (float)totalAllocCount * lineHeight;
float maxHeight = lineHeight * 25.0f;
float listHeight = currentNeededHeight;
if (listHeight > maxHeight) listHeight = maxHeight;
if (listHeight < lineHeight) listHeight = lineHeight; // Min 1 line
if (ImGui::BeginChild("AllocList", ImVec2(0, listHeight), true))
{
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 isSelected = (state.SelectedAlloc == alloc);
bool isHovered = (currentHighlight == alloc);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
if (isSelected || isHovered)
{
flags |= ImGuiTreeNodeFlags_Selected;
}
if (isSelected && state.ScrollListToSelected)
{
ImGui::SetScrollHereY();
state.ScrollListToSelected = false; // Consumed
}
// Color Square
ImU32 color = GetColorForTag(alloc->Tag);
ImGui::ColorButton("##color", ImColor(color),
ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop,
ImVec2(12, 12));
ImGui::SameLine();
// 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::IsItemClicked())
{
state.SelectedAlloc = alloc;
state.ScrollVisualToSelected = true; // Sync to visual
state.ScrollListToSelected = false;
}
if (ImGui::IsItemHovered())
{
outNewHighlight = alloc;
}
alloc = alloc->Next;
}
ImGui::TreePop();
}
blk = blk->Next;
blkIdx++;
}
}
ImGui::EndChild();
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 #else
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Compile with JULIET_DEBUG for Memory Visualization"); ImGui::TextColored(ImVec4(1, 0, 0, 1), "Compile with JULIET_DEBUG for Memory Visualization");
#endif #endif
@@ -193,7 +421,6 @@ namespace Juliet::Debug
{ {
DrawMemoryArena(ConstString("Game Arena"), *GetGameArena(), s_ConfirmedHovered, frameHovered); DrawMemoryArena(ConstString("Game Arena"), *GetGameArena(), s_ConfirmedHovered, frameHovered);
DrawMemoryArena(ConstString("Engine Arena"), *GetEngineArena(), s_ConfirmedHovered, frameHovered); DrawMemoryArena(ConstString("Engine Arena"), *GetEngineArena(), s_ConfirmedHovered, frameHovered);
DrawMemoryArena(ConstString("Scratch Arena"), *GetScratchArena(), s_ConfirmedHovered, frameHovered);
} }
ImGui::End(); ImGui::End();