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::ops::{Deref, DerefMut};
12use core::ptr;
13use std::sync::{Arc, Mutex, MutexGuard};
14
15pub(crate) mod advanced;
16pub(crate) mod argument;
17pub(crate) mod command;
18pub(crate) mod exhaustive;
19/// Groups `Metal` framework constants for `ffi`.
20pub mod ffi;
21pub(crate) mod metalfx;
22pub(crate) mod pipeline;
23pub(crate) mod render;
24pub(crate) mod state;
25pub(crate) mod util;
26
27/// Re-exports the `Metal` framework surface for this item.
28pub use advanced::*;
29/// Re-exports the `Metal` framework surface for this item.
30pub use argument::*;
31/// Re-exports the `Metal` framework surface for this item.
32pub use command::*;
33/// Re-exports the `Metal` framework surface for this item.
34pub use exhaustive::*;
35/// Re-exports the `Metal` framework surface for this item.
36pub use metalfx::*;
37/// Re-exports the `Metal` framework surface for this item.
38pub use pipeline::*;
39/// Re-exports the `Metal` framework surface for this item.
40pub use render::*;
41/// Re-exports the `Metal` framework surface for this item.
42pub use state::*;
43
44/// Common `MTLPixelFormat` constants.
45pub mod pixel_format {
46    /// Mirrors the `Metal` framework constant `A8UNORM`.
47    pub const A8UNORM: usize = 1;
48    /// Mirrors the `Metal` framework constant `R8UNORM`.
49    pub const R8UNORM: usize = 10;
50    /// Mirrors the `Metal` framework constant `R8SNORM`.
51    pub const R8SNORM: usize = 12;
52    /// Mirrors the `Metal` framework constant `R8UINT`.
53    pub const R8UINT: usize = 13;
54    /// Mirrors the `Metal` framework constant `R8SINT`.
55    pub const R8SINT: usize = 14;
56    /// Mirrors the `Metal` framework constant `R16UNORM`.
57    pub const R16UNORM: usize = 20;
58    /// Mirrors the `Metal` framework constant `R16SNORM`.
59    pub const R16SNORM: usize = 22;
60    /// Mirrors the `Metal` framework constant `R16UINT`.
61    pub const R16UINT: usize = 23;
62    /// Mirrors the `Metal` framework constant `R16SINT`.
63    pub const R16SINT: usize = 24;
64    /// Mirrors the `Metal` framework constant `R16FLOAT`.
65    pub const R16FLOAT: usize = 25;
66    /// Mirrors the `Metal` framework constant `RG8UNORM`.
67    pub const RG8UNORM: usize = 30;
68    /// Mirrors the `Metal` framework constant `RG8SNORM`.
69    pub const RG8SNORM: usize = 32;
70    /// Mirrors the `Metal` framework constant `RG8UINT`.
71    pub const RG8UINT: usize = 33;
72    /// Mirrors the `Metal` framework constant `RG8SINT`.
73    pub const RG8SINT: usize = 34;
74    /// Mirrors the `Metal` framework constant `RGBA8UNORM`.
75    pub const RGBA8UNORM: usize = 70;
76    /// Mirrors the `Metal` framework constant `RGBA8UNORM_SRGB`.
77    pub const RGBA8UNORM_SRGB: usize = 71;
78    /// Mirrors the `Metal` framework constant `RGBA8SNORM`.
79    pub const RGBA8SNORM: usize = 72;
80    /// Mirrors the `Metal` framework constant `RGBA8UINT`.
81    pub const RGBA8UINT: usize = 73;
82    /// Mirrors the `Metal` framework constant `RGBA8SINT`.
83    pub const RGBA8SINT: usize = 74;
84    /// Mirrors the `Metal` framework constant `BGRA8UNORM`.
85    pub const BGRA8UNORM: usize = 80;
86    /// Mirrors the `Metal` framework constant `BGRA8UNORM_SRGB`.
87    pub const BGRA8UNORM_SRGB: usize = 81;
88    /// Mirrors the `Metal` framework constant `R32FLOAT`.
89    pub const R32FLOAT: usize = 55;
90    /// Mirrors the `Metal` framework constant `RG16FLOAT`.
91    pub const RG16FLOAT: usize = 65;
92    /// Mirrors the `Metal` framework constant `RGBA16FLOAT`.
93    pub const RGBA16FLOAT: usize = 115;
94    /// Mirrors the `Metal` framework constant `RGBA32FLOAT`.
95    pub const RGBA32FLOAT: usize = 125;
96    /// Mirrors the `Metal` framework constant `DEPTH32FLOAT`.
97    pub const DEPTH32FLOAT: usize = 252;
98    /// Mirrors the `Metal` framework constant `STENCIL8`.
99    pub const STENCIL8: usize = 253;
100    /// Mirrors the `Metal` framework constant `BGRA10_XR`.
101    pub const BGRA10_XR: usize = 552;
102    /// Mirrors the `Metal` framework constant `BGR10_XR`.
103    pub const BGR10_XR: usize = 554;
104}
105
106/// `MTLStorageMode` enum values — memory residency hints.
107pub mod storage_mode {
108    /// Mirrors the `Metal` framework constant `SHARED`.
109    pub const SHARED: usize = 0;
110    /// Mirrors the `Metal` framework constant `MANAGED`.
111    pub const MANAGED: usize = 1;
112    /// Mirrors the `Metal` framework constant `PRIVATE`.
113    pub const PRIVATE: usize = 2;
114    /// Mirrors the `Metal` framework constant `MEMORYLESS`.
115    pub const MEMORYLESS: usize = 3;
116}
117
118/// `MTLCPUCacheMode` enum values.
119pub mod cpu_cache_mode {
120    /// Mirrors the `Metal` framework constant `DEFAULT_CACHE`.
121    pub const DEFAULT_CACHE: usize = 0;
122    /// Mirrors the `Metal` framework constant `WRITE_COMBINED`.
123    pub const WRITE_COMBINED: usize = 1;
124}
125
126/// `MTLHazardTrackingMode` enum values.
127pub mod hazard_tracking_mode {
128    /// Mirrors the `Metal` framework constant `DEFAULT`.
129    pub const DEFAULT: usize = 0;
130    /// Mirrors the `Metal` framework constant `UNTRACKED`.
131    pub const UNTRACKED: usize = 1;
132    /// Mirrors the `Metal` framework constant `TRACKED`.
133    pub const TRACKED: usize = 2;
134}
135
136/// `MTLResourceOptions` bitmask values.
137pub mod resource_options {
138    /// Mirrors the `Metal` framework constant `CPU_CACHE_MODE_DEFAULT`.
139    pub const CPU_CACHE_MODE_DEFAULT: usize = 0;
140    /// Mirrors the `Metal` framework constant `CPU_CACHE_MODE_WRITE_COMBINED`.
141    pub const CPU_CACHE_MODE_WRITE_COMBINED: usize = 1;
142    /// Mirrors the `Metal` framework constant `STORAGE_MODE_SHARED`.
143    pub const STORAGE_MODE_SHARED: usize = 0;
144    /// Mirrors the `Metal` framework constant `STORAGE_MODE_MANAGED`.
145    pub const STORAGE_MODE_MANAGED: usize = 1 << 4;
146    /// Mirrors the `Metal` framework constant `STORAGE_MODE_PRIVATE`.
147    pub const STORAGE_MODE_PRIVATE: usize = 2 << 4;
148    /// Mirrors the `Metal` framework constant `HAZARD_TRACKING_MODE_DEFAULT`.
149    pub const HAZARD_TRACKING_MODE_DEFAULT: usize = 0;
150    /// Mirrors the `Metal` framework constant `HAZARD_TRACKING_MODE_UNTRACKED`.
151    pub const HAZARD_TRACKING_MODE_UNTRACKED: usize = 1 << 8;
152    /// Mirrors the `Metal` framework constant `HAZARD_TRACKING_MODE_TRACKED`.
153    pub const HAZARD_TRACKING_MODE_TRACKED: usize = 2 << 8;
154}
155
156/// `MTLTextureUsage` bitmask.
157pub mod texture_usage {
158    /// Mirrors the `Metal` framework constant `SHADER_READ`.
159    pub const SHADER_READ: usize = 0x01;
160    /// Mirrors the `Metal` framework constant `SHADER_WRITE`.
161    pub const SHADER_WRITE: usize = 0x02;
162    /// Mirrors the `Metal` framework constant `RENDER_TARGET`.
163    pub const RENDER_TARGET: usize = 0x04;
164}
165
166/// `MTLGPUFamily` — feature-family identifiers.
167pub mod gpu_family {
168    /// Mirrors the `Metal` framework constant `APPLE1`.
169    pub const APPLE1: i64 = 1001;
170    /// Mirrors the `Metal` framework constant `APPLE2`.
171    pub const APPLE2: i64 = 1002;
172    /// Mirrors the `Metal` framework constant `APPLE3`.
173    pub const APPLE3: i64 = 1003;
174    /// Mirrors the `Metal` framework constant `APPLE4`.
175    pub const APPLE4: i64 = 1004;
176    /// Mirrors the `Metal` framework constant `APPLE5`.
177    pub const APPLE5: i64 = 1005;
178    /// Mirrors the `Metal` framework constant `APPLE6`.
179    pub const APPLE6: i64 = 1006;
180    /// Mirrors the `Metal` framework constant `APPLE7`.
181    pub const APPLE7: i64 = 1007;
182    /// Mirrors the `Metal` framework constant `APPLE8`.
183    pub const APPLE8: i64 = 1008;
184    /// Mirrors the `Metal` framework constant `APPLE9`.
185    pub const APPLE9: i64 = 1009;
186    /// Mirrors the `Metal` framework constant `MAC1`.
187    pub const MAC1: i64 = 2001;
188    /// Mirrors the `Metal` framework constant `MAC2`.
189    pub const MAC2: i64 = 2002;
190    /// Mirrors the `Metal` framework constant `COMMON1`.
191    pub const COMMON1: i64 = 3001;
192    /// Mirrors the `Metal` framework constant `COMMON2`.
193    pub const COMMON2: i64 = 3002;
194    /// Mirrors the `Metal` framework constant `COMMON3`.
195    pub const COMMON3: i64 = 3003;
196    /// Mirrors the `Metal` framework constant `METAL3`.
197    pub const METAL3: i64 = 5001;
198}
199
200// ---- Device ----
201
202/// Apple's `id<MTLDevice>` — handle to a Metal GPU.
203pub struct MetalDevice {
204    ptr: *mut c_void,
205    drop_on_release: bool,
206}
207
208// SAFETY: `id<MTLDevice>` is thread-safe: creation, capability queries, and
209// resource allocation all synchronize internally via ObjC ARC + Metal's own locks.
210unsafe impl Send for MetalDevice {}
211unsafe impl Sync for MetalDevice {}
212
213impl Drop for MetalDevice {
214    fn drop(&mut self) {
215        if self.drop_on_release && !self.ptr.is_null() {
216            unsafe { ffi::am_device_release(self.ptr) };
217            self.ptr = ptr::null_mut();
218        }
219    }
220}
221
222impl MetalDevice {
223    /// Return the system's default Metal device.
224    #[must_use]
225    pub fn system_default() -> Option<Self> {
226        let p = unsafe { ffi::am_device_system_default() };
227        if p.is_null() {
228            None
229        } else {
230            Some(unsafe { Self::from_retained_ptr(p) })
231        }
232    }
233
234    /// Raw `id<MTLDevice>` pointer.
235    #[must_use]
236    pub const fn as_ptr(&self) -> *mut c_void {
237        self.ptr
238    }
239
240    /// True if the GPU uses unified memory (Apple Silicon).
241    #[must_use]
242    pub fn has_unified_memory(&self) -> bool {
243        unsafe { ffi::am_device_has_unified_memory(self.ptr) }
244    }
245
246    /// Recommended maximum working-set size in bytes.
247    #[must_use]
248    pub fn recommended_max_working_set_size(&self) -> u64 {
249        unsafe { ffi::am_device_recommended_max_working_set_size(self.ptr) }
250    }
251
252    /// True if this device supports the requested feature family —
253    /// see [`gpu_family`].
254    #[must_use]
255    pub fn supports_family(&self, family: i64) -> bool {
256        unsafe { ffi::am_device_supports_family(self.ptr, family) }
257    }
258
259    /// Allocate a GPU-visible buffer of `length` bytes.
260    /// `options` is an `MTLResourceOptions` bitmask (see
261    /// [`resource_options`]).
262    #[must_use]
263    pub fn new_buffer(&self, length: usize, options: usize) -> Option<MetalBuffer> {
264        if length > isize::MAX as usize {
265            return None;
266        }
267        let p = unsafe { ffi::am_device_new_buffer(self.ptr, length, options) };
268        if p.is_null() {
269            None
270        } else {
271            Some(unsafe { MetalBuffer::from_retained_ptr(p) })
272        }
273    }
274
275    /// Allocate a fresh `MTLTexture` matching `descriptor`.
276    #[must_use]
277    pub fn new_texture(&self, descriptor: TextureDescriptor) -> Option<MetalTexture> {
278        if [
279            descriptor.pixel_format,
280            descriptor.width,
281            descriptor.height,
282            descriptor.usage,
283            descriptor.storage_mode,
284        ]
285        .into_iter()
286        .any(|value| value > isize::MAX as usize)
287        {
288            return None;
289        }
290        let p = unsafe {
291            ffi::am_device_new_texture_2d(
292                self.ptr,
293                descriptor.pixel_format,
294                descriptor.width,
295                descriptor.height,
296                descriptor.mipmapped,
297                descriptor.usage,
298                descriptor.storage_mode,
299            )
300        };
301        if p.is_null() {
302            None
303        } else {
304            Some(MetalTexture { ptr: p })
305        }
306    }
307
308    /// Create a new `MTLCommandQueue` to schedule GPU work.
309    #[must_use]
310    pub fn new_command_queue(&self) -> Option<CommandQueue> {
311        let p = unsafe { ffi::am_device_new_command_queue(self.ptr) };
312        if p.is_null() {
313            None
314        } else {
315            Some(CommandQueue { ptr: p })
316        }
317    }
318
319    /// Compile a Metal Shading Language source string into a runtime
320    /// `MTLLibrary`. On error, returns the localized Metal compiler
321    /// diagnostic.
322    ///
323    /// # Errors
324    ///
325    /// Returns the Metal compiler's localized error string on failure.
326    pub fn new_library_with_source(&self, source: &str) -> Result<MetalLibrary, String> {
327        let csrc = std::ffi::CString::new(source).map_err(|e| e.to_string())?;
328        let mut err_msg: *mut core::ffi::c_char = core::ptr::null_mut();
329        let p = unsafe {
330            ffi::am_device_new_library_with_source(self.ptr, csrc.as_ptr(), &mut err_msg)
331        };
332        if p.is_null() {
333            let msg = if err_msg.is_null() {
334                "MTLDevice.makeLibrary returned nil".to_string()
335            } else {
336                let s = unsafe { std::ffi::CStr::from_ptr(err_msg) }
337                    .to_string_lossy()
338                    .into_owned();
339                unsafe { libc::free(err_msg.cast()) };
340                s
341            };
342            Err(msg)
343        } else {
344            Ok(MetalLibrary { ptr: p })
345        }
346    }
347
348    /// Compile a kernel into a `MTLComputePipelineState` ready for
349    /// dispatch on a command buffer.
350    ///
351    /// # Errors
352    ///
353    /// Returns the Metal pipeline compiler's localized error string
354    /// on failure.
355    pub fn new_compute_pipeline_state(
356        &self,
357        function: &MetalFunction,
358    ) -> Result<ComputePipelineState, String> {
359        let mut err_msg: *mut core::ffi::c_char = core::ptr::null_mut();
360        let p = unsafe {
361            ffi::am_device_new_compute_pipeline_state(self.ptr, function.ptr, &mut err_msg)
362        };
363        if p.is_null() {
364            let msg = if err_msg.is_null() {
365                "MTLDevice.makeComputePipelineState returned nil".to_string()
366            } else {
367                let s = unsafe { std::ffi::CStr::from_ptr(err_msg) }
368                    .to_string_lossy()
369                    .into_owned();
370                unsafe { libc::free(err_msg.cast()) };
371                s
372            };
373            Err(msg)
374        } else {
375            Ok(ComputePipelineState { ptr: p })
376        }
377    }
378
379    /// Wrap a raw `id<MTLDevice>` pointer **without** taking ownership.
380    /// The returned handle will NOT release the underlying object on
381    /// drop.
382    ///
383    /// # Safety
384    ///
385    /// `ptr` must be a valid `id<MTLDevice>` whose lifetime is managed
386    /// by some other owner.
387    #[must_use]
388    pub unsafe fn from_raw_borrowed(ptr: *mut c_void) -> ManuallyDropDevice {
389        ManuallyDropDevice {
390            inner: Self {
391                ptr,
392                drop_on_release: false,
393            },
394        }
395    }
396}
397
398/// Borrowed [`MetalDevice`] that does not release on drop.
399pub struct ManuallyDropDevice {
400    inner: MetalDevice,
401}
402
403impl core::ops::Deref for ManuallyDropDevice {
404    type Target = MetalDevice;
405    fn deref(&self) -> &Self::Target {
406        &self.inner
407    }
408}
409
410// ---- Command queue + command buffer ----
411
412/// Apple's `id<MTLCommandQueue>` — schedules GPU work.
413pub struct CommandQueue {
414    ptr: *mut c_void,
415}
416
417// SAFETY: `id<MTLCommandQueue>` is documented by Apple as thread-safe; multiple
418// threads may independently create command buffers from the same queue.
419unsafe impl Send for CommandQueue {}
420unsafe impl Sync for CommandQueue {}
421
422impl Drop for CommandQueue {
423    fn drop(&mut self) {
424        if !self.ptr.is_null() {
425            unsafe { ffi::am_command_queue_release(self.ptr) };
426            self.ptr = ptr::null_mut();
427        }
428    }
429}
430
431impl CommandQueue {
432    /// Create a new command buffer for recording GPU commands.
433    #[must_use]
434    pub fn new_command_buffer(&self) -> Option<CommandBuffer> {
435        let p = unsafe { ffi::am_command_queue_new_command_buffer(self.ptr) };
436        if p.is_null() {
437            None
438        } else {
439            Some(unsafe { CommandBuffer::from_retained_ptr(p) })
440        }
441    }
442
443    /// Raw `id<MTLCommandQueue>` pointer.
444    #[must_use]
445    pub const fn as_ptr(&self) -> *mut c_void {
446        self.ptr
447    }
448}
449
450/// Apple's `id<MTLCommandBuffer>` — a recorded batch of GPU commands.
451#[derive(Clone)]
452pub struct CommandBuffer {
453    pub(crate) inner: Arc<CommandBufferInner>,
454}
455
456pub(crate) struct CommandBufferInner {
457    pub(crate) ptr: *mut c_void,
458    pub(crate) state: Mutex<CommandBufferState>,
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq)]
462pub(crate) enum CommandBufferPhase {
463    Recording,
464    Enqueued,
465    Committed,
466    Completed,
467    Error,
468}
469
470pub(crate) struct CommandBufferState {
471    pub(crate) phase: CommandBufferPhase,
472    pub(crate) active_encoder: bool,
473}
474
475// SAFETY: all command-buffer state transitions and native mutations exposed by
476// this crate are serialized by `state`.
477unsafe impl Send for CommandBufferInner {}
478unsafe impl Sync for CommandBufferInner {}
479
480impl Drop for CommandBufferInner {
481    fn drop(&mut self) {
482        if !self.ptr.is_null() {
483            unsafe { ffi::am_command_buffer_release(self.ptr) };
484            self.ptr = ptr::null_mut();
485        }
486    }
487}
488
489impl CommandBuffer {
490    /// Borrowed raw `id<MTLCommandBuffer>` pointer.
491    ///
492    /// The pointer remains valid only while at least one clone of this wrapper
493    /// is alive. Calling lifecycle or encoding methods through the pointer can
494    /// bypass this crate's state validation.
495    #[must_use]
496    pub fn as_ptr(&self) -> *mut c_void {
497        self.inner.ptr
498    }
499}
500
501// ---- Library + Function + ComputePipelineState ----
502
503/// Apple's `id<MTLLibrary>` — compiled MSL source.
504pub struct MetalLibrary {
505    ptr: *mut c_void,
506}
507
508// SAFETY: `id<MTLLibrary>` is immutable after creation; all its methods are
509// thread-safe per Apple documentation.
510unsafe impl Send for MetalLibrary {}
511unsafe impl Sync for MetalLibrary {}
512
513impl Drop for MetalLibrary {
514    fn drop(&mut self) {
515        if !self.ptr.is_null() {
516            unsafe { ffi::am_library_release(self.ptr) };
517            self.ptr = ptr::null_mut();
518        }
519    }
520}
521
522impl MetalLibrary {
523    /// Look up a kernel function by its source name.
524    #[must_use]
525    pub fn new_function(&self, name: &str) -> Option<MetalFunction> {
526        let cname = std::ffi::CString::new(name).ok()?;
527        let p = unsafe { ffi::am_library_new_function(self.ptr, cname.as_ptr()) };
528        if p.is_null() {
529            None
530        } else {
531            Some(MetalFunction { ptr: p })
532        }
533    }
534
535    /// Raw `id<MTLLibrary>` pointer.
536    #[must_use]
537    pub const fn as_ptr(&self) -> *mut c_void {
538        self.ptr
539    }
540}
541
542/// Apple's `id<MTLFunction>` — a single compiled shader entry point.
543pub struct MetalFunction {
544    ptr: *mut c_void,
545}
546
547// SAFETY: `id<MTLFunction>` is immutable after creation and its handle is safe
548// to share across threads.
549unsafe impl Send for MetalFunction {}
550unsafe impl Sync for MetalFunction {}
551
552impl Drop for MetalFunction {
553    fn drop(&mut self) {
554        if !self.ptr.is_null() {
555            unsafe { ffi::am_function_release(self.ptr) };
556            self.ptr = ptr::null_mut();
557        }
558    }
559}
560
561impl MetalFunction {
562    /// Raw `id<MTLFunction>` pointer.
563    #[must_use]
564    pub const fn as_ptr(&self) -> *mut c_void {
565        self.ptr
566    }
567}
568
569/// Apple's `id<MTLComputePipelineState>` — a compiled compute kernel.
570pub struct ComputePipelineState {
571    ptr: *mut c_void,
572}
573
574// SAFETY: `id<MTLComputePipelineState>` is immutable after creation and
575// thread-safe per Apple documentation.
576unsafe impl Send for ComputePipelineState {}
577unsafe impl Sync for ComputePipelineState {}
578
579impl Drop for ComputePipelineState {
580    fn drop(&mut self) {
581        if !self.ptr.is_null() {
582            unsafe { ffi::am_compute_pipeline_state_release(self.ptr) };
583            self.ptr = ptr::null_mut();
584        }
585    }
586}
587
588impl ComputePipelineState {
589    /// Raw `id<MTLComputePipelineState>` pointer.
590    #[must_use]
591    pub const fn as_ptr(&self) -> *mut c_void {
592        self.ptr
593    }
594
595    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
596        Self { ptr }
597    }
598}
599
600// ---- Buffer ----
601
602/// Errors returned by CPU access to a [`MetalBuffer`].
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub enum MetalBufferAccessError {
605    /// The buffer's storage mode does not expose CPU-addressable bytes.
606    CpuInaccessibleStorage { storage_mode: usize },
607    /// Metal did not provide a CPU mapping for an otherwise accessible buffer.
608    MappingUnavailable,
609    /// Another mapping panicked while holding the allocation's mapping lock.
610    MappingLockPoisoned,
611    /// The requested byte range is outside the allocation.
612    RangeOutOfBounds {
613        offset: usize,
614        length: usize,
615        buffer_length: usize,
616    },
617    /// The supplied range has its end before its start.
618    InvalidRange,
619    /// The operation requires managed storage.
620    ManagedStorageRequired { storage_mode: usize },
621}
622
623impl core::fmt::Display for MetalBufferAccessError {
624    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
625        match self {
626            Self::CpuInaccessibleStorage { storage_mode } => {
627                write!(
628                    formatter,
629                    "storage mode {storage_mode} is not CPU-addressable"
630                )
631            }
632            Self::MappingUnavailable => formatter.write_str("Metal did not provide a CPU mapping"),
633            Self::MappingLockPoisoned => formatter.write_str("buffer mapping lock is poisoned"),
634            Self::RangeOutOfBounds {
635                offset,
636                length,
637                buffer_length,
638            } => write!(
639                formatter,
640                "byte range {offset}..{} exceeds buffer length {buffer_length}",
641                offset.saturating_add(*length)
642            ),
643            Self::InvalidRange => formatter.write_str("range end precedes range start"),
644            Self::ManagedStorageRequired { storage_mode } => {
645                write!(
646                    formatter,
647                    "managed storage required, got mode {storage_mode}"
648                )
649            }
650        }
651    }
652}
653
654impl std::error::Error for MetalBufferAccessError {}
655
656/// Scoped read-only CPU mapping of a [`MetalBuffer`].
657pub struct MetalBufferReadMapping<'a> {
658    pointer: core::ptr::NonNull<u8>,
659    length: usize,
660    _mapping_lock: MutexGuard<'a, ()>,
661}
662
663impl Deref for MetalBufferReadMapping<'_> {
664    type Target = [u8];
665
666    fn deref(&self) -> &Self::Target {
667        unsafe { core::slice::from_raw_parts(self.pointer.as_ptr(), self.length) }
668    }
669}
670
671/// Scoped writable CPU mapping of a [`MetalBuffer`].
672pub struct MetalBufferWriteMapping<'a> {
673    buffer: &'a MetalBuffer,
674    pointer: core::ptr::NonNull<u8>,
675    length: usize,
676    storage_mode: usize,
677    _mapping_lock: MutexGuard<'a, ()>,
678}
679
680impl Deref for MetalBufferWriteMapping<'_> {
681    type Target = [u8];
682
683    fn deref(&self) -> &Self::Target {
684        unsafe { core::slice::from_raw_parts(self.pointer.as_ptr(), self.length) }
685    }
686}
687
688impl DerefMut for MetalBufferWriteMapping<'_> {
689    fn deref_mut(&mut self) -> &mut Self::Target {
690        unsafe { core::slice::from_raw_parts_mut(self.pointer.as_ptr(), self.length) }
691    }
692}
693
694impl Drop for MetalBufferWriteMapping<'_> {
695    fn drop(&mut self) {
696        if self.storage_mode == storage_mode::MANAGED {
697            unsafe {
698                ffi::am_buffer_did_modify_range(self.buffer.as_ptr(), 0, self.length);
699            }
700        }
701    }
702}
703
704/// Apple's `id<MTLBuffer>` — a GPU-visible byte buffer.
705#[derive(Clone)]
706pub struct MetalBuffer {
707    inner: Arc<MetalBufferInner>,
708}
709
710struct MetalBufferInner {
711    ptr: *mut c_void,
712    mapping_lock: Mutex<()>,
713}
714
715// SAFETY: immutable resource queries are thread-safe and scoped CPU mappings
716// are serialized across clones by `mapping_lock`.
717unsafe impl Send for MetalBufferInner {}
718unsafe impl Sync for MetalBufferInner {}
719
720impl Drop for MetalBufferInner {
721    fn drop(&mut self) {
722        if !self.ptr.is_null() {
723            unsafe { ffi::am_buffer_release(self.ptr) };
724            self.ptr = ptr::null_mut();
725        }
726    }
727}
728
729#[allow(clippy::missing_errors_doc)]
730impl MetalBuffer {
731    /// Buffer length in bytes.
732    #[must_use]
733    pub fn length(&self) -> usize {
734        unsafe { ffi::am_buffer_length(self.as_ptr()) }
735    }
736
737    /// `MTLStorageMode` enum value.
738    #[must_use]
739    pub fn storage_mode(&self) -> usize {
740        unsafe { ffi::am_buffer_storage_mode(self.as_ptr()) }
741    }
742
743    /// Whether this buffer's storage mode permits CPU mapping.
744    #[must_use]
745    pub fn is_cpu_accessible(&self) -> bool {
746        matches!(
747            self.storage_mode(),
748            storage_mode::SHARED | storage_mode::MANAGED
749        )
750    }
751
752    /// Create shared staging storage with the same length as this buffer.
753    ///
754    /// Use a blit encoder to copy between the staging allocation and private
755    /// storage before mapping the staging buffer.
756    #[must_use]
757    pub fn new_staging_buffer(&self) -> Option<Self> {
758        let pointer = unsafe { ffi::am_buffer_new_staging_buffer(self.as_ptr()) };
759        if pointer.is_null() {
760            None
761        } else {
762            Some(unsafe { Self::from_retained_ptr(pointer) })
763        }
764    }
765
766    /// Map this buffer for scoped CPU reads.
767    ///
768    /// Clones of this Rust handle share a mapping lock. Independently-created
769    /// native aliases are outside that lock.
770    ///
771    /// # Safety
772    ///
773    /// The caller must ensure that no GPU write or CPU/native alias mutation
774    /// overlaps the mapping. This includes texture views backed by this buffer.
775    /// For managed storage, GPU writes must first be made visible with a
776    /// completed blit-encoder resource synchronization.
777    pub unsafe fn map_read(&self) -> Result<MetalBufferReadMapping<'_>, MetalBufferAccessError> {
778        let storage_mode = self.ensure_cpu_accessible()?;
779        let mapping_lock = self.lock_mapping()?;
780        let pointer = core::ptr::NonNull::new(ffi::am_buffer_contents(self.as_ptr()).cast::<u8>())
781            .ok_or(MetalBufferAccessError::MappingUnavailable)?;
782        let _ = storage_mode;
783        Ok(MetalBufferReadMapping {
784            pointer,
785            length: self.length(),
786            _mapping_lock: mapping_lock,
787        })
788    }
789
790    /// Map this buffer for scoped CPU writes.
791    ///
792    /// Managed mappings notify Metal of the modified allocation when the guard
793    /// is dropped. Clones of this Rust handle share a mapping lock, but native
794    /// aliases created outside this wrapper do not.
795    ///
796    /// # Safety
797    ///
798    /// The caller must ensure that no GPU access or CPU/native alias access
799    /// overlaps the mapping and must not submit GPU work using the buffer until
800    /// the mapping guard is dropped. This includes texture views backed by this
801    /// buffer.
802    pub unsafe fn map_write(&self) -> Result<MetalBufferWriteMapping<'_>, MetalBufferAccessError> {
803        let storage_mode = self.ensure_cpu_accessible()?;
804        let mapping_lock = self.lock_mapping()?;
805        let pointer = core::ptr::NonNull::new(ffi::am_buffer_contents(self.as_ptr()).cast::<u8>())
806            .ok_or(MetalBufferAccessError::MappingUnavailable)?;
807        Ok(MetalBufferWriteMapping {
808            buffer: self,
809            pointer,
810            length: self.length(),
811            storage_mode,
812            _mapping_lock: mapping_lock,
813        })
814    }
815
816    /// Copy `src` into a CPU-visible byte range.
817    ///
818    /// # Safety
819    ///
820    /// The caller must uphold the same GPU exclusion requirements as
821    /// [`Self::map_write`].
822    pub unsafe fn write_bytes(
823        &self,
824        offset: usize,
825        src: &[u8],
826    ) -> Result<(), MetalBufferAccessError> {
827        let end = self.checked_range_end(offset, src.len())?;
828        let mut mapping = self.map_write()?;
829        mapping[offset..end].copy_from_slice(src);
830        drop(mapping);
831        Ok(())
832    }
833
834    /// Copy a CPU-visible byte range into `destination`.
835    ///
836    /// # Safety
837    ///
838    /// The caller must uphold the same GPU exclusion and managed-storage
839    /// synchronization requirements as [`Self::map_read`].
840    pub unsafe fn read_bytes(
841        &self,
842        offset: usize,
843        destination: &mut [u8],
844    ) -> Result<(), MetalBufferAccessError> {
845        let end = self.checked_range_end(offset, destination.len())?;
846        let mapping = self.map_read()?;
847        destination.copy_from_slice(&mapping[offset..end]);
848        drop(mapping);
849        Ok(())
850    }
851
852    /// Borrowed raw `id<MTLBuffer>` pointer.
853    ///
854    /// The pointer remains valid only while at least one clone of this wrapper
855    /// is alive. CPU access through the raw object bypasses the mapping lock.
856    #[must_use]
857    pub fn as_ptr(&self) -> *mut c_void {
858        self.inner.ptr
859    }
860
861    fn ensure_cpu_accessible(&self) -> Result<usize, MetalBufferAccessError> {
862        let storage_mode = self.storage_mode();
863        if matches!(storage_mode, storage_mode::SHARED | storage_mode::MANAGED) {
864            Ok(storage_mode)
865        } else {
866            Err(MetalBufferAccessError::CpuInaccessibleStorage { storage_mode })
867        }
868    }
869
870    pub(crate) fn checked_range_end(
871        &self,
872        offset: usize,
873        length: usize,
874    ) -> Result<usize, MetalBufferAccessError> {
875        let end =
876            offset
877                .checked_add(length)
878                .ok_or_else(|| MetalBufferAccessError::RangeOutOfBounds {
879                    offset,
880                    length,
881                    buffer_length: self.length(),
882                })?;
883        let buffer_length = self.length();
884        if end > buffer_length {
885            Err(MetalBufferAccessError::RangeOutOfBounds {
886                offset,
887                length,
888                buffer_length,
889            })
890        } else {
891            Ok(end)
892        }
893    }
894
895    pub(crate) fn lock_mapping(&self) -> Result<MutexGuard<'_, ()>, MetalBufferAccessError> {
896        self.inner
897            .mapping_lock
898            .lock()
899            .map_err(|_| MetalBufferAccessError::MappingLockPoisoned)
900    }
901}
902
903// ---- Texture descriptor + texture ----
904
905/// Configuration for `MetalDevice::new_texture`.
906#[derive(Debug, Clone, Copy)]
907pub struct TextureDescriptor {
908    /// Mirrors the `Metal` framework property for `pixel_format`.
909    pub pixel_format: usize,
910    /// Mirrors the `Metal` framework property for `width`.
911    pub width: usize,
912    /// Mirrors the `Metal` framework property for `height`.
913    pub height: usize,
914    /// Mirrors the `Metal` framework property for `mipmapped`.
915    pub mipmapped: bool,
916    /// Mirrors the `Metal` framework property for `usage`.
917    pub usage: usize,
918    /// Mirrors the `Metal` framework property for `storage_mode`.
919    pub storage_mode: usize,
920}
921
922impl TextureDescriptor {
923    /// Sensible defaults for a shader-read+write 2D texture in shared storage.
924    #[must_use]
925    pub const fn new_2d(width: usize, height: usize, pixel_format: usize) -> Self {
926        Self {
927            pixel_format,
928            width,
929            height,
930            mipmapped: false,
931            usage: texture_usage::SHADER_READ | texture_usage::SHADER_WRITE,
932            storage_mode: storage_mode::SHARED,
933        }
934    }
935}
936
937/// Apple's `id<MTLTexture>` — a GPU-resident 2D image.
938pub struct MetalTexture {
939    ptr: *mut c_void,
940}
941
942// SAFETY: `id<MTLTexture>` is a GPU resource handle.  ObjC ARC operations are
943// atomic; descriptor queries are read-only and thread-safe.
944unsafe impl Send for MetalTexture {}
945unsafe impl Sync for MetalTexture {}
946
947impl Drop for MetalTexture {
948    fn drop(&mut self) {
949        if !self.ptr.is_null() {
950            unsafe { ffi::am_texture_release(self.ptr) };
951            self.ptr = ptr::null_mut();
952        }
953    }
954}
955
956impl MetalTexture {
957    /// Texture width in pixels.
958    #[must_use]
959    pub fn width(&self) -> usize {
960        unsafe { ffi::am_texture_width(self.ptr) }
961    }
962
963    /// Texture height in pixels.
964    #[must_use]
965    pub fn height(&self) -> usize {
966        unsafe { ffi::am_texture_height(self.ptr) }
967    }
968
969    /// Underlying `MTLPixelFormat` enum value — see [`pixel_format`].
970    #[must_use]
971    pub fn pixel_format(&self) -> usize {
972        unsafe { ffi::am_texture_pixel_format(self.ptr) }
973    }
974
975    /// Underlying `MTLTextureType` enum value — see [`texture_type`].
976    #[must_use]
977    pub fn texture_type(&self) -> usize {
978        unsafe { ffi::am_texture_type(self.ptr) }
979    }
980
981    /// Raw `id<MTLTexture>` pointer.
982    #[must_use]
983    pub const fn as_ptr(&self) -> *mut c_void {
984        self.ptr
985    }
986
987    /// Wrap a raw, **+1-retained** `id<MTLTexture>` pointer. Ownership is
988    /// transferred to the returned wrapper and released once on drop (no extra
989    /// retain is taken here).
990    ///
991    /// # Safety
992    ///
993    /// `ptr` must be a valid `id<MTLTexture>` whose ownership the
994    /// caller is transferring.
995    #[must_use]
996    pub const unsafe fn from_raw(ptr: *mut c_void) -> Self {
997        Self { ptr }
998    }
999}
1000
1001impl MetalDevice {
1002    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
1003        Self {
1004            ptr,
1005            drop_on_release: true,
1006        }
1007    }
1008}
1009
1010impl CommandQueue {
1011    pub(crate) const unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
1012        Self { ptr }
1013    }
1014}
1015
1016impl CommandBuffer {
1017    pub(crate) unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
1018        Self {
1019            inner: Arc::new(CommandBufferInner {
1020                ptr,
1021                state: Mutex::new(CommandBufferState {
1022                    phase: CommandBufferPhase::Recording,
1023                    active_encoder: false,
1024                }),
1025            }),
1026        }
1027    }
1028}
1029
1030impl MetalBuffer {
1031    pub(crate) unsafe fn from_retained_ptr(ptr: *mut c_void) -> Self {
1032        Self {
1033            inner: Arc::new(MetalBufferInner {
1034                ptr,
1035                mapping_lock: Mutex::new(()),
1036            }),
1037        }
1038    }
1039}
1040
1041// ---- IOSurface extension ----
1042
1043#[cfg(feature = "iosurface")]
1044#[cfg_attr(docsrs, doc(cfg(feature = "iosurface")))]
1045mod iosurface_ext {
1046    use super::{ffi, pixel_format, MetalDevice, MetalTexture};
1047    use apple_cf::iosurface::IOSurface;
1048    use core::ffi::c_void;
1049
1050    /// Errors returned while creating an `IOSurface`-backed texture.
1051    #[derive(Debug, Clone, PartialEq, Eq)]
1052    pub enum IOSurfaceMetalError {
1053        /// The requested plane does not exist for this surface.
1054        InvalidPlane {
1055            plane_index: usize,
1056            plane_count: usize,
1057        },
1058        /// The surface format and plane do not map to a supported Metal format.
1059        UnsupportedPixelFormat { fourcc: u32, plane_index: usize },
1060        /// The selected plane has zero dimensions or row stride.
1061        EmptyPlane {
1062            plane_index: usize,
1063            width: usize,
1064            height: usize,
1065            bytes_per_row: usize,
1066        },
1067        /// The plane row stride cannot contain its selected Metal format.
1068        IncompatiblePlaneLayout {
1069            plane_index: usize,
1070            bytes_per_row: usize,
1071            minimum_bytes_per_row: usize,
1072        },
1073        /// Plane metadata cannot be represented by the native bridge.
1074        IntegerOutOfRange { field: &'static str, value: usize },
1075        /// Metal rejected the validated `IOSurface` texture descriptor.
1076        NativeCreationFailed,
1077    }
1078
1079    impl core::fmt::Display for IOSurfaceMetalError {
1080        fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1081            match self {
1082                Self::InvalidPlane {
1083                    plane_index,
1084                    plane_count,
1085                } => write!(
1086                    formatter,
1087                    "IOSurface plane {plane_index} is outside the available count {plane_count}"
1088                ),
1089                Self::UnsupportedPixelFormat {
1090                    fourcc,
1091                    plane_index,
1092                } => write!(
1093                    formatter,
1094                    "IOSurface format {fourcc:#010x} plane {plane_index} is unsupported"
1095                ),
1096                Self::EmptyPlane {
1097                    plane_index,
1098                    width,
1099                    height,
1100                    bytes_per_row,
1101                } => write!(
1102                    formatter,
1103                    "IOSurface plane {plane_index} has invalid layout {width}x{height}, row {bytes_per_row}"
1104                ),
1105                Self::IncompatiblePlaneLayout {
1106                    plane_index,
1107                    bytes_per_row,
1108                    minimum_bytes_per_row,
1109                } => write!(
1110                    formatter,
1111                    "IOSurface plane {plane_index} row {bytes_per_row} is shorter than {minimum_bytes_per_row}"
1112                ),
1113                Self::IntegerOutOfRange { field, value } => {
1114                    write!(formatter, "{field} value {value} exceeds native Int")
1115                }
1116                Self::NativeCreationFailed => {
1117                    formatter.write_str("Metal could not create the IOSurface-backed texture")
1118                }
1119            }
1120        }
1121    }
1122
1123    impl std::error::Error for IOSurfaceMetalError {}
1124
1125    #[derive(Clone, Copy)]
1126    struct PlaneTextureFormat {
1127        pixel_format: usize,
1128        bytes_per_pixel: usize,
1129    }
1130
1131    /// Add Metal interop methods to [`IOSurface`].
1132    pub trait IOSurfaceMetalExt {
1133        /// Wrap the given plane of this `IOSurface` as a zero-copy
1134        /// [`MetalTexture`] on the given device.
1135        ///
1136        /// The native `IOSurfaceRef` pointer is borrowed only for the duration
1137        /// of this call. The returned Metal texture retains its backing surface.
1138        ///
1139        /// # Errors
1140        ///
1141        /// Returns plane, format, row-layout, integer-conversion, or native
1142        /// texture-creation failures.
1143        fn create_metal_texture(
1144            &self,
1145            device: &MetalDevice,
1146            plane_index: usize,
1147        ) -> Result<MetalTexture, IOSurfaceMetalError>;
1148    }
1149
1150    impl IOSurfaceMetalExt for IOSurface {
1151        fn create_metal_texture(
1152            &self,
1153            device: &MetalDevice,
1154            plane_index: usize,
1155        ) -> Result<MetalTexture, IOSurfaceMetalError> {
1156            let plane_count = self.plane_count();
1157            let (width, height, bytes_per_row) = if plane_count == 0 {
1158                if plane_index != 0 {
1159                    return Err(IOSurfaceMetalError::InvalidPlane {
1160                        plane_index,
1161                        plane_count: 1,
1162                    });
1163                }
1164                (self.width(), self.height(), self.bytes_per_row())
1165            } else {
1166                if plane_index >= plane_count {
1167                    return Err(IOSurfaceMetalError::InvalidPlane {
1168                        plane_index,
1169                        plane_count,
1170                    });
1171                }
1172                (
1173                    self.width_of_plane(plane_index),
1174                    self.height_of_plane(plane_index),
1175                    self.bytes_per_row_of_plane(plane_index),
1176                )
1177            };
1178            if width == 0 || height == 0 || bytes_per_row == 0 {
1179                return Err(IOSurfaceMetalError::EmptyPlane {
1180                    plane_index,
1181                    width,
1182                    height,
1183                    bytes_per_row,
1184                });
1185            }
1186            let format = pixel_format_for_fourcc(self.pixel_format(), plane_index)?;
1187            let minimum_bytes_per_row = width.checked_mul(format.bytes_per_pixel).ok_or(
1188                IOSurfaceMetalError::IntegerOutOfRange {
1189                    field: "minimum plane row bytes",
1190                    value: usize::MAX,
1191                },
1192            )?;
1193            if bytes_per_row < minimum_bytes_per_row || bytes_per_row % format.bytes_per_pixel != 0
1194            {
1195                return Err(IOSurfaceMetalError::IncompatiblePlaneLayout {
1196                    plane_index,
1197                    bytes_per_row,
1198                    minimum_bytes_per_row,
1199                });
1200            }
1201            for (field, value) in [
1202                ("plane index", plane_index),
1203                ("plane width", width),
1204                ("plane height", height),
1205                ("pixel format", format.pixel_format),
1206            ] {
1207                if value > isize::MAX as usize {
1208                    return Err(IOSurfaceMetalError::IntegerOutOfRange { field, value });
1209                }
1210            }
1211            let p = unsafe {
1212                ffi::am_device_new_texture_from_iosurface(
1213                    device.as_ptr(),
1214                    self.as_ptr().cast::<c_void>(),
1215                    plane_index,
1216                    format.pixel_format,
1217                    width,
1218                    height,
1219                )
1220            };
1221            if p.is_null() {
1222                Err(IOSurfaceMetalError::NativeCreationFailed)
1223            } else {
1224                Ok(unsafe { MetalTexture::from_raw(p) })
1225            }
1226        }
1227    }
1228
1229    fn pixel_format_for_fourcc(
1230        fourcc: u32,
1231        plane_index: usize,
1232    ) -> Result<PlaneTextureFormat, IOSurfaceMetalError> {
1233        const BGRA: u32 = u32::from_be_bytes(*b"BGRA");
1234        const YUV420V: u32 = u32::from_be_bytes(*b"420v");
1235        const YUV420F: u32 = u32::from_be_bytes(*b"420f");
1236
1237        match (fourcc, plane_index) {
1238            (BGRA, 0) => Ok(PlaneTextureFormat {
1239                pixel_format: pixel_format::BGRA8UNORM,
1240                bytes_per_pixel: 4,
1241            }),
1242            (YUV420V | YUV420F, 0) => Ok(PlaneTextureFormat {
1243                pixel_format: pixel_format::R8UNORM,
1244                bytes_per_pixel: 1,
1245            }),
1246            (YUV420V | YUV420F, 1) => Ok(PlaneTextureFormat {
1247                pixel_format: pixel_format::RG8UNORM,
1248                bytes_per_pixel: 2,
1249            }),
1250            _ => Err(IOSurfaceMetalError::UnsupportedPixelFormat {
1251                fourcc,
1252                plane_index,
1253            }),
1254        }
1255    }
1256
1257    #[cfg(test)]
1258    mod tests {
1259        use super::*;
1260
1261        #[test]
1262        fn packed_ten_bit_surface_is_not_guessed() {
1263            let fourcc = u32::from_be_bytes(*b"l10r");
1264            assert!(matches!(
1265                pixel_format_for_fourcc(fourcc, 0),
1266                Err(IOSurfaceMetalError::UnsupportedPixelFormat { .. })
1267            ));
1268        }
1269
1270        #[test]
1271        fn odd_chroma_plane_uses_two_bytes_per_actual_plane_pixel() {
1272            let fourcc = u32::from_be_bytes(*b"420v");
1273            let format = pixel_format_for_fourcc(fourcc, 1).expect("chroma format");
1274            assert_eq!(format.pixel_format, pixel_format::RG8UNORM);
1275            assert_eq!(3 * format.bytes_per_pixel, 6);
1276        }
1277    }
1278}
1279
1280/// Re-exports the `Metal` framework surface for this item.
1281#[cfg(feature = "iosurface")]
1282pub use iosurface_ext::{IOSurfaceMetalError, IOSurfaceMetalExt};
1283
1284/// True if `fourcc` identifies a YCbCr biplanar (`Y` + `CbCr`) format.
1285#[must_use]
1286pub const fn is_ycbcr_biplanar(fourcc: u32) -> bool {
1287    const YUV420V: u32 = u32::from_be_bytes(*b"420v");
1288    const YUV420F: u32 = u32::from_be_bytes(*b"420f");
1289    matches!(fourcc, YUV420V | YUV420F)
1290}