From 2cfbbc001df2c020cb705a3420595adea0c434fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 19 Oct 2026 10:56:05 +0200 Subject: [PATCH] ImDebugger: Symbol deletion, select in symbol list, callstack thread choice - Symbol list: "Delete symbol" in the context menu, which leaves the code without a function (useful to test stack walks with missing symbols). - Disassembly context menu: "Select in symbol list" selects the function in the list without moving the disassembly, clearing a filter that hides it. - Callstacks: a thread selector (following the current thread by default, with a Current button), selectable frames, double-click to show the frame in the disassembly, and a context menu to show or copy frames. - Threads: "Show callstack", "Go to PC" and "Go to entry" in the context menu. Co-Authored-By: Claude Opus 5.5 (1M context) --- UI/ImDebugger/ImDebugger.cpp | 130 ++++++++++++++++++++++++++++++--- UI/ImDebugger/ImDebugger.h | 3 + UI/ImDebugger/ImDisasmView.cpp | 41 +++++++++++ UI/ImDebugger/ImDisasmView.h | 12 +++ 4 files changed, 175 insertions(+), 11 deletions(-) diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index dad09f84dd..62336433ba 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -525,6 +525,19 @@ void DrawThreadView(ImConfig &cfg, ImControl &control) { if (ImGui::BeginPopup("threadPopup")) { DebugThreadInfo &thread = info[i]; ImGui::Text("Thread: %s", thread.name); + if (ImGui::MenuItem("Show callstack")) { + cfg.callstackThreadId = thread.id; + cfg.selectedCallstackFrame = -1; + cfg.callstackOpen = true; + ImGui::SetWindowFocus("Callstacks"); + } + if (ImGui::MenuItem("Go to PC")) { + control.command = { ImCmd::SHOW_IN_CPU_DISASM, thread.curPC }; + } + if (ImGui::MenuItem("Go to entry")) { + control.command = { ImCmd::SHOW_IN_CPU_DISASM, thread.entrypoint }; + } + ImGui::Separator(); if (ImGui::MenuItem("Copy entry to clipboard")) { char temp[64]; snprintf(temp, sizeof(temp), "%08x", thread.entrypoint); @@ -2020,18 +2033,80 @@ static void DrawCallStacks(const MIPSDebugInterface *debug, ImConfig &config, Im } std::vector info = GetThreadsInfo(); - // TODO: Add dropdown for thread choice, so you can check the callstacks of other threads. - u32 entry = 0; - u32 stackTop = 0; + const DebugThreadInfo *shown = nullptr; + const DebugThreadInfo *current = nullptr; for (auto &thread : info) { if (thread.isCurrent) { - entry = thread.entrypoint; - stackTop = thread.initialStack; - break; + current = &thread; + } + if (thread.id == config.callstackThreadId) { + shown = &thread; } } + const bool following = config.callstackThreadId == -1; + if (following) { + shown = current; + } - if (entry != 0 && ImGui::BeginTable("frames", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersH)) { + // Following the current thread is the default, "Current" goes back to that. + char preview[96]; + if (following) { + snprintf(preview, sizeof(preview), "Current (%s)", current ? current->name : "none"); + } else if (shown) { + snprintf(preview, sizeof(preview), "%s (%d)", shown->name, shown->id); + } else { + snprintf(preview, sizeof(preview), "Thread %d (gone)", config.callstackThreadId); + } + ImGui::SetNextItemWidth(250.0f); + if (ImGui::BeginCombo("Thread", preview)) { + if (ImGui::Selectable("Current", following)) { + config.callstackThreadId = -1; + config.selectedCallstackFrame = -1; + } + for (auto &thread : info) { + ImGui::PushID(thread.id); + char label[96]; + snprintf(label, sizeof(label), "%s (%d)%s", thread.name, thread.id, thread.isCurrent ? " - current" : ""); + if (ImGui::Selectable(label, !following && thread.id == config.callstackThreadId)) { + config.callstackThreadId = thread.id; + config.selectedCallstackFrame = -1; + } + ImGui::PopID(); + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + if (ImGui::Button("Current")) { + config.callstackThreadId = -1; + config.selectedCallstackFrame = -1; + } + + // Other threads' registers are in their saved contexts. + const DebugInterface *threadDebug = nullptr; + if (shown) { + threadDebug = shown->isCurrent ? debug : KernelDebugThread(shown->id); + } + if (!threadDebug) { + ImGui::TextUnformatted(following ? "No current thread" : "Thread not found"); + ImGui::End(); + return; + } + + const u32 entry = shown->entrypoint; + const u32 stackTop = shown->initialStack; + std::vector frames = MIPSStackWalk::Walk(threadDebug->GetPC(), threadDebug->GetRegValue(0, MIPS_REG_RA), threadDebug->GetRegValue(0, MIPS_REG_SP), entry, stackTop); + + auto describeFrame = [](const MIPSStackWalk::StackFrame &frame) { + const std::string entrySym = g_symbolMap->GetLabelString(frame.entry); + std::string desc = StringFromFormat("%s entry=%08x pc=%08x sp=%08x size=%d", entrySym.empty() ? "(no symbol)" : entrySym.c_str(), frame.entry, frame.pc, frame.sp, frame.stackSize); + const std::string source = g_lineInfo.LookupString(frame.pc); + if (!source.empty()) { + desc += " " + source; + } + return desc; + }; + + if (ImGui::BeginTable("frames", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersH)) { ImGui::TableSetupColumn("Entry", ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("EntryAddr", ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("CurPC", ImGuiTableColumnFlags_WidthFixed); @@ -2042,16 +2117,49 @@ static void DrawCallStacks(const MIPSDebugInterface *debug, ImConfig &config, Im ImGui::TableHeadersRow(); - std::vector frames = MIPSStackWalk::Walk(debug->GetPC(), debug->GetRegValue(0, 31), debug->GetRegValue(0, 29), entry, stackTop); - - // TODO: Add context menu and clickability int i = 0; for (auto &frame : frames) { const std::string entrySym = g_symbolMap->GetLabelString(frame.entry); ImGui::PushID(i); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(entrySym.c_str()); + // AllowOverlap keeps the clickable values in the other columns working. + const ImGuiSelectableFlags flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowOverlap; + if (ImGui::Selectable(entrySym.empty() ? "(no symbol)" : entrySym.c_str(), config.selectedCallstackFrame == i, flags)) { + config.selectedCallstackFrame = i; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + control.command = { ImCmd::SHOW_IN_CPU_DISASM, frame.pc }; + } + } + if (ImGui::BeginPopupContextItem("framePopup")) { + config.selectedCallstackFrame = i; + if (ImGui::MenuItem("Show in disassembly")) { + control.command = { ImCmd::SHOW_IN_CPU_DISASM, frame.pc }; + } + if (ImGui::MenuItem("Show function entry in disassembly")) { + control.command = { ImCmd::SHOW_IN_CPU_DISASM, frame.entry }; + } + ShowInMemoryViewerMenuItem(frame.sp, control); + ImGui::Separator(); + if (ImGui::MenuItem("Copy")) { + System_CopyStringToClipboard(describeFrame(frame)); + } + if (ImGui::MenuItem("Copy PC")) { + System_CopyStringToClipboard(StringFromFormat("%08x", frame.pc)); + } + if (!entrySym.empty() && ImGui::MenuItem("Copy function name")) { + System_CopyStringToClipboard(entrySym); + } + if (ImGui::MenuItem("Copy whole callstack")) { + std::string text; + for (const auto &f : frames) { + text += describeFrame(f); + text += "\n"; + } + System_CopyStringToClipboard(text); + } + ImGui::EndPopup(); + } ImGui::TableSetColumnIndex(1); ImClickableValue("frameentry", frame.entry, control, ImCmd::SHOW_IN_CPU_DISASM); ImGui::TableSetColumnIndex(2); diff --git a/UI/ImDebugger/ImDebugger.h b/UI/ImDebugger/ImDebugger.h index 289d7b216b..b79209b509 100644 --- a/UI/ImDebugger/ImDebugger.h +++ b/UI/ImDebugger/ImDebugger.h @@ -102,6 +102,9 @@ struct ImConfig { int selectedSymbolModule = 0; int selectedUtilityModule = 0; int selectedThread = 0; + // Thread ID whose callstack to show, -1 to follow the current thread. + int callstackThreadId = -1; + int selectedCallstackFrame = -1; int selectedKernelObject = 0; int selectedFramebuffer = -1; int selectedBreakpoint = -1; diff --git a/UI/ImDebugger/ImDisasmView.cpp b/UI/ImDebugger/ImDisasmView.cpp index 28bd7ec1ac..2aec473d20 100644 --- a/UI/ImDebugger/ImDisasmView.cpp +++ b/UI/ImDebugger/ImDisasmView.cpp @@ -844,6 +844,10 @@ void ImDisasmView::PopupMenu(MIPSState *mips, ImControl &control) { } } ImGui::Separator(); + const u32 funcStart = g_symbolMap->GetFunctionStart(curAddress_); + if (ImGui::MenuItem("Select in symbol list", nullptr, false, funcStart != -1)) { + symbolListRequest_ = funcStart; + } if (ImGui::MenuItem("Rename function")) { funcBegin_ = g_symbolMap->GetFunctionStart(curAddress_); if (funcBegin_ != -1) { @@ -1338,6 +1342,25 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro } } + // From "Select in symbol list" in the disassembly's context menu. Only moves the list's + // selection, the disassembly stays where it is. + u32 requestedSymbol; + if (disasmView_.TakeSymbolListRequest(&requestedSymbol)) { + for (int i = 0; i < (int)symCache_.size(); i++) { + if (symCache_[i].address != requestedSymbol) { + continue; + } + selectedSymbol_ = i; + truncate_cpy(selectedSymbolName_, symCache_[i].name); + scrollToSelectedSymbol_ = true; + if (symFilter_[0] != '\0' && !containsNoCase(symCache_[i].name, symFilter_)) { + symFilter_[0] = '\0'; + symMatchesDirty_ = true; + } + break; + } + } + if (selectedSymbol_ >= 0 && selectedSymbol_ < (int)symCache_.size()) { auto &sym = symCache_[selectedSymbol_]; if (ImGui::TreeNode("Edit Symbol", "Edit %s", sym.name.c_str())) { @@ -1377,6 +1400,15 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro } if (ImGui::BeginListBox("##symbols", ImGui::GetContentRegionAvail())) { + if (scrollToSelectedSymbol_) { + scrollToSelectedSymbol_ = false; + auto it = std::find(symMatches_.begin(), symMatches_.end(), selectedSymbol_); + if (it != symMatches_.end()) { + // The clipper below only submits visible rows, so scroll by row height to center it. + const float rowHeight = ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetScrollY((float)(it - symMatches_.begin()) * rowHeight - (ImGui::GetWindowHeight() - rowHeight) * 0.5f); + } + } ImGuiListClipper clipper; clipper.Begin((int)symMatches_.size(), -1); while (clipper.Step()) { @@ -1398,6 +1430,15 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro if (ImGui::MenuItem("Copy address to clipboard")) { System_CopyStringToClipboard(StringFromFormat("%08x", symCache_[i].address)); } + ImGui::Separator(); + // Leaves the code uncovered by any function, unlike "Remove function" in the + // disassembly, which merges it into the previous one. Handy for testing stack + // walks without symbols. The list rebuilds next frame, from the map's new version. + if (ImGui::MenuItem("Delete symbol")) { + g_symbolMap->RemoveFunction(symCache_[i].address, true); + g_symbolMap->SortSymbols(); + g_disassemblyManager.clear(); + } ImGui::EndPopup(); } ImGui::PopID(); diff --git a/UI/ImDebugger/ImDisasmView.h b/UI/ImDebugger/ImDisasmView.h index 61429968b5..b35b95652b 100644 --- a/UI/ImDebugger/ImDisasmView.h +++ b/UI/ImDebugger/ImDisasmView.h @@ -116,6 +116,15 @@ public: mapReloaded_ = false; return retval; } + // The function start "Select in symbol list" was picked for, if it was since the last call. + bool TakeSymbolListRequest(u32 *funcStart) { + if (symbolListRequest_ == 0xFFFFFFFF) { + return false; + } + *funcStart = symbolListRequest_; + symbolListRequest_ = 0xFFFFFFFF; + return true; + } private: enum class CopyInstructionsMode { @@ -177,6 +186,7 @@ private: bool searching_ = false; bool keyTaken = false; bool mapReloaded_ = false; + u32 symbolListRequest_ = 0xFFFFFFFF; int positionLocked_ = 0; @@ -227,6 +237,8 @@ private: char symFilter_[64]{}; std::vector symMatches_; bool symMatchesDirty_ = true; + // Set when the selection was moved from outside the list, so it scrolls there. + bool scrollToSelectedSymbol_ = false; ImDisasmView disasmView_; char searchTerm_[64]{};