use crate::AdapterPolicy;
pub const GPU_ENCODE_CODECS: [&str; 3] = ["h264", "hevc", "av1"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdapterRecord {
pub index: usize,
pub name: String,
pub backend: &'static str,
pub luid: Option<u64>,
pub software: bool,
pub renders: Result<(), String>,
pub decodes: Result<(), String>,
pub gpu_encodes: Vec<(&'static str, Result<(), String>)>,
}
impl AdapterRecord {
#[must_use]
pub fn unasked(index: usize, name: String, backend: &'static str, software: bool) -> Self {
Self {
index,
name,
backend,
luid: None,
software,
renders: Err("not opened".to_owned()),
decodes: Err("not asked".to_owned()),
gpu_encodes: Vec::new(),
}
}
#[must_use]
pub fn encodes(&self, codec: &str) -> bool {
self.gpu_encodes
.iter()
.any(|(asked, answer)| *asked == codec && answer.is_ok())
}
#[must_use]
pub fn encoded_codecs(&self) -> Vec<&'static str> {
self.gpu_encodes
.iter()
.filter(|(_, answer)| answer.is_ok())
.map(|(codec, _)| *codec)
.collect()
}
#[must_use]
pub fn gpu_encode_reason(&self) -> Option<String> {
if !self.encoded_codecs().is_empty() {
return None;
}
Some(
match self
.gpu_encodes
.iter()
.find_map(|(_, answer)| answer.as_ref().err())
{
Some(reason) => reason.clone(),
None => "not asked".to_owned(),
},
)
}
#[must_use]
pub fn label(&self) -> String {
format!(
"[{}] {}{}",
self.index,
self.name,
if self.software {
" (a software adapter)"
} else {
""
}
)
}
#[must_use]
pub fn summary(&self) -> String {
let label = self.label();
if let Err(reason) = &self.renders {
return format!("{label}: does not render ({reason})");
}
let decodes = match &self.decodes {
Ok(()) => "yes".to_owned(),
Err(reason) => format!("no ({reason})"),
};
let encodes = match self.gpu_encode_reason() {
None => self.encoded_codecs().join(","),
Some(reason) => format!("none ({reason})"),
};
format!("{label}: renders, decodes: {decodes}, encodes on the GPU: {encodes}")
}
fn renders_in_hardware(&self) -> bool {
self.renders.is_ok() && !self.software
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChoiceReason {
Pinned,
OnlyAdapter,
MostGpuEncoders,
Decodes,
HighPerformanceOrder,
PerformancePolicy,
NoSharedDevice,
}
impl ChoiceReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Pinned => "pinned",
Self::OnlyAdapter => "only-adapter",
Self::MostGpuEncoders => "most-gpu-encoders",
Self::Decodes => "decodes",
Self::HighPerformanceOrder => "high-performance-order",
Self::PerformancePolicy => "performance-policy",
Self::NoSharedDevice => "no-shared-device",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AdapterOrder {
pub order: Vec<usize>,
pub reason: ChoiceReason,
}
pub fn rank_adapters(
records: &[AdapterRecord],
policy: &AdapterPolicy,
decode_allowed: bool,
) -> Result<AdapterOrder, String> {
let considered: Vec<usize> = (0..records.len())
.filter(|&at| !records[at].software)
.chain((0..records.len()).filter(|&at| records[at].software))
.collect();
if policy.is_pin() {
let named: Vec<usize> = considered
.iter()
.copied()
.filter(|&at| policy.names(records[at].index, &records[at].name))
.collect();
if named.is_empty() {
return Err(format!(
"{} matches no adapter; the adapters are {}",
policy.setting(),
adapter_list(records)
));
}
let order: Vec<usize> = named
.iter()
.copied()
.filter(|&at| records[at].renders.is_ok())
.collect();
if order.is_empty() {
return Err(format!(
"{} names {}, which does not render",
policy.setting(),
named
.iter()
.map(|&at| records[at].summary())
.collect::<Vec<_>>()
.join("; ")
));
}
return Ok(AdapterOrder {
order,
reason: ChoiceReason::Pinned,
});
}
let mut order: Vec<usize> = considered
.into_iter()
.filter(|&at| records[at].renders.is_ok())
.collect();
if order.is_empty() {
return Err(format!(
"no adapter renders: {}",
records
.iter()
.map(AdapterRecord::summary)
.collect::<Vec<_>>()
.join("; ")
));
}
if *policy == AdapterPolicy::Performance {
return Ok(AdapterOrder {
order,
reason: ChoiceReason::PerformancePolicy,
});
}
let decodes = |at: usize| decode_allowed && records[at].decodes.is_ok();
order.sort_by_key(|&at| {
let record = &records[at];
let hardware = !record.software;
(
!hardware,
std::cmp::Reverse(if hardware {
record.encoded_codecs().len()
} else {
0
}),
!(hardware && decodes(at)),
)
});
let reason = match order.get(1) {
None => ChoiceReason::OnlyAdapter,
Some(&next) if !records[order[0]].software && records[next].software => {
ChoiceReason::OnlyAdapter
}
Some(&next) => {
let (first, second) = (&records[order[0]], &records[next]);
let encoders = |record: &AdapterRecord| {
if record.software {
0
} else {
record.encoded_codecs().len()
}
};
if encoders(first) > encoders(second) {
ChoiceReason::MostGpuEncoders
} else if first.renders_in_hardware()
&& decodes(order[0])
&& !(second.renders_in_hardware() && decodes(next))
{
ChoiceReason::Decodes
} else {
ChoiceReason::HighPerformanceOrder
}
}
};
Ok(AdapterOrder { order, reason })
}
#[must_use]
pub fn explain_choice(
records: &[AdapterRecord],
chosen: usize,
next: Option<usize>,
reason: ChoiceReason,
policy: &AdapterPolicy,
) -> String {
let Some(record) = records.get(chosen) else {
return "no adapter was chosen".to_owned();
};
let encodes = |record: &AdapterRecord| match record.gpu_encode_reason() {
None => format!("encodes {} on the GPU", record.encoded_codecs().join(",")),
Some(why) => format!("encodes nothing on the GPU ({why})"),
};
let next = next.and_then(|at| records.get(at));
match (reason, next) {
(ChoiceReason::Pinned, _) => format!("{} names {}", policy.setting(), record.name),
(ChoiceReason::OnlyAdapter, _) => format!(
"{} is the only {}adapter that renders",
record.name,
if record.software { "" } else { "hardware " }
),
(ChoiceReason::PerformancePolicy, _) if record.software => format!(
"{} renders in software: no hardware adapter renders here",
record.name
),
(ChoiceReason::PerformancePolicy, _) => format!(
"{} is the first hardware adapter in the high-performance order",
record.name
),
(ChoiceReason::NoSharedDevice, _) => format!(
"{} renders without a device shared with the video decoder and encoder",
record.name
),
(ChoiceReason::MostGpuEncoders, Some(next)) => format!(
"{} {}, more codecs than {}, which {}",
record.name,
encodes(record),
next.name,
encodes(next)
),
(ChoiceReason::Decodes, Some(next)) => format!(
"{} decodes on the GPU and {} does not ({}); both encode as many codecs on the GPU",
record.name,
next.name,
next.decodes
.as_ref()
.err()
.map_or("decode is in software", String::as_str)
),
(ChoiceReason::HighPerformanceOrder, Some(next)) => format!(
"{} comes before {} in the high-performance order, and does no less on the GPU",
record.name, next.name
),
(_, None) => format!("{} is the only adapter that renders", record.name),
}
}
fn adapter_list(records: &[AdapterRecord]) -> String {
if records.is_empty() {
return "none".to_owned();
}
records
.iter()
.map(|record| format!("[{}] {}", record.index, record.name))
.collect::<Vec<_>>()
.join(", ")
}
#[must_use]
pub fn encode_elsewhere(records: &[AdapterRecord], chosen: usize) -> Vec<(&'static str, String)> {
let Some(current) = records.get(chosen) else {
return Vec::new();
};
GPU_ENCODE_CODECS
.iter()
.filter(|codec| !current.encodes(codec))
.filter_map(|&codec| {
let other = records
.iter()
.enumerate()
.find(|(at, record)| {
*at != chosen && record.renders_in_hardware() && record.encodes(codec)
})
.map(|(_, record)| record)?;
Some((
codec,
format!(
"{} encodes {codec} on the GPU ({} renders there)",
other.name,
AdapterPolicy::Index(other.index).setting()
),
))
})
.collect()
}
#[must_use]
pub fn decode_refusals(records: &[AdapterRecord], chosen: usize, pinned: bool) -> String {
let considered: Vec<&AdapterRecord> = if pinned {
records.get(chosen).into_iter().collect()
} else {
records
.iter()
.filter(|record| record.renders_in_hardware())
.collect()
};
considered
.into_iter()
.filter_map(|record| {
record
.decodes
.as_ref()
.err()
.map(|reason| format!("{}: {reason}", record.name))
})
.collect::<Vec<_>>()
.join("; ")
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdapterChoiceReport {
pub policy: AdapterPolicy,
pub chosen: Option<usize>,
pub reason: ChoiceReason,
pub explanation: String,
pub records: Vec<AdapterRecord>,
}
impl AdapterChoiceReport {
#[must_use]
pub fn ranked(
policy: AdapterPolicy,
records: Vec<AdapterRecord>,
order: &AdapterOrder,
) -> Self {
let chosen = order.order.first().copied();
let explanation = chosen.map_or_else(
|| "no adapter was chosen".to_owned(),
|chosen| {
explain_choice(
&records,
chosen,
order.order.get(1).copied(),
order.reason,
&policy,
)
},
);
Self {
policy,
chosen,
reason: order.reason,
explanation,
records,
}
}
#[must_use]
pub fn chosen_record(&self) -> Option<&AdapterRecord> {
self.chosen.and_then(|at| self.records.get(at))
}
}
#[cfg(test)]
#[path = "adapter_choice_tests.rs"]
mod tests;