feat(gamepad): DualSense Edge backend — Linux UHID + Windows UMDF (N1)
The plain-DualSense transport + report codec under the Edge USB identity (054C:0DF2, verbatim 389-byte real-device descriptor cross-checked against the raw usbmon capture + hhd's production virtual Edge), so the wire back grips (BTN_PADDLE1..4: Deck L4/L5/R4/R5, Elite P1-P4) land on the Edge's NATIVE buttons[2] bits instead of the fold/drop policy: PADDLE1/2 -> the right/left back buttons, PADDLE3/4 -> the right/left Fn buttons (kernel BTN_TRIGGER_HAPPY1..4 on >= 7.2; SDL/Steam read hidraw on any kernel). - proto: Edge descriptor + btn2 bits + edge_paddle_bits(), pinned against hid-playstation DS_EDGE_BUTTONS_* and SDL_hidapi_ps5 (tests). - Linux: DsUhidIdentity parameterizes the UHID create; DsEdgeLinuxProto / DualSenseEdgeManager. Headless-validated on .21 (7.1): driver=playstation binds 0DF2, all 4 input devices created, probe lightbar/player-LED feedback round-trips; dualsense-test grew --edge (cycles all 4 paddles). - Windows: UMDF driver serves device_type=2 (Edge descriptor/attrs/strings, DS feature blobs); WinDsIdentity parameterizes the SwDevice profile + devtype stamp; DsEdgeWinProto / DualSenseEdgeWindowsManager; INF gains pf_dualsenseedge. Driver change => resign + reinstall before on-glass. - Router: DualSenseEdge arms in route_handle/apply_rich/pump/heartbeat; pick_gamepad folds Edge -> itself on linux||windows; degrade_if_no_uhid covers it. - Client (SDL): 054C:0DF2 declares DualSenseEdge (no distinct SDL type); Edge physical pads take the raw DS5 effects path; console-UI glyphs = Shapes. Apple/Android pickers follow separately. Verified: .21 clippy -D warnings + 292/0 host tests + on-box UHID bind smoke; .133 clippy pending in this push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -264,6 +264,7 @@ impl PadInfo {
|
||||
pub fn kind_label(&self) -> &'static str {
|
||||
match self.pref {
|
||||
GamepadPref::DualSense => "DualSense",
|
||||
GamepadPref::DualSenseEdge => "DualSense Edge",
|
||||
GamepadPref::DualShock4 => "DualShock 4",
|
||||
GamepadPref::XboxOne => "Xbox One",
|
||||
GamepadPref::SteamDeck => "Steam Deck",
|
||||
@@ -783,6 +784,12 @@ impl Worker {
|
||||
if vid == 0x28DE && matches!(pid, 0x1205 | 0x1102 | 0x1142) {
|
||||
pref = GamepadPref::SteamDeck;
|
||||
}
|
||||
// The DualSense Edge has no distinct SDL gamepad type either (it reports PS5) — detect by
|
||||
// VID/PID so the host builds the virtual Edge and this pad's back paddles land on native
|
||||
// slots instead of the fold/drop policy.
|
||||
if vid == 0x054C && pid == 0x0DF2 {
|
||||
pref = GamepadPref::DualSenseEdge;
|
||||
}
|
||||
let name = self
|
||||
.subsystem
|
||||
.name_for_id(jid)
|
||||
@@ -1556,7 +1563,12 @@ impl Worker {
|
||||
let Some(slot) = self.slots.iter_mut().find(|s| s.index == idx) else {
|
||||
continue;
|
||||
};
|
||||
let is_ds = slot.pref == GamepadPref::DualSense;
|
||||
// A physical Edge takes the same raw DS5 effects packets (SDL's DS5EffectsState_t
|
||||
// layout is shared; SDL keys the enhanced path off the Edge PID itself).
|
||||
let is_ds = matches!(
|
||||
slot.pref,
|
||||
GamepadPref::DualSense | GamepadPref::DualSenseEdge
|
||||
);
|
||||
match hid {
|
||||
HidOutput::Led { r, g, b, .. } if is_ds => {
|
||||
let _ = slot.pad.send_effect(&Ds5Feedback::lightbar_packet(r, g, b));
|
||||
|
||||
@@ -22,7 +22,9 @@ pub(crate) enum GlyphStyle {
|
||||
impl GlyphStyle {
|
||||
pub(crate) fn from_pref(pref: Option<GamepadPref>) -> GlyphStyle {
|
||||
match pref {
|
||||
Some(GamepadPref::DualSense | GamepadPref::DualShock4) => GlyphStyle::Shapes,
|
||||
Some(
|
||||
GamepadPref::DualSense | GamepadPref::DualSenseEdge | GamepadPref::DualShock4,
|
||||
) => GlyphStyle::Shapes,
|
||||
Some(_) => GlyphStyle::Letters,
|
||||
None => GlyphStyle::Keyboard,
|
||||
}
|
||||
|
||||
@@ -531,10 +531,13 @@ pub mod gamepad {
|
||||
pub const PAD_MAGIC: u32 = 0x5046_4453;
|
||||
|
||||
/// `device_type` selector the `pf_dualsense` driver reads to pick its HID identity. The section is
|
||||
/// zeroed, so `0` = DualSense is the default; one driver serves either identity.
|
||||
/// zeroed, so `0` = DualSense is the default; one driver serves every identity.
|
||||
pub const DEVTYPE_DUALSENSE: u8 = 0;
|
||||
/// `device_type` = DualShock 4 (`VID_054C&PID_09CC` HID identity).
|
||||
pub const DEVTYPE_DUALSHOCK4: u8 = 1;
|
||||
/// `device_type` = DualSense Edge (`VID_054C&PID_0DF2` HID identity — the DualSense report
|
||||
/// codec plus the four native back/Fn button bits).
|
||||
pub const DEVTYPE_DUALSENSE_EDGE: u8 = 2;
|
||||
|
||||
/// The value a gamepad driver writes into its section's `driver_proto` field once it attaches —
|
||||
/// the host's positive "driver is alive on this section" signal (health check + version audit).
|
||||
|
||||
@@ -482,6 +482,11 @@ pub mod dualsense_proto;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "inject/windows/dualsense_windows.rs"]
|
||||
pub mod dualsense_windows;
|
||||
/// Windows: virtual DualSense **Edge** via the same UMDF minidriver + shared-memory channel
|
||||
/// (device-type 2) — the wire back grips land on the Edge's native back/Fn buttons.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "inject/windows/dualsense_edge_windows.rs"]
|
||||
pub mod dualsense_edge_windows;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "inject/linux/dualshock4.rs"]
|
||||
pub mod dualshock4;
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
//! 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,
|
||||
DS_FEATURE_FIRMWARE, DS_FEATURE_PAIRING, DS_INPUT_REPORT_LEN, DS_PRODUCT, DS_TOUCH_H,
|
||||
DS_TOUCH_W, DS_VENDOR, DUALSENSE_RDESC,
|
||||
edge_paddle_bits, parse_ds_output, serialize_state, DsFeedback, DsState,
|
||||
DS_EDGE_PRODUCT, DS_FEATURE_CALIBRATION, DS_FEATURE_FIRMWARE, DS_FEATURE_PAIRING,
|
||||
DS_INPUT_REPORT_LEN, DS_PRODUCT, DS_TOUCH_H, DS_TOUCH_W, DS_VENDOR, DUALSENSE_EDGE_RDESC,
|
||||
DUALSENSE_RDESC,
|
||||
};
|
||||
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager};
|
||||
use anyhow::{Context, Result};
|
||||
@@ -43,9 +44,45 @@ fn put_cstr(ev: &mut [u8], off: usize, cap: usize, s: &str) {
|
||||
ev[off..off + n].copy_from_slice(&s.as_bytes()[..n]); // rest already zero (NUL-terminated)
|
||||
}
|
||||
|
||||
/// A virtual DualSense backed by `/dev/uhid` (hand-rolled codec — no bindgen, mirroring the
|
||||
/// uinput pad's style). Dropping it destroys the device (the kernel tears down the bound
|
||||
/// `hid-playstation` interface).
|
||||
/// The UHID identity a [`DualSensePad`] is created with — the plain DualSense or the Edge (same
|
||||
/// driver, same report codec; the Edge differs by PID + descriptor and carries the four extra
|
||||
/// `buttons[2]` bits). Mirrors the uinput pad's `PadIdentity` shape.
|
||||
pub struct DsUhidIdentity {
|
||||
product: u32,
|
||||
rdesc: &'static [u8],
|
||||
/// Device name prefix ("Punktfunk <name> <index>").
|
||||
name: &'static str,
|
||||
/// Path token for the phys string ("punktfunk/<phys>/<index>").
|
||||
phys: &'static str,
|
||||
/// Short slug for the uniq string ("punktfunk-<slug>-<index>").
|
||||
slug: &'static str,
|
||||
}
|
||||
|
||||
impl DsUhidIdentity {
|
||||
pub const fn dualsense() -> DsUhidIdentity {
|
||||
DsUhidIdentity {
|
||||
product: DS_PRODUCT,
|
||||
rdesc: DUALSENSE_RDESC,
|
||||
name: "DualSense",
|
||||
phys: "dualsense",
|
||||
slug: "ds",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn dualsense_edge() -> DsUhidIdentity {
|
||||
DsUhidIdentity {
|
||||
product: DS_EDGE_PRODUCT,
|
||||
rdesc: DUALSENSE_EDGE_RDESC,
|
||||
name: "DualSense Edge",
|
||||
phys: "dualsense-edge",
|
||||
slug: "dsedge",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual DualSense / DualSense Edge backed by `/dev/uhid` (hand-rolled codec — no bindgen,
|
||||
/// mirroring the uinput pad's style). Dropping it destroys the device (the kernel tears down the
|
||||
/// bound `hid-playstation` interface).
|
||||
pub struct DualSensePad {
|
||||
fd: File,
|
||||
seq: u8,
|
||||
@@ -53,8 +90,9 @@ pub struct DualSensePad {
|
||||
}
|
||||
|
||||
impl DualSensePad {
|
||||
/// Create the UHID DualSense for pad `index` (used only to make the device name/uniq unique).
|
||||
pub fn open(index: u8) -> Result<DualSensePad> {
|
||||
/// Create the UHID pad for wire index `index` under `id`'s identity (`index` is used only to
|
||||
/// make the device name/uniq unique).
|
||||
pub fn open(index: u8, id: &DsUhidIdentity) -> Result<DualSensePad> {
|
||||
let fd = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
@@ -64,24 +102,24 @@ impl DualSensePad {
|
||||
format!("open {UHID_PATH} (is the 60-punktfunk.rules uhid rule installed + are you in 'input'?)")
|
||||
})?;
|
||||
let mut ds = DualSensePad { fd, seq: 0, ts: 0 };
|
||||
ds.send_create2(index).context("UHID_CREATE2 DualSense")?;
|
||||
ds.send_create2(index, id).context("UHID_CREATE2 DualSense")?;
|
||||
Ok(ds)
|
||||
}
|
||||
|
||||
fn send_create2(&mut self, index: u8) -> Result<()> {
|
||||
fn send_create2(&mut self, index: u8, id: &DsUhidIdentity) -> Result<()> {
|
||||
let mut ev = [0u8; UHID_EVENT_SIZE];
|
||||
ev[0..4].copy_from_slice(&UHID_CREATE2.to_ne_bytes());
|
||||
// union (uhid_create2_req) starts at byte 4.
|
||||
put_cstr(&mut ev, 4, 128, &format!("Punktfunk DualSense {index}")); // name[128]
|
||||
put_cstr(&mut ev, 132, 64, &format!("punktfunk/dualsense/{index}")); // phys[64]
|
||||
put_cstr(&mut ev, 196, 64, &format!("punktfunk-ds-{index}")); // uniq[64]
|
||||
ev[260..262].copy_from_slice(&(DUALSENSE_RDESC.len() as u16).to_ne_bytes()); // rd_size
|
||||
put_cstr(&mut ev, 4, 128, &format!("Punktfunk {} {index}", id.name)); // name[128]
|
||||
put_cstr(&mut ev, 132, 64, &format!("punktfunk/{}/{index}", id.phys)); // phys[64]
|
||||
put_cstr(&mut ev, 196, 64, &format!("punktfunk-{}-{index}", id.slug)); // uniq[64]
|
||||
ev[260..262].copy_from_slice(&(id.rdesc.len() as u16).to_ne_bytes()); // rd_size
|
||||
ev[262..264].copy_from_slice(&BUS_USB.to_ne_bytes()); // bus
|
||||
ev[264..268].copy_from_slice(&DS_VENDOR.to_ne_bytes());
|
||||
ev[268..272].copy_from_slice(&DS_PRODUCT.to_ne_bytes());
|
||||
ev[268..272].copy_from_slice(&id.product.to_ne_bytes());
|
||||
ev[272..276].copy_from_slice(&0x0100u32.to_ne_bytes()); // version
|
||||
ev[276..280].copy_from_slice(&0u32.to_ne_bytes()); // country
|
||||
ev[280..280 + DUALSENSE_RDESC.len()].copy_from_slice(DUALSENSE_RDESC); // rd_data
|
||||
ev[280..280 + id.rdesc.len()].copy_from_slice(id.rdesc); // rd_data
|
||||
self.fd.write_all(&ev).context("write UHID_CREATE2")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -186,7 +224,7 @@ impl PadProto for DsLinuxProto {
|
||||
const CREATE_HINT: &'static str = "";
|
||||
|
||||
fn open(&mut self, idx: u8) -> Result<DualSensePad> {
|
||||
let p = DualSensePad::open(idx)?;
|
||||
let p = DualSensePad::open(idx, &DsUhidIdentity::dualsense())?;
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual DualSense created (UHID hid-playstation)"
|
||||
@@ -251,3 +289,83 @@ impl PadProto for DsLinuxProto {
|
||||
/// source changes, and heartbeats it through input silence (a real DualSense streams report `0x01`
|
||||
/// continuously — `hid-playstation`/Proton/SDL treat a multi-second gap as an unplug).
|
||||
pub type DualSenseManager = UhidManager<DsLinuxProto>;
|
||||
|
||||
/// The DualSense **Edge** half of the shared stateful manager: the plain-DualSense transport and
|
||||
/// report codec under the Edge USB identity (`054C:0DF2` + the Edge descriptor), with the four
|
||||
/// wire back-grip bits mapped onto the Edge's native `buttons[2]` slots instead of the
|
||||
/// fold/drop policy — the whole point of this backend (a client's Deck grips / Elite paddles
|
||||
/// stop vanishing). No remap config: every paddle has a native home.
|
||||
///
|
||||
/// Kernel note: `hid-playstation` binds the Edge PID since 6.1 (forced vibration-v2 output), but
|
||||
/// only kernels ≥ 7.2 surface the Fn/back bits as evdev keys (`BTN_TRIGGER_HAPPY1..4`); SDL /
|
||||
/// Steam Input read the report off hidraw and see them on any kernel.
|
||||
#[derive(Default)]
|
||||
pub struct DsEdgeLinuxProto;
|
||||
|
||||
impl PadProto for DsEdgeLinuxProto {
|
||||
type Pad = DualSensePad;
|
||||
type State = DsState;
|
||||
const LABEL: &'static str = "DualSense Edge";
|
||||
const DEVICE: &'static str = "DualSense Edge";
|
||||
const CREATE_HINT: &'static str = "";
|
||||
|
||||
fn open(&mut self, idx: u8) -> Result<DualSensePad> {
|
||||
let p = DualSensePad::open(idx, &DsUhidIdentity::dualsense_edge())?;
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual DualSense Edge created (UHID hid-playstation)"
|
||||
);
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn neutral(&self) -> DsState {
|
||||
DsState::neutral()
|
||||
}
|
||||
|
||||
/// Merge buttons/sticks/triggers from the frame, preserving the rich-plane fields — like the
|
||||
/// plain DualSense, EXCEPT the wire paddles are not folded away: they land on the Edge's own
|
||||
/// `buttons[2]` bits (rebuilt from every button frame, so no extra persistence).
|
||||
fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState {
|
||||
let mut s = DsState::from_gamepad(
|
||||
f.buttons,
|
||||
f.ls_x,
|
||||
f.ls_y,
|
||||
f.rs_x,
|
||||
f.rs_y,
|
||||
f.left_trigger,
|
||||
f.right_trigger,
|
||||
);
|
||||
s.buttons[2] |= edge_paddle_bits(f.buttons);
|
||||
s.touch = prev.touch;
|
||||
s.gyro = prev.gyro;
|
||||
s.accel = prev.accel;
|
||||
s.touch_click = prev.touch_click;
|
||||
s
|
||||
}
|
||||
|
||||
/// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam dual pads
|
||||
/// split the one touchpad left/right, pad clicks ride touch_click.
|
||||
fn apply_rich(&self, st: &mut DsState, rich: RichInput) {
|
||||
st.apply_rich(rich, DS_TOUCH_W, DS_TOUCH_H);
|
||||
}
|
||||
|
||||
fn write_state(&self, pad: &mut DualSensePad, st: &DsState) {
|
||||
let _ = pad.write_state(st);
|
||||
}
|
||||
|
||||
/// Same kernel handshake + feedback parse as the plain DualSense — the Edge's GET_REPORT set
|
||||
/// (calibration 0x05 / pairing 0x09 / firmware 0x20) and output report 0x02 are identical
|
||||
/// (the Edge's rumble arrives via the vibration-v2 valid_flag2 bit, which
|
||||
/// [`parse_ds_output`] already handles).
|
||||
fn service(&self, pad: &mut DualSensePad, idx: u8) -> PadFeedback {
|
||||
let fb = pad.service(idx);
|
||||
PadFeedback {
|
||||
rumble: fb.rumble,
|
||||
hidout: fb.hidout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All virtual DualSense Edge pads of a session — `PUNKTFUNK_GAMEPAD=edge`, or the per-pad kind a
|
||||
/// client declares for a paddle-bearing physical controller.
|
||||
pub type DualSenseEdgeManager = UhidManager<DsEdgeLinuxProto>;
|
||||
|
||||
@@ -66,8 +66,45 @@ pub const DUALSENSE_RDESC: &[u8] = &[
|
||||
0xC0,
|
||||
];
|
||||
|
||||
/// Sony DualSense **Edge** USB HID report descriptor (389 bytes) — a verbatim real-device
|
||||
/// capture (hid-recorder, hhd-dev/hwinfo `devices/ds5_edge`, cross-checked byte-for-byte against
|
||||
/// the raw usbmon pcap in the same repo and the descriptor Handheld Daemon ships for ITS virtual
|
||||
/// UHID Edge). vs the plain DS5 descriptor: output report `0x02` grows 47→63 bytes, feature
|
||||
/// `0xF2` 15→52, and 19 vendor feature reports (`0x60..=0x7B`, the Edge profile slots) are
|
||||
/// appended — input report `0x01` is bit-identical (the Edge's Fn/back buttons ride previously
|
||||
/// reserved bits of `buttons[2]`, see [`btn2`]).
|
||||
#[rustfmt::skip]
|
||||
pub const DUALSENSE_EDGE_RDESC: &[u8] = &[
|
||||
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35,
|
||||
0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06,
|
||||
0x00, 0xFF, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07,
|
||||
0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05,
|
||||
0x09, 0x19, 0x01, 0x29, 0x0F, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02, 0x06,
|
||||
0x00, 0xFF, 0x09, 0x21, 0x95, 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x22, 0x15, 0x00, 0x26,
|
||||
0xFF, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x3F, 0x91, 0x02,
|
||||
0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xB1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2F, 0xB1, 0x02,
|
||||
0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xB1, 0x02, 0x85, 0x0A, 0x09, 0x25, 0x95, 0x1A, 0xB1, 0x02,
|
||||
0x85, 0x20, 0x09, 0x26, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xB1, 0x02,
|
||||
0x85, 0x22, 0x09, 0x40, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3F, 0xB1, 0x02,
|
||||
0x85, 0x81, 0x09, 0x29, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x2A, 0x95, 0x09, 0xB1, 0x02,
|
||||
0x85, 0x83, 0x09, 0x2B, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x84, 0x09, 0x2C, 0x95, 0x3F, 0xB1, 0x02,
|
||||
0x85, 0x85, 0x09, 0x2D, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xA0, 0x09, 0x2E, 0x95, 0x01, 0xB1, 0x02,
|
||||
0x85, 0xE0, 0x09, 0x2F, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x30, 0x95, 0x3F, 0xB1, 0x02,
|
||||
0x85, 0xF1, 0x09, 0x31, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2, 0x09, 0x32, 0x95, 0x34, 0xB1, 0x02,
|
||||
0x85, 0xF4, 0x09, 0x35, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF5, 0x09, 0x36, 0x95, 0x03, 0xB1, 0x02,
|
||||
0x85, 0x60, 0x09, 0x41, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x61, 0x09, 0x42, 0xB1, 0x02, 0x85, 0x62,
|
||||
0x09, 0x43, 0xB1, 0x02, 0x85, 0x63, 0x09, 0x44, 0xB1, 0x02, 0x85, 0x64, 0x09, 0x45, 0xB1, 0x02,
|
||||
0x85, 0x65, 0x09, 0x46, 0xB1, 0x02, 0x85, 0x68, 0x09, 0x47, 0xB1, 0x02, 0x85, 0x70, 0x09, 0x48,
|
||||
0xB1, 0x02, 0x85, 0x71, 0x09, 0x49, 0xB1, 0x02, 0x85, 0x72, 0x09, 0x4A, 0xB1, 0x02, 0x85, 0x73,
|
||||
0x09, 0x4B, 0xB1, 0x02, 0x85, 0x74, 0x09, 0x4C, 0xB1, 0x02, 0x85, 0x75, 0x09, 0x4D, 0xB1, 0x02,
|
||||
0x85, 0x76, 0x09, 0x4E, 0xB1, 0x02, 0x85, 0x77, 0x09, 0x4F, 0xB1, 0x02, 0x85, 0x78, 0x09, 0x50,
|
||||
0xB1, 0x02, 0x85, 0x79, 0x09, 0x51, 0xB1, 0x02, 0x85, 0x7A, 0x09, 0x52, 0xB1, 0x02, 0x85, 0x7B,
|
||||
0x09, 0x53, 0xB1, 0x02, 0xC0,
|
||||
];
|
||||
|
||||
pub const DS_VENDOR: u32 = 0x054C; // Sony Interactive Entertainment
|
||||
pub const DS_PRODUCT: u32 = 0x0CE6; // DualSense Wireless Controller
|
||||
pub const DS_EDGE_PRODUCT: u32 = 0x0DF2; // DualSense Edge Wireless Controller
|
||||
/// USB input report `0x01` is 64 bytes total (report id + 63-byte body).
|
||||
pub const DS_INPUT_REPORT_LEN: usize = 64;
|
||||
/// The DualSense touchpad's reported resolution (the kernel exposes it as ABS_MT 0..1920/1080).
|
||||
@@ -92,12 +129,47 @@ pub mod btn1 {
|
||||
pub const L3: u8 = 0x40;
|
||||
pub const R3: u8 = 0x80;
|
||||
}
|
||||
/// `buttons[2]`: PS, touchpad click, mute (+ a rolling counter in the high bits).
|
||||
/// `buttons[2]`: PS, touchpad click, mute — plus, on the DualSense **Edge**, the two Fn and two
|
||||
/// back buttons in bits 4–7 (kernel `DS_EDGE_BUTTONS_*` / SDL `SDL_GAMEPAD_BUTTON_PS5_*`; the
|
||||
/// plain DS5 leaves those bits reserved). The kernel maps them to `BTN_TRIGGER_HAPPY1..4`
|
||||
/// (Fn-L, Fn-R, back-L, back-R) since 7.2; SDL/Steam read them off hidraw on any kernel.
|
||||
pub mod btn2 {
|
||||
pub const PS: u8 = 0x01;
|
||||
pub const TOUCHPAD: u8 = 0x02;
|
||||
/// Mic-mute / capture button — set from the wire `BTN_MISC1` in `DsState::from_gamepad`.
|
||||
pub const MUTE: u8 = 0x04;
|
||||
/// Edge left Fn button (below the left stick).
|
||||
pub const EDGE_FN_LEFT: u8 = 0x10;
|
||||
/// Edge right Fn button.
|
||||
pub const EDGE_FN_RIGHT: u8 = 0x20;
|
||||
/// Edge left back button (rear paddle).
|
||||
pub const EDGE_BACK_LEFT: u8 = 0x40;
|
||||
/// Edge right back button (rear paddle).
|
||||
pub const EDGE_BACK_RIGHT: u8 = 0x80;
|
||||
}
|
||||
|
||||
/// Map the wire back-grip bits onto the DualSense Edge's `buttons[2]` bits — the reason the Edge
|
||||
/// backend exists: all four client paddles (Deck grips L4/L5/R4/R5, Elite P1–P4) land on native
|
||||
/// slots instead of the fold/drop policy. Wire PADDLE1/2 = R4/L4 (the primary pair, Steam
|
||||
/// convention) → the Edge's right/left BACK buttons; PADDLE3/4 = R5/L5 → the right/left Fn
|
||||
/// buttons (real-HW Fn is profile-switch chrome, but on a virtual pad the bits reach consumers
|
||||
/// as ordinary buttons — kernel `BTN_TRIGGER_HAPPY1/2`, SDL `LEFT/RIGHT_FUNCTION`).
|
||||
pub fn edge_paddle_bits(buttons: u32) -> u8 {
|
||||
use punktfunk_core::input::gamepad as gs;
|
||||
let mut b = 0;
|
||||
if buttons & gs::BTN_PADDLE1 != 0 {
|
||||
b |= btn2::EDGE_BACK_RIGHT; // R4
|
||||
}
|
||||
if buttons & gs::BTN_PADDLE2 != 0 {
|
||||
b |= btn2::EDGE_BACK_LEFT; // L4
|
||||
}
|
||||
if buttons & gs::BTN_PADDLE3 != 0 {
|
||||
b |= btn2::EDGE_FN_RIGHT; // R5
|
||||
}
|
||||
if buttons & gs::BTN_PADDLE4 != 0 {
|
||||
b |= btn2::EDGE_FN_LEFT; // L5
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// One touchpad contact for the report.
|
||||
@@ -798,6 +870,51 @@ mod tests {
|
||||
assert_eq!(s.buttons[2], 0);
|
||||
}
|
||||
|
||||
/// The Edge paddle map, pinned against hid-playstation's `DS_EDGE_BUTTONS_*` masks (bits
|
||||
/// 4–7 of `buttons[2]`) and SDL's `SDL_GAMEPAD_BUTTON_PS5_*` (same byte off hidraw):
|
||||
/// PADDLE1/2 (R4/L4) → right/left BACK, PADDLE3/4 (R5/L5) → right/left Fn — and the mapped
|
||||
/// bits land in the serialized report's byte 10 next to the ordinary buttons[2] bits.
|
||||
#[test]
|
||||
fn edge_paddles_map_to_native_bits() {
|
||||
use punktfunk_core::input::gamepad as gs;
|
||||
assert_eq!(edge_paddle_bits(0), 0);
|
||||
assert_eq!(edge_paddle_bits(gs::BTN_PADDLE1), btn2::EDGE_BACK_RIGHT);
|
||||
assert_eq!(edge_paddle_bits(gs::BTN_PADDLE2), btn2::EDGE_BACK_LEFT);
|
||||
assert_eq!(edge_paddle_bits(gs::BTN_PADDLE3), btn2::EDGE_FN_RIGHT);
|
||||
assert_eq!(edge_paddle_bits(gs::BTN_PADDLE4), btn2::EDGE_FN_LEFT);
|
||||
// Exact kernel/SDL bit values (a one-bit slip ships dead paddles).
|
||||
assert_eq!(btn2::EDGE_FN_LEFT, 0x10);
|
||||
assert_eq!(btn2::EDGE_FN_RIGHT, 0x20);
|
||||
assert_eq!(btn2::EDGE_BACK_LEFT, 0x40);
|
||||
assert_eq!(btn2::EDGE_BACK_RIGHT, 0x80);
|
||||
// All four + a non-paddle bit: paddles map, the rest is ignored here.
|
||||
let all = gs::BTN_PADDLE1 | gs::BTN_PADDLE2 | gs::BTN_PADDLE3 | gs::BTN_PADDLE4 | gs::BTN_A;
|
||||
assert_eq!(edge_paddle_bits(all), 0xF0);
|
||||
// Serialized: the Edge merge ORs into buttons[2]; byte 10 carries both the paddles and
|
||||
// the ordinary bits (e.g. a simultaneous PS press).
|
||||
let mut s = DsState::from_gamepad(gs::BTN_GUIDE, 0, 0, 0, 0, 0, 0);
|
||||
s.buttons[2] |= edge_paddle_bits(gs::BTN_PADDLE2 | gs::BTN_PADDLE3);
|
||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||
serialize_state(&mut r, &s, 0, 0);
|
||||
assert_eq!(r[10], btn2::PS | btn2::EDGE_BACK_LEFT | btn2::EDGE_FN_RIGHT);
|
||||
}
|
||||
|
||||
/// The Edge descriptor is the real-device capture: exact length, the three deltas vs the
|
||||
/// plain DS5 descriptor (output 0x02 count 63, feature 0xF2 count 52, the appended profile
|
||||
/// feature reports), and an unchanged input-report prefix (report 0x01 is bit-identical —
|
||||
/// the serializer needs no Edge variant).
|
||||
#[test]
|
||||
fn edge_descriptor_shape() {
|
||||
assert_eq!(DUALSENSE_RDESC.len(), 273);
|
||||
assert_eq!(DUALSENSE_EDGE_RDESC.len(), 389);
|
||||
// Identical through the input-report + output-report-id prefix; the first delta is the
|
||||
// output report 0x02's Report Count at offset 109 (47 → 63 bytes of payload).
|
||||
assert_eq!(DUALSENSE_EDGE_RDESC[..109], DUALSENSE_RDESC[..109]);
|
||||
assert_eq!(DUALSENSE_RDESC[109], 0x2F);
|
||||
assert_eq!(DUALSENSE_EDGE_RDESC[109], 0x3F);
|
||||
assert_eq!(*DUALSENSE_EDGE_RDESC.last().unwrap(), 0xC0);
|
||||
}
|
||||
|
||||
/// A short / wrong-id report yields nothing.
|
||||
#[test]
|
||||
fn parse_output_rejects_garbage() {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Virtual Sony DualSense **Edge** on Windows via the UMDF minidriver — the Edge sibling of
|
||||
//! [`super::dualsense_windows`]. Same transport ([`DsWinPad`]: a per-session `SwDeviceCreate`
|
||||
//! devnode + the sealed shared-memory channel), same report codec ([`super::dualsense_proto`]);
|
||||
//! the host stamps `device_type = 2` so the one UMDF driver serves the Edge descriptor /
|
||||
//! `VID_054C&PID_0DF2` attributes, and the wire back-grip bits map onto the Edge's native
|
||||
//! `buttons[2]` slots instead of the fold/drop policy — a client's Deck grips / Elite paddles
|
||||
//! reach games as real buttons. Feedback is identical to the plain DualSense (rumble arrives with
|
||||
//! the vibration-v2 flag, which [`parse_ds_output`](super::dualsense_proto::parse_ds_output)
|
||||
//! already handles).
|
||||
|
||||
use super::dualsense_proto::{edge_paddle_bits, DsState, DS_TOUCH_H, DS_TOUCH_W};
|
||||
use super::dualsense_windows::{DsWinPad, WinDsIdentity};
|
||||
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager};
|
||||
use anyhow::Result;
|
||||
use punktfunk_core::quic::RichInput;
|
||||
|
||||
/// The Windows-Edge half of the shared stateful manager (see [`PadProto`]): the shared
|
||||
/// [`DsWinPad`] transport under the Edge identity, with the Edge paddle mapping in `merge_frame`.
|
||||
/// No remap config — every wire paddle has a native slot.
|
||||
#[derive(Default)]
|
||||
pub struct DsEdgeWinProto;
|
||||
|
||||
impl PadProto for DsEdgeWinProto {
|
||||
type Pad = DsWinPad;
|
||||
type State = DsState;
|
||||
const LABEL: &'static str = "DualSense Edge/Windows";
|
||||
const DEVICE: &'static str = "DualSense Edge";
|
||||
const CREATE_HINT: &'static str =
|
||||
" (install/repair: punktfunk-host.exe driver install --gamepad)";
|
||||
|
||||
fn open(&mut self, idx: u8) -> Result<DsWinPad> {
|
||||
let p = DsWinPad::open(idx, &WinDsIdentity::dualsense_edge())?;
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual DualSense Edge created (Windows UMDF shm channel)"
|
||||
);
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn neutral(&self) -> DsState {
|
||||
DsState::neutral()
|
||||
}
|
||||
|
||||
/// Merge buttons/sticks/triggers from the frame, preserving the rich-plane fields — like the
|
||||
/// plain DualSense, EXCEPT the wire paddles land on the Edge's own `buttons[2]` bits
|
||||
/// (rebuilt from every button frame, so no extra persistence).
|
||||
fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState {
|
||||
let mut s = DsState::from_gamepad(
|
||||
f.buttons,
|
||||
f.ls_x,
|
||||
f.ls_y,
|
||||
f.rs_x,
|
||||
f.rs_y,
|
||||
f.left_trigger,
|
||||
f.right_trigger,
|
||||
);
|
||||
s.buttons[2] |= edge_paddle_bits(f.buttons);
|
||||
s.touch = prev.touch;
|
||||
s.gyro = prev.gyro;
|
||||
s.accel = prev.accel;
|
||||
s.touch_click = prev.touch_click;
|
||||
s
|
||||
}
|
||||
|
||||
/// The shared DualSense-family mapping (dualsense_proto::DsState::apply_rich): Steam dual pads
|
||||
/// split the one touchpad left/right, pad clicks ride touch_click.
|
||||
fn apply_rich(&self, st: &mut DsState, rich: RichInput) {
|
||||
st.apply_rich(rich, DS_TOUCH_W, DS_TOUCH_H);
|
||||
}
|
||||
|
||||
fn write_state(&self, pad: &mut DsWinPad, st: &DsState) {
|
||||
pad.write_state(st);
|
||||
}
|
||||
|
||||
/// Poll the section for a game's feedback: motor rumble on the universal 0xCA plane, the rich
|
||||
/// lightbar/player-LED/trigger events on the 0xCD plane.
|
||||
fn service(&self, pad: &mut DsWinPad, idx: u8) -> PadFeedback {
|
||||
let fb = pad.service(idx);
|
||||
PadFeedback {
|
||||
rumble: fb.rumble,
|
||||
hidout: fb.hidout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All virtual DualSense Edge pads of a session — the Windows analogue of
|
||||
/// [`DualSenseEdgeManager`](crate::inject::dualsense::DualSenseEdgeManager), with the same method
|
||||
/// surface (via the shared [`UhidManager`]) as the other Windows pad managers.
|
||||
pub type DualSenseEdgeWindowsManager = UhidManager<DsEdgeWinProto>;
|
||||
@@ -55,6 +55,7 @@ pub(super) const OFF_DRIVER_PROTO: usize =
|
||||
pub(super) const OFF_PAD_INDEX: usize =
|
||||
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, pad_index);
|
||||
pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSHOCK4;
|
||||
pub(super) const DEVTYPE_DUALSENSE_EDGE: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSENSE_EDGE;
|
||||
|
||||
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
|
||||
/// loads on it and the HID DualSense appears to games) plus the sealed shared-memory channel.
|
||||
@@ -228,20 +229,57 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<
|
||||
Ok((hsw, ctx.instance_id()))
|
||||
}
|
||||
|
||||
/// The identity a [`DsWinPad`] enumerates with — the plain DualSense or the Edge share the whole
|
||||
/// transport (section layout, input report shape, output parse); only the `device_type` stamp and
|
||||
/// the PnP identity differ. The DS4 differs in report codec too, so it keeps its own pad type.
|
||||
pub(super) struct WinDsIdentity {
|
||||
/// `device_type` stamped into the section (the driver picks its HID identity off it).
|
||||
pub devtype: u8,
|
||||
/// PnP instance-id prefix (`pf_pad` / `pf_edge`) — distinct namespaces per type.
|
||||
pub instance_prefix: &'static str,
|
||||
/// The INF-matched hardware id.
|
||||
pub hwid: &'static str,
|
||||
/// The USB VID&PID token for the synthesized bus identity.
|
||||
pub usb_vid_pid: &'static str,
|
||||
/// Device Manager description.
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
impl WinDsIdentity {
|
||||
pub(super) const fn dualsense() -> WinDsIdentity {
|
||||
WinDsIdentity {
|
||||
devtype: 0,
|
||||
instance_prefix: "pf_pad",
|
||||
hwid: "pf_dualsense",
|
||||
usb_vid_pid: "VID_054C&PID_0CE6",
|
||||
description: "punktfunk Virtual DualSense",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn dualsense_edge() -> WinDsIdentity {
|
||||
WinDsIdentity {
|
||||
devtype: DEVTYPE_DUALSENSE_EDGE,
|
||||
instance_prefix: "pf_edge",
|
||||
hwid: "pf_dualsenseedge",
|
||||
usb_vid_pid: "VID_054C&PID_0DF2",
|
||||
description: "punktfunk Virtual DualSense Edge",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DsWinPad {
|
||||
/// Create the sealed channel (unnamed DATA section + `Global\pfds-boot-<index>` mailbox), stamp
|
||||
/// the pad index + neutral report + the magic LAST, then spawn the `pf_pad_<index>` devnode (the
|
||||
/// driver loads on it and receives the DATA handle over the bootstrap). The devnode lives for the
|
||||
/// pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
|
||||
fn open(index: u8) -> Result<DsWinPad> {
|
||||
/// the device type FIRST (so it's visible the moment magic is) + the pad index + a neutral
|
||||
/// report + the magic LAST, then spawn the devnode (the driver loads on it and receives the
|
||||
/// DATA handle over the bootstrap). The devnode lives for the pad's lifetime — dropping the pad
|
||||
/// removes it (`SwDeviceClose`).
|
||||
pub(super) fn open(index: u8, id: &WinDsIdentity) -> Result<DsWinPad> {
|
||||
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
|
||||
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
|
||||
let base = channel.data_base();
|
||||
// Stamp the pad index (the driver validates it on attach) + the neutral input report, then
|
||||
// the magic LAST (the driver only accepts the section once magic is set). The device-type
|
||||
// stays 0 (DualSense — the section arrives zeroed).
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; OFF_PAD_INDEX/OFF_INPUT are in range.
|
||||
// SAFETY: base points at SHM_SIZE writable bytes; the OFF_* offsets are in range.
|
||||
unsafe {
|
||||
*base.add(OFF_DEVTYPE) = id.devtype;
|
||||
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
|
||||
std::ptr::write_unaligned(base.add(OFF_INPUT) as *mut [u8; DS_INPUT_REPORT_LEN], {
|
||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||
@@ -251,19 +289,19 @@ impl DsWinPad {
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
}
|
||||
// Spawn the per-session devnode via SwDeviceCreate; `SwDeviceClose` removes it on drop. On the
|
||||
// rare failure we keep the section + data plane and fall back to an out-of-band `pf_dualsense`
|
||||
// devnode (installer / dev-box devgen) — its persistent driver polls the same mailbox name.
|
||||
let inst = format!("pf_pad_{index}");
|
||||
// rare failure we keep the section + data plane and fall back to an out-of-band devnode
|
||||
// (installer / dev-box devgen) — its persistent driver polls the same mailbox name.
|
||||
let inst = format!("{}_{index}", id.instance_prefix);
|
||||
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
|
||||
instance: &inst,
|
||||
container_index: index,
|
||||
hwid: "pf_dualsense",
|
||||
usb_vid_pid: "VID_054C&PID_0CE6",
|
||||
description: "punktfunk Virtual DualSense",
|
||||
hwid: id.hwid,
|
||||
usb_vid_pid: id.usb_vid_pid,
|
||||
description: id.description,
|
||||
}) {
|
||||
Ok((h, id)) => (Some(h), id),
|
||||
Ok((h, i)) => (Some(h), i),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; falling back to an out-of-band pf_dualsense devnode");
|
||||
tracing::warn!(error = %format!("{e:#}"), hwid = id.hwid, "SwDeviceCreate failed; falling back to an out-of-band devnode");
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
@@ -275,8 +313,8 @@ impl DsWinPad {
|
||||
_sw,
|
||||
channel,
|
||||
attach: super::gamepad_raii::DriverAttach::new(
|
||||
"pf_dualsense",
|
||||
"pf_dualsense.inf",
|
||||
id.hwid,
|
||||
"pf_dualsense.inf", // one driver package serves every PS identity
|
||||
"C:\\Users\\Public\\pfds-driver.log",
|
||||
boot_name,
|
||||
instance_id,
|
||||
@@ -288,7 +326,7 @@ impl DsWinPad {
|
||||
}
|
||||
|
||||
/// Serialize `st` into report `0x01` and publish it to the section's input slot.
|
||||
fn write_state(&mut self, st: &DsState) {
|
||||
pub(super) fn write_state(&mut self, st: &DsState) {
|
||||
self.seq = self.seq.wrapping_add(1);
|
||||
self.ts = self.ts.wrapping_add(1);
|
||||
let mut r = [0u8; DS_INPUT_REPORT_LEN];
|
||||
@@ -318,7 +356,7 @@ impl DsWinPad {
|
||||
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything
|
||||
/// new. Also ticks the sealed-channel delivery and feeds the driver-attach health watcher (the
|
||||
/// driver's ~125 Hz timer stamps `driver_proto` while it has the section mapped).
|
||||
fn service(&mut self, pad: u8) -> DsFeedback {
|
||||
pub(super) fn service(&mut self, pad: u8) -> DsFeedback {
|
||||
self.channel.pump();
|
||||
let mut fb = DsFeedback::default();
|
||||
// SAFETY: base points at SHM_SIZE bytes.
|
||||
@@ -378,7 +416,7 @@ impl PadProto for DsWinProto {
|
||||
" (install/repair: punktfunk-host.exe driver install --gamepad)";
|
||||
|
||||
fn open(&mut self, idx: u8) -> Result<DsWinPad> {
|
||||
let p = DsWinPad::open(idx)?;
|
||||
let p = DsWinPad::open(idx, &WinDsIdentity::dualsense())?;
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual DualSense created (Windows UMDF shm channel)"
|
||||
|
||||
@@ -255,19 +255,28 @@ fn real_main() -> Result<()> {
|
||||
// Create a virtual DualSense via UHID and exercise it (validation, no streaming session):
|
||||
// toggles the Cross button, sweeps the left stick, and prints any HID output the kernel
|
||||
// sends back. Verify with `evtest` / `ls /dev/input/by-id/*Punktfunk*` / `wpctl status`.
|
||||
// `--edge` creates a DualSense **Edge** (054C:0DF2) instead and additionally cycles the
|
||||
// four back/Fn buttons (kernel ≥ 7.2 exposes them as BTN_TRIGGER_HAPPY1..4; on older
|
||||
// kernels verify the bind + `hidraw` byte 10 instead).
|
||||
#[cfg(target_os = "linux")]
|
||||
Some("dualsense-test") => {
|
||||
use inject::dualsense::DualSensePad;
|
||||
use inject::dualsense_proto::DsState;
|
||||
use inject::dualsense::{DsUhidIdentity, DualSensePad};
|
||||
use inject::dualsense_proto::{edge_paddle_bits, DsState};
|
||||
let secs: u64 = args
|
||||
.iter()
|
||||
.skip_while(|a| *a != "--seconds")
|
||||
.nth(1)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(20);
|
||||
let edge = args.iter().any(|a| a == "--edge");
|
||||
let (identity, label) = if edge {
|
||||
(DsUhidIdentity::dualsense_edge(), "DualSense Edge")
|
||||
} else {
|
||||
(DsUhidIdentity::dualsense(), "DualSense")
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
let mut pad =
|
||||
DualSensePad::open(0).context("create virtual DualSense via /dev/uhid")?;
|
||||
let mut pad = DualSensePad::open(0, &identity)
|
||||
.with_context(|| format!("create virtual {label} via /dev/uhid"))?;
|
||||
// Answer the kernel's init GET_REPORTs promptly so hid-playstation creates the input
|
||||
// devices before we start streaming state.
|
||||
let init = Instant::now() + Duration::from_millis(800);
|
||||
@@ -276,7 +285,7 @@ fn real_main() -> Result<()> {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
println!(
|
||||
"virtual DualSense created — check `evtest`, `ls /dev/input/by-id/*Punktfunk*`, \
|
||||
"virtual {label} created — check `evtest`, `ls /dev/input/by-id/*Punktfunk*`, \
|
||||
`ls /sys/class/leds/`. Cycling Cross + sweeping LS for {secs}s."
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(secs);
|
||||
@@ -292,14 +301,22 @@ fn real_main() -> Result<()> {
|
||||
if last_write.elapsed() >= Duration::from_millis(300) {
|
||||
last_write = Instant::now();
|
||||
i += 1;
|
||||
let buttons = if i % 2 == 0 {
|
||||
let mut buttons = if i % 2 == 0 {
|
||||
punktfunk_core::input::gamepad::BTN_A
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if edge {
|
||||
// Cycle one paddle per beat (R4 → L4 → R5 → L5) so all four Edge slots
|
||||
// are visible in evtest / hidraw.
|
||||
buttons |= punktfunk_core::input::gamepad::BTN_PADDLE1 << (i % 4);
|
||||
}
|
||||
let lx = (((i % 64) - 32) * 1024) as i16; // sweep left stick X
|
||||
let st = DsState::from_gamepad(buttons, lx, 0, 0, 0, 0, 0);
|
||||
pad.write_state(&st).context("write DualSense report")?;
|
||||
let mut st = DsState::from_gamepad(buttons, lx, 0, 0, 0, 0, 0);
|
||||
if edge {
|
||||
st.buttons[2] |= edge_paddle_bits(buttons);
|
||||
}
|
||||
pad.write_state(&st).context("write report")?;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(15));
|
||||
}
|
||||
|
||||
@@ -1752,7 +1752,8 @@ const INJECTOR_REOPEN_BACKOFF: std::time::Duration = std::time::Duration::from_s
|
||||
///
|
||||
/// - Xbox 360 / One — uinput on Linux ([`GamepadManager`](crate::inject::gamepad::GamepadManager),
|
||||
/// two identities), the XUSB companion driver (classic XInput) on Windows.
|
||||
/// - DualSense / DualShock 4 — Linux UHID `hid-playstation`, or the Windows UMDF minidriver.
|
||||
/// - DualSense / DualSense Edge / DualShock 4 — Linux UHID `hid-playstation`, or the Windows UMDF
|
||||
/// minidriver (device-type 0/2/1).
|
||||
/// - Steam Deck — Linux UHID `hid-steam`.
|
||||
///
|
||||
/// [`resolve_pad_kind`] folds any kind a platform can't build into one it can, so this never
|
||||
@@ -1771,12 +1772,16 @@ struct Pads {
|
||||
#[cfg(target_os = "linux")]
|
||||
dualsense: Option<crate::inject::dualsense::DualSenseManager>,
|
||||
#[cfg(target_os = "linux")]
|
||||
dualsense_edge: Option<crate::inject::dualsense::DualSenseEdgeManager>,
|
||||
#[cfg(target_os = "linux")]
|
||||
dualshock4: Option<crate::inject::dualshock4::DualShock4Manager>,
|
||||
#[cfg(target_os = "linux")]
|
||||
steamdeck: Option<crate::inject::steam_controller::SteamControllerManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_win: Option<crate::inject::dualsense_windows::DualSenseWindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_edge_win: Option<crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualshock4_win: Option<crate::inject::dualshock4_windows::DualShock4WindowsManager>,
|
||||
}
|
||||
|
||||
@@ -1798,12 +1803,16 @@ impl Pads {
|
||||
#[cfg(target_os = "linux")]
|
||||
dualsense: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
dualsense_edge: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
dualshock4: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
steamdeck: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_edge_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualshock4_win: None,
|
||||
}
|
||||
}
|
||||
@@ -1855,6 +1864,11 @@ impl Pads {
|
||||
.get_or_insert_with(crate::inject::dualsense::DualSenseManager::new)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::DualSenseEdge => self
|
||||
.dualsense_edge
|
||||
.get_or_insert_with(crate::inject::dualsense::DualSenseEdgeManager::new)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::DualShock4 => self
|
||||
.dualshock4
|
||||
.get_or_insert_with(crate::inject::dualshock4::DualShock4Manager::new)
|
||||
@@ -1879,6 +1893,13 @@ impl Pads {
|
||||
.get_or_insert_with(crate::inject::dualsense_windows::DualSenseWindowsManager::new)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::DualSenseEdge => self
|
||||
.dualsense_edge_win
|
||||
.get_or_insert_with(
|
||||
crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager::new,
|
||||
)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::DualShock4 => self
|
||||
.dualshock4_win
|
||||
.get_or_insert_with(
|
||||
@@ -1920,6 +1941,12 @@ impl Pads {
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::DualSenseEdge => {
|
||||
if let Some(m) = &mut self.dualsense_edge {
|
||||
m.apply_rich(rich)
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::DualShock4 => {
|
||||
if let Some(m) = &mut self.dualshock4 {
|
||||
m.apply_rich(rich)
|
||||
@@ -1938,6 +1965,12 @@ impl Pads {
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::DualSenseEdge => {
|
||||
if let Some(m) = &mut self.dualsense_edge_win {
|
||||
m.apply_rich(rich)
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::DualShock4 => {
|
||||
if let Some(m) = &mut self.dualshock4_win {
|
||||
m.apply_rich(rich)
|
||||
@@ -1967,6 +2000,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.dualsense {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.dualsense_edge {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.dualshock4 {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
@@ -1979,6 +2015,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.dualsense_win {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.dualsense_edge_win {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.dualshock4_win {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
@@ -1996,6 +2035,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.dualsense {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
if let Some(m) = &mut self.dualsense_edge {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
if let Some(m) = &mut self.dualshock4 {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
@@ -2009,6 +2051,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.dualsense_win {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
if let Some(m) = &mut self.dualsense_edge_win {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
if let Some(m) = &mut self.dualshock4_win {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
@@ -2700,11 +2745,10 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
|
||||
// DualSense touchpad left/right per DsState::apply_rich). Folding to Xbox360 dropped
|
||||
// all of that silently.
|
||||
GamepadPref::SteamDeck if windows => GamepadPref::DualSense,
|
||||
// DualSense Edge: until its backend lands (N1), fold to the plain DualSense wherever
|
||||
// that exists — it keeps the rich planes (touchpad/motion/lightbar) alive; only the
|
||||
// back-button bits go through the paddle fold. NOT Xbox360 (that would drop the rich
|
||||
// planes entirely, the SteamDeck-on-Windows lesson above).
|
||||
GamepadPref::DualSenseEdge if linux || windows => GamepadPref::DualSense,
|
||||
// DualSense Edge: Linux UHID hid-playstation / Windows UMDF (device-type 2) — the plain
|
||||
// DualSense plus native back/Fn buttons, so the wire paddles stop hitting the fold/drop
|
||||
// policy. Degrades to Xbox360 elsewhere like its siblings.
|
||||
GamepadPref::DualSenseEdge if linux || windows => GamepadPref::DualSenseEdge,
|
||||
// Switch Pro: no backend yet (N2) — falls through to Xbox360 below.
|
||||
_ => GamepadPref::Xbox360,
|
||||
}
|
||||
@@ -2718,7 +2762,10 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
|
||||
fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
|
||||
let needs_uhid = matches!(
|
||||
chosen,
|
||||
GamepadPref::DualSense | GamepadPref::DualShock4 | GamepadPref::SteamDeck
|
||||
GamepadPref::DualSense
|
||||
| GamepadPref::DualSenseEdge
|
||||
| GamepadPref::DualShock4
|
||||
| GamepadPref::SteamDeck
|
||||
);
|
||||
if needs_uhid
|
||||
&& std::fs::OpenOptions::new()
|
||||
@@ -5286,11 +5333,20 @@ mod tests {
|
||||
assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), DualSense);
|
||||
assert_eq!(pick_gamepad(SteamDeck, None, false, false), Xbox360);
|
||||
|
||||
// DualSense Edge: folds to the plain DualSense (keeps the rich planes) until the Edge
|
||||
// backend lands (gamepad-new-types N1); Xbox360 where no DualSense backend exists.
|
||||
assert_eq!(pick_gamepad(DualSenseEdge, None, true, false), DualSense);
|
||||
assert_eq!(pick_gamepad(DualSenseEdge, None, false, true), DualSense);
|
||||
assert_eq!(pick_gamepad(Auto, Some("edge"), true, false), DualSense);
|
||||
// DualSense Edge: native on Linux (UHID) AND Windows (UMDF device-type 2); Xbox360
|
||||
// elsewhere.
|
||||
assert_eq!(
|
||||
pick_gamepad(DualSenseEdge, None, true, false),
|
||||
DualSenseEdge
|
||||
);
|
||||
assert_eq!(
|
||||
pick_gamepad(DualSenseEdge, None, false, true),
|
||||
DualSenseEdge
|
||||
);
|
||||
assert_eq!(
|
||||
pick_gamepad(Auto, Some("edge"), true, false),
|
||||
DualSenseEdge
|
||||
);
|
||||
assert_eq!(pick_gamepad(DualSenseEdge, None, false, false), Xbox360);
|
||||
// Switch Pro: no backend yet (gamepad-new-types N2) — folds to Xbox360 everywhere.
|
||||
assert_eq!(pick_gamepad(SwitchPro, None, true, false), Xbox360);
|
||||
|
||||
Reference in New Issue
Block a user