feat(protocol,clients): codec preference negotiation + Linux client decodes per Welcome (Phase 2a)

Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)

**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
  `video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
  can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
  resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
  HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
  (drift-guarded against the wire values). Header regenerated.

**Host:** passes `hello.preferred_codec` into `resolve_codec`.

**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.

**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).

Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 00:13:26 +00:00
parent ffc0b07b46
commit 12843fe253
36 changed files with 529 additions and 144 deletions
+2 -1
View File
@@ -108,7 +108,8 @@ read it from `%ProgramData%\punktfunk\web-password`.
## Dev iteration on the test box (driver)
Two helpers wrap the painful manual steps of iterating on the pf-vdisplay driver against a live host
service. Run **elevated**; both default to the `PunktfunkHost` service.
service. Run **elevated**; both default to the `PunktfunkHost` service. (The `C:\t-goal1\...` probe
path below is the maintainer's test box — substitute your own `punktfunk-probe.exe` build.)
```powershell
# Recover a WEDGED driver. Symptom: every session fails with
+2 -2
View File
@@ -10,8 +10,8 @@
`cargo build --release` builds the whole workspace (this shares wdk-sys/wdk-build + the bindgen pin with
pf-vdisplay). Then, per driver: CLEAR the FORCE_INTEGRITY PE bit, sign the .dll, stampinf a DriverVer
into the INF; then Inf2Cat both catalogs and sign them. Both drivers share ONE self-signed cert (or a
supplied DRIVER_CERT secret) + ONE exported .cer - the layout install-gamepad-drivers.ps1 consumes
(per-driver .inf/.cat/.dll + one shared punktfunk-driver.cer).
supplied DRIVER_CERT secret) + ONE exported .cer - the layout `punktfunk-host.exe driver install
--gamepad` consumes (per-driver .inf/.cat/.dll + one shared punktfunk-driver.cer).
Output (-Out): pf_dualsense.{dll,inf,cat} + pf_xusb.{dll,inf,cat} + punktfunk-driver.cer.
@@ -8,7 +8,7 @@ DualSense a near-native feel with **no external gamepad dependencies** (no ViGEm
Shipping: the driver is one member of the in-tree driver workspace
([`packaging/windows/drivers/`](../../README.md)), built from source in CI, and bundled +
`pnputil`-installed by the Windows host [installer](../../README.md). The host feeds it over a shared
memory channel from `crates/punktfunk-host/src/inject/dualsense_windows.rs`. The same UMDF driver also
memory channel from `crates/punktfunk-host/src/inject/windows/dualsense_windows.rs`. The same UMDF driver also
serves the **DualShock 4** identity per a `device_type` byte the host stamps.
This README captures the driver-authoring lore — the bugs and the signing recipe that make a
@@ -37,7 +37,9 @@ $env:LIBCLANG_PATH = 'C:\Program Files\LLVM\bin'
$env:Version_Number = '10.0.26100.0' # else wdk-build picks 10.0.28000.0 (no km/crt) and bindgen fails
```
Then, in the example dir:
The shipping flow is `build-gamepad-drivers.ps1` (one level up): workspace `cargo build --release`
plus the sign steps below, staged for the installer. The original manual dev-box recipe, kept as
lore (paths reflect that era's cargo-make layout):
```powershell
cargo make # -> target\debug\pf_dualsense_package\ (.inf/.cat/.dll)
@@ -45,7 +47,8 @@ cargo make # -> target\debug\pf_dualsense_package\
# *** CRITICAL: clear the PE FORCE_INTEGRITY bit ***
# windows-drivers-rs links the DLL with /INTEGRITYCHECK, which forces a CI-trusted page-hash
# signature a self-signed cert cannot satisfy (CodeIntegrity 3004 "hash not found" /
# 3089 VerificationError 7). SudoVDA.dll has this bit OFF. Clear bit 0x80 at PE-header offset +0x5e:
# 3089 VerificationError 7). SudoVDA.dll (third-party VDD prior art, not used by punktfunk) has
# this bit OFF. Clear bit 0x80 at PE-header offset +0x5e:
$f = 'target\debug\pf_dualsense_package\pf_dualsense.dll'
$b = [IO.File]::ReadAllBytes($f); $pe = [BitConverter]::ToInt32($b,0x3c); $off = $pe + 0x5e
$dc = [BitConverter]::ToUInt16($b,$off); $bb = [BitConverter]::GetBytes([uint16]($dc -band 0xFF7F))
@@ -60,9 +63,10 @@ pnputil /add-driver target\debug\pf_dualsense_package\pf_dualsense.inf /install
devgen /add /hardwareid "root\pf_dualsense" # creates the (transient, SWD) device node
```
`devgen` is at `...\Windows Kits\10\Tools\10.0.26100.0\x64\devgen.exe`. SWD devgen devices clear on
reboot (recreate after each boot). TODO: drop the post-build PE patch by stopping wdk-build emitting
`/INTEGRITYCHECK`.
`devgen` (under `Windows Kits\10\Tools\<ver>\x64\`) is only for manual testing — the shipping
install is `punktfunk-host.exe driver install --gamepad`, and the host SwDeviceCreate's the device
per session (no persistent devnode). SWD devgen devices clear on reboot. TODO: drop the post-build
PE patch by stopping wdk-build emitting `/INTEGRITYCHECK`.
## The three bugs that made it work (porting a WDK C sample to Rust)
+11 -8
View File
@@ -43,19 +43,21 @@ Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payl
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
`large @28` · `small @29`.
## Validated live on `.173` (2026-06-22)
## 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: `C:\Users\Public\giprobe\xusbtest.exe` (creates the `pf_xusb`
`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 from `C:\Users\Public\m0\windows-drivers-rs\examples\pf-xusb` (the `../../crates` paths resolve
there); these repo files are the canonical copies — keep them in sync.
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 make` (env `LIBCLANG_PATH`, `Version_Number=10.0.26100.0`) → `target\debug\pf_xusb_package\`.
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.
@@ -63,11 +65,12 @@ there); these repo files are the canonical copies — keep them in sync.
## Host integration (done)
`crates/punktfunk-host/src/inject/gamepad_windows.rs` is the Windows `GamepadManager` (used by
`crates/punktfunk-host/src/inject/windows/gamepad_windows.rs` is the Windows `GamepadManager` (used by
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-<index>`, 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 from source (`packaging/windows/drivers/pf-xusb`),
signed, and pnputil-installed by the Inno Setup installer (via `install-gamepad-drivers.ps1`).
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
+5 -4
View File
@@ -285,8 +285,9 @@ end;
function WebSetupParams(Param: String): String;
begin
{ Pass the password to web-setup.ps1 via a temp file, not the cmdline (which lands in the install
log). Only on a fresh install - on upgrade web-setup keeps the existing file. }
{ Pass the password to `punktfunk-host.exe web setup` via a temp file, not the cmdline (which
lands in the install log). Only on a fresh install - on upgrade web setup keeps the existing
file. }
Result := '--app-dir "' + ExpandConstant('{app}') + '"';
if FreshWebInstall then
Result := Result + ' --password-file "' + ExpandConstant('{tmp}\webpw.txt') + '"';
@@ -312,7 +313,7 @@ end;
#ifdef WithWeb
{ Stop a running web console + free :3000 BEFORE the file copy, so the old server doesn't lock
.output / web-run.cmd / bun.exe and the new task can bind. Killing the :3000 listener owner is
runtime-agnostic (an early install may have run node, the current one runs bun). web-setup.ps1
runtime-agnostic (an early install may have run node, the current one runs bun). `web setup`
repeats this idempotently after the copy. Best-effort; a fresh install is a no-op. }
procedure StopWebConsole;
var
@@ -334,7 +335,7 @@ begin
StopHostServiceAndWait;
#ifdef WithWeb
StopWebConsole; { upgrade-safe: free :3000 + unlock the web files before the copy }
{ Stash the chosen password for web-setup.ps1 (fresh install only); the temp copy is auto-cleaned. }
{ Stash the chosen password for `web setup` (fresh install only); the temp copy is auto-cleaned. }
if FreshWebInstall then
SaveStringToFile(ExpandConstant('{tmp}\webpw.txt'), Trim(WebPwPage.Values[0]), False);
#endif