Skip to main content

alpha2_native/
video_frame.rs

1//! Native export frame contract shared by software and hardware decoders.
2
3use std::any::Any;
4use std::fmt::Debug;
5use std::sync::Arc;
6
7use alpha2_media::{
8    CancelToken, ChromaLocation, ColorMetadata, DecodeError, DecodeEvent, DecodeRequest,
9    DecodedVideoFrame, Decoder, Orientation, PixelFormat, Rect, SampleAspectRatio, StreamInfo,
10};
11use alpha2_time::MediaTime;
12
13use crate::NativeDecoder;
14#[cfg(all(
15    any(target_os = "linux", target_os = "macos", target_os = "windows"),
16    feature = "hwdecode"
17))]
18use crate::convert::timestamp;
19use crate::decode::DecodeStageTimings;
20#[cfg(all(target_os = "windows", feature = "hwdecode"))]
21use alpha2_native_gpu::SharedD3D12Context;
22#[cfg(all(target_os = "macos", feature = "hwdecode"))]
23use alpha2_native_gpu::SharedMetalContext;
24#[cfg(all(target_os = "linux", feature = "hwdecode"))]
25use alpha2_native_gpu::SharedVulkanContext;
26#[cfg(all(
27    any(target_os = "linux", target_os = "macos", target_os = "windows"),
28    feature = "hwdecode"
29))]
30use rsmpeg::{avutil::AVFrame, ffi};
31
32/// Why a video source is decoded in software instead of on the GPU, recorded per source.
33#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub enum HardwareDecodeFallback {
35    /// No shared GPU decode context was created for this run.
36    ContextUnavailable,
37    /// The codec exposes no hardware configuration for this backend.
38    NoHardwareConfig,
39    /// The stream declares a color space the hardware path does not present.
40    UnsupportedColor,
41    /// The stream's subsampling or bit depth has no resident path on this device (4:2:2,
42    /// 4:4:4, or 10-bit without P010).
43    UnsupportedProfile,
44    /// The decoder produced a surface format that cannot be imported.
45    UnsupportedSurfaceFormat,
46    /// The container or stream could not be opened at all.
47    SourceUnavailable,
48    /// The hardware decoder rejected this stream while opening.
49    DecoderOpenFailed,
50    /// Reverse playback keeps this cursor on the software decoder.
51    ReverseWindow,
52    /// The SPS crops the left or top edge, which this backend's surfaces lose (`VideoToolbox`
53    /// clears the crop and returns the top-left corner). A D3D12VA picture larger than its
54    /// surface is refused the same way.
55    UnsupportedCrop,
56    /// The renderer could not import a decoded surface, so its pixels went through the CPU.
57    ImportFailed,
58    /// The stream carries alpha, which a hardware session would drop (VP9 block additions, an HEVC
59    /// auxiliary layer, or an alpha plane).
60    AlphaChannel,
61}
62
63impl HardwareDecodeFallback {
64    /// Stable key used when reporting fallback counts.
65    #[must_use]
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::ContextUnavailable => "contextUnavailable",
69            Self::NoHardwareConfig => "noHardwareConfig",
70            Self::UnsupportedColor => "unsupportedColor",
71            Self::UnsupportedProfile => "unsupportedProfile",
72            Self::UnsupportedSurfaceFormat => "unsupportedSurfaceFormat",
73            Self::SourceUnavailable => "sourceUnavailable",
74            Self::DecoderOpenFailed => "decoderOpenFailed",
75            Self::ReverseWindow => "reverseWindow",
76            Self::UnsupportedCrop => "unsupportedCrop",
77            Self::ImportFailed => "importFailed",
78            Self::AlphaChannel => "alphaChannel",
79        }
80    }
81}
82
83/// A hardware decode session that could not be opened, with its typed reason.
84#[derive(Debug)]
85pub struct HardwareOpenError {
86    /// Why the hardware path is unavailable for this source.
87    pub reason: HardwareDecodeFallback,
88    /// The underlying decode failure.
89    pub source: DecodeError,
90}
91
92impl HardwareOpenError {
93    /// Pairs a decode failure with the reason a caller should record.
94    #[must_use]
95    pub const fn new(reason: HardwareDecodeFallback, source: DecodeError) -> Self {
96        Self { reason, source }
97    }
98}
99
100impl std::fmt::Display for HardwareOpenError {
101    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        write!(formatter, "{}: {:?}", self.reason.as_str(), self.source)
103    }
104}
105
106impl std::error::Error for HardwareOpenError {}
107
108/// One backend's hardware-decoder negotiation parameters.
109#[cfg(all(
110    any(target_os = "linux", target_os = "macos", target_os = "windows"),
111    feature = "hwdecode"
112))]
113pub(crate) struct HardwareDecoderSpec<'a> {
114    /// `FFmpeg` hardware device type to match in the codec's configurations.
115    pub device_type: ffi::AVHWDeviceType,
116    /// Opaque hardware pixel format the decoder must produce.
117    pub pixel_format: ffi::AVPixelFormat,
118    /// Backend name used in diagnostics.
119    pub backend: &'static str,
120    /// Whether the configuration must accept a shared device context.
121    pub require_device_context: bool,
122    /// Whether the renderer can present 10-bit planes from this backend.
123    pub ten_bit: bool,
124    /// Whether this backend's frames keep an SPS's left and top crop.
125    ///
126    /// `FFmpeg`'s `VideoToolbox` hwaccel clears the crop, but the session only trims right/bottom
127    /// (H.264) or rescales (HEVC), so a leading crop would show wrong pixels. D3D12VA keeps the
128    /// crop as long as surfaces are the coded size (`alpha2_d3d12_get_format`), since `FFmpeg`
129    /// 8.1.3 would size them to the visible size.
130    pub keeps_leading_crop: bool,
131    /// The codecs the device decodes in hardware, by `FFmpeg` name, when it lists them; others are
132    /// refused before a session opens. Needed for `VideoToolbox`, where `FFmpeg`'s HEVC request
133    /// lets a Mac without the hardware decoder run in software.
134    pub hardware_codecs: Option<&'a [&'static str]>,
135    /// Callback that selects `pixel_format` during negotiation.
136    pub get_format: unsafe extern "C" fn(
137        *mut ffi::AVCodecContext,
138        *const ffi::AVPixelFormat,
139    ) -> ffi::AVPixelFormat,
140}
141
142/// Plane organization of a native export frame.
143#[derive(Copy, Clone, Debug, Eq, PartialEq)]
144pub enum NativePlaneLayout {
145    /// Owned CPU YCbCr planes.
146    CpuYuv(alpha2_media::YuvFormat),
147    /// Owned CPU straight-alpha 8-bit RGBA ([`PixelFormat::Rgba8`]).
148    CpuRgba8,
149    /// Owned CPU premultiplied linear-light half-float RGBA ([`PixelFormat::LinearRgbaF16`]).
150    CpuLinearRgbaF16,
151    /// Three imported R8 GPU planes.
152    GpuI420,
153    /// Imported R8 luma and RG8 chroma planes.
154    GpuNv12,
155    /// Imported R16 luma and RG16 chroma planes holding 10 significant bits.
156    GpuP010,
157}
158
159impl NativePlaneLayout {
160    /// Significant bits per component for this layout.
161    #[must_use]
162    pub const fn bit_depth(self) -> u8 {
163        match self {
164            Self::CpuRgba8 | Self::GpuI420 | Self::GpuNv12 => 8,
165            Self::CpuLinearRgbaF16 => 16,
166            Self::CpuYuv(format) => format.bit_depth,
167            Self::GpuP010 => 10,
168        }
169    }
170
171    /// Bytes one coded pixel occupies across every plane of this layout.
172    #[cfg_attr(
173        not(all(
174            any(target_os = "linux", target_os = "macos", target_os = "windows"),
175            feature = "hwdecode"
176        )),
177        allow(dead_code)
178    )]
179    const fn bytes_per_pixel_numerator(self) -> u64 {
180        match self {
181            Self::CpuRgba8 => 8,
182            Self::CpuLinearRgbaF16 => 16,
183            Self::GpuI420 | Self::GpuNv12 => 3,
184            Self::GpuP010 => 6,
185            // Luma, then two chroma planes of 4 >> (shift x + shift y) its area each,
186            // in half bytes per sample.
187            Self::CpuYuv(format) => {
188                format.bytes_per_sample() as u64
189                    * (2 + (4 >> (format.chroma_shift_x + format.chroma_shift_y)))
190            }
191        }
192    }
193}
194
195/// Maps a hardware surface's software pixel format to its plane layout; `None` makes the caller
196/// report a typed fallback instead of presenting it as 4:2:0.
197#[cfg(all(
198    any(target_os = "linux", target_os = "macos", target_os = "windows"),
199    feature = "hwdecode"
200))]
201pub(crate) const fn surface_plane_layout(format: ffi::AVPixelFormat) -> Option<NativePlaneLayout> {
202    match format {
203        ffi::AV_PIX_FMT_NV12 => Some(NativePlaneLayout::GpuNv12),
204        ffi::AV_PIX_FMT_P010LE => Some(NativePlaneLayout::GpuP010),
205        _ => None,
206    }
207}
208
209/// The visible rectangle of a hardware frame with `crop` (left, top, right, bottom) still on it, or
210/// `None` when it reaches past the `surface_width` x `surface_height` texture.
211///
212/// `FFmpeg` applies only the right and bottom crop to a hardware frame's size. A surface smaller
213/// than that never held the last columns or rows; this guards against a decoder making surfaces
214/// visible-sized (as `FFmpeg` 8.1.3 D3D12VA would without `alpha2_d3d12_get_format`).
215#[cfg_attr(
216    not(all(target_os = "windows", feature = "hwdecode")),
217    allow(dead_code)
218)]
219pub(crate) fn visible_rect_within_surface(
220    width: u32,
221    height: u32,
222    [left, top, right, bottom]: [usize; 4],
223    surface_width: u32,
224    surface_height: u32,
225) -> Option<Rect> {
226    let x = u32::try_from(left).ok()?;
227    let y = u32::try_from(top).ok()?;
228    let visible = Rect {
229        x,
230        y,
231        width: width
232            .checked_sub(x)?
233            .checked_sub(u32::try_from(right).ok()?)?,
234        height: height
235            .checked_sub(y)?
236            .checked_sub(u32::try_from(bottom).ok()?)?,
237    };
238    (u64::from(x) + u64::from(visible.width) <= u64::from(surface_width)
239        && u64::from(y) + u64::from(visible.height) <= u64::from(surface_height))
240    .then_some(visible)
241}
242
243/// Platform-neutral metadata carried identically by CPU and hardware frames.
244///
245/// It says nothing about backing memory: a hardware surface may be larger (1088 rows for 1080p
246/// H.264), and an importer reports the extent it samples.
247#[derive(Clone, Debug, PartialEq)]
248pub struct NativeVideoFrameMetadata {
249    /// Presentation timestamp.
250    pub pts: MediaTime,
251    /// Decode timestamp when available.
252    pub dts: Option<MediaTime>,
253    /// Nominal duration when available.
254    pub duration: Option<MediaTime>,
255    /// Visible crop.
256    pub visible_rect: Rect,
257    /// How the picture is shown upright.
258    pub orientation: Orientation,
259    /// Pixel aspect ratio.
260    pub sample_aspect_ratio: SampleAspectRatio,
261    /// Color declaration.
262    pub color: Option<ColorMetadata>,
263    /// Chroma sample position.
264    pub chroma_location: ChromaLocation,
265    /// Significant component bits.
266    pub bit_depth: u8,
267    /// Memory plane organization.
268    pub plane_layout: NativePlaneLayout,
269    /// GPU device identity, zero for CPU frames.
270    pub device_identity: u64,
271    /// Decoder or surface-pool generation.
272    pub decoder_generation: u64,
273    /// Physical picture identity within the generation.
274    pub picture_identity: u64,
275}
276
277/// Opaque decoder surface retained until GPU completion.
278pub trait HardwareFrameLease: Debug + Send + Sync + 'static {
279    /// Approximate allocation size charged to the surface budget.
280    fn retained_bytes(&self) -> u64;
281
282    /// Allows the platform interop crate to recover its private lease type.
283    fn as_any(&self) -> &dyn Any;
284
285    /// Converts a retained lease for platform-specific import without exposing the backend type.
286    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
287}
288
289#[cfg(all(target_os = "linux", feature = "hwdecode"))]
290impl HardwareFrameLease for alpha2_ffmpeg_bridge::VulkanFrameLease {
291    fn retained_bytes(&self) -> u64 {
292        self.info().map_or(0, |info| {
293            let pixels = u64::try_from(info.width)
294                .unwrap_or(0)
295                .saturating_mul(u64::try_from(info.height).unwrap_or(0));
296            // An unrecognized layout is charged as 8-bit 4:2:0; the frame is
297            // rejected before import anyway.
298            let numerator = surface_plane_layout(info.software_format)
299                .unwrap_or(NativePlaneLayout::GpuNv12)
300                .bytes_per_pixel_numerator();
301            pixels.saturating_mul(numerator).div_ceil(2)
302        })
303    }
304
305    fn as_any(&self) -> &dyn Any {
306        self
307    }
308
309    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
310        self
311    }
312}
313
314#[cfg(all(target_os = "macos", feature = "hwdecode"))]
315impl HardwareFrameLease for alpha2_ffmpeg_bridge::VideoToolboxFrameLease {
316    fn retained_bytes(&self) -> u64 {
317        self.info().map_or(0, |info| {
318            let pixels = u64::try_from(info.width)
319                .unwrap_or(0)
320                .saturating_mul(u64::try_from(info.height).unwrap_or(0));
321            // An unrecognized layout is charged as 8-bit 4:2:0; the frame is
322            // rejected before import anyway.
323            let numerator = surface_plane_layout(info.software_format)
324                .unwrap_or(NativePlaneLayout::GpuNv12)
325                .bytes_per_pixel_numerator();
326            pixels.saturating_mul(numerator).div_ceil(2)
327        })
328    }
329
330    fn as_any(&self) -> &dyn Any {
331        self
332    }
333
334    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
335        self
336    }
337}
338
339#[cfg(all(target_os = "windows", feature = "hwdecode"))]
340impl HardwareFrameLease for alpha2_ffmpeg_bridge::D3D12FrameLease {
341    fn retained_bytes(&self) -> u64 {
342        self.info().map_or(0, |info| {
343            let pixels = u64::try_from(info.width)
344                .unwrap_or(0)
345                .saturating_mul(u64::try_from(info.height).unwrap_or(0));
346            // An unrecognized layout is charged as 8-bit 4:2:0; the frame is
347            // rejected before import anyway.
348            let numerator = surface_plane_layout(info.software_format)
349                .unwrap_or(NativePlaneLayout::GpuNv12)
350                .bytes_per_pixel_numerator();
351            pixels.saturating_mul(numerator).div_ceil(2)
352        })
353    }
354
355    fn as_any(&self) -> &dyn Any {
356        self
357    }
358
359    fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
360        self
361    }
362}
363
364/// One decoded export frame with either owned CPU pixels or an opaque surface.
365#[derive(Debug)]
366pub enum NativeVideoFrame {
367    /// Software-decoded frame.
368    Cpu(DecodedVideoFrame),
369    /// Hardware-decoded frame whose platform details remain behind its lease.
370    Hardware {
371        /// Presentation and cache identity.
372        metadata: NativeVideoFrameMetadata,
373        /// Ref-counted decoder-surface ownership.
374        surface: Arc<dyn HardwareFrameLease>,
375    },
376}
377
378impl NativeVideoFrame {
379    /// Presentation timestamp.
380    #[must_use]
381    pub fn pts(&self) -> MediaTime {
382        match self {
383            Self::Cpu(frame) => frame.pts,
384            Self::Hardware { metadata, .. } => metadata.pts,
385        }
386    }
387
388    /// Nominal frame duration when known.
389    #[must_use]
390    pub fn duration(&self) -> Option<MediaTime> {
391        match self {
392            Self::Cpu(frame) => frame.duration,
393            Self::Hardware { metadata, .. } => metadata.duration,
394        }
395    }
396
397    /// Returns metadata without exposing the backing representation.
398    #[must_use]
399    pub fn metadata(&self) -> NativeVideoFrameMetadata {
400        match self {
401            Self::Cpu(frame) => metadata_from_cpu(frame),
402            Self::Hardware { metadata, .. } => metadata.clone(),
403        }
404    }
405
406    /// Whether this frame is backed by a decoder surface rather than CPU pixels.
407    #[must_use]
408    pub const fn is_hardware(&self) -> bool {
409        matches!(self, Self::Hardware { .. })
410    }
411
412    /// Returns the software frame when this is the CPU variant.
413    #[must_use]
414    pub fn as_cpu(&self) -> Option<&DecodedVideoFrame> {
415        match self {
416            Self::Cpu(frame) => Some(frame),
417            Self::Hardware { .. } => None,
418        }
419    }
420
421    /// A CPU copy of this frame: itself, or the decoder surface downloaded through `FFmpeg`.
422    ///
423    /// The fallback for a surface the renderer could not import. Timing comes from this frame's
424    /// metadata; pixels go through the same conversion as software frames.
425    ///
426    /// # Errors
427    /// Returns a native decode error when the surface cannot be downloaded or converted.
428    pub fn to_cpu(&self) -> Result<DecodedVideoFrame, DecodeError> {
429        let (metadata, surface) = match self {
430            Self::Cpu(frame) => return Ok(frame.clone()),
431            Self::Hardware { metadata, surface } => (metadata, surface),
432        };
433        let mut software = download_surface(surface.as_ref())?;
434        // In tick units so the timestamp cannot overflow; the metadata replaces it below.
435        software.set_pts(metadata.pts.ticks());
436        let mut scaler = crate::convert::FrameScaler::default();
437        // The surface's own siting replaces the conversion's below.
438        let mut frame = crate::convert::convert_video_frame(
439            &software,
440            rsmpeg::ffi::AV_CODEC_ID_NONE,
441            crate::convert::tick_base(),
442            metadata.color.unwrap_or_default(),
443            &mut scaler,
444        )?;
445        frame.pts = metadata.pts;
446        frame.duration = metadata.duration;
447        frame.orientation = metadata.orientation;
448        frame.sample_aspect_ratio = metadata.sample_aspect_ratio;
449        frame.chroma_location = metadata.chroma_location;
450        // The conversion keeps the given color unless it repacked RGB under the identity matrix; an
451        // untagged surface stays untagged.
452        if metadata.color.is_none() {
453            frame.color = None;
454        }
455        Ok(frame)
456    }
457
458    /// Approximate retained bytes for separate CPU and surface budgets.
459    #[must_use]
460    pub fn retained_bytes(&self) -> u64 {
461        match self {
462            Self::Cpu(frame) => frame.planes.iter().fold(0_u64, |total, plane| {
463                total.saturating_add(u64::try_from(plane.bytes.len()).unwrap_or(u64::MAX))
464            }),
465            Self::Hardware { surface, .. } => surface.retained_bytes(),
466        }
467    }
468}
469
470/// Downloads a decoder surface of whichever platform lease this build knows.
471#[cfg_attr(
472    not(all(
473        any(target_os = "linux", target_os = "macos", target_os = "windows"),
474        feature = "hwdecode"
475    )),
476    allow(clippy::unnecessary_wraps)
477)]
478fn download_surface(
479    surface: &dyn HardwareFrameLease,
480) -> Result<alpha2_ffmpeg_bridge::DecodedFrame, DecodeError> {
481    let any = surface.as_any();
482    let downloaded: Option<
483        Result<alpha2_ffmpeg_bridge::DecodedFrame, alpha2_ffmpeg_bridge::BridgeError>,
484    > = None;
485    #[cfg(all(target_os = "linux", feature = "hwdecode"))]
486    let downloaded = downloaded.or_else(|| {
487        any.downcast_ref::<alpha2_ffmpeg_bridge::VulkanFrameLease>()
488            .map(alpha2_ffmpeg_bridge::VulkanFrameLease::download)
489    });
490    #[cfg(all(target_os = "macos", feature = "hwdecode"))]
491    let downloaded = downloaded.or_else(|| {
492        any.downcast_ref::<alpha2_ffmpeg_bridge::VideoToolboxFrameLease>()
493            .map(alpha2_ffmpeg_bridge::VideoToolboxFrameLease::download)
494    });
495    #[cfg(all(target_os = "windows", feature = "hwdecode"))]
496    let downloaded = downloaded.or_else(|| {
497        any.downcast_ref::<alpha2_ffmpeg_bridge::D3D12FrameLease>()
498            .map(alpha2_ffmpeg_bridge::D3D12FrameLease::download)
499    });
500    let _ = any;
501    downloaded
502        .ok_or_else(|| {
503            DecodeError::Unsupported("this hardware surface cannot be downloaded".into())
504        })?
505        .map_err(|error| DecodeError::Adapter(error.to_string()))
506}
507
508/// Export decoder producing [`NativeVideoFrame`]s from one `FFmpeg` session.
509///
510/// Software and hardware sessions share demux, seek and timing ([`NativeDecoder`]); the backend
511/// only changes where pixels live.
512pub struct NativeExportDecoder {
513    backend: ExportDecoderBackend,
514    hardware_frames: u64,
515    /// Why the hardware path gave up, once it has.
516    hardware_failure: Option<HardwareDecodeFallback>,
517}
518
519enum ExportDecoderBackend {
520    Software(NativeDecoder),
521    #[cfg(all(target_os = "linux", feature = "hwdecode"))]
522    Vulkan {
523        decoder: NativeDecoder,
524        generation: u64,
525    },
526    #[cfg(all(target_os = "macos", feature = "hwdecode"))]
527    VideoToolbox {
528        decoder: NativeDecoder,
529        generation: u64,
530        device_identity: u64,
531    },
532    #[cfg(all(target_os = "windows", feature = "hwdecode"))]
533    D3D12 {
534        decoder: NativeDecoder,
535        generation: u64,
536        device_identity: u64,
537    },
538}
539
540impl NativeExportDecoder {
541    /// Takes the decode-stage spans accumulated since the last call. Hardware sessions report no
542    /// conversion time; their cost is in `receive_frame`.
543    pub fn take_stage_timings(&mut self) -> DecodeStageTimings {
544        match &mut self.backend {
545            ExportDecoderBackend::Software(decoder) => decoder.take_stage_timings(),
546            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
547            ExportDecoderBackend::Vulkan { decoder, .. } => decoder.take_stage_timings(),
548            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
549            ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.take_stage_timings(),
550            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
551            ExportDecoderBackend::D3D12 { decoder, .. } => decoder.take_stage_timings(),
552        }
553    }
554
555    /// Whether this session currently produces device-resident frames.
556    #[must_use]
557    pub const fn is_hardware(&self) -> bool {
558        match &self.backend {
559            ExportDecoderBackend::Software(_) => false,
560            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
561            ExportDecoderBackend::Vulkan { .. } => true,
562            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
563            ExportDecoderBackend::VideoToolbox { .. } => true,
564            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
565            ExportDecoderBackend::D3D12 { .. } => true,
566        }
567    }
568
569    /// Opens the selected video stream using the software implementation.
570    ///
571    /// # Errors
572    /// Returns a native decode error if the input or selected stream cannot be opened.
573    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, DecodeError> {
574        NativeDecoder::open(path).map(|decoder| Self {
575            backend: ExportDecoderBackend::Software(decoder),
576            hardware_frames: 0,
577            hardware_failure: None,
578        })
579    }
580
581    /// Opens with an explicit codec worker allocation.
582    ///
583    /// # Errors
584    /// Returns a native decode error for invalid worker counts or stream setup failures.
585    pub fn open_with_threads(
586        path: impl AsRef<std::path::Path>,
587        threads: i64,
588    ) -> Result<Self, DecodeError> {
589        NativeDecoder::open_with_threads(path, threads).map(|decoder| Self {
590            backend: ExportDecoderBackend::Software(decoder),
591            hardware_frames: 0,
592            hardware_failure: None,
593        })
594    }
595
596    /// Opens a Vulkan hardware decoder on the renderer's logical device.
597    ///
598    /// # Errors
599    /// Returns a native decode error if the stream lacks a compatible Vulkan configuration or the
600    /// decoder cannot use the shared device.
601    #[cfg(all(target_os = "linux", feature = "hwdecode"))]
602    pub fn open_vulkan(
603        path: impl AsRef<std::path::Path>,
604        threads: i64,
605        context: &SharedVulkanContext,
606        generation: u64,
607    ) -> Result<Self, HardwareOpenError> {
608        let spec = HardwareDecoderSpec {
609            device_type: ffi::AV_HWDEVICE_TYPE_VULKAN,
610            pixel_format: ffi::AV_PIX_FMT_VULKAN,
611            backend: "Vulkan",
612            require_device_context: true,
613            ten_bit: context.supports_ten_bit(),
614            keeps_leading_crop: true,
615            // The device has decode extensions only for these codecs; `FFmpeg` would open a session
616            // for any other and fail on its first packet.
617            hardware_codecs: Some(context.decode_codecs()),
618            get_format: alpha2_ffmpeg_bridge::vulkan_get_format_callback(),
619        };
620        NativeDecoder::open_hardware(path, threads, context.ffmpeg_device(), &spec).map(|decoder| {
621            Self {
622                backend: ExportDecoderBackend::Vulkan {
623                    decoder,
624                    generation,
625                },
626                hardware_frames: 0,
627                hardware_failure: None,
628            }
629        })
630    }
631
632    /// Opens a `VideoToolbox` hardware decoder producing `CoreVideo` surfaces.
633    ///
634    /// # Errors
635    /// Returns a native decode error if the stream cannot negotiate `AV_PIX_FMT_VIDEOTOOLBOX` or
636    /// create a hardware session.
637    #[cfg(all(target_os = "macos", feature = "hwdecode"))]
638    pub fn open_videotoolbox(
639        path: impl AsRef<std::path::Path>,
640        threads: i64,
641        context: &SharedMetalContext,
642        generation: u64,
643    ) -> Result<Self, HardwareOpenError> {
644        // `VideoToolbox` creates its own session rather than adopting the renderer's device; Metal
645        // interop happens at import.
646        let mark = alpha2_ffmpeg_bridge::log_mark();
647        let device = rsmpeg::avutil::AVHWDeviceContext::create(
648            ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
649            None,
650            None,
651            0,
652        )
653        .map_err(crate::convert::adapter_error)
654        .map_err(|error| {
655            HardwareOpenError::new(HardwareDecodeFallback::DecoderOpenFailed, error)
656        })?;
657        let spec = HardwareDecoderSpec {
658            device_type: ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
659            pixel_format: ffi::AV_PIX_FMT_VIDEOTOOLBOX,
660            backend: "VideoToolbox",
661            require_device_context: false,
662            ten_bit: context.supports_ten_bit(),
663            keeps_leading_crop: false,
664            hardware_codecs: Some(context.decode_codecs()),
665            get_format: alpha2_ffmpeg_bridge::videotoolbox_get_format_callback(),
666        };
667        NativeDecoder::open_hardware(path, threads, device, &spec)
668            .map(|decoder| Self {
669                backend: ExportDecoderBackend::VideoToolbox {
670                    decoder,
671                    generation,
672                    device_identity: context.identity().device,
673                },
674                hardware_frames: 0,
675                hardware_failure: None,
676            })
677            .map_err(|mut error| {
678                error.source = with_videotoolbox_log(error.source, mark);
679                error
680            })
681    }
682
683    /// Opens a D3D12VA hardware decoder on the renderer's D3D12 device.
684    ///
685    /// # Errors
686    /// Returns a native decode error if the stream lacks a compatible D3D12VA configuration or the
687    /// shared device cannot create a decode session.
688    #[cfg(all(target_os = "windows", feature = "hwdecode"))]
689    pub fn open_d3d12(
690        path: impl AsRef<std::path::Path>,
691        threads: i64,
692        context: &SharedD3D12Context,
693        generation: u64,
694    ) -> Result<Self, HardwareOpenError> {
695        let spec = HardwareDecoderSpec {
696            device_type: ffi::AV_HWDEVICE_TYPE_D3D12VA,
697            pixel_format: ffi::AV_PIX_FMT_D3D12,
698            backend: "D3D12VA",
699            require_device_context: true,
700            ten_bit: context.supports_ten_bit(),
701            keeps_leading_crop: true,
702            hardware_codecs: None,
703            get_format: alpha2_ffmpeg_bridge::d3d12_get_format_callback(),
704        };
705        NativeDecoder::open_hardware(path, threads, context.ffmpeg_device(), &spec).map(|decoder| {
706            Self {
707                backend: ExportDecoderBackend::D3D12 {
708                    decoder,
709                    generation,
710                    device_identity: context.identity().device,
711                },
712                hardware_frames: 0,
713                hardware_failure: None,
714            }
715        })
716    }
717
718    /// Stream descriptions from the common decode session.
719    #[must_use]
720    pub fn streams(&self) -> &[StreamInfo] {
721        match &self.backend {
722            ExportDecoderBackend::Software(decoder) => decoder.streams(),
723            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
724            ExportDecoderBackend::Vulkan { decoder, .. } => decoder.streams(),
725            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
726            ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.streams(),
727            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
728            ExportDecoderBackend::D3D12 { decoder, .. } => decoder.streams(),
729        }
730    }
731
732    /// What the decoder leaves out ([`crate::NativeDecoder::dropped_layer`]); hardware sessions
733    /// never open for alpha streams, so they drop nothing.
734    #[must_use]
735    pub fn dropped_layer(&self) -> Option<&crate::DroppedLayer> {
736        match &self.backend {
737            ExportDecoderBackend::Software(decoder) => decoder.dropped_layer(),
738            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
739            ExportDecoderBackend::Vulkan { .. } => None,
740            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
741            ExportDecoderBackend::VideoToolbox { .. } => None,
742            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
743            ExportDecoderBackend::D3D12 { .. } => None,
744        }
745    }
746
747    /// Effective end of the selected video source.
748    #[must_use]
749    pub fn video_duration(&self) -> Option<MediaTime> {
750        match &self.backend {
751            ExportDecoderBackend::Software(decoder) => decoder.video_duration(),
752            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
753            ExportDecoderBackend::Vulkan { decoder, .. } => decoder.video_duration(),
754            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
755            ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.video_duration(),
756            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
757            ExportDecoderBackend::D3D12 { decoder, .. } => decoder.video_duration(),
758        }
759    }
760
761    /// Resets codec and demux state.
762    ///
763    /// # Errors
764    /// Returns a native decode error if the codec state cannot be reset.
765    pub fn flush(&mut self) -> Result<(), DecodeError> {
766        match &mut self.backend {
767            ExportDecoderBackend::Software(decoder) => decoder.flush(),
768            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
769            ExportDecoderBackend::Vulkan { decoder, .. } => decoder.flush(),
770            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
771            ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.flush(),
772            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
773            ExportDecoderBackend::D3D12 { decoder, .. } => decoder.flush(),
774        }
775    }
776
777    /// Seeks and returns the selected native frame.
778    ///
779    /// # Errors
780    /// Returns a native decode error for cancellation, seek, decode, or timestamp failures.
781    pub fn seek_video(
782        &mut self,
783        request: DecodeRequest,
784        cancel: &CancelToken,
785    ) -> Result<NativeVideoFrame, DecodeError> {
786        let result = self.seek_video_on_backend(request, cancel);
787        self.observe(result, |_| true)
788    }
789
790    /// Decodes the next native frame, or `None` at end of stream.
791    ///
792    /// # Errors
793    /// Returns a native decode error for cancellation or codec failures.
794    pub fn decode_video_next(
795        &mut self,
796        cancel: &CancelToken,
797    ) -> Result<Option<NativeVideoFrame>, DecodeError> {
798        let result = self.decode_video_next_on_backend(cancel);
799        self.observe(result, Option::is_some)
800    }
801
802    /// When the frame [`Self::decode_video_next`] returns next is presented, or `None` at the end.
803    ///
804    /// The frame is decoded now and kept, so a caller can find the frame shown at a time
805    /// without consuming the next one.
806    ///
807    /// # Errors
808    /// Returns a native decode error for cancellation or codec failures.
809    pub fn peek_video_pts(
810        &mut self,
811        cancel: &CancelToken,
812    ) -> Result<Option<MediaTime>, DecodeError> {
813        let result = self.native_decoder().peek_raw_pts(cancel);
814        self.observe(result, |_| false)
815    }
816
817    /// Configures corruption handling on the selected native backend.
818    pub fn set_decode_policy(&mut self, policy: crate::DecodePolicy) {
819        self.native_decoder().set_decode_policy(policy);
820    }
821
822    /// Sets the project's declared color for this source ([`NativeDecoder::declare_color`]).
823    ///
824    /// # Errors
825    /// Returns [`HardwareDecodeFallback::UnsupportedColor`] for HDR on a hardware
826    /// session, which presents SDR only.
827    pub fn with_declared_color(
828        mut self,
829        color: Option<ColorMetadata>,
830    ) -> Result<Self, HardwareOpenError> {
831        if self.is_hardware() && color.is_some_and(ColorMetadata::is_hdr) {
832            return Err(HardwareOpenError::new(
833                HardwareDecodeFallback::UnsupportedColor,
834                DecodeError::Unsupported("hardware export currently supports SDR only".into()),
835            ));
836        }
837        self.native_decoder().declare_color(color);
838        Ok(self)
839    }
840
841    /// Reports damage before any recovery or terminal error.
842    pub fn set_decode_reporter(
843        &mut self,
844        reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
845    ) {
846        self.native_decoder().set_decode_reporter(reporter);
847    }
848
849    /// Cumulative corruption diagnostics for this session.
850    pub fn decode_diagnostics(&mut self) -> &crate::DecodeDiagnostics {
851        self.native_decoder().decode_diagnostics()
852    }
853
854    /// The latest timestamp a frame may carry and still be shown at `time`
855    /// ([`NativeDecoder::presentation_deadline`]).
856    #[must_use]
857    pub fn presentation_deadline(&self, time: MediaTime) -> MediaTime {
858        match &self.backend {
859            ExportDecoderBackend::Software(decoder) => decoder,
860            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
861            ExportDecoderBackend::Vulkan { decoder, .. } => decoder,
862            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
863            ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder,
864            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
865            ExportDecoderBackend::D3D12 { decoder, .. } => decoder,
866        }
867        .presentation_deadline(time)
868    }
869
870    fn native_decoder(&mut self) -> &mut NativeDecoder {
871        match &mut self.backend {
872            ExportDecoderBackend::Software(decoder) => decoder,
873            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
874            ExportDecoderBackend::Vulkan { decoder, .. } => decoder,
875            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
876            ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder,
877            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
878            ExportDecoderBackend::D3D12 { decoder, .. } => decoder,
879        }
880    }
881
882    /// Why this session's hardware path gave up, once a call returned [`DecodeError::Unsupported`].
883    #[must_use]
884    pub const fn hardware_failure(&self) -> Option<HardwareDecodeFallback> {
885        self.hardware_failure
886    }
887
888    /// Classifies a hardware session's failures so a caller can fall back.
889    ///
890    /// `FFmpeg` initializes a hardware decoder lazily in `get_format`, so an unsupported profile or
891    /// codec (VP9 without VP9 decode, 10-bit H.264 on NVDEC) fails on the first packet with a
892    /// generic error. Before the first frame such an error becomes [`DecodeError::Unsupported`]
893    /// with [`HardwareDecodeFallback::DecoderOpenFailed`]; afterwards it stays a real failure.
894    fn observe<T>(
895        &mut self,
896        result: Result<T, DecodeError>,
897        produced: impl Fn(&T) -> bool,
898    ) -> Result<T, DecodeError> {
899        if !self.is_hardware() {
900            return result;
901        }
902        match result {
903            Ok(value) => {
904                if produced(&value) {
905                    self.hardware_frames = self.hardware_frames.saturating_add(1);
906                }
907                Ok(value)
908            }
909            Err(DecodeError::Adapter(message)) if self.hardware_frames == 0 => {
910                self.hardware_failure = Some(HardwareDecodeFallback::DecoderOpenFailed);
911                Err(DecodeError::Unsupported(format!(
912                    "the hardware decoder failed before its first frame: {message}"
913                )))
914            }
915            Err(DecodeError::Unsupported(message)) => {
916                self.hardware_failure
917                    .get_or_insert(HardwareDecodeFallback::UnsupportedSurfaceFormat);
918                Err(DecodeError::Unsupported(message))
919            }
920            Err(error) => Err(error),
921        }
922    }
923
924    fn seek_video_on_backend(
925        &mut self,
926        request: DecodeRequest,
927        cancel: &CancelToken,
928    ) -> Result<NativeVideoFrame, DecodeError> {
929        match &mut self.backend {
930            ExportDecoderBackend::Software(decoder) => match decoder.seek(request, cancel)?.event {
931                DecodeEvent::Video(frame) => Ok(NativeVideoFrame::Cpu(frame)),
932                _ => Err(DecodeError::Adapter(
933                    "video decoder returned a non-video event".into(),
934                )),
935            },
936            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
937            ExportDecoderBackend::Vulkan {
938                decoder,
939                generation,
940            } => {
941                let frame = decoder.seek_raw(request.time, cancel)?;
942                hardware_frame(decoder, frame.into_frame(), *generation)
943            }
944            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
945            ExportDecoderBackend::VideoToolbox {
946                decoder,
947                generation,
948                device_identity,
949            } => {
950                let mark = alpha2_ffmpeg_bridge::log_mark();
951                let frame = decoder
952                    .seek_raw(request.time, cancel)
953                    .map_err(|error| with_videotoolbox_log(error, mark))?;
954                videotoolbox_frame(decoder, frame.into_frame(), *generation, *device_identity)
955            }
956            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
957            ExportDecoderBackend::D3D12 {
958                decoder,
959                generation,
960                device_identity,
961            } => {
962                let frame = decoder.seek_raw(request.time, cancel)?;
963                d3d12_frame(
964                    decoder,
965                    frame.into_frame(),
966                    *generation,
967                    *device_identity,
968                    &mut self.hardware_failure,
969                )
970            }
971        }
972    }
973
974    fn decode_video_next_on_backend(
975        &mut self,
976        cancel: &CancelToken,
977    ) -> Result<Option<NativeVideoFrame>, DecodeError> {
978        match &mut self.backend {
979            ExportDecoderBackend::Software(decoder) => match decoder.decode_next(cancel)? {
980                DecodeEvent::Video(frame) => Ok(Some(NativeVideoFrame::Cpu(frame))),
981                DecodeEvent::EndOfStream => Ok(None),
982                _ => Err(DecodeError::Adapter(
983                    "video decoder returned a non-video event".into(),
984                )),
985            },
986            #[cfg(all(target_os = "linux", feature = "hwdecode"))]
987            ExportDecoderBackend::Vulkan {
988                decoder,
989                generation,
990            } => decoder
991                .decode_raw_next(cancel)?
992                .map(|frame| hardware_frame(decoder, frame.into_frame(), *generation))
993                .transpose(),
994            #[cfg(all(target_os = "macos", feature = "hwdecode"))]
995            ExportDecoderBackend::VideoToolbox {
996                decoder,
997                generation,
998                device_identity,
999            } => {
1000                let mark = alpha2_ffmpeg_bridge::log_mark();
1001                decoder
1002                    .decode_raw_next(cancel)
1003                    .map_err(|error| with_videotoolbox_log(error, mark))?
1004                    .map(|frame| {
1005                        videotoolbox_frame(
1006                            decoder,
1007                            frame.into_frame(),
1008                            *generation,
1009                            *device_identity,
1010                        )
1011                    })
1012                    .transpose()
1013            }
1014            #[cfg(all(target_os = "windows", feature = "hwdecode"))]
1015            ExportDecoderBackend::D3D12 {
1016                decoder,
1017                generation,
1018                device_identity,
1019            } => decoder
1020                .decode_raw_next(cancel)?
1021                .map(|frame| {
1022                    d3d12_frame(
1023                        decoder,
1024                        frame.into_frame(),
1025                        *generation,
1026                        *device_identity,
1027                        &mut self.hardware_failure,
1028                    )
1029                })
1030                .transpose(),
1031        }
1032    }
1033}
1034
1035/// A decoded D3D12VA frame as a [`NativeVideoFrame`]. A picture reaching past its surface
1036/// ([`visible_rect_within_surface`]) is refused with [`HardwareDecodeFallback::UnsupportedCrop`] so
1037/// the source continues in software.
1038#[cfg(all(target_os = "windows", feature = "hwdecode"))]
1039fn d3d12_frame(
1040    decoder: &NativeDecoder,
1041    frame: AVFrame,
1042    generation: u64,
1043    device_identity: u64,
1044    failure: &mut Option<HardwareDecodeFallback>,
1045) -> Result<NativeVideoFrame, DecodeError> {
1046    let info = alpha2_ffmpeg_bridge::d3d12_frame_info(&frame)
1047        .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1048    let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1049        DecodeError::Unsupported(format!(
1050            "D3D12VA output {} is not an importable 4:2:0 surface",
1051            info.software_format
1052        ))
1053    })?;
1054    let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
1055    let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
1056    let crop = [
1057        frame.crop_left,
1058        frame.crop_top,
1059        frame.crop_right,
1060        frame.crop_bottom,
1061    ];
1062    let Some(visible_rect) =
1063        visible_rect_within_surface(width, height, crop, info.surface_width, info.surface_height)
1064    else {
1065        *failure = Some(HardwareDecodeFallback::UnsupportedCrop);
1066        return Err(DecodeError::Unsupported(format!(
1067            "D3D12VA decoded a {width}x{height} picture cropped {} columns from the left and {} rows from the top into a {}x{} surface, which does not hold it",
1068            frame.crop_left, frame.crop_top, info.surface_width, info.surface_height
1069        )));
1070    };
1071    let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1072        frame.pts
1073    } else {
1074        frame.best_effort_timestamp
1075    };
1076    let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1077    let metadata = NativeVideoFrameMetadata {
1078        pts,
1079        dts: None,
1080        duration: timestamp(frame.duration, decoder.time_base),
1081        visible_rect,
1082        orientation: decoder.orientation,
1083        sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1084        color: decoder.picture_color(&frame),
1085        chroma_location: decoder.picture_chroma_location(&frame),
1086        bit_depth: plane_layout.bit_depth(),
1087        plane_layout,
1088        device_identity,
1089        decoder_generation: generation,
1090        picture_identity: info.resource
1091            ^ info.fence_value
1092            ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1093    };
1094    Ok(NativeVideoFrame::Hardware {
1095        metadata,
1096        surface: Arc::new(alpha2_ffmpeg_bridge::D3D12FrameLease::new(frame)),
1097    })
1098}
1099
1100/// The log classes of a `VideoToolbox` decode: the codec context (where the hwaccel logs
1101/// `OSStatus`) and the hardware device and frames contexts.
1102#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1103const VIDEOTOOLBOX_DECODE_LOG_CLASSES: &[&str] = &[
1104    "h264",
1105    "hevc",
1106    "vp9",
1107    "av1",
1108    "AVHWDeviceContext",
1109    "AVHWFramesContext",
1110];
1111
1112/// `error` with the `VideoToolbox` lines logged since `mark` appended, so the session's `OSStatus`
1113/// reaches the typed error. Errors without a message are returned unchanged.
1114#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1115fn with_videotoolbox_log(error: DecodeError, mark: alpha2_ffmpeg_bridge::LogMark) -> DecodeError {
1116    let detail = alpha2_ffmpeg_bridge::log_detail(&alpha2_ffmpeg_bridge::log_since(
1117        mark,
1118        VIDEOTOOLBOX_DECODE_LOG_CLASSES,
1119    ));
1120    if detail.is_empty() {
1121        return error;
1122    }
1123    match error {
1124        DecodeError::Adapter(message) => DecodeError::Adapter(format!("{message}{detail}")),
1125        DecodeError::Unsupported(message) => DecodeError::Unsupported(format!("{message}{detail}")),
1126        DecodeError::Gpu { fault, message } => DecodeError::Gpu {
1127            fault,
1128            message: format!("{message}{detail}"),
1129        },
1130        other => other,
1131    }
1132}
1133
1134#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1135fn videotoolbox_frame(
1136    decoder: &NativeDecoder,
1137    frame: AVFrame,
1138    generation: u64,
1139    device_identity: u64,
1140) -> Result<NativeVideoFrame, DecodeError> {
1141    let info = alpha2_ffmpeg_bridge::videotoolbox_frame_info(&frame)
1142        .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1143    let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1144        DecodeError::Unsupported(format!(
1145            "VideoToolbox output {} is not an importable 4:2:0 surface",
1146            info.software_format
1147        ))
1148    })?;
1149    let width = u32::try_from(info.width).map_err(|_| DecodeError::OutOfRange)?;
1150    let height = u32::try_from(info.height).map_err(|_| DecodeError::OutOfRange)?;
1151    let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1152        frame.pts
1153    } else {
1154        frame.best_effort_timestamp
1155    };
1156    let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1157    let metadata = NativeVideoFrameMetadata {
1158        pts,
1159        dts: None,
1160        duration: timestamp(frame.duration, decoder.time_base),
1161        visible_rect: Rect {
1162            x: u32::try_from(frame.crop_left).unwrap_or(0),
1163            y: u32::try_from(frame.crop_top).unwrap_or(0),
1164            width: width.saturating_sub(
1165                u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
1166            ),
1167            height: height.saturating_sub(
1168                u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
1169            ),
1170        },
1171        orientation: decoder.orientation,
1172        sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1173        color: decoder.picture_color(&frame),
1174        chroma_location: decoder.picture_chroma_location(&frame),
1175        bit_depth: plane_layout.bit_depth(),
1176        plane_layout,
1177        device_identity,
1178        decoder_generation: generation,
1179        picture_identity: info.pixel_buffer ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1180    };
1181    Ok(NativeVideoFrame::Hardware {
1182        metadata,
1183        surface: Arc::new(alpha2_ffmpeg_bridge::VideoToolboxFrameLease::new(frame)),
1184    })
1185}
1186
1187#[cfg(all(target_os = "linux", feature = "hwdecode"))]
1188fn hardware_frame(
1189    decoder: &NativeDecoder,
1190    frame: AVFrame,
1191    generation: u64,
1192) -> Result<NativeVideoFrame, DecodeError> {
1193    let info = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
1194        .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1195    let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1196        DecodeError::Unsupported(format!(
1197            "Vulkan output {} is not an importable 4:2:0 surface",
1198            info.software_format
1199        ))
1200    })?;
1201    // For hardware formats `av_frame_apply_cropping` only subtracts right and bottom crop; the
1202    // surface keeps the padded extent, so the visible rectangle sits inside it.
1203    let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
1204    let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
1205    let coded_width = u32::try_from(info.coded_width).map_err(|_| DecodeError::OutOfRange)?;
1206    let coded_height = u32::try_from(info.coded_height).map_err(|_| DecodeError::OutOfRange)?;
1207    if coded_width < width || coded_height < height {
1208        return Err(DecodeError::Adapter(
1209            "Vulkan frame is larger than its allocated surface".into(),
1210        ));
1211    }
1212    let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1213        frame.pts
1214    } else {
1215        frame.best_effort_timestamp
1216    };
1217    let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1218    let metadata = NativeVideoFrameMetadata {
1219        pts,
1220        dts: None,
1221        duration: timestamp(frame.duration, decoder.time_base),
1222        visible_rect: Rect {
1223            x: u32::try_from(frame.crop_left).unwrap_or(0),
1224            y: u32::try_from(frame.crop_top).unwrap_or(0),
1225            width: width.saturating_sub(
1226                u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
1227            ),
1228            height: height.saturating_sub(
1229                u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
1230            ),
1231        },
1232        orientation: decoder.orientation,
1233        sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1234        color: decoder.picture_color(&frame),
1235        chroma_location: decoder.picture_chroma_location(&frame),
1236        bit_depth: plane_layout.bit_depth(),
1237        plane_layout,
1238        device_identity: info.device,
1239        decoder_generation: generation,
1240        picture_identity: info.images[0] ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1241    };
1242    Ok(NativeVideoFrame::Hardware {
1243        metadata,
1244        surface: Arc::new(alpha2_ffmpeg_bridge::VulkanFrameLease::new(frame)),
1245    })
1246}
1247
1248fn metadata_from_cpu(frame: &DecodedVideoFrame) -> NativeVideoFrameMetadata {
1249    let plane_layout = match frame.pixel_format {
1250        PixelFormat::Yuv(format) => NativePlaneLayout::CpuYuv(format),
1251        PixelFormat::LinearRgbaF16 => NativePlaneLayout::CpuLinearRgbaF16,
1252        _ => NativePlaneLayout::CpuRgba8,
1253    };
1254    NativeVideoFrameMetadata {
1255        pts: frame.pts,
1256        dts: frame.dts,
1257        duration: frame.duration,
1258        visible_rect: frame.visible_rect,
1259        orientation: frame.orientation,
1260        sample_aspect_ratio: frame.sample_aspect_ratio,
1261        color: frame.color,
1262        chroma_location: frame.chroma_location,
1263        bit_depth: plane_layout.bit_depth(),
1264        plane_layout,
1265        device_identity: 0,
1266        decoder_generation: 0,
1267        picture_identity: u64::from_ne_bytes(frame.pts.ticks().to_ne_bytes()),
1268    }
1269}