diff --git a/core/libs/prx/libSceMouse/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt index cf563cd6a7..9d75d16ebd 100644 --- a/core/libs/prx/libSceMouse/CMakeLists.txt +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -7,6 +7,18 @@ target_include_directories(libSceMouse PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(libSceMouse PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceMouse) +if(BUILD_TESTING) + add_test_executable(mouse_absolute_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/MouseAbsolute.cpp) + target_include_directories(mouse_absolute_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(mouse_absolute_tests PRIVATE libSceMouse libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(mouse_absolute_tests) + add_test(NAME mouse_absolute_position COMMAND mouse_absolute_tests) + if(WIN32) + set_tests_properties(mouse_absolute_position PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(mouse_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Mouse.cpp diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index e7a4202cef..7fc6bae2e4 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -82,19 +82,16 @@ int APS5_VABI sceMouseSetProcessPrivilege() { return 0; } -int APS5_VABI sceMouseReadAbsolutePosition() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num) { + return Mouse::ReadAbsolutePosition(handle, data, num); } -int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode) { + return Mouse::SetAbsoluteHighPrecisionMode(handle, mode); } -int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size) { + return Mouse::SetAbsoluteVirtualScreenSize(handle, size); } } diff --git a/core/libs/prx/libSceMouse/include/Api.hpp b/core/libs/prx/libSceMouse/include/Api.hpp index 1e30e9a3fc..6ed1540333 100644 --- a/core/libs/prx/libSceMouse/include/Api.hpp +++ b/core/libs/prx/libSceMouse/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" extern "C" { @@ -19,8 +20,8 @@ int APS5_VABI sceMouseMbusInit(); int APS5_VABI sceMouseSetHandType(); int APS5_VABI sceMouseSetPointerSpeed(); int APS5_VABI sceMouseSetProcessPrivilege(); -int APS5_VABI sceMouseReadAbsolutePosition(); -int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode(); -int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize(); +int APS5_VABI sceMouseReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num); +int APS5_VABI sceMouseSetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode); +int APS5_VABI sceMouseSetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size); } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 390cf33968..b6a161f68d 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -6,6 +6,8 @@ struct MouseInputEvent { std::int32_t x = 0; std::int32_t y = 0; + std::int32_t countsX = 0; + std::int32_t countsY = 0; std::int32_t wheel = 0; std::int32_t tilt = 0; std::uint32_t button = 0; @@ -22,6 +24,9 @@ int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam int Close(std::int32_t handle); int Read(std::int32_t handle, MouseData* data, std::int32_t num); int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y); +int SetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size); +int SetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode); +int ReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num); void Publish(const MouseInputEvent& event); } diff --git a/core/libs/prx/libSceMouse/include/mouse_structs.h b/core/libs/prx/libSceMouse/include/mouse_structs.h index 30c7d3a8f9..a1da9bb48d 100644 --- a/core/libs/prx/libSceMouse/include/mouse_structs.h +++ b/core/libs/prx/libSceMouse/include/mouse_structs.h @@ -10,6 +10,18 @@ struct MouseOpenParam { std::uint8_t reserved[7]; }; +struct MouseAbsoluteData { + std::uint64_t timestamp; + std::uint8_t connected; + std::uint8_t padding[3]; + std::uint32_t buttons; + std::uint16_t x; + std::uint16_t y; + std::int32_t wheel; + std::int32_t tilt; + std::uint8_t reserved[12]; +}; + constexpr int MOUSE_OK = 0; constexpr int MOUSE_ERROR_INVALID_ARG = static_cast(0x80df0001u); constexpr int MOUSE_ERROR_INVALID_HANDLE = static_cast(0x80df0003u); @@ -20,6 +32,13 @@ constexpr int MOUSE_HANDLE = 1; constexpr std::uint8_t MOUSE_OPEN_PARAM_MERGED = 1; static_assert(sizeof(MouseOpenParam) == 8); +static_assert(sizeof(MouseAbsoluteData) == 40); +static_assert(offsetof(MouseAbsoluteData, connected) == 8); +static_assert(offsetof(MouseAbsoluteData, buttons) == 12); +static_assert(offsetof(MouseAbsoluteData, x) == 16); +static_assert(offsetof(MouseAbsoluteData, y) == 18); +static_assert(offsetof(MouseAbsoluteData, wheel) == 20); +static_assert(offsetof(MouseAbsoluteData, tilt) == 24); static_assert(sizeof(MouseData) == 40); static_assert(alignof(MouseData) == 8); static_assert(offsetof(MouseData, timestamp) == 0); diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index b4df78375a..ac1b19e9d8 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include namespace { @@ -14,6 +16,74 @@ bool initialized = false; bool opened = false; bool connected = true; std::uint32_t buttons = 0; +constexpr std::array SCREEN_WIDTHS{1280, 1920, 2560, 3840}; +constexpr std::array SCREEN_HEIGHTS{720, 1080, 1440, 2160}; +constexpr std::array CURVE_SPEEDS{0.0f, 0.4300079345703125f, 1.25f, 3.8600006103515625f, 40.0f}; +constexpr std::array CURVE_MOVES{0.0f, 1.0702667236328125f, 4.140625f, 18.984375f, 443.75f}; +struct AbsoluteSample { + std::uint64_t timestamp; + std::uint32_t buttons; + std::int32_t x; + std::int32_t y; + std::int32_t wheel; + std::int32_t tilt; + bool connected; +}; + +std::array absoluteHistory{}; +int absoluteHead = 0; +int absoluteCount = 0; +std::uint32_t screenSize = 1; +std::int64_t absoluteX = 0; +std::int64_t absoluteY = 0; +std::uint8_t highPrecision = 0; +float fractionX = 0.0f; +float fractionY = 0.0f; + +float curve(float speed) { + std::size_t segment = 1; + while (segment + 1 < CURVE_SPEEDS.size() && speed > CURVE_SPEEDS[segment]) ++segment; + const float lowSpeed = CURVE_SPEEDS[segment - 1]; + const float lowMove = CURVE_MOVES[segment - 1]; + return lowMove + (speed - lowSpeed) * (CURVE_MOVES[segment] - lowMove) / (CURVE_SPEEDS[segment] - lowSpeed); +} + +std::int64_t step(float& fraction, float delta, float scale) { + const float total = delta * scale + fraction; + const float whole = std::trunc(total); + fraction = total - whole; + return static_cast(whole); +} + +void moveAbsolute(std::int32_t dx, std::int32_t dy) { + std::int64_t moveX = dx; + std::int64_t moveY = dy; + if (highPrecision != 0) { + moveX = 0; + moveY = 0; + const float length = std::sqrt(static_cast(dx) * static_cast(dx) + static_cast(dy) * static_cast(dy)); + if (length != 0.0f) { + const float scale = curve(length * 120.0f / 420.0f) * 96.0f / 120.0f / length; + moveX = step(fractionX, static_cast(dx), scale); + moveY = step(fractionY, static_cast(dy), scale); + } + } + absoluteX = std::clamp(absoluteX + moveX, 0, SCREEN_WIDTHS[screenSize] - 1); + absoluteY = std::clamp(absoluteY + moveY, 0, SCREEN_HEIGHTS[screenSize] - 1); +} + +void enqueueAbsolute(const AbsoluteSample& sample) { + if (absoluteCount == MOUSE_MAX_DATA_NUM) { + absoluteHead = (absoluteHead + 1) % MOUSE_MAX_DATA_NUM; + --absoluteCount; + } + absoluteHistory[(absoluteHead + absoluteCount) % MOUSE_MAX_DATA_NUM] = sample; + ++absoluteCount; +} + +bool sharesRecord(const AbsoluteSample& first, const AbsoluteSample& second) { + return first.connected && second.connected && first.buttons == second.buttons; +} void enqueue(const MouseData& data) { if (count == MOUSE_MAX_DATA_NUM) { @@ -23,6 +93,24 @@ void enqueue(const MouseData& data) { history[(head + count) % MOUSE_MAX_DATA_NUM] = data; ++count; } + +bool motionOnly(const MouseData& data) { + return data.wheel == 0 && data.tilt == 0 && (data.x_axis != 0 || data.y_axis != 0); +} + +std::int32_t saturatedSum(std::int32_t a, std::int32_t b) { + return static_cast(std::clamp(static_cast(a) + b, INT32_MIN, INT32_MAX)); +} + +bool mergeMotion(const MouseData& data) { + if (count == 0 || !motionOnly(data)) return false; + MouseData& tail = history[(head + count - 1) % MOUSE_MAX_DATA_NUM]; + if (!motionOnly(tail) || tail.buttons != data.buttons || tail.connected != data.connected) return false; + tail.timestamp = data.timestamp; + tail.x_axis = saturatedSum(tail.x_axis, data.x_axis); + tail.y_axis = saturatedSum(tail.y_axis, data.y_axis); + return true; +} } namespace Mouse { @@ -50,10 +138,19 @@ int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam buttons = 0; head = 0; count = 0; + screenSize = 1; + absoluteX = 0; + absoluteY = 0; + highPrecision = 0; + fractionX = 0.0f; + fractionY = 0.0f; MouseData initial{}; initial.timestamp = sceKernelGetProcessTime(); initial.connected = connected; enqueue(initial); + absoluteHead = 0; + absoluteCount = 0; + enqueueAbsolute(AbsoluteSample{initial.timestamp, 0, 0, 0, 0, 0, connected}); return MOUSE_HANDLE; } @@ -65,6 +162,8 @@ int Close(std::int32_t handle) { buttons = 0; head = 0; count = 0; + absoluteHead = 0; + absoluteCount = 0; return MOUSE_OK; } @@ -82,11 +181,87 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } +int SetAbsoluteVirtualScreenSize(std::int32_t handle, std::int32_t size) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + const auto next = static_cast(size); + if (next >= SCREEN_WIDTHS.size()) return MOUSE_ERROR_INVALID_ARG; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + absoluteX = absoluteX * SCREEN_WIDTHS[next] / SCREEN_WIDTHS[screenSize]; + absoluteY = absoluteY * SCREEN_HEIGHTS[next] / SCREEN_HEIGHTS[screenSize]; + screenSize = next; + return MOUSE_OK; +} + +int SetAbsoluteHighPrecisionMode(std::int32_t handle, std::int32_t mode) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + highPrecision = static_cast(mode); + return MOUSE_OK; +} + +int ReadAbsolutePosition(std::int32_t handle, MouseAbsoluteData* data, std::int32_t num) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (data == nullptr || num <= 0 || num > MOUSE_MAX_DATA_NUM) return MOUSE_ERROR_INVALID_ARG; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + if (!connected) { + data[0] = MouseAbsoluteData{}; + return 1; + } + std::array samples{}; + std::array endsRecord{}; + const int pending = absoluteCount; + for (int i = 0; i < pending; ++i) { + samples[i] = absoluteHistory[(absoluteHead + i) % MOUSE_MAX_DATA_NUM]; + endsRecord[i] = true; + } + absoluteHead = 0; + absoluteCount = 0; + int records = pending; + for (int i = 0; i + 1 < pending && records > num; ++i) { + if (!sharesRecord(samples[i], samples[i + 1])) continue; + endsRecord[i] = false; + --records; + } + int first = pending; + for (int kept = 0; first > 0 && kept < num;) { + --first; + if (endsRecord[first]) ++kept; + while (first > 0 && !endsRecord[first - 1]) --first; + } + int written = 0; + std::int32_t wheel = 0; + std::int32_t tilt = 0; + for (int i = first; i < pending; ++i) { + const AbsoluteSample& sample = samples[i]; + if (!sample.connected) continue; + moveAbsolute(sample.x, sample.y); + wheel += sample.wheel; + tilt += sample.tilt; + if (!endsRecord[i]) continue; + MouseAbsoluteData& record = data[written++]; + record.timestamp = sample.timestamp; + record.connected = 1; + record.buttons = sample.buttons; + record.x = static_cast(absoluteX); + record.y = static_cast(absoluteY); + record.wheel = wheel; + record.tilt = tilt; + wheel = 0; + tilt = 0; + } + return written; +} + int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y) { std::lock_guard lock(mouseMutex); if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; - if (x >= 1920 || y >= 1080) return MOUSE_ERROR_INVALID_ARG; + if (x >= SCREEN_WIDTHS[screenSize] || y >= SCREEN_HEIGHTS[screenSize]) return MOUSE_ERROR_INVALID_ARG; + absoluteX = x; + absoluteY = y; return MOUSE_OK; } @@ -100,19 +275,26 @@ void Publish(const MouseInputEvent& event) { return; } if (!opened) return; + const bool countsGiven = event.countsX != 0 || event.countsY != 0; + const bool noReport = event.button == 0 && !event.resetButtons && !event.connectionChange && event.wheel == 0 && event.tilt == 0; + if (noReport && event.x == 0 && event.y == 0 && !countsGiven) return; if (event.resetButtons) buttons = 0; if (event.button != 0) { if (event.pressed) buttons |= event.button; else buttons &= ~event.button; } + const std::uint64_t timestamp = sceKernelGetProcessTime(); + enqueueAbsolute(AbsoluteSample{timestamp, buttons, countsGiven ? event.countsX : event.x, countsGiven ? event.countsY : event.y, event.wheel, event.tilt, connected}); + if (noReport && event.x == 0 && event.y == 0) return; MouseData data{}; - data.timestamp = sceKernelGetProcessTime(); + data.timestamp = timestamp; data.connected = connected; data.buttons = buttons; data.x_axis = event.x; data.y_axis = event.y; data.wheel = event.wheel; data.tilt = event.tilt; + if (event.button == 0 && !event.resetButtons && mergeMotion(data)) return; enqueue(data); } diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index 27c99cc622..16eb9d3477 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -5,7 +5,9 @@ #include "SDL_events.h" #include "SDL_mouse.h" +#include #include +#include extern "C" { bool MouseIsOpen_nid_no_patch(); @@ -14,6 +16,18 @@ bool MouseIsOpen_nid_no_patch(); static void Require(bool value) { if (!value) std::abort(); } +struct AbsoluteRecord { + std::uint64_t timestamp; + std::uint8_t connected; + std::uint8_t padding[3]; + std::uint32_t buttons; + std::uint16_t x; + std::uint16_t y; + std::int32_t wheel; + std::int32_t tilt; + std::uint8_t reserved[12]; +}; + int main() { MouseData data[64]{}; Require(!MouseIsOpen_nid_no_patch()); @@ -97,13 +111,87 @@ int main() { input.HandleEvent(event, 7); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].connected); - for (int i = 0; i < 70; ++i) { + for (int i = 0; i < 300; ++i) { MouseInputEvent motion{}; - motion.x = i; + motion.x = -1; + motion.y = i % 2; MousePublishInput_nid_no_patch(motion); } + Require(sceMouseRead(MOUSE_HANDLE, data, 8) == 1); + Require(data[0].x_axis == -300 && data[0].y_axis == 150 && data[0].buttons == 0); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + + MouseInputEvent still{}; + MousePublishInput_nid_no_patch(still); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + for (int i = 0; i < 300; ++i) { + MouseInputEvent alternating{}; + alternating.x = i % 2; + MousePublishInput_nid_no_patch(alternating); + } + Require(sceMouseRead(MOUSE_HANDLE, data, 8) == 1); + Require(data[0].x_axis == 150 && data[0].y_axis == 0); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); + + MouseInputEvent zero{}; + MouseInputEvent two{}; + two.x = 2; + MouseInputEvent three{}; + three.x = 3; + MouseInputEvent down{}; + down.button = 1; + down.pressed = true; + MouseInputEvent up{}; + up.button = 1; + for (const MouseInputEvent& report : {two, zero, down, zero, three, zero, up, zero}) MousePublishInput_nid_no_patch(report); + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 4); + Require(data[0].x_axis == 2 && data[0].buttons == 0); + Require(data[1].x_axis == 0 && data[1].buttons == 1); + Require(data[2].x_axis == 3 && data[2].buttons == 1); + Require(data[3].x_axis == 0 && data[3].buttons == 0); + + MouseInputEvent step{}; + step.x = 3; + MousePublishInput_nid_no_patch(step); + MousePublishInput_nid_no_patch(step); + MouseInputEvent press{}; + press.button = 1; + press.pressed = true; + MousePublishInput_nid_no_patch(press); + MousePublishInput_nid_no_patch(step); + MousePublishInput_nid_no_patch(step); + MouseInputEvent wheel{}; + wheel.wheel = 1; + MousePublishInput_nid_no_patch(wheel); + MousePublishInput_nid_no_patch(step); + MouseInputEvent release{}; + release.button = 1; + MousePublishInput_nid_no_patch(release); + MousePublishInput_nid_no_patch(step); + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 7); + Require(data[0].x_axis == 6 && data[0].buttons == 0); + Require(data[1].x_axis == 0 && data[1].buttons == 1); + Require(data[2].x_axis == 6 && data[2].buttons == 1); + Require(data[3].wheel == 1 && data[3].x_axis == 0 && data[3].buttons == 1); + Require(data[4].x_axis == 3 && data[4].wheel == 0 && data[4].buttons == 1); + Require(data[5].x_axis == 0 && data[5].buttons == 0); + Require(data[6].x_axis == 3 && data[6].buttons == 0); + Require(data[2].timestamp >= data[1].timestamp && data[6].timestamp >= data[5].timestamp); + + MouseInputEvent large{}; + large.x = INT32_MAX; + MousePublishInput_nid_no_patch(large); + MousePublishInput_nid_no_patch(large); + Require(sceMouseRead(MOUSE_HANDLE, data, 8) == 1 && data[0].x_axis == INT32_MAX); + + for (int i = 0; i < 70; ++i) { + MouseInputEvent click{}; + click.button = 1; + click.pressed = (i % 2) == 0; + MousePublishInput_nid_no_patch(click); + } Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); - Require(data[0].x_axis == 6 && data[63].x_axis == 69); + Require(data[0].buttons == 1 && data[63].buttons == 0 && data[62].buttons == 1); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(!MouseIsOpen_nid_no_patch()); @@ -119,4 +207,22 @@ int main() { Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(!MouseIsOpen_nid_no_patch()); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); + + using ReadAbsolute = int (APS5_VABI*)(std::int32_t, AbsoluteRecord*, std::int32_t); + const auto readAbsolute = reinterpret_cast(reinterpret_cast(&sceMouseReadAbsolutePosition)); + AbsoluteRecord absolute[MOUSE_MAX_DATA_NUM]{}; + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); + Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 1); + Require(readAbsolute(MOUSE_HANDLE, absolute, MOUSE_MAX_DATA_NUM) == 1); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK); + input.SetPresentation(3840, 2160, 1920, 1080); + event = {}; + event.type = SDL_MOUSEMOTION; + event.motion.windowID = 7; + event.motion.xrel = 10; + event.motion.yrel = 5; + input.HandleEvent(event, 7); + Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].x_axis == 20 && data[0].y_axis == 10); + Require(readAbsolute(MOUSE_HANDLE, absolute, 1) == 1 && absolute[0].x == 110 && absolute[0].y == 105); + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); } diff --git a/core/libs/prx/libSceMouse/tests/MouseAbsolute.cpp b/core/libs/prx/libSceMouse/tests/MouseAbsolute.cpp new file mode 100644 index 0000000000..5659f4bf73 --- /dev/null +++ b/core/libs/prx/libSceMouse/tests/MouseAbsolute.cpp @@ -0,0 +1,193 @@ +#include "prx/libSceMouse/include/Api.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" + +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +struct AbsoluteRecord { + std::uint64_t timestamp; + std::uint8_t connected; + std::uint8_t padding[3]; + std::uint32_t buttons; + std::uint16_t x; + std::uint16_t y; + std::int32_t wheel; + std::int32_t tilt; + std::uint8_t reserved[12]; +}; +static_assert(sizeof(AbsoluteRecord) == 40); + +using SetValueFn = int (APS5_VABI*)(std::int32_t, std::int32_t); +using ReadAbsoluteFn = int (APS5_VABI*)(std::int32_t, AbsoluteRecord*, std::int32_t); + +template +Function Resolve(const char* name) { +#ifdef _WIN32 + void* symbol = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceMouse.prx"), name)); +#else + void* symbol = dlsym(RTLD_DEFAULT, name); +#endif + if (!symbol) { + std::fprintf(stderr, "%s is not exported by libSceMouse\n", name); + std::abort(); + } + return reinterpret_cast(symbol); +} + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "mouse absolute position: %s\n", what); + std::abort(); +} + +void Motion(std::int32_t x, std::int32_t y) { + MouseInputEvent event{}; + event.x = x; + event.y = y; + MousePublishInput_nid_no_patch(event); +} + +void Button(std::uint32_t button, bool pressed) { + MouseInputEvent event{}; + event.button = button; + event.pressed = pressed; + MousePublishInput_nid_no_patch(event); +} + +} + +int main() { + const auto setSize = Resolve("sceMouseSetAbsoluteVirtualScreenSize"); + const auto setMode = Resolve("sceMouseSetAbsoluteHighPrecisionMode"); + const auto readAbsolute = Resolve("sceMouseReadAbsolutePosition"); + AbsoluteRecord records[64]; + + Require(setSize(MOUSE_HANDLE, 9) == MOUSE_ERROR_NOT_INITIALIZED, "the screen size before initialization did not return NOT_INITIALIZED first"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_ERROR_NOT_INITIALIZED, "the precision mode before initialization did not return NOT_INITIALIZED"); + Require(readAbsolute(MOUSE_HANDLE, nullptr, 0) == MOUSE_ERROR_NOT_INITIALIZED, "a read before initialization did not return NOT_INITIALIZED first"); + Require(sceMouseInit() == MOUSE_OK, "initialization failed"); + Require(setSize(MOUSE_HANDLE, 4) == MOUSE_ERROR_INVALID_ARG && setSize(MOUSE_HANDLE, -1) == MOUSE_ERROR_INVALID_ARG, "a size past 3 was not refused before the handle"); + Require(setSize(MOUSE_HANDLE, 1) == MOUSE_ERROR_INVALID_HANDLE, "the screen size of a closed handle was set"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_ERROR_INVALID_HANDLE, "the precision mode of a closed handle was set"); + Require(readAbsolute(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG && readAbsolute(MOUSE_HANDLE, records, 0) == MOUSE_ERROR_INVALID_ARG && readAbsolute(MOUSE_HANDLE, records, 65) == MOUSE_ERROR_INVALID_ARG, "a bad read was not refused before the handle"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == MOUSE_ERROR_INVALID_HANDLE, "a closed handle was read"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 5000, 0) == MOUSE_ERROR_INVALID_HANDLE, "a reset with a bad handle and a bad position did not return INVALID_HANDLE"); + + Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE, "open failed"); + std::memset(records, 0xA5, sizeof(records)); + Require(readAbsolute(MOUSE_HANDLE, records, 64) == 1, "the sample queued at open was not read"); + Require(records[0].connected == 1 && records[0].x == 0 && records[0].y == 0, "the position does not start at the top-left corner"); + Require(records[0].padding[0] == 0xA5 && records[0].padding[2] == 0xA5 && records[0].reserved[0] == 0xA5 && records[0].reserved[11] == 0xA5, "bytes the console leaves alone were written"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 0, "a read with no samples returned a record"); + + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1920, 0) == MOUSE_ERROR_INVALID_ARG, "x 1920 was taken at 1920x1080"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1000, 500) == MOUSE_OK, "the reset failed"); + Require(setSize(MOUSE_HANDLE, 3) == MOUSE_OK, "the size 3840x2160 was refused"); + Button(1, true); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 2000 && records[0].y == 1000 && records[0].buttons == 1, "the position was not scaled to 3840x2160"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 3839, 2159) == MOUSE_OK, "the corner of 3840x2160 was refused"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 2000, 1000) == MOUSE_OK, "the reset at 3840x2160 failed"); + Require(setSize(MOUSE_HANDLE, 0) == MOUSE_OK, "the size 1280x720 was refused"); + Button(1, false); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 666 && records[0].y == 333 && records[0].buttons == 0, "the position was not scaled to 1280x720 with truncation"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1280, 0) == MOUSE_ERROR_INVALID_ARG, "x 1280 was taken at 1280x720"); + + Require(setSize(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "returning to 1920x1080 failed"); + Motion(5, -3); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 105 && records[0].y == 97, "motion did not move the position one for one"); + Motion(-5000, 5000); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 0 && records[0].y == 1079, "the position was not clamped to the screen"); + MouseInputEvent wheel{}; + wheel.wheel = 2; + wheel.tilt = -1; + MousePublishInput_nid_no_patch(wheel); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].wheel == 2 && records[0].tilt == -1, "the wheel and tilt were not copied"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "the reset before the merge checks failed"); + Motion(1, 0); + Motion(2, 0); + Motion(4, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 3) == 3 && records[0].x == 101 && records[1].x == 103 && records[2].x == 107, "three samples read with num 3 did not give one record each"); + Motion(1, 0); + Motion(2, 0); + Motion(4, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 114, "three samples read with num 1 did not share one record with all their motion"); + + Require(setMode(MOUSE_HANDLE, 0x100) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "the precision mode was refused"); + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 101, "mode 0x100 did not keep its low byte 0, which is off"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "turning high precision on failed"); + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 100, "a delta of 1 moved a whole pixel in high precision"); + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 101, "the kept fraction was not added to the next delta"); + Motion(10, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 111, "a delta of 10 did not follow the curve"); + Motion(100, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 359 && records[0].y == 100, "a delta of 100 did not follow the curve"); + + MouseData relative[64]; + const auto reopen = [&](const char* what) { + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK && sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE, what); + Require(readAbsolute(MOUSE_HANDLE, records, 64) == 1 && sceMouseRead(MOUSE_HANDLE, relative, 64) == 1, what); + }; + reopen("reopening for the queued precision check failed"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "turning high precision on failed"); + for (int i = 0; i < 10; ++i) Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 105, "ten queued deltas of 1 did not each go through the curve"); + reopen("reopening for the incremental precision check failed"); + Require(setMode(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "turning high precision on failed"); + for (int i = 0; i < 10; ++i) { + Motion(1, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1, "a delta read on its own gave no record"); + } + Require(records[0].x == 105, "ten deltas of 1 read one at a time did not end where the queued ones did"); + + Require(setMode(MOUSE_HANDLE, 0) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 0, 100) == MOUSE_OK, "the edge reset failed"); + Motion(-5, 0); + Motion(5, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 5, "queued motion at the left edge was not clamped per sample"); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 0, 100) == MOUSE_OK, "the edge reset failed"); + Motion(-5, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 0, "motion past the left edge was not clamped"); + Motion(5, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 5, "motion read one at a time at the edge did not end where the queued motion did"); + + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "the reset before the size check failed"); + Motion(10, 0); + Require(setSize(MOUSE_HANDLE, 3) == MOUSE_OK && readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 210 && records[0].y == 200, "a queued sample was not applied at the size in force when it was read"); + Require(setSize(MOUSE_HANDLE, 1) == MOUSE_OK && sceMouseResetAbsolutePosition(MOUSE_HANDLE, 100, 100) == MOUSE_OK, "returning to 1920x1080 failed"); + Motion(10, 0); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 50, 50) == MOUSE_OK && readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 60 && records[0].y == 50, "a queued sample did not move the position a later reset set"); + + Require(sceMouseRead(MOUSE_HANDLE, relative, 64) >= 0, "draining sceMouseRead failed"); + Motion(7, 0); + Require(sceMouseRead(MOUSE_HANDLE, relative, 1) == 1 && relative[0].x_axis == 7, "sceMouseRead did not get the motion"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 67, "a relative read took the sample from the absolute reads"); + Motion(3, 0); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == 1 && records[0].x == 70, "the absolute read missed the motion"); + Require(sceMouseRead(MOUSE_HANDLE, relative, 1) == 1 && relative[0].x_axis == 3, "an absolute read took the sample from sceMouseRead"); + + MouseInputEvent disconnect{}; + disconnect.connectionChange = true; + disconnect.connected = false; + MousePublishInput_nid_no_patch(disconnect); + std::memset(records, 0xA5, sizeof(records)); + Require(readAbsolute(MOUSE_HANDLE, records, 4) == 1, "a disconnected mouse did not return one record"); + const AbsoluteRecord zero{}; + Require(std::memcmp(&records[0], &zero, sizeof(zero)) == 0 && records[1].connected == 0xA5, "a disconnected mouse did not return one zeroed record"); + + Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK, "close failed"); + Require(readAbsolute(MOUSE_HANDLE, records, 1) == MOUSE_ERROR_INVALID_HANDLE, "a closed handle was read"); + std::puts("mouse absolute position tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 95b53ac6c0..ffa1a6d6a6 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -14,6 +14,8 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { if (event.motion.windowID != windowId || !focused || ModalDialogCapturesInput_nid_postfix()) return; input.x = scaled(event.motion.xrel, scaleX, remainderX); input.y = scaled(event.motion.yrel, scaleY, remainderY); + input.countsX = event.motion.xrel; + input.countsY = event.motion.yrel; break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md index df77103a23..3dbb311dfc 100644 --- a/docs/dev/debt/libSceMouse.md +++ b/docs/dev/debt/libSceMouse.md @@ -21,8 +21,9 @@ - [sceMouseDisconnectAll, sceMouseVirtualDeviceAddDevice, sceMouseVirtualDeviceDeleteDevice and sceMouseVirtualDeviceGetCursor](../../../core/libs/prx/libSceMouse/InventoryExports.cpp) - the names and NIDs are recorded in the [13.60 export inventory](https://github.com/xdCloudy/PS5-13.60-Function-Stub-Library/blob/e495d3cf24aea17cefda59decdfda9e3e4e3e5ff/data/firmware-13.60/libscemouse-acdd5b5e23.jsonl). Signatures and behavior remain unknown; the parameterless `int` declarations only resolve imports and every call throws. Not measured on a console. - [sceMouseConnectPort, sceMouseDebugGetDeviceId, sceMouseDeviceOpen, sceMouseDisconnectDevice, sceMouseDisconnectPort, sceMouseGetDeviceInfo, sceMouseMbusInit, sceMouseSetHandType, sceMouseSetPointerSpeed and sceMouseSetProcessPrivilege](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMouse.c); the no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/mouse/mouse.cpp#L34-L146), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. -- [sceMouseReadAbsolutePosition, sceMouseSetAbsoluteHighPrecisionMode and sceMouseSetAbsoluteVirtualScreenSize](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the names match unresolved NIDs reported by a title, but their signatures and behavior are not verified. These no-argument declarations only resolve imports; the exports throw if called. ### Functional -- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the position is not stored because the host reports relative motion only. Native input widths and validation are confirmed, including the default 1920 by 1080 bounds; other native coordinate modes and absolute reports remain unimplemented +- [Publish](../../../core/libs/prx/libSceMouse/src/mouse_impl.cpp) (libSceMouse) - consecutive motion-only reports with the same buttons are summed into the newest unread entry, so a title that reads a few entries per frame gets current motion instead of a full queue of old reports. Reports with no motion and no button, wheel, tilt or connection change are dropped. Per-report granularity and the console's own report rate are not reproduced + +- [sceMouseResetAbsolutePosition, sceMouseSetAbsoluteVirtualScreenSize, sceMouseSetAbsoluteHighPrecisionMode, sceMouseReadAbsolutePosition](../../../core/libs/prx/libSceMouse/src/mouse_impl.cpp) (libSceMouse) - follow firmware 13.42 [run natively](https://github.com/boykopovar/AnyPS5/issues/11477#issuecomment-6502712241) with the driver stubbed: checks and order, screen sizes and rescale, the mode byte, the curve with its fraction, the 40-byte record. Host choices, after [XatiX's title-tested version](https://github.com/boykopovar/AnyPS5/issues/11477#issuecomment-6503782633): each host event is one sample with unscaled counts, in its own queue, so `sceMouseRead`'s motion merging does not feed the curve. A read takes all pending samples; past `num`, same-button neighbours share a record, each still moving the pointer, and the last `num` records are kept. Speed is taken as 4, the hand swap and flagged bit never apply, and a disconnected mouse stands in for a zero driver status