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