diff --git a/src/graphics/presentation/window/hostInput.cpp b/src/graphics/presentation/window/hostInput.cpp index 37b9aa522..b71ba111b 100644 --- a/src/graphics/presentation/window/hostInput.cpp +++ b/src/graphics/presentation/window/hostInput.cpp @@ -1,18 +1,24 @@ #include "graphics/presentation/window/hostInput.h" +#include "SDL_error.h" +#include "SDL_events.h" #include "SDL_keyboard.h" #include "SDL_keycode.h" #include "SDL_mouse.h" +#include "SDL_timer.h" +#include "SDL_video.h" #include "common/assert.h" #include "common/emulatorConfig.h" +#include "common/logging/log.h" #include "libs/controller.h" #include #include +#include #include +#include #include #include -#include namespace Libs::Graphics { @@ -60,6 +66,17 @@ struct Binding { std::size_t control = INVALID_CONTROL; }; +constexpr int MOUSE_POLL_INTERVAL_MS = 33; +constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity="; + +struct MouseJoystickState { + bool enabled = false; + bool output = false; + uint64_t next_poll = 0; +}; + +MouseJoystickState g_mouse; + std::size_t ControlFromName(std::string_view name) { const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(), [name](const auto& item) { return item.name == name; }); @@ -103,10 +120,17 @@ bool Conflicts(const Binding& first, const Binding& second) { class InputMap { public: - InputMap(): m_custom(!Config::GetKeymap().empty()) { + InputMap() { for (const auto& value: Config::GetKeymap()) { const std::string_view entry = value; - const auto split = entry.find('='); + if (entry.starts_with(MOUSE_SENSITIVITY)) { + const float sensitivity = + std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr); + m_mouse_sensitivity = + std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f); + continue; + } + const auto split = entry.find('='); Binding binding; if (split != std::string_view::npos) { @@ -118,9 +142,9 @@ public: } } - const bool reserved = - binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1 || - binding.key == SDLK_F11; + const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || + binding.key == SDLK_F1 || binding.key == SDLK_F7 || + binding.key == SDLK_F11; if (binding.control == INVALID_CONTROL || reserved || (binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) { EXIT("Invalid input mapping: %s\n", value.c_str()); @@ -128,7 +152,8 @@ public: Add(binding); } } - [[nodiscard]] bool Custom() const { return m_custom; } + [[nodiscard]] bool Custom() const { return m_size != 0; } + [[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; } [[nodiscard]] std::size_t FindKey(int key_code) const { key_code = NormalizeKey(static_cast(key_code)); @@ -159,8 +184,8 @@ private: } std::array m_bindings {}; - std::size_t m_size = 0; - bool m_custom; + std::size_t m_size = 0; + float m_mouse_sensitivity = 1.0f; }; const InputMap& GetInputMap() { @@ -281,6 +306,33 @@ void DefaultKeyboardInput(int key_code, bool down) { default: SetButton(DefaultKeyboardButton(key_code), down); return; } } + +void MouseToJoystick(int delta_x, int delta_y) { + const double distance = std::hypot(delta_x, delta_y); + const double scale = + std::clamp(distance * GetInputMap().MouseSensitivity() + 16.0, 64.0, 128.0) / distance; + const auto map_axis = [scale](int delta) { + return std::clamp(128 + static_cast(std::lround(delta * scale)), 0, 255); + }; + Controller::ControllerRightStick(Controller::HOST_INPUT_CONTROLLER_ID, map_axis(delta_x), + map_axis(delta_y)); +} + +void CenterMouseStick() { + if (!g_mouse.output) { + return; + } + Controller::ControllerRightStick(Controller::HOST_INPUT_CONTROLLER_ID, 128, 128); + g_mouse.output = false; +} + +bool SetRelativeMouseMode(bool enabled) { + if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) == 0) { + return true; + } + LOGF("Mouse-to-joystick relative mode failed: %s\n", SDL_GetError()); + return false; +} } // namespace void HostInputInit() { @@ -303,4 +355,63 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) { } } +void HostInputToggleMouseToJoystick() { + if (g_mouse.enabled) { + SetRelativeMouseMode(false); + CenterMouseStick(); + g_mouse = {}; + LOGF("Mouse to right stick: disabled\n"); + return; + } + + if (!SetRelativeMouseMode(true)) { + return; + } + int ignored_x = 0; + int ignored_y = 0; + SDL_GetRelativeMouseState(&ignored_x, &ignored_y); + g_mouse.enabled = true; + g_mouse.next_poll = SDL_GetTicks64() + MOUSE_POLL_INTERVAL_MS; + LOGF("Mouse to right stick: enabled (F7 to release)\n"); +} + +int PollMouse(uint64_t now_ms) { + if (now_ms < g_mouse.next_poll) { + return static_cast(g_mouse.next_poll - now_ms); + } + g_mouse.next_poll = now_ms + MOUSE_POLL_INTERVAL_MS; + + int delta_x = 0; + int delta_y = 0; + SDL_GetRelativeMouseState(&delta_x, &delta_y); + if (delta_x == 0 && delta_y == 0) { + CenterMouseStick(); + return MOUSE_POLL_INTERVAL_MS; + } + + MouseToJoystick(delta_x, delta_y); + g_mouse.output = true; + return MOUSE_POLL_INTERVAL_MS; +} + +bool HostInputWaitEvent(SDL_Event* event) { + if (!g_mouse.enabled || SDL_GetKeyboardFocus() == nullptr) { + CenterMouseStick(); + if (SDL_WaitEvent(event) == 0) { + EXIT("%s\n", SDL_GetError()); + } + return true; + } + + const int timeout_ms = PollMouse(SDL_GetTicks64()); + SDL_ClearError(); + if (SDL_WaitEventTimeout(event, timeout_ms) != 0) { + return true; + } + if (SDL_GetError()[0] != '\0') { + EXIT("%s\n", SDL_GetError()); + } + return false; +} + } // namespace Libs::Graphics diff --git a/src/graphics/presentation/window/hostInput.h b/src/graphics/presentation/window/hostInput.h index 92121c446..e0a788b10 100644 --- a/src/graphics/presentation/window/hostInput.h +++ b/src/graphics/presentation/window/hostInput.h @@ -3,11 +3,15 @@ #include +union SDL_Event; + namespace Libs::Graphics { -void HostInputInit(); -void HostInputKey(int key_code, bool down); -void HostInputMouseButton(uint8_t mouse_button, bool down); +void HostInputInit(); +void HostInputKey(int key_code, bool down); +void HostInputMouseButton(uint8_t mouse_button, bool down); +void HostInputToggleMouseToJoystick(); +[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event); } // namespace Libs::Graphics diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 7378de743..a61afca8e 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -507,6 +507,13 @@ void WindowContext::ProcessEvent(double time_s) { auto& game = loop; auto* event = &game.event; EXIT_IF(SDL_GetEventState(SDL_DISPLAYEVENT) != SDL_ENABLE); + if ((event->type == SDL_KEYDOWN || event->type == SDL_KEYUP) && + event->key.keysym.sym == SDLK_F7) { + if (event->type == SDL_KEYDOWN && event->key.repeat == 0) { + HostInputToggleMouseToJoystick(); + } + return; + } if (ProcessImeInput(*event)) { return; } @@ -783,8 +790,8 @@ void WindowContext::Run() { timer.Resume(); } - if (SDL_WaitEvent(&loop.event) == 0) { - EXIT("%s\n", SDL_GetError()); + if (!HostInputWaitEvent(&loop.event)) { + continue; } ProcessEvent(timer.GetTimeS()); } diff --git a/src/launcher/include/inputMappingDialog.h b/src/launcher/include/inputMappingDialog.h index 280b3a0aa..ff7aac256 100644 --- a/src/launcher/include/inputMappingDialog.h +++ b/src/launcher/include/inputMappingDialog.h @@ -5,6 +5,7 @@ #include class QPushButton; +class QDoubleSpinBox; class QTreeWidget; class QTreeWidgetItem; class QWidget; @@ -22,10 +23,11 @@ private: void SetBinding(QTreeWidgetItem* item, const QString& binding); void UpdateButtons(); - QTreeWidget* m_bindings = nullptr; - QPushButton* m_change_button = nullptr; - QPushButton* m_clear_button = nullptr; - bool m_customized = false; + QTreeWidget* m_bindings = nullptr; + QPushButton* m_change_button = nullptr; + QPushButton* m_clear_button = nullptr; + QDoubleSpinBox* m_sensitivity = nullptr; + bool m_custom_bindings = false; }; #endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */ diff --git a/src/launcher/src/inputMappingDialog.cpp b/src/launcher/src/inputMappingDialog.cpp index 074356179..e2e29e35f 100644 --- a/src/launcher/src/inputMappingDialog.cpp +++ b/src/launcher/src/inputMappingDialog.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -16,7 +17,9 @@ namespace { -constexpr int BINDING_COLUMN = 1; +constexpr int BINDING_COLUMN = 1; +constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0; +constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity="; struct PadControl { const char* id; @@ -128,7 +131,8 @@ public: auto* layout = new QVBoxLayout(this); m_label = new QLabel( - tr("Press a key or mouse button.\nSpace and F1 are reserved; Esc cancels."), this); + tr("Press a key or mouse button.\nSpace, F1, F7, and F11 are reserved; Esc cancels."), + this); m_label->setAlignment(Qt::AlignCenter); layout->addWidget(m_label); } @@ -144,7 +148,8 @@ protected: reject(); return; } - if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1) { + if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1 || + event->key() == Qt::Key_F7 || event->key() == Qt::Key_F11) { m_label->setText(tr("That key is reserved by the emulator.")); return; } @@ -177,6 +182,9 @@ private: QHash ParseMapping(const QStringList& mapping) { QHash result; for (const auto& entry: mapping) { + if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) { + continue; + } const auto separator = entry.indexOf(QLatin1Char('=')); if (separator > 0 && separator + 1 < entry.size()) { const auto binding = entry.mid(separator + 1); @@ -193,15 +201,42 @@ QHash ParseMapping(const QStringList& mapping) { return result; } +double ParseMouseSensitivity(const QStringList& mapping) { + for (const auto& entry: mapping) { + if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) { + return entry.mid(sizeof(MOUSE_SENSITIVITY) - 1).toDouble(); + } + } + return DEFAULT_MOUSE_SENSITIVITY; +} + } // namespace InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent) - : QDialog(parent), m_customized(!mapping.isEmpty()) { + : QDialog(parent) { setWindowTitle(tr("Input Mapping")); resize(460, 650); auto* layout = new QVBoxLayout(this); - layout->addWidget(new QLabel(tr("Map keyboard or mouse input to DualSense controls."), this)); + layout->addWidget( + new QLabel(tr("Map keyboard or mouse buttons to DualSense controls.\n" + "Press F7 in-game to toggle mouse movement on the right stick."), + this)); + + const auto parsed = ParseMapping(mapping); + m_custom_bindings = !parsed.isEmpty(); + + auto* sensitivity_layout = new QHBoxLayout; + sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this)); + m_sensitivity = new QDoubleSpinBox(this); + m_sensitivity->setRange(0.1, 5.0); + m_sensitivity->setSingleStep(0.1); + m_sensitivity->setDecimals(1); + m_sensitivity->setSuffix(QStringLiteral("x")); + m_sensitivity->setValue(ParseMouseSensitivity(mapping)); + sensitivity_layout->addWidget(m_sensitivity); + sensitivity_layout->addStretch(); + layout->addLayout(sensitivity_layout); m_bindings = new QTreeWidget(this); m_bindings->setColumnCount(2); @@ -212,13 +247,12 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* pare m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch); layout->addWidget(m_bindings); - const auto parsed = ParseMapping(mapping); for (const auto& control: PAD_CONTROLS) { auto* item = new QTreeWidgetItem(m_bindings); item->setText(0, tr(control.label)); item->setData(0, Qt::UserRole, QString::fromLatin1(control.id)); - SetBinding(item, m_customized ? parsed.value(QString::fromLatin1(control.id)) - : QString::fromLatin1(control.default_binding)); + SetBinding(item, m_custom_bindings ? parsed.value(QString::fromLatin1(control.id)) + : QString::fromLatin1(control.default_binding)); } m_bindings->setCurrentItem(m_bindings->topLevelItem(0)); @@ -248,16 +282,19 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* pare QStringList InputMappingDialog::Mapping() const { QStringList result; - if (!m_customized) { - return result; - } - for (int index = 0; index < m_bindings->topLevelItemCount(); index++) { - const auto* item = m_bindings->topLevelItem(index); - const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString(); - if (!binding.isEmpty()) { - result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding); + if (m_custom_bindings) { + for (int index = 0; index < m_bindings->topLevelItemCount(); index++) { + const auto* item = m_bindings->topLevelItem(index); + const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString(); + if (!binding.isEmpty()) { + result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding); + } } } + if (m_sensitivity->value() != DEFAULT_MOUSE_SENSITIVITY) { + result.append(QLatin1String(MOUSE_SENSITIVITY) + + QString::number(m_sensitivity->value(), 'f', 1)); + } return result; } @@ -281,12 +318,12 @@ void InputMappingDialog::ChangeBinding() { } } SetBinding(item, dialog.Binding()); - m_customized = true; + m_custom_bindings = true; } void InputMappingDialog::ClearBinding() { SetBinding(m_bindings->currentItem(), {}); - m_customized = true; + m_custom_bindings = true; } void InputMappingDialog::RestoreDefaults() { @@ -294,7 +331,8 @@ void InputMappingDialog::RestoreDefaults() { SetBinding(m_bindings->topLevelItem(index), QString::fromLatin1(PAD_CONTROLS[index].default_binding)); } - m_customized = false; + m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY); + m_custom_bindings = false; } void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) { diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index 32a6c3483..d8712b9c8 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -61,6 +61,7 @@ public: void Disconnect(int id); void Button(int id, uint32_t button, bool down); void Axis(int id, Axis axis, int value); + void RightStick(int id, int x, int y); void ResetInputState(); void GetConnectionInfo(bool* flag, int* count); void SetVibration(uint8_t large_motor, uint8_t small_motor); @@ -265,6 +266,18 @@ void GameController::Axis(int id, Controller::Axis axis, int value) { } } +void GameController::RightStick(int id, int x, int y) { + Common::LockGuard lock(m_mutex); + + if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) { + auto state = GetLastState(); + state.time = LibKernel::KernelGetProcessTime(); + state.axes[static_cast(Axis::RightX)] = x; + state.axes[static_cast(Axis::RightY)] = y; + AddState(state); + } +} + void GameController::ResetInputState() { Common::LockGuard lock(m_mutex); ControllerState state {}; @@ -371,6 +384,12 @@ void ControllerAxis(int id, Axis axis, int value) { g_controller->Axis(id, axis, value); } +void ControllerRightStick(int id, int x, int y) { + EXIT_IF(g_controller == nullptr); + + g_controller->RightStick(id, x, y); +} + void ControllerResetInputState() { EXIT_IF(g_controller == nullptr); g_controller->ResetInputState(); diff --git a/src/libs/controller.h b/src/libs/controller.h index 3403f6b8f..bdc938433 100644 --- a/src/libs/controller.h +++ b/src/libs/controller.h @@ -58,6 +58,7 @@ void ControllerConnect(int id); void ControllerDisconnect(int id); void ControllerButton(int id, uint32_t button, bool down); void ControllerAxis(int id, Axis axis, int value); +void ControllerRightStick(int id, int x, int y); void ControllerResetInputState(); int KYTY_SYSV_ABI PadInit();