diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 5c079715c4..a4e29bbd83 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -168,6 +168,7 @@ else() ${DBUS_LINK_LIBRARIES} X11::X11 X11::Xrandr + X11::Xi ) if(USE_BACKTRACE) target_compile_definitions(common PRIVATE "HAS_LIBBACKTRACE=1") diff --git a/common/Darwin/DarwinMisc.cpp b/common/Darwin/DarwinMisc.cpp index 1ebe1a1b88..400c26c9b5 100644 --- a/common/Darwin/DarwinMisc.cpp +++ b/common/Darwin/DarwinMisc.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include // Darwin (OSX) is a bit different from Linux when requesting properties of @@ -127,6 +128,69 @@ bool Common::InhibitScreensaver(bool inhibit) return true; } +void Common::SetMousePosition(int x, int y) +{ + // Little bit ugly but; + // Creating mouse move events and posting them wasn't very reliable. + // Calling CGWarpMouseCursorPosition without CGAssociateMouseAndMouseCursorPosition(false) + // ends up with the cursor feeling "sticky". + CGAssociateMouseAndMouseCursorPosition(false); + CGWarpMouseCursorPosition(CGPointMake(x, y)); + CGAssociateMouseAndMouseCursorPosition(true); // The default state + return; +} + +CFMachPortRef mouseEventTap = nullptr; +CFRunLoopSourceRef mouseRunLoopSource = nullptr; + +static std::function fnMouseMoveCb; +CGEventRef mouseMoveCallback(CGEventTapProxy, CGEventType type, CGEventRef event, void* arg) +{ + if (type == kCGEventMouseMoved) + { + const CGPoint location = CGEventGetLocation(event); + fnMouseMoveCb(location.x, location.y); + } + return event; +} + +bool Common::AttachMousePositionCb(std::function cb) +{ + if (!AXIsProcessTrusted()) + { + Console.Warning("Process isn't trusted with accessibility permissions. Mouse tracking will not work!"); + } + + fnMouseMoveCb = cb; + mouseEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, + CGEventMaskBit(kCGEventMouseMoved), mouseMoveCallback, nullptr); + if (!mouseEventTap) + { + Console.Warning("Unable to create mouse moved event tap. Mouse tracking will not work!"); + return false; + } + + mouseRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopCommonModes); + + return true; +} + +void Common::DetachMousePositionCb() +{ + if (mouseRunLoopSource) + { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopCommonModes); + CFRelease(mouseRunLoopSource); + } + if (mouseEventTap) + { + CFRelease(mouseEventTap); + } + mouseRunLoopSource = nullptr; + mouseEventTap = nullptr; +} + void Threading::Sleep(int ms) { usleep(1000 * ms); diff --git a/common/HostSys.h b/common/HostSys.h index c9a0a2759b..b41581a7d1 100644 --- a/common/HostSys.h +++ b/common/HostSys.h @@ -6,6 +6,7 @@ #include "common/Pcsx2Defs.h" #include +#include #include #include #include @@ -198,4 +199,8 @@ namespace Common /// Abstracts platform-specific code for asynchronously playing a sound. /// On Windows, this will use PlaySound(). On Linux, it will shell out to aplay. On MacOS, it uses NSSound. bool PlaySoundAsync(const char* path); + + void SetMousePosition(int x, int y); + bool AttachMousePositionCb(std::function cb); + void DetachMousePositionCb(); } // namespace Common diff --git a/common/Linux/LnxMisc.cpp b/common/Linux/LnxMisc.cpp index 2c36c9bab7..acefebe701 100644 --- a/common/Linux/LnxMisc.cpp +++ b/common/Linux/LnxMisc.cpp @@ -13,17 +13,20 @@ #include "fmt/format.h" -#include -#include -#include -#include +#include #include #include #include #include -#include +#include +#include + #include #include +#include +#include +#include +#include // Returns 0 on failure (not supported by the operating system). u64 GetPhysicalMemory() @@ -177,6 +180,111 @@ bool Common::InhibitScreensaver(bool inhibit) return SetScreensaverInhibitDBus(inhibit, "PCSX2", "PCSX2 VM is running."); } +void Common::SetMousePosition(int x, int y) +{ + Display* display = XOpenDisplay(nullptr); + if (!display) + return; + + Window root = DefaultRootWindow(display); + XWarpPointer(display, None, root, 0, 0, 0, 0, x, y); + XFlush(display); + + XCloseDisplay(display); +} + +static std::function fnMouseMoveCb; +static std::atomic trackingMouse = false; +static std::thread mouseThread; + +void mouseEventLoop() +{ + Threading::SetNameOfCurrentThread("X11 Mouse Thread"); + Display* display = XOpenDisplay(nullptr); + if (!display) + { + return; + } + + int opcode, eventcode, error; + if (!XQueryExtension(display, "XInputExtension", &opcode, &eventcode, &error)) + { + XCloseDisplay(display); + return; + } + + const Window root = DefaultRootWindow(display); + XIEventMask evmask; + unsigned char mask[(XI_LASTEVENT + 7) / 8] = {0}; + + evmask.deviceid = XIAllDevices; + evmask.mask_len = sizeof(mask); + evmask.mask = mask; + XISetMask(mask, XI_RawMotion); + + XISelectEvents(display, root, &evmask, 1); + XSync(display, False); + + XEvent event; + while (trackingMouse) + { + // XNextEvent is blocking, this is a zombie process risk if no events arrive + // while we are trying to shutdown. + // https://nrk.neocities.org/articles/x11-timeout-with-xsyncalarm might be + // a better solution than using XPending. + if (!XPending(display)) + { + Threading::Sleep(1); + Threading::SpinWait(); + continue; + } + + XNextEvent(display, &event); + if (event.xcookie.type == GenericEvent && + event.xcookie.extension == opcode && + XGetEventData(display, &event.xcookie)) + { + XIRawEvent* raw_event = reinterpret_cast(event.xcookie.data); + if (raw_event->evtype == XI_RawMotion) + { + Window w; + int root_x, root_y, win_x, win_y; + unsigned int mask; + XQueryPointer(display, root, &w, &w, &root_x, &root_y, &win_x, &win_y, &mask); + + if (fnMouseMoveCb) + fnMouseMoveCb(root_x, root_y); + } + XFreeEventData(display, &event.xcookie); + } + } + + XCloseDisplay(display); +} + +bool Common::AttachMousePositionCb(std::function cb) +{ + fnMouseMoveCb = cb; + + if (trackingMouse) + return true; + + trackingMouse = true; + mouseThread = std::thread(mouseEventLoop); + mouseThread.detach(); + return true; +} + +void Common::DetachMousePositionCb() +{ + trackingMouse = false; + fnMouseMoveCb = nullptr; + if (mouseThread.joinable()) + { + mouseThread.join(); + } +} + bool Common::PlaySoundAsync(const char* path) { #ifdef __linux__ diff --git a/common/Windows/WinMisc.cpp b/common/Windows/WinMisc.cpp index 44c5da73fe..3cbdc201d0 100644 --- a/common/Windows/WinMisc.cpp +++ b/common/Windows/WinMisc.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ +#include "common/Console.h" #include "common/FileSystem.h" #include "common/HostSys.h" #include "common/RedtapeWindows.h" @@ -86,6 +87,61 @@ bool Common::InhibitScreensaver(bool inhibit) return true; } +void Common::SetMousePosition(int x, int y) +{ + SetCursorPos(x, y); +} + +/* +static HHOOK mouseHook = nullptr; +static std::function fnMouseMoveCb; +LRESULT CALLBACK Mousecb(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode >= 0 && wParam == WM_MOUSEMOVE) + { + MSLLHOOKSTRUCT* mouse = (MSLLHOOKSTRUCT*)lParam; + fnMouseMoveCb(mouse->pt.x, mouse->pt.y); + } + return CallNextHookEx(mouseHook, nCode, wParam, lParam); +} +*/ + +// This (and the above) works, but is not recommended on Windows and is only here for consistency. +// Defer to using raw input instead. +bool Common::AttachMousePositionCb(std::function cb) +{ + /* + if (mouseHook) + Common::DetachMousePositionCb(); + + fnMouseMoveCb = cb; + mouseHook = SetWindowsHookEx(WH_MOUSE_LL, Mousecb, GetModuleHandle(NULL), 0); + if (!mouseHook) + { + Console.Warning("Failed to set mouse hook: %d", GetLastError()); + return false; + } + + #if defined(PCSX2_DEBUG) || defined(PCSX2_DEVBUILD) + static bool warned = false; + if (!warned) + { + Console.Warning("Mouse hooks are enabled, and this isn't a release build! Using a debugger, or loading symbols, _will_ stall the hook and cause global mouse lag."); + warned = true; + } + #endif + */ + return true; +} + +void Common::DetachMousePositionCb() +{ + /* + UnhookWindowsHookEx(mouseHook); + mouseHook = nullptr; + */ +} + bool Common::PlaySoundAsync(const char* path) { const std::wstring wpath = FileSystem::GetWin32Path(path); diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index f6c3488a01..3d94c00fac 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -107,7 +107,6 @@ MainWindow::MainWindow() #if !defined(_WIN32) && !defined(__APPLE__) s_use_central_widget = DisplayContainer::isRunningOnWayland(); #endif - createCheckMousePositionTimer(); } MainWindow::~MainWindow() @@ -116,6 +115,8 @@ MainWindow::~MainWindow() cancelGameListRefresh(); destroySubWindows(); + Common::DetachMousePositionCb(); + // we compare here, since recreate destroys the window later if (g_main_window == this) g_main_window = nullptr; @@ -151,6 +152,9 @@ void MainWindow::initialize() #ifdef _WIN32 registerForDeviceNotifications(); #endif + + if (Host::GetBoolSettingValue("EmuCore", "EnableMouseLock", false)) + setupMouseMoveHandler(); } // TODO: Figure out how to set this in the .ui file @@ -1070,12 +1074,12 @@ bool MainWindow::shouldHideMainWindow() const QtHost::InNoGUIMode(); } -bool MainWindow::shouldMouseGrab() const +bool MainWindow::shouldMouseLock() const { if (!s_vm_valid || s_vm_paused) return false; - if (!Host::GetBoolSettingValue("EmuCore", "EnableMouseGrab", false)) + if (!Host::GetBoolSettingValue("EmuCore", "EnableMouseLock", false)) return false; bool windowsHidden = (!m_debugger_window || m_debugger_window->isHidden()) && @@ -2238,6 +2242,15 @@ void MainWindow::registerForDeviceNotifications() DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE}; m_device_notification_handle = RegisterDeviceNotificationW((HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES); + + // Set up the raw input device for mouse grabbing + RAWINPUTDEVICE rid; + rid.usUsagePage = 0x01; // Generic desktop controls + rid.usUsage = 0x02; // Mouse + rid.dwFlags = RIDEV_INPUTSINK; + rid.hwndTarget = (HWND)winId(); + + RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)); #endif } @@ -2266,6 +2279,26 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr *result = 1; return true; } + + if (msg->message == WM_INPUT) + { + UINT dwSize = 40; + static BYTE lpb[40]; + if (GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER))) + { + const RAWINPUT* raw = (RAWINPUT*)lpb; + if (raw->header.dwType == RIM_TYPEMOUSE) + { + const RAWMOUSE& mouse = raw->data.mouse; + if (mouse.usFlags == MOUSE_MOVE_ABSOLUTE || mouse.usFlags == MOUSE_MOVE_RELATIVE) + { + POINT cursorPos; + GetCursorPos(&cursorPos); + checkMousePosition(cursorPos.x, cursorPos.y); + } + } + } + } } return QMainWindow::nativeEvent(eventType, message, result); @@ -2545,35 +2578,53 @@ QWidget* MainWindow::getDisplayContainer() const return (m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget)); } -void MainWindow::createCheckMousePositionTimer() +void MainWindow::setupMouseMoveHandler() { - m_mouse_check_timer = new QTimer(this); - connect(m_mouse_check_timer, &QTimer::timeout, this, &MainWindow::checkMousePosition); - m_mouse_check_timer->start(16); + auto mouse_cb_fn = [](int x, int y) + { + if(g_main_window) + g_main_window->checkMousePosition(x, y); + }; + + if(!Common::AttachMousePositionCb(mouse_cb_fn)) + { + Console.Warning("Unable to setup mouse position cb!"); + } + + return; } -void MainWindow::checkMousePosition() +void MainWindow::checkMousePosition(int x, int y) { - if (!shouldMouseGrab()) + if (!shouldMouseLock()) return; - QPoint globalCursorPos = QCursor::pos(); - const QRect& windowBounds = isRenderingFullscreen() ? screen()->geometry() : geometry(); - + const QPoint globalCursorPos = {x, y}; + QRect windowBounds = isRenderingFullscreen() ? screen()->geometry() : geometry(); if (windowBounds.contains(globalCursorPos)) return; - QCursor::setPos( + Common::SetMousePosition( std::clamp(globalCursorPos.x(), windowBounds.left(), windowBounds.right()), std::clamp(globalCursorPos.y(), windowBounds.top(), windowBounds.bottom())); -} -void MainWindow::mouseMoveEvent(QMouseEvent* event) -{ - QWidget::mouseMoveEvent(event); + /* + Provided below is how we would handle this if we were using low level hooks (What is used in Common::AttachMouseCb) + We currently use rawmouse on Windows, so Common::SetMousePosition called directly works fine. + */ +#if 0 + // We are currently in a low level hook. SetCursorPos here (what is in Common::SetMousePosition) will not work! + // Let's (a)buse Qt's event loop to dispatch the call at a later time, outside of the hook. + QMetaObject::invokeMethod( + this, [=]() { + Common::SetMousePosition( + std::clamp(globalCursorPos.x(), windowBounds.left(), windowBounds.right()), + std::clamp(globalCursorPos.y(), windowBounds.top(), windowBounds.bottom())); + }, + Qt::QueuedConnection); +#endif } - void MainWindow::saveDisplayWindowGeometryToConfig() { QWidget* container = getDisplayContainer(); diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 6cd1f81ad5..d73621d1b3 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -104,7 +104,7 @@ public: void rescanFile(const std::string& path); void openDebugger(); - + void checkMousePosition(int x, int y); public Q_SLOTS: void checkForUpdates(bool display_message, bool force_check); void refreshGameList(bool invalidate_cache); @@ -128,7 +128,7 @@ private Q_SLOTS: void mouseModeRequested(bool relative_mode, bool hide_cursor); void releaseRenderWindow(); void focusDisplayWidget(); - void createCheckMousePositionTimer(); + void setupMouseMoveHandler(); void onGameListRefreshComplete(); void onGameListRefreshProgress(const QString& status, int current, int total); void onGameListSelectionChanged(); @@ -186,8 +186,6 @@ private Q_SLOTS: void onVMResumed(); void onVMStopped(); - void checkMousePosition(); - void onGameChanged(const QString& title, const QString& elf_override, const QString& disc_path, const QString& serial, quint32 disc_crc, quint32 crc); @@ -205,7 +203,6 @@ protected: void dropEvent(QDropEvent* event) override; void moveEvent(QMoveEvent* event) override; void resizeEvent(QResizeEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; #ifdef _WIN32 bool nativeEvent(const QByteArray& eventType, void* message, qintptr* result) override; @@ -240,7 +237,7 @@ private: bool isRenderingToMain() const; bool shouldHideMouseCursor() const; bool shouldHideMainWindow() const; - bool shouldMouseGrab() const; + bool shouldMouseLock() const; void switchToGameListView(); void switchToEmulationView(); @@ -313,8 +310,6 @@ private: QString m_last_fps_status; - QTimer* m_mouse_check_timer = nullptr; - #ifdef _WIN32 void* m_device_notification_handle = nullptr; #endif diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp index 7e1b0ee966..f73ebb0e35 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp @@ -3,6 +3,7 @@ #include "InterfaceSettingsWidget.h" #include "AutoUpdaterDialog.h" +#include "Common.h" #include "MainWindow.h" #include "SettingWidgetBinder.h" #include "SettingsWindow.h" @@ -80,8 +81,13 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnControllerDisconnection, "UI", "PauseOnControllerDisconnection", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.discordPresence, "EmuCore", "EnableDiscordPresence", false); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mouseGrab, "EmuCore", "EnableMouseGrab", false); - + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mouseLock, "EmuCore", "EnableMouseLock", false); + connect(m_ui.mouseLock, &QCheckBox::checkStateChanged, [](Qt::CheckState state) { + if (state == Qt::Checked) + Common::AttachMousePositionCb([](int x, int y) { g_main_window->checkMousePosition(x, y); }); + else + Common::DetachMousePositionCb(); + }); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startFullscreen, "UI", "StartFullscreen", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.doubleClickTogglesFullscreen, "UI", "DoubleClickTogglesFullscreen", true); @@ -164,8 +170,13 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget m_ui.discordPresence, tr("Enable Discord Presence"), tr("Unchecked"), tr("Shows the game you are currently playing as part of your profile in Discord.")); dialog->registerWidgetHelp( +<<<<<<< Updated upstream m_ui.mouseGrab, tr("Enable Mouse Grab"), tr("Unchecked"), tr("Locks the mouse cursor to the windows when PCSX2 is in focus.")); +======= + m_ui.mouseLock, tr("Enable Mouse Lock"), tr("Unchecked"), + tr("Locks the mouse cursor to the windows when PCSX2 is in focus and all other windows are closed.
Unavailable on Linux Wayland.
Requires accessibility permissions on macOS.")); +>>>>>>> Stashed changes dialog->registerWidgetHelp( m_ui.doubleClickTogglesFullscreen, tr("Double-Click Toggles Fullscreen"), tr("Checked"), tr("Allows switching in and out of fullscreen mode by double-clicking the game window.")); diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui index 6fc709aa20..05874a1a92 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui @@ -51,9 +51,9 @@ - + - Enable Mouse Grab + Enable Mouse Lock