feat(host): PyroWave capture advertises the Vulkan device's dmabuf modifiers
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 51s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 59s
ci / bench (push) Successful in 6m35s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m57s
docker / deploy-docs (push) Successful in 32s
android / android (push) Successful in 12m46s
arch / build-publish (push) Successful in 15m29s
deb / build-publish (push) Successful in 16m21s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 12m33s
windows-host / package (push) Failing after 12m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m22s
ci / rust (push) Successful in 28m2s
apple / swift (push) Successful in 4m45s
apple / screenshots (push) Successful in 19m36s

The pyrowave passthrough rode VAAPI's LINEAR-only modifier policy, which
starves it on Mutter+NVIDIA (tiled-only allocations → the compositor
declines the offer → CPU capture fallback). The encoder imports through
VK_EXT_image_drm_format_modifier, not libva, so the capture now extends
the advertisement with every single-memory-plane modifier the PyroWave
device samples from (probed via DrmFormatModifierPropertiesListEXT with
the same device selection as the encoder).

Live on .21 (Mutter+NVIDIA, RTX 5070 Ti): 7 modifiers advertised, the
compositor negotiated block-linear (216172782120099861), no CPU
downgrade, and the encoder's per-buffer import cache populated exactly
as designed (8 PipeWire pool buffers imported once, silent reuse after).
Zero-copy session numbers: static 60 fps, e2e 2.9-3.0 ms p50 (p95 3.4),
host stage 1.6 ms; full-window motion 60 fps at ~80 Mb/s all-intra,
decode ~1 ms. Also neutralizes the VAAPI-specific wording in the
passthrough hand-off log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:30:59 +02:00
parent 719b1ef403
commit 8dc5d672e2
3 changed files with 92 additions and 1 deletions
+18 -1
View File
@@ -998,7 +998,7 @@ mod pipewire {
h,
modifier = ud.modifier,
fourcc = format_args!("{:#010x}", fourcc),
"zero-copy: handing dmabuf to VAAPI (GPU import + CSC)"
"zero-copy: handing the raw dmabuf to the encoder (GPU import + CSC)"
);
}
return;
@@ -1328,6 +1328,23 @@ mod pipewire {
if (importer.is_some() || vaapi_passthrough) && !modifiers.contains(&0) {
modifiers.push(0); // DRM_FORMAT_MOD_LINEAR
}
// PyroWave passthrough: the encoder imports through Vulkan, not libva — extend the
// advertisement with every modifier its device samples from, so compositors that
// never allocate LINEAR (Mutter+NVIDIA) still negotiate zero-copy dmabufs.
#[cfg(feature = "pyrowave")]
if vaapi_passthrough && crate::config::config().encoder_pref.as_str() == "pyrowave" {
for m in crate::encode::pyrowave_capture_modifiers(
crate::zerocopy::drm_fourcc(PixelFormat::Bgrx).unwrap(),
) {
if !modifiers.contains(&m) {
modifiers.push(m);
}
}
tracing::info!(
count = modifiers.len(),
"zero-copy: advertising the PyroWave device's Vulkan-importable dmabuf modifiers"
);
}
let want_dmabuf =
(importer.is_some() || vaapi_passthrough) && !modifiers.is_empty() && !force_shm;
if force_shm {
+8
View File
@@ -1023,6 +1023,14 @@ fn linux_auto_is_vaapi() -> bool {
!nvidia_present()
}
/// The dmabuf modifiers the PyroWave encoder's Vulkan device imports for the capture's
/// packed-RGB fourcc — advertised by the capture when the pyrowave passthrough is active
/// (the VAAPI LINEAR-only policy starves it on Mutter+NVIDIA, which allocates tiled only).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
pub(crate) fn pyrowave_capture_modifiers(fourcc: u32) -> Vec<u64> {
pyrowave::capture_modifiers(fourcc)
}
/// True if the Linux GPU encode backend resolves to VAAPI (AMD/Intel) rather than NVENC — mirrors
/// [`open_video`]'s dispatch so the capturer can choose the matching zero-copy path (raw dmabuf
/// passthrough for VAAPI vs the EGL→CUDA import for NVENC).
@@ -44,6 +44,72 @@ const IMPORT_CACHE_CAP: usize = 16;
/// the rate controller itself never exceeds the budget).
const BS_SLACK: usize = 256 * 1024;
/// The DRM modifiers the PyroWave device can import as a SAMPLED image of the capture's
/// packed-RGB format. The capture advertises these for the pyrowave passthrough instead of
/// VAAPI's LINEAR-only policy — Mutter+NVIDIA never allocates LINEAR, but its tiled
/// dmabufs import fine through `VK_EXT_image_drm_format_modifier` (validated by upstream's
/// interop test). Instance + physical device only; probed per session setup (cheap).
pub(crate) fn capture_modifiers(fourcc: u32) -> Vec<u64> {
let Some(fmt) = super::vk_util::fourcc_to_vk(fourcc) else {
return Vec::new();
};
// SAFETY: fresh instance, plain physical-device property queries, destroyed before
// returning; nothing borrows across the call.
unsafe {
let Ok(entry) = ash::Entry::load() else {
return Vec::new();
};
let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_3);
let Ok(instance) = entry.create_instance(
&vk::InstanceCreateInfo::default().application_info(&app),
None,
) else {
return Vec::new();
};
// Same device selection as `open_inner`: the first real GPU with graphics+compute.
let pd = instance
.enumerate_physical_devices()
.unwrap_or_default()
.into_iter()
.find(|&pd| {
instance.get_physical_device_properties(pd).device_type
!= vk::PhysicalDeviceType::CPU
&& instance
.get_physical_device_queue_family_properties(pd)
.iter()
.any(|q| {
q.queue_flags
.contains(vk::QueueFlags::GRAPHICS | vk::QueueFlags::COMPUTE)
})
});
let mods = pd
.map(|pd| {
let mut list = vk::DrmFormatModifierPropertiesListEXT::default();
let mut fp2 = vk::FormatProperties2::default().push_next(&mut list);
instance.get_physical_device_format_properties2(pd, fmt, &mut fp2);
let n = list.drm_format_modifier_count as usize;
let mut props = vec![vk::DrmFormatModifierPropertiesEXT::default(); n];
list.p_drm_format_modifier_properties = props.as_mut_ptr();
let mut fp2 = vk::FormatProperties2::default().push_next(&mut list);
instance.get_physical_device_format_properties2(pd, fmt, &mut fp2);
props.truncate(list.drm_format_modifier_count as usize);
props
.into_iter()
.filter(|p| {
p.drm_format_modifier_tiling_features
.contains(vk::FormatFeatureFlags::SAMPLED_IMAGE)
// Single-memory-plane only: the capture hands one fd/offset/stride.
&& p.drm_format_modifier_plane_count == 1
})
.map(|p| p.drm_format_modifier)
.collect()
})
.unwrap_or_default();
instance.destroy_instance(None);
mods
}
}
fn pw_check(r: pw::pyrowave_result, what: &str) -> Result<()> {
if r == pw::pyrowave_result_PYROWAVE_SUCCESS {
Ok(())