Skip to main content

alpha2_ffmpeg_bridge/
lib.rs

1//! The `unsafe` boundary between the native binding and `FFmpeg`.
2//!
3//! It holds what rsmpeg's safe surface lacks (frame and packet copies, codec and side-data helpers,
4//! container I/O, hardware-frame shims) so the rest of the binding can forbid `unsafe`.
5
6use std::{error::Error, ffi::CStr, fmt, ptr, slice};
7
8#[cfg(all(target_os = "linux", feature = "hwdevice"))]
9use std::ffi::CString;
10
11#[cfg(all(
12    any(target_os = "linux", target_os = "macos", target_os = "windows"),
13    feature = "hwdevice"
14))]
15use std::sync::Mutex;
16#[cfg(all(target_os = "linux", feature = "hwdevice"))]
17use std::sync::MutexGuard;
18
19pub use alpha2_media::GpuFault;
20use rsmpeg::{
21    avcodec::{AVCodecContext, AVCodecParameters},
22    avutil::AVFrame,
23    error::RsmpegError,
24    ffi,
25    swscale::SwsContext,
26};
27
28#[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "hwdevice"))]
29use rsmpeg::avutil::AVHWDeviceContext;
30
31mod container;
32mod log;
33
34pub use container::{
35    CreateOutputError, DemuxInput, OpenInputError, OutputFile, PathError, file_url, open_input,
36};
37pub use log::{LogLine, LogMark, install_log_capture, log_detail, log_mark, log_since};
38
39/// The C shims' ABI, generated from `include/` by the build script.
40#[allow(
41    dead_code,
42    non_camel_case_types,
43    unreachable_pub,
44    clippy::all,
45    clippy::pedantic,
46    clippy::undocumented_unsafe_blocks
47)]
48mod interop {
49    include!(concat!(env!("OUT_DIR"), "/interop.rs"));
50}
51
52// Pins the layouts of the D3D12 and VideoToolbox interop structs so a header edit fails on every
53// target, not only where those shims compile.
54const _: () = {
55    use interop::{Alpha2D3D12FrameInfo as D3D12, Alpha2VideoToolboxFrameInfo as VideoToolbox};
56    use std::mem::{align_of, offset_of, size_of};
57    assert!(size_of::<D3D12>() == 64 && align_of::<D3D12>() == 8);
58    assert!(offset_of!(D3D12, resource) == 0);
59    assert!(offset_of!(D3D12, fence) == 8);
60    assert!(offset_of!(D3D12, fence_value) == 16);
61    assert!(offset_of!(D3D12, device) == 24);
62    assert!(offset_of!(D3D12, dxgi_format) == 32);
63    assert!(offset_of!(D3D12, width) == 36);
64    assert!(offset_of!(D3D12, height) == 40);
65    assert!(offset_of!(D3D12, hardware_format) == 44);
66    assert!(offset_of!(D3D12, software_format) == 48);
67    assert!(offset_of!(D3D12, surface_width) == 52);
68    assert!(offset_of!(D3D12, surface_height) == 56);
69    assert!(size_of::<VideoToolbox>() == 24 && align_of::<VideoToolbox>() == 8);
70    assert!(offset_of!(VideoToolbox, pixel_buffer) == 0);
71    assert!(offset_of!(VideoToolbox, width) == 8);
72    assert!(offset_of!(VideoToolbox, height) == 12);
73    assert!(offset_of!(VideoToolbox, hardware_format) == 16);
74    assert!(offset_of!(VideoToolbox, software_format) == 20);
75};
76
77/// Copies a hardware frame into a software frame in its `sw_format`, keeping timestamps, crop and
78/// color properties. Used for surfaces the renderer could not import.
79#[cfg(all(
80    any(target_os = "linux", target_os = "macos", target_os = "windows"),
81    feature = "hwdevice"
82))]
83// A frames context's buffer data is the `AVHWFramesContext` FFmpeg allocated, aligned for it.
84#[allow(clippy::cast_ptr_alignment)]
85fn download_hardware_frame(frame: &AVFrame) -> Result<AVFrame, BridgeError> {
86    if frame.hw_frames_ctx.is_null() {
87        return Err(BridgeError::InvalidInput(
88            "hardware frame has no frames context",
89        ));
90    }
91    // SAFETY: a non-null `hw_frames_ctx` is a reference the frame holds to its live
92    // `AVHWFramesContext`; only its size is read.
93    let written = unsafe {
94        let frames = (*frame.hw_frames_ctx).data.cast::<ffi::AVHWFramesContext>();
95        ((*frames).width, (*frames).height)
96    };
97    let mut software = AVFrame::new();
98    software
99        .hwframe_transfer_data(frame)
100        .map_err(BridgeError::Ffmpeg)?;
101    // The copy's row padding is not zeroed and some transfers write only the frames context's
102    // width, but `DecodedFrame::yuv_planes` shares whole rows.
103    zero_row_padding(&mut software, written.0, written.1)?;
104    // SAFETY: both frames are live; `av_frame_copy_props` only reads `frame` and writes metadata
105    // into `software`.
106    let copied = unsafe { ffi::av_frame_copy_props(software.as_mut_ptr(), frame.as_ptr()) };
107    if copied < 0 {
108        return Err(BridgeError::FfmpegCode(copied));
109    }
110    Ok(software)
111}
112
113/// Zeroes each row of the first `height` rows past `width` pixels, up to the linesize.
114///
115/// `frame` must be the only owner of its buffers.
116///
117/// # Errors
118/// Returns an error for a format without a descriptor, a negative size, or rows outside the
119/// frame's buffers.
120#[cfg_attr(not(feature = "hwdevice"), allow(dead_code))]
121fn zero_row_padding(frame: &mut AVFrame, width: i32, height: i32) -> Result<(), BridgeError> {
122    let format = frame.format;
123    // SAFETY: av_pix_fmt_desc_get only reads the static descriptor table; null for unknown formats.
124    let descriptor = unsafe { ffi::av_pix_fmt_desc_get(format).as_ref() }
125        .ok_or(BridgeError::InvalidInput("pixel format has no descriptor"))?;
126    if width < 0 || height < 0 {
127        return Err(BridgeError::InvalidInput("negative frame size"));
128    }
129    // SAFETY: as above.
130    let planes = unsafe { ffi::av_pix_fmt_count_planes(format) };
131    for plane in 0..usize::try_from(planes).unwrap_or(0).min(4) {
132        let plane_index = i32::try_from(plane).unwrap_or(0);
133        // SAFETY: av_image_get_linesize only reads the descriptor table.
134        let written = unsafe { ffi::av_image_get_linesize(format, width, plane_index) };
135        let (Ok(written), Ok(stride)) = (
136            usize::try_from(written),
137            usize::try_from(frame.linesize[plane]),
138        ) else {
139            continue;
140        };
141        let data = frame.data[plane];
142        if data.is_null() || stride <= written {
143            continue;
144        }
145        let shift = if plane == 1 || plane == 2 {
146            descriptor.log2_chroma_h
147        } else {
148            0
149        };
150        let rows = usize::try_from(-((-height) >> shift)).unwrap_or(0);
151        let start = data as usize;
152        let end = stride
153            .checked_mul(rows)
154            .and_then(|len| start.checked_add(len))
155            .ok_or(BridgeError::InvalidInput("frame plane size overflow"))?;
156        let inside = frame.buf.iter().any(|buffer| {
157            // SAFETY: a non-null `AVFrame::buf` entry is a live `AVBufferRef` the frame holds.
158            unsafe { buffer.as_ref() }.is_some_and(|buffer| {
159                let buffer_start = buffer.data as usize;
160                buffer_start <= start && buffer_start.saturating_add(buffer.size) >= end
161            })
162        });
163        if !inside {
164            return Err(BridgeError::InvalidInput(
165                "frame plane lies outside its buffers",
166            ));
167        }
168        for row in 0..rows {
169            // SAFETY: the check above puts every row inside a frame buffer the caller solely owns.
170            unsafe { ptr::write_bytes(data.add(row * stride + written), 0, stride - written) };
171        }
172    }
173    Ok(())
174}
175
176#[cfg(all(target_os = "windows", feature = "hwdevice"))]
177use interop::Alpha2D3D12FrameInfo as D3D12FrameDescriptor;
178
179#[cfg(all(target_os = "windows", feature = "hwdevice"))]
180unsafe extern "C" {
181    fn alpha2_d3d12_device_init(
182        context: *mut ffi::AVHWDeviceContext,
183        device: *mut std::ffi::c_void,
184    ) -> i32;
185    fn alpha2_d3d12_get_format(
186        context: *mut ffi::AVCodecContext,
187        formats: *const ffi::AVPixelFormat,
188    ) -> ffi::AVPixelFormat;
189    fn alpha2_d3d12_frame_info(
190        frame: *const ffi::AVFrame,
191        output: *mut D3D12FrameDescriptor,
192    ) -> i32;
193    fn alpha2_d3d12_frame_set_fence_value(frame: *mut ffi::AVFrame, value: u64) -> i32;
194    fn alpha2_d3d12_frame_fence_value(frame: *const ffi::AVFrame, output: *mut u64) -> i32;
195    fn alpha2_d3d12_frames_configure(
196        frames: *mut ffi::AVBufferRef,
197        dxgi_format: u32,
198        resource_flags: u32,
199    ) -> i32;
200}
201
202/// `DXGI_FORMAT_NV12`, the D3D12VA encoder-input texture format.
203#[cfg(all(target_os = "windows", feature = "hwdevice"))]
204pub const DXGI_FORMAT_NV12: u32 = 103;
205
206/// Sets a D3D12VA frames context's `dxgi_format` and `resource_flags`, with one texture per frame.
207///
208/// Encoder input needs one texture per frame because each frame has its own fence and a copy
209/// addresses its planes as subresources 0 and 1. Call between `av_hwframe_ctx_alloc` and
210/// `av_hwframe_ctx_init`.
211///
212/// # Errors
213/// Returns an error when the context is not a D3D12VA one.
214#[cfg(all(target_os = "windows", feature = "hwdevice"))]
215pub fn d3d12_frames_configure(
216    frames: &mut rsmpeg::avutil::AVHWFramesContext,
217    dxgi_format: u32,
218    resource_flags: u32,
219) -> Result<(), BridgeError> {
220    // SAFETY: `frames` is live; the shim checks the device type before writing D3D12VA fields.
221    let status =
222        unsafe { alpha2_d3d12_frames_configure(frames.as_mut_ptr(), dxgi_format, resource_flags) };
223    if status < 0 {
224        return Err(BridgeError::FfmpegCode(status));
225    }
226    Ok(())
227}
228
229/// D3D12 resource and decode fence retained by one hardware frame.
230///
231/// Defined on every target so a change to the shim's record fails to compile everywhere.
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub struct D3D12FrameInfo {
234    /// Borrowed `ID3D12Resource` pointer.
235    pub resource: u64,
236    /// Borrowed decode `ID3D12Fence` pointer.
237    pub fence: u64,
238    /// Fence value signaled after decode completes.
239    pub fence_value: u64,
240    /// `ID3D12Device` identity that allocated the resource.
241    pub device: u64,
242    /// Native `DXGI_FORMAT` value.
243    pub dxgi_format: u32,
244    /// Width with the right crop applied but not the left.
245    pub width: i32,
246    /// Height with the bottom crop applied but not the top.
247    pub height: i32,
248    /// Software pixel format represented by the hardware frame.
249    pub software_format: i32,
250    /// Width of the texture the frame was decoded into.
251    pub surface_width: u32,
252    /// Height of the texture the frame was decoded into.
253    pub surface_height: u32,
254}
255
256impl D3D12FrameInfo {
257    #[cfg_attr(
258        not(all(target_os = "windows", feature = "hwdevice")),
259        allow(dead_code)
260    )]
261    const fn from_descriptor(descriptor: &interop::Alpha2D3D12FrameInfo) -> Self {
262        Self {
263            resource: descriptor.resource,
264            fence: descriptor.fence,
265            fence_value: descriptor.fence_value,
266            device: descriptor.device,
267            dxgi_format: descriptor.dxgi_format,
268            width: descriptor.width,
269            height: descriptor.height,
270            software_format: descriptor.software_format,
271            surface_width: descriptor.surface_width,
272            surface_height: descriptor.surface_height,
273        }
274    }
275}
276
277/// Ref-counted ownership of one opaque D3D12 `AVFrame`.
278#[cfg(all(target_os = "windows", feature = "hwdevice"))]
279pub struct D3D12FrameLease {
280    frame: Mutex<AVFrame>,
281}
282
283#[cfg(all(target_os = "windows", feature = "hwdevice"))]
284impl fmt::Debug for D3D12FrameLease {
285    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
286        formatter
287            .debug_struct("D3D12FrameLease")
288            .finish_non_exhaustive()
289    }
290}
291
292#[cfg(all(target_os = "windows", feature = "hwdevice"))]
293impl D3D12FrameLease {
294    /// Takes ownership of one decoded `AV_PIX_FMT_D3D12` frame.
295    #[must_use]
296    pub fn new(frame: AVFrame) -> Self {
297        Self {
298            frame: Mutex::new(frame),
299        }
300    }
301
302    /// Copies this frame's pixels to the CPU. Row padding is zeroed, so the planes can be shared.
303    ///
304    /// # Errors
305    /// Returns [`BridgeError`] when `FFmpeg` cannot transfer the frame.
306    pub fn download(&self) -> Result<DecodedFrame, BridgeError> {
307        let frame = self
308            .frame
309            .lock()
310            .unwrap_or_else(std::sync::PoisonError::into_inner);
311        download_hardware_frame(&frame).map(|frame| DecodedFrame {
312            frame,
313            written: true,
314        })
315    }
316
317    /// Returns the retained frame's resource and synchronization descriptor.
318    ///
319    /// # Errors
320    /// Returns an error when this is not a valid D3D12 hardware frame.
321    pub fn info(&self) -> Result<D3D12FrameInfo, BridgeError> {
322        let frame = self
323            .frame
324            .lock()
325            .unwrap_or_else(std::sync::PoisonError::into_inner);
326        d3d12_frame_info(&frame)
327    }
328}
329
330/// Initializes an `FFmpeg` D3D12VA device context over the renderer's device.
331///
332/// # Safety
333/// `device` must be the address of a live `ID3D12Device`. The context takes its own COM reference.
334///
335/// # Errors
336/// Returns an error if the pointer is null or `FFmpeg` rejects the device.
337#[cfg(all(target_os = "windows", feature = "hwdevice"))]
338#[allow(clippy::cast_ptr_alignment)]
339pub unsafe fn create_d3d12_device_context(device: u64) -> Result<AVHWDeviceContext, BridgeError> {
340    let device = usize::try_from(device)
341        .map_err(|_| BridgeError::InvalidInput("invalid D3D12 device pointer"))?
342        as *mut std::ffi::c_void;
343    if device.is_null() {
344        return Err(BridgeError::InvalidInput("D3D12 device is null"));
345    }
346    let mut context = alloc_hwdevice(ffi::AV_HWDEVICE_TYPE_D3D12VA, "D3D12")?;
347    let device_context = context.data.cast::<ffi::AVHWDeviceContext>();
348    // SAFETY: the allocated context is writable, the caller guarantees `device` is a live
349    // `ID3D12Device`, and the shim takes its own COM reference.
350    let result = unsafe { alpha2_d3d12_device_init(device_context, device) };
351    if result < 0 {
352        return Err(BridgeError::FfmpegCode(result));
353    }
354    context.init().map_err(BridgeError::Ffmpeg)?;
355    Ok(context)
356}
357
358/// Returns the pixel-format selector used by D3D12VA decoders.
359#[cfg(all(target_os = "windows", feature = "hwdevice"))]
360#[must_use]
361pub fn d3d12_get_format_callback()
362-> unsafe extern "C" fn(*mut ffi::AVCodecContext, *const ffi::AVPixelFormat) -> ffi::AVPixelFormat {
363    alpha2_d3d12_get_format
364}
365
366/// Reads one retained D3D12 frame descriptor.
367///
368/// # Errors
369/// Returns an error if the frame is not a valid D3D12VA frame.
370#[cfg(all(target_os = "windows", feature = "hwdevice"))]
371pub fn d3d12_frame_info(frame: &AVFrame) -> Result<D3D12FrameInfo, BridgeError> {
372    let mut descriptor = D3D12FrameDescriptor::default();
373    // SAFETY: the frame remains live for the duration of the descriptor copy.
374    let result = unsafe { alpha2_d3d12_frame_info(frame.as_ptr(), &raw mut descriptor) };
375    if result != 0 {
376        return Err(BridgeError::FfmpegCode(result));
377    }
378    Ok(D3D12FrameInfo::from_descriptor(&descriptor))
379}
380
381/// The fence value `FFmpeg` waits for before next using the frame (`sync_ctx.fence_value`).
382///
383/// # Errors
384/// Returns an error if the frame is not a valid D3D12VA frame.
385#[cfg(all(target_os = "windows", feature = "hwdevice"))]
386pub fn d3d12_frame_fence_value(frame: &AVFrame) -> Result<u64, BridgeError> {
387    let mut value = 0;
388    // SAFETY: the frame is live for the call and the output is a writable local.
389    let result = unsafe { alpha2_d3d12_frame_fence_value(frame.as_ptr(), &raw mut value) };
390    if result != 0 {
391        return Err(BridgeError::FfmpegCode(result));
392    }
393    Ok(value)
394}
395
396/// One encoder-pool D3D12VA frame held open for a native GPU write.
397///
398/// The encoder waits on the frame's fence for `sync_ctx.fence_value` before reading it, so after a
399/// fenced copy is submitted, [`Self::commit`] records the value it signals. A guard dropped without
400/// a commit leaves the frame unchanged.
401#[cfg(all(target_os = "windows", feature = "hwdevice"))]
402pub struct D3D12EncodeFrameAccess<'a> {
403    frame: &'a mut AVFrame,
404    info: D3D12FrameInfo,
405}
406
407#[cfg(all(target_os = "windows", feature = "hwdevice"))]
408impl fmt::Debug for D3D12EncodeFrameAccess<'_> {
409    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
410        formatter
411            .debug_struct("D3D12EncodeFrameAccess")
412            .field("info", &self.info)
413            .finish_non_exhaustive()
414    }
415}
416
417#[cfg(all(target_os = "windows", feature = "hwdevice"))]
418impl<'a> D3D12EncodeFrameAccess<'a> {
419    /// Opens one handoff on an encoder-pool frame.
420    ///
421    /// # Errors
422    /// Returns an error when the frame is not a usable D3D12VA frame.
423    pub fn begin(frame: &'a mut AVFrame) -> Result<Self, BridgeError> {
424        let info = d3d12_frame_info(frame)?;
425        Ok(Self { frame, info })
426    }
427
428    /// The frame's texture, fence and fence value as `begin` read them.
429    #[must_use]
430    pub const fn info(&self) -> &D3D12FrameInfo {
431        &self.info
432    }
433
434    /// Retains the frame's buffers and hardware pool beyond this access guard.
435    ///
436    /// # Errors
437    /// Returns an allocation error if `FFmpeg` cannot create the reference.
438    pub fn retain_frame(&self) -> Result<AVFrame, BridgeError> {
439        // SAFETY: the frame is live; av_frame_clone makes an independent AVFrame with buffer refs.
440        let raw = unsafe { ffi::av_frame_clone(self.frame.as_ptr()) };
441        let raw = std::ptr::NonNull::new(raw)
442            .ok_or(BridgeError::InvalidInput("cannot retain encode frame"))?;
443        // SAFETY: the newly allocated frame is uniquely owned by this value.
444        Ok(unsafe { AVFrame::from_raw(raw) })
445    }
446
447    /// Records the fence value a submitted write into the frame signals.
448    ///
449    /// # Errors
450    /// Returns an error if the frame is not a valid D3D12VA frame (`begin` already checked).
451    pub fn commit(&mut self, fence_value: u64) -> Result<(), BridgeError> {
452        // SAFETY: the guard borrows the frame exclusively; the shim checks its type before writing.
453        let result =
454            unsafe { alpha2_d3d12_frame_set_fence_value(self.frame.as_mut_ptr(), fence_value) };
455        if result != 0 {
456            return Err(BridgeError::FfmpegCode(result));
457        }
458        self.info.fence_value = fence_value;
459        Ok(())
460    }
461}
462
463#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "hwdevice"))]
464use interop::Alpha2VideoToolboxFrameInfo as VideoToolboxFrameDescriptor;
465
466// The VideoToolbox shim reads only FFmpeg structures, so Linux builds and tests it too.
467#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "hwdevice"))]
468#[cfg_attr(target_os = "linux", allow(dead_code))]
469unsafe extern "C" {
470    fn alpha2_videotoolbox_get_format(
471        context: *mut ffi::AVCodecContext,
472        formats: *const ffi::AVPixelFormat,
473    ) -> ffi::AVPixelFormat;
474    fn alpha2_videotoolbox_frame_info(
475        frame: *const ffi::AVFrame,
476        output: *mut VideoToolboxFrameDescriptor,
477    ) -> i32;
478}
479
480/// `CoreVideo` pixel-buffer identity retained by one `VideoToolbox` frame.
481///
482/// Defined on every target, like [`D3D12FrameInfo`].
483#[derive(Clone, Copy, Debug, Eq, PartialEq)]
484pub struct VideoToolboxFrameInfo {
485    /// Borrowed `CVPixelBufferRef`, valid while the frame lease is retained.
486    pub pixel_buffer: u64,
487    /// Coded width reported by `FFmpeg`.
488    pub width: i32,
489    /// Coded height reported by `FFmpeg`.
490    pub height: i32,
491    /// Sample layout the frames context allocated, such as NV12 or P010.
492    pub software_format: i32,
493}
494
495impl VideoToolboxFrameInfo {
496    #[cfg_attr(
497        not(all(any(target_os = "linux", target_os = "macos"), feature = "hwdevice")),
498        allow(dead_code)
499    )]
500    const fn from_descriptor(descriptor: &interop::Alpha2VideoToolboxFrameInfo) -> Self {
501        Self {
502            pixel_buffer: descriptor.pixel_buffer,
503            width: descriptor.width,
504            height: descriptor.height,
505            software_format: descriptor.software_format,
506        }
507    }
508}
509
510/// Reads a `VideoToolbox` frame's descriptor, refusing other frames.
511#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "hwdevice"))]
512#[cfg_attr(target_os = "linux", allow(dead_code))]
513fn read_videotoolbox_frame_info(frame: &AVFrame) -> Result<VideoToolboxFrameInfo, BridgeError> {
514    let mut descriptor = VideoToolboxFrameDescriptor::default();
515    // SAFETY: `frame` is live for the copy; the shim checks each pointer before reading through it.
516    let result = unsafe { alpha2_videotoolbox_frame_info(frame.as_ptr(), &raw mut descriptor) };
517    if result != 0 {
518        return Err(BridgeError::InvalidInput(
519            "frame is not a VideoToolbox frame with a pixel buffer",
520        ));
521    }
522    Ok(VideoToolboxFrameInfo::from_descriptor(&descriptor))
523}
524
525/// Ref-counted ownership of one opaque `VideoToolbox` `AVFrame`.
526#[cfg(all(target_os = "macos", feature = "hwdevice"))]
527pub struct VideoToolboxFrameLease {
528    frame: Mutex<AVFrame>,
529}
530
531#[cfg(all(target_os = "macos", feature = "hwdevice"))]
532impl fmt::Debug for VideoToolboxFrameLease {
533    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
534        formatter
535            .debug_struct("VideoToolboxFrameLease")
536            .finish_non_exhaustive()
537    }
538}
539
540#[cfg(all(target_os = "macos", feature = "hwdevice"))]
541impl VideoToolboxFrameLease {
542    /// Takes ownership of one decoded `AV_PIX_FMT_VIDEOTOOLBOX` frame.
543    #[must_use]
544    pub fn new(frame: AVFrame) -> Self {
545        Self {
546            frame: Mutex::new(frame),
547        }
548    }
549
550    /// Copies this frame's pixels to the CPU. Row padding is zeroed, so the planes can be shared.
551    ///
552    /// # Errors
553    /// Returns [`BridgeError`] when `FFmpeg` cannot transfer the frame.
554    pub fn download(&self) -> Result<DecodedFrame, BridgeError> {
555        let frame = self
556            .frame
557            .lock()
558            .unwrap_or_else(std::sync::PoisonError::into_inner);
559        download_hardware_frame(&frame).map(|frame| DecodedFrame {
560            frame,
561            written: true,
562        })
563    }
564
565    /// Returns the retained frame's `CoreVideo` surface identity.
566    ///
567    /// # Errors
568    /// Returns an error when the frame is not a `VideoToolbox` hardware frame.
569    pub fn info(&self) -> Result<VideoToolboxFrameInfo, BridgeError> {
570        let frame = self
571            .frame
572            .lock()
573            .unwrap_or_else(std::sync::PoisonError::into_inner);
574        read_videotoolbox_frame_info(&frame)
575    }
576}
577
578/// Returns the pixel-format selector used by `VideoToolbox` decoders.
579#[cfg(all(target_os = "macos", feature = "hwdevice"))]
580#[must_use]
581pub fn videotoolbox_get_format_callback()
582-> unsafe extern "C" fn(*mut ffi::AVCodecContext, *const ffi::AVPixelFormat) -> ffi::AVPixelFormat {
583    alpha2_videotoolbox_get_format
584}
585
586/// Reads the `CoreVideo` surface identity without taking ownership of the frame.
587///
588/// # Errors
589/// Returns an error when the frame is not a `VideoToolbox` hardware frame.
590#[cfg(all(target_os = "macos", feature = "hwdevice"))]
591pub fn videotoolbox_frame_info(frame: &AVFrame) -> Result<VideoToolboxFrameInfo, BridgeError> {
592    read_videotoolbox_frame_info(frame)
593}
594
595/// Maximum number of Vulkan images described by an `FFmpeg` hardware frame.
596#[cfg(all(target_os = "linux", feature = "hwdevice"))]
597pub const MAX_VULKAN_PLANES: usize = interop::ALPHA2_MAX_VULKAN_PLANES as usize;
598
599/// Raw shared-device description used to initialize `FFmpeg`'s Vulkan context.
600#[cfg(all(target_os = "linux", feature = "hwdevice"))]
601#[derive(Clone, Debug)]
602pub struct VulkanDeviceInfo {
603    /// Raw `VkInstance`.
604    pub instance: u64,
605    /// Raw `VkPhysicalDevice`.
606    pub physical_device: u64,
607    /// Raw `VkDevice`.
608    pub device: u64,
609    /// Queue family used by wgpu and native barriers.
610    pub graphics_queue_family: u32,
611    /// Enabled graphics queues in that family.
612    pub graphics_queue_count: u32,
613    /// Dedicated video decode queue family.
614    pub decode_queue_family: u32,
615    /// Enabled decode queues in that family.
616    pub decode_queue_count: u32,
617    /// Vulkan video codec-operation flags.
618    pub decode_video_caps: u32,
619    /// Dedicated video encode queue family. Ignored when the count is zero.
620    pub encode_queue_family: u32,
621    /// Enabled encode queues in that family, or zero when this device has none.
622    pub encode_queue_count: u32,
623    /// Vulkan video encode codec-operation flags.
624    pub encode_video_caps: u32,
625    /// Enabled core feature bits required by `FFmpeg`'s context contract.
626    pub core_features: [u32; 4],
627    /// Whether `synchronization2` is enabled on the device.
628    pub synchronization2: u32,
629    /// Whether `samplerYcbcrConversion` is enabled on the device.
630    pub sampler_ycbcr_conversion: u32,
631    /// Whether `videoMaintenance1` is enabled on the device.
632    pub video_maintenance1: u32,
633    /// Whether `videoDecodeVP9` (`VK_KHR_video_decode_vp9`) is enabled on the device.
634    pub video_decode_vp9: u32,
635    /// Enabled instance extension names.
636    pub instance_extensions: Vec<String>,
637    /// Enabled device extension names.
638    pub device_extensions: Vec<String>,
639}
640
641#[cfg(all(target_os = "linux", feature = "hwdevice"))]
642use interop::Alpha2VulkanDeviceInfo as VulkanDeviceDescriptor;
643
644/// Serializes one shared Vulkan queue family across `FFmpeg` and the renderer.
645///
646/// Takes the same recursive lock `FFmpeg` takes around its submissions. Only contexts made by
647/// [`create_vulkan_device_context`] have it.
648#[cfg(all(target_os = "linux", feature = "hwdevice"))]
649pub struct VulkanQueueLock {
650    device: AVHWDeviceContext,
651    queue_family: u32,
652}
653
654// SAFETY: after construction the context is only read to pass its pointer to the shim's lock and
655// unlock, which synchronize on their own mutex. The single owner drops the reference once.
656#[cfg(all(target_os = "linux", feature = "hwdevice"))]
657unsafe impl Sync for VulkanQueueLock {}
658
659#[cfg(all(target_os = "linux", feature = "hwdevice"))]
660impl fmt::Debug for VulkanQueueLock {
661    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
662        formatter
663            .debug_struct("VulkanQueueLock")
664            .field("queue_family", &self.queue_family)
665            .finish_non_exhaustive()
666    }
667}
668
669#[cfg(all(target_os = "linux", feature = "hwdevice"))]
670impl VulkanQueueLock {
671    /// Locks submissions to `queue_family` on `device`.
672    ///
673    /// # Errors
674    /// Returns an input error unless `device` came from [`create_vulkan_device_context`] and has a
675    /// lock for `queue_family`.
676    #[allow(clippy::cast_ptr_alignment)]
677    pub fn new(device: AVHWDeviceContext, queue_family: u32) -> Result<Self, BridgeError> {
678        // SAFETY: `device` keeps the context live; the shim reads only its type, free callback and
679        // its own state.
680        let lockable = unsafe {
681            alpha2_vulkan_queue_lockable(
682                device.data.cast::<ffi::AVHWDeviceContext>().cast_const(),
683                queue_family,
684            )
685        };
686        if lockable == 0 {
687            return Err(BridgeError::InvalidInput(
688                "Vulkan queue lock needs a family of a device the bridge initialized",
689            ));
690        }
691        Ok(Self {
692            device,
693            queue_family,
694        })
695    }
696
697    /// Holds the queue lock until the guard drops; the guard stays on this thread.
698    #[must_use]
699    pub fn lock(&self) -> VulkanQueueGuard<'_> {
700        // SAFETY: the guard is neither Send nor Sync and releases this exactly once.
701        unsafe { self.raw_lock() };
702        VulkanQueueGuard {
703            lock: self,
704            _owning_thread: std::marker::PhantomData,
705        }
706    }
707
708    /// Acquires the queue lock without a guard, for adapters with their own lock/unlock protocol.
709    ///
710    /// # Safety
711    /// The calling thread must release it with exactly one [`Self::raw_unlock`], in reverse order
712    /// of its other holds on this lock.
713    #[allow(clippy::cast_ptr_alignment)]
714    pub unsafe fn raw_lock(&self) {
715        // SAFETY: `new` checked the shim set up this context with a lock for this family, and the
716        // retained context keeps it alive.
717        unsafe {
718            alpha2_vulkan_queue_lock(
719                self.device.data.cast::<ffi::AVHWDeviceContext>(),
720                self.queue_family,
721            );
722        }
723    }
724
725    /// Releases a hold taken by [`Self::raw_lock`].
726    ///
727    /// # Safety
728    /// The calling thread must hold the lock from an unreleased [`Self::raw_lock`], and this must
729    /// be its most recent hold. An unbalanced release lets two submissions reach the queue at once.
730    #[allow(clippy::cast_ptr_alignment)]
731    pub unsafe fn raw_unlock(&self) {
732        // SAFETY: the caller holds this lock on this thread, per the contract above.
733        unsafe {
734            alpha2_vulkan_queue_unlock(
735                self.device.data.cast::<ffi::AVHWDeviceContext>(),
736                self.queue_family,
737            );
738        }
739    }
740}
741
742/// One hold of a [`VulkanQueueLock`], released on drop on the thread that took it.
743#[cfg(all(target_os = "linux", feature = "hwdevice"))]
744#[derive(Debug)]
745pub struct VulkanQueueGuard<'a> {
746    lock: &'a VulkanQueueLock,
747    _owning_thread: std::marker::PhantomData<*const ()>,
748}
749
750#[cfg(all(target_os = "linux", feature = "hwdevice"))]
751impl Drop for VulkanQueueGuard<'_> {
752    fn drop(&mut self) {
753        // SAFETY: `lock` took this hold on this thread (the guard is not Send), and guards drop in
754        // reverse order of creation.
755        unsafe { self.lock.raw_unlock() };
756    }
757}
758
759#[cfg(all(target_os = "linux", feature = "hwdevice"))]
760unsafe extern "C" {
761    fn alpha2_vulkan_queue_lockable(
762        context: *const ffi::AVHWDeviceContext,
763        queue_family: u32,
764    ) -> i32;
765    fn alpha2_vulkan_queue_lock(context: *mut ffi::AVHWDeviceContext, queue_family: u32);
766    fn alpha2_vulkan_queue_unlock(context: *mut ffi::AVHWDeviceContext, queue_family: u32);
767    fn alpha2_vulkan_device_init(
768        context: *mut ffi::AVHWDeviceContext,
769        info: *const VulkanDeviceDescriptor,
770    ) -> i32;
771}
772
773/// Initializes an `FFmpeg` Vulkan device context over an application-owned device.
774///
775/// # Safety
776/// The handles in `info` must be live, belong together, have been created with the queues,
777/// features and extensions `info` names, and outlive the returned context.
778///
779/// # Errors
780/// Returns an input error for invalid extension names or an `FFmpeg` error when the context cannot
781/// be initialized.
782#[cfg(all(target_os = "linux", feature = "hwdevice"))]
783#[allow(clippy::cast_ptr_alignment)]
784pub unsafe fn create_vulkan_device_context(
785    info: &VulkanDeviceInfo,
786) -> Result<AVHWDeviceContext, BridgeError> {
787    let instance = info
788        .instance_extensions
789        .iter()
790        .map(|s| CString::new(s.as_str()))
791        .collect::<Result<Vec<_>, _>>()
792        .map_err(|_| BridgeError::InvalidInput("Vulkan extension contains NUL"))?;
793    let device = info
794        .device_extensions
795        .iter()
796        .map(|s| CString::new(s.as_str()))
797        .collect::<Result<Vec<_>, _>>()
798        .map_err(|_| BridgeError::InvalidInput("Vulkan extension contains NUL"))?;
799    let instance_ptrs = instance.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
800    let device_ptrs = device.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
801    let raw = VulkanDeviceDescriptor {
802        instance: info.instance,
803        physical_device: info.physical_device,
804        device: info.device,
805        graphics_queue_family: info.graphics_queue_family,
806        graphics_queue_count: info.graphics_queue_count,
807        decode_queue_family: info.decode_queue_family,
808        decode_queue_count: info.decode_queue_count,
809        decode_video_caps: info.decode_video_caps,
810        encode_queue_family: info.encode_queue_family,
811        encode_queue_count: info.encode_queue_count,
812        encode_video_caps: info.encode_video_caps,
813        shader_image_gather_extended: info.core_features[0],
814        fragment_stores_and_atomics: info.core_features[1],
815        shader_int64: info.core_features[2],
816        vertex_pipeline_stores_and_atomics: info.core_features[3],
817        synchronization2: info.synchronization2,
818        sampler_ycbcr_conversion: info.sampler_ycbcr_conversion,
819        video_maintenance1: info.video_maintenance1,
820        video_decode_vp9: info.video_decode_vp9,
821        instance_extensions: instance_ptrs.as_ptr(),
822        instance_extension_count: u32::try_from(instance_ptrs.len())
823            .map_err(|_| BridgeError::InvalidInput("too many Vulkan extensions"))?,
824        device_extensions: device_ptrs.as_ptr(),
825        device_extension_count: u32::try_from(device_ptrs.len())
826            .map_err(|_| BridgeError::InvalidInput("too many Vulkan extensions"))?,
827    };
828    let mut context = alloc_hwdevice(ffi::AV_HWDEVICE_TYPE_VULKAN, "Vulkan")?;
829    let device_context = context.data.cast::<ffi::AVHWDeviceContext>();
830    // SAFETY: the context is freshly allocated and writable, the shim copies the extension names,
831    // and the caller guarantees the handles.
832    let result = unsafe { alpha2_vulkan_device_init(device_context, &raw const raw) };
833    if result < 0 {
834        return Err(BridgeError::FfmpegCode(result));
835    }
836    context.init().map_err(BridgeError::Ffmpeg)?;
837    Ok(context)
838}
839
840/// Opaque Vulkan handles and synchronization state for one `FFmpeg` hardware frame.
841///
842/// The caller must not destroy the handles and must retain the source `AVFrame` while using them.
843#[cfg(all(target_os = "linux", feature = "hwdevice"))]
844#[derive(Clone, Debug, Eq, PartialEq)]
845pub struct VulkanFrameInfo {
846    /// `FFmpeg` hardware pixel format.
847    pub hardware_format: i32,
848    /// Software layout represented by the hardware frames context.
849    pub software_format: i32,
850    /// Visible frame width.
851    pub width: i32,
852    /// Visible frame height.
853    pub height: i32,
854    /// Allocated image width, padded to whole macroblocks.
855    pub coded_width: i32,
856    /// Allocated image height, padded to whole macroblocks.
857    pub coded_height: i32,
858    /// Number of non-null Vulkan images.
859    pub plane_count: u32,
860    /// Whether `FFmpeg` allocated the image with sampled-image usage.
861    pub sampled: bool,
862    /// `VkInstance` identity.
863    pub instance: u64,
864    /// `VkPhysicalDevice` identity.
865    pub physical_device: u64,
866    /// `VkDevice` identity.
867    pub device: u64,
868    /// Per-plane `VkImage` identities.
869    pub images: [u64; MAX_VULKAN_PLANES],
870    /// Per-plane Vulkan formats.
871    pub formats: [u32; MAX_VULKAN_PLANES],
872    /// Current per-plane image layouts.
873    pub layouts: [u32; MAX_VULKAN_PLANES],
874    /// Current per-plane access masks.
875    pub access: [u32; MAX_VULKAN_PLANES],
876    /// Per-plane timeline semaphore identities.
877    pub semaphores: [u64; MAX_VULKAN_PLANES],
878    /// Values that an importing submission must wait for.
879    pub semaphore_values: [u64; MAX_VULKAN_PLANES],
880    /// Current image queue-family ownership.
881    pub queue_families: [u32; MAX_VULKAN_PLANES],
882}
883
884/// Vulkan image and synchronization state of one hardware frame, read under the frame lock.
885#[cfg(all(target_os = "linux", feature = "hwdevice"))]
886pub type VulkanFrameDescriptor = interop::Alpha2VulkanFrameInfo;
887
888/// Locked `FFmpeg` Vulkan surface state passed to the native GPU boundary.
889///
890/// Editing a record changes no frame by itself. The record a guard writes back is reachable only
891/// through the `unsafe` `access_mut`, since its values must describe work actually submitted.
892#[cfg(all(target_os = "linux", feature = "hwdevice"))]
893#[repr(transparent)]
894#[derive(Default)]
895pub struct VulkanFrameAccess(interop::Alpha2VulkanFrameAccess);
896
897#[cfg(all(target_os = "linux", feature = "hwdevice"))]
898impl VulkanFrameAccess {
899    /// The frame's images and their synchronization state.
900    #[must_use]
901    pub const fn frame(&self) -> &VulkanFrameDescriptor {
902        &self.0.frame
903    }
904
905    /// Mutable frame state; see [`VulkanFrameAccessGuard::access_mut`] for what it must hold.
906    pub const fn frame_mut(&mut self) -> &mut VulkanFrameDescriptor {
907        &mut self.0.frame
908    }
909
910    /// The frames context's `VkAllocationCallbacks`, or zero.
911    #[must_use]
912    pub const fn allocator(&self) -> u64 {
913        self.0.allocator
914    }
915
916    /// The queue family the frame's images are owned by.
917    #[must_use]
918    pub const fn queue_family(&self) -> u32 {
919        self.0.queue_family
920    }
921
922    /// Array layers of each image.
923    #[must_use]
924    pub const fn layer_count(&self) -> u32 {
925        self.0.layer_count
926    }
927}
928
929#[cfg(all(target_os = "linux", feature = "hwdevice"))]
930unsafe extern "C" {
931    fn alpha2_vulkan_get_format(
932        context: *mut ffi::AVCodecContext,
933        formats: *const ffi::AVPixelFormat,
934    ) -> ffi::AVPixelFormat;
935    fn alpha2_vulkan_frame_info(
936        frame: *const ffi::AVFrame,
937        output: *mut VulkanFrameDescriptor,
938    ) -> i32;
939    fn alpha2_vulkan_frames_add_usage(frames: *mut ffi::AVBufferRef, usage: u32) -> i32;
940    fn alpha2_vulkan_frames_usage(frames: *const ffi::AVBufferRef) -> u32;
941    fn alpha2_vulkan_frame_access_begin(
942        frame: *mut ffi::AVFrame,
943        output: *mut VulkanFrameAccess,
944    ) -> i32;
945    fn alpha2_vulkan_frame_access_end(
946        begun: *const interop::Alpha2VulkanFrameAccess,
947        updated: *const VulkanFrameAccess,
948    );
949}
950
951/// Why the Vulkan shim refused a frame (its own codes, not `FFmpeg`'s).
952#[cfg(all(target_os = "linux", feature = "hwdevice"))]
953const fn vulkan_frame_refusal(status: i32) -> BridgeError {
954    BridgeError::InvalidInput(match status {
955        -1 => "frame is not a Vulkan hardware frame",
956        -2 => "Vulkan frame has no hardware frames or device context",
957        -3 => "Vulkan frame has no images",
958        -4 => "Vulkan frames context has no frame or queue locks, or no layers",
959        -5 => "Vulkan device has no graphics or compute queue",
960        _ => "Vulkan frame could not be read",
961    })
962}
963
964/// Ref-counted ownership of an opaque `FFmpeg` Vulkan frame.
965#[cfg(all(target_os = "linux", feature = "hwdevice"))]
966pub struct VulkanFrameLease {
967    frame: Mutex<AVFrame>,
968}
969
970#[cfg(all(target_os = "linux", feature = "hwdevice"))]
971impl fmt::Debug for VulkanFrameLease {
972    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
973        formatter
974            .debug_struct("VulkanFrameLease")
975            .finish_non_exhaustive()
976    }
977}
978
979#[cfg(all(target_os = "linux", feature = "hwdevice"))]
980impl VulkanFrameLease {
981    /// Takes ownership of one decoded `AV_PIX_FMT_VULKAN` frame.
982    #[must_use]
983    pub fn new(frame: AVFrame) -> Self {
984        Self {
985            frame: Mutex::new(frame),
986        }
987    }
988
989    /// Copies this frame's pixels to the CPU. Row padding is zeroed, so the planes can be shared.
990    ///
991    /// # Errors
992    /// Returns [`BridgeError`] when `FFmpeg` cannot transfer the frame.
993    pub fn download(&self) -> Result<DecodedFrame, BridgeError> {
994        let frame = self
995            .frame
996            .lock()
997            .unwrap_or_else(std::sync::PoisonError::into_inner);
998        download_hardware_frame(&frame).map(|frame| DecodedFrame {
999            frame,
1000            written: true,
1001        })
1002    }
1003
1004    /// Returns a locked snapshot of the frame's native image state.
1005    ///
1006    /// # Errors
1007    /// Returns [`BridgeError`] when the frame or its hardware context is invalid.
1008    pub fn info(&self) -> Result<VulkanFrameInfo, BridgeError> {
1009        let frame = self
1010            .frame
1011            .lock()
1012            .unwrap_or_else(std::sync::PoisonError::into_inner);
1013        vulkan_frame_info(&frame)
1014    }
1015
1016    /// Locks the `FFmpeg` frame and queue for one native GPU handoff.
1017    ///
1018    /// # Errors
1019    /// Returns [`BridgeError`] if the frame has no usable sampled images or queue.
1020    pub fn lock_access(&self) -> Result<VulkanFrameAccessGuard<'_>, BridgeError> {
1021        let mut frame = self
1022            .frame
1023            .lock()
1024            .unwrap_or_else(std::sync::PoisonError::into_inner);
1025        let mut access = VulkanFrameAccess::default();
1026        // SAFETY: the mutex guard borrows the frame exclusively until the end call in Drop.
1027        let result =
1028            unsafe { alpha2_vulkan_frame_access_begin(frame.as_mut_ptr(), &raw mut access) };
1029        if result != 0 {
1030            return Err(vulkan_frame_refusal(result));
1031        }
1032        Ok(VulkanFrameAccessGuard {
1033            _frame: frame,
1034            begun: access.0,
1035            access,
1036            committed: false,
1037        })
1038    }
1039}
1040
1041/// Guard for one locked `FFmpeg` frame and queue handoff.
1042///
1043/// Safe code cannot change what is written back: an unsignaled timeline value hangs later users and
1044/// a wrong layout makes the contents undefined.
1045///
1046/// ```compile_fail,E0133
1047/// # fn forge(lease: &alpha2_ffmpeg_bridge::VulkanFrameLease) {
1048/// let mut guard = lease.lock_access().unwrap();
1049/// guard.access_mut().frame_mut().semaphore_values[0] += 1000;
1050/// guard.commit();
1051/// # }
1052/// ```
1053#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1054pub struct VulkanFrameAccessGuard<'a> {
1055    _frame: MutexGuard<'a, AVFrame>,
1056    // What `begin` locked, which the end call releases and checks the write-back against.
1057    begun: interop::Alpha2VulkanFrameAccess,
1058    access: VulkanFrameAccess,
1059    committed: bool,
1060}
1061
1062#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1063impl VulkanFrameAccessGuard<'_> {
1064    /// The frame's state while its `FFmpeg` locks are held.
1065    #[must_use]
1066    pub const fn access(&self) -> &VulkanFrameAccess {
1067        &self.access
1068    }
1069
1070    /// The record written back on [`Self::commit`]. Planes whose image or semaphore changed and
1071    /// timeline values below the frame's are ignored.
1072    ///
1073    /// # Safety
1074    /// On commit, each layout and access mask must be what the submitted work leaves the image in,
1075    /// and each timeline value one that work submitted before the guard drops signals.
1076    #[must_use]
1077    pub const unsafe fn access_mut(&mut self) -> &mut VulkanFrameAccess {
1078        &mut self.access
1079    }
1080
1081    /// Commits updated synchronization values when the guard is released.
1082    pub fn commit(&mut self) {
1083        self.committed = true;
1084    }
1085}
1086
1087#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1088impl Drop for VulkanFrameAccessGuard<'_> {
1089    fn drop(&mut self) {
1090        let updated = if self.committed {
1091            &raw const self.access
1092        } else {
1093            ptr::null()
1094        };
1095        // SAFETY: `begun` is this frame's begin record on this thread (the guard is not Send), and
1096        // this is the unique end call. `access_mut`'s caller vouched for `access`.
1097        unsafe { alpha2_vulkan_frame_access_end(&raw const self.begun, updated) };
1098    }
1099}
1100
1101/// One encoder-pool frame held open for a native GPU write.
1102///
1103/// Unlike a [`VulkanFrameLease`], an encoder-input frame has one owner, so it is borrowed directly.
1104/// The guard stays on the thread that began it, which holds the frame and queue mutexes.
1105#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1106pub struct VulkanEncodeFrameAccess<'a> {
1107    frame: &'a mut AVFrame,
1108    // What `begin` locked, which the end call releases and checks the write-back against.
1109    begun: interop::Alpha2VulkanFrameAccess,
1110    access: VulkanFrameAccess,
1111    committed: bool,
1112    _owning_thread: std::marker::PhantomData<*const ()>,
1113}
1114
1115#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1116impl<'a> VulkanEncodeFrameAccess<'a> {
1117    /// Retains the frame's buffers and hardware pool beyond this access guard.
1118    ///
1119    /// # Errors
1120    /// Returns an allocation error if `FFmpeg` cannot create the reference.
1121    pub fn retain_frame(&self) -> Result<AVFrame, BridgeError> {
1122        // SAFETY: the frame is live and locked; av_frame_clone makes an independent AVFrame.
1123        let raw = unsafe { ffi::av_frame_clone(self.frame.as_ptr()) };
1124        let raw = std::ptr::NonNull::new(raw)
1125            .ok_or(BridgeError::InvalidInput("cannot retain encode frame"))?;
1126        // SAFETY: the newly allocated frame is uniquely owned by this value.
1127        Ok(unsafe { AVFrame::from_raw(raw) })
1128    }
1129
1130    /// Opens one handoff on an encoder-pool frame.
1131    ///
1132    /// # Errors
1133    /// Returns an error when the frame is not a usable Vulkan frame.
1134    pub fn begin(frame: &'a mut AVFrame) -> Result<Self, BridgeError> {
1135        let mut access = VulkanFrameAccess::default();
1136        // SAFETY: `frame` is borrowed until the guard drops, and Drop is the unique end call.
1137        let result =
1138            unsafe { alpha2_vulkan_frame_access_begin(frame.as_mut_ptr(), &raw mut access) };
1139        if result != 0 {
1140            return Err(vulkan_frame_refusal(result));
1141        }
1142        Ok(Self {
1143            frame,
1144            begun: access.0,
1145            access,
1146            committed: false,
1147            _owning_thread: std::marker::PhantomData,
1148        })
1149    }
1150
1151    /// The frame's state while its `FFmpeg` locks are held.
1152    #[must_use]
1153    pub const fn access(&self) -> &VulkanFrameAccess {
1154        &self.access
1155    }
1156
1157    /// The record written back on [`Self::commit`], checked as in
1158    /// [`VulkanFrameAccessGuard::access_mut`].
1159    ///
1160    /// # Safety
1161    /// As for [`VulkanFrameAccessGuard::access_mut`].
1162    #[must_use]
1163    pub const unsafe fn access_mut(&mut self) -> &mut VulkanFrameAccess {
1164        &mut self.access
1165    }
1166
1167    /// Writes the updated layout and timeline values back when released.
1168    pub fn commit(&mut self) {
1169        self.committed = true;
1170    }
1171}
1172
1173#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1174impl Drop for VulkanEncodeFrameAccess<'_> {
1175    fn drop(&mut self) {
1176        let updated = if self.committed {
1177            &raw const self.access
1178        } else {
1179            ptr::null()
1180        };
1181        // SAFETY: `begun` is this frame's begin record on this thread (the guard is not Send), and
1182        // this is the unique end call. `access_mut`'s caller vouched for `access`.
1183        unsafe { alpha2_vulkan_frame_access_end(&raw const self.begun, updated) };
1184    }
1185}
1186
1187/// Returns the callback used by a Vulkan hardware decoder to select its opaque format.
1188#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1189#[must_use]
1190pub fn vulkan_get_format_callback()
1191-> unsafe extern "C" fn(*mut ffi::AVCodecContext, *const ffi::AVPixelFormat) -> ffi::AVPixelFormat {
1192    alpha2_vulkan_get_format
1193}
1194
1195/// Reads Vulkan image and synchronization metadata from an opaque hardware frame.
1196///
1197/// The shim takes the frame lock while copying layouts and semaphore values.
1198///
1199/// # Errors
1200/// Returns an error if the frame is not a valid Vulkan hardware frame.
1201#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1202pub fn vulkan_frame_info(frame: &AVFrame) -> Result<VulkanFrameInfo, BridgeError> {
1203    let mut raw = VulkanFrameDescriptor::default();
1204    // SAFETY: `frame` is live and `raw` is a writable result; the shim checks FFmpeg's pointers
1205    // before dereferencing them.
1206    let result = unsafe { alpha2_vulkan_frame_info(frame.as_ptr(), &raw mut raw) };
1207    if result != 0 {
1208        return Err(vulkan_frame_refusal(result));
1209    }
1210    Ok(VulkanFrameInfo {
1211        hardware_format: raw.hardware_format,
1212        software_format: raw.software_format,
1213        width: raw.width,
1214        height: raw.height,
1215        coded_width: raw.coded_width,
1216        coded_height: raw.coded_height,
1217        plane_count: raw.plane_count,
1218        sampled: raw.sampled != 0,
1219        instance: raw.instance,
1220        physical_device: raw.physical_device,
1221        device: raw.device,
1222        images: raw.images,
1223        formats: raw.formats,
1224        layouts: raw.layouts,
1225        access: raw.access,
1226        semaphores: raw.semaphores,
1227        semaphore_values: raw.semaphore_values,
1228        queue_families: raw.queue_families,
1229    })
1230}
1231
1232/// Whether the loaded libavutil was built with hardware device type `kind`.
1233#[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "hwdevice"))]
1234fn hwdevice_available(kind: ffi::AVHWDeviceType) -> bool {
1235    let mut each = ffi::AV_HWDEVICE_TYPE_NONE;
1236    loop {
1237        // SAFETY: av_hwdevice_iterate_types only reads FFmpeg's static table of compiled-in types.
1238        each = unsafe { ffi::av_hwdevice_iterate_types(each) };
1239        if each == ffi::AV_HWDEVICE_TYPE_NONE {
1240            return false;
1241        }
1242        if each == kind {
1243            return true;
1244        }
1245    }
1246}
1247
1248/// rsmpeg's `alloc` unwraps the null context returned for a missing type, so check the type first.
1249#[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "hwdevice"))]
1250fn alloc_hwdevice(
1251    kind: ffi::AVHWDeviceType,
1252    name: &'static str,
1253) -> Result<AVHWDeviceContext, BridgeError> {
1254    if !hwdevice_available(kind) {
1255        return Err(BridgeError::MissingHwDevice(name));
1256    }
1257    Ok(AVHWDeviceContext::alloc(kind))
1258}
1259
1260/// Failure returned by an operation of this crate.
1261#[derive(Debug)]
1262#[non_exhaustive]
1263pub enum BridgeError {
1264    /// The caller supplied invalid dimensions or a mismatched byte count.
1265    InvalidInput(&'static str),
1266    /// `FFmpeg` could not allocate or prepare its destination frame.
1267    Ffmpeg(RsmpegError),
1268    /// An `FFmpeg` operation returned a negative `AVERROR` code (platform GPU statuses are
1269    /// [`Self::NativeGpu`]).
1270    FfmpegCode(i32),
1271    /// A platform GPU call in a native interop helper failed.
1272    NativeGpu(NativeGpuFailure),
1273    /// The loaded `FFmpeg` runtime was built without this hardware device type.
1274    MissingHwDevice(&'static str),
1275    /// A decoded surface the GPU interop does not take, with its description and the reason.
1276    UnsupportedSurface(String),
1277}
1278
1279/// How a planar or semi-planar YCbCr pixel format stores its samples.
1280#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1281pub struct YuvPlaneLayout {
1282    /// Log2 of the horizontal chroma subsampling.
1283    pub chroma_shift_x: u8,
1284    /// Log2 of the vertical chroma subsampling.
1285    pub chroma_shift_y: u8,
1286    /// Significant bits per sample; above eight, samples are `u16`s.
1287    pub bit_depth: u8,
1288    /// Whether U and V interleave in the second plane.
1289    pub interleaved_chroma: bool,
1290    /// Whether samples sit in the high bits of their `u16`.
1291    pub msb_aligned: bool,
1292}
1293
1294impl YuvPlaneLayout {
1295    /// Bytes per stored sample.
1296    #[must_use]
1297    pub const fn bytes_per_sample(self) -> u32 {
1298        if self.bit_depth > 8 { 2 } else { 1 }
1299    }
1300}
1301
1302/// One plane of a decoded frame.
1303#[derive(Clone, Debug, Eq, PartialEq)]
1304pub struct FramePlane {
1305    /// `stride * height` fully written bytes, either a view of the frame's buffer or a packed copy.
1306    pub bytes: bytes::Bytes,
1307    /// Bytes between rows, at least the row's samples.
1308    pub stride: u32,
1309    /// Sample positions per row (a U, V pair each for interleaved chroma).
1310    pub width: u32,
1311    /// Rows.
1312    pub height: u32,
1313}
1314
1315/// The planes of a YCbCr `AVFrame`: luma first, then chroma.
1316#[derive(Clone, Debug, Eq, PartialEq)]
1317pub struct YuvPlanes {
1318    /// How the planes store their samples.
1319    pub layout: YuvPlaneLayout,
1320    /// Coded luma width.
1321    pub width: u32,
1322    /// Coded luma height.
1323    pub height: u32,
1324    /// Luma, then U and V, or one interleaved chroma plane.
1325    pub planes: Vec<FramePlane>,
1326}
1327
1328impl fmt::Display for BridgeError {
1329    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1330        match self {
1331            Self::InvalidInput(context) => {
1332                write!(formatter, "invalid media buffer input: {context}")
1333            }
1334            Self::Ffmpeg(error) => write!(formatter, "{error}"),
1335            Self::FfmpegCode(code) => write!(formatter, "{}", RsmpegError::AVError(*code)),
1336            Self::NativeGpu(failure) => write!(formatter, "{failure}"),
1337            Self::MissingHwDevice(kind) => {
1338                write!(formatter, "the loaded FFmpeg has no {kind} hardware device")
1339            }
1340            Self::UnsupportedSurface(description) => formatter.write_str(description),
1341        }
1342    }
1343}
1344
1345/// Applies explicit matrix and range coefficients to a scaling context.
1346///
1347/// Coefficient arguments are `SWS_CS_*` constants. RGB endpoints still need
1348/// an explicit full-range flag.
1349///
1350/// # Errors
1351/// Returns an error when `libswscale` rejects the configuration.
1352pub fn configure_colorspace(
1353    context: &mut SwsContext,
1354    source_coefficients: i32,
1355    source_full_range: bool,
1356    destination_coefficients: i32,
1357    destination_full_range: bool,
1358) -> Result<(), BridgeError> {
1359    // SAFETY: the context is live and exclusively borrowed; both coefficient pointers refer to
1360    // static arrays owned by `libswscale`.
1361    let result = unsafe {
1362        ffi::sws_setColorspaceDetails(
1363            context.as_mut_ptr(),
1364            ffi::sws_getCoefficients(source_coefficients),
1365            i32::from(source_full_range),
1366            ffi::sws_getCoefficients(destination_coefficients),
1367            i32::from(destination_full_range),
1368            0,
1369            1 << 16,
1370            1 << 16,
1371        )
1372    };
1373    if result < 0 {
1374        Err(BridgeError::FfmpegCode(result))
1375    } else {
1376        Ok(())
1377    }
1378}
1379
1380/// Converts `frame` into `output` as `target`'s planes packed without row padding (the
1381/// `av_image_copy_to_buffer` layout at alignment 1).
1382///
1383/// # Errors
1384/// Returns an error when `output` is not exactly that layout's size, or when `libswscale` fails.
1385pub fn scale_frame_packed(
1386    context: &mut SwsContext,
1387    frame: &AVFrame,
1388    target: ffi::AVPixelFormat,
1389    output: &mut [u8],
1390) -> Result<(), BridgeError> {
1391    // SAFETY: a pure size computation over plain values.
1392    let size = unsafe { ffi::av_image_get_buffer_size(target, frame.width, frame.height, 1) };
1393    if size < 0 {
1394        return Err(BridgeError::FfmpegCode(size));
1395    }
1396    if usize::try_from(size).ok() != Some(output.len()) {
1397        return Err(BridgeError::InvalidInput(
1398            "packed output is not the size of the converted frame",
1399        ));
1400    }
1401    let mut data = [ptr::null_mut::<u8>(); 4];
1402    let mut linesize = [0; 4];
1403    // SAFETY: `output` holds exactly the bytes this layout needs, and the arrays fit every plane.
1404    let filled = unsafe {
1405        ffi::av_image_fill_arrays(
1406            data.as_mut_ptr(),
1407            linesize.as_mut_ptr(),
1408            output.as_mut_ptr(),
1409            target,
1410            frame.width,
1411            frame.height,
1412            1,
1413        )
1414    };
1415    if filled < 0 {
1416        return Err(BridgeError::FfmpegCode(filled));
1417    }
1418    // SAFETY: the context is exclusively borrowed, the source planes belong to the live `frame`,
1419    // and every destination pointer lies inside the exclusively borrowed `output`.
1420    let rows = unsafe {
1421        ffi::sws_scale(
1422            context.as_mut_ptr(),
1423            frame.data.as_ptr().cast(),
1424            frame.linesize.as_ptr(),
1425            0,
1426            frame.height,
1427            data.as_ptr(),
1428            linesize.as_ptr(),
1429        )
1430    };
1431    if rows < 0 {
1432        Err(BridgeError::FfmpegCode(rows))
1433    } else {
1434        Ok(())
1435    }
1436}
1437
1438impl Error for BridgeError {}
1439
1440impl BridgeError {
1441    /// The device fault a failed platform GPU call's status names, if any.
1442    #[must_use]
1443    pub const fn gpu_fault(&self) -> Option<GpuFault> {
1444        match self {
1445            Self::NativeGpu(failure) => failure.fault,
1446            _ => None,
1447        }
1448    }
1449}
1450
1451/// The graphics API a native interop helper called.
1452#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1453pub enum NativeGpuBackend {
1454    /// Vulkan; the status is a `VkResult`.
1455    Vulkan,
1456    /// Direct3D 12; the status is an `HRESULT`.
1457    D3D12,
1458    /// Metal through `CoreVideo`; the status is a `CVReturn`.
1459    Metal,
1460}
1461
1462impl NativeGpuBackend {
1463    /// The API's name.
1464    #[must_use]
1465    pub const fn as_str(self) -> &'static str {
1466        match self {
1467            Self::Vulkan => "Vulkan",
1468            Self::D3D12 => "D3D12",
1469            Self::Metal => "Metal",
1470        }
1471    }
1472}
1473
1474/// A platform GPU call that failed inside a native interop helper.
1475///
1476/// Classify by `fault`, which the backend reads from `status`, not by the message.
1477#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1478pub struct NativeGpuFailure {
1479    /// The API the call belongs to.
1480    pub backend: NativeGpuBackend,
1481    /// What the helper was doing, such as `encode copy`.
1482    pub operation: &'static str,
1483    /// The step of it that failed, such as `queue submit`.
1484    pub step: &'static str,
1485    /// A `VkResult`, `HRESULT` bits, or `CVReturn`; zero when the helper's own check failed.
1486    pub status: i32,
1487    /// What the status is evidence of, when it names a fault.
1488    pub fault: Option<GpuFault>,
1489}
1490
1491impl fmt::Display for NativeGpuFailure {
1492    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1493        let Self {
1494            backend,
1495            operation,
1496            step,
1497            status,
1498            ..
1499        } = *self;
1500        let backend = backend.as_str();
1501        // Use the renderer's timeout sentence so a hung GPU reads the same everywhere.
1502        if self.fault == Some(GpuFault::Timeout) {
1503            write!(
1504                formatter,
1505                "{backend} {operation} ({step}): {}",
1506                alpha2_media::GPU_TIMEOUT_MESSAGE
1507            )?;
1508        } else {
1509            write!(formatter, "{backend} {operation} failed at {step}")?;
1510        }
1511        if status == 0 {
1512            return Ok(());
1513        }
1514        match self.backend {
1515            NativeGpuBackend::Vulkan => write!(formatter, " (VkResult {status})"),
1516            NativeGpuBackend::D3D12 => write!(formatter, " (HRESULT {status:#010X})"),
1517            NativeGpuBackend::Metal => write!(formatter, " (CVReturn {status})"),
1518        }
1519    }
1520}
1521
1522impl From<RsmpegError> for BridgeError {
1523    fn from(error: RsmpegError) -> Self {
1524        Self::Ffmpeg(error)
1525    }
1526}
1527
1528/// Returns the linked `FFmpeg` runtime version string.
1529#[must_use]
1530pub fn runtime_version() -> String {
1531    // SAFETY: FFmpeg returns a process-lifetime, NUL-terminated string.
1532    unsafe { CStr::from_ptr(ffi::av_version_info()) }
1533        .to_string_lossy()
1534        .into_owned()
1535}
1536
1537/// Returns the linked libavcodec configure string.
1538#[must_use]
1539pub fn runtime_configuration() -> String {
1540    // SAFETY: FFmpeg returns a process-lifetime, NUL-terminated string.
1541    unsafe { CStr::from_ptr(ffi::avcodec_configuration()) }
1542        .to_string_lossy()
1543        .into_owned()
1544}
1545
1546/// Whether muxer `format` can store `codec` at normal compliance; `None` when it is not linked.
1547#[must_use]
1548pub fn muxer_accepts_codec(format: &CStr, codec: ffi::AVCodecID) -> Option<bool> {
1549    // SAFETY: `format` is NUL-terminated; FFmpeg returns a registered, process-lifetime muxer
1550    // or null.
1551    let muxer = unsafe { ffi::av_guess_format(format.as_ptr(), ptr::null(), ptr::null()) };
1552    if muxer.is_null() {
1553        return None;
1554    }
1555    // SAFETY: `muxer` is the live registered muxer found above.
1556    let accepted = unsafe {
1557        ffi::avformat_query_codec(
1558            muxer,
1559            codec,
1560            i32::try_from(ffi::FF_COMPLIANCE_NORMAL).unwrap_or(0),
1561        )
1562    };
1563    Some(accepted == 1)
1564}
1565
1566/// Chroma layout, component depth, and alpha of one pixel format.
1567#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1568pub struct SampleProfile {
1569    /// Whether the format is planar/semi-planar YCbCr with 4:2:0 subsampling.
1570    pub is_yuv_420: bool,
1571    /// Largest component depth in bits.
1572    pub bit_depth: u8,
1573    /// Whether the format carries an alpha component.
1574    pub has_alpha: bool,
1575    /// Whether the components are RGB (palettes included) rather than luma and chroma or gray.
1576    pub is_rgb: bool,
1577}
1578
1579/// Describes a pixel format, or `None` when the value has no descriptor.
1580///
1581/// A stream's format may be unset until the first frame decodes, so `None` means "not yet known".
1582#[must_use]
1583pub fn sample_profile(format: ffi::AVPixelFormat) -> Option<SampleProfile> {
1584    if format == ffi::AV_PIX_FMT_NONE {
1585        return None;
1586    }
1587    // SAFETY: `av_pix_fmt_desc_get` accepts any value and returns null or a static descriptor.
1588    let descriptor = unsafe { ffi::av_pix_fmt_desc_get(format).as_ref() }?;
1589    let components = usize::from(descriptor.nb_components.min(4));
1590    Some(SampleProfile {
1591        is_yuv_420: descriptor.log2_chroma_w == 1
1592            && descriptor.log2_chroma_h == 1
1593            && descriptor.nb_components >= 3
1594            && descriptor.flags & u64::from(ffi::AV_PIX_FMT_FLAG_RGB) == 0,
1595        bit_depth: descriptor.comp[..components]
1596            .iter()
1597            .map(|component| u8::try_from(component.depth).unwrap_or(u8::MAX))
1598            .max()
1599            .unwrap_or(8),
1600        has_alpha: descriptor.flags & u64::from(ffi::AV_PIX_FMT_FLAG_ALPHA) != 0,
1601        is_rgb: descriptor.flags & u64::from(ffi::AV_PIX_FMT_FLAG_RGB | ffi::AV_PIX_FMT_FLAG_PAL)
1602            != 0,
1603    })
1604}
1605
1606/// Returns `FFmpeg`'s stable name for a pixel-format value.
1607#[must_use]
1608pub fn pixel_format_name(format: ffi::AVPixelFormat) -> Option<String> {
1609    // SAFETY: `av_get_pix_fmt_name` accepts any value and returns null or a static C string.
1610    let name = unsafe { ffi::av_get_pix_fmt_name(format) };
1611    if name.is_null() {
1612        None
1613    } else {
1614        // SAFETY: non-null return values obey the FFmpeg string contract above.
1615        Some(
1616            unsafe { CStr::from_ptr(name) }
1617                .to_string_lossy()
1618                .into_owned(),
1619        )
1620    }
1621}
1622
1623/// Opens `context` with `options`, returning what `FFmpeg` did not consume.
1624///
1625/// `avcodec_open2` frees the dictionary and writes the remainder back on failure as well as
1626/// success. rsmpeg's `AVCodecContext::open` double-frees it on failure, which would abort an
1627/// encoder fallback.
1628///
1629/// # Errors
1630/// Returns the `FFmpeg` status when the encoder or decoder cannot be opened.
1631pub fn open_codec_context(
1632    context: &mut rsmpeg::avcodec::AVCodecContext,
1633    options: rsmpeg::avutil::AVDictionary,
1634) -> Result<Option<rsmpeg::avutil::AVDictionary>, RsmpegError> {
1635    let _media_foundation = media_foundation_lock(context);
1636    let mut dictionary = options.into_raw().as_ptr();
1637    // SAFETY: `context` is live and FFmpeg owns `dictionary`, replacing it with a null or owned
1638    // remainder.
1639    let status = unsafe {
1640        ffi::avcodec_open2(
1641            context.as_mut_ptr(),
1642            ptr::null_mut(),
1643            std::ptr::addr_of_mut!(dictionary),
1644        )
1645    };
1646    // SAFETY: a non-null remainder is a dictionary FFmpeg handed back; this is its only owner.
1647    let unused = ptr::NonNull::new(dictionary)
1648        .map(|dictionary| unsafe { rsmpeg::avutil::AVDictionary::from_raw(dictionary) });
1649    if status < 0 {
1650        return Err(RsmpegError::from(status));
1651    }
1652    Ok(unused)
1653}
1654
1655/// Serializes opening and closing `MediaFoundation` (`_mf`) encoders; `None` for other codecs.
1656///
1657/// `MFTEnumEx` (on open) and `MFShutdown` (on close) take mfplat's critical section and the
1658/// work-queue lock in opposite orders and deadlock across threads, whatever the `MFStartup` count.
1659fn media_foundation_lock(
1660    context: &rsmpeg::avcodec::AVCodecContext,
1661) -> Option<std::sync::MutexGuard<'static, ()>> {
1662    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1663    if !cfg!(target_os = "windows") || context.codec.is_null() {
1664        return None;
1665    }
1666    // SAFETY: a non-null `codec` is a static codec descriptor with a static C-string `name`.
1667    let name = unsafe { CStr::from_ptr((*context.codec).name) };
1668    name.to_bytes().ends_with(b"_mf").then(|| {
1669        LOCK.lock()
1670            .unwrap_or_else(std::sync::PoisonError::into_inner)
1671    })
1672}
1673
1674/// An [`AVCodecContext`] closed under the `MediaFoundation` lock when it is an `_mf` encoder.
1675///
1676/// [`AVCodecContext`]: rsmpeg::avcodec::AVCodecContext
1677pub struct EncoderContext(Option<rsmpeg::avcodec::AVCodecContext>);
1678
1679impl From<rsmpeg::avcodec::AVCodecContext> for EncoderContext {
1680    fn from(context: rsmpeg::avcodec::AVCodecContext) -> Self {
1681        Self(Some(context))
1682    }
1683}
1684
1685impl std::ops::Deref for EncoderContext {
1686    type Target = rsmpeg::avcodec::AVCodecContext;
1687
1688    fn deref(&self) -> &Self::Target {
1689        self.0.as_ref().expect("taken only when dropped")
1690    }
1691}
1692
1693impl std::ops::DerefMut for EncoderContext {
1694    fn deref_mut(&mut self) -> &mut Self::Target {
1695        self.0.as_mut().expect("taken only when dropped")
1696    }
1697}
1698
1699impl Drop for EncoderContext {
1700    fn drop(&mut self) {
1701        if let Some(context) = self.0.take() {
1702            let _media_foundation = media_foundation_lock(&context);
1703            drop(context);
1704        }
1705    }
1706}
1707
1708/// `VkImageUsageFlags` an encoder-input pool needs beyond `FFmpeg`'s own.
1709///
1710/// `TRANSFER_DST` for the renderer's copy, `SAMPLED` for wgpu import, `TRANSFER_SRC` for readback
1711/// checks, `VIDEO_ENCODE_SRC` for the encoder.
1712#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1713pub const VULKAN_ENCODE_INPUT_USAGE: u32 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004 | 0x0000_4000;
1714
1715/// Adds `usage` to a Vulkan frames context before it allocates its pool.
1716///
1717/// Left at zero, `hwcontext_vulkan` picks a default that requires `STORAGE` support. A nonzero
1718/// usage is used exactly as given, so every flag must be supported by the format; encoder-input
1719/// pools ask for [`VULKAN_ENCODE_INPUT_USAGE`]. Call between `av_hwframe_ctx_alloc` and
1720/// `av_hwframe_ctx_init`.
1721///
1722/// # Errors
1723/// Returns an error when the context is not a Vulkan one.
1724#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1725pub fn vulkan_frames_add_usage(
1726    frames: &mut rsmpeg::avutil::AVHWFramesContext,
1727    usage: u32,
1728) -> Result<(), BridgeError> {
1729    // SAFETY: `frames` is live; the shim checks the device type and only ORs into `usage`.
1730    let status = unsafe { alpha2_vulkan_frames_add_usage(frames.as_mut_ptr(), usage) };
1731    if status < 0 {
1732        return Err(BridgeError::FfmpegCode(status));
1733    }
1734    Ok(())
1735}
1736
1737/// Usage the pool was created with, including the default `FFmpeg` picks for zero.
1738#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1739#[must_use]
1740pub fn vulkan_frames_usage(frames: &rsmpeg::avutil::AVHWFramesContext) -> u32 {
1741    // SAFETY: `frames` is live; the shim checks the device type before reading `hwctx`.
1742    unsafe { alpha2_vulkan_frames_usage(frames.as_ptr()) }
1743}
1744
1745/// Copies one packet's payload out of `FFmpeg`'s buffer.
1746#[must_use]
1747pub fn packet_bytes(packet: &rsmpeg::avcodec::AVPacket) -> Vec<u8> {
1748    let size = usize::try_from(packet.size).unwrap_or(0);
1749    if packet.data.is_null() || size == 0 {
1750        return Vec::new();
1751    }
1752    // SAFETY: the borrowed packet owns `size` readable bytes at `data`.
1753    unsafe { slice::from_raw_parts(packet.data.cast_const(), size) }.to_vec()
1754}
1755
1756/// A copy of a stream's codec extradata (parameter sets for H.264 and HEVC).
1757///
1758/// Takes rsmpeg's wrapper because a bare `AVCodecParameters` can carry a forged pointer:
1759///
1760/// ```compile_fail,E0308
1761/// let mut forged: rsmpeg::ffi::AVCodecParameters = *rsmpeg::avcodec::AVCodecParameters::new();
1762/// forged.extradata = 0x10 as *mut u8;
1763/// forged.extradata_size = 64;
1764/// alpha2_ffmpeg_bridge::codec_parameters_extradata(&forged);
1765/// ```
1766#[must_use]
1767pub fn codec_parameters_extradata(parameters: &rsmpeg::avcodec::AVCodecParameters) -> Vec<u8> {
1768    let size = usize::try_from(parameters.extradata_size).unwrap_or(0);
1769    if parameters.extradata.is_null() || size == 0 {
1770        return Vec::new();
1771    }
1772    // SAFETY: the borrowed parameters own `extradata_size` readable bytes at `extradata`.
1773    unsafe { slice::from_raw_parts(parameters.extradata.cast_const(), size) }.to_vec()
1774}
1775
1776/// Whether `packet` carries side data of `kind`.
1777#[must_use]
1778pub fn packet_has_side_data(
1779    packet: &rsmpeg::avcodec::AVPacket,
1780    kind: ffi::AVPacketSideDataType,
1781) -> bool {
1782    let mut size = 0_usize;
1783    // SAFETY: `packet` is live; the result is only compared with null.
1784    !unsafe { ffi::av_packet_get_side_data(packet.as_ptr(), kind, &raw mut size) }.is_null()
1785}
1786
1787/// Removes and returns `packet`'s side data of `kind`.
1788///
1789/// Encoder parameter sets must be taken off, not copied, or the MP4 muxer reads them as a sample
1790/// description change.
1791#[must_use]
1792pub fn take_packet_side_data(
1793    packet: &mut rsmpeg::avcodec::AVPacket,
1794    kind: ffi::AVPacketSideDataType,
1795) -> Option<Vec<u8>> {
1796    let mut size = 0_usize;
1797    // SAFETY: `packet` is live; a non-null result points at `size` bytes the packet owns.
1798    let data = unsafe { ffi::av_packet_get_side_data(packet.as_ptr(), kind, &raw mut size) };
1799    if data.is_null() {
1800        return None;
1801    }
1802    // SAFETY: as above; the bytes are copied before the entry is freed below.
1803    let bytes = unsafe { slice::from_raw_parts(data, size) }.to_vec();
1804    let packet = packet.as_mut_ptr();
1805    // SAFETY: `side_data` and `side_data_elems` are the packet's own array and count, which
1806    // `av_packet_side_data_remove` keeps consistent while freeing the `av_malloc`ed entry.
1807    unsafe {
1808        ffi::av_packet_side_data_remove(
1809            (*packet).side_data,
1810            &raw mut (*packet).side_data_elems,
1811            kind,
1812        );
1813    }
1814    Some(bytes)
1815}
1816
1817/// Gives `packet` side data of `kind` holding `data`, replacing any it had.
1818///
1819/// # Errors
1820/// When the side data cannot be allocated or `data` is too large.
1821pub fn set_packet_side_data(
1822    packet: &mut rsmpeg::avcodec::AVPacket,
1823    kind: ffi::AVPacketSideDataType,
1824    data: &[u8],
1825) -> Result<(), BridgeError> {
1826    let _ = take_packet_side_data(packet, kind);
1827    // SAFETY: `packet` is mutably borrowed; `av_packet_new_side_data` returns null or a padded
1828    // entry of `data.len()` bytes.
1829    let entry = unsafe { ffi::av_packet_new_side_data(packet.as_mut_ptr(), kind, data.len()) };
1830    if entry.is_null() {
1831        return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
1832    }
1833    // SAFETY: the entry just made holds `data.len()` writable bytes.
1834    unsafe { ptr::copy_nonoverlapping(data.as_ptr(), entry, data.len()) };
1835    Ok(())
1836}
1837
1838/// Replaces the extradata of `output`'s stream `index` before the header is written.
1839///
1840/// rsmpeg exposes an output's streams read-only. `FFmpeg` needs padded `av_malloc` memory here, not
1841/// a Rust buffer.
1842///
1843/// # Errors
1844/// When there is no stream `index`, or `data` is too large or cannot be allocated.
1845pub fn set_output_stream_extradata(
1846    output: &mut rsmpeg::avformat::AVFormatContextOutput,
1847    index: usize,
1848    data: &[u8],
1849) -> Result<(), BridgeError> {
1850    let parameters = output
1851        .streams()
1852        .get(index)
1853        .ok_or(BridgeError::InvalidInput("the output has no such stream"))?
1854        .codecpar;
1855    let size = i32::try_from(data.len())
1856        .map_err(|_| BridgeError::InvalidInput("extradata is larger than FFmpeg holds"))?;
1857    let padding = usize::try_from(ffi::AV_INPUT_BUFFER_PADDING_SIZE).unwrap_or(64);
1858    // SAFETY: `av_mallocz` returns null or a zeroed buffer of the size asked for.
1859    let buffer = unsafe { ffi::av_mallocz(data.len() + padding) }.cast::<u8>();
1860    if buffer.is_null() {
1861        return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
1862    }
1863    // SAFETY: `buffer` holds `data.len()` writable bytes and does not overlap `data`.
1864    unsafe { ptr::copy_nonoverlapping(data.as_ptr(), buffer, data.len()) };
1865    // SAFETY: the stream owns its codec parameters and the output is mutably borrowed. The old
1866    // extradata is null or `av_malloc`ed and is freed; the parameters now own `buffer`.
1867    unsafe {
1868        ffi::av_freep((&raw mut (*parameters).extradata).cast());
1869        (*parameters).extradata = buffer;
1870        (*parameters).extradata_size = size;
1871    }
1872    Ok(())
1873}
1874
1875/// A packet holding a copy of `data`, every other field at its default.
1876///
1877/// # Errors
1878/// When `data` is too large or the packet cannot be allocated.
1879pub fn packet_from_bytes(data: &[u8]) -> Result<rsmpeg::avcodec::AVPacket, BridgeError> {
1880    let size = i32::try_from(data.len())
1881        .map_err(|_| BridgeError::InvalidInput("a packet is larger than FFmpeg holds"))?;
1882    let mut packet = rsmpeg::avcodec::AVPacket::new();
1883    // SAFETY: `packet` is fresh and empty; `av_new_packet` gives it a padded buffer or fails.
1884    let status = unsafe { ffi::av_new_packet(packet.as_mut_ptr(), size) };
1885    if status < 0 {
1886        return Err(BridgeError::FfmpegCode(status));
1887    }
1888    if !data.is_empty() {
1889        // SAFETY: the packet's buffer holds `size` writable bytes and does not overlap `data`.
1890        unsafe { ptr::copy_nonoverlapping(data.as_ptr(), packet.data, data.len()) };
1891    }
1892    Ok(packet)
1893}
1894
1895/// Replaces `packet`'s payload with `data`, no longer than it, keeping timestamps and side data.
1896///
1897/// # Errors
1898/// When `data` is longer than the payload or the buffer cannot be made writable.
1899pub fn shrink_packet_to(
1900    packet: &mut rsmpeg::avcodec::AVPacket,
1901    data: &[u8],
1902) -> Result<(), BridgeError> {
1903    let size = i32::try_from(data.len())
1904        .ok()
1905        .filter(|size| *size <= packet.size)
1906        .ok_or(BridgeError::InvalidInput("a packet cannot grow in place"))?;
1907    // SAFETY: `packet` is live; `av_packet_make_writable` gives it an unshared buffer of its size.
1908    let status = unsafe { ffi::av_packet_make_writable(packet.as_mut_ptr()) };
1909    if status < 0 {
1910        return Err(BridgeError::FfmpegCode(status));
1911    }
1912    if !data.is_empty() {
1913        // SAFETY: the writable buffer holds at least `data.len()` bytes and cannot overlap `data`.
1914        unsafe { ptr::copy_nonoverlapping(data.as_ptr(), packet.data, data.len()) };
1915    }
1916    // SAFETY: `size` is not above the packet's size; FFmpeg zeroes the padding after it.
1917    unsafe { ffi::av_shrink_packet(packet.as_mut_ptr(), size) };
1918    Ok(())
1919}
1920
1921/// Codec parameters for a video stream not produced by an `FFmpeg` encoder.
1922#[derive(Clone, Copy, Debug)]
1923pub struct VideoStreamDescription<'a> {
1924    /// The codec.
1925    pub codec_id: ffi::AVCodecID,
1926    /// Coded width.
1927    pub width: i32,
1928    /// Coded height.
1929    pub height: i32,
1930    /// The pixel format the stream decodes to.
1931    pub pixel_format: ffi::AVPixelFormat,
1932    /// `AV_PROFILE_*` for the codec.
1933    pub profile: i32,
1934    /// The codec's level number.
1935    pub level: i32,
1936    /// Colour primaries tag.
1937    pub color_primaries: ffi::AVColorPrimaries,
1938    /// Transfer characteristics tag.
1939    pub color_trc: ffi::AVColorTransferCharacteristic,
1940    /// Matrix coefficients tag.
1941    pub color_space: ffi::AVColorSpace,
1942    /// Sample range.
1943    pub color_range: ffi::AVColorRange,
1944    /// Pixel aspect ratio; `0/1` when undeclared.
1945    pub sample_aspect_ratio: ffi::AVRational,
1946    /// Frames per second.
1947    pub frame_rate: ffi::AVRational,
1948    /// Average bitrate, `0` when unknown.
1949    pub bit_rate: i64,
1950    /// The sample description (`avcC` for H.264).
1951    pub extradata: &'a [u8],
1952}
1953
1954/// Codec parameters for a video stream `description` describes.
1955///
1956/// # Errors
1957/// When the extradata is larger than `FFmpeg` holds or cannot be allocated.
1958pub fn video_codec_parameters(
1959    description: &VideoStreamDescription<'_>,
1960) -> Result<rsmpeg::avcodec::AVCodecParameters, BridgeError> {
1961    let extradata_size = i32::try_from(description.extradata.len())
1962        .map_err(|_| BridgeError::InvalidInput("extradata is larger than FFmpeg holds"))?;
1963    let mut parameters = rsmpeg::avcodec::AVCodecParameters::new();
1964    let raw = parameters.as_mut_ptr();
1965    // SAFETY: `raw` is freshly allocated and uniquely owned; the fields are plain values.
1966    unsafe {
1967        (*raw).codec_type = ffi::AVMEDIA_TYPE_VIDEO;
1968        (*raw).codec_id = description.codec_id;
1969        (*raw).width = description.width;
1970        (*raw).height = description.height;
1971        (*raw).format = description.pixel_format;
1972        (*raw).profile = description.profile;
1973        (*raw).level = description.level;
1974        (*raw).color_primaries = description.color_primaries;
1975        (*raw).color_trc = description.color_trc;
1976        (*raw).color_space = description.color_space;
1977        (*raw).color_range = description.color_range;
1978        (*raw).sample_aspect_ratio = description.sample_aspect_ratio;
1979        (*raw).framerate = description.frame_rate;
1980        (*raw).bit_rate = description.bit_rate;
1981        (*raw).field_order = ffi::AV_FIELD_PROGRESSIVE;
1982    }
1983    if !description.extradata.is_empty() {
1984        let padding = usize::try_from(ffi::AV_INPUT_BUFFER_PADDING_SIZE).unwrap_or(64);
1985        // SAFETY: `av_mallocz` returns null or a zeroed buffer of the size asked for.
1986        let buffer = unsafe { ffi::av_mallocz(description.extradata.len() + padding) }.cast::<u8>();
1987        if buffer.is_null() {
1988            return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
1989        }
1990        // SAFETY: `buffer` holds the extradata's length and does not overlap it. The parameters
1991        // had no extradata and now own `buffer`.
1992        unsafe {
1993            ptr::copy_nonoverlapping(
1994                description.extradata.as_ptr(),
1995                buffer,
1996                description.extradata.len(),
1997            );
1998            (*raw).extradata = buffer;
1999            (*raw).extradata_size = extradata_size;
2000        }
2001    }
2002    Ok(parameters)
2003}
2004
2005/// Opens a bitstream filter chain in `ffmpeg -bsf` list syntax for a stream described by
2006/// `parameters` with packets in `time_base`.
2007///
2008/// rsmpeg cannot parse a list or set a filter's private options.
2009///
2010/// # Errors
2011/// Returns the `FFmpeg` status when the list does not parse, names a filter or option the runtime
2012/// lacks, or a filter refuses the stream.
2013pub fn bitstream_filter_list(
2014    spec: &CStr,
2015    parameters: &AVCodecParameters,
2016    time_base: rsmpeg::avutil::AVRational,
2017) -> Result<rsmpeg::avcodec::AVBSFContext, BridgeError> {
2018    let mut raw = ptr::null_mut();
2019    // SAFETY: `spec` is NUL-terminated; on success `raw` holds a new context taken over below.
2020    let status = unsafe { ffi::av_bsf_list_parse_str(spec.as_ptr(), &raw mut raw) };
2021    if status < 0 {
2022        return Err(BridgeError::FfmpegCode(status));
2023    }
2024    let raw = ptr::NonNull::new(raw).ok_or(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)))?;
2025    // SAFETY: `raw` was just allocated and is unshared; the wrapper frees it with `av_bsf_free`.
2026    let mut context = unsafe { rsmpeg::avcodec::AVBSFContextUninit::from_raw(raw) };
2027    // rsmpeg's `set_par_in` panics when the copy cannot allocate.
2028    // SAFETY: `par_in` is the context's own parameter set and `parameters` is live;
2029    // `avcodec_parameters_copy` deep-copies into it.
2030    let status = unsafe { ffi::avcodec_parameters_copy(context.par_in, parameters.as_ptr()) };
2031    if status < 0 {
2032        return Err(BridgeError::FfmpegCode(status));
2033    }
2034    context.set_time_base_in(time_base);
2035    context.init().map_err(BridgeError::Ffmpeg)
2036}
2037
2038/// Sets a stream's codec tag (sample entry); zero lets the muxer pick its default.
2039pub fn set_stream_codec_tag(stream: &mut rsmpeg::avformat::AVStreamMut<'_>, tag: u32) {
2040    let mut parameters = stream.codecpar_mut();
2041    // SAFETY: the mutably borrowed stream owns its codec parameters; `codec_tag` is a plain field.
2042    unsafe { (*parameters.as_mut_ptr()).codec_tag = tag };
2043}
2044
2045/// Replaces a stream's display matrix (MP4's `tkhd` matrix) with `display`'s rotation and flip, as
2046/// [`display_rotation`] reads it back.
2047///
2048/// # Errors
2049/// When the side data cannot be allocated.
2050pub fn set_stream_display_matrix(
2051    stream: &mut rsmpeg::avformat::AVStreamMut<'_>,
2052    display: DisplayRotation,
2053) -> Result<(), BridgeError> {
2054    let mut parameters = stream.codecpar_mut();
2055    let parameters = parameters.as_mut_ptr();
2056    // SAFETY: the mutably borrowed stream owns its codec parameters; `av_packet_side_data_new`
2057    // keeps the array and count consistent and returns null only on allocation failure.
2058    let entry = unsafe {
2059        ffi::av_packet_side_data_new(
2060            &raw mut (*parameters).coded_side_data,
2061            &raw mut (*parameters).nb_coded_side_data,
2062            ffi::AV_PKT_DATA_DISPLAYMATRIX,
2063            size_of::<[i32; 9]>(),
2064            0,
2065        )
2066    };
2067    if entry.is_null() {
2068        return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
2069    }
2070    let mut matrix = [0_i32; 9];
2071    // `av_display_rotation_set` turns clockwise, opposite to `av_display_rotation_get`.
2072    // SAFETY: `matrix` is a complete, aligned 3x3 display matrix.
2073    unsafe {
2074        ffi::av_display_rotation_set(matrix.as_mut_ptr(), -display.counter_clockwise_degrees);
2075        if display.flipped {
2076            ffi::av_display_matrix_flip(matrix.as_mut_ptr(), 1, 0);
2077        }
2078    }
2079    // SAFETY: the new entry holds `size_of::<[i32; 9]>()` writable bytes; a byte copy needs no
2080    // alignment.
2081    unsafe {
2082        ptr::copy_nonoverlapping(
2083            matrix.as_ptr().cast::<u8>(),
2084            (*entry).data,
2085            size_of::<[i32; 9]>(),
2086        );
2087    }
2088    Ok(())
2089}
2090
2091/// Returns `FFmpeg`'s stable name for a hardware device type.
2092#[must_use]
2093pub fn hardware_device_type_name(device_type: ffi::AVHWDeviceType) -> Option<String> {
2094    // SAFETY: `av_hwdevice_get_type_name` accepts any value and returns null or a static C string.
2095    let name = unsafe { ffi::av_hwdevice_get_type_name(device_type) };
2096    if name.is_null() {
2097        None
2098    } else {
2099        // SAFETY: non-null return values obey the FFmpeg string contract above.
2100        Some(
2101            unsafe { CStr::from_ptr(name) }
2102                .to_string_lossy()
2103                .into_owned(),
2104        )
2105    }
2106}
2107
2108/// The plane layout of a YCbCr format the renderer samples directly, or `None`.
2109///
2110/// Accepts three components of one depth from 8 to 16 bits, chroma subsampled by at most four per
2111/// axis, three planes or U, V interleaved in the second, native byte order, low- or (above eight
2112/// bits) high-bit aligned. Rejects RGB, alpha, gray, palette, Bayer, float, XYZ, bitstream,
2113/// hardware formats and NV21.
2114#[must_use]
2115pub fn yuv_layout(format: ffi::AVPixelFormat) -> Option<YuvPlaneLayout> {
2116    if format == ffi::AV_PIX_FMT_NONE {
2117        return None;
2118    }
2119    // SAFETY: `av_pix_fmt_desc_get` accepts any value and returns null or a static descriptor.
2120    let descriptor = unsafe { ffi::av_pix_fmt_desc_get(format).as_ref() }?;
2121    let foreign_endian = if cfg!(target_endian = "little") {
2122        ffi::AV_PIX_FMT_FLAG_BE
2123    } else {
2124        0
2125    };
2126    let rejected = foreign_endian
2127        | ffi::AV_PIX_FMT_FLAG_PAL
2128        | ffi::AV_PIX_FMT_FLAG_BITSTREAM
2129        | ffi::AV_PIX_FMT_FLAG_HWACCEL
2130        | ffi::AV_PIX_FMT_FLAG_RGB
2131        | ffi::AV_PIX_FMT_FLAG_ALPHA
2132        | ffi::AV_PIX_FMT_FLAG_BAYER
2133        | ffi::AV_PIX_FMT_FLAG_FLOAT
2134        | ffi::AV_PIX_FMT_FLAG_XYZ;
2135    if descriptor.flags & u64::from(rejected) != 0 || descriptor.nb_components != 3 {
2136        return None;
2137    }
2138    let [luma, u, v, _] = descriptor.comp;
2139    let depth = luma.depth;
2140    if !(8..=16).contains(&depth) || u.depth != depth || v.depth != depth {
2141        return None;
2142    }
2143    if descriptor.log2_chroma_w > 2 || descriptor.log2_chroma_h > 2 {
2144        return None;
2145    }
2146    let bytes = if depth > 8 { 2 } else { 1 };
2147    let shift = luma.shift;
2148    let msb_aligned = match shift {
2149        0 => false,
2150        _ if depth > 8 && shift == 16 - depth => true,
2151        _ => return None,
2152    };
2153    let planar = u.plane == 1
2154        && v.plane == 2
2155        && u.step == bytes
2156        && v.step == bytes
2157        && u.offset == 0
2158        && v.offset == 0;
2159    let interleaved = u.plane == 1
2160        && v.plane == 1
2161        && u.step == 2 * bytes
2162        && v.step == 2 * bytes
2163        && u.offset == 0
2164        && v.offset == bytes;
2165    if luma.plane != 0
2166        || luma.step != bytes
2167        || luma.offset != 0
2168        || u.shift != shift
2169        || v.shift != shift
2170        || !(planar || interleaved)
2171    {
2172        return None;
2173    }
2174    Some(YuvPlaneLayout {
2175        chroma_shift_x: descriptor.log2_chroma_w,
2176        chroma_shift_y: descriptor.log2_chroma_h,
2177        bit_depth: u8::try_from(depth).ok()?,
2178        interleaved_chroma: interleaved,
2179        msb_aligned,
2180    })
2181}
2182
2183/// Copies the samples of a frame whose format [`yuv_layout`] accepts into packed forward rows, or
2184/// returns `Ok(None)` for other formats. Odd dimensions round chroma up.
2185///
2186/// Row padding is never read since it may be uninitialized; [`DecodedFrame::yuv_planes`] can share
2187/// fully written frames instead.
2188///
2189/// # Errors
2190/// Returns an error for invalid dimensions, null planes, or linesizes shorter than the plane width.
2191pub fn yuv_planes(frame: &AVFrame) -> Result<Option<YuvPlanes>, BridgeError> {
2192    frame_yuv_planes(frame, false)
2193}
2194
2195/// A decoded frame, and whether every byte of its buffers, row padding included, is written.
2196///
2197/// Only this crate constructs one; frames from elsewhere go to [`yuv_planes`].
2198pub struct DecodedFrame {
2199    frame: AVFrame,
2200    written: bool,
2201}
2202
2203impl DecodedFrame {
2204    /// Takes the next frame `decoder` has ready.
2205    ///
2206    /// # Errors
2207    /// Returns what `avcodec_receive_frame` does for a drained decoder or a decode failure.
2208    pub fn receive(decoder: &mut AVCodecContext) -> Result<Self, RsmpegError> {
2209        let written = decoder_frames_are_initialized(decoder);
2210        let frame = decoder.receive_frame()?;
2211        Ok(Self { frame, written })
2212    }
2213
2214    /// Like [`yuv_planes`], but shares the frame's buffer when it is fully written. Planes with a
2215    /// negative linesize or outside the frame's buffers are still copied.
2216    ///
2217    /// # Errors
2218    /// Returns an error for invalid dimensions, null planes, or linesizes shorter than the plane
2219    /// width.
2220    pub fn yuv_planes(&self) -> Result<Option<YuvPlanes>, BridgeError> {
2221        frame_yuv_planes(&self.frame, self.written)
2222    }
2223
2224    /// Sets the frame's presentation timestamp.
2225    pub fn set_pts(&mut self, pts: i64) {
2226        self.frame.set_pts(pts);
2227    }
2228
2229    /// Takes ownership of the frame.
2230    #[must_use]
2231    pub fn into_frame(self) -> AVFrame {
2232        self.frame
2233    }
2234}
2235
2236impl std::ops::Deref for DecodedFrame {
2237    type Target = AVFrame;
2238
2239    fn deref(&self) -> &AVFrame {
2240        &self.frame
2241    }
2242}
2243
2244impl fmt::Debug for DecodedFrame {
2245    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2246        formatter
2247            .debug_struct("DecodedFrame")
2248            .field("format", &self.frame.format)
2249            .field("width", &self.frame.width)
2250            .field("height", &self.frame.height)
2251            .field("pts", &self.frame.pts)
2252            .field("written", &self.written)
2253            .finish_non_exhaustive()
2254    }
2255}
2256
2257/// Whether `decoder`'s frame buffers are fully initialized, so they may be shared.
2258///
2259/// The default `get_buffer2` uses zeroed pools. Wrapped external decoders (`wrapper_name`, such as
2260/// libdav1d) and replaced `get_buffer2` callbacks do not qualify.
2261fn decoder_frames_are_initialized(decoder: &AVCodecContext) -> bool {
2262    static DEFAULT_GET_BUFFER2: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2263    // Read the default from a new context; Rust's address of the function could be a linker stub.
2264    let default = *DEFAULT_GET_BUFFER2.get_or_init(|| {
2265        // SAFETY: a context allocated without a codec is only read and then freed.
2266        unsafe {
2267            let mut context = ffi::avcodec_alloc_context3(ptr::null());
2268            let get_buffer2 = context
2269                .as_ref()
2270                .and_then(|context| context.get_buffer2)
2271                .map_or(0, |function| function as usize);
2272            ffi::avcodec_free_context(&raw mut context);
2273            get_buffer2
2274        }
2275    });
2276    // SAFETY: a context's `codec` is null or a static, read-only description.
2277    let wrapped =
2278        unsafe { decoder.codec.as_ref() }.is_none_or(|codec| !codec.wrapper_name.is_null());
2279    let get_buffer2 = decoder.get_buffer2.map_or(0, |function| function as usize);
2280    !wrapped && default != 0 && get_buffer2 == default
2281}
2282
2283/// The planes of `frame`; `share` only for fully initialized frames.
2284fn frame_yuv_planes(frame: &AVFrame, share: bool) -> Result<Option<YuvPlanes>, BridgeError> {
2285    let Some(layout) = yuv_layout(frame.format) else {
2286        return Ok(None);
2287    };
2288    let width =
2289        u32::try_from(frame.width).map_err(|_| BridgeError::InvalidInput("negative YUV width"))?;
2290    let height = u32::try_from(frame.height)
2291        .map_err(|_| BridgeError::InvalidInput("negative YUV height"))?;
2292    if width == 0 || height == 0 {
2293        return Err(BridgeError::InvalidInput("empty YUV frame"));
2294    }
2295    let bytes = layout.bytes_per_sample();
2296    let chroma_width = width.div_ceil(1 << layout.chroma_shift_x);
2297    let chroma_height = height.div_ceil(1 << layout.chroma_shift_y);
2298    // Only a vouched-for, reference-counted frame is shared; `AVFrame::clone` adds buffer refs.
2299    let shared =
2300        (share && !frame.buf[0].is_null()).then(|| std::sync::Arc::new(SharedFrame(frame.clone())));
2301    let copy = |index: usize,
2302                samples: u32,
2303                pairs: u32,
2304                rows: u32,
2305                context: &'static str|
2306     -> Result<FramePlane, BridgeError> {
2307        let row_bytes = samples
2308            .checked_mul(bytes)
2309            .and_then(|row| row.checked_mul(pairs))
2310            .ok_or(BridgeError::InvalidInput(context))?;
2311        if let Some(view) = shared
2312            .as_ref()
2313            .and_then(|frame| PlaneView::new(frame, index, row_bytes, rows))
2314        {
2315            let stride =
2316                u32::try_from(view.stride).map_err(|_| BridgeError::InvalidInput(context))?;
2317            return Ok(FramePlane {
2318                bytes: bytes::Bytes::from_owner(view),
2319                stride,
2320                width: samples,
2321                height: rows,
2322            });
2323        }
2324        Ok(FramePlane {
2325            bytes: copy_video_plane(
2326                frame.data[index],
2327                frame.linesize[index],
2328                row_bytes,
2329                rows,
2330                context,
2331            )?
2332            .into(),
2333            stride: row_bytes,
2334            width: samples,
2335            height: rows,
2336        })
2337    };
2338    let mut planes = vec![copy(0, width, 1, height, "invalid Y plane")?];
2339    if layout.interleaved_chroma {
2340        planes.push(copy(1, chroma_width, 2, chroma_height, "invalid UV plane")?);
2341    } else {
2342        planes.push(copy(1, chroma_width, 1, chroma_height, "invalid U plane")?);
2343        planes.push(copy(2, chroma_width, 1, chroma_height, "invalid V plane")?);
2344    }
2345    Ok(Some(YuvPlanes {
2346        layout,
2347        width,
2348        height,
2349        planes,
2350    }))
2351}
2352
2353/// A decoded frame whose buffers several plane views share.
2354struct SharedFrame(AVFrame);
2355
2356// SAFETY: the wrapped frame is only read. FFmpeg never writes to a buffer with more than one
2357// reference, so nothing mutates it while shared.
2358unsafe impl Sync for SharedFrame {}
2359
2360/// `stride * rows` bytes of one plane of a fully written [`SharedFrame`], which it keeps alive.
2361struct PlaneView {
2362    frame: std::sync::Arc<SharedFrame>,
2363    data: *const u8,
2364    len: usize,
2365    stride: usize,
2366}
2367
2368// SAFETY: the view reads immutable memory in `frame`'s buffers, which it keeps alive;
2369// `SharedFrame` is `Send + Sync`.
2370unsafe impl Send for PlaneView {}
2371
2372impl PlaneView {
2373    /// `None` unless the linesize is positive, covers `row_bytes`, and all rows lie in one buffer.
2374    fn new(
2375        frame: &std::sync::Arc<SharedFrame>,
2376        index: usize,
2377        row_bytes: u32,
2378        rows: u32,
2379    ) -> Option<Self> {
2380        let inner = &frame.0;
2381        let data = inner.data[index].cast_const();
2382        let stride = usize::try_from(inner.linesize[index]).ok()?;
2383        let row_bytes = usize::try_from(row_bytes).ok()?;
2384        if data.is_null() || stride < row_bytes || rows == 0 {
2385            return None;
2386        }
2387        let len = stride.checked_mul(usize::try_from(rows).ok()?)?;
2388        let start = data as usize;
2389        let end = start.checked_add(len)?;
2390        let inside = inner.buf.iter().any(|buffer| {
2391            // SAFETY: a non-null `AVFrame::buf` entry is a live `AVBufferRef` the frame holds.
2392            let Some(buffer) = (unsafe { buffer.as_ref() }) else {
2393                return false;
2394            };
2395            let buffer_start = buffer.data as usize;
2396            buffer_start
2397                .checked_add(buffer.size)
2398                .is_some_and(|buffer_end| buffer_start <= start && end <= buffer_end)
2399        });
2400        inside.then(|| Self {
2401            frame: std::sync::Arc::clone(frame),
2402            data,
2403            len,
2404            stride,
2405        })
2406    }
2407}
2408
2409impl AsRef<[u8]> for PlaneView {
2410    fn as_ref(&self) -> &[u8] {
2411        let _ = &self.frame;
2412        // SAFETY: `new` checked `data..data + len` lies in a buffer of the frame `self` holds.
2413        unsafe { slice::from_raw_parts(self.data, self.len) }
2414    }
2415}
2416
2417fn copy_video_plane(
2418    data: *const u8,
2419    linesize: i32,
2420    width: u32,
2421    height: u32,
2422    context: &'static str,
2423) -> Result<Vec<u8>, BridgeError> {
2424    let row_bytes = usize::try_from(width).map_err(|_| BridgeError::InvalidInput(context))?;
2425    let rows = usize::try_from(height).map_err(|_| BridgeError::InvalidInput(context))?;
2426    let stride =
2427        usize::try_from(linesize.unsigned_abs()).map_err(|_| BridgeError::InvalidInput(context))?;
2428    if data.is_null() || linesize == 0 || stride < row_bytes {
2429        return Err(BridgeError::InvalidInput(context));
2430    }
2431    let byte_len = row_bytes
2432        .checked_mul(rows)
2433        .ok_or(BridgeError::InvalidInput(context))?;
2434    // Append row by row to avoid an extra zeroing pass over the buffer.
2435    let mut output = Vec::with_capacity(byte_len);
2436    let signed_stride =
2437        isize::try_from(linesize).map_err(|_| BridgeError::InvalidInput(context))?;
2438    for row in 0..rows {
2439        let row = isize::try_from(row).map_err(|_| BridgeError::InvalidInput(context))?;
2440        let offset = row
2441            .checked_mul(signed_stride)
2442            .ok_or(BridgeError::InvalidInput(context))?;
2443        // SAFETY: a non-null plane has `height` rows at its signed linesize, whose absolute value
2444        // was checked to cover `row_bytes`.
2445        let source = unsafe { slice::from_raw_parts(data.offset(offset), row_bytes) };
2446        output.extend_from_slice(source);
2447    }
2448    debug_assert_eq!(output.len(), byte_len);
2449    Ok(output)
2450}
2451
2452/// Copies tightly packed RGBA bytes into an owned `FFmpeg` frame.
2453///
2454/// # Errors
2455/// Returns an `FFmpeg` error when its frame buffer cannot be allocated.
2456pub fn rgba_frame(bytes: &[u8], width: i32, height: i32) -> Result<AVFrame, BridgeError> {
2457    let row_bytes = usize::try_from(width)
2458        .ok()
2459        .and_then(|value| value.checked_mul(4))
2460        .ok_or(BridgeError::InvalidInput("RGBA row width overflow"))?;
2461    let rows =
2462        usize::try_from(height).map_err(|_| BridgeError::InvalidInput("negative RGBA height"))?;
2463    let expected = row_bytes
2464        .checked_mul(rows)
2465        .ok_or(BridgeError::InvalidInput("RGBA frame size overflow"))?;
2466    if bytes.len() != expected {
2467        return Err(BridgeError::InvalidInput("RGBA byte length mismatch"));
2468    }
2469
2470    let mut frame = AVFrame::new();
2471    frame.set_format(ffi::AV_PIX_FMT_RGBA);
2472    frame.set_width(width);
2473    frame.set_height(height);
2474    frame.get_buffer(1)?;
2475    frame.make_writable()?;
2476    let linesize = usize::try_from(frame.linesize[0])
2477        .map_err(|_| BridgeError::InvalidInput("negative RGBA linesize"))?;
2478    if frame.data[0].is_null() || linesize < row_bytes {
2479        return Err(BridgeError::InvalidInput(
2480            "RGBA destination plane is too small",
2481        ));
2482    }
2483
2484    // SAFETY: av_frame_get_buffer allocated `height` writable rows of `linesize` bytes; both slices
2485    // span one validated RGBA row.
2486    unsafe {
2487        for row in 0..rows {
2488            let destination_offset = row.checked_mul(linesize).ok_or(BridgeError::InvalidInput(
2489                "RGBA destination offset overflow",
2490            ))?;
2491            let source_offset = row
2492                .checked_mul(row_bytes)
2493                .ok_or(BridgeError::InvalidInput("RGBA source offset overflow"))?;
2494            let destination =
2495                slice::from_raw_parts_mut(frame.data[0].add(destination_offset), row_bytes);
2496            destination.copy_from_slice(&bytes[source_offset..source_offset + row_bytes]);
2497        }
2498    }
2499    Ok(frame)
2500}
2501
2502/// Copies owned planar 8-bit 4:2:0 samples into an `FFmpeg` video frame.
2503///
2504/// # Errors
2505/// Returns an error for invalid dimensions, short source planes, or an allocation failure.
2506#[allow(clippy::too_many_arguments)]
2507pub fn yuv420p_frame(
2508    y: &[u8],
2509    y_stride: u32,
2510    u: &[u8],
2511    u_stride: u32,
2512    v: &[u8],
2513    v_stride: u32,
2514    width: i32,
2515    height: i32,
2516) -> Result<AVFrame, BridgeError> {
2517    let width =
2518        u32::try_from(width).map_err(|_| BridgeError::InvalidInput("negative YUV width"))?;
2519    let height =
2520        u32::try_from(height).map_err(|_| BridgeError::InvalidInput("negative YUV height"))?;
2521    if width == 0 || height == 0 {
2522        return Err(BridgeError::InvalidInput("empty YUV frame"));
2523    }
2524    let chroma_width = width.div_ceil(2);
2525    let chroma_height = height.div_ceil(2);
2526    validate_source_plane(y, y_stride, width, height, "invalid Y source plane")?;
2527    validate_source_plane(
2528        u,
2529        u_stride,
2530        chroma_width,
2531        chroma_height,
2532        "invalid U source plane",
2533    )?;
2534    validate_source_plane(
2535        v,
2536        v_stride,
2537        chroma_width,
2538        chroma_height,
2539        "invalid V source plane",
2540    )?;
2541
2542    let mut frame = AVFrame::new();
2543    frame.set_format(ffi::AV_PIX_FMT_YUV420P);
2544    frame.set_width(i32::try_from(width).map_err(|_| BridgeError::InvalidInput("YUV width"))?);
2545    frame.set_height(i32::try_from(height).map_err(|_| BridgeError::InvalidInput("YUV height"))?);
2546    frame.get_buffer(1)?;
2547    frame.make_writable()?;
2548    copy_plane_to_frame(y, y_stride, width, height, frame.data[0], frame.linesize[0])?;
2549    copy_plane_to_frame(
2550        u,
2551        u_stride,
2552        chroma_width,
2553        chroma_height,
2554        frame.data[1],
2555        frame.linesize[1],
2556    )?;
2557    copy_plane_to_frame(
2558        v,
2559        v_stride,
2560        chroma_width,
2561        chroma_height,
2562        frame.data[2],
2563        frame.linesize[2],
2564    )?;
2565    Ok(frame)
2566}
2567
2568/// The first pixel format in `FFmpeg`'s table laid out as `layout` (`yuv420p`, not the deprecated
2569/// `yuvj420p`).
2570#[must_use]
2571pub fn yuv_pixel_format(layout: YuvPlaneLayout) -> Option<ffi::AVPixelFormat> {
2572    let mut descriptor = ptr::null();
2573    loop {
2574        // SAFETY: null starts the walk; other values are static descriptors from the previous call.
2575        descriptor = unsafe { ffi::av_pix_fmt_desc_next(descriptor) };
2576        if descriptor.is_null() {
2577            return None;
2578        }
2579        // SAFETY: a descriptor from the table names its own format.
2580        let format = unsafe { ffi::av_pix_fmt_desc_get_id(descriptor) };
2581        if yuv_layout(format) == Some(layout) {
2582            return Some(format);
2583        }
2584    }
2585}
2586
2587/// A `width` by `height` frame of `format` holding a copy of `planes` (bytes and stride each).
2588///
2589/// # Errors
2590/// For a format without a descriptor, the wrong plane count, or a plane smaller than the picture.
2591pub fn planar_frame(
2592    format: ffi::AVPixelFormat,
2593    planes: &[(&[u8], u32)],
2594    width: u32,
2595    height: u32,
2596) -> Result<AVFrame, BridgeError> {
2597    // SAFETY: `av_pix_fmt_desc_get` accepts any value and returns null or a static descriptor.
2598    let descriptor = unsafe { ffi::av_pix_fmt_desc_get(format).as_ref() }
2599        .ok_or(BridgeError::InvalidInput("pixel format has no descriptor"))?;
2600    // SAFETY: a pure computation over plain values.
2601    let count = unsafe { ffi::av_pix_fmt_count_planes(format) };
2602    if usize::try_from(count).ok() != Some(planes.len()) || width == 0 || height == 0 {
2603        return Err(BridgeError::InvalidInput(
2604            "planes do not match the pixel format",
2605        ));
2606    }
2607    let signed = |value: u32| {
2608        i32::try_from(value).map_err(|_| BridgeError::InvalidInput("frame size overflow"))
2609    };
2610    let mut frame = AVFrame::new();
2611    frame.set_format(format);
2612    frame.set_width(signed(width)?);
2613    frame.set_height(signed(height)?);
2614    frame.get_buffer(1)?;
2615    frame.make_writable()?;
2616    for (index, (bytes, stride)) in planes.iter().enumerate() {
2617        let plane = i32::try_from(index).unwrap_or(i32::MAX);
2618        // SAFETY: a pure computation over plain values.
2619        let row_bytes = unsafe { ffi::av_image_get_linesize(format, signed(width)?, plane) };
2620        let row_bytes = u32::try_from(row_bytes).map_err(|_| BridgeError::FfmpegCode(row_bytes))?;
2621        let rows = if index == 0 || index == 3 {
2622            height
2623        } else {
2624            height.div_ceil(1 << descriptor.log2_chroma_h)
2625        };
2626        validate_source_plane(bytes, *stride, row_bytes, rows, "invalid source plane")?;
2627        copy_plane_to_frame(
2628            bytes,
2629            *stride,
2630            row_bytes,
2631            rows,
2632            frame.data[index],
2633            frame.linesize[index],
2634        )?;
2635    }
2636    Ok(frame)
2637}
2638
2639fn validate_source_plane(
2640    bytes: &[u8],
2641    stride: u32,
2642    width: u32,
2643    height: u32,
2644    context: &'static str,
2645) -> Result<(), BridgeError> {
2646    let required = usize::try_from(stride).ok().and_then(|stride| {
2647        usize::try_from(height)
2648            .ok()
2649            .and_then(|height| stride.checked_mul(height))
2650    });
2651    if stride < width || required != Some(bytes.len()) {
2652        Err(BridgeError::InvalidInput(context))
2653    } else {
2654        Ok(())
2655    }
2656}
2657
2658fn copy_plane_to_frame(
2659    source: &[u8],
2660    source_stride: u32,
2661    width: u32,
2662    height: u32,
2663    destination: *mut u8,
2664    destination_stride: i32,
2665) -> Result<(), BridgeError> {
2666    let row_bytes =
2667        usize::try_from(width).map_err(|_| BridgeError::InvalidInput("YUV row width overflow"))?;
2668    let rows =
2669        usize::try_from(height).map_err(|_| BridgeError::InvalidInput("YUV row count overflow"))?;
2670    let source_stride = usize::try_from(source_stride)
2671        .map_err(|_| BridgeError::InvalidInput("YUV source stride overflow"))?;
2672    let destination_stride = usize::try_from(destination_stride)
2673        .map_err(|_| BridgeError::InvalidInput("negative YUV destination stride"))?;
2674    if destination.is_null() || destination_stride < row_bytes {
2675        return Err(BridgeError::InvalidInput(
2676            "YUV destination plane is too small",
2677        ));
2678    }
2679    for row in 0..rows {
2680        let source_offset = row
2681            .checked_mul(source_stride)
2682            .ok_or(BridgeError::InvalidInput("YUV source offset overflow"))?;
2683        let destination_offset = row
2684            .checked_mul(destination_stride)
2685            .ok_or(BridgeError::InvalidInput("YUV destination offset overflow"))?;
2686        // SAFETY: the destination has `height` writable rows covering `width` bytes, and the source
2687        // length was checked against its stride and row count.
2688        unsafe {
2689            ptr::copy_nonoverlapping(
2690                source.as_ptr().add(source_offset),
2691                destination.add(destination_offset),
2692                row_bytes,
2693            );
2694        }
2695    }
2696    Ok(())
2697}
2698
2699/// Copies planar `f32` samples into an audio frame of `format` (`FLTP`, `FLT` or `S16`). 16-bit
2700/// samples are clamped to [-1, 1] and rounded.
2701///
2702/// # Errors
2703/// Returns an error for mismatched channels, invalid dimensions, another sample format, or an
2704/// allocation failure.
2705pub fn audio_frame(
2706    channels: &[Vec<f32>],
2707    format: ffi::AVSampleFormat,
2708    sample_rate: i32,
2709    pts: i64,
2710) -> Result<AVFrame, BridgeError> {
2711    let Some(samples) = channels.first().map(Vec::len) else {
2712        return Err(BridgeError::InvalidInput("invalid planar audio shape"));
2713    };
2714    if sample_rate <= 0
2715        || samples == 0
2716        || channels.len() > ffi::AV_NUM_DATA_POINTERS as usize
2717        || channels.iter().any(|channel| channel.len() != samples)
2718    {
2719        return Err(BridgeError::InvalidInput(
2720            "planar audio channel lengths differ",
2721        ));
2722    }
2723    let sample_bytes = match format {
2724        ffi::AV_SAMPLE_FMT_FLTP | ffi::AV_SAMPLE_FMT_FLT => size_of::<f32>(),
2725        ffi::AV_SAMPLE_FMT_S16 => size_of::<i16>(),
2726        _ => return Err(BridgeError::InvalidInput("unsupported audio sample format")),
2727    };
2728    let planar = format == ffi::AV_SAMPLE_FMT_FLTP;
2729    let channel_count = i32::try_from(channels.len())
2730        .map_err(|_| BridgeError::InvalidInput("audio channel count overflow"))?;
2731    let sample_count = i32::try_from(samples)
2732        .map_err(|_| BridgeError::InvalidInput("audio sample count overflow"))?;
2733    let mut frame = AVFrame::new();
2734    frame.set_format(format);
2735    frame.set_ch_layout(
2736        rsmpeg::avutil::AVChannelLayout::from_nb_channels(channel_count).into_inner(),
2737    );
2738    frame.set_sample_rate(sample_rate);
2739    frame.set_nb_samples(sample_count);
2740    frame.set_pts(pts);
2741    frame.set_time_base(ffi::AVRational {
2742        num: 1,
2743        den: sample_rate,
2744    });
2745    frame.get_buffer(0)?;
2746    frame.make_writable()?;
2747    let plane_bytes = samples
2748        .checked_mul(sample_bytes)
2749        .and_then(|bytes| bytes.checked_mul(if planar { 1 } else { channels.len() }))
2750        .ok_or(BridgeError::InvalidInput("audio plane byte count overflow"))?;
2751    let plane_capacity = usize::try_from(frame.linesize[0]).unwrap_or(0);
2752    if plane_capacity < plane_bytes {
2753        return Err(BridgeError::InvalidInput(
2754            "audio plane capacity is too small",
2755        ));
2756    }
2757    let planes = if planar { channels.len() } else { 1 };
2758    if frame.data[..planes].iter().any(|plane| plane.is_null()) {
2759        return Err(BridgeError::InvalidInput("audio plane pointer is null"));
2760    }
2761    if planar {
2762        for (index, source) in channels.iter().enumerate() {
2763            // SAFETY: av_frame_get_buffer allocated one writable plane per channel of `linesize[0]`
2764            // bytes, checked above to hold the channel.
2765            unsafe {
2766                ptr::copy_nonoverlapping(
2767                    source.as_ptr().cast::<u8>(),
2768                    frame.data[index],
2769                    plane_bytes,
2770                );
2771            }
2772        }
2773        return Ok(frame);
2774    }
2775    // SAFETY: the single interleaved plane was checked to hold every sample and is unshared.
2776    let plane = unsafe { std::slice::from_raw_parts_mut(frame.data[0], plane_bytes) };
2777    let interleaved = (0..samples).flat_map(|sample| channels.iter().map(move |c| c[sample]));
2778    if format == ffi::AV_SAMPLE_FMT_FLT {
2779        for (bytes, value) in plane.as_chunks_mut::<4>().0.iter_mut().zip(interleaved) {
2780            *bytes = value.to_ne_bytes();
2781        }
2782    } else {
2783        for (bytes, value) in plane.as_chunks_mut::<2>().0.iter_mut().zip(interleaved) {
2784            // In range after the clamp, so the cast cannot truncate.
2785            #[allow(clippy::cast_possible_truncation)]
2786            let value = (value.clamp(-1.0, 1.0) * 32_767.0).round() as i16;
2787            *bytes = value.to_ne_bytes();
2788        }
2789    }
2790    Ok(frame)
2791}
2792
2793/// The `AVChannel` of each channel of `layout`, in order; empty for `AV_CHANNEL_ORDER_UNSPEC`.
2794///
2795/// Takes rsmpeg's wrapper because a bare `AVChannelLayout` can carry a forged channel map:
2796///
2797/// ```compile_fail,E0308
2798/// use rsmpeg::ffi;
2799/// let forged = ffi::AVChannelLayout {
2800///     order: ffi::AV_CHANNEL_ORDER_CUSTOM,
2801///     nb_channels: 4,
2802///     u: ffi::AVChannelLayout__bindgen_ty_1 { map: 0x10 as *mut _ },
2803///     opaque: std::ptr::null_mut(),
2804/// };
2805/// alpha2_ffmpeg_bridge::channel_layout_channels(&forged);
2806/// ```
2807#[must_use]
2808pub fn channel_layout_channels(layout: &rsmpeg::avutil::AVChannelLayout) -> Vec<ffi::AVChannel> {
2809    if layout.order == ffi::AV_CHANNEL_ORDER_UNSPEC {
2810        return Vec::new();
2811    }
2812    let channels = u32::try_from(layout.nb_channels).unwrap_or(0);
2813    (0..channels)
2814        .map(|index| {
2815            layout
2816                .channel_from_index(index)
2817                .unwrap_or(ffi::AV_CHAN_NONE)
2818        })
2819        .collect()
2820}
2821
2822/// A channel layout of `channels` in order, native when possible, else custom.
2823///
2824/// # Errors
2825/// Returns an error for no channels or when `FFmpeg` cannot allocate or retype the layout.
2826pub fn channel_layout_of(
2827    channels: &[ffi::AVChannel],
2828) -> Result<rsmpeg::avutil::AVChannelLayout, BridgeError> {
2829    let count = i32::try_from(channels.len())
2830        .ok()
2831        .filter(|count| *count > 0)
2832        .ok_or(BridgeError::InvalidInput("a channel layout needs channels"))?;
2833    let mut raw = std::mem::MaybeUninit::<ffi::AVChannelLayout>::zeroed();
2834    // SAFETY: `custom_init` gives the zeroed layout a map of `count` entries, all written below.
2835    let initialized = unsafe { ffi::av_channel_layout_custom_init(raw.as_mut_ptr(), count) };
2836    if initialized < 0 {
2837        return Err(BridgeError::FfmpegCode(initialized));
2838    }
2839    // SAFETY: initialized above; from here `layout` owns it and uninitializes it on drop.
2840    let mut layout = unsafe { rsmpeg::avutil::AVChannelLayout::new(raw.assume_init()) };
2841    for (index, channel) in channels.iter().enumerate() {
2842        // SAFETY: a custom layout's map holds `count` entries, and `index` is below it.
2843        unsafe { (*layout.u.map.add(index)).id = *channel };
2844    }
2845    let flags = i32::try_from(ffi::AV_CHANNEL_LAYOUT_RETYPE_FLAG_CANONICAL).unwrap_or(0);
2846    // SAFETY: `layout` is a valid custom layout that `retype` may rewrite in place.
2847    let retyped = unsafe {
2848        ffi::av_channel_layout_retype(layout.as_mut_ptr(), ffi::AV_CHANNEL_ORDER_UNSPEC, flags)
2849    };
2850    if retyped < 0 {
2851        return Err(BridgeError::FfmpegCode(retyped));
2852    }
2853    Ok(layout)
2854}
2855
2856/// Copies a packed `AV_SAMPLE_FMT_FLT` frame into an owned interleaved vector.
2857///
2858/// # Errors
2859/// Returns an error if the frame format, dimensions, or buffer is invalid.
2860pub fn audio_f32_interleaved(frame: &AVFrame) -> Result<Vec<f32>, BridgeError> {
2861    if frame.format != ffi::AV_SAMPLE_FMT_FLT || frame.nb_samples < 0 {
2862        return Err(BridgeError::InvalidInput(
2863            "packed audio format or sample count is invalid",
2864        ));
2865    }
2866    let channels = usize::try_from(frame.ch_layout().nb_channels)
2867        .map_err(|_| BridgeError::InvalidInput("packed audio channel count is negative"))?;
2868    let frames = usize::try_from(frame.nb_samples)
2869        .map_err(|_| BridgeError::InvalidInput("packed audio sample count is negative"))?;
2870    let samples = frames
2871        .checked_mul(channels)
2872        .ok_or(BridgeError::InvalidInput(
2873            "packed audio sample count overflow",
2874        ))?;
2875    let bytes = samples
2876        .checked_mul(size_of::<f32>())
2877        .ok_or(BridgeError::InvalidInput(
2878            "packed audio byte count overflow",
2879        ))?;
2880    if channels == 0
2881        || frame.data[0].is_null()
2882        || usize::try_from(frame.linesize[0]).unwrap_or(0) < bytes
2883    {
2884        return Err(BridgeError::InvalidInput(
2885            "packed audio plane is missing or too small",
2886        ));
2887    }
2888    let mut output = vec![0.0_f32; samples];
2889    // SAFETY: the FLT plane was checked to hold every sample, and `output` is exactly that size.
2890    unsafe {
2891        ptr::copy_nonoverlapping(frame.data[0], output.as_mut_ptr().cast::<u8>(), bytes);
2892    }
2893    Ok(output)
2894}
2895
2896/// The rotation a display matrix asks for, in `FFmpeg`'s convention.
2897#[derive(Copy, Clone, Debug, PartialEq)]
2898pub struct DisplayRotation {
2899    /// Counter-clockwise angle in degrees, in `[-180, 180]`.
2900    pub counter_clockwise_degrees: f64,
2901    /// Whether the matrix also mirrors: this rotation followed by a horizontal flip.
2902    pub flipped: bool,
2903}
2904
2905/// Reads a stream display matrix.
2906#[must_use]
2907pub fn display_rotation(parameters: &AVCodecParameters) -> Option<DisplayRotation> {
2908    let count = usize::try_from(parameters.nb_coded_side_data).ok()?;
2909    if count == 0 || count > 64 || parameters.coded_side_data.is_null() {
2910        return None;
2911    }
2912    // SAFETY: the borrowed parameters own exactly `nb_coded_side_data` entries.
2913    let entries = unsafe { slice::from_raw_parts(parameters.coded_side_data, count) };
2914    let matrix = entries.iter().find(|entry| {
2915        entry.type_ == ffi::AV_PKT_DATA_DISPLAYMATRIX
2916            && entry.size >= 9 * size_of::<i32>()
2917            && !entry.data.is_null()
2918    })?;
2919    // SAFETY: the matching side-data entry was validated to hold a complete 3x3 display matrix.
2920    unsafe { matrix_rotation(matrix.data) }
2921}
2922
2923/// Reads a frame's display matrix, such as the JPEG decoder attaches for Exif `Orientation`.
2924#[must_use]
2925pub fn frame_display_rotation(frame: &AVFrame) -> Option<DisplayRotation> {
2926    // SAFETY: `frame` is a live frame; the returned entry, if any, is owned by it for this borrow.
2927    let entry =
2928        unsafe { ffi::av_frame_get_side_data(frame.as_ptr(), ffi::AV_FRAME_DATA_DISPLAYMATRIX) };
2929    if entry.is_null() {
2930        return None;
2931    }
2932    // SAFETY: a non-null entry returned by `av_frame_get_side_data` is a valid side-data record.
2933    let entry = unsafe { &*entry };
2934    if entry.size < 9 * size_of::<i32>() || entry.data.is_null() {
2935        return None;
2936    }
2937    // SAFETY: the entry was validated to hold a complete 3x3 display matrix.
2938    unsafe { matrix_rotation(entry.data) }
2939}
2940
2941/// # Safety
2942/// `data` must point at nine readable `i32`s, aligned or not.
2943unsafe fn matrix_rotation(data: *const u8) -> Option<DisplayRotation> {
2944    let mut aligned = [0_i32; 9];
2945    // SAFETY: the caller guarantees a complete 3x3 matrix; a byte copy needs no alignment.
2946    unsafe {
2947        ptr::copy_nonoverlapping(
2948            data,
2949            aligned.as_mut_ptr().cast::<u8>(),
2950            size_of::<[i32; 9]>(),
2951        );
2952    }
2953    // `av_display_rotation_get` is meaningless for a mirroring matrix (negative 2x2 determinant),
2954    // so remove the horizontal flip first.
2955    let determinant = i64::from(aligned[0]) * i64::from(aligned[4])
2956        - i64::from(aligned[1]) * i64::from(aligned[3]);
2957    let flipped = determinant < 0;
2958    if flipped {
2959        // SAFETY: `aligned` is a complete, correctly aligned 3x3 matrix.
2960        unsafe { ffi::av_display_matrix_flip(aligned.as_mut_ptr(), 1, 0) };
2961    }
2962    // SAFETY: `aligned` is a complete, correctly aligned 3x3 display matrix.
2963    let angle = unsafe { ffi::av_display_rotation_get(aligned.as_ptr()) };
2964    angle.is_finite().then_some(DisplayRotation {
2965        counter_clockwise_degrees: angle,
2966        flipped,
2967    })
2968}
2969
2970const fn size_of<T>() -> usize {
2971    std::mem::size_of::<T>()
2972}
2973
2974/// A reusable `FFmpeg` YUV420P frame the caller writes into directly.
2975///
2976/// Avoids [`yuv420p_frame`]'s per-frame allocation and copy. [`Yuv420pFrameBuffer::begin_write`]
2977/// returns plane memory only once `FFmpeg` no longer shares the buffer, copying it if needed.
2978pub struct Yuv420pFrameBuffer {
2979    frame: AVFrame,
2980    width: usize,
2981    height: usize,
2982    /// Each plane's visible bytes per row and rows; empty for YUV420P.
2983    planes: Vec<(usize, usize)>,
2984}
2985
2986/// Writable plane memory for one reusable frame.
2987pub struct Yuv420pFramePlanes<'a> {
2988    /// Luma rows, `y_stride` bytes apart.
2989    pub y: &'a mut [u8],
2990    /// Distance between luma rows.
2991    pub y_stride: usize,
2992    /// Blue-difference chroma rows, `chroma_stride` bytes apart.
2993    pub u: &'a mut [u8],
2994    /// Red-difference chroma rows, `chroma_stride` bytes apart.
2995    pub v: &'a mut [u8],
2996    /// Distance between chroma rows.
2997    pub chroma_stride: usize,
2998}
2999
3000impl Yuv420pFrameBuffer {
3001    /// Allocates one reusable frame for this exact coded size.
3002    ///
3003    /// # Errors
3004    /// Returns an error for a non-positive size or a failed allocation.
3005    pub fn new(width: i32, height: i32) -> Result<Self, BridgeError> {
3006        let coded_width =
3007            usize::try_from(width).map_err(|_| BridgeError::InvalidInput("negative YUV width"))?;
3008        let coded_height = usize::try_from(height)
3009            .map_err(|_| BridgeError::InvalidInput("negative YUV height"))?;
3010        if coded_width == 0 || coded_height == 0 {
3011            return Err(BridgeError::InvalidInput("empty YUV frame"));
3012        }
3013        Ok(Self {
3014            frame: zeroed_frame(width, height, ffi::AV_PIX_FMT_YUV420P)?,
3015            width: coded_width,
3016            height: coded_height,
3017            planes: Vec::new(),
3018        })
3019    }
3020
3021    /// Allocates one reusable frame of `format`; `planes` gives each plane's visible bytes per row
3022    /// and rows in `FFmpeg` order.
3023    ///
3024    /// # Errors
3025    /// Returns an error for a non-positive size, a failed allocation, or planes the format lacks.
3026    pub fn with_format(
3027        width: i32,
3028        height: i32,
3029        format: ffi::AVPixelFormat,
3030        planes: &[(usize, usize)],
3031    ) -> Result<Self, BridgeError> {
3032        let coded_width = usize::try_from(width)
3033            .map_err(|_| BridgeError::InvalidInput("negative frame width"))?;
3034        let coded_height = usize::try_from(height)
3035            .map_err(|_| BridgeError::InvalidInput("negative frame height"))?;
3036        if coded_width == 0 || coded_height == 0 || planes.is_empty() || planes.len() > 4 {
3037            return Err(BridgeError::InvalidInput("empty frame"));
3038        }
3039        let frame = zeroed_frame(width, height, format)?;
3040        for (index, &(row_bytes, _)) in planes.iter().enumerate() {
3041            if frame.data[index].is_null()
3042                || plane_stride(frame.linesize[index], row_bytes).is_err()
3043            {
3044                return Err(BridgeError::InvalidInput(
3045                    "the frame's planes are not the ones described",
3046                ));
3047            }
3048        }
3049        Ok(Self {
3050            frame,
3051            width: coded_width,
3052            height: coded_height,
3053            planes: planes.to_vec(),
3054        })
3055    }
3056
3057    /// Each plane's memory and stride after making the frame unshared, for any format.
3058    ///
3059    /// # Errors
3060    /// As [`Self::begin_write`].
3061    pub fn begin_write_planes(&mut self) -> Result<Vec<(&mut [u8], usize)>, BridgeError> {
3062        if self.planes.is_empty() {
3063            let planes = self.begin_write()?;
3064            let chroma = planes.chroma_stride;
3065            return Ok(vec![
3066                (planes.y, planes.y_stride),
3067                (planes.u, chroma),
3068                (planes.v, chroma),
3069            ]);
3070        }
3071        self.make_writable()?;
3072        let mut written = Vec::with_capacity(self.planes.len());
3073        for (index, &(row_bytes, rows)) in self.planes.iter().enumerate() {
3074            let stride = plane_stride(self.frame.linesize[index], row_bytes)?;
3075            // SAFETY: `zeroed_frame` allocated `rows` rows of `linesize` bytes per plane, and
3076            // `make_writable` left this frame the sole owner.
3077            let plane = unsafe { writable_plane(self.frame.data[index], stride, row_bytes, rows)? };
3078            written.push((plane, stride));
3079        }
3080        Ok(written)
3081    }
3082
3083    fn make_writable(&mut self) -> Result<(), BridgeError> {
3084        // Not `av_frame_make_writable`: it leaves SIMD row padding unwritten under slices that span
3085        // it.
3086        if !self.frame.is_writable().map_err(BridgeError::Ffmpeg)? {
3087            let mut copy = zeroed_frame(self.frame.width, self.frame.height, self.frame.format)?;
3088            // SAFETY: both frames are live with one size and format; `copy` is unshared and
3089            // `self.frame` is only read.
3090            let status = unsafe {
3091                let copied = ffi::av_frame_copy(copy.as_mut_ptr(), self.frame.as_ptr());
3092                if copied < 0 {
3093                    copied
3094                } else {
3095                    ffi::av_frame_copy_props(copy.as_mut_ptr(), self.frame.as_ptr())
3096                }
3097            };
3098            if status < 0 {
3099                return Err(BridgeError::FfmpegCode(status));
3100            }
3101            self.frame = copy;
3102        }
3103        Ok(())
3104    }
3105
3106    /// Luma width and height in pixels.
3107    #[must_use]
3108    pub const fn dimensions(&self) -> (usize, usize) {
3109        (self.width, self.height)
3110    }
3111
3112    /// Returns the plane memory to overwrite, after making the frame unshared.
3113    ///
3114    /// # Errors
3115    /// Returns an error when the buffer cannot be made writable or its planes changed layout.
3116    pub fn begin_write(&mut self) -> Result<Yuv420pFramePlanes<'_>, BridgeError> {
3117        if !self.planes.is_empty() {
3118            return Err(BridgeError::InvalidInput("the frame is not YUV420P"));
3119        }
3120        self.make_writable()?;
3121        let chroma_width = self.width.div_ceil(2);
3122        let chroma_height = self.height.div_ceil(2);
3123        let y_stride = plane_stride(self.frame.linesize[0], self.width)?;
3124        let u_stride = plane_stride(self.frame.linesize[1], chroma_width)?;
3125        let v_stride = plane_stride(self.frame.linesize[2], chroma_width)?;
3126        if u_stride != v_stride {
3127            return Err(BridgeError::InvalidInput("mismatched chroma strides"));
3128        }
3129        // SAFETY: `zeroed_frame` allocated and wrote `height` rows of `linesize` bytes per plane,
3130        // and the writability check makes this frame the sole owner.
3131        let y = unsafe { writable_plane(self.frame.data[0], y_stride, self.width, self.height)? };
3132        // SAFETY: as above, for the two half-resolution chroma planes.
3133        let u =
3134            unsafe { writable_plane(self.frame.data[1], u_stride, chroma_width, chroma_height)? };
3135        // SAFETY: as above.
3136        let v =
3137            unsafe { writable_plane(self.frame.data[2], v_stride, chroma_width, chroma_height)? };
3138        Ok(Yuv420pFramePlanes {
3139            y,
3140            y_stride,
3141            u,
3142            v,
3143            chroma_stride: u_stride,
3144        })
3145    }
3146
3147    /// Stamps the presentation timestamp this frame will be encoded with.
3148    pub fn set_presentation(&mut self, pts: i64, time_base: ffi::AVRational) {
3149        self.frame.set_pts(pts);
3150        self.frame.set_time_base(time_base);
3151    }
3152
3153    /// The frame to hand to an encoder.
3154    #[must_use]
3155    pub const fn frame(&self) -> &AVFrame {
3156        &self.frame
3157    }
3158
3159    /// Coded luma dimensions this buffer was allocated for.
3160    #[must_use]
3161    pub const fn size(&self) -> (usize, usize) {
3162        (self.width, self.height)
3163    }
3164}
3165
3166/// A frame of `format` with tightly packed rows, every byte of it zero.
3167fn zeroed_frame(
3168    width: i32,
3169    height: i32,
3170    format: ffi::AVPixelFormat,
3171) -> Result<AVFrame, BridgeError> {
3172    let mut frame = AVFrame::new();
3173    frame.set_format(format);
3174    frame.set_width(width);
3175    frame.set_height(height);
3176    frame.get_buffer(1)?;
3177    for buffer in frame.buf {
3178        // SAFETY: a non-null `AVFrame::buf` entry is live, and this new frame is its only owner.
3179        if let Some(buffer) = unsafe { buffer.as_ref() } {
3180            // SAFETY: as above.
3181            unsafe { ptr::write_bytes(buffer.data, 0, buffer.size) };
3182        }
3183    }
3184    Ok(frame)
3185}
3186
3187impl fmt::Debug for Yuv420pFrameBuffer {
3188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3189        formatter
3190            .debug_struct("Yuv420pFrameBuffer")
3191            .field("width", &self.width)
3192            .field("height", &self.height)
3193            .finish_non_exhaustive()
3194    }
3195}
3196
3197fn plane_stride(linesize: i32, width: usize) -> Result<usize, BridgeError> {
3198    let stride = usize::try_from(linesize)
3199        .map_err(|_| BridgeError::InvalidInput("negative frame linesize"))?;
3200    if stride < width {
3201        return Err(BridgeError::InvalidInput(
3202            "frame linesize is below its width",
3203        ));
3204    }
3205    Ok(stride)
3206}
3207
3208/// # Safety
3209/// `data` must point at `height` rows of `stride` bytes the caller solely owns.
3210unsafe fn writable_plane<'a>(
3211    data: *mut u8,
3212    stride: usize,
3213    width: usize,
3214    height: usize,
3215) -> Result<&'a mut [u8], BridgeError> {
3216    if data.is_null() {
3217        return Err(BridgeError::InvalidInput("frame plane is null"));
3218    }
3219    let len = stride
3220        .checked_mul(height.saturating_sub(1))
3221        .and_then(|rows| rows.checked_add(width))
3222        .ok_or(BridgeError::InvalidInput("frame plane size overflow"))?;
3223    // SAFETY: delegated to this function's contract.
3224    Ok(unsafe { slice::from_raw_parts_mut(data, len) })
3225}
3226
3227#[cfg(all(test, target_os = "linux", feature = "hwdevice"))]
3228mod vulkan_shim_tests;
3229
3230#[cfg(all(test, target_os = "linux", feature = "hwdevice"))]
3231mod vulkan_access_tests;
3232
3233#[cfg(test)]
3234mod tests;