mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-30 17:35:09 +02:00
The acknowledgement added in "Reply to every debugger request" broke two cases
Nemoumbra pointed out, both of which come down to it reusing the request's own
event name.
A ticketless request is the bad one. {"event":"cpu.resume"} with no ticket drew
an immediate {"event":"cpu.resume"} - byte-identical to the broadcast that fires
when the game actually resumes. A client waiting for that broadcast concluded
the game was running while it was still stopped. Before, it correctly got
nothing until the resume really happened.
input.buttons.press is broken even with a ticket: it answers with the request's
own event name *and* ticket once the button has been held for the requested
frames, so the acknowledgement was indistinguishable from the real completion
and a client resolved on the first of the two. The claim in that commit that
the two are easy to tell apart was simply wrong for this handler.
So the acknowledgement is now off by default - the wire behaviour for every
existing client is exactly what it was - and a client that wants it asks, with
client.config.set {"acknowledgeDeferred": true}. It then arrives as its own
event rather than an echo:
-> {"event":"cpu.resume","ticket":7}
<- {"event":"deferred","for":"cpu.resume","ticket":7}
<- {"event":"cpu.resume"}
which is unambiguous in both cases above. That still gets the original goal -
correlating any request to a reply without hardcoding which events answer
immediately, including ones added later - just without imposing it on clients
that never asked.
Also documents the ticket convention this rests on: send one when you care
about the answer, leave it off to say you aren't waiting. wsdbg followed that
convention badly, silently inserting a ticket into a raw JSON line that
deliberately omitted one; it now sends raw lines exactly as written and simply
doesn't wait on those. It opts into acknowledgements at connect, so --sync
keeps working.
pspautotests 314/314, UnitTest 55/55.
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.
|
|
//
|
|
// No immediate response (only a "deferred" event, if the client asked for those via
|
|
// client.config.set). 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 (only a "deferred" event, if the client asked for those via
|
|
// client.config.set). Once CPU is running again, 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, ®)) {
|
|
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));
|
|
});
|
|
}
|