Skip to content
Open
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
4 changes: 3 additions & 1 deletion cmake/compile_definitions/linux.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ if(WAYLAND_FOUND)
GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/xdg-output" xdg-output-unstable-v1)
GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/linux-dmabuf" linux-dmabuf-unstable-v1)
GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/wlr-protocols" "unstable" wlr-screencopy-unstable-v1)
GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/wlr-protocols" "unstable" wlr-virtual-pointer-unstable-v1)

include_directories(
SYSTEM
Expand All @@ -217,7 +218,8 @@ if(WAYLAND_FOUND)
list(APPEND PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/linux/wlgrab.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.cpp")
"${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/input/wl_mouse.cpp")
endif()

# x11
Expand Down
2 changes: 2 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ namespace config {
true, // always send scancodes
true, // high resolution scrolling
true, // native pen/touch support
false, // wlr_virtual_mouse (use uinput by default)
};

sunshine_t sunshine {
Expand Down Expand Up @@ -1297,6 +1298,7 @@ namespace config {

bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling);
bool_f(vars, "native_pen_touch", input.native_pen_touch);
bool_f(vars, "wlr_virtual_mouse", input.wlr_virtual_mouse);

bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases);
bool_f(vars, "system_tray", sunshine.system_tray);
Expand Down
2 changes: 2 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ namespace config {

bool high_resolution_scrolling;
bool native_pen_touch;

bool wlr_virtual_mouse; ///< Linux/Wayland only: use wlr-virtual-pointer protocol instead of uinput
};

namespace flag {
Expand Down
19 changes: 18 additions & 1 deletion src/platform/linux/input/inputtino_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/utility.h"
#include "wl_mouse.h"

using namespace std::literals;

Expand Down Expand Up @@ -48,14 +49,30 @@ namespace platf {
if (!keyboard) {
BOOST_LOG(warning) << "Unable to create virtual keyboard: " << keyboard.getErrorMessage();
}
#ifdef SUNSHINE_BUILD_WAYLAND
if (config::input.wlr_virtual_mouse) {
if (!wl_mouse.init()) {
BOOST_LOG(error) << "Failed to initialize wlr-virtual-pointer; "
<< "check WAYLAND_DISPLAY and compositor support";
}
}
#endif
}

~input_raw_t() = default;
~input_raw_t() {
#ifdef SUNSHINE_BUILD_WAYLAND
wl_mouse.destroy();
#endif
}

// All devices are wrapped in Result because it might be that we aren't able to create them (ex: udev permission denied)
inputtino::Result<inputtino::Mouse> mouse;
inputtino::Result<inputtino::Keyboard> keyboard;

#ifdef SUNSHINE_BUILD_WAYLAND
platf::wl_mouse::state_t wl_mouse;
#endif

/**
* A list of gamepads that are currently connected.
* The pointer is shared because that state will be shared with background threads that deal with rumble and LED
Expand Down
31 changes: 31 additions & 0 deletions src/platform/linux/input/inputtino_mouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,43 @@
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/utility.h"
#include "wl_mouse.h"

using namespace std::literals;

namespace platf::mouse {

void move(input_raw_t *raw, int deltaX, int deltaY) {
#ifdef SUNSHINE_BUILD_WAYLAND
if (config::input.wlr_virtual_mouse && raw->wl_mouse.pointer) {
platf::wl_mouse::move(&raw->wl_mouse, deltaX, deltaY);
return;
}
#endif
if (raw->mouse) {
(*raw->mouse).move(deltaX, deltaY);
}
}

void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) {
#ifdef SUNSHINE_BUILD_WAYLAND
if (config::input.wlr_virtual_mouse && raw->wl_mouse.pointer) {
platf::wl_mouse::move_abs(&raw->wl_mouse, x, y, touch_port.width, touch_port.height);
return;
}
#endif
if (raw->mouse) {
(*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height);
}
}

void button(input_raw_t *raw, int button, bool release) {
#ifdef SUNSHINE_BUILD_WAYLAND
if (config::input.wlr_virtual_mouse && raw->wl_mouse.pointer) {
platf::wl_mouse::button(&raw->wl_mouse, button, release);
return;
}
#endif
if (raw->mouse) {
inputtino::Mouse::MOUSE_BUTTON btn_type;
switch (button) {
Expand Down Expand Up @@ -63,12 +82,24 @@ namespace platf::mouse {
}

void scroll(input_raw_t *raw, int high_res_distance) {
#ifdef SUNSHINE_BUILD_WAYLAND
if (config::input.wlr_virtual_mouse && raw->wl_mouse.pointer) {
platf::wl_mouse::scroll(&raw->wl_mouse, high_res_distance);
return;
}
#endif
if (raw->mouse) {
(*raw->mouse).vertical_scroll(high_res_distance);
}
}

void hscroll(input_raw_t *raw, int high_res_distance) {
#ifdef SUNSHINE_BUILD_WAYLAND
if (config::input.wlr_virtual_mouse && raw->wl_mouse.pointer) {
platf::wl_mouse::hscroll(&raw->wl_mouse, high_res_distance);
return;
}
#endif
if (raw->mouse) {
(*raw->mouse).horizontal_scroll(high_res_distance);
}
Expand Down
239 changes: 239 additions & 0 deletions src/platform/linux/input/wl_mouse.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/**
* @file src/platform/linux/input/wl_mouse.cpp
* @brief Definitions for wlr-virtual-pointer mouse input handling.
*
* Implements mouse movement, button, and scroll injection using the
* wlr-virtual-pointer-unstable-v1 Wayland protocol. Only compiled when
* SUNSHINE_BUILD_WAYLAND is defined (i.e. Wayland dev libraries are present).
*/

#ifdef SUNSHINE_BUILD_WAYLAND

// standard includes
#include <ctime>

// lib includes
#include <wayland-client.h>

// generated protocol bindings (produced by wayland-scanner at cmake configure time)
#include <wlr-virtual-pointer-unstable-v1.h>

// local includes
#include "src/config.h"
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/utility.h"
#include "wl_mouse.h"

namespace platf::wl_mouse {

namespace {

/**
* Return a monotonic timestamp in milliseconds, as required by Wayland
* input event timestamps.
*/
uint32_t
now_ms() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint32_t) (ts.tv_sec * 1000 + ts.tv_nsec / 1'000'000);
}

// -------------------------------------------------------------------------
// Registry listener — binds zwlr_virtual_pointer_manager_v1 and wl_output
// -------------------------------------------------------------------------

void
registry_global(void *data, wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {

Check failure on line 48 in src/platform/linux/input/wl_mouse.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this use of "void *" with a more meaningful type.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ16vhIcTiMI7OSQQarp&open=AZ16vhIcTiMI7OSQQarp&pullRequest=4972
auto *state = reinterpret_cast<state_t *>(data);

Check warning on line 49 in src/platform/linux/input/wl_mouse.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a "static_cast".

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ16vhIcTiMI7OSQQarm&open=AZ16vhIcTiMI7OSQQarm&pullRequest=4972
if (strcmp(interface, zwlr_virtual_pointer_manager_v1_interface.name) == 0) {
uint32_t bind_version = std::min(version, (uint32_t) 2);
state->manager = reinterpret_cast<zwlr_virtual_pointer_manager_v1 *>(

Check warning on line 52 in src/platform/linux/input/wl_mouse.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ16vhIcTiMI7OSQQarn&open=AZ16vhIcTiMI7OSQQarn&pullRequest=4972
wl_registry_bind(registry, name, &zwlr_virtual_pointer_manager_v1_interface, bind_version));
}
else if (strcmp(interface, "wl_output") == 0) {
// Collect outputs in announcement order — same ordering wlgrab uses for interface.monitors,
// so index N here corresponds to the same physical output as display_name "N" in capture config.
auto *out = reinterpret_cast<wl_output *>(

Check warning on line 58 in src/platform/linux/input/wl_mouse.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a "static_cast".

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ16vhIcTiMI7OSQQaro&open=AZ16vhIcTiMI7OSQQaro&pullRequest=4972
wl_registry_bind(registry, name, &wl_output_interface, 1));
state->outputs.push_back(out);
}
}

void
registry_global_remove(void * /*data*/, wl_registry * /*registry*/, uint32_t /*name*/) {

Check failure on line 65 in src/platform/linux/input/wl_mouse.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this use of "void *" with a more meaningful type.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ16vhIcTiMI7OSQQarq&open=AZ16vhIcTiMI7OSQQarq&pullRequest=4972
// nothing to do
}

constexpr wl_registry_listener registry_listener = {
.global = registry_global,
.global_remove = registry_global_remove,
};

} // anonymous namespace

// ---------------------------------------------------------------------------
// state_t lifecycle
// ---------------------------------------------------------------------------

bool
state_t::init() {
display = wl_display_connect(nullptr);
if (!display) {
BOOST_LOG(error) << "wl_mouse: failed to connect to Wayland display (is WAYLAND_DISPLAY set?)";
return false;
}

registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, this);
wl_display_roundtrip(display); // discover globals

if (!manager) {
BOOST_LOG(error) << "wl_mouse: compositor does not support zwlr_virtual_pointer_manager_v1; "
<< "ensure you are running a wlroots-based compositor (sway, Hyprland, labwc, …)";
destroy();
return false;
}

// Select the output that matches the configured capture monitor (same index logic as wlgrab).
// Binding the pointer to an output makes motion_absolute coordinates relative to that
// output's local frame instead of the full global compositor space, which prevents
// absolute inputs from mapping across multiple monitors.
int monitor_idx = 0;
if (!config::video.capture.empty()) {
auto idx = util::from_view(config::video.capture);
if (idx >= 0 && idx < (int) outputs.size()) {
monitor_idx = (int) idx;
}
}

if (!outputs.empty()) {
bound_output = outputs[monitor_idx];
pointer = zwlr_virtual_pointer_manager_v1_create_virtual_pointer_with_output(
manager, nullptr, bound_output);
BOOST_LOG(info) << "wl_mouse: virtual pointer bound to output index " << monitor_idx
<< " (matches capture display)";
}
else {
BOOST_LOG(warning) << "wl_mouse: no wl_output globals found; "
<< "absolute coordinates will use global compositor space";
pointer = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr);
}

wl_display_roundtrip(display); // finish device creation
return true;
}

void
state_t::destroy() {
if (pointer) {
zwlr_virtual_pointer_v1_destroy(pointer);
pointer = nullptr;
}
if (manager) {
zwlr_virtual_pointer_manager_v1_destroy(manager);
manager = nullptr;
}
for (auto *out : outputs) {
wl_output_destroy(out);
}
outputs.clear();
bound_output = nullptr;
if (registry) {
wl_registry_destroy(registry);
registry = nullptr;
}
if (display) {
wl_display_disconnect(display);
display = nullptr;
}
}

// ---------------------------------------------------------------------------
// Mouse operations
// ---------------------------------------------------------------------------

void
move(state_t *state, int deltaX, int deltaY) {
zwlr_virtual_pointer_v1_motion(state->pointer, now_ms(),
wl_fixed_from_int(deltaX), wl_fixed_from_int(deltaY));
zwlr_virtual_pointer_v1_frame(state->pointer);
wl_display_flush(state->display);
}

void
move_abs(state_t *state, float x, float y, uint32_t width, uint32_t height) {
// Protocol expects unsigned fixed-point coordinates in the range [0, extent)
zwlr_virtual_pointer_v1_motion_absolute(state->pointer, now_ms(),
(uint32_t) x, (uint32_t) y, width, height);
zwlr_virtual_pointer_v1_frame(state->pointer);
wl_display_flush(state->display);
}

