feat(libSceVideoOut): capture the host cursor for guest mice and scale their deltas
The console's mouse reports relative motion, so the host cursor can never line up with the cursor a title draws. While a title has a mouse open and the window is focused the host cursor is hidden and captured, 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, so the title's cursor travels as far as the host pointer would, on scaled desktops and letterboxed windows alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2cf23fcc56
commit
94087d08b0
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -27,4 +27,9 @@ int APS5_VABI sceMouseResetAbsolutePosition() {
|
|||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool MouseIsOpen_nid_postfix() {
|
||||
return Mouse::IsOpen();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:$<TARGET_FILE_DIR:libSceVideoOut>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceMouse>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,24 @@
|
|||
#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_MOUSEINPUT_HPP
|
||||
|
||||
#include "SDL_events.h"
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@
|
|||
#include "prx/libSceMouse/include/MouseState.hpp"
|
||||
|
||||
#include "SDL_mouse.h"
|
||||
#include "SDL_video.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
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<double>(sourceWidth) / static_cast<double>(presentedWidth);
|
||||
const double y = static_cast<double>(sourceHeight) / static_cast<double>(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<double>(delta) * scale + remainder;
|
||||
const double whole = std::trunc(exact);
|
||||
remainder = exact - whole;
|
||||
return static_cast<std::int32_t>(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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void>& 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<std::uint32_t>(windowWidth), static_cast<std::uint32_t>(windowHeight), true);
|
||||
mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height);
|
||||
}
|
||||
const auto finished = AgcDriver::FrameTiming::Clock::now();
|
||||
AgcDriver::FrameTiming::Clock::duration interval{};
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
Loading…
Reference in New Issue