Skip to main content

alpha2_native/
capabilities.rs

1//! Codec capability discovery and encoder selection.
2
3use std::collections::HashMap;
4use std::ffi::CString;
5use std::sync::{LazyLock, Mutex};
6#[cfg(target_os = "linux")]
7use std::{fs, path::Path};
8
9use crate::convert::{encode_adapter_error, reject_unused_encode_options, video_encoder_options};
10use crate::video_encode::VideoEncodeConfig;
11use alpha2_media::{ColorMetadata, EncodeError};
12use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
13use rsmpeg::avutil::AVRational;
14use rsmpeg::error::RsmpegError;
15use rsmpeg::ffi;
16
17/// Configuration for transactional audio/video export.
18#[derive(Copy, Clone, Debug, PartialEq)]
19pub struct AvEncodeConfig {
20    /// Video encoder settings.
21    pub video: VideoEncodeConfig,
22    /// Project audio sample rate.
23    pub audio_sample_rate: u32,
24    /// Audio channel count.
25    pub audio_channels: u16,
26    /// Audio codec selected by the downstream host.
27    pub audio_codec: NativeAudioCodec,
28    /// Resolved audio bitrate in bits per second; the host resolves the level.
29    pub audio_bit_rate: u32,
30}
31
32/// Video codecs implemented by the native encode sinks.
33#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
34#[non_exhaustive]
35pub enum NativeVideoCodec {
36    /// H.264/AVC, preferred for broad browser, Windows, and Apple playback.
37    H264,
38    /// H.265/HEVC.
39    Hevc,
40    /// VP9, when the linked `FFmpeg` build provides a VP9 encoder.
41    Vp9,
42    /// AV1, when the linked `FFmpeg` build provides an AV1 encoder.
43    Av1,
44    /// MPEG-4 Part 2, retained for compatibility with existing alpha2 hosts.
45    Mpeg4Part2,
46}
47
48impl NativeVideoCodec {
49    /// Stable identifier suitable for an application protocol or settings file.
50    #[must_use]
51    pub const fn as_str(self) -> &'static str {
52        match self {
53            Self::H264 => "h264",
54            Self::Hevc => "hevc",
55            Self::Vp9 => "vp9",
56            Self::Av1 => "av1",
57            Self::Mpeg4Part2 => "mpeg4-part2",
58        }
59    }
60
61    /// The engine's codec this encoder family implements.
62    ///
63    /// The rate mapping is keyed on [`alpha2_media::VideoCodec`] alone, so
64    /// there is one front door to it; this is the native side's way in.
65    #[must_use]
66    pub const fn model(self) -> alpha2_media::VideoCodec {
67        match self {
68            Self::H264 => alpha2_media::VideoCodec::H264,
69            Self::Hevc => alpha2_media::VideoCodec::Hevc,
70            Self::Vp9 => alpha2_media::VideoCodec::Vp9,
71            Self::Av1 => alpha2_media::VideoCodec::Av1,
72            Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
73        }
74    }
75
76    /// The native codec for the engine's `codec`.
77    #[must_use]
78    pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
79        match codec {
80            alpha2_media::VideoCodec::H264 => Self::H264,
81            alpha2_media::VideoCodec::Hevc => Self::Hevc,
82            alpha2_media::VideoCodec::Vp9 => Self::Vp9,
83            alpha2_media::VideoCodec::Av1 => Self::Av1,
84            alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
85        }
86    }
87
88    pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
89        match self {
90            Self::H264 => ffi::AV_CODEC_ID_H264,
91            Self::Hevc => ffi::AV_CODEC_ID_HEVC,
92            Self::Vp9 => ffi::AV_CODEC_ID_VP9,
93            Self::Av1 => ffi::AV_CODEC_ID_AV1,
94            Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
95        }
96    }
97}
98
99/// Audio codecs implemented by the native encode sinks.
100#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
101#[non_exhaustive]
102pub enum NativeAudioCodec {
103    /// AAC-LC compatible audio, through `FFmpeg`'s `aac`.
104    Aac,
105    /// Opus, through `libopus`.
106    Opus,
107}
108
109impl NativeAudioCodec {
110    /// Every codec, in the order the engine lists them.
111    pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
112
113    /// Stable identifier suitable for an application protocol or settings file.
114    #[must_use]
115    pub const fn as_str(self) -> &'static str {
116        match self {
117            Self::Aac => "aac",
118            Self::Opus => "opus",
119        }
120    }
121
122    /// The engine's codec this encoder family implements.
123    #[must_use]
124    pub const fn model(self) -> alpha2_media::AudioCodec {
125        match self {
126            Self::Aac => alpha2_media::AudioCodec::Aac,
127            Self::Opus => alpha2_media::AudioCodec::Opus,
128        }
129    }
130
131    /// The native codec for the engine's `codec`.
132    #[must_use]
133    pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
134        match codec {
135            alpha2_media::AudioCodec::Aac => Self::Aac,
136            alpha2_media::AudioCodec::Opus => Self::Opus,
137        }
138    }
139
140    /// The one encoder alpha2 opens for the codec: `FFmpeg`'s `aac`, and the
141    /// reference `libopus` for Opus. `FFmpeg`'s own `opus` encoder is
142    /// experimental and is never a candidate.
143    pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
144        match self {
145            Self::Aac => c"aac",
146            Self::Opus => c"libopus",
147        }
148    }
149}
150
151/// The encoder this runtime opens for `codec`, or `None` when it links none,
152/// or only one marked experimental.
153pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
154    AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
155        u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
156    })
157}
158
159/// How a concrete encoder executes, as the linked runtime declares it.
160///
161/// Taken from capability bits rather than the name suffix:
162/// `AV_CODEC_CAP_HARDWARE` and `AV_CODEC_CAP_HYBRID` are set by the encoder
163/// itself, so a build that adds an encoder alpha2 has never heard of is still
164/// classified correctly, and a hybrid encoder is not described as though it
165/// were one or the other.
166/// The one exception is a short list of encoders the pinned runtime
167/// misdeclares (`declared_encoder_class`).
168#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
169#[non_exhaustive]
170pub enum NativeEncoderClass {
171    /// Runs on the CPU and draws on the software codec thread budget.
172    Software,
173    /// Runs on a hardware engine and ignores the thread budget.
174    Hardware,
175    /// Can do either. `MediaFoundation`'s `_mf` encoders are the shipped case:
176    /// `hw_encoding=0` enumerates a synchronous software transform and
177    /// `hw_encoding=1` an asynchronous hardware one.
178    Hybrid,
179}
180
181impl NativeEncoderClass {
182    /// Stable identifier for diagnostics and the export result.
183    #[must_use]
184    pub const fn as_str(self) -> &'static str {
185        match self {
186            Self::Software => "software",
187            Self::Hardware => "hardware",
188            Self::Hybrid => "hybrid",
189        }
190    }
191
192    /// The ways alpha2 runs an encoder of this class, most preferred first:
193    /// `true` on the GPU, `false` on the CPU. A hybrid encoder runs on the
194    /// CPU unless `hybrid_hardware` asks for the GPU, and then still has the
195    /// CPU behind it, because a machine without a hardware transform cannot
196    /// run the GPU mode at all.
197    #[must_use]
198    pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
199        match self {
200            Self::Hardware => &[true],
201            Self::Hybrid if hybrid_hardware => &[true, false],
202            Self::Software | Self::Hybrid => &[false],
203        }
204    }
205}
206
207/// One way to run a registered encoder: the encoder, and whether alpha2
208/// configures it to encode on the GPU. Only a hybrid encoder has two.
209pub(crate) struct VideoEncoderCandidate {
210    pub(crate) encoder: AVCodecRef<'static>,
211    pub(crate) on_hardware: bool,
212}
213
214impl VideoEncoderCandidate {
215    pub(crate) fn name(&self) -> String {
216        self.encoder.name().to_string_lossy().into_owned()
217    }
218}
219
220/// Whether hybrid encoders are configured to run on the GPU.
221///
222/// `h264_mf` and its siblings default to `hw_encoding=0`, which enumerates
223/// `MFT_ENUM_FLAG_SYNCMFT` (the synchronous *software* transform category),
224/// so the shipped default is a CPU encoder that correctly draws on the codec
225/// thread budget. `ALPHA2_VIDEO_ENCODER_HYBRID=hardware` tries `hw_encoding=1`
226/// first, which asks `MediaFoundation` for a hardware MFT, and keeps the
227/// software transform of the same encoder behind it for the SKUs and VMs
228/// that have none: `FFmpeg` does not fall back from one to the other itself.
229#[must_use]
230pub fn hybrid_encoders_use_hardware() -> bool {
231    std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
232        .is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
233}
234
235/// Why one candidate encoder was not the one an export opened.
236#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
237#[non_exhaustive]
238pub enum NativeEncoderRejection {
239    /// The encoder refuses an output this small.
240    Dimensions,
241    /// `avcodec_open2` failed: no device, no licence, unsupported profile.
242    OpenFailed,
243    /// The encoder silently ignored an option alpha2 requires it to honour.
244    OptionsIgnored,
245}
246
247impl NativeEncoderRejection {
248    /// Stable identifier for diagnostics and the export result.
249    #[must_use]
250    pub const fn as_str(self) -> &'static str {
251        match self {
252            Self::Dimensions => "dimensions",
253            Self::OpenFailed => "open-failed",
254            Self::OptionsIgnored => "options-ignored",
255        }
256    }
257}
258
259/// One candidate encoder an export tried and moved past.
260#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct NativeVideoEncoderFallback {
262    /// `FFmpeg` short name of the candidate.
263    pub encoder_name: String,
264    /// Whether the candidate was configured to encode on the GPU. A hybrid
265    /// encoder can be tried on the GPU and then on the CPU.
266    pub hardware_accelerated: bool,
267    /// Typed reason it was rejected.
268    pub reason: NativeEncoderRejection,
269    /// The runtime's own message, kept verbatim.
270    pub detail: String,
271}
272
273/// The encoder an export actually opened, and what it passed over.
274///
275/// Advertisement is not instantiation: a platform can register an encoder it
276/// cannot open on this machine. Every export reports what it opened rather
277/// than letting the capability list imply it.
278#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct NativeVideoEncoderSelection {
280    /// `FFmpeg` short name of the encoder that opened.
281    pub encoder_name: String,
282    /// How that encoder executes.
283    pub class: NativeEncoderClass,
284    /// Whether it is configured to encode on the GPU.
285    pub hardware_accelerated: bool,
286    /// Candidates tried first, in preference order.
287    pub rejected: Vec<NativeVideoEncoderFallback>,
288    /// Whether the encoder runs the rate control's quantizer or its bitrate.
289    pub rate_control: crate::AppliedRateControl,
290    /// Why a quantizer that was asked for is not the one running.
291    pub quantizer_refused: Option<String>,
292    /// `FFmpeg` workers the encoder was opened with when it runs on the CPU;
293    /// `None` on the GPU, which takes no share of the software budget.
294    pub software_threads: Option<i64>,
295}
296
297/// One linked video encoder available to the native sinks.
298#[derive(Clone, Debug, Eq, PartialEq)]
299pub struct NativeVideoEncoderCapability {
300    /// Stable logical codec requested through [`VideoEncodeConfig`].
301    pub codec: NativeVideoCodec,
302    /// Concrete `FFmpeg` encoder implementation for this codec.
303    pub encoder_name: String,
304    /// How the linked runtime declares this encoder executes.
305    pub class: NativeEncoderClass,
306    /// Whether this entry runs the encoder on the GPU. A hybrid encoder is
307    /// listed once per way it runs here ([`hybrid_encoders_use_hardware`]).
308    pub hardware_accelerated: bool,
309    /// Whether this is the default encoder for its logical codec.
310    pub preferred: bool,
311    /// Smallest even width accepted by this concrete encoder.
312    pub minimum_width: u32,
313    /// Smallest even height accepted by this concrete encoder.
314    pub minimum_height: u32,
315}
316
317/// One linked audio encoder available to the native sinks.
318#[derive(Clone, Debug, Eq, PartialEq)]
319pub struct NativeAudioEncoderCapability {
320    /// Stable logical codec requested through [`AvEncodeConfig`].
321    pub codec: NativeAudioCodec,
322    /// Concrete `FFmpeg` encoder selected for that codec on this host.
323    pub encoder_name: String,
324}
325
326/// Runtime codec inventory for the native sinks.
327///
328/// Which containers offer which codec is the engine's policy
329/// ([`alpha2_media::Container`]), the same on every runtime; this says which
330/// of those codecs this runtime can encode.
331#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct NativeCodecCapabilities {
333    /// Video encoders accepted by [`NativeVideoEncodeSink`](crate::NativeVideoEncodeSink) and
334    /// [`NativeAvEncodeSink`](crate::NativeAvEncodeSink).
335    pub video: Vec<NativeVideoEncoderCapability>,
336    /// Audio encoders accepted by [`NativeAvEncodeSink`](crate::NativeAvEncodeSink) and the
337    /// GPU-resident and packet-copy exports.
338    pub audio: Vec<NativeAudioEncoderCapability>,
339}
340
341/// Reports the linked `FFmpeg` encoders usable by alpha2's native sinks.
342///
343/// A logical codec may have multiple entries, and a hybrid encoder one per
344/// way it runs. Hardware entries appear first when they accept the native
345/// YUV420P path and their required device is present; exactly one entry per
346/// codec is marked preferred.
347#[must_use]
348pub fn codec_capabilities() -> NativeCodecCapabilities {
349    let video = [
350        NativeVideoCodec::H264,
351        NativeVideoCodec::Hevc,
352        NativeVideoCodec::Vp9,
353        NativeVideoCodec::Av1,
354        NativeVideoCodec::Mpeg4Part2,
355    ]
356    .into_iter()
357    .flat_map(|codec| {
358        registered_video_encoders(codec)
359            .into_iter()
360            .filter(encoder_is_compatible)
361            .enumerate()
362            .map(move |(index, candidate)| {
363                let encoder_name = candidate.name();
364                let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
365                NativeVideoEncoderCapability {
366                    codec,
367                    class: encoder_class(&candidate.encoder),
368                    hardware_accelerated: candidate.on_hardware,
369                    preferred: index == 0,
370                    encoder_name,
371                    minimum_width,
372                    minimum_height,
373                }
374            })
375    })
376    .collect();
377    let audio = NativeAudioCodec::ALL
378        .into_iter()
379        .filter_map(|codec| {
380            let encoder = audio_encoder(codec)?;
381            Some(NativeAudioEncoderCapability {
382                codec,
383                encoder_name: encoder.name().to_string_lossy().into_owned(),
384            })
385        })
386        .collect();
387    NativeCodecCapabilities { video, audio }
388}
389
390/// One codec's hardware decode registration for this platform's backend.
391#[derive(Clone, Debug, Eq, PartialEq)]
392pub struct NativeHardwareDecodeCapability {
393    /// Logical codec.
394    pub codec: NativeVideoCodec,
395    /// Concrete `FFmpeg` decoder implementation, when one is linked.
396    pub decoder_name: Option<String>,
397    /// Whether that decoder advertises this platform's hardware configuration.
398    ///
399    /// This is registration only. Whether a device, profile, and surface
400    /// format actually work is decided when a stream is opened, and every
401    /// export reports which path its frames took.
402    pub hardware_config: bool,
403    /// The decoder a software session opens for the codec, or `None` when
404    /// only a hardware session can decode it (AV1 on a runtime without
405    /// libdav1d).
406    pub software_decoder: Option<String>,
407}
408
409/// Hardware decode registration reported by the linked `FFmpeg` build.
410#[derive(Clone, Debug, Eq, PartialEq)]
411pub struct NativeHardwareDecodeCapabilities {
412    /// Stable backend identifier, or `none` on a build without a GPU path.
413    pub backend: &'static str,
414    /// One entry per codec alpha2 can present from a decoder surface.
415    pub codecs: Vec<NativeHardwareDecodeCapability>,
416}
417
418/// Backend this build decodes with, independent of any device being present.
419#[must_use]
420pub const fn hardware_decode_backend() -> &'static str {
421    #[cfg(feature = "hwdecode")]
422    {
423        #[cfg(target_os = "linux")]
424        {
425            "vulkan"
426        }
427        #[cfg(target_os = "macos")]
428        {
429            "videotoolbox"
430        }
431        #[cfg(target_os = "windows")]
432        {
433            "d3d12va"
434        }
435        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
436        {
437            "none"
438        }
439    }
440    #[cfg(not(feature = "hwdecode"))]
441    {
442        "none"
443    }
444}
445
446/// Reports which codecs the linked `FFmpeg` registers for this backend.
447#[must_use]
448pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
449    let codecs = [
450        NativeVideoCodec::H264,
451        NativeVideoCodec::Hevc,
452        NativeVideoCodec::Av1,
453        NativeVideoCodec::Vp9,
454    ]
455    .into_iter()
456    .map(|codec| {
457        let decoder = AVCodec::find_decoder(codec.codec_id());
458        NativeHardwareDecodeCapability {
459            codec,
460            decoder_name: decoder
461                .as_ref()
462                .map(|decoder| decoder.name().to_string_lossy().into_owned()),
463            hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
464                .any(|decoder| decoder_has_hardware_config(&decoder)),
465            software_decoder: crate::decode::software_video_decoder(codec.codec_id())
466                .ok()
467                .map(|decoder| decoder.name().to_string_lossy().into_owned()),
468        }
469    })
470    .collect();
471    NativeHardwareDecodeCapabilities {
472        backend: hardware_decode_backend(),
473        codecs,
474    }
475}
476
477/// Whether `decoder` advertises this platform's hardware device configuration.
478#[allow(unused_variables)]
479fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
480    #[cfg(all(
481        any(target_os = "linux", target_os = "macos", target_os = "windows"),
482        feature = "hwdecode"
483    ))]
484    {
485        #[cfg(target_os = "linux")]
486        let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
487        #[cfg(target_os = "macos")]
488        let (device_type, pixel_format) = (
489            ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
490            ffi::AV_PIX_FMT_VIDEOTOOLBOX,
491        );
492        #[cfg(target_os = "windows")]
493        let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
494        (0..)
495            .map_while(|index| decoder.hw_config(index))
496            .any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
497    }
498    #[cfg(not(all(
499        any(target_os = "linux", target_os = "macos", target_os = "windows"),
500        feature = "hwdecode"
501    )))]
502    {
503        false
504    }
505}
506
507/// Test builds only: the encoders `ALPHA2_TEST_SKIP_ENCODERS` (names, comma separated) hides
508/// from this run, as though the runtime registered none of them. The valgrind run sets it for
509/// `libsvtav1`, whose own code reads memory it never wrote; no production build reads it.
510#[cfg(test)]
511pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
512    static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
513        std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
514            .unwrap_or_default()
515            .split(',')
516            .map(str::trim)
517            .filter(|name| !name.is_empty())
518            .map(str::to_owned)
519            .collect()
520    });
521    let name = name.to_string_lossy();
522    SKIPPED.iter().any(|skipped| *skipped == name)
523}
524
525#[cfg(not(test))]
526const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
527    false
528}
529
530/// Every way to run each encoder registered for `codec`, GPU ones first.
531pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
532    let hybrid_hardware = hybrid_encoders_use_hardware();
533    let mut candidates = AVCodec::iterate()
534        .filter(|encoder| {
535            encoder.id == codec.codec_id()
536                && !test_skips_encoder(encoder.name())
537                && AVCodec::find_encoder_by_name(encoder.name())
538                    .is_some_and(|found| found.as_ptr() == encoder.as_ptr())
539        })
540        .flat_map(|encoder| {
541            encoder_class(&encoder)
542                .modes(hybrid_hardware)
543                .iter()
544                .filter_map(move |&on_hardware| {
545                    AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
546                        VideoEncoderCandidate {
547                            encoder,
548                            on_hardware,
549                        }
550                    })
551                })
552        })
553        .collect::<Vec<_>>();
554    // Stable sort: FFmpeg's registration order within each tier, hardware first.
555    candidates.sort_by_key(|candidate| !candidate.on_hardware);
556    candidates
557}
558
559/// Classifies `encoder` from the capability bits it declares
560/// ([`declared_encoder_class`]).
561pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
562    let name = encoder.name().to_string_lossy();
563    declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
564}
565
566/// Encoders the pinned runtime registers as software although they run only
567/// on a hardware engine.
568///
569/// `FFmpeg` 8.0 and 8.1 declare `h264_videotoolbox` with neither
570/// `AV_CODEC_CAP_HARDWARE` nor `AV_CODEC_CAP_HYBRID`, unlike
571/// `hevc_videotoolbox`, yet opens it with `allow_sw=0`
572/// (`RequireHardwareAcceleratedVideoEncoder`), so it runs on the Mac's
573/// hardware encoder or not at all. An entry applies only while the runtime
574/// declares neither bit: a runtime that declares one is taken at its word,
575/// and the entry can go once no supported runtime needs it.
576const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
577
578/// The class of the encoder `name` that declares `capabilities`: the bits it
579/// declares, except that an encoder in [`MISDECLARED_HARDWARE_ENCODERS`]
580/// declaring neither is `Hardware`.
581pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
582    if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
583        NativeEncoderClass::Hybrid
584    } else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
585        || MISDECLARED_HARDWARE_ENCODERS.contains(&name)
586    {
587        NativeEncoderClass::Hardware
588    } else {
589        NativeEncoderClass::Software
590    }
591}
592
593pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
594    let name = candidate.name();
595    registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
596}
597
598/// What the registration alone rules out: an encoder the YUV420P sink cannot
599/// feed, and a hardware one whose device has no node here.
600fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
601    // The native sink submits software YUV420P frames directly. Hardware wrappers
602    // requiring an AVHWFramesContext must not be advertised.
603    if candidate
604        .encoder
605        .pix_fmts()
606        .is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
607    {
608        return false;
609    }
610    !candidate.on_hardware || hardware_device_node(name) != Some(false)
611}
612
613/// Whether the encoder `name`, run on the GPU when `on_hardware` is set, is
614/// listed only after an open probe ([`encoder_runs_here`]): a hardware
615/// encoder with no device node to stand for its device, and an `_mf` or
616/// `_videotoolbox` one either way it runs. A `VideoToolbox` encoder opens only
617/// where the Mac has a hardware encoder, and a runtime may declare one
618/// software (`FFmpeg` 8.1's `h264_videotoolbox`, which the class corrects),
619/// so the name keeps it probed whatever its class.
620pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
621    (on_hardware && hardware_device_node(name).is_none())
622        || name.ends_with("_mf")
623        || name.ends_with("_videotoolbox")
624}
625
626/// Whether `encoder` encodes a frame on this machine, for the encoders whose
627/// registration does not say.
628///
629/// Registration says an encoder was compiled in, not that this machine runs
630/// it. `MediaFoundation` registers every `_mf` encoder whether or not Windows
631/// has a transform that runs it: on one runner `hevc_mf` found the HEVC
632/// Video Extension, opened, and failed its first frame with `E_FAIL` in
633/// every rate mode, and `av1_mf` found no transform at all. A hardware
634/// encoder is the same claim about a device: `h264_videotoolbox` registers in
635/// a headless macOS VM with no encoder to open, and NVENC on a Windows
636/// machine without the driver. Advertised on registration alone, they would
637/// be preferred and exports could only fall through. So an `_mf` or
638/// `_videotoolbox` encoder either way it runs, and a hardware one with no
639/// device node to stand for its device ([`hardware_device_node`]), is
640/// listed only after it has encoded one 256x144 frame with the options an
641/// export sends, once per process for each way it runs (a machine with no
642/// hardware transform still runs the software one); a failure is logged once.
643/// Any other software encoder is taken at its registration, and any failure
644/// when an export opens a listed encoder falls through to the next candidate.
645fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
646    static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
647    if !needs_open_probe(candidate.on_hardware, name) {
648        return true;
649    }
650    let key = (name.to_owned(), candidate.on_hardware);
651    let mut runs = RUNS
652        .lock()
653        .unwrap_or_else(std::sync::PoisonError::into_inner);
654    *runs.entry(key).or_insert_with(|| {
655        match probe_software_encode(candidate, name, 256, 144, None).0 {
656            None => true,
657            Some(error) => {
658                alpha2_os::diagnostic!(
659                    "alpha2 native: video encoder {name} {} is registered but did not encode a \
660                     frame on this machine, so it is not offered that way: {error}",
661                    mode_label(candidate.on_hardware)
662                );
663                false
664            }
665        }
666    })
667}
668
669/// How a log line names the way a candidate runs.
670pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
671    if on_hardware { "on GPU" } else { "on CPU" }
672}
673
674/// Whether the device a hardware encoder needs has a node here, for the
675/// encoder families Linux knows a node for; `None` for the rest.
676///
677/// A node is the evidence Linux takes for those families instead of an open
678/// probe ([`encoder_runs_here`]): the probe would be exact, but each NVENC one
679/// sets up a CUDA context, and probing the three NVENC encoders added 1.27 s
680/// to every host process's startup on an RTX 4090 (1.56 s against 0.29 s),
681/// for a runtime the pinned Linux SDK is not: it has no NVENC. A node whose
682/// device still does not open costs the export one failed open, and the
683/// encoder it falls through to is given the split for what it is.
684#[cfg(target_os = "linux")]
685fn hardware_device_node(name: &str) -> Option<bool> {
686    if name.ends_with("_nvenc") {
687        return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
688    }
689    if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
690        return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
691            entries.flatten().any(|entry| {
692                entry
693                    .file_name()
694                    .to_str()
695                    .is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
696            })
697        }));
698    }
699    if name.ends_with("_v4l2m2m") {
700        return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
701    }
702    None
703}
704
705/// Off Linux there is no device node to look at: whether the device is there
706/// is the open probe's to answer ([`encoder_runs_here`]).
707#[cfg(not(target_os = "linux"))]
708const fn hardware_device_node(_name: &str) -> Option<bool> {
709    None
710}
711
712/// Names the loaded runtime and everything it does register for `codec`.
713///
714/// "h264 encoder is unavailable" reads like a bug in alpha2. The runtime is the
715/// thing that decides, so the message says which runtime answered and what it
716/// offered instead.
717fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
718    let identity = crate::ffmpeg_runtime_identity();
719    let inventory = codec_capabilities()
720        .video
721        .iter()
722        .map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
723        .collect::<Vec<_>>()
724        .join(", ");
725    let inventory = if inventory.is_empty() {
726        "none".to_owned()
727    } else {
728        inventory
729    };
730    EncodeError::Adapter(format!(
731        "the linked FFmpeg runtime ({}, pinned SDK {}) registers no usable {} encoder; it offers [{inventory}]",
732        identity.version,
733        identity.pinned_sdk_release,
734        codec.as_str()
735    ))
736}
737
738/// Encoders to try for `codec`, most preferred first.
739///
740/// A named request is that encoder alone: a caller that pinned an encoder
741/// wants that encoder or an error, not a quiet substitution. A hybrid
742/// encoder is still tried each way it runs here.
743pub(crate) fn video_encoder_candidates(
744    codec: NativeVideoCodec,
745    requested_name: Option<&str>,
746) -> Result<Vec<VideoEncoderCandidate>, EncodeError> {
747    if let Some(name) = requested_name {
748        let encoder = select_video_encoder(codec, Some(name))?;
749        return Ok(registered_video_encoders(codec)
750            .into_iter()
751            .filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
752            .filter(encoder_is_compatible)
753            .collect());
754    }
755    let candidates = registered_video_encoders(codec)
756        .into_iter()
757        .filter(encoder_is_compatible)
758        .collect::<Vec<_>>();
759    if candidates.is_empty() {
760        return Err(unavailable_codec_error(codec));
761    }
762    Ok(candidates)
763}
764
765/// The one encoder an export of `codec` opens without a fallback: the pinned
766/// `requested_name`, which must implement `codec` and be advertised for the
767/// YUV420P sink, else the codec's preferred advertised encoder.
768pub(crate) fn select_video_encoder(
769    codec: NativeVideoCodec,
770    requested_name: Option<&str>,
771) -> Result<AVCodecRef<'static>, EncodeError> {
772    if let Some(name) = requested_name {
773        let name_c = CString::new(name)
774            .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
775        let encoder = AVCodec::find_encoder_by_name(&name_c)
776            .ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
777        if encoder.id != codec.codec_id() {
778            return Err(EncodeError::Adapter(format!(
779                "video encoder {name} does not implement {}",
780                codec.as_str()
781            )));
782        }
783        if !codec_capabilities()
784            .video
785            .iter()
786            .any(|capability| capability.codec == codec && capability.encoder_name == name)
787        {
788            return Err(EncodeError::Adapter(format!(
789                "video encoder {name} is not compatible with the native encode path"
790            )));
791        }
792        return Ok(encoder);
793    }
794
795    let capability = codec_capabilities()
796        .video
797        .into_iter()
798        .find(|capability| capability.codec == codec)
799        .ok_or_else(|| unavailable_codec_error(codec))?;
800    let name = CString::new(capability.encoder_name)
801        .map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
802    AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
803}
804
805/// What one sink needs to open its video encoder.
806pub(crate) struct VideoEncoderOpenRequest<'a> {
807    /// Output format the encoder is configured for.
808    pub config: &'a VideoEncodeConfig,
809    /// Concrete encoder the caller pinned, if any.
810    pub requested_name: Option<&'a str>,
811    /// Software worker budget, asked for only when a candidate that runs on
812    /// the CPU is opened; a hardware candidate takes none.
813    pub threads: SoftwareEncoderThreads<'a>,
814    /// Whether the container wants the extradata in the stream header.
815    pub global_header: bool,
816    /// Coded size, already range-checked by the caller.
817    pub width: i32,
818    /// Coded size, already range-checked by the caller.
819    pub height: i32,
820}
821
822/// The worker count a software video encoder opens with, decided only when
823/// one is opened.
824///
825/// An export divides its CPU between decode and the encoder, and which way it
826/// divides depends on whether the encoder runs on the CPU at all. That is
827/// known only once a candidate opens: the preferred one can be a hardware
828/// encoder that fails `avcodec_open2` and falls through to a software one.
829/// A count decided before the open would give that fallback the hardware
830/// split's share of the encoder, which is none, and it would run on one
831/// worker. The caller passes how to decide instead; it is asked at most once.
832pub(crate) struct SoftwareEncoderThreads<'a> {
833    decide: &'a dyn Fn() -> i64,
834    decided: std::cell::OnceCell<i64>,
835}
836
837impl<'a> SoftwareEncoderThreads<'a> {
838    /// The count `decide` returns, asked for when a software candidate opens.
839    pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
840        Self {
841            decide,
842            decided: std::cell::OnceCell::new(),
843        }
844    }
845
846    /// The decided count, at least one worker.
847    pub(crate) fn get(&self) -> i64 {
848        *self.decided.get_or_init(|| (self.decide)().max(1))
849    }
850}
851
852/// Opens the first candidate encoder that this machine can actually run.
853///
854/// `avcodec_open2` is where a registered encoder becomes a running one, and
855/// the two are not the same claim: a Windows SKU or VM without the
856/// `MediaFoundation` H.264 MFT registers `h264_mf` and cannot open it, and a
857/// headless container registers `h264_videotoolbox` and cannot open it. Each
858/// rejection is recorded with a typed reason and carried on the sink so the
859/// run can report which encoder it ended up on.
860///
861/// # Errors
862/// Returns the last candidate's failure, prefixed with every earlier one, when
863/// no candidate opens.
864pub(crate) fn open_video_encoder(
865    request: &VideoEncoderOpenRequest<'_>,
866) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
867    let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
868    open_first_video_encoder(request, candidates)
869}
870
871/// Opens the first of `candidates` that runs, recording why each one before
872/// it did not; when none does, the error names every candidate's reason.
873pub(crate) fn open_first_video_encoder(
874    request: &VideoEncoderOpenRequest<'_>,
875    candidates: Vec<VideoEncoderCandidate>,
876) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
877    let mut rejected = Vec::new();
878    let last = candidates.len().saturating_sub(1);
879    for (index, candidate) in candidates.into_iter().enumerate() {
880        let name = candidate.name();
881        let class = encoder_class(&candidate.encoder);
882        match open_one_video_encoder(request, &candidate, &name) {
883            Ok((encoder, rate_control, quantizer_refused)) => {
884                return Ok((
885                    encoder,
886                    NativeVideoEncoderSelection {
887                        encoder_name: name,
888                        class,
889                        hardware_accelerated: candidate.on_hardware,
890                        rejected,
891                        rate_control,
892                        quantizer_refused,
893                        software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
894                    },
895                ));
896            }
897            Err((reason, error)) => {
898                let detail = error.to_string();
899                if index == last {
900                    let earlier = rejected
901                        .iter()
902                        .map(|fallback: &NativeVideoEncoderFallback| {
903                            format!(
904                                "{} {} ({}): {}",
905                                fallback.encoder_name,
906                                mode_label(fallback.hardware_accelerated),
907                                fallback.reason.as_str(),
908                                fallback.detail
909                            )
910                        })
911                        .collect::<Vec<_>>();
912                    let prefix = if earlier.is_empty() {
913                        String::new()
914                    } else {
915                        format!("after {}; ", earlier.join("; "))
916                    };
917                    return Err(EncodeError::Adapter(format!(
918                        "{prefix}{name} {} ({}): {detail}",
919                        mode_label(candidate.on_hardware),
920                        reason.as_str()
921                    )));
922                }
923                alpha2_os::diagnostic!(
924                    "alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
925                    mode_label(candidate.on_hardware),
926                    reason.as_str()
927                );
928                rejected.push(NativeVideoEncoderFallback {
929                    encoder_name: name,
930                    hardware_accelerated: candidate.on_hardware,
931                    reason,
932                    detail,
933                });
934            }
935        }
936    }
937    Err(unavailable_codec_error(request.config.codec))
938}
939
940/// Opens `candidate`, on the rate control's quantizer when it has one and the
941/// encoder accepts it, otherwise on its bitrate.
942///
943/// A refused quantizer is not a refused encoder: the bitrate is the rate
944/// control's own fallback, so the export continues on it and the selection
945/// says so rather than failing over to another candidate.
946fn open_one_video_encoder(
947    request: &VideoEncoderOpenRequest<'_>,
948    candidate: &VideoEncoderCandidate,
949    name: &str,
950) -> Result<
951    (AVCodecContext, crate::AppliedRateControl, Option<String>),
952    (NativeEncoderRejection, EncodeError),
953> {
954    validate_encoder_dimensions(name, request.config.width, request.config.height)
955        .map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
956    let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
957        None => None,
958        Some(Err(reason)) => Some(reason),
959        Some(Ok(quantized)) => {
960            match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
961                Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
962                Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
963            }
964        }
965    };
966    if let Some(reason) = &refused {
967        alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
968    }
969    open_video_encoder_context(request, candidate, name, None)
970        .map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
971}
972
973fn open_video_encoder_context(
974    request: &VideoEncoderOpenRequest<'_>,
975    candidate: &VideoEncoderCandidate,
976    name: &str,
977    quantized: Option<&crate::rate_control::MechanismRequest>,
978) -> Result<AVCodecContext, (NativeEncoderRejection, EncodeError)> {
979    let mut encoder = AVCodecContext::new(&candidate.encoder);
980    encoder.set_width(request.width);
981    encoder.set_height(request.height);
982    encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
983    let rate = request.config.frame_rate;
984    let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
985        .map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
986    encoder.set_time_base(time_base);
987    // Square pixels are what an undeclared ratio means; only another is written.
988    let sar = request.config.display.sample_aspect_ratio;
989    if sar != alpha2_media::SampleAspectRatio::default() {
990        encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
991            i32::try_from(sar.num).unwrap_or(1),
992            i32::try_from(sar.den).unwrap_or(1),
993        ));
994    }
995    encoder.set_framerate(rsmpeg::avutil::ra(
996        i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
997        i32::try_from(rate.denominator()).unwrap_or(1),
998    ));
999    encoder.set_bit_rate(quantized.map_or_else(
1000        || i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
1001        |quantized| quantized.bit_rate,
1002    ));
1003    encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
1004    encoder.set_max_b_frames(encoder_max_b_frames(name));
1005    if request.global_header {
1006        encoder.set_flags(
1007            encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
1008        );
1009    }
1010    // A hardware candidate never asks, so an export whose GPU encoder opens never decides a
1011    // software split it would not use.
1012    let threads = if candidate.on_hardware {
1013        0
1014    } else {
1015        request.threads.get()
1016    };
1017    let base = video_encoder_options(
1018        request.config.color,
1019        &candidate.encoder,
1020        candidate.on_hardware,
1021        threads,
1022        request.config.width,
1023    );
1024    let options = match quantized {
1025        Some(quantized) => quantized.dictionary(base),
1026        None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
1027    };
1028    let unused =
1029        alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
1030            let hint = match quantized {
1031                Some(_) => "",
1032                None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
1033            };
1034            let error = match encode_adapter_error(error) {
1035                EncodeError::Adapter(message) if !hint.is_empty() => {
1036                    EncodeError::Adapter(format!("{message}{hint}"))
1037                }
1038                error => error,
1039            };
1040            (NativeEncoderRejection::OpenFailed, error)
1041        })?;
1042    reject_unused_encode_options(unused)
1043        .map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
1044    Ok(encoder)
1045}
1046
1047/// Frames between forced keyframes: two seconds of output.
1048///
1049/// libavcodec's own `gop_size` default is 12, which at 24 fps is a keyframe
1050/// every half second. Nothing in alpha2 seeks or thumbnails its own output, so the
1051/// interval is only a rate question. Measured at a fixed rate on the 4K
1052/// showcase, 12 s of the colour clip, PSNR-Y: going from 12 to two seconds
1053/// (48) gained 0.53-0.64 dB on `h264_vulkan` and 0.67-0.77 dB on
1054/// `h264_nvenc`; going on to ten seconds (240) gained a further 0.11-0.12 dB
1055/// on `h264_vulkan`. Two seconds is where nearly all of that is and a player
1056/// can still start or seek within two seconds of any point.
1057pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
1058    let frames = (2 * u64::from(frame_rate.numerator()))
1059        .div_ceil(u64::from(frame_rate.denominator()))
1060        .max(1);
1061    i32::try_from(frames).unwrap_or(i32::MAX)
1062}
1063
1064/// The largest time base denominator encoders of `codec` can write, `None`
1065/// for a codec without one. MPEG-4 Part 2 and H.263 store it as the 16-bit
1066/// `vop_time_increment_resolution`, so `FFmpeg`'s encoders refuse a time base
1067/// of 1001/120000 (119.88 fps).
1068pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
1069    match codec {
1070        ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
1071        _ => None,
1072    }
1073}
1074
1075/// The time base an encoder of frames at `rate` runs in: the frame length
1076/// itself (1001/30000 for 29.97 fps), when its denominator is within `limit`.
1077/// Otherwise 1/D for the D within `limit` that rounds frame times the least
1078/// (the largest error over every frame), so a rate a time base of this
1079/// encoder cannot hold keeps its frame count and frame times, each rounded
1080/// by under one unit. 119.88 fps (120000/1001) is 1/64000: frame times are
1081/// off by at most 7/960000 s.
1082///
1083/// # Errors
1084/// For a rate whose terms do not fit an `FFmpeg` rational.
1085pub(crate) fn encoder_time_base(
1086    rate: alpha2_time::FrameRate,
1087    limit: Option<u32>,
1088) -> Result<AVRational, EncodeError> {
1089    let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
1090    let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
1091    let exact = || {
1092        Ok(rsmpeg::avutil::ra(
1093            i32::try_from(den).map_err(|_| out_of_range())?,
1094            i32::try_from(num).map_err(|_| out_of_range())?,
1095        ))
1096    };
1097    let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
1098        return exact();
1099    };
1100    // Frame k is at k*den/num s, k*den*D/num units of 1/D. Its fractional part is a multiple of
1101    // 1/q, q = num / gcd(num, den*D), so the largest rounding is floor(q/2)/q units,
1102    // floor(q/2)/(q*D) s.
1103    // A unit no longer than a frame keeps every frame its own time.
1104    let mut best: Option<(u64, u64, u64)> = None;
1105    for units in num.div_ceil(den).max(1)..=limit {
1106        let q = num / gcd_u64(num, den * units);
1107        let (error, scale) = (q / 2, q * units);
1108        if best.is_none_or(|(_, best_error, best_scale)| {
1109            u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
1110        }) {
1111            best = Some((units, error, scale));
1112        }
1113    }
1114    let (units, _, _) = best.ok_or_else(out_of_range)?;
1115    Ok(rsmpeg::avutil::ra(
1116        1,
1117        i32::try_from(units).map_err(|_| out_of_range())?,
1118    ))
1119}
1120
1121const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
1122    while b != 0 {
1123        (a, b) = (b, a % b);
1124    }
1125    a
1126}
1127
1128/// `max_b_frames` for encoder `name`.
1129pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
1130    // VideoToolbox's default is no frame reordering. Forcing B-frames makes support
1131    // device-dependent and can yield invalid packet timestamps on otherwise valid hardware
1132    // sessions, so preserve that native default.
1133    if name.ends_with("_videotoolbox") {
1134        0
1135    } else {
1136        2
1137    }
1138}
1139
1140/// The smallest even width and height encoder `name` accepts.
1141pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
1142    // NVIDIA AV1 requires at least 145x128. Its H.264/HEVC encoders reject dimensions below 145x49.
1143    // alpha2 exposes even 4:2:0 dimensions.
1144    if name == "av1_nvenc" {
1145        (146, 128)
1146    } else if name.ends_with("_nvenc") {
1147        (146, 50)
1148    } else {
1149        (2, 2)
1150    }
1151}
1152
1153pub(crate) fn validate_encoder_dimensions(
1154    encoder_name: &str,
1155    width: u32,
1156    height: u32,
1157) -> Result<(), EncodeError> {
1158    let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
1159    if width < minimum_width || height < minimum_height {
1160        Err(EncodeError::Adapter(format!(
1161            "encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
1162        )))
1163    } else {
1164        Ok(())
1165    }
1166}
1167
1168/// One hardware configuration a video encoder declares.
1169#[derive(Clone, Debug, Eq, PartialEq)]
1170pub struct NativeEncoderHardwareConfig {
1171    /// Hardware pixel format this configuration carries frames in.
1172    pub pixel_format: String,
1173    /// `FFmpeg` hardware device type backing it.
1174    pub device_type: String,
1175    /// `AV_CODEC_HW_CONFIG_METHOD_*` bits.
1176    pub methods: i32,
1177    /// Whether the encoder takes its frames through an `AVHWFramesContext`.
1178    pub needs_hardware_frames: bool,
1179}
1180
1181/// What one registered video encoder declares, and what happened when alpha2
1182/// opened it on this machine.
1183///
1184/// Registration, advertisement and instantiation are three different claims,
1185/// and only the third is the one an export depends on. This probe makes all
1186/// three observable in one place, per platform, without an export.
1187#[derive(Clone, Debug, Eq, PartialEq)]
1188// Independent observations of one encoder, reported side by side, not a state.
1189#[allow(clippy::struct_excessive_bools)]
1190pub struct NativeVideoEncoderProbe {
1191    /// Logical codec this encoder implements.
1192    pub codec: NativeVideoCodec,
1193    /// `FFmpeg` short name.
1194    pub encoder_name: String,
1195    /// Class taken from the encoder's own capability bits.
1196    pub class: NativeEncoderClass,
1197    /// Whether this row runs the encoder on the GPU.
1198    pub hardware_accelerated: bool,
1199    /// Raw `AV_CODEC_CAP_*` bits, so an unexpected class can be traced.
1200    pub capability_bits: u32,
1201    /// Pixel formats the encoder declares, or empty when it declares none.
1202    pub pixel_formats: Vec<String>,
1203    /// Hardware configurations the encoder declares.
1204    pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
1205    /// Whether `AV_PIX_FMT_YUV420P` is among the declared formats.
1206    pub accepts_yuv420p: bool,
1207    /// Whether every declared format is a hardware one, so a software frame
1208    /// can never be sent to it. This is what keeps `h264_vulkan` unadvertised.
1209    pub requires_hardware_frames: bool,
1210    /// Whether [`codec_capabilities`] advertises it for the YUV420P sink.
1211    pub advertised: bool,
1212    /// Smallest frame the encoder accepts, as alpha2 records it.
1213    pub minimum_size: (u32, u32),
1214    /// `None` when the encoder opened and produced at least one packet from a
1215    /// software YUV420P frame; otherwise why it did not.
1216    pub software_open_error: Option<String>,
1217    /// Packets drained from that attempt.
1218    pub software_packets: u32,
1219    /// Which rate-control mechanisms opened, and with what options.
1220    pub rate_control: Vec<NativeRateControlProbe>,
1221}
1222
1223/// Whether one encoder opened with one rate-control mechanism.
1224#[derive(Clone, Debug, Eq, PartialEq)]
1225pub struct NativeRateControlProbe {
1226    /// The mechanism asked for.
1227    pub mechanism: crate::RateControlMechanism,
1228    /// The options that carried it, or `None` where alpha2 knows no spelling for
1229    /// it on this encoder's family.
1230    pub options: Option<String>,
1231    /// Packets drained when it opened, consumed every option and encoded a
1232    /// frame; otherwise why not.
1233    pub outcome: Result<u32, String>,
1234    /// The device a hardware-frames encoder was probed on: a Vulkan device
1235    /// index for `*_vulkan`, and for `*_d3d12va` the DX12 adapter that
1236    /// answered (`DX12 adapter <index> (<name>)`).
1237    pub device: Option<String>,
1238}
1239
1240/// A quantizer every encoder of `codec` accepts, for a probe open: the
1241/// software VP9 and AV1 encoders (`libvpx-vp9`, `libsvtav1`) take `crf` on
1242/// 0-63, the hardware ones a QP on 0-255.
1243pub(crate) const fn probe_quantizer(codec: NativeVideoCodec, software: bool) -> u32 {
1244    match codec {
1245        NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
1246        NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
1247            if software {
1248                32
1249            } else {
1250                100
1251            }
1252        }
1253        NativeVideoCodec::Mpeg4Part2 => 4,
1254    }
1255}
1256
1257/// Opens every registered video encoder and reports what it actually does.
1258///
1259/// The name of an encoder says nothing about whether this machine can run it:
1260/// a Windows SKU without the `MediaFoundation` H.264 MFT registers `h264_mf`
1261/// and cannot open it, and `h264_vulkan` is registered on every Linux build
1262/// and cannot take a software frame at all. Each candidate is opened at
1263/// `width`x`height` and fed one YUV420P frame, so the report distinguishes
1264/// "registered", "advertised" and "ran". A hybrid encoder is a row for each
1265/// way it runs here.
1266#[must_use]
1267#[allow(clippy::too_many_lines)]
1268pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
1269    let advertised = codec_capabilities();
1270    [
1271        NativeVideoCodec::H264,
1272        NativeVideoCodec::Hevc,
1273        NativeVideoCodec::Vp9,
1274        NativeVideoCodec::Av1,
1275        NativeVideoCodec::Mpeg4Part2,
1276    ]
1277    .into_iter()
1278    .flat_map(|codec| {
1279        registered_video_encoders(codec)
1280            .into_iter()
1281            .map(move |candidate| (codec, candidate))
1282    })
1283    .map(|(codec, candidate)| {
1284        let encoder = &candidate.encoder;
1285        let encoder_name = candidate.name();
1286        let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
1287            formats
1288                .iter()
1289                .map(|format| {
1290                    alpha2_ffmpeg_bridge::pixel_format_name(*format)
1291                        .unwrap_or_else(|| "unknown".to_owned())
1292                })
1293                .collect()
1294        });
1295        let hardware_configs = (0..)
1296            .map_while(|index| encoder.hw_config(index))
1297            .map(|config| NativeEncoderHardwareConfig {
1298                pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
1299                    .unwrap_or_else(|| "unknown".to_owned()),
1300                device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
1301                    .unwrap_or_else(|| "none".to_owned()),
1302                methods: config.methods,
1303                needs_hardware_frames: crate::decode::has_hw_config_method(
1304                    config.methods,
1305                    ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
1306                ),
1307            })
1308            .collect::<Vec<_>>();
1309        let accepts_yuv420p = encoder
1310            .pix_fmts()
1311            .is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
1312        let (software_open_error, software_packets) =
1313            probe_software_encode(&candidate, &encoder_name, width, height, None);
1314        let requires_hardware_frames = !accepts_yuv420p
1315            && hardware_configs
1316                .iter()
1317                .any(|config| config.needs_hardware_frames);
1318        let rate_control = crate::RateControlMechanism::ALL
1319            .into_iter()
1320            .map(|mechanism| {
1321                let request = crate::rate_control::mechanism_request(
1322                    &encoder_name,
1323                    mechanism,
1324                    2_000_000,
1325                    // D3D12VA takes 0-52 for every codec, AV1 included.
1326                    if encoder_name.ends_with("_d3d12va") {
1327                        26
1328                    } else {
1329                        probe_quantizer(
1330                            codec,
1331                            encoder_class(encoder) == NativeEncoderClass::Software,
1332                        )
1333                    },
1334                );
1335                let mut device = None;
1336                let outcome = match &request {
1337                    None => Err("no known option for this encoder family".to_owned()),
1338                    Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
1339                        encoder,
1340                        &encoder_name,
1341                        (width, height),
1342                        mechanism,
1343                        request,
1344                    )
1345                    .map(|(packets, on)| {
1346                        device = Some(on);
1347                        packets
1348                    }),
1349                    Some(request) => {
1350                        match probe_software_encode(
1351                            &candidate,
1352                            &encoder_name,
1353                            width,
1354                            height,
1355                            Some(request),
1356                        ) {
1357                            (None, packets) => Ok(packets),
1358                            (Some(error), _) => Err(error),
1359                        }
1360                    }
1361                };
1362                NativeRateControlProbe {
1363                    mechanism,
1364                    options: request
1365                        .as_ref()
1366                        .map(crate::rate_control::MechanismRequest::describe),
1367                    outcome,
1368                    device,
1369                }
1370            })
1371            .collect();
1372        NativeVideoEncoderProbe {
1373            codec,
1374            class: encoder_class(encoder),
1375            hardware_accelerated: candidate.on_hardware,
1376            capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
1377            advertised: advertised.video.iter().any(|capability| {
1378                capability.codec == codec
1379                    && capability.encoder_name == encoder_name
1380                    && capability.hardware_accelerated == candidate.on_hardware
1381            }),
1382            minimum_size: encoder_minimum_dimensions(&encoder_name),
1383            requires_hardware_frames,
1384            accepts_yuv420p,
1385            pixel_formats,
1386            hardware_configs,
1387            encoder_name,
1388            software_open_error,
1389            software_packets,
1390            rate_control,
1391        }
1392    })
1393    .collect()
1394}
1395
1396/// Opens an encoder that only takes hardware frames, with `request`.
1397///
1398/// `h264_vulkan` is the shipped Linux encoder and cannot take a software
1399/// frame at all, so its rate-control surface is only observable through a
1400/// device. `FFmpeg` creates its own Vulkan device here, with whatever
1401/// video-encode queue the physical device offers, the same capability the
1402/// export's shared device is checked for.
1403///
1404/// The D3D12VA encoders are Windows' GPU-path encoders and take D3D12
1405/// frames only. Their rows open on a DX12 adapter's device, the first one
1406/// whose D3D12 Video Encode reports the encoder, and a mode that device does
1407/// not list is refused with its reason rather than opened.
1408///
1409/// Returns the packets and the device that answered: a Vulkan device's
1410/// index, or a DX12 adapter's index and name.
1411#[allow(unused_variables)]
1412fn probe_hardware_frames_encode(
1413    encoder: &AVCodecRef<'static>,
1414    name: &str,
1415    (width, height): (u32, u32),
1416    mechanism: crate::RateControlMechanism,
1417    request: &crate::rate_control::MechanismRequest,
1418) -> Result<(u32, String), String> {
1419    #[cfg(all(target_os = "linux", feature = "hwencode"))]
1420    if name.ends_with("_vulkan") {
1421        return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
1422    }
1423    #[cfg(all(target_os = "windows", feature = "hwencode"))]
1424    if name.ends_with("_d3d12va") {
1425        return crate::hwencode::probe_d3d12va_rate_control(
1426            name, width, height, mechanism, request,
1427        );
1428    }
1429    Err("takes hardware frames this probe cannot supply".to_owned())
1430}
1431
1432/// Opens `candidate` and feeds it one software YUV420P frame.
1433fn probe_software_encode(
1434    candidate: &VideoEncoderCandidate,
1435    name: &str,
1436    width: u32,
1437    height: u32,
1438    request: Option<&crate::rate_control::MechanismRequest>,
1439) -> (Option<String>, u32) {
1440    let Ok(coded_width) = i32::try_from(width) else {
1441        return (Some("width is out of range".to_owned()), 0);
1442    };
1443    let Ok(coded_height) = i32::try_from(height) else {
1444        return (Some("height is out of range".to_owned()), 0);
1445    };
1446    if let Err(error) = validate_encoder_dimensions(name, width, height) {
1447        return (Some(error.to_string()), 0);
1448    }
1449    let encoder = &candidate.encoder;
1450    let mut context = AVCodecContext::new(encoder);
1451    context.set_width(coded_width);
1452    context.set_height(coded_height);
1453    context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
1454    let time_base = rsmpeg::avutil::ra(1, 30);
1455    context.set_time_base(time_base);
1456    context.set_framerate(rsmpeg::avutil::ra(30, 1));
1457    context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
1458    context.set_gop_size(12);
1459    context.set_max_b_frames(encoder_max_b_frames(name));
1460    let options = video_encoder_options(
1461        ColorMetadata::EXPORT,
1462        encoder,
1463        candidate.on_hardware,
1464        1,
1465        width,
1466    );
1467    let options = match request {
1468        Some(request) => request.dictionary(options),
1469        None => options,
1470    };
1471    match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
1472        Err(error) => return (Some(error.to_string()), 0),
1473        Ok(unused) => {
1474            if let Err(error) = reject_unused_encode_options(unused) {
1475                return (Some(error.to_string()), 0);
1476            }
1477        }
1478    }
1479    let Some(image) =
1480        rsmpeg::avutil::AVImage::new(ffi::AV_PIX_FMT_YUV420P, coded_width, coded_height, 1)
1481    else {
1482        return (Some("cannot allocate a probe frame".to_owned()), 0);
1483    };
1484    let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
1485    frame.set_pts(0);
1486    frame.set_time_base(time_base);
1487    if let Err(error) = context.send_frame(Some(&frame)) {
1488        return (Some(error.to_string()), 0);
1489    }
1490    if let Err(error) = context.send_frame(None) {
1491        return (Some(error.to_string()), 0);
1492    }
1493    let mut packets = 0;
1494    loop {
1495        match context.receive_packet() {
1496            Ok(_) => packets += 1,
1497            Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
1498            Err(error) => return (Some(error.to_string()), packets),
1499        }
1500    }
1501    if packets == 0 {
1502        return (
1503            Some("opened but produced no packet from a software frame".to_owned()),
1504            0,
1505        );
1506    }
1507    (None, packets)
1508}