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