ref:dd30d0555f2c62c4f80e988e5623dd967fd37ffb

feat(input): implement multiseat support and virtual device naming (#4954)

SHA: dd30d0555f2c62c4f80e988e5623dd967fd37ffb
Author: Willian Barreto <78451161+willybarret@users.noreply.github.com>
Date: 2026-05-05 22:12
Parents: 3cb8277
6 files changed +97 -12
Type
docs/troubleshooting.md +27 −0
@@ -159,6 +159,33 @@
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.
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.
src/platform/linux/input/inputtino_common.h +20 −4
@@ -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<inputtino::XboxOneJoypad, inputtino::SwitchJoypad, inputtino::PS5Joypad>;
struct joypad_state {
@@ -30,13 +46,13 @@
struct input_raw_t {
input_raw_t():
mouse(inputtino::Mouse::create({
.name = inputtino_name_for_seat("Mouse passthrough"sv),
.name = "Mouse passthrough",
.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 @@
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 = inputtino_name_for_seat("Pen passthrough"sv),
.name = "Pen passthrough",
.vendor_id = 0xBEEF,
.product_id = 0xDEAD,
.version = 0x111,
src/platform/linux/input/inputtino_gamepad.cpp +4 −3
@@ -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 @@
};
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 @@
}
auto create_switch() {
return inputtino::SwitchJoypad::create({.name = inputtino_name_for_seat("Sunshine Nintendo (virtual) pad"sv),
return inputtino::SwitchJoypad::create({.name = "Sunshine Nintendo (virtual) pad",
// https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981
.vendor_id = 0x057e,
.product_id = 0x2009,
@@ -50,7 +51,7 @@
device_mac = std::format("02:00:00:00:00:{:02x}", globalIndex);
}
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});
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});
}
int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
src/platform/linux/input/inputtino_seat.cpp +23 −0
@@ -1,0 +1,23 @@
/**
* @file src/platform/linux/input/inputtino_seat.cpp
* @brief Implementation for multi-seat naming (udev-only).
*/
// standard includes
#include <cstdlib>
// local includes
#include "inputtino_seat.h"
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
src/platform/linux/input/inputtino_seat.h +17 −0
@@ -1,0 +1,17 @@
/**
* @file src/platform/linux/input/inputtino_seat.h
* @brief Helpers for multi-seat naming (udev-only).
*/
#pragma once
#include <string>
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
src_assets/linux/misc/60-sunshine.rules +6 −5
@@ -1,11 +1,12 @@
# Allows Sunshine to acces /dev/uinput
# Allows Sunshine to access /dev/uinput
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", 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"
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"