Debugger: Time input.buttons.press in emulated vblanks

The release was counted down on the WebSocket thread, one step per poll
of host time however many vblanks had passed, so how long a scripted
press lasted depended on how fast the emulator ran, and scripted runs
went different ways. sceCtrl now releases it after that many vblank
samples, on the emulator thread; the debugger only reports when it's done.

Also: wsdbg's :screenshot works in headless with Vulkan.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-30 09:58:37 -06:00
1 parent 00c9a2d389
commit a974440c79
4 files changed
+73 -15

No files matched your search

+12 -14
View File
@@ -70,12 +70,12 @@ protected:
std::string ticket; std::string ticket;
uint32_t button; uint32_t button;
uint32_t duration; uint32_t duration;
int pressId;
std::string Event(); std::string Event();
}; };
std::vector<PressInfo> pressTickets_; std::vector<PressInfo> pressTickets_;
int lastCounter_ = -1;
}; };
std::string WebSocketInputState::PressInfo::Event() { std::string WebSocketInputState::PressInfo::Event() {
@@ -170,7 +170,8 @@ void WebSocketInputState::ButtonsSend(DebuggerRequest &req) {
// //
// Parameters: // Parameters:
// - button: required string indicating button name (see input.buttons.send.) // - 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. // Response (same event name) with no extra data once released.
void WebSocketInputState::ButtonsPress(DebuggerRequest &req) { void WebSocketInputState::ButtonsPress(DebuggerRequest &req) {
@@ -193,27 +194,24 @@ void WebSocketInputState::ButtonsPress(DebuggerRequest &req) {
} }
press.button = info->second; 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); pressTickets_.push_back(press);
} }
void WebSocketInputState::Broadcast(net::WebSocketServer *ws) { void WebSocketInputState::Broadcast(net::WebSocketServer *ws) {
int counter = __DisplayGetNumVblanks(); if (pressTickets_.empty())
if (pressTickets_.empty() || lastCounter_ == counter)
return; return;
lastCounter_ = counter; auto done = [](const PressInfo &press) -> bool {
return !__CtrlPressActive(press.pressId);
};
for (PressInfo &press : pressTickets_) { for (PressInfo &press : pressTickets_) {
press.duration--; if (done(press)) {
if (press.duration == -1) {
__CtrlUpdateButtons(0, press.button);
ws->Send(press.Event()); ws->Send(press.Event());
} }
} }
auto negative = [](const PressInfo &press) -> bool { pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), done), pressTickets_.end());
return press.duration < 0;
};
pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), negative), pressTickets_.end());
} }
static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) { static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) {
+57
View File
@@ -15,8 +15,10 @@
// Official git repository and contact information can be found at // Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <cmath> #include <cmath>
#include <mutex> #include <mutex>
#include <vector>
#include "Common/Serialize/Serializer.h" #include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h" #include "Common/Serialize/SerializeFuncs.h"
@@ -205,6 +207,58 @@ u32 __CtrlReadLatch()
return ret; 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<TimedPress> timedPresses;
static int nextTimedPressId = 1;
int __CtrlPressFor(u32 buttons, int vblanks) {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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) void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear)
{ {
bitsToClear &= CTRL_MASK_USER; bitsToClear &= CTRL_MASK_USER;
@@ -343,6 +397,7 @@ void __CtrlVblank() {
// This always runs, so make sure we're in vblank mode. // This always runs, so make sure we're in vblank mode.
if (ctrlCycle == 0) if (ctrlCycle == 0)
__CtrlDoSample(); __CtrlDoSample();
__CtrlUpdateTimedPresses();
} }
static void __CtrlTimerUpdate(u64 userdata, int cyclesLate) static void __CtrlTimerUpdate(u64 userdata, int cyclesLate)
@@ -423,6 +478,8 @@ void __CtrlDoState(PointerWrap &p)
void __CtrlShutdown() void __CtrlShutdown()
{ {
waitingThreads.clear(); waitingThreads.clear();
std::lock_guard<std::mutex> guard(ctrlMutex);
timedPresses.clear();
} }
static u32 sceCtrlSetSamplingCycle(u32 cycle) static u32 sceCtrlSetSamplingCycle(u32 cycle)
+3
View File
@@ -73,6 +73,9 @@ void __CtrlVblank();
// Clears and sets selected buttons. NOTE: Clearing happens first. // Clears and sets selected buttons. NOTE: Clearing happens first.
void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear); 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. // 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.) // X and Y values should be from -1 to 1, inclusive, in a square (no need to force to a circle.)
+1 -1
View File
@@ -117,7 +117,7 @@ way took minutes per run. These run inside the one session instead:
|---|---| |---|---|
| `:sleep <seconds>` | Wall-clock pause. Keeps draining and printing messages while it waits. | | `:sleep <seconds>` | Wall-clock pause. Keeps draining and printing messages while it waits. |
| `:wait <event> [timeout]` | Blocks until a message with that event name arrives. Exits non-zero if it never does. | | `:wait <event> [timeout]` | Blocks until a message with that event name arrives. Exits non-zero if it never does. |
| `:screenshot <file.png>` | 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 <file.png>` | 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 <text>` | Prints text, for marking up a script's output. | | `:echo <text>` | Prints text, for marking up a script's output. |
| `# comment` | Ignored. | | `# comment` | Ignored. |