Files
ppsspp/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp
T
Henrik RydgårdandClaude Opus 5 59eb8a9a81 Report emulated time in cpu.status, and note headless/wsdbg traps in AGENTS.md
cpu.status only reported raw CPU ticks, which a client can't turn into a time:
the PSP's clock frequency is changeable and games do change it, so the
ticks-per-second ratio isn't fixed over a run. CrossCraft Classic runs at
333MHz, so assuming the default 222MHz reads 9.1s where the truth is 6.3s -
enough to put scripted input injection in the wrong place entirely.

Adds "us" (emulated microseconds) and "clockHz" alongside "ticks".
CoreTiming::GetGlobalTimeUs() can't be used directly for this: it rebases its
own internal counters as a side effect, and cpu.status is deliberately served
straight from the WebSocket thread rather than queued to the CPU thread (it's
meant to be cheap and frequently pollable). So PeekGlobalTimeUs() computes the
same value without the rebasing.

AGENTS.md picks up the things that cost time while driving headless over the
websocket API: --sync silently desynchronises on raw JSON lines because only
wsdbg's key=value shorthand gets a ticket; headless reports HAS_DEBUGGER as
false so anything gated on it silently does nothing there; the memstick is
hardcoded next to the executable; a leftover headless process turns a build
into an LNK1168 that looks like a compile error; wrapping the launcher in
`timeout` kills the emulator along with it, losing the crash you stopped at;
response field names aren't uniform (value vs uintValue); broadcast.config.set
rejects two of the four keys the docs list.

Also writes down the ELF-as-oracle technique that cracked the relocation bug -
when homebrew ships app.elf next to app.prx, the pre-link ELF still has the
symbols and the relocation symbol indices the PRX format discards, so the
loader's output can be checked exhaustively offline instead of by re-running.

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

457 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([&] {
g_breakpoints.SetSkipFirst(currentMIPS->pc);
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));
});
}