Files
ppsspp/Core/Debugger/WebSocket/WebSocketUtils.cpp
Henrik RydgårdandClaude Opus 5 8120338530 Add cpu.runUntilTime: run to a point in emulated time and break there
Lining a scripted repro up with a bug report ("about five seconds in, press
X") had no support at all. The only way to do it was to poll cpu.status in a
loop from the client, which is slow - a process spawn per poll, minutes for a
single run - and lands somewhere different every time, so the repro isn't one.

cpu.runUntilTime takes either an absolute `us` (as reported by cpu.status) or
`relativeUs` from now, resumes, and breaks when emulated time gets there. It
answers immediately with the target, and the usual cpu.stepping event follows
when it arrives. Anything else that stops the CPU first - a breakpoint, an
exception - cancels the deadline, the same way it cancels a pending step.

The deadline is held in microseconds, not ticks, and recomputed whenever
SetClockFrequencyHz() runs. Converting to a tick count once up front looks
right and isn't: games change the CPU clock while running, and CrossCraft
Classic goes 222 -> 333MHz during startup, which made a request for 3.0s stop
at 2.24s. With the recompute it stops at exactly 3000000us. Advance() also
shortens its slice to land on the deadline instead of up to a slice past it,
so repeated runs stop at the same instruction rather than somewhere in the
following frame.

Nothing is added to CoreTiming's event list, so savestates are unaffected -
the deadline is debugger session state and isn't serialized.

Also adds DebuggerRequest::ParamF64, since microseconds outgrow 32 bits after
about 71 minutes. Like the other Param* helpers it fails loudly on a missing
or unparseable value rather than defaulting.

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

325 lines
8.9 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 <cmath>
#include <cstdlib>
#include <limits>
#include "Common/Data/Text/Parsers.h"
#include "Common/StringUtils.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
#include "Core/MemMap.h"
inline void DebuggerJsonAddTicket(JsonWriter &writer, const JsonGet &data) {
const JsonNode *value = data.get("ticket");
if (value)
writer.writeRaw("ticket", json_stringify(value));
}
JsonWriter &DebuggerRequest::Respond() {
writer_.begin();
writer_.writeString("event", name);
DebuggerJsonAddTicket(writer_, data);
responseBegun_ = true;
return writer_;
}
bool DebuggerRequest::Finish() {
if (responseBegun_ && !responseSent_) {
writer_.end();
if (responsePartial_)
ws->AddFragment(true, writer_.str());
else
ws->Send(writer_.str());
responseBegun_ = false;
responseSent_ = true;
responsePartial_ = false;
}
return responseSent_;
}
void DebuggerRequest::Flush() {
ws->AddFragment(false, writer_.flush());
responsePartial_ = true;
}
static bool U32FromString(const char *str, uint32_t *out, bool allowFloat) {
if (TryParse(str, out))
return true;
// Now let's try signed (the above parses only positive.)
if (str[0] == '-' && TryParse(&str[1], out)) {
*out = static_cast<uint32_t>(-static_cast<int>(*out));
return true;
}
// We have to try float last because we use float bits, so 1.0 != 1.
if (allowFloat) {
union {
uint32_t u;
float f;
} bits;
if (TryParse(str, &bits.f)) {
*out = bits.u;
return true;
}
if (!strcasecmp(str, "nan")) {
*out = 0x7FC00000;
return true;
} else if (!strcasecmp(str, "infinity") || !strcasecmp(str, "inf")) {
*out = 0x7F800000;
return true;
} else if (!strcasecmp(str, "-infinity") || !strcasecmp(str, "-inf")) {
*out = 0xFF800000;
return true;
}
}
return false;
}
bool DebuggerRequest::HasParam(const char *name, bool ignoreNull) {
const JsonNode *node = data.get(name);
if (!node) {
return false;
}
if (node->value.getTag() == JSON_NULL) {
return !ignoreNull;
}
return true;
}
bool DebuggerRequest::ParamU32(const char *name, uint32_t *out, bool allowFloatBits, DebuggerParamType type) {
bool allowLoose = type == DebuggerParamType::REQUIRED_LOOSE || type == DebuggerParamType::OPTIONAL_LOOSE;
bool required = type == DebuggerParamType::REQUIRED || type == DebuggerParamType::REQUIRED_LOOSE;
const JsonNode *node = data.get(name);
if (!node) {
if (required)
Fail(StringFromFormat("Missing '%s' parameter", name));
return !required;
}
auto tag = node->value.getTag();
if (tag == JSON_NUMBER) {
double val = node->value.toNumber();
bool isInteger = trunc(val) == val;
if (!isInteger && !allowLoose) {
// JSON doesn't give a great way to differentiate ints and floats.
// Let's play it safe and require a string.
if (allowFloatBits)
Fail(StringFromFormat("Could not parse '%s' parameter: use a string for non integer values", name));
else
Fail(StringFromFormat("Could not parse '%s' parameter: integer required", name));
return false;
} else if (!isInteger && allowFloatBits) {
union {
float f;
uint32_t u;
} bits = { (float)val };
*out = bits.u;
return true;
}
if (val < 0 && val >= std::numeric_limits<int32_t>::min()) {
// Convert to unsigned representation.
*out = (uint32_t)(int32_t)val;
return true;
} else if (val >= 0 && val <= std::numeric_limits<uint32_t>::max()) {
*out = (uint32_t)val;
return true;
} else if (allowLoose) {
*out = val >= 0 ? std::numeric_limits<uint32_t>::max() : std::numeric_limits<uint32_t>::min();
return true;
}
if (allowFloatBits)
Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range (use string for float)", name));
else
Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range", name));
return false;
}
if (tag != JSON_STRING) {
if (type == DebuggerParamType::REQUIRED || tag != JSON_NULL) {
Fail(StringFromFormat("Invalid '%s' parameter type", name));
return false;
}
return true;
}
if (U32FromString(node->value.toString(), out, allowFloatBits))
return true;
if (allowFloatBits)
Fail(StringFromFormat("Could not parse '%s' parameter: number expected", name));
else
Fail(StringFromFormat("Could not parse '%s' parameter: integer required", name));
return false;
}
bool DebuggerRequest::ParamF64(const char *name, double *out, DebuggerParamType type) {
const bool required = type == DebuggerParamType::REQUIRED || type == DebuggerParamType::REQUIRED_LOOSE;
const JsonNode *node = data.get(name);
if (!node) {
if (required)
Fail(StringFromFormat("Missing '%s' parameter", name));
return !required;
}
const auto tag = node->value.getTag();
if (tag == JSON_NUMBER) {
*out = node->value.toNumber();
return true;
}
if (tag == JSON_STRING) {
// Same escape hatch as the integer params: a string for values a JSON number would mangle.
const char *s = node->value.toString();
char *end = nullptr;
const double val = strtod(s, &end);
if (end != s && *end == '\0') {
*out = val;
return true;
}
Fail(StringFromFormat("Could not parse '%s' parameter: number required", name));
return false;
}
if (tag == JSON_NULL && !required) {
return false;
}
Fail(StringFromFormat("Invalid '%s' parameter type", name));
return false;
}
bool DebuggerRequest::ParamBool(const char *name, bool *out, DebuggerParamType type) {
bool allowLoose = type == DebuggerParamType::REQUIRED_LOOSE || type == DebuggerParamType::OPTIONAL_LOOSE;
bool required = type == DebuggerParamType::REQUIRED || type == DebuggerParamType::REQUIRED_LOOSE;
const JsonNode *node = data.get(name);
if (!node) {
if (required)
Fail(StringFromFormat("Missing '%s' parameter", name));
return !required;
}
auto tag = node->value.getTag();
if (tag == JSON_NUMBER) {
double val = node->value.toNumber();
if (val == 1.0 || val == 0.0 || allowLoose) {
*out = val != 0.0;
return true;
}
Fail(StringFromFormat("Could not parse '%s' parameter: should be true/1 or false/0", name));
return false;
}
if (tag == JSON_TRUE) {
*out = true;
return true;
}
if (tag == JSON_FALSE) {
*out = false;
return true;
}
if (tag != JSON_STRING) {
if (type == DebuggerParamType::REQUIRED || tag != JSON_NULL) {
Fail(StringFromFormat("Invalid '%s' parameter type", name));
return false;
}
return true;
}
const std::string s = node->value.toString();
if (s == "1" || s == "true") {
*out = true;
return true;
}
if (s == "0" || s == "false" || (s.empty() && allowLoose)) {
*out = false;
return true;
}
if (allowLoose) {
*out = true;
return true;
}
Fail(StringFromFormat("Could not parse '%s' parameter: boolean required", name));
return false;
}
bool DebuggerRequest::ParamString(const char *name, std::string *out, DebuggerParamType type) {
bool allowLoose = type == DebuggerParamType::REQUIRED_LOOSE || type == DebuggerParamType::OPTIONAL_LOOSE;
bool required = type == DebuggerParamType::REQUIRED || type == DebuggerParamType::REQUIRED_LOOSE;
const JsonNode *node = data.get(name);
if (!node) {
if (required)
Fail(StringFromFormat("Missing '%s' parameter", name));
return !required;
}
auto tag = node->value.getTag();
if (tag == JSON_STRING) {
*out = node->value.toString();
return true;
} else if (!allowLoose) {
if (required || tag != JSON_NULL) {
Fail(StringFromFormat("Invalid '%s' parameter type", name));
return false;
}
return true;
}
// For loose, let's allow a few things.
if (tag == JSON_TRUE) {
*out = "true";
return true;
} else if (tag == JSON_FALSE) {
*out = "false";
return true;
} else if (tag == JSON_NULL) {
if (required) {
out->clear();
}
return true;
} else if (tag == JSON_NUMBER) {
// Will have a decimal place, though.
*out = StringFromFormat("%f", node->value.toNumber());
return true;
}
Fail(StringFromFormat("Invalid '%s' parameter type", name));
return false;
}
uint32_t RoundMemAddressUp(uint32_t addr) {
if (addr < PSP_GetScratchpadMemoryBase())
return PSP_GetScratchpadMemoryBase();
else if (addr >= PSP_GetScratchpadMemoryEnd() && addr < PSP_GetVidMemBase())
return PSP_GetVidMemBase();
else if (addr >= PSP_GetVidMemEnd() && addr < PSP_GetKernelMemoryBase())
return PSP_GetKernelMemoryBase();
else if (addr >= PSP_GetUserMemoryEnd())
return PSP_GetScratchpadMemoryBase();
return addr;
}