Skip to main content

alpha2_gpu/
adapter_choice.rs

1//! Which adapter a host renders on when a machine has several. The same rule applies on every
2//! OS and is decided once at host start.
3//!
4//! Under `auto` the adapters that render are ranked by:
5//! 1. how many of H.264, HEVC and AV1 the adapter encodes on the GPU,
6//! 2. whether it decodes on the GPU, when the decode policy allows it,
7//! 3. the high-performance order of the records.
8//!
9//! Capability leads because neither GPU of a hybrid machine is faster for every project: the
10//! discrete GPU renders heavy compositions faster, while an integrated GPU with a video encoder
11//! exports video-led projects faster at a fraction of the host CPU. `performance` takes the first
12//! hardware adapter in high-performance order. A number or name pins one adapter, and a pin that
13//! matches nothing is refused. Software adapters come last under every policy except a pin.
14//!
15//! The choice is a pure function over per-adapter records, so every policy is testable without a
16//! device.
17
18use crate::AdapterPolicy;
19
20/// The codecs a GPU encoder is asked about, in the order a report lists them.
21pub const GPU_ENCODE_CODECS: [&str; 3] = ["h264", "hevc", "av1"];
22
23/// What one adapter was found to do. Checks that were not made are `Err`, so the choice never
24/// assumes a capability.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct AdapterRecord {
27    /// Its position in the backend's enumeration, which an index pin names.
28    pub index: usize,
29    /// The adapter's name, which a name pin matches.
30    pub name: String,
31    /// `dx12`, `vulkan` or `metal`.
32    pub backend: &'static str,
33    /// The DXGI adapter LUID, where the backend has one.
34    pub luid: Option<u64>,
35    /// WARP, lavapipe, llvmpipe or another adapter the backend flags as software.
36    pub software: bool,
37    /// Whether a device opens on it, or why not.
38    pub renders: Result<(), String>,
39    /// Whether hardware decode imports into its device, or why not.
40    pub decodes: Result<(), String>,
41    /// Per codec asked about, whether the device encodes it on the GPU, or why not.
42    pub gpu_encodes: Vec<(&'static str, Result<(), String>)>,
43}
44
45impl AdapterRecord {
46    /// A record of an adapter nothing has been asked of yet.
47    #[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    /// Whether the device encodes `codec` on the GPU.
62    #[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    /// The codecs the device encodes on the GPU.
70    #[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    /// Why the device encodes no codec on the GPU: the first refusal, or that none was asked.
80    #[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    /// `[index] name`, with a note for a software adapter.
98    #[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    /// One line describing this adapter, for the start diagnostic of a passed-over adapter.
113    #[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/// Why the chosen adapter was taken.
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub enum ChoiceReason {
138    /// `ALPHA2_GPU_ADAPTER` names it.
139    Pinned,
140    /// It is the only hardware adapter that renders, or the only adapter at all.
141    OnlyAdapter,
142    /// It encodes more codecs on the GPU than the next one.
143    MostGpuEncoders,
144    /// It encodes as many codecs as the next one, and decodes on the GPU where that one does not.
145    Decodes,
146    /// Nothing else tells it from the next one: it comes first in the high-performance order.
147    HighPerformanceOrder,
148    /// `ALPHA2_GPU_ADAPTER=performance`: the first hardware adapter in the high-performance order.
149    PerformancePolicy,
150    /// No adapter could share a device with the video codecs, so decode and encode run without
151    /// the GPU.
152    NoSharedDevice,
153}
154
155impl ChoiceReason {
156    /// The reason as a capability report names it.
157    #[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/// The adapters a policy takes, best first, and why the first one leads.
172#[derive(Clone, Debug, Eq, PartialEq)]
173pub struct AdapterOrder {
174    /// Positions in the records: every adapter that renders and that the policy accepts.
175    pub order: Vec<usize>,
176    /// Why the first leads.
177    pub reason: ChoiceReason,
178}
179
180/// The adapters `policy` accepts among `records` (in high-performance order), best first.
181/// `decode_allowed` says whether the decode policy allows GPU decoding. Software adapters always
182/// rank after hardware ones.
183///
184/// # Errors
185/// Returns why no adapter can be taken: none renders, or the pin matches no rendering adapter.
186pub fn rank_adapters(
187    records: &[AdapterRecord],
188    policy: &AdapterPolicy,
189    decode_allowed: bool,
190) -> Result<AdapterOrder, String> {
191    // Hardware adapters in the order given, then software ones.
192    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    // Stable, so ties keep the high-performance order.
252    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        // A software adapter is no alternative to a hardware one.
267        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/// One sentence explaining why `chosen` was taken, naming the runner-up `next` where relevant.
296#[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
355/// `[0] name, [1] name, ...`.
356fn 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/// For each codec `chosen` does not encode on the GPU, another hardware adapter that does and
368/// the `ALPHA2_GPU_ADAPTER` setting that selects it.
369#[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/// Why `chosen` cannot decode, from the records of every hardware adapter considered.
398#[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/// What a host's adapter choice was, as its capabilities and failure reports state it.
422#[derive(Clone, Debug, PartialEq, Eq)]
423pub struct AdapterChoiceReport {
424    /// The `ALPHA2_GPU_ADAPTER` policy the choice was made under.
425    pub policy: AdapterPolicy,
426    /// The renderer's adapter in `records`; `None` for a device from elsewhere.
427    pub chosen: Option<usize>,
428    /// Why the renderer is on that adapter.
429    pub reason: ChoiceReason,
430    /// One sentence naming the chosen adapter, and the runner-up where the reason compares them.
431    pub explanation: String,
432    /// Every adapter the choice considered, in the high-performance order.
433    pub records: Vec<AdapterRecord>,
434}
435
436impl AdapterChoiceReport {
437    /// The report for `records` ranked into `order` under `policy`, taking the first.
438    #[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    /// The record of the adapter the renderer is on.
467    #[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;