diff --git a/core/libs/prx/libSceMouse/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt index d98fdf1a5..b7f58761d 100644 --- a/core/libs/prx/libSceMouse/CMakeLists.txt +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -12,7 +12,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/MouseInput.cpp ) target_include_directories(mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) - target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc) + target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc SDL2-static) configure_windows_unwind(mouse_tests) add_test(NAME mouse_input COMMAND mouse_tests) if(WIN32) diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 157e3afcc..f41c9eaf4 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -27,4 +27,9 @@ int APS5_VABI sceMouseResetAbsolutePosition() { NotImplemented_nid_no_patch(__func__); return 0; } + +bool MouseIsOpen_nid_postfix() { + return Mouse::IsOpen(); +} + } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index bf7d57a09..15c89831b 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -16,6 +16,7 @@ struct MouseInputEvent { }; namespace Mouse { +bool IsOpen(); int Initialize(); int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); int Close(std::int32_t handle); @@ -24,5 +25,6 @@ void Publish(const MouseInputEvent& event); } extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); +extern "C" bool MouseIsOpen_nid_postfix(); #endif diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index 5bcf78e72..c139fff6a 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -27,6 +27,11 @@ void enqueue(const MouseData& data) { namespace Mouse { +bool IsOpen() { + std::lock_guard lock(mouseMutex); + return opened; +} + int Initialize() { std::lock_guard lock(mouseMutex); initialized = true; diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 042deeed6..6b9a38c83 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -27,6 +27,20 @@ configure_windows_unwind(libSceVideoOut) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() +add_test_executable(video_out_mouse_tests EXCLUDE_FROM_ALL tests/MouseInput.cpp) +target_include_directories(video_out_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) +target_link_libraries(video_out_mouse_tests PRIVATE libSceVideoOut libSceMouse libc SDL2-static) +set_target_properties(video_out_mouse_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(video_out_mouse_tests) +if(BUILD_TESTING) + set_target_properties(video_out_mouse_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME video_out_mouse COMMAND video_out_mouse_tests) + if(WIN32) + set_tests_properties(video_out_mouse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(video_out_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp) target_include_directories(video_out_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(video_out_flip_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) diff --git a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp index 56ea31b6d..be37a2c2c 100644 --- a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp @@ -2,13 +2,24 @@ #define CORE_LIBS_PRX_LIBSCEVIDEOOUT_MOUSEINPUT_HPP #include "SDL_events.h" +#include class MouseInput { public: void HandleEvent(const SDL_Event& event, unsigned windowId); + void SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight); + bool CaptureWanted(bool allowed) const; + void UpdateCapture(bool allowed); private: + std::int32_t scaled(std::int32_t delta, double scale, double& remainder); + bool focused = true; + bool captured = false; + double scaleX = 1.0; + double scaleY = 1.0; + double remainderX = 0.0; + double remainderY = 0.0; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index ce5188877..b68b5e5cd 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -17,6 +17,7 @@ public: ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(); + bool MouseModeEnabled() const { return mouseEnabled; } private: explicit PadInput(Pad::InputConfiguration configuration); diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 3a5443e6e..c00aef3ff 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -2,14 +2,17 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "SDL_mouse.h" +#include "SDL_video.h" +#include +#include void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { MouseInputEvent input{}; switch (event.type) { case SDL_MOUSEMOTION: if (event.motion.windowID != windowId || !focused) return; - input.x = event.motion.xrel; - input.y = event.motion.yrel; + input.x = scaled(event.motion.xrel, scaleX, remainderX); + input.y = scaled(event.motion.yrel, scaleY, remainderY); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: @@ -53,3 +56,46 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { } MousePublishInput_nid_postfix(input); } + +void MouseInput::SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight) { + if (sourceWidth == 0 || sourceHeight == 0 || presentedWidth == 0 || presentedHeight == 0) return; + const double x = static_cast(sourceWidth) / static_cast(presentedWidth); + const double y = static_cast(sourceHeight) / static_cast(presentedHeight); + if (x != scaleX || y != scaleY) { + std::fprintf(stderr, "[mouse] video driver %s: guest %ux%u presented at %ux%u, delta scale %.4f x %.4f\n", SDL_GetCurrentVideoDriver() ? SDL_GetCurrentVideoDriver() : "?", sourceWidth, sourceHeight, presentedWidth, presentedHeight, x, y); + scaleX = x; + scaleY = y; + remainderX = 0.0; + remainderY = 0.0; + } +} + +std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remainder) { + const double exact = static_cast(delta) * scale + remainder; + const double whole = std::trunc(exact); + remainder = exact - whole; + return static_cast(whole); +} + +bool MouseInput::CaptureWanted(bool allowed) const { + return allowed && focused && MouseIsOpen_nid_postfix(); +} + +void MouseInput::UpdateCapture(bool allowed) { + const bool wanted = CaptureWanted(allowed); + if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; + if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { + static bool warned = false; + if (!warned) { + warned = true; + std::fprintf(stderr, "[mouse] relative mouse mode unavailable: %s\n", SDL_GetError()); + } + return; + } + int ignoredX = 0; + int ignoredY = 0; + SDL_GetRelativeMouseState(&ignoredX, &ignoredY); + remainderX = 0.0; + remainderY = 0.0; + captured = wanted; +} diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index f605f9dbb..5b3dcdc23 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -12,6 +12,7 @@ #include "SDL_vulkan.h" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceVideoOut/include/KeyboardInput.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -619,11 +620,19 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } padInput.Update(); updateDisplayRefreshRate(); + mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); current->timing->Add(current->timing->Get("VideoOut", "queue"), dequeued - current->queuedAt); processFlip(*current); + int windowWidth = 0; + int windowHeight = 0; + if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); + if (windowWidth > 0 && windowHeight > 0) { + const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); + mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); + } const auto finished = AgcDriver::FrameTiming::Clock::now(); AgcDriver::FrameTiming::Clock::duration interval{}; { diff --git a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp new file mode 100644 index 000000000..5d740a05e --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SDL.h" +#include "SDL_events.h" +#include "SDL_mouse.h" +#include +#include +#include +#undef main + +extern "C" { +int APS5_VABI sceMouseInit(); +int APS5_VABI sceMouseOpen(int userId, std::int32_t type, std::int32_t index, const void* param); +int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t num); +int APS5_VABI sceMouseClose(std::int32_t handle); +} + +namespace { + +void Require(bool condition, const char* reason) { + if (!condition) { + std::fprintf(stderr, "mouse input: %s\n", reason); + std::abort(); + } +} + +SDL_Event Motion(unsigned windowId, int xrel, int yrel) { + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.windowID = windowId; + event.motion.xrel = xrel; + event.motion.yrel = yrel; + return event; +} + +SDL_Event Window(unsigned windowId, std::uint8_t kind) { + SDL_Event event{}; + event.type = SDL_WINDOWEVENT; + event.window.windowID = windowId; + event.window.event = kind; + return event; +} +void CaptureDecision(MouseInput& input, std::int32_t handle) { + Require(input.CaptureWanted(true), "a focused window with an open mouse does not want the capture"); + Require(!input.CaptureWanted(false), "the pad's mouse-to-stick toggle does not release the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_LOST), 3); + Require(!input.CaptureWanted(true), "losing focus does not release the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_GAINED), 3); + Require(input.CaptureWanted(true), "regaining focus does not restore the capture"); + input.HandleEvent(Window(4, SDL_WINDOWEVENT_FOCUS_LOST), 3); + Require(input.CaptureWanted(true), "another window's focus loss released the capture"); + Require(sceMouseClose(handle) == 0, "close failed"); + Require(!input.CaptureWanted(true), "a closed mouse keeps the capture"); + Require(sceMouseOpen(255, 0, 0, nullptr) >= 0, "reopen failed"); + Require(input.CaptureWanted(true), "a reopened mouse does not want the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_CLOSE), 3); + Require(!input.CaptureWanted(true), "a closed window keeps the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_GAINED), 3); +} +void CaptureApplied(MouseInput& input) { + SDL_SetHint("SDL_VIDEODRIVER", "dummy"); + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + std::fprintf(stderr, "mouse input: SDL video unavailable (%s): capture application not checked\n", SDL_GetError()); + return; + } + input.UpdateCapture(true); + const bool applied = SDL_GetRelativeMouseMode() == SDL_TRUE; + input.UpdateCapture(false); + Require(SDL_GetRelativeMouseMode() == SDL_FALSE, "the capture was not released"); + std::printf("mouse input: SDL %s relative mode under the %s driver\n", applied ? "applied" : "refused", SDL_GetCurrentVideoDriver()); + SDL_Quit(); +} +void Sum(std::int32_t handle, std::int32_t& x, std::int32_t& y) { + MouseData data[MOUSE_MAX_DATA_NUM]{}; + const int count = sceMouseRead(handle, data, MOUSE_MAX_DATA_NUM); + Require(count >= 0, "read failed"); + x = 0; + y = 0; + for (int index = 0; index < count; ++index) { + x += data[index].x_axis; + y += data[index].y_axis; + } +} + +} + +int main() { + Require(sceMouseInit() == 0, "init failed"); + const std::int32_t handle = sceMouseOpen(255, 0, 0, nullptr); + Require(handle >= 0, "open failed"); + MouseInput input; + std::int32_t x = 0; + std::int32_t y = 0; + + input.HandleEvent(Motion(3, 10, -7), 3); + input.HandleEvent(Motion(4, 100, 100), 3); + Sum(handle, x, y); + Require(x == 10 && y == -7, "unscaled deltas are not passed through or another window's motion was not ignored"); + + input.SetPresentation(1920, 1080, 1843, 1037); + for (int step = 0; step < 5; ++step) input.HandleEvent(Motion(3, 10, -7), 3); + Sum(handle, x, y); + Require(x == 52 && y == -36, "deltas are not scaled by the presented image with sub-pixel carry"); + + input.SetPresentation(1920, 1080, 960, 540); + input.HandleEvent(Motion(3, 1, 1), 3); + input.HandleEvent(Motion(3, 1, 1), 3); + Sum(handle, x, y); + Require(x == 4 && y == 4, "a half-size presentation does not double the deltas"); + + input.SetPresentation(0, 0, 0, 0); + input.HandleEvent(Motion(3, 3, 3), 3); + Sum(handle, x, y); + Require(x == 6 && y == 6, "an empty presentation changed the scale"); + CaptureDecision(input, handle); + CaptureApplied(input); + std::puts("mouse input tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 2744eb258..a20f0876a 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -22,3 +22,4 @@ ### Functional - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses +- [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss or while the pad's mouse-to-stick toggle is on). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 83876a67d..950c7b0b4 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -12,6 +12,8 @@ Supported input sources are: - `MOUSE:Left`, `MOUSE:Middle`, `MOUSE:Right`, `MOUSE:X1`, or `MOUSE:X2`. - `WHEEL:Up` or `WHEEL:Down` for pad buttons. +While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS5 hides and captures the host cursor, so the title's own cursor is the only one; the capture ends when the window loses focus or when the mouse-to-stick toggle (`ToggleMouse`) is on. Mouse movement reaches the title scaled from the window's presented image to the title's display resolution, so the title's cursor moves as far as the host cursor would. + Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`.