diff --git a/Core/CoreParameter.h b/Core/CoreParameter.h index 2e8e0c090b..f09f9d0c55 100644 --- a/Core/CoreParameter.h +++ b/Core/CoreParameter.h @@ -35,6 +35,10 @@ enum class FPSLimit { CUSTOM1 = 1, CUSTOM2 = 2, ANALOG = 3, + // Set over the WebSocket debugger (game.speed.set). Deliberately its own mode instead of + // reusing CUSTOM1/2: those read their rate from g_Config, which is persisted per game, so a + // debugger session would permanently overwrite the user's configured alternative speed. + DEBUGGER = 4, }; class FileLoader; @@ -77,6 +81,10 @@ struct CoreParameter { bool fastForward = false; FPSLimit fpsLimit = FPSLimit::NORMAL; int analogFpsLimit = 0; + // Target FPS for FPSLimit::DEBUGGER. Lives here rather than in g_Config on purpose - see the + // enum. Resets with the rest of this struct on every boot, so a debugger can't leave a game + // permanently slowed down. + int debuggerFpsLimit = 0; bool updateRecent = true; diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index cf958709aa..a9c1c8508d 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -36,8 +36,15 @@ #include "Core/Debugger/WebSocket/GameSubscriber.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" #include "Core/ELF/ParamSFO.h" +#include "Core/HLE/sceDisplay.h" +#include "Core/RetroAchievements.h" #include "Core/System.h" +// Declared rather than included from Core/HLE/sceNet.h, which reaches winsock and so windows.h +// through proAdhoc.h - and that redefines the OPTIONAL macro that collides with +// DebuggerParamType::OPTIONAL, exactly the problem the note above is avoiding. +bool NetworkAllowSpeedControl(); + static uint32_t GetOwnProcessID() { #if PPSSPP_PLATFORM(UWP) return (uint32_t)GetCurrentProcessId(); @@ -51,6 +58,8 @@ static uint32_t GetOwnProcessID() { DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map) { map["game.reset"] = &WebSocketGameReset; map["game.status"] = &WebSocketGameStatus; + map["game.speed.get"] = &WebSocketGameSpeedGet; + map["game.speed.set"] = &WebSocketGameSpeedSet; map["version"] = &WebSocketVersion; return nullptr; @@ -116,6 +125,122 @@ void WebSocketGameStatus(DebuggerRequest &req) { }); } +// Percentages are relative to 60 FPS, matching how the in-app settings present the alternative +// speeds (see GameSettingsScreen.cpp) - so a client and the UI agree on what "200%" means. +static int PercentToFps(int percent) { + return (percent * 60) / 100; +} + +static int FpsToPercent(int fps) { + return (fps * 100) / 60; +} + +// Fails the request if something else owns the speed right now, rather than accepting it and +// having FrameTimingLimit() quietly ignore it - silently doing nothing is the worst outcome for +// an automation client. +static bool CheckSpeedControlAllowed(DebuggerRequest &req) { + if (Achievements::HardcoreModeActive()) { + req.Fail("Speed control is not allowed in RetroAchievements hardcore mode"); + return false; + } + if (!NetworkAllowSpeedControl()) { + req.Fail("Speed control is not allowed while connected to a network game"); + return false; + } + return true; +} + +static void WriteSpeedState(JsonWriter &json) { + json.writeBool("fastForward", PSP_CoreParameter().fastForward); + if (PSP_CoreParameter().fpsLimit == FPSLimit::DEBUGGER) + json.writeInt("percent", FpsToPercent(PSP_CoreParameter().debuggerFpsLimit)); + else + json.writeNull("percent"); + json.writeInt("limitFps", __DisplayGetFrameTimingLimit()); +} + +// Request the current emulation speed (game.speed.get) +// +// No parameters. +// +// Response (same event name): +// - fastForward: boolean, whether unlimited fast-forward is on. +// - percent: null, or the debugger's speed override as a percentage of 60 FPS. +// - limitFps: the frame rate throttling is actually aiming for, after fastForward, percent, the +// user's own alternative-speed hotkeys and any restrictions have all been applied. 0 means +// unlimited. This is the ground truth - prefer it over inferring from the other two. +void WebSocketGameSpeedGet(DebuggerRequest &req) { + // Route the reads to the CPU thread - these are what the frame timing consumes there. + Core_RunOnCPUThread([&] { + JsonWriter &json = req.Respond(); + WriteSpeedState(json); + }); +} + +// Change the emulation speed (game.speed.set) +// +// Parameters (at least one required): +// - fastForward: optional boolean, run unlimited. Wins over 'percent' while on, same as the +// in-app fast-forward key. +// - percent: optional integer percentage of 60 FPS (100 = normal, 200 = double speed, 50 = half), +// or null to drop the override and go back to the game's normal rate. Must be at least 1 - +// use fastForward for unlimited rather than 0, so there's one way to say it. +// +// Response (same event name): the same fields as game.speed.get, after applying the change. +// +// Fails if RetroAchievements hardcore mode is active, or if connected to a network game without +// "allow speed control while connected" - in either case the setting would be ignored. +// +// Note this is a separate channel from the user's own alternative speeds (the CUSTOM1/CUSTOM2 +// hotkeys and their settings): it deliberately doesn't touch g_Config, which is persisted per +// game, so a debugger session can't permanently change what the user configured. +void WebSocketGameSpeedSet(DebuggerRequest &req) { + const bool hasFastForward = req.HasParam("fastForward"); + const bool hasPercent = req.HasParam("percent"); + if (!hasFastForward && !hasPercent) + return req.Fail("Need at least one of 'fastForward' or 'percent'"); + + bool fastForward = false; + if (hasFastForward && !req.ParamBool("fastForward", &fastForward)) + return; + + // An explicit null clears the override; the parameter being absent leaves it alone. + const bool clearPercent = hasPercent && !req.HasParam("percent", true); + uint32_t percent = 0; + if (hasPercent && !clearPercent) { + if (!req.ParamU32("percent", &percent)) + return; + if (percent < 1 || percent > 10000) + return req.Fail("Parameter 'percent' must be between 1 and 10000"); + if (PercentToFps((int)percent) < 1) + return req.Fail("Parameter 'percent' is too small to produce a frame rate"); + } + + if (!CheckSpeedControlAllowed(req)) + return; + + // Route the writes to the CPU thread instead of poking at them directly from this WebSocket + // handler thread - see Core_RunOnCPUThread() in Core.h. + Core_RunOnCPUThread([&] { + if (hasFastForward) + PSP_CoreParameter().fastForward = fastForward; + + if (clearPercent) { + // Only stand down from a limit we set ourselves - the user may have an alternative + // speed of their own active, and that isn't ours to cancel. + if (PSP_CoreParameter().fpsLimit == FPSLimit::DEBUGGER) + PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL; + PSP_CoreParameter().debuggerFpsLimit = 0; + } else if (hasPercent) { + PSP_CoreParameter().debuggerFpsLimit = PercentToFps((int)percent); + PSP_CoreParameter().fpsLimit = FPSLimit::DEBUGGER; + } + + JsonWriter &json = req.Respond(); + WriteSpeedState(json); + }); +} + // Notify debugger version info (version) // // Parameters: diff --git a/Core/Debugger/WebSocket/GameSubscriber.h b/Core/Debugger/WebSocket/GameSubscriber.h index af43c692d0..68943c1af7 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.h +++ b/Core/Debugger/WebSocket/GameSubscriber.h @@ -23,4 +23,6 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map); void WebSocketGameReset(DebuggerRequest &req); void WebSocketGameStatus(DebuggerRequest &req); +void WebSocketGameSpeedGet(DebuggerRequest &req); +void WebSocketGameSpeedSet(DebuggerRequest &req); void WebSocketVersion(DebuggerRequest &req); diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp index decd6a36aa..54b1400033 100644 --- a/Core/HLE/sceDisplay.cpp +++ b/Core/HLE/sceDisplay.cpp @@ -384,9 +384,15 @@ static int FrameTimingLimit() { return fixRate(g_Config.iFpsLimit2); if (PSP_CoreParameter().fpsLimit == FPSLimit::ANALOG) return fixRate(PSP_CoreParameter().analogFpsLimit); + if (PSP_CoreParameter().fpsLimit == FPSLimit::DEBUGGER) + return fixRate(PSP_CoreParameter().debuggerFpsLimit); return framerate; } +int __DisplayGetFrameTimingLimit() { + return FrameTimingLimit(); +} + static bool FrameTimingThrottled() { return FrameTimingLimit() != 0; } diff --git a/Core/HLE/sceDisplay.h b/Core/HLE/sceDisplay.h index eef5d2c9da..edb3bc6b23 100644 --- a/Core/HLE/sceDisplay.h +++ b/Core/HLE/sceDisplay.h @@ -34,6 +34,10 @@ void __DisplaySetFramebuf(u32 topaddr, int linesize, int pixelformat, int sync); // Call this when resuming to avoid a small speedup burst void __DisplaySetWasPaused(); +// The frame rate throttling is actually aiming for right now, after fast-forward, the alternative +// speeds, and the netplay/achievement restrictions have all had their say. 0 means unlimited. +int __DisplayGetFrameTimingLimit(); + void Register_sceDisplay_driver(); void __DisplayWaitForVblanks(const char* reason, int vblanks, bool callbacks = false); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index f3e789ee47..f259aab430 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -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` |