Files
punktfunk/crates/pf-client-core/Cargo.toml
T
enricobuehler a9e7c033c3 test(client/vaapi): the last rung of the ladder, finally checked in pixels — 7 legs, all bit-identical
Every other decode rung earns `verified` with frame-hash parity against
libavcodec. VAAPI could not: it hands out a DRM-PRIME dmabuf whose memory the
driver tiles, so nothing could read its decoded pixels back, and all four of its
legs sat at "never frame-hash parity-checked".

That was never bookkeeping. The D3D11VA AV1 rung decoded 250 frames, streamed
4K60 through a clean five-minute soak, and produced WRONG PIXELS for 186 of 250
frames on NVIDIA and 245 of 250 on Intel. It looked perfect on glass; only the
goldens caught it, and the same defect turned out to be in H.264 on two other
rungs. VAAPI was the one rung where that class of bug could still be sitting
with nothing able to see it.

It is not. Measured on .25 (Radeon 780M, RDNA3, radeonsi, Mesa 26.0.3, VA-API
1.23) on 2026-08-08, against the SAME golden files the Vulkan and D3D11VA rungs
are held to, read across the crate boundary rather than copied:

  H.264 vendored vector            250/250 bit-identical  (7 from the flush)
  H.264 our host, low-delay 640x480 120/120 bit-identical  (3 from the flush)
  H.265 vendored vector            250/250 bit-identical  (2 from the flush)
  H.265 our host, low-delay 640x480 120/120 bit-identical  (0 from the flush)
  HEVC Main 10, P010                 50/50 bit-identical  (2 from the flush)
  AV1 vendored vector              250/250 delivered of 274 decoded, and
                                   display frame 0 byte-identical to
                                   libavcodec's own PIXELS
  AV1 our host, 4K two-tile          60/60 bit-identical

⚠ ONE vendor. AMD/radeonsi only; no Intel iHD box has run these legs.

The readback that made it possible:

* `pf-vaadec`'s `va` module gains `VAImage` and `VAImageFormat`, hand-declared
  with every size and offset measured off libva 2.23.0's real headers by
  `layout-probe.c` and pinned as compile-time assertions — the same discipline
  the decode buffers already keep. The trap: `VAImage::width`/`height` are
  16-bit, so `data_size` sits at 60 and not at the 64 counting 32-bit fields
  gives, and every field after them is two bytes earlier than it looks.
* `pack_two_plane` is the pure geometry — the crop to the picture, the padding
  columns dropped per row, and the chroma plane taken from the driver's OWN
  `offsets[1]` rather than from `pitch * display_height`, which is the 1088-row
  smear this program has already paid for once. It needs no device, so ten CPU
  tests cover it on macOS and in the container.
* `video_vaapi_native::parity` drives the seven streams above through the
  production entry point and hashes what the rung DELIVERS, in delivery order,
  tail included — so the delivery path is under test as well as the decode, and
  a frame's surface comes from its own release token rather than from an
  inference about which pool entry holds which picture.

THE READBACK CANNOT REACH THE PRODUCTION PATH, and that is structural rather
than a promise. `vaDeriveImage`, `vaCreateImage`, `vaGetImage`, `vaMapBuffer`
and the rest are resolved by a `#[cfg(test)]` type that dlopens libva itself;
the production `Libva` gains no field; `sha2` is a dev dependency. A CPU test
scans this file's own source and fails if any of those symbols is dlsym'd
outside the harness, so a refactor cannot quietly undo it.

Derive is not guaranteed, so both routes are implemented and neither is
optional: `vaDeriveImage` first, `vaCreateImage` + `vaGetImage` as the fallback
(which also detiles), and if neither yields the pool's own fourcc the leg FAILS
naming what the driver gave it. There is no skip path — a parity test that
passes because it could not read anything is the failure mode this program has
been bitten by three times. Both answer on radeonsi, the first frame of every
leg is read through BOTH and they must agree, and `PF_VAAPI_READBACK=getimage`
reproduces the H.264 leg's 250/250 through the copying route alone, so the
fallback is exercised rather than merely written.

And it can fail — proven, not asserted. Planting the real geometry defect this
driver's layout makes visible (rows read contiguously, ignoring the 512-byte
pitch behind a 320-wide picture) fails at display frame 0 with the full
localisation: 68312 luma and 14998 chroma samples differing, max |delta| 255,
luma bounding box (0,1)..(319,239) — and with the goldens forced through one
route, 250/250 diverging with "suspect the readback geometry". `compare` and
`localise` also have CPU counterfactuals, and a hardware leg proves the readback
reads real and DISTINCT pixels and localises a one-byte flip to the exact pixel.

⚠ One thing the hardware legs do NOT cover, found by planting the other defect
and watching it do nothing: radeonsi's decode surfaces for every fixture here
have no VERTICAL padding — `offsets[1]` is exactly `pitch * height` — so the
chroma-plane trap is untested on this driver, and `pf-vaadec`'s
`reading_chroma_at_the_display_height_would_have_been_caught` is the only place
it is checked at all. `probe_this_machines_readback_routes` now prints the
derived layout and says which of the two it is, so the next driver answers for
itself instead of being assumed.
2026-08-08 00:55:03 +02:00

189 lines
11 KiB
TOML

[package]
name = "pf-client-core"
description = "Shared client plumbing (Linux + Windows) — session pump, native video decode, PipeWire/WASAPI audio, SDL3 gamepads, trust store, discovery — extracted from the GTK client so the shells and the Vulkan session binary build on one implementation"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
# Linux + Windows: the Vulkan session client builds on both; `cargo build --workspace`
# stays green on macOS (the Mac client lives in clients/apple) — there this crate is
# `wol` plus stubs-free emptiness. `wol` is pure std and stays cross-platform, matching
# the old main.rs. Audio is the one per-OS swap: PipeWire on Linux, WASAPI on Windows
# (same public surface — see lib.rs).
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
# Native Vulkan Video decode (WP-C of the native-decode program, HEVC added by M3
# WP-2, AV1 by M7): auto's TOP rung on both desktop OSes since M9 — for every codec it
# speaks, AV1 included — also pinnable via `PUNKTFUNK_DECODER=native-vulkan` —
# video_vk_native.rs, running pf-vkdecode's VkH264Decoder/VkH265Decoder/VkAv1Decoder on
# the presenter's shared device.
pf-vkdecode = { path = "../pf-vkdecode" }
# The one bitstream parser (M1): the SOFTWARE rung reads its per-picture colour
# signalling, IDR flag and recovery-point SEI from the same `AuPlan` every hardware rung
# already submits from (`video_software.rs`). That is what makes the swscale BT.601
# default unrepresentable rather than merely fixed — there is no second colour source
# left to disagree with.
pf-bitstream = { path = "../pf-bitstream" }
async-channel = "2"
# M8's software rung, the ladder's last one — no FFmpeg in either half.
#
# H.264: openh264 (BSD-2), already a workspace dependency (the HOST's GPU-less encoder,
# `pf-encode/src/enc/sw.rs`), so the licence posture and the bundled-source build are
# both already settled and already compiled by every `--workspace` leg.
#
# AV1: rav1d (BSD-2) — dav1d itself, ported to Rust by the ISRG/Prossimo memory-safety
# project. The plan of record names "dav1d"; the `dav1d` crate reaches it through
# `dav1d-sys`, which is `system-deps`-only (no vendored build): it needs `dav1d.pc` +
# headers at build time and `libdav1d.so`/`dav1d.dll` at run time on EVERY client
# package. That is a new system codec dependency added by the milestone family whose
# §6 excision checklist exists to delete exactly those. rav1d is the same decoder with
# none of that: pure Rust, no linker, nothing new in any package.
#
# ⚠ Both of these are NEW COMPILE COST on the client packaging legs, which is easy to
# miss because the workspace already built openh264: every client leg is `-p`-scoped and
# excludes pf-encode (flatpak's `cargo build -p punktfunk-client-linux -p
# punktfunk-client-session -p punktfunk-cli`, windows.yml/windows-msix.yml's
# `-p punktfunk-client-windows …`, deb.yml's client job, packaging/nix's
# `punktfunk-client`), so all of them compile the bundled OpenH264 tree for the FIRST
# time here. Only the `--workspace` CI legs and the host packages built it before.
#
# `default-features = false` drops two things deliberately:
# * `asm` — rav1d's hand-written assembly needs `nasm` at build time, and it is NOT the
# same trade openh264 makes next to it: openh264-sys2's `try_compile_nasm` returns
# quietly when nasm is missing ("Failed to compile NASM files, not using any
# assembly") and the C build still succeeds, whereas rav1d's build.rs PANICS ("NASM
# build failed. Make sure you have nasm installed or disable the \"asm\" feature").
# So turning `asm` on makes nasm a hard build requirement of every client package,
# and `ci/rust-ci.Dockerfile` — the container the client .deb and the workspace CI
# build in — does not have it (arch, rpm, nix and the FFmpeg-building noble image
# all do; the flatpak GNOME SDK and the Windows runner are not provisioned by
# anything in this tree). Making the rung that only ever runs BECAUSE the GPU
# already failed a build-breaker for the legs that ship it is the wrong way round.
# Turn it back on the day every client leg provisions nasm — and expect a large
# speedup when you do; this is dav1d's asm, and the Rust fallbacks are much slower.
# * `bitdepth_16` — the CPU rung is 8-bit by contract (`video_software.rs` refuses
# anything else rather than mis-scaling it), so building the 10/12-bit half would be
# compiling a path the code refuses to take.
#
# One packaging risk this DOESN'T carry: rav1d exports dav1d's C ABI as `#[no_mangle]`
# symbols (`dav1d_open`, `dav1d_send_data`, …), which could in principle interpose on a
# real libdav1d loaded into the same process. It cannot here — these are Rust `staticlib`
# symbols in an executable with no `-rdynamic` and no dynamic export table entry, so the
# loader never offers them to anyone. That changes if pf-client-core ever becomes a
# `cdylib` or a leg adds `-rdynamic`/`--export-dynamic`; re-check it then.
openh264 = "0.9"
rav1d = { version = "1", default-features = false, features = ["bitdepth_8"] }
# errno names for rav1d's negated-`c_int` returns (`video_software.rs`): `ENOPROTOOPT` —
# the code a `bitdepth_8`-only build answers a 10-bit stream with — is 92 on Linux and
# 123 on Windows, and rav1d re-exports only `Dav1dResult`, so the typed enum that would
# otherwise name it is out of reach. Already in the tree (rav1d's own dependency).
libc = "0.2"
# Opus for the audio planes. The VIDEO side has no FFmpeg at all since M10: every decode
# rung is native (pf-vkdecode / pf-dxvadec / pf-vaadec / openh264+rav1d) and the codec
# vocabulary is `punktfunk_core::quic`'s own `CODEC_*` wire bits. The HOST still encodes
# with libavcodec (`pf-encode`); nothing in this crate does.
opus = "0.3"
mdns-sd = "0.20"
# PyroWave decode (the opt-in wired-LAN wavelet codec, design/pyrowave-codec-plan.md
# §4.5) — pure Vulkan compute on the presenter's shared device, so it builds wherever the
# spawned Vulkan session presenter runs: Linux AND Windows (pyrowave-sys covers both; it
# is an empty stub elsewhere). `ash` only wraps the presenter's existing raw handles
# (same pinned version as pf-presenter).
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
ash = { version = "0.38", optional = true }
# Game-library fetch from the host's management API over mTLS + fingerprint pinning.
# `ureq` is small + sync (the host uses it too) and its rustls unifies with the
# workspace's (quinn's) 0.23; the pinning verifier mirrors core's private `PinVerify`.
ureq = "2"
# Signed update-manifest fetch/verify + the install-kind ladder, shared with the host so one
# trust rule serves both (crates/pf-update-check).
pf-update-check = { path = "../pf-update-check" }
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
# Stable ids for profiles and host records (profiles.rs) — the OS RNG only, same version the
# workspace already resolves for punktfunk-core. No uuid crate: the v4 layout is four lines.
rand = "0.9"
# Gamepads: capture + feedback (full DualSense fidelity — touchpad/motion/triggers/LEDs
# need the hidapi driver). Linux links the system SDL3; Windows builds it from source
# (no system SDL3 there — same choice as clients/windows).
[target.'cfg(target_os = "linux")'.dependencies]
pipewire = "0.9"
sdl3 = { version = "0.18", features = ["hidapi"] }
# Native VAAPI decode (M6 of the native-decode program): the hand-declared libva buffer
# layouts, the profile/format/surface decisions, the AuPlan → picparams/IQ/slice
# conversion and the DRM-PRIME export descriptor that `video_vaapi_native` marshals.
# Cross-platform on purpose — everything decidable without a device is tested by the
# ordinary macOS and container gates, exactly as pf-dxvadec does for Windows.
pf-vaadec = { path = "../pf-vaadec" }
# libva itself is dlopen'd, never linked (see `video_vaapi_native`'s module docs): the
# container can then compile and clippy the whole rung without `libva-dev`, and a machine
# without a VAAPI runtime gets a clean refusal instead of a packaging dependency.
libloading = "0.8"
[target.'cfg(windows)'.dependencies]
wasapi = "0.23"
# Native D3D11VA decode (M5 of the native-decode program): the hand-declared DXVA buffer
# layouts and the AuPlan → picparams/qmatrix/slice-control conversion that video_d3d11_native
# submits. Windows-only because the rung is; the crate itself is cross-platform CPU code so
# its tests run on every CI leg (which is the point — `cfg(windows)` code cannot be tested by
# the Linux or macOS gates at all).
pf-dxvadec = { path = "../pf-dxvadec" }
# Pad-audio correlation (pad_audio.rs): the HID devnode's ContainerID and a render endpoint's
# stamped PKEY_Device_ContainerId both live in the registry — read-only, which sidesteps COM
# property stores entirely (the same version the host pins).
winreg = "0.56"
sdl3 = { version = "0.18", features = ["hidapi", "build-from-source"] }
# D3D11 decode-device plumbing (video_d3d11.rs): device/adapter selection, DXVA probes, and
# the shared NT-handle hand-off ring `video_d3d11_native` fills. Same pinned rev as
# clients/windows so the workspace builds ONE windows-rs.
windows = { git = "https://github.com/microsoft/windows-rs", rev = "acb5a1a7441033d9312b16842af02eb0c2b403dc", features = [
# Features are header-named since windows-rs generates from the SDK headers directly
# (#4689) — one feature per header, replacing the old `Win32_*` namespace features.
"d3d11",
"d3dcommon",
"dxgi",
"handleapi",
# RECT/HMONITOR for DXGI_OUTPUT_DESC1 (the display-HDR volume query).
"windef",
# IDXGIResource1::CreateSharedHandle takes an optional SECURITY_ATTRIBUTES.
"minwinbase",
# The GlobalAlloc block the clipboard takes ownership of (clipboard.rs).
"winbase",
# The OS-clipboard bridge (clipboard.rs): Open/Get/SetClipboardData + the sequence
# number — all in winuser now.
"winuser",
] }
[target.'cfg(any(target_os = "linux", windows))'.dev-dependencies]
# The two platform native rungs' frame-hash parity tests compare decoded surfaces against
# the libavcodec goldens M5 captured — the same SHA-256 list, and the same crate,
# pf-vkdecode's Vulkan parity legs use (already in the workspace lock). The goldens are
# checked-in hashes; nothing links FFmpeg to read them.
#
# Windows was the only platform here until the VAAPI rung grew a readback: `cfg(windows)`
# for `video_d3d11_native::parity`, now `cfg(linux)` as well for
# `video_vaapi_native::parity`. A DEV dependency, so no shipped binary gains anything —
# which is also part of why the VAAPI readback cannot reach the production video path.
sha2 = "0.10"
[features]
# PyroWave client decode ships in every default build (flatpak included; pyrowave-sys is a
# vendored in-repo tree, offline-safe, and an empty stub off Linux/Windows). The codec is
# still strictly per-session opt-in (Settings codec pick / PUNKTFUNK_PREFER_PYROWAVE=1).
default = ["pyrowave"]
pyrowave = ["dep:pyrowave-sys", "dep:ash"]
[lints]
workspace = true