ef39050dbc
windows-drivers / probe-and-proto (push) Successful in 47s
ci / web (push) Successful in 58s
apple / swift (push) Successful in 1m13s
decky / build-publish (push) Successful in 16s
ci / docs-site (push) Successful in 1m19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
windows-drivers / driver-build (push) Successful in 1m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 55s
ci / bench (push) Successful in 7m18s
flatpak / build-publish (push) Successful in 6m18s
docker / deploy-docs (push) Successful in 25s
windows-host / package (push) Successful in 8m31s
release / apple (push) Successful in 11m41s
android / android (push) Successful in 13m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m57s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m16s
arch / build-publish (push) Successful in 16m24s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m6s
deb / build-publish (push) Successful in 17m54s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m27s
ci / rust (push) Successful in 18m23s
apple / screenshots (push) Successful in 5m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m24s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m43s
Six parallel audits swept the root docs, docs-site, every per-directory README, and the packaging docs; every claim below was verified against the source before editing. - README: Layout gains the six missing crates (pf-client-core, pf-presenter, pf-console-ui, pf-ffvk, pf-driver-proto, punktfunk-tray), clients/session, api/ and ci/; Linux/Windows client rows reflect the shell + Vulkan-session split and the Vulkan Video -> VAAPI/D3D11VA -> software decode chains; the "every client over a C ABI" claim is corrected (Rust clients link the core directly); tiered stats overlay + console shell noted; Apple row mentions AV1. - CONTRIBUTING: drop the dead CLAUDE.md link (deliberately untracked); point at the README's build/invariants sections. SECURITY: 0.9.0. - host-cli/pairing: --allow-pairing/--require-pairing are no-op legacy names — pairing is required by default, --allow-tofu is the real flag; document --data-port and --idle-timeout-ms. - configuration: document PUNKTFUNK_RECOVER_SESSION_CMD (session-crash recovery hook), PUNKTFUNK_MDNS, PUNKTFUNK_DATA_PORT. - virtual-displays/gnome: GNOME per-client scaling shipped (host- persisted) — flip the ❌ to ✅ and describe how it works. - stats: new "Detail levels" section (Off/Compact/Normal/Detailed + per-platform cycle gestures); retire the GTK hand-off note. - clients/install-client/status/roadmap: decode chains, Windows client validation narrowed to HDR-only pending, adaptive bitrate, console shell, Apple AV1, Windows host vendor list. - Sub-READMEs: clients/linux rewritten for the re-architecture; session Windows decode rung + d3d11va knob; Windows tiered overlay; Android minSdk 28; decky file table; host zerocopy/ path; scripts port 47992 and steamos-host.md; pf-dualsense source path. - packaging: canary version bases are tag-derived (<next-minor> via pf-version.sh/.ps1), codecs-extra not ffmpeg-full, document the pinned offline-Skia tarball + SKIA_BINARIES_URL and vulkan-headers. - Convert 15 dangling design/*.md links to the punktfunk-planning prose convention (those docs live in the private planning repo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
5.9 KiB
Markdown
89 lines
5.9 KiB
Markdown
# pf-xusb — virtual Xbox 360 XUSB companion (UMDF2, classic XInput)
|
||
|
||
A **pure-user-mode** UMDF2 driver that makes a virtual Xbox 360 controller visible to classic
|
||
**`XInputGetState`** with **no kernel bus driver** (no ViGEmBus) — the HIDMaestro approach. It is the
|
||
Windows counterpart to ViGEm's X360 target, owned in-tree.
|
||
|
||
## Why this is not the HID driver
|
||
|
||
XInput does **not** use HID. `xinput1_4.dll` enumerates the **XUSB device-interface GUID**
|
||
`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}` (`SetupDiEnumDeviceInterfaces`), opens the Nth present
|
||
instance (= player slot 0–3) with `CreateFile`, and polls it with buffered IOCTLs. So this driver:
|
||
|
||
- is **not** a HID minidriver (no `MsHidUmdf`) — it's a plain UMDF2 function driver under `WUDFRd`,
|
||
**System** setup class;
|
||
- registers the XUSB interface with `WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL)`;
|
||
- answers the XUSB IOCTLs (all `METHOD_BUFFERED`, delivered to user mode by the reflector) from
|
||
controller state the host publishes into an **unnamed** shared DATA section reached over the
|
||
**sealed pad channel** (punktfunk-planning: `gamepad-channel-sealing.md`): the host duplicates the section
|
||
handle into this driver's WUDFHost, bootstrapped via the named `Global\pfxusb-boot-<index>`
|
||
mailbox (`pf_driver_proto::gamepad::PadBootstrap`); a game's rumble (`SET_STATE`) is published
|
||
back for the host to forward to the client.
|
||
|
||
The WAIT_* IOCTLs return `STATUS_INVALID_DEVICE_REQUEST`, which makes `xinput1_4` fall back to
|
||
synchronous `GET_STATE` polling — so no manual queue / timer is needed for classic XInput. (WGI/
|
||
GameInput admission additionally needs a `xinputhid` `UpperFilters` registry tripwire + the async
|
||
`WAIT_FOR_INPUT` pump — not implemented; classic XInput does not need it.)
|
||
|
||
## Verified wire formats (source: HIDMaestro `driver/companion.c`, nefarius/XInputHooker `XUSB.h`, ViGEm)
|
||
|
||
| IOCTL | Code | Reply |
|
||
| --- | --- | --- |
|
||
| `GET_INFORMATION` | `0x80006000` | 12 B: `[0]`=ver `0x0103`, `[2]`=count `0x01`, `[8]`=VID `045E`, `[10]`=PID `028E` — marks the slot **connected** |
|
||
| `GET_CAPABILITIES` | `0x8000E004` | 24 B (or 36 B V2 if `outLen>=36`): Type `0x03`/SubType `0x01`, motor max `0xFFFF` (advertise rumble) |
|
||
| `GET_STATE` | `0x8000E00C` | **29 B**: `[0]`ver `[2]`count `[5]`u32 packet# `[0x0B]`u16 wButtons `[0x0D]`LT `[0x0E]`RT `[0x0F..0x16]`4×i16 sticks |
|
||
| `SET_STATE` | `0x8000A010` | input 5 B `{00, led, large, small, subcmd}`: `subcmd 0x02`=rumble (large `[2]`, small `[3]`), `0x01`=player-LED |
|
||
| `GET_LED_STATE` | `0x8000E008` | `{0,0,0x06}` |
|
||
| `GET_BATTERY_INFORMATION` | `0x8000E018` | `{0,0x01,0x03,0}` |
|
||
| `WAIT_GUIDE_BUTTON` / `WAIT_FOR_INPUT` | `0x8000E014` / `0x8000E3AC` | `STATUS_INVALID_DEVICE_REQUEST` → GET_STATE fallback |
|
||
|
||
`wButtons` is the `XINPUT_GAMEPAD_*` bitmap (DPAD_UP `0x0001` … A `0x1000` B `0x2000` X `0x4000`
|
||
Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payload changes.
|
||
|
||
## Shared-memory layout (unnamed DATA section, 64 B) — host writes state, driver writes rumble
|
||
|
||
`pf_driver_proto::gamepad::XusbShm` (the crate owns the offsets; both sides compile against it):
|
||
`magic u32 @0` (`"PFXU"` `0x55584650`) · `packet u32 @4` (host bumps → dwPacketNumber) · `wButtons u16
|
||
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
|
||
`large @28` · `small @29` · health marks `@32/@36` · `pad_index u32 @40` (validated against the
|
||
devnode's Location index when the delivered handle is mapped).
|
||
|
||
## Validated live (2026-06-22, maintainer's RTX test box)
|
||
|
||
`XInputGetState(0)` returns **CONNECTED** with the pushed buttons/sticks and an incrementing
|
||
`dwPacketNumber`; `XInputSetState(0xC000, 0x4000)` reaches the driver as `00 00 c0 40 02` → host sees
|
||
`large=192 small=64`. Test tools (on that box): `xusbtest.exe` (creates the `pf_xusb`
|
||
devnode + cycling state via shm) and `xinputtest.exe` (`XInputGetState`/`SetState` harness).
|
||
|
||
## Build / sign / install (same recipe as the DualSense driver)
|
||
|
||
Built as a member of the in-tree [`packaging/windows/drivers/`](../) workspace — one
|
||
`cargo build --release` builds all three drivers; `build-gamepad-drivers.ps1` (one level up) wraps
|
||
the whole build/sign/stage flow in CI. The manual steps:
|
||
|
||
1. `cargo build --release` in the workspace (env `LIBCLANG_PATH`, `Version_Number=10.0.26100.0`) →
|
||
`target\x86_64-pc-windows-msvc\release\pf_xusb.dll`.
|
||
2. Clear the FORCE_INTEGRITY PE bit (bit `0x80` at `e_lfanew+0x5e` of `pf_xusb.dll`).
|
||
3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll`.
|
||
4. `Inf2Cat /driver:<pkg> /os:10_X64` → re-sign `pf_xusb.cat` with the same thumbprint.
|
||
5. `pnputil /add-driver pf_xusb.inf` (no `/install`; the host SwDeviceCreate's `pf_xusb` per session).
|
||
|
||
## Host integration (done)
|
||
|
||
`crates/punktfunk-host/src/inject/windows/gamepad_windows.rs` is the Windows `GamepadManager` (used by
|
||
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, delivers the unnamed DATA
|
||
section over the sealed channel (`PadChannel`), writes
|
||
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
|
||
is **no ViGEmBus dependency** anymore. The driver is built + signed from source in CI
|
||
(`build-gamepad-drivers.ps1`) and installed by the Inno Setup installer via
|
||
`punktfunk-host.exe driver install --gamepad`.
|
||
|
||
## Multi-pad
|
||
|
||
The host stamps each pad's index into the device Location (`pszDeviceLocation`); the driver reads it
|
||
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and polls its own
|
||
`pfxusb-boot-<index>` bootstrap mailbox (the delivered DATA section's `pad_index` is validated against it). `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
|
||
WUDFHost, so the per-pad `SHM_INDEX` static doesn't collide. Validated live: two pads → two distinct
|
||
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
|
||
index — which only routes shared memory.)
|