Files
ppsspp/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp
Henrik RydgårdandClaude Opus 5 256de40a30 Tidy up the breakpoint skip-first mechanism
The tick basis was actually sound - CoreTiming::GetTicks() is continuous across
Advance(), so "ticks unchanged" really does mean "no instruction retired since",
which is exactly the window the suppression needs. The plumbing around it was
the problem:

- ExecRegBreakpoint() applied the skip only to the pause, so stepping off a
  log+pause register breakpoint printed it again and counted a second hit. The
  check now sits at the top of ExecBreakPoint(), ExecMemCheck(), ExecOpMemCheck()
  and ExecRegBreakpoint() instead of being repeated at seven call sites across
  the interpreter and four JIT frontends, where one of them had it wrong and
  another checked a different address than the rest.
- Address 0 doubled as "nothing to skip" (ClearSkipFirst() existed but was dead
  code; the JITs cleared by calling SetSkipFirst(0)), so a breakpoint at 0 would
  have been permanently suppressed. There's an explicit valid flag now, and
  ClearSkipFirst() is what clears it.
- The marker was set from five places and never cleared when execution stopped,
  so one could outlive the resume that armed it. Core_Break() clears it now, and
  the two WebSocket subscribers that set it immediately before asking for a
  step - which sets it again itself - no longer do.
- SetSkipFirst() now only arms when some breakpoint machinery actually exists,
  so a stale marker can't sit around waiting to swallow a breakpoint added later.

CheckSkipFirst() returning an address (compared against pc by each caller) is
replaced by ShouldSkipBreakpoint(addr), which compares against both addr and
currentMIPS->pc - under a JIT those differ, and only some callers knew that.

Covered by the Breakpoints unit test, including that a suppressed breakpoint
neither logs nor counts a hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 23:25:56 +02:00

