Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
1 change: 1 addition & 0 deletions packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 12 additions & 3 deletions src/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ namespace input {
touch_port.logical_height,
touch_port.env_offset_x,
touch_port.env_offset_y,
touch_port.output_name,
};
}

Expand Down Expand Up @@ -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<platf::touch_port_t> monitor_touch_port(const input::touch_port_t &touch_port, std::pair<float, float> &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;
Expand All @@ -1401,6 +1403,9 @@ namespace input {
static_cast<int>(monitor_logical_h),
static_cast<int>(monitor_logical_w),
static_cast<int>(monitor_logical_h),
touch_port.env_offset_x,
touch_port.env_offset_y,
touch_port.output_name,
};
}

Expand Down Expand Up @@ -2472,6 +2477,10 @@ namespace input {
gamepadMask.reset();
}

platf::client_input_t *client_context(const std::shared_ptr<input_t> &input) {
return input->client_context.get();
}

int alloc_gamepad(std::shared_ptr<input_t> &input, std::uint8_t client_index, const platf::gamepad_arrival_t &metadata) {
return ::input::alloc_gamepad(input, client_index, metadata);
}
Expand Down
8 changes: 8 additions & 0 deletions src/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_t> &input);

/**
* @brief Allocate a gamepad directly in retained input state for a unit test.
*
Expand Down
2 changes: 2 additions & 0 deletions src/platform/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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.
Expand Down
79 changes: 79 additions & 0 deletions src/platform/linux/display_geometry.h
Original file line number Diff line number Diff line change
@@ -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 <algorithm>

// 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
75 changes: 29 additions & 46 deletions src/platform/linux/kmsgrab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

// local includes
#include "cuda.h"
#include "display_geometry.h"
#include "graphics.h"
#include "kms_plane.h"
#include "src/config.h"
Expand Down Expand Up @@ -343,11 +344,7 @@ namespace platf {
*/
using conn_type_count_t = std::map<std::uint32_t, std::uint32_t>;

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.
Expand Down Expand Up @@ -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<wrapper_fb>(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<wrapper_fb>(fd.el, fb);
}

Expand Down Expand Up @@ -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 {};
}

Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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)) {
Expand All @@ -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.
Expand Down Expand Up @@ -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<platf::img_t> &img_out, std::chrono::milliseconds /* timeout */, bool cursor) {
template<typename PullFreeImage>
capture_e snapshot(const PullFreeImage &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds /* timeout */, bool cursor) {
std::array<file_t, 4> fb_fd;

if (!pull_free_image_cb(img_out)) {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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++;
}

Expand All @@ -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);

Expand Down
1 change: 1 addition & 0 deletions src/platform/linux/kwingrab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading