diff --git a/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp b/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp index cdd0b942d0..75827582bc 100644 --- a/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp +++ b/core/libs/prx/libSceVideoOut/include/DualSenseHaptics.hpp @@ -1,7 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_DUALSENSEHAPTICS_HPP #define CORE_LIBS_PRX_LIBSCEVIDEOOUT_DUALSENSEHAPTICS_HPP +#include #include +#include #include #include #include @@ -14,6 +16,11 @@ constexpr std::size_t PowerReportBytes = 78; 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}; + +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); +} constexpr std::array CrcTable = [] { std::array table{}; diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 42f2cb45a8..936d97f72e 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -14,6 +14,7 @@ #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/PadStatusSdl.hpp" #include "prx/libSceVideoOut/include/DualSenseHaptics.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" @@ -164,15 +165,17 @@ void PadInput::streamHaptics() { const auto write = [this](std::span report) { if (SDL_hid_write(haptics, report.data(), report.size()) >= 0) return true; APS5_LOG_ERR("Pad: a Bluetooth DualSense haptics report failed (%s); haptics stop until the controller reconnects", SDL_GetError()); - PadSetHapticStreamOpen_nid_postfix(false); hapticsRunning = false; return false; }; if (!write(DualSenseHaptics::PowerReport(sequence++))) return; + auto next = std::chrono::steady_clock::now() - DualSenseHaptics::ReportInterval; while (hapticsRunning) { std::array samples{}; if (!PadWaitTakeHapticBlock_nid_postfix(samples.data(), 50)) continue; 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 (!write(DualSenseHaptics::Report(sequence++, counter++, samples))) return; } } diff --git a/core/libs/prx/libSceVideoOut/tests/DualSenseHaptics.cpp b/core/libs/prx/libSceVideoOut/tests/DualSenseHaptics.cpp index 2e561e83e2..af0a6c4643 100644 --- a/core/libs/prx/libSceVideoOut/tests/DualSenseHaptics.cpp +++ b/core/libs/prx/libSceVideoOut/tests/DualSenseHaptics.cpp @@ -87,6 +87,20 @@ void BufferFollowsTheInputRate() { Require(buffer.Frames() == 30, "441 frames at 44.1 kHz are 30 frames at 3 kHz"); } +void ReportsArePaced() { + using namespace std::chrono; + const steady_clock::time_point start{seconds(100)}; + Require(DualSenseHaptics::ReportInterval == nanoseconds(10666666), "one 32-frame block at 3 kHz lasts 10.67 ms"); + auto next = DualSenseHaptics::NextReport(start - DualSenseHaptics::ReportInterval, start); + Require(next == start, "the first report goes out at once"); + for (int report = 1; report <= 4; ++report) { + next = DualSenseHaptics::NextReport(next, start); + Require(next == start + report * DualSenseHaptics::ReportInterval, "buffered reports are spaced one interval apart"); + } + const auto resumed = start + seconds(1); + Require(DualSenseHaptics::NextReport(next, resumed) == resumed, "a report after a pause goes out at once, without a catch-up burst"); +} + } int main() { @@ -94,6 +108,7 @@ int main() { PowerReportLayout(); BufferAveragesTo3kHz(); BufferFollowsTheInputRate(); + ReportsArePaced(); std::puts("dualsense haptics tests passed"); return 0; } diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ee5c94020e..aba0991ffa 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, 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 stops the stream 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 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. - 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).