From eecd04247fcdef1603d7ca42ce9c2a049028b618 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 6 Aug 2026 19:27:24 +0200 Subject: [PATCH] feat(client): AV1 is advertised on hardware, not on a decoder existing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standing open item M7 was meant to close. `decodable_codecs` answered the AV1 bit from `ffmpeg::decoder::find(AV1)`, which says yes on every build that links libdav1d — a software decoder. So the client told the host "send me AV1" on machines that would then decode a 4K stream on the CPU, and codec negotiation happens once at Welcome, so there is nothing to fall back to afterwards. A promise the client cannot keep is worse than not making it. `av1_hardware_decodable` answers from device facts only: the presenter's Vulkan device listing DECODE_AV1 among its decode queue family's codec operations, or — on Windows — the D3D11 import path, which is the same gate the D3D11VA rung sits behind and that rung decodes AV1 Profile 0 today. VAAPI is deliberately not consulted: asking libva costs opening a display, and this is called too early and too often for that. The Vulkan bit covers the Mesa devices where VAAPI AV1 exists in practice, and a machine with VAAPI AV1 but no Vulkan AV1 loses the advertisement, not a working path. The test pins what the gate must not accept: a device that decodes H.264 and H.265 but lists no AV1 operation, and a device whose caps word claims AV1 while it has no decode queue at all. Gates: macOS fmt/clippy, container clippy -D warnings over six crates, 805 tests, workspace check. --- crates/pf-client-core/src/video.rs | 90 +++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index caa46550..86a1ea9a 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -809,6 +809,11 @@ const VIDEO_CODEC_OP_DECODE_H264: u32 = 0x0000_0001; /// would only invite a gate that admits a session nothing can decode.) const VIDEO_CODEC_OP_DECODE_H265: u32 = 0x0000_0002; +/// `VK_VIDEO_CODEC_OPERATION_DECODE_AV1_BIT_KHR`. The Deck's VanGogh advertises +/// it alongside H.264/H.265/VP9, and it is what +/// [`av1_hardware_decodable`] reads. +const VIDEO_CODEC_OP_DECODE_AV1: u32 = 0x0000_0004; + /// The native decoder for a negotiated wire codec, plus the /// `VkVideoCodecOperationFlagBitsKHR` the presenter's decode family must advertise /// for it — or `None` for a codec pf-vkdecode cannot decode natively. @@ -990,6 +995,9 @@ pub(crate) unsafe fn codec_name(codec: *const ffmpeg::ffi::AVCodec) -> String { /// The `quic` codec bitfield this client can decode — whatever FFmpeg has a decoder for (HEVC/H.264 /// always; AV1 when built in). Advertised to the host so it never emits a codec we can't decode. +/// +/// ⚠ **AV1 here is a decoder EXISTING, not a decoder that can keep up.** Use +/// [`decodable_codecs_for`], which gates it on hardware — see [`av1_hardware_decodable`]. pub fn decodable_codecs() -> u8 { let _ = ffmpeg::init(); let mut bits = 0u8; @@ -1005,12 +1013,56 @@ pub fn decodable_codecs() -> u8 { bits } +/// Can this machine decode AV1 in HARDWARE? +/// +/// The question exists because `ffmpeg::decoder::find(AV1)` answers yes on every +/// build that links libdav1d — a SOFTWARE decoder — so advertising AV1 off that +/// answer tells the host "send me AV1" on machines that will then try to decode a +/// 4K stream on the CPU. That is the standing open item M7 closes: the wire's codec +/// negotiation is a promise about capability, and a promise the client cannot keep +/// is worse than not making it, because the host has no other codec to fall back to +/// once the session is running. +/// +/// Answered from device facts only, never from a decoder registry: +/// +/// * the presenter's Vulkan device advertises `DECODE_AV1` in its decode queue +/// family's codec operations, or +/// * (Windows) the presenter can import D3D11 textures, which is the same gate the +/// D3D11VA rung itself sits behind — that rung decodes AV1 Profile 0 today +/// (`video_d3d11.rs`'s profile table), so a machine reaching it has hardware AV1. +/// +/// ⚠ Deliberately NOT consulted: VAAPI. Asking libva costs opening a display, which +/// this function is called too early and too often to do; the Vulkan bit covers the +/// Mesa devices where VAAPI AV1 exists in practice, and a machine with VAAPI AV1 but +/// no Vulkan AV1 loses only the ADVERTISEMENT, not a working path. +pub fn av1_hardware_decodable(vk: Option<&VulkanDecodeDevice>) -> bool { + if vk.is_some_and(|v| v.video_decode && v.decode_video_caps & VIDEO_CODEC_OP_DECODE_AV1 != 0) { + return true; + } + #[cfg(windows)] + { + return vk.is_some_and(|v| v.d3d11_import); + } + #[cfg(not(windows))] + false +} + /// [`decodable_codecs`] plus the PyroWave bit when the presenter's device passed the /// compute-feature probe. Advertisement-only: `resolve_codec` never auto-picks PyroWave — /// the session must also name it `preferred_codec` (plan §3), which the client does only /// under its explicit opt-in. pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 { - let bits = decodable_codecs(); + let mut bits = decodable_codecs(); + // AV1 is hardware-gated (M7). Without this the bit rides on libdav1d's mere + // presence and the host is told to send AV1 to a machine that would decode it on + // the CPU — and once the session is negotiated there is nothing to fall back to. + if bits & punktfunk_core::quic::CODEC_AV1 != 0 && !av1_hardware_decodable(vk) { + tracing::info!( + "AV1 not advertised: no hardware AV1 decode on this device (a software \ + decoder exists, but a 4K AV1 stream is not survivable on it)" + ); + bits &= !punktfunk_core::quic::CODEC_AV1; + } #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if vk.map(|v| v.pyrowave_decode).unwrap_or(false) { return bits | punktfunk_core::quic::CODEC_PYROWAVE; @@ -2150,6 +2202,42 @@ mod tests { /// decision and again by M3 WP-2's HEVC wiring): the pin AND the auto family /// admit on a capable H.264 or HEVC session — native sits immediately above /// FFmpeg-Vulkan because the program is dropping FFmpeg — while every explicit + /// AV1 is advertised on a HARDWARE fact, never on a decoder existing. + /// + /// The standing open item M7 closes. `ffmpeg::decoder::find(AV1)` says yes + /// wherever libdav1d is linked, so the old advertisement told the host "send me + /// AV1" on machines that would then decode it on the CPU — and codec negotiation + /// happens once, so there is no falling back afterwards. + #[test] + fn av1_is_advertised_only_where_hardware_can_decode_it() { + // No device at all: no claim. + assert!(!av1_hardware_decodable(None)); + + // A decode-capable device that does NOT list AV1 among its codec + // operations. `video_decode` alone is not the question — plenty of devices + // decode H.264 and H.265 and no AV1. + let mut dev = decode_device(0x10de, "no-av1"); + dev.decode_video_caps = VIDEO_CODEC_OP_DECODE_H264 | VIDEO_CODEC_OP_DECODE_H265; + #[cfg(not(windows))] + assert!( + !av1_hardware_decodable(Some(&dev)), + "H.264+H.265 decode support says nothing about AV1" + ); + + // The AV1 operation bit is the yes. + let mut dev = decode_device(0x1002, "vangogh-ish"); + dev.decode_video_caps = + VIDEO_CODEC_OP_DECODE_H264 | VIDEO_CODEC_OP_DECODE_H265 | VIDEO_CODEC_OP_DECODE_AV1; + assert!(av1_hardware_decodable(Some(&dev))); + + // A device whose decode queue is absent cannot be taken at its caps word. + let mut dev = decode_device(0x1002, "no-decode-queue"); + dev.decode_video_caps = VIDEO_CODEC_OP_DECODE_AV1; + dev.video_decode = false; + #[cfg(not(windows))] + assert!(!av1_hardware_decodable(Some(&dev))); + } + /// backend pin refuses (`vulkan` names the FFmpeg-Vulkan backend specifically and /// must keep meaning exactly that), and the codec/device legs still refuse for /// every choice. The codec's OWN caps bit is the device leg: admitting HEVC on an