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);