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