Controllers: one pad reader for a session, so KDE's pointer emulation stays paused

Entering a world opened the split's gilrs and closed P1's. gilrs opens a pad twice
at once, which inotify can merge into one event, and closes it twice apart. KWin
counts those events to pause its emulation, so its count could reach zero while
Horizon still held the pad, and the left stick also moved the mouse.

The router now takes P1's reader and gives it back. Its reader also gets the extra
SDL rows, which it was missing.

Refs #9730

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
martipopsandClaude Opus 5.5 committed 2026-11-05 07:28:34 -05:00
1 parent 696d021699
commit 924c7015df
3 files changed
+80 -6

No files matched your search

+2
View File
@@ -113,6 +113,8 @@ mod hardware {
#[derive(Default)]
pub(in crate::game) struct PadRouter {}
impl PadRouter {
pub(in crate::game) fn adopt(&mut self, _: &mut Pads) {}
pub(in crate::game) fn hand_back(&mut self, _: &mut Pads) {}
pub(in crate::game) fn events(&mut self) -> Vec<super::PadEvent> {
Vec::new()
}
+76 -5
View File
@@ -67,6 +67,11 @@ const EXTRA_SDL_MAPPINGS: &str = concat!(
"03000000c82d00000b31000014010000,8BitDo Ultimate 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,\n",
);
/// The one way to open gilrs: each reader needs the extra rows.
fn gilrs_builder() -> gilrs::GilrsBuilder {
gilrs::GilrsBuilder::new().add_mappings(EXTRA_SDL_MAPPINGS)
}
impl Pads {
/// Open gilrs on first use. A platform without it (or a Linux without
/// udev) leaves controllers off; the game runs as before.
@@ -75,10 +80,7 @@ impl Pads {
return;
}
self.devices.started = true;
self.devices.gilrs = match gilrs::GilrsBuilder::new()
.add_mappings(EXTRA_SDL_MAPPINGS)
.build()
{
self.devices.gilrs = match gilrs_builder().build() {
Ok(g) => {
for (_, pad) in g.gamepads() {
log::info!("Controller connected: {}", pad.name());
@@ -211,6 +213,28 @@ pub(in crate::game) struct PadRouter {
}
impl PadRouter {
/// Take over the reader P1 already has open, instead of opening a second and closing the
/// first. A compositor can count who holds a pad to pause its own use of it (KWin's pointer
/// emulation), and that count goes wrong when readers overlap: gilrs opens a pad twice at
/// once, which inotify can merge into one event, and closes it twice apart (#9730).
pub(in crate::game) fn adopt(&mut self, pads: &mut Pads) {
if !self.started
&& let Some(gilrs) = pads.devices.gilrs.take()
{
self.started = true;
self.gilrs = Some(gilrs);
}
}
/// Routing ends: P1 gets the reader back, open as it was.
pub(in crate::game) fn hand_back(&mut self, pads: &mut Pads) {
self.started = false;
if let Some(gilrs) = self.gilrs.take() {
pads.devices.gilrs = Some(gilrs);
pads.devices.started = true;
}
}
/// Solo play uses this reader's device IDs for feedback too: another pad cannot steal
/// the active output while its input is being offered to join. Reclaimed after P1's turn.
pub(in crate::game) fn lend_output(&mut self, pads: &mut Pads, id: PadId) {
@@ -240,7 +264,7 @@ impl PadRouter {
pub(in crate::game) fn events(&mut self) -> Vec<PadEvent> {
if !self.started {
self.started = true;
self.gilrs = gilrs::Gilrs::new().ok();
self.gilrs = gilrs_builder().build().ok();
}
let Some(gilrs) = self.gilrs.as_mut() else {
return Vec::new();
@@ -562,3 +586,50 @@ impl Game<'_> {
})
}
}
#[cfg(test)]
mod one_reader_tests {
use super::*;
/// Whether a machine can open gilrs at all; a test without it has nothing to check.
fn reader() -> Option<gilrs::Gilrs> {
gilrs_builder().build().ok()
}
#[test]
fn routing_takes_p1s_reader_and_gives_it_back_without_closing_it() {
let Some(gilrs) = reader() else {
return;
};
let mut pads = Pads::default();
pads.devices.gilrs = Some(gilrs);
pads.devices.started = true;
let mut router = PadRouter::default();
router.adopt(&mut pads);
assert!(pads.devices.gilrs.is_none(), "P1 no longer holds it");
assert!(router.gilrs.is_some(), "the router holds the same reader");
assert!(router.started, "so the router won't open a second");
pads.release_hardware();
router.hand_back(&mut pads);
assert!(router.gilrs.is_none());
assert!(pads.devices.gilrs.is_some(), "P1 holds it again");
assert!(pads.devices.started, "so P1 won't open a second");
}
#[test]
fn a_router_with_its_own_reader_leaves_a_lent_ones_owner_alone() {
let (Some(own), Some(other)) = (reader(), reader()) else {
return;
};
let mut pads = Pads::default();
pads.devices.gilrs = Some(other);
let mut router = PadRouter {
gilrs: Some(own),
started: true,
};
router.adopt(&mut pads);
assert!(pads.devices.gilrs.is_some());
}
}
+2 -1
View File
@@ -125,7 +125,7 @@ impl Split<'_> {
self.fresh.clear();
self.lost.clear();
self.p1_pad = P1Pad::default();
self.pads = PadRouter::default();
self.pads.hand_back(&mut self.host.pad);
}
}
@@ -154,6 +154,7 @@ impl Split<'_> {
let offers_before = self.offers.len();
let mut joins = Vec::new();
let mut presses = Vec::new();
self.pads.adopt(&mut self.host.pad);
for event in self.pads.events() {
match event {
PadEvent::Press(id, button) => presses.push((id, button)),