feat(input): turn mouse speed into a smooth right-stick tilt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b94f923a5
commit
38bc0fd348
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
namespace Pad {
|
||||
|
||||
inline constexpr int MousePollIntervalMs = 33;
|
||||
inline constexpr int WheelPressDurationMs = 80;
|
||||
inline constexpr double DefaultMouseSensitivity = 1.0;
|
||||
inline constexpr double MaxMouseSensitivity = 10.0;
|
||||
|
|
@ -21,6 +20,12 @@ struct InputSettings {
|
|||
int leftStickDeadzonePercent = 0;
|
||||
int rightStickDeadzonePercent = 0;
|
||||
double mouseSensitivity = DefaultMouseSensitivity;
|
||||
double mouseVerticalScale = 1.0;
|
||||
double mouseCurve = 1.0;
|
||||
double mouseDeadzoneOffset = 0.2;
|
||||
double mouseMaxSpeed = 1.0;
|
||||
double mouseSmoothingMs = 10.0;
|
||||
double mouseDecayMs = 30.0;
|
||||
};
|
||||
|
||||
struct InputConfiguration {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "SDL_events.h"
|
||||
|
||||
#include "InputMapping.hpp"
|
||||
#include "PadState.hpp"
|
||||
|
||||
namespace Pad {
|
||||
|
||||
inline constexpr double MouseFullTiltCountsPerSecond = 3000.0;
|
||||
inline constexpr double MouseMinimumStepSeconds = 0.0005;
|
||||
|
||||
class MouseStick {
|
||||
public:
|
||||
explicit MouseStick(const InputSettings& settings);
|
||||
void Move(int deltaX, int deltaY);
|
||||
void Advance(double seconds);
|
||||
void Reset();
|
||||
double Speed() const;
|
||||
std::array<double, 2> Deflection() const;
|
||||
|
||||
private:
|
||||
InputSettings settings;
|
||||
double pendingX = 0.0;
|
||||
double pendingY = 0.0;
|
||||
double pendingSeconds = 0.0;
|
||||
double speedX = 0.0;
|
||||
double speedY = 0.0;
|
||||
double idleSeconds = 0.0;
|
||||
};
|
||||
|
||||
struct InputRequests {
|
||||
bool toggleFullscreen = false;
|
||||
bool toggleMouse = false;
|
||||
bool releaseMouse = false;
|
||||
};
|
||||
|
||||
class KeyboardMouse {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
explicit KeyboardMouse(InputConfiguration configuration);
|
||||
InputRequests HandleEvent(const SDL_Event& event, Clock::time_point now);
|
||||
bool Advance(Clock::time_point now);
|
||||
void SetMouseMode(bool enabled);
|
||||
void Release();
|
||||
bool MouseMode() const;
|
||||
PadInputState Compose(const PadInputState& controller) const;
|
||||
const InputConfiguration& Configuration() const;
|
||||
|
||||
private:
|
||||
InputConfiguration configuration;
|
||||
std::vector<bool> pressed;
|
||||
std::vector<Clock::time_point> wheelReleaseTimes;
|
||||
MouseStick mouse;
|
||||
Clock::time_point lastAdvance{};
|
||||
bool advanced = false;
|
||||
bool mouseMode = false;
|
||||
};
|
||||
|
||||
std::uint8_t StickByte(double deflection);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -9,6 +9,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardMouse.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp
|
||||
)
|
||||
|
||||
|
|
@ -112,6 +113,17 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(pad_keyboard_mouse_tests tests/KeyboardMouse.cpp src/KeyboardMouse.cpp)
|
||||
target_include_directories(pad_keyboard_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include)
|
||||
target_link_libraries(pad_keyboard_mouse_tests PRIVATE SDL2-static)
|
||||
if(MINGW)
|
||||
target_compile_options(pad_keyboard_mouse_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(pad_keyboard_mouse_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_test(NAME pad_keyboard_mouse COMMAND pad_keyboard_mouse_tests)
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_video_out_wqhd_detection_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutWqhdDetection.cpp)
|
||||
target_include_directories(guest_video_out_wqhd_detection_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@
|
|||
|
||||
#include "SDL_events.h"
|
||||
#include "SDL_gamecontroller.h"
|
||||
#include "prx/libScePad/include/InputMapping.hpp"
|
||||
#include "prx/libScePad/include/KeyboardMouse.hpp"
|
||||
#include "prx/libScePad/include/PadState.hpp"
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
class DisplayWindow;
|
||||
|
||||
|
|
@ -30,13 +28,7 @@ private:
|
|||
void applyOutput();
|
||||
void enableSensors();
|
||||
|
||||
std::vector<Pad::InputBinding> bindings;
|
||||
Pad::InputSettings settings;
|
||||
std::vector<bool> pressed;
|
||||
std::vector<std::chrono::steady_clock::time_point> wheelReleaseTimes;
|
||||
std::array<std::uint8_t, 2> mouseStick{128, 128};
|
||||
std::chrono::steady_clock::time_point nextMousePoll{};
|
||||
bool mouseEnabled = false;
|
||||
Pad::KeyboardMouse keyboardMouse;
|
||||
SDL_GameController* controller = nullptr;
|
||||
PadInputState controllerState{};
|
||||
std::uint32_t outputSequence = 0;
|
||||
|
|
|
|||
|
|
@ -126,7 +126,13 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc
|
|||
enum class InputSetting {
|
||||
LeftStickDeadzone,
|
||||
RightStickDeadzone,
|
||||
MouseSensitivity
|
||||
MouseSensitivity,
|
||||
MouseVerticalScale,
|
||||
MouseCurve,
|
||||
MouseDeadzoneOffset,
|
||||
MouseMaxSpeed,
|
||||
MouseSmoothingMs,
|
||||
MouseDecayMs
|
||||
};
|
||||
|
||||
struct InputSettingName {
|
||||
|
|
@ -137,7 +143,13 @@ struct InputSettingName {
|
|||
constexpr auto settingNames = std::array{
|
||||
InputSettingName{"LeftStickDeadzone", InputSetting::LeftStickDeadzone},
|
||||
InputSettingName{"RightStickDeadzone", InputSetting::RightStickDeadzone},
|
||||
InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity}
|
||||
InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity},
|
||||
InputSettingName{"MouseVerticalScale", InputSetting::MouseVerticalScale},
|
||||
InputSettingName{"MouseCurve", InputSetting::MouseCurve},
|
||||
InputSettingName{"MouseDeadzoneOffset", InputSetting::MouseDeadzoneOffset},
|
||||
InputSettingName{"MouseMaxSpeed", InputSetting::MouseMaxSpeed},
|
||||
InputSettingName{"MouseSmoothingMs", InputSetting::MouseSmoothingMs},
|
||||
InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs}
|
||||
};
|
||||
|
||||
const InputSettingName* findSetting(std::string_view name) {
|
||||
|
|
@ -168,11 +180,33 @@ double parseMouseSensitivity(std::string_view value) {
|
|||
return parsed;
|
||||
}
|
||||
|
||||
std::string formatNumber(double value) {
|
||||
std::array<char, 32> buffer{};
|
||||
const auto result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value);
|
||||
return std::string(buffer.data(), result.ptr);
|
||||
}
|
||||
|
||||
double parseNumber(std::string_view value, std::string_view name, double minimum, double maximum) {
|
||||
double parsed = 0.0;
|
||||
const auto end = value.data() + value.size();
|
||||
const auto result = std::from_chars(value.data(), end, parsed);
|
||||
if (value.empty() || result.ec != std::errc{} || result.ptr != end || !std::isfinite(parsed) || parsed < minimum || parsed > maximum) {
|
||||
throw std::runtime_error(std::string(name) + " must be a number from " + formatNumber(minimum) + " to " + formatNumber(maximum));
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
void applySetting(Pad::InputSettings& settings, InputSetting setting, std::string_view value) {
|
||||
switch (setting) {
|
||||
case InputSetting::LeftStickDeadzone: settings.leftStickDeadzonePercent = parseDeadzonePercent(value); break;
|
||||
case InputSetting::RightStickDeadzone: settings.rightStickDeadzonePercent = parseDeadzonePercent(value); break;
|
||||
case InputSetting::MouseSensitivity: settings.mouseSensitivity = parseMouseSensitivity(value); break;
|
||||
case InputSetting::MouseVerticalScale: settings.mouseVerticalScale = parseNumber(value, "mouse vertical scale", 0.1, 10.0); break;
|
||||
case InputSetting::MouseCurve: settings.mouseCurve = parseNumber(value, "mouse curve", 0.2, 5.0); break;
|
||||
case InputSetting::MouseDeadzoneOffset: settings.mouseDeadzoneOffset = parseNumber(value, "mouse deadzone offset", 0.0, 0.9); break;
|
||||
case InputSetting::MouseMaxSpeed: settings.mouseMaxSpeed = parseNumber(value, "mouse max speed", 0.1, 10.0); break;
|
||||
case InputSetting::MouseSmoothingMs: settings.mouseSmoothingMs = parseNumber(value, "mouse smoothing", 0.0, 500.0); break;
|
||||
case InputSetting::MouseDecayMs: settings.mouseDecayMs = parseNumber(value, "mouse decay", 1.0, 1000.0); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
#include "prx/libScePad/include/KeyboardMouse.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
Pad::MouseStick::MouseStick(const InputSettings& settings) : settings(settings) {}
|
||||
|
||||
void Pad::MouseStick::Move(int deltaX, int deltaY) {
|
||||
pendingX += deltaX;
|
||||
pendingY += deltaY;
|
||||
}
|
||||
|
||||
void Pad::MouseStick::Advance(double seconds) {
|
||||
if (!(seconds > 0.0)) return;
|
||||
pendingSeconds += seconds;
|
||||
if (pendingSeconds < MouseMinimumStepSeconds) return;
|
||||
const double step = pendingSeconds;
|
||||
pendingSeconds = 0.0;
|
||||
const bool moved = pendingX != 0.0 || pendingY != 0.0;
|
||||
const double scale = settings.mouseSensitivity / (MouseFullTiltCountsPerSecond * step);
|
||||
const double targetX = pendingX * scale;
|
||||
const double targetY = pendingY * scale * settings.mouseVerticalScale;
|
||||
pendingX = 0.0;
|
||||
pendingY = 0.0;
|
||||
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;
|
||||
speedX = targetX + (speedX - targetX) * keep;
|
||||
speedY = targetY + (speedY - targetY) * keep;
|
||||
const double speed = std::hypot(speedX, speedY);
|
||||
if (speed > settings.mouseMaxSpeed) {
|
||||
speedX *= settings.mouseMaxSpeed / speed;
|
||||
speedY *= settings.mouseMaxSpeed / speed;
|
||||
}
|
||||
}
|
||||
|
||||
void Pad::MouseStick::Reset() {
|
||||
pendingX = 0.0;
|
||||
pendingY = 0.0;
|
||||
pendingSeconds = 0.0;
|
||||
speedX = 0.0;
|
||||
speedY = 0.0;
|
||||
idleSeconds = 0.0;
|
||||
}
|
||||
|
||||
double Pad::MouseStick::Speed() const {
|
||||
return std::hypot(speedX, speedY);
|
||||
}
|
||||
|
||||
std::array<double, 2> Pad::MouseStick::Deflection() const {
|
||||
const double speed = std::hypot(speedX, speedY);
|
||||
if (speed <= 0.0) return {0.0, 0.0};
|
||||
const double shaped = std::pow(std::min(speed, 1.0), settings.mouseCurve);
|
||||
const double deflection = settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * shaped;
|
||||
return {speedX / speed * deflection, speedY / speed * deflection};
|
||||
}
|
||||
|
||||
std::uint8_t Pad::StickByte(double deflection) {
|
||||
const double clamped = std::clamp(deflection, -1.0, 1.0);
|
||||
return static_cast<std::uint8_t>(std::lround(clamped < 0.0 ? 128.0 + clamped * 128.0 : 128.0 + clamped * 127.0));
|
||||
}
|
||||
|
||||
Pad::KeyboardMouse::KeyboardMouse(InputConfiguration configuration)
|
||||
: configuration(std::move(configuration)),
|
||||
pressed(this->configuration.bindings.size()),
|
||||
wheelReleaseTimes(this->configuration.bindings.size()),
|
||||
mouse(this->configuration.settings) {}
|
||||
|
||||
const Pad::InputConfiguration& Pad::KeyboardMouse::Configuration() const {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
bool Pad::KeyboardMouse::MouseMode() const {
|
||||
return mouseMode;
|
||||
}
|
||||
|
||||
bool Pad::KeyboardMouse::Advance(Clock::time_point now) {
|
||||
if (!advanced) {
|
||||
advanced = true;
|
||||
lastAdvance = now;
|
||||
}
|
||||
if (now > lastAdvance) {
|
||||
const double seconds = std::chrono::duration<double>(now - lastAdvance).count();
|
||||
lastAdvance = now;
|
||||
if (mouseMode) mouse.Advance(seconds);
|
||||
}
|
||||
bool released = false;
|
||||
for (std::size_t index = 0; index < configuration.bindings.size(); ++index) {
|
||||
if (configuration.bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue;
|
||||
pressed[index] = false;
|
||||
wheelReleaseTimes[index] = {};
|
||||
released = true;
|
||||
}
|
||||
return released;
|
||||
}
|
||||
|
||||
void Pad::KeyboardMouse::SetMouseMode(bool enabled) {
|
||||
mouseMode = enabled;
|
||||
mouse.Reset();
|
||||
}
|
||||
|
||||
void Pad::KeyboardMouse::Release() {
|
||||
std::fill(pressed.begin(), pressed.end(), false);
|
||||
std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), Clock::time_point{});
|
||||
}
|
||||
|
||||
Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock::time_point now) {
|
||||
InputRequests requests;
|
||||
if (event.type == SDL_WINDOWEVENT) {
|
||||
if (event.window.event != SDL_WINDOWEVENT_FOCUS_LOST && event.window.event != SDL_WINDOWEVENT_CLOSE) return requests;
|
||||
Release();
|
||||
requests.releaseMouse = mouseMode;
|
||||
return requests;
|
||||
}
|
||||
if (event.type == SDL_MOUSEMOTION) {
|
||||
if (mouseMode) mouse.Move(event.motion.xrel, event.motion.yrel);
|
||||
return requests;
|
||||
}
|
||||
if (event.type == SDL_MOUSEWHEEL) {
|
||||
int direction = (event.wheel.y > 0) - (event.wheel.y < 0);
|
||||
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction;
|
||||
if (direction == 0) return requests;
|
||||
const auto releaseTime = now + std::chrono::milliseconds(WheelPressDurationMs);
|
||||
for (std::size_t index = 0; index < configuration.bindings.size(); ++index) {
|
||||
const auto& binding = configuration.bindings[index];
|
||||
if (binding.wheelDirection == 0) continue;
|
||||
pressed[index] = binding.wheelDirection == direction;
|
||||
wheelReleaseTimes[index] = pressed[index] ? releaseTime : Clock::time_point{};
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP;
|
||||
const bool mouseButton = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP;
|
||||
if (!keyboard && !mouseButton) return requests;
|
||||
if (keyboard && event.key.repeat != 0) return requests;
|
||||
const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN;
|
||||
for (std::size_t index = 0; index < configuration.bindings.size(); ++index) {
|
||||
const auto& binding = configuration.bindings[index];
|
||||
const bool matches = keyboard
|
||||
? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode
|
||||
: binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast<MouseButton>(event.button.button);
|
||||
if (!matches) continue;
|
||||
if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true;
|
||||
if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true;
|
||||
pressed[index] = down;
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const {
|
||||
PadInputState state = controller;
|
||||
std::array<bool, 4> negative{};
|
||||
std::array<bool, 4> positive{};
|
||||
for (std::size_t index = 0; index < configuration.bindings.size(); ++index) {
|
||||
if (!pressed[index]) continue;
|
||||
const auto& binding = configuration.bindings[index];
|
||||
switch (binding.control) {
|
||||
case InputControl::Button:
|
||||
state.buttons |= static_cast<std::uint32_t>(binding.button);
|
||||
if (binding.button == PadButton::L2) state.analogButtonsL2 = 255;
|
||||
if (binding.button == PadButton::R2) state.analogButtonsR2 = 255;
|
||||
break;
|
||||
case InputControl::LeftStickLeft: negative[0] = true; break;
|
||||
case InputControl::LeftStickRight: positive[0] = true; break;
|
||||
case InputControl::LeftStickUp: negative[1] = true; break;
|
||||
case InputControl::LeftStickDown: positive[1] = true; break;
|
||||
case InputControl::RightStickLeft: negative[2] = true; break;
|
||||
case InputControl::RightStickRight: positive[2] = true; break;
|
||||
case InputControl::RightStickUp: negative[3] = true; break;
|
||||
case InputControl::RightStickDown: positive[3] = true; break;
|
||||
case InputControl::TouchLeft: state.touchLeft = true; break;
|
||||
case InputControl::TouchRight: state.touchRight = true; break;
|
||||
case InputControl::ToggleMouse: break;
|
||||
case InputControl::ToggleFullscreen: break;
|
||||
}
|
||||
}
|
||||
for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) {
|
||||
if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255;
|
||||
}
|
||||
if (mouseMode) {
|
||||
const auto deflection = mouse.Deflection();
|
||||
state.sticks[2] = StickByte(deflection[0]);
|
||||
state.sticks[3] = StickByte(deflection[1]);
|
||||
}
|
||||
if (state.analogButtonsL2 != 0) state.buttons |= static_cast<std::uint32_t>(PadButton::L2);
|
||||
if (state.analogButtonsR2 != 0) state.buttons |= static_cast<std::uint32_t>(PadButton::R2);
|
||||
return state;
|
||||
}
|
||||
|
|
@ -17,8 +17,7 @@
|
|||
PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) {
|
||||
}
|
||||
|
||||
PadInput::PadInput(Pad::InputConfiguration configuration)
|
||||
: bindings(std::move(configuration.bindings)), settings(configuration.settings), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) {
|
||||
PadInput::PadInput(Pad::InputConfiguration configuration) : keyboardMouse(std::move(configuration)) {
|
||||
openFirstAvailableController();
|
||||
}
|
||||
|
||||
|
|
@ -112,13 +111,10 @@ void PadInput::applyOutput() {
|
|||
}
|
||||
|
||||
void PadInput::setMouseMode(bool enabled) {
|
||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0");
|
||||
if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError());
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
SDL_GetRelativeMouseState(&deltaX, &deltaY);
|
||||
mouseEnabled = enabled;
|
||||
mouseStick = {128, 128};
|
||||
nextMousePoll = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::MousePollIntervalMs);
|
||||
keyboardMouse.SetMouseMode(enabled);
|
||||
}
|
||||
|
||||
PadInputState PadInput::sampleController() const {
|
||||
|
|
@ -160,8 +156,8 @@ PadInputState PadInput::sampleController() const {
|
|||
const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) {
|
||||
return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent);
|
||||
};
|
||||
const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, settings.leftStickDeadzonePercent);
|
||||
const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, settings.rightStickDeadzonePercent);
|
||||
const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, keyboardMouse.Configuration().settings.leftStickDeadzonePercent);
|
||||
const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, keyboardMouse.Configuration().settings.rightStickDeadzonePercent);
|
||||
result.sticks = {left[0], left[1], right[0], right[1]};
|
||||
switch (SDL_GameControllerGetType(controller)) {
|
||||
case SDL_CONTROLLER_TYPE_PS5: result.deviceKind = 1; break;
|
||||
|
|
@ -213,137 +209,37 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) {
|
|||
publish();
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_WINDOWEVENT && (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST || event.window.event == SDL_WINDOWEVENT_CLOSE)) {
|
||||
std::fill(pressed.begin(), pressed.end(), false);
|
||||
std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{});
|
||||
if (mouseEnabled) setMouseMode(false);
|
||||
if (event.type == SDL_WINDOWEVENT) {
|
||||
if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false);
|
||||
publish();
|
||||
return;
|
||||
}
|
||||
// APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach
|
||||
// its window (a stray press advances the title into another stage).
|
||||
static const bool ignoreInput = std::getenv("APS5_NO_PAD_INPUT") != nullptr;
|
||||
if (ignoreInput && event.type == SDL_MOUSEWHEEL) return;
|
||||
if (event.type == SDL_MOUSEWHEEL) {
|
||||
int direction = (event.wheel.y > 0) - (event.wheel.y < 0);
|
||||
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction;
|
||||
if (direction == 0) return;
|
||||
const auto releaseTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::WheelPressDurationMs);
|
||||
for (std::size_t index = 0; index < bindings.size(); ++index) {
|
||||
const auto& binding = bindings[index];
|
||||
if (binding.wheelDirection == 0) continue;
|
||||
pressed[index] = binding.wheelDirection == direction;
|
||||
wheelReleaseTimes[index] = pressed[index] ? releaseTime : std::chrono::steady_clock::time_point{};
|
||||
}
|
||||
publish();
|
||||
return;
|
||||
}
|
||||
const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP;
|
||||
const bool mouse = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP;
|
||||
if (!keyboard && !mouse) return;
|
||||
if (ignoreInput) return;
|
||||
if (keyboard && event.key.repeat != 0) return;
|
||||
const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN;
|
||||
for (std::size_t index = 0; index < bindings.size(); ++index) {
|
||||
const auto& binding = bindings[index];
|
||||
const bool matches = keyboard
|
||||
? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode
|
||||
: binding.mouseButton != Pad::MouseButton::None && binding.mouseButton == static_cast<Pad::MouseButton>(event.button.button);
|
||||
|
||||
if (!matches) continue;
|
||||
if (binding.control == Pad::InputControl::ToggleFullscreen) {
|
||||
if (keyboard && down && !pressed[index] && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen();
|
||||
}
|
||||
if (binding.control == Pad::InputControl::ToggleMouse && down && !pressed[index]) setMouseMode(!mouseEnabled);
|
||||
pressed[index] = down;
|
||||
}
|
||||
publish();
|
||||
const bool input = event.type == SDL_MOUSEWHEEL || event.type == SDL_MOUSEMOTION || event.type == SDL_KEYDOWN || event.type == SDL_KEYUP ||
|
||||
event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP;
|
||||
if (!input || ignoreInput) return;
|
||||
const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now());
|
||||
if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen();
|
||||
if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode());
|
||||
if (event.type != SDL_MOUSEMOTION) publish();
|
||||
}
|
||||
|
||||
void PadInput::Update() {
|
||||
if (controller != nullptr) SDL_GameControllerUpdate();
|
||||
applyOutput();
|
||||
if (controller != nullptr) {
|
||||
controllerState = sampleController();
|
||||
publish();
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
bool released = false;
|
||||
for (std::size_t index = 0; index < bindings.size(); ++index) {
|
||||
if (bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue;
|
||||
pressed[index] = false;
|
||||
wheelReleaseTimes[index] = {};
|
||||
released = true;
|
||||
}
|
||||
if (released) publish();
|
||||
if (!mouseEnabled) return;
|
||||
if (SDL_GetKeyboardFocus() == nullptr) {
|
||||
std::fill(pressed.begin(), pressed.end(), false);
|
||||
std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{});
|
||||
if (controller != nullptr) controllerState = sampleController();
|
||||
if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) {
|
||||
keyboardMouse.Release();
|
||||
setMouseMode(false);
|
||||
publish();
|
||||
return;
|
||||
}
|
||||
if (now < nextMousePoll) return;
|
||||
nextMousePoll = now + std::chrono::milliseconds(Pad::MousePollIntervalMs);
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
SDL_GetRelativeMouseState(&deltaX, &deltaY);
|
||||
mouseStick = {128, 128};
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
const double distance = std::hypot(deltaX, deltaY);
|
||||
const double scale = std::clamp(distance * settings.mouseSensitivity + 16.0, 64.0, 128.0) / distance;
|
||||
const auto mapAxis = [scale](int delta) { return static_cast<std::uint8_t>(std::clamp(128L + std::lround(delta * scale), 0L, 255L)); };
|
||||
mouseStick = {mapAxis(deltaX), mapAxis(deltaY)};
|
||||
}
|
||||
publish();
|
||||
const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now());
|
||||
if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish();
|
||||
}
|
||||
|
||||
void PadInput::publish() {
|
||||
PadInputState state;
|
||||
state.buttons = controllerState.buttons;
|
||||
state.sticks = controllerState.sticks;
|
||||
state.analogButtonsL2 = controllerState.analogButtonsL2;
|
||||
state.analogButtonsR2 = controllerState.analogButtonsR2;
|
||||
state.hasMotion = controllerState.hasMotion;
|
||||
state.accel = controllerState.accel;
|
||||
state.gyro = controllerState.gyro;
|
||||
state.touch = controllerState.touch;
|
||||
state.deviceKind = controllerState.deviceKind;
|
||||
|
||||
std::array<bool, 4> negative{};
|
||||
std::array<bool, 4> positive{};
|
||||
for (std::size_t index = 0; index < bindings.size(); ++index) {
|
||||
if (!pressed[index]) continue;
|
||||
const auto& binding = bindings[index];
|
||||
switch (binding.control) {
|
||||
case Pad::InputControl::Button:
|
||||
state.buttons |= static_cast<std::uint32_t>(binding.button);
|
||||
if (binding.button == Pad::PadButton::L2) state.analogButtonsL2 = 255;
|
||||
if (binding.button == Pad::PadButton::R2) state.analogButtonsR2 = 255;
|
||||
break;
|
||||
case Pad::InputControl::LeftStickLeft: negative[0] = true; break;
|
||||
case Pad::InputControl::LeftStickRight: positive[0] = true; break;
|
||||
case Pad::InputControl::LeftStickUp: negative[1] = true; break;
|
||||
case Pad::InputControl::LeftStickDown: positive[1] = true; break;
|
||||
case Pad::InputControl::RightStickLeft: negative[2] = true; break;
|
||||
case Pad::InputControl::RightStickRight: positive[2] = true; break;
|
||||
case Pad::InputControl::RightStickUp: negative[3] = true; break;
|
||||
case Pad::InputControl::RightStickDown: positive[3] = true; break;
|
||||
case Pad::InputControl::TouchLeft: state.touchLeft = true; break;
|
||||
case Pad::InputControl::TouchRight: state.touchRight = true; break;
|
||||
case Pad::InputControl::ToggleMouse: break;
|
||||
case Pad::InputControl::ToggleFullscreen: break;
|
||||
}
|
||||
}
|
||||
for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) {
|
||||
if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255;
|
||||
}
|
||||
if (mouseEnabled) {
|
||||
state.sticks[2] = mouseStick[0];
|
||||
state.sticks[3] = mouseStick[1];
|
||||
}
|
||||
if (state.analogButtonsL2 != 0) state.buttons |= static_cast<std::uint32_t>(Pad::PadButton::L2);
|
||||
if (state.analogButtonsR2 != 0) state.buttons |= static_cast<std::uint32_t>(Pad::PadButton::R2);
|
||||
PadPublishInput_nid_postfix(state);
|
||||
PadPublishInput_nid_postfix(keyboardMouse.Compose(controllerState));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,36 @@ void settingsReject() {
|
|||
require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected");
|
||||
}
|
||||
|
||||
void mouseLookDefault() {
|
||||
useConfig("Cross = KEY:F\n");
|
||||
const auto settings = Pad::LoadInputMapping().settings;
|
||||
require(settings.mouseVerticalScale == 1.0 && settings.mouseCurve == 1.0 && settings.mouseMaxSpeed == 1.0, "mouse look scales default to 1");
|
||||
require(settings.mouseDeadzoneOffset == 0.2 && settings.mouseSmoothingMs == 10.0 && settings.mouseDecayMs == 30.0, "mouse look timing and offset defaults");
|
||||
}
|
||||
|
||||
void mouseLookParse() {
|
||||
useConfig("MouseCurve=0.7 ; softer start\nmousedeadzoneoffset = 0.15\nMouseMaxSpeed = 1.5\nMouseSmoothingMs = 0\nMouseDecayMs = 50\nMouseVerticalScale = 0.5\n");
|
||||
const auto settings = Pad::LoadInputMapping().settings;
|
||||
require(settings.mouseCurve == 0.7, "mouse curve is read");
|
||||
require(settings.mouseDeadzoneOffset == 0.15, "mouse deadzone offset is read case-insensitively");
|
||||
require(settings.mouseMaxSpeed == 1.5, "mouse max speed is read");
|
||||
require(settings.mouseSmoothingMs == 0.0, "a 0 smoothing is accepted");
|
||||
require(settings.mouseDecayMs == 50.0, "mouse decay is read");
|
||||
require(settings.mouseVerticalScale == 0.5, "mouse vertical scale is read");
|
||||
}
|
||||
|
||||
void mouseLookReject() {
|
||||
require(rejects("MouseCurve = 0.1\n"), "a mouse curve below 0.2 is rejected");
|
||||
require(rejects("MouseCurve = steep\n"), "a non-numeric mouse curve is rejected");
|
||||
require(rejects("MouseDeadzoneOffset = 0.95\n"), "a deadzone offset above 0.9 is rejected");
|
||||
require(rejects("MouseDecayMs =\n"), "an empty decay is rejected");
|
||||
require(rejects("MouseDecayMs = 0\n"), "a zero decay is rejected");
|
||||
require(rejects("MouseSmoothingMs = 501\n"), "a smoothing above 500 is rejected");
|
||||
require(rejects("MouseMaxSpeed = nan\n"), "a non-finite max speed is rejected");
|
||||
require(rejects("MouseVerticalScale = 11\n"), "a vertical scale above 10 is rejected");
|
||||
require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected");
|
||||
}
|
||||
|
||||
void deadzoneOffMatchesRawSticks() {
|
||||
for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) {
|
||||
const auto stick = Pad::StickWithDeadzone(axis, axis, 0);
|
||||
|
|
@ -108,6 +138,9 @@ int main() try {
|
|||
settingsDefault();
|
||||
settingsParse();
|
||||
settingsReject();
|
||||
mouseLookDefault();
|
||||
mouseLookParse();
|
||||
mouseLookReject();
|
||||
deadzoneOffMatchesRawSticks();
|
||||
deadzoneShapesSticks();
|
||||
std::filesystem::remove_all(root);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,318 @@
|
|||
#include "prx/libScePad/include/InputMapping.hpp"
|
||||
#include "prx/libScePad/include/KeyboardMouse.hpp"
|
||||
#include "PadSimulation.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using PadSimulation::At;
|
||||
using PadSimulation::KeyEvent;
|
||||
using PadSimulation::Sample;
|
||||
using PadSimulation::Simulate;
|
||||
using PadSimulation::TimedEvent;
|
||||
|
||||
constexpr double Polls[] = {33.0, 8.0, 1.0};
|
||||
|
||||
void check(bool condition, const std::string& reason) {
|
||||
if (!condition) throw std::runtime_error(reason);
|
||||
}
|
||||
|
||||
Pad::InputConfiguration defaults() {
|
||||
return Pad::InputConfiguration{{Pad::InputMapping.begin(), Pad::InputMapping.end()}, {}};
|
||||
}
|
||||
|
||||
Pad::InputConfiguration withSettings(const std::function<void(Pad::InputSettings&)>& change) {
|
||||
auto configuration = defaults();
|
||||
change(configuration.settings);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
double axis(std::uint8_t value) {
|
||||
return value < 128 ? (static_cast<double>(value) - 128.0) / 128.0 : (static_cast<double>(value) - 128.0) / 127.0;
|
||||
}
|
||||
|
||||
double rightMagnitude(const Sample& sample) {
|
||||
return std::hypot(axis(sample.state.sticks[2]), axis(sample.state.sticks[3]));
|
||||
}
|
||||
|
||||
double meanRightX(const std::vector<Sample>& samples, double fromMs, double toMs) {
|
||||
double sum = 0.0;
|
||||
int count = 0;
|
||||
for (const auto& sample : samples) {
|
||||
if (sample.ms <= fromMs || sample.ms > toMs) continue;
|
||||
sum += axis(sample.state.sticks[2]);
|
||||
++count;
|
||||
}
|
||||
check(count > 0, "no samples in window");
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
double heldMeanRightX(const std::vector<Sample>& samples, double pollMs, double fromMs, double toMs) {
|
||||
double sum = 0.0;
|
||||
for (const auto& sample : samples) {
|
||||
if (sample.ms >= fromMs - 1e-9 && sample.ms < toMs - 1e-9) sum += axis(sample.state.sticks[2]) * pollMs;
|
||||
}
|
||||
return sum / (toMs - fromMs);
|
||||
}
|
||||
|
||||
std::vector<TimedEvent> mouseOnThenMotion(double durationMs, double countsPerSecondX, double countsPerSecondY = 0.0) {
|
||||
std::vector<TimedEvent> events{PadSimulation::MouseModeAt(0.0, true)};
|
||||
PadSimulation::AppendMotion(events, 0.0, durationMs, countsPerSecondX, countsPerSecondY);
|
||||
return events;
|
||||
}
|
||||
|
||||
double expectedDeflection(const Pad::InputSettings& settings, double normalizedSpeed) {
|
||||
const double clamped = std::min(normalizedSpeed, settings.mouseMaxSpeed);
|
||||
return settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * std::pow(std::min(clamped, 1.0), settings.mouseCurve);
|
||||
}
|
||||
|
||||
void testCenteredWithoutMotion() {
|
||||
for (const double poll : Polls) {
|
||||
const auto samples = Simulate(defaults(), {PadSimulation::MouseModeAt(0.0, true)}, poll, 300.0);
|
||||
for (const auto& sample : samples) {
|
||||
check(sample.state.sticks[2] == 128 && sample.state.sticks[3] == 128, "a resting mouse leaves the stick centered");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testSteadyDeflectionFollowsCurve(const Pad::InputConfiguration& configuration, const char* name) {
|
||||
const auto& settings = configuration.settings;
|
||||
for (const double speed : {0.05, 0.25, 0.5, 0.75, 0.95, 3.0}) {
|
||||
const double countsPerSecond = speed * Pad::MouseFullTiltCountsPerSecond / settings.mouseSensitivity;
|
||||
const double expected = expectedDeflection(settings, speed);
|
||||
for (const double poll : Polls) {
|
||||
const auto samples = Simulate(configuration, mouseOnThenMotion(600.0, countsPerSecond), poll, 600.0);
|
||||
const double measured = meanRightX(samples, 300.0, 600.0);
|
||||
if (std::abs(measured - expected) > 0.02) {
|
||||
throw std::runtime_error(std::string(name) + ": speed " + std::to_string(speed) + " at " + std::to_string(poll) + " ms polls gives " +
|
||||
std::to_string(measured) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testDeflectionCurve() {
|
||||
testSteadyDeflectionFollowsCurve(defaults(), "defaults");
|
||||
testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 2.0; s.mouseDeadzoneOffset = 0.1; s.mouseSensitivity = 2.0; }), "quadratic");
|
||||
testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 0.5; s.mouseDeadzoneOffset = 0.0; }), "square root");
|
||||
}
|
||||
|
||||
void testDeadzoneOffset() {
|
||||
const auto configuration = defaults();
|
||||
for (const double poll : Polls) {
|
||||
const auto samples = Simulate(configuration, mouseOnThenMotion(400.0, 40.0), poll, 400.0);
|
||||
bool moved = false;
|
||||
for (const auto& sample : samples) {
|
||||
const double magnitude = rightMagnitude(sample);
|
||||
if (magnitude == 0.0) continue;
|
||||
moved = true;
|
||||
check(magnitude >= configuration.settings.mouseDeadzoneOffset - 0.01, "the slowest movement still clears the deadzone offset at " + std::to_string(poll) + " ms polls");
|
||||
}
|
||||
check(moved, "slow movement moves the stick");
|
||||
}
|
||||
const auto samples = Simulate(withSettings([](auto& s) { s.mouseDeadzoneOffset = 0.0; }), mouseOnThenMotion(400.0, 40.0), 8.0, 400.0);
|
||||
check(meanRightX(samples, 200.0, 400.0) < 0.05, "without an offset slow movement stays small");
|
||||
}
|
||||
|
||||
double centeredAfter(const std::vector<Sample>& samples, double stopMs) {
|
||||
double lastMoving = stopMs;
|
||||
double previous = 2.0;
|
||||
for (const auto& sample : samples) {
|
||||
if (sample.ms <= stopMs) continue;
|
||||
const double magnitude = rightMagnitude(sample);
|
||||
check(magnitude <= previous + 1e-9, "the stick never grows after the mouse stops");
|
||||
previous = magnitude;
|
||||
if (magnitude != 0.0) lastMoving = sample.ms;
|
||||
}
|
||||
return lastMoving - stopMs;
|
||||
}
|
||||
|
||||
void testDecay() {
|
||||
const auto configuration = defaults();
|
||||
for (const double speed : {0.3, 1.0}) {
|
||||
for (const double poll : Polls) {
|
||||
const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, speed * Pad::MouseFullTiltCountsPerSecond), poll, 600.0);
|
||||
const double tail = centeredAfter(samples, 300.0);
|
||||
check(tail < configuration.settings.mouseDecayMs + poll, "stick centered within decay plus one poll at " + std::to_string(poll) + " ms polls, took " + std::to_string(tail));
|
||||
check(samples.back().state.sticks[2] == 128 && samples.back().state.sticks[3] == 128, "stick returns exactly to center");
|
||||
}
|
||||
}
|
||||
const auto slow = withSettings([](auto& s) { s.mouseDecayMs = 120.0; });
|
||||
const auto samples = Simulate(slow, mouseOnThenMotion(300.0, Pad::MouseFullTiltCountsPerSecond), 1.0, 600.0);
|
||||
const double tail = centeredAfter(samples, 300.0);
|
||||
check(tail >= 100.0 && tail < 121.0, "a longer decay keeps the stick moving longer");
|
||||
}
|
||||
|
||||
void testMaxSpeedClamp() {
|
||||
const auto configuration = defaults();
|
||||
constexpr double StopMs = 264.0;
|
||||
for (const double poll : Polls) {
|
||||
const auto flick = Simulate(configuration, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0);
|
||||
const auto full = Simulate(configuration, mouseOnThenMotion(StopMs, 1.5 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0);
|
||||
check(flick[flick.size() / 3].state.sticks[2] == 255, "a fast flick reaches full tilt");
|
||||
check(centeredAfter(flick, StopMs) <= centeredAfter(full, StopMs) + 1e-9, "a flick faster than full tilt does not extend the tail");
|
||||
double flickSum = 0.0;
|
||||
double fullSum = 0.0;
|
||||
for (std::size_t index = 0; index < flick.size(); ++index) {
|
||||
if (flick[index].ms <= StopMs) continue;
|
||||
flickSum += rightMagnitude(flick[index]);
|
||||
fullSum += rightMagnitude(full[index]);
|
||||
}
|
||||
check(std::abs(flickSum - fullSum) <= 0.02 * std::max(1.0, fullSum), "the stick decays from a flick like from full tilt at " + std::to_string(poll) + " ms polls");
|
||||
}
|
||||
const auto headroom = withSettings([](auto& s) { s.mouseMaxSpeed = 3.0; });
|
||||
const auto flick = Simulate(headroom, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0);
|
||||
const auto full = Simulate(defaults(), mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0);
|
||||
check(rightMagnitude(flick[275]) > rightMagnitude(full[275]), "a higher max speed lets a flick hold full tilt longer");
|
||||
}
|
||||
|
||||
void testSmoothing() {
|
||||
const auto instant = Simulate(withSettings([](auto& s) { s.mouseSmoothingMs = 0.0; }), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 8.0, 100.0);
|
||||
check(std::abs(axis(instant[1].state.sticks[2]) - expectedDeflection(defaults().settings, 0.5)) < 0.02, "without smoothing the stick follows at once");
|
||||
const auto smoothed = Simulate(defaults(), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 1.0, 100.0);
|
||||
const double early = (axis(smoothed[9].state.sticks[2]) - 0.2) / 0.8;
|
||||
check(early > 0.5 * 0.55 && early < 0.5 * 0.72, "smoothing reaches about 63 percent after one time constant");
|
||||
}
|
||||
|
||||
std::vector<TimedEvent> waveMotion(double base, double swing) {
|
||||
constexpr double Pi = 3.14159265358979323846;
|
||||
std::vector<TimedEvent> events{PadSimulation::MouseModeAt(0.0, true)};
|
||||
for (int segment = 0; segment < 200; ++segment) {
|
||||
const double speed = (base + swing * std::sin(2.0 * Pi * segment * 8.0 / 800.0)) * Pad::MouseFullTiltCountsPerSecond;
|
||||
PadSimulation::AppendMotion(events, segment * 8.0, 8.0, speed, 0.3 * speed);
|
||||
}
|
||||
PadSimulation::AppendMotion(events, 1700.0, 200.0, 250.0, -120.0);
|
||||
return events;
|
||||
}
|
||||
|
||||
double integratedRightMagnitude(const std::vector<Sample>& samples, double pollMs) {
|
||||
double sum = 0.0;
|
||||
for (const auto& sample : samples) sum += rightMagnitude(sample) * pollMs / 1000.0;
|
||||
return sum;
|
||||
}
|
||||
|
||||
void testPollRateIndependence() {
|
||||
for (const auto& [name, events] : {std::pair{"one-way wave", waveMotion(0.5, 0.3)}, std::pair{"back-and-forth wave", waveMotion(0.0, 0.6)}}) {
|
||||
std::vector<std::vector<Sample>> runs;
|
||||
for (const double poll : Polls) runs.push_back(Simulate(defaults(), events, poll, 2112.0));
|
||||
const double reference = integratedRightMagnitude(runs[2], Polls[2]);
|
||||
for (std::size_t run = 0; run < runs.size(); ++run) {
|
||||
const double integrated = integratedRightMagnitude(runs[run], Polls[run]);
|
||||
check(std::abs(integrated - reference) <= 0.02 * reference, std::string(name) + ": total deflection at " + std::to_string(Polls[run]) + " ms polls is " +
|
||||
std::to_string(integrated) + ", 1 ms gives " + std::to_string(reference));
|
||||
const double tolerance = 0.03 + 0.0025 * Polls[run];
|
||||
for (double window = 0.0; window + 264.0 <= 2112.0; window += 264.0) {
|
||||
const double mean = heldMeanRightX(runs[run], Polls[run], window, window + 264.0);
|
||||
const double expected = heldMeanRightX(runs[2], Polls[2], window, window + 264.0);
|
||||
check(std::abs(mean - expected) < tolerance, std::string(name) + ": held deflection at " + std::to_string(Polls[run]) + " ms polls from " +
|
||||
std::to_string(window) + " ms is " + std::to_string(mean) + ", 1 ms gives " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testVerticalScale() {
|
||||
const auto configuration = withSettings([](auto& s) { s.mouseVerticalScale = 0.5; s.mouseDeadzoneOffset = 0.0; });
|
||||
const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, 0.0, 0.8 * Pad::MouseFullTiltCountsPerSecond), 8.0, 300.0);
|
||||
const double y = axis(samples.back().state.sticks[3]);
|
||||
check(std::abs(y - 0.4) < 0.02, "vertical scale halves the vertical deflection");
|
||||
}
|
||||
|
||||
void testMouseModeOff() {
|
||||
std::vector<TimedEvent> events;
|
||||
PadSimulation::AppendMotion(events, 0.0, 300.0, Pad::MouseFullTiltCountsPerSecond, 0.0);
|
||||
for (const auto& sample : Simulate(defaults(), events, 8.0, 300.0)) {
|
||||
check(sample.state.sticks[2] == 128, "motion without mouse mode leaves the stick alone");
|
||||
}
|
||||
events.insert(events.begin(), At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true)));
|
||||
events.insert(events.begin() + 1, At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false)));
|
||||
const auto samples = Simulate(defaults(), events, 8.0, 300.0);
|
||||
check(samples.back().state.sticks[2] == 255, "middle click turns mouse mode on");
|
||||
}
|
||||
|
||||
void testKeyboardSticksUnchanged() {
|
||||
const std::vector<TimedEvent> events{
|
||||
At(0.0, KeyEvent(SDL_SCANCODE_W, true)),
|
||||
At(10.0, KeyEvent(SDL_SCANCODE_D, true)),
|
||||
At(20.0, KeyEvent(SDL_SCANCODE_A, true)),
|
||||
At(30.0, KeyEvent(SDL_SCANCODE_W, false)),
|
||||
At(30.0, KeyEvent(SDL_SCANCODE_D, false)),
|
||||
At(40.0, KeyEvent(SDL_SCANCODE_A, false))};
|
||||
const auto samples = Simulate(defaults(), events, 1.0, 50.0);
|
||||
check(samples[4].state.sticks[1] == 0 && samples[4].state.sticks[0] == 128, "W tilts the left stick fully up");
|
||||
check(samples[14].state.sticks[0] == 255, "D tilts the left stick fully right");
|
||||
check(samples[24].state.sticks[0] == 128, "A and D together cancel");
|
||||
check(samples[34].state.sticks[0] == 0 && samples[34].state.sticks[1] == 128, "A alone tilts fully left");
|
||||
check(samples[44].state.sticks[0] == 128, "released keys center the stick");
|
||||
}
|
||||
|
||||
void testWheelPress() {
|
||||
const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1))}, 1.0, 200.0);
|
||||
const auto up = static_cast<std::uint32_t>(Pad::PadButton::Up);
|
||||
check((samples[40].state.buttons & up) != 0, "wheel up presses Up");
|
||||
check((samples[120].state.buttons & up) == 0, "wheel press releases after its duration");
|
||||
}
|
||||
|
||||
|
||||
void testWheelFocusLoss() {
|
||||
const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1)), At(10.0, KeyEvent(SDL_SCANCODE_W, true)), At(20.0, PadSimulation::FocusLostEvent())}, 1.0, 40.0);
|
||||
check(samples[15].state.sticks[1] == 0, "a held key tilts the stick");
|
||||
check(samples[30].state.sticks[1] == 128, "losing focus lets go of held keys");
|
||||
check((samples[30].state.buttons & static_cast<std::uint32_t>(Pad::PadButton::Up)) == 0, "losing focus lets go of the wheel press");
|
||||
}
|
||||
|
||||
void testFocusLossReleasesMouse() {
|
||||
Pad::KeyboardMouse keyboardMouse(defaults());
|
||||
keyboardMouse.SetMouseMode(true);
|
||||
const auto requests = keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now());
|
||||
check(requests.releaseMouse, "losing focus asks to release the captured mouse");
|
||||
keyboardMouse.SetMouseMode(false);
|
||||
check(!keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()).releaseMouse, "nothing to release without capture");
|
||||
}
|
||||
|
||||
void testToggleRequests() {
|
||||
Pad::KeyboardMouse keyboardMouse(defaults());
|
||||
const auto now = Pad::KeyboardMouse::Clock::now();
|
||||
check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse");
|
||||
check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing");
|
||||
check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const std::pair<const char*, std::function<void()>> tests[] = {
|
||||
{"centered without motion", testCenteredWithoutMotion},
|
||||
{"deflection curve", testDeflectionCurve},
|
||||
{"deadzone offset", testDeadzoneOffset},
|
||||
{"decay", testDecay},
|
||||
{"max speed clamp", testMaxSpeedClamp},
|
||||
{"smoothing", testSmoothing},
|
||||
{"poll rate independence", testPollRateIndependence},
|
||||
{"vertical scale", testVerticalScale},
|
||||
{"mouse mode off", testMouseModeOff},
|
||||
{"keyboard sticks unchanged", testKeyboardSticksUnchanged},
|
||||
{"wheel press", testWheelPress},
|
||||
{"wheel and keys on focus loss", testWheelFocusLoss},
|
||||
{"focus loss releases the mouse", testFocusLossReleasesMouse},
|
||||
{"toggle requests", testToggleRequests},
|
||||
};
|
||||
for (const auto& [name, test] : tests) {
|
||||
test();
|
||||
std::printf("pad_keyboard_mouse: %s passed\n", name);
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "pad_keyboard_mouse: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP
|
||||
|
||||
#include "prx/libScePad/include/KeyboardMouse.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "SDL_keyboard.h"
|
||||
|
||||
namespace PadSimulation {
|
||||
|
||||
struct TimedEvent {
|
||||
double ms = 0.0;
|
||||
SDL_Event event{};
|
||||
bool setMouseMode = false;
|
||||
bool mouseMode = false;
|
||||
bool release = false;
|
||||
};
|
||||
|
||||
struct Sample {
|
||||
double ms = 0.0;
|
||||
PadInputState state;
|
||||
};
|
||||
|
||||
inline SDL_Event MotionEvent(int deltaX, int deltaY) {
|
||||
SDL_Event event{};
|
||||
event.type = SDL_MOUSEMOTION;
|
||||
event.motion.xrel = deltaX;
|
||||
event.motion.yrel = deltaY;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline SDL_Event KeyEvent(SDL_Scancode key, bool down) {
|
||||
SDL_Event event{};
|
||||
event.type = down ? SDL_KEYDOWN : SDL_KEYUP;
|
||||
event.key.keysym.scancode = key;
|
||||
event.key.state = down ? SDL_PRESSED : SDL_RELEASED;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline SDL_Event ButtonEvent(Uint8 button, bool down) {
|
||||
SDL_Event event{};
|
||||
event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
|
||||
event.button.button = button;
|
||||
event.button.state = down ? SDL_PRESSED : SDL_RELEASED;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline SDL_Event FocusLostEvent() {
|
||||
SDL_Event event{};
|
||||
event.type = SDL_WINDOWEVENT;
|
||||
event.window.event = SDL_WINDOWEVENT_FOCUS_LOST;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline SDL_Event WheelEvent(int direction) {
|
||||
SDL_Event event{};
|
||||
event.type = SDL_MOUSEWHEEL;
|
||||
event.wheel.y = direction;
|
||||
event.wheel.direction = SDL_MOUSEWHEEL_NORMAL;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline TimedEvent At(double ms, const SDL_Event& event) {
|
||||
return {ms, event, false, false, false};
|
||||
}
|
||||
|
||||
inline TimedEvent MouseModeAt(double ms, bool enabled) {
|
||||
return {ms, SDL_Event{}, true, enabled, false};
|
||||
}
|
||||
|
||||
inline TimedEvent ReleaseAt(double ms) {
|
||||
return {ms, SDL_Event{}, false, false, true};
|
||||
}
|
||||
|
||||
inline void AppendMotion(std::vector<TimedEvent>& events, double startMs, double durationMs, double countsPerSecondX, double countsPerSecondY, double reportHz = 1000.0) {
|
||||
const double periodMs = 1000.0 / reportHz;
|
||||
double owedX = 0.0;
|
||||
double owedY = 0.0;
|
||||
for (double ms = startMs + periodMs; ms <= startMs + durationMs + 1e-9; ms += periodMs) {
|
||||
owedX += countsPerSecondX * periodMs / 1000.0;
|
||||
owedY += countsPerSecondY * periodMs / 1000.0;
|
||||
const int deltaX = static_cast<int>(std::trunc(owedX));
|
||||
const int deltaY = static_cast<int>(std::trunc(owedY));
|
||||
owedX -= deltaX;
|
||||
owedY -= deltaY;
|
||||
if (deltaX != 0 || deltaY != 0) events.push_back(At(ms, MotionEvent(deltaX, deltaY)));
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<Sample> Simulate(const Pad::InputConfiguration& configuration, const std::vector<TimedEvent>& events, double pollMs, double endMs) {
|
||||
using Clock = Pad::KeyboardMouse::Clock;
|
||||
const auto origin = Clock::time_point{} + std::chrono::hours(1);
|
||||
const auto at = [origin](double ms) {
|
||||
return origin + std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double, std::milli>(ms));
|
||||
};
|
||||
Pad::KeyboardMouse keyboardMouse(configuration);
|
||||
keyboardMouse.Advance(at(0.0));
|
||||
std::vector<Sample> samples;
|
||||
std::size_t next = 0;
|
||||
const auto steps = static_cast<long long>(std::floor(endMs / pollMs + 1e-9));
|
||||
for (long long step = 1; step <= steps; ++step) {
|
||||
const double ms = static_cast<double>(step) * pollMs;
|
||||
const auto now = at(ms);
|
||||
for (; next < events.size() && events[next].ms <= ms + 1e-9; ++next) {
|
||||
const auto& timed = events[next];
|
||||
if (timed.release) {
|
||||
keyboardMouse.Release();
|
||||
continue;
|
||||
}
|
||||
if (timed.setMouseMode) {
|
||||
keyboardMouse.SetMouseMode(timed.mouseMode);
|
||||
continue;
|
||||
}
|
||||
const auto requests = keyboardMouse.HandleEvent(timed.event, now);
|
||||
if (requests.toggleMouse) keyboardMouse.SetMouseMode(!keyboardMouse.MouseMode());
|
||||
if (requests.releaseMouse) keyboardMouse.SetMouseMode(false);
|
||||
}
|
||||
keyboardMouse.Advance(now);
|
||||
samples.push_back({ms, keyboardMouse.Compose(PadInputState{})});
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -37,7 +37,13 @@ The same file also accepts these settings, each at most once, in the form `Setti
|
|||
| Setting | Value | Default |
|
||||
|---------|-------|---------|
|
||||
| `LeftStickDeadzone`, `RightStickDeadzone` | Whole percentage from 0 to 90 of the controller stick's travel that reads as centered. Outside it the travel is rescaled, so the stick still reaches its full range. Keyboard and mouse stick bindings are not affected. | `0` |
|
||||
| `MouseSensitivity` | Number greater than 0 and at most 10 that scales mouse movement on the right stick while the mouse is captured (`ToggleMouse`). | `1` |
|
||||
| `MouseSensitivity` | Number greater than 0 and at most 10. At 1, moving the mouse 3000 counts per second tilts the stick fully, about 5 cm per second at 1600 DPI; a larger number needs less movement. | `1` |
|
||||
| `MouseVerticalScale` | Number from 0.1 to 10 that multiplies vertical mouse speed, for games that pitch slower or faster than they turn. | `1` |
|
||||
| `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` |
|
||||
| `MouseDecayMs` | Number from 1 to 1000, the milliseconds without movement after which the stick is centered. | `30` |
|
||||
|
||||
```ini
|
||||
LeftStickDeadzone = 12
|
||||
|
|
@ -45,4 +51,20 @@ RightStickDeadzone = 8
|
|||
MouseSensitivity = 2.5
|
||||
```
|
||||
|
||||
## Mouse as the right stick
|
||||
|
||||
`ToggleMouse` (the middle mouse button by default) captures the mouse and drives the right stick from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus.
|
||||
|
||||
The stick tilts in the direction the mouse moves, and further the faster it moves. Speed is measured in counts per second over the time between updates, so the same hand speed gives the same tilt at any frame rate. Any movement tilts the stick at least `MouseDeadzoneOffset`, so slow movement isn't lost inside the game's own deadzone. When the mouse stops, the stick eases back and is centered `MouseDecayMs` after the last movement.
|
||||
|
||||
For example, for a game with a small deadzone that needs fast turns:
|
||||
|
||||
```ini
|
||||
MouseSensitivity = 1.6
|
||||
MouseDeadzoneOffset = 0.12
|
||||
MouseCurve = 1.3
|
||||
```
|
||||
|
||||
Earlier versions sampled the mouse every 33 ms and tilted the stick at least halfway for any movement. `MouseDeadzoneOffset = 0.5`, `MouseSmoothingMs = 0` and `MouseDecayMs = 33` come close to that behavior.
|
||||
|
||||
An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used.
|
||||
|
|
|
|||
Loading…
Reference in New Issue