diag(vkdecode): --probe-decode reports what the driver says about video images
ci / bun-nix (pull_request) Successful in 38s
apple / swift (pull_request) Successful in 1m31s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m11s
ci / docs-site (pull_request) Successful in 2m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m44s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 6m29s
android / android (pull_request) Successful in 7m44s
ci / rust (pull_request) Canceled after 9m40s
nix / flake (pull_request) Canceled after 9m38s

The Intel Arc refusal moved one step down the caps query and stopped again: the
coincide NV12 entry does not advertise SAMPLED. That sentence is punktfunk's, not
the driver's, and the last two times a conclusion was drawn from a sentence of ours
the conclusion was wrong.

So --probe-decode now prints the driver's own answers instead. For every profile the
client can negotiate (H.264 High, H.265 Main and Main 10, AV1 Main 8- and 10-bit) it
asks vkGetPhysicalDeviceVideoFormatPropertiesKHR in six usage combinations — the
three the image pools really create with, plus DPB|DST without sampling, SAMPLED
alone and DST alone, which are what localise a refusal to a half. Each answer is
printed as the driver gave it: format, usage and create flags named AND in hex with
unrecognised bits called out, image type, tiling. A failed query prints its VkResult
rather than vanishing into an empty list.

It goes through pf-vkdecode's own query rather than a copy of it, which meant
splitting query_formats into a physical-device form — the call never needed the
VkDevice the old signature demanded. VideoFormat gains imageType and imageTiling to
carry the whole record; VUID-VkImageCreateInfo-pNext-06811 compares both for
equality, so they were being assumed rather than read.

And because a driver that under-reports usage would be indistinguishable from one
that genuinely lacks it, the probe asks a second, independent question —
vkGetPhysicalDeviceImageFormatProperties2 over the same profile list — and prints it
only where the two disagree. A disagreement is the finding.

