#![cfg_attr(
not(all(target_os = "windows", feature = "hwencode")),
allow(dead_code)
)]
use alpha2_native::NativeVideoCodec;
pub const ENCODE_CODECS: [NativeVideoCodec; 3] = [
NativeVideoCodec::H264,
NativeVideoCodec::Hevc,
NativeVideoCodec::Av1,
];
pub const DEFAULT_ENCODE_CODEC: NativeVideoCodec = NativeVideoCodec::H264;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AdapterPolicy {
Default,
Encoder,
Index(usize),
Name(String),
}
impl AdapterPolicy {
pub fn parse(value: Option<&str>) -> Self {
match value.map(str::trim) {
None | Some("" | "default") => Self::Default,
Some("encoder") => Self::Encoder,
Some(text) => text
.parse::<usize>()
.map_or_else(|_| Self::Name(text.to_owned()), Self::Index),
}
}
pub fn from_env() -> Self {
Self::parse(std::env::var("ALPHA2_GPU_ADAPTER").ok().as_deref())
}
pub const fn is_pin(&self) -> bool {
matches!(self, Self::Index(_) | Self::Name(_))
}
pub fn names(&self, record: &AdapterRecord) -> bool {
match self {
Self::Index(index) => record.index == *index,
Self::Name(name) => record.name.to_lowercase().contains(&name.to_lowercase()),
Self::Default | Self::Encoder => false,
}
}
fn setting(&self) -> String {
match self {
Self::Default => "ALPHA2_GPU_ADAPTER=default".to_owned(),
Self::Encoder => "ALPHA2_GPU_ADAPTER=encoder".to_owned(),
Self::Index(index) => format!("ALPHA2_GPU_ADAPTER={index}"),
Self::Name(name) => format!("ALPHA2_GPU_ADAPTER={name}"),
}
}
}
#[derive(Clone, Debug)]
pub struct AdapterRecord {
pub index: usize,
pub name: String,
pub software: bool,
pub renders: Result<(), String>,
pub decodes: Result<(), String>,
pub encodes: Vec<(NativeVideoCodec, Result<(), String>)>,
}
impl AdapterRecord {
pub fn encodes(&self, codec: NativeVideoCodec) -> bool {
self.encodes
.iter()
.any(|(asked, answer)| *asked == codec && answer.is_ok())
}
pub fn encoded_codecs(&self) -> Vec<NativeVideoCodec> {
self.encodes
.iter()
.filter(|(_, answer)| answer.is_ok())
.map(|(codec, _)| *codec)
.collect()
}
pub fn summary(&self) -> String {
let label = format!(
"[{}] {}{}",
self.index,
self.name,
if self.software {
" (a software adapter)"
} else {
""
}
);
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 reported = self.encoded_codecs();
let encodes = if !reported.is_empty() {
codec_list(&reported)
} else if let Some((_, Err(reason))) = self.encodes.first() {
format!("none ({reason})")
} else {
"not asked".to_owned()
};
format!("{label}: renders, decodes: {decodes}, reports encoders: {encodes}")
}
}
pub fn codec_list(codecs: &[NativeVideoCodec]) -> String {
codecs
.iter()
.map(|codec| codec.as_str())
.collect::<Vec<_>>()
.join(",")
}
fn ranked(records: &[AdapterRecord]) -> impl Iterator<Item = usize> + '_ {
let hardware = (0..records.len()).filter(|&at| !records[at].software);
let software = (0..records.len()).filter(|&at| records[at].software);
hardware.chain(software)
}
pub fn choose_adapter(
records: &[AdapterRecord],
policy: &AdapterPolicy,
decode: bool,
codec: NativeVideoCodec,
) -> Result<usize, String> {
let rendering = || ranked(records).filter(|&at| records[at].renders.is_ok());
let hardware = |at: &usize| !records[*at].software;
let decodes = |at: &usize| records[*at].decodes.is_ok();
let by_default = || {
decode
.then(|| rendering().filter(hardware).find(decodes))
.flatten()
.or_else(|| rendering().next())
};
let chosen = match policy {
AdapterPolicy::Default => by_default(),
AdapterPolicy::Encoder => {
let encoding = || {
rendering()
.filter(hardware)
.filter(|&at| records[at].encodes(codec))
};
decode
.then(|| encoding().find(decodes))
.flatten()
.or_else(|| encoding().next())
.or_else(by_default)
}
AdapterPolicy::Index(_) | AdapterPolicy::Name(_) => {
let named: Vec<usize> = ranked(records)
.filter(|&at| policy.names(&records[at]))
.collect();
if named.is_empty() {
return Err(format!(
"{} matches no DX12 adapter; the adapters are {}",
policy.setting(),
adapter_list(records)
));
}
let chosen = named
.iter()
.copied()
.find(|&at| records[at].renders.is_ok());
if chosen.is_none() {
return Err(format!(
"{} names {}, which does not render",
policy.setting(),
named
.iter()
.map(|&at| records[at].summary())
.collect::<Vec<_>>()
.join("; ")
));
}
chosen
}
};
chosen.ok_or_else(|| {
format!(
"no DX12 adapter renders: {}",
records
.iter()
.map(AdapterRecord::summary)
.collect::<Vec<_>>()
.join("; ")
)
})
}
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(", ")
}
pub fn encode_elsewhere(
records: &[AdapterRecord],
chosen: usize,
decode: bool,
) -> Vec<(NativeVideoCodec, String)> {
let Some(current) = records.get(chosen) else {
return Vec::new();
};
let by_encoder = choose_adapter(
records,
&AdapterPolicy::Encoder,
decode,
DEFAULT_ENCODE_CODEC,
)
.ok()
.filter(|&at| at != chosen && records[at].encodes(DEFAULT_ENCODE_CODEC));
ENCODE_CODECS
.iter()
.filter(|codec| !current.encodes(**codec))
.filter_map(|&codec| {
let other = ranked(records).find(|&at| {
at != chosen
&& !records[at].software
&& records[at].renders.is_ok()
&& records[at].encodes(codec)
})?;
let setting = if codec == DEFAULT_ENCODE_CODEC && by_encoder == Some(other) {
AdapterPolicy::Encoder.setting()
} else {
AdapterPolicy::Index(records[other].index).setting()
};
Some((
codec,
format!(
"{} reports a D3D12 video encoder for {} ({setting} renders there)",
records[other].name,
codec.as_str()
),
))
})
.collect()
}
pub fn decode_refusals(records: &[AdapterRecord], chosen: usize, pinned: bool) -> String {
let considered: Vec<usize> = if pinned {
vec![chosen]
} else {
ranked(records)
.filter(|&at| !records[at].software && records[at].renders.is_ok())
.collect()
};
considered
.into_iter()
.filter_map(|at| {
records[at]
.decodes
.as_ref()
.err()
.map(|reason| format!("{}: {reason}", records[at].name))
})
.collect::<Vec<_>>()
.join("; ")
}
#[derive(Clone, Debug, Default)]
pub struct EncodeAdapterNotes {
pub no_device: Option<String>,
pub elsewhere: Vec<(NativeVideoCodec, String)>,
}
impl EncodeAdapterNotes {
pub fn explain(&self, codec: NativeVideoCodec, reason: String) -> String {
match self.elsewhere.iter().find(|(other, _)| *other == codec) {
Some((_, hint)) => format!("{reason}; {hint}"),
None => reason,
}
}
}
#[cfg(test)]
#[path = "adapter_choice_tests.rs"]
mod tests;