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    // Ownership moves to FFmpeg here, before the call that frees it.
1782    let mut dictionary = options.into_raw().as_ptr();
1783    // SAFETY: `context` is a live codec context, and `dictionary` is a valid dictionary FFmpeg now
1784    // owns. FFmpeg replaces it with the remainder, which is either null or a dictionary this
1785    // function takes ownership of below.
1786    let status = unsafe {
1787        ffi::avcodec_open2(
1788            context.as_mut_ptr(),
1789            ptr::null_mut(),
1790            std::ptr::addr_of_mut!(dictionary),
1791        )
1792    };
1793    // SAFETY: a non-null remainder is a dictionary FFmpeg allocated and handed back through the
1794    // same pointer, so this is the only owner of it.
1795    let unused = ptr::NonNull::new(dictionary)
1796        .map(|dictionary| unsafe { rsmpeg::avutil::AVDictionary::from_raw(dictionary) });
1797    if status < 0 {
1798        return Err(RsmpegError::from(status));
1799    }
1800    Ok(unused)
1801}
1802
1803/// `VkImageUsageFlags` an encoder-input pool needs beyond `FFmpeg`'s own.
1804///
1805/// `TRANSFER_DST` is what the renderer copies into, `SAMPLED` is what lets
1806/// wgpu import the image for a read, and `TRANSFER_SRC` keeps a readback
1807/// possible for parity checks. `VIDEO_ENCODE_SRC` is the encoder's own.
1808#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1809pub const VULKAN_ENCODE_INPUT_USAGE: u32 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004 | 0x0000_4000;
1810
1811/// Adds `usage` to a Vulkan frames context before it allocates its pool.
1812///
1813/// `FFmpeg`'s Vulkan encoder requests no usage of its own. Left at zero,
1814/// `hwcontext_vulkan` picks one: whichever of `TRANSFER_DST`, `TRANSFER_SRC`,
1815/// `STORAGE` and `SAMPLED` the format supports, plus host transfer and
1816/// `VIDEO_ENCODE_SRC` when the device has them, and it then only takes a
1817/// format that supports `STORAGE`. A non-zero usage is used exactly as given:
1818/// `FFmpeg` 8.0.3 drops no flag of it, and every flag must be one the format
1819/// supports, or the pool's images are created with a usage they cannot have.
1820/// So an encoder-input pool asks for [`VULKAN_ENCODE_INPUT_USAGE`], what an
1821/// export does with its images and nothing else.
1822///
1823/// This must be called between `av_hwframe_ctx_alloc` and
1824/// `av_hwframe_ctx_init`; afterwards the images already exist.
1825///
1826/// # Errors
1827/// Returns an error when the context is not a Vulkan one.
1828#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1829pub fn vulkan_frames_add_usage(
1830    frames: &mut rsmpeg::avutil::AVHWFramesContext,
1831    usage: u32,
1832) -> Result<(), BridgeError> {
1833    // SAFETY: `frames` is a live frames context. The shim checks that its device is a Vulkan one
1834    // before treating `hwctx` as Vulkan's, and only ORs into its `usage` field.
1835    let status = unsafe { alpha2_vulkan_frames_add_usage(frames.as_mut_ptr(), usage) };
1836    if status < 0 {
1837        return Err(BridgeError::FfmpegCode(status));
1838    }
1839    Ok(())
1840}
1841
1842/// Usage the pool was created with.
1843///
1844/// For a pool given a usage before its init, that usage: `hwcontext_vulkan`
1845/// keeps a non-zero usage as given and drops nothing from it (see
1846/// [`vulkan_frames_add_usage`]). For one left at zero, the usage `FFmpeg`
1847/// picked.
1848#[cfg(all(target_os = "linux", feature = "hwdevice"))]
1849#[must_use]
1850pub fn vulkan_frames_usage(frames: &rsmpeg::avutil::AVHWFramesContext) -> u32 {
1851    // SAFETY: `frames` is a live frames context; the shim checks its device type before reading
1852    // Vulkan's `hwctx`, and returns 0 for any other.
1853    unsafe { alpha2_vulkan_frames_usage(frames.as_ptr()) }
1854}
1855
1856/// Copies one packet's payload out of `FFmpeg`'s buffer.
1857///
1858/// `AVPacket` exposes its bytes as a raw pointer and a length, which is the
1859/// kind of pair this crate exists to keep out of the rest of the binding.
1860#[must_use]
1861pub fn packet_bytes(packet: &rsmpeg::avcodec::AVPacket) -> Vec<u8> {
1862    let size = usize::try_from(packet.size).unwrap_or(0);
1863    if packet.data.is_null() || size == 0 {
1864        return Vec::new();
1865    }
1866    // SAFETY: a received packet owns `size` readable bytes at `data` for as long as the packet is
1867    // alive, and `packet` is borrowed for this call.
1868    unsafe { slice::from_raw_parts(packet.data.cast_const(), size) }.to_vec()
1869}
1870
1871/// A copy of a stream's codec extradata: the sample description's parameter
1872/// sets for H.264 and HEVC.
1873///
1874/// It takes rsmpeg's parameters, which only `FFmpeg` or `unsafe` code builds
1875/// (`AVStream::codecpar`): a bare `AVCodecParameters` can carry a forged
1876/// extradata pointer.
1877///
1878/// ```compile_fail,E0308
1879/// let mut forged: rsmpeg::ffi::AVCodecParameters = *rsmpeg::avcodec::AVCodecParameters::new();
1880/// forged.extradata = 0x10 as *mut u8;
1881/// forged.extradata_size = 64;
1882/// alpha2_ffmpeg_bridge::codec_parameters_extradata(&forged);
1883/// ```
1884#[must_use]
1885pub fn codec_parameters_extradata(parameters: &rsmpeg::avcodec::AVCodecParameters) -> Vec<u8> {
1886    let size = usize::try_from(parameters.extradata_size).unwrap_or(0);
1887    if parameters.extradata.is_null() || size == 0 {
1888        return Vec::new();
1889    }
1890    // SAFETY: codec parameters own `extradata_size` readable bytes at `extradata` for as long as
1891    // they are borrowed, which covers this copy.
1892    unsafe { slice::from_raw_parts(parameters.extradata.cast_const(), size) }.to_vec()
1893}
1894
1895/// Whether `packet` carries side data of `kind`, as a demuxer marks the
1896/// packet where a stream's sample description changes
1897/// (`AV_PKT_DATA_NEW_EXTRADATA`).
1898#[must_use]
1899pub fn packet_has_side_data(
1900    packet: &rsmpeg::avcodec::AVPacket,
1901    kind: ffi::AVPacketSideDataType,
1902) -> bool {
1903    let mut size = 0_usize;
1904    // SAFETY: `packet` is a live packet borrowed for the call; the returned pointer is only
1905    // compared with null.
1906    !unsafe { ffi::av_packet_get_side_data(packet.as_ptr(), kind, &raw mut size) }.is_null()
1907}
1908
1909/// Takes `packet`'s side data of `kind` off it: a copy of its bytes, or
1910/// `None` when it carries none.
1911///
1912/// An encoder's parameter sets arrive as `AV_PKT_DATA_NEW_EXTRADATA` once
1913/// `extract_extradata` has cut them out of the bitstream. They belong in the
1914/// sample description, and the MP4 muxer reads a packet that still carries
1915/// them as a change of sample description, so they are taken rather than
1916/// copied.
1917#[must_use]
1918pub fn take_packet_side_data(
1919    packet: &mut rsmpeg::avcodec::AVPacket,
1920    kind: ffi::AVPacketSideDataType,
1921) -> Option<Vec<u8>> {
1922    let mut size = 0_usize;
1923    // SAFETY: `packet` is a live packet borrowed for the call; a non-null result points at `size`
1924    // bytes the packet owns.
1925    let data = unsafe { ffi::av_packet_get_side_data(packet.as_ptr(), kind, &raw mut size) };
1926    if data.is_null() {
1927        return None;
1928    }
1929    // SAFETY: as above; the bytes are copied before the entry is freed below.
1930    let bytes = unsafe { slice::from_raw_parts(data, size) }.to_vec();
1931    let packet = packet.as_mut_ptr();
1932    // SAFETY: `side_data` and `side_data_elems` are the packet's own array and count, which
1933    // `av_packet_side_data_remove` keeps consistent; it frees the entry's `av_malloc`ed data, which
1934    // is how a packet's side data is allocated.
1935    unsafe {
1936        ffi::av_packet_side_data_remove(
1937            (*packet).side_data,
1938            &raw mut (*packet).side_data_elems,
1939            kind,
1940        );
1941    }
1942    Some(bytes)
1943}
1944
1945/// Gives `packet` side data of `kind` holding `data`, replacing any it had.
1946///
1947/// # Errors
1948/// When the side data cannot be allocated, or `data` is larger than a
1949/// packet's side data can be.
1950pub fn set_packet_side_data(
1951    packet: &mut rsmpeg::avcodec::AVPacket,
1952    kind: ffi::AVPacketSideDataType,
1953    data: &[u8],
1954) -> Result<(), BridgeError> {
1955    let _ = take_packet_side_data(packet, kind);
1956    // SAFETY: `packet` is a live packet mutably borrowed for the call; `av_packet_new_side_data`
1957    // appends a zero-padded entry of `data.len()` bytes and returns null only when it cannot.
1958    let entry = unsafe { ffi::av_packet_new_side_data(packet.as_mut_ptr(), kind, data.len()) };
1959    if entry.is_null() {
1960        return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
1961    }
1962    // SAFETY: the entry just made holds `data.len()` writable bytes.
1963    unsafe { ptr::copy_nonoverlapping(data.as_ptr(), entry, data.len()) };
1964    Ok(())
1965}
1966
1967/// Replaces the extradata (the sample description's parameter sets, for
1968/// H.264 and HEVC) of `output`'s stream `index` with a copy of `data`, for a
1969/// muxer whose header is not written yet.
1970///
1971/// rsmpeg gives an output's existing streams out read-only, and a muxer
1972/// reads a stream's extradata into its sample description when the header
1973/// is written, so this is how a header that waits for an encoder's first
1974/// packet learns its parameter sets. `FFmpeg` owns extradata through
1975/// `av_malloc` with input padding after it, which a parser may read past
1976/// the end, so it cannot be handed a Rust buffer.
1977///
1978/// # Errors
1979/// When there is no stream `index`, or `data` is larger than extradata can
1980/// be or cannot be allocated.
1981pub fn set_output_stream_extradata(
1982    output: &mut rsmpeg::avformat::AVFormatContextOutput,
1983    index: usize,
1984    data: &[u8],
1985) -> Result<(), BridgeError> {
1986    let parameters = output
1987        .streams()
1988        .get(index)
1989        .ok_or(BridgeError::InvalidInput("the output has no such stream"))?
1990        .codecpar;
1991    let size = i32::try_from(data.len())
1992        .map_err(|_| BridgeError::InvalidInput("extradata is larger than FFmpeg holds"))?;
1993    let padding = usize::try_from(ffi::AV_INPUT_BUFFER_PADDING_SIZE).unwrap_or(64);
1994    // SAFETY: `av_mallocz` returns null or a zeroed buffer of the size asked for.
1995    let buffer = unsafe { ffi::av_mallocz(data.len() + padding) }.cast::<u8>();
1996    if buffer.is_null() {
1997        return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
1998    }
1999    // SAFETY: `buffer` holds `data.len()` writable bytes and does not overlap `data`.
2000    unsafe { ptr::copy_nonoverlapping(data.as_ptr(), buffer, data.len()) };
2001    // SAFETY: a stream's codec parameters are always allocated and owned by the stream, and the
2002    // output is mutably borrowed for the call. Their extradata is null or an `av_malloc`ed buffer
2003    // they own, which is freed and replaced by `buffer`, which they now own.
2004    unsafe {
2005        ffi::av_freep((&raw mut (*parameters).extradata).cast());
2006        (*parameters).extradata = buffer;
2007        (*parameters).extradata_size = size;
2008    }
2009    Ok(())
2010}
2011
2012/// A packet holding a copy of `data`, every other field at its default.
2013///
2014/// An encoder that is not `FFmpeg`'s (`OpenH264`, loaded at run time) hands
2015/// its output over as bytes, and the muxer takes packets; rsmpeg can make an
2016/// empty packet but not give one a payload.
2017///
2018/// # Errors
2019/// When `data` is larger than a packet can be, or the packet cannot be
2020/// allocated.
2021pub fn packet_from_bytes(data: &[u8]) -> Result<rsmpeg::avcodec::AVPacket, BridgeError> {
2022    let size = i32::try_from(data.len())
2023        .map_err(|_| BridgeError::InvalidInput("a packet is larger than FFmpeg holds"))?;
2024    let mut packet = rsmpeg::avcodec::AVPacket::new();
2025    // SAFETY: `packet` is a freshly allocated, empty packet; `av_new_packet` gives it a
2026    // reference-counted, zero-padded buffer of `size` bytes or fails without touching it.
2027    let status = unsafe { ffi::av_new_packet(packet.as_mut_ptr(), size) };
2028    if status < 0 {
2029        return Err(BridgeError::FfmpegCode(status));
2030    }
2031    if !data.is_empty() {
2032        // SAFETY: the packet's buffer holds `size` writable bytes and does not overlap `data`.
2033        unsafe { ptr::copy_nonoverlapping(data.as_ptr(), packet.data, data.len()) };
2034    }
2035    Ok(packet)
2036}
2037
2038/// What the codec parameters of a video stream no `FFmpeg` encoder produced
2039/// say: the fields an encoder's `avcodec_parameters_from_context` would
2040/// fill in for it.
2041#[derive(Clone, Copy, Debug)]
2042pub struct VideoStreamDescription<'a> {
2043    /// The codec.
2044    pub codec_id: ffi::AVCodecID,
2045    /// Coded width.
2046    pub width: i32,
2047    /// Coded height.
2048    pub height: i32,
2049    /// The pixel format the stream decodes to.
2050    pub pixel_format: ffi::AVPixelFormat,
2051    /// `AV_PROFILE_*` for the codec.
2052    pub profile: i32,
2053    /// The codec's level number.
2054    pub level: i32,
2055    /// Colour primaries tag.
2056    pub color_primaries: ffi::AVColorPrimaries,
2057    /// Transfer characteristics tag.
2058    pub color_trc: ffi::AVColorTransferCharacteristic,
2059    /// Matrix coefficients tag.
2060    pub color_space: ffi::AVColorSpace,
2061    /// Sample range.
2062    pub color_range: ffi::AVColorRange,
2063    /// Pixel aspect ratio; `0/1` when undeclared.
2064    pub sample_aspect_ratio: ffi::AVRational,
2065    /// Frames per second.
2066    pub frame_rate: ffi::AVRational,
2067    /// Average bitrate, `0` when unknown.
2068    pub bit_rate: i64,
2069    /// The sample description (`avcC` for H.264).
2070    pub extradata: &'a [u8],
2071}
2072
2073/// Codec parameters for a video stream `description` describes.
2074///
2075/// # Errors
2076/// When the extradata is larger than `FFmpeg` holds or cannot be allocated.
2077pub fn video_codec_parameters(
2078    description: &VideoStreamDescription<'_>,
2079) -> Result<rsmpeg::avcodec::AVCodecParameters, BridgeError> {
2080    let extradata_size = i32::try_from(description.extradata.len())
2081        .map_err(|_| BridgeError::InvalidInput("extradata is larger than FFmpeg holds"))?;
2082    let mut parameters = rsmpeg::avcodec::AVCodecParameters::new();
2083    let raw = parameters.as_mut_ptr();
2084    // SAFETY: `raw` is the freshly allocated parameters, uniquely owned here; the fields written
2085    // are plain values.
2086    unsafe {
2087        (*raw).codec_type = ffi::AVMEDIA_TYPE_VIDEO;
2088        (*raw).codec_id = description.codec_id;
2089        (*raw).width = description.width;
2090        (*raw).height = description.height;
2091        (*raw).format = description.pixel_format;
2092        (*raw).profile = description.profile;
2093        (*raw).level = description.level;
2094        (*raw).color_primaries = description.color_primaries;
2095        (*raw).color_trc = description.color_trc;
2096        (*raw).color_space = description.color_space;
2097        (*raw).color_range = description.color_range;
2098        (*raw).sample_aspect_ratio = description.sample_aspect_ratio;
2099        (*raw).framerate = description.frame_rate;
2100        (*raw).bit_rate = description.bit_rate;
2101        (*raw).field_order = ffi::AV_FIELD_PROGRESSIVE;
2102    }
2103    if !description.extradata.is_empty() {
2104        let padding = usize::try_from(ffi::AV_INPUT_BUFFER_PADDING_SIZE).unwrap_or(64);
2105        // SAFETY: `av_mallocz` returns null or a zeroed buffer of the size asked for.
2106        let buffer = unsafe { ffi::av_mallocz(description.extradata.len() + padding) }.cast::<u8>();
2107        if buffer.is_null() {
2108            return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
2109        }
2110        // SAFETY: `buffer` holds the extradata's length in writable bytes and does not overlap
2111        // it. The parameters had no extradata, and now own `buffer`, which they free with
2112        // `av_free` as they free any.
2113        unsafe {
2114            ptr::copy_nonoverlapping(
2115                description.extradata.as_ptr(),
2116                buffer,
2117                description.extradata.len(),
2118            );
2119            (*raw).extradata = buffer;
2120            (*raw).extradata_size = extradata_size;
2121        }
2122    }
2123    Ok(parameters)
2124}
2125
2126/// Opens a bitstream filter chain, written in `FFmpeg`'s list syntax
2127/// (`name=option=value:option=value,name...`, as `ffmpeg -bsf` takes it), for
2128/// a stream that `parameters` describes and whose packets are timed in
2129/// `time_base`.
2130///
2131/// rsmpeg 0.18 wraps a single filter context, but it can neither parse a
2132/// list nor set a filter's private options, and every filter the exports
2133/// chain needs one (`remove=1`, a VUI field). A filter or an option the
2134/// loaded `FFmpeg` lacks fails here, before any packet.
2135///
2136/// # Errors
2137/// Returns the `FFmpeg` status when the list does not parse, names a filter
2138/// or option the runtime lacks, or a filter refuses the stream.
2139pub fn bitstream_filter_list(
2140    spec: &CStr,
2141    parameters: &AVCodecParameters,
2142    time_base: rsmpeg::avutil::AVRational,
2143) -> Result<rsmpeg::avcodec::AVBSFContext, BridgeError> {
2144    let mut raw = ptr::null_mut();
2145    // SAFETY: `spec` is NUL-terminated for the call; on success FFmpeg stores a context it
2146    // allocated in `raw`, which is taken over below.
2147    let status = unsafe { ffi::av_bsf_list_parse_str(spec.as_ptr(), &raw mut raw) };
2148    if status < 0 {
2149        return Err(BridgeError::FfmpegCode(status));
2150    }
2151    let raw = ptr::NonNull::new(raw).ok_or(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)))?;
2152    // SAFETY: `raw` is a context `av_bsf_list_parse_str` just allocated and nothing else holds; the
2153    // wrapper frees it with `av_bsf_free`.
2154    let mut context = unsafe { rsmpeg::avcodec::AVBSFContextUninit::from_raw(raw) };
2155    // rsmpeg's own `set_par_in` panics when the copy cannot allocate.
2156    // SAFETY: `par_in` is the context's own allocated parameter set, and `parameters` is a live one
2157    // borrowed for the call; `avcodec_parameters_copy` replaces the first with a deep copy.
2158    let status = unsafe { ffi::avcodec_parameters_copy(context.par_in, parameters.as_ptr()) };
2159    if status < 0 {
2160        return Err(BridgeError::FfmpegCode(status));
2161    }
2162    context.set_time_base_in(time_base);
2163    context.init().map_err(BridgeError::Ffmpeg)
2164}
2165
2166/// Sets the codec tag (sample entry) a stream is written with; zero lets the
2167/// output muxer pick the first tag its container uses for the codec.
2168pub fn set_stream_codec_tag(stream: &mut rsmpeg::avformat::AVStreamMut<'_>, tag: u32) {
2169    let mut parameters = stream.codecpar_mut();
2170    // SAFETY: a stream's codec parameters are always allocated and owned by the stream, which is
2171    // mutably borrowed here, and `codec_tag` is a plain integer field.
2172    unsafe { (*parameters.as_mut_ptr()).codec_tag = tag };
2173}
2174
2175/// Gives a stream the display matrix `display` describes, as coded side data
2176/// a muxer writes (MP4's `tkhd` matrix): the rotation, then a horizontal
2177/// flip when it mirrors, as [`display_rotation`] reads one back. Replaces
2178/// any matrix the stream had.
2179///
2180/// # Errors
2181/// When the side data cannot be allocated.
2182pub fn set_stream_display_matrix(
2183    stream: &mut rsmpeg::avformat::AVStreamMut<'_>,
2184    display: DisplayRotation,
2185) -> Result<(), BridgeError> {
2186    let mut parameters = stream.codecpar_mut();
2187    let parameters = parameters.as_mut_ptr();
2188    // SAFETY: the stream's codec parameters are allocated and owned by the stream, mutably borrowed
2189    // here; `av_packet_side_data_new` replaces an entry of the type or appends one, keeping the
2190    // array and its count consistent, and returns null only when it cannot allocate.
2191    let entry = unsafe {
2192        ffi::av_packet_side_data_new(
2193            &raw mut (*parameters).coded_side_data,
2194            &raw mut (*parameters).nb_coded_side_data,
2195            ffi::AV_PKT_DATA_DISPLAYMATRIX,
2196            size_of::<[i32; 9]>(),
2197            0,
2198        )
2199    };
2200    if entry.is_null() {
2201        return Err(BridgeError::FfmpegCode(ffi::AVERROR(ffi::ENOMEM)));
2202    }
2203    let mut matrix = [0_i32; 9];
2204    // `av_display_rotation_set` turns clockwise by its angle, the opposite of what
2205    // `av_display_rotation_get` reads back. SAFETY: `matrix` is a complete, aligned 3x3 display
2206    // matrix.
2207    unsafe {
2208        ffi::av_display_rotation_set(matrix.as_mut_ptr(), -display.counter_clockwise_degrees);
2209        if display.flipped {
2210            ffi::av_display_matrix_flip(matrix.as_mut_ptr(), 1, 0);
2211        }
2212    }
2213    // SAFETY: the entry just made holds `size_of::<[i32; 9]>()` writable bytes, which `matrix`
2214    // fills; copying bytes assumes no alignment.
2215    unsafe {
2216        ptr::copy_nonoverlapping(
2217            matrix.as_ptr().cast::<u8>(),
2218            (*entry).data,
2219            size_of::<[i32; 9]>(),
2220        );
2221    }
2222    Ok(())
2223}
2224
2225/// Returns `FFmpeg`'s stable name for a hardware device type.
2226#[must_use]
2227pub fn hardware_device_type_name(device_type: ffi::AVHWDeviceType) -> Option<String> {
2228    // SAFETY: `av_hwdevice_get_type_name` accepts every enum value and returns either null or a
2229    // process-lifetime, NUL-terminated string.
2230    let name = unsafe { ffi::av_hwdevice_get_type_name(device_type) };
2231    if name.is_null() {
2232        None
2233    } else {
2234        // SAFETY: non-null return values obey the FFmpeg string contract above.
2235        Some(
2236            unsafe { CStr::from_ptr(name) }
2237                .to_string_lossy()
2238                .into_owned(),
2239        )
2240    }
2241}
2242
2243/// Reads the plane layout of a YCbCr pixel format the renderer samples
2244/// directly, or `None` for any other format.
2245///
2246/// Accepted: three components of one depth from 8 to 16 bits, chroma
2247/// subsampled by at most four on each axis, in three planes or with U, V
2248/// interleaved in the second, each sample in the low bits of its container
2249/// or, above eight bits, in the high bits, in this machine's byte order. Not
2250/// accepted: RGB, alpha, gray, palette, Bayer, float, XYZ, bitstream, and
2251/// hardware formats, and V-first interleaving (NV21).
2252#[must_use]
2253pub fn yuv_layout(format: ffi::AVPixelFormat) -> Option<YuvPlaneLayout> {
2254    if format == ffi::AV_PIX_FMT_NONE {
2255        return None;
2256    }
2257    // SAFETY: `av_pix_fmt_desc_get` accepts every enum value and returns either null or a pointer
2258    // into FFmpeg's process-lifetime descriptor table.
2259    let descriptor = unsafe { ffi::av_pix_fmt_desc_get(format).as_ref() }?;
2260    let foreign_endian = if cfg!(target_endian = "little") {
2261        ffi::AV_PIX_FMT_FLAG_BE
2262    } else {
2263        0
2264    };
2265    let rejected = foreign_endian
2266        | ffi::AV_PIX_FMT_FLAG_PAL
2267        | ffi::AV_PIX_FMT_FLAG_BITSTREAM
2268        | ffi::AV_PIX_FMT_FLAG_HWACCEL
2269        | ffi::AV_PIX_FMT_FLAG_RGB
2270        | ffi::AV_PIX_FMT_FLAG_ALPHA
2271        | ffi::AV_PIX_FMT_FLAG_BAYER
2272        | ffi::AV_PIX_FMT_FLAG_FLOAT
2273        | ffi::AV_PIX_FMT_FLAG_XYZ;
2274    if descriptor.flags & u64::from(rejected) != 0 || descriptor.nb_components != 3 {
2275        return None;
2276    }
2277    let [luma, u, v, _] = descriptor.comp;
2278    let depth = luma.depth;
2279    if !(8..=16).contains(&depth) || u.depth != depth || v.depth != depth {
2280        return None;
2281    }
2282    if descriptor.log2_chroma_w > 2 || descriptor.log2_chroma_h > 2 {
2283        return None;
2284    }
2285    let bytes = if depth > 8 { 2 } else { 1 };
2286    let shift = luma.shift;
2287    let msb_aligned = match shift {
2288        0 => false,
2289        _ if depth > 8 && shift == 16 - depth => true,
2290        _ => return None,
2291    };
2292    let planar = u.plane == 1
2293        && v.plane == 2
2294        && u.step == bytes
2295        && v.step == bytes
2296        && u.offset == 0
2297        && v.offset == 0;
2298    let interleaved = u.plane == 1
2299        && v.plane == 1
2300        && u.step == 2 * bytes
2301        && v.step == 2 * bytes
2302        && u.offset == 0
2303        && v.offset == bytes;
2304    if luma.plane != 0
2305        || luma.step != bytes
2306        || luma.offset != 0
2307        || u.shift != shift
2308        || v.shift != shift
2309        || !(planar || interleaved)
2310    {
2311        return None;
2312    }
2313    Some(YuvPlaneLayout {
2314        chroma_shift_x: descriptor.log2_chroma_w,
2315        chroma_shift_y: descriptor.log2_chroma_h,
2316        bit_depth: u8::try_from(depth).ok()?,
2317        interleaved_chroma: interleaved,
2318        msb_aligned,
2319    })
2320}
2321
2322/// Copies the planes of a frame whose format [`yuv_layout`] accepts into
2323/// tightly packed, forward rows of its samples, or returns `Ok(None)` for any
2324/// other format so callers can convert it explicitly. Odd coded dimensions
2325/// use ceil-divided chroma dimensions.
2326///
2327/// Only each row's samples are read: the rest of a padded row may be memory
2328/// nothing wrote (a frame from `av_frame_get_buffer`, or a picture from
2329/// libdav1d's pool), so it is never handed on. [`DecodedFrame::yuv_planes`]
2330/// shares a decoder's frame instead when every byte of it is written.
2331///
2332/// # Errors
2333/// Returns an error for invalid dimensions, null planes, or linesizes shorter
2334/// than their plane width.
2335pub fn yuv_planes(frame: &AVFrame) -> Result<Option<YuvPlanes>, BridgeError> {
2336    frame_yuv_planes(frame, false)
2337}
2338
2339/// A frame a decoder returned, and whether every byte of its buffers, row
2340/// padding included, is written.
2341///
2342/// Only this crate can say where a frame came from, so only it makes one:
2343/// [`DecodedFrame::receive`] takes one from a decoder, and a hardware frame
2344/// lease's `download` copies one out. A frame from anywhere else goes to
2345/// [`yuv_planes`], which copies its samples.
2346pub struct DecodedFrame {
2347    frame: AVFrame,
2348    /// Every byte of each plane's rows, up to its linesize, is initialized.
2349    written: bool,
2350}
2351
2352impl DecodedFrame {
2353    /// Takes the next frame `decoder` has ready, as
2354    /// `AVCodecContext::receive_frame` does.
2355    ///
2356    /// # Errors
2357    /// Returns what `avcodec_receive_frame` does: a drained or flushed
2358    /// decoder, or a decode failure.
2359    pub fn receive(decoder: &mut AVCodecContext) -> Result<Self, RsmpegError> {
2360        let written = decoder_frames_are_initialized(decoder);
2361        let frame = decoder.receive_frame()?;
2362        Ok(Self { frame, written })
2363    }
2364
2365    /// Hands over the planes of this frame, as [`yuv_planes`] does, but
2366    /// shares the frame's buffer when every byte of it is written: each plane
2367    /// is then a view of the buffer, row padding included, and the buffer
2368    /// stays alive, unmodified, as long as a view does. A plane that cannot be
2369    /// shared, with a negative linesize or outside every buffer the frame
2370    /// references, is copied into tightly packed, forward rows.
2371    ///
2372    /// # Errors
2373    /// Returns an error for invalid dimensions, null planes, or linesizes
2374    /// shorter than their plane width.
2375    pub fn yuv_planes(&self) -> Result<Option<YuvPlanes>, BridgeError> {
2376        frame_yuv_planes(&self.frame, self.written)
2377    }
2378
2379    /// Sets the frame's presentation timestamp.
2380    pub fn set_pts(&mut self, pts: i64) {
2381        self.frame.set_pts(pts);
2382    }
2383
2384    /// The frame itself, for a caller that takes ownership of it.
2385    #[must_use]
2386    pub fn into_frame(self) -> AVFrame {
2387        self.frame
2388    }
2389}
2390
2391impl std::ops::Deref for DecodedFrame {
2392    type Target = AVFrame;
2393
2394    fn deref(&self) -> &AVFrame {
2395        &self.frame
2396    }
2397}
2398
2399impl fmt::Debug for DecodedFrame {
2400    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2401        formatter
2402            .debug_struct("DecodedFrame")
2403            .field("format", &self.frame.format)
2404            .field("width", &self.frame.width)
2405            .field("height", &self.frame.height)
2406            .field("pts", &self.frame.pts)
2407            .field("written", &self.written)
2408            .finish_non_exhaustive()
2409    }
2410}
2411
2412/// Whether every byte of every frame buffer `decoder` returns, row padding
2413/// included, is initialized, so its frames' buffers may be shared.
2414///
2415/// `FFmpeg`'s own decoders allocate pictures through the context's
2416/// `get_buffer2`, and its default draws on pools that zero each buffer when
2417/// they allocate it (or fill it with a poison byte in a memory-poisoning
2418/// build). A decoder backed by an external library (`wrapper_name`, such as
2419/// libdav1d) allocates its own pictures, from memory nothing zeroes, and a
2420/// replaced `get_buffer2` promises nothing either: neither qualifies.
2421fn decoder_frames_are_initialized(decoder: &AVCodecContext) -> bool {
2422    static DEFAULT_GET_BUFFER2: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2423    // The default as `FFmpeg` itself installs it in a new context: comparing against Rust's own
2424    // address of `avcodec_default_get_buffer2` could compare a linker stub with the function.
2425    let default = *DEFAULT_GET_BUFFER2.get_or_init(|| {
2426        // SAFETY: a context allocated without a codec is only read and then freed.
2427        unsafe {
2428            let mut context = ffi::avcodec_alloc_context3(ptr::null());
2429            let get_buffer2 = context
2430                .as_ref()
2431                .and_then(|context| context.get_buffer2)
2432                .map_or(0, |function| function as usize);
2433            ffi::avcodec_free_context(&raw mut context);
2434            get_buffer2
2435        }
2436    });
2437    // SAFETY: a context's `codec` is null or points at the codec's static, read-only description.
2438    let wrapped =
2439        unsafe { decoder.codec.as_ref() }.is_none_or(|codec| !codec.wrapper_name.is_null());
2440    let get_buffer2 = decoder.get_buffer2.map_or(0, |function| function as usize);
2441    !wrapped && default != 0 && get_buffer2 == default
2442}
2443
2444/// The planes of `frame`, sharing its buffer only with `share`, which the
2445/// caller passes only for a frame every byte of whose rows is initialized.
2446fn frame_yuv_planes(frame: &AVFrame, share: bool) -> Result<Option<YuvPlanes>, BridgeError> {
2447    let Some(layout) = yuv_layout(frame.format) else {
2448        return Ok(None);
2449    };
2450    let width =
2451        u32::try_from(frame.width).map_err(|_| BridgeError::InvalidInput("negative YUV width"))?;
2452    let height = u32::try_from(frame.height)
2453        .map_err(|_| BridgeError::InvalidInput("negative YUV height"))?;
2454    if width == 0 || height == 0 {
2455        return Err(BridgeError::InvalidInput("empty YUV frame"));
2456    }
2457    let bytes = layout.bytes_per_sample();
2458    let chroma_width = width.div_ceil(1 << layout.chroma_shift_x);
2459    let chroma_height = height.div_ceil(1 << layout.chroma_shift_y);
2460    // `AVFrame::clone` takes a new reference to every buffer of a reference-counted frame; one
2461    // without buffers can only be copied, and so is every frame the caller did not vouch for.
2462    let shared =
2463        (share && !frame.buf[0].is_null()).then(|| std::sync::Arc::new(SharedFrame(frame.clone())));
2464    let copy = |index: usize,
2465                samples: u32,
2466                pairs: u32,
2467                rows: u32,
2468                context: &'static str|
2469     -> Result<FramePlane, BridgeError> {
2470        let row_bytes = samples
2471            .checked_mul(bytes)
2472            .and_then(|row| row.checked_mul(pairs))
2473            .ok_or(BridgeError::InvalidInput(context))?;
2474        if let Some(view) = shared
2475            .as_ref()
2476            .and_then(|frame| PlaneView::new(frame, index, row_bytes, rows))
2477        {
2478            let stride =
2479                u32::try_from(view.stride).map_err(|_| BridgeError::InvalidInput(context))?;
2480            return Ok(FramePlane {
2481                bytes: bytes::Bytes::from_owner(view),
2482                stride,
2483                width: samples,
2484                height: rows,
2485            });
2486        }
2487        Ok(FramePlane {
2488            bytes: copy_video_plane(
2489                frame.data[index],
2490                frame.linesize[index],
2491                row_bytes,
2492                rows,
2493                context,
2494            )?
2495            .into(),
2496            stride: row_bytes,
2497            width: samples,
2498            height: rows,
2499        })
2500    };
2501    let mut planes = vec![copy(0, width, 1, height, "invalid Y plane")?];
2502    if layout.interleaved_chroma {
2503        planes.push(copy(1, chroma_width, 2, chroma_height, "invalid UV plane")?);
2504    } else {
2505        planes.push(copy(1, chroma_width, 1, chroma_height, "invalid U plane")?);
2506        planes.push(copy(2, chroma_width, 1, chroma_height, "invalid V plane")?);
2507    }
2508    Ok(Some(YuvPlanes {
2509        layout,
2510        width,
2511        height,
2512        planes,
2513    }))
2514}
2515
2516/// A decoded frame whose buffers several plane views share.
2517struct SharedFrame(AVFrame);
2518
2519// SAFETY: the frame is only read, through the data pointers of its planes, after it is wrapped;
2520// nothing mutates it or its buffers while it is shared, because every holder holds a reference and
2521// FFmpeg does not write to a buffer with more than one.
2522unsafe impl Sync for SharedFrame {}
2523
2524/// `stride * rows` bytes of one plane of a [`SharedFrame`], kept alive by it:
2525/// made only of a frame every byte of whose rows is written.
2526struct PlaneView {
2527    frame: std::sync::Arc<SharedFrame>,
2528    data: *const u8,
2529    len: usize,
2530    stride: usize,
2531}
2532
2533// SAFETY: the view reads immutable memory owned by the reference-counted buffers of `frame`, which
2534// it keeps alive; `SharedFrame` is `Send + Sync`.
2535unsafe impl Send for PlaneView {}
2536
2537impl PlaneView {
2538    /// A view of plane `index`, or `None` unless its linesize is positive and
2539    /// covers `row_bytes`, and every one of its `rows` rows lies inside one
2540    /// of the buffers the frame references.
2541    fn new(
2542        frame: &std::sync::Arc<SharedFrame>,
2543        index: usize,
2544        row_bytes: u32,
2545        rows: u32,
2546    ) -> Option<Self> {
2547        let inner = &frame.0;
2548        let data = inner.data[index].cast_const();
2549        let stride = usize::try_from(inner.linesize[index]).ok()?;
2550        let row_bytes = usize::try_from(row_bytes).ok()?;
2551        if data.is_null() || stride < row_bytes || rows == 0 {
2552            return None;
2553        }
2554        let len = stride.checked_mul(usize::try_from(rows).ok()?)?;
2555        let start = data as usize;
2556        let end = start.checked_add(len)?;
2557        let inside = inner.buf.iter().any(|buffer| {
2558            // SAFETY: a non-null entry of `AVFrame::buf` points at a live `AVBufferRef` for as long
2559            // as the frame holds it.
2560            let Some(buffer) = (unsafe { buffer.as_ref() }) else {
2561                return false;
2562            };
2563            let buffer_start = buffer.data as usize;
2564            buffer_start
2565                .checked_add(buffer.size)
2566                .is_some_and(|buffer_end| buffer_start <= start && end <= buffer_end)
2567        });
2568        inside.then(|| Self {
2569            frame: std::sync::Arc::clone(frame),
2570            data,
2571            len,
2572            stride,
2573        })
2574    }
2575}
2576
2577impl AsRef<[u8]> for PlaneView {
2578    fn as_ref(&self) -> &[u8] {
2579        // Keeps the frame, and so the buffer, alive for the slice's lifetime.
2580        let _ = &self.frame;
2581        // SAFETY: `new` checked that `data..data + len` lies inside a buffer the frame references,
2582        // and the frame lives as long as `self`.
2583        unsafe { slice::from_raw_parts(self.data, self.len) }
2584    }
2585}
2586
2587fn copy_video_plane(
2588    data: *const u8,
2589    linesize: i32,
2590    width: u32,
2591    height: u32,
2592    context: &'static str,
2593) -> Result<Vec<u8>, BridgeError> {
2594    let row_bytes = usize::try_from(width).map_err(|_| BridgeError::InvalidInput(context))?;
2595    let rows = usize::try_from(height).map_err(|_| BridgeError::InvalidInput(context))?;
2596    let stride =
2597        usize::try_from(linesize.unsigned_abs()).map_err(|_| BridgeError::InvalidInput(context))?;
2598    if data.is_null() || linesize == 0 || stride < row_bytes {
2599        return Err(BridgeError::InvalidInput(context));
2600    }
2601    let byte_len = row_bytes
2602        .checked_mul(rows)
2603        .ok_or(BridgeError::InvalidInput(context))?;
2604    // Appending row by row writes each byte once; `vec![0; len]` would add a pass over ~11 MB
2605    // per 4K frame.
2606    let mut output = Vec::with_capacity(byte_len);
2607    let signed_stride =
2608        isize::try_from(linesize).map_err(|_| BridgeError::InvalidInput(context))?;
2609    for row in 0..rows {
2610        let row = isize::try_from(row).map_err(|_| BridgeError::InvalidInput(context))?;
2611        let offset = row
2612            .checked_mul(signed_stride)
2613            .ok_or(BridgeError::InvalidInput(context))?;
2614        // SAFETY: `AVFrame` guarantees that each non-null video plane exposes `height` rows
2615        // separated by its signed linesize. The absolute stride was validated to cover `width`
2616        // bytes, and each borrowed slice is limited to that visible row width.
2617        let source = unsafe { slice::from_raw_parts(data.offset(offset), row_bytes) };
2618        output.extend_from_slice(source);
2619    }
2620    debug_assert_eq!(output.len(), byte_len);
2621    Ok(output)
2622}
2623
2624/// Copies tightly packed RGBA bytes into an owned `FFmpeg` frame.
2625///
2626/// # Errors
2627/// Returns an `FFmpeg` error when its frame buffer cannot be allocated.
2628pub fn rgba_frame(bytes: &[u8], width: i32, height: i32) -> Result<AVFrame, BridgeError> {
2629    let row_bytes = usize::try_from(width)
2630        .ok()
2631        .and_then(|value| value.checked_mul(4))
2632        .ok_or(BridgeError::InvalidInput("RGBA row width overflow"))?;
2633    let rows =
2634        usize::try_from(height).map_err(|_| BridgeError::InvalidInput("negative RGBA height"))?;
2635    let expected = row_bytes
2636        .checked_mul(rows)
2637        .ok_or(BridgeError::InvalidInput("RGBA frame size overflow"))?;
2638    if bytes.len() != expected {
2639        return Err(BridgeError::InvalidInput("RGBA byte length mismatch"));
2640    }
2641
2642    let mut frame = AVFrame::new();
2643    frame.set_format(ffi::AV_PIX_FMT_RGBA);
2644    frame.set_width(width);
2645    frame.set_height(height);
2646    frame.get_buffer(1)?;
2647    frame.make_writable()?;
2648    let linesize = usize::try_from(frame.linesize[0])
2649        .map_err(|_| BridgeError::InvalidInput("negative RGBA linesize"))?;
2650    if frame.data[0].is_null() || linesize < row_bytes {
2651        return Err(BridgeError::InvalidInput(
2652            "RGBA destination plane is too small",
2653        ));
2654    }
2655
2656    // SAFETY: av_frame_get_buffer allocated `height` writable rows of at least `linesize` bytes.
2657    // Both slices are bounded to one validated RGBA row.
2658    unsafe {
2659        for row in 0..rows {
2660            let destination_offset = row.checked_mul(linesize).ok_or(BridgeError::InvalidInput(
2661                "RGBA destination offset overflow",
2662            ))?;
2663            let source_offset = row
2664                .checked_mul(row_bytes)
2665                .ok_or(BridgeError::InvalidInput("RGBA source offset overflow"))?;
2666            let destination =
2667                slice::from_raw_parts_mut(frame.data[0].add(destination_offset), row_bytes);
2668            destination.copy_from_slice(&bytes[source_offset..source_offset + row_bytes]);
2669        }
2670    }
2671    Ok(frame)
2672}
2673
2674/// Copies owned planar 8-bit 4:2:0 samples into an `FFmpeg` video frame.
2675///
2676/// # Errors
2677/// Returns an error for invalid dimensions, short source planes, or an
2678/// `FFmpeg` allocation failure.
2679#[allow(clippy::too_many_arguments)]
2680pub fn yuv420p_frame(
2681    y: &[u8],
2682    y_stride: u32,
2683    u: &[u8],
2684    u_stride: u32,
2685    v: &[u8],
2686    v_stride: u32,
2687    width: i32,
2688    height: i32,
2689) -> Result<AVFrame, BridgeError> {
2690    let width =
2691        u32::try_from(width).map_err(|_| BridgeError::InvalidInput("negative YUV width"))?;
2692    let height =
2693        u32::try_from(height).map_err(|_| BridgeError::InvalidInput("negative YUV height"))?;
2694    if width == 0 || height == 0 {
2695        return Err(BridgeError::InvalidInput("empty YUV frame"));
2696    }
2697    let chroma_width = width.div_ceil(2);
2698    let chroma_height = height.div_ceil(2);
2699    validate_source_plane(y, y_stride, width, height, "invalid Y source plane")?;
2700    validate_source_plane(
2701        u,
2702        u_stride,
2703        chroma_width,
2704        chroma_height,
2705        "invalid U source plane",
2706    )?;
2707    validate_source_plane(
2708        v,
2709        v_stride,
2710        chroma_width,
2711        chroma_height,
2712        "invalid V source plane",
2713    )?;
2714
2715    let mut frame = AVFrame::new();
2716    frame.set_format(ffi::AV_PIX_FMT_YUV420P);
2717    frame.set_width(i32::try_from(width).map_err(|_| BridgeError::InvalidInput("YUV width"))?);
2718    frame.set_height(i32::try_from(height).map_err(|_| BridgeError::InvalidInput("YUV height"))?);
2719    frame.get_buffer(1)?;
2720    frame.make_writable()?;
2721    copy_plane_to_frame(y, y_stride, width, height, frame.data[0], frame.linesize[0])?;
2722    copy_plane_to_frame(
2723        u,
2724        u_stride,
2725        chroma_width,
2726        chroma_height,
2727        frame.data[1],
2728        frame.linesize[1],
2729    )?;
2730    copy_plane_to_frame(
2731        v,
2732        v_stride,
2733        chroma_width,
2734        chroma_height,
2735        frame.data[2],
2736        frame.linesize[2],
2737    )?;
2738    Ok(frame)
2739}
2740
2741/// The pixel format whose planes are laid out as `layout`: the first in
2742/// `FFmpeg`'s table, which for 8-bit planar YUV is the limited-range format
2743/// (`yuv420p`) rather than its deprecated full-range twin (`yuvj420p`).
2744#[must_use]
2745pub fn yuv_pixel_format(layout: YuvPlaneLayout) -> Option<ffi::AVPixelFormat> {
2746    let mut descriptor = ptr::null();
2747    loop {
2748        // SAFETY: null starts the walk; any other value is a descriptor the previous call returned,
2749        // from `FFmpeg`'s process-lifetime table.
2750        descriptor = unsafe { ffi::av_pix_fmt_desc_next(descriptor) };
2751        if descriptor.is_null() {
2752            return None;
2753        }
2754        // SAFETY: a descriptor from the table names its own format.
2755        let format = unsafe { ffi::av_pix_fmt_desc_get_id(descriptor) };
2756        if yuv_layout(format) == Some(layout) {
2757            return Some(format);
2758        }
2759    }
2760}
2761
2762/// A frame of `format` holding a copy of `planes` (bytes and stride each),
2763/// `width` by `height` pictures in the planes `format` stores.
2764///
2765/// # Errors
2766/// For a format without a descriptor, a plane count other than the
2767/// format's, or a plane smaller than the picture.
2768pub fn planar_frame(
2769    format: ffi::AVPixelFormat,
2770    planes: &[(&[u8], u32)],
2771    width: u32,
2772    height: u32,
2773) -> Result<AVFrame, BridgeError> {
2774    // SAFETY: `av_pix_fmt_desc_get` accepts every enum value and returns either null or a pointer
2775    // into FFmpeg's process-lifetime descriptor table.
2776    let descriptor = unsafe { ffi::av_pix_fmt_desc_get(format).as_ref() }
2777        .ok_or(BridgeError::InvalidInput("pixel format has no descriptor"))?;
2778    // SAFETY: a pure computation over plain values.
2779    let count = unsafe { ffi::av_pix_fmt_count_planes(format) };
2780    if usize::try_from(count).ok() != Some(planes.len()) || width == 0 || height == 0 {
2781        return Err(BridgeError::InvalidInput(
2782            "planes do not match the pixel format",
2783        ));
2784    }
2785    let signed = |value: u32| {
2786        i32::try_from(value).map_err(|_| BridgeError::InvalidInput("frame size overflow"))
2787    };
2788    let mut frame = AVFrame::new();
2789    frame.set_format(format);
2790    frame.set_width(signed(width)?);
2791    frame.set_height(signed(height)?);
2792    frame.get_buffer(1)?;
2793    frame.make_writable()?;
2794    for (index, (bytes, stride)) in planes.iter().enumerate() {
2795        let plane = i32::try_from(index).unwrap_or(i32::MAX);
2796        // SAFETY: a pure computation over plain values.
2797        let row_bytes = unsafe { ffi::av_image_get_linesize(format, signed(width)?, plane) };
2798        let row_bytes = u32::try_from(row_bytes).map_err(|_| BridgeError::FfmpegCode(row_bytes))?;
2799        let rows = if index == 0 || index == 3 {
2800            height
2801        } else {
2802            height.div_ceil(1 << descriptor.log2_chroma_h)
2803        };
2804        validate_source_plane(bytes, *stride, row_bytes, rows, "invalid source plane")?;
2805        copy_plane_to_frame(
2806            bytes,
2807            *stride,
2808            row_bytes,
2809            rows,
2810            frame.data[index],
2811            frame.linesize[index],
2812        )?;
2813    }
2814    Ok(frame)
2815}
2816
2817fn validate_source_plane(
2818    bytes: &[u8],
2819    stride: u32,
2820    width: u32,
2821    height: u32,
2822    context: &'static str,
2823) -> Result<(), BridgeError> {
2824    let required = usize::try_from(stride).ok().and_then(|stride| {
2825        usize::try_from(height)
2826            .ok()
2827            .and_then(|height| stride.checked_mul(height))
2828    });
2829    if stride < width || required != Some(bytes.len()) {
2830        Err(BridgeError::InvalidInput(context))
2831    } else {
2832        Ok(())
2833    }
2834}
2835
2836fn copy_plane_to_frame(
2837    source: &[u8],
2838    source_stride: u32,
2839    width: u32,
2840    height: u32,
2841    destination: *mut u8,
2842    destination_stride: i32,
2843) -> Result<(), BridgeError> {
2844    let row_bytes =
2845        usize::try_from(width).map_err(|_| BridgeError::InvalidInput("YUV row width overflow"))?;
2846    let rows =
2847        usize::try_from(height).map_err(|_| BridgeError::InvalidInput("YUV row count overflow"))?;
2848    let source_stride = usize::try_from(source_stride)
2849        .map_err(|_| BridgeError::InvalidInput("YUV source stride overflow"))?;
2850    let destination_stride = usize::try_from(destination_stride)
2851        .map_err(|_| BridgeError::InvalidInput("negative YUV destination stride"))?;
2852    if destination.is_null() || destination_stride < row_bytes {
2853        return Err(BridgeError::InvalidInput(
2854            "YUV destination plane is too small",
2855        ));
2856    }
2857    for row in 0..rows {
2858        let source_offset = row
2859            .checked_mul(source_stride)
2860            .ok_or(BridgeError::InvalidInput("YUV source offset overflow"))?;
2861        let destination_offset = row
2862            .checked_mul(destination_stride)
2863            .ok_or(BridgeError::InvalidInput("YUV destination offset overflow"))?;
2864        // SAFETY: the destination frame owns `height` writable rows whose validated linesize covers
2865        // `width` bytes. The source plane length was validated above against its positive stride
2866        // and row count.
2867        unsafe {
2868            ptr::copy_nonoverlapping(
2869                source.as_ptr().add(source_offset),
2870                destination.add(destination_offset),
2871                row_bytes,
2872            );
2873        }
2874    }
2875    Ok(())
2876}
2877
2878/// Copies planar `f32` samples into an owned `FFmpeg` audio frame of
2879/// `format`: planar float (`AV_SAMPLE_FMT_FLTP`), interleaved float
2880/// (`AV_SAMPLE_FMT_FLT`), or interleaved 16-bit (`AV_SAMPLE_FMT_S16`). A
2881/// 16-bit sample is clamped to [-1, 1] and rounded to the nearest step.
2882///
2883/// # Errors
2884/// Returns an error for mismatched channels, invalid dimensions, another
2885/// sample format, or an `FFmpeg` allocation failure.
2886pub fn audio_frame(
2887    channels: &[Vec<f32>],
2888    format: ffi::AVSampleFormat,
2889    sample_rate: i32,
2890    pts: i64,
2891) -> Result<AVFrame, BridgeError> {
2892    let Some(samples) = channels.first().map(Vec::len) else {
2893        return Err(BridgeError::InvalidInput("invalid planar audio shape"));
2894    };
2895    if sample_rate <= 0
2896        || samples == 0
2897        || channels.len() > ffi::AV_NUM_DATA_POINTERS as usize
2898        || channels.iter().any(|channel| channel.len() != samples)
2899    {
2900        return Err(BridgeError::InvalidInput(
2901            "planar audio channel lengths differ",
2902        ));
2903    }
2904    let sample_bytes = match format {
2905        ffi::AV_SAMPLE_FMT_FLTP | ffi::AV_SAMPLE_FMT_FLT => size_of::<f32>(),
2906        ffi::AV_SAMPLE_FMT_S16 => size_of::<i16>(),
2907        _ => return Err(BridgeError::InvalidInput("unsupported audio sample format")),
2908    };
2909    let planar = format == ffi::AV_SAMPLE_FMT_FLTP;
2910    let channel_count = i32::try_from(channels.len())
2911        .map_err(|_| BridgeError::InvalidInput("audio channel count overflow"))?;
2912    let sample_count = i32::try_from(samples)
2913        .map_err(|_| BridgeError::InvalidInput("audio sample count overflow"))?;
2914    let mut frame = AVFrame::new();
2915    frame.set_format(format);
2916    frame.set_ch_layout(
2917        rsmpeg::avutil::AVChannelLayout::from_nb_channels(channel_count).into_inner(),
2918    );
2919    frame.set_sample_rate(sample_rate);
2920    frame.set_nb_samples(sample_count);
2921    frame.set_pts(pts);
2922    frame.set_time_base(ffi::AVRational {
2923        num: 1,
2924        den: sample_rate,
2925    });
2926    frame.get_buffer(0)?;
2927    frame.make_writable()?;
2928    let plane_bytes = samples
2929        .checked_mul(sample_bytes)
2930        .and_then(|bytes| bytes.checked_mul(if planar { 1 } else { channels.len() }))
2931        .ok_or(BridgeError::InvalidInput("audio plane byte count overflow"))?;
2932    let plane_capacity = usize::try_from(frame.linesize[0]).unwrap_or(0);
2933    if plane_capacity < plane_bytes {
2934        return Err(BridgeError::InvalidInput(
2935            "audio plane capacity is too small",
2936        ));
2937    }
2938    let planes = if planar { channels.len() } else { 1 };
2939    if frame.data[..planes].iter().any(|plane| plane.is_null()) {
2940        return Err(BridgeError::InvalidInput("audio plane pointer is null"));
2941    }
2942    if planar {
2943        for (index, source) in channels.iter().enumerate() {
2944            // SAFETY: av_frame_get_buffer allocated one writable FLTP plane per channel with the
2945            // common `linesize[0]` capacity used by planar audio, checked above to hold the
2946            // channel.
2947            unsafe {
2948                ptr::copy_nonoverlapping(
2949                    source.as_ptr().cast::<u8>(),
2950                    frame.data[index],
2951                    plane_bytes,
2952                );
2953            }
2954        }
2955        return Ok(frame);
2956    }
2957    // SAFETY: an interleaved format has one writable plane, checked above to hold every channel's
2958    // samples, and nothing else refers to it while this slice lives.
2959    let plane = unsafe { std::slice::from_raw_parts_mut(frame.data[0], plane_bytes) };
2960    let interleaved = (0..samples).flat_map(|sample| channels.iter().map(move |c| c[sample]));
2961    if format == ffi::AV_SAMPLE_FMT_FLT {
2962        for (bytes, value) in plane.as_chunks_mut::<4>().0.iter_mut().zip(interleaved) {
2963            *bytes = value.to_ne_bytes();
2964        }
2965    } else {
2966        for (bytes, value) in plane.as_chunks_mut::<2>().0.iter_mut().zip(interleaved) {
2967            // In range after the clamp, so the cast cannot truncate.
2968            #[allow(clippy::cast_possible_truncation)]
2969            let value = (value.clamp(-1.0, 1.0) * 32_767.0).round() as i16;
2970            *bytes = value.to_ne_bytes();
2971        }
2972    }
2973    Ok(frame)
2974}
2975
2976/// The `FFmpeg` channel (`enum AVChannel`) of each channel of `layout`, in
2977/// its order; empty for a layout that gives only a count
2978/// (`AV_CHANNEL_ORDER_UNSPEC`).
2979///
2980/// It takes rsmpeg's layout, which only `FFmpeg` or `unsafe` code builds
2981/// (`AVFrame::ch_layout`, `AVCodecParameters::ch_layout`): a bare
2982/// `AVChannelLayout` can carry a forged channel map.
2983///
2984/// ```compile_fail,E0308
2985/// use rsmpeg::ffi;
2986/// let forged = ffi::AVChannelLayout {
2987///     order: ffi::AV_CHANNEL_ORDER_CUSTOM,
2988///     nb_channels: 4,
2989///     u: ffi::AVChannelLayout__bindgen_ty_1 { map: 0x10 as *mut _ },
2990///     opaque: std::ptr::null_mut(),
2991/// };
2992/// alpha2_ffmpeg_bridge::channel_layout_channels(&forged);
2993/// ```
2994#[must_use]
2995pub fn channel_layout_channels(layout: &rsmpeg::avutil::AVChannelLayout) -> Vec<ffi::AVChannel> {
2996    if layout.order == ffi::AV_CHANNEL_ORDER_UNSPEC {
2997        return Vec::new();
2998    }
2999    let channels = u32::try_from(layout.nb_channels).unwrap_or(0);
3000    (0..channels)
3001        .map(|index| {
3002            layout
3003                .channel_from_index(index)
3004                .unwrap_or(ffi::AV_CHAN_NONE)
3005        })
3006        .collect()
3007}
3008
3009/// A channel layout of `channels` in this order: `FFmpeg`'s native order
3010/// when they are in it, a custom order otherwise.
3011///
3012/// # Errors
3013/// Returns an error for no channels or when `FFmpeg` cannot allocate or
3014/// retype the layout.
3015pub fn channel_layout_of(
3016    channels: &[ffi::AVChannel],
3017) -> Result<rsmpeg::avutil::AVChannelLayout, BridgeError> {
3018    let count = i32::try_from(channels.len())
3019        .ok()
3020        .filter(|count| *count > 0)
3021        .ok_or(BridgeError::InvalidInput("a channel layout needs channels"))?;
3022    let mut raw = std::mem::MaybeUninit::<ffi::AVChannelLayout>::zeroed();
3023    // SAFETY: `custom_init` initializes the zeroed layout with a map of `count` entries, each of
3024    // which is written exactly once below.
3025    let initialized = unsafe { ffi::av_channel_layout_custom_init(raw.as_mut_ptr(), count) };
3026    if initialized < 0 {
3027        return Err(BridgeError::FfmpegCode(initialized));
3028    }
3029    // SAFETY: initialized above; from here `layout` owns it and uninitializes it on drop.
3030    let mut layout = unsafe { rsmpeg::avutil::AVChannelLayout::new(raw.assume_init()) };
3031    for (index, channel) in channels.iter().enumerate() {
3032        // SAFETY: a custom layout's map holds `count` entries, and `index` is below it.
3033        unsafe { (*layout.u.map.add(index)).id = *channel };
3034    }
3035    let flags = i32::try_from(ffi::AV_CHANNEL_LAYOUT_RETYPE_FLAG_CANONICAL).unwrap_or(0);
3036    // SAFETY: `layout` is a valid custom layout that `retype` may rewrite in place.
3037    let retyped = unsafe {
3038        ffi::av_channel_layout_retype(layout.as_mut_ptr(), ffi::AV_CHANNEL_ORDER_UNSPEC, flags)
3039    };
3040    if retyped < 0 {
3041        return Err(BridgeError::FfmpegCode(retyped));
3042    }
3043    Ok(layout)
3044}
3045
3046/// Copies a packed `AV_SAMPLE_FMT_FLT` frame into an owned interleaved vector.
3047///
3048/// # Errors
3049/// Returns an error if the frame format, dimensions, or buffer is invalid.
3050pub fn audio_f32_interleaved(frame: &AVFrame) -> Result<Vec<f32>, BridgeError> {
3051    if frame.format != ffi::AV_SAMPLE_FMT_FLT || frame.nb_samples < 0 {
3052        return Err(BridgeError::InvalidInput(
3053            "packed audio format or sample count is invalid",
3054        ));
3055    }
3056    let channels = usize::try_from(frame.ch_layout().nb_channels)
3057        .map_err(|_| BridgeError::InvalidInput("packed audio channel count is negative"))?;
3058    let frames = usize::try_from(frame.nb_samples)
3059        .map_err(|_| BridgeError::InvalidInput("packed audio sample count is negative"))?;
3060    let samples = frames
3061        .checked_mul(channels)
3062        .ok_or(BridgeError::InvalidInput(
3063            "packed audio sample count overflow",
3064        ))?;
3065    let bytes = samples
3066        .checked_mul(size_of::<f32>())
3067        .ok_or(BridgeError::InvalidInput(
3068            "packed audio byte count overflow",
3069        ))?;
3070    if channels == 0
3071        || frame.data[0].is_null()
3072        || usize::try_from(frame.linesize[0]).unwrap_or(0) < bytes
3073    {
3074        return Err(BridgeError::InvalidInput(
3075            "packed audio plane is missing or too small",
3076        ));
3077    }
3078    let mut output = vec![0.0_f32; samples];
3079    // SAFETY: the packed FLT plane was validated to contain every requested sample, and `output`
3080    // owns exactly the destination byte count.
3081    unsafe {
3082        ptr::copy_nonoverlapping(frame.data[0], output.as_mut_ptr().cast::<u8>(), bytes);
3083    }
3084    Ok(output)
3085}
3086
3087/// The rotation a display matrix asks for, in `FFmpeg`'s convention.
3088#[derive(Copy, Clone, Debug, PartialEq)]
3089pub struct DisplayRotation {
3090    /// Counter-clockwise angle in degrees, in `[-180, 180]`
3091    /// (`av_display_rotation_get`).
3092    pub counter_clockwise_degrees: f64,
3093    /// Whether the matrix also mirrors the picture. The matrix is then this
3094    /// rotation followed by a horizontal flip, and the angle is the rotation
3095    /// left once that flip is removed.
3096    pub flipped: bool,
3097}
3098
3099/// Reads a stream display matrix.
3100#[must_use]
3101pub fn display_rotation(parameters: &AVCodecParameters) -> Option<DisplayRotation> {
3102    let count = usize::try_from(parameters.nb_coded_side_data).ok()?;
3103    if count == 0 || count > 64 || parameters.coded_side_data.is_null() {
3104        return None;
3105    }
3106    // SAFETY: AVCodecParameters owns an array containing exactly `nb_coded_side_data` entries for
3107    // the lifetime of this borrow.
3108    let entries = unsafe { slice::from_raw_parts(parameters.coded_side_data, count) };
3109    let matrix = entries.iter().find(|entry| {
3110        entry.type_ == ffi::AV_PKT_DATA_DISPLAYMATRIX
3111            && entry.size >= 9 * size_of::<i32>()
3112            && !entry.data.is_null()
3113    })?;
3114    // SAFETY: the matching side-data entry was validated to hold a complete 3x3 display matrix.
3115    unsafe { matrix_rotation(matrix.data) }
3116}
3117
3118/// Reads the display matrix a decoder attached to one frame, as the JPEG
3119/// decoder does for an Exif `Orientation`.
3120#[must_use]
3121pub fn frame_display_rotation(frame: &AVFrame) -> Option<DisplayRotation> {
3122    // SAFETY: `frame` is a live frame; the returned entry, if any, is owned by it for this borrow.
3123    let entry =
3124        unsafe { ffi::av_frame_get_side_data(frame.as_ptr(), ffi::AV_FRAME_DATA_DISPLAYMATRIX) };
3125    if entry.is_null() {
3126        return None;
3127    }
3128    // SAFETY: a non-null entry returned by `av_frame_get_side_data` is a valid side-data record.
3129    let entry = unsafe { &*entry };
3130    if entry.size < 9 * size_of::<i32>() || entry.data.is_null() {
3131        return None;
3132    }
3133    // SAFETY: the entry was validated to hold a complete 3x3 display matrix.
3134    unsafe { matrix_rotation(entry.data) }
3135}
3136
3137/// # Safety
3138/// `data` must point at nine readable `i32`s, aligned or not.
3139unsafe fn matrix_rotation(data: *const u8) -> Option<DisplayRotation> {
3140    let mut aligned = [0_i32; 9];
3141    // SAFETY: the caller guarantees a complete 3x3 matrix. Copying as bytes avoids assuming source
3142    // alignment.
3143    unsafe {
3144        ptr::copy_nonoverlapping(
3145            data,
3146            aligned.as_mut_ptr().cast::<u8>(),
3147            size_of::<[i32; 9]>(),
3148        );
3149    }
3150    // A mirroring matrix has a negative determinant in its 2x2 part, and `av_display_rotation_get`
3151    // reads a meaningless angle from one. Removing the horizontal flip, which `FFmpeg` applies
3152    // after the rotation, leaves a pure rotation.
3153    let determinant = i64::from(aligned[0]) * i64::from(aligned[4])
3154        - i64::from(aligned[1]) * i64::from(aligned[3]);
3155    let flipped = determinant < 0;
3156    if flipped {
3157        // SAFETY: `aligned` is a complete, correctly aligned 3x3 matrix.
3158        unsafe { ffi::av_display_matrix_flip(aligned.as_mut_ptr(), 1, 0) };
3159    }
3160    // SAFETY: `aligned` is a complete, correctly aligned 3x3 display matrix.
3161    let angle = unsafe { ffi::av_display_rotation_get(aligned.as_ptr()) };
3162    angle.is_finite().then_some(DisplayRotation {
3163        counter_clockwise_degrees: angle,
3164        flipped,
3165    })
3166}
3167
3168const fn size_of<T>() -> usize {
3169    std::mem::size_of::<T>()
3170}
3171
3172/// A reusable `FFmpeg` YUV420P frame the caller writes into directly.
3173///
3174/// [`yuv420p_frame`] allocates a frame and copies three planes into it on
3175/// whichever thread calls it, which for an export is the encode worker. A
3176/// caller that can produce the pixels itself (a GPU readback packing straight
3177/// into encoder memory) wants the frame allocated once and rewritten, so
3178/// neither the allocation nor the copy happens per frame.
3179///
3180/// The buffer stays valid while `FFmpeg` holds a reference to it, which it
3181/// does between `send_frame` and the packet that consumes it.
3182/// [`Yuv420pFrameBuffer::begin_write`] is what makes reuse safe: it returns
3183/// the plane memory only once the buffer is no longer shared, copying it away
3184/// from `FFmpeg` if it still is.
3185pub struct Yuv420pFrameBuffer {
3186    frame: AVFrame,
3187    width: usize,
3188    height: usize,
3189}
3190
3191/// Writable plane memory for one reusable frame.
3192pub struct Yuv420pFramePlanes<'a> {
3193    /// Luma rows, `y_stride` bytes apart.
3194    pub y: &'a mut [u8],
3195    /// Distance between luma rows.
3196    pub y_stride: usize,
3197    /// Blue-difference chroma rows, `chroma_stride` bytes apart.
3198    pub u: &'a mut [u8],
3199    /// Red-difference chroma rows, `chroma_stride` bytes apart.
3200    pub v: &'a mut [u8],
3201    /// Distance between chroma rows.
3202    pub chroma_stride: usize,
3203}
3204
3205impl Yuv420pFrameBuffer {
3206    /// Allocates one reusable frame for this exact coded size.
3207    ///
3208    /// # Errors
3209    /// Returns an error for a non-positive size or a failed allocation.
3210    pub fn new(width: i32, height: i32) -> Result<Self, BridgeError> {
3211        let coded_width =
3212            usize::try_from(width).map_err(|_| BridgeError::InvalidInput("negative YUV width"))?;
3213        let coded_height = usize::try_from(height)
3214            .map_err(|_| BridgeError::InvalidInput("negative YUV height"))?;
3215        if coded_width == 0 || coded_height == 0 {
3216            return Err(BridgeError::InvalidInput("empty YUV frame"));
3217        }
3218        Ok(Self {
3219            frame: zeroed_yuv420p_frame(width, height)?,
3220            width: coded_width,
3221            height: coded_height,
3222        })
3223    }
3224
3225    /// Luma width and height in pixels.
3226    #[must_use]
3227    pub const fn dimensions(&self) -> (usize, usize) {
3228        (self.width, self.height)
3229    }
3230
3231    /// Returns the plane memory to overwrite, after making the frame unshared.
3232    ///
3233    /// # Errors
3234    /// Returns an error when `FFmpeg` cannot make the buffer writable or its
3235    /// planes are not laid out as this frame was allocated.
3236    pub fn begin_write(&mut self) -> Result<Yuv420pFramePlanes<'_>, BridgeError> {
3237        // `av_frame_make_writable` would copy into rows padded for SIMD and leave that padding
3238        // unwritten, under slices that span it. The copy goes to a frame allocated as this one was,
3239        // written through.
3240        if !self.frame.is_writable().map_err(BridgeError::Ffmpeg)? {
3241            let mut copy = zeroed_yuv420p_frame(self.frame.width, self.frame.height)?;
3242            // SAFETY: both frames are live YUV420P frames of one size; `copy` is uniquely owned,
3243            // and both calls only read `self.frame`.
3244            let status = unsafe {
3245                let copied = ffi::av_frame_copy(copy.as_mut_ptr(), self.frame.as_ptr());
3246                if copied < 0 {
3247                    copied
3248                } else {
3249                    ffi::av_frame_copy_props(copy.as_mut_ptr(), self.frame.as_ptr())
3250                }
3251            };
3252            if status < 0 {
3253                return Err(BridgeError::FfmpegCode(status));
3254            }
3255            self.frame = copy;
3256        }
3257        let chroma_width = self.width.div_ceil(2);
3258        let chroma_height = self.height.div_ceil(2);
3259        let y_stride = plane_stride(self.frame.linesize[0], self.width)?;
3260        let u_stride = plane_stride(self.frame.linesize[1], chroma_width)?;
3261        let v_stride = plane_stride(self.frame.linesize[2], chroma_width)?;
3262        if u_stride != v_stride {
3263            return Err(BridgeError::InvalidInput("mismatched chroma strides"));
3264        }
3265        // SAFETY: `zeroed_yuv420p_frame` allocated and wrote each plane, `height` rows of
3266        // `linesize` bytes, so the last row's visible width is the furthest byte any caller may
3267        // touch; the writability check above guarantees this frame is the only owner of that
3268        // memory.
3269        let y = unsafe { writable_plane(self.frame.data[0], y_stride, self.width, self.height)? };
3270        // SAFETY: as above, for the two half-resolution chroma planes.
3271        let u =
3272            unsafe { writable_plane(self.frame.data[1], u_stride, chroma_width, chroma_height)? };
3273        // SAFETY: as above.
3274        let v =
3275            unsafe { writable_plane(self.frame.data[2], v_stride, chroma_width, chroma_height)? };
3276        Ok(Yuv420pFramePlanes {
3277            y,
3278            y_stride,
3279            u,
3280            v,
3281            chroma_stride: u_stride,
3282        })
3283    }
3284
3285    /// Stamps the presentation timestamp this frame will be encoded with.
3286    pub fn set_presentation(&mut self, pts: i64, time_base: ffi::AVRational) {
3287        self.frame.set_pts(pts);
3288        self.frame.set_time_base(time_base);
3289    }
3290
3291    /// The frame to hand to an encoder.
3292    #[must_use]
3293    pub const fn frame(&self) -> &AVFrame {
3294        &self.frame
3295    }
3296
3297    /// Coded luma dimensions this buffer was allocated for.
3298    #[must_use]
3299    pub const fn size(&self) -> (usize, usize) {
3300        (self.width, self.height)
3301    }
3302}
3303
3304/// A YUV420P frame with tightly packed rows, every byte of it zero.
3305fn zeroed_yuv420p_frame(width: i32, height: i32) -> Result<AVFrame, BridgeError> {
3306    let mut frame = AVFrame::new();
3307    frame.set_format(ffi::AV_PIX_FMT_YUV420P);
3308    frame.set_width(width);
3309    frame.set_height(height);
3310    frame.get_buffer(1)?;
3311    for buffer in frame.buf {
3312        // SAFETY: a non-null entry of `AVFrame::buf` points at a live `AVBufferRef`; this frame was
3313        // just allocated, so it is the buffer's only reference, `size` bytes long.
3314        if let Some(buffer) = unsafe { buffer.as_ref() } {
3315            // SAFETY: as above: `data..data + size` is this frame's own, unshared allocation.
3316            unsafe { ptr::write_bytes(buffer.data, 0, buffer.size) };
3317        }
3318    }
3319    Ok(frame)
3320}
3321
3322impl fmt::Debug for Yuv420pFrameBuffer {
3323    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3324        formatter
3325            .debug_struct("Yuv420pFrameBuffer")
3326            .field("width", &self.width)
3327            .field("height", &self.height)
3328            .finish_non_exhaustive()
3329    }
3330}
3331
3332fn plane_stride(linesize: i32, width: usize) -> Result<usize, BridgeError> {
3333    let stride = usize::try_from(linesize)
3334        .map_err(|_| BridgeError::InvalidInput("negative frame linesize"))?;
3335    if stride < width {
3336        return Err(BridgeError::InvalidInput(
3337            "frame linesize is below its width",
3338        ));
3339    }
3340    Ok(stride)
3341}
3342
3343/// # Safety
3344/// `data` must point at `height` rows of `stride` bytes allocated for this
3345/// frame, and the caller must hold the only reference to them.
3346unsafe fn writable_plane<'a>(
3347    data: *mut u8,
3348    stride: usize,
3349    width: usize,
3350    height: usize,
3351) -> Result<&'a mut [u8], BridgeError> {
3352    if data.is_null() {
3353        return Err(BridgeError::InvalidInput("frame plane is null"));
3354    }
3355    let len = stride
3356        .checked_mul(height.saturating_sub(1))
3357        .and_then(|rows| rows.checked_add(width))
3358        .ok_or(BridgeError::InvalidInput("frame plane size overflow"))?;
3359    // SAFETY: delegated to this function's contract.
3360    Ok(unsafe { slice::from_raw_parts_mut(data, len) })
3361}
3362
3363/// Ends the process now with `code`, without running its exit handlers.
3364///
3365/// For a process with a thread stuck inside a hardware decoder, which is
3366/// inside the GPU driver: the driver's exit handlers wait for that thread, so
3367/// a normal exit never finishes. The caller flushes what it owes first;
3368/// nothing else is released.
3369///
3370/// On Windows the handlers are the DLLs' detach code. `std::process::exit`
3371/// and the CRT's `_exit` both end in `ExitProcess`, which stops the other
3372/// threads wherever they are and then runs that code, and a driver's detach
3373/// can wait on a lock a stopped thread held. `TerminateProcess` runs none.
3374pub fn exit_without_handlers(code: u8) -> ! {
3375    #[cfg(unix)]
3376    {
3377        unsafe extern "C" {
3378            fn _exit(status: std::ffi::c_int) -> !;
3379        }
3380        // SAFETY: `_exit` accepts any status and never returns.
3381        unsafe { _exit(std::ffi::c_int::from(code)) }
3382    }
3383    #[cfg(windows)]
3384    {
3385        #[link(name = "kernel32")]
3386        unsafe extern "system" {
3387            fn GetCurrentProcess() -> *mut std::ffi::c_void;
3388            fn TerminateProcess(process: *mut std::ffi::c_void, exit_code: u32) -> i32;
3389        }
3390        // SAFETY: `GetCurrentProcess` returns this process's pseudo-handle, which has every access
3391        // right, including the one to terminate it.
3392        if unsafe { TerminateProcess(GetCurrentProcess(), u32::from(code)) } != 0 {
3393            // Termination is under way and takes this thread with it.
3394            loop {
3395                std::thread::park();
3396            }
3397        }
3398        // Not expected for this process's own handle; the normal exit at least reports the code.
3399        std::process::exit(i32::from(code))
3400    }
3401    #[cfg(not(any(unix, windows)))]
3402    std::process::exit(i32::from(code))
3403}
3404
3405#[cfg(all(test, target_os = "linux", feature = "hwdevice"))]
3406mod vulkan_shim_tests;
3407
3408#[cfg(all(test, target_os = "linux", feature = "hwdevice"))]
3409mod vulkan_access_tests;
3410
3411#[cfg(test)]
3412mod tests;