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:
+10
-4
@@ -1,8 +1,9 @@
|
||||
# Packaging punktfunk for Fedora / Bazzite
|
||||
|
||||
The punktfunk host is Linux-only and links system FFmpeg (NVENC), PipeWire, Opus and
|
||||
the NVIDIA driver. This directory packages it for the **Fedora Atomic / Bazzite** world
|
||||
(rpm-ostree + bootc), where most of those deps are already present.
|
||||
The punktfunk host links system FFmpeg (NVENC on NVIDIA, VAAPI on AMD/Intel, with a GPU-less
|
||||
software-H.264 fallback), PipeWire and Opus. This page covers packaging it for the
|
||||
**Fedora Atomic / Bazzite** world (rpm-ostree + bootc), where most of those deps are already
|
||||
present; the NVIDIA-specific notes below apply to the NVENC path.
|
||||
|
||||
> 👉 **Ubuntu/Debian hosts** install via `apt` from Gitea's package registry — see
|
||||
> [`debian/README.md`](debian/README.md) (`apt update && apt upgrade` for new builds).
|
||||
@@ -20,6 +21,10 @@ packaging/
|
||||
copr/ # COPR build-from-SCM settings
|
||||
```
|
||||
|
||||
The other packaging targets have their own READMEs: [`debian/`](debian/README.md) (apt),
|
||||
[`arch/`](arch/README.md) (PKGBUILD + sysext), [`flatpak/`](flatpak/README.md) (the client),
|
||||
[`windows/`](windows/README.md) (host installer + drivers), plus `kde/` and `linux/` helpers.
|
||||
|
||||
## What's needed beyond base Fedora
|
||||
|
||||
| Dependency | Where it comes from |
|
||||
@@ -121,7 +126,8 @@ An RPM (or the bootc layer) installs into the host system where those just work.
|
||||
## Building the SRPM/RPM locally (Fedora only)
|
||||
|
||||
```sh
|
||||
git archive --format=tar.gz --prefix=punktfunk-0.0.1/ -o ~/rpmbuild/SOURCES/punktfunk-0.0.1.tar.gz HEAD
|
||||
git archive --format=tar.gz --prefix=punktfunk-0.3.0/ -o ~/rpmbuild/SOURCES/punktfunk-0.3.0.tar.gz HEAD
|
||||
rpmbuild -ba packaging/rpm/punktfunk.spec # needs the BuildRequires from the spec
|
||||
# (0.3.0 = the spec's default %{pf_version}; the prefix and tarball name must match it)
|
||||
```
|
||||
(Not buildable on Debian/Ubuntu — use a Fedora toolbox/container or COPR.)
|
||||
|
||||
@@ -404,8 +404,8 @@ Debian/Ubuntu — the host links system FFmpeg/PipeWire and won't build there),
|
||||
`packaging/README.md`:
|
||||
|
||||
```sh
|
||||
git archive --format=tar.gz --prefix=punktfunk-0.0.1/ \
|
||||
-o ~/rpmbuild/SOURCES/punktfunk-0.0.1.tar.gz HEAD
|
||||
git archive --format=tar.gz --prefix=punktfunk-0.3.0/ \
|
||||
-o ~/rpmbuild/SOURCES/punktfunk-0.3.0.tar.gz HEAD # 0.3.0 = the spec's default version
|
||||
rpmbuild -ba packaging/rpm/punktfunk.spec # needs the spec's BuildRequires + RPM Fusion
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
`punktfunk-host` is published as a `.deb` to **Gitea's Debian package registry** in the public
|
||||
`unom` org, so the Ubuntu hosts update with plain `apt`. CI (`.gitea/workflows/deb.yml`) builds
|
||||
and publishes on every push to `main` (a rolling `0.3.0~ciN.g<sha>` build to the **`canary`** apt
|
||||
and publishes on every push to `main` (a rolling `0.5.0~ciN.g<sha>` build to the **`canary`** apt
|
||||
distribution) and on `vX.Y.Z` tags (a clean `X.Y.Z` to the **`stable`** distribution, plus attached
|
||||
to the unified Gitea Release). The two are separate apt distributions, so a stable box never jumps
|
||||
to a canary build — see [Release Channels](https://punktfunk.unom.io/docs/channels). The repo line
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
`punktfunk-host` is published as an RPM to **Gitea's RPM package registry** in the public `unom`
|
||||
org (stable groups `bazzite`/`fedora-44`, canary groups `bazzite-canary`/`fedora-44-canary`), so
|
||||
Bazzite / Fedora Atomic hosts layer and update it with `rpm-ostree`. CI (`.gitea/workflows/rpm.yml`)
|
||||
builds and publishes on every push to `main` (a rolling `0.3.0-0.ciN.<sha>` build to the `*-canary`
|
||||
builds and publishes on every push to `main` (a rolling `0.5.0-0.ciN.g<sha>` build to the `*-canary`
|
||||
groups) and on `vX.Y.Z` tags (a clean `X.Y.Z-1` to the base groups, plus attached to the unified
|
||||
Gitea Release) — separate repos, so a stable box never jumps to a canary build (see
|
||||
[Release Channels](https://punktfunk.unom.io/docs/channels)). The `baseurl` below subscribes to the
|
||||
@@ -107,8 +107,9 @@ tracking: `rpm-ostree override` / `rpm-ostree uninstall punktfunk`.
|
||||
|
||||
```sh
|
||||
PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh # host + client
|
||||
PF_VERSION=0.0.1 PF_WITH_WEB=1 bash packaging/rpm/build-rpm.sh # + the noarch punktfunk-web (needs bun on PATH)
|
||||
# -> dist/punktfunk-0.0.1-1.fcNN.x86_64.rpm (+ punktfunk-web-0.0.1-1.fcNN.noarch.rpm with PF_WITH_WEB=1)
|
||||
PF_VERSION=0.0.1 PF_WITH_WEB=1 bash packaging/rpm/build-rpm.sh # + punktfunk-web (needs bun on PATH)
|
||||
# -> dist/punktfunk-0.0.1-1.fcNN.x86_64.rpm (+ punktfunk-web-0.0.1-1.fcNN.x86_64.rpm with PF_WITH_WEB=1;
|
||||
# the web subpackage vendors a bun binary, so it's arch-specific, not noarch)
|
||||
```
|
||||
|
||||
Run it inside the Fedora 43 builder image so the deps resolve and match Bazzite:
|
||||
@@ -119,4 +120,5 @@ docker run --rm -v "$PWD:/src" -w /src punktfunk-fedora-rpm \
|
||||
bash -lc 'git config --global --add safe.directory /src && PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh'
|
||||
```
|
||||
|
||||
A plain `rpmbuild`/COPR build with no `pf_version`/`pf_release` defines produces `0.0.1-1`.
|
||||
A plain `rpmbuild`/COPR build with no `pf_version`/`pf_release` defines produces `0.3.0-1` (the
|
||||
spec defaults).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user