Debugger: Fix breakpoints swallowed or reported twice when resuming

The skip-first marker suppressed everything at the address you resume or
step from. That's needed for the pause, or you could never get off a
breakpoint, but it also dropped the log and hit count. So a log breakpoint
you'd stepped onto, or were parked on for some other reason, never logged
when execution moved on.

Now the skip only drops the pause, unless the breakpoint at that address is
the one that stopped us, which has already logged and counted this
execution. The check moves into the Exec* functions instead of being
repeated in the interpreter, IR interpreter and three JITs, where the
register breakpoint path only applied it to the pause and so logged twice
when stepping off a log+pause one. The marker gets an explicit valid flag,
since address 0 meant "none", and Core_Break() clears it.

Based on the earlier cleanup-skip-first branch, reduced to one marker plus
a flag. Checked over the WebSocket debugger on the interpreter, IR, x86 JIT
and JIT-IR: log breakpoints on a parked and a stepped-onto address each log
once, and stepping off a log+pause one doesn't report it again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-16 18:16:33 +02:00
1 parent ab0fa898ae
commit 6a458bf148
7 files changed
+142 -41

No files matched your search

+49
View File
@@ -903,6 +903,55 @@ bool TestTempBreakpoints() {
g_breakpoints.RemoveBreakPoint(kAddrA);
EXPECT_FALSE(g_breakpoints.HasBreakPoints());
// Resuming from a breakpoint must not pause there again, and must not report it again if it's
// the one that stopped us. A breakpoint we merely stepped onto still has to log and count.
// Nothing retires an instruction here unless we move downcount, so the ticks stay put.
{
auto hitsAt = [](u32 addr) -> int {
for (const BreakPoint &bp : g_breakpoints.GetBreakpoints()) {
if (bp.addr == addr)
return (int)bp.numHits;
}
return -1;
};
g_breakpoints.ClearSkipFirst();
g_breakpoints.AddBreakPoint(kAddrA);
g_breakpoints.AddBreakPoint(kAddrB);
EXPECT_TRUE((g_breakpoints.ExecBreakPoint(kAddrA) & BREAK_ACTION_PAUSE) != 0);
EXPECT_EQ_INT(hitsAt(kAddrA), 1);
// Resuming off it. A step-over sets the skip twice for one resume, and that must not lose it.
g_breakpoints.SetSkipFirst(kAddrA);
g_breakpoints.SetSkipFirst(kAddrA);
EXPECT_EQ_INT((int)g_breakpoints.ExecBreakPoint(kAddrA), (int)BREAK_ACTION_NONE);
EXPECT_EQ_INT(hitsAt(kAddrA), 1);
// Stepped onto B, never reported there: it logs and counts, only the pause is dropped.
g_breakpoints.SetSkipFirst(kAddrB);
EXPECT_EQ_INT((int)(g_breakpoints.ExecBreakPoint(kAddrB) & BREAK_ACTION_PAUSE), 0);
EXPECT_EQ_INT(hitsAt(kAddrB), 1);
// An unrelated address still pauses and reports.
EXPECT_TRUE((g_breakpoints.ExecBreakPoint(kAddrA) & BREAK_ACTION_PAUSE) != 0);
EXPECT_EQ_INT(hitsAt(kAddrA), 2);
// Step off A, which runs it, and land on A again (a one-instruction loop). That's a new
// execution, which nothing has reported yet.
g_breakpoints.SetSkipFirst(kAddrA);
EXPECT_EQ_INT((int)g_breakpoints.ExecBreakPoint(kAddrA), (int)BREAK_ACTION_NONE);
currentMIPS->downcount -= 1;
g_breakpoints.SetSkipFirst(kAddrA);
EXPECT_EQ_INT((int)(g_breakpoints.ExecBreakPoint(kAddrA) & BREAK_ACTION_PAUSE), 0);
EXPECT_EQ_INT(hitsAt(kAddrA), 3);
currentMIPS->downcount += 1;
g_breakpoints.ClearSkipFirst();
g_breakpoints.RemoveBreakPoint(kAddrA);
g_breakpoints.RemoveBreakPoint(kAddrB);
}
g_symbolMap = nullptr;
return true;
}