mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-01 18:25:19 +02:00
Reported hang: the CPU thread held g_frameMutex (NativeFrame) and blocked on g_shutdownLock inside a queued memory.read, while the GUI thread held g_shutdownLock (CtrlMemView::onPaint) and blocked on g_frameMutex. Textbook ABBA. The CPU thread's order is structural - NativeFrame wraps everything below it in g_frameMutex, and both Core_ProcessCPUQueue() and runImDebugger() -> DisassembleRange() lock memory from under there - so the GUI side is the one that has to match. Swaps the three handlers that had it backwards (CtrlMemView::onPaint, CtrlDisAsmView::onPaint, CtrlStackTraceView:: loadStackTrace) to take g_frameMutex first. They already took both locks, so this is ordering only, and g_shutdownLock is recursive so nesting is fine. Also drops the Memory::MemoryInitedLock from the WebSocket LockMemory(), which is what made the CPU thread want that lock in the first place. It was guarding against another thread tearing down the memory system, but that doesn't happen: Memory::Shutdown() is only reached via CPU_Shutdown() <- PSP_Shutdown(), whose callers all run on the CPU thread, and Memory::Reinit() runs from Memory::DoState() on savestate load, likewise. WebSocket.cpp additionally holds lifecycleLock across the whole handler and takes it on STOPPING. Note this second part isn't sufficient on its own - ImMemView's copy-disassembly path also locks memory from inside the frame span - which is why the ordering fix is the real one. Not removing Memory::Lock() from the Win32 paint handlers: teardown isn't fully inside the g_frameMutex span yet. EmuScreen::render()'s PSP_Shutdown() is, but the ones in EmuScreen::sendMessage() (game reset, loading a new game) run from g_screenManager->sendMessage(), above where NativeFrame takes the guard. Closing that is the prerequisite, and is left for later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
1048 lines
26 KiB
C++
1048 lines
26 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/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_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.12f },
|
|
{ L"Frame Size", 0.12f }
|
|
};
|
|
|
|
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);
|
|
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);
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 Memory::Lock() - never the other way around. See CtrlMemView::onPaint.
|
|
std::lock_guard<std::mutex> frameGuard(g_frameMutex);
|
|
Memory::MemoryInitedLock memLock = Memory::Lock();
|
|
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);
|
|
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);
|
|
|
|
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;
|
|
}
|