diff --git a/pcsx2-gsrunner/Main.cpp b/pcsx2-gsrunner/Main.cpp index 61464d8d9d..67e2350a15 100644 --- a/pcsx2-gsrunner/Main.cpp +++ b/pcsx2-gsrunner/Main.cpp @@ -257,7 +257,7 @@ void Host::OnInputDeviceDisconnected(const std::string_view& identifier) { } -void Host::SetRelativeMouseMode(bool enabled) +void Host::SetMouseMode(bool relative_mode, bool hide_cursor) { } diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 2e2b063837..7ec1b75a4f 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -414,7 +414,7 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread) connect(thread, &EmuThread::onAcquireRenderWindowRequested, this, &MainWindow::acquireRenderWindow, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onReleaseRenderWindowRequested, this, &MainWindow::releaseRenderWindow, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onResizeRenderWindowRequested, this, &MainWindow::displayResizeRequested); - connect(thread, &EmuThread::onRelativeMouseModeRequested, this, &MainWindow::relativeMouseModeRequested); + connect(thread, &EmuThread::onMouseModeRequested, this, &MainWindow::mouseModeRequested); connect(thread, &EmuThread::onVMStarting, this, &MainWindow::onVMStarting); connect(thread, &EmuThread::onVMStarted, this, &MainWindow::onVMStarted); connect(thread, &EmuThread::onVMPaused, this, &MainWindow::onVMPaused); @@ -887,7 +887,8 @@ bool MainWindow::isRenderingToMain() const bool MainWindow::shouldHideMouseCursor() const { - return (isRenderingFullscreen() && Host::GetBoolSettingValue("UI", "HideMouseCursor", false)) || m_relative_mouse_mode; + return ((isRenderingFullscreen() && Host::GetBoolSettingValue("UI", "HideMouseCursor", false)) || + m_relative_mouse_mode || m_hide_mouse_cursor); } bool MainWindow::shouldHideMainWindow() const @@ -2003,12 +2004,13 @@ void MainWindow::displayResizeRequested(qint32 width, qint32 height) QtUtils::ResizePotentiallyFixedSizeWindow(this, width, height + extra_height); } -void MainWindow::relativeMouseModeRequested(bool enabled) +void MainWindow::mouseModeRequested(bool relative_mode, bool hide_cursor) { - if (m_relative_mouse_mode == enabled) + if (m_relative_mouse_mode == relative_mode && m_hide_mouse_cursor == hide_cursor) return; - m_relative_mouse_mode = enabled; + m_relative_mouse_mode = relative_mode; + m_hide_mouse_cursor = hide_cursor; if (m_display_widget && !s_vm_paused) updateDisplayWidgetCursor(); } diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 9691e157b4..acf402adfc 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -124,7 +124,7 @@ private Q_SLOTS: std::optional acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless); void displayResizeRequested(qint32 width, qint32 height); - void relativeMouseModeRequested(bool enabled); + void mouseModeRequested(bool relative_mode, bool hide_cursor); void releaseRenderWindow(); void focusDisplayWidget(); @@ -293,6 +293,7 @@ private: bool m_display_created = false; bool m_relative_mouse_mode = false; + bool m_hide_mouse_cursor = false; bool m_was_paused_on_surface_loss = false; bool m_was_disc_change_request = false; bool m_is_closing = false; diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index c5fdda4d17..92ed1b81f2 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -1468,9 +1468,9 @@ void Host::OnInputDeviceDisconnected(const std::string_view& identifier) emit g_emu_thread->onInputDeviceDisconnected(identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size())); } -void Host::SetRelativeMouseMode(bool enabled) +void Host::SetMouseMode(bool relative_mode, bool hide_cursor) { - emit g_emu_thread->onRelativeMouseModeRequested(enabled); + emit g_emu_thread->onMouseModeRequested(relative_mode, hide_cursor); } ////////////////////////////////////////////////////////////////////////// diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index b2b8e69e34..2b2477fe51 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -119,7 +119,7 @@ Q_SIGNALS: std::optional onAcquireRenderWindowRequested(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless); void onResizeRenderWindowRequested(qint32 width, qint32 height); void onReleaseRenderWindowRequested(); - void onRelativeMouseModeRequested(bool enabled); + void onMouseModeRequested(bool relative_mode, bool hide_cursor); /// Called when the VM is starting initialization, but has not been completed yet. void onVMStarting(); diff --git a/pcsx2/ImGui/ImGuiManager.cpp b/pcsx2/ImGui/ImGuiManager.cpp index 0aa7a05684..92ff5f747b 100644 --- a/pcsx2/ImGui/ImGuiManager.cpp +++ b/pcsx2/ImGui/ImGuiManager.cpp @@ -38,6 +38,7 @@ #include "fmt/core.h" #include "imgui.h" #include "imgui_internal.h" +#include "common/Image.h" #include #include @@ -47,6 +48,17 @@ namespace ImGuiManager { + struct SoftwareCursor + { + std::string image_path; + std::unique_ptr texture; + u32 color; + float scale; + float extent_x; + float extent_y; + std::pair pos; + }; + static void SetStyle(); static void SetKeyMap(); static bool LoadFontData(); @@ -57,6 +69,11 @@ namespace ImGuiManager static bool AddIconFonts(float size); static void AcquirePendingOSDMessages(); static void DrawOSDMessages(); + static void CreateSoftwareCursorTextures(); + static void UpdateSoftwareCursorTexture(u32 index); + static void DestroySoftwareCursorTextures(); + static void DrawSoftwareCursor(const SoftwareCursor& sc, const std::pair& pos); + static void DrawSoftwareCursors(); } // namespace ImGuiManager static float s_global_scale = 1.0f; @@ -86,6 +103,8 @@ static std::unordered_map s_imgui_key_map; // need to keep track of this, so we can reinitialize on renderer switch static bool s_fullscreen_ui_was_initialized = false; +static std::array s_software_cursors = {}; + void ImGuiManager::SetFontPath(std::string path) { s_font_path = std::move(path); @@ -146,6 +165,7 @@ bool ImGuiManager::Initialize() if (add_fullscreen_fonts) InitializeFullscreenUI(); + CreateSoftwareCursorTextures(); return true; } @@ -157,6 +177,8 @@ bool ImGuiManager::InitializeFullscreenUI() void ImGuiManager::Shutdown(bool clear_state) { + DestroySoftwareCursorTextures(); + FullscreenUI::Shutdown(clear_state); ImGuiFullscreen::SetFonts(nullptr, nullptr, nullptr); if (clear_state) @@ -672,6 +694,7 @@ void ImGuiManager::RenderOSD() AcquirePendingOSDMessages(); DrawOSDMessages(); + DrawSoftwareCursors(); } float ImGuiManager::GetGlobalScale() @@ -810,3 +833,112 @@ bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value return true; } + +void ImGuiManager::CreateSoftwareCursorTextures() +{ + for (u32 i = 0; i < InputManager::MAX_POINTER_DEVICES; i++) + { + if (!s_software_cursors[i].image_path.empty()) + UpdateSoftwareCursorTexture(i); + } +} + +void ImGuiManager::DestroySoftwareCursorTextures() +{ + for (u32 i = 0; i < InputManager::MAX_POINTER_DEVICES; i++) + { + s_software_cursors[i].texture.reset(); + } +} + +void ImGuiManager::UpdateSoftwareCursorTexture(u32 index) +{ + SoftwareCursor& sc = s_software_cursors[index]; + if (sc.image_path.empty()) + { + sc.texture.reset(); + return; + } + + Common::RGBA8Image image; + if (!image.LoadFromFile(sc.image_path.c_str())) + { + Console.Error("Failed to load software cursor %u image '%s'", index, sc.image_path.c_str()); + return; + } + sc.texture = std::unique_ptr(g_gs_device->CreateTexture(image.GetWidth(), image.GetHeight(), 1, GSTexture::Format::Color)); + if (!sc.texture) + { + Console.Error( + "Failed to upload %ux%u software cursor %u image '%s'", image.GetWidth(), image.GetHeight(), index, sc.image_path.c_str()); + return; + } + sc.texture->Update(GSVector4i(0, 0, image.GetWidth(), image.GetHeight()), image.GetPixels(), image.GetByteStride(), 0); + + sc.extent_x = std::ceil(static_cast(image.GetWidth()) * sc.scale * s_global_scale) / 2.0f; + sc.extent_y = std::ceil(static_cast(image.GetHeight()) * sc.scale * s_global_scale) / 2.0f; +} + +void ImGuiManager::DrawSoftwareCursor(const SoftwareCursor& sc, const std::pair& pos) +{ + if (!sc.texture) + return; + + const ImVec2 min(pos.first - sc.extent_x, pos.second - sc.extent_y); + const ImVec2 max(pos.first + sc.extent_x, pos.second + sc.extent_y); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + + dl->AddImage( + reinterpret_cast(sc.texture.get()->GetNativeHandle()), min, max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), sc.color); +} + +void ImGuiManager::DrawSoftwareCursors() +{ + // This one's okay to race, worst that happens is we render the wrong number of cursors for a frame. + const u32 pointer_count = InputManager::MAX_POINTER_DEVICES; + for (u32 i = 0; i < pointer_count; i++) + DrawSoftwareCursor(s_software_cursors[i], InputManager::GetPointerAbsolutePosition(i)); + + for (u32 i = InputManager::MAX_POINTER_DEVICES; i < InputManager::MAX_SOFTWARE_CURSORS; i++) + DrawSoftwareCursor(s_software_cursors[i], s_software_cursors[i].pos); +} + +void ImGuiManager::SetSoftwareCursor(u32 index, std::string image_path, float image_scale, u32 multiply_color) +{ + MTGS::RunOnGSThread([index, image_path = std::move(image_path), image_scale, multiply_color]() { + pxAssert(index < std::size(s_software_cursors)); + SoftwareCursor& sc = s_software_cursors[index]; + sc.color = multiply_color | 0xFF000000; + if (sc.image_path == image_path && sc.scale == image_scale) + return; + + const bool is_hiding_or_showing = (image_path.empty() != sc.image_path.empty()); + sc.image_path = std::move(image_path); + sc.scale = image_scale; + if (MTGS::IsOpen()) + UpdateSoftwareCursorTexture(index); + + // Hide the system cursor when we activate a software cursor. + if (is_hiding_or_showing && index == 0) + Host::RunOnCPUThread(&InputManager::UpdateHostMouseMode); + }); +} + +bool ImGuiManager::HasSoftwareCursor(u32 index) +{ + return (index < s_software_cursors.size() && !s_software_cursors[index].image_path.empty()); +} + +void ImGuiManager::ClearSoftwareCursor(u32 index) +{ + SetSoftwareCursor(index, std::string(), 0.0f, 0); +} + +void ImGuiManager::SetSoftwareCursorPosition(u32 index, float pos_x, float pos_y) +{ + pxAssert(index >= InputManager::MAX_POINTER_DEVICES); + SoftwareCursor& sc = s_software_cursors[index]; + sc.pos.first = pos_x; + sc.pos.second = pos_y; +} diff --git a/pcsx2/ImGui/ImGuiManager.h b/pcsx2/ImGui/ImGuiManager.h index 6fe120d7be..da7c3a26e8 100644 --- a/pcsx2/ImGui/ImGuiManager.h +++ b/pcsx2/ImGui/ImGuiManager.h @@ -102,6 +102,14 @@ namespace ImGuiManager /// Called on the CPU thread when any input event fires. Allows imgui to take over controller navigation. bool ProcessGenericInputEvent(GenericInputBinding key, float value); + + /// Sets an image and scale for a software cursor. Software cursors can be used for things like crosshairs. + void SetSoftwareCursor(u32 index, std::string image_path, float image_scale, u32 multiply_color = 0xFFFFFF); + bool HasSoftwareCursor(u32 index); + void ClearSoftwareCursor(u32 index); + + /// Sets the position of a software cursor, used when we have relative coordinates such as controllers. + void SetSoftwareCursorPosition(u32 index, float pos_x, float pos_y); } // namespace ImGuiManager namespace Host diff --git a/pcsx2/Input/InputManager.cpp b/pcsx2/Input/InputManager.cpp index ac09cf1cce..74e9ba072c 100644 --- a/pcsx2/Input/InputManager.cpp +++ b/pcsx2/Input/InputManager.cpp @@ -1309,6 +1309,11 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind for (u32 port = 0; port < USB::NUM_PORTS; port++) AddUSBBindings(binding_si, port); + UpdateHostMouseMode(); +} + +void InputManager::UpdateHostMouseMode() +{ // Check for relative mode bindings, and enable if there's anything using it. bool has_relative_mode_bindings = !s_pointer_move_callbacks.empty(); if (!has_relative_mode_bindings) @@ -1324,7 +1329,9 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind } } } - Host::SetRelativeMouseMode(has_relative_mode_bindings); + + const bool has_software_cursor = ImGuiManager::HasSoftwareCursor(0); + Host::SetMouseMode(has_relative_mode_bindings, has_relative_mode_bindings || has_software_cursor); } // ------------------------------------------------------------------------ diff --git a/pcsx2/Input/InputManager.h b/pcsx2/Input/InputManager.h index 894af7e650..642383d844 100644 --- a/pcsx2/Input/InputManager.h +++ b/pcsx2/Input/InputManager.h @@ -170,6 +170,10 @@ namespace InputManager static constexpr u32 MAX_POINTER_DEVICES = 1; static constexpr u32 MAX_POINTER_BUTTONS = 3; + /// Maximum number of software cursors. We allocate an extra two for USB devices with + /// positioning data from the controller instead of a mouse. + static constexpr u32 MAX_SOFTWARE_CURSORS = MAX_POINTER_BUTTONS + 2; + /// Returns a pointer to the external input source class, if present. InputSource* GetInputSourceInterface(InputSourceType type); @@ -287,6 +291,9 @@ namespace InputManager /// Updates relative pointer position. Can call from the UI thread, use when host supports relative coordinate reporting. void UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis, float d, bool raw_input = false); + /// Updates host mouse mode (relative/cursor hiding). + void UpdateHostMouseMode(); + /// Called when a new input device is connected. void OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name); @@ -305,6 +312,6 @@ namespace Host /// Called when an input device is disconnected. void OnInputDeviceDisconnected(const std::string_view& identifier); - /// Enables relative mouse mode in the host. - void SetRelativeMouseMode(bool enabled); + /// Enables relative mouse mode in the host, and/or hides the cursor. + void SetMouseMode(bool relative_mode, bool hide_cursor); } // namespace Host diff --git a/tests/ctest/core/StubHost.cpp b/tests/ctest/core/StubHost.cpp index 10b32da7cf..e8434e215e 100644 --- a/tests/ctest/core/StubHost.cpp +++ b/tests/ctest/core/StubHost.cpp @@ -95,7 +95,7 @@ void Host::OnInputDeviceDisconnected(const std::string_view& identifier) { } -void Host::SetRelativeMouseMode(bool enabled) +void Host::SetMouseMode(bool relative_mode, bool hide_cursor) { }