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
This commit is contained in:
Henrik Rydgård
2026-08-18 09:32:04 +02:00
co-authored by Claude Opus 5
parent 38cdc8bf68
commit 8120338530
7 changed files with 160 additions and 1 deletions
@@ -16,6 +16,7 @@
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <cmath>
#include <cstdlib>
#include <limits>
#include "Common/Data/Text/Parsers.h"
@@ -173,6 +174,41 @@ bool DebuggerRequest::ParamU32(const char *name, uint32_t *out, bool allowFloatB
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;