diff --git a/Core/Debugger/WebSocket/InputSubscriber.cpp b/Core/Debugger/WebSocket/InputSubscriber.cpp index 63a5e1103e..4eb324656a 100644 --- a/Core/Debugger/WebSocket/InputSubscriber.cpp +++ b/Core/Debugger/WebSocket/InputSubscriber.cpp @@ -70,12 +70,12 @@ protected: std::string ticket; uint32_t button; uint32_t duration; + int pressId; std::string Event(); }; std::vector pressTickets_; - int lastCounter_ = -1; }; std::string WebSocketInputState::PressInfo::Event() { @@ -170,7 +170,8 @@ void WebSocketInputState::ButtonsSend(DebuggerRequest &req) { // // Parameters: // - button: required string indicating button name (see input.buttons.send.) -// - duration: optional integer indicating frames to press for, defaults to 1. +// - duration: optional integer indicating frames to press for, defaults to 1. Counted in emulated +// vblanks: the game sees the button in exactly that many vblank samples. // // Response (same event name) with no extra data once released. void WebSocketInputState::ButtonsPress(DebuggerRequest &req) { @@ -193,27 +194,24 @@ void WebSocketInputState::ButtonsPress(DebuggerRequest &req) { } press.button = info->second; - __CtrlUpdateButtons(press.button, 0); + // Released by sceCtrl after that many vblanks of emulated time, so a scripted press lasts the + // same however fast the emulator runs. This thread only reports when it's over. + press.pressId = __CtrlPressFor(press.button, (int)press.duration); pressTickets_.push_back(press); } void WebSocketInputState::Broadcast(net::WebSocketServer *ws) { - int counter = __DisplayGetNumVblanks(); - if (pressTickets_.empty() || lastCounter_ == counter) + if (pressTickets_.empty()) return; - lastCounter_ = counter; - + auto done = [](const PressInfo &press) -> bool { + return !__CtrlPressActive(press.pressId); + }; for (PressInfo &press : pressTickets_) { - press.duration--; - if (press.duration == -1) { - __CtrlUpdateButtons(0, press.button); + if (done(press)) { ws->Send(press.Event()); } } - auto negative = [](const PressInfo &press) -> bool { - return press.duration < 0; - }; - pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), negative), pressTickets_.end()); + pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), done), pressTickets_.end()); } static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) { diff --git a/Core/HLE/sceCtrl.cpp b/Core/HLE/sceCtrl.cpp index 839143701f..fd098ccb52 100644 --- a/Core/HLE/sceCtrl.cpp +++ b/Core/HLE/sceCtrl.cpp @@ -15,8 +15,10 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#include #include #include +#include #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" @@ -205,6 +207,58 @@ u32 __CtrlReadLatch() return ret; } +// Presses held for a number of vblank samples (the debugger's input.buttons.press). Counted down on +// the emulator thread at each vblank, so how long a press lasts doesn't depend on how fast the +// emulator runs. Guarded by ctrlMutex; not saved. +struct TimedPress { + int id; + u32 buttons; + int samplesLeft; +}; +static std::vector timedPresses; +static int nextTimedPressId = 1; + +int __CtrlPressFor(u32 buttons, int vblanks) { + std::lock_guard guard(ctrlMutex); + buttons &= CTRL_MASK_USER; + ctrlCurrent.buttons |= buttons; + const int id = nextTimedPressId++; + timedPresses.push_back(TimedPress{ id, buttons, std::max(vblanks, 1) }); + return id; +} + +bool __CtrlPressActive(int id) { + std::lock_guard guard(ctrlMutex); + for (const TimedPress &press : timedPresses) { + if (press.id == id) { + return true; + } + } + return false; +} + +// After the vblank's sample, so a press of N vblanks is seen by N samples. +static void __CtrlUpdateTimedPresses() { + std::lock_guard guard(ctrlMutex); + if (timedPresses.empty()) { + return; + } + u32 released = 0; + for (TimedPress &press : timedPresses) { + if (--press.samplesLeft <= 0) { + released |= press.buttons; + } + } + timedPresses.erase(std::remove_if(timedPresses.begin(), timedPresses.end(), [](const TimedPress &press) { + return press.samplesLeft <= 0; + }), timedPresses.end()); + // Unless another press still holds them. + for (const TimedPress &press : timedPresses) { + released &= ~press.buttons; + } + ctrlCurrent.buttons &= ~released; +} + void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear) { bitsToClear &= CTRL_MASK_USER; @@ -343,6 +397,7 @@ void __CtrlVblank() { // This always runs, so make sure we're in vblank mode. if (ctrlCycle == 0) __CtrlDoSample(); + __CtrlUpdateTimedPresses(); } static void __CtrlTimerUpdate(u64 userdata, int cyclesLate) @@ -423,6 +478,8 @@ void __CtrlDoState(PointerWrap &p) void __CtrlShutdown() { waitingThreads.clear(); + std::lock_guard guard(ctrlMutex); + timedPresses.clear(); } static u32 sceCtrlSetSamplingCycle(u32 cycle) diff --git a/Core/HLE/sceCtrl.h b/Core/HLE/sceCtrl.h index f37325fae6..768c1f8f2d 100644 --- a/Core/HLE/sceCtrl.h +++ b/Core/HLE/sceCtrl.h @@ -73,6 +73,9 @@ void __CtrlVblank(); // Clears and sets selected buttons. NOTE: Clearing happens first. void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear); +// Presses buttons for a number of vblanks of emulated time; returns an id for __CtrlPressActive. +int __CtrlPressFor(u32 buttons, int vblanks); +bool __CtrlPressActive(int id); // Call this to set the position of an analog stick, ideally when it changes. // X and Y values should be from -1 to 1, inclusive, in a square (no need to force to a circle.) diff --git a/Tools/wsdbg/README.md b/Tools/wsdbg/README.md index e51eaf9486..84dbd34297 100644 --- a/Tools/wsdbg/README.md +++ b/Tools/wsdbg/README.md @@ -117,7 +117,7 @@ way took minutes per run. These run inside the one session instead: |---|---| | `:sleep ` | Wall-clock pause. Keeps draining and printing messages while it waits. | | `:wait [timeout]` | Blocks until a message with that event name arrives. Exits non-zero if it never does. | -| `:screenshot ` | Saves `gpu.buffer.screenshot` as a PNG, without printing the image data. Needs the CPU stopped. Use a native path (`C:/...`) on Windows. With headless, use `--graphics=software`: the Vulkan backend has no output image to read back there. | +| `:screenshot ` | Saves `gpu.buffer.screenshot` as a PNG, without printing the image data. Needs the CPU stopped. Use a native path (`C:/...`) on Windows. Works in headless with `--graphics=vulkan`. | | `:echo ` | Prints text, for marking up a script's output. | | `# comment` | Ignored. |