From d8a1808b3d9ae08b0b0c8b513259b91e64713c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 18 Aug 2026 10:04:38 +0200 Subject: [PATCH] ImDebugger: add "Run to here, next frame", gated on the flip count Prototype of the frame-gated run-to-cursor idea. "Run to here" stops at the first hit, which isn't what you want for an address hit many times per frame - you end up stepping through the rest of the current frame to reach the state you actually care about. Built on machinery that was already there rather than a new stepping mode: the one-shot breakpoint behind run-to-cursor already takes a condition (step-into uses it to pin a step to one thread), and a hit that fails the condition leaves it armed for the next one. So "the next frame" is just a condition that isn't true yet - here "flipcount > ". Counting presented frames rather than vblanks matters for a game that doesn't render at the full refresh rate: at 30fps there are two vblanks per frame, so a vblank-based condition would let you through halfway into the frame you were trying to skip. The flip side is that the counter only advances when the framebuffer actually changed, so if the game has stopped drawing - or is wedged in the loop you're trying to debug - this never trips and the core keeps running. Both counters are exposed to the expression parser, next to threadid/moduleid/usec/ticks, so they're usable in ordinary breakpoint conditions and cpu.evaluate too, not just from this menu item: "flipcount" for presented frames and "vcount" for the PSP's own vblank counter, which is what sceDisplayGetVcount returns and is the one a game's own timing is written against. Verified with a headless session: across a second of emulated time flipcount went 120 -> 172 and vcount 119 -> 172 (a game rendering every vblank, so they track). pspautotests 314/314, UnitTest 55/55. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Core/Debugger/Breakpoints.cpp | 4 ++- Core/MIPS/MIPSDebugInterface.cpp | 18 +++++++++++++ UI/ImDebugger/ImDisasmView.cpp | 44 ++++++++++++++++++++++++++++---- UI/ImDebugger/ImDisasmView.h | 3 +++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Core/Debugger/Breakpoints.cpp b/Core/Debugger/Breakpoints.cpp index a7c7381cc0..de051cfa7e 100644 --- a/Core/Debugger/Breakpoints.cpp +++ b/Core/Debugger/Breakpoints.cpp @@ -357,7 +357,9 @@ BreakAction BreakpointManager::ExecBreakPoint(u32 addr) { } if (tempBreakPoint_.valid && tempBreakPoint_.addr == addr) { - // The condition, when set, restricts the step to one thread - see SetTempBreakPointCond(). + // The condition, when set, narrows down which hit counts - to one thread for a step + // ("threadid == ..."), or to a later frame for run-to-cursor ("flipcount > ..."). A hit that + // fails it leaves the breakpoint armed, so the next one gets a chance. if (!tempBreakPoint_.hasCond || tempBreakPoint_.cond.Evaluate() != 0) { DEBUG_LOG(Log::Debugger, "Reached temporary breakpoint at %08x", addr); result |= BREAK_ACTION_PAUSE; diff --git a/Core/MIPS/MIPSDebugInterface.cpp b/Core/MIPS/MIPSDebugInterface.cpp index 57a9189c36..b690a9781a 100644 --- a/Core/MIPS/MIPSDebugInterface.cpp +++ b/Core/MIPS/MIPSDebugInterface.cpp @@ -24,6 +24,7 @@ #include "Common/StringUtils.h" #include "Core/CoreTiming.h" +#include "Core/HW/Display.h" #include "Core/Debugger/Breakpoints.h" #include "Core/Debugger/SymbolMap.h" #include "Core/Debugger/DebugInterface.h" @@ -49,6 +50,8 @@ enum ReferenceIndexType { REF_INDEX_MODULE = REF_INDEX_HLE | 1, REF_INDEX_USEC = REF_INDEX_HLE | 2, REF_INDEX_TICKS = REF_INDEX_HLE | 3, + REF_INDEX_VCOUNT = REF_INDEX_HLE | 4, + REF_INDEX_FLIPS = REF_INDEX_HLE | 5, }; @@ -133,6 +136,17 @@ public: referenceIndex = REF_INDEX_TICKS; return true; } + // The PSP's own vblank counter, as returned by sceDisplayGetVcount. + if (strcasecmp(str, "vcount") == 0) { + referenceIndex = REF_INDEX_VCOUNT; + return true; + } + // Frames actually presented. Unlike vcount this tracks what reached the screen, so + // "flipcount > N" is how a breakpoint condition says "only from the next frame onwards". + if (strcasecmp(str, "flipcount") == 0) { + referenceIndex = REF_INDEX_FLIPS; + return true; + } return false; } @@ -160,6 +174,10 @@ public: return (uint32_t)CoreTiming::GetGlobalTimeUs(); // Loses information if (referenceIndex == REF_INDEX_TICKS) return (uint32_t)CoreTiming::GetTicks(currentMIPS); + if (referenceIndex == REF_INDEX_VCOUNT) + return (uint32_t)__DisplayGetVCount(); + if (referenceIndex == REF_INDEX_FLIPS) + return (uint32_t)__DisplayGetFlipCount(); if ((referenceIndex & ~(REF_INDEX_FPU | REF_INDEX_FPU_INT)) < 32) return cpu->GetRegValue(1, referenceIndex & ~(REF_INDEX_FPU | REF_INDEX_FPU_INT)); if ((referenceIndex & ~(REF_INDEX_VFPU | REF_INDEX_VFPU_INT)) < 128) diff --git a/UI/ImDebugger/ImDisasmView.cpp b/UI/ImDebugger/ImDisasmView.cpp index 5cbeca8e2d..7b325298c5 100644 --- a/UI/ImDebugger/ImDisasmView.cpp +++ b/UI/ImDebugger/ImDisasmView.cpp @@ -14,6 +14,7 @@ #include "Core/MIPS/MIPSTables.h" #include "Core/MIPS/MIPSAnalyst.h" #include "Core/MIPS/MIPSAsm.h" +#include "Core/HW/Display.h" #include "Core/Reporting.h" #include "Core/Debugger/SymbolMap.h" #include "Core/MemMap.h" @@ -77,6 +78,37 @@ void ImDisasmView::assembleOpcode(u32 address, std::string_view defaultText) { assemblePopup_ = true; } +void ImDisasmView::RunToAddress(u32 address, bool nextFrame) { + g_breakpoints.SetTempBreakPoint(address); + + if (nextFrame) { + // The flip counter, so "greater than what it is now" means "not until something new has + // reached the screen". Handy when the address you're interested in is hit many times per + // frame and you want the next frame's first hit, not this frame's next one. + // + // Counting presented frames rather than vblanks matters for a game that doesn't render at + // the full refresh rate - at 30fps there are two vblanks per frame, so a vblank-based + // condition would let you through halfway into the frame you're trying to skip. + // + // The flip side is that it only advances when the framebuffer actually changed, so if the + // game has stopped drawing - or is wedged in the loop you're trying to debug - this never + // trips at all and the core just keeps running. + BreakPointCond cond; + cond.debug = currentDebugMIPS; + cond.expressionString = StringFromFormat("flipcount > %d", __DisplayGetFlipCount()); + if (initExpression(currentDebugMIPS, cond.expressionString.c_str(), cond.expression)) { + g_breakpoints.SetTempBreakPointCond(cond); + } else { + statusBarText_ = "Couldn't compile the frame condition - running without it."; + } + } + + g_breakpoints.SetSkipFirst(address); + if (Core_IsStepping()) { + Core_Resume(); + } +} + bool ImDisasmView::applyAssembly(u32 address, std::string_view op) { // Re-checked here, not just when the popup was requested: the popup is modeless, so the core // can have been resumed in between. @@ -776,11 +808,13 @@ void ImDisasmView::PopupMenu(MIPSState *mips, ImControl &control) { FollowBranch(); } if (ImGui::MenuItem("Run to here")) { - g_breakpoints.SetTempBreakPoint(curAddress_); - g_breakpoints.SetSkipFirst(curAddress_); - if (Core_IsStepping()) { - Core_Resume(); - } + RunToAddress(curAddress_, false); + } + if (ImGui::MenuItem("Run to here, next frame")) { + RunToAddress(curAddress_, true); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { + ImGui::SetTooltip("Ignores hits until a new frame has been presented, to skip the rest of this one."); } ImGui::Separator(); if (ImGui::MenuItem("Assemble")) { diff --git a/UI/ImDebugger/ImDisasmView.h b/UI/ImDebugger/ImDisasmView.h index 3f07a99f4e..8e331f3e31 100644 --- a/UI/ImDebugger/ImDisasmView.h +++ b/UI/ImDebugger/ImDisasmView.h @@ -126,6 +126,9 @@ private: }; void ProcessKeyboardShortcuts(bool focused); + // Plants the one-shot "run to cursor" breakpoint and resumes. With nextFrame, hits are ignored + // until the next vblank, so the rest of the current frame is skipped over. + void RunToAddress(u32 address, bool nextFrame); // Requests the assemble popup - the actual input happens in PopupMenu(), since ImGui popups // can only be opened and drawn from inside the frame that owns them. void assembleOpcode(u32 address, std::string_view defaultText);