From 06ca6a066bc3c1f5189991becb9b3b5eb356b658 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:50:00 +0200 Subject: [PATCH] fix(libScePad): smooth a slow-reporting mouse over its measured report interval --- .../prx/libScePad/include/KeyboardMouse.hpp | 14 +++++ .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 35 ++++++++++- .../libSceVideoOut/tests/KeyboardMouse.cpp | 58 +++++++++++++++++++ docs/user/INPUT_MAPPING.md | 2 +- 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 53336f6a47..c302c3f3b4 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -14,6 +14,10 @@ namespace Pad { inline constexpr double MouseFullTiltCountsPerSecond = 3000.0; inline constexpr double MouseMinimumStepSeconds = 0.0005; +inline constexpr double MouseRateWindowSeconds = 0.25; +inline constexpr double MouseRateGapSeconds = 0.05; +inline constexpr double MouseSmoothingReports = 3.0; +inline constexpr double MouseSmoothingLimitMs = 30.0; class MouseStick { public: @@ -22,16 +26,26 @@ public: void Advance(double seconds); void Reset(); double Speed() const; + double ReportRate() const; + double SmoothingMs() const; std::array Deflection() const; private: + void measureReportRate(double step, bool moved); + InputSettings settings; double pendingX = 0.0; double pendingY = 0.0; double pendingSeconds = 0.0; + double pendingCounts = 0.0; + int pendingReports = 0; double speedX = 0.0; double speedY = 0.0; double idleSeconds = 0.0; + double rateSeconds = 0.0; + double rateCounts = 0.0; + int rateReports = 0; + double reportRate = 0.0; }; struct InputRequests { diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index 9b58c374de..75052bfb4f 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -9,6 +9,32 @@ Pad::MouseStick::MouseStick(const InputSettings& settings) : settings(settings) void Pad::MouseStick::Move(int deltaX, int deltaY) { pendingX += deltaX; pendingY += deltaY; + pendingCounts += std::hypot(deltaX, deltaY); + ++pendingReports; +} + +void Pad::MouseStick::measureReportRate(double step, bool moved) { + if (moved) { + rateSeconds += std::min(idleSeconds + step, MouseRateGapSeconds); + rateCounts += pendingCounts; + rateReports += pendingReports; + } + pendingCounts = 0.0; + pendingReports = 0; + if (rateSeconds < MouseRateWindowSeconds) return; + if (rateReports >= 10 && rateCounts >= 2.0 * rateReports) reportRate = rateReports / rateSeconds; + rateSeconds = 0.0; + rateCounts = 0.0; + rateReports = 0; +} + +double Pad::MouseStick::ReportRate() const { + return reportRate; +} + +double Pad::MouseStick::SmoothingMs() const { + const double reports = reportRate > 0.0 ? std::min(MouseSmoothingReports * 1000.0 / reportRate, MouseSmoothingLimitMs) : 0.0; + return settings.mouseSmoothingMs > 0.0 ? std::max(settings.mouseSmoothingMs, reports) : 0.0; } void Pad::MouseStick::Advance(double seconds) { @@ -23,13 +49,15 @@ void Pad::MouseStick::Advance(double seconds) { const double targetY = pendingY * scale * settings.mouseVerticalScale; pendingX = 0.0; pendingY = 0.0; + measureReportRate(step, moved); idleSeconds = moved ? 0.0 : idleSeconds + step; if (idleSeconds * 1000.0 >= settings.mouseDecayMs) { speedX = 0.0; speedY = 0.0; return; } - const double keep = settings.mouseSmoothingMs > 0.0 ? std::exp(-step * 1000.0 / settings.mouseSmoothingMs) : 0.0; + const double smoothing = SmoothingMs(); + const double keep = smoothing > 0.0 ? std::exp(-step * 1000.0 / smoothing) : 0.0; speedX = targetX + (speedX - targetX) * keep; speedY = targetY + (speedY - targetY) * keep; const double speed = std::hypot(speedX, speedY); @@ -43,6 +71,11 @@ void Pad::MouseStick::Reset() { pendingX = 0.0; pendingY = 0.0; pendingSeconds = 0.0; + pendingCounts = 0.0; + pendingReports = 0; + rateSeconds = 0.0; + rateCounts = 0.0; + rateReports = 0; speedX = 0.0; speedY = 0.0; idleSeconds = 0.0; diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index fa9b6f7e3a..ad8464174e 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -182,6 +182,62 @@ void testSmoothing() { check(early > 0.5 * 0.55 && early < 0.5 * 0.72, "smoothing reaches about 63 percent after one time constant"); } +void testSlowReportingMouse() { + const double speed = 0.5 * Pad::MouseFullTiltCountsPerSecond; + const double expected = expectedDeflection(defaults().settings, 0.5); + for (const double reportHz : {125.0, 250.0, 1000.0}) { + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + PadSimulation::AppendMotion(events, 0.0, 2000.0, speed, 0.0, reportHz); + for (const double poll : {16.7, 8.3, 6.9, 1.0}) { + const auto samples = Simulate(defaults(), events, poll, 2000.0); + double low = 1.0; + double high = 0.0; + for (const auto& sample : samples) { + if (sample.ms < 1000.0) continue; + low = std::min(low, axis(sample.state.sticks[2])); + high = std::max(high, axis(sample.state.sticks[2])); + } + check(high - low < 0.125, "a " + std::to_string(reportHz) + " Hz mouse at " + std::to_string(poll) + " ms polls wavers by " + std::to_string(high - low)); + check(std::abs((high + low) / 2.0 - expected) < 0.05, "a " + std::to_string(reportHz) + " Hz mouse at " + std::to_string(poll) + " ms polls stays around the expected tilt"); + } + } +} + +void testReportRateSmoothing() { + for (const double reportHz : {125.0, 250.0, 1000.0}) { + Pad::MouseStick mouse(defaults().settings); + check(mouse.SmoothingMs() == defaults().settings.mouseSmoothingMs, "smoothing starts at the setting"); + const double periodSeconds = 1.0 / reportHz; + for (int report = 0; report < static_cast(reportHz); ++report) { + mouse.Move(6, 0); + mouse.Advance(periodSeconds); + } + check(std::abs(mouse.ReportRate() - reportHz) < 0.1 * reportHz, "measured " + std::to_string(mouse.ReportRate()) + " reports per second for a " + std::to_string(reportHz) + " Hz mouse"); + const double smoothing = mouse.SmoothingMs(); + check(std::abs(smoothing - std::max(10.0, 3000.0 / reportHz)) < 0.15 * smoothing, "smoothing " + std::to_string(smoothing) + " ms for a " + std::to_string(reportHz) + " Hz mouse"); + } + Pad::MouseStick slow(defaults().settings); + for (int step = 0; step < 40; ++step) { + slow.Move(30, 0); + slow.Advance(0.025); + } + check(std::abs(slow.ReportRate() - 40.0) < 2.0 && slow.SmoothingMs() == Pad::MouseSmoothingLimitMs, "smoothing for very slow reports stops at 30 ms"); + auto off = defaults().settings; + off.mouseSmoothingMs = 0.0; + Pad::MouseStick unsmoothed(off); + for (int report = 0; report < 125; ++report) { + unsmoothed.Move(6, 0); + unsmoothed.Advance(0.008); + } + check(unsmoothed.SmoothingMs() == 0.0, "a smoothing of 0 stays off for a slow mouse"); + Pad::MouseStick single(defaults().settings); + for (int report = 0; report < 125; ++report) { + single.Move(1, 0); + single.Advance(0.008); + } + check(single.ReportRate() == 0.0 && single.SmoothingMs() == defaults().settings.mouseSmoothingMs, "single-count reports are not taken for a slow mouse"); +} + std::vector waveMotion(double base, double swing) { constexpr double Pi = 3.14159265358979323846; std::vector events{PadSimulation::MouseModeAt(0.0, true)}; @@ -341,6 +397,8 @@ int main() { {"decay", testDecay}, {"max speed clamp", testMaxSpeedClamp}, {"smoothing", testSmoothing}, + {"slow reporting mouse", testSlowReportingMouse}, + {"report rate smoothing", testReportRateSmoothing}, {"poll rate independence", testPollRateIndependence}, {"vertical scale", testVerticalScale}, {"mouse mode off", testMouseModeOff}, diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 882ce0a000..2568fc894d 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -67,7 +67,7 @@ The same file also accepts these settings, each at most once, in the form `Setti | `MouseCurve` | Number from 0.2 to 5, the response exponent. Above 1 gives finer control at low speed; below 1 reaches large tilts sooner. | `1` | | `MouseDeadzoneOffset` | Number from 0 to 0.9, the smallest tilt any movement produces as a fraction of full tilt. Set it just above the game's own deadzone: raise it if slow movement doesn't move the camera, lower it if small movements jump. | `0.2` | | `MouseMaxSpeed` | Number from 0.1 to 10, the highest tracked speed as a multiple of the full-tilt speed. Above 1, a fast flick keeps the stick at full tilt a little longer after the mouse stops. | `1` | -| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. 0 follows every mouse report directly. | `10` | +| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. A mouse that reports fewer than 300 times per second, such as a 125 or 250 Hz mouse, is smoothed over at least three of its report intervals, up to 30 ms; the report rate is measured from the movement itself. 0 follows every mouse report directly and turns this off too. | `10` | | `MouseDecayMs` | Number from 1 to 1000, the milliseconds without movement after which the stick is centered. | `30` | ```ini