forked from unom/punktfunk
fix(client): Linux auto decoder tries VAAPI before FFmpeg-Vulkan on desktop Mesa
Mesa now exposes Vulkan Video decode queues by default (and the session binary opts RADV in for the Deck's sake), which silently moved every desktop AMD/Intel box onto FFmpeg-Vulkan-on-Mesa under `auto` — user-reported (CachyOS/KDE) to judder or error-streak into the software demotion while an explicit VAAPI pick streams perfectly. Auto's hardware order is now device-aware (`VulkanDecodeDevice::prefer_vulkan_over_vaapi`, fed vendor id + device name by the presenter): Vulkan-first stays only where it is the established right answer — NVIDIA (no usable VAAPI) and the Deck's VanGogh (VAAPI dmabuf import chroma-fringes) — and everything else gets the battle-tested zero-copy VAAPI first, with Vulkan as its fallback. A mid-session Vulkan failure streak now also demotes to VAAPI before software, so a broken Mesa Vulkan path can never strand a box with a perfectly good VAAPI driver on CPU decode. The GTK shell's decoder setting gains the missing "Vulkan Video" option (values now mirror the console UI's auto/vulkan/vaapi/software) and drops its pre-Vulkan "Automatic (VAAPI → software)" label. Verified on the RTX 5070 Ti box (loopback session, auto → "Vulkan Video hardware decode active", 60 fps); policy locked by unit test; clippy -D warnings + pf-client-core/pf-presenter tests green on Linux. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ const COMPOSITORS: &[&str] = &["auto", "kwin", "wlroots", "mutter", "gamescope"]
|
|||||||
/// Codec setting values (persisted) paired with their display labels below.
|
/// Codec setting values (persisted) paired with their display labels below.
|
||||||
const CODECS: &[&str] = &["auto", "hevc", "h264", "av1"];
|
const CODECS: &[&str] = &["auto", "hevc", "h264", "av1"];
|
||||||
const CODEC_LABELS: &[&str] = &["Automatic", "HEVC (H.265)", "H.264 (AVC)", "AV1"];
|
const CODEC_LABELS: &[&str] = &["Automatic", "HEVC (H.265)", "H.264 (AVC)", "AV1"];
|
||||||
const DECODERS: &[&str] = &["auto", "vaapi", "software"];
|
const DECODERS: &[&str] = &["auto", "vulkan", "vaapi", "software"];
|
||||||
/// Touch-input model values (persisted) paired with their display labels below — the
|
/// Touch-input model values (persisted) paired with their display labels below — the
|
||||||
/// cross-client set (Android/Apple). Only meaningful on a touchscreen (Deck/tablet).
|
/// cross-client set (Android/Apple). Only meaningful on a touchscreen (Deck/tablet).
|
||||||
const TOUCH_MODES: &[&str] = &["trackpad", "pointer", "touch"];
|
const TOUCH_MODES: &[&str] = &["trackpad", "pointer", "touch"];
|
||||||
@@ -324,10 +324,12 @@ pub fn show(
|
|||||||
&dialog,
|
&dialog,
|
||||||
inline,
|
inline,
|
||||||
"Video decoder",
|
"Video decoder",
|
||||||
"Automatic tries VAAPI hardware decode, then software",
|
"Automatic picks the best hardware decode for this GPU (VAAPI on AMD/Intel, \
|
||||||
|
Vulkan Video on NVIDIA), falling back to software",
|
||||||
&[
|
&[
|
||||||
"Automatic (VAAPI → software)",
|
"Automatic (hardware → software)",
|
||||||
"Hardware (VAAPI)",
|
"Vulkan Video",
|
||||||
|
"VAAPI",
|
||||||
"Software",
|
"Software",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Video decode: reassembled HEVC access units → frames for the presenter.
|
//! Video decode: reassembled HEVC access units → frames for the presenter.
|
||||||
//!
|
//!
|
||||||
//! Three backends, picked at session start (auto: vulkan → vaapi → software;
|
//! Three backends, picked at session start (auto on Linux: vaapi → vulkan → software on
|
||||||
|
//! desktop Mesa, vulkan first on NVIDIA/VanGogh — see
|
||||||
|
//! [`VulkanDecodeDevice::prefer_vulkan_over_vaapi`];
|
||||||
//! override: `PUNKTFUNK_DECODER=vulkan|vaapi|software`):
|
//! override: `PUNKTFUNK_DECODER=vulkan|vaapi|software`):
|
||||||
//!
|
//!
|
||||||
//! * **Vulkan Video**: FFmpeg's Vulkan decoder running on the PRESENTER's own VkDevice
|
//! * **Vulkan Video**: FFmpeg's Vulkan decoder running on the PRESENTER's own VkDevice
|
||||||
@@ -384,8 +386,11 @@ impl Decoder {
|
|||||||
/// `vk` is the presenter's shared Vulkan device when its stack can run FFmpeg's
|
/// `vk` is the presenter's shared Vulkan device when its stack can run FFmpeg's
|
||||||
/// Vulkan Video decoder — decode lands as VkImages the presenter samples directly.
|
/// Vulkan Video decoder — decode lands as VkImages the presenter samples directly.
|
||||||
/// Precedence: the `PUNKTFUNK_DECODER` env override wins (support/debug escape
|
/// Precedence: the `PUNKTFUNK_DECODER` env override wins (support/debug escape
|
||||||
/// hatch, and the documented knob), then the setting; both default to auto
|
/// hatch, and the documented knob), then the setting; both default to auto.
|
||||||
/// (Vulkan → VAAPI → software; no VAAPI on Windows).
|
/// Auto's hardware order on Linux depends on the device
|
||||||
|
/// ([`VulkanDecodeDevice::prefer_vulkan_over_vaapi`]): VAAPI → Vulkan → software on
|
||||||
|
/// desktop Mesa (AMD/Intel), Vulkan → VAAPI → software on NVIDIA and the Deck's
|
||||||
|
/// VanGogh. Windows is Vulkan → D3D11VA → software (no VAAPI there).
|
||||||
pub fn new(
|
pub fn new(
|
||||||
codec_id: ffmpeg::codec::Id,
|
codec_id: ffmpeg::codec::Id,
|
||||||
pref: &str,
|
pref: &str,
|
||||||
@@ -405,6 +410,31 @@ impl Decoder {
|
|||||||
want_keyframe: false,
|
want_keyframe: false,
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
// Linux `auto`: try VAAPI FIRST unless this device is one where Vulkan Video is
|
||||||
|
// the established right answer (NVIDIA — no usable VAAPI; VanGogh — VAAPI
|
||||||
|
// chroma-fringes). Mesa now exposes decode queues by default (and the session
|
||||||
|
// binary opts RADV in for the Deck's sake), which silently moved every desktop
|
||||||
|
// AMD/Intel box onto FFmpeg-Vulkan-on-Mesa — user-reported to judder/error-streak
|
||||||
|
// (then demote to software) where explicit VAAPI streams perfectly.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let mut vaapi_tried = false;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if matches!(choice.as_str(), "auto" | "" | "hardware")
|
||||||
|
&& !vk
|
||||||
|
.filter(|v| v.video_decode)
|
||||||
|
.is_some_and(|v| v.prefer_vulkan_over_vaapi())
|
||||||
|
{
|
||||||
|
vaapi_tried = true;
|
||||||
|
match VaapiDecoder::new(codec_id) {
|
||||||
|
Ok(v) => {
|
||||||
|
tracing::info!(?codec_id, "VAAPI hardware decode active (zero-copy dmabuf)");
|
||||||
|
return done(Backend::Vaapi(v));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::info!(reason = %e, "VAAPI unavailable — trying Vulkan Video");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if matches!(choice.as_str(), "auto" | "" | "vulkan" | "hardware") {
|
if matches!(choice.as_str(), "auto" | "" | "vulkan" | "hardware") {
|
||||||
// `video_decode` gates the Vulkan Video attempt: the presenter now exports its
|
// `video_decode` gates the Vulkan Video attempt: the presenter now exports its
|
||||||
// handle bundle even when the device has no decode queue (Windows D3D11 interop
|
// handle bundle even when the device has no decode queue (Windows D3D11 interop
|
||||||
@@ -423,7 +453,7 @@ impl Decoder {
|
|||||||
return Err(e.context("PUNKTFUNK_DECODER=vulkan but it failed"));
|
return Err(e.context("PUNKTFUNK_DECODER=vulkan but it failed"));
|
||||||
}
|
}
|
||||||
tracing::info!(reason = %format!("{e:#}"),
|
tracing::info!(reason = %format!("{e:#}"),
|
||||||
"Vulkan Video unavailable — trying VAAPI");
|
"Vulkan Video unavailable — falling back");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None if choice == "vulkan" => {
|
None if choice == "vulkan" => {
|
||||||
@@ -435,12 +465,13 @@ impl Decoder {
|
|||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deck note: `auto` reaches VAAPI when Vulkan Video isn't available. A presenter
|
// Deck/NVIDIA note: `auto` reaches VAAPI here when Vulkan Video isn't available
|
||||||
// that can't display the dmabufs demotes this decoder to software mid-session
|
// (on desktop Mesa it was already tried above — `vaapi_tried` skips the repeat).
|
||||||
// via [`Decoder::force_software`]. Windows has no VAAPI — auto falls straight
|
// A presenter that can't display the dmabufs demotes this decoder to software
|
||||||
// through to software there.
|
// mid-session via [`Decoder::force_software`]. Windows has no VAAPI — auto falls
|
||||||
|
// straight through to software there.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if choice != "software" && choice != "vulkan" {
|
if choice != "software" && choice != "vulkan" && !vaapi_tried {
|
||||||
match VaapiDecoder::new(codec_id) {
|
match VaapiDecoder::new(codec_id) {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
tracing::info!(?codec_id, "VAAPI hardware decode active (zero-copy dmabuf)");
|
tracing::info!(?codec_id, "VAAPI hardware decode active (zero-copy dmabuf)");
|
||||||
@@ -558,6 +589,24 @@ impl Decoder {
|
|||||||
self.vaapi_fails += 1;
|
self.vaapi_fails += 1;
|
||||||
self.want_keyframe = true;
|
self.want_keyframe = true;
|
||||||
if self.vaapi_fails >= VAAPI_DEMOTE_AFTER {
|
if self.vaapi_fails >= VAAPI_DEMOTE_AFTER {
|
||||||
|
// A failing Vulkan backend still has a hardware rung below it on
|
||||||
|
// Linux — demote to VAAPI first (user-reported: FFmpeg-Vulkan-on-Mesa
|
||||||
|
// error-streaking where VAAPI streams perfectly); only when that
|
||||||
|
// can't be built either does the session land on software.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if matches!(self.backend, Backend::Vulkan(_)) {
|
||||||
|
match VaapiDecoder::new(self.codec_id) {
|
||||||
|
Ok(v) => {
|
||||||
|
tracing::warn!(error = %e, fails = self.vaapi_fails,
|
||||||
|
"Vulkan Video decode failing repeatedly — demoting to VAAPI");
|
||||||
|
self.backend = Backend::Vaapi(v);
|
||||||
|
self.vaapi_fails = 0;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(va) => tracing::info!(reason = %va,
|
||||||
|
"VAAPI unavailable for demotion — software decode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
tracing::warn!(error = %e, fails = self.vaapi_fails,
|
tracing::warn!(error = %e, fails = self.vaapi_fails,
|
||||||
"{which} decode failing repeatedly — demoting to software");
|
"{which} decode failing repeatedly — demoting to software");
|
||||||
self.backend = Backend::Software(SoftwareDecoder::new(self.codec_id)?);
|
self.backend = Backend::Software(SoftwareDecoder::new(self.codec_id)?);
|
||||||
@@ -1002,6 +1051,12 @@ pub struct VulkanDecodeDevice {
|
|||||||
pub instance: usize,
|
pub instance: usize,
|
||||||
pub physical_device: usize,
|
pub physical_device: usize,
|
||||||
pub device: usize,
|
pub device: usize,
|
||||||
|
/// PCI vendor of the presenter's physical device (0x10DE NVIDIA, 0x1002 AMD,
|
||||||
|
/// 0x8086 Intel) — drives [`Self::prefer_vulkan_over_vaapi`].
|
||||||
|
pub vendor_id: u32,
|
||||||
|
/// The driver's device-name string (e.g. "AMD RADV VANGOGH") — the VanGogh/Deck
|
||||||
|
/// detection for [`Self::prefer_vulkan_over_vaapi`].
|
||||||
|
pub device_name: String,
|
||||||
/// The presenter's graphics+present family (FFmpeg's "required" tx/comp family too).
|
/// The presenter's graphics+present family (FFmpeg's "required" tx/comp family too).
|
||||||
pub graphics_qf: u32,
|
pub graphics_qf: u32,
|
||||||
/// Raw `VkQueueFlags` of that family (the qf[] entry wants the real capabilities).
|
/// Raw `VkQueueFlags` of that family (the qf[] entry wants the real capabilities).
|
||||||
@@ -1035,6 +1090,25 @@ pub struct VulkanDecodeDevice {
|
|||||||
pub queue_lock: std::sync::Arc<QueueLock>,
|
pub queue_lock: std::sync::Arc<QueueLock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VulkanDecodeDevice {
|
||||||
|
/// Should `auto` try Vulkan Video BEFORE VAAPI on this device? Only where that's the
|
||||||
|
/// established right answer:
|
||||||
|
/// * **NVIDIA** — Vulkan is its only hardware path (no usable VAAPI; the
|
||||||
|
/// nvidia-vaapi-driver is broken for this, Moonlight blacklists it).
|
||||||
|
/// * **VanGogh (Steam Deck)** — VAAPI's separate-plane dmabuf import shows chroma
|
||||||
|
/// fringing there; the session binary opts RADV into `video_decode` precisely to
|
||||||
|
/// get the Vulkan path.
|
||||||
|
///
|
||||||
|
/// Every other Mesa device (desktop RADV, ANV) keeps the battle-tested zero-copy
|
||||||
|
/// VAAPI first: Mesa now exposes decode queues by default, and FFmpeg-Vulkan-on-Mesa
|
||||||
|
/// regressing (judder, error-streaks that used to demote to software) is a real,
|
||||||
|
/// user-reported failure — while VAAPI is the path every other Linux client uses.
|
||||||
|
pub fn prefer_vulkan_over_vaapi(&self) -> bool {
|
||||||
|
const VENDOR_NVIDIA: u32 = 0x10DE;
|
||||||
|
self.vendor_id == VENDOR_NVIDIA || self.device_name.to_ascii_uppercase().contains("VANGOGH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `fourcc(a,b,c,d)` — the DRM FourCC packing (little-endian, `a | b<<8 | c<<16 | d<<24`).
|
/// `fourcc(a,b,c,d)` — the DRM FourCC packing (little-endian, `a | b<<8 | c<<16 | d<<24`).
|
||||||
const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
|
const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
|
||||||
(a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24)
|
(a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24)
|
||||||
@@ -1505,6 +1579,50 @@ unsafe extern "C" fn pick_vulkan(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn decode_device(vendor_id: u32, device_name: &str) -> VulkanDecodeDevice {
|
||||||
|
VulkanDecodeDevice {
|
||||||
|
get_instance_proc_addr: 0,
|
||||||
|
instance: 0,
|
||||||
|
physical_device: 0,
|
||||||
|
device: 0,
|
||||||
|
vendor_id,
|
||||||
|
device_name: device_name.into(),
|
||||||
|
graphics_qf: 0,
|
||||||
|
graphics_queue_flags: 0,
|
||||||
|
decode_qf: 0,
|
||||||
|
decode_video_caps: 0,
|
||||||
|
instance_extensions: Vec::new(),
|
||||||
|
device_extensions: Vec::new(),
|
||||||
|
f_sampler_ycbcr: true,
|
||||||
|
f_timeline_semaphore: true,
|
||||||
|
f_synchronization2: true,
|
||||||
|
video_decode: true,
|
||||||
|
d3d11_import: false,
|
||||||
|
adapter_luid: None,
|
||||||
|
queue_lock: std::sync::Arc::new(QueueLock::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auto's Linux hardware order: Vulkan-first ONLY on NVIDIA (no usable VAAPI) and the
|
||||||
|
/// Deck's VanGogh (VAAPI chroma-fringes); desktop RADV/ANV keep VAAPI first — the
|
||||||
|
/// user-reported judder/software regression came from FFmpeg-Vulkan-on-Mesa winning auto.
|
||||||
|
#[test]
|
||||||
|
fn vulkan_over_vaapi_only_on_nvidia_and_vangogh() {
|
||||||
|
assert!(decode_device(0x10DE, "NVIDIA GeForce RTX 5070 Ti").prefer_vulkan_over_vaapi());
|
||||||
|
assert!(decode_device(0x1002, "AMD RADV VANGOGH").prefer_vulkan_over_vaapi());
|
||||||
|
assert!(
|
||||||
|
decode_device(0x1002, "AMD Custom GPU 0405 (RADV VANGOGH)").prefer_vulkan_over_vaapi()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!decode_device(0x1002, "AMD Radeon RX 7800 XT (RADV NAVI32)")
|
||||||
|
.prefer_vulkan_over_vaapi()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!decode_device(0x8086, "Intel(R) Arc(tm) A770 Graphics (DG2)")
|
||||||
|
.prefer_vulkan_over_vaapi()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn desc(matrix: u8, full_range: bool) -> ColorDesc {
|
fn desc(matrix: u8, full_range: bool) -> ColorDesc {
|
||||||
ColorDesc {
|
ColorDesc {
|
||||||
primaries: 1,
|
primaries: 1,
|
||||||
|
|||||||
@@ -661,6 +661,11 @@ impl Presenter {
|
|||||||
instance: instance.handle().as_raw() as usize,
|
instance: instance.handle().as_raw() as usize,
|
||||||
physical_device: pdev.as_raw() as usize,
|
physical_device: pdev.as_raw() as usize,
|
||||||
device: device.handle().as_raw() as usize,
|
device: device.handle().as_raw() as usize,
|
||||||
|
vendor_id: dev_props.vendor_id,
|
||||||
|
device_name: dev_props
|
||||||
|
.device_name_as_c_str()
|
||||||
|
.map(|c| c.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default(),
|
||||||
graphics_qf: qfi,
|
graphics_qf: qfi,
|
||||||
graphics_queue_flags: qf_props[qfi as usize].queue_flags.as_raw(),
|
graphics_queue_flags: qf_props[qfi as usize].queue_flags.as_raw(),
|
||||||
decode_qf,
|
decode_qf,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ A few knobs are read by the native **clients**, not the host:
|
|||||||
|
|
||||||
| Setting | Values | Meaning |
|
| Setting | Values | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PUNKTFUNK_DECODER` | `software` · `vaapi` (Linux) | Force the decode path. Default auto-selects hardware (VAAPI on Intel/AMD, D3D11VA on Windows) with a software fallback. |
|
| `PUNKTFUNK_DECODER` | `software` · `vaapi` · `vulkan` (Linux) · `d3d11va` (Windows) | Force the decode path. Default auto-selects hardware (VAAPI on Intel/AMD, Vulkan Video on NVIDIA and the Steam Deck, D3D11VA/Vulkan on Windows) with a software fallback. |
|
||||||
|
|
||||||
## Bitrate
|
## Bitrate
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user