No behaviour change to any decode path: derivation reads the same fields it did.
This commit is contained in:
2026-08-07 15:34:12 +02:00
parent ca667cb79a
commit c06ee55b61
11 changed files with 585 additions and 12 deletions
Generated
+1
View File
@@ -3196,6 +3196,7 @@ dependencies = [
"ash",
"async-channel",
"pf-client-core",
"pf-vkdecode",
"punktfunk-core",
"sdl3",
"tracing",
+56
View File
@@ -510,6 +510,61 @@ mod session_main {
);
}
/// The driver's own answers about video images, printed with nothing in front of
/// them (`--probe-decode`).
///
/// Passing the five conjuncts above only says Vulkan Video EXISTS on a device; this
/// says whether the zero-copy pipeline can be BUILT on it — a different question
/// with, on at least one shipping driver, a different answer. Verbatim on purpose:
/// the Intel Arc refusal was twice diagnosed from punktfunk's own error text and
/// twice the diagnosis was wrong, and what broke it open both times was reading what
/// the driver actually said.
fn print_video_formats(a: &pf_presenter::vk::AdapterDecode) {
use pf_presenter::vk::probe::{describe_create_flags, describe_usage};
for p in &a.formats {
println!(" {} (wants {:?}):", p.profile, p.wanted);
for u in &p.usages {
let answer = match &u.formats {
Err(e) => format!("query failed: {e:?}"),
Ok(entries) if entries.is_empty() => "no formats offered".to_string(),
Ok(entries) => entries
.iter()
.map(|f| {
format!(
"{:?} usage={} create={} {:?} {:?}",
f.format,
describe_usage(f.image_usage),
describe_create_flags(f.image_create_flags),
f.image_type,
f.image_tiling,
)
})
.collect::<Vec<_>>()
.join("; "),
};
println!(" {:<24} {answer}", u.label);
// The independent second opinion, printed only where it DISAGREES with
// the format query. Agreement is the normal case and would be noise; a
// disagreement means one of the driver's two paths is wrong, which is
// the whole reason a second question gets asked at all.
let listed = u
.wanted_entry(p.wanted)
.is_some_and(|f| f.image_usage.contains(u.usage));
if listed != u.image_format_support.is_ok() {
let second = match &u.image_format_support {
Ok(()) => "says creatable".to_string(),
Err(e) => format!("says {e:?}"),
};
println!(
" {:<24} ^ DISAGREES: \
vkGetPhysicalDeviceImageFormatProperties2 {second}",
""
);
}
}
}
}
pub fn run() -> u8 {
// Logs to STDERR — stdout is the machine interface (ready/stats/error lines).
tracing_subscriber::fmt()
@@ -628,6 +683,7 @@ mod session_main {
} else {
println!(" extensions: {}", a.codec_exts.join(", "));
}
print_video_formats(a);
}
if adapters.len() > 1 {
// The single most common misreading of this output: seeing a
+6
View File
@@ -17,6 +17,12 @@ repository.workspace = true
# C++ in — fatal on Windows ARM64, where Granite has no SIMD path.
pf-client-core = { path = "../pf-client-core", default-features = false }
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
# `--probe-decode` reports the driver's own video-format answers through pf-vkdecode's
# query rather than a second copy of it — a probe that keeps its own copy is a probe
# that eventually disagrees with the code it exists to explain (VIDEO_BASE's doc says
# the same thing about the extension list). Already in the tree via pf-client-core;
# named here because setup.rs calls it directly.
pf-vkdecode = { path = "../pf-vkdecode" }
# `loaded` dlopens libvulkan at runtime (no link-time dependency — GPU-less boxes still
# start and fail into a clean error; on Windows vulkan-1.dll is a GPU-driver component).
+5
View File
@@ -43,6 +43,11 @@ mod setup;
pub use setup::{list_adapters, probe_decode, AdapterDecode, PresentPref};
/// The video-format probe behind [`AdapterDecode::formats`], re-exported so a caller
/// that prints the report does not need its own `pf-vkdecode` dependency (and cannot
/// end up printing a DIFFERENT crate version's idea of the flag names).
pub use pf_vkdecode::probe;
/// One presenter iteration's video input.
pub enum FrameInput<'a> {
/// No new frame — re-composite the retained video image (expose/resize).
+21
View File
@@ -85,6 +85,16 @@ pub struct AdapterDecode {
pub codec_exts: Vec<String>,
/// [`video_decode_gate`] over the fields above.
pub usable: bool,
/// What the driver answers about video image formats, verbatim — one row per
/// (profile, usage) question ([`pf_vkdecode::probe`]).
///
/// This is the half of the report that says why a device which passes every gate
/// above still cannot host the decoder. The five conjuncts answer "is Vulkan Video
/// here at all"; this answers "can the pipeline actually use it", which on at least
/// one shipping driver (Intel Arc, Windows) is a different question with a different
/// answer. Empty when the gate already failed — there is nothing to ask a device
/// with no video queue.
pub formats: Vec<pf_vkdecode::probe::ProfileProbe>,
}
/// `VK_EXT_present_mode_fifo_latest_ready`, hand-declared: it postdates the Vulkan headers
@@ -845,6 +855,16 @@ pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
base_missing.is_empty(),
!codec_exts.is_empty(),
);
// Only where the gate passed: the format queries need `VK_KHR_video_queue`'s
// entry points, and asking a device that does not expose them produces a null
// dispatch, not an answer.
let formats = if usable {
// SAFETY: `instance` is the live instance created above and `pdev` one of
// the physical devices it enumerated; the probe only reads.
unsafe { pf_vkdecode::probe::probe_video_formats(&entry, &instance, pdev) }
} else {
Vec::new()
};
out.push((
rank,
AdapterDecode {
@@ -858,6 +878,7 @@ pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
base_missing,
codec_exts,
usable,
formats,
},
));
}
+70 -12
View File
@@ -86,7 +86,7 @@ pub const COINCIDE_USAGE: vk::ImageUsageFlags = vk::ImageUsageFlags::from_raw(
/// plus the driver's advertised usage/create-flag envelope for it — creation must
/// stay INSIDE that envelope (finding of the adversarial round: the flags used to
/// be assumed, not honoured).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VideoFormat {
pub format: vk::Format,
/// `imageUsageFlags` the driver supports for this format under the queried
@@ -94,7 +94,38 @@ pub struct VideoFormat {
pub image_usage: vk::ImageUsageFlags,
/// `imageCreateFlags` the driver allows — per-plane views require
/// `MUTABLE_FORMAT` to appear here.
///
/// ⚠ This field is also the ONLY gate on `VK_IMAGE_CREATE_EXTENDED_USAGE_BIT`,
/// which is the spec's one escape hatch from `image_usage`: `supportedVideoFormat`
/// (VUID-VkImageCreateInfo-pNext-06811) admits a usage bit outside `image_usage`
/// only when `VkImageCreateInfo::flags` includes `EXTENDED_USAGE`, and admits that
/// flag only when it is "also set in `VkVideoFormatPropertiesKHR::imageCreateFlags`"
/// (or is `VIDEO_PROFILE_INDEPENDENT`, which needs `VK_KHR_video_maintenance1`).
/// So an EMPTY value here closes the escape hatch as well as the door — measured on
/// Intel Arc, where it is empty for every profile ([`crate::probe`] docs).
pub image_create_flags: vk::ImageCreateFlags,
/// `imageType` — the image type this format may be created with. Part of the
/// `supportedVideoFormat` match (VUID-06811 compares it for EQUALITY), so it is
/// recorded rather than assumed; every fleet driver reports `TYPE_2D`, which is
/// what [`crate::images`] creates.
pub image_type: vk::ImageType,
/// `imageTiling` — likewise compared for equality by VUID-06811; every fleet
/// driver reports `OPTIMAL`.
pub image_tiling: vk::ImageTiling,
}
impl Default for VideoFormat {
/// The shape the pools create with (`TYPE_2D` + `OPTIMAL`), so a fixture that
/// names only the interesting fields still describes a creatable image.
fn default() -> Self {
Self {
format: vk::Format::UNDEFINED,
image_usage: vk::ImageUsageFlags::empty(),
image_create_flags: vk::ImageCreateFlags::empty(),
image_type: vk::ImageType::TYPE_2D,
image_tiling: vk::ImageTiling::OPTIMAL,
}
}
}
/// Everything the thin query copies out of the driver, hand-buildable for tests.
@@ -677,6 +708,36 @@ pub(crate) unsafe fn query_formats(
dev: &DecodeDevice,
decode_profile: DecodeProfile,
usage: vk::ImageUsageFlags,
) -> Result<Vec<VideoFormat>, vk::Result> {
// SAFETY: the caller's DeviceHandles contract makes these two live, which is
// exactly what the physical-device form needs.
unsafe {
query_formats_on(
dev.video_queue_instance(),
dev.physical_device(),
decode_profile,
usage,
)
}
}
/// [`query_formats`] against a bare physical device — no `VkDevice` in sight.
///
/// Split out so [`crate::probe`] enumerates through the SAME code the session's caps
/// query runs, rather than a second copy that would drift (the probe's whole value is
/// that its answer is the one derivation will see). `vkGetPhysicalDeviceVideoFormat-
/// PropertiesKHR` is an instance-level command over a physical device, so nothing here
/// ever needed the logical device the old signature demanded.
///
/// # Safety
///
/// `video_queue_instance` must be loaded against a live `VkInstance`, and
/// `physical_device` must be one of that instance's physical devices.
pub(crate) unsafe fn query_formats_on(
video_queue_instance: &ash::khr::video_queue::Instance,
physical_device: vk::PhysicalDevice,
decode_profile: DecodeProfile,
usage: vk::ImageUsageFlags,
) -> Result<Vec<VideoFormat>, vk::Result> {
let mut chain = decode_profile.chain();
let profile = chain.wire();
@@ -686,21 +747,13 @@ pub(crate) unsafe fn query_formats(
.image_usage(usage)
.push_next(&mut profile_list);
let fp = dev
.video_queue_instance()
let fp = video_queue_instance
.fp()
.get_physical_device_video_format_properties_khr;
let mut count = 0u32;
// SAFETY: live physical device; `info` roots a wired chain outliving the call;
// null properties pointer is the spec's count-query form.
let r = unsafe {
fp(
dev.physical_device(),
&info,
&mut count,
std::ptr::null_mut(),
)
};
let r = unsafe { fp(physical_device, &info, &mut count, std::ptr::null_mut()) };
match r {
vk::Result::SUCCESS => {}
// "This usage/profile combination has no formats" — an arrangement gap,
@@ -711,7 +764,7 @@ pub(crate) unsafe fn query_formats(
}
let mut props = vec![vk::VideoFormatPropertiesKHR::default(); count as usize];
// SAFETY: as above, with a properties array of exactly the driver-reported count.
let r = unsafe { fp(dev.physical_device(), &info, &mut count, props.as_mut_ptr()) };
let r = unsafe { fp(physical_device, &info, &mut count, props.as_mut_ptr()) };
if r != vk::Result::SUCCESS && r != vk::Result::INCOMPLETE {
return Err(r);
}
@@ -722,6 +775,8 @@ pub(crate) unsafe fn query_formats(
format: p.format,
image_usage: p.image_usage_flags,
image_create_flags: p.image_create_flags,
image_type: p.image_type,
image_tiling: p.image_tiling,
})
.collect())
}
@@ -738,6 +793,7 @@ mod tests {
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT
| vk::ImageCreateFlags::ALIAS
| vk::ImageCreateFlags::EXTENDED_USAGE,
..Default::default()
}
}
@@ -783,6 +839,7 @@ mod tests {
format: NV12,
image_usage: DPB_USAGE,
image_create_flags: vk::ImageCreateFlags::empty(),
..Default::default()
}],
output_formats: vec![entry(NV12, OUTPUT_USAGE)],
coincide_formats: vec![],
@@ -927,6 +984,7 @@ mod tests {
format: NV12,
image_usage: COINCIDE_USAGE,
image_create_flags: vk::ImageCreateFlags::empty(),
..Default::default()
}];
assert_eq!(
derive_caps(&raw).unwrap_err(),
+3
View File
@@ -354,6 +354,7 @@ mod tests {
format,
image_usage: usage,
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT,
..Default::default()
}
}
@@ -638,6 +639,7 @@ mod tests {
format: P010,
image_usage: DPB_USAGE,
image_create_flags: vk::ImageCreateFlags::empty(),
..Default::default()
}],
output_formats: vec![entry(NV12, OUTPUT_USAGE)],
..coincide_device(vec![])
@@ -683,6 +685,7 @@ mod tests {
format: NV12,
image_usage: COINCIDE_USAGE,
image_create_flags: vk::ImageCreateFlags::empty(),
..Default::default()
}]);
assert_eq!(
derive_caps_av1(&raw, NV12).unwrap_err(),
+3
View File
@@ -383,6 +383,7 @@ mod tests {
format,
image_usage: usage,
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT,
..Default::default()
}
}
@@ -722,6 +723,7 @@ mod tests {
format: P010,
image_usage: DPB_USAGE,
image_create_flags: vk::ImageCreateFlags::empty(),
..Default::default()
}],
output_formats: vec![entry(NV12, OUTPUT_USAGE)],
..coincide_device(vec![])
@@ -767,6 +769,7 @@ mod tests {
format: P010,
image_usage: COINCIDE_USAGE,
image_create_flags: vk::ImageCreateFlags::empty(),
..Default::default()
}]);
assert_eq!(
derive_caps_h265(&raw, P010).unwrap_err(),
+1
View File
@@ -497,6 +497,7 @@ mod tests {
format: NV12,
image_usage: usage,
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT,
..Default::default()
};
let raw = RawH264Caps {
capability_flags: if layered {
+1
View File
@@ -133,6 +133,7 @@ pub mod params_h265;
pub mod pic;
pub mod pic_av1;
pub mod pic_h265;
pub mod probe;
pub mod recovery;
pub mod ring;
pub mod session;
+418
View File
@@ -0,0 +1,418 @@
//! What the driver ACTUALLY answers about video image formats, verbatim — the
//! physical-device-only probe behind `punktfunk-session --probe-decode`.
//!
//! # Why this exists
//!
//! Twice in a row an Intel Arc refusal was diagnosed from punktfunk's OWN error text
//! and twice the conclusion ("Intel driver bug") was wrong — the bug was ours, in the
//! `pNext` order of the capability query. What broke it open both times was logging
//! the driver's answer with no interpretation in front of it. This module makes that
//! the DEFAULT rather than a debugging afterthought: for every decode profile the
//! client can negotiate, it asks the driver the same question the session's caps query
//! asks, in every usage combination the image pools would create with, and records
//! what came back — including the failures, spelled as the `VkResult` the driver
//! returned.
//!
//! It shares [`crate::caps::query_formats_on`] with the real caps path on purpose. A
//! probe with its own copy of the query is a probe that eventually disagrees with the
//! code it is meant to explain, which is worse than no probe at all.
//!
//! # What it measured
//!
//! Intel Arc (Windows driver 101.8861, 2026-08): every one of the 26 decode profiles
//! reports the SAME envelope for its NV12/P010 picture format —
//! `TRANSFER_SRC | VIDEO_DECODE_DST | VIDEO_DECODE_DPB`, and `imageCreateFlags` EMPTY —
//! with `DPB_AND_OUTPUT_COINCIDE` as the only decode mode. No `SAMPLED`, so a shader
//! cannot read the decoded picture; and no `MUTABLE_FORMAT`/`EXTENDED_USAGE`, which
//! closes the spec's only escape hatch (see [`crate::caps::VideoFormat::
//! image_create_flags`]). `TRANSFER_SRC` is the sole way out of the image — i.e. a
//! copy. NVIDIA (596.41) answers the same queries with
//! `TRANSFER_SRC|TRANSFER_DST|SAMPLED|DECODE_DST|DECODE_DPB|ENCODE_SRC|ENCODE_DPB` and
//! `MUTABLE_FORMAT|EXTENDED_USAGE`, which is what makes the zero-copy path work there.
//!
//! The [`UsageProbe::image_format_support`] cross-check exists because of the ONE
//! inconsistency in the above: asked for a usage it does not support, the Intel driver
//! returns `VK_SUCCESS` with an entry that lacks the requested bit, where the spec says
//! the returned `imageUsageFlags` "will contain at least the same set of image usage
//! flags" (and `VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR` is the documented refusal). So
//! the probe asks a SECOND, independent question — `vkGetPhysicalDeviceImageFormat-
//! Properties2` with the same profile list chained — and prints both answers. Where the
//! two disagree, the disagreement itself is the finding.
use ash::vk;
use ash::vk::native as hh;
use crate::caps::query_formats_on;
use crate::caps::DecodeProfile;
use crate::caps::VideoFormat;
use crate::caps::COINCIDE_USAGE;
use crate::caps::DPB_USAGE;
use crate::caps::NV12;
use crate::caps::OUTPUT_USAGE;
use crate::caps::P010;
use crate::caps_av1::Av1ProfileKey;
use crate::caps_h265::H265ProfileKey;
/// The usage combinations the probe asks about, widest question first.
///
/// The first three are the REAL ones — exactly what [`crate::images`] creates with, so
/// their answers are the ones derivation acts on. The rest exist to localise a refusal:
/// when `DPB|DST|SAMPLED` fails, `DPB|DST` says whether the decode roles alone are fine
/// (i.e. the gap is sampling) and `SAMPLED` alone says whether the format is sampleable
/// under this profile at all. Without them, a single failed query leaves "which half is
/// missing?" to inference — which is how this device got misdiagnosed twice.
const USAGE_MATRIX: [(&str, vk::ImageUsageFlags); 6] = [
("coincide DPB|DST|SAMPLED", COINCIDE_USAGE),
("distinct DPB", DPB_USAGE),
("distinct DST|SAMPLED", OUTPUT_USAGE),
("DPB|DST (no SAMPLED)", DECODE_ONLY_USAGE),
("SAMPLED alone", vk::ImageUsageFlags::SAMPLED),
("DST alone", vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR),
];
/// Decode roles with sampling deliberately withheld — the discriminator between "this
/// device cannot decode this profile" and "it can decode it but not let anyone read it".
const DECODE_ONLY_USAGE: vk::ImageUsageFlags = vk::ImageUsageFlags::from_raw(
vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR.as_raw()
| vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR.as_raw(),
);
/// One decode profile's worth of driver answers.
#[derive(Debug, Clone)]
pub struct ProfileProbe {
/// The profile as a human would name it ("H.265 Main 4:2:0 8-bit").
pub profile: &'static str,
/// The picture format this profile decodes to — the entry the probe looks for.
pub wanted: vk::Format,
/// One row per [`USAGE_MATRIX`] entry, in that order.
pub usages: Vec<UsageProbe>,
}
/// The driver's answer for ONE (profile, usage) question.
#[derive(Debug, Clone)]
pub struct UsageProbe {
pub label: &'static str,
pub usage: vk::ImageUsageFlags,
/// `vkGetPhysicalDeviceVideoFormatPropertiesKHR`: every entry it returned, or the
/// `VkResult` it failed with. An EMPTY vector is itself an answer — the two
/// "no formats for this combination" results
/// (`ERROR_FORMAT_NOT_SUPPORTED`/`ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR`) are mapped
/// to it by the shared query, exactly as derivation sees them.
pub formats: Result<Vec<VideoFormat>, vk::Result>,
/// `vkGetPhysicalDeviceImageFormatProperties2` for the SAME profile list, format
/// and usage — the independent second opinion (module docs). `Ok(())` means the
/// driver says an image of that shape is creatable.
pub image_format_support: Result<(), vk::Result>,
}
impl UsageProbe {
/// The entry for the profile's picture format, if the driver returned one.
pub fn wanted_entry(&self, wanted: vk::Format) -> Option<VideoFormat> {
self.formats
.as_ref()
.ok()?
.iter()
.copied()
.find(|f| f.format == wanted)
}
}
/// The profiles worth probing: one per codec rung the client can negotiate, plus the
/// 10-bit legs, because an HDR stream picks a DIFFERENT Vulkan profile from an SDR one
/// and a device may well host one and not the other.
///
/// Deliberately NOT every profile the driver supports — this answers "can punktfunk
/// decode here", and a list padded with profiles no rung ever requests makes the row
/// that matters harder to find. `vulkaninfo --show-video-props` is the tool for the
/// exhaustive sweep.
fn probed_profiles() -> Vec<(&'static str, DecodeProfile, vk::Format)> {
// Every key is built through the SAME constructor the decoders negotiate with
// (`from_negotiated`), so a profile the client could never request cannot appear
// here — and a combination those constructors refuse simply drops out of the list
// instead of being hand-rolled into existence for the probe's benefit.
// 4:2:0 is chroma_format_idc 1 in both codecs' vocabulary; H.265 states depth as
// `bit_depth_luma_minus8`, AV1 in whole bits.
let mut out: Vec<(&'static str, DecodeProfile, vk::Format)> = vec![(
"H.264 High 4:2:0 8-bit",
DecodeProfile::H264(hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH),
NV12,
)];
if let Ok(key) = H265ProfileKey::from_negotiated(1, 0) {
out.push(("H.265 Main 4:2:0 8-bit", DecodeProfile::H265(key), NV12));
}
if let Ok(key) = H265ProfileKey::from_negotiated(1, 2) {
out.push(("H.265 Main 10 4:2:0 10-bit", DecodeProfile::H265(key), P010));
}
// AV1 without film grain: `filmGrainSupport` is part of the Vulkan PROFILE, and
// grain-less is what a punktfunk host encodes.
if let Ok(key) = Av1ProfileKey::from_negotiated(1, 8, false) {
out.push(("AV1 Main 4:2:0 8-bit", DecodeProfile::Av1(key), NV12));
}
if let Ok(key) = Av1ProfileKey::from_negotiated(1, 10, false) {
out.push(("AV1 Main 4:2:0 10-bit", DecodeProfile::Av1(key), P010));
}
out
}
/// Ask one physical device every question in the matrix, for every probed profile.
///
/// Never fails as a whole: a driver that refuses a profile outright is a ROW in the
/// output, not an error return — the point is to come back with the full picture even
/// when most of it is refusals.
///
/// # Safety
///
/// `instance` must be a live `VkInstance` created through `entry`, and
/// `physical_device` one of its physical devices. Nothing here creates or destroys
/// anything; every call is a physical-device query.
pub unsafe fn probe_video_formats(
entry: &ash::Entry,
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
) -> Vec<ProfileProbe> {
let video_queue_instance = ash::khr::video_queue::Instance::new(entry, instance);
probed_profiles()
.into_iter()
.map(|(profile, decode_profile, wanted)| {
let usages = USAGE_MATRIX
.iter()
.map(|(label, usage)| {
// SAFETY: caller contract — live instance the video_queue table was
// loaded from, and one of its physical devices.
let formats = unsafe {
query_formats_on(
&video_queue_instance,
physical_device,
decode_profile,
*usage,
)
};
// SAFETY: as above.
let image_format_support = unsafe {
image_format_supported(
instance,
physical_device,
decode_profile,
wanted,
*usage,
)
};
UsageProbe {
label,
usage: *usage,
formats,
image_format_support,
}
})
.collect();
ProfileProbe {
profile,
wanted,
usages,
}
})
.collect()
}
/// The second opinion: can an image of (`format`, `usage`) exist for this video profile,
/// according to `vkGetPhysicalDeviceImageFormatProperties2`?
///
/// This is the same question `vkCreateImage` will be validated against
/// (VUID-VkImageCreateInfo-pNext-06811 routes through the video format properties, but
/// the general image-format query is what reports whether the combination is creatable
/// at all), asked through a DIFFERENT entry point. Where it disagrees with the video
/// format properties, one of the two driver paths is wrong — and knowing which is the
/// difference between a bug report to Intel and a fix in this repository.
///
/// # Safety
///
/// As [`probe_video_formats`].
unsafe fn image_format_supported(
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
decode_profile: DecodeProfile,
format: vk::Format,
usage: vk::ImageUsageFlags,
) -> Result<(), vk::Result> {
let mut chain = decode_profile.chain();
let profile = chain.wire();
let mut profile_list =
vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(profile));
let info = vk::PhysicalDeviceImageFormatInfo2::default()
.format(format)
.ty(vk::ImageType::TYPE_2D)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(usage)
.push_next(&mut profile_list);
let mut props = vk::ImageFormatProperties2::default();
// SAFETY: caller contract (live instance + one of its physical devices); `info`
// roots a wired chain of locals that outlive the call, and `props` is a local the
// driver fills.
unsafe {
instance.get_physical_device_image_format_properties2(physical_device, &info, &mut props)
}
}
/// `usage` as `NAME|NAME (0xHEX)`, with any bit this build cannot name kept VISIBLE.
///
/// The raw value is always printed beside the words for the same reason the codec-op
/// line prints its mask: a reader must be able to check the names against the number,
/// and a bit the tool has no word for must not silently vanish from a mask it reports.
pub fn describe_usage(usage: vk::ImageUsageFlags) -> String {
const BITS: [(vk::ImageUsageFlags, &str); 9] = [
(vk::ImageUsageFlags::TRANSFER_SRC, "TRANSFER_SRC"),
(vk::ImageUsageFlags::TRANSFER_DST, "TRANSFER_DST"),
(vk::ImageUsageFlags::SAMPLED, "SAMPLED"),
(vk::ImageUsageFlags::STORAGE, "STORAGE"),
(vk::ImageUsageFlags::COLOR_ATTACHMENT, "COLOR_ATTACHMENT"),
(vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR, "DECODE_DST"),
(vk::ImageUsageFlags::VIDEO_DECODE_SRC_KHR, "DECODE_SRC"),
(vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR, "DECODE_DPB"),
(vk::ImageUsageFlags::INPUT_ATTACHMENT, "INPUT_ATTACHMENT"),
];
describe_mask(usage.as_raw(), &BITS.map(|(f, n)| (f.as_raw(), n)))
}
/// `imageCreateFlags` as `NAME|NAME (0xHEX)`, same accounting rule as
/// [`describe_usage`].
pub fn describe_create_flags(flags: vk::ImageCreateFlags) -> String {
const BITS: [(vk::ImageCreateFlags, &str); 5] = [
(vk::ImageCreateFlags::MUTABLE_FORMAT, "MUTABLE_FORMAT"),
(vk::ImageCreateFlags::EXTENDED_USAGE, "EXTENDED_USAGE"),
(vk::ImageCreateFlags::ALIAS, "ALIAS"),
(vk::ImageCreateFlags::DISJOINT, "DISJOINT"),
(vk::ImageCreateFlags::PROTECTED, "PROTECTED"),
];
describe_mask(flags.as_raw(), &BITS.map(|(f, n)| (f.as_raw(), n)))
}
/// The shared naming rule: named bits joined by `|`, then the raw value, then any
/// leftover bits called out as unrecognised rather than dropped.
fn describe_mask(raw: u32, bits: &[(u32, &str)]) -> String {
if raw == 0 {
return "(none) (0x0)".to_string();
}
let mut names: Vec<&str> = bits
.iter()
.filter(|(bit, _)| raw & bit != 0)
.map(|(_, name)| *name)
.collect();
let named: u32 = bits.iter().map(|(bit, _)| bit).sum();
let leftover = raw & !named;
let extra;
if leftover != 0 {
extra = format!("unrecognised 0x{leftover:X}");
names.push(&extra);
}
format!("{} (0x{raw:X})", names.join("|"))
}
#[cfg(test)]
mod tests {
use super::*;
/// The matrix must contain the three combinations the pools REALLY create with —
/// a probe that answers about usages nobody creates explains nothing.
#[test]
fn the_matrix_covers_every_usage_the_pools_create_with() {
for real in [COINCIDE_USAGE, DPB_USAGE, OUTPUT_USAGE] {
assert!(
USAGE_MATRIX.iter().any(|(_, u)| *u == real),
"{real:?} is created by the image pools but never probed"
);
}
// And the two discriminators that localise a refusal.
assert!(USAGE_MATRIX.iter().any(|(_, u)| *u == DECODE_ONLY_USAGE));
assert!(USAGE_MATRIX
.iter()
.any(|(_, u)| *u == vk::ImageUsageFlags::SAMPLED));
}
/// Every profile the client can negotiate gets a row, each with the picture format
/// its caps derivation will look for — a probe that reported a 10-bit profile
/// against NV12 would "find" nothing and read as a device gap.
#[test]
fn every_probed_profile_names_the_format_derivation_wants() {
let profiles = probed_profiles();
assert!(
profiles.len() >= 5,
"expected H.264 + H.265 8/10-bit + AV1 8/10-bit, got {}",
profiles.len()
);
for (name, _, wanted) in &profiles {
assert!(
crate::caps::OUTPUT_FORMATS.contains(wanted),
"{name} wants {wanted:?}, which is outside this crate's output vocabulary"
);
}
assert!(profiles.iter().any(|(_, _, w)| *w == P010), "no 10-bit leg");
}
/// The mask printers must never drop a bit: names AND the raw value AND anything
/// unnamed. This is the accounting rule the codec-op line already follows, and the
/// reason it exists is that a silently-dropped bit reads as a capability the device
/// does not have (or worse, hides one it does).
#[test]
fn mask_descriptions_account_for_every_bit_they_print() {
assert_eq!(
describe_usage(COINCIDE_USAGE),
"SAMPLED|DECODE_DST|DECODE_DPB (0x1404)"
);
assert_eq!(describe_usage(vk::ImageUsageFlags::empty()), "(none) (0x0)");
// The Intel Arc envelope, verbatim — the string a field report will contain.
let intel = vk::ImageUsageFlags::TRANSFER_SRC
| vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR
| vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR;
assert_eq!(
describe_usage(intel),
"TRANSFER_SRC|DECODE_DST|DECODE_DPB (0x1401)"
);
// A bit with no name still shows up.
let unknown = vk::ImageUsageFlags::from_raw(0x8000_0000);
assert_eq!(
describe_usage(unknown),
"unrecognised 0x80000000 (0x80000000)"
);
assert_eq!(
describe_create_flags(
vk::ImageCreateFlags::MUTABLE_FORMAT | vk::ImageCreateFlags::EXTENDED_USAGE
),
"MUTABLE_FORMAT|EXTENDED_USAGE (0x108)"
);
assert_eq!(
describe_create_flags(vk::ImageCreateFlags::empty()),
"(none) (0x0)"
);
}
/// `wanted_entry` picks by FORMAT, so a driver that returns several entries cannot
/// hide the one derivation will act on behind a different format.
#[test]
fn the_wanted_entry_is_found_by_format_among_others() {
let probe = UsageProbe {
label: "x",
usage: COINCIDE_USAGE,
formats: Ok(vec![
VideoFormat {
format: P010,
image_usage: COINCIDE_USAGE,
..Default::default()
},
VideoFormat {
format: NV12,
image_usage: DPB_USAGE,
..Default::default()
},
]),
image_format_support: Ok(()),
};
assert_eq!(probe.wanted_entry(NV12).unwrap().image_usage, DPB_USAGE);
assert!(probe.wanted_entry(crate::caps::YUV444_8).is_none());
// A failed query has no entry at all — distinct from "returned nothing".
let failed = UsageProbe {
formats: Err(vk::Result::ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR),
..probe
};
assert!(failed.wanted_entry(NV12).is_none());
}
}