perf(inject/host): dedup the DualSense HID-output feedback plane (G17)
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, but forwarded every rich `HidOutput` event verbatim — flooding the 0xCD feedback plane to the client during continuous rumble. Add a shared `HidoutDedup` (dualsense_proto, used by both the Linux UHID and Windows UMDF managers) that forwards Led/PlayerLeds/Trigger only on a value change (per side for the two triggers) and always forwards one-shot TrackpadHaptic pulses — mirroring the rumble dedup two lines above and the DS4 backend's lightbar dedup. Reset per pad on create/unplug. Verified on Linux .21 (clippy -D warnings clean, new HidoutDedup unit test + full suite green); Windows .173 with the rest of Phase 3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
//! UMDF-driver backend; this module is just the `/dev/uhid` plumbing around it.
|
||||
|
||||
use super::dualsense_proto::{
|
||||
parse_ds_output, serialize_state, DsFeedback, DsState, DS_FEATURE_CALIBRATION,
|
||||
parse_ds_output, serialize_state, DsFeedback, DsState, HidoutDedup, DS_FEATURE_CALIBRATION,
|
||||
DS_FEATURE_FIRMWARE, DS_FEATURE_PAIRING, DS_INPUT_REPORT_LEN, DS_PRODUCT, DS_TOUCH_H,
|
||||
DS_TOUCH_W, DS_VENDOR, DUALSENSE_RDESC,
|
||||
};
|
||||
@@ -178,6 +178,9 @@ pub struct DualSenseManager {
|
||||
state: Vec<DsState>,
|
||||
/// Last rumble forwarded per pad, so a report that only changes the LED doesn't re-send it.
|
||||
last_rumble: Vec<(u16, u16)>,
|
||||
/// Last rich feedback (lightbar / player LEDs / adaptive triggers) forwarded per pad, so an
|
||||
/// output report that only changed the rumble doesn't re-send unchanged 0xCD feedback.
|
||||
hidout_dedup: Vec<HidoutDedup>,
|
||||
/// When each pad last wrote an input report — drives [`DualSenseManager::heartbeat`], which
|
||||
/// re-emits the current state during input silence so the kernel never sees the device go quiet.
|
||||
last_write: Vec<Instant>,
|
||||
@@ -201,6 +204,7 @@ impl DualSenseManager {
|
||||
pads: (0..MAX_PADS).map(|_| None).collect(),
|
||||
state: vec![DsState::neutral(); MAX_PADS],
|
||||
last_rumble: vec![(0, 0); MAX_PADS],
|
||||
hidout_dedup: vec![HidoutDedup::default(); MAX_PADS],
|
||||
last_write: vec![Instant::now(); MAX_PADS],
|
||||
gate: PadGate::new(),
|
||||
remap: crate::inject::steam_remap::RemapConfig::from_env(),
|
||||
@@ -226,6 +230,7 @@ impl DualSenseManager {
|
||||
*slot = None;
|
||||
self.state[i] = DsState::neutral();
|
||||
self.last_rumble[i] = (0, 0);
|
||||
self.hidout_dedup[i].clear();
|
||||
}
|
||||
}
|
||||
if f.active_mask & (1 << idx) == 0 {
|
||||
@@ -314,6 +319,7 @@ impl DualSenseManager {
|
||||
self.pads[idx] = Some(p);
|
||||
self.state[idx] = DsState::neutral();
|
||||
self.last_rumble[idx] = (0, 0);
|
||||
self.hidout_dedup[idx].clear();
|
||||
self.last_write[idx] = Instant::now();
|
||||
self.gate.on_success();
|
||||
}
|
||||
@@ -346,7 +352,11 @@ impl DualSenseManager {
|
||||
}
|
||||
}
|
||||
for h in fb.hidout {
|
||||
hidout(h);
|
||||
// Skip rich feedback that repeats the last-forwarded value (the game's output report
|
||||
// re-sends unchanged lightbar/LED/trigger state alongside every rumble update).
|
||||
if self.hidout_dedup[i].should_forward(&h) {
|
||||
hidout(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user