fix(host): trackpad Y rides the wire in screen convention, flips at the Deck boundary
Both Deck trackpads were inverted on the virtual pad (first live Deck-to-Deck session): the client sends SDL's screen-convention coordinates (+y down) onto the wire, but the Deck's raw report fields are stick convention (+y up) — and Steam Input parses our report as real Deck hardware — while the host applier passed the value straight through. Pin the wire meaning down (quic.rs: TouchpadEx/Touchpad are screen convention, +x right +y down — what every client capture API produces) and translate to device-raw in the host applier, the layer that knows the quirk: steam_proto's apply_rich now negates y (saturating: -32768 has no i16 negation) for both TouchpadEx surfaces and the DualSense-standin Touchpad arm, which carried the same latent inversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1349,8 +1349,10 @@ const RICH_TOUCHPAD_EX: u8 = 0x03;
|
|||||||
/// kind decodes to `None` and is dropped).
|
/// kind decodes to `None` and is dropped).
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum RichInput {
|
pub enum RichInput {
|
||||||
/// One touchpad contact. `x`/`y` are normalized `0..=65535` (the host scales to the
|
/// One touchpad contact. `x`/`y` are normalized `0..=65535` in SCREEN convention —
|
||||||
/// DualSense touchpad resolution); `active = false` lifts the finger.
|
/// origin top-left, +y DOWN, exactly what SDL/Windows/Android capture APIs produce
|
||||||
|
/// (the host scales to the DualSense touchpad resolution); `active = false` lifts
|
||||||
|
/// the finger.
|
||||||
Touchpad {
|
Touchpad {
|
||||||
pad: u8,
|
pad: u8,
|
||||||
finger: u8,
|
finger: u8,
|
||||||
@@ -1368,9 +1370,13 @@ pub enum RichInput {
|
|||||||
/// A richer trackpad contact that also identifies *which* physical pad (Steam Controller / Deck
|
/// A richer trackpad contact that also identifies *which* physical pad (Steam Controller / Deck
|
||||||
/// have two), carries a separate click vs touch state, and a pressure reading. `surface`:
|
/// have two), carries a separate click vs touch state, and a pressure reading. `surface`:
|
||||||
/// `0` = the single / DualSense touchpad, `1` = the Steam left pad, `2` = the Steam right pad.
|
/// `0` = the single / DualSense touchpad, `1` = the Steam left pad, `2` = the Steam right pad.
|
||||||
/// Coordinates are **signed** (centred at 0), matching the real Steam report; `pressure` is `0`
|
/// Coordinates are **signed** (centred at 0) in SCREEN convention — +x right, +y DOWN,
|
||||||
/// for a surface with no force sensor. New clients send this for every touch surface; the host
|
/// what every client capture API produces. Device-raw quirks are the HOST applier's job
|
||||||
/// decodes both `Touchpad` (`0x01`) and `TouchpadEx` (`0x03`) indefinitely.
|
/// (the Deck report is +y up: `steam_proto` flips it — the first live session shipped
|
||||||
|
/// clients that sent screen-y straight through, so the wire meaning is fixed as screen-y
|
||||||
|
/// and hosts translate). `pressure` is `0` for a surface with no force sensor. New clients
|
||||||
|
/// send this for every touch surface; the host decodes both `Touchpad` (`0x01`) and
|
||||||
|
/// `TouchpadEx` (`0x03`) indefinitely.
|
||||||
TouchpadEx {
|
TouchpadEx {
|
||||||
pad: u8,
|
pad: u8,
|
||||||
surface: u8,
|
surface: u8,
|
||||||
|
|||||||
@@ -236,13 +236,23 @@ impl SteamState {
|
|||||||
/// wire [`RichInput::Touchpad`] maps to the **right** trackpad (the Deck pad analogous to the
|
/// wire [`RichInput::Touchpad`] maps to the **right** trackpad (the Deck pad analogous to the
|
||||||
/// DualSense touchpad); the left pad arrives via the M3 `TouchpadEx` surface. [`RichInput::Motion`]
|
/// DualSense touchpad); the left pad arrives via the M3 `TouchpadEx` surface. [`RichInput::Motion`]
|
||||||
/// passes gyro/accel straight through (raw i16; cross-device unit scaling is M3).
|
/// passes gyro/accel straight through (raw i16; cross-device unit scaling is M3).
|
||||||
|
///
|
||||||
|
/// The wire's touch coordinates are SCREEN convention — +y DOWN, what SDL/Windows/Android
|
||||||
|
/// capture APIs all produce — but the Deck's raw trackpad fields are stick convention
|
||||||
|
/// (+y UP, centre origin), and Steam Input parses our report as real Deck hardware. Y is
|
||||||
|
/// therefore negated here, on the device boundary; leaving it through was the "both
|
||||||
|
/// trackpads inverted" bug the first live Deck-to-Deck session surfaced (2026-07-08).
|
||||||
pub fn apply_rich(&mut self, rich: RichInput) {
|
pub fn apply_rich(&mut self, rich: RichInput) {
|
||||||
|
/// Screen-convention (+down) wire Y → Deck raw (+up), saturating (-32768 has no i16 negation).
|
||||||
|
fn flip_y(y: i16) -> i16 {
|
||||||
|
(y as i32).saturating_neg().clamp(-32768, 32767) as i16
|
||||||
|
}
|
||||||
match rich {
|
match rich {
|
||||||
RichInput::Touchpad { active, x, y, .. } => {
|
RichInput::Touchpad { active, x, y, .. } => {
|
||||||
self.press(btn::RPAD_TOUCH, active);
|
self.press(btn::RPAD_TOUCH, active);
|
||||||
// Normalized 0..=65535 (centre 32768) → the pad's centred s16 range.
|
// Normalized 0..=65535 (centre 32768, +y down) → the pad's centred s16 range (+y up).
|
||||||
self.rpad_x = ((x as i32) - 32768) as i16;
|
self.rpad_x = ((x as i32) - 32768) as i16;
|
||||||
self.rpad_y = ((y as i32) - 32768) as i16;
|
self.rpad_y = (32768 - (y as i32)).min(32767) as i16;
|
||||||
}
|
}
|
||||||
RichInput::Motion { gyro, accel, .. } => {
|
RichInput::Motion { gyro, accel, .. } => {
|
||||||
// The wire carries DualSense-convention units (what every client capture emits); the
|
// The wire carries DualSense-convention units (what every client capture emits); the
|
||||||
@@ -259,18 +269,18 @@ impl SteamState {
|
|||||||
y,
|
y,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
// Steam pads are natively signed (centre 0), so x/y map straight in. surface 1 =
|
// Signed centre-0 x maps straight in; y flips to the Deck's +up. surface 1 =
|
||||||
// left pad, anything else (0 single / 2 right) = right pad.
|
// left pad, anything else (0 single / 2 right) = right pad.
|
||||||
if surface == 1 {
|
if surface == 1 {
|
||||||
self.press(btn::LPAD_TOUCH, touch);
|
self.press(btn::LPAD_TOUCH, touch);
|
||||||
self.press(btn::LPAD_CLICK, click);
|
self.press(btn::LPAD_CLICK, click);
|
||||||
self.lpad_x = x;
|
self.lpad_x = x;
|
||||||
self.lpad_y = y;
|
self.lpad_y = flip_y(y);
|
||||||
} else {
|
} else {
|
||||||
self.press(btn::RPAD_TOUCH, touch);
|
self.press(btn::RPAD_TOUCH, touch);
|
||||||
self.press(btn::RPAD_CLICK, click);
|
self.press(btn::RPAD_CLICK, click);
|
||||||
self.rpad_x = x;
|
self.rpad_x = x;
|
||||||
self.rpad_y = y;
|
self.rpad_y = flip_y(y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +567,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
assert_ne!(s.buttons & btn::RPAD_TOUCH, 0);
|
assert_ne!(s.buttons & btn::RPAD_TOUCH, 0);
|
||||||
assert_eq!(s.rpad_x, 32767); // 65535-32768
|
assert_eq!(s.rpad_x, 32767); // 65535-32768
|
||||||
assert_eq!(s.rpad_y, -32768); // 0-32768
|
assert_eq!(s.rpad_y, 32767); // wire y=0 = TOP (screen conv) → Deck raw +up (clamped)
|
||||||
// Motion is rescaled from the wire (DualSense) convention into Deck units (gyro ×16/20,
|
// Motion is rescaled from the wire (DualSense) convention into Deck units (gyro ×16/20,
|
||||||
// accel ×16384/10000) — see steam_remap::motion_wire_to_deck.
|
// accel ×16384/10000) — see steam_remap::motion_wire_to_deck.
|
||||||
s.apply_rich(RichInput::Motion {
|
s.apply_rich(RichInput::Motion {
|
||||||
@@ -570,7 +580,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// M3: the wire back-button bits map to the four Deck grips + QAM, and `TouchpadEx` routes the
|
/// M3: the wire back-button bits map to the four Deck grips + QAM, and `TouchpadEx` routes the
|
||||||
/// left / right surfaces to the matching pad (signed coords pass straight through).
|
/// left / right surfaces to the matching pad (x passes straight through; y flips from the
|
||||||
|
/// wire's screen convention (+down) to the Deck's raw +up — the live-verified direction).
|
||||||
#[test]
|
#[test]
|
||||||
fn back_buttons_and_dual_trackpad_mapping() {
|
fn back_buttons_and_dual_trackpad_mapping() {
|
||||||
let s = SteamState::from_gamepad(
|
let s = SteamState::from_gamepad(
|
||||||
@@ -601,7 +612,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
assert_ne!(s.buttons & btn::LPAD_TOUCH, 0);
|
assert_ne!(s.buttons & btn::LPAD_TOUCH, 0);
|
||||||
assert_ne!(s.buttons & btn::LPAD_CLICK, 0);
|
assert_ne!(s.buttons & btn::LPAD_CLICK, 0);
|
||||||
assert_eq!((s.lpad_x, s.lpad_y), (-5000, 6000));
|
assert_eq!((s.lpad_x, s.lpad_y), (-5000, -6000));
|
||||||
s.apply_rich(RichInput::TouchpadEx {
|
s.apply_rich(RichInput::TouchpadEx {
|
||||||
pad: 0,
|
pad: 0,
|
||||||
surface: 2,
|
surface: 2,
|
||||||
@@ -613,7 +624,20 @@ mod tests {
|
|||||||
pressure: 0,
|
pressure: 0,
|
||||||
});
|
});
|
||||||
assert_ne!(s.buttons & btn::RPAD_TOUCH, 0);
|
assert_ne!(s.buttons & btn::RPAD_TOUCH, 0);
|
||||||
assert_eq!((s.rpad_x, s.rpad_y), (7000, -8000));
|
assert_eq!((s.rpad_x, s.rpad_y), (7000, 8000));
|
||||||
|
|
||||||
|
// The i16 edge: wire y = -32768 (top-most) must clamp, not overflow.
|
||||||
|
s.apply_rich(RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface: 2,
|
||||||
|
finger: 0,
|
||||||
|
touch: true,
|
||||||
|
click: false,
|
||||||
|
x: 0,
|
||||||
|
y: -32768,
|
||||||
|
pressure: 0,
|
||||||
|
});
|
||||||
|
assert_eq!(s.rpad_y, 32767);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The serial reply carries the leading report-id byte the kernel strips, so the *stripped*
|
/// The serial reply carries the leading report-id byte the kernel strips, so the *stripped*
|
||||||
|
|||||||
Reference in New Issue
Block a user