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