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..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,26 +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, - }; - - 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); } /** @@ -2378,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 e8aff70ec2c..8e579b826ca 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. @@ -1154,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/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..1983ac89047 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); } @@ -799,18 +883,34 @@ 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) { - 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) { @@ -1048,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 d6302b25529..b05638c522d 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 pointer 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. }; @@ -237,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/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..3ea1621b8eb 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); @@ -713,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); @@ -721,6 +750,74 @@ 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, 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); +} 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