1use crate::AdapterPolicy;
19
20pub const GPU_ENCODE_CODECS: [&str; 3] = ["h264", "hevc", "av1"];
22
23#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct AdapterRecord {
27 pub index: usize,
29 pub name: String,
31 pub backend: &'static str,
33 pub luid: Option<u64>,
35 pub software: bool,
37 pub renders: Result<(), String>,
39 pub decodes: Result<(), String>,
41 pub gpu_encodes: Vec<(&'static str, Result<(), String>)>,
43}
44
45impl AdapterRecord {
46 #[must_use]
48 pub fn unasked(index: usize, name: String, backend: &'static str, software: bool) -> Self {
49 Self {
50 index,
51 name,
52 backend,
53 luid: None,
54 software,
55 renders: Err("not opened".to_owned()),
56 decodes: Err("not asked".to_owned()),
57 gpu_encodes: Vec::new(),
58 }
59 }
60
61 #[must_use]
63 pub fn encodes(&self, codec: &str) -> bool {
64 self.gpu_encodes
65 .iter()
66 .any(|(asked, answer)| *asked == codec && answer.is_ok())
67 }
68
69 #[must_use]
71 pub fn encoded_codecs(&self) -> Vec<&'static str> {
72 self.gpu_encodes
73 .iter()
74 .filter(|(_, answer)| answer.is_ok())
75 .map(|(codec, _)| *codec)
76 .collect()
77 }
78
79 #[must_use]
81 pub fn gpu_encode_reason(&self) -> Option<String> {
82 if !self.encoded_codecs().is_empty() {
83 return None;
84 }
85 Some(
86 match self
87 .gpu_encodes
88 .iter()
89 .find_map(|(_, answer)| answer.as_ref().err())
90 {
91 Some(reason) => reason.clone(),
92 None => "not asked".to_owned(),
93 },
94 )
95 }
96
97 #[must_use]
99 pub fn label(&self) -> String {
100 format!(
101 "[{}] {}{}",
102 self.index,
103 self.name,
104 if self.software {
105 " (a software adapter)"
106 } else {
107 ""
108 }
109 )
110 }
111
112 #[must_use]
114 pub fn summary(&self) -> String {
115 let label = self.label();
116 if let Err(reason) = &self.renders {
117 return format!("{label}: does not render ({reason})");
118 }
119 let decodes = match &self.decodes {
120 Ok(()) => "yes".to_owned(),
121 Err(reason) => format!("no ({reason})"),
122 };
123 let encodes = match self.gpu_encode_reason() {
124 None => self.encoded_codecs().join(","),
125 Some(reason) => format!("none ({reason})"),
126 };
127 format!("{label}: renders, decodes: {decodes}, encodes on the GPU: {encodes}")
128 }
129
130 fn renders_in_hardware(&self) -> bool {
131 self.renders.is_ok() && !self.software
132 }
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub enum ChoiceReason {
138 Pinned,
140 OnlyAdapter,
142 MostGpuEncoders,
144 Decodes,
146 HighPerformanceOrder,
148 PerformancePolicy,
150 NoSharedDevice,
153}
154
155impl ChoiceReason {
156 #[must_use]
158 pub const fn as_str(self) -> &'static str {
159 match self {
160 Self::Pinned => "pinned",
161 Self::OnlyAdapter => "only-adapter",
162 Self::MostGpuEncoders => "most-gpu-encoders",
163 Self::Decodes => "decodes",
164 Self::HighPerformanceOrder => "high-performance-order",
165 Self::PerformancePolicy => "performance-policy",
166 Self::NoSharedDevice => "no-shared-device",
167 }
168 }
169}
170
171#[derive(Clone, Debug, Eq, PartialEq)]
173pub struct AdapterOrder {
174 pub order: Vec<usize>,
176 pub reason: ChoiceReason,
178}
179
180pub fn rank_adapters(
187 records: &[AdapterRecord],
188 policy: &AdapterPolicy,
189 decode_allowed: bool,
190) -> Result<AdapterOrder, String> {
191 let considered: Vec<usize> = (0..records.len())
193 .filter(|&at| !records[at].software)
194 .chain((0..records.len()).filter(|&at| records[at].software))
195 .collect();
196 if policy.is_pin() {
197 let named: Vec<usize> = considered
198 .iter()
199 .copied()
200 .filter(|&at| policy.names(records[at].index, &records[at].name))
201 .collect();
202 if named.is_empty() {
203 return Err(format!(
204 "{} matches no adapter; the adapters are {}",
205 policy.setting(),
206 adapter_list(records)
207 ));
208 }
209 let order: Vec<usize> = named
210 .iter()
211 .copied()
212 .filter(|&at| records[at].renders.is_ok())
213 .collect();
214 if order.is_empty() {
215 return Err(format!(
216 "{} names {}, which does not render",
217 policy.setting(),
218 named
219 .iter()
220 .map(|&at| records[at].summary())
221 .collect::<Vec<_>>()
222 .join("; ")
223 ));
224 }
225 return Ok(AdapterOrder {
226 order,
227 reason: ChoiceReason::Pinned,
228 });
229 }
230 let mut order: Vec<usize> = considered
231 .into_iter()
232 .filter(|&at| records[at].renders.is_ok())
233 .collect();
234 if order.is_empty() {
235 return Err(format!(
236 "no adapter renders: {}",
237 records
238 .iter()
239 .map(AdapterRecord::summary)
240 .collect::<Vec<_>>()
241 .join("; ")
242 ));
243 }
244 if *policy == AdapterPolicy::Performance {
245 return Ok(AdapterOrder {
246 order,
247 reason: ChoiceReason::PerformancePolicy,
248 });
249 }
250 let decodes = |at: usize| decode_allowed && records[at].decodes.is_ok();
251 order.sort_by_key(|&at| {
253 let record = &records[at];
254 let hardware = !record.software;
255 (
256 !hardware,
257 std::cmp::Reverse(if hardware {
258 record.encoded_codecs().len()
259 } else {
260 0
261 }),
262 !(hardware && decodes(at)),
263 )
264 });
265 let reason = match order.get(1) {
266 None => ChoiceReason::OnlyAdapter,
268 Some(&next) if !records[order[0]].software && records[next].software => {
269 ChoiceReason::OnlyAdapter
270 }
271 Some(&next) => {
272 let (first, second) = (&records[order[0]], &records[next]);
273 let encoders = |record: &AdapterRecord| {
274 if record.software {
275 0
276 } else {
277 record.encoded_codecs().len()
278 }
279 };
280 if encoders(first) > encoders(second) {
281 ChoiceReason::MostGpuEncoders
282 } else if first.renders_in_hardware()
283 && decodes(order[0])
284 && !(second.renders_in_hardware() && decodes(next))
285 {
286 ChoiceReason::Decodes
287 } else {
288 ChoiceReason::HighPerformanceOrder
289 }
290 }
291 };
292 Ok(AdapterOrder { order, reason })
293}
294
295#[must_use]
297pub fn explain_choice(
298 records: &[AdapterRecord],
299 chosen: usize,
300 next: Option<usize>,
301 reason: ChoiceReason,
302 policy: &AdapterPolicy,
303) -> String {
304 let Some(record) = records.get(chosen) else {
305 return "no adapter was chosen".to_owned();
306 };
307 let encodes = |record: &AdapterRecord| match record.gpu_encode_reason() {
308 None => format!("encodes {} on the GPU", record.encoded_codecs().join(",")),
309 Some(why) => format!("encodes nothing on the GPU ({why})"),
310 };
311 let next = next.and_then(|at| records.get(at));
312 match (reason, next) {
313 (ChoiceReason::Pinned, _) => format!("{} names {}", policy.setting(), record.name),
314 (ChoiceReason::OnlyAdapter, _) => format!(
315 "{} is the only {}adapter that renders",
316 record.name,
317 if record.software { "" } else { "hardware " }
318 ),
319 (ChoiceReason::PerformancePolicy, _) if record.software => format!(
320 "{} renders in software: no hardware adapter renders here",
321 record.name
322 ),
323 (ChoiceReason::PerformancePolicy, _) => format!(
324 "{} is the first hardware adapter in the high-performance order",
325 record.name
326 ),
327 (ChoiceReason::NoSharedDevice, _) => format!(
328 "{} renders without a device shared with the video decoder and encoder",
329 record.name
330 ),
331 (ChoiceReason::MostGpuEncoders, Some(next)) => format!(
332 "{} {}, more codecs than {}, which {}",
333 record.name,
334 encodes(record),
335 next.name,
336 encodes(next)
337 ),
338 (ChoiceReason::Decodes, Some(next)) => format!(
339 "{} decodes on the GPU and {} does not ({}); both encode as many codecs on the GPU",
340 record.name,
341 next.name,
342 next.decodes
343 .as_ref()
344 .err()
345 .map_or("decode is in software", String::as_str)
346 ),
347 (ChoiceReason::HighPerformanceOrder, Some(next)) => format!(
348 "{} comes before {} in the high-performance order, and does no less on the GPU",
349 record.name, next.name
350 ),
351 (_, None) => format!("{} is the only adapter that renders", record.name),
352 }
353}
354
355fn adapter_list(records: &[AdapterRecord]) -> String {
357 if records.is_empty() {
358 return "none".to_owned();
359 }
360 records
361 .iter()
362 .map(|record| format!("[{}] {}", record.index, record.name))
363 .collect::<Vec<_>>()
364 .join(", ")
365}
366
367#[must_use]
370pub fn encode_elsewhere(records: &[AdapterRecord], chosen: usize) -> Vec<(&'static str, String)> {
371 let Some(current) = records.get(chosen) else {
372 return Vec::new();
373 };
374 GPU_ENCODE_CODECS
375 .iter()
376 .filter(|codec| !current.encodes(codec))
377 .filter_map(|&codec| {
378 let other = records
379 .iter()
380 .enumerate()
381 .find(|(at, record)| {
382 *at != chosen && record.renders_in_hardware() && record.encodes(codec)
383 })
384 .map(|(_, record)| record)?;
385 Some((
386 codec,
387 format!(
388 "{} encodes {codec} on the GPU ({} renders there)",
389 other.name,
390 AdapterPolicy::Index(other.index).setting()
391 ),
392 ))
393 })
394 .collect()
395}
396
397#[must_use]
399pub fn decode_refusals(records: &[AdapterRecord], chosen: usize, pinned: bool) -> String {
400 let considered: Vec<&AdapterRecord> = if pinned {
401 records.get(chosen).into_iter().collect()
402 } else {
403 records
404 .iter()
405 .filter(|record| record.renders_in_hardware())
406 .collect()
407 };
408 considered
409 .into_iter()
410 .filter_map(|record| {
411 record
412 .decodes
413 .as_ref()
414 .err()
415 .map(|reason| format!("{}: {reason}", record.name))
416 })
417 .collect::<Vec<_>>()
418 .join("; ")
419}
420
421#[derive(Clone, Debug, PartialEq, Eq)]
423pub struct AdapterChoiceReport {
424 pub policy: AdapterPolicy,
426 pub chosen: Option<usize>,
428 pub reason: ChoiceReason,
430 pub explanation: String,
432 pub records: Vec<AdapterRecord>,
434}
435
436impl AdapterChoiceReport {
437 #[must_use]
439 pub fn ranked(
440 policy: AdapterPolicy,
441 records: Vec<AdapterRecord>,
442 order: &AdapterOrder,
443 ) -> Self {
444 let chosen = order.order.first().copied();
445 let explanation = chosen.map_or_else(
446 || "no adapter was chosen".to_owned(),
447 |chosen| {
448 explain_choice(
449 &records,
450 chosen,
451 order.order.get(1).copied(),
452 order.reason,
453 &policy,
454 )
455 },
456 );
457 Self {
458 policy,
459 chosen,
460 reason: order.reason,
461 explanation,
462 records,
463 }
464 }
465
466 #[must_use]
468 pub fn chosen_record(&self) -> Option<&AdapterRecord> {
469 self.chosen.and_then(|at| self.records.get(at))
470 }
471}
472
473#[cfg(test)]
474#[path = "adapter_choice_tests.rs"]
475mod tests;