Skip to main content

alpha2_ffmpeg_bridge/
lib.rs

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