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 > <now>".

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-18 10:59:08 +02:00
1 parent 434f1b4e13
commit d8a1808b3d
4 files changed
+63 -6

No files matched your search

+39 -5
View File
@@ -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")) {