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