456 lines
14 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 "Common/StringUtils.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "Core/CoreTiming.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/Debugger/WebSocket/CPUCoreSubscriber.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
#include "Core/HLE/sceKernelThread.h"
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSDebugInterface.h"
#include "Core/Reporting.h"
DebuggerSubscriber *WebSocketCPUCoreInit(DebuggerEventHandlerMap &map) {
// No need to bind or alloc state, these are all global.
map["cpu.stepping"] = &WebSocketCPUStepping;
map["cpu.resume"] = &WebSocketCPUResume;
map["cpu.status"] = &WebSocketCPUStatus;
map["cpu.getAllRegs"] = &WebSocketCPUGetAllRegs;
map["cpu.getReg"] = &WebSocketCPUGetReg;
map["cpu.setReg"] = &WebSocketCPUSetReg;
map["cpu.evaluate"] = &WebSocketCPUEvaluate;
return nullptr;
}
static std::string RegValueAsFloat(uint32_t u) {
union {
uint32_t u;
float f;
} bits = { u };
return StringFromFormat("%f", bits.f);
}
static DebugInterface *CPUFromRequest(DebuggerRequest &req) {
if (!req.HasParam("thread"))
return currentDebugMIPS;
u32 uid;
if (!req.ParamU32("thread", &uid))
return nullptr;
DebugInterface *cpuDebug = KernelDebugThread((SceUID)uid);
if (!cpuDebug)
req.Fail("Thread could not be found");
return cpuDebug;
}
// Begin stepping and pause the CPU (cpu.stepping)
//
// No parameters.
//
// No immediate response. Once CPU is stepping, a "cpu.stepping" event will be sent.
void WebSocketCPUStepping(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive()) {
return req.Fail("CPU not started");
}
if (!Core_IsStepping() && Core_IsActive()) {
// Core_Break() is explicitly free-threaded (see Core.cpp), so no need to bounce this to the CPU
// thread - and we can't anyway, since queuing to it only makes sense once the CPU actually *is*
// stepping, which this call is what triggers in the first place.
Core_Break(BreakReason::DebugStep, 0);
}
}
// Stop stepping and resume the CPU (cpu.resume)
//
// No parameters.
//
// No immediate response. Once CPU is stepping, a "cpu.resume" event will be sent.
void WebSocketCPUResume(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive()) {
return req.Fail("CPU not started");
}
if (!Core_IsStepping() || coreState == CORE_POWERDOWN) {
return req.Fail("CPU not stepping");
}
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
if (currentMIPS->inDelaySlot) {
Core_RequestCPUStep(CPUStepType::Into);
}
Core_Resume();
});
}
// Request the current CPU status (cpu.status)
//
// No parameters.
//
// Response (same event name):
// - stepping: boolean, CPU currently stepping.
// - paused: boolean, CPU paused or not started yet.
// - pc: number value of PC register (inaccurate unless stepping.)
// - ticks: number of CPU cycles into emulation.
// - us: microseconds of emulated time. Not derivable from ticks on the client side: the PSP's
// clock frequency is changeable (scePowerSetCpuClockFrequency), and games do change it, so the
// ticks-per-second ratio isn't fixed over a run. This is what to use to answer "how far into
// the game am I", e.g. to line up input injection with a wall-clock repro.
// - clockHz: the CPU clock frequency right now, for reference.
// Deliberately not routed through Core_RunOnCPUThread(): this is meant to be a cheap, frequently-pollable
// status check, and the "pc" field is already documented as inaccurate unless stepping - queuing it would
// add up to a frame of latency to every poll for no real accuracy benefit. Matches how SteppingBroadcaster
// already reads this same state directly from the WebSocket thread.
void WebSocketCPUStatus(DebuggerRequest &req) {
JsonWriter &json = req.Respond();
const bool pspInited = PSP_GetBootState() == BootState::Complete;
json.writeBool("stepping", pspInited && Core_IsStepping() && coreState != CORE_POWERDOWN);
json.writeBool("paused", GetUIState() != UISTATE_INGAME);
// Avoid NULL deference.
json.writeUint("pc", pspInited ? currentMIPS->pc : 0);
// A double ought to be good enough for a 156 day debug session.
json.writeFloat("ticks", pspInited ? CoreTiming::GetTicks(currentMIPS) : 0);
json.writeFloat("us", pspInited ? (double)CoreTiming::PeekGlobalTimeUs() : 0);
json.writeUint("clockHz", pspInited ? CoreTiming::GetClockFrequencyHz() : 0);
}
// Retrieve all regs and their values (cpu.getAllRegs)
//
// Parameters:
// - thread: optional number indicating the thread id to get regs for.
//
// Response (same event name):
// - categories: array of objects:
// - id: "category" property to use for other events.
// - name: a category name, such as "GPR".
// - registerNames: array of string names of the registers (size varies per category.)
// - uintValues: array of unsigned integer values for the registers.
// - floatValues: array of strings showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUGetAllRegs(DebuggerRequest &req) {
// Route the actual register reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
JsonWriter &json = req.Respond();
json.pushArray("categories");
for (int c = 0; c < MIPSDebugInterface::GetNumCategories(); ++c) {
json.pushDict();
json.writeInt("id", c);
json.writeString("name", MIPSDebugInterface::GetCategoryName(c));
int total = MIPSDebugInterface::GetNumRegsInCategory(c);
json.pushArray("registerNames");
for (int r = 0; r < total; ++r)
json.writeString(MIPSDebugInterface::GetRegName(c, r));
if (c == 0) {
json.writeString("pc");
json.writeString("hi");
json.writeString("lo");
}
json.pop();
json.pushArray("uintValues");
// Writing as floating point to avoid negatives. Actually double, so safe.
for (int r = 0; r < total; ++r)
json.writeUint(cpuDebug->GetRegValue(c, r));
if (c == 0) {
json.writeUint(cpuDebug->GetPC());
json.writeUint(cpuDebug->GetHi());
json.writeUint(cpuDebug->GetLo());
}
json.pop();
json.pushArray("floatValues");
// Note: String so it can have Infinity and NaN.
for (int r = 0; r < total; ++r)
json.writeString(RegValueAsFloat(cpuDebug->GetRegValue(c, r)));
if (c == 0) {
json.writeString(RegValueAsFloat(cpuDebug->GetPC()));
json.writeString(RegValueAsFloat(cpuDebug->GetHi()));
json.writeString(RegValueAsFloat(cpuDebug->GetLo()));
}
json.pop();
json.pop();
}
json.pop();
});
}
enum class DebuggerRegType {
INVALID,
NORMAL,
PC,
HI,
LO,
};
static DebuggerRegType ValidateRegName(DebuggerRequest &req, const std::string &name, int *cat, int *reg) {
if (name == "pc") {
*cat = 0;
*reg = 32;
return DebuggerRegType::PC;
}
if (name == "hi") {
*cat = 0;
*reg = 33;
return DebuggerRegType::HI;
}
if (name == "lo") {
*cat = 0;
*reg = 34;
return DebuggerRegType::LO;
}
for (int c = 0; c < currentDebugMIPS->GetNumCategories(); ++c) {
int total = currentDebugMIPS->GetNumRegsInCategory(c);
for (int r = 0; r < total; ++r) {
if (name == currentDebugMIPS->GetRegName(c, r)) {
*cat = c;
*reg = r;
return DebuggerRegType::NORMAL;
}
}
}
req.Fail("Invalid 'name' parameter");
return DebuggerRegType::INVALID;
}
static DebuggerRegType ValidateCatReg(DebuggerRequest &req, int *cat, int *reg) {
const char *name = req.data.getStringOr("name", nullptr);
if (name)
return ValidateRegName(req, name, cat, reg);
*cat = req.data.getInt("category", -1);
*reg = req.data.getInt("register", -1);
if (*cat < 0 || *cat >= currentDebugMIPS->GetNumCategories()) {
req.Fail("Invalid 'category' parameter");
return DebuggerRegType::INVALID;
}
// TODO: We fake it for GPR... not sure yet if this is a good thing.
if (*cat == 0) {
// Intentionally retains the reg value.
if (*reg == 32)
return DebuggerRegType::PC;
if (*reg == 33)
return DebuggerRegType::HI;
if (*reg == 34)
return DebuggerRegType::LO;
}
if (*reg < 0 || *reg >= currentDebugMIPS->GetNumRegsInCategory(*cat)) {
req.Fail("Invalid 'register' parameter");
return DebuggerRegType::INVALID;
}
return DebuggerRegType::NORMAL;
}
// Retrieve the value of a single register (cpu.getReg)
//
// Parameters (by name):
// - thread: optional number indicating the thread id to get from.
// - name: string name of register to lookup.
//
// Parameters (by category id and index, ignored if name specified):
// - thread: optional number indicating the thread id to get from.
// - category: id of category for the register.
// - register: index into array of registers.
//
// Response (same event name):
// - category: id of category for the register.
// - register: index into array of registers.
// - uintValue: value in register.
// - floatValue: string showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUGetReg(DebuggerRequest &req) {
// Route the actual register read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
int cat, reg;
uint32_t val = 0;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::PC:
val = cpuDebug->GetPC();
break;
case DebuggerRegType::HI:
val = cpuDebug->GetHi();
break;
case DebuggerRegType::LO:
val = cpuDebug->GetLo();
break;
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
JsonWriter &json = req.Respond();
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
// Update the value of a single register (cpu.setReg)
//
// Parameters (by name):
// - thread: optional number indicating the thread id to update.
// - name: string name of register to lookup.
// - value: number (uint values only) or string to set to. Values may include
// "0x1234", "1.5", "nan", "-inf", etc. For a float, use a string with decimal e.g. "1.0".
//
// Parameters (by category id and index, ignored if name specified):
// - thread: optional number indicating the thread id to update.
// - category: id of category for the register.
// - register: index into array of registers.
// - value: number (uint values only) or string to set to. Values may include
// "0x1234", "1.5", "nan", "-inf", etc. For a float, use a string with decimal e.g. "1.0".
//
// Response (same event name):
// - category: id of category for the register.
// - register: index into array of registers.
// - uintValue: new value in register.
// - floatValue: string showing float representation. May be "nan", "inf", or "-inf".
//
// NOTE: Cannot be called unless the CPU is currently stepping.
void WebSocketCPUSetReg(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive()) {
return req.Fail("CPU not started");
}
if (!Core_IsStepping()) {
return req.Fail("CPU currently running (cpu.stepping first)");
}
// Route the actual register write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
uint32_t val;
if (!req.ParamU32("value", &val, true)) {
// Already sent error.
return;
}
int cat, reg;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
if (cat == 0 && reg == 0 && val != 0) {
req.Fail("Cannot change reg zero");
return;
}
cpuDebug->SetRegValue(cat, reg, val);
// In case part of it was ignored (e.g. flags reg.)
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::PC:
cpuDebug->SetPC(val);
break;
case DebuggerRegType::HI:
cpuDebug->SetHi(val);
break;
case DebuggerRegType::LO:
cpuDebug->SetLo(val);
break;
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
// Repeat it back just to avoid confusion on how it parsed.
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
// Evaluate an expression (cpu.evaluate)
//
// Parameters:
// - thread: optional number indicating the thread id to update.
// - expression: string containing labels, operators, regs, etc.
//
// Response (same event name):
// - uintValue: the computed value.
// - floatValue: string showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUEvaluate(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive()) {
return req.Fail("CPU not started");
}
// Route the actual register/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
std::string exp;
if (!req.ParamString("expression", &exp)) {
// Already sent error.
return;
}
u32 val;
PostfixExpression postfix;
if (!initExpression(cpuDebug, exp.c_str(), postfix)) {
req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
return;
}
if (!parseExpression(cpuDebug, postfix, val)) {
req.Fail(StringFromFormat("Could not evaluate expression: %s", getExpressionError()));
return;
}
JsonWriter &json = req.Respond();
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}