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
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:
Generated
+1
@@ -3196,6 +3196,7 @@ dependencies = [
|
||||
"ash",
|
||||
"async-channel",
|
||||
"pf-client-core",
|
||||
"pf-vkdecode",
|
||||
"punktfunk-core",
|
||||
"sdl3",
|
||||
"tracing",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user