Skip to content

Offer: multi-seat Incus test rig, plus findings on the Incus strategy and uhid #16

Description

@superuser404notfound

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions