From 0c427cb3f18130fc648191877e8e16472a1116c5 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 13 Jul 2026 13:14:02 +0200 Subject: [PATCH] fix(inject/host/linux): re-assert absolute gamepad button state each frame (G8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The uinput gamepad backend emitted only XOR-changed button edges while advancing `prev_buttons` unconditionally. Because `emit()` is best-effort (a full kernel queue silently drops the write), a dropped EV_KEY edge was never re-synced — the button stayed stuck (pressed-not-released, or vice versa) until it next toggled. The axes never had this problem: they re-emit their absolute value every frame. Re-assert every mapped button's absolute state each frame, exactly like the axes, and drop the now-unused `prev_buttons` field. Restating an unchanged key is free downstream: the kernel input core discards an EV_KEY whose value already matches the device's current state (no duplicate event reaches consumers, and BTN_* keys don't autorepeat). The `emit()` "next frame re-syncs state" comment is now honest for buttons too. Verified on the Linux host build (.21): cargo clippy -D warnings clean (no dead-field warning), full punktfunk-host suite 277 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../punktfunk-host/src/inject/linux/gamepad.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/punktfunk-host/src/inject/linux/gamepad.rs b/crates/punktfunk-host/src/inject/linux/gamepad.rs index 722e6803..491cd1a9 100644 --- a/crates/punktfunk-host/src/inject/linux/gamepad.rs +++ b/crates/punktfunk-host/src/inject/linux/gamepad.rs @@ -266,7 +266,6 @@ struct Effect { /// One virtual X-Box-360 pad backed by a uinput device. pub struct VirtualPad { fd: OwnedFd, - prev_buttons: u32, effects: HashMap, next_effect_id: i16, gain: u32, @@ -370,7 +369,6 @@ impl VirtualPad { Ok(VirtualPad { fd, - prev_buttons: 0, effects: HashMap::new(), next_effect_id: 0, gain: 0xFFFF, @@ -413,15 +411,17 @@ impl VirtualPad { }; } - /// Apply one decoded frame: button transitions, axes, D-pad hat, one SYN_REPORT. + /// Apply one decoded frame: button state, axes, D-pad hat, one SYN_REPORT. pub fn apply(&mut self, f: &GamepadFrame) { - let changed = self.prev_buttons ^ f.buttons; + // Re-assert every mapped button's absolute state each frame — exactly like the axes below — + // instead of only writing XOR-changed edges. `emit` is best-effort (a full kernel queue drops + // the write), so an edge-only scheme would strand a dropped press/release until that button + // next toggles; re-asserting re-syncs it on the following frame. Restating an unchanged key is + // free downstream: the kernel input core discards an EV_KEY whose value already matches the + // device's current state (no duplicate event reaches consumers, and BTN_* keys don't autorepeat). for (bit, key) in BUTTON_MAP { - if changed & bit != 0 { - self.emit(EV_KEY, key, ((f.buttons & bit) != 0) as i32); - } + self.emit(EV_KEY, key, ((f.buttons & bit) != 0) as i32); } - self.prev_buttons = f.buttons; // Moonlight: +Y = up; evdev: +Y = down → negate (i32 math avoids -(-32768) overflow). self.emit(EV_ABS, ABS_X, f.ls_x as i32);