From 01d03fb44e18d13bebd4d2fd933aec2d9bbe9e9c Mon Sep 17 00:00:00 2001 From: Willian Barreto Date: Mon, 6 Apr 2026 10:59:08 +0000 Subject: [PATCH 1/2] feat(input): implement multi-seat support and virtual device naming --- docs/getting_started.md | 42 +++++++++++++++++++ src/platform/linux/input/inputtino_common.h | 24 +++++++++-- .../linux/input/inputtino_gamepad.cpp | 7 ++-- src/platform/linux/input/inputtino_seat.cpp | 22 ++++++++++ src/platform/linux/input/inputtino_seat.h | 17 ++++++++ src_assets/linux/misc/60-sunshine.rules | 17 ++++---- 6 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 src/platform/linux/input/inputtino_seat.cpp create mode 100644 src/platform/linux/input/inputtino_seat.h diff --git a/docs/getting_started.md b/docs/getting_started.md index dc7b8e4a4d1..7c69ec0bdfa 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -445,6 +445,48 @@ After adding yourself to the group, log out and log back in for the changes to t ### Linux +#### Virtual Input Devices + +Sunshine uses virtual input devices (via uinput) to inject keyboard, mouse, and gamepad events. Access to `/dev/uinput` is typically restricted to the `input` group. + +Add your user to the group: + +```bash +sudo usermod -aG input $USER +``` + +> Log out and back in after running this. + +The required udev rules are bundled with Sunshine and installed automatically. No manual udev configuration is needed for single-seat setups. + +--- + +#### Multi-seat + +If you run multiple concurrent Wayland sessions on separate logind seats (e.g. `seat0`, `seat1`), your compositor may ignore injected input unless Sunshine's virtual devices are assigned to the correct seat. + +Sunshine determines its target seat from `XDG_SEAT`, which is typically set automatically by your display manager. If needed, you can override it manually in your systemd service file or shell environment before starting Sunshine. + +When the seat is not `seat0`, Sunshine appends the seat name to its virtual device names, for example: + +- `Keyboard passthrough (seat1)` +- `Sunshine PS5 (virtual) pad (seat1)` + +> Sunshine creates two mouse devices: a relative one and an absolute one (suffixed with ` (absolute)`). + +To assign Sunshine's virtual devices to the correct seat, create this udev rules file: + +**`/etc/udev/rules.d/72-sunshine-virtual-seat.rules`** +```udev +SUBSYSTEM=="input", KERNEL=="input*", ATTR{name}=="*(seat1)*", TAG+="seat", ENV{ID_SEAT}="seat1" +``` + +Then reload udev: + +```bash +sudo udevadm control --reload-rules && sudo udevadm trigger -s input +``` + #### Services **Start once** diff --git a/src/platform/linux/input/inputtino_common.h b/src/platform/linux/input/inputtino_common.h index 855f88a4186..acc1b759def 100644 --- a/src/platform/linux/input/inputtino_common.h +++ b/src/platform/linux/input/inputtino_common.h @@ -13,12 +13,28 @@ #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" +#include "src/platform/linux/input/inputtino_seat.h" #include "src/utility.h" using namespace std::literals; namespace platf { + inline std::string inputtino_name_for_seat(std::string_view base_name) { + auto seat_id = inputtino_seat::get_target_seat(); + if (seat_id.empty() || seat_id == "seat0") { + return std::string(base_name); + } + + std::string name; + name.reserve(base_name.size() + seat_id.size() + 3); + name.append(base_name); + name.append(" ("); + name.append(seat_id); + name.push_back(')'); + return name; + } + using joypads_t = std::variant; struct joypad_state { @@ -30,13 +46,13 @@ namespace platf { struct input_raw_t { input_raw_t(): mouse(inputtino::Mouse::create({ - .name = "Mouse passthrough", + .name = inputtino_name_for_seat("Mouse passthrough"sv), .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, })), keyboard(inputtino::Keyboard::create({ - .name = "Keyboard passthrough", + .name = inputtino_name_for_seat("Keyboard passthrough"sv), .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, @@ -66,13 +82,13 @@ namespace platf { struct client_input_raw_t: public client_input_t { client_input_raw_t(input_t &input): touch(inputtino::TouchScreen::create({ - .name = "Touch passthrough", + .name = inputtino_name_for_seat("Touch passthrough"sv), .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, })), pen(inputtino::PenTablet::create({ - .name = "Pen passthrough", + .name = inputtino_name_for_seat("Pen passthrough"sv), .vendor_id = 0xBEEF, .product_id = 0xDEAD, .version = 0x111, diff --git a/src/platform/linux/input/inputtino_gamepad.cpp b/src/platform/linux/input/inputtino_gamepad.cpp index 7e782b59b21..dfecc10844b 100644 --- a/src/platform/linux/input/inputtino_gamepad.cpp +++ b/src/platform/linux/input/inputtino_gamepad.cpp @@ -10,6 +10,7 @@ // local includes #include "inputtino_common.h" #include "inputtino_gamepad.h" +#include "inputtino_seat.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" @@ -27,7 +28,7 @@ namespace platf::gamepad { }; auto create_xbox_one() { - return inputtino::XboxOneJoypad::create({.name = "Sunshine X-Box One (virtual) pad", + return inputtino::XboxOneJoypad::create({.name = inputtino_name_for_seat("Sunshine X-Box One (virtual) pad"sv), // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 .vendor_id = 0x045E, .product_id = 0x02EA, @@ -35,7 +36,7 @@ namespace platf::gamepad { } auto create_switch() { - return inputtino::SwitchJoypad::create({.name = "Sunshine Nintendo (virtual) pad", + return inputtino::SwitchJoypad::create({.name = inputtino_name_for_seat("Sunshine Nintendo (virtual) pad"sv), // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 .vendor_id = 0x057e, .product_id = 0x2009, @@ -50,7 +51,7 @@ namespace platf::gamepad { device_mac = std::format("02:00:00:00:00:{:02x}", globalIndex); } - return inputtino::PS5Joypad::create({.name = "Sunshine PS5 (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111, .device_phys = device_mac, .device_uniq = device_mac}); + return inputtino::PS5Joypad::create({.name = inputtino_name_for_seat("Sunshine PS5 (virtual) pad"sv), .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111, .device_phys = device_mac, .device_uniq = device_mac}); } int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { diff --git a/src/platform/linux/input/inputtino_seat.cpp b/src/platform/linux/input/inputtino_seat.cpp new file mode 100644 index 00000000000..dba9a7878c7 --- /dev/null +++ b/src/platform/linux/input/inputtino_seat.cpp @@ -0,0 +1,22 @@ +/** + * @file src/platform/linux/input/inputtino_seat.cpp + * @brief Implementation for multi-seat naming (udev-only). + */ + +#include "inputtino_seat.h" + +#include + +namespace platf::inputtino_seat { + + std::string get_target_seat() { + if (const char *seat = std::getenv("XDG_SEAT")) { + if (seat[0] != '\0') { + return seat; + } + } + + return {}; + } + +} // namespace platf::inputtino_seat diff --git a/src/platform/linux/input/inputtino_seat.h b/src/platform/linux/input/inputtino_seat.h new file mode 100644 index 00000000000..63fe54013e2 --- /dev/null +++ b/src/platform/linux/input/inputtino_seat.h @@ -0,0 +1,17 @@ +/** + * @file src/platform/linux/input/inputtino_seat.h + * @brief Helpers for multi-seat naming (udev-only). + */ +#pragma once + +#include + +namespace platf::inputtino_seat { + + /** + * Determine the target seat for the current Sunshine instance. + * Returns empty string if no seat could be determined. + */ + std::string get_target_seat(); + +} // namespace platf::inputtino_seat diff --git a/src_assets/linux/misc/60-sunshine.rules b/src_assets/linux/misc/60-sunshine.rules index 14cc59e421b..9961bc4a633 100644 --- a/src_assets/linux/misc/60-sunshine.rules +++ b/src_assets/linux/misc/60-sunshine.rules @@ -1,11 +1,10 @@ -# Allows Sunshine to acces /dev/uinput +# Allows Sunshine to access /dev/uinput and /dev/uhid KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess" +KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess" -# Allows Sunshine to access /dev/uhid -KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess" - -# Joypads -KERNEL=="hidraw*", ATTRS{name}=="Sunshine PS5 (virtual) pad", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine X-Box One (virtual) pad", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine gamepad (virtual) motion sensors", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine Nintendo (virtual) pad", GROUP="input", MODE="0660", TAG+="uaccess" +# Virtual joypads (seat suffix e.g. "... (seat1)" is appended on multi-seat systems) +KERNEL=="hidraw*", ATTRS{name}=="Sunshine PS5 (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine PS5 (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine X-Box One (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine Nintendo (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine gamepad (virtual) motion sensors*", GROUP="input", MODE="0660", TAG+="uaccess" \ No newline at end of file From 2bdec59a149713c4eef152f229e9e0d71c45c503 Mon Sep 17 00:00:00 2001 From: Willian Barreto Date: Mon, 6 Apr 2026 21:45:19 +0000 Subject: [PATCH 2/2] docs: enhance multiseat support documentation and clarify udev rules --- docs/getting_started.md | 42 ------------------------- docs/troubleshooting.md | 26 +++++++++++++++ src_assets/linux/misc/60-sunshine.rules | 18 ++++++----- 3 files changed, 36 insertions(+), 50 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 7c69ec0bdfa..dc7b8e4a4d1 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -445,48 +445,6 @@ After adding yourself to the group, log out and log back in for the changes to t ### Linux -#### Virtual Input Devices - -Sunshine uses virtual input devices (via uinput) to inject keyboard, mouse, and gamepad events. Access to `/dev/uinput` is typically restricted to the `input` group. - -Add your user to the group: - -```bash -sudo usermod -aG input $USER -``` - -> Log out and back in after running this. - -The required udev rules are bundled with Sunshine and installed automatically. No manual udev configuration is needed for single-seat setups. - ---- - -#### Multi-seat - -If you run multiple concurrent Wayland sessions on separate logind seats (e.g. `seat0`, `seat1`), your compositor may ignore injected input unless Sunshine's virtual devices are assigned to the correct seat. - -Sunshine determines its target seat from `XDG_SEAT`, which is typically set automatically by your display manager. If needed, you can override it manually in your systemd service file or shell environment before starting Sunshine. - -When the seat is not `seat0`, Sunshine appends the seat name to its virtual device names, for example: - -- `Keyboard passthrough (seat1)` -- `Sunshine PS5 (virtual) pad (seat1)` - -> Sunshine creates two mouse devices: a relative one and an absolute one (suffixed with ` (absolute)`). - -To assign Sunshine's virtual devices to the correct seat, create this udev rules file: - -**`/etc/udev/rules.d/72-sunshine-virtual-seat.rules`** -```udev -SUBSYSTEM=="input", KERNEL=="input*", ATTR{name}=="*(seat1)*", TAG+="seat", ENV{ID_SEAT}="seat1" -``` - -Then reload udev: - -```bash -sudo udevadm control --reload-rules && sudo udevadm trigger -s input -``` - #### Services **Start once** diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index eae5692b939..a4579389fda 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -159,6 +159,32 @@ If the input is still not working, you may need to add your user to the `input` sudo usermod -aG input $USER ``` +#### Multiseat + +If you run multiple concurrent Wayland sessions on separate logind seats (e.g. `seat0`, `seat1`), your compositor may ignore injected input unless Sunshine's virtual devices are assigned to the correct seat. + +Sunshine determines its target seat from `XDG_SEAT`, which is typically set automatically by your display manager. If needed, you can override it manually in your systemd service file or shell environment before starting Sunshine. + +When the seat is not `seat0`, Sunshine appends the seat name to its virtual device names, for example: + +- `Keyboard passthrough (seat1)` +- `Sunshine PS5 (virtual) pad (seat1)` + +> Sunshine creates two mouse devices: a relative one and an absolute one (suffixed with ` (absolute)`). + +To assign Sunshine's virtual devices to the correct seat, create this udev rules file: + +**`/etc/udev/rules.d/72-sunshine-virtual-seat.rules`** +```udev +SUBSYSTEM=="input", KERNEL=="input*", ATTR{name}=="*(seat1)*", TAG+="seat", ENV{ID_SEAT}="seat1" +``` + +Then reload udev: + +```bash +sudo udevadm control --reload-rules && sudo udevadm trigger -s input +``` + ### KMS Streaming fails KMS screencasting requires elevated privileges which are not allowed for Flatpak or AppImage packages. This means that you must install Sunshine using the native package format of your distribution, if available. diff --git a/src_assets/linux/misc/60-sunshine.rules b/src_assets/linux/misc/60-sunshine.rules index 9961bc4a633..6592f6d0993 100644 --- a/src_assets/linux/misc/60-sunshine.rules +++ b/src_assets/linux/misc/60-sunshine.rules @@ -1,10 +1,12 @@ -# Allows Sunshine to access /dev/uinput and /dev/uhid +# Allows Sunshine to access /dev/uinput KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess" -KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess" -# Virtual joypads (seat suffix e.g. "... (seat1)" is appended on multi-seat systems) -KERNEL=="hidraw*", ATTRS{name}=="Sunshine PS5 (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine PS5 (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine X-Box One (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine Nintendo (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" -SUBSYSTEMS=="input", ATTRS{name}=="Sunshine gamepad (virtual) motion sensors*", GROUP="input", MODE="0660", TAG+="uaccess" \ No newline at end of file +# Allows Sunshine to access /dev/uhid +KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess" + +# Joypads +KERNEL=="hidraw*", ATTRS{name}=="Sunshine PS5 (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine X-Box One (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine gamepad (virtual) motion sensors*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine Nintendo (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Sunshine PS5 (virtual) pad*", GROUP="input", MODE="0660", TAG+="uaccess" \ No newline at end of file