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.
This commit is contained in:
Windsurf7 2026-01-26 14:19:56 +03:00
parent f883718b23
commit 05531db92f
2 changed files with 123 additions and 1 deletions

View File

@ -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<bool> g_mouse_gyro_rmb{false}; // Whether right mouse button is currently held (gyro active)
std::atomic<s32> g_mouse_gyro_dx{0}; // Accumulated mouse X delta
std::atomic<s32> g_mouse_gyro_dy{0}; // Accumulated mouse Y delta
std::atomic<s32> g_mouse_gyro_wheel{0}; // Accumulated mouse wheel delta
std::atomic<bool> 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<u16>(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<s32>(pad->m_sensors[0].m_value) + dx); // Mouse X → Motion X
pad->m_sensors[1].m_value =
clamp_u16_0_1023(static_cast<s32>(pad->m_sensors[1].m_value) + wh); // Mouse Wheel → Motion Y
pad->m_sensors[2].m_value =
clamp_u16_0_1023(static_cast<s32>(pad->m_sensors[2].m_value) + dy); // Mouse Y → Motion Z
}
}
}
}
if (Emu.IsRunning())
{
update_pad_states();

View File

@ -70,6 +70,13 @@
LOG_CHANNEL(gui_log, "GUI");
// Mouse-based motion sensor emulation state (defined in pad_thread)
extern std::atomic<bool> g_mouse_gyro_rmb;
extern std::atomic<s32> g_mouse_gyro_dx;
extern std::atomic<s32> g_mouse_gyro_dy;
extern std::atomic<s32> g_mouse_gyro_wheel;
extern std::atomic<bool> g_mouse_gyro_reset;
std::unique_ptr<raw_mouse_handler> g_raw_mouse_handler;
s32 gui_application::m_language_id = static_cast<s32>(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<MSG*>(message); msg && (msg->message == WM_INPUT || msg->message == WM_KEYDOWN || msg->message == WM_KEYUP || msg->message == WM_DEVICECHANGE))
if (MSG* msg = static_cast<MSG*>(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<short>(LOWORD(msg->lParam));
cur.y = static_cast<short>(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<MouseHandlerBase>(); handler && handler->type == mouse_handler::raw)
{
static_cast<raw_mouse_handler*>(handler)->handle_native_event(*msg);