Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff62b087c |
@@ -537,6 +537,80 @@ mod session_main {
|
||||
};
|
||||
}
|
||||
|
||||
// `--probe-decode`: per-adapter Vulkan Video decode capability, then exit. Human
|
||||
// output on purpose — this is a triage tool, not a picker source, which is also
|
||||
// why it is a separate flag: `--list-adapters` is parsed line-by-line by the
|
||||
// desktop shells' GPU picker and must keep printing bare names.
|
||||
if arg_flag("--probe-decode") {
|
||||
return match pf_presenter::vk::probe_decode() {
|
||||
Ok(adapters) => {
|
||||
if adapters.is_empty() {
|
||||
println!("no Vulkan physical devices");
|
||||
}
|
||||
for (i, a) in adapters.iter().enumerate() {
|
||||
// The index IS the PUNKTFUNK_VK_DEVICE value, and entry 0 is what
|
||||
// a default run presents on — the decoder shares that device, so
|
||||
// on a hybrid box this line is usually the answer.
|
||||
let kind = if a.discrete { "discrete" } else { "integrated" };
|
||||
println!(
|
||||
"[{i}] {} ({kind}){}",
|
||||
a.name,
|
||||
if i == 0 { " <- default presenter" } else { "" }
|
||||
);
|
||||
println!(
|
||||
" vulkan video decode: {}",
|
||||
if a.usable { "YES" } else { "no" }
|
||||
);
|
||||
let codecs: Vec<&str> = [(0x1u32, "H.264"), (0x2, "H.265"), (0x4, "AV1")]
|
||||
.iter()
|
||||
.filter(|(bit, _)| a.codec_ops & bit != 0)
|
||||
.map(|(_, n)| *n)
|
||||
.collect();
|
||||
println!(
|
||||
" driver decode ops: {}",
|
||||
if codecs.is_empty() {
|
||||
format!("none (0x{:X})", a.codec_ops)
|
||||
} else {
|
||||
format!("{} (0x{:X})", codecs.join(", "), a.codec_ops)
|
||||
}
|
||||
);
|
||||
if !a.usable {
|
||||
// Say which conjunct failed. "no" with no reason is the thing
|
||||
// this whole flag exists to stop.
|
||||
let mut why: Vec<String> = Vec::new();
|
||||
if !a.api_1_3 {
|
||||
why.push("device is not Vulkan 1.3".into());
|
||||
}
|
||||
if !a.features_ok {
|
||||
why.push(
|
||||
"missing samplerYcbcrConversion / timelineSemaphore / \
|
||||
synchronization2"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if a.decode_family.is_none() {
|
||||
why.push("no queue family advertises VIDEO_DECODE".into());
|
||||
}
|
||||
if !a.base_missing.is_empty() {
|
||||
why.push(format!("missing {}", a.base_missing.join(", ")));
|
||||
}
|
||||
if a.codec_exts.is_empty() {
|
||||
why.push("no VK_KHR_video_decode_{h264,h265,av1} extension".into());
|
||||
}
|
||||
println!(" why not: {}", why.join("; "));
|
||||
} else {
|
||||
println!(" extensions: {}", a.codec_exts.join(", "));
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("probe-decode: {e:#}");
|
||||
EXIT_PRESENTER_FAILED
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// `--list-audio`: the PipeWire endpoints the settings pickers offer, as
|
||||
// `sink|source<TAB>node.name<TAB>description` lines — a debug window into the
|
||||
// same enumeration the GTK shell probes.
|
||||
|
||||
@@ -1766,9 +1766,20 @@ impl Decoder {
|
||||
"native Vulkan decode init failed — demoting to the standard ladder"),
|
||||
}
|
||||
} else {
|
||||
// The gate is an AND of three, so name all three. `video_decode=true`
|
||||
// beside "refused" is otherwise unreadable: it says the device decodes
|
||||
// SOMETHING while refusing THIS codec, and the bit that would explain it
|
||||
// — the decode family's advertised operations — went unprinted. That is
|
||||
// the difference between "your GPU can't do this" and "we asked for the
|
||||
// wrong thing", and only the second is our bug.
|
||||
tracing::warn!(
|
||||
codec = codec_name,
|
||||
video_decode = vk.is_some_and(|v| v.video_decode),
|
||||
decode_video_caps =
|
||||
format_args!("0x{:X}", vk.map_or(0, |v| v.decode_video_caps)),
|
||||
codec_op_needed =
|
||||
format_args!("0x{:X}", native_codec(wire).map_or(0, |(_, op)| op)),
|
||||
device = vk.map_or("", |v| v.device_name.as_str()),
|
||||
"PUNKTFUNK_DECODER=native-vulkan refused (needs an H.264, HEVC or AV1 \
|
||||
session and a presenter device whose decode family advertises that \
|
||||
codec) — standard ladder"
|
||||
|
||||
@@ -41,7 +41,7 @@ mod reconfig;
|
||||
mod resources;
|
||||
mod setup;
|
||||
|
||||
pub use setup::{list_adapters, PresentPref};
|
||||
pub use setup::{list_adapters, probe_decode, AdapterDecode, PresentPref};
|
||||
|
||||
/// One presenter iteration's video input.
|
||||
pub enum FrameInput<'a> {
|
||||
|
||||
@@ -13,6 +13,68 @@ use ash::vk;
|
||||
use ash::vk::Handle as _;
|
||||
use std::ffi::{c_char, CString};
|
||||
|
||||
/// The two extensions Vulkan Video decode cannot work without, whatever the codec.
|
||||
///
|
||||
/// Module scope, not function scope, because [`probe_decode`] answers "can this GPU do
|
||||
/// Vulkan Video decode?" and MUST answer it with the same list the device creation path
|
||||
/// gates on. A probe that keeps its own copy is a probe that eventually lies — and a
|
||||
/// lying capability report is worse than none, because it sends a field reporter looking
|
||||
/// in the wrong half of the stack.
|
||||
pub(crate) const VIDEO_BASE: [&std::ffi::CStr; 2] = [
|
||||
ash::khr::video_queue::NAME,
|
||||
ash::khr::video_decode_queue::NAME,
|
||||
];
|
||||
|
||||
/// The per-codec decode extensions, same sharing rule as [`VIDEO_BASE`]. AV1's name is
|
||||
/// spelled out because ash 0.38's headers (1.3.281) predate its promotion.
|
||||
pub(crate) const VIDEO_CODECS: [&std::ffi::CStr; 3] = [
|
||||
ash::khr::video_decode_h264::NAME,
|
||||
ash::khr::video_decode_h265::NAME,
|
||||
c"VK_KHR_video_decode_av1",
|
||||
];
|
||||
|
||||
/// The Vulkan Video decode gate: all five must hold. One function so the device creation
|
||||
/// path and [`probe_decode`] cannot drift into disagreeing about what "supported" means —
|
||||
/// the failure that produces is a probe reporting a capability the session then refuses,
|
||||
/// which reads to everyone as a bug in the decoder rather than in the probe.
|
||||
pub(crate) fn video_decode_gate(
|
||||
api_1_3: bool,
|
||||
features_ok: bool,
|
||||
has_decode_family: bool,
|
||||
base_exts_present: bool,
|
||||
any_codec_ext: bool,
|
||||
) -> bool {
|
||||
api_1_3 && features_ok && has_decode_family && base_exts_present && any_codec_ext
|
||||
}
|
||||
|
||||
/// What one physical device can do for Vulkan Video decode, without creating a logical
|
||||
/// device or a surface — the answer `punktfunk-session --probe-decode` prints.
|
||||
///
|
||||
/// Exists because "I pinned the Vulkan rung and got D3D11VA instead" was, until this,
|
||||
/// only answerable by starting a session and reading a log. Every field question about
|
||||
/// hardware decode starts here: WHICH adapter, and does it advertise the codec.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdapterDecode {
|
||||
/// Marketing name — also the `PUNKTFUNK_VK_ADAPTER` match key.
|
||||
pub name: String,
|
||||
/// Discrete GPUs sort first, exactly as `pick_device` ranks them, so index 0 here is
|
||||
/// the device a default run will pick.
|
||||
pub discrete: bool,
|
||||
pub api_1_3: bool,
|
||||
pub features_ok: bool,
|
||||
/// The queue family index that advertises `VIDEO_DECODE_KHR`, if any.
|
||||
pub decode_family: Option<u32>,
|
||||
/// Raw `VkVideoCodecOperationFlagsKHR` from that family — what the DRIVER says it can
|
||||
/// decode, independent of which extensions are exposed.
|
||||
pub codec_ops: u32,
|
||||
/// Required base extensions this device does NOT expose.
|
||||
pub base_missing: Vec<String>,
|
||||
/// Per-codec decode extensions it does.
|
||||
pub codec_exts: Vec<String>,
|
||||
/// [`video_decode_gate`] over the fields above.
|
||||
pub usable: bool,
|
||||
}
|
||||
|
||||
/// `VK_EXT_present_mode_fifo_latest_ready`, hand-declared: it postdates the Vulkan headers
|
||||
/// ash 0.38 is generated from (1.3.281), so there is no binding for it — which is also why
|
||||
/// an unenabled driver reports the mode back as the bare number `1000361000`.
|
||||
@@ -283,22 +345,15 @@ impl Presenter {
|
||||
.map(|(i, (_, v))| (i as u32, v.video_codec_operations))
|
||||
};
|
||||
|
||||
const VIDEO_BASE: [&std::ffi::CStr; 2] = [
|
||||
ash::khr::video_queue::NAME,
|
||||
ash::khr::video_decode_queue::NAME,
|
||||
];
|
||||
const VIDEO_CODECS: [&std::ffi::CStr; 3] = [
|
||||
ash::khr::video_decode_h264::NAME,
|
||||
ash::khr::video_decode_h265::NAME,
|
||||
c"VK_KHR_video_decode_av1",
|
||||
];
|
||||
let codec_exts: Vec<&std::ffi::CStr> =
|
||||
VIDEO_CODECS.into_iter().filter(|n| has(n)).collect();
|
||||
let video_ok = dev_is_13
|
||||
&& features_ok
|
||||
&& decode_family.is_some()
|
||||
&& VIDEO_BASE.iter().all(|n| has(n))
|
||||
&& !codec_exts.is_empty();
|
||||
let video_ok = video_decode_gate(
|
||||
dev_is_13,
|
||||
features_ok,
|
||||
decode_family.is_some(),
|
||||
VIDEO_BASE.iter().all(|n| has(n)),
|
||||
!codec_exts.is_empty(),
|
||||
);
|
||||
|
||||
let (decode_qf, decode_caps) = decode_family.unwrap_or((qfi, Default::default()));
|
||||
let mut video_ext_names: Vec<&std::ffi::CStr> = Vec::new();
|
||||
@@ -320,11 +375,42 @@ impl Presenter {
|
||||
"Vulkan Video decode available on this device"
|
||||
);
|
||||
} else {
|
||||
// ALL FIVE conjuncts, and the evidence behind each. The three this used to
|
||||
// print could every one be true while the answer was still no — a device with
|
||||
// Vulkan 1.3, the features and a decode queue family, but missing a codec
|
||||
// extension, logged `dev_is_13=true features_ok=true decode_family=true` next
|
||||
// to the word "unavailable" and named nothing that could be acted on. A field
|
||||
// reporter cannot then tell "this build never tried" from "the driver said
|
||||
// no", which is the single most expensive ambiguity a fallback can have: it
|
||||
// makes a missing capability and a bug in our gate look identical.
|
||||
//
|
||||
// `queue_codec_ops` matters most on a device that HAS a decode queue: the
|
||||
// driver names the codecs it can decode there, so an empty `codec_exts`
|
||||
// beside a non-empty ops mask means the extensions are what is missing, not
|
||||
// the hardware.
|
||||
let base_missing: Vec<&str> = VIDEO_BASE
|
||||
.iter()
|
||||
.filter(|n| !has(n))
|
||||
.map(|n| n.to_str().unwrap_or("?"))
|
||||
.collect();
|
||||
let codec_ext_names: Vec<&str> = codec_exts
|
||||
.iter()
|
||||
.map(|n| n.to_str().unwrap_or("?"))
|
||||
.collect();
|
||||
tracing::info!(
|
||||
dev_is_13,
|
||||
features_ok,
|
||||
decode_family = decode_family.is_some(),
|
||||
"Vulkan Video decode unavailable — decoder falls back (VAAPI/software)"
|
||||
video_base_missing = ?base_missing,
|
||||
codec_exts_present = ?codec_ext_names,
|
||||
queue_codec_ops = ?decode_family.map(|(_, ops)| ops),
|
||||
device = %dev_props
|
||||
.device_name_as_c_str()
|
||||
.map(|c| c.to_string_lossy().into_owned())
|
||||
.unwrap_or_default(),
|
||||
vendor_id = format_args!("0x{:04X}", dev_props.vendor_id),
|
||||
"Vulkan Video decode unavailable on this device — the decoder falls back \
|
||||
one rung (D3D11VA on Windows, VAAPI on Linux, then software)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -621,6 +707,152 @@ impl Presenter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every physical device's Vulkan Video decode capability
|
||||
/// (`punktfunk-session --probe-decode`). No surface, no logical device, no session.
|
||||
///
|
||||
/// The question this answers is the one every hardware-decode field report starts with:
|
||||
/// the rung was pinned and something else ran — did this build never try, or did the
|
||||
/// driver refuse? Ordered like [`list_adapters`] (discrete first, `pick_device`'s
|
||||
/// tie-break), so the FIRST entry is the device a default run presents on — which is also
|
||||
/// the device Vulkan Video decodes on, because the decoder shares the presenter's device
|
||||
/// by design. On a hybrid laptop those two facts together are usually the whole answer:
|
||||
/// pinning the decoder does not move the presenter, so probing the iGPU's capability
|
||||
/// while the dGPU is presenting reports on the wrong GPU. `PUNKTFUNK_VK_DEVICE=<index>`
|
||||
/// is the knob that moves it.
|
||||
pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
|
||||
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
|
||||
// builder structs that are locals outliving the call; the handle it returns is owned by the
|
||||
// value being built here.
|
||||
let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?;
|
||||
let app_name = CString::new("punktfunk-session").unwrap();
|
||||
let app_info = vk::ApplicationInfo::default()
|
||||
.application_name(&app_name)
|
||||
.api_version(vk::API_VERSION_1_3);
|
||||
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
|
||||
// builder structs that are locals outliving the call; the handle it returns is owned by the
|
||||
// value being built here.
|
||||
let instance = unsafe {
|
||||
entry.create_instance(
|
||||
&vk::InstanceCreateInfo::default().application_info(&app_info),
|
||||
None,
|
||||
)
|
||||
}
|
||||
.context("vkCreateInstance")?;
|
||||
|
||||
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
|
||||
// filling locals returned by value.
|
||||
let devices = unsafe { instance.enumerate_physical_devices() }?;
|
||||
let mut out: Vec<(u8, AdapterDecode)> = Vec::with_capacity(devices.len());
|
||||
for pdev in devices {
|
||||
// SAFETY: per the Vulkan contract above - a read-only query on the live
|
||||
// instance/device, filling locals returned by value.
|
||||
let props = unsafe { instance.get_physical_device_properties(pdev) };
|
||||
let name = props
|
||||
.device_name_as_c_str()
|
||||
.map(|c| c.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rank = match props.device_type {
|
||||
vk::PhysicalDeviceType::DISCRETE_GPU => 0u8,
|
||||
vk::PhysicalDeviceType::INTEGRATED_GPU => 1,
|
||||
_ => 2,
|
||||
};
|
||||
let api_1_3 = vk::api_version_major(props.api_version) > 1
|
||||
|| vk::api_version_minor(props.api_version) >= 3;
|
||||
|
||||
// The same three features the creation path demands (`features_ok` there).
|
||||
let mut f11 = vk::PhysicalDeviceVulkan11Features::default();
|
||||
let mut f12 = vk::PhysicalDeviceVulkan12Features::default();
|
||||
let mut f13 = vk::PhysicalDeviceVulkan13Features::default();
|
||||
let mut feats = vk::PhysicalDeviceFeatures2::default()
|
||||
.push_next(&mut f11)
|
||||
.push_next(&mut f12)
|
||||
.push_next(&mut f13);
|
||||
// SAFETY: per the Vulkan contract above - a read-only query on the live
|
||||
// instance/device, filling locals returned by value.
|
||||
unsafe { instance.get_physical_device_features2(pdev, &mut feats) };
|
||||
let features_ok = f11.sampler_ycbcr_conversion == vk::TRUE
|
||||
&& f12.timeline_semaphore == vk::TRUE
|
||||
&& f13.synchronization2 == vk::TRUE;
|
||||
|
||||
// SAFETY: per the Vulkan contract above - a read-only query on the live
|
||||
// instance/device, filling locals returned by value.
|
||||
let ext_props =
|
||||
unsafe { instance.enumerate_device_extension_properties(pdev) }.unwrap_or_default();
|
||||
let has = |n: &std::ffi::CStr| {
|
||||
ext_props
|
||||
.iter()
|
||||
.any(|e| e.extension_name_as_c_str() == Ok(n))
|
||||
};
|
||||
let base_missing: Vec<String> = VIDEO_BASE
|
||||
.iter()
|
||||
.filter(|n| !has(n))
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
let codec_exts: Vec<String> = VIDEO_CODECS
|
||||
.iter()
|
||||
.filter(|n| has(n))
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
|
||||
// The decode queue family and the codec operations the DRIVER claims for it —
|
||||
// reported even when the extensions are absent, because "the hardware can, the
|
||||
// driver does not expose it" is a different conversation from "this GPU cannot".
|
||||
// SAFETY: per the Vulkan contract above - a read-only query on the live
|
||||
// instance/device, filling locals returned by value.
|
||||
let n = unsafe { instance.get_physical_device_queue_family_properties2_len(pdev) };
|
||||
let mut video: Vec<vk::QueueFamilyVideoPropertiesKHR> =
|
||||
vec![vk::QueueFamilyVideoPropertiesKHR::default(); n];
|
||||
let mut qprops: Vec<vk::QueueFamilyProperties2> = video
|
||||
.iter_mut()
|
||||
.map(|v| vk::QueueFamilyProperties2::default().push_next(v))
|
||||
.collect();
|
||||
// SAFETY: per the Vulkan contract above - a read-only query on the live
|
||||
// instance/device, filling locals returned by value.
|
||||
unsafe { instance.get_physical_device_queue_family_properties2(pdev, &mut qprops) };
|
||||
let flags: Vec<vk::QueueFlags> = qprops
|
||||
.iter()
|
||||
.map(|p| p.queue_family_properties.queue_flags)
|
||||
.collect();
|
||||
drop(qprops);
|
||||
let found = flags
|
||||
.iter()
|
||||
.zip(&video)
|
||||
.enumerate()
|
||||
.find(|(_, (f, _))| f.contains(vk::QueueFlags::VIDEO_DECODE_KHR))
|
||||
.map(|(i, (_, v))| (i as u32, v.video_codec_operations));
|
||||
|
||||
let usable = video_decode_gate(
|
||||
api_1_3,
|
||||
features_ok,
|
||||
found.is_some(),
|
||||
base_missing.is_empty(),
|
||||
!codec_exts.is_empty(),
|
||||
);
|
||||
out.push((
|
||||
rank,
|
||||
AdapterDecode {
|
||||
name,
|
||||
discrete: rank == 0,
|
||||
api_1_3,
|
||||
features_ok,
|
||||
decode_family: found.map(|(i, _)| i),
|
||||
codec_ops: found.map_or(0, |(_, ops)| ops.as_raw()),
|
||||
base_missing,
|
||||
codec_exts,
|
||||
usable,
|
||||
},
|
||||
));
|
||||
}
|
||||
out.sort_by_key(|(rank, _)| *rank);
|
||||
// SAFETY: per the Vulkan contract above - this destroys objects this type owns, and no
|
||||
// logical device was created against this instance, so nothing is in flight on it.
|
||||
unsafe { instance.destroy_instance(None) };
|
||||
Ok(out.into_iter().map(|(_, a)| a).collect())
|
||||
}
|
||||
|
||||
/// The physical devices' marketing names — the shells' GPU-picker source
|
||||
/// (`punktfunk-session --list-adapters`). No surface and no logical device; discrete
|
||||
/// GPUs first (mirroring `pick_device`'s tie-break), duplicates collapsed (the name is
|
||||
|
||||
Reference in New Issue
Block a user