Skip to main content

apple_metal/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! ---
4//!
5//! # API Documentation
6
7#![cfg_attr(docsrs, feature(doc_cfg))]
8#![allow(clippy::missing_const_for_fn)]
9
10use core::ffi::c_void;
11use core::ptr;
12
13pub(crate) mod advanced;
14pub(crate) mod command;
15pub mod ffi;
16pub(crate) mod render;
17pub(crate) mod util;
18
19pub use advanced::*;
20pub use command::*;
21pub use render::*;
22
23/// Common `MTLPixelFormat` constants.
24pub mod pixel_format {
25    pub const A8UNORM: usize = 1;
26    pub const R8UNORM: usize = 10;
27    pub const R8SNORM: usize = 12;
28    pub const R8UINT: usize = 13;
29    pub const R8SINT: usize = 14;
30    pub const R16UNORM: usize = 20;
31    pub const R16SNORM: usize = 22;
32    pub const R16UINT: usize = 23;
33    pub const R16SINT: usize = 24;
34    pub const R16FLOAT: usize = 25;
35    pub const RG8UNORM: usize = 30;
36    pub const RG8SNORM: usize = 32;
37    pub const RG8UINT: usize = 33;
38    pub const RG8SINT: usize = 34;
39    pub const RGBA8UNORM: usize = 70;
40    pub const RGBA8UNORM_SRGB: usize = 71;
41    pub const RGBA8SNORM: usize = 72;
42    pub const RGBA8UINT: usize = 73;
43    pub const RGBA8SINT: usize = 74;
44    pub const BGRA8UNORM: usize = 80;
45    pub const BGRA8UNORM_SRGB: usize = 81;
46    pub const R32FLOAT: usize = 55;
47    pub const RG16FLOAT: usize = 65;
48    pub const RGBA16FLOAT: usize = 115;
49    pub const RGBA32FLOAT: usize = 125;
50    pub const DEPTH32FLOAT: usize = 252;
51    pub const STENCIL8: usize = 253;
52    pub const BGRA10_XR: usize = 552;
53    pub const BGR10_XR: usize = 554;
54}
55
56/// `MTLStorageMode` enum values — memory residency hints.
57pub mod storage_mode {
58    pub const SHARED: usize = 0;
59    pub const MANAGED: usize = 1;
60    pub const PRIVATE: usize = 2;
61    pub const MEMORYLESS: usize = 3;
62}
63
64/// `MTLCPUCacheMode` enum values.
65pub mod cpu_cache_mode {
66    pub const DEFAULT_CACHE: usize = 0;
67    pub const WRITE_COMBINED: usize = 1;
68}
69
70/// `MTLHazardTrackingMode` enum values.
71pub mod hazard_tracking_mode {
72    pub const DEFAULT: usize = 0;
73    pub const UNTRACKED: usize = 1;
74    pub const TRACKED: usize = 2;
75}
76
77/// `MTLResourceOptions` bitmask values.
78pub mod resource_options {
79    pub const CPU_CACHE_MODE_DEFAULT: usize = 0;
80    pub const CPU_CACHE_MODE_WRITE_COMBINED: usize = 1;
81    pub const STORAGE_MODE_SHARED: usize = 0;
82    pub const STORAGE_MODE_MANAGED: usize = 1 << 4;
83    pub const STORAGE_MODE_PRIVATE: usize = 2 << 4;
84    pub const HAZARD_TRACKING_MODE_DEFAULT: usize = 0;
85    pub const HAZARD_TRACKING_MODE_UNTRACKED: usize = 1 << 8;
86    pub const HAZARD_TRACKING_MODE_TRACKED: usize = 2 << 8;
87}
88
89/// `MTLTextureUsage` bitmask.
90pub mod texture_usage {
91    pub const SHADER_READ: usize = 0x01;
92    pub const SHADER_WRITE: usize = 0x02;
93    pub const RENDER_TARGET: usize = 0x04;
94}
95
96/// `MTLGPUFamily` — feature-family identifiers.
97pub mod gpu_family {
98    pub const APPLE1: i64 = 1001;
99    pub const APPLE2: i64 = 1002;
100    pub const APPLE3: i64 = 1003;
101    pub const APPLE4: i64 = 1004;
102    pub const APPLE5: i64 = 1005;
103    pub const APPLE6: i64 = 1006;
104    pub const APPLE7: i64 = 1007;
105    pub const APPLE8: i64 = 1008;
106    pub const APPLE9: i64 = 1009;
107    pub const MAC1: i64 = 2001;
108    pub const MAC2: i64 = 2002;
109    pub const COMMON1: i64 = 3001;
110    pub const COMMON2: i64 = 3002;
111    pub const COMMON3: i64 = 3003;
112    pub const METAL3: i64 = 5001;
113}
114
115// ---- Device ----
116
117/// Apple's `id<MTLDevice>` — handle to a Metal GPU.
118pub struct MetalDevice {
119    ptr: *mut c_void,
120    drop_on_release: bool,
121}
122
123unsafe impl Send for MetalDevice {}
124unsafe impl Sync for MetalDevice {}
125
126impl Drop for MetalDevice {
127    fn drop(&mut self) {
128        if self.drop_on_release && !self.ptr.is_null() {
129            unsafe { ffi::am_device_release(self.ptr) };
130            self.ptr = ptr::null_mut();
131        }
132    }
133}
134
135impl MetalDevice {
136    /// Return the system's default Metal device.
137    #[must_use]
138    pub fn system_default() -> Option<Self> {
139        let p = unsafe { ffi::am_device_system_default() };
140        if p.is_null() {
141            None
142        } else {
143            Some(unsafe { Self::from_retained_ptr(p) })
144        }
145    }
146
147    /// Raw `id<MTLDevice>` pointer.
148    #[must_use]
149    pub const fn as_ptr(&self) -> *mut c_void {
150        self.ptr
151    }
152
153    /// True if the GPU uses unified memory (Apple Silicon).
154    #[must_use]
155    pub fn has_unified_memory(&self) -> bool {
156        unsafe { ffi::am_device_has_unified_memory(self.ptr) }
157    }
158
159    /// Recommended maximum working-set size in bytes.
160    #[must_use]
161    pub fn recommended_max_working_set_size(&self) -> u64 {
162        unsafe { ffi::am_device_recommended_max_working_set_size(self.ptr) }
163    }
164
165    /// True if this device supports the requested feature family —
166    /// see [`gpu_family`].
167    #[must_use]
168    pub fn supports_family(&self, family: i64) -> bool {
169        unsafe { ffi::am_device_supports_family(self.ptr, family) }
170    }
171
172    /// Allocate a GPU-visible buffer of `length` bytes.
173    /// `options` is an `MTLResourceOptions` bitmask (see
174    /// [`resource_options`]).
175    #[must_use]
176    pub fn new_buffer(&self, length: usize, options: usize) -> Option<MetalBuffer> {
177        let p = unsafe { ffi::am_device_new_buffer(self.ptr, length, options) };
178        if p.is_null() {
179            None
180        } else {
181            Some(MetalBuffer { ptr: p })
182        }
183    }
184
185    /// Allocate a fresh `MTLTexture` matching `descriptor`.
186    #[must_use]
187    pub fn new_texture(&self, descriptor: TextureDescriptor) -> Option<MetalTexture> {
188        let p = unsafe {
189            ffi::am_device_new_texture_2d(
190                self.ptr,
191                descriptor.pixel_format,
192                descriptor.width,
193                descriptor.height,
194                descriptor.mipmapped,
195                descriptor.usage,
196                descriptor.storage_mode,
197            )
198        };
199        if p.is_null() {
200            None
201        } else {
202            Some(MetalTexture { ptr: p })
203        }
204    }
205
206    /// Create a new `MTLCommandQueue` to schedule GPU work.
207    #[must_use]
208    pub fn new_command_queue(&self) -> Option<CommandQueue> {
209        let p = unsafe { ffi::am_device_new_command_queue(self.ptr) };
210        if p.is_null() {
211            None
212        } else {
213            Some(CommandQueue { ptr: p })
214        }
215    }
216
217    /// Compile a Metal Shading Language source string into a runtime
218    /// `MTLLibrary`. On error, returns the localized Metal compiler
219    /// diagnostic.
220    ///
221    /// # Errors
222    ///
223    /// Returns the Metal compiler's localized error string on failure.
224    pub fn new_library_with_source(&self, source: &str) -> Result<MetalLibrary, String> {
225        let csrc = std::ffi::CString::new(source).map_err(|e| e.to_string())?;
226        let mut err_msg: *mut core::ffi::c_char = core::ptr::null_mut();
227        let p = unsafe {
228            ffi::am_device_new_library_with_source(self.ptr, csrc.as_ptr(), &mut err_msg)
229        };
230        if p.is_null() {
231            let msg = if err_msg.is_null() {
232                "MTLDevice.makeLibrary returned nil".to_string()
233            } else {
234                let s = unsafe { std::ffi::CStr::from_ptr(err_msg) }
235                    .to_string_lossy()
236                    .into_owned();
237                unsafe { libc::free(err_msg.cast()) };
238                s
239            };
240            Err(msg)
241        } else {
242            Ok(MetalLibrary { ptr: p })
243        }
244    }
245
246    /// Compile a kernel into a `MTLComputePipelineState` ready for
247    /// dispatch on a command buffer.
248    ///
249    /// # Errors
250    ///
251    /// Returns the Metal pipeline compiler's localized error string
252    /// on failure.
253    pub fn new_compute_pipeline_state(
254        &self,
255        function: &MetalFunction,
256    ) -> Result<ComputePipelineState, String> {
257        let mut err_msg: *mut core::ffi::c_char = core::ptr::null_mut();
258        let p = unsafe {
259            ffi::am_device_new_compute_pipeline_state(self.ptr, function.ptr, &mut err_msg)
260        };
261        if p.is_null() {
262            let msg = if err_msg.is_null() {
263                "MTLDevice.makeComputePipelineState returned nil".to_string()
264            } else {
265                let s = unsafe { std::ffi::CStr::from_ptr(err_msg) }
266                    .to_string_lossy()
267                    .into_owned();
268                unsafe { libc::free(err_msg.cast()) };
269                s
270            };
271            Err(msg)
272        } else {
273            Ok(ComputePipelineState { ptr: p })
274        }
275    }
276
277    /// Wrap a raw `id<MTLDevice>` pointer **without** taking ownership.
278    /// The returned handle will NOT release the underlying object on
279    /// drop.
280    ///
281    /// # Safety
282    ///
283    /// `ptr` must be a valid `id<MTLDevice>` whose lifetime is managed
284    /// by some other owner.
285    #[must_use]
286    pub unsafe fn from_raw_borrowed(ptr: *mut c_void) -> ManuallyDropDevice {
287        ManuallyDropDevice {
288            inner: Self {
289                ptr,
290                drop_on_release: false,
291            },
292        }
293    }
294}
295
296/// Borrowed [`MetalDevice`] that does not release on drop.
297pub struct ManuallyDropDevice {
298    inner: MetalDevice,
299}
300
301impl core::ops::Deref for ManuallyDropDevice {
302    type Target = MetalDevice;
303    fn deref(&self) -> &Self::Target {
304        &self.inner
305    }
306}
307
308// ---- Command queue + command buffer ----
309
310/// Apple's `id<MTLCommandQueue>` — schedules GPU work.
311pub struct CommandQueue {
312    ptr: *mut c_void,
313}
314
315unsafe impl Send for CommandQueue {}
316unsafe impl Sync for CommandQueue {}
317
318impl Drop for CommandQueue {
319    fn drop(&mut self) {
320        if !self.ptr.is_null() {
321            unsafe { ffi::am_command_queue_release(self.ptr) };
322            self.ptr = ptr::null_mut();
323        }
324    }
325}
326
327impl CommandQueue {
328    /// Create a new command buffer for recording GPU commands.
329    #[must_use]
330    pub fn new_command_buffer(&self) -> Option<CommandBuffer> {
331        let p = unsafe { ffi::am_command_queue_new_command_buffer(self.ptr) };
332        if p.is_null() {
333            None
334        } else {
335            Some(CommandBuffer { ptr: p })
336        }
337    }
338
339    /// Raw `id<MTLCommandQueue>` pointer.
340    #[must_use]
341    pub const fn as_ptr(&self) -> *mut c_void {
342        self.ptr
343    }
344}
345
346/// Apple's `id<MTLCommandBuffer>` — a recorded batch of GPU commands.
347pub struct CommandBuffer {
348    ptr: *mut c_void,
349}
350
351unsafe impl Send for CommandBuffer {}
352unsafe impl Sync for CommandBuffer {}
353
354impl Drop for CommandBuffer {
355    fn drop(&mut self) {
356        if !self.ptr.is_null() {
357            unsafe { ffi::am_command_buffer_release(self.ptr) };
358            self.ptr = ptr::null_mut();
359        }
360    }
361}
362
363impl CommandBuffer {
364    /// Submit the recorded commands for execution.
365    pub fn commit(&self) {
366        unsafe { ffi::am_command_buffer_commit(self.ptr) };
367    }
368
369    /// Block the current thread until all submitted commands finish.
370    pub fn wait_until_completed(&self) {
371        unsafe { ffi::am_command_buffer_wait_until_completed(self.ptr) };
372    }
373
374    /// Record a blit copy from `src` into `dst` for `size` bytes.
375    /// Convenience for GPU↔GPU byte copies.
376    #[must_use]
377    pub fn blit_copy_buffer(
378        &self,
379        src: &MetalBuffer,
380        src_offset: usize,
381        dst: &MetalBuffer,
382        dst_offset: usize,
383        size: usize,
384    ) -> bool {
385        unsafe {
386            ffi::am_command_buffer_blit_copy_buffer(
387                self.ptr,
388                src.as_ptr(),
389                src_offset,
390                dst.as_ptr(),
391                dst_offset,
392                size,
393            )
394        }
395    }
396
397    /// Record a 1-D compute dispatch: binds `pso`, sets `buffers` at
398    /// argument slots `0..buffers.len()`, and dispatches `threadgroups`
399    /// of `threads_per_group` threads.
400    #[must_use]
401    pub fn dispatch_compute_1d(
402        &self,
403        pso: &ComputePipelineState,
404        buffers: &[&MetalBuffer],
405        threadgroups: usize,
406        threads_per_group: usize,
407    ) -> bool {
408        let raw: Vec<*mut c_void> = buffers.iter().map(|b| b.as_ptr()).collect();
409        unsafe {
410            ffi::am_command_buffer_dispatch_compute_1d(
411                self.ptr,
412                pso.ptr,
413                raw.as_ptr(),
414                raw.len(),
415                threadgroups,
416                threads_per_group,
417            )
418        }
419    }
420
421    /// Raw `id<MTLCommandBuffer>` pointer.
422    #[must_use]
423    pub const fn as_ptr(&self) -> *mut c_void {
424        self.ptr
425    }
426}
427
428// ---- Library + Function + ComputePipelineState ----
429
430/// Apple's `id<MTLLibrary>` — compiled MSL source.
431pub struct MetalLibrary {
432    ptr: *mut c_void,
433}
434
435unsafe impl Send for MetalLibrary {}
436unsafe impl Sync for MetalLibrary {}
437
438impl Drop for MetalLibrary {
439    fn drop(&mut self) {
440        if !self.ptr.is_null() {
441            unsafe { ffi::am_library_release(self.ptr) };
442            self.ptr = ptr::null_mut();
443        }
444    }
445}
446
447impl MetalLibrary {
448    /// Look up a kernel function by its source name.
449    #[must_use]
450    pub fn new_function(&self, name: &str) -> Option<MetalFunction> {
451        let cname = std::ffi::CString::new(name).ok()?;
452        let p = unsafe { ffi::am_library_new_function(self.ptr, cname.as_ptr()) };
453        if p.is_null() {
454            None
455        } else {
456            Some(MetalFunction { ptr: p })
457        }
458    }
459
460    /// Raw `id<MTLLibrary>` pointer.
461    #[must_use]
462    pub const fn as_ptr(&self) -> *mut c_void {
463        self.ptr
464    }
465}
466
467/// Apple's `id<MTLFunction>` — a single compiled shader entry point.
468pub struct MetalFunction {
469    ptr: *mut c_void,
470}
471
472unsafe impl Send for MetalFunction {}
473unsafe impl Sync for MetalFunction {}
474
475impl Drop for MetalFunction {
476    fn drop(&mut self) {
477        if !self.ptr.is_null() {
478            unsafe { ffi::am_function_release(self.ptr) };
479            self.ptr = ptr::null_mut();
480        }
481    }
482}
483
484impl MetalFunction {
485    /// Raw `id<MTLFunction>` pointer.
486    #[must_use]
487    pub const fn as_ptr(&self) -> *mut c_void {
488        self.ptr
489    }
490}
491
492/// Apple's `id<MTLComputePipelineState>` — a compiled compute kernel.
493pub struct ComputePipelineState {
494    ptr: *mut c_void,
495}
496
497unsafe impl Send for ComputePipelineState {}
498unsafe impl Sync for ComputePipelineState {}
499
500impl Drop for ComputePipelineState {
501    fn drop(&mut self) {
502        if !self.ptr.is_null() {
503            unsafe { ffi::am_compute_pipeline_state_release(self.ptr) };
504            self.ptr = ptr::null_mut();
505        }
506    }
507}
508
509impl ComputePipelineState {
510    /// Raw `id<MTLComputePipelineState>` pointer.
511    #[must_use]
512    pub const fn as_ptr(&self) -> *mut c_void {
513        self.ptr
514    }
515}
516
517// ---- Buffer ----
518
519/// Apple's `id<MTLBuffer>` — a GPU-visible byte buffer.
520pub struct MetalBuffer {
521    ptr: *mut c_void,
522}
523
524unsafe impl Send for MetalBuffer {}
525unsafe impl Sync for MetalBuffer {}
526
527impl Drop for MetalBuffer {
528    fn drop(&mut self) {
529        if !self.ptr.is_null() {
530            unsafe { ffi::am_buffer_release(self.ptr) };
531            self.ptr = ptr::null_mut();
532        }
533    }
534}
535
536impl MetalBuffer {
537    /// Buffer length in bytes.
538    #[must_use]
539    pub fn length(&self) -> usize {
540        unsafe { ffi::am_buffer_length(self.ptr) }
541    }
542
543    /// Raw `void *` to the buffer's CPU-visible bytes. `None` for
544    /// `MTLStorageMode::Private` (GPU-only) buffers.
545    #[must_use]
546    pub fn contents(&self) -> Option<*mut c_void> {
547        let p = unsafe { ffi::am_buffer_contents(self.ptr) };
548        if p.is_null() {
549            None
550        } else {
551            Some(p)
552        }
553    }
554
555    /// Copy `src` into this buffer at byte offset `0`. Returns the
556    /// number of bytes actually written.
557    #[must_use]
558    pub fn write_bytes(&self, src: &[u8]) -> usize {
559        let Some(dst) = self.contents() else {
560            return 0;
561        };
562        let n = core::cmp::min(src.len(), self.length());
563        unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), dst.cast::<u8>(), n) };
564        n
565    }
566
567    /// Raw `id<MTLBuffer>` pointer.
568    #[must_use]
569    pub const fn as_ptr(&self) -> *mut c_void {
570        self.ptr
571    }
572}
573
574// ---- Texture descriptor + texture ----
575
576/// Configuration for `MetalDevice::new_texture`.
577#[derive(Debug, Clone, Copy)]
578pub struct TextureDescriptor {
579    pub pixel_format: usize,
580    pub width: usize,
581    pub height: usize,
582    pub mipmapped: bool,
583    pub usage: usize,
584    pub storage_mode: usize,
585}
586
587impl TextureDescriptor {
588    /// Sensible defaults for a shader-read+write 2D texture in shared storage.
589    #[must_use]
590    pub const fn new_2d(width: usize, height: usize, pixel_format: usize) -> Self {
591        Self {
592            pixel_format,
593            width,
594            height,
595            mipmapped: false,
596            usage: texture_usage::SHADER_READ | texture_usage::SHADER_WRITE,
597            storage_mode: storage_mode::SHARED,
598        }
599    }
600}
601
602/// Apple's `id<MTLTexture>` — a GPU-resident 2D image.
603pub struct MetalTexture {
604    ptr: *mut c_void,
605}
606
607unsafe impl Send for MetalTexture {}
608unsafe impl Sync for MetalTexture {}
609
610impl Drop for MetalTexture {
611    fn drop(&mut self) {
612        if !self.ptr.is_null() {
613            unsafe { ffi::am_texture_release(self.ptr) };
614            self.ptr = ptr::null_mut();
615        }
616    }
617}
618
619impl MetalTexture {
620    /// Texture width in pixels.
621    #[must_use]
622    pub fn width(&self) -> usize {
623        unsafe { ffi::am_texture_width(self.ptr) }
624    }
625
626    /// Texture height in pixels.
627    #[must_use]
628    pub fn height(&self) -> usize {
629        unsafe { ffi::am_texture_height(self.ptr) }
630    }
631
632    /// Underlying `MTLPixelFormat` enum value — see [`pixel_format`].
633    #[must_use]
634    pub fn pixel_format(&self) -> usize {
635        unsafe { ffi::am_texture_pixel_format(self.ptr) }
636    }
637
638    /// Raw `id<MTLTexture>` pointer.
639    #[must_use]
640    pub const fn as_ptr(&self) -> *mut c_void {
641        self.ptr
642    }
643
644    /// Wrap a raw `id<MTLTexture>` pointer. Pointer is taken without
645    /// retain.
646    ///
647    /// # Safety
648    ///
649    /// `ptr` must be a valid `id<MTLTexture>` whose ownership the
650    /// caller is transferring.
651    #[must_use]
652    pub const unsafe fn from_raw(ptr: *mut c_void) -> Self {
653        Self { ptr }
654    }
655}
656
657impl MetalDevice {
658    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
659        Self {
660            ptr,
661            drop_on_release: true,
662        }
663    }
664}
665
666impl CommandQueue {
667    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
668        Self { ptr }
669    }
670}
671
672impl CommandBuffer {
673    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
674        Self { ptr }
675    }
676}
677
678impl MetalBuffer {
679    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
680        Self { ptr }
681    }
682}
683
684// ---- IOSurface extension ----
685
686#[cfg(feature = "iosurface")]
687#[cfg_attr(docsrs, doc(cfg(feature = "iosurface")))]
688mod iosurface_ext {
689    use super::{ffi, pixel_format, MetalDevice, MetalTexture};
690    use apple_cf::iosurface::IOSurface;
691    use core::ffi::c_void;
692
693    /// Add Metal interop methods to [`IOSurface`].
694    pub trait IOSurfaceMetalExt {
695        /// Wrap the given plane of this `IOSurface` as a zero-copy
696        /// [`MetalTexture`] on the given device.
697        fn create_metal_texture(
698            &self,
699            device: &MetalDevice,
700            plane_index: usize,
701        ) -> Option<MetalTexture>;
702    }
703
704    impl IOSurfaceMetalExt for IOSurface {
705        fn create_metal_texture(
706            &self,
707            device: &MetalDevice,
708            plane_index: usize,
709        ) -> Option<MetalTexture> {
710            let format = pixel_format_for_fourcc(self.pixel_format(), plane_index)?;
711            let (width, height) = if plane_index == 0 {
712                (self.width(), self.height())
713            } else {
714                (self.width() / 2, self.height() / 2)
715            };
716            let p = unsafe {
717                ffi::am_device_new_texture_from_iosurface(
718                    device.as_ptr(),
719                    self.as_ptr().cast::<c_void>(),
720                    plane_index,
721                    format,
722                    width,
723                    height,
724                )
725            };
726            if p.is_null() {
727                None
728            } else {
729                Some(unsafe { MetalTexture::from_raw(p) })
730            }
731        }
732    }
733
734    fn pixel_format_for_fourcc(fourcc: u32, plane_index: usize) -> Option<usize> {
735        const BGRA: u32 = u32::from_be_bytes(*b"BGRA");
736        const L10R: u32 = u32::from_be_bytes(*b"l10r");
737        const YUV420V: u32 = u32::from_be_bytes(*b"420v");
738        const YUV420F: u32 = u32::from_be_bytes(*b"420f");
739
740        match (fourcc, plane_index) {
741            (BGRA, 0) => Some(pixel_format::BGRA8UNORM),
742            (L10R, 0) => Some(pixel_format::BGRA10_XR),
743            (YUV420V | YUV420F, 0) => Some(pixel_format::R8UNORM),
744            (YUV420V | YUV420F, 1) => Some(pixel_format::RG8UNORM),
745            _ => None,
746        }
747    }
748}
749
750#[cfg(feature = "iosurface")]
751pub use iosurface_ext::IOSurfaceMetalExt;
752
753/// True if `fourcc` identifies a YCbCr biplanar (`Y` + `CbCr`) format.
754#[must_use]
755pub const fn is_ycbcr_biplanar(fourcc: u32) -> bool {
756    const YUV420V: u32 = u32::from_be_bytes(*b"420v");
757    const YUV420F: u32 = u32::from_be_bytes(*b"420f");
758    matches!(fourcc, YUV420V | YUV420F)
759}