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