From 588cf69dad726a4485af945ba0507193c526ccf7 Mon Sep 17 00:00:00 2001 From: Windsurf7 <70599421+Windsurf7@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:21:02 +0300 Subject: [PATCH] Add mouse-based gyro emulation (#18113) This change adds a hardcoded mouse-based motion sensor emulation feature, inspired by how Cemu handles mouse-driven gyro input. While the game window is focused, holding the right mouse button enables gyro emulation: - Mouse X movement feeds Motion X - Mouse Y movement feeds Motion Z - Mouse Wheel feeds Motion Y The axis mapping and behavior were tested with the "Spark Runner" minigame in _Sly Cooper: Thieves in Time_ and _Bentley's Hackpack_. In accordance with this minigame, a top-down view motion control scheme relies on the X/Z axes. While the right mouse button is being held, mouse deltas are captured via the Qt native event filter and accumulated in the frontend, then consumed by the pad thread. On right mouse button release, motion values are reset to the neutral center to avoid residual drift. This input path is intentionally independent of pad configuration and works even when a keyboard-only profile is selected. This implementation thus resolves issue #13883 by allowing motion-only gameplay without requiring a physical motion-capable controller. --- rpcs3/Input/mouse_gyro_handler.cpp | 146 ++++++++++++++++++++++++++++ rpcs3/Input/mouse_gyro_handler.h | 33 +++++++ rpcs3/Input/pad_thread.cpp | 7 ++ rpcs3/Input/pad_thread.h | 5 + rpcs3/rpcs3.vcxproj | 2 + rpcs3/rpcs3.vcxproj.filters | 6 ++ rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/gs_frame.cpp | 20 ++++ rpcs3/rpcs3qt/shortcut_settings.cpp | 2 + rpcs3/rpcs3qt/shortcut_settings.h | 1 + 10 files changed, 223 insertions(+) create mode 100644 rpcs3/Input/mouse_gyro_handler.cpp create mode 100644 rpcs3/Input/mouse_gyro_handler.h diff --git a/rpcs3/Input/mouse_gyro_handler.cpp b/rpcs3/Input/mouse_gyro_handler.cpp new file mode 100644 index 0000000000..6f1c7cd637 --- /dev/null +++ b/rpcs3/Input/mouse_gyro_handler.cpp @@ -0,0 +1,146 @@ +#include "mouse_gyro_handler.h" + +#include +#include +#include +#include + +#include + +void mouse_gyro_handler::clear() +{ + active = false; + reset = false; + gyro_x = DEFAULT_MOTION_X; + gyro_y = DEFAULT_MOTION_Y; + gyro_z = DEFAULT_MOTION_Z; +} + +bool mouse_gyro_handler::toggle_enabled() +{ + enabled = !enabled; + clear(); + return enabled; +} + +void mouse_gyro_handler::set_gyro_active() +{ + active = true; +} + +void mouse_gyro_handler::set_gyro_reset() +{ + active = false; + reset = true; +} + +void mouse_gyro_handler::set_gyro_xz(s32 off_x, s32 off_y) +{ + if (!active) + return; + + gyro_x = static_cast(std::clamp(off_x, 0, DEFAULT_MOTION_X * 2 - 1)); + gyro_z = static_cast(std::clamp(off_y, 0, DEFAULT_MOTION_Z * 2 - 1)); +} + +void mouse_gyro_handler::set_gyro_y(s32 steps) +{ + if (!active) + return; + + gyro_y = static_cast(std::clamp(gyro_y + steps, 0, DEFAULT_MOTION_Y * 2 - 1)); +} + +void mouse_gyro_handler::handle_event(QEvent* ev, const QWindow& win) +{ + if (!enabled) + return; + + // Mouse-based motion input. + // Captures mouse events while the game window is focused. + // Updates motion sensor values via mouse position and mouse wheel while RMB is held. + // Intentionally independent of chosen pad configuration. + switch (ev->type()) + { + case QEvent::MouseButtonPress: + { + auto* e = static_cast(ev); + if (e->button() == Qt::RightButton) + { + // Enable mouse-driven gyro emulation while RMB is held. + set_gyro_active(); + } + break; + } + case QEvent::MouseButtonRelease: + { + auto* e = static_cast(ev); + if (e->button() == Qt::RightButton) + { + // Disable gyro emulation and request a one-shot motion reset. + set_gyro_reset(); + } + break; + } + case QEvent::MouseMove: + { + auto* e = static_cast(ev); + + // Track cursor offset from window center. + const QPoint center(win.width() / 2, win.height() / 2); + const QPoint cur = e->position().toPoint(); + + const s32 off_x = cur.x() - center.x() + DEFAULT_MOTION_X; + const s32 off_y = cur.y() - center.y() + DEFAULT_MOTION_Z; + + // Determine motion from relative mouse position while gyro emulation is active. + set_gyro_xz(off_x, off_y); + + break; + } + case QEvent::Wheel: + { + auto* e = static_cast(ev); + + // Track mouse wheel steps. + const s32 steps = e->angleDelta().y() / 120; + + // Accumulate mouse wheel steps while gyro emulation is active. + set_gyro_y(steps); + + break; + } + default: + { + break; + } + } +} + +void mouse_gyro_handler::apply_gyro(const std::shared_ptr& pad) +{ + if (!enabled) + return; + + if (!pad || !pad->is_connected()) + return; + + // Inject mouse-based motion sensor values into pad sensors for gyro emulation. + // The Qt frontend maps cursor offset and wheel input to absolute motion values while RMB is held. + if (reset) + { + // RMB released → reset motion + pad->m_sensors[0].m_value = DEFAULT_MOTION_X; + pad->m_sensors[1].m_value = DEFAULT_MOTION_Y; + pad->m_sensors[2].m_value = DEFAULT_MOTION_Z; + clear(); + } + else + { + // RMB held → accumulate motion + // Axes have been chosen as tested in Sly 4 minigames. Top-down view motion uses X/Z axes. + pad->m_sensors[0].m_value = gyro_x; // Mouse X → Motion X + pad->m_sensors[1].m_value = gyro_y; // Mouse Wheel → Motion Y + pad->m_sensors[2].m_value = gyro_z; // Mouse Y → Motion Z + } +} diff --git a/rpcs3/Input/mouse_gyro_handler.h b/rpcs3/Input/mouse_gyro_handler.h new file mode 100644 index 0000000000..97a745d919 --- /dev/null +++ b/rpcs3/Input/mouse_gyro_handler.h @@ -0,0 +1,33 @@ +#pragma once + +#include "util/types.hpp" +#include "util/atomic.hpp" +#include "Emu/Io/pad_types.h" + +class QEvent; +class QWindow; + +// Mouse-based motion sensor emulation state. +class mouse_gyro_handler +{ +private: + atomic_t enabled = false; // Whether mouse-based gyro emulation mode has been enabled by using the associated hotkey + + atomic_t active = false; // Whether right mouse button is currently held (gyro active) + atomic_t reset = false; // One-shot reset request on right mouse button release + atomic_t gyro_x = DEFAULT_MOTION_X; // Accumulated from mouse X position relative to center + atomic_t gyro_y = DEFAULT_MOTION_Y; // Accumulated from mouse wheel delta + atomic_t gyro_z = DEFAULT_MOTION_Z; // Accumulated from mouse Y position relative to center + + void set_gyro_active(); + void set_gyro_reset(); + void set_gyro_xz(s32 off_x, s32 off_y); + void set_gyro_y(s32 steps); + +public: + void clear(); + bool toggle_enabled(); + + void handle_event(QEvent* ev, const QWindow& win); + void apply_gyro(const std::shared_ptr& pad); +}; diff --git a/rpcs3/Input/pad_thread.cpp b/rpcs3/Input/pad_thread.cpp index afc5a73fce..40887ae5fb 100644 --- a/rpcs3/Input/pad_thread.cpp +++ b/rpcs3/Input/pad_thread.cpp @@ -81,6 +81,9 @@ void pad_thread::Init() { std::lock_guard lock(pad::g_pad_mutex); + // Reset mouse-based gyro state + m_mouse_gyro.clear(); + // Cache old settings if possible std::array pad_settings; for (u32 i = 0; i < CELL_PAD_MAX_PORT_NUM; i++) // max 7 pads @@ -606,6 +609,10 @@ void pad_thread::operator()() if (Emu.IsRunning()) { update_pad_states(); + + // Apply mouse-based gyro emulation. + // Intentionally bound to Player 1 only. + m_mouse_gyro.apply_gyro(m_pads[0]); } m_info.now_connect = connected_devices + num_ldd_pad; diff --git a/rpcs3/Input/pad_thread.h b/rpcs3/Input/pad_thread.h index 7b0e0b79fb..20f53e9034 100644 --- a/rpcs3/Input/pad_thread.h +++ b/rpcs3/Input/pad_thread.h @@ -5,6 +5,7 @@ #include "Emu/Io/pad_types.h" #include "Emu/Io/pad_config.h" #include "Emu/Io/pad_config_types.h" +#include "Input/mouse_gyro_handler.h" #include "Utilities/mutex.h" #include @@ -41,6 +42,8 @@ public: static auto constexpr thread_name = "Pad Thread"sv; + mouse_gyro_handler& get_mouse_gyro() { return m_mouse_gyro; } + protected: void Init(); void InitLddPad(u32 handle, const u32* port_status); @@ -67,6 +70,8 @@ private: bool m_resume_emulation_flag = false; bool m_ps_button_pressed = false; atomic_t m_home_menu_open = false; + + mouse_gyro_handler m_mouse_gyro; }; namespace pad diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 5fbf0491e9..9749f60fcd 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -194,6 +194,7 @@ + @@ -1079,6 +1080,7 @@ $(QTDIR)\bin\moc.exe;%(FullPath) $(QTDIR)\bin\moc.exe;%(FullPath) + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 100a9b1d8f..a011ddf62e 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1272,6 +1272,9 @@ Generated Files\Release + + Io + @@ -1511,6 +1514,9 @@ Io\camera + + Io + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index a8fc4c5886..b59d6f7a11 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -157,6 +157,7 @@ add_library(rpcs3_ui STATIC ../Input/hid_pad_handler.cpp ../Input/keyboard_pad_handler.cpp ../Input/mm_joystick_handler.cpp + ../Input/mouse_gyro_handler.cpp ../Input/pad_thread.cpp ../Input/product_info.cpp ../Input/ps_move_calibration.cpp diff --git a/rpcs3/rpcs3qt/gs_frame.cpp b/rpcs3/rpcs3qt/gs_frame.cpp index db66ce68a4..6557168cce 100644 --- a/rpcs3/rpcs3qt/gs_frame.cpp +++ b/rpcs3/rpcs3qt/gs_frame.cpp @@ -19,6 +19,7 @@ #include "Emu/RSX/Overlays/overlay_message.h" #include "Emu/Io/interception.h" #include "Emu/Io/recording_config.h" +#include "Input/pad_thread.h" #include #include @@ -402,6 +403,15 @@ void gs_frame::handle_shortcut(gui::shortcuts::shortcut shortcut_key, const QKey audio::change_volume(-5); break; } + case gui::shortcuts::shortcut::gw_toggle_mouse_gyro: + { + if (auto* pad_thr = pad::get_pad_thread(true)) + { + const bool mouse_gyro_enabled = pad_thr->get_mouse_gyro().toggle_enabled(); + gui_log.notice("Mouse-based gyro emulation %s", mouse_gyro_enabled ? "enabled" : "disabled"); + } + break; + } default: { break; @@ -1216,6 +1226,16 @@ bool gs_frame::event(QEvent* ev) // This will make the cursor visible again if it was hidden by the mouse idle timeout handle_cursor(visibility(), false, false, true); } + + // Handle events for mouse-based gyro emulation. + if (Emu.IsRunning()) + { + if (auto* pad_thr = pad::get_pad_thread(true)) + { + pad_thr->get_mouse_gyro().handle_event(ev, *this); + } + } + return QWindow::event(ev); } diff --git a/rpcs3/rpcs3qt/shortcut_settings.cpp b/rpcs3/rpcs3qt/shortcut_settings.cpp index 039f493dae..64feb94777 100644 --- a/rpcs3/rpcs3qt/shortcut_settings.cpp +++ b/rpcs3/rpcs3qt/shortcut_settings.cpp @@ -37,6 +37,7 @@ void fmt_class_string::format(std::string& out, u64 arg) case shortcut::gw_mute_unmute: return "gw_mute_unmute"; case shortcut::gw_volume_up: return "gw_volume_up"; case shortcut::gw_volume_down: return "gw_volume_down"; + case shortcut::gw_toggle_mouse_gyro: return "gw_toggle_mouse_gyro"; case shortcut::count: return "count"; } @@ -88,6 +89,7 @@ shortcut_settings::shortcut_settings() { shortcut::gw_mute_unmute, shortcut_info{ "gw_mute_unmute", tr("Mute/Unmute Audio"), "Ctrl+Shift+M", shortcut_handler_id::game_window, false } }, { shortcut::gw_volume_up, shortcut_info{ "gw_volume_up", tr("Volume Up"), "Ctrl+Shift++", shortcut_handler_id::game_window, true } }, { shortcut::gw_volume_down, shortcut_info{ "gw_volume_down", tr("Volume Down"), "Ctrl+Shift+-", shortcut_handler_id::game_window, true } }, + { shortcut::gw_toggle_mouse_gyro, shortcut_info{ "gw_toggle_mouse_gyro", tr("Toggle Mouse-based Gyro"), "Ctrl+G", shortcut_handler_id::game_window, false } }, }) { } diff --git a/rpcs3/rpcs3qt/shortcut_settings.h b/rpcs3/rpcs3qt/shortcut_settings.h index db6458accb..be14ee1e30 100644 --- a/rpcs3/rpcs3qt/shortcut_settings.h +++ b/rpcs3/rpcs3qt/shortcut_settings.h @@ -46,6 +46,7 @@ namespace gui gw_mute_unmute, gw_volume_up, gw_volume_down, + gw_toggle_mouse_gyro, count };