void
button(state_t *state, int btn, bool release) {
// Map Sunshine button constants to Linux evdev BTN_* codes
uint32_t code;
switch (btn) {
case BUTTON_LEFT:
code = 0x110; // BTN_LEFT
break;
case BUTTON_RIGHT:
code = 0x111; // BTN_RIGHT
break;
case BUTTON_MIDDLE:
code = 0x112; // BTN_MIDDLE
break;
case BUTTON_X1:
code = 0x113; // BTN_SIDE
break;
case BUTTON_X2:
code = 0x114; // BTN_EXTRA
break;
default:
BOOST_LOG(warning) << "wl_mouse: unknown button: " << btn;
return;
}

uint32_t state_val = release ? WL_POINTER_BUTTON_STATE_RELEASED : WL_POINTER_BUTTON_STATE_PRESSED;
zwlr_virtual_pointer_v1_button(state->pointer, now_ms(), code, state_val);
zwlr_virtual_pointer_v1_frame(state->pointer);
wl_display_flush(state->display);
}

void
scroll(state_t *state, int high_res_distance) {
// Sunshine uses 120 units per detent (same as Windows HID).
// Convert to a pixel-like wl_fixed_t value; 15 px per detent is a common desktop default.
wl_fixed_t value = wl_fixed_from_double(high_res_distance / 120.0 * 15.0);
int32_t discrete = high_res_distance / 120; // whole detents

zwlr_virtual_pointer_v1_axis_source(state->pointer, WL_POINTER_AXIS_SOURCE_WHEEL);
zwlr_virtual_pointer_v1_axis(state->pointer, now_ms(), WL_POINTER_AXIS_VERTICAL_SCROLL, value);
if (discrete != 0) {
zwlr_virtual_pointer_v1_axis_discrete(state->pointer, now_ms(),
WL_POINTER_AXIS_VERTICAL_SCROLL, value, discrete);
}
zwlr_virtual_pointer_v1_frame(state->pointer);
wl_display_flush(state->display);
}

void
hscroll(state_t *state, int high_res_distance) {
wl_fixed_t value = wl_fixed_from_double(high_res_distance / 120.0 * 15.0);
int32_t discrete = high_res_distance / 120;

zwlr_virtual_pointer_v1_axis_source(state->pointer, WL_POINTER_AXIS_SOURCE_WHEEL);
zwlr_virtual_pointer_v1_axis(state->pointer, now_ms(), WL_POINTER_AXIS_HORIZONTAL_SCROLL, value);
if (discrete != 0) {
zwlr_virtual_pointer_v1_axis_discrete(state->pointer, now_ms(),
WL_POINTER_AXIS_HORIZONTAL_SCROLL, value, discrete);
}
zwlr_virtual_pointer_v1_frame(state->pointer);
wl_display_flush(state->display);
}

} // namespace platf::wl_mouse

#endif // SUNSHINE_BUILD_WAYLAND
Loading