diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3c3d604ff01..873e3439926 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -31,6 +31,11 @@ For absolute-positioning problems, set **Configuration > Advanced > Log Level** Sunshine forwards libvirtualhid diagnostics with a `[libvirtualhid]` prefix, including the streamed display viewport and full desktop bounds used for mouse coordinate mapping. +For native touch or pen input on KDE Wayland, Sunshine maps its virtual devices to the streamed display. +Use Plasma 6.5.1 or later, which fixes changing a device's assigned output. Display layouts with monitors +left of or above the desktop origin are supported. Other Wayland compositors retain their device mapping +settings; assign Sunshine's virtual touchscreen and pen tablet through the compositor's input configuration. + ### Web UI Access Can't access the web UI? diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml index c6eba43e68e..7e03d69bff5 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml @@ -24,6 +24,7 @@ finish-args: - --socket=fallback-x11 # show windows using X11 - --system-talk-name=org.freedesktop.Avahi # talk to avahi on the system bus - --talk-name=org.freedesktop.Flatpak # talk to flatpak on the session bus + - --talk-name=org.kde.KWin # assign virtual touch and pen devices to the streamed output - --talk-name=org.kde.StatusNotifierWatcher # talk to system tray on the session bus cleanup: diff --git a/src/input.cpp b/src/input.cpp index 1facd6afd23..4c7917cd5bc 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -759,6 +759,7 @@ namespace input { touch_port.logical_height, touch_port.env_offset_x, touch_port.env_offset_y, + touch_port.output_name, }; } @@ -1378,14 +1379,15 @@ namespace input { } /** - * @brief Normalizes coordinates to monitor-local logical touch dimensions. + * @brief Normalize coordinates within the streamed monitor, excluding client letterboxing. + * * @param touch_port The current touch port metadata. * @param coords The in/out coordinate pair to normalize. * @return The monitor-local touch port, or std::nullopt if dimensions are invalid. */ std::optional monitor_touch_port(const input::touch_port_t &touch_port, std::pair &coords) { - const float monitor_logical_w = (touch_port.width * touch_port.scalar_inv) / touch_port.scalar_tpcoords; - const float monitor_logical_h = (touch_port.height * touch_port.scalar_inv) / touch_port.scalar_tpcoords; + const float monitor_logical_w = ((touch_port.width - 2.0f * touch_port.client_offsetX) * touch_port.scalar_inv) / touch_port.scalar_tpcoords; + const float monitor_logical_h = ((touch_port.height - 2.0f * touch_port.client_offsetY) * touch_port.scalar_inv) / touch_port.scalar_tpcoords; if (monitor_logical_w <= 0.0f || monitor_logical_h <= 0.0f) { BOOST_LOG(warning) << "Ignoring touch/pen input due to invalid logical touch dimensions"sv; return std::nullopt; @@ -1401,6 +1403,9 @@ namespace input { static_cast(monitor_logical_h), static_cast(monitor_logical_w), static_cast(monitor_logical_h), + touch_port.env_offset_x, + touch_port.env_offset_y, + touch_port.output_name, }; } @@ -2472,6 +2477,10 @@ namespace input { gamepadMask.reset(); } + platf::client_input_t *client_context(const std::shared_ptr &input) { + return input->client_context.get(); + } + int alloc_gamepad(std::shared_ptr &input, std::uint8_t client_index, const platf::gamepad_arrival_t &metadata) { return ::input::alloc_gamepad(input, client_index, metadata); } diff --git a/src/input.h b/src/input.h index f181f633611..41e0439aa31 100644 --- a/src/input.h +++ b/src/input.h @@ -93,6 +93,14 @@ namespace input { */ void set_platform_input(platf::input_t input); + /** + * @brief Access a retained stream's platform client devices in a unit test. + * + * @param input Retained input state. + * @return Borrowed platform client input context. + */ + platf::client_input_t *client_context(const std::shared_ptr &input); + /** * @brief Allocate a gamepad directly in retained input state for a unit test. * diff --git a/src/platform/common.h b/src/platform/common.h index 62961d7a310..7351e64cae1 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -395,6 +395,7 @@ namespace platf { 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. + std::string output_name {}; ///< Native connector name used to assign compositor-managed touch and pen devices. }; // These values must match Limelight-internal.h's SS_FF_* constants! @@ -802,6 +803,7 @@ namespace platf { virtual ~display_t() = default; // Offsets for when streaming a specific monitor. By default, they are 0. + std::string output_name; ///< Native connector name of the captured display for absolute input assignment. 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. diff --git a/src/platform/linux/display_geometry.h b/src/platform/linux/display_geometry.h new file mode 100644 index 00000000000..ab38dae2520 --- /dev/null +++ b/src/platform/linux/display_geometry.h @@ -0,0 +1,79 @@ +/** + * @file src/platform/linux/display_geometry.h + * @brief Desktop bounds for absolute input across Linux capture backends. + */ +#pragma once + +// standard includes +#include + +// local includes +#include "src/platform/common.h" + +namespace platf { + /** + * @brief Accumulate monitor extents while preserving a negative desktop origin. + * + * Output offsets are compositor coordinates. Logical dimensions define the + * absolute input desktop; physical dimensions retain capture's pixel extents. + */ + class desktop_geometry_t { + public: + /** + * @brief Include an active monitor in the desktop bounds. + * + * @param viewport Monitor position and physical/logical dimensions. + */ + void add(const touch_port_t &viewport) { + if (viewport.width <= 0 || viewport.height <= 0) { + return; + } + const bool has_logical_size = viewport.logical_width > 0 && viewport.logical_height > 0; + const int logical_width = has_logical_size ? viewport.logical_width : viewport.width; + const int logical_height = has_logical_size ? viewport.logical_height : viewport.height; + if (!initialized_) { + bounds_ = viewport; + bounds_.logical_width = logical_width; + bounds_.logical_height = logical_height; + bounds_.output_name.clear(); + initialized_ = true; + return; + } + const int left = std::min(bounds_.offset_x, viewport.offset_x); + const int top = std::min(bounds_.offset_y, viewport.offset_y); + bounds_.width = std::max(bounds_.offset_x + bounds_.width, viewport.offset_x + viewport.width) - left; + bounds_.height = std::max(bounds_.offset_y + bounds_.height, viewport.offset_y + viewport.height) - top; + bounds_.logical_width = std::max(bounds_.offset_x + bounds_.logical_width, viewport.offset_x + logical_width) - left; + bounds_.logical_height = std::max(bounds_.offset_y + bounds_.logical_height, viewport.offset_y + logical_height) - top; + bounds_.offset_x = left; + bounds_.offset_y = top; + } + + /** + * @brief Get the union of included monitors. + * + * @return Desktop origin and extents, or zero dimensions if no valid monitor was included. + */ + const touch_port_t &bounds() const { + return bounds_; + } + + /** + * @brief Publish the accumulated input desktop on a capture display. + * + * @param display Capture display receiving physical and logical desktop geometry. + */ + void apply(display_t &display) const { + display.env_offset_x = bounds_.offset_x; + display.env_offset_y = bounds_.offset_y; + display.env_width = bounds_.width; + display.env_height = bounds_.height; + display.env_logical_width = bounds_.logical_width; + display.env_logical_height = bounds_.logical_height; + } + + private: + touch_port_t bounds_ {}; ///< Union of all valid monitor extents. + bool initialized_ = false; ///< Whether a valid monitor has established the origin. + }; +} // namespace platf diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 0274f0b755a..a2f6eb50558 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -24,6 +24,7 @@ // local includes #include "cuda.h" +#include "display_geometry.h" #include "graphics.h" #include "kms_plane.h" #include "src/config.h" @@ -343,11 +344,7 @@ namespace platf { */ using conn_type_count_t = std::map; - static int env_width; - static int env_height; - - static int env_logical_width; - static int env_logical_height; + static platf::touch_port_t env_bounds {}; ///< Full desktop origin and extents for absolute input. /** * @brief Convert a DRM plane type value to a diagnostic string. @@ -752,17 +749,15 @@ namespace platf { /** * @brief Return framebuffer metadata for the current CRTC. * - * @param plane DRM or EGL plane index being described. + * @param plane DRM plane whose framebuffer metadata should be read. * @return Framebuffer metadata wrapper, or nullptr when the framebuffer cannot be read. */ - fb_t fb(plane_t::pointer plane) { - auto fb2 = platf::kms::privileged_drm_worker::drmModeGetFB2_privileged(fd.el, plane->fb_id); - if (fb2) { + fb_t fb(const drmModePlane *plane) { + if (auto fb2 = platf::kms::privileged_drm_worker::drmModeGetFB2_privileged(fd.el, plane->fb_id); fb2) { return std::make_unique(fd.el, fb2); } - auto fb = platf::kms::privileged_drm_worker::drmModeGetFB_privileged(fd.el, plane->fb_id); - if (fb) { + if (auto fb = platf::kms::privileged_drm_worker::drmModeGetFB_privileged(fd.el, plane->fb_id); fb) { return std::make_unique(fd.el, fb); } @@ -937,8 +932,7 @@ namespace platf { file_t handleFD(std::uint32_t handle) { file_t fb_fd; - auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el); - if (status) { + if (const auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el); status) { return {}; } @@ -1072,7 +1066,7 @@ namespace platf { /** * @brief Write a debug log representation of the input packet. * - * @param plane DRM or EGL plane index being described. + * @param plane DRM plane whose framebuffer metadata should be read. * @param fb Framebuffer object to bind or update. * @param crtc DRM CRTC identifier to map. */ @@ -1259,11 +1253,13 @@ namespace platf { img_offset_x = crtc->x; img_offset_y = crtc->y; - this->env_width = ::platf::kms::env_width; - this->env_height = ::platf::kms::env_height; + this->env_offset_x = ::platf::kms::env_bounds.offset_x; + this->env_offset_y = ::platf::kms::env_bounds.offset_y; + this->env_width = ::platf::kms::env_bounds.width; + this->env_height = ::platf::kms::env_bounds.height; - this->env_logical_width = ::platf::kms::env_logical_width; - this->env_logical_height = ::platf::kms::env_logical_height; + this->env_logical_width = ::platf::kms::env_bounds.logical_width; + this->env_logical_height = ::platf::kms::env_bounds.logical_height; auto monitor_pos = pos->crtc_to_monitor.find(plane->crtc_id); if (monitor_pos != std::end(pos->crtc_to_monitor)) { @@ -1289,6 +1285,7 @@ namespace platf { offset_x = viewport.offset_x; offset_y = viewport.offset_y; + output_name = viewport.output_name; } // This code path shouldn't happen, but it's there just in case. @@ -2119,12 +2116,14 @@ namespace platf { /** * @brief Capture a display frame into the provided image object. * - * @param pull_free_image_cb Pull free image cb. + * @tparam PullFreeImage Callback type used to retrieve a free capture image. + * @param pull_free_image_cb Callback retrieving a free capture image. * @param img_out Captured KMS image descriptor returned to the streaming pipeline. * @param cursor Cursor image or visibility state to composite. * @return Capture status reported to the streaming pipeline. */ - capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) { + template + capture_e snapshot(const PullFreeImage &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) { std::array fb_fd; if (!pull_free_image_cb(img_out)) { @@ -2133,8 +2132,7 @@ namespace platf { auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); - auto status = refresh(fb_fd.data(), &img->sd, img->frame_timestamp); - if (status != capture_e::ok) { + if (const auto status = refresh(fb_fd.data(), &img->sd, img->frame_timestamp); status != capture_e::ok) { return status; } @@ -2264,6 +2262,7 @@ namespace platf { for (auto &card_descriptor : cds) { for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { if (monitor_descriptor.index == index && monitor_descriptor.type == type) { + monitor_descriptor.viewport.output_name = monitor->name; monitor_descriptor.viewport.offset_x = monitor->viewport.offset_x; monitor_descriptor.viewport.offset_y = monitor->viewport.offset_y; monitor_descriptor.viewport.logical_width = monitor->viewport.logical_width; @@ -2398,11 +2397,9 @@ namespace platf { it->second.monitor_index = count; } - kms::env_width = std::max(kms::env_width, (int) (crtc->x + crtc->width)); - kms::env_height = std::max(kms::env_height, (int) (crtc->y + crtc->height)); - kms::print(plane.get(), fb.get(), crtc.get()); - display_names.emplace_back(std::format("{}-{}", drmModeGetConnectorTypeName(it->second.type), it->second.index)); + it->second.viewport.output_name = std::format("{}-{}", drmModeGetConnectorTypeName(it->second.type), it->second.index); + display_names.emplace_back(it->second.viewport.output_name); count++; } @@ -2416,28 +2413,14 @@ namespace platf { correlate_to_wayland(cds); } - // Deduce the full virtual desktop size - kms::env_width = 0; - kms::env_height = 0; - - kms::env_logical_width = 0; - kms::env_logical_height = 0; - - for (auto &card_descriptor : cds) { - for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { - BOOST_LOG(debug) << "Monitor description"sv; - BOOST_LOG(debug) << "Resolution: "sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height; - BOOST_LOG(debug) << "Offset: "sv << monitor_descriptor.viewport.offset_x << 'x' << monitor_descriptor.viewport.offset_y; - - kms::env_width = std::max(kms::env_width, monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.width); - kms::env_height = std::max(kms::env_height, monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.height); - - kms::env_logical_height = std::max(kms::env_logical_height, monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.logical_height); - kms::env_logical_width = std::max(kms::env_logical_width, monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.logical_width); + platf::desktop_geometry_t desktop; + for (const auto &card_descriptor : cds) { + for (const auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { + desktop.add(monitor_descriptor.viewport); } } - - BOOST_LOG(debug) << "Desktop resolution: "sv << kms::env_width << 'x' << kms::env_height; + kms::env_bounds = desktop.bounds(); + BOOST_LOG(debug) << "Desktop resolution: "sv << kms::env_bounds.width << 'x' << kms::env_bounds.height; kms::card_descriptors = std::move(cds); diff --git a/src/platform/linux/kwingrab.cpp b/src/platform/linux/kwingrab.cpp index 0fcfb507d10..d0f3cbd0786 100644 --- a/src/platform/linux/kwingrab.cpp +++ b/src/platform/linux/kwingrab.cpp @@ -689,6 +689,7 @@ namespace kwin { out_pipewire_node = screencast->out_node_id; out_pipewire_objectserial = screencast->out_objectserial; // Set/update basic stream parameters on display_t + this->output_name = screencast->out_params->name; this->offset_x = screencast->out_params->pos_x; this->offset_y = screencast->out_params->pos_y; this->width = screencast->out_params->width; diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 0715f971a54..69632cb245d 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -16,6 +16,7 @@ // local includes #include "cuda.h" +#include "display_geometry.h" #include "graphics.h" #include "src/main.h" #include "src/platform/common.h" @@ -1010,42 +1011,25 @@ namespace pipewire { virtual int configure_stream(const std::string &display_name, int &out_pipewire_fd, uint32_t &out_pipewire_node, uint64_t &out_pipewire_objectserial) = 0; /** - * @brief Verify and update display parameters for logical dimensions, desktop dimensions and logical desktop dimensions (default is adapted from wlgrab) + * @brief Resolve the streamed connector and full desktop bounds for absolute input. */ virtual void verify_and_update_display_parameters() { - // Query outputs directly using wayland wl::monitors() - if (logical_height <= 0 || logical_width <= 0 || env_logical_height <= 0 || env_logical_width <= 0 || env_height <= 0 || env_width <= 0) { - int desktop_width = 0; - int desktop_height = 0; - int desktop_logical_width = 0; - int desktop_logical_height = 0; - for (const auto &monitor : wl::monitors()) { - BOOST_LOG(debug) << "[pipewire] Found output: '"sv << monitor->name << "' offset: "sv << monitor->viewport.offset_x << 'x' << monitor->viewport.offset_y << " resolution: "sv << monitor->viewport.width << 'x' << monitor->viewport.height << " logical resolution: "sv << monitor->viewport.logical_width << 'x' << monitor->viewport.logical_height; - // If logical_width and logical_height are not valid try to update them to correct values by matching to monitor - // position/dimension or position/logical dimensions here since we're iterating for maximum environment size anyway - if ((logical_width <= 0 || logical_height <= 0) && monitor->viewport.offset_x == offset_x && monitor->viewport.offset_y == offset_y && ((monitor->viewport.width == width && monitor->viewport.height == height) || (monitor->viewport.logical_width == width && monitor->viewport.logical_height == height))) { - this->logical_width = monitor->viewport.logical_width; - this->logical_height = monitor->viewport.logical_height; - BOOST_LOG(debug) << "[pipewire] Set logical resolution: "sv << logical_width << 'x' << logical_height; + platf::desktop_geometry_t desktop; + for (const auto &monitor : wl::monitors()) { + desktop.add(monitor->viewport); + if (monitor->viewport.offset_x == offset_x && monitor->viewport.offset_y == offset_y && ((monitor->viewport.width == width && monitor->viewport.height == height) || (monitor->viewport.logical_width == width && monitor->viewport.logical_height == height))) { + if (logical_width <= 0 || logical_height <= 0) { + logical_width = monitor->viewport.logical_width; + logical_height = monitor->viewport.logical_height; + } + if (output_name.empty()) { + output_name = monitor->name; } - // Update desktop dimensions to setup maximum environment size over all screens - desktop_width = std::max(desktop_width, monitor->viewport.offset_x + monitor->viewport.width); - desktop_height = std::max(desktop_height, monitor->viewport.offset_y + monitor->viewport.height); - // Update desktop logical dimensions to setup maximum logical environment size over all screens - desktop_logical_width = std::max(desktop_logical_width, monitor->viewport.offset_x + monitor->viewport.logical_width); - desktop_logical_height = std::max(desktop_logical_height, monitor->viewport.offset_y + monitor->viewport.logical_height); - } - if (env_height <= 0 || env_width <= 0) { - this->env_width = desktop_width; - this->env_height = desktop_height; - BOOST_LOG(debug) << "[pipewire] Set desktop resolution: "sv << env_width << 'x' << env_height; - } - if (env_logical_height <= 0 || env_logical_width <= 0) { - this->env_logical_width = desktop_logical_width; - this->env_logical_height = desktop_logical_height; - BOOST_LOG(debug) << "[pipewire] Set desktop logical resolution: "sv << env_logical_width << 'x' << env_logical_height; } } + if (desktop.bounds().width > 0 && desktop.bounds().height > 0) { + desktop.apply(*this); + } } /** diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index 6cbc21b9dd2..49ece2b7f11 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -1042,6 +1042,7 @@ namespace portal { out_pipewire_node = stream.pipewire_node; out_pipewire_object_serial = stream.pipewire_object_serial; // Set/update basic stream parameters on display_t + this->output_name = stream.monitor_name; this->offset_x = stream.pos_x; this->offset_y = stream.pos_y; this->width = stream.width; diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index f6fb66b205e..be23ec493be 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -21,6 +21,7 @@ // local includes #include "cuda.h" +#include "display_geometry.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" @@ -31,8 +32,6 @@ using namespace std::literals; namespace wl { - static int env_width; - static int env_height; static bool force_ram_capture = false; // NOSONAR(cpp:S5421): Retains SHM selection across VRAM-to-RAM capture backend recreation. /** @@ -422,22 +421,15 @@ namespace wl { width = monitor->viewport.width; height = monitor->viewport.height; - this->env_width = ::wl::env_width; - this->env_height = ::wl::env_height; - + this->output_name = monitor->name; this->logical_width = monitor->viewport.logical_width; this->logical_height = monitor->viewport.logical_height; - int desktop_logical_width = 0; - int desktop_logical_height = 0; - for (auto &monitor_entry : interface.monitors) { - auto output_monitor = monitor_entry.get(); - desktop_logical_width = std::max(desktop_logical_width, output_monitor->viewport.offset_x + output_monitor->viewport.logical_width); - desktop_logical_height = std::max(desktop_logical_height, output_monitor->viewport.offset_y + output_monitor->viewport.logical_height); + platf::desktop_geometry_t desktop; + for (const auto &monitor_entry : interface.monitors) { + desktop.add(monitor_entry->viewport); } - - this->env_logical_width = desktop_logical_width; - this->env_logical_height = desktop_logical_height; + desktop.apply(*this); BOOST_LOG(info) << "[wlgrab] Selected monitor ["sv << monitor->description << "] for streaming"sv; BOOST_LOG(debug) << "[wlgrab] Offset: "sv << offset_x << 'x' << offset_y; @@ -953,9 +945,6 @@ namespace platf { return {}; } - wl::env_width = 0; - wl::env_height = 0; - for (auto &monitor : interface.monitors) { monitor->listen(interface.output_manager); } @@ -967,9 +956,6 @@ namespace platf { for (int x = 0; x < interface.monitors.size(); ++x) { auto monitor = interface.monitors[x].get(); - wl::env_width = std::max(wl::env_width, monitor->viewport.offset_x + monitor->viewport.width); - wl::env_height = std::max(wl::env_height, monitor->viewport.offset_y + monitor->viewport.height); - BOOST_LOG(info) << "[wlgrab] Monitor " << x << " is "sv << monitor->name << ": "sv << monitor->description; display_names.emplace_back(monitor->name.empty() ? std::to_string(x) : monitor->name); diff --git a/src/platform/virtualhid_input.cpp b/src/platform/virtualhid_input.cpp index 7e255df5231..5eeb4647f92 100644 --- a/src/platform/virtualhid_input.cpp +++ b/src/platform/virtualhid_input.cpp @@ -367,6 +367,7 @@ namespace platf::virtualhid { .offset_y = touch_port.offset_y, .width = touch_port.width, .height = touch_port.height, + .output_name = touch_port.output_name, }; } diff --git a/src/video.cpp b/src/video.cpp index 13ece17213d..5c7d324c53c 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -2515,13 +2515,6 @@ namespace video { } } - /** - * @brief Create a port object or message. - * - * @param display Display object or identifier associated with the operation. - * @param config Configuration values to apply. - * @return Constructed port object. - */ input::touch_port_t make_port(platf::display_t *display, const config_t &config) { float wd = display->width; float hd = display->height; @@ -2559,6 +2552,7 @@ namespace video { display->logical_height, display->env_offset_x, display->env_offset_y, + display->output_name, }, display->env_width, display->env_height, diff --git a/src/video.h b/src/video.h index 2092fcacf0c..f40040c9414 100644 --- a/src/video.h +++ b/src/video.h @@ -45,6 +45,15 @@ namespace video { int enableIntraRefresh; ///< Intra refresh setting: 0 = disabled, 1 = enabled. }; + /** + * @brief Describe the streamed display, desktop bounds, and client letterboxing for absolute input. + * + * @param display Capture display supplying native output identity and geometry. + * @param config Client stream dimensions. + * @return Touch port forwarded from the video pipeline to input handling. + */ + input::touch_port_t make_port(platf::display_t *display, const config_t &config); + namespace amf { /** diff --git a/tests/unit/platform/linux/test_display_geometry.cpp b/tests/unit/platform/linux/test_display_geometry.cpp new file mode 100644 index 00000000000..202f5d7a3e2 --- /dev/null +++ b/tests/unit/platform/linux/test_display_geometry.cpp @@ -0,0 +1,140 @@ +/** + * @file tests/unit/platform/linux/test_display_geometry.cpp + * @brief Verify Linux input desktop bounds and streamed output metadata. + */ + +// test includes +#include "../../../tests_common.h" + +// standard includes +#include +#include + +// local includes +#include "src/platform/linux/display_geometry.h" +#include "src/video.h" + +namespace { + /** + * @brief Geometry-only capture display, without installed graphics devices. + */ + class geometry_display_t: public platf::display_t { + public: + /** + * @brief Complete without capturing frames. + * @return Successful capture status. + */ + platf::capture_e capture(const push_captured_image_cb_t &, const pull_free_image_cb_t &, bool *) override { + return platf::capture_e::ok; + } + + /** + * @brief Return no image for a geometry-only display. + * @return Null image. + */ + std::shared_ptr alloc_img() override { + return nullptr; + } + + /** + * @brief Complete without populating an image. + * @return Success. + */ + int dummy_img(platf::img_t *) override { + return 0; + } + }; +} // namespace + +TEST(LinuxDisplayGeometryTest, EmptyAndInvalidMonitorsDoNotCreateAnInputDesktop) { + platf::desktop_geometry_t desktop; + EXPECT_EQ(desktop.bounds().width, 0); + EXPECT_EQ(desktop.bounds().height, 0); + desktop.add({.width = 0, .height = 1080}); + desktop.add({.width = 1920, .height = -1}); + EXPECT_EQ(desktop.bounds().width, 0); + EXPECT_EQ(desktop.bounds().logical_width, 0); +} + +TEST(LinuxDisplayGeometryTest, PreservesTheFirstMonitorOriginAndFallsBackToPhysicalDimensions) { + platf::desktop_geometry_t desktop; + desktop.add({.offset_x = 800, .offset_y = 300, .width = 1920, .height = 1080, .output_name = "DP-1"}); + const auto &bounds = desktop.bounds(); + EXPECT_EQ(bounds.offset_x, 800); + EXPECT_EQ(bounds.offset_y, 300); + EXPECT_EQ(bounds.width, 1920); + EXPECT_EQ(bounds.height, 1080); + EXPECT_EQ(bounds.logical_width, 1920); + EXPECT_EQ(bounds.logical_height, 1080); + EXPECT_TRUE(bounds.output_name.empty()); + desktop.add({.width = -1, .height = 1080}); + EXPECT_EQ(bounds.width, 1920); +} + +TEST(LinuxDisplayGeometryTest, IncludesMonitorsLeftOfAndAboveTheOriginInEitherOrder) { + std::array monitors { + platf::touch_port_t {.offset_x = -1920, .offset_y = -1080, .width = 1920, .height = 1080, .logical_width = 1920, .logical_height = 1080}, + platf::touch_port_t {.offset_x = 0, .offset_y = 0, .width = 1920, .height = 1080, .logical_width = 1920, .logical_height = 1080}, + }; + for (int order = 0; order < 2; ++order) { + platf::desktop_geometry_t desktop; + for (const auto &monitor : monitors) { + desktop.add(monitor); + } + const auto &bounds = desktop.bounds(); + EXPECT_EQ(bounds.offset_x, -1920); + EXPECT_EQ(bounds.offset_y, -1080); + EXPECT_EQ(bounds.width, 3840); + EXPECT_EQ(bounds.height, 2160); + EXPECT_EQ(bounds.logical_width, 3840); + EXPECT_EQ(bounds.logical_height, 2160); + std::ranges::reverse(monitors); + } +} + +TEST(LinuxDisplayGeometryTest, UsesLogicalSizesForMixedScalingAndPreservesGaps) { + platf::desktop_geometry_t desktop; + desktop.add({.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 2160, .logical_width = 1920, .logical_height = 1080}); + desktop.add({.offset_x = 300, .offset_y = -720, .width = 2560, .height = 1440, .logical_width = 1280, .logical_height = 720}); + EXPECT_EQ(desktop.bounds().offset_x, -1920); + EXPECT_EQ(desktop.bounds().offset_y, -720); + EXPECT_EQ(desktop.bounds().logical_width, 3500); + EXPECT_EQ(desktop.bounds().logical_height, 1800); + desktop.add({.offset_x = 1580, .offset_y = 0, .width = 640, .height = 480, .logical_width = 640, .logical_height = 0}); + EXPECT_EQ(desktop.bounds().logical_width, 4140); +} + +TEST(LinuxDisplayGeometryTest, PublishesDesktopOriginAndStreamedConnectorThroughTheTouchPort) { + geometry_display_t display; + display.width = 3840; + display.height = 2160; + display.logical_width = 1920; + display.logical_height = 1080; + display.offset_x = -1920; + display.offset_y = -1080; + display.output_name = "DP-1"; + platf::desktop_geometry_t desktop; + desktop.add({.offset_x = -1920, .offset_y = -1080, .width = 3840, .height = 2160, .logical_width = 1920, .logical_height = 1080}); + desktop.add({.width = 1920, .height = 1080, .logical_width = 1920, .logical_height = 1080}); + desktop.apply(display); + EXPECT_EQ(display.env_offset_x, -1920); + EXPECT_EQ(display.env_offset_y, -1080); + EXPECT_EQ(display.env_width, 3840); + EXPECT_EQ(display.env_height, 2160); + EXPECT_EQ(display.env_logical_width, 3840); + EXPECT_EQ(display.env_logical_height, 2160); + video::config_t config {}; + config.width = 2560; + config.height = 1080; + const auto port = video::make_port(&display, config); + EXPECT_EQ(port.output_name, "DP-1"); + EXPECT_EQ(port.offset_x, -1920); + EXPECT_EQ(port.offset_y, -1080); + EXPECT_EQ(port.env_offset_x, -1920); + EXPECT_EQ(port.env_offset_y, -1080); + EXPECT_EQ(port.env_logical_width, 3840); + EXPECT_EQ(port.env_logical_height, 2160); + EXPECT_FLOAT_EQ(port.scalar_inv, 2.0F); + EXPECT_FLOAT_EQ(port.scalar_tpcoords, 2.0F); + EXPECT_FLOAT_EQ(port.client_offsetX, 320.0F); +} diff --git a/tests/unit/platform/test_virtualhid_input.cpp b/tests/unit/platform/test_virtualhid_input.cpp index d143a7e926e..b1ec943563f 100644 --- a/tests/unit/platform/test_virtualhid_input.cpp +++ b/tests/unit/platform/test_virtualhid_input.cpp @@ -18,15 +18,16 @@ #include #include +#if defined(_WIN32) || defined(__APPLE__) + // lib includes + #include +#endif + // local includes #include "src/config.h" #include "src/globals.h" #include "src/platform/virtualhid_input.h" -#if defined(_WIN32) || defined(__APPLE__) - #include -#endif - using namespace std::chrono_literals; using namespace std::literals; @@ -885,7 +886,7 @@ TEST_F(VirtualHidDeviceTest, RetargetsRelativeMouseWhenStreamedViewportChanges) } TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) { - const platf::touch_port_t viewport {10, 20, 800, 600, 400, 300}; + const platf::touch_port_t viewport {10, 20, 800, 600, 400, 300, 0, 0, "DP-1"}; platf::touch_input_t touch {LI_TOUCH_EVENT_DOWN, 100, 7, -0.1F, 1.1F, 1.2F, 15.0F, 10.0F}; platf::virtualhid::touch_update(*client(), viewport, touch); auto contact = client()->touch->last_submitted_contact(); @@ -896,6 +897,7 @@ TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) { EXPECT_EQ(contact.orientation, 80); EXPECT_TRUE(contact.touching); EXPECT_EQ(contact.viewport.offset_x, 10); + EXPECT_EQ(contact.viewport.output_name, "DP-1"); EXPECT_EQ(contact.viewport.offset_y, 20); EXPECT_EQ(contact.viewport.width, 800); EXPECT_EQ(contact.viewport.height, 600); @@ -953,7 +955,7 @@ TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) { } TEST_F(VirtualHidDeviceTest, TranslatesPenButtonsToolsAndTransitions) { - const platf::touch_port_t viewport {1, 2, 1000, 500, 1000, 500}; + const platf::touch_port_t viewport {1, 2, 1000, 500, 1000, 500, 0, 0, "HDMI-A-1"}; platf::pen_input_t pen { LI_TOUCH_EVENT_DOWN, LI_TOOL_TYPE_PEN, @@ -978,6 +980,7 @@ TEST_F(VirtualHidDeviceTest, TranslatesPenButtonsToolsAndTransitions) { EXPECT_EQ(state.transition, lvh::PointerTransition::update); EXPECT_EQ(state.viewport.offset_x, 1); EXPECT_EQ(state.viewport.offset_y, 2); + EXPECT_EQ(state.viewport.output_name, "HDMI-A-1"); EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::primary)); EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::secondary)); diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp index 4913dd7b198..c7a170c9b4f 100644 --- a/tests/unit/test_input.cpp +++ b/tests/unit/test_input.cpp @@ -9,6 +9,7 @@ // standard includes #include #include +#include #include #include #include @@ -19,7 +20,7 @@ #include #include -// moonlight-common-c includes +// lib includes extern "C" { #include } @@ -642,3 +643,115 @@ TEST_F(InputGamepadSessionTest, RetargetsRelativeMouseFromLatestStreamGeometry) EXPECT_EQ(mouse_event.y, 9); EXPECT_EQ(context().mouse_viewport.offset_x, 0); } + +TEST_F(InputGamepadSessionTest, RetargetsNativeTouchAndPenWithoutApplyingTheMonitorOffsetTwice) { + auto session_mail = std::make_shared(); + auto stream_input = input::alloc(session_mail, "absolute-input-client"); + ASSERT_NE(stream_input, nullptr); + auto &client = platf::virtualhid::get_client_context(input::testing::client_context(stream_input)); + ASSERT_NE(client.touch, nullptr); + ASSERT_NE(client.pen, nullptr); + input::testing::set_input_task_sink([](std::shared_ptr) { + }); + input::touch_port_t viewport { + {.offset_x = -1920, .offset_y = -1080, .width = 2560, .height = 1080, .logical_width = 1920, .logical_height = 1080, .env_offset_x = -1920, .env_offset_y = -1080, .output_name = "DP-1"}, + 7680, + 4320, + 320.0F, + 0.0F, + 2.0F, + 2.0F, + 3840, + 2160, + }; + const auto write_float = [](netfloat &target, float value) { + const auto bits = util::endian::little(std::bit_cast(value)); + std::memcpy(target, &bits, sizeof(bits)); + }; + const auto send_packet = [&](Packet &packet, std::uint32_t magic) { + packet.header.size = util::endian::big(sizeof(packet) - sizeof(packet.header.size)); + packet.header.magic = util::endian::little(magic); + std::vector bytes(sizeof(packet)); + std::memcpy(bytes.data(), &packet, sizeof(packet)); + input::passthrough(stream_input, std::move(bytes)); + static_cast(input::testing::process_queued_messages(stream_input)); + }; + SS_TOUCH_PACKET touch {}; + touch.eventType = LI_TOUCH_EVENT_DOWN; + touch.pointerId = util::endian::little(42); + write_float(touch.x, 0.5F); + write_float(touch.y, 0.25F); + write_float(touch.pressureOrDistance, 0.75F); + SS_PEN_PACKET pen {}; + pen.eventType = LI_TOUCH_EVENT_DOWN; + pen.toolType = LI_TOOL_TYPE_PEN; + write_float(pen.x, 0.5F); + write_float(pen.y, 0.25F); + write_float(pen.pressureOrDistance, 0.75F); + for (const auto *name : {"DP-1", "HDMI-A-1"}) { + viewport.output_name = name; + session_mail->event(mail::touch_port)->raise(viewport); + send_packet(touch, SS_TOUCH_MAGIC); + const auto contact = client.touch->last_submitted_contact(); + EXPECT_EQ(contact.viewport.output_name, name); + EXPECT_EQ(contact.viewport.offset_x, viewport.offset_x); + EXPECT_EQ(contact.viewport.offset_y, viewport.offset_y); + EXPECT_EQ(contact.viewport.width, 1920); + EXPECT_EQ(contact.viewport.height, 1080); + EXPECT_FLOAT_EQ(contact.x, 0.5F); + EXPECT_FLOAT_EQ(contact.y, 0.25F); + send_packet(pen, SS_PEN_MAGIC); + const auto tool = client.pen->last_submitted_tool(); + EXPECT_EQ(tool.viewport.output_name, name); + EXPECT_EQ(tool.viewport.offset_x, viewport.offset_x); + EXPECT_EQ(tool.viewport.offset_y, viewport.offset_y); + EXPECT_FLOAT_EQ(tool.x, 0.5F); + EXPECT_FLOAT_EQ(tool.y, 0.25F); + viewport.offset_x = 0; + viewport.offset_y = 0; + touch.eventType = LI_TOUCH_EVENT_MOVE; + pen.eventType = LI_TOUCH_EVENT_MOVE; + } + // A capture backend can report monitor scaling before the desktop bounds are available. + viewport.scalar_tpcoords = 1.0F; + viewport.env_logical_width = 0; + viewport.env_logical_height = 0; + session_mail->event(mail::touch_port)->raise(viewport); + send_packet(touch, SS_TOUCH_MAGIC); + send_packet(pen, SS_PEN_MAGIC); + EXPECT_FLOAT_EQ(client.touch->last_submitted_contact().x, 0.5F); + EXPECT_FLOAT_EQ(client.touch->last_submitted_contact().y, 0.25F); + EXPECT_FLOAT_EQ(client.pen->last_submitted_tool().x, 0.5F); + EXPECT_FLOAT_EQ(client.pen->last_submitted_tool().y, 0.25F); + viewport.scalar_tpcoords = 2.0F; + viewport.logical_width = 0; + viewport.logical_height = 0; + session_mail->event(mail::touch_port)->raise(viewport); + send_packet(touch, SS_TOUCH_MAGIC); + send_packet(pen, SS_PEN_MAGIC); + EXPECT_EQ(client.touch->last_submitted_contact().viewport.width, 1920); + EXPECT_EQ(client.pen->last_submitted_tool().viewport.width, 1920); + EXPECT_FLOAT_EQ(client.touch->last_submitted_contact().x, 0.5F); + EXPECT_FLOAT_EQ(client.pen->last_submitted_tool().x, 0.5F); + viewport.logical_width = 1920; + session_mail->event(mail::touch_port)->raise(viewport); + send_packet(touch, SS_TOUCH_MAGIC); + EXPECT_EQ(client.touch->last_submitted_contact().viewport.width, 1920); + + const auto touch_count = client.touch->submit_count(); + const auto pen_count = client.pen->submit_count(); + viewport.logical_width = 0; + viewport.client_offsetX = viewport.width / 2.0F; + session_mail->event(mail::touch_port)->raise(viewport); + send_packet(touch, SS_TOUCH_MAGIC); + send_packet(pen, SS_PEN_MAGIC); + EXPECT_EQ(client.touch->submit_count(), touch_count); + EXPECT_EQ(client.pen->submit_count(), pen_count); + viewport.client_offsetX = 0; + viewport.client_offsetY = viewport.height / 2.0F; + session_mail->event(mail::touch_port)->raise(viewport); + send_packet(touch, SS_TOUCH_MAGIC); + send_packet(pen, SS_PEN_MAGIC); + EXPECT_EQ(client.touch->submit_count(), touch_count); + EXPECT_EQ(client.pen->submit_count(), pen_count); +} diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index 9f4cac3fff7..86da2c2d32a 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit 9f4cac3fff75430c028ef8bb8ddc4459968949f8 +Subproject commit 86da2c2d32af6a36146f2ad406578bca5a23643c