Previously, the mouse processing code had some issues that reduced mouse | control quality. No matter how you configured it or how you used the mouse, it was impossible to be able to make both fine inputs and maximum inputs to the control stick with the same configuration, as inputs were clamped to the range, and *outputs* were multiplied by the sensitivity setting. It was also impossible to make brief inputs, as two built-in exponential moving average filters in the mouse driver caused any mouse inputs, no matter how brief, to continue to produce stick outputs for several hundred milliseconds before dissipating. This commit is an attempt to address those issues. With this commit, an entirely different model of processing inputs is used. Instead of applying the sesnsitivity to the stick output, this commit applies the sensitivity at the beginning of processing to the mouse input. Next, inputs from the mouse are accumulated into a buffer variable. When the stick is updated, stick output is clamped to the range of the stick, and that value is subtracted from the input buffer. Any *excess* motion still remaining in the buffer gets decayed exponentially (similar to one of the exponential moving averages before). This gives a good compromise between responsiveness (brief mouse movement causes a brief stick response) and inertia (large mouse movement which exceeds the stick's instantaneous dynamic range causes stick movement that persists for a short time afterwards, mimicking inertia). Finally, to improve the linearity of the response, the stick motion is square-rooted to amplify small movements. This commit changes the semantics of the sensitivity option. It's no longer a percent, and "100(%)" no longer means anything special, so the input configuration UI has been changed to allow values up to 10,000 instead of being limited to 100. The changes to the filtration also affect the sensitivity. There are some magic constants in the code to try to keep the sensitivity about the same with a given value, but it's not exact, and users are encouraged to play around with it again on the new code.
298 lines
9.3 KiB
C++
298 lines
9.3 KiB
C++
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <thread>
|
|
#include <fmt/format.h>
|
|
#include <math.h>
|
|
|
|
#include "common/param_package.h"
|
|
#include "common/settings.h"
|
|
#include "common/thread.h"
|
|
#include "input_common/drivers/mouse.h"
|
|
|
|
namespace InputCommon {
|
|
constexpr int update_time = 10;
|
|
constexpr float default_stick_sensitivity = 0.0044f;
|
|
constexpr float default_motion_sensitivity = 0.0003f;
|
|
constexpr float maximum_rotation_speed = 2.0f;
|
|
constexpr int mouse_axis_x = 0;
|
|
constexpr int mouse_axis_y = 1;
|
|
constexpr int wheel_axis_x = 2;
|
|
constexpr int wheel_axis_y = 3;
|
|
constexpr PadIdentifier identifier = {
|
|
.guid = Common::UUID{},
|
|
.port = 0,
|
|
.pad = 0,
|
|
};
|
|
|
|
constexpr PadIdentifier motion_identifier = {
|
|
.guid = Common::UUID{},
|
|
.port = 0,
|
|
.pad = 1,
|
|
};
|
|
|
|
constexpr PadIdentifier real_mouse_identifier = {
|
|
.guid = Common::UUID{},
|
|
.port = 1,
|
|
.pad = 0,
|
|
};
|
|
|
|
constexpr PadIdentifier touch_identifier = {
|
|
.guid = Common::UUID{},
|
|
.port = 2,
|
|
.pad = 0,
|
|
};
|
|
|
|
Mouse::Mouse(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
|
PreSetController(identifier);
|
|
PreSetController(real_mouse_identifier);
|
|
PreSetController(touch_identifier);
|
|
PreSetController(motion_identifier);
|
|
|
|
// Initialize all mouse axis
|
|
PreSetAxis(identifier, mouse_axis_x);
|
|
PreSetAxis(identifier, mouse_axis_y);
|
|
PreSetAxis(identifier, wheel_axis_x);
|
|
PreSetAxis(identifier, wheel_axis_y);
|
|
PreSetAxis(real_mouse_identifier, mouse_axis_x);
|
|
PreSetAxis(real_mouse_identifier, mouse_axis_y);
|
|
PreSetAxis(touch_identifier, mouse_axis_x);
|
|
PreSetAxis(touch_identifier, mouse_axis_y);
|
|
|
|
// Initialize variables
|
|
mouse_origin = {};
|
|
last_mouse_position = {};
|
|
wheel_position = {};
|
|
last_mouse_change = {};
|
|
last_motion_change = {};
|
|
|
|
update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
|
|
}
|
|
|
|
void Mouse::UpdateThread(std::stop_token stop_token) {
|
|
Common::SetCurrentThreadName("Mouse");
|
|
|
|
while (!stop_token.stop_requested()) {
|
|
UpdateStickInput();
|
|
UpdateMotionInput();
|
|
|
|
if (mouse_panning_timeout++ > 20) {
|
|
StopPanning();
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
|
|
}
|
|
}
|
|
|
|
void Mouse::UpdateStickInput() {
|
|
if (!Settings::values.mouse_panning) {
|
|
return;
|
|
}
|
|
|
|
auto mouse_change = last_mouse_change;
|
|
auto move_distance = mouse_change.Length();
|
|
|
|
// Perform at most 1 unit of buffered mouse movement
|
|
if (move_distance > 1.0f) {
|
|
mouse_change *= 1.0f / move_distance;
|
|
move_distance = mouse_change.Length();
|
|
}
|
|
|
|
last_mouse_change -= mouse_change;
|
|
|
|
// Decay remaining buffered movement by 5%
|
|
last_mouse_change *= 0.95f;
|
|
|
|
// Stick response is nonlinear, and is not sensitive enough to fine changes
|
|
const auto sqrt_distance = sqrt(move_distance);
|
|
mouse_change *= sqrt_distance / move_distance;
|
|
|
|
SetAxis(identifier, mouse_axis_x, mouse_change.x);
|
|
SetAxis(identifier, mouse_axis_y, -mouse_change.y);
|
|
}
|
|
|
|
void Mouse::UpdateMotionInput() {
|
|
const float sensitivity =
|
|
Settings::values.mouse_panning_sensitivity.GetValue() * default_motion_sensitivity;
|
|
|
|
const float rotation_velocity = std::sqrt(last_motion_change.x * last_motion_change.x +
|
|
last_motion_change.y * last_motion_change.y);
|
|
|
|
if (rotation_velocity > maximum_rotation_speed / sensitivity) {
|
|
const float multiplier = maximum_rotation_speed / rotation_velocity / sensitivity;
|
|
last_motion_change.x = last_motion_change.x * multiplier;
|
|
last_motion_change.y = last_motion_change.y * multiplier;
|
|
}
|
|
|
|
const BasicMotion motion_data{
|
|
.gyro_x = last_motion_change.x * sensitivity,
|
|
.gyro_y = last_motion_change.y * sensitivity,
|
|
.gyro_z = last_motion_change.z * sensitivity,
|
|
.accel_x = 0,
|
|
.accel_y = 0,
|
|
.accel_z = 0,
|
|
.delta_timestamp = update_time * 1000,
|
|
};
|
|
|
|
if (Settings::values.mouse_panning) {
|
|
last_motion_change.x = 0;
|
|
last_motion_change.y = 0;
|
|
}
|
|
last_motion_change.z = 0;
|
|
|
|
SetMotion(motion_identifier, 0, motion_data);
|
|
}
|
|
|
|
void Mouse::Move(int x, int y, int center_x, int center_y) {
|
|
const float sensitivity = Settings::values.mouse_panning_sensitivity.GetValue() * 0.0012f;
|
|
if (Settings::values.mouse_panning) {
|
|
mouse_panning_timeout = 0;
|
|
|
|
auto mouse_change =
|
|
(Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
|
|
last_motion_change += {-mouse_change.y, -mouse_change.x, 0};
|
|
last_mouse_change += mouse_change * sensitivity * 0.1f;
|
|
|
|
return;
|
|
}
|
|
|
|
if (button_pressed) {
|
|
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
|
|
SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * sensitivity);
|
|
SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * sensitivity);
|
|
|
|
last_motion_change = {
|
|
static_cast<float>(-mouse_move.y) / 50.0f,
|
|
static_cast<float>(-mouse_move.x) / 50.0f,
|
|
last_motion_change.z,
|
|
};
|
|
}
|
|
}
|
|
|
|
void Mouse::MouseMove(f32 touch_x, f32 touch_y) {
|
|
SetAxis(real_mouse_identifier, mouse_axis_x, touch_x);
|
|
SetAxis(real_mouse_identifier, mouse_axis_y, touch_y);
|
|
}
|
|
|
|
void Mouse::TouchMove(f32 touch_x, f32 touch_y) {
|
|
SetAxis(touch_identifier, mouse_axis_x, touch_x);
|
|
SetAxis(touch_identifier, mouse_axis_y, touch_y);
|
|
}
|
|
|
|
void Mouse::PressButton(int x, int y, MouseButton button) {
|
|
SetButton(identifier, static_cast<int>(button), true);
|
|
|
|
// Set initial analog parameters
|
|
mouse_origin = {x, y};
|
|
last_mouse_position = {x, y};
|
|
button_pressed = true;
|
|
}
|
|
|
|
void Mouse::PressMouseButton(MouseButton button) {
|
|
SetButton(real_mouse_identifier, static_cast<int>(button), true);
|
|
}
|
|
|
|
void Mouse::PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button) {
|
|
SetAxis(touch_identifier, mouse_axis_x, touch_x);
|
|
SetAxis(touch_identifier, mouse_axis_y, touch_y);
|
|
SetButton(touch_identifier, static_cast<int>(button), true);
|
|
}
|
|
|
|
void Mouse::ReleaseButton(MouseButton button) {
|
|
SetButton(identifier, static_cast<int>(button), false);
|
|
SetButton(real_mouse_identifier, static_cast<int>(button), false);
|
|
SetButton(touch_identifier, static_cast<int>(button), false);
|
|
|
|
if (!Settings::values.mouse_panning) {
|
|
SetAxis(identifier, mouse_axis_x, 0);
|
|
SetAxis(identifier, mouse_axis_y, 0);
|
|
}
|
|
|
|
last_motion_change.x = 0;
|
|
last_motion_change.y = 0;
|
|
|
|
button_pressed = false;
|
|
}
|
|
|
|
void Mouse::MouseWheelChange(int x, int y) {
|
|
wheel_position.x += x;
|
|
wheel_position.y += y;
|
|
last_motion_change.z += static_cast<f32>(y) / 100.0f;
|
|
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
|
|
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
|
|
}
|
|
|
|
void Mouse::ReleaseAllButtons() {
|
|
ResetButtonState();
|
|
button_pressed = false;
|
|
}
|
|
|
|
void Mouse::StopPanning() {
|
|
last_mouse_change = {};
|
|
}
|
|
|
|
std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
|
|
std::vector<Common::ParamPackage> devices;
|
|
devices.emplace_back(Common::ParamPackage{
|
|
{"engine", GetEngineName()},
|
|
{"display", "Keyboard/Mouse"},
|
|
});
|
|
return devices;
|
|
}
|
|
|
|
AnalogMapping Mouse::GetAnalogMappingForDevice(
|
|
[[maybe_unused]] const Common::ParamPackage& params) {
|
|
// Only overwrite different buttons from default
|
|
AnalogMapping mapping = {};
|
|
Common::ParamPackage right_analog_params;
|
|
right_analog_params.Set("engine", GetEngineName());
|
|
right_analog_params.Set("axis_x", 0);
|
|
right_analog_params.Set("axis_y", 1);
|
|
right_analog_params.Set("threshold", 0.5f);
|
|
right_analog_params.Set("range", 1.0f);
|
|
right_analog_params.Set("deadzone", 0.0f);
|
|
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
|
|
return mapping;
|
|
}
|
|
|
|
Common::Input::ButtonNames Mouse::GetUIButtonName(const Common::ParamPackage& params) const {
|
|
const auto button = static_cast<MouseButton>(params.Get("button", 0));
|
|
switch (button) {
|
|
case MouseButton::Left:
|
|
return Common::Input::ButtonNames::ButtonLeft;
|
|
case MouseButton::Right:
|
|
return Common::Input::ButtonNames::ButtonRight;
|
|
case MouseButton::Wheel:
|
|
return Common::Input::ButtonNames::ButtonMouseWheel;
|
|
case MouseButton::Backward:
|
|
return Common::Input::ButtonNames::ButtonBackward;
|
|
case MouseButton::Forward:
|
|
return Common::Input::ButtonNames::ButtonForward;
|
|
case MouseButton::Task:
|
|
return Common::Input::ButtonNames::ButtonTask;
|
|
case MouseButton::Extra:
|
|
return Common::Input::ButtonNames::ButtonExtra;
|
|
case MouseButton::Undefined:
|
|
default:
|
|
return Common::Input::ButtonNames::Undefined;
|
|
}
|
|
}
|
|
|
|
Common::Input::ButtonNames Mouse::GetUIName(const Common::ParamPackage& params) const {
|
|
if (params.Has("button")) {
|
|
return GetUIButtonName(params);
|
|
}
|
|
if (params.Has("axis")) {
|
|
return Common::Input::ButtonNames::Value;
|
|
}
|
|
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
|
|
return Common::Input::ButtonNames::Engine;
|
|
}
|
|
if (params.Has("motion")) {
|
|
return Common::Input::ButtonNames::Engine;
|
|
}
|
|
|
|
return Common::Input::ButtonNames::Invalid;
|
|
}
|
|
|
|
} // namespace InputCommon
|