Compare commits
2 Commits
6a501f484a
...
e919fa6a2e
| Author | SHA1 | Date | |
|---|---|---|---|
| e919fa6a2e | |||
| 6db3525e29 |
@@ -11,9 +11,10 @@
|
||||
//! bytes. `hidclass` gates the device stack, so this user-mode IPC is the only viable channel (a
|
||||
//! UMDF driver has no control device); see `windows-dualsense-scoping.md`.
|
||||
//!
|
||||
//! Device lifecycle: each pad `SwDeviceCreate`s the `root\pf_dualsense` devnode on open and
|
||||
//! `SwDeviceClose`s it on drop, so the virtual DualSense appears/disappears with the session —
|
||||
//! matching the Linux UHID pad. (The driver itself must already be installed; the installer stages it.)
|
||||
//! Device lifecycle: each pad `SwDeviceCreate`s a `pf_pad_<index>` software devnode (hardware id
|
||||
//! `pf_dualsense`, enumerator `punktfunk`) on open and `SwDeviceClose`s it on drop, so the virtual
|
||||
//! DualSense appears/disappears with the session — matching the Linux UHID pad. (The driver itself
|
||||
//! must already be installed; the installer stages it.)
|
||||
|
||||
use super::dualsense_proto::{
|
||||
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
||||
@@ -46,9 +47,9 @@ const OFF_INPUT: usize = 8;
|
||||
const OFF_OUT_SEQ: usize = 72;
|
||||
const OFF_OUTPUT: usize = 76;
|
||||
|
||||
/// A single virtual DualSense: the `root\pf_dualsense` software devnode (the driver loads on it and
|
||||
/// the HID DualSense appears to games) plus the shared-memory section the driver maps. Dropping it
|
||||
/// removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||
/// 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 shared-memory section the driver maps.
|
||||
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||
struct DsWinPad {
|
||||
/// Per-session devnode from SwDeviceCreate, when it succeeds. `None` falls back to an out-of-band
|
||||
/// `pf_dualsense` devnode (installer/devgen).
|
||||
@@ -60,16 +61,15 @@ struct DsWinPad {
|
||||
last_out_seq: u32,
|
||||
}
|
||||
|
||||
/// Context for the async `SwDeviceCreate` completion callback: the event to signal + the result.
|
||||
/// Context for the `SwDeviceCreate` completion callback: an event to signal + the HRESULT it reports.
|
||||
#[repr(C)]
|
||||
struct SwCreateCtx {
|
||||
event: HANDLE,
|
||||
result: HRESULT,
|
||||
}
|
||||
|
||||
/// `SwDeviceCreate` fires this on a worker thread once the device is created. We stash the result and
|
||||
/// wake the waiting [`create_swdevice`]; the creator blocks on the event, so there's no concurrent
|
||||
/// access to `*ctx`.
|
||||
/// `SwDeviceCreate` fires this once PnP has enumerated the device; stash the result and wake the
|
||||
/// creator, which blocks on the event (so there's no concurrent access to `*ctx`).
|
||||
unsafe extern "system" fn sw_create_cb(
|
||||
_dev: HSWDEVICE,
|
||||
result: HRESULT,
|
||||
@@ -77,27 +77,30 @@ unsafe extern "system" fn sw_create_cb(
|
||||
_id: PCWSTR,
|
||||
) {
|
||||
if !ctx.is_null() {
|
||||
let c = ctx as *mut SwCreateCtx;
|
||||
// SAFETY: c is the &mut SwCreateCtx the creator passed; it outlives this callback (the
|
||||
// creator waits on the event before dropping it).
|
||||
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback.
|
||||
unsafe {
|
||||
let c = ctx as *mut SwCreateCtx;
|
||||
(*c).result = result;
|
||||
let _ = SetEvent((*c).event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the virtual DualSense software device under enumerator `punktfunk` (hardware id
|
||||
/// `pf_dualsense`, which the INF matches). The returned `HSWDEVICE` owns the devnode for the session
|
||||
/// — `SwDeviceClose` removes it.
|
||||
/// Spawn the per-session virtual DualSense devnode for pad `index` under enumerator `punktfunk`
|
||||
/// (instance `pf_pad_<index>`, hardware id `pf_dualsense` which the INF matches). The returned
|
||||
/// `HSWDEVICE` owns it — `SwDeviceClose` removes it on drop, so the pad appears/disappears with the
|
||||
/// session and nothing persists.
|
||||
///
|
||||
/// NB: enumerator names with an underscore (`pf_dualsense`) get E_INVALIDARG — hence `punktfunk`.
|
||||
/// TODO: a SECOND E_INVALIDARG remains — passing the completion callback is rejected (callback-absent
|
||||
/// is accepted but then the devnode doesn't materialize). Until that's resolved [`DsWinPad::open`]
|
||||
/// treats a failure as non-fatal and relies on an out-of-band `pf_dualsense` devnode (installer /
|
||||
/// dev-box `devgen`); see `docs/windows-dualsense-scoping.md`.
|
||||
fn create_swdevice() -> Result<HSWDEVICE> {
|
||||
/// Two requirements each yield E_INVALIDARG if violated: the enumerator name must not contain `_`
|
||||
/// (hence `punktfunk`, not `pf_dualsense`), and the completion callback is mandatory (the docs mark
|
||||
/// `pCallback` as `[in]`, not optional — a NULL callback is rejected). The caller must be
|
||||
/// Administrator (the host service runs as LocalSystem).
|
||||
fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
|
||||
let hwids: Vec<u16> = "pf_dualsense".encode_utf16().chain([0u16, 0u16]).collect();
|
||||
let instid: Vec<u16> = format!("pf_pad_{index}")
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let desc: Vec<u16> = "punktfunk Virtual DualSense"
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
@@ -105,10 +108,10 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
||||
// SAFETY: zeroed then the fields we use are set; cbSize identifies the struct version.
|
||||
let mut info: SW_DEVICE_CREATE_INFO = unsafe { std::mem::zeroed() };
|
||||
info.cbSize = std::mem::size_of::<SW_DEVICE_CREATE_INFO>() as u32;
|
||||
info.pszInstanceId = PCWSTR(instid.as_ptr());
|
||||
info.pszzHardwareIds = PCWSTR(hwids.as_ptr());
|
||||
info.pszDeviceDescription = PCWSTR(desc.as_ptr());
|
||||
// SWDeviceCapabilities: DriverRequired (8) | SilentInstall (2) | Removable (1).
|
||||
info.CapabilityFlags = 0x0000_000B;
|
||||
info.CapabilityFlags = 0x0000_000B; // DriverRequired | SilentInstall | Removable
|
||||
|
||||
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
|
||||
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
|
||||
@@ -116,7 +119,7 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
||||
event,
|
||||
result: HRESULT(0),
|
||||
};
|
||||
// SAFETY: info + hwids/desc outlive the call; ctx outlives the callback (we wait below).
|
||||
// SAFETY: info + the buffers + ctx outlive the call (we wait on the event before returning);
|
||||
// windows-rs returns the HSWDEVICE (the C out-param) as the Result value.
|
||||
let hsw = match unsafe {
|
||||
SwDeviceCreate(
|
||||
@@ -137,7 +140,8 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
||||
return Err(anyhow!("SwDeviceCreate failed: {e}"));
|
||||
}
|
||||
};
|
||||
// SAFETY: event is valid; block up to 10s for the creation callback.
|
||||
// Block until PnP finishes enumerating (the callback signals), then check its result.
|
||||
// SAFETY: event is valid.
|
||||
unsafe {
|
||||
WaitForSingleObject(event, 10_000);
|
||||
let _ = CloseHandle(event);
|
||||
@@ -145,7 +149,10 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
||||
if ctx.result.is_err() {
|
||||
// SAFETY: hsw is the handle SwDeviceCreate returned.
|
||||
unsafe { SwDeviceClose(hsw) };
|
||||
return Err(anyhow!("SwDeviceCreate callback reported {:?}", ctx.result));
|
||||
return Err(anyhow!(
|
||||
"SwDeviceCreate enumeration failed: {:?}",
|
||||
ctx.result
|
||||
));
|
||||
}
|
||||
Ok(hsw)
|
||||
}
|
||||
@@ -207,13 +214,13 @@ impl DsWinPad {
|
||||
});
|
||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||
}
|
||||
// Best-effort: spawn a per-session devnode via SwDeviceCreate. It currently fails with a
|
||||
// SwDevice quirk (see create_swdevice), so on failure we keep the section + data plane and
|
||||
// rely on an out-of-band `pf_dualsense` devnode (installer / dev-box devgen).
|
||||
let hsw = match create_swdevice() {
|
||||
// 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).
|
||||
let hsw = match create_swdevice(index) {
|
||||
Ok(h) => Some(h),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; using an out-of-band pf_dualsense devnode");
|
||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; falling back to an out-of-band pf_dualsense devnode");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
@@ -224,26 +224,41 @@ fn real_main() -> Result<()> {
|
||||
kind: 2,
|
||||
capabilities: 0,
|
||||
});
|
||||
// ls_x 16384 → report byte1 0xC0; BTN_A (Cross) → report byte8 0x28.
|
||||
mgr.handle(&GamepadEvent::State(GamepadFrame {
|
||||
index: 0,
|
||||
active_mask: 1,
|
||||
buttons: punktfunk_core::input::gamepad::BTN_A,
|
||||
left_trigger: 0,
|
||||
right_trigger: 0,
|
||||
ls_x: 16384,
|
||||
ls_y: 0,
|
||||
rs_x: 0,
|
||||
rs_y: 0,
|
||||
}));
|
||||
println!(
|
||||
"virtual DualSense created via SwDeviceCreate (VID 054C/PID 0CE6). Holding {secs}s — \
|
||||
verify Get-PnpDevice VID_054C + a HID read (expect byte1=0xC0, byte8=0x28)."
|
||||
"virtual DualSense up — cycling Cross + sweeping the left stick for {secs}s. Watch it \
|
||||
in joy.cpl / Steam / a game; any rumble / lightbar / trigger the game sends prints below."
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(secs);
|
||||
let (mut i, mut last) = (0i32, Instant::now());
|
||||
while Instant::now() < deadline {
|
||||
mgr.pump(|_, _, _| {}, |_| {});
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
// Surface a game's feedback: rumble (universal) + lightbar / player-LED / adaptive
|
||||
// triggers (DualSense-only) coming back over the shared section.
|
||||
mgr.pump(
|
||||
|pad, lo, hi| println!(" rumble from game: pad={pad} low={lo} high={hi}"),
|
||||
|o| println!(" hid output from game: {o:?}"),
|
||||
);
|
||||
if last.elapsed() >= Duration::from_millis(400) {
|
||||
last = Instant::now();
|
||||
i += 1;
|
||||
let buttons = if i % 2 == 0 {
|
||||
punktfunk_core::input::gamepad::BTN_A // Cross
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let lx = (((i % 64) - 32) * 1024) as i16; // sweep left stick X
|
||||
mgr.handle(&GamepadEvent::State(GamepadFrame {
|
||||
index: 0,
|
||||
active_mask: 1,
|
||||
buttons,
|
||||
left_trigger: 0,
|
||||
right_trigger: 0,
|
||||
ls_x: lx,
|
||||
ls_y: 0,
|
||||
rs_x: 0,
|
||||
rs_y: 0,
|
||||
}));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(15));
|
||||
}
|
||||
println!("dualsense-windows-test: done (devnode removed)");
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# Windows virtual DualSense — game detection handoff
|
||||
|
||||
Goal: get the host's virtual DualSense **detected and usable in games** (Cyberpunk's native PS5 path +
|
||||
others) on the Windows host. This doc is the portable handoff (the investigation lives here, not in any
|
||||
one agent's memory). Run the experiments **on the Windows host** (`.173`, repo at
|
||||
`C:\Users\Public\punktfunk-native`).
|
||||
|
||||
## Status (2026-06-22)
|
||||
|
||||
- **Input works.** Client → host → virtual DualSense → games read input. Verified in Steam's controller
|
||||
test (buttons/sticks).
|
||||
- **The HID is a CORRECT, COMPLETE DualSense.** An SDL3 probe reports our live device as
|
||||
`name='DualSense Wireless Controller' vid=0x054C pid=0x0CE6 isGamepad=True gamepadType=PS5`. SDL =
|
||||
HIDAPI = what Steam (and many games) build on → that's why Steam works. So the report descriptor,
|
||||
feature reports, and identity are right; this is **not** a descriptor/feature-report problem.
|
||||
- **Cyberpunk's native DualSense path does NOT detect it at all.** (Steam Input was off — Cyberpunk was
|
||||
reading the raw HID.)
|
||||
- **Rumble:** host-side is proven working (driver captures the game's `0x02`, `parse_ds_output` extracts
|
||||
the motors, host forwards `0xCA` — log: `rumble: forwarding to client (0xCA) low=16128 high=16128`).
|
||||
The break is the **client** (macOS) not rendering `0xCA` onto the physical pad. Separate task/agent.
|
||||
|
||||
## Root-cause hypothesis (the thing to confirm/fix)
|
||||
|
||||
The device is **software-enumerated**: `SWD\PUNKTFUNK\PF_PAD_0` → child `HID\VID_054C&PID_0CE6`. It is NOT
|
||||
a real USB or Bluetooth device. SDL/HIDAPI enumerate any HID by VID/PID (incl. SWD) — so they see it. A
|
||||
game's *native* DualSense path is pickier. Two likely causes:
|
||||
1. **Windows.Gaming.Input (WGI) / GameInput exclude SWD (software) HID devices** that raw-HID
|
||||
enumeration includes. Many modern titles use these.
|
||||
2. **USB-vs-Bluetooth detection by device-path prefix.** Native DS5 code picks the report format (64-byte
|
||||
USB report `0x01` vs 78-byte BT report `0x31`) from the connection type. If it keys off the device
|
||||
path (`USB\…` vs `BTHENUM\…`) rather than the report length, our `SWD\…` path matches neither and it
|
||||
mis-detects. (SDL keys off the *report length* = 64 → USB → works.)
|
||||
|
||||
## How to reproduce / iterate (on `.173`)
|
||||
|
||||
### 1. Spawn a live virtual DualSense to test against
|
||||
```
|
||||
C:\Users\Public\punktfunk-native\target\debug\punktfunk-host.exe dualsense-windows-test --seconds 60
|
||||
```
|
||||
Creates `SWD\PUNKTFUNK\PF_PAD_0` (+ its HID child) and holds it, pushing a cycling input. Or just connect
|
||||
a client — the real session creates the identical device. (Build with the env `CMAKE_POLICY_VERSION_MINIMUM=3.5`.)
|
||||
|
||||
### 2. SDL3 detection oracle (already set up: `C:\Users\Public\sdltest\SDL3.dll`)
|
||||
Confirms HID-level recognition (HIDAPI). Run while a device from step 1 is live. PowerShell + C# (note:
|
||||
PS 5.1's Add-Type is C# 5 — **no** interpolated strings, **no** inline `out` vars, **no**
|
||||
`Marshal.PtrToStringUTF8`; SDL3 bools are 1 byte → `[return: MarshalAs(UnmanagedType.I1)]`):
|
||||
```powershell
|
||||
$cs = @'
|
||||
using System; using System.Runtime.InteropServices; using System.Text;
|
||||
public static class S {
|
||||
const string D = @"C:\Users\Public\sdltest\SDL3.dll";
|
||||
[DllImport(D)][return: MarshalAs(UnmanagedType.I1)] public static extern bool SDL_Init(uint f);
|
||||
[DllImport(D)] public static extern IntPtr SDL_GetJoysticks(out int c);
|
||||
[DllImport(D)] public static extern IntPtr SDL_GetJoystickNameForID(uint id);
|
||||
[DllImport(D)] public static extern ushort SDL_GetJoystickVendorForID(uint id);
|
||||
[DllImport(D)] public static extern ushort SDL_GetJoystickProductForID(uint id);
|
||||
[DllImport(D)][return: MarshalAs(UnmanagedType.I1)] public static extern bool SDL_IsGamepad(uint id);
|
||||
[DllImport(D)] public static extern IntPtr SDL_OpenGamepad(uint id);
|
||||
[DllImport(D)] public static extern int SDL_GetGamepadType(IntPtr g);
|
||||
static string U(IntPtr p){ if(p==IntPtr.Zero)return""; int n=0; while(Marshal.ReadByte(p,n)!=0)n++; byte[] b=new byte[n]; Marshal.Copy(p,b,0,n); return Encoding.UTF8.GetString(b); }
|
||||
public static string Run(){ if(!SDL_Init(0x2000))return"init fail"; System.Threading.Thread.Sleep(1500);
|
||||
int n=0; IntPtr a=SDL_GetJoysticks(out n); StringBuilder sb=new StringBuilder("joysticks: "+n+"\n");
|
||||
for(int i=0;i<n;i++){ uint id=(uint)Marshal.ReadInt32(a,i*4); bool ig=SDL_IsGamepad(id); int t=ig?SDL_GetGamepadType(SDL_OpenGamepad(id)):-1;
|
||||
sb.AppendLine(" '"+U(SDL_GetJoystickNameForID(id))+"' vid=0x"+SDL_GetJoystickVendorForID(id).ToString("x4")+" pid=0x"+SDL_GetJoystickProductForID(id).ToString("x4")+" isGamepad="+ig+" type="+t+" (PS5=6)"); }
|
||||
return sb.ToString(); }
|
||||
}
|
||||
'@
|
||||
Add-Type -TypeDefinition $cs; [S]::Run()
|
||||
```
|
||||
Expected today: it lists our device with `type=6` (PS5). That's the baseline "HID is correct".
|
||||
|
||||
## Next experiments — MUST run ON THE INTERACTIVE DESKTOP, not over SSH
|
||||
|
||||
WGI / RawInput / GameInput enumeration returns **empty from a headless SSH session** (no window/message
|
||||
pump) — only HIDAPI works headless. So these must run in the logged-in desktop session (RDP in, or run
|
||||
locally) while a DualSense session is live:
|
||||
|
||||
1. **Determine which API Cyberpunk uses and whether it sees the SWD device.** Enumerate via, separately:
|
||||
- `Windows.Gaming.Input` (`RawGameController.RawGameControllers`, `Gamepad.Gamepads`),
|
||||
- RawInput (`GetRawInputDeviceList` → filter HID gamepad usage 01/05),
|
||||
- GameInput (`GameInputCreate` → `EnumerateDevices`) — `GameInputRedistService` is installed on `.173`.
|
||||
Compare which list our `VID_054C&PID_0CE6` appears in. The one(s) it's *missing from* point at the API
|
||||
Cyberpunk uses.
|
||||
2. **If WGI/GameInput exclude it:** make the SwDeviceCreate device enumerate more like a real USB device.
|
||||
`SwDeviceCreate` takes a `pProperties` (`DEVPROPERTY[]`) array — try setting bus-type / container-id /
|
||||
compatible-IDs so the newer APIs accept it. If that's insufficient, the heavyweight option is a
|
||||
USB-emulating bus driver (the way ViGEmBus presents a real-looking device) instead of SwDeviceCreate +
|
||||
UMDF-HID.
|
||||
3. **Rule out an XInput device taking priority** (a leftover ViGEm pad, etc.).
|
||||
4. **Correctness (not the detection blocker):** `DS_FEATURE_CALIBRATION` in the driver is **42 bytes**
|
||||
but the report descriptor declares feature `0x05` as **41** (1 id + 40 data, `0x95 0x28`). Trim to 41;
|
||||
wrong calibration only affects motion, and SDL accepts the device regardless.
|
||||
|
||||
## On-box layout (`.173`, builds + tools)
|
||||
|
||||
- **Host repo / build:** `C:\Users\Public\punktfunk-native` → `cargo build -p punktfunk-host`
|
||||
(debug for `dualsense-windows-test`; `--release --features nvenc` is what the service runs). Env:
|
||||
`set CMAKE_POLICY_VERSION_MINIMUM=3.5` (audiopus_sys). bun=`C:\Users\Public\bun`, a standalone
|
||||
node=`C:\Users\Public\node-v22.11.0-win-x64`.
|
||||
- **Host service:** scheduled task / SCM `PunktfunkHost` runs `…\target\release\punktfunk-host.exe
|
||||
service run` → spawns `serve` (currently native-only, `PUNKTFUNK_HOST_CMD=serve` in
|
||||
`C:\ProgramData\punktfunk\host.env`). Restart: `sc stop/start PunktfunkHost`. Native port 9777, mgmt
|
||||
47990. (NB: Sunshine/Apollo conflicts on the GameStream ports — keep it stopped, or run native-only.)
|
||||
- **UMDF driver build project:** `C:\Users\Public\m0\windows-drivers-rs\examples\pf-dualsense`
|
||||
(`pf_dualsense.inx` + `src\lib.rs` live here; the canonical copies are in the repo under
|
||||
`packaging/windows/dualsense-driver/` — keep them in sync). Rebuild + reinstall recipe (e.g. after the
|
||||
calibration fix), all from that dir, env `LIBCLANG_PATH=C:\Program Files\LLVM\bin`,
|
||||
`Version_Number=10.0.26100.0`:
|
||||
1. `cargo make` → `target\debug\pf_dualsense_package\`
|
||||
2. **Clear the FORCE_INTEGRITY PE bit** (wdk-build sets `/INTEGRITYCHECK`, which blocks self-signed
|
||||
load): clear bit 0x80 at `PE_header_offset+0x5e` of `pf_dualsense.dll`, then re-sign.
|
||||
3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_dualsense.dll`
|
||||
4. `Inf2Cat /driver:<pkg> /os:10_x64` → re-sign the `.cat` with the same thumbprint.
|
||||
5. `pnputil /delete-driver <old oemNN.inf> /uninstall /force` then `pnputil /add-driver
|
||||
pf_dualsense.inf /install`. (Self-signed cert is already trusted on `.173`; Secure Boot ON, HVCI off.)
|
||||
- **SDL oracle:** `C:\Users\Public\sdltest\SDL3.dll`. **Test device:** `punktfunk-host.exe
|
||||
dualsense-windows-test --seconds N` creates one `SWD\PUNKTFUNK\PF_PAD_0` and holds it.
|
||||
|
||||
## Key code
|
||||
|
||||
| What | File |
|
||||
| --- | --- |
|
||||
| Host backend (`create_swdevice`, the `Global\pfds-shm-<idx>` section, write_state/service/pump) | `crates/punktfunk-host/src/inject/dualsense_windows.rs` |
|
||||
| UMDF driver (HID descriptor, feature reports, `on_output_report`) | `packaging/windows/dualsense-driver/src/lib.rs` |
|
||||
| Shared report codec (`serialize_state` input, `parse_ds_output` feedback) | `crates/punktfunk-host/src/inject/dualsense_proto.rs` |
|
||||
| Pad seam (`PadBackend`, `pump` → rumble `0xCA` / hidout `0xCD`) | `crates/punktfunk-host/src/punktfunk1.rs` |
|
||||
|
||||
## Facts proven (don't re-litigate)
|
||||
- `SwDeviceCreate` requirements: enumerator must have **no underscore** (`punktfunk`); the completion
|
||||
**callback is mandatory** (NULL → E_INVALIDARG). Per-session device works; auto-removed on disconnect.
|
||||
- HID descriptor + feature reports are DS5-accurate enough that **SDL identifies it as PS5**.
|
||||
- Host-side rumble works end to end; the client (macOS) rendering of `0xCA` is the open rumble bug.
|
||||
Reference in New Issue
Block a user