From 49b9b5396d7596f66a9874b01f28f39d73ec55b9 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:02:08 -0400 Subject: [PATCH 1/2] chore(deps): bump libvirtualhid --- docs/troubleshooting.md | 4 + src/input.cpp | 2 + src/platform/common.h | 4 + src/platform/macos/display.mm | 47 +++++++- src/platform/virtualhid_input.cpp | 111 ++++++++++++++++-- src/platform/virtualhid_input.h | 17 +++ src/video.cpp | 2 + tests/unit/platform/test_virtualhid_input.cpp | 68 ++++++++++- third-party/libvirtualhid | 2 +- 9 files changed, 240 insertions(+), 17 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3a40f399379..633537f3a37 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -27,6 +27,10 @@ If you forgot your credentials to the web UI, try this. ### Unusual Mouse Behavior If you experience unusual mouse behavior, try attaching a physical mouse to the Sunshine host. +For absolute-positioning problems, set **Configuration > Advanced > Log Level** to **Debug** and reproduce the issue. +Sunshine forwards libvirtualhid diagnostics with a `[libvirtualhid]` prefix, including the streamed display viewport and +full desktop bounds used for mouse coordinate mapping. + ### Web UI Access Can't access the web UI? diff --git a/src/input.cpp b/src/input.cpp index a12514d4e5a..7063e495163 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -863,6 +863,8 @@ namespace input { touch_port_dim_y, touch_port.logical_width, touch_port.logical_height, + touch_port.env_offset_x, + touch_port.env_offset_y, }; platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second); diff --git a/src/platform/common.h b/src/platform/common.h index e8aff70ec2c..ed496c83736 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -389,6 +389,8 @@ namespace platf { int height; ///< Frame or display height in pixels. int logical_width; ///< Logical width after display scaling. int logical_height; ///< Logical height after display scaling. + int env_offset_x {}; ///< Horizontal origin of the full input environment in native desktop coordinates. + int env_offset_y {}; ///< Vertical origin of the full input environment in native desktop coordinates. }; // These values must match Limelight-internal.h's SS_FF_* constants! @@ -785,6 +787,8 @@ namespace platf { // Offsets for when streaming a specific monitor. By default, they are 0. int offset_x {0}; ///< Horizontal capture offset in physical pixels. int offset_y {0}; ///< Vertical capture offset in physical pixels. + int env_offset_x {0}; ///< Horizontal origin of the full capture environment in native desktop coordinates. + int env_offset_y {0}; ///< Vertical origin of the full capture environment in native desktop coordinates. int env_width {0}; ///< Width of the full capture environment in physical pixels. int env_height {0}; ///< Height of the full capture environment in physical pixels. int env_logical_width {0}; ///< Width of the full capture environment after display scaling. diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index d5fe3d024b2..903a139fe0a 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -6,8 +6,10 @@ // standard includes #include #include +#include #include #include +#include // local includes #include "src/config.h" @@ -48,6 +50,30 @@ return display_id; } + /** + * @brief Return the union of active CoreGraphics display bounds. + * + * @param fallback_display Display whose bounds are used if enumeration fails. + * @return Full active desktop bounds in CoreGraphics coordinates. + */ + CGRect active_desktop_bounds(CGDirectDisplayID fallback_display) { + std::uint32_t display_count {}; + if (CGGetActiveDisplayList(0, nullptr, &display_count) != kCGErrorSuccess || display_count == 0) { + return CGDisplayBounds(fallback_display); + } + + std::vector displays(display_count); + if (CGGetActiveDisplayList(display_count, displays.data(), &display_count) != kCGErrorSuccess || display_count == 0) { + return CGDisplayBounds(fallback_display); + } + + auto bounds = CGDisplayBounds(displays.front()); + for (std::uint32_t index = 1; index < display_count; ++index) { + bounds = CGRectUnion(bounds, CGDisplayBounds(displays[index])); + } + return bounds; + } + OSType videotoolbox_pixel_format(const video::config_t &config) { const auto colorspace {video::colorspace_from_client_config(config, false)}; return colorspace.bit_depth == 10 ? kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; @@ -258,6 +284,24 @@ static void setPixelFormat(void *display, OSType pixelFormat) { BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv; + const auto selected_display_bounds = CGDisplayBounds(display->display_id); + const auto desktop_bounds = active_desktop_bounds(display->display_id); + display->offset_x = static_cast(selected_display_bounds.origin.x); + display->offset_y = static_cast(selected_display_bounds.origin.y); + display->logical_width = static_cast(selected_display_bounds.size.width); + display->logical_height = static_cast(selected_display_bounds.size.height); + display->env_offset_x = static_cast(desktop_bounds.origin.x); + display->env_offset_y = static_cast(desktop_bounds.origin.y); + display->env_logical_width = static_cast(desktop_bounds.size.width); + display->env_logical_height = static_cast(desktop_bounds.size.height); + display->env_width = display->env_logical_width; + display->env_height = display->env_logical_height; + + BOOST_LOG(debug) << "Selected display bounds: "sv << display->offset_x << 'x' << display->offset_y << ' ' + << display->logical_width << 'x' << display->logical_height; + BOOST_LOG(debug) << "Active desktop bounds: "sv << display->env_offset_x << 'x' << display->env_offset_y << ' ' + << display->env_logical_width << 'x' << display->env_logical_height; + display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; if (!display->av_capture) { @@ -267,9 +311,6 @@ static void setPixelFormat(void *display, OSType pixelFormat) { display->width = display->av_capture.frameWidth; display->height = display->av_capture.frameHeight; - // We also need set env_width and env_height for absolute mouse coordinates - display->env_width = display->width; - display->env_height = display->height; if (hwdevice_type == platf::mem_type_e::videotoolbox) { const auto pixel_format {videotoolbox_pixel_format(config)}; diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp index 362f50af5e0..19cf31c19e4 100644 --- a/src/platform/virtualhid_input.cpp +++ b/src/platform/virtualhid_input.cpp @@ -80,6 +80,72 @@ namespace platf::virtualhid { } } + /** + * @brief Forward one libvirtualhid diagnostic to Sunshine's logger. + * + * @param level libvirtualhid diagnostic severity. + * @param message Diagnostic message. + */ + void log_virtualhid_message(lvh::LogLevel level, const std::string &message) { + switch (level) { + case lvh::LogLevel::debug: + BOOST_LOG(debug) << "[libvirtualhid] "sv << message; + break; + case lvh::LogLevel::info: + BOOST_LOG(info) << "[libvirtualhid] "sv << message; + break; + case lvh::LogLevel::warning: + BOOST_LOG(warning) << "[libvirtualhid] "sv << message; + break; + case lvh::LogLevel::error: + BOOST_LOG(error) << "[libvirtualhid] "sv << message; + break; + } + } + + /** + * @brief Compare two pointer viewports. + * + * @param left First viewport. + * @param right Second viewport. + * @return `true` when every bound matches. + */ + bool pointer_viewports_equal(const lvh::PointerViewport &left, const lvh::PointerViewport &right) { + return left.offset_x == right.offset_x && + left.offset_y == right.offset_y && + left.width == right.width && + left.height == right.height; + } + + /** + * @brief Convert Sunshine absolute-input geometry to libvirtualhid bounds. + * + * @param touch_port Sunshine desktop and display geometry. + * @return Full virtual-desktop bounds followed by the streamed viewport. + */ + std::pair pointer_viewports(const touch_port_t &touch_port) { + const lvh::PointerViewport desktop { + .offset_x = touch_port.env_offset_x, + .offset_y = touch_port.env_offset_y, + .width = touch_port.width, + .height = touch_port.height, + }; + + if (touch_port.logical_width <= 0 || touch_port.logical_height <= 0) { + return {desktop, desktop}; + } + + return { + desktop, + { + .offset_x = touch_port.offset_x, + .offset_y = touch_port.offset_y, + .width = touch_port.logical_width, + .height = touch_port.logical_height, + }, + }; + } + float normalize_axis(std::int16_t value) { if (value < 0) { return std::max(-1.0F, static_cast(value) / 32768.0F); @@ -475,7 +541,13 @@ namespace platf::virtualhid { } void input_context_t::refresh_mouse() { + refresh_mouse({}, {}); + } + + void input_context_t::refresh_mouse(const lvh::PointerViewport &desktop, const lvh::PointerViewport &viewport) { mouse.reset(); + mouse_desktop = desktop; + mouse_viewport = viewport; if (!runtime || !runtime->capabilities().supports_mouse) { return; } @@ -483,6 +555,8 @@ namespace platf::virtualhid { lvh::CreateMouseOptions options; options.profile = lvh::profiles::mouse(); options.stable_id = "sunshine-mouse"; + options.desktop = desktop; + options.viewport = viewport; auto created = runtime->create_mouse(options); if (created) { mouse = std::move(created.mouse); @@ -491,6 +565,15 @@ namespace platf::virtualhid { } } + void input_context_t::update_mouse_viewport(const touch_port_t &touch_port) { + const auto [desktop, viewport] = pointer_viewports(touch_port); + if (mouse && pointer_viewports_equal(mouse_desktop, desktop) && pointer_viewports_equal(mouse_viewport, viewport)) { + return; + } + + refresh_mouse(desktop, viewport); + } + client_context_t::client_context_t(input_context_t &input): global {&input} { if (!global->runtime) { @@ -525,6 +608,7 @@ namespace platf::virtualhid { std::unique_ptr create_runtime(lvh::BackendKind backend) { lvh::RuntimeOptions options; options.backend = backend; + options.log_callback = log_virtualhid_message; return lvh::Runtime::create(options); } @@ -800,17 +884,24 @@ namespace platf::virtualhid { } void abs_mouse(input_context_t &context, const touch_port_t &touch_port, float x, float y) { - if (context.mouse) { - log_failure( - "submit libvirtualhid absolute mouse movement"sv, - context.mouse->move_absolute( - static_cast(std::lround(x)), - static_cast(std::lround(y)), - touch_port.width, - touch_port.height - ) - ); + if (!context.mouse) { + return; } + + context.update_mouse_viewport(touch_port); + if (!context.mouse) { + return; + } + + log_failure( + "submit libvirtualhid absolute mouse movement"sv, + context.mouse->move_absolute( + x - static_cast(context.mouse_viewport.offset_x), + y - static_cast(context.mouse_viewport.offset_y), + context.mouse_viewport.width, + context.mouse_viewport.height + ) + ); } void button_mouse(input_context_t &context, int button, bool release) { diff --git a/src/platform/virtualhid_input.h b/src/platform/virtualhid_input.h index d6302b25529..11738d15656 100644 --- a/src/platform/virtualhid_input.h +++ b/src/platform/virtualhid_input.h @@ -46,9 +46,26 @@ namespace platf::virtualhid { */ void refresh_mouse(); + /** + * @brief Recreate the shared mouse for a virtual desktop and target viewport. + * + * @param desktop Full native virtual-desktop bounds. + * @param viewport Native bounds of the streamed display. + */ + void refresh_mouse(const lvh::PointerViewport &desktop, const lvh::PointerViewport &viewport); + + /** + * @brief Retarget the shared mouse when streamed display geometry changes. + * + * @param touch_port Desktop and viewport geometry for absolute input. + */ + void update_mouse_viewport(const touch_port_t &touch_port); + std::unique_ptr runtime; ///< libvirtualhid runtime. std::unique_ptr keyboard; ///< Shared virtual keyboard. std::unique_ptr mouse; ///< Shared virtual mouse. + lvh::PointerViewport mouse_desktop; ///< Virtual-desktop bounds used to create the shared mouse. + lvh::PointerViewport mouse_viewport; ///< Streamed display bounds used to create the shared mouse. std::vector> gamepads {static_cast(MAX_GAMEPADS)}; ///< Virtual gamepad slots. }; diff --git a/src/video.cpp b/src/video.cpp index 843a19a0a58..2505938466f 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -2553,6 +2553,8 @@ namespace video { config.height, display->logical_width, display->logical_height, + display->env_offset_x, + display->env_offset_y, }, display->env_width, display->env_height, diff --git a/tests/unit/platform/test_virtualhid_input.cpp b/tests/unit/platform/test_virtualhid_input.cpp index 637b64b00b7..ca57dfd0c5b 100644 --- a/tests/unit/platform/test_virtualhid_input.cpp +++ b/tests/unit/platform/test_virtualhid_input.cpp @@ -5,6 +5,7 @@ // test includes #include "../../tests_common.h" +#include "../../tests_log_checker.h" // standard includes #include @@ -295,6 +296,24 @@ TEST_F(VirtualHidDeviceTest, RejectsUnavailableAndInvalidGamepadSlots) { EXPECT_EQ(platf::virtualhid::rebind_gamepad(*context(), valid_id, feedback_queue()), -1); } +TEST_F(VirtualHidDeviceTest, RoutesLibvirtualhidDiagnosticsToSunshineLog) { + auto runtime = platf::virtualhid::create_runtime(lvh::BackendKind::fake); + ASSERT_NE(runtime, nullptr); + + auto created = runtime->create_mouse(); + ASSERT_TRUE(created); + ASSERT_NE(created.mouse, nullptr); + EXPECT_TRUE(created.mouse->move_relative(1, -1).ok()); + EXPECT_FALSE(created.mouse->move_absolute(0, 0, 0, 1).ok()); + EXPECT_TRUE(created.mouse->close().ok()); + EXPECT_FALSE(created.mouse->move_relative(1, 1).ok()); + + EXPECT_TRUE(log_checker::line_contains("test_sunshine.log", "Info: [libvirtualhid] initialized fake backend")); + EXPECT_TRUE(log_checker::line_contains("test_sunshine.log", "Debug: [libvirtualhid] mouse ")); + EXPECT_TRUE(log_checker::line_contains("test_sunshine.log", "Warning: [libvirtualhid] rejected mouse event:")); + EXPECT_TRUE(log_checker::line_contains("test_sunshine.log", "Error: [libvirtualhid] mouse input failed: mouse is closed")); +} + TEST_F(VirtualHidDeviceTest, AllocatesManualProfileAndTranslatesFullState) { const auto active_devices_before_gamepad = context()->runtime->active_device_count(); config::input.ds4_back_as_touchpad_click = true; @@ -641,12 +660,21 @@ TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) { EXPECT_EQ(mouse_event.x, -4); EXPECT_EQ(mouse_event.y, 7); - const platf::touch_port_t viewport {10, 20, 1920, 1080, 1920, 1080}; + const platf::touch_port_t viewport { + .offset_x = 10, + .offset_y = 20, + .width = 3840, + .height = 2160, + .logical_width = 1920, + .logical_height = 1080, + }; platf::virtualhid::abs_mouse(*context(), viewport, 10.6F, 20.4F); mouse_event = context()->mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::absolute_motion); - EXPECT_EQ(mouse_event.x, 11); - EXPECT_EQ(mouse_event.y, 20); + EXPECT_EQ(mouse_event.x, 1); + EXPECT_EQ(mouse_event.y, 0); + EXPECT_NEAR(mouse_event.absolute_x, 0.6F, 0.0001F); + EXPECT_NEAR(mouse_event.absolute_y, 0.4F, 0.0001F); EXPECT_EQ(mouse_event.width, 1920); EXPECT_EQ(mouse_event.height, 1080); @@ -721,6 +749,40 @@ TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) { platf::virtualhid::unicode(*context(), text.data(), static_cast(text.size())); } +TEST_F(VirtualHidDeviceTest, RetargetsAbsoluteMouseWhenStreamedViewportChanges) { + const platf::touch_port_t left_viewport { + .offset_x = -1920, + .offset_y = 0, + .width = 3840, + .height = 1080, + .logical_width = 1920, + .logical_height = 1080, + .env_offset_x = -1920, + .env_offset_y = 0, + }; + + const auto initial_mouse_id = context()->mouse->device_id(); + platf::virtualhid::abs_mouse(*context(), left_viewport, -960.25F, 540.5F); + const auto left_mouse_id = context()->mouse->device_id(); + EXPECT_NE(left_mouse_id, initial_mouse_id); + auto mouse_event = context()->mouse->last_submitted_event(); + EXPECT_FLOAT_EQ(mouse_event.absolute_x, 959.75F); + EXPECT_FLOAT_EQ(mouse_event.absolute_y, 540.5F); + EXPECT_EQ(mouse_event.width, 1920); + EXPECT_EQ(mouse_event.height, 1080); + + platf::virtualhid::abs_mouse(*context(), left_viewport, -480.0F, 270.0F); + EXPECT_EQ(context()->mouse->device_id(), left_mouse_id); + + auto right_viewport = left_viewport; + right_viewport.offset_x = 0; + platf::virtualhid::abs_mouse(*context(), right_viewport, 960.0F, 540.0F); + EXPECT_NE(context()->mouse->device_id(), left_mouse_id); + mouse_event = context()->mouse->last_submitted_event(); + EXPECT_FLOAT_EQ(mouse_event.absolute_x, 960.0F); + EXPECT_FLOAT_EQ(mouse_event.absolute_y, 540.0F); +} + TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) { const platf::touch_port_t viewport {10, 20, 800, 600, 400, 300}; platf::touch_input_t touch {LI_TOUCH_EVENT_DOWN, 100, 7, -0.1F, 1.1F, 1.2F, 15.0F, 10.0F}; diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index 53e1a949fc0..36a1976a300 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit 53e1a949fc0784af716b782ddfa6c647cafd1f05 +Subproject commit 36a1976a300bdf283c51755f856d06e215cc1792 From 123df481e4d1775a706c6b908143bec2fc950f4f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:05:35 -0400 Subject: [PATCH 2/2] fix(input): retarget relative mouse after display switches Consume pending touch-port geometry before forwarding relative mouse motion so libvirtualhid receives the streamed viewport selected by display switching. Reuse the same desktop conversion for absolute and relative input, avoid recreating the mouse when bounds are unchanged, and preserve the no-device path used on unsupported platforms. Add input-pipeline and virtual HID regression coverage. --- src/input.cpp | 82 +++++++++++++------ src/input.h | 9 ++ src/platform/common.h | 9 ++ src/platform/virtualhid_input.cpp | 13 +++ src/platform/virtualhid_input.h | 12 ++- tests/unit/platform/test_virtualhid_input.cpp | 35 ++++++++ tests/unit/test_input.cpp | 52 ++++++++++++ 7 files changed, 184 insertions(+), 28 deletions(-) diff --git a/src/input.cpp b/src/input.cpp index 7063e495163..eedf5123508 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -707,6 +707,38 @@ namespace input { } } + /** + * @brief Consume the latest streamed-display geometry for an input session. + * + * @param input Input session receiving the geometry. + */ + void refresh_touch_port(std::shared_ptr &input) { + auto &touch_port_event = input->touch_port_event; + if (touch_port_event->peek()) { + input->touch_port = *touch_port_event->pop(); + } + } + + /** + * @brief Convert stream touch geometry to platform pointer bounds. + * + * @param touch_port Stream touch geometry. + * @return Platform pointer bounds for the full desktop and streamed display. + */ + platf::touch_port_t platform_touch_port(const input::touch_port_t &touch_port) { + const bool has_logical_environment = touch_port.env_logical_width != 0 && touch_port.env_logical_height != 0; + return { + touch_port.offset_x, + touch_port.offset_y, + has_logical_environment ? touch_port.env_logical_width : touch_port.env_width, + has_logical_environment ? touch_port.env_logical_height : touch_port.env_height, + touch_port.logical_width, + touch_port.logical_height, + touch_port.env_offset_x, + touch_port.env_offset_y, + }; + } + /** * @brief Forward a client input packet directly to the platform backend. * @@ -719,7 +751,17 @@ namespace input { } input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY; - platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); + refresh_touch_port(input); + if (input->touch_port) { + platf::move_mouse( + platf_input, + platform_touch_port(input->touch_port), + util::endian::big(packet->deltaX), + util::endian::big(packet->deltaY) + ); + } else { + platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); + } } /** @@ -730,11 +772,8 @@ namespace input { * @return The host-relative coordinate pair if a touchport is available. */ std::optional> client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { - auto &touch_port_event = input->touch_port_event; + refresh_touch_port(input); auto &touch_port = input->touch_port; - if (touch_port_event->peek()) { - touch_port = *touch_port_event->pop(); - } if (!touch_port) { BOOST_LOG(verbose) << "Ignoring early absolute input without a touch port"sv; return std::nullopt; @@ -846,28 +885,7 @@ namespace input { auto &touch_port = input->touch_port; - int touch_port_dim_x; - int touch_port_dim_y; - if (touch_port.env_logical_width != 0 && touch_port.env_logical_height != 0) { - touch_port_dim_x = touch_port.env_logical_width; - touch_port_dim_y = touch_port.env_logical_height; - } else { - touch_port_dim_x = touch_port.env_width; - touch_port_dim_y = touch_port.env_height; - } - - platf::touch_port_t abs_port { - touch_port.offset_x, - touch_port.offset_y, - touch_port_dim_x, - touch_port_dim_y, - touch_port.logical_width, - touch_port.logical_height, - touch_port.env_offset_x, - touch_port.env_offset_y, - }; - - platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second); + platf::abs_mouse(platf_input, platform_touch_port(touch_port), tpcoords->first, tpcoords->second); } /** @@ -2380,6 +2398,16 @@ namespace input { ::input::passthrough(input, &packet); } + void send_relative_mouse_packet(std::shared_ptr &input, std::int16_t delta_x, std::int16_t delta_y) { + NV_REL_MOUSE_MOVE_PACKET packet {}; + packet.header.size = util::endian::big(sizeof(packet) - sizeof(packet.header.size)); + packet.header.magic = util::endian::little(MOUSE_MOVE_REL_MAGIC_GEN5); + packet.deltaX = util::endian::big(delta_x); + packet.deltaY = util::endian::big(delta_y); + + ::input::passthrough(input, &packet); + } + void reset_keyboard_state() { task_pool.cancel(key_press_repeat_id); key_press_repeat_id = nullptr; diff --git a/src/input.h b/src/input.h index d383e425b95..7b0e7f291d0 100644 --- a/src/input.h +++ b/src/input.h @@ -143,6 +143,15 @@ namespace input { */ void send_keyboard_packet(std::shared_ptr &input, std::uint16_t key_code, std::uint8_t modifiers, std::uint8_t flags, bool release); + /** + * @brief Process one client relative-mouse packet on the calling thread. + * + * @param input Retained input state. + * @param delta_x Horizontal client mouse delta. + * @param delta_y Vertical client mouse delta. + */ + void send_relative_mouse_packet(std::shared_ptr &input, std::int16_t delta_x, std::int16_t delta_y); + /** * @brief Forget every key Sunshine tracks as pressed and cancel any pending key repeat. */ diff --git a/src/platform/common.h b/src/platform/common.h index ed496c83736..8e579b826ca 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -1158,6 +1158,15 @@ namespace platf { * @param deltaY Delta y. */ void move_mouse(input_t &input, int deltaX, int deltaY); + /** + * @brief Retarget the virtual mouse and move it relatively. + * + * @param input Platform input backend that receives the event. + * @param touch_port Desktop and streamed-display bounds for the pointer. + * @param deltaX Delta x. + * @param deltaY Delta y. + */ + void move_mouse(input_t &input, const touch_port_t &touch_port, int deltaX, int deltaY); /** * @brief Move the pointer to an absolute client-provided touch coordinate. * diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp index 19cf31c19e4..1983ac89047 100644 --- a/src/platform/virtualhid_input.cpp +++ b/src/platform/virtualhid_input.cpp @@ -883,6 +883,15 @@ namespace platf::virtualhid { } } + void move_mouse(input_context_t &context, const touch_port_t &touch_port, int delta_x, int delta_y) { + if (!context.mouse) { + return; + } + + context.update_mouse_viewport(touch_port); + move_mouse(context, delta_x, delta_y); + } + void abs_mouse(input_context_t &context, const touch_port_t &touch_port, float x, float y) { if (!context.mouse) { return; @@ -1139,6 +1148,10 @@ namespace platf { virtualhid::move_mouse(virtualhid::get_input_context(input), deltaX, deltaY); } + void move_mouse(input_t &input, const touch_port_t &touch_port, int deltaX, int deltaY) { + virtualhid::move_mouse(virtualhid::get_input_context(input), touch_port, deltaX, deltaY); + } + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { virtualhid::abs_mouse(virtualhid::get_input_context(input), touch_port, x, y); } diff --git a/src/platform/virtualhid_input.h b/src/platform/virtualhid_input.h index 11738d15656..b05638c522d 100644 --- a/src/platform/virtualhid_input.h +++ b/src/platform/virtualhid_input.h @@ -57,7 +57,7 @@ namespace platf::virtualhid { /** * @brief Retarget the shared mouse when streamed display geometry changes. * - * @param touch_port Desktop and viewport geometry for absolute input. + * @param touch_port Desktop and viewport geometry for pointer input. */ void update_mouse_viewport(const touch_port_t &touch_port); @@ -254,6 +254,16 @@ namespace platf::virtualhid { */ void move_mouse(input_context_t &context, int delta_x, int delta_y); + /** + * @brief Retarget the virtual mouse and move it relatively. + * + * @param context Input context. + * @param touch_port Desktop and streamed-display bounds for the pointer. + * @param delta_x Horizontal delta. + * @param delta_y Vertical delta. + */ + void move_mouse(input_context_t &context, const touch_port_t &touch_port, int delta_x, int delta_y); + /** * @brief Move the virtual mouse absolutely inside a target touch port. * diff --git a/tests/unit/platform/test_virtualhid_input.cpp b/tests/unit/platform/test_virtualhid_input.cpp index ca57dfd0c5b..3ea1621b8eb 100644 --- a/tests/unit/platform/test_virtualhid_input.cpp +++ b/tests/unit/platform/test_virtualhid_input.cpp @@ -741,6 +741,7 @@ TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) { context()->mouse.reset(); context()->keyboard.reset(); platf::virtualhid::move_mouse(*context(), 1, 1); + platf::virtualhid::move_mouse(*context(), viewport, 1, 1); platf::virtualhid::abs_mouse(*context(), viewport, 1.0F, 1.0F); platf::virtualhid::button_mouse(*context(), BUTTON_LEFT, false); platf::virtualhid::scroll(*context(), 1); @@ -783,6 +784,40 @@ TEST_F(VirtualHidDeviceTest, RetargetsAbsoluteMouseWhenStreamedViewportChanges) EXPECT_FLOAT_EQ(mouse_event.absolute_y, 540.0F); } +TEST_F(VirtualHidDeviceTest, RetargetsRelativeMouseWhenStreamedViewportChanges) { + const platf::touch_port_t left_viewport { + .offset_x = -1920, + .offset_y = 0, + .width = 3840, + .height = 1080, + .logical_width = 1920, + .logical_height = 1080, + .env_offset_x = -1920, + .env_offset_y = 0, + }; + + const auto initial_mouse_id = context()->mouse->device_id(); + platf::virtualhid::move_mouse(*context(), left_viewport, 12, -7); + const auto left_mouse_id = context()->mouse->device_id(); + EXPECT_NE(left_mouse_id, initial_mouse_id); + auto mouse_event = context()->mouse->last_submitted_event(); + EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); + EXPECT_EQ(mouse_event.x, 12); + EXPECT_EQ(mouse_event.y, -7); + + platf::virtualhid::move_mouse(*context(), left_viewport, 3, 4); + EXPECT_EQ(context()->mouse->device_id(), left_mouse_id); + + auto right_viewport = left_viewport; + right_viewport.offset_x = 0; + platf::virtualhid::move_mouse(*context(), right_viewport, -5, 9); + EXPECT_NE(context()->mouse->device_id(), left_mouse_id); + mouse_event = context()->mouse->last_submitted_event(); + EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); + EXPECT_EQ(mouse_event.x, -5); + EXPECT_EQ(mouse_event.y, 9); +} + TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) { const platf::touch_port_t viewport {10, 20, 800, 600, 400, 300}; platf::touch_input_t touch {LI_TOUCH_EVENT_DOWN, 100, 7, -0.1F, 1.1F, 1.2F, 15.0F, 10.0F}; diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp index e8854721882..5507a97abb8 100644 --- a/tests/unit/test_input.cpp +++ b/tests/unit/test_input.cpp @@ -67,6 +67,7 @@ namespace { void SetUp() override { original_input_ = config::input; config::input.controller = true; + config::input.mouse = true; config::input.gamepad = "xseries"; auto platform_input = platf::input(); @@ -297,3 +298,54 @@ TEST_F(InputGamepadSessionTest, RefreshesSharedVirtualInputAfterLicenseStateChan EXPECT_NE(context().mouse->device_id(), original_mouse_id); EXPECT_EQ(runtime().active_device_count(), active_devices); } + +TEST_F(InputGamepadSessionTest, RetargetsRelativeMouseFromLatestStreamGeometry) { + auto session_mail = std::make_shared(); + auto stream_input = input::alloc(session_mail, "relative-mouse-client"); + ASSERT_NE(stream_input, nullptr); + + input::touch_port_t left_viewport { + { + .offset_x = -1920, + .offset_y = 0, + .width = 1920, + .height = 1080, + .logical_width = 1920, + .logical_height = 1080, + .env_offset_x = -1920, + .env_offset_y = 0, + }, + 3840, + 1080, + 0.0F, + 0.0F, + 1.0F, + 1.0F, + 3840, + 1080, + }; + + const auto initial_mouse_id = context().mouse->device_id(); + session_mail->event(mail::touch_port)->raise(left_viewport); + input::testing::send_relative_mouse_packet(stream_input, 12, -7); + const auto left_mouse_id = context().mouse->device_id(); + EXPECT_NE(left_mouse_id, initial_mouse_id); + auto mouse_event = context().mouse->last_submitted_event(); + EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); + EXPECT_EQ(mouse_event.x, 12); + EXPECT_EQ(mouse_event.y, -7); + EXPECT_EQ(context().mouse_desktop.offset_x, -1920); + EXPECT_EQ(context().mouse_desktop.width, 3840); + EXPECT_EQ(context().mouse_viewport.offset_x, -1920); + EXPECT_EQ(context().mouse_viewport.width, 1920); + + left_viewport.offset_x = 0; + session_mail->event(mail::touch_port)->raise(left_viewport); + input::testing::send_relative_mouse_packet(stream_input, -5, 9); + EXPECT_NE(context().mouse->device_id(), left_mouse_id); + mouse_event = context().mouse->last_submitted_event(); + EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); + EXPECT_EQ(mouse_event.x, -5); + EXPECT_EQ(mouse_event.y, 9); + EXPECT_EQ(context().mouse_viewport.offset_x, 0); +}