Files
ppsspp/Core/Debugger/WebSocket/SteppingBroadcaster.cpp
Henrik RydgårdandClaude Opus 5 bb5d7d5b65 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
2026-08-18 10:59:08 +02:00

97 lines
3.7 KiB
C++

// Copyright (c) 2018- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/Debugger/WebSocket/BreakpointSubscriber.h"
#include "Core/Debugger/WebSocket/SteppingBroadcaster.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
#include "Core/MIPS/MIPS.h"
#include "Core/System.h"
struct CPUSteppingEvent {
// By value: the SteppingReason this is built from is a temporary at every call site, and it
// carries strings now, so binding a reference to it is asking for trouble later.
CPUSteppingEvent(const SteppingReason &reason) : reason_(reason) {
}
operator std::string() {
JsonWriter j;
j.begin();
j.writeString("event", "cpu.stepping");
j.writeUint("pc", currentMIPS->pc);
// A double ought to be good enough for a 156 day debug session.
j.writeFloat("ticks", CoreTiming::GetTicks(currentMIPS));
if (reason_.reason != BreakReason::None) {
j.writeString("reason", BreakReasonToString(reason_.reason));
j.writeUint("relatedAddress", reason_.relatedAddress);
}
// Present only when a breakpoint was what stopped us, so its absence is the test rather
// than some "kind": "none" the client would have to check for.
if (reason_.hit.kind != BreakpointKind::None) {
WriteBreakpointHit(j, reason_.hit);
}
j.end();
return j.str();
}
private:
const SteppingReason reason_;
};
// CPU has begun stepping (cpu.stepping)
//
// Sent unexpectedly with these properties:
// - pc: number value of PC register (inaccurate unless stepping.)
// - ticks: number of CPU cycles into emulation.
// - reason: an optional property, if present, it's equal to the value submitted to Core_EnableStepping ("jit.branchdebug", "savestate.load", "ui.lost_focus", etc.)
// - relatedAddress: an optional address (often zero, but it can be a value of PC saved at some point, a related memory address, etc.), always present if 'reason' is present
// CPU has resumed from stepping (cpu.resume)
//
// Sent unexpectedly with no other properties.
// Tracked globally rather than per connection: this runs on the CPU thread, which owns the state
// being read, and the resulting event is then handed to every connected debugger.
static CoreState g_prevState = CORE_POWERDOWN;
static int g_lastCounter = 0;
std::string SteppingBroadcaster::PollChange() {
if (PSP_GetBootState() != BootState::Complete) {
g_lastCounter = -1;
g_prevState = CORE_POWERDOWN;
return std::string();
}
std::string result;
const int steppingCounter = Core_GetSteppingCounter();
// We ignore CORE_POWERDOWN as a stepping state.
if (coreState == CORE_STEPPING_CPU && steppingCounter != g_lastCounter) {
result = CPUSteppingEvent(Core_GetSteppingReason());
} else if (g_prevState == CORE_STEPPING_CPU && coreState != CORE_STEPPING_CPU && Core_IsActive()) {
result = R"({"event":"cpu.resume"})";
}
g_lastCounter = steppingCounter;
g_prevState = coreState;
return result;
}
std::string SteppingBroadcaster::CurrentState() {
if (PSP_GetBootState() != BootState::Complete || coreState != CORE_STEPPING_CPU)
return std::string();
return CPUSteppingEvent(Core_GetSteppingReason());
}