From ebd1da49139b6725dda8c5413a25a69334e7095a Mon Sep 17 00:00:00 2001 From: Ty Lamontagne Date: Thu, 16 Oct 2025 19:01:54 -0400 Subject: [PATCH 1/4] Qt: Make mouse screen locking DPI aware Also removed some global mouse hook stuff. Don't ever want to use that anyways. --- common/Windows/WinMisc.cpp | 44 ++------------------------ pcsx2-qt/MainWindow.cpp | 63 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 72 deletions(-) diff --git a/common/Windows/WinMisc.cpp b/common/Windows/WinMisc.cpp index b7bdb89d24c26..5b302cf52c7f2 100644 --- a/common/Windows/WinMisc.cpp +++ b/common/Windows/WinMisc.cpp @@ -100,54 +100,16 @@ 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 - */ + // We use raw input messages which are handled by the windows message loop. + // The alternative is to use a low-level mouse hook, but this passes Windows all mouse messages to PCSX2. + // If PCSX2 hangs, or you attach a debugger, the mouse will stop working system-wide. return true; } void Common::DetachMousePositionCb() { - /* - UnhookWindowsHookEx(mouseHook); - mouseHook = nullptr; - */ } bool Common::PlaySoundAsync(const char* path) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 89a9e24acbbf2..2d21e927b57ae 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -2322,19 +2322,24 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr if (msg->message == WM_INPUT) { - UINT dwSize = 40; - static BYTE lpb[40]; - if (GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER))) + UINT dwSize = 0; + GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER)); + + if (dwSize > 0) { - const RAWINPUT* raw = (RAWINPUT*)lpb; - if (raw->header.dwType == RIM_TYPEMOUSE) + std::vector lpb(dwSize); + if (GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, lpb.data(), &dwSize, sizeof(RAWINPUTHEADER)) == dwSize) { - const RAWMOUSE& mouse = raw->data.mouse; - if (mouse.usFlags == MOUSE_MOVE_ABSOLUTE || mouse.usFlags == MOUSE_MOVE_RELATIVE) + const RAWINPUT* raw = reinterpret_cast(lpb.data()); + if (raw->header.dwType == RIM_TYPEMOUSE) { - POINT cursorPos; - GetCursorPos(&cursorPos); - checkMousePosition(cursorPos.x, cursorPos.y); + const RAWMOUSE& mouse = raw->data.mouse; + if (mouse.usFlags == MOUSE_MOVE_ABSOLUTE || mouse.usFlags == MOUSE_MOVE_RELATIVE) + { + POINT cursorPos; + if (GetCursorPos(&cursorPos)) + checkMousePosition(cursorPos.x, cursorPos.y); + } } } } @@ -2638,30 +2643,26 @@ void MainWindow::checkMousePosition(int x, int y) if (!shouldMouseLock()) return; - const QPoint globalCursorPos = {x, y}; - QRect windowBounds = isRenderingFullscreen() ? screen()->geometry() : geometry(); - if (windowBounds.contains(globalCursorPos)) + // physical mouse position + const QPoint physicalPos(x, y); + + // logical (DIP) frame rect + QRectF logicalBounds = isRenderingFullscreen() ? screen()->geometry() : geometry(); + + // physical frame rect + const qreal scale = window()->devicePixelRatioF(); + QRectF physicalBounds( + logicalBounds.x() * scale, + logicalBounds.y() * scale, + logicalBounds.width() * scale, + logicalBounds.height() * scale); + + if (physicalBounds.contains(physicalPos)) return; Common::SetMousePosition( - std::clamp(globalCursorPos.x(), windowBounds.left(), windowBounds.right()), - std::clamp(globalCursorPos.y(), windowBounds.top(), windowBounds.bottom())); - - /* - 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 + std::clamp(physicalPos.x(), (int)physicalBounds.left(), (int)physicalBounds.right()), + std::clamp(physicalPos.y(), (int)physicalBounds.top(), (int)physicalBounds.bottom())); } void MainWindow::saveDisplayWindowGeometryToConfig() From 2a7f034cf762c145955f4a42964870195a610553 Mon Sep 17 00:00:00 2001 From: Ty Lamontagne Date: Sat, 18 Oct 2025 12:22:44 -0400 Subject: [PATCH 2/4] Qt: Visually disable the mouse lock button on wayland --- pcsx2-qt/Settings/InterfaceSettingsWidget.cpp | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp index abfa7e5cc336e..b94a94056bb2a 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp @@ -88,13 +88,27 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnControllerDisconnection, "UI", "PauseOnControllerDisconnection", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.discordPresence, "EmuCore", "EnableDiscordPresence", 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(); - }); +#ifdef __linux__ // Mouse locking is only supported on X11 + const bool mouse_lock_supported = QGuiApplication::platformName().toLower() == "xcb"; +#else + const bool mouse_lock_supported = true; +#endif + + if(mouse_lock_supported) + { + 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(); + }); + } + else + { + m_ui.mouseLock->setEnabled(false); + } + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startFullscreen, "UI", "StartFullscreen", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.doubleClickTogglesFullscreen, "UI", "DoubleClickTogglesFullscreen", true); @@ -251,4 +265,4 @@ void InterfaceSettingsWidget::onClearGameListBackgroundTriggered() Host::RemoveBaseSettingValue("UI", "GameListBackgroundPath"); Host::CommitBaseSettingChanges(); emit backgroundChanged(); -} \ No newline at end of file +} From a875fdd5d8eb3f9148a0f2b2d5ae146542fbf380 Mon Sep 17 00:00:00 2001 From: Ty Lamontagne Date: Sat, 18 Oct 2025 12:56:35 -0400 Subject: [PATCH 3/4] Qt: Implement mouse locking when rendering to separate window --- pcsx2-qt/MainWindow.cpp | 52 ++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 2d21e927b57ae..e4a9200d2c758 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -1151,11 +1151,19 @@ bool MainWindow::shouldMouseLock() const if (!Host::GetBoolSettingValue("EmuCore", "EnableMouseLock", false)) return false; + if(m_display_created == false || m_display_widget == nullptr && !isRenderingToMain()) + return false; + bool windowsHidden = (!g_debugger_window || g_debugger_window->isHidden()) && (!m_controller_settings_window || m_controller_settings_window->isHidden()) && (!m_settings_window || m_settings_window->isHidden()); - return windowsHidden && (isActiveWindow() || isRenderingFullscreen()); + auto* displayWindow = isRenderingToMain() ? window() : m_display_widget->window(); + + if(displayWindow == nullptr) + return false; + + return windowsHidden && (displayWindow->isActiveWindow() || displayWindow->isFullScreen()); } bool MainWindow::shouldAbortForMemcardBusy(const VMLock& lock) @@ -2640,29 +2648,35 @@ void MainWindow::setupMouseMoveHandler() void MainWindow::checkMousePosition(int x, int y) { - if (!shouldMouseLock()) - return; + // This function is called from a different thread on Linux/macOS + // kaboom can happen when the widget is destroyed after shouldMouseLock is called, so queue everything to the UI thread + QtHost::RunOnUIThread([this, x, y]() { + if (!shouldMouseLock()) + return; - // physical mouse position - const QPoint physicalPos(x, y); + // physical mouse position + const QPoint physicalPos(x, y); - // logical (DIP) frame rect - QRectF logicalBounds = isRenderingFullscreen() ? screen()->geometry() : geometry(); + const auto* displayWindow = getDisplayContainer()->window(); - // physical frame rect - const qreal scale = window()->devicePixelRatioF(); - QRectF physicalBounds( - logicalBounds.x() * scale, - logicalBounds.y() * scale, - logicalBounds.width() * scale, - logicalBounds.height() * scale); + // logical (DIP) frame rect + QRectF logicalBounds = displayWindow->geometry(); - if (physicalBounds.contains(physicalPos)) - return; + // physical frame rect + const qreal scale = displayWindow->devicePixelRatioF(); + QRectF physicalBounds( + logicalBounds.x() * scale, + logicalBounds.y() * scale, + logicalBounds.width() * scale, + logicalBounds.height() * scale); - Common::SetMousePosition( - std::clamp(physicalPos.x(), (int)physicalBounds.left(), (int)physicalBounds.right()), - std::clamp(physicalPos.y(), (int)physicalBounds.top(), (int)physicalBounds.bottom())); + if (physicalBounds.contains(physicalPos)) + return; + + Common::SetMousePosition( + std::clamp(physicalPos.x(), (int)physicalBounds.left(), (int)physicalBounds.right()), + std::clamp(physicalPos.y(), (int)physicalBounds.top(), (int)physicalBounds.bottom())); + }); } void MainWindow::saveDisplayWindowGeometryToConfig() From fe761a369ea76478dd7664807523f74a6ad0e6b0 Mon Sep 17 00:00:00 2001 From: Ty Lamontagne Date: Sat, 18 Oct 2025 13:29:13 -0400 Subject: [PATCH 4/4] Qt: Mouse Lock: Add warning about mixed-resolution non 100% DPI configs --- pcsx2-qt/Settings/InterfaceSettingsWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp index b94a94056bb2a..1d3c47a96a714 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp @@ -200,7 +200,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog tr("Shows the game you are currently playing as part of your profile in Discord.")); dialog()->registerWidgetHelp( 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.")); + 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.
Limited support for mixed-resolution with non-100% DPI configurations.")); 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."));