From 2e2f6b5eeff3ff749da00184ee5ea736b079c2ca Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 30 Oct 2026 19:35:39 -0400 Subject: [PATCH] fix(pad): expire a held injected button when its scripted press ends A scripted button that answered a dialog stayed masked from the title until a title read sampled it released. When the title did not read during the release, a later scripted press of the same button was masked as well. The mask now keeps only the buttons the command timeline held without a break since the last read, so a press that ends, or a new command, clears it whether or not it was sampled. Co-Authored-By: Claude Opus 5.5 --- .../libScePad/include/InputCommandFile.hpp | 18 +++++++ core/libs/prx/libScePad/src/PadState.cpp | 13 +++-- core/libs/prx/libSceVideoOut/tests/PadSdl.cpp | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 172735fd62..0d2fd0bc2a 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -60,6 +60,24 @@ public: std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } + std::uint32_t HeldThrough(std::uint64_t since, std::uint64_t now) const { + if (events.empty() || since < started || now < since) return 0; + const auto from = since - started; + const auto to = now - started; + auto event = std::upper_bound(events.begin(), events.end(), from, + [](std::uint64_t time, const Event& candidate) { return time < candidate.start; }); + if (event == events.begin()) return 0; + --event; + std::uint32_t held = ~0u; + for (std::uint64_t covered = from; event != events.end() && event->start <= covered; ++event) { + if (covered >= event->end) return 0; + held &= event->input.buttons; + covered = event->end; + if (to < covered) return held; + } + return 0; + } + private: struct Event { std::uint64_t start = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index cf909cb70b..cabe3b3adf 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -19,6 +19,7 @@ namespace { constexpr std::uint32_t kInterceptedButton = 0x80000000; std::uint64_t injectedTimestamp = 0; std::uint32_t injectedHeldThroughCapture = 0; + std::uint64_t injectedHeldAt = 0; struct QueuedInput { PadInputState input; @@ -244,7 +245,7 @@ PadData BuildData(PadSlot& slot, const PadInputState& state, std::uint64_t chang return data; } -Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { +Pad::InputCommandFile& InjectedCommands() { static Pad::InputCommandFile commands([] { #ifdef _WIN32 const auto* path = _wgetenv(L"APS5_PAD_INPUT_FILE"); @@ -253,8 +254,12 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { #endif return path ? std::filesystem::path(path) : std::filesystem::path{}; }()); + return commands; +} + +Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { static Pad::InputCommandFile::Input lastInjected; - const auto injected = commands.Poll(now); + const auto injected = InjectedCommands().Poll(now); if (injected != lastInjected) { lastInjected = injected; slots[0].timestamp = now; @@ -266,7 +271,8 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { Pad::InputCommandFile::Input InjectedTitleInput(std::uint64_t now) { auto injected = InjectedInput(now); if (slots[0].state.intercepted) injectedHeldThroughCapture |= injected.buttons; - else injectedHeldThroughCapture &= injected.buttons; + else injectedHeldThroughCapture &= InjectedCommands().HeldThrough(injectedHeldAt, now); + injectedHeldAt = now; injected.buttons &= ~injectedHeldThroughCapture; return injected; } @@ -519,6 +525,7 @@ extern "C" void PadApplyInjectedInput_nid_postfix(PadInputState* state) { std::lock_guard lock(stateMutex); const auto injected = InjectedInput(sceKernelGetProcessTime()); injectedHeldThroughCapture |= injected.buttons; + injectedHeldAt = sceKernelGetProcessTime(); state->buttons |= injected.buttons; if (injected.hasSticks) state->sticks = injected.sticks; } diff --git a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp index 8ab6fd2bb3..d1e0d0e437 100644 --- a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp +++ b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp @@ -7,7 +7,9 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libSceIme/include/ImeInput.hpp" #include "prx/libSceIme/include/ModalDialog.hpp" +#include #include +#include #include #include #include @@ -157,6 +159,50 @@ void InjectedAnswer(PadInput& padInput, const DisplayWindow& window, const std:: ModalDialogSetPresenter_nid_postfix(false); } +std::uint32_t TitleButtons(bool useRead) { + if (!useRead) return Pad::ReadState(0).buttons; + std::array data{}; + const int count = Pad::Read(data.data(), static_cast(data.size()), 0); + Require(count > 0); + return data[static_cast(count - 1)].buttons; +} + +void InjectedPressAfterUnreadRelease(PadInput& padInput, const DisplayWindow& window, const std::filesystem::path& commandPath, std::uint64_t sequence, bool useRead) { + constexpr std::uint32_t Cross = 0x4000; + constexpr std::uint32_t Intercepted = 0x80000000; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + padInput.Update(window); + Require(TitleButtons(useRead) == 0); + ModalDialogSetPresenter_nid_postfix(true); + ModalDialogRequest request; + request.message = u"Create new data?"; + request.buttons = {u"OK"}; + Require(ModalDialogOpen_nid_postfix(request)); + padInput.Update(window); + Require(TitleButtons(useRead) == Intercepted); + std::ofstream(commandPath) << "APS5_PAD_TIMELINE 1 " << sequence << " 2\n0 400 16384\n1000 400 16384\n"; + const auto written = std::chrono::steady_clock::now(); + std::size_t chosen = 9; + ModalDialogState state = ModalDialogState::Showing; + for (int frame = 0; frame < 30 && state == ModalDialogState::Showing; ++frame) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + padInput.Update(window); + state = ModalDialogPoll_nid_postfix(chosen); + } + Require(state == ModalDialogState::Answered && chosen == 0); + padInput.Update(window); + Require(TitleButtons(useRead) == 0); + Require(std::chrono::steady_clock::now() - written < std::chrono::milliseconds(350)); + std::this_thread::sleep_until(written + std::chrono::milliseconds(1150)); + padInput.Update(window); + Require(TitleButtons(useRead) == Cross); + std::this_thread::sleep_until(written + std::chrono::milliseconds(1700)); + padInput.Update(window); + Require(TitleButtons(useRead) == 0); + ModalDialogReset_nid_postfix(); + ModalDialogSetPresenter_nid_postfix(false); +} + } int main() { @@ -255,6 +301,8 @@ int main() { Require(!padInput.SlotOccupied(0)); Require(!padInput.SlotOccupied(1)); InjectedAnswer(padInput, window, commandPath); + InjectedPressAfterUnreadRelease(padInput, window, commandPath, 3, false); + InjectedPressAfterUnreadRelease(padInput, window, commandPath, 4, true); Require(SDL_JoystickDetachVirtual(dev1) == 0); Require(SDL_JoystickDetachVirtual(dev0) == 0);