Following up from LizardByte/Sunshine#3768, where you
mentioned uhid is the missing piece. I read through the codebase and the open
issues first, so this is partly an offer of help and partly two concrete
findings that might be useful regardless of whether I end up contributing code.
Where I am coming from. I run a multi-seat setup on Incus: two seats, each
an Incus system container with headless sway, its own Sunshine instance and its
own Steam account, streaming to separate Moonlight clients. Both run in parallel
today with keyboard, mouse and gamepad reaching exactly one seat each. My input
isolation is the naive one, host udev rules plus a broker that injects nodes
into the right container by matching the seat tag Sunshine writes into the
device name. It works, but attribution by name only protects against accidents,
not against a compromised seat, which is exactly the axis where mediating
/dev/uinput is better. Code and measurement logs:
https://github.com/superuser404notfound/Polyseat
1. The Incus strategy cannot do multi-container today (relates to #11)
Every generic strategy derives its target from requesting_process and acts in
that process's namespaces, so multiple containers work by construction.
impl InjectionStrategy for Incus does the opposite: it marks the parameter
_requesting_process as unused and takes the container name from
get_scope(), bailing out on Scope::Multi. So Incus effectively requires one
vuinputd instance per container, and a setup like mine cannot be served by a
single daemon.
The container name is derivable from the requesting process. On a live two-seat
host here, reading the caller's cgroup gives it directly:
$ cat /proc/52144/cgroup
0::/lxc.payload.seat1/user.slice/user-1000.slice/user@1000.service/app...
$ cat /proc/71439/cgroup
0::/lxc.payload.seat2/user.slice/user-1000.slice/user@1000.service/app...
That is Incus 7.2 on a CachyOS host, where the prefix is lxc.payload. because
Incus still uses LXC underneath; other versions may write incus.payload., so
it would want matching on both. Either way it would let the Incus strategy
resolve the target the same way the generic ones do and drop the Scope::Single
restriction.
While reading that function I also noticed mknod_device_node builds
hostpath and incuspath as the same path=... string and passes both, never
setting source=. Incus tolerates it because path doubles as the host path
when source is absent, so this is cosmetic rather than broken, but it reads
like one of the two was meant to be source=.
2. What I measured about uhid
Sunshine creates gamepads through inputtino as HID devices via /dev/uhid, not
through uinput, and it does so for the default emulated model as well, not
only for a DualSense. So you do not need to buy a controller to reproduce it:
any Moonlight client with an on-screen controller overlay is enough. Without
uhid passed through, Sunshine still logs Gamepad 0 will be Xbox One controller
while no device ever appears inside the container, which is what made it look
like a client-side problem to me for quite a while.
A uhid device also produces a hidraw node alongside the input event node, and
Steam Input uses hidraw. So whatever handles uhid has to inject hidraw too. That
is an open gap on my side as well.
On the protocol: uinput is essentially write-only from the application, while
uhid is bidirectional and stateful. The application writes UHID_CREATE2 and
UHID_INPUT2 but has to read UHID_START, UHID_OPEN, UHID_OUTPUT and
UHID_GET_REPORT back. Having seen vuinput_read.rs and vuinput_poll.rs, the
relaying shape clearly already exists, so my question is not whether it fits but
how you would want it cut: a second CUSE device for /dev/uhid next to the
existing one, or something more shared?
Worth noting that this overlaps with #10: UHID_OUTPUT is how rumble arrives
for HID devices, so uhid support and the vibration work are partly the same
problem.
3. What I can actually offer
A real two-seat Incus rig with Sunshine, gamepads and the habit of writing down
what each experiment measured. I can test branches against it, and I can
reproduce #11, #10 and #7. #7 in particular looks familiar: Sunshine's "Mouse
passthrough (absolute)" reports EV_ABS rather than EV_REL, and classifying it
wrongly made sway treat it as a keyboard in my setup, so the pointer stayed dead
without an error appearing anywhere.
What I am not offering is a promise to land uhid support on my own in a codebase
I have read but never written in. If you want it from outside, tell me how you
would want it cut and I will start with a narrow piece. If you would rather build
it yourself, testing and diagnosis are probably where I am more useful anyway.
Following up from LizardByte/Sunshine#3768, where you
mentioned uhid is the missing piece. I read through the codebase and the open
issues first, so this is partly an offer of help and partly two concrete
findings that might be useful regardless of whether I end up contributing code.
Where I am coming from. I run a multi-seat setup on Incus: two seats, each
an Incus system container with headless sway, its own Sunshine instance and its
own Steam account, streaming to separate Moonlight clients. Both run in parallel
today with keyboard, mouse and gamepad reaching exactly one seat each. My input
isolation is the naive one, host udev rules plus a broker that injects nodes
into the right container by matching the seat tag Sunshine writes into the
device name. It works, but attribution by name only protects against accidents,
not against a compromised seat, which is exactly the axis where mediating
/dev/uinputis better. Code and measurement logs:https://github.com/superuser404notfound/Polyseat
1. The Incus strategy cannot do multi-container today (relates to #11)
Every generic strategy derives its target from
requesting_processand acts inthat process's namespaces, so multiple containers work by construction.
impl InjectionStrategy for Incusdoes the opposite: it marks the parameter_requesting_processas unused and takes the container name fromget_scope(), bailing out onScope::Multi. So Incus effectively requires onevuinputd instance per container, and a setup like mine cannot be served by a
single daemon.
The container name is derivable from the requesting process. On a live two-seat
host here, reading the caller's cgroup gives it directly:
That is Incus 7.2 on a CachyOS host, where the prefix is
lxc.payload.becauseIncus still uses LXC underneath; other versions may write
incus.payload., soit would want matching on both. Either way it would let the Incus strategy
resolve the target the same way the generic ones do and drop the
Scope::Singlerestriction.
While reading that function I also noticed
mknod_device_nodebuildshostpathandincuspathas the samepath=...string and passes both, neversetting
source=. Incus tolerates it becausepathdoubles as the host pathwhen
sourceis absent, so this is cosmetic rather than broken, but it readslike one of the two was meant to be
source=.2. What I measured about uhid
Sunshine creates gamepads through inputtino as HID devices via
/dev/uhid, notthrough uinput, and it does so for the default emulated model as well, not
only for a DualSense. So you do not need to buy a controller to reproduce it:
any Moonlight client with an on-screen controller overlay is enough. Without
uhid passed through, Sunshine still logs
Gamepad 0 will be Xbox One controllerwhile no device ever appears inside the container, which is what made it look
like a client-side problem to me for quite a while.
A uhid device also produces a
hidrawnode alongside the input event node, andSteam Input uses hidraw. So whatever handles uhid has to inject hidraw too. That
is an open gap on my side as well.
On the protocol: uinput is essentially write-only from the application, while
uhid is bidirectional and stateful. The application writes
UHID_CREATE2andUHID_INPUT2but has to readUHID_START,UHID_OPEN,UHID_OUTPUTandUHID_GET_REPORTback. Having seenvuinput_read.rsandvuinput_poll.rs, therelaying shape clearly already exists, so my question is not whether it fits but
how you would want it cut: a second CUSE device for
/dev/uhidnext to theexisting one, or something more shared?
Worth noting that this overlaps with #10:
UHID_OUTPUTis how rumble arrivesfor HID devices, so uhid support and the vibration work are partly the same
problem.
3. What I can actually offer
A real two-seat Incus rig with Sunshine, gamepads and the habit of writing down
what each experiment measured. I can test branches against it, and I can
reproduce #11, #10 and #7. #7 in particular looks familiar: Sunshine's "Mouse
passthrough (absolute)" reports
EV_ABSrather thanEV_REL, and classifying itwrongly made sway treat it as a keyboard in my setup, so the pointer stayed dead
without an error appearing anywhere.
What I am not offering is a promise to land uhid support on my own in a codebase
I have read but never written in. If you want it from outside, tell me how you
would want it cut and I will start with a narrow piece. If you would rather build
it yourself, testing and diagnosis are probably where I am more useful anyway.