Files
ppsspp/Windows/Debugger/Debugger_Lists.cpp
Henrik RydgårdandClaude Opus 5 3f0c2da5a9 Debugger: surface source line info in six more places
Following on from the DWARF line table: the lookup was only reachable from
hle.backtrace, the breakpoint hit object and the ImGui disassembly status bar.
Now also in

- the ImDebugger call stack (new Source column),
- the Win32 call stack (new Source column),
- the Win32 disassembly status bar, matching the ImGui one,
- the ImDisasmView right-click menu, which showed a bare address as its heading
  and now leads with "mesh.zig:163 (08841f98)" when there's a line for it,
- breakpoint log lines - a log-only breakpoint's entire output is those lines,
  and "BKP PC=08841f98 mesh.zig:163" reads a great deal better than an address
  when you're scanning a few thousand of them,
- crash stack traces, via FormatStackTrace, which is what the crash screen and
  crash reporting both use.

That last one is where it earns its keep, and it needed the invalid-jump path to
produce a stack trace at all - it was the one exec exception that didn't. It's
also the one that most deserves it: the address it jumped to tells you nothing,
the callers tell you everything. Execution has already moved to the bad address
by the time it's noticed, so a walk from pc finds no function to start from;
WalkCurrentStack takes an explicit starting pc now, and falling back to ra
recovers the chain. Reproducing the original CrossCraft bug:

  CPU Jump: Invalid jump to ae870000 from PC ae870000(invalid) RA 08841f98
  MIPS call stack:
  rendering.mesh.Mesh(PspVertex).draw at mesh.zig:163 (08841c30+368, ...)
  state.MenuState.draw at MenuState.zig:821 (0883ab90+414, ...)
  engine.Engine.stepFrameInternal at State.zig:40 (08820f74+5164, ...)
  utils.module._module_main_thread at engine.zig:468 (088272c4+2fb8, ...)

Fixed a pre-existing double-report while in there: every case in
Core_ExecException sent its message and then fell through to an unconditional
send of the same message, so each exec exception was logged twice. The message
is built in the switch and sent once at the end now.

pspautotests 314/314, UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 13:42:14 +02:00

1062 lines
27 KiB
C++

#include "Windows/Debugger/Debugger_Lists.h"
#include "Common/CommonWindows.h"
#include <windowsx.h>
#include <commctrl.h>
#include "Windows/Debugger/BreakpointWindow.h"
#include "Windows/Debugger/CtrlDisAsmView.h"
#include "Windows/Debugger/DebuggerShared.h"
#include "Windows/Debugger/WatchItemWindow.h"
#include "Windows/W32Util/ContextMenu.h"
#include "Windows/MainWindow.h"
#include "Windows/resource.h"
#include "Windows/main.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Core/Core.h"
#include "Core/Debugger/LineInfo.h"
#include "Core/MemMap.h"
#include "Core/HLE/sceKernelThread.h"
enum { TL_NAME, TL_PROGRAMCOUNTER, TL_ENTRYPOINT, TL_PRIORITY, TL_STATE, TL_WAITTYPE, TL_COLUMNCOUNT };
enum { BPL_ENABLED, BPL_TYPE, BPL_OFFSET, BPL_SIZELABEL, BPL_OPCODE, BPL_CONDITION, BPL_HITS, BPL_COLUMNCOUNT };
enum { SF_ENTRY, SF_ENTRYNAME, SF_CURPC, SF_CUROPCODE, SF_CURSP, SF_FRAMESIZE, SF_SOURCE, SF_COLUMNCOUNT };
enum { ML_NAME, ML_ADDRESS, ML_SIZE, ML_ACTIVE, ML_COLUMNCOUNT };
enum { WL_NAME, WL_EXPRESSION, WL_VALUE, WL_COLUMNCOUNT };
GenericListViewColumn threadColumns[TL_COLUMNCOUNT] = {
{ L"Name", 0.20f },
{ L"PC", 0.15f },
{ L"Entry Point", 0.15f },
{ L"Priority", 0.15f },
{ L"State", 0.15f },
{ L"Wait type", 0.20f }
};
GenericListViewDef threadListDef = {
threadColumns, ARRAY_SIZE(threadColumns), NULL, false
};
GenericListViewColumn breakpointColumns[BPL_COLUMNCOUNT] = {
{ L"", 0.03f }, // enabled
{ L"Type", 0.15f },
{ L"Offset", 0.12f },
{ L"Size/Label", 0.20f },
{ L"Opcode", 0.28f },
{ L"Condition", 0.17f },
{ L"Hits", 0.05f },
};
GenericListViewDef breakpointListDef = {
breakpointColumns, ARRAY_SIZE(breakpointColumns), NULL, true
};
GenericListViewColumn stackTraceColumns[SF_COLUMNCOUNT] = {
{ L"Entry", 0.12f },
{ L"Name", 0.24f },
{ L"PC", 0.12f },
{ L"Opcode", 0.28f },
{ L"SP", 0.10f },
{ L"Frame Size", 0.10f },
{ L"Source", 0.16f }
};
GenericListViewDef stackTraceListDef = {
stackTraceColumns, ARRAY_SIZE(stackTraceColumns), NULL, false
};
GenericListViewColumn moduleListColumns[ML_COLUMNCOUNT] = {
{ L"Name", 0.25f },
{ L"Address", 0.25f },
{ L"Size", 0.25f },
{ L"Active", 0.25f },
};
GenericListViewDef moduleListDef = {
moduleListColumns, ARRAY_SIZE(moduleListColumns), NULL, false
};
GenericListViewColumn watchListColumns[WL_COLUMNCOUNT] = {
{ L"Name", 0.25f },
{ L"Expression", 0.5f },
{ L"Value", 0.25f },
};
GenericListViewDef watchListDef = {
watchListColumns, ARRAY_SIZE(watchListColumns), nullptr, false,
};
//
// CtrlThreadList
//
CtrlThreadList::CtrlThreadList(HWND hwnd): GenericListControl(hwnd,threadListDef)
{
Update();
}
bool CtrlThreadList::WindowMessage(UINT msg, WPARAM wParam, LPARAM lParam, LRESULT& returnValue)
{
switch (msg)
{
case WM_KEYDOWN:
if (wParam == VK_TAB)
{
SendMessage(GetParent(GetHandle()),WM_DEB_TABPRESSED,0,0);
returnValue = 0;
return true;
}
break;
case WM_GETDLGCODE:
if (lParam && ((MSG*)lParam)->message == WM_KEYDOWN)
{
if (wParam == VK_TAB)
{
returnValue = DLGC_WANTMESSAGE;
return true;
}
}
break;
}
return false;
}
void CtrlThreadList::showMenu(int itemIndex, const POINT &pt)
{
auto threadInfo = threads[itemIndex];
// Can't do it, sorry. Needs to not be running.
if (Core_IsActive())
return;
HMENU subMenu = GetContextMenu(ContextMenuID::THREADLIST);
switch (threadInfo.status) {
case THREADSTATUS_DEAD:
case THREADSTATUS_DORMANT:
case THREADSTATUS_RUNNING:
EnableMenuItem(subMenu, ID_DISASM_THREAD_FORCERUN, MF_BYCOMMAND | MF_DISABLED);
EnableMenuItem(subMenu, ID_DISASM_THREAD_KILL, MF_BYCOMMAND | MF_DISABLED);
break;
case THREADSTATUS_READY:
EnableMenuItem(subMenu, ID_DISASM_THREAD_FORCERUN, MF_BYCOMMAND | MF_DISABLED);
EnableMenuItem(subMenu, ID_DISASM_THREAD_KILL, MF_BYCOMMAND | MF_ENABLED);
break;
case THREADSTATUS_SUSPEND:
case THREADSTATUS_WAIT:
case THREADSTATUS_WAITSUSPEND:
default:
EnableMenuItem(subMenu, ID_DISASM_THREAD_FORCERUN, MF_BYCOMMAND | MF_ENABLED);
EnableMenuItem(subMenu, ID_DISASM_THREAD_KILL, MF_BYCOMMAND | MF_ENABLED);
break;
}
switch (TriggerContextMenu(ContextMenuID::THREADLIST, GetHandle(), ContextPoint::FromClient(pt)))
{
case ID_DISASM_THREAD_FORCERUN:
{
// Route the actual thread-state mutation to the CPU thread instead of poking at it
// directly from this GUI thread - see Core_RunOnCPUThread() in Core.h.
SceUID threadID = threadInfo.id;
Core_RunOnCPUThread([&] { __KernelResumeThreadFromWait(threadID, 0); });
}
reloadThreads();
break;
case ID_DISASM_THREAD_KILL:
{
SceUID threadID = threadInfo.id;
Core_RunOnCPUThread([&] { sceKernelTerminateThread(threadID); });
}
reloadThreads();
break;
}
}
void CtrlThreadList::GetColumnText(wchar_t* dest, size_t destSize, int row, int col)
{
if (row < 0 || row >= (int)threads.size()) {
return;
}
switch (col)
{
case TL_NAME:
wcscpy(dest, ConvertUTF8ToWString(threads[row].name).c_str());
break;
case TL_PROGRAMCOUNTER:
switch (threads[row].status)
{
case THREADSTATUS_DORMANT:
case THREADSTATUS_DEAD:
wcscpy(dest, L"N/A");
break;
default:
wsprintf(dest, L"0x%08X",threads[row].curPC);
break;
};
break;
case TL_ENTRYPOINT:
wsprintf(dest,L"0x%08X",threads[row].entrypoint);
break;
case TL_PRIORITY:
wsprintf(dest,L"%d",threads[row].priority);
break;
case TL_STATE:
switch (threads[row].status)
{
case THREADSTATUS_RUNNING:
wcscpy(dest,L"Running");
break;
case THREADSTATUS_READY:
wcscpy(dest,L"Ready");
break;
case THREADSTATUS_WAIT:
wcscpy(dest,L"Waiting");
break;
case THREADSTATUS_SUSPEND:
wcscpy(dest,L"Suspended");
break;
case THREADSTATUS_DORMANT:
wcscpy(dest,L"Dormant");
break;
case THREADSTATUS_DEAD:
wcscpy(dest,L"Dead");
break;
case THREADSTATUS_WAITSUSPEND:
wcscpy(dest,L"Waiting/Suspended");
break;
default:
wcscpy(dest,L"Invalid");
break;
}
break;
case TL_WAITTYPE:
wcscpy(dest, ConvertUTF8ToWString(WaitTypeToString(threads[row].waitType)).c_str());
break;
}
}
void CtrlThreadList::OnDoubleClick(int itemIndex, int column)
{
u32 address;
switch (threads[itemIndex].status)
{
case THREADSTATUS_DORMANT:
case THREADSTATUS_DEAD:
address = threads[itemIndex].entrypoint;
break;
default:
address = threads[itemIndex].curPC;
break;
}
SendMessage(GetParent(GetHandle()),WM_DEB_GOTOWPARAM,address,0);
}
void CtrlThreadList::OnRightClick(int itemIndex, int column, const POINT& point)
{
showMenu(itemIndex,point);
}
void CtrlThreadList::reloadThreads()
{
// Reading live kernel thread state here on the GUI thread would otherwise race with the CPU
// thread - hold g_frameMutex for the duration of the read, which NativeFrame() also holds
// while it's actually touching that state. See g_frameMutex in Core.h.
{
std::lock_guard<std::mutex> frameGuard(g_frameMutex);
CoreShutdownLock coreLock = Core_LockAgainstShutdown();
threads = GetThreadsInfo();
}
Update();
}
const char* CtrlThreadList::getCurrentThreadName()
{
for (size_t i = 0; i < threads.size(); i++)
{
if (threads[i].isCurrent) return threads[i].name;
}
return "N/A";
}
//
// CtrlBreakpointList
//
CtrlBreakpointList::CtrlBreakpointList(HWND hwnd, MIPSDebugInterface* cpu, CtrlDisAsmView* disasm)
: GenericListControl(hwnd,breakpointListDef),cpu(cpu),disasm(disasm)
{
SetSendInvalidRows(true);
Update();
}
bool CtrlBreakpointList::WindowMessage(UINT msg, WPARAM wParam, LPARAM lParam, LRESULT& returnValue)
{
switch(msg)
{
case WM_KEYDOWN:
returnValue = 0;
if(wParam == VK_RETURN)
{
int index = GetSelectedIndex();
editBreakpoint(index);
return true;
} else if (wParam == VK_DELETE)
{
int index = GetSelectedIndex();
removeBreakpoint(index);
return true;
} else if (wParam == VK_TAB)
{
SendMessage(GetParent(GetHandle()),WM_DEB_TABPRESSED,0,0);
return true;
} else if (wParam == VK_SPACE)
{
int index = GetSelectedIndex();
toggleEnabled(index);
return true;
}
break;
case WM_GETDLGCODE:
if (lParam && ((MSG*)lParam)->message == WM_KEYDOWN)
{
if (wParam == VK_TAB || wParam == VK_RETURN)
{
returnValue = DLGC_WANTMESSAGE;
return true;
}
}
break;
}
return false;
}
void CtrlBreakpointList::reloadBreakpoints()
{
// Update the items we're displaying from the debugger. g_frameMutex guards this against races
// with the CPU thread - see g_frameMutex in Core.h.
{
std::lock_guard<std::mutex> frameGuard(g_frameMutex);
CoreShutdownLock coreLock = Core_LockAgainstShutdown();
displayedBreakPoints_ = g_breakpoints.GetBreakpoints();
displayedMemChecks_= g_breakpoints.GetMemChecks();
}
for (int i = 0; i < GetRowCount(); i++)
{
bool isMemory;
int index = getBreakpointIndex(i, isMemory);
if (index < 0)
continue;
if (isMemory)
SetCheckState(i, displayedMemChecks_[index].IsEnabled());
else
SetCheckState(i, displayedBreakPoints_[index].IsEnabled());
}
Update();
}
void CtrlBreakpointList::editBreakpoint(int itemIndex)
{
bool isMemory;
int index = getBreakpointIndex(itemIndex, isMemory);
if (index == -1) return;
// Route the breakpoint mutation to the CPU thread instead of poking at it directly from this
// GUI thread - see Core_RunOnCPUThread() in Core.h. win.exec() is a modal dialog, so it must
// stay outside any queued callback, or we'd block the CPU thread on user input.
BreakpointWindow win(GetHandle(),cpu);
if (isMemory)
{
auto mem = displayedMemChecks_[index];
win.loadFromMemcheck(mem);
if (win.exec())
{
Core_RunOnCPUThread([&] {
g_breakpoints.RemoveMemCheck(mem.start,mem.end);
win.addBreakpoint();
});
}
} else {
auto bp = displayedBreakPoints_[index];
win.loadFromBreakpoint(bp);
if (win.exec())
{
Core_RunOnCPUThread([&] {
g_breakpoints.RemoveBreakPoint(bp.addr);
win.addBreakpoint();
});
}
}
}
void CtrlBreakpointList::toggleEnabled(int itemIndex)
{
bool isMemory;
int index = getBreakpointIndex(itemIndex, isMemory);
if (index == -1) return;
// Route the breakpoint mutation to the CPU thread instead of poking at it directly from this
// GUI thread - see Core_RunOnCPUThread() in Core.h.
if (isMemory) {
MemCheck mcPrev = displayedMemChecks_[index];
Core_RunOnCPUThread([&] { g_breakpoints.ChangeMemCheck(mcPrev.start, mcPrev.end, mcPrev.cond, BreakAction(mcPrev.action ^ BREAK_ACTION_PAUSE)); });
} else {
BreakPoint bpPrev = displayedBreakPoints_[index];
Core_RunOnCPUThread([&] { g_breakpoints.ChangeBreakPoint(bpPrev.addr, BreakAction(bpPrev.action ^ BREAK_ACTION_PAUSE)); });
}
}
void CtrlBreakpointList::gotoBreakpointAddress(int itemIndex)
{
bool isMemory;
int index = getBreakpointIndex(itemIndex, isMemory);
if (index == -1)
return;
if (isMemory) {
u32 address = displayedMemChecks_[index].start;
MainWindow::CreateMemoryWindow();
if (memoryWindow)
memoryWindow->Goto(address);
} else {
u32 address = displayedBreakPoints_[index].addr;
MainWindow::CreateDisasmWindow();
if (disasmWindow)
disasmWindow->Goto(address);
}
}
void CtrlBreakpointList::removeBreakpoint(int itemIndex)
{
bool isMemory;
int index = getBreakpointIndex(itemIndex,isMemory);
if (index == -1) return;
// Route the breakpoint mutation to the CPU thread instead of poking at it directly from this
// GUI thread - see Core_RunOnCPUThread() in Core.h.
if (isMemory) {
auto mc = displayedMemChecks_[index];
Core_RunOnCPUThread([&] { g_breakpoints.RemoveMemCheck(mc.start, mc.end); });
} else {
u32 address = displayedBreakPoints_[index].addr;
Core_RunOnCPUThread([&] { g_breakpoints.RemoveBreakPoint(address); });
}
}
int CtrlBreakpointList::getTotalBreakpointCount() {
// displayedBreakPoints_ never contains the internal step-over/run-until breakpoint.
return (int)displayedMemChecks_.size() + (int)displayedBreakPoints_.size();
}
int CtrlBreakpointList::getBreakpointIndex(int itemIndex, bool& isMemory)
{
// memory breakpoints first
if (itemIndex < (int)displayedMemChecks_.size())
{
isMemory = true;
return itemIndex;
}
itemIndex -= (int)displayedMemChecks_.size();
size_t i = 0;
while (i < displayedBreakPoints_.size())
{
// the index is 0 when there are no more breakpoints to skip
if (itemIndex == 0)
{
isMemory = false;
return (int)i;
}
i++;
itemIndex--;
}
return -1;
}
void CtrlBreakpointList::GetColumnText(wchar_t* dest, size_t destSize, int row, int col)
{
if (!PSP_IsInited()) {
return;
}
bool isMemory;
int index = getBreakpointIndex(row,isMemory);
if (index == -1) return;
switch (col)
{
case BPL_TYPE:
{
if (isMemory) {
switch ((int)displayedMemChecks_[index].cond) {
case MEMCHECK_READ:
wcscpy(dest,L"Read");
break;
case MEMCHECK_WRITE:
wcscpy(dest,L"Write");
break;
case MEMCHECK_READWRITE:
wcscpy(dest,L"Read/Write");
break;
case MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE:
wcscpy(dest,L"Write Change");
break;
case MEMCHECK_READWRITE | MEMCHECK_WRITE_ONCHANGE:
wcscpy(dest,L"Read/Write Change");
break;
}
} else {
wcscpy(dest, L"Execute");
}
}
break;
case BPL_OFFSET:
{
if (isMemory) {
wsprintf(dest,L"0x%08X",displayedMemChecks_[index].start);
} else {
wsprintf(dest,L"0x%08X",displayedBreakPoints_[index].addr);
}
}
break;
case BPL_SIZELABEL:
{
if (isMemory) {
auto mc = displayedMemChecks_[index];
if (mc.end == 0)
wsprintf(dest,L"0x%08X",1);
else
wsprintf(dest,L"0x%08X",mc.end-mc.start);
} else {
const std::string sym = g_symbolMap->GetLabelString(displayedBreakPoints_[index].addr);
if (!sym.empty()) {
ConvertUTF8ToWString(dest, destSize, sym);
} else {
wcscpy(dest,L"-");
}
}
}
break;
case BPL_OPCODE:
{
if (isMemory) {
wcscpy(dest,L"-");
} else {
char temp[256];
disasm->getOpcodeText(displayedBreakPoints_[index].addr, temp, sizeof(temp));
ConvertUTF8ToWString(dest, destSize, temp);
}
}
break;
case BPL_CONDITION:
{
if (isMemory || displayedBreakPoints_[index].hasCond == false) {
wcscpy(dest,L"-");
} else {
std::wstring s = ConvertUTF8ToWString(displayedBreakPoints_[index].cond.expressionString);
wcscpy(dest,s.c_str());
}
}
break;
case BPL_HITS:
{
if (isMemory) {
wsprintf(dest,L"%d",displayedMemChecks_[index].numHits);
} else {
wsprintf(dest,L"-");
}
}
break;
case BPL_ENABLED:
{
wsprintf(dest,L"\xFFFE");
}
break;
}
}
void CtrlBreakpointList::OnDoubleClick(int itemIndex, int column)
{
gotoBreakpointAddress(itemIndex);
}
void CtrlBreakpointList::OnRightClick(int itemIndex, int column, const POINT& point)
{
showBreakpointMenu(itemIndex,point);
}
void CtrlBreakpointList::OnToggle(int item, bool newValue)
{
toggleEnabled(item);
}
void CtrlBreakpointList::showBreakpointMenu(int itemIndex, const POINT &pt)
{
bool isMemory;
int index = getBreakpointIndex(itemIndex, isMemory);
if (index == -1)
{
switch (TriggerContextMenu(ContextMenuID::NEWBREAKPOINT, GetHandle(), ContextPoint::FromClient(pt)))
{
case ID_DISASM_ADDNEWBREAKPOINT:
{
// bpw.exec() is a modal dialog - must stay outside any queued callback, or we'd
// block the CPU thread on user input - see Core_RunOnCPUThread() in Core.h.
BreakpointWindow bpw(GetHandle(),cpu);
if (bpw.exec()) {
Core_RunOnCPUThread([&] { bpw.addBreakpoint(); });
}
}
break;
}
} else {
MemCheck mcPrev;
BreakPoint bpPrev;
if (isMemory) {
mcPrev = displayedMemChecks_[index];
} else {
bpPrev = displayedBreakPoints_[index];
}
HMENU subMenu = GetContextMenu(ContextMenuID::BREAKPOINTLIST);
if (isMemory) {
CheckMenuItem(subMenu, ID_DISASM_DISABLEBREAKPOINT, MF_BYCOMMAND | (mcPrev.IsEnabled() ? MF_CHECKED : MF_UNCHECKED));
} else {
CheckMenuItem(subMenu, ID_DISASM_DISABLEBREAKPOINT, MF_BYCOMMAND | (bpPrev.IsEnabled() ? MF_CHECKED : MF_UNCHECKED));
}
switch (TriggerContextMenu(ContextMenuID::BREAKPOINTLIST, GetHandle(), ContextPoint::FromClient(pt)))
{
case ID_DISASM_DISABLEBREAKPOINT:
// Route the breakpoint mutation to the CPU thread instead of poking at it directly
// from this GUI thread - see Core_RunOnCPUThread() in Core.h.
if (isMemory) {
Core_RunOnCPUThread([&] { g_breakpoints.ChangeMemCheck(mcPrev.start, mcPrev.end, mcPrev.cond, BreakAction(mcPrev.action ^ BREAK_ACTION_PAUSE)); });
} else {
Core_RunOnCPUThread([&] { g_breakpoints.ChangeBreakPoint(bpPrev.addr, BreakAction(bpPrev.action ^ BREAK_ACTION_PAUSE)); });
}
break;
case ID_DISASM_EDITBREAKPOINT:
editBreakpoint(itemIndex);
break;
case ID_DISASM_ADDNEWBREAKPOINT:
{
// bpw.exec() is a modal dialog - must stay outside any queued callback, or we'd
// block the CPU thread on user input - see Core_RunOnCPUThread() in Core.h.
BreakpointWindow bpw(GetHandle(),cpu);
if (bpw.exec()) {
Core_RunOnCPUThread([&] { bpw.addBreakpoint(); });
}
}
break;
case ID_DISASM_DELETEBREAKPOINT:
removeBreakpoint(itemIndex);
break;
}
}
}
//
// CtrlStackTraceView
//
CtrlStackTraceView::CtrlStackTraceView(HWND hwnd, DebugInterface* cpu, CtrlDisAsmView* disasm)
: GenericListControl(hwnd,stackTraceListDef),cpu(cpu),disasm(disasm)
{
Update();
}
bool CtrlStackTraceView::WindowMessage(UINT msg, WPARAM wParam, LPARAM lParam, LRESULT& returnValue)
{
switch(msg)
{
case WM_KEYDOWN:
if (wParam == VK_TAB)
{
returnValue = 0;
SendMessage(GetParent(GetHandle()),WM_DEB_TABPRESSED,0,0);
return true;
}
break;
case WM_GETDLGCODE:
if (lParam && ((MSG*)lParam)->message == WM_KEYDOWN)
{
if (wParam == VK_TAB || wParam == VK_RETURN)
{
returnValue = DLGC_WANTMESSAGE;
return true;
}
}
break;
}
return false;
}
void CtrlStackTraceView::GetColumnText(wchar_t* dest, size_t destSize, int row, int col)
{
// We should have emptied the list if g_symbolMap is nullptr, but apparently we don't,
// so let's have a sanity check here.
if (row < 0 || row >= (int)frames.size() || !g_symbolMap) {
return;
}
switch (col)
{
case SF_ENTRY:
wsprintf(dest,L"%08X",frames[row].entry);
break;
case SF_ENTRYNAME:
{
const std::string sym = g_symbolMap->GetLabelString(frames[row].entry);
if (!sym.empty()) {
wcscpy(dest, ConvertUTF8ToWString(sym).c_str());
} else {
wcscpy(dest,L"-");
}
}
break;
case SF_CURPC:
wsprintf(dest,L"%08X",frames[row].pc);
break;
case SF_CUROPCODE:
{
char temp[512];
disasm->getOpcodeText(frames[row].pc, temp, sizeof(temp));
wcscpy(dest, ConvertUTF8ToWString(temp).c_str());
}
break;
case SF_CURSP:
wsprintf(dest,L"%08X",frames[row].sp);
break;
case SF_FRAMESIZE:
wsprintf(dest,L"%08X",frames[row].stackSize);
break;
case SF_SOURCE:
{
// Empty unless the game shipped an unstripped ELF - see Core/Debugger/LineInfo.h.
const std::string source = g_lineInfo.LookupString(frames[row].pc);
wcscpy(dest, source.empty() ? L"-" : ConvertUTF8ToWString(source).c_str());
}
break;
}
}
void CtrlStackTraceView::OnDoubleClick(int itemIndex, int column)
{
SendMessage(GetParent(GetHandle()),WM_DEB_GOTOWPARAM,frames[itemIndex].pc,0);
}
void CtrlStackTraceView::loadStackTrace() {
// Reading live thread/register/stack state here on the GUI thread would otherwise race with
// the CPU thread - hold g_frameMutex for the duration of the read, which NativeFrame() also
// holds while it's actually touching that state. See g_frameMutex in Core.h.
//
// g_frameMutex first, then Core_LockAgainstShutdown() - never the other way around. See CtrlMemView::onPaint.
std::lock_guard<std::mutex> frameGuard(g_frameMutex);
CoreShutdownLock coreLock = Core_LockAgainstShutdown();
if (!PSP_IsInited())
return;
std::vector<DebugThreadInfo> threads = GetThreadsInfo();
u32 entry = 0, stackTop = 0;
for (size_t i = 0; i < threads.size(); i++)
{
if (threads[i].isCurrent)
{
entry = threads[i].entrypoint;
stackTop = threads[i].initialStack;
break;
}
}
if (entry != 0) {
frames = MIPSStackWalk::Walk(cpu->GetPC(),cpu->GetRegValue(0,31),cpu->GetRegValue(0,29),entry,stackTop);
} else {
frames.clear();
}
Update();
}
//
// CtrlModuleList
//
CtrlModuleList::CtrlModuleList(HWND hwnd, DebugInterface* cpu)
: GenericListControl(hwnd,moduleListDef),cpu(cpu)
{
Update();
}
bool CtrlModuleList::WindowMessage(UINT msg, WPARAM wParam, LPARAM lParam, LRESULT& returnValue)
{
switch(msg)
{
case WM_KEYDOWN:
if (wParam == VK_TAB)
{
returnValue = 0;
SendMessage(GetParent(GetHandle()),WM_DEB_TABPRESSED,0,0);
return true;
}
break;
case WM_GETDLGCODE:
if (lParam && ((MSG*)lParam)->message == WM_KEYDOWN)
{
if (wParam == VK_TAB || wParam == VK_RETURN)
{
returnValue = DLGC_WANTMESSAGE;
return true;
}
}
break;
}
return false;
}
void CtrlModuleList::GetColumnText(wchar_t* dest, size_t destSize, int row, int col)
{
if (row < 0 || row >= (int)modules.size()) {
return;
}
switch (col) {
case ML_NAME:
ConvertUTF8ToWString(dest, destSize, modules[row].name);
break;
case ML_ADDRESS:
wsprintf(dest,L"%08X",modules[row].address);
break;
case ML_SIZE:
wsprintf(dest,L"%08X",modules[row].size);
break;
case ML_ACTIVE:
wcscpy(dest,modules[row].active ? L"true" : L"false");
break;
}
}
void CtrlModuleList::OnDoubleClick(int itemIndex, int column)
{
SendMessage(GetParent(GetHandle()),WM_DEB_GOTOWPARAM,modules[itemIndex].address,0);
}
void CtrlModuleList::loadModules()
{
// Reading the live symbol map here on the GUI thread would otherwise race with the CPU thread -
// hold g_frameMutex for the duration of the read, which NativeFrame() also holds while it's
// actually touching that state. See g_frameMutex in Core.h.
{
std::lock_guard<std::mutex> frameGuard(g_frameMutex);
CoreShutdownLock coreLock = Core_LockAgainstShutdown();
if (g_symbolMap) {
modules = g_symbolMap->getAllModules();
} else {
modules.clear();
}
}
Update();
}
// In case you modify things in the memory view.
static constexpr UINT_PTR IDT_CHECK_REFRESH = 0xC0DE0044;
CtrlWatchList::CtrlWatchList(HWND hwnd, DebugInterface *cpu)
: GenericListControl(hwnd, watchListDef), cpu_(cpu) {
SetSendInvalidRows(true);
Update();
SetTimer(GetHandle(), IDT_CHECK_REFRESH, 1000U, nullptr);
}
void CtrlWatchList::RefreshValues() {
// Evaluating watch expressions reads live registers/memory here on the GUI thread, which would
// otherwise race with the CPU thread - hold g_frameMutex for the duration, which NativeFrame()
// also holds while it's actually touching that state. See g_frameMutex in Core.h.
std::lock_guard<std::mutex> frameGuard(g_frameMutex);
CoreShutdownLock coreLock = Core_LockAgainstShutdown();
int steppingCounter = Core_GetSteppingCounter();
int changes = false;
for (auto &watch : watches_) {
if (watch.steppingCounter != steppingCounter) {
watch.lastValue = watch.currentValue;
watch.steppingCounter = steppingCounter;
changes = true;
}
uint32_t prevValue = watch.currentValue;
watch.evaluateFailed = !parseExpression(cpu_, watch.expression, watch.currentValue);
if (prevValue != watch.currentValue)
changes = true;
}
if (changes)
Update();
}
bool CtrlWatchList::WindowMessage(UINT msg, WPARAM wParam, LPARAM lParam, LRESULT &returnValue) {
switch (msg) {
case WM_KEYDOWN:
switch (wParam) {
case VK_TAB:
returnValue = 0;
SendMessage(GetParent(GetHandle()), WM_DEB_TABPRESSED, 0, 0);
return true;
case VK_RETURN:
returnValue = 0;
EditWatch(GetSelectedIndex());
return true;
case VK_DELETE:
returnValue = 0;
DeleteWatch(GetSelectedIndex());
return true;
default:
break;
}
break;
case WM_GETDLGCODE:
if (lParam && ((MSG *)lParam)->message == WM_KEYDOWN) {
if (wParam == VK_TAB || wParam == VK_RETURN || wParam == VK_DELETE) {
returnValue = DLGC_WANTMESSAGE;
return true;
}
}
break;
case WM_TIMER:
if (wParam == IDT_CHECK_REFRESH) {
RefreshValues();
return true;
}
break;
}
return false;
}
void CtrlWatchList::GetColumnText(wchar_t *dest, size_t destSize, int row, int col) {
const auto &watch = watches_[row];
switch (col) {
case WL_NAME:
wcsncpy(dest, ConvertUTF8ToWString(watch.name).c_str(), 255);
dest[255] = 0;
break;
case WL_EXPRESSION:
wcsncpy(dest, ConvertUTF8ToWString(watch.originalExpression).c_str(), 255);
dest[255] = 0;
break;
case WL_VALUE:
if (watch.evaluateFailed) {
wcscpy(dest, L"(failed to evaluate)");
} else {
const uint32_t &value = watch.currentValue;
float valuef = 0.0f;
switch (watch.format) {
case WatchFormat::HEX:
wsprintf(dest, L"0x%08X", value);
break;
case WatchFormat::INT:
wsprintf(dest, L"%d", (int32_t)value);
break;
case WatchFormat::FLOAT:
memcpy(&valuef, &value, sizeof(valuef));
swprintf_s(dest, destSize, L"%f", valuef);
break;
case WatchFormat::STR:
if (Memory::IsValidAddress(value)) {
uint32_t len = Memory::ClampValidSizeAt(value, 255);
swprintf_s(dest, destSize, L"%.*S", len, Memory::GetCharPointer(value));
} else {
wsprintf(dest, L"(0x%08X)", value);
}
break;
}
}
break;
}
}
void CtrlWatchList::OnRightClick(int itemIndex, int column, const POINT &pt) {
if (itemIndex == -1) {
switch (TriggerContextMenu(ContextMenuID::CPUADDWATCH, GetHandle(), ContextPoint::FromClient(pt))) {
case ID_DISASM_ADDNEWBREAKPOINT:
AddWatch();
break;
}
} else {
switch (TriggerContextMenu(ContextMenuID::CPUWATCHLIST, GetHandle(), ContextPoint::FromClient(pt))) {
case ID_DISASM_EDITBREAKPOINT:
EditWatch(itemIndex);
break;
case ID_DISASM_DELETEBREAKPOINT:
DeleteWatch(itemIndex);
break;
case ID_DISASM_ADDNEWBREAKPOINT:
AddWatch();
break;
}
}
}
bool CtrlWatchList::OnRowPrePaint(int row, LPNMLVCUSTOMDRAW msg) {
if (row >= 0 && HasWatchChanged(row)) {
msg->clrText = RGB(255, 0, 0);
return true;
}
return false;
}
void CtrlWatchList::AddWatch() {
WatchItemWindow win(nullptr, GetHandle(), cpu_);
if (win.Exec()) {
WatchInfo info;
if (initExpression(cpu_, win.GetExpression().c_str(), info.expression)) {
info.name = win.GetName();
info.originalExpression = win.GetExpression();
info.format = win.GetFormat();
watches_.push_back(info);
RefreshValues();
} else {
char errorMessage[512];
snprintf(errorMessage, sizeof(errorMessage), "Invalid expression \"%s\": %s", win.GetExpression().c_str(), getExpressionError());
MessageBoxA(GetHandle(), errorMessage, "Error", MB_OK);
}
}
}
void CtrlWatchList::EditWatch(int pos) {
auto &watch = watches_[pos];
WatchItemWindow win(nullptr, GetHandle(), cpu_);
win.Init(watch.name, watch.originalExpression, watch.format);
if (win.Exec()) {
if (initExpression(cpu_, win.GetExpression().c_str(), watch.expression)) {
watch.name = win.GetName();
watch.originalExpression = win.GetExpression();
watch.format = win.GetFormat();
RefreshValues();
} else {
char errorMessage[512];
snprintf(errorMessage, sizeof(errorMessage), "Invalid expression \"%s\": %s", win.GetExpression().c_str(), getExpressionError());
MessageBoxA(GetHandle(), errorMessage, "Error", MB_OK);
}
}
}
void CtrlWatchList::DeleteWatch(int pos) {
watches_.erase(watches_.begin() + pos);
Update();
}
bool CtrlWatchList::HasWatchChanged(int pos) {
return watches_[pos].lastValue != watches_[pos].currentValue;
}