mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-03 19:25:18 +02:00
Debugger: report structured breakpoint hits, including log-only ones
A breakpoint hit reached a WebSocket client as two fields on cpu.stepping: a reason string and one address. Everything else the hit site knew was formatted into a log line and dropped. What was missing per kind: - exec: hit count, condition, symbol. - memory: the address actually accessed, read vs write, size, and who did it. The address that reached the client was the *start of the watched range*, so a client watching 4KB learned only that something in it was touched. - register: which register. Entirely - the event carried pc and nothing else. There's now a BreakpointHit captured where the hit happens and carried through Core_Break() on the stepping reason, rendered as a "hit" object on cpu.stepping. It's absent rather than empty when the break wasn't a breakpoint (a pause, a savestate load, an exception), so presence is the test. relatedAddress keeps reporting the range start for compatibility; hit.address is the accurate one. The formatter is shared with the new event below, so the two can't drift. And a new cpu.breakpoint.hit broadcast fires on *every* hit whose condition passes, whether or not it stops the CPU. That's the part that makes log-only breakpoints usable for automation: until now their only trace was a line in the log stream, so a client couldn't count hits, or react to one, without scraping text. Same "hit" object, plus a sequence number. Volume needed handling, since a log-only breakpoint in a hot loop produces events far faster than a connection drains them - measured 13719 hits in three seconds of one homebrew's draw function. The per-connection queue is capped and drops rather than growing without bound, and the sequence number is what makes that honest: a gap tells a client exactly how many it missed. Clients that don't want the traffic at all can disallow the new "breakpoint" broadcast category. Building the hit record is skipped entirely when no debugger is connected, which is one relaxed atomic load on that path. Verified against a running game, all three kinds. The memory case shows why the address/range split matters - accessed address 200540160 against a watched range starting at 200941120, with source "ThreadFillStack" identifying the HLE call responsible. libretro gets stubs: it builds Core.cpp and Breakpoints.cpp but not Core/Debugger/WebSocket.cpp. 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:
co-authored by
Claude Opus 5
parent
d8a1808b3d
commit
bb5d7d5b65
@@ -20,6 +20,7 @@
|
||||
#include "Common/System/System.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Debugger/WebSocket.h"
|
||||
#include "Core/Debugger/Breakpoints.h"
|
||||
#include "Core/Debugger/MemBlockInfo.h"
|
||||
#include "Core/Debugger/SymbolMap.h"
|
||||
@@ -62,8 +63,32 @@ BreakAction MemCheck::Apply(u32 addr, bool write, int size, u32 pc) {
|
||||
BreakAction MemCheck::Action(u32 addr, bool write, int size, u32 pc, const char *reason) {
|
||||
// Conditions have always already been checked if we get here.
|
||||
Log(addr, write, size, pc, reason);
|
||||
|
||||
BreakpointHit hit;
|
||||
if (WebSocketDebuggerHasClients() || (action & BREAK_ACTION_PAUSE)) {
|
||||
hit.kind = BreakpointKind::Memory;
|
||||
hit.pc = pc;
|
||||
hit.address = addr;
|
||||
hit.size = size;
|
||||
hit.write = write;
|
||||
hit.rangeStart = start;
|
||||
hit.rangeEnd = end;
|
||||
// This is a copy of the stored memcheck, taken after Apply() bumped the count, so it's
|
||||
// already the post-hit value.
|
||||
hit.numHits = numHits;
|
||||
hit.logged = (action & BREAK_ACTION_LOG) != 0;
|
||||
hit.paused = (action & BREAK_ACTION_PAUSE) != 0;
|
||||
if (hasCondition)
|
||||
hit.condition = condition.expressionString;
|
||||
if (reason)
|
||||
hit.source = reason;
|
||||
WebSocketNotifyBreakpointHit(hit);
|
||||
}
|
||||
|
||||
if (action & BREAK_ACTION_PAUSE) {
|
||||
Core_Break(BreakReason::MemoryBreakpoint, start);
|
||||
// relatedAddress stays the range start for compatibility - the address actually touched
|
||||
// is in the hit, which is the whole point of it.
|
||||
Core_Break(BreakReason::MemoryBreakpoint, start, &hit);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
@@ -329,6 +354,7 @@ BreakAction BreakpointManager::ExecBreakPoint(u32 addr) {
|
||||
return BREAK_ACTION_NONE;
|
||||
|
||||
BreakAction result = BREAK_ACTION_NONE;
|
||||
BreakpointHit hit;
|
||||
|
||||
size_t bp = FindBreakpoint(addr);
|
||||
if (bp != INVALID_BREAKPOINT) {
|
||||
@@ -342,6 +368,20 @@ BreakAction BreakpointManager::ExecBreakPoint(u32 addr) {
|
||||
if (condPassed) {
|
||||
++info.numHits;
|
||||
|
||||
if (action != BREAK_ACTION_NONE && (WebSocketDebuggerHasClients() || (action & BREAK_ACTION_PAUSE))) {
|
||||
hit.kind = BreakpointKind::Exec;
|
||||
hit.pc = addr;
|
||||
hit.address = addr;
|
||||
hit.rangeStart = addr;
|
||||
hit.rangeEnd = addr;
|
||||
hit.numHits = info.numHits;
|
||||
hit.logged = (action & BREAK_ACTION_LOG) != 0;
|
||||
hit.paused = (action & BREAK_ACTION_PAUSE) != 0;
|
||||
if (info.hasCond)
|
||||
hit.condition = info.cond.expressionString;
|
||||
WebSocketNotifyBreakpointHit(hit);
|
||||
}
|
||||
|
||||
if (action & BREAK_ACTION_LOG) {
|
||||
if (info.logFormat.empty()) {
|
||||
NOTICE_LOG(Log::JIT, "BKP PC=%08x (%s)", addr, g_symbolMap->GetDescription(addr).c_str());
|
||||
@@ -367,7 +407,9 @@ BreakAction BreakpointManager::ExecBreakPoint(u32 addr) {
|
||||
}
|
||||
|
||||
if (result & BREAK_ACTION_PAUSE) {
|
||||
Core_Break(BreakReason::CpuBreakpoint, addr);
|
||||
// hit stays kind None when only the temporary breakpoint fired - there's no user
|
||||
// breakpoint to describe in that case, just a step completing.
|
||||
Core_Break(BreakReason::CpuBreakpoint, addr, hit.kind != BreakpointKind::None ? &hit : nullptr);
|
||||
System_Notify(SystemNotification::DISASSEMBLY);
|
||||
}
|
||||
|
||||
@@ -698,6 +740,20 @@ BreakAction BreakpointManager::ExecRegBreakpoint(int reg, u32 pc) {
|
||||
|
||||
++info.numHits;
|
||||
|
||||
BreakpointHit hit;
|
||||
if (WebSocketDebuggerHasClients() || (info.result & BREAK_ACTION_PAUSE)) {
|
||||
hit.kind = BreakpointKind::Register;
|
||||
hit.pc = pc;
|
||||
hit.address = pc;
|
||||
hit.reg = reg;
|
||||
hit.numHits = info.numHits;
|
||||
hit.logged = (info.result & BREAK_ACTION_LOG) != 0;
|
||||
hit.paused = (info.result & BREAK_ACTION_PAUSE) != 0;
|
||||
if (info.hasCond)
|
||||
hit.condition = info.cond.expressionString;
|
||||
WebSocketNotifyBreakpointHit(hit);
|
||||
}
|
||||
|
||||
if (info.result & BREAK_ACTION_LOG) {
|
||||
if (info.logFormat.empty()) {
|
||||
NOTICE_LOG(Log::JIT, "BKP reg write r%d, PC=%08x (%s)", reg, pc, g_symbolMap->GetDescription(pc).c_str());
|
||||
@@ -708,7 +764,7 @@ BreakAction BreakpointManager::ExecRegBreakpoint(int reg, u32 pc) {
|
||||
}
|
||||
}
|
||||
if ((info.result & BREAK_ACTION_PAUSE) && g_breakpoints.CheckSkipFirst() != pc) {
|
||||
Core_Break(BreakReason::RegBreakpoint, pc);
|
||||
Core_Break(BreakReason::RegBreakpoint, pc, &hit);
|
||||
}
|
||||
|
||||
return info.result;
|
||||
|
||||
Reference in New Issue
Block a user