From d7fb2a3ec3a84dfd2a29044a4f8760a525e35565 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Mon, 26 Oct 2026 11:17:02 +0100 Subject: [PATCH] fix(pad): restore the DualSense haptics configuration after a rumble stops SDL sends scePadSetVibration rumble as report 0x31 with audio haptics turned off. Once the rumble stopped, the stream's haptics came back at a different strength than before it. The stream thread now sends its configuration report 0x31 again 100 ms after slot 0's rumble stops, which brought the haptics back to their level before the rumble. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceVideoOut/include/DualSenseHaptics.hpp | 1 + core/libs/prx/libSceVideoOut/include/PadInput.hpp | 2 ++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 10 ++++++++++ docs/dev/debt/libSceVideoOut.md | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp b/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp index 75827582bc..d4e45de462 100644 --- a/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp +++ b/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp @@ -17,6 +17,7 @@ constexpr std::size_t SamplesOffset = 13; constexpr std::size_t SamplesBytes = 64; constexpr std::uint8_t ControllerQueueFrames = 64; constexpr std::chrono::nanoseconds ReportInterval{1'000'000'000LL * SamplesBytes / 2 / 3000}; +constexpr std::chrono::milliseconds RumbleSettle{100}; inline std::chrono::steady_clock::time_point NextReport(std::chrono::steady_clock::time_point previous, std::chrono::steady_clock::time_point now) { return std::max(previous + ReportInterval, now); diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index f6e24eee13..d83d736d47 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -81,6 +81,8 @@ private: SDL_hid_device* haptics = nullptr; std::atomic hapticsRunning{false}; std::thread hapticsThread; + bool hapticsRumbled = false; + std::atomic hapticsReconfigureAt{0}; }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 936d97f72e..14ee159d3f 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -176,6 +176,10 @@ void PadInput::streamHaptics() { if (std::all_of(samples.begin(), samples.end(), [](std::int8_t sample) { return sample == 0; })) continue; next = DualSenseHaptics::NextReport(next, std::chrono::steady_clock::now()); PreciseSleepUntil(next); + if (const auto due = hapticsReconfigureAt.load(); due != 0 && std::chrono::steady_clock::now().time_since_epoch().count() >= due) { + hapticsReconfigureAt = 0; + if (!write(DualSenseHaptics::PowerReport(sequence++))) return; + } if (!write(DualSenseHaptics::Report(sequence++, counter++, samples))) return; } } @@ -187,6 +191,8 @@ void PadInput::closeHaptics() { if (hapticsThread.joinable()) hapticsThread.join(); SDL_hid_close(haptics); haptics = nullptr; + hapticsRumbled = false; + hapticsReconfigureAt = 0; } void PadInput::closeController(int slot) { @@ -279,6 +285,10 @@ void PadInput::applyOutput() { slots[slot].outputPending = false; slots[slot].nextRumbleRefresh = now + std::chrono::milliseconds(700); SDL_GameControllerRumble(controller, motors.low, motors.high, motors.durationMs); + if (slot == 0 && haptics != nullptr) { + if (hapticsRumbled && !rumbling) hapticsReconfigureAt = (now + DualSenseHaptics::RumbleSettle).time_since_epoch().count(); + hapticsRumbled = rumbling; + } if (slots[slot].outputState.triggerTouched) { if (isPs5) { TriggerEffectCache::Effect effect{}; diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index aba0991ffa..47a03c5f67 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -53,7 +53,7 @@ - [HDR output](../../../core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp) (libSceVideoOut, libSceSystemService) - HDR output is reported only on Windows; elsewhere it stays SDR: SDL2 and Vulkan expose no output HDR state, and a surface can offer HDR10 formats with HDR off (KDE Wayland with RADV). The console's events for an HDR change are unknown; here every open port's output mode event fires when HDR availability or luminance changes. Console HDR settings (calibrated peak, the SDR-in-HDR conversion block) are not modelled. Surface format changes that leave the swapchain valid are seen only when DXGI's report changes or the swapchain is recreated. `sceSystemServiceGetHdrToneMapLuminance` reports SDR before VideoOut is first used, and no HDR change event fires for the state found at start-up. HDR needs a 10-bit HDR10 swapchain and a monitor DXGI reports in `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020`; without a usable peak, luminance is 1000 nits peak and full frame, 0 black - [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device. SDL's Windows.Gaming.Input backend stays on by default. Setting `APS5_NO_WGI` turns it off at default hint priority, for hosts where it faults at `Windows.Gaming.Input.dll+0x10a84` a few seconds into every run: seen on Windows 10 Pro 22H2 (19045) with the Nefarius and Oculus virtual gamepad emulation buses installed and no physical controller connected. With it off, devices and features reachable only through Windows.Gaming.Input are lost: racing wheels, arcade sticks, flight sticks, HID pads without an HIDAPI driver, and Xbox One and Series trigger rumble. - [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) (libSceVideoOut) forwards SDL_TEXTEDITING with its text cut to 32 bytes by SDL and `start` and `length` read as counts of characters. SDL_TEXTEDITING_EXT (longer text) is not requested, `SDL_SetTextInputRect` is not called so the system candidate window is not placed at the field, and SDL's own text input, started when video starts, is left on outside dialogs. How each platform reports `start` and `length` (characters or UTF-16 units, cursor or selection) is not measured. Closes when: measured on Linux (ibus and fcitx) -- [Bluetooth DualSense haptics](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp) (libSceVideoOut) - the controller's queue depth, the console's filter and the rumble interplay are unmeasured. Vibration ports are averaged to 3 kHz in 16-frame boxes (not the console's low-pass) and sent as report 0x32, one 32-frame block once two are buffered, at most one per 10.67 ms, so reports follow the title's audio clock. The report asks the controller to queue 64 frames (21 ms) to absorb Bluetooth jitter and the title's rate against the controller's crystal; 16 dropped out about once a second on an RX 9070 XT host. The host buffer keeps the newest 160 frames (53 ms). `scePadSetVibration` rumble still goes out as report 0x31, which may take the actuators out of audio mode. A failed write silences the vibration ports, without rumble, until the controller reconnects. Windows only verified. Closes when: the clock difference and the rumble interplay are measured on a title that uses both. +- [Bluetooth DualSense haptics](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp) (libSceVideoOut) - the controller's queue depth, the console's filter and the rumble interplay are unmeasured. Vibration ports are averaged to 3 kHz in 16-frame boxes (not the console's low-pass) and sent as report 0x32, one 32-frame block once two are buffered, at most every 10.67 ms, following the title's audio clock. The report asks the controller to queue 64 frames (21 ms) to absorb Bluetooth jitter and clock drift; 16 dropped out about once a second. The host buffer keeps the newest 160 frames (53 ms). `scePadSetVibration` rumble goes out as SDL's report 0x31 with audio haptics off; the stream resends its own 0x31 100 ms after the rumble stops, as haptics came back changed without it. A failed write silences the vibration ports, without rumble, until the controller reconnects. Windows only. Closes when: the clock difference and the rumble interplay are measured on a title that uses both. - The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceDbgVideoOutAddOutputModeVr60Privilege`](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp).