mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
Eight events answered nothing at all: cpu.stepping, cpu.resume, gpu.stats.feed
and the five stepping requests. Their documented contract was "no immediate
response, an event follows", which leaves a client unable to tell an accepted
request from one that was dropped - and forces any request/response
correlation to carry a hardcoded list of events that don't answer. wsdbg's
--sync doesn't have that list, so it waits for the next message and treats
whatever broadcast arrives first as the answer, silently misattributing every
later response in the script.
Fixed centrally in the dispatch loop rather than in the eight handlers: if a
handler finishes without having sent anything, send an empty response carrying
its ticket. That also covers handlers added later, which is the part a
per-handler fix wouldn't.
The asynchronous event that reports the real outcome is unchanged and still
follows. The two are easy to tell apart - the acknowledgement carries the
ticket from the request, a broadcast has none:
-> {"event":"cpu.stepInto","ticket":3}
<- {"event":"cpu.stepInto","ticket":3}
<- {"event":"cpu.stepping","pc":142622896,"reason":"cpu.stepInto",...}
Existing clients ignore events they didn't ask for, and this adds a message
rather than changing or removing one, so nothing that worked before breaks.
pspautotests 314/314.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
459 lines
15 KiB
C++
459 lines
15 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.
|
|
//
|
|
// Response (same event name) with no extra data, acknowledging the request. The CPU may not be
|
|
// stepping yet at that point - a "cpu.stepping" event follows once it is.
|
|
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.
|
|
//
|
|
// Response (same event name) with no extra data, acknowledging the request. A "cpu.resume"
|
|
// event follows once the CPU is actually running again.
|
|
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, ®)) {
|
|
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, ®)) {
|
|
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));
|
|
});
|
|
}
|