refactor(host/W4): extract inject keymap tables + rehome HidoutDedup
apple / swift (push) Successful in 1m19s
apple / screenshots (push) Successful in 4m27s
ci / web (push) Successful in 1m17s
ci / docs-site (push) Successful in 1m17s
android / android (push) Successful in 12m55s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 36s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
arch / build-publish (push) Successful in 13m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m40s
ci / rust (push) Successful in 17m58s
deb / build-publish (push) Successful in 12m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m56s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m47s
windows-host / package (push) Successful in 15m17s
docker / deploy-docs (push) Failing after 30s
apple / swift (push) Successful in 1m19s
apple / screenshots (push) Successful in 4m27s
ci / web (push) Successful in 1m17s
ci / docs-site (push) Successful in 1m17s
android / android (push) Successful in 12m55s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 36s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
arch / build-publish (push) Successful in 13m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m40s
ci / rust (push) Successful in 17m58s
deb / build-publish (push) Successful in 12m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m56s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m47s
windows-host / package (push) Successful in 15m17s
docker / deploy-docs (push) Failing after 30s
Two device-agnostic pieces carved out of the inject facade (plan §W4): - inject/keymap.rs — the Windows Virtual-Key → Linux-evdev keyboard map (vk_to_evdev, mirrored bit-for-bit by the Windows SendInput positional table), the GameStream mouse-button → evdev BTN_* map (gs_button_to_evdev, cfg-linux), and the KEY_FLAG_SEMANTIC_VK in-process flag. - inject/hidout_dedup.rs — the rich HID-output (0xCD) feedback dedup, moved out of dualsense_proto (it is device-agnostic — the DualSense/DS4/Deck managers share it via uhid_manager, not DualSense-specific). Its unit test moves with it. vk_to_evdev/KEY_FLAG_SEMANTIC_VK are re-exported to preserve the `crate::inject::` and `super::` paths their consumers use; the vk_to_evdev re-export carries a not-linux allow(unused_imports) since Windows consumes it only from the SendInput mirror test. uhid_manager's import repointed to the new home. Pure move; no behavior change. Linux clippy+tests + Windows host clippy (nvenc,amf-qsv) both green; fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -535,124 +535,10 @@ pub fn parse_ds_output(pad: u8, data: &[u8], fb: &mut DsFeedback) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-pad dedup for the DualSense HID-output feedback plane (0xCD). A game's DualSense output report
|
||||
/// bundles rumble + lightbar + player-LEDs + adaptive-triggers into one report, so a pad that is
|
||||
/// merely *rumbling* re-sends its (unchanged) lightbar / LED / trigger state on every output report.
|
||||
/// The managers already dedup rumble; this does the same for the rich [`HidOutput`] feedback so the
|
||||
/// 0xCD plane carries only genuine changes. State (`Led` / `PlayerLeds` / `Trigger`) is deduped by
|
||||
/// value; a one-shot `TrackpadHaptic` pulse is always forwarded (each pulse must fire).
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HidoutDedup {
|
||||
led: Option<(u8, u8, u8)>,
|
||||
player_leds: Option<u8>,
|
||||
/// Last-forwarded adaptive-trigger effect per side: `[0]` = L2, `[1]` = R2.
|
||||
trigger: [Option<Vec<u8>>; 2],
|
||||
}
|
||||
|
||||
impl HidoutDedup {
|
||||
/// Forget all remembered state — call when a pad is created or unplugged so the first feedback
|
||||
/// after a (re)connect is always forwarded.
|
||||
pub fn clear(&mut self) {
|
||||
*self = HidoutDedup::default();
|
||||
}
|
||||
|
||||
/// Whether `h` should be forwarded: `true` for a genuine change (remembering the new value) or a
|
||||
/// one-shot pulse; `false` if it repeats the last-forwarded value for its kind.
|
||||
pub fn should_forward(&mut self, h: &HidOutput) -> bool {
|
||||
match h {
|
||||
HidOutput::Led { r, g, b, .. } => {
|
||||
let v = Some((*r, *g, *b));
|
||||
if self.led == v {
|
||||
false
|
||||
} else {
|
||||
self.led = v;
|
||||
true
|
||||
}
|
||||
}
|
||||
HidOutput::PlayerLeds { bits, .. } => {
|
||||
let v = Some(*bits);
|
||||
if self.player_leds == v {
|
||||
false
|
||||
} else {
|
||||
self.player_leds = v;
|
||||
true
|
||||
}
|
||||
}
|
||||
HidOutput::Trigger { which, effect, .. } => {
|
||||
let slot = (*which as usize).min(1);
|
||||
if self.trigger[slot].as_deref() == Some(effect.as_slice()) {
|
||||
false
|
||||
} else {
|
||||
self.trigger[slot] = Some(effect.clone());
|
||||
true
|
||||
}
|
||||
}
|
||||
// One-shot haptic pulse (Steam voice-coil) — state-less, always fires.
|
||||
HidOutput::TrackpadHaptic { .. } => true,
|
||||
// Raw as-is passthrough reports must NEVER dedup: the physical device's firmware
|
||||
// watchdogs RELY on identical periodic refreshes (Triton rumble re-sent every ~40 ms
|
||||
// against a ~50 ms safety timeout, lizard-off every ~3 s) — dropping a repeat would
|
||||
// silence the motors / re-enable lizard mode on the real controller.
|
||||
HidOutput::HidRaw { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `HidoutDedup` forwards a value once, drops exact repeats, re-forwards a change, tracks the two
|
||||
/// trigger sides independently, never dedups one-shot haptic pulses, and re-arms after `clear`.
|
||||
#[test]
|
||||
fn hidout_dedup_forwards_only_changes() {
|
||||
let mut d = HidoutDedup::default();
|
||||
let led = |r| HidOutput::Led {
|
||||
pad: 0,
|
||||
r,
|
||||
g: 0,
|
||||
b: 0,
|
||||
};
|
||||
// First value forwards; an exact repeat is dropped; a change forwards again.
|
||||
assert!(d.should_forward(&led(10)));
|
||||
assert!(!d.should_forward(&led(10)));
|
||||
assert!(d.should_forward(&led(20)));
|
||||
|
||||
// Player LEDs dedup on their own field, independent of the lightbar.
|
||||
let pl = |bits| HidOutput::PlayerLeds { pad: 0, bits };
|
||||
assert!(d.should_forward(&pl(0b101)));
|
||||
assert!(!d.should_forward(&pl(0b101)));
|
||||
assert!(!d.should_forward(&led(20))); // lightbar still unchanged
|
||||
|
||||
// The two adaptive triggers (L2=0, R2=1) are tracked separately.
|
||||
let trig = |which, byte| HidOutput::Trigger {
|
||||
pad: 0,
|
||||
which,
|
||||
effect: vec![byte, 0, 0],
|
||||
};
|
||||
assert!(d.should_forward(&trig(0, 1)));
|
||||
assert!(d.should_forward(&trig(1, 1))); // same bytes, other side → still forwards
|
||||
assert!(!d.should_forward(&trig(0, 1)));
|
||||
assert!(d.should_forward(&trig(0, 2))); // L2 effect changed
|
||||
|
||||
// One-shot haptic pulses are never deduped.
|
||||
let haptic = HidOutput::TrackpadHaptic {
|
||||
pad: 0,
|
||||
side: 0,
|
||||
amplitude: 1,
|
||||
period: 2,
|
||||
count: 3,
|
||||
};
|
||||
assert!(d.should_forward(&haptic));
|
||||
assert!(d.should_forward(&haptic));
|
||||
|
||||
// `clear` re-arms every kind.
|
||||
d.clear();
|
||||
assert!(d.should_forward(&led(20)));
|
||||
assert!(d.should_forward(&pl(0b101)));
|
||||
assert!(d.should_forward(&trig(0, 2)));
|
||||
}
|
||||
|
||||
/// The Steam dual-pad → DualSense touchpad SPLIT: left pad (surface 1) lands contact 0
|
||||
/// on the left half, right pad (surface 2) contact 1 on the right half; y follows the
|
||||
/// shared screen convention (top → 0) with no flip; pad clicks set the touchpad-click
|
||||
|
||||
Reference in New Issue
Block a user