Files
punktfunk/docs/windows-dualsense-scoping.md
T
enricobuehler 450bcf1e7b feat(host): Apollo-backlog hardening — cert gate, NVENC RFI, media QoS, async injector
A pass over the apollo-comparison backlog (re-verified against current code).
Lands four items end-to-end plus a Windows-DualSense scoping doc.

- #5/#92/#26 — GameStream paired-cert allow-list. tls.rs surfaces the verified
  peer cert to handlers (serve_https + PeerCertFingerprint, now shared with the
  mgmt API instead of duplicated); nvhttp gates /launch /resume /applist /cancel
  on AppState.paired and reports a real PairStatus; save_paired writes atomically
  (temp+rename). Closes the "mTLS accepts any client cert" hole. + regression test.

- #6/#51/#19/#22 — NVENC caps query -> reference-frame invalidation. nvenc.rs
  query_caps probes nvEncGetEncodeCaps (max dims / 10-bit / custom-VBV / RFI),
  rejecting over-range modes and degrading 10-bit->8-bit instead of an opaque
  InvalidParam. New Encoder::invalidate_ref_frames (default false -> caller
  keyframes); the Windows NVENC path implements real RFI (multi-ref DPB +
  nvEncInvalidateRefFrames, dedup + IDR-on-overflow). control.rs decodes the
  0x0301 lost-frame range (Apollo's IDX_INVALIDATE_REF_FRAMES) -> AppState.rfi_range
  -> encode loop, falling back to a keyframe. NOTE: the Windows NVENC impl is
  RTX-box/CI-pending (can't compile on Linux); adversarially reviewed vs the SDK.

- #43/#72 — media socket QoS + buffer growth. New punktfunk_core::transport::qos:
  grow_socket_buffers (factored out the native plane's 32MB SO_SNDBUF growth so the
  GameStream sockets reuse it) + set_media_qos (opt-in PUNKTFUNK_DSCP=1: DSCP CS5
  video / CS6 audio + Linux SO_PRIORITY, Apollo's scheme). Wired into UdpTransport
  and the GameStream video/audio sockets. Windows IP_TOS needs qWAVE (follow-up).

- #8/#45 — GameStream input injection off the ENet service thread. on_receive no
  longer injects inline (a slow inject head-blocked ENet keepalive/retransmit); it
  forwards to a dedicated injector thread. The hardened InjectorService moved from
  punktfunk1 into crate::inject (shared by both planes) + a coalesce step that sums
  adjacent relative-mouse/scroll deltas while preserving button/key/abs ordering.

Docs: re-verified apollo-comparison.md status (22 items already done/obsolete since
the snapshot) + windows-dualsense-scoping.md (ViGEm can't emulate a DualSense; real
DS5 on Windows needs a VHF virtual-HID driver — web-research pass pending).

fmt + clippy -D warnings clean; full workspace test suite green; no C-ABI/OpenAPI drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 00:06:30 +00:00

135 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Windows host — virtual DualSense scoping
**Status:** scoping (2026-06-20). Decision pending the web-research pass (see *Open questions* — web
search was unavailable when this was written, so the VHF API/signing specifics and the
"existing-driver-to-vendor" survey are marked TO-CONFIRM).
## TL;DR
Apollo's backlog item #23/#89 ("DS4 ViGEm target on Windows") is the **wrong target** if the goal is
*actual DualSense*. ViGEmBus emulates only **Xbox 360 (XUSB)** and **DualShock 4 (DS4)** — never a
DualSense. Because this is a *host-side* virtual pad, the DualSense-defining features (adaptive
triggers, the fine haptic actuators, DS5 identity) can only work end-to-end if the **game sees a real
DualSense** and therefore drives them; a DS4 virtual pad means the game uses its DS4 code path and
never emits those commands, so the client's adaptive-trigger rendering is never exercised. ViGEm DS4
structurally **cannot** deliver adaptive triggers.
The right path is the Windows analog of what the Linux host already does: present a **real virtual
DualSense HID device** (Sony VID `054C` / PID `0CE6`, the inputtino PS5 report descriptor). On Windows
that means a kernel-mode virtual-HID device via the **Virtual HID Framework (VHF)** — the UHID analog —
which is a SudoVDA-class driver effort (vendored + signed, installed by the existing Inno installer).
## Why this is the wrong place to copy Apollo
Apollo (and all of Sunshine's lineage) **does DualSense only on Linux** (`inputtino`,
`DualSenseWired`). Its Windows input path (`src/platform/windows/input.cpp`) is ViGEm
`XUSB_REPORT` + `DS4_REPORT_EX` only — `MPS2_TO_DS4_ACCEL` motion conversion, inverse-ViGEmBus gyro
calibration, DS4 touchpad packing. There is **zero** VHF / virtual-HID / DualSense code on Apollo's
Windows side. So:
- Copying Apollo on Windows gets us a **DS4**, with the adaptive-trigger ceiling baked in.
- There is **no in-ecosystem upstream** (Sunshine/Apollo/Wolf) that already solved virtual DualSense
on Windows to vendor from. This would be novel work for the streaming-host space.
## The parity target — and what's *already* done
The Linux host (`crates/punktfunk-host/src/inject/dualsense.rs`) creates a **UHID** device presenting
the genuine DualSense descriptor, so the kernel `hid-playstation` driver binds it and games see a real
DualSense — gamepad + motion + touchpad + lightbar/player-LEDs + adaptive triggers. It writes HID
**input** report `0x01` (controller state) and reads HID **output** report `0x02` (the game's
rumble/LED/trigger feedback), which it forwards to the client as `punktfunk_core::quic::HidOutput`.
Crucially, **everything except the host backend is already platform-agnostic and DualSense-complete:**
| Layer | State | Where |
|---|---|---|
| Protocol planes (rich input `0xCC`, rumble `0xCA`, HID-output `0xCD`) | done | `punktfunk_core::quic` |
| Feedback abstraction (`HidOutput::{Led,PlayerLeds,Trigger,…}`) | done | `punktfunk_core::quic` |
| Pad-type negotiation (client pref > env > default), `GamepadPref::DualSense` | done | `punktfunk1.rs::resolve_gamepad` |
| Backend dispatch (`enum PadBackend`) | done; `DualSense` arm is `#[cfg(target_os="linux")]` | `punktfunk1.rs:1229` |
| Clients (capture + adaptive-trigger/lightbar/haptic rendering) | done, all platforms | `clients/*` |
| C-ABI (`next_hidout` / `send_rich_input`) | done | `abi.rs` |
| **Host virtual-DualSense backend** | **Linux only (UHID)** | `inject/dualsense.rs` |
So a Windows DualSense backend needs **no protocol, client, or C-ABI change**. It must only: create a
virtual DualSense HID device, translate our pad state → HID input report `0x01`, and surface the game's
HID output report `0x02` as the same `HidOutput` events the Linux path already emits. That is a
well-bounded host-side addition (driver + a `DualSenseManager`-shaped userspace bridge + a
`PadBackend::DualSense` Windows arm).
## The Windows mechanism — VHF (primary candidate)
Windows has **no userspace HID-device creation** (unlike Linux UHID), so a real virtual DualSense
requires a kernel component. The Microsoft-sanctioned one is the **Virtual HID Framework (VHF)**: a
small KMDF driver creates a virtual HID device from an arbitrary report descriptor, submits **input**
reports to the OS, and receives **output/feature** reports written by applications (our feedback hook).
This is the structural twin of `/dev/uhid`.
Sketch of the integration (TO-CONFIRM details in *Open questions*):
```
host process (Rust) <--IOCTL/named-pipe--> punktfunk-ds5.sys (KMDF + VHF) <--HID--> game / Steam / GameInput
PadState ----------- input report 0x01 -----------> VhfReadReportSubmit
HidOutput <-- output report 0x02 (write callback) --- EvtVhf*WriteReport
```
- **Descriptor reuse:** the exact inputtino PS5 descriptor + feature-report replies we already ship for
Linux (`dualsense.rs` `DS_*` constants) — same bytes, same VID/PID, so Windows + games recognize it
as a DualSense.
- **Userspace bridge:** a `DualSenseManager`-shaped struct mirroring the Linux one (same `RichInput`
report `0x01` packing, same `HidOutput` parsing from report `0x02`), talking to the driver over an
IOCTL/pipe instead of `/dev/uhid`.
- **Packaging:** vendor + sign the `.sys`/`.inf`/`.cat` and install via the existing
`packaging/windows/sudovda` machinery (`nefconc.exe` + an `install-*.ps1`, bundled in the Inno
`setup.exe`). The precedent is already in the repo.
## Effort & risk
| Piece | Rough size | Notes / risk |
|---|---|---|
| KMDF + VHF virtual-HID driver | large | KMDF (kernel) is a higher bar than SudoVDA's UMDF/IddCx; bulk of the work |
| Driver signing + distribution | medium | EV cert + Microsoft attestation for production; test-signing for dev; SudoVDA precedent but it's pre-signed/vendored, not built here |
| Userspace `DualSenseManager` (Windows) | smallmedium | Mostly a port of the Linux report packing/parsing; reuses descriptors |
| `PadBackend::DualSense` Windows arm + negotiation | small | Un-gate the existing dispatch for Windows |
| HidHide-style hiding of a physical pad | small (maybe unneeded) | Headless host usually has no physical pad; only matters if one is attached |
**Top risks:** (1) a KMDF/VHF driver is real kernel work + signing logistics; (2) whether VHF's
output-report callback cleanly surfaces the DualSense `0x02` effect report we need for adaptive
triggers; (3) whether games/Steam/`Windows.Gaming.Input`/GameInput accept a VHF-sourced DualSense the
same as a physical one (descriptor + VID/PID should suffice, but unverified on Windows).
## Decision matrix
| Option | Adaptive triggers / DS5 identity | Effort | When it's right |
|---|---|---|---|
| **A. VHF virtual DualSense** (parity) | ✅ full | large (kernel driver) | the goal — matches the Linux host |
| **B. ViGEm DS4** (interim) | ❌ never (DS4 ceiling) | small | quick PS-pad-on-Windows w/ touchpad/motion/lightbar/rumble, no adaptive triggers |
| **C. Hybrid** | A for DS5 clients, B/Xbox360 fallback | A + small | belt-and-suspenders once A exists |
| **D. Defer** | — | — | if a higher-ROI item (#9 launch, #7/#18 audio) wins the slot |
Xbox 360 (XInput) is already implemented and covers most Windows games regardless.
## Open questions — REQUIRES the web-research pass (search was down)
1. **VHF specifics:** confirm VHF is the right/current mechanism (vs. a newer HID-injection API);
exact API (`VhfCreate`/`VhfStart`/`VhfReadReportSubmit`/the output-report `EvtVhf…WriteReport`
callback); KMDF-only or UMDF-capable; minimum Windows version; the MS `vhidmini`/VHF sample.
2. **Existing driver to vendor:** is there a maintained virtual-HID / virtual-DualSense Windows driver
(Nefarius/community) we can vendor like SudoVDA, instead of writing a KMDF driver from scratch?
3. **Recognition:** does a VHF device with VID `054C`/PID `0CE6` + the DualSense descriptor get
recognized as a DualSense by Windows.Gaming.Input / GameInput / Steam Input / native-DS5 games —
including adaptive triggers via the `0x02` output report?
4. **Signing/distribution:** attestation vs. WHQL for a KMDF driver; can we test-sign for dev and ship
an attestation-signed driver via the Inno installer like SudoVDA?
5. **HidHide:** needed at all on a (usually headless) host, or only when a physical pad is present?
## Recommended plan
1. **Web-research pass** (when search is back) to close the five questions above — especially #2
(vendor vs. build) and #1 (VHF feasibility + output-report support), which gate the whole effort.
2. If VHF (or a vendorable driver) is confirmed feasible: build **Option A** — driver + Windows
`DualSenseManager` + un-gate `PadBackend::DualSense`, reusing the inputtino descriptor and the
existing `HidOutput` plane (no protocol/client/ABI change), packaged via the SudoVDA path.
3. Keep **Xbox 360** as-is and treat **ViGEm DS4** only as an optional fallback (Option C), never as
the DualSense answer.