Skip to main content

apple_cf/metal/
mod.rs

1//! Metal interop — zero-copy GPU access to `IOSurface`-backed pixel data.
2//!
3//! This module provides the minimum-viable Metal bridge: a [`MetalDevice`]
4//! handle, a [`MetalTexture`] wrapper, and an extension trait that lets
5//! [`IOSurface`] hand out Metal textures with no CPU copy.
6//!
7//! For higher-level rendering (built-in shaders, YCbCr-to-RGB conversion,
8//! multi-plane texture descriptors), see `screencapturekit::metal`.
9
10#![allow(
11    clippy::missing_safety_doc,
12    clippy::too_many_lines,
13    clippy::doc_markdown,
14    clippy::module_name_repetitions,
15    non_camel_case_types,
16    clippy::upper_case_acronyms,
17    clippy::duplicated_attributes,
18    clippy::missing_const_for_fn,
19    clippy::ptr_as_ptr
20)]
21
22use core::ffi::c_void;
23use core::ptr;
24use std::ffi::CString;
25
26use crate::iosurface::IOSurface;
27
28// ---- FFI ----
29
30type id = *mut c_void;
31type SEL = *const c_void;
32type Class = *const c_void;
33
34#[link(name = "Metal", kind = "framework")]
35#[link(name = "Foundation", kind = "framework")]
36#[link(name = "objc")]
37extern "C" {
38    fn MTLCreateSystemDefaultDevice() -> id;
39    fn objc_msgSend();
40    fn sel_registerName(name: *const i8) -> SEL;
41    fn objc_getClass(name: *const i8) -> Class;
42    fn objc_release(obj: id);
43}
44
45unsafe fn sel(name: &str) -> SEL {
46    let c = CString::new(name).expect("selector name has no NUL");
47    sel_registerName(c.as_ptr())
48}
49
50unsafe fn class_named(name: &str) -> Class {
51    let c = CString::new(name).expect("class name has no NUL");
52    objc_getClass(c.as_ptr())
53}
54
55// `objc_msgSend` is variadic; we cast it to the concrete signature per call.
56type MsgSend0 = unsafe extern "C" fn(id, SEL) -> id;
57type MsgSendUsize = unsafe extern "C" fn(id, SEL) -> usize;
58type MsgSendU32 = unsafe extern "C" fn(id, SEL) -> u32;
59type MsgSendTextureFromIOSurface =
60    unsafe extern "C" fn(id, SEL, id, *mut c_void, usize) -> id;
61type MsgSendTextureDescriptorInit =
62    unsafe extern "C" fn(id, SEL, u32, usize, usize, bool) -> id;
63type MsgSendSetUsage = unsafe extern "C" fn(id, SEL, usize);
64type MsgSendSetStorage = unsafe extern "C" fn(id, SEL, usize);
65
66/// Common `MTLPixelFormat` constants.
67pub mod pixel_format {
68    pub const BGRA8UNORM: u32 = 80;
69    pub const RGBA8UNORM: u32 = 70;
70    pub const R8UNORM: u32 = 10;
71    pub const RG8UNORM: u32 = 30;
72    pub const BGRA10_XR: u32 = 552;
73    pub const BGR10_XR: u32 = 554;
74}
75
76const MTL_TEXTURE_USAGE_SHADER_READ: usize = 0x01;
77const MTL_TEXTURE_USAGE_SHADER_WRITE: usize = 0x02;
78const MTL_STORAGE_MODE_SHARED: usize = 0;
79
80// ---- Device ----
81
82/// Apple's `id<MTLDevice>` — handle to a Metal GPU.
83pub struct MetalDevice {
84    ptr: id,
85}
86
87unsafe impl Send for MetalDevice {}
88unsafe impl Sync for MetalDevice {}
89
90impl Drop for MetalDevice {
91    fn drop(&mut self) {
92        if !self.ptr.is_null() {
93            unsafe { objc_release(self.ptr) };
94            self.ptr = ptr::null_mut();
95        }
96    }
97}
98
99impl MetalDevice {
100    /// Return the system's default Metal device.
101    #[must_use]
102    pub fn system_default() -> Option<Self> {
103        let p = unsafe { MTLCreateSystemDefaultDevice() };
104        if p.is_null() {
105            None
106        } else {
107            Some(Self { ptr: p })
108        }
109    }
110
111    /// Raw `id<MTLDevice>` pointer — for interop with other Metal-using
112    /// Rust crates (`metal`, `objc2-metal`, ...).
113    #[must_use]
114    pub const fn as_ptr(&self) -> *mut c_void {
115        self.ptr
116    }
117}
118
119// ---- Texture ----
120
121/// Apple's `id<MTLTexture>` — a GPU-resident 2D image.
122pub struct MetalTexture {
123    ptr: id,
124}
125
126unsafe impl Send for MetalTexture {}
127unsafe impl Sync for MetalTexture {}
128
129impl Drop for MetalTexture {
130    fn drop(&mut self) {
131        if !self.ptr.is_null() {
132            unsafe { objc_release(self.ptr) };
133            self.ptr = ptr::null_mut();
134        }
135    }
136}
137
138impl MetalTexture {
139    /// Texture width in pixels.
140    #[must_use]
141    pub fn width(&self) -> usize {
142        unsafe {
143            let m: MsgSendUsize = core::mem::transmute(objc_msgSend as *const c_void);
144            m(self.ptr, sel("width"))
145        }
146    }
147
148    /// Texture height in pixels.
149    #[must_use]
150    pub fn height(&self) -> usize {
151        unsafe {
152            let m: MsgSendUsize = core::mem::transmute(objc_msgSend as *const c_void);
153            m(self.ptr, sel("height"))
154        }
155    }
156
157    /// Underlying `MTLPixelFormat` enum value — see [`pixel_format`].
158    #[must_use]
159    pub fn pixel_format(&self) -> u32 {
160        unsafe {
161            let m: MsgSendU32 = core::mem::transmute(objc_msgSend as *const c_void);
162            m(self.ptr, sel("pixelFormat"))
163        }
164    }
165
166    /// Raw `id<MTLTexture>` pointer.
167    #[must_use]
168    pub const fn as_ptr(&self) -> *mut c_void {
169        self.ptr
170    }
171}
172
173// ---- IOSurface extension trait ----
174
175/// Add Metal interop methods to [`IOSurface`].
176pub trait IOSurfaceMetalExt {
177    /// Wrap the given plane of this `IOSurface` as a zero-copy
178    /// [`MetalTexture`] on the given device.
179    fn create_metal_texture(&self, device: &MetalDevice, plane_index: usize)
180        -> Option<MetalTexture>;
181}
182
183impl IOSurfaceMetalExt for IOSurface {
184    fn create_metal_texture(
185        &self,
186        device: &MetalDevice,
187        plane_index: usize,
188    ) -> Option<MetalTexture> {
189        let format = pixel_format_for_fourcc(self.pixel_format(), plane_index)?;
190        let (width, height) = if plane_index == 0 {
191            (self.width(), self.height())
192        } else {
193            (self.width() / 2, self.height() / 2)
194        };
195
196        unsafe {
197            let desc_class = class_named("MTLTextureDescriptor");
198            let m_alloc: MsgSend0 = core::mem::transmute(objc_msgSend as *const c_void);
199            let raw_desc = m_alloc(desc_class.cast_mut(), sel("alloc"));
200            let init: MsgSendTextureDescriptorInit =
201                core::mem::transmute(objc_msgSend as *const c_void);
202            let desc = init(
203                raw_desc,
204                sel("texture2DDescriptorWithPixelFormat:width:height:mipmapped:"),
205                format,
206                width,
207                height,
208                false,
209            );
210            if desc.is_null() {
211                return None;
212            }
213            let set_usage: MsgSendSetUsage =
214                core::mem::transmute(objc_msgSend as *const c_void);
215            set_usage(
216                desc,
217                sel("setUsage:"),
218                MTL_TEXTURE_USAGE_SHADER_READ | MTL_TEXTURE_USAGE_SHADER_WRITE,
219            );
220            let set_storage: MsgSendSetStorage =
221                core::mem::transmute(objc_msgSend as *const c_void);
222            set_storage(desc, sel("setStorageMode:"), MTL_STORAGE_MODE_SHARED);
223
224            let m_tx: MsgSendTextureFromIOSurface =
225                core::mem::transmute(objc_msgSend as *const c_void);
226            let tx = m_tx(
227                device.ptr,
228                sel("newTextureWithDescriptor:iosurface:plane:"),
229                desc,
230                self.as_ptr().cast::<c_void>(),
231                plane_index,
232            );
233            objc_release(desc);
234            if tx.is_null() {
235                None
236            } else {
237                Some(MetalTexture { ptr: tx })
238            }
239        }
240    }
241}
242
243/// Map an `IOSurface` `FourCC` + plane index to the matching `MTLPixelFormat`.
244fn pixel_format_for_fourcc(fourcc: u32, plane_index: usize) -> Option<u32> {
245    const BGRA: u32 = u32::from_be_bytes(*b"BGRA");
246    const L10R: u32 = u32::from_be_bytes(*b"l10r");
247    const YUV420V: u32 = u32::from_be_bytes(*b"420v");
248    const YUV420F: u32 = u32::from_be_bytes(*b"420f");
249
250    match (fourcc, plane_index) {
251        (BGRA, 0) => Some(pixel_format::BGRA8UNORM),
252        (L10R, 0) => Some(pixel_format::BGRA10_XR),
253        (YUV420V | YUV420F, 0) => Some(pixel_format::R8UNORM),
254        (YUV420V | YUV420F, 1) => Some(pixel_format::RG8UNORM),
255        _ => None,
256    }
257}
258
259/// True if `fourcc` identifies a YCbCr biplanar (`Y` + `CbCr`) format.
260#[must_use]
261pub const fn is_ycbcr_biplanar(fourcc: u32) -> bool {
262    const YUV420V: u32 = u32::from_be_bytes(*b"420v");
263    const YUV420F: u32 = u32::from_be_bytes(*b"420f");
264    matches!(fourcc, YUV420V | YUV420F)
265}