Merge pull request 'Intel Arc never used Vulkan Video because our pNext order fed the decode caps to the wrong struct' (#92) from worktree-probe-codec-names into main
audit / bun-audit (plugin-kit) (push) Successful in 19s
audit / bun-audit (sdk) (push) Successful in 21s
audit / bun-audit (web) (push) Failing after 20s
audit / cargo-audit (push) Successful in 32s
audit / docs-site-audit (push) Successful in 15s
audit / pnpm-audit (push) Failing after 39s
apple / swift (push) Successful in 1m28s
ci / rust-arm64 (push) Successful in 1m25s
ci / web (push) Successful in 1m37s
ci / bun-nix (push) Successful in 27s
ci / docs-site (push) Successful in 1m19s
ci / rust (push) Successful in 4m47s
audit / license-gate (push) Successful in 5m7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
deb / build-publish-client-arm64 (push) Successful in 2m33s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m21s
android / android (push) Successful in 8m3s
deb / build-publish (push) Successful in 7m40s
deb / build-publish-host (push) Successful in 7m22s
docker / builders-arm64cross (push) Successful in 5s
release / apple (push) Successful in 9m44s
windows-host / package (push) Successful in 11m14s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 16s
docker / deploy-docs (push) Failing after 1m41s
arch / build-publish (push) Successful in 13m59s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m27s
flatpak / build-publish (push) Successful in 8m26s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m37s
apple / screenshots (push) Successful in 5m47s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m6s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m18s
nix / flake (push) Successful in 15m19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m14s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 24m25s
audit / bun-audit (plugin-kit) (push) Successful in 19s
audit / bun-audit (sdk) (push) Successful in 21s
audit / bun-audit (web) (push) Failing after 20s
audit / cargo-audit (push) Successful in 32s
audit / docs-site-audit (push) Successful in 15s
audit / pnpm-audit (push) Failing after 39s
apple / swift (push) Successful in 1m28s
ci / rust-arm64 (push) Successful in 1m25s
ci / web (push) Successful in 1m37s
ci / bun-nix (push) Successful in 27s
ci / docs-site (push) Successful in 1m19s
ci / rust (push) Successful in 4m47s
audit / license-gate (push) Successful in 5m7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
deb / build-publish-client-arm64 (push) Successful in 2m33s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m21s
android / android (push) Successful in 8m3s
deb / build-publish (push) Successful in 7m40s
deb / build-publish-host (push) Successful in 7m22s
docker / builders-arm64cross (push) Successful in 5s
release / apple (push) Successful in 9m44s
windows-host / package (push) Successful in 11m14s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 16s
docker / deploy-docs (push) Failing after 1m41s
arch / build-publish (push) Successful in 13m59s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m27s
flatpak / build-publish (push) Successful in 8m26s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m37s
apple / screenshots (push) Successful in 5m47s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m6s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m18s
nix / flake (push) Successful in 15m19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m14s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 24m25s
Reviewed-on: #92
This commit was merged in pull request #92.
This commit is contained in:
Generated
+1
@@ -3196,6 +3196,7 @@ dependencies = [
|
||||
"ash",
|
||||
"async-channel",
|
||||
"pf-client-core",
|
||||
"pf-vkdecode",
|
||||
"punktfunk-core",
|
||||
"sdl3",
|
||||
"tracing",
|
||||
|
||||
+111
-4
@@ -510,6 +510,65 @@ 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 second opinion, printed only where it differs from the video
|
||||
// format query. Worded as "also asked" rather than "disagrees" on
|
||||
// purpose: measured on both vendors this call answers "creatable" for
|
||||
// combinations the video query rejects (NVIDIA included, for SAMPLED
|
||||
// alone), so it does not honour the profile list and a difference here
|
||||
// is NOT the driver contradicting itself. Printed anyway because the
|
||||
// question gets re-asked by everyone who reads a refusal.
|
||||
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(()) => "creatable".to_string(),
|
||||
Err(e) => format!("{e:?}"),
|
||||
};
|
||||
println!(
|
||||
" {:<24} (also asked: \
|
||||
vkGetPhysicalDeviceImageFormatProperties2 says {second} — that \
|
||||
call does not honour the profile list; not authority)",
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> u8 {
|
||||
// Logs to STDERR — stdout is the machine interface (ready/stats/error lines).
|
||||
tracing_subscriber::fmt()
|
||||
@@ -548,12 +607,21 @@ mod session_main {
|
||||
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
|
||||
// The bracketed number is the PUNKTFUNK_VK_DEVICE value, and the
|
||||
// FIRST listed entry 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" };
|
||||
// `a.index`, NOT the loop position. This list is sorted
|
||||
// discrete-first for reading, but PUNKTFUNK_VK_DEVICE indexes the
|
||||
// raw enumeration, which puts the iGPU first on some hybrids —
|
||||
// printing the loop position would name the other GPU on exactly
|
||||
// the machines this flag is for. The `i == 0` marker is still the
|
||||
// loop position, because sorted-first IS what pick_device lands on
|
||||
// when nothing overrides it.
|
||||
println!(
|
||||
"[{i}] {} ({kind}){}",
|
||||
"[{}] {} ({kind}){}",
|
||||
a.index,
|
||||
a.name,
|
||||
if i == 0 { " <- default presenter" } else { "" }
|
||||
);
|
||||
@@ -561,11 +629,29 @@ mod session_main {
|
||||
" vulkan video decode: {}",
|
||||
if a.usable { "YES" } else { "no" }
|
||||
);
|
||||
let codecs: Vec<&str> = [(0x1u32, "H.264"), (0x2, "H.265"), (0x4, "AV1")]
|
||||
// Name every bit, and ACCOUNT for the ones we cannot name. The
|
||||
// 5070 Ti reports 0xF — four bits — while punktfunk decodes three
|
||||
// codecs, so the first version of this line printed three names
|
||||
// beside a four-bit mask and looked complete. VP9 (bit 3) is a
|
||||
// real decode operation this client has no rung for; a codec the
|
||||
// tool cannot name must not silently vanish from a mask it prints,
|
||||
// or the reader is left to trust that the words cover the number.
|
||||
const OPS: [(u32, &str); 4] = [
|
||||
(0x1, "H.264"),
|
||||
(0x2, "H.265"),
|
||||
(0x4, "AV1"),
|
||||
(0x8, "VP9 (no punktfunk rung)"),
|
||||
];
|
||||
let mut codecs: Vec<String> = OPS
|
||||
.iter()
|
||||
.filter(|(bit, _)| a.codec_ops & bit != 0)
|
||||
.map(|(_, n)| *n)
|
||||
.map(|(_, n)| (*n).to_string())
|
||||
.collect();
|
||||
let named: u32 = OPS.iter().map(|(b, _)| b).sum();
|
||||
let unknown = a.codec_ops & !named;
|
||||
if unknown != 0 {
|
||||
codecs.push(format!("unrecognised bits 0x{unknown:X}"));
|
||||
}
|
||||
println!(
|
||||
" driver decode ops: {}",
|
||||
if codecs.is_empty() {
|
||||
@@ -601,6 +687,27 @@ 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
|
||||
// capable GPU listed and concluding the decoder will use it.
|
||||
// Vulkan Video decodes on the PRESENTER's device, and the decoder
|
||||
// preference does not move the presenter.
|
||||
println!();
|
||||
println!(
|
||||
"Vulkan Video decodes on the presenter's device. PUNKTFUNK_DECODER \
|
||||
picks the rung,"
|
||||
);
|
||||
println!(
|
||||
"not the GPU — move the presenter with PUNKTFUNK_VK_DEVICE=<index \
|
||||
above> or"
|
||||
);
|
||||
println!(
|
||||
"PUNKTFUNK_VK_ADAPTER=<name substring>, which is the safer knob \
|
||||
where two"
|
||||
);
|
||||
println!("adapters share a name.");
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
@@ -1340,11 +1340,30 @@ pub fn migrate_decoder_pref(pref: &str) -> String {
|
||||
/// Same precedence as [`Decoder::new`] resolves (env first, then the setting), because a
|
||||
/// second reading of the same two inputs is a second place for them to drift.
|
||||
pub fn decode_pinned_to_software(pref: &str) -> bool {
|
||||
std::env::var("PUNKTFUNK_DECODER")
|
||||
.ok()
|
||||
resolve_decoder_pref(std::env::var("PUNKTFUNK_DECODER").ok().as_deref(), pref) == "software"
|
||||
}
|
||||
|
||||
/// Resolve the decoder preference: the `PUNKTFUNK_DECODER` override if it carries a
|
||||
/// value, else the stored setting. Pure, so the rule is testable without touching the
|
||||
/// process environment — and shared, because [`Decoder::new`] and
|
||||
/// [`decode_pinned_to_software`] read the same two inputs and a second reading is a
|
||||
/// second place for them to drift.
|
||||
///
|
||||
/// **Trimmed**, which is the part that had to be fixed rather than merely factored out.
|
||||
/// `PUNKTFUNK_VK_ADAPTER` already trimmed; this did not, so `"native-vulkan "` — one
|
||||
/// trailing space, which a Windows `.cmd` produces for free because `echo x>> file`
|
||||
/// keeps the space before the redirect — matched no arm of [`native_vulkan_gate`] and
|
||||
/// fell through to `auto` SILENTLY. An operator's pin was ignored and nothing said so,
|
||||
/// which is the exact failure the rest of this module's logging exists to prevent. It
|
||||
/// cost a full on-glass session to find.
|
||||
///
|
||||
/// Whitespace-only is treated as absent, not as a pin to `""`: someone who exported the
|
||||
/// variable empty means "no override", and `""` is a value `native_vulkan_gate` happens
|
||||
/// to accept.
|
||||
pub(crate) fn resolve_decoder_pref(env: Option<&str>, pref: &str) -> String {
|
||||
env.map(str::trim)
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or_else(|| pref.to_string())
|
||||
== "software"
|
||||
.map_or_else(|| pref.to_string(), str::to_string)
|
||||
}
|
||||
|
||||
/// The `quic` codec bitfield this client can decode — the union of the codecs the RUNGS
|
||||
@@ -1609,10 +1628,7 @@ impl Decoder {
|
||||
vk: Option<&VulkanDecodeDevice>,
|
||||
stream: StreamFormat,
|
||||
) -> Result<Decoder> {
|
||||
let stored = std::env::var("PUNKTFUNK_DECODER")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or_else(|| pref.to_string());
|
||||
let stored = resolve_decoder_pref(std::env::var("PUNKTFUNK_DECODER").ok().as_deref(), pref);
|
||||
let choice = migrate_decoder_pref(&stored);
|
||||
if choice != stored {
|
||||
// Said once per session, at `warn`, because a developer who set the env var to
|
||||
@@ -3006,6 +3022,49 @@ mod tests {
|
||||
/// device leg: admitting HEVC on an H.264-only decode family would create a video
|
||||
/// session for an operation the family cannot run, which is undefined behaviour
|
||||
/// rather than an error.
|
||||
/// A pin with stray whitespace is still a pin, and the gate must accept it.
|
||||
///
|
||||
/// This is a regression test with a field cost: `"native-vulkan "` (one trailing
|
||||
/// space, which a Windows `.cmd` adds for free) matched no arm of
|
||||
/// `native_vulkan_gate`, so the rung fell through to `auto` with nothing logged —
|
||||
/// on a box where `auto` picks a different rung, that reads exactly like the pin
|
||||
/// being refused for a hardware reason. The second half is what makes it a *shared*
|
||||
/// rule: `decode_pinned_to_software` reads the same variable, and its own docs say
|
||||
/// a second reading is a second place to drift.
|
||||
#[test]
|
||||
fn a_decoder_pin_survives_the_whitespace_a_shell_script_adds() {
|
||||
assert_eq!(
|
||||
resolve_decoder_pref(Some("native-vulkan "), "auto"),
|
||||
"native-vulkan",
|
||||
"a trailing space must not turn a pin into an unrecognised value"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_decoder_pref(Some(" software\t"), "auto"),
|
||||
"software"
|
||||
);
|
||||
// Trimmed to nothing means ABSENT — fall back to the stored setting rather than
|
||||
// pinning to "", which the gate would otherwise accept as the auto family.
|
||||
assert_eq!(
|
||||
resolve_decoder_pref(Some(" "), "native-vaapi"),
|
||||
"native-vaapi"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_decoder_pref(Some(""), "native-vaapi"),
|
||||
"native-vaapi"
|
||||
);
|
||||
assert_eq!(resolve_decoder_pref(None, "native-vaapi"), "native-vaapi");
|
||||
// …and the trimmed value is what the gate actually admits.
|
||||
assert!(
|
||||
native_vulkan_gate(
|
||||
&resolve_decoder_pref(Some("native-vulkan "), "auto"),
|
||||
punktfunk_core::quic::CODEC_HEVC,
|
||||
true,
|
||||
VIDEO_CODEC_OP_DECODE_H265,
|
||||
),
|
||||
"the whole point: the trimmed pin reaches the gate and is admitted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_vulkan_gate_admits_pin_and_auto_family_per_codec_on_a_capable_family() {
|
||||
// Pin the raw spec values, not the implementation constants — a typo'd bit
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -55,7 +55,19 @@ pub(crate) fn video_decode_gate(
|
||||
/// 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.
|
||||
/// The device's position in the RAW `vkEnumeratePhysicalDevices` order — and
|
||||
/// therefore the value `PUNKTFUNK_VK_DEVICE` takes, because `pick_device` indexes the
|
||||
/// unsorted list (`devices.get(i)`) before any ranking runs.
|
||||
///
|
||||
/// ⚠ NOT the display position. This list is sorted discrete-first for readability,
|
||||
/// while enumeration order puts the iGPU first on some hybrids — so the two disagree
|
||||
/// on exactly the machines this probe exists to diagnose. Printing the display
|
||||
/// position as if it were the env value would hand a hybrid-laptop reporter the
|
||||
/// number for the other GPU.
|
||||
pub index: usize,
|
||||
/// Marketing name — also the `PUNKTFUNK_VK_ADAPTER` match key. Not necessarily
|
||||
/// unique: a hybrid can expose the same iGPU twice, and a name match then resolves to
|
||||
/// whichever enumerates first.
|
||||
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.
|
||||
@@ -73,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
|
||||
@@ -743,7 +765,9 @@ pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
|
||||
// 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 {
|
||||
// `enumerate()` BEFORE any filtering or sorting: this index is what
|
||||
// `PUNKTFUNK_VK_DEVICE` selects, so it has to survive both.
|
||||
for (raw_index, pdev) in devices.into_iter().enumerate() {
|
||||
// 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) };
|
||||
@@ -831,9 +855,20 @@ 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 {
|
||||
index: raw_index,
|
||||
name,
|
||||
discrete: rank == 0,
|
||||
api_1_3,
|
||||
@@ -843,6 +878,7 @@ pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
|
||||
base_missing,
|
||||
codec_exts,
|
||||
usable,
|
||||
formats,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
+162
-23
@@ -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.
|
||||
@@ -257,12 +288,18 @@ pub enum CapsError {
|
||||
/// anyway would be a silent VUID violation.
|
||||
UsageUnsupported {
|
||||
mode: &'static str,
|
||||
/// The picture format whose entry fell short — NOT always NV12 (a Main 10
|
||||
/// stream is refused about P010), which is what this used to say regardless.
|
||||
format: vk::Format,
|
||||
missing: vk::ImageUsageFlags,
|
||||
},
|
||||
/// The presenter-facing entry for `mode` does not allow `MUTABLE_FORMAT`, so
|
||||
/// the per-plane views the presenter samples through ([`plane_formats`])
|
||||
/// cannot exist on this device.
|
||||
NoMutableFormat { mode: &'static str },
|
||||
NoMutableFormat {
|
||||
mode: &'static str,
|
||||
format: vk::Format,
|
||||
},
|
||||
/// The driver forces COINCIDE mode AND a layered DPB (one image array, no
|
||||
/// `SEPARATE_REFERENCE_IMAGES`): the picture-pool model — a re-activated slot
|
||||
/// binding a fresh free image, so delivered pictures are never decode targets
|
||||
@@ -291,16 +328,36 @@ impl std::fmt::Display for CapsError {
|
||||
CapsError::NoPlaneMapping { format } => {
|
||||
write!(f, "no per-plane view mapping for {format:?}")
|
||||
}
|
||||
CapsError::UsageUnsupported { mode, missing } => {
|
||||
CapsError::UsageUnsupported {
|
||||
mode,
|
||||
format,
|
||||
missing,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"the {mode} NV12 entry does not advertise usage {missing:?}"
|
||||
)
|
||||
"the {mode} {format:?} entry does not advertise usage {missing:?}"
|
||||
)?;
|
||||
// Name the CONSEQUENCE for the one missing bit that is not a passing
|
||||
// driver quirk. Without `SAMPLED` nothing in a shader can read the
|
||||
// decoded picture, so the zero-copy path this rung exists for cannot be
|
||||
// built here at all — a fact worth stating in the log line rather than
|
||||
// leaving a field reporter to work out from a flag name. Measured on
|
||||
// Intel Arc (Windows 101.8861), where the whole advertised envelope is
|
||||
// TRANSFER_SRC|DECODE_DST|DECODE_DPB with no image create flags:
|
||||
// `punktfunk-session --probe-decode` prints the driver's own answer.
|
||||
if missing.contains(vk::ImageUsageFlags::SAMPLED) {
|
||||
write!(
|
||||
f,
|
||||
" — no shader can read this device's decoded pictures, so the \
|
||||
zero-copy path cannot exist on it (see --probe-decode)"
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
CapsError::NoMutableFormat { mode } => {
|
||||
CapsError::NoMutableFormat { mode, format } => {
|
||||
write!(
|
||||
f,
|
||||
"the {mode} NV12 entry does not allow MUTABLE_FORMAT (per-plane views)"
|
||||
"the {mode} {format:?} entry does not allow MUTABLE_FORMAT (per-plane views)"
|
||||
)
|
||||
}
|
||||
CapsError::CoincideLayeredDpb => {
|
||||
@@ -472,7 +529,14 @@ fn require_usage(
|
||||
if missing.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CapsError::UsageUnsupported { mode, missing })
|
||||
// The entry's OWN format, not the caller's `wanted`: they are equal here (the
|
||||
// entry was picked by format), and taking it from the driver's record keeps the
|
||||
// message describing what the driver actually said.
|
||||
Err(CapsError::UsageUnsupported {
|
||||
mode,
|
||||
format: entry.format,
|
||||
missing,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +547,10 @@ fn require_mutable(entry: &VideoFormat, mode: &'static str) -> Result<(), CapsEr
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CapsError::NoMutableFormat { mode })
|
||||
Err(CapsError::NoMutableFormat {
|
||||
mode,
|
||||
format: entry.format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,9 +672,12 @@ pub(crate) unsafe fn query_h264_caps(
|
||||
|
||||
let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default();
|
||||
let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default();
|
||||
// ⚠ ORDER IS LOAD-BEARING — see the measured Intel Arc swap in
|
||||
// [`crate::caps_h265::query_h265_caps`]. `push_next` prepends, so pushing the codec
|
||||
// struct FIRST leaves VkVideoDecodeCapabilitiesKHR directly after the base struct.
|
||||
let mut caps = vk::VideoCapabilitiesKHR::default()
|
||||
.push_next(&mut decode_caps)
|
||||
.push_next(&mut h264_caps);
|
||||
.push_next(&mut h264_caps)
|
||||
.push_next(&mut decode_caps);
|
||||
// SAFETY: physical device is live (DeviceHandles contract); `profile` roots a
|
||||
// fully wired, immovable chain; `caps` chains driver-fillable structs that all
|
||||
// outlive the call.
|
||||
@@ -674,6 +744,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();
|
||||
@@ -683,21 +783,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,
|
||||
@@ -708,7 +800,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);
|
||||
}
|
||||
@@ -719,6 +811,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())
|
||||
}
|
||||
@@ -735,6 +829,7 @@ mod tests {
|
||||
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT
|
||||
| vk::ImageCreateFlags::ALIAS
|
||||
| vk::ImageCreateFlags::EXTENDED_USAGE,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,6 +875,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![],
|
||||
@@ -901,9 +997,21 @@ mod tests {
|
||||
derive_caps(&raw).unwrap_err(),
|
||||
CapsError::UsageUnsupported {
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: NV12,
|
||||
missing: vk::ImageUsageFlags::SAMPLED
|
||||
}
|
||||
);
|
||||
// The missing bit is SAMPLED, so the message says what that COSTS — a field
|
||||
// report carrying this line should not need a second round trip to learn that
|
||||
// the device cannot host the rung at all.
|
||||
assert!(
|
||||
derive_caps(&raw)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("zero-copy path cannot exist"),
|
||||
"a missing SAMPLED must name its consequence: {}",
|
||||
derive_caps(&raw).unwrap_err()
|
||||
);
|
||||
|
||||
// Same on the distinct output half.
|
||||
let mut raw = nvidia_like();
|
||||
@@ -912,9 +1020,38 @@ mod tests {
|
||||
derive_caps(&raw).unwrap_err(),
|
||||
CapsError::UsageUnsupported {
|
||||
mode: "output (DST|SAMPLED)",
|
||||
format: NV12,
|
||||
missing: vk::ImageUsageFlags::SAMPLED
|
||||
}
|
||||
);
|
||||
|
||||
// A 10-bit stream is refused about P010, not about NV12 — the message used to
|
||||
// say "NV12" whatever the stream was, which sends a reader looking at the wrong
|
||||
// format's support.
|
||||
let mut raw = radv_like();
|
||||
raw.coincide_formats = vec![entry(
|
||||
P010,
|
||||
vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR,
|
||||
)];
|
||||
let err = crate::caps_h265::derive_caps_h265(
|
||||
&crate::caps_h265::RawH265Caps {
|
||||
capability_flags: raw.capability_flags,
|
||||
decode_flags: raw.decode_flags,
|
||||
coincide_formats: raw.coincide_formats.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
P010,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
CapsError::UsageUnsupported {
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: P010,
|
||||
missing: vk::ImageUsageFlags::SAMPLED
|
||||
}
|
||||
);
|
||||
assert!(err.to_string().contains("G10X6"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -924,11 +1061,13 @@ mod tests {
|
||||
format: NV12,
|
||||
image_usage: COINCIDE_USAGE,
|
||||
image_create_flags: vk::ImageCreateFlags::empty(),
|
||||
..Default::default()
|
||||
}];
|
||||
assert_eq!(
|
||||
derive_caps(&raw).unwrap_err(),
|
||||
CapsError::NoMutableFormat {
|
||||
mode: "coincide (DPB|DST|SAMPLED)"
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: NV12,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -278,9 +278,12 @@ pub(crate) unsafe fn query_av1_caps(
|
||||
|
||||
let mut av1_caps = vk::VideoDecodeAV1CapabilitiesKHR::default();
|
||||
let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default();
|
||||
// ⚠ ORDER IS LOAD-BEARING — see the measured Intel Arc swap in
|
||||
// [`crate::caps_h265::query_h265_caps`]. `push_next` prepends, so pushing the codec
|
||||
// struct FIRST leaves VkVideoDecodeCapabilitiesKHR directly after the base struct.
|
||||
let mut caps = vk::VideoCapabilitiesKHR::default()
|
||||
.push_next(&mut decode_caps)
|
||||
.push_next(&mut av1_caps);
|
||||
.push_next(&mut av1_caps)
|
||||
.push_next(&mut decode_caps);
|
||||
// SAFETY: physical device is live (DeviceHandles contract); `profile` roots a
|
||||
// fully wired, immovable chain; `caps` chains driver-fillable structs that all
|
||||
// outlive the call.
|
||||
@@ -351,6 +354,7 @@ mod tests {
|
||||
format,
|
||||
image_usage: usage,
|
||||
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,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![])
|
||||
@@ -671,6 +676,7 @@ mod tests {
|
||||
derive_caps_av1(&raw, NV12).unwrap_err(),
|
||||
CapsError::UsageUnsupported {
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: NV12,
|
||||
missing: vk::ImageUsageFlags::SAMPLED
|
||||
}
|
||||
);
|
||||
@@ -680,11 +686,13 @@ 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(),
|
||||
CapsError::NoMutableFormat {
|
||||
mode: "coincide (DPB|DST|SAMPLED)"
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: NV12,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -279,9 +279,22 @@ pub(crate) unsafe fn query_h265_caps(
|
||||
|
||||
let mut h265_caps = vk::VideoDecodeH265CapabilitiesKHR::default();
|
||||
let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default();
|
||||
// ⚠ ORDER IS LOAD-BEARING on at least one shipping driver. `push_next` PREPENDS, so
|
||||
// the chain is the reverse of the call order: pushing the codec struct last puts
|
||||
// VkVideoDecodeCapabilitiesKHR FIRST after the base struct, which is the order every
|
||||
// Vulkan sample writes it in.
|
||||
//
|
||||
// Measured on Intel Arc (Windows 101.8724) with the previous order — codec struct
|
||||
// first — the driver filled the two by POSITION rather than by sType and returned
|
||||
// them SWAPPED: `decode_caps.flags` came back 12 (= STD_VIDEO_H265_LEVEL_IDC_6_2)
|
||||
// and `h265_caps.maxLevelIdc` came back 1 (= DPB_AND_OUTPUT_COINCIDE). Reading a
|
||||
// level as a flag bitmask means neither COINCIDE nor DISTINCT appeared set, so the
|
||||
// rung refused a device that in fact supports it, and every Arc fell back to D3D11VA.
|
||||
// NVIDIA and RADV dispatch by sType and are indifferent to the order, which is why
|
||||
// the fleet was green and this survived to the field.
|
||||
let mut caps = vk::VideoCapabilitiesKHR::default()
|
||||
.push_next(&mut decode_caps)
|
||||
.push_next(&mut h265_caps);
|
||||
.push_next(&mut h265_caps)
|
||||
.push_next(&mut decode_caps);
|
||||
// SAFETY: physical device is live (DeviceHandles contract); `profile` roots a
|
||||
// fully wired, immovable chain; `caps` chains driver-fillable structs that all
|
||||
// outlive the call.
|
||||
@@ -308,6 +321,28 @@ pub(crate) unsafe fn query_h265_caps(
|
||||
let decode_flags = decode_caps.flags;
|
||||
let max_level_idc = h265_caps.max_level_idc;
|
||||
|
||||
// What the driver ACTUALLY said, before any of our interpretation. Nothing in this
|
||||
// module logged, so a refusal downstream ("advertises neither COINCIDE nor DISTINCT")
|
||||
// was indistinguishable from our own chain never reaching the struct: both present as
|
||||
// a zero. Printing the BASE capabilities beside the decode ones is the discriminator —
|
||||
// a populated `max_dpb_slots` next to `decode_flags: 0` means the driver filled the
|
||||
// chain and genuinely declared no DPB mode; zeros across both mean the query never
|
||||
// landed. Debug rather than info: one line per profile per session, wanted only when
|
||||
// someone is asking this exact question.
|
||||
tracing::debug!(
|
||||
codec = "H.265",
|
||||
?capability_flags,
|
||||
?decode_flags,
|
||||
decode_flags_raw = decode_flags.as_raw(),
|
||||
max_level_idc,
|
||||
max_dpb_slots,
|
||||
max_active_reference_pictures,
|
||||
?min_coded_extent,
|
||||
?max_coded_extent,
|
||||
?picture_access_granularity,
|
||||
"driver video capabilities, verbatim"
|
||||
);
|
||||
|
||||
// The three queries carry the REAL creation usages (SAMPLED included for the
|
||||
// presenter-facing roles) so the answers validate the images the pools build.
|
||||
let decode_profile = DecodeProfile::H265(key);
|
||||
@@ -348,6 +383,7 @@ mod tests {
|
||||
format,
|
||||
image_usage: usage,
|
||||
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,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![])
|
||||
@@ -723,6 +760,7 @@ mod tests {
|
||||
derive_caps_h265(&raw, P010).unwrap_err(),
|
||||
CapsError::UsageUnsupported {
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: P010,
|
||||
missing: vk::ImageUsageFlags::SAMPLED
|
||||
}
|
||||
);
|
||||
@@ -732,11 +770,13 @@ 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(),
|
||||
CapsError::NoMutableFormat {
|
||||
mode: "coincide (DPB|DST|SAMPLED)"
|
||||
mode: "coincide (DPB|DST|SAMPLED)",
|
||||
format: P010,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,442 @@
|
||||
//! 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 Intel answers carry one genuine conformance bug, which is what made the refusal
|
||||
//! read oddly: the driver ignores the REQUESTED `imageUsage` completely. Asked for
|
||||
//! `SAMPLED` alone it still returns that same decode envelope, 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. That is why
|
||||
//! derivation reported "the NV12 entry does not advertise SAMPLED" (an entry was found)
|
||||
//! rather than "no NV12 in the coincide list" (nothing returned). It changes the error
|
||||
//! text, not the answer.
|
||||
//!
|
||||
//! # What the cross-check is, and is NOT
|
||||
//!
|
||||
//! [`UsageProbe::image_format_support`] asks `vkGetPhysicalDeviceImageFormatProperties2`
|
||||
//! the same question with the same profile list chained. It is deliberately kept, and
|
||||
//! deliberately NOT treated as authority, because measuring it settled what it is worth:
|
||||
//! on BOTH vendors it answers "creatable" for combinations the video-format query
|
||||
//! rejects — NVIDIA included, for `SAMPLED` alone, which is not a legal video image
|
||||
//! usage at all. So that entry point does not fully honour the video profile list on any
|
||||
//! driver measured here, and a "creatable" from it is NOT evidence that a decode picture
|
||||
//! can be sampled. It is reported because the question is otherwise re-asked by every
|
||||
//! person who reads the refusal; the answer is on the record instead.
|
||||
//!
|
||||
//! `vkGetPhysicalDeviceVideoFormatPropertiesKHR` remains the authority, and two
|
||||
//! independent implementations of it — this probe and `vulkaninfo --show-video-props` —
|
||||
//! agree on every value above.
|
||||
|
||||
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. `Ok(())` means that call says an image of the shape is creatable —
|
||||
/// which, as the module docs record, is a WEAKER claim than it looks: measured on
|
||||
/// both vendors, this entry point does not fully honour the chained video profile
|
||||
/// list, so it must not be read as permission to create a video image.
|
||||
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 {
|
||||
// The encode trio is here because NVIDIA advertises it on DECODE pictures (measured:
|
||||
// 0xC000 beside the decode bits), and a mask printed as "unrecognised" invites the
|
||||
// reader to wonder whether the tool is out of date rather than reading the answer.
|
||||
const BITS: [(vk::ImageUsageFlags, &str); 12] = [
|
||||
(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::INPUT_ATTACHMENT, "INPUT_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::VIDEO_ENCODE_DST_KHR, "ENCODE_DST"),
|
||||
(vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR, "ENCODE_SRC"),
|
||||
(vk::ImageUsageFlags::VIDEO_ENCODE_DPB_KHR, "ENCODE_DPB"),
|
||||
];
|
||||
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