diff --git a/Core/Core.cpp b/Core/Core.cpp index e058a8573a..f7cc149cbc 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -712,6 +712,22 @@ void Core_Break(BreakReason reason, u32 relatedAddress, const BreakpointHit *hit System_Notify(SystemNotification::DEBUG_MODE_CHANGE); } +void Core_ReportBreakWithoutRunning(BreakReason reason, u32 relatedAddress) { + if (coreState != CORE_STEPPING_CPU) { + return; + } + std::lock_guard lock(g_stepMutex); + if (!g_cpuStepCommand.empty()) { + // A step is about to run, and its own stop will be reported. + return; + } + g_breakReason = reason; + g_breakHit = BreakpointHit{}; + g_cpuStepCommand.reason = reason; + g_cpuStepCommand.relatedAddr = relatedAddress; + steppingCounter++; +} + // Free-threaded (or at least should be) void Core_Resume() { // If the current PC is on a breakpoint, the user doesn't want to do nothing. diff --git a/Core/Core.h b/Core/Core.h index 0645b5f4b9..6581abaa28 100644 --- a/Core/Core.h +++ b/Core/Core.h @@ -117,6 +117,9 @@ struct BreakpointHit { // hit is optional detail for the breakpoint kinds, forwarded to the debugger. Only stored when // the break actually takes effect, so a rejected Core_Break() can't leave a stale one behind. void Core_Break(BreakReason reason, u32 relatedAddress = 0, const BreakpointHit *hit = nullptr); +// While stepping: reports a stop for reason without running, for a run whose stop condition already +// holds (cpu.runUntilTime with a time that's already passed), so a debugger waiting for the stop gets one. +void Core_ReportBreakWithoutRunning(BreakReason reason, u32 relatedAddress = 0); // Resumes execution. Works both when stepping the CPU and the GE. void Core_Resume(); diff --git a/Core/CoreTiming.cpp b/Core/CoreTiming.cpp index b64b877c84..a7f325e88f 100644 --- a/Core/CoreTiming.cpp +++ b/Core/CoreTiming.cpp @@ -567,6 +567,9 @@ void DoState(PointerWrap &p) { return; // States are saved between slices. otherCPUsRan = 0; + // A debugger's run-until deadline keeps the time it had left, not its absolute time, which + // belongs to the timeline being replaced. + const s64 deadlineLeftUs = p.mode == PointerWrap::MODE_READ && breakDeadlineUs ? breakDeadlineUs - (s64)GetGlobalTimeUs() : 0; int n = (int)event_types.size(); int current = n; @@ -632,7 +635,9 @@ void DoState(PointerWrap &p) { lastGlobalTimeUs = 0; } if (p.mode == PointerWrap::MODE_READ) { - // A debugger's run-until deadline is in emulated us, and the ticks it maps to just changed. + if (breakDeadlineUs) { + breakDeadlineUs = (s64)GetGlobalTimeUs() + std::max(deadlineLeftUs, (s64)0); + } RecomputeBreakDeadline(); } diff --git a/Core/Debugger/WebSocket/SteppingSubscriber.cpp b/Core/Debugger/WebSocket/SteppingSubscriber.cpp index e9f5493e99..954176aa61 100644 --- a/Core/Debugger/WebSocket/SteppingSubscriber.cpp +++ b/Core/Debugger/WebSocket/SteppingSubscriber.cpp @@ -293,6 +293,9 @@ void WebSocketSteppingState::RunUntil(DebuggerRequest &req) { // - 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. +// A time that has already passed (relativeUs 0, say) doesn't run at all: the response and the +// cpu.stepping event come right away. A savestate loaded during the run moves the deadline along +// with the clock, so it still runs for the time that was left. void WebSocketSteppingState::RunUntilTime(DebuggerRequest &req) { if (!currentDebugMIPS->isAlive()) { return req.Fail("CPU not started"); @@ -318,15 +321,14 @@ void WebSocketSteppingState::RunUntilTime(DebuggerRequest &req) { 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; + // Already there. A client waiting for the stop would otherwise never get one. + Core_ReportBreakWithoutRunning(BreakReason::RunUntilTime, currentMIPS->pc); + } else { + CoreTiming::SetBreakDeadlineUs(targetUs); + PrepareResume(); + Core_Resume(); } - CoreTiming::SetBreakDeadlineUs(targetUs); - - PrepareResume(); - Core_Resume(); - JsonWriter &json = req.Respond(); json.writeFloat("targetUs", (double)targetUs); json.writeFloat("us", (double)nowUs); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 32908c5ba2..56b2b1195f 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -273,7 +273,7 @@ file - this is just an index. |---|---|---| | Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game (any slot in headless, otherwise one the app's slot count shows), applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.state.rewind` (the same for the newest rewind snapshot, which it keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `game.state.verify` (the same for checking that saving the current state and reading it back agree; all but this one are refused in a session booted into the XMB, whose discs come and go), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `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.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` | +| 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; a time already passed, such as `relativeUs` 0, stops right away without running, and a savestate loaded during the run keeps the time that was left), `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`. The numeric ones report the result as both `value` and `uintValue` - the latter is what `cpu.getReg`/`cpu.getAllRegs` call it, so a client can read either without caring which event answered | `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` |