diff --git a/Core/Core.cpp b/Core/Core.cpp index 1aa508c030..f3eed66c79 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -594,6 +594,9 @@ void Core_Break(BreakReason reason, u32 relatedAddress) { // breakpoint, or lldb discarding the thread plan, on any stop. g_breakpoints.ClearTempBreakPoint(); + // Same reasoning for a cpu.runUntilTime deadline - it belonged to the run that just ended. + CoreTiming::SetBreakDeadlineUs(0); + g_breakReason = reason; g_cpuStepCommand.type = CPUStepType::None; g_cpuStepCommand.reason = reason; diff --git a/Core/CoreTiming.cpp b/Core/CoreTiming.cpp index 572545c5d0..5ff1812a78 100644 --- a/Core/CoreTiming.cpp +++ b/Core/CoreTiming.cpp @@ -60,6 +60,11 @@ alignas(16) static s64 globalTimer; static s64 idledCycles; static s64 lastGlobalTimeTicks; static s64 lastGlobalTimeUs; +// See SetBreakDeadlineUs. 0 = none. Deliberately not saved in savestates - it belongs to a +// debugger session, not to the emulated machine. +static s64 breakDeadlineUs; +static s64 breakDeadlineTicks; +static void RecomputeBreakDeadline(); bool SetClockFrequencyHz(int cpuHz) { if (cpuHz <= 0) { @@ -79,6 +84,9 @@ bool SetClockFrequencyHz(int cpuHz) { CPU_HZ = cpuHz; + // The remaining time to a debugger deadline is now a different number of ticks. + RecomputeBreakDeadline(); + // TODO: Rescale times of scheduled events? __AudioCPUMHzChange(); return true; @@ -105,6 +113,26 @@ u64 GetGlobalTimeUs() { return lastGlobalTimeUs + usSinceLast; } +// Turns the microsecond deadline into the tick count Advance() compares against. Has to be redone +// whenever the clock frequency changes, since that changes how many ticks the remaining time is. +static void RecomputeBreakDeadline() { + if (!breakDeadlineUs) { + breakDeadlineTicks = 0; + return; + } + const s64 remainingUs = breakDeadlineUs - (s64)GetGlobalTimeUs(); + breakDeadlineTicks = (s64)GetTicks(currentMIPS) + (remainingUs > 0 ? usToCycles(remainingUs) : 0); +} + +void SetBreakDeadlineUs(u64 us) { + breakDeadlineUs = (s64)us; + RecomputeBreakDeadline(); +} + +u64 GetBreakDeadlineUs() { + return (u64)breakDeadlineUs; +} + u64 PeekGlobalTimeUs() { // Same sum as above without the rebasing, so this stays callable from a thread that isn't the // CPU thread. The rebasing exists purely to keep the multiply below from overflowing, and it @@ -193,6 +221,8 @@ void Init(MIPSState *mips) { idledCycles = 0; lastGlobalTimeTicks = 0; lastGlobalTimeUs = 0; + breakDeadlineUs = 0; + breakDeadlineTicks = 0; CPU_HZ = initialHz; } @@ -393,6 +423,14 @@ void Advance(MIPSState *mips) { globalTimer += cyclesExecuted; mips->downcount = slicelength; + // Debugger deadline - see SetBreakDeadlineTicks. Checked before the events so the break lands + // on the requested tick rather than after whatever the events do. + if (breakDeadlineTicks && globalTimer >= breakDeadlineTicks) { + breakDeadlineTicks = 0; + breakDeadlineUs = 0; + Core_Break(BreakReason::DebugBreak, mips->pc); + } + ProcessEvents(); if (!first) { @@ -411,6 +449,17 @@ void Advance(MIPSState *mips) { slicelength += diff; mips->downcount += diff; } + + // Shorten the slice so we come back exactly on the deadline instead of up to a whole slice + // past it - the point of cpu.runUntilTime is that it stops at a reproducible place. + if (breakDeadlineTicks) { + const s64 remaining = breakDeadlineTicks - globalTimer; + if (remaining > 0 && remaining < slicelength) { + const int diff = (int)remaining - slicelength; + slicelength += diff; + mips->downcount += diff; + } + } } void LogPendingEvents() { diff --git a/Core/CoreTiming.h b/Core/CoreTiming.h index 6e62aa4a59..bc52e6cc38 100644 --- a/Core/CoreTiming.h +++ b/Core/CoreTiming.h @@ -100,6 +100,15 @@ namespace CoreTiming { // for the debugger's status poll. Same value, just doesn't help the next call along. u64 PeekGlobalTimeUs(); + // Debugger support (cpu.runUntilTime): break as soon as emulated time reaches this many + // microseconds. Advance() shortens its slice to land exactly on it rather than overshooting, + // so this stops at a reproducible point rather than "somewhere in the next frame". 0 clears it. + // Core_Break() clears it too, so a deadline can't outlive the run it belonged to. + // Deliberately in microseconds and not ticks: games change the CPU clock mid-run (CrossCraft + // Classic goes 222 -> 333MHz during startup), so a tick count fixed up front drifts. + void SetBreakDeadlineUs(u64 us); + u64 GetBreakDeadlineUs(); + // Returns the event_type identifier. int RegisterEvent(const char *name, TimedCallback callback); diff --git a/Core/Debugger/WebSocket/SteppingSubscriber.cpp b/Core/Debugger/WebSocket/SteppingSubscriber.cpp index 5086268156..bf834008f7 100644 --- a/Core/Debugger/WebSocket/SteppingSubscriber.cpp +++ b/Core/Debugger/WebSocket/SteppingSubscriber.cpp @@ -21,6 +21,7 @@ #include "Core/Debugger/WebSocket/SteppingSubscriber.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" #include "Core/Core.h" +#include "Core/CoreTiming.h" #include "Core/HLE/HLE.h" #include "Core/HLE/sceKernelThread.h" #include "Core/MIPS/MIPSDebugInterface.h" @@ -40,6 +41,7 @@ struct WebSocketSteppingState : public DebuggerSubscriber { void Over(DebuggerRequest &req); void Out(DebuggerRequest &req); void RunUntil(DebuggerRequest &req); + void RunUntilTime(DebuggerRequest &req); void HLE(DebuggerRequest &req); protected: @@ -54,6 +56,7 @@ DebuggerSubscriber *WebSocketSteppingInit(DebuggerEventHandlerMap &map) { map["cpu.stepOver"] = [p](DebuggerRequest &req) { p->Over(req); }; map["cpu.stepOut"] = [p](DebuggerRequest &req) { p->Out(req); }; map["cpu.runUntil"] = [p](DebuggerRequest &req) { p->RunUntil(req); }; + map["cpu.runUntilTime"] = [p](DebuggerRequest &req) { p->RunUntilTime(req); }; map["cpu.nextHLE"] = [p](DebuggerRequest &req) { p->HLE(req); }; return p; } @@ -275,6 +278,62 @@ void WebSocketSteppingState::RunUntil(DebuggerRequest &req) { }); } +// Run until a point in emulated time (cpu.runUntilTime) +// +// The counterpart to cpu.runUntil for "let the game get N seconds in", which is what lining a +// scripted repro up with a wall-clock description of a bug needs. Polling cpu.status in a loop +// does the same job far more slowly and lands somewhere different every run; this stops on the +// requested tick, so the same script reaches the same place every time. +// +// Parameters (exactly one of): +// - us: absolute emulated microseconds to run until, as reported by cpu.status. +// - relativeUs: microseconds to run for, measured from now. +// +// Response (same event name): +// - targetUs: the absolute emulated time it will stop at. +// - us: emulated time right now. +// A cpu.stepping event follows once it gets there. Note that anything else that stops the CPU +// first - a breakpoint, an exception - cancels the deadline, same as it cancels a step. +void WebSocketSteppingState::RunUntilTime(DebuggerRequest &req) { + if (!currentDebugMIPS->isAlive()) { + return req.Fail("CPU not started"); + } + + const bool absolute = req.HasParam("us"); + if (absolute == req.HasParam("relativeUs")) { + return req.Fail("Pass exactly one of 'us' or 'relativeUs'"); + } + + double requested = 0.0; + if (!req.ParamF64(absolute ? "us" : "relativeUs", &requested)) { + // Error already sent. + return; + } + if (requested < 0.0) { + return req.Fail("Time must not be negative"); + } + + // Route the actual 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([&] { + const u64 nowUs = CoreTiming::GetGlobalTimeUs(); + const u64 targetUs = absolute ? (u64)requested : nowUs + (u64)requested; + if (targetUs <= nowUs) { + req.Fail("Target time has already passed"); + return; + } + + CoreTiming::SetBreakDeadlineUs(targetUs); + + PrepareResume(); + Core_Resume(); + + JsonWriter &json = req.Respond(); + json.writeFloat("targetUs", (double)targetUs); + json.writeFloat("us", (double)nowUs); + }); +} + // Jump after the next HLE call (cpu.nextHLE) // // No parameters. diff --git a/Core/Debugger/WebSocket/WebSocketUtils.cpp b/Core/Debugger/WebSocket/WebSocketUtils.cpp index 3ba5984f4b..c7a821e8ad 100644 --- a/Core/Debugger/WebSocket/WebSocketUtils.cpp +++ b/Core/Debugger/WebSocket/WebSocketUtils.cpp @@ -16,6 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include #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; diff --git a/Core/Debugger/WebSocket/WebSocketUtils.h b/Core/Debugger/WebSocket/WebSocketUtils.h index ba74d4c9f9..04161baf2e 100644 --- a/Core/Debugger/WebSocket/WebSocketUtils.h +++ b/Core/Debugger/WebSocket/WebSocketUtils.h @@ -94,6 +94,9 @@ struct DebuggerRequest { bool HasParam(const char *name, bool ignoreNull = false); bool ParamU32(const char *name, uint32_t *out, bool allowFloatBits = false, DebuggerParamType type = DebuggerParamType::REQUIRED); + // For quantities that don't fit in 32 bits, like emulated microseconds - JSON numbers are + // doubles anyway, which is exact well past any plausible session length. + bool ParamF64(const char *name, double *out, DebuggerParamType type = DebuggerParamType::REQUIRED); bool ParamBool(const char *name, bool *out, DebuggerParamType type = DebuggerParamType::REQUIRED); bool ParamString(const char *name, std::string *out, DebuggerParamType type = DebuggerParamType::REQUIRED); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 4599bf883b..deb9806e52 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -108,7 +108,7 @@ file - this is just an index. |---|---|---| | Game/version | `game.reset`, `game.status`, `version` | `GameSubscriber.cpp` | | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | -| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.nextHLE` | `SteppingSubscriber.cpp` | +| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` | | Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` | | Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write` | `MemorySubscriber.cpp` | | Memory search | `memory.search` - scan a range for a `u8`/`u16`/`u32`/`float` value or a `bytes` pattern (with an optional wildcard mask), for narrowing down where an unknown value lives (Cheat Engine style) | `MemorySubscriber.cpp` |