From 8eb643ffb113015f9818bb796c2683cf424653a5 Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Tue, 7 Apr 2026 10:17:17 -0700 Subject: [PATCH 1/7] Add PSYQo heap allocator viewer to Redux PSYQo's allocator now registers a metadata struct with the emulator via a new pcsxhw register (0x1f8020a0), giving Redux live access to the heap's free list head, bottom, top, high-water mark, and marker pointers. The viewer walks the free list from guest memory each frame, reconstructs the full block layout, and displays it as a visual bar map with a detailed block table. The walker is hardened against memory corruption: bounds checks on every pointer read, free list ascending-order validation (catches cycles), size alignment and range checks, iteration caps, and accounting mismatch detection. Corruption is surfaced prominently in the UI with specific diagnostics. Signed-off-by: Nicolas 'Pixel' Noble --- src/core/psxhw.cc | 4 + src/core/psxmem.cc | 1 + src/core/psxmem.h | 4 + src/gui/gui.cc | 2 + src/gui/gui.h | 5 +- src/gui/widgets/heap_viewer.cc | 340 +++++++++++++++++++++++++++++ src/gui/widgets/heap_viewer.h | 56 +++++ src/mips/common/hardware/pcsxhw.h | 4 + src/mips/psyqo/src/alloc.c | 35 ++- vsprojects/gui/gui.vcxproj | 2 + vsprojects/gui/gui.vcxproj.filters | 6 + 11 files changed, 446 insertions(+), 13 deletions(-) create mode 100644 src/gui/widgets/heap_viewer.cc create mode 100644 src/gui/widgets/heap_viewer.h diff --git a/src/core/psxhw.cc b/src/core/psxhw.cc index 12f6e366d..6828b30f1 100644 --- a/src/core/psxhw.cc +++ b/src/core/psxhw.cc @@ -831,6 +831,10 @@ void PCSX::HW::write32(uint32_t add, uint32_t value) { g_emulator->m_mem->msanFree(value); break; } + case 0x1f8020a0: { + g_emulator->m_mem->m_psyqoHeapMetadata = value; + break; + } case 0x1f802094: { PSXAddress headerAddrInfo(value); switch (headerAddrInfo.type) { diff --git a/src/core/psxmem.cc b/src/core/psxmem.cc index 3ac1137ee..ac72b32d0 100644 --- a/src/core/psxmem.cc +++ b/src/core/psxmem.cc @@ -174,6 +174,7 @@ void PCSX::Memory::reset() { memset(m_wram, 0, 0x00800000); memset(m_exp1, 0xff, exp1_size); memset(m_bios, 0, bios_size); + m_psyqoHeapMetadata = 0; static const uint32_t nobios[6] = { Mips::Encoder::lui(Mips::Encoder::Reg::V0, 0xbfc0), // v0 = 0xbfc00000 Mips::Encoder::lui(Mips::Encoder::Reg::V1, 0x1f80), // v1 = 0x1f800000 diff --git a/src/core/psxmem.h b/src/core/psxmem.h index 6055695dc..2df4f7c5e 100644 --- a/src/core/psxmem.h +++ b/src/core/psxmem.h @@ -331,6 +331,10 @@ class Memory { uint32_t m_msanPtr = 1024; EventBus::Listener m_listener; + // Address of the psyqo heap metadata struct in guest memory, + // registered by the MIPS allocator via pcsxhw write to 0x1f8020a0. + uint32_t m_psyqoHeapMetadata = 0; + std::unordered_map m_msanAllocs; static constexpr uint32_t c_msanChainMarker = 0x7ffffd; std::unordered_map m_msanChainRegistry; diff --git a/src/gui/gui.cc b/src/gui/gui.cc index 189a5ff62..47b0551e0 100644 --- a/src/gui/gui.cc +++ b/src/gui/gui.cc @@ -1457,6 +1457,7 @@ in Configuration->Emulation, restart PCSX-Redux, then try again.)")); ImGui::MenuItem(_("Show SIO1 debug"), nullptr, &m_sio1.m_show); ImGui::EndMenu(); } + ImGui::MenuItem(_("Show PSYQo heap viewer"), nullptr, &m_heapViewer.m_show); ImGui::Separator(); if (ImGui::BeginMenu(_("Kernel"))) { ImGui::MenuItem(_("Kernel Events"), nullptr, &m_events.m_show); @@ -1729,6 +1730,7 @@ in Configuration->Emulation, restart PCSX-Redux, then try again.)")); if (g_emulator->m_gpu->m_showCfg) changed |= g_emulator->m_gpu->configure(); if (g_emulator->m_gpu->m_showDebug) g_emulator->m_gpu->debug(); if (m_gpuLogger.m_show) m_gpuLogger.draw(g_emulator->m_gpuLogger.get(), _("GPU Logger")); + if (m_heapViewer.m_show) m_heapViewer.draw(g_emulator->m_mem.get(), _("PSYQo Heap Viewer")); if (m_showUiCfg) { if (ImGui::Begin(_("UI Configuration"), &m_showUiCfg)) { diff --git a/src/gui/gui.h b/src/gui/gui.h index 88b39ebcf..3662dbc79 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -44,6 +44,7 @@ #include "gui/widgets/events.h" #include "gui/widgets/filedialog.h" #include "gui/widgets/gpulogger.h" +#include "gui/widgets/heap_viewer.h" #include "gui/widgets/handlers.h" #include "gui/widgets/isobrowser.h" #include "gui/widgets/kernellog.h" @@ -113,6 +114,7 @@ class GUI final : public UI { typedef Setting ShowSIO1; typedef Setting ShowIsoBrowser; typedef Setting ShowGPULogger; + typedef Setting ShowHeapViewer; typedef Setting WindowPosX; typedef Setting WindowPosY; typedef Setting WindowSizeX; @@ -157,7 +159,7 @@ class GUI final : public UI { ShowCLUTVRAMViewer, ShowVRAMViewer1, ShowVRAMViewer2, ShowVRAMViewer3, ShowVRAMViewer4, ShowMemoryObserver, ShowTypedDebugger, ShowPatches, ShowMemcardManager, ShowRegisters, ShowAssembly, ShowDisassembly, ShowBreakpoints, ShowNamedSaveStates, ShowEvents, ShowHandlers, ShowKernelLog, ShowCallstacks, ShowSIO1, - ShowIsoBrowser, ShowGPULogger, MainFontSize, MonoFontSize, GUITheme, AllowMouseCaptureToggle, + ShowIsoBrowser, ShowGPULogger, ShowHeapViewer, MainFontSize, MonoFontSize, GUITheme, AllowMouseCaptureToggle, EnableRawMouseMotion, WidescreenRatio, ShowPIOCartConfig, ShowMemoryEditor1, ShowMemoryEditor2, ShowMemoryEditor3, ShowMemoryEditor4, ShowMemoryEditor5, ShowMemoryEditor6, ShowMemoryEditor7, ShowMemoryEditor8, ShowParallelPortEditor, ShowScratchpadEditor, ShowHWRegsEditor, ShowBiosEditor, @@ -403,6 +405,7 @@ class GUI final : public UI { Widgets::SIO1 m_sio1 = {settings.get().value}; Widgets::GPULogger m_gpuLogger{settings.get().value}; + Widgets::HeapViewer m_heapViewer{settings.get().value}; EventBus::Listener m_listener; diff --git a/src/gui/widgets/heap_viewer.cc b/src/gui/widgets/heap_viewer.cc new file mode 100644 index 000000000..77188827e --- /dev/null +++ b/src/gui/widgets/heap_viewer.cc @@ -0,0 +1,340 @@ +/*************************************************************************** + * Copyright (C) 2026 PCSX-Redux authors * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * + ***************************************************************************/ + +#include "gui/widgets/heap_viewer.h" + +#include "core/psxmem.h" +#include "fmt/format.h" +#include "imgui.h" + +// The MIPS-side metadata struct layout (5 pointers, 4 bytes each): +// uint32_t head; // offset 0: pointer to first free block +// uint32_t bottom; // offset 4: start of heap +// uint32_t top; // offset 8: end of heap +// uint32_t maximum_heap_end; // offset 12: high-water mark +// uint32_t marker_next; // offset 16: end-of-list sentinel's next (NULL) +// +// Each free block (empty_block) in guest memory: +// uint32_t next; // offset 0: pointer to next free block +// uint32_t size; // offset 4: size in bytes including header + +static uint32_t readGuest32(PCSX::Memory* memory, uint32_t addr) { + auto* ptr = memory->getPointer(addr); + if (!ptr) return 0; + return *ptr; +} + +PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory* memory) { + WalkResult result; + + uint32_t metaAddr = memory->m_psyqoHeapMetadata; + if (metaAddr == 0) return result; + + uint32_t headPtr = readGuest32(memory, metaAddr + 0); + uint32_t bottomPtr = readGuest32(memory, metaAddr + 4); + uint32_t topPtr = readGuest32(memory, metaAddr + 8); + uint32_t markerPtr = metaAddr + 16; + + if (bottomPtr == 0 || topPtr == 0 || bottomPtr >= topPtr) return result; + + // Sanity check: heap shouldn't be larger than 8MB (max PS1 RAM). + if (topPtr - bottomPtr > 8 * 1024 * 1024) { + result.error = "Heap range exceeds 8MB - likely corrupted metadata."; + return result; + } + + // Validate that all metadata pointers resolve to readable memory. + if (!memory->getPointer(bottomPtr) || !memory->getPointer(topPtr - 1)) { + result.error = "Heap range points to unmapped memory."; + return result; + } + + // Collect all free blocks by walking the free list. + // Guard against cycles, out-of-range pointers, and absurd sizes. + struct FreeBlock { + uint32_t address; + uint32_t size; + }; + std::vector freeBlocks; + + uint32_t curr = headPtr; + uint32_t prevAddr = 0; + constexpr int maxFreeBlocks = 100000; + + while (curr != markerPtr && curr != 0 && (int)freeBlocks.size() < maxFreeBlocks) { + // Free block must be within heap range. + if (curr < bottomPtr || curr >= topPtr) { + result.error = fmt::format("Free list entry at {:08x} is outside heap range [{:08x}, {:08x}).", curr, + bottomPtr, topPtr); + break; + } + + // Free list must be sorted by address (ascending). Detects cycles too. + if (prevAddr != 0 && curr <= prevAddr) { + result.error = + fmt::format("Free list not ascending at {:08x} (prev {:08x}) - cycle or corruption.", curr, prevAddr); + break; + } + + if (!memory->getPointer(curr)) { + result.error = fmt::format("Free list entry at {:08x} points to unmapped memory.", curr); + break; + } + + uint32_t next = readGuest32(memory, curr + 0); + uint32_t size = readGuest32(memory, curr + 4); + + // Size must be at least 8 (sizeof empty_block) and aligned to 8. + if (size < 8 || (size & 7) != 0) { + result.error = fmt::format("Free block at {:08x} has invalid size {} (must be >= 8 and 8-aligned).", curr, size); + break; + } + + // Block must not extend past top. + if (curr + size > topPtr) { + result.error = + fmt::format("Free block at {:08x} (size {}) extends past heap top {:08x}.", curr, size, topPtr); + break; + } + + freeBlocks.push_back({curr, size}); + prevAddr = curr; + curr = next; + } + + if (freeBlocks.size() >= maxFreeBlocks && result.error.empty()) { + result.error = "Free list exceeded 100000 entries - likely corrupted."; + } + + // Walk from bottom to top, emitting allocated and free blocks. + // Even if the free list walk hit an error, use whatever we collected. + uint32_t pos = bottomPtr; + size_t freeIdx = 0; + constexpr int maxBlocks = 200000; + + while (pos < topPtr && (int)result.blocks.size() < maxBlocks) { + if (freeIdx < freeBlocks.size() && freeBlocks[freeIdx].address == pos) { + result.blocks.push_back({pos, freeBlocks[freeIdx].size, true}); + pos += freeBlocks[freeIdx].size; + freeIdx++; + } else { + // Allocated block. Read size from header (offset 4, same layout as empty_block). + uint32_t size = readGuest32(memory, pos + 4); + + if (size < 8 || (size & 7) != 0 || size > (topPtr - pos)) { + // Corrupted allocated block. Emit remainder as unknown. + if (result.error.empty()) { + result.error = fmt::format( + "Allocated block at {:08x} has invalid size {} (remaining space: {}).", pos, size, topPtr - pos); + } + result.blocks.push_back({pos, topPtr - pos, false}); + break; + } + + // Skip past any free blocks that fall within this allocated block's range. + // (Shouldn't happen in a healthy heap, but be defensive.) + while (freeIdx < freeBlocks.size() && freeBlocks[freeIdx].address < pos + size) { + if (result.error.empty()) { + result.error = fmt::format("Free block at {:08x} overlaps allocated block at {:08x}.", + freeBlocks[freeIdx].address, pos); + } + freeIdx++; + } + + result.blocks.push_back({pos, size, false}); + pos += size; + } + } + + if (result.blocks.size() >= maxBlocks && result.error.empty()) { + result.error = "Block count exceeded 200000 - likely corrupted."; + } + + return result; +} + +void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { + if (!ImGui::Begin(title, &m_show)) { + ImGui::End(); + return; + } + + uint32_t metaAddr = memory->m_psyqoHeapMetadata; + if (metaAddr == 0) { + ImGui::TextUnformatted("No PSYQo heap registered."); + ImGui::TextWrapped("The running program has not registered its heap metadata with the emulator. " + "This requires a PSYQo build with heap registration support."); + ImGui::End(); + return; + } + + uint32_t headPtr = readGuest32(memory, metaAddr + 0); + uint32_t bottomPtr = readGuest32(memory, metaAddr + 4); + uint32_t topPtr = readGuest32(memory, metaAddr + 8); + uint32_t maxEnd = readGuest32(memory, metaAddr + 12); + uint32_t markerPtr = metaAddr + 16; + + ImGui::Text("Heap range: %08x - %08x (%u bytes)", bottomPtr, topPtr, topPtr - bottomPtr); + ImGui::Text("High-water mark: %08x", maxEnd); + ImGui::Text("Free list head: %08x Marker: %08x", headPtr, markerPtr); + ImGui::Separator(); + + auto result = walkHeap(memory); + + // Show corruption warning prominently if detected. + if (!result.error.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f)); + ImGui::TextWrapped("Heap corruption detected: %s", result.error.c_str()); + ImGui::PopStyleColor(); + ImGui::Separator(); + } + + auto& blocks = result.blocks; + if (blocks.empty()) { + ImGui::TextUnformatted("Heap not yet initialized."); + ImGui::End(); + return; + } + + // Summary stats. + uint32_t totalFree = 0; + uint32_t totalAlloc = 0; + uint32_t freeCount = 0; + uint32_t allocCount = 0; + uint32_t largestFree = 0; + for (auto& b : blocks) { + if (b.free) { + totalFree += b.size; + freeCount++; + if (b.size > largestFree) largestFree = b.size; + } else { + totalAlloc += b.size; + allocCount++; + } + } + + uint32_t totalAccountedFor = totalFree + totalAlloc; + uint32_t heapSize = topPtr > bottomPtr ? topPtr - bottomPtr : 0; + + ImGui::Text("Allocated: %u bytes (%u blocks) Free: %u bytes (%u blocks)", totalAlloc, allocCount, totalFree, + freeCount); + ImGui::Text("Largest free block: %u bytes Fragmentation: %u fragments", largestFree, freeCount); + + // Flag if accounting doesn't add up. + if (heapSize > 0 && totalAccountedFor != heapSize) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f)); + ImGui::Text("Accounting mismatch: blocks sum to %u bytes, heap is %u bytes (%d unaccounted)", + totalAccountedFor, heapSize, (int)heapSize - (int)totalAccountedFor); + ImGui::PopStyleColor(); + } + + ImGui::Separator(); + + // Visual heap map. + if (heapSize > 0) { + ImVec2 avail = ImGui::GetContentRegionAvail(); + float barWidth = avail.x; + float barHeight = 20.0f; + + ImVec2 barPos = ImGui::GetCursorScreenPos(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + for (auto& b : blocks) { + float x0 = barWidth * (float)(b.address - bottomPtr) / (float)heapSize; + float x1 = barWidth * (float)(b.address + b.size - bottomPtr) / (float)heapSize; + // Clamp to bar bounds. + if (x0 < 0) x0 = 0; + if (x1 > barWidth) x1 = barWidth; + ImU32 color = b.free ? IM_COL32(60, 120, 60, 255) : IM_COL32(180, 60, 60, 255); + drawList->AddRectFilled(ImVec2(barPos.x + x0, barPos.y), + ImVec2(barPos.x + x1, barPos.y + barHeight), color); + } + + drawList->AddRect(ImVec2(barPos.x, barPos.y), ImVec2(barPos.x + barWidth, barPos.y + barHeight), + IM_COL32(200, 200, 200, 255)); + ImGui::Dummy(ImVec2(barWidth, barHeight)); + + // Tooltip on hover. + if (ImGui::IsItemHovered()) { + float mouseX = ImGui::GetMousePos().x - barPos.x; + uint32_t hoverAddr = bottomPtr + (uint32_t)((float)heapSize * mouseX / barWidth); + for (auto& b : blocks) { + if (hoverAddr >= b.address && hoverAddr < b.address + b.size) { + ImGui::BeginTooltip(); + ImGui::Text("%s at %08x, %u bytes", b.free ? "Free" : "Allocated", b.address, b.size); + if (!b.free && b.size > 8) { + ImGui::Text("User payload: %u bytes", b.size - 8); + } + ImGui::EndTooltip(); + break; + } + } + } + + // Legend. + ImGui::ColorButton("##alloc", ImVec4(180.0f / 255, 60.0f / 255, 60.0f / 255, 1.0f), + ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoPicker, ImVec2(12, 12)); + ImGui::SameLine(); + ImGui::Text("Allocated"); + ImGui::SameLine(); + ImGui::ColorButton("##free", ImVec4(60.0f / 255, 120.0f / 255, 60.0f / 255, 1.0f), + ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoPicker, ImVec2(12, 12)); + ImGui::SameLine(); + ImGui::Text("Free"); + } + + ImGui::Separator(); + + // Block table. + if (ImGui::BeginTable("HeapBlocks", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp, + ImVec2(0, 0))) { + ImGui::TableSetupColumn("Address"); + ImGui::TableSetupColumn("Size"); + ImGui::TableSetupColumn("User size"); + ImGui::TableSetupColumn("Status"); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + for (auto& b : blocks) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%08x", b.address); + ImGui::TableNextColumn(); + ImGui::Text("%u", b.size); + ImGui::TableNextColumn(); + if (!b.free) { + ImGui::Text("%u", b.size > 8 ? b.size - 8 : 0); + } else { + ImGui::TextUnformatted("-"); + } + ImGui::TableNextColumn(); + if (b.free) { + ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Free"); + } else { + ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "Allocated"); + } + } + + ImGui::EndTable(); + } + + ImGui::End(); +} diff --git a/src/gui/widgets/heap_viewer.h b/src/gui/widgets/heap_viewer.h new file mode 100644 index 000000000..d4dc48bee --- /dev/null +++ b/src/gui/widgets/heap_viewer.h @@ -0,0 +1,56 @@ +/*************************************************************************** + * Copyright (C) 2026 PCSX-Redux authors * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * + ***************************************************************************/ + +#pragma once + +#include + +#include +#include + +namespace PCSX { + +class Memory; + +namespace Widgets { + +class HeapViewer { + public: + HeapViewer(bool& show) : m_show(show) {} + void draw(Memory* memory, const char* title); + + bool& m_show; + + private: + struct Block { + uint32_t address; + uint32_t size; + bool free; + }; + + struct WalkResult { + std::vector blocks; + std::string error; + }; + + WalkResult walkHeap(Memory* memory); +}; + +} // namespace Widgets +} // namespace PCSX diff --git a/src/mips/common/hardware/pcsxhw.h b/src/mips/common/hardware/pcsxhw.h index 54bfb1d3e..427b67dfe 100644 --- a/src/mips/common/hardware/pcsxhw.h +++ b/src/mips/common/hardware/pcsxhw.h @@ -63,4 +63,8 @@ static __inline__ void* pcsx_msanGetChainPtr(void* headerAddr) { return ret; } +static __inline__ void pcsx_registerHeapMetadata(const void* metadata) { + *((void* volatile* const)0x1f8020a0) = (void*)metadata; +} + static __inline__ int pcsx_present() { return *((volatile uint32_t* const)0x1f802080) == 0x58534350; } diff --git a/src/mips/psyqo/src/alloc.c b/src/mips/psyqo/src/alloc.c index ed160b70d..911728579 100644 --- a/src/mips/psyqo/src/alloc.c +++ b/src/mips/psyqo/src/alloc.c @@ -91,18 +91,26 @@ _Static_assert(sizeof(empty_block) == (2 * sizeof(void *)), "empty_block is of t // The same goes with allocated_block. _Static_assert(sizeof(allocated_block) == (2 * sizeof(void *)), "allocated_block is of the wrong size"); -// We keep track of the head of the list of empty blocks here. -// It is initialized to NULL at compile-time, and will be initialized -// when the first allocation is made. -static empty_block *head = NULL; -static allocated_block *bottom = NULL; -static allocated_block *top = NULL; -static void *maximum_heap_end = NULL; -// The marker is here to make sure that the list is always terminated, -// so when we completely fill the heap, we don't end up with a NULL pointer -// back to the head. It will never fit any allocation, and will always -// be the last block in the list. -static empty_block marker; +// Heap metadata struct, registered with the emulator so +// it can walk the allocator state for the heap viewer. +static struct { + empty_block *head; + allocated_block *bottom; + allocated_block *top; + void *maximum_heap_end; + // The marker is here to make sure that the list is always terminated, + // so when we completely fill the heap, we don't end up with a NULL pointer + // back to the head. It will never fit any allocation, and will always + // be the last block in the list. + empty_block marker; +} s_heap_metadata = {NULL, NULL, NULL, NULL, NULL}; + +// Convenience aliases so the rest of the code reads the same. +#define head s_heap_metadata.head +#define bottom s_heap_metadata.bottom +#define top s_heap_metadata.top +#define maximum_heap_end s_heap_metadata.maximum_heap_end +#define marker s_heap_metadata.marker // Enable this to debug the allocator very thoroughly. May be used to // detect memory corruption, and other issues. @@ -260,6 +268,9 @@ void *psyqo_malloc(size_t size_) { // Its size needs to be aligned to the empty_block size. curr->size = ALIGN_TO(((size_t)&__stack_start) - ((size_t)curr) - sizeof(empty_block)); top = (allocated_block *)((char *)curr + curr->size); + if (pcsx_present()) { + pcsx_registerHeapMetadata(&s_heap_metadata); + } } // Walk the full list of empty blocks, and find the best fit, diff --git a/vsprojects/gui/gui.vcxproj b/vsprojects/gui/gui.vcxproj index 67fb9f7a1..48834fe39 100644 --- a/vsprojects/gui/gui.vcxproj +++ b/vsprojects/gui/gui.vcxproj @@ -142,6 +142,7 @@ + @@ -176,6 +177,7 @@ + diff --git a/vsprojects/gui/gui.vcxproj.filters b/vsprojects/gui/gui.vcxproj.filters index 5b8169ad7..f9431bff6 100644 --- a/vsprojects/gui/gui.vcxproj.filters +++ b/vsprojects/gui/gui.vcxproj.filters @@ -109,6 +109,9 @@ Source Files\widgets + + Source Files + @@ -210,6 +213,9 @@ Header Files\widgets + + Header Files + From bc60f0cf529fca8d12667b949da73b1dbbe8e85e Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Wed, 15 Apr 2026 22:38:51 -0700 Subject: [PATCH 2/7] Add click-to-jump from heap viewer to memory editor Clicking a block in the visual bar or a row in the block table fires a JumpToMemory event that opens the main memory editor at that location. For allocated blocks, the jump targets the user data (past the 8-byte header). For free blocks, it targets the block start. Signed-off-by: Nicolas 'Pixel' Noble --- src/gui/widgets/heap_viewer.cc | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/gui/widgets/heap_viewer.cc b/src/gui/widgets/heap_viewer.cc index 77188827e..0834fbc44 100644 --- a/src/gui/widgets/heap_viewer.cc +++ b/src/gui/widgets/heap_viewer.cc @@ -20,6 +20,7 @@ #include "gui/widgets/heap_viewer.h" #include "core/psxmem.h" +#include "core/system.h" #include "fmt/format.h" #include "imgui.h" @@ -270,7 +271,7 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { IM_COL32(200, 200, 200, 255)); ImGui::Dummy(ImVec2(barWidth, barHeight)); - // Tooltip on hover. + // Tooltip on hover, jump to memory on click. if (ImGui::IsItemHovered()) { float mouseX = ImGui::GetMousePos().x - barPos.x; uint32_t hoverAddr = bottomPtr + (uint32_t)((float)heapSize * mouseX / barWidth); @@ -279,9 +280,19 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { ImGui::BeginTooltip(); ImGui::Text("%s at %08x, %u bytes", b.free ? "Free" : "Allocated", b.address, b.size); if (!b.free && b.size > 8) { - ImGui::Text("User payload: %u bytes", b.size - 8); + ImGui::Text("User payload: %u bytes (click to jump)", b.size - 8); + } else { + ImGui::TextUnformatted("Click to jump"); } ImGui::EndTooltip(); + if (ImGui::IsMouseClicked(0)) { + // For allocated blocks, jump to user data (past the 8-byte header). + // For free blocks, jump to the block start. + uint32_t jumpAddr = b.free ? b.address : b.address + 8; + uint32_t jumpSize = b.free ? b.size : (b.size > 8 ? b.size - 8 : b.size); + g_system->m_eventBus->signal( + Events::GUI::JumpToMemory{jumpAddr | 0x80000000, jumpSize}); + } break; } } @@ -313,10 +324,20 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); - for (auto& b : blocks) { + for (size_t i = 0; i < blocks.size(); i++) { + auto& b = blocks[i]; ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::Text("%08x", b.address); + + // Make the entire row clickable via a Selectable spanning all columns. + char label[32]; + snprintf(label, sizeof(label), "%08x##block%zu", b.address, i); + if (ImGui::Selectable(label, false, + ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) { + uint32_t jumpAddr = b.free ? b.address : b.address + 8; + uint32_t jumpSize = b.free ? b.size : (b.size > 8 ? b.size - 8 : b.size); + g_system->m_eventBus->signal(Events::GUI::JumpToMemory{jumpAddr | 0x80000000, jumpSize}); + } ImGui::TableNextColumn(); ImGui::Text("%u", b.size); ImGui::TableNextColumn(); From 8be795ee03a8db899d31d05bdf027a4f497b1aad Mon Sep 17 00:00:00 2001 From: Nicolas Pixel Noble Date: Sat, 18 Apr 2026 19:46:11 -0700 Subject: [PATCH 3/7] Safer memory accesses. --- src/gui/widgets/heap_viewer.cc | 88 +++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/src/gui/widgets/heap_viewer.cc b/src/gui/widgets/heap_viewer.cc index 0834fbc44..728162c68 100644 --- a/src/gui/widgets/heap_viewer.cc +++ b/src/gui/widgets/heap_viewer.cc @@ -18,6 +18,7 @@ ***************************************************************************/ #include "gui/widgets/heap_viewer.h" +#include #include "core/psxmem.h" #include "core/system.h" @@ -35,9 +36,9 @@ // uint32_t next; // offset 0: pointer to next free block // uint32_t size; // offset 4: size in bytes including header -static uint32_t readGuest32(PCSX::Memory* memory, uint32_t addr) { +static std::optional readGuest32(PCSX::Memory* memory, uint32_t addr) { auto* ptr = memory->getPointer(addr); - if (!ptr) return 0; + if (!ptr) return std::nullopt; return *ptr; } @@ -47,21 +48,26 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory uint32_t metaAddr = memory->m_psyqoHeapMetadata; if (metaAddr == 0) return result; - uint32_t headPtr = readGuest32(memory, metaAddr + 0); - uint32_t bottomPtr = readGuest32(memory, metaAddr + 4); - uint32_t topPtr = readGuest32(memory, metaAddr + 8); - uint32_t markerPtr = metaAddr + 16; + auto headPtr = readGuest32(memory, metaAddr + 0); + auto bottomPtr = readGuest32(memory, metaAddr + 4); + auto topPtr = readGuest32(memory, metaAddr + 8); + auto markerPtr = metaAddr + 16; + + if (!headPtr || !bottomPtr || !topPtr) { + result.error = "Heap metadata contains invalid pointers - likely corrupted metadata."; + return result; + } if (bottomPtr == 0 || topPtr == 0 || bottomPtr >= topPtr) return result; // Sanity check: heap shouldn't be larger than 8MB (max PS1 RAM). - if (topPtr - bottomPtr > 8 * 1024 * 1024) { + if (*topPtr - *bottomPtr > 8 * 1024 * 1024) { result.error = "Heap range exceeds 8MB - likely corrupted metadata."; return result; } // Validate that all metadata pointers resolve to readable memory. - if (!memory->getPointer(bottomPtr) || !memory->getPointer(topPtr - 1)) { + if (!memory->getPointer(*bottomPtr) || !memory->getPointer(*topPtr - 1)) { result.error = "Heap range points to unmapped memory."; return result; } @@ -74,7 +80,7 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory }; std::vector freeBlocks; - uint32_t curr = headPtr; + uint32_t curr = *headPtr; uint32_t prevAddr = 0; constexpr int maxFreeBlocks = 100000; @@ -82,7 +88,7 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory // Free block must be within heap range. if (curr < bottomPtr || curr >= topPtr) { result.error = fmt::format("Free list entry at {:08x} is outside heap range [{:08x}, {:08x}).", curr, - bottomPtr, topPtr); + *bottomPtr, *topPtr); break; } @@ -98,25 +104,25 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory break; } - uint32_t next = readGuest32(memory, curr + 0); - uint32_t size = readGuest32(memory, curr + 4); + auto next = readGuest32(memory, curr + 0); + auto size = readGuest32(memory, curr + 4); // Size must be at least 8 (sizeof empty_block) and aligned to 8. - if (size < 8 || (size & 7) != 0) { - result.error = fmt::format("Free block at {:08x} has invalid size {} (must be >= 8 and 8-aligned).", curr, size); + if (!size || *size < 8 || (*size & 7) != 0) { + result.error = fmt::format("Free block at {:08x} has invalid size {} (must be >= 8 and 8-aligned).", curr, size ? *size : 0); break; } // Block must not extend past top. - if (curr + size > topPtr) { + if (curr + *size > *topPtr) { result.error = - fmt::format("Free block at {:08x} (size {}) extends past heap top {:08x}.", curr, size, topPtr); + fmt::format("Free block at {:08x} (size {}) extends past heap top {:08x}.", curr, *size, *topPtr); break; } - freeBlocks.push_back({curr, size}); + freeBlocks.push_back({curr, *size}); prevAddr = curr; - curr = next; + curr = *next; } if (freeBlocks.size() >= maxFreeBlocks && result.error.empty()) { @@ -125,32 +131,32 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory // Walk from bottom to top, emitting allocated and free blocks. // Even if the free list walk hit an error, use whatever we collected. - uint32_t pos = bottomPtr; + uint32_t pos = *bottomPtr; size_t freeIdx = 0; constexpr int maxBlocks = 200000; - while (pos < topPtr && (int)result.blocks.size() < maxBlocks) { + while (pos < *topPtr && (int)result.blocks.size() < maxBlocks) { if (freeIdx < freeBlocks.size() && freeBlocks[freeIdx].address == pos) { result.blocks.push_back({pos, freeBlocks[freeIdx].size, true}); pos += freeBlocks[freeIdx].size; freeIdx++; } else { // Allocated block. Read size from header (offset 4, same layout as empty_block). - uint32_t size = readGuest32(memory, pos + 4); + auto size = readGuest32(memory, pos + 4); - if (size < 8 || (size & 7) != 0 || size > (topPtr - pos)) { + if (!size || *size < 8 || (*size & 7) != 0 || *size > (*topPtr - pos)) { // Corrupted allocated block. Emit remainder as unknown. if (result.error.empty()) { result.error = fmt::format( - "Allocated block at {:08x} has invalid size {} (remaining space: {}).", pos, size, topPtr - pos); + "Allocated block at {:08x} has invalid size {} (remaining space: {}).", pos, size ? *size : 0, *topPtr - pos); } - result.blocks.push_back({pos, topPtr - pos, false}); + result.blocks.push_back({pos, *topPtr - pos, false}); break; } // Skip past any free blocks that fall within this allocated block's range. // (Shouldn't happen in a healthy heap, but be defensive.) - while (freeIdx < freeBlocks.size() && freeBlocks[freeIdx].address < pos + size) { + while (freeIdx < freeBlocks.size() && freeBlocks[freeIdx].address < pos + *size) { if (result.error.empty()) { result.error = fmt::format("Free block at {:08x} overlaps allocated block at {:08x}.", freeBlocks[freeIdx].address, pos); @@ -158,8 +164,8 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory freeIdx++; } - result.blocks.push_back({pos, size, false}); - pos += size; + result.blocks.push_back({pos, *size, false}); + pos += *size; } } @@ -185,15 +191,21 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { return; } - uint32_t headPtr = readGuest32(memory, metaAddr + 0); - uint32_t bottomPtr = readGuest32(memory, metaAddr + 4); - uint32_t topPtr = readGuest32(memory, metaAddr + 8); - uint32_t maxEnd = readGuest32(memory, metaAddr + 12); + auto headPtr = readGuest32(memory, metaAddr + 0); + auto bottomPtr = readGuest32(memory, metaAddr + 4); + auto topPtr = readGuest32(memory, metaAddr + 8); + auto maxEnd = readGuest32(memory, metaAddr + 12); uint32_t markerPtr = metaAddr + 16; - ImGui::Text("Heap range: %08x - %08x (%u bytes)", bottomPtr, topPtr, topPtr - bottomPtr); - ImGui::Text("High-water mark: %08x", maxEnd); - ImGui::Text("Free list head: %08x Marker: %08x", headPtr, markerPtr); + if (!headPtr || !bottomPtr || !topPtr) { + ImGui::Text("Heap metadata at %08x contains invalid pointers - likely corrupted metadata.", metaAddr); + ImGui::End(); + return; + } + + ImGui::Text("Heap range: %08x - %08x (%u bytes)", bottomPtr ? *bottomPtr : 0, topPtr ? *topPtr : 0, bottomPtr && topPtr ? *topPtr - *bottomPtr : 0); + ImGui::Text("High-water mark: %08x", maxEnd ? *maxEnd : 0); + ImGui::Text("Free list head: %08x Marker: %08x", headPtr ? *headPtr : 0, markerPtr); ImGui::Separator(); auto result = walkHeap(memory); @@ -231,7 +243,7 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { } uint32_t totalAccountedFor = totalFree + totalAlloc; - uint32_t heapSize = topPtr > bottomPtr ? topPtr - bottomPtr : 0; + uint32_t heapSize = topPtr && bottomPtr ? *topPtr - *bottomPtr : 0; ImGui::Text("Allocated: %u bytes (%u blocks) Free: %u bytes (%u blocks)", totalAlloc, allocCount, totalFree, freeCount); @@ -257,8 +269,8 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { ImDrawList* drawList = ImGui::GetWindowDrawList(); for (auto& b : blocks) { - float x0 = barWidth * (float)(b.address - bottomPtr) / (float)heapSize; - float x1 = barWidth * (float)(b.address + b.size - bottomPtr) / (float)heapSize; + float x0 = barWidth * (float)(b.address - (bottomPtr ? *bottomPtr : 0)) / (float)heapSize; + float x1 = barWidth * (float)(b.address + b.size - (bottomPtr ? *bottomPtr : 0)) / (float)heapSize; // Clamp to bar bounds. if (x0 < 0) x0 = 0; if (x1 > barWidth) x1 = barWidth; @@ -274,7 +286,7 @@ void PCSX::Widgets::HeapViewer::draw(Memory* memory, const char* title) { // Tooltip on hover, jump to memory on click. if (ImGui::IsItemHovered()) { float mouseX = ImGui::GetMousePos().x - barPos.x; - uint32_t hoverAddr = bottomPtr + (uint32_t)((float)heapSize * mouseX / barWidth); + uint32_t hoverAddr = bottomPtr ? *bottomPtr + (uint32_t)((float)heapSize * mouseX / barWidth) : 0; for (auto& b : blocks) { if (hoverAddr >= b.address && hoverAddr < b.address + b.size) { ImGui::BeginTooltip(); From a29b026e9734ef50f4aab958f8db15271926e65e Mon Sep 17 00:00:00 2001 From: "Nicolas \"Pixel\" Noble" Date: Sat, 18 Apr 2026 20:35:52 -0700 Subject: [PATCH 4/7] Avoid macro collisions. --- src/mips/psyqo/src/alloc.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mips/psyqo/src/alloc.c b/src/mips/psyqo/src/alloc.c index 7af4fe6e3..3f7bf200e 100644 --- a/src/mips/psyqo/src/alloc.c +++ b/src/mips/psyqo/src/alloc.c @@ -128,16 +128,16 @@ static void print_block(const empty_block *block) { } } -static int check_subintegrity(const allocated_block *first, const allocated_block *top, size_t size_start, +static int check_subintegrity(const allocated_block *first, const allocated_block *top_block, size_t size_start, size_t hypothetical_size) { - if (first == top) { + if (first == top_block) { return 0; } - printf("Integrity check: checking sublist from %p to %p, size_start = %u, hypothetical_size: %u\n", first, top, + printf("Integrity check: checking sublist from %p to %p, size_start = %u, hypothetical_size: %u\n", first, top_block, size_start, hypothetical_size); const allocated_block *curr = first; size_t size = size_start; - while (curr < top) { + while (curr < top_block) { size += curr->size; printf("Integrity check: checking allocated block at %p (size: %u) - current total = %u\n", curr, curr->size, size); From 602f382c16888127033833fc94c1ce3b9b717605 Mon Sep 17 00:00:00 2001 From: "Nicolas \"Pixel\" Noble" Date: Sat, 18 Apr 2026 20:36:04 -0700 Subject: [PATCH 5/7] Better end-of-chain parsing. --- src/gui/widgets/heap_viewer.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/gui/widgets/heap_viewer.cc b/src/gui/widgets/heap_viewer.cc index 728162c68..faf32d3fa 100644 --- a/src/gui/widgets/heap_viewer.cc +++ b/src/gui/widgets/heap_viewer.cc @@ -84,7 +84,13 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory uint32_t prevAddr = 0; constexpr int maxFreeBlocks = 100000; - while (curr != markerPtr && curr != 0 && (int)freeBlocks.size() < maxFreeBlocks) { + while (curr != markerPtr && (int)freeBlocks.size() < maxFreeBlocks) { + // A null next-pointer is not a valid terminator; it means the link was smashed. + if (curr == 0) { + result.error = "Free list contains a null pointer (expected marker) - heap corruption."; + break; + } + // Free block must be within heap range. if (curr < bottomPtr || curr >= topPtr) { result.error = fmt::format("Free list entry at {:08x} is outside heap range [{:08x}, {:08x}).", curr, From d912d1d782f740477c57553b5d40c9c27cd86503 Mon Sep 17 00:00:00 2001 From: "Nicolas \"Pixel\" Noble" Date: Sat, 18 Apr 2026 20:49:31 -0700 Subject: [PATCH 6/7] Overflow protection. --- src/gui/widgets/heap_viewer.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/widgets/heap_viewer.cc b/src/gui/widgets/heap_viewer.cc index faf32d3fa..64bf842e6 100644 --- a/src/gui/widgets/heap_viewer.cc +++ b/src/gui/widgets/heap_viewer.cc @@ -119,8 +119,8 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory break; } - // Block must not extend past top. - if (curr + *size > *topPtr) { + // Block must not extend past top (overflow-safe form). + if (*size > (*topPtr - curr)) { result.error = fmt::format("Free block at {:08x} (size {}) extends past heap top {:08x}.", curr, *size, *topPtr); break; From 012cbe05a9860d99a8e6ea2b9a4808bc7b3bb454 Mon Sep 17 00:00:00 2001 From: "Nicolas \"Pixel\" Noble" Date: Sat, 18 Apr 2026 20:49:41 -0700 Subject: [PATCH 7/7] Early exit. --- src/gui/widgets/heap_viewer.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/gui/widgets/heap_viewer.cc b/src/gui/widgets/heap_viewer.cc index 64bf842e6..cfa7d3fe1 100644 --- a/src/gui/widgets/heap_viewer.cc +++ b/src/gui/widgets/heap_viewer.cc @@ -126,6 +126,11 @@ PCSX::Widgets::HeapViewer::WalkResult PCSX::Widgets::HeapViewer::walkHeap(Memory break; } + if (!next) { + result.error = fmt::format("Free block at {:08x} has unreadable next pointer.", curr); + break; + } + freeBlocks.push_back({curr, *size}); prevAddr = curr; curr = *next;