fix(host/dualsense): report full battery + log rumble forwarding

Two DualSense (UHID) fixes surfaced live on the Bazzite host:

- Battery: serialize_state never set the input report's status byte (struct off 52 →
  r[53]), so hid-playstation read battery capacity 0 and SteamOS warned "low battery"
  even on a fully-charged pad. Set it to 0x0A (discharging, low nibble 0xA → 100 %) —
  a virtual pad has no real cell. (Forwarding the client pad's real charge is a later
  feature.) Regression assert added to the layout test.
- Rumble diagnostic: log the silent→active transition when forwarding a buzz on the
  0xCA plane, so a live test can tell "host never receives rumble from the game"
  (Steam Input / parse) apart from "client doesn't render it". Once per buzz, no spam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 16:14:28 +00:00
parent 5706e7ebf4
commit 01409d9d8a
2 changed files with 13 additions and 0 deletions
@@ -275,6 +275,11 @@ fn serialize_state(r: &mut [u8; DS_INPUT_REPORT_LEN], st: &DsState, seq: u8, ts:
r[28..32].copy_from_slice(&ts.to_le_bytes()); // sensor_timestamp (struct off 27) r[28..32].copy_from_slice(&ts.to_le_bytes()); // sensor_timestamp (struct off 27)
pack_touch(&mut r[33..37], &st.touch[0]); // touch point 1 (struct off 32) pack_touch(&mut r[33..37], &st.touch[0]); // touch point 1 (struct off 32)
pack_touch(&mut r[37..41], &st.touch[1]); // touch point 2 pack_touch(&mut r[37..41], &st.touch[1]); // touch point 2
// status byte (struct off 52 → r[53]) — hid-playstation reads battery here: low nibble =
// capacity (×10+5 %), high nibble = charging state (0 = discharging). A virtual pad has no
// real cell, so report "discharging, full" (0x0A → 100 %); leaving it 0 makes SteamOS / the
// kernel see ~5 % and warn "low battery". (We don't forward the client pad's real charge yet.)
r[53] = 0x0A;
} }
fn pack_touch(dst: &mut [u8], t: &Touch) { fn pack_touch(dst: &mut [u8], t: &Touch) {
@@ -754,6 +759,9 @@ mod tests {
assert_eq!(r[35], 0x61); // x_hi nibble 0x1 | (y & 0xF) << 4 (y=0x356 → 0x6 << 4) assert_eq!(r[35], 0x61); // x_hi nibble 0x1 | (y & 0xF) << 4 (y=0x356 → 0x6 << 4)
assert_eq!(r[36], 0x35); // y >> 4 assert_eq!(r[36], 0x35); // y >> 4
assert_eq!(r[37] & 0x80, 0x80); // touch point 2 inactive assert_eq!(r[37] & 0x80, 0x80); // touch point 2 inactive
// status byte (struct off 52): discharging (high nibble 0) + full capacity (low nibble
// 0xA → 100 %), so SteamOS/hid-playstation never reports a false "low battery".
assert_eq!(r[53], 0x0A);
} }
/// The wire touchpad-click bit (Moonlight's extended position) lands in `buttons[2]`. /// The wire touchpad-click bit (Moonlight's extended position) lands in `buttons[2]`.
+5
View File
@@ -1254,6 +1254,11 @@ fn input_thread(
pads.pump( pads.pump(
|pad, low, high| { |pad, low, high| {
if let Some(s) = rumble_state.get_mut(pad as usize) { if let Some(s) = rumble_state.get_mut(pad as usize) {
// Log the silent→active transition (once per buzz) so a live test can tell
// "host never gets rumble from the game" apart from "client doesn't render it".
if *s == (0, 0) && (low != 0 || high != 0) {
tracing::info!(pad, low, high, "rumble: forwarding to client (0xCA)");
}
*s = (low, high); *s = (low, high);
rumble_seen[pad as usize] = true; rumble_seen[pad as usize] = true;
} }