Debugger: add game.speed.get/set for fast-forward and a speed percentage

Both mechanisms already existed and just weren't reachable from the WebSocket
API: PSP_CoreParameter().fastForward for unlimited, and an FPSLimit mode plus a
target frame rate for everything else. FrameTimingLimit() in sceDisplay.cpp is
where they all resolve to a single number.

The one thing worth being careful about is which knob to drive. Reusing
CUSTOM1/CUSTOM2 - the user's own alternative speeds - would have meant writing
g_Config.iFpsLimit1, which is persisted per game, so a debugger session would
permanently overwrite whatever speed the user had configured. Same class of
mistake as a debug setting leaking into the saved config. So this gets its own
FPSLimit::DEBUGGER mode and a debuggerFpsLimit field on CoreParameter, which
isn't persisted and is value-initialized on every boot. Two things fall out for
free: the analog-speed handler already backs off for any mode it doesn't own
(EmuScreen.cpp), and a debugger can't leave a game slowed down after a restart.

Percentages are relative to 60 FPS, the same convention GameSettingsScreen uses
when presenting the alternative speeds, so "200%" means one thing across the
app. Unlimited is fastForward rather than percent 0, so there's a single way to
express it.

The response reports limitFps straight from FrameTimingLimit(), exposed for the
purpose. That's the number the frame timing actually consumes, so a client never
has to reconstruct the interaction between fast-forward, this override and the
user's own hotkeys - which is exactly the sort of thing that goes stale.

Requests fail rather than being quietly ignored when something else owns the
speed: achievements hardcore mode, or netplay without the "allow speed control
while connected" option. Being ignored with a successful response is the worst
outcome for an automation client.

Verified against a running headless instance: percent 200 with fast-forward off
resolves to limitFps 120, fast-forward takes it to 0 while remembering the 200
underneath, an explicit null clears it, and out-of-range or empty requests are
rejected. The throttle *behaviour* is not verified end to end - a debug,
software-rendered headless build runs this game at about 1% of real time, so it
never reaches any of these targets and the limit can't be observed. That path is
shared with the existing CUSTOM1/CUSTOM2 speeds and unchanged.

sceNet.h is forward-declared rather than included: it reaches windows.h through
proAdhoc.h, which redefines the OPTIONAL macro that collides with
DebuggerParamType::OPTIONAL - the hazard this file's header comment already
warns about.

pspautotests 314/314, UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-18 13:42:13 +02:00
1 parent a0fbf32469
commit 44b832f755
6 files changed
+184 -1

No files matched your search

+39 -1
View File
@@ -132,6 +132,44 @@ way for periodic GPU stats. `client.config.set` in the same file carries
per-connection settings that aren't about broadcasts - currently just
`acknowledgeDeferred`, described under "Message protocol" above.
### Emulation speed
`game.speed.set` drives two independent things:
```json
-> { "event": "game.speed.set", "fastForward": true } // unlimited
-> { "event": "game.speed.set", "percent": 200 } // double speed
-> { "event": "game.speed.set", "percent": 25 } // quarter speed
-> { "event": "game.speed.set", "percent": null } // drop the override
<- { "event": "game.speed.set", "fastForward": false, "percent": 200, "limitFps": 120 }
```
Percentages are relative to 60 FPS, matching how the in-app settings present the
alternative speeds - so `200` means the same thing in both places. `percent`
must be at least 1; use `fastForward` for unlimited rather than `0`, so there's
only one way to say it. `fastForward` wins while it is on, and `percent` is
remembered underneath it.
`limitFps` in the response is the frame rate throttling is actually aiming for
once everything - fast-forward, this override, the user's own alternative-speed
hotkeys - has been taken into account, with `0` meaning unlimited. It comes
straight from the function the frame timing itself consumes, so prefer reading
it over inferring the result from the other two fields.
This is a **separate channel** from the user's own alternative speeds. It
deliberately never touches `g_Config`, whose speed settings are persisted per
game, so a debugger session can't permanently change what the user configured.
Clearing the override also only stands down from a limit set through this event,
never from one the user set themselves. It resets on every game boot.
The request **fails** rather than being silently ignored when something else
owns the speed: RetroAchievements hardcore mode, or being connected to a network
game without "allow speed control while connected".
Headless sets fast-forward at startup and never turns it off on its own, so
`game.speed.set` works there too - turning fast-forward off makes headless
throttle to real time, which is occasionally useful for a wall-clock repro.
### Breakpoint hits
`cpu.breakpoint.hit` fires every time a breakpoint's condition passes and it has
@@ -206,7 +244,7 @@ file - this is just an index.
| Category | Events | File |
|---|---|---|
| Game/version | `game.reset`, `game.status`, `version` | `GameSubscriber.cpp` |
| Game/version | `game.reset`, `game.status`, `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `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` |
| 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` |