From 05531db92fe2d94cc166803c9cff54e872afb8db Mon Sep 17 00:00:00 2001 From: Windsurf7 Date: Mon, 26 Jan 2026 14:19:56 +0300 Subject: [PATCH] Add mouse-based gyro emulation 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/pad_thread.cpp | 53 +++++++++++++++++++++++ rpcs3/rpcs3qt/gui_application.cpp | 71 ++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/rpcs3/Input/pad_thread.cpp b/rpcs3/Input/pad_thread.cpp index afc5a73fce..4a2eb83530 100644 --- a/rpcs3/Input/pad_thread.cpp +++ b/rpcs3/Input/pad_thread.cpp @@ -34,6 +34,14 @@ LOG_CHANNEL(sys_log, "SYS"); +// Mouse-based motion sensor emulation state. +// Written from the Qt native event handler and consumed by pad_thread. +std::atomic g_mouse_gyro_rmb{false}; // Whether right mouse button is currently held (gyro active) +std::atomic g_mouse_gyro_dx{0}; // Accumulated mouse X delta +std::atomic g_mouse_gyro_dy{0}; // Accumulated mouse Y delta +std::atomic g_mouse_gyro_wheel{0}; // Accumulated mouse wheel delta +std::atomic g_mouse_gyro_reset{false}; // One-shot reset request on right mouse button release + extern void pad_state_notify_state_change(usz index, u32 state); extern bool is_input_allowed(); extern std::string g_input_config_override; @@ -603,6 +611,51 @@ void pad_thread::operator()() apply_copilots(); + // Inject hardcoded mouse-based motion deltas into pad sensors for gyro emulation. + // The Qt frontend accumulates deltas while RMB is held. + if (Emu.IsRunning()) + { + const bool reset = g_mouse_gyro_reset.exchange(false, std::memory_order_relaxed); + + const s32 dx = g_mouse_gyro_dx.exchange(0, std::memory_order_relaxed); + const s32 dy = g_mouse_gyro_dy.exchange(0, std::memory_order_relaxed); + const s32 wh = g_mouse_gyro_wheel.exchange(0, std::memory_order_relaxed); + + if (dx || dy || wh || reset) + { + auto clamp_u16_0_1023 = [](s32 v) -> u16 + { + return static_cast(std::clamp(v, 0, 1023)); + }; + + for (const auto& pad : m_pads) + { + if (!pad || !pad->is_connected()) + continue; + + if (reset) + { + // RMB released → reset motion + // 512 is the neutral value within the 0-1023 motion range. + pad->m_sensors[0].m_value = 512; + pad->m_sensors[1].m_value = 512; + pad->m_sensors[2].m_value = 512; + } + 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 = + clamp_u16_0_1023(static_cast(pad->m_sensors[0].m_value) + dx); // Mouse X → Motion X + pad->m_sensors[1].m_value = + clamp_u16_0_1023(static_cast(pad->m_sensors[1].m_value) + wh); // Mouse Wheel → Motion Y + pad->m_sensors[2].m_value = + clamp_u16_0_1023(static_cast(pad->m_sensors[2].m_value) + dy); // Mouse Y → Motion Z + } + } + } + } + if (Emu.IsRunning()) { update_pad_states(); diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index 113f5a6ebb..26795e2b13 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -70,6 +70,13 @@ LOG_CHANNEL(gui_log, "GUI"); +// Mouse-based motion sensor emulation state (defined in pad_thread) +extern std::atomic g_mouse_gyro_rmb; +extern std::atomic g_mouse_gyro_dx; +extern std::atomic g_mouse_gyro_dy; +extern std::atomic g_mouse_gyro_wheel; +extern std::atomic g_mouse_gyro_reset; + std::unique_ptr g_raw_mouse_handler; s32 gui_application::m_language_id = static_cast(CELL_SYSUTIL_LANG_ENGLISH_US); @@ -1356,7 +1363,9 @@ bool gui_application::native_event_filter::nativeEventFilter([[maybe_unused]] co if (eventType == "windows_generic_MSG") { - if (MSG* msg = static_cast(message); msg && (msg->message == WM_INPUT || msg->message == WM_KEYDOWN || msg->message == WM_KEYUP || msg->message == WM_DEVICECHANGE)) + if (MSG* msg = static_cast(message); msg && (msg->message == WM_INPUT || msg->message == WM_KEYDOWN || msg->message == WM_KEYUP || msg->message == WM_DEVICECHANGE + || msg->message == WM_MOUSEMOVE || msg->message == WM_MOUSEWHEEL + || msg->message == WM_LBUTTONDOWN || msg->message == WM_LBUTTONUP || msg->message == WM_RBUTTONDOWN || msg->message == WM_RBUTTONUP)) { if (msg->message == WM_DEVICECHANGE && (msg->wParam == DBT_DEVICEARRIVAL || msg->wParam == DBT_DEVICEREMOVECOMPLETE)) { @@ -1367,6 +1376,66 @@ bool gui_application::native_event_filter::nativeEventFilter([[maybe_unused]] co return false; } + // Hardcoded mouse-based motion input. + // Captures native mouse events while the game window is focused. + // Accumulates deltas for motion sensor emulation when RMB is held. + // Intentionally independent of chosen pad configuration. + if (Emu.IsRunning() && GetForegroundWindow() == msg->hwnd) + { + switch (msg->message) + { + case WM_RBUTTONDOWN: + // Enable mouse-driven gyro emulation while RMB is held. + g_mouse_gyro_rmb.store(true, std::memory_order_relaxed); + break; + + case WM_RBUTTONUP: + // Disable gyro emulation and request a one-shot motion reset. + g_mouse_gyro_rmb.store(false, std::memory_order_relaxed); + g_mouse_gyro_reset.store(true, std::memory_order_relaxed); + break; + + case WM_MOUSEMOVE: + { + // Track relative mouse movement using a persistent last cursor position. + static POINT last{}; + POINT cur; + cur.x = static_cast(LOWORD(msg->lParam)); + cur.y = static_cast(HIWORD(msg->lParam)); + + // Initialize reference position on first event. + if (last.x == 0 && last.y == 0) + last = cur; + + const s32 dx = cur.x - last.x; + const s32 dy = cur.y - last.y; + last = cur; + + // Accumulate deltas only while gyro emulation is active. + if (g_mouse_gyro_rmb.load(std::memory_order_relaxed)) + { + g_mouse_gyro_dx.fetch_add(dx, std::memory_order_relaxed); + g_mouse_gyro_dy.fetch_add(dy, std::memory_order_relaxed); + } + break; + } + + case WM_MOUSEWHEEL: + { + // Accumulate mouse wheel steps as motion input as well. + if (g_mouse_gyro_rmb.load(std::memory_order_relaxed)) + { + const s32 steps = GET_WHEEL_DELTA_WPARAM(msg->wParam) / WHEEL_DELTA; + g_mouse_gyro_wheel.fetch_add(steps, std::memory_order_relaxed); + } + break; + } + + default: + break; + } + } + if (auto* handler = g_fxo->try_get(); handler && handler->type == mouse_handler::raw) { static_cast(handler)->handle_native_event(*msg);