Skip to main content

apple_metal/
advanced.rs

1use crate::{
2    ffi,
3    util::{c_string, take_optional_string, take_string},
4    ArgumentEncoder, CommandQueue, ComputePipelineState, MetalBuffer, MetalBufferAccessError,
5    MetalDevice, MetalFunction, MetalTexture, TextureDescriptor,
6};
7use core::ffi::c_void;
8use core::ops::Range;
9use std::path::Path;
10
11macro_rules! opaque_handle {
12    ($(#[$meta:meta])* pub struct $name:ident;) => {
13        $(#[$meta])*
14/// Mirrors the `Metal` framework counterpart for this type.
15        pub struct $name {
16            ptr: *mut c_void,
17        }
18
19        // SAFETY: Metal ObjC objects use atomic reference counting and are safe
20        // to move across threads.  All `&self` methods on these types either read
21        // immutable state or call ObjC methods documented as thread-safe by Apple.
22        unsafe impl Send for $name {}
23        unsafe impl Sync for $name {}
24
25        impl Drop for $name {
26            fn drop(&mut self) {
27                if !self.ptr.is_null() {
28                    // SAFETY: `ptr` is a non-null, +1-retained ObjC handle
29                    // exclusively owned by this struct.  Setting it to null
30                    // immediately prevents any subsequent release.
31                    unsafe { ffi::am_object_release(self.ptr) };
32                    self.ptr = core::ptr::null_mut();
33                }
34            }
35        }
36
37        impl $name {
38/// Mirrors the `Metal` framework constant `fn`.
39            #[must_use]
40            pub const fn as_ptr(&self) -> *mut c_void {
41                self.ptr
42            }
43
44            fn wrap(ptr: *mut c_void) -> Option<Self> {
45                if ptr.is_null() {
46                    None
47                } else {
48                    Some(Self { ptr })
49                }
50            }
51        }
52    };
53}
54
55/// `MTLIndirectCommandType` bit values.
56pub mod indirect_command_type {
57    /// Mirrors the `Metal` framework constant `DRAW`.
58    pub const DRAW: usize = 1 << 0;
59    /// Mirrors the `Metal` framework constant `DRAW_INDEXED`.
60    pub const DRAW_INDEXED: usize = 1 << 1;
61    /// Mirrors the `Metal` framework constant `CONCURRENT_DISPATCH`.
62    pub const CONCURRENT_DISPATCH: usize = 1 << 5;
63    /// Mirrors the `Metal` framework constant `CONCURRENT_DISPATCH_THREADS`.
64    pub const CONCURRENT_DISPATCH_THREADS: usize = 1 << 6;
65}
66
67/// `MTLCounterSamplingPoint` enum values.
68pub mod counter_sampling_point {
69    /// Mirrors the `Metal` framework constant `AT_STAGE_BOUNDARY`.
70    pub const AT_STAGE_BOUNDARY: usize = 0;
71    /// Mirrors the `Metal` framework constant `AT_DRAW_BOUNDARY`.
72    pub const AT_DRAW_BOUNDARY: usize = 1;
73    /// Mirrors the `Metal` framework constant `AT_DISPATCH_BOUNDARY`.
74    pub const AT_DISPATCH_BOUNDARY: usize = 2;
75    /// Mirrors the `Metal` framework constant `AT_TILE_DISPATCH_BOUNDARY`.
76    pub const AT_TILE_DISPATCH_BOUNDARY: usize = 3;
77    /// Mirrors the `Metal` framework constant `AT_BLIT_BOUNDARY`.
78    pub const AT_BLIT_BOUNDARY: usize = 4;
79}
80
81/// `MTLLogLevel` enum values.
82pub mod log_level {
83    /// Mirrors the `Metal` framework constant `UNDEFINED`.
84    pub const UNDEFINED: usize = 0;
85    /// Mirrors the `Metal` framework constant `DEBUG`.
86    pub const DEBUG: usize = 1;
87    /// Mirrors the `Metal` framework constant `INFO`.
88    pub const INFO: usize = 2;
89    /// Mirrors the `Metal` framework constant `NOTICE`.
90    pub const NOTICE: usize = 3;
91    /// Mirrors the `Metal` framework constant `ERROR`.
92    pub const ERROR: usize = 4;
93    /// Mirrors the `Metal` framework constant `FAULT`.
94    pub const FAULT: usize = 5;
95}
96
97/// `MTLPurgeableState` enum values.
98pub mod purgeable_state {
99    /// Mirrors the `Metal` framework constant `KEEP_CURRENT`.
100    pub const KEEP_CURRENT: usize = 1;
101    /// Mirrors the `Metal` framework constant `NON_VOLATILE`.
102    pub const NON_VOLATILE: usize = 2;
103    /// Mirrors the `Metal` framework constant `VOLATILE`.
104    pub const VOLATILE: usize = 3;
105    /// Mirrors the `Metal` framework constant `EMPTY`.
106    pub const EMPTY: usize = 4;
107}
108
109/// `MTLCaptureDestination` enum values.
110pub mod capture_destination {
111    /// Mirrors the `Metal` framework constant `DEVELOPER_TOOLS`.
112    pub const DEVELOPER_TOOLS: usize = 1;
113    /// Mirrors the `Metal` framework constant `GPU_TRACE_DOCUMENT`.
114    pub const GPU_TRACE_DOCUMENT: usize = 2;
115}
116
117/// `MTLIntersectionFunctionSignature` bit values.
118pub mod intersection_function_signature {
119    /// Mirrors the `Metal` framework constant `NONE`.
120    pub const NONE: usize = 0;
121    /// Mirrors the `Metal` framework constant `INSTANCING`.
122    pub const INSTANCING: usize = 1 << 0;
123    /// Mirrors the `Metal` framework constant `TRIANGLE_DATA`.
124    pub const TRIANGLE_DATA: usize = 1 << 1;
125    /// Mirrors the `Metal` framework constant `WORLD_SPACE_DATA`.
126    pub const WORLD_SPACE_DATA: usize = 1 << 2;
127}
128
129opaque_handle!(
130    /// Apple's `id<MTLHeap>` — shared GPU allocation arena.
131    pub struct Heap;
132);
133opaque_handle!(
134    /// Apple's `id<MTLEvent>` backed by `MTLSharedEvent`.
135    pub struct Event;
136);
137opaque_handle!(
138    /// Apple's `id<MTLFence>` — intra-queue synchronization primitive.
139    pub struct Fence;
140);
141opaque_handle!(
142    /// Apple's `id<MTLDynamicLibrary>` — device-linked Metal code bundle.
143    pub struct DynamicLibrary;
144);
145opaque_handle!(
146    /// Apple's `id<MTLBinaryArchive>` — persistent pipeline cache.
147    pub struct BinaryArchive;
148);
149opaque_handle!(
150    /// Apple's `id<MTLIndirectCommandBuffer>` — stores GPU-executable commands.
151    pub struct IndirectCommandBuffer;
152);
153opaque_handle!(
154    /// Apple's `id<MTLAccelerationStructure>` — storage for ray tracing data.
155    pub struct AccelerationStructure;
156);
157opaque_handle!(
158    /// Apple's `id<MTLIntersectionFunctionTable>` — table of ray intersection functions.
159    pub struct IntersectionFunctionTable;
160);
161opaque_handle!(
162    /// Apple's `id<MTLVisibleFunctionTable>` — table of callable function handles.
163    pub struct VisibleFunctionTable;
164);
165opaque_handle!(
166    /// Apple's `id<MTLCounterSampleBuffer>` — storage for GPU counter samples.
167    pub struct CounterSampleBuffer;
168);
169opaque_handle!(
170    /// Apple's `id<MTLLogState>` — shader logging configuration.
171    pub struct LogState;
172);
173opaque_handle!(
174    /// Apple's `id<MTLResidencySet>` — explicit residency tracking set.
175    pub struct ResidencySet;
176);
177opaque_handle!(
178    /// Apple's `MTLCaptureManager` singleton.
179    pub struct CaptureManager;
180);
181opaque_handle!(
182    /// Apple's `id<MTLCaptureScope>` — named capture region.
183    pub struct CaptureScope;
184);
185
186impl MetalDevice {
187    /// Human-readable device name.
188    #[must_use]
189    pub fn name(&self) -> String {
190        unsafe { take_string(ffi::am_device_name(self.as_ptr())) }
191    }
192
193    /// Global `IORegistry` identifier for the device.
194    #[must_use]
195    pub fn registry_id(&self) -> u64 {
196        unsafe { ffi::am_device_registry_id(self.as_ptr()) }
197    }
198
199    /// Whether this device supports Metal dynamic libraries.
200    #[must_use]
201    pub fn supports_dynamic_libraries(&self) -> bool {
202        unsafe { ffi::am_device_supports_dynamic_libraries(self.as_ptr()) }
203    }
204
205    /// Whether this device supports render-stage dynamic libraries.
206    #[must_use]
207    pub fn supports_render_dynamic_libraries(&self) -> bool {
208        unsafe { ffi::am_device_supports_render_dynamic_libraries(self.as_ptr()) }
209    }
210
211    /// Whether this device supports compute ray tracing.
212    #[must_use]
213    pub fn supports_raytracing(&self) -> bool {
214        unsafe { ffi::am_device_supports_raytracing(self.as_ptr()) }
215    }
216
217    /// Query support for a hardware counter sampling point.
218    #[must_use]
219    pub fn supports_counter_sampling(&self, sampling_point: usize) -> bool {
220        unsafe { ffi::am_device_supports_counter_sampling(self.as_ptr(), sampling_point) }
221    }
222
223    /// Return the names of all counter sets exposed by this device.
224    #[must_use]
225    pub fn counter_set_names(&self) -> Vec<String> {
226        let count = unsafe { ffi::am_device_counter_set_count(self.as_ptr()) };
227        (0..count)
228            .filter_map(|index| unsafe {
229                take_optional_string(ffi::am_device_counter_set_name_at(self.as_ptr(), index))
230            })
231            .collect()
232    }
233
234    /// Create a command queue with an explicit maximum in-flight command-buffer count.
235    #[must_use]
236    pub fn new_command_queue_with_max_command_buffer_count(
237        &self,
238        max_command_buffer_count: usize,
239    ) -> Option<CommandQueue> {
240        let ptr = unsafe {
241            ffi::am_device_new_command_queue_with_max_command_buffer_count(
242                self.as_ptr(),
243                max_command_buffer_count,
244            )
245        };
246        if ptr.is_null() {
247            None
248        } else {
249            Some(unsafe { CommandQueue::from_retained_ptr(ptr) })
250        }
251    }
252
253    /// Create a command queue that uses `log_state` for shader logging.
254    #[must_use]
255    pub fn new_command_queue_with_log_state(
256        &self,
257        max_command_buffer_count: usize,
258        log_state: &LogState,
259    ) -> Option<CommandQueue> {
260        let ptr = unsafe {
261            ffi::am_device_new_command_queue_with_log_state(
262                self.as_ptr(),
263                max_command_buffer_count,
264                log_state.as_ptr(),
265            )
266        };
267        if ptr.is_null() {
268            None
269        } else {
270            Some(unsafe { CommandQueue::from_retained_ptr(ptr) })
271        }
272    }
273
274    /// Create a heap with the requested size and storage mode.
275    #[must_use]
276    pub fn new_heap(&self, size: usize, storage_mode: usize) -> Option<Heap> {
277        Heap::wrap(unsafe { ffi::am_device_new_heap(self.as_ptr(), size, storage_mode) })
278    }
279
280    /// Create a new fence.
281    #[must_use]
282    pub fn new_fence(&self) -> Option<Fence> {
283        Fence::wrap(unsafe { ffi::am_device_new_fence(self.as_ptr()) })
284    }
285
286    /// Create a new shared event.
287    #[must_use]
288    pub fn new_shared_event(&self) -> Option<Event> {
289        Event::wrap(unsafe { ffi::am_device_new_shared_event(self.as_ptr()) })
290    }
291
292    /// Compile `source` as a Metal dynamic library with the given `install_name`.
293    ///
294    /// # Errors
295    ///
296    /// Returns Metal's localized compiler or linker error on failure.
297    pub fn new_dynamic_library_with_source(
298        &self,
299        source: &str,
300        install_name: &str,
301    ) -> Result<DynamicLibrary, String> {
302        let source = c_string(source)?;
303        let install_name = c_string(install_name)?;
304        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
305        let ptr = unsafe {
306            ffi::am_device_new_dynamic_library_with_source(
307                self.as_ptr(),
308                source.as_ptr(),
309                install_name.as_ptr(),
310                &mut err,
311            )
312        };
313        DynamicLibrary::wrap(ptr).ok_or_else(|| unsafe {
314            take_optional_string(err)
315                .unwrap_or_else(|| "MTLDevice.makeDynamicLibrary(source:) returned nil".to_string())
316        })
317    }
318
319    /// Load a serialized Metal dynamic library from `path`.
320    ///
321    /// # Errors
322    ///
323    /// Returns Metal's localized file or linker error on failure.
324    pub fn load_dynamic_library(&self, path: &Path) -> Result<DynamicLibrary, String> {
325        let path = c_string(path.to_string_lossy().as_ref())?;
326        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
327        let ptr = unsafe {
328            ffi::am_device_new_dynamic_library_with_url(self.as_ptr(), path.as_ptr(), &mut err)
329        };
330        DynamicLibrary::wrap(ptr).ok_or_else(|| unsafe {
331            take_optional_string(err)
332                .unwrap_or_else(|| "MTLDevice.makeDynamicLibrary(URL:) returned nil".to_string())
333        })
334    }
335
336    /// Create a binary archive, optionally loading it from `path` first.
337    ///
338    /// # Errors
339    ///
340    /// Returns Metal's localized archive creation error on failure.
341    pub fn new_binary_archive(&self, path: Option<&Path>) -> Result<BinaryArchive, String> {
342        let owned_path = path
343            .map(|path| c_string(path.to_string_lossy().as_ref()))
344            .transpose()?;
345        let raw_path = owned_path
346            .as_ref()
347            .map_or(core::ptr::null(), |path| path.as_c_str().as_ptr());
348        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
349        let ptr = unsafe { ffi::am_device_new_binary_archive(self.as_ptr(), raw_path, &mut err) };
350        BinaryArchive::wrap(ptr).ok_or_else(|| unsafe {
351            take_optional_string(err)
352                .unwrap_or_else(|| "MTLDevice.makeBinaryArchive returned nil".to_string())
353        })
354    }
355
356    /// Create a new indirect command buffer.
357    #[must_use]
358    pub fn new_indirect_command_buffer(
359        &self,
360        command_types: usize,
361        max_command_count: usize,
362        max_vertex_buffer_bind_count: usize,
363        max_fragment_buffer_bind_count: usize,
364        max_kernel_buffer_bind_count: usize,
365        options: usize,
366    ) -> Option<IndirectCommandBuffer> {
367        IndirectCommandBuffer::wrap(unsafe {
368            ffi::am_device_new_indirect_command_buffer(
369                self.as_ptr(),
370                command_types,
371                max_command_count,
372                max_vertex_buffer_bind_count,
373                max_fragment_buffer_bind_count,
374                max_kernel_buffer_bind_count,
375                options,
376            )
377        })
378    }
379
380    /// Allocate storage for a ray-tracing acceleration structure.
381    #[must_use]
382    pub fn new_acceleration_structure_with_size(
383        &self,
384        size: usize,
385    ) -> Option<AccelerationStructure> {
386        AccelerationStructure::wrap(unsafe {
387            ffi::am_device_new_acceleration_structure_with_size(self.as_ptr(), size)
388        })
389    }
390
391    /// Create a counter sample buffer for the named counter set.
392    ///
393    /// # Errors
394    ///
395    /// Returns Metal's localized counter-sample-buffer error on failure.
396    pub fn new_counter_sample_buffer(
397        &self,
398        counter_set_name: &str,
399        sample_count: usize,
400        storage_mode: usize,
401        label: Option<&str>,
402    ) -> Result<CounterSampleBuffer, String> {
403        let counter_set_name = c_string(counter_set_name)?;
404        let label = label.map(c_string).transpose()?;
405        let raw_label = label
406            .as_ref()
407            .map_or(core::ptr::null(), |label| label.as_c_str().as_ptr());
408        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
409        let ptr = unsafe {
410            ffi::am_device_new_counter_sample_buffer(
411                self.as_ptr(),
412                counter_set_name.as_ptr(),
413                sample_count,
414                storage_mode,
415                raw_label,
416                &mut err,
417            )
418        };
419        CounterSampleBuffer::wrap(ptr).ok_or_else(|| unsafe {
420            take_optional_string(err)
421                .unwrap_or_else(|| "MTLDevice.makeCounterSampleBuffer returned nil".to_string())
422        })
423    }
424
425    /// Create a shader log state.
426    ///
427    /// # Errors
428    ///
429    /// Returns Metal's localized log-state creation error on failure.
430    pub fn new_log_state(&self, level: usize, buffer_size: isize) -> Result<LogState, String> {
431        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
432        let ptr =
433            unsafe { ffi::am_device_new_log_state(self.as_ptr(), level, buffer_size, &mut err) };
434        LogState::wrap(ptr).ok_or_else(|| unsafe {
435            take_optional_string(err)
436                .unwrap_or_else(|| "MTLDevice.makeLogState returned nil".to_string())
437        })
438    }
439
440    /// Create a residency set.
441    ///
442    /// # Errors
443    ///
444    /// Returns Metal's localized residency-set creation error on failure.
445    pub fn new_residency_set(
446        &self,
447        label: Option<&str>,
448        initial_capacity: usize,
449    ) -> Result<ResidencySet, String> {
450        let label = label.map(c_string).transpose()?;
451        let raw_label = label
452            .as_ref()
453            .map_or(core::ptr::null(), |label| label.as_c_str().as_ptr());
454        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
455        let ptr = unsafe {
456            ffi::am_device_new_residency_set(self.as_ptr(), raw_label, initial_capacity, &mut err)
457        };
458        ResidencySet::wrap(ptr).ok_or_else(|| unsafe {
459            take_optional_string(err)
460                .unwrap_or_else(|| "MTLDevice.makeResidencySet returned nil".to_string())
461        })
462    }
463}
464
465impl CommandQueue {
466    /// Add `residency_set` to the queue-wide residency list.
467    pub fn add_residency_set(&self, residency_set: &ResidencySet) {
468        unsafe { ffi::am_command_queue_add_residency_set(self.as_ptr(), residency_set.as_ptr()) };
469    }
470
471    /// Remove `residency_set` from the queue-wide residency list.
472    pub fn remove_residency_set(&self, residency_set: &ResidencySet) {
473        unsafe {
474            ffi::am_command_queue_remove_residency_set(self.as_ptr(), residency_set.as_ptr());
475        };
476    }
477}
478
479impl MetalBuffer {
480    /// Notify Metal that CPU writes modified the given managed-memory byte range.
481    ///
482    /// # Errors
483    ///
484    /// Returns an error for non-managed storage or an invalid byte range.
485    pub fn did_modify_range(&self, range: Range<usize>) -> Result<(), MetalBufferAccessError> {
486        if range.start > range.end {
487            return Err(MetalBufferAccessError::InvalidRange);
488        }
489        let storage_mode = self.storage_mode();
490        if storage_mode != crate::storage_mode::MANAGED {
491            return Err(MetalBufferAccessError::ManagedStorageRequired { storage_mode });
492        }
493        let length = range.end - range.start;
494        self.checked_range_end(range.start, length)?;
495        unsafe {
496            ffi::am_buffer_did_modify_range(self.as_ptr(), range.start, length);
497        };
498        Ok(())
499    }
500
501    /// Create a 2D texture view that shares this buffer's storage.
502    #[must_use]
503    pub fn new_texture_view_2d(
504        &self,
505        pixel_format: usize,
506        width: usize,
507        height: usize,
508        bytes_per_row: usize,
509        offset: usize,
510    ) -> Option<MetalTexture> {
511        let ptr = unsafe {
512            ffi::am_buffer_new_texture_view_2d(
513                self.as_ptr(),
514                pixel_format,
515                width,
516                height,
517                bytes_per_row,
518                offset,
519            )
520        };
521        if ptr.is_null() {
522            None
523        } else {
524            Some(unsafe { MetalTexture::from_raw(ptr) })
525        }
526    }
527}
528
529/// Errors returned by CPU texture uploads and readback.
530#[derive(Debug, Clone, PartialEq, Eq)]
531pub enum TextureTransferError {
532    /// The texture format does not have a supported CPU byte layout.
533    UnsupportedPixelFormat { pixel_format: usize },
534    /// The texture storage mode cannot be accessed through CPU transfer APIs.
535    CpuInaccessibleStorage { storage_mode: usize },
536    /// The requested mipmap level does not exist.
537    InvalidMipmapLevel {
538        mipmap_level: usize,
539        mipmap_level_count: usize,
540    },
541    /// The requested array slice does not exist.
542    InvalidSlice { slice: usize, array_length: usize },
543    /// A zero-sized transfer region was requested.
544    EmptyRegion,
545    /// A two-dimensional transfer was requested for a depth texture.
546    UnsupportedDepth { depth: usize },
547    /// The texture kind is not compatible with a two-dimensional CPU transfer.
548    UnsupportedTextureType { texture_type: usize },
549    /// The region exceeds the selected mip level.
550    RegionOutOfBounds {
551        origin: (usize, usize),
552        size: (usize, usize),
553        mip_size: (usize, usize),
554    },
555    /// A compressed-format coordinate is not block aligned.
556    BlockMisaligned {
557        field: &'static str,
558        value: usize,
559        block_size: usize,
560    },
561    /// The row stride is shorter than the format requires.
562    BytesPerRowTooSmall {
563        bytes_per_row: usize,
564        minimum: usize,
565    },
566    /// The row stride is not aligned to a complete format block.
567    BytesPerRowMisaligned {
568        bytes_per_row: usize,
569        bytes_per_block: usize,
570    },
571    /// Checked layout arithmetic overflowed.
572    LayoutOverflow,
573    /// A value cannot be represented by the native API.
574    IntegerOutOfRange { field: &'static str, value: usize },
575    /// The source or destination byte slice is too short.
576    BufferTooShort { actual: usize, required: usize },
577    /// The native bridge rejected a validated transfer.
578    NativeRejected,
579}
580
581impl core::fmt::Display for TextureTransferError {
582    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
583        match self {
584            Self::UnsupportedPixelFormat { pixel_format } => {
585                write!(
586                    formatter,
587                    "pixel format {pixel_format} has no supported CPU layout"
588                )
589            }
590            Self::CpuInaccessibleStorage { storage_mode } => {
591                write!(
592                    formatter,
593                    "storage mode {storage_mode} is not CPU-accessible"
594                )
595            }
596            Self::InvalidMipmapLevel {
597                mipmap_level,
598                mipmap_level_count,
599            } => write!(
600                formatter,
601                "mipmap level {mipmap_level} is outside 0..{mipmap_level_count}"
602            ),
603            Self::InvalidSlice {
604                slice,
605                array_length,
606            } => write!(
607                formatter,
608                "texture slice {slice} is outside 0..{array_length}"
609            ),
610            Self::EmptyRegion => formatter.write_str("texture transfer region is empty"),
611            Self::UnsupportedDepth { depth } => {
612                write!(
613                    formatter,
614                    "2D texture transfer does not support depth {depth}"
615                )
616            }
617            Self::UnsupportedTextureType { texture_type } => {
618                write!(
619                    formatter,
620                    "texture type {texture_type} is not a supported 2D transfer"
621                )
622            }
623            Self::RegionOutOfBounds {
624                origin,
625                size,
626                mip_size,
627            } => write!(
628                formatter,
629                "region {origin:?} + {size:?} exceeds mip dimensions {mip_size:?}"
630            ),
631            Self::BlockMisaligned {
632                field,
633                value,
634                block_size,
635            } => write!(
636                formatter,
637                "{field} value {value} is not aligned to block size {block_size}"
638            ),
639            Self::BytesPerRowTooSmall {
640                bytes_per_row,
641                minimum,
642            } => write!(
643                formatter,
644                "bytes_per_row {bytes_per_row} is smaller than required {minimum}"
645            ),
646            Self::BytesPerRowMisaligned {
647                bytes_per_row,
648                bytes_per_block,
649            } => write!(
650                formatter,
651                "bytes_per_row {bytes_per_row} is not aligned to {bytes_per_block}-byte blocks"
652            ),
653            Self::LayoutOverflow => formatter.write_str("texture byte layout overflowed"),
654            Self::IntegerOutOfRange { field, value } => {
655                write!(formatter, "{field} value {value} exceeds native Int")
656            }
657            Self::BufferTooShort { actual, required } => write!(
658                formatter,
659                "byte slice length {actual} is shorter than required {required}"
660            ),
661            Self::NativeRejected => formatter.write_str("Metal rejected the texture transfer"),
662        }
663    }
664}
665
666impl std::error::Error for TextureTransferError {}
667
668#[derive(Clone, Copy)]
669struct TextureTransferMetadata {
670    width: usize,
671    height: usize,
672    depth: usize,
673    mipmap_level_count: usize,
674    array_length: usize,
675    pixel_format: usize,
676    texture_type: usize,
677    storage_mode: usize,
678}
679
680#[derive(Clone, Copy)]
681struct PixelFormatLayout {
682    block_width: usize,
683    block_height: usize,
684    bytes_per_block: usize,
685}
686
687#[allow(clippy::missing_errors_doc)]
688impl MetalTexture {
689    /// Texture depth in pixels.
690    #[must_use]
691    pub fn depth(&self) -> usize {
692        unsafe { ffi::am_texture_depth(self.as_ptr()) }
693    }
694
695    /// Number of mipmap levels.
696    #[must_use]
697    pub fn mipmap_level_count(&self) -> usize {
698        unsafe { ffi::am_texture_mipmap_level_count(self.as_ptr()) }
699    }
700
701    /// Number of array slices.
702    #[must_use]
703    pub fn array_length(&self) -> usize {
704        unsafe { ffi::am_texture_array_length(self.as_ptr()) }
705    }
706
707    /// `MTLTextureUsage` bitmask.
708    #[must_use]
709    pub fn usage(&self) -> usize {
710        unsafe { ffi::am_texture_usage(self.as_ptr()) }
711    }
712
713    /// `MTLStorageMode` enum value.
714    #[must_use]
715    pub fn storage_mode(&self) -> usize {
716        unsafe { ffi::am_texture_storage_mode(self.as_ptr()) }
717    }
718
719    /// Upload bytes into slice zero of a 2D texture region.
720    ///
721    /// # Safety
722    ///
723    /// No GPU access or CPU/native alias access may overlap this transfer. For
724    /// buffer-backed textures, this includes mappings of the backing buffer.
725    pub unsafe fn replace_region_2d(
726        &self,
727        bytes: &[u8],
728        bytes_per_row: usize,
729        origin: (usize, usize),
730        size: (usize, usize),
731        mipmap_level: usize,
732    ) -> Result<(), TextureTransferError> {
733        unsafe {
734            self.replace_region_2d_at_slice(bytes, bytes_per_row, origin, size, mipmap_level, 0)
735        }
736    }
737
738    /// Upload bytes into a selected array slice of a 2D texture region.
739    ///
740    /// # Safety
741    ///
742    /// No GPU access or CPU/native alias access may overlap this transfer. For
743    /// buffer-backed textures, this includes mappings of the backing buffer.
744    pub unsafe fn replace_region_2d_at_slice(
745        &self,
746        bytes: &[u8],
747        bytes_per_row: usize,
748        origin: (usize, usize),
749        size: (usize, usize),
750        mipmap_level: usize,
751        slice: usize,
752    ) -> Result<(), TextureTransferError> {
753        validate_texture_transfer(
754            self.transfer_metadata(),
755            bytes.len(),
756            bytes_per_row,
757            origin,
758            size,
759            mipmap_level,
760            slice,
761        )?;
762        let accepted = unsafe {
763            ffi::am_texture_replace_region_2d(
764                self.as_ptr(),
765                origin.0,
766                origin.1,
767                size.0,
768                size.1,
769                mipmap_level,
770                slice,
771                bytes.as_ptr(),
772                bytes.len(),
773                bytes_per_row,
774            )
775        };
776        if accepted {
777            Ok(())
778        } else {
779            Err(TextureTransferError::NativeRejected)
780        }
781    }
782
783    /// Read slice zero of a 2D texture region into `out`.
784    ///
785    /// # Safety
786    ///
787    /// No GPU write or CPU/native alias mutation may overlap this transfer.
788    /// Managed GPU writes require a completed resource synchronization first.
789    pub unsafe fn read_bytes_2d(
790        &self,
791        out: &mut [u8],
792        bytes_per_row: usize,
793        origin: (usize, usize),
794        size: (usize, usize),
795        mipmap_level: usize,
796    ) -> Result<(), TextureTransferError> {
797        unsafe { self.read_bytes_2d_at_slice(out, bytes_per_row, origin, size, mipmap_level, 0) }
798    }
799
800    /// Read a selected array slice of a 2D texture region into `out`.
801    ///
802    /// # Safety
803    ///
804    /// No GPU write or CPU/native alias mutation may overlap this transfer.
805    /// Managed GPU writes require a completed resource synchronization first.
806    pub unsafe fn read_bytes_2d_at_slice(
807        &self,
808        out: &mut [u8],
809        bytes_per_row: usize,
810        origin: (usize, usize),
811        size: (usize, usize),
812        mipmap_level: usize,
813        slice: usize,
814    ) -> Result<(), TextureTransferError> {
815        validate_texture_transfer(
816            self.transfer_metadata(),
817            out.len(),
818            bytes_per_row,
819            origin,
820            size,
821            mipmap_level,
822            slice,
823        )?;
824        let accepted = unsafe {
825            ffi::am_texture_get_bytes_2d(
826                self.as_ptr(),
827                out.as_mut_ptr(),
828                out.len(),
829                bytes_per_row,
830                origin.0,
831                origin.1,
832                size.0,
833                size.1,
834                mipmap_level,
835                slice,
836            )
837        };
838        if accepted {
839            Ok(())
840        } else {
841            Err(TextureTransferError::NativeRejected)
842        }
843    }
844
845    /// Create a texture view with a compatible pixel format.
846    #[must_use]
847    pub fn new_view(&self, pixel_format: usize) -> Option<Self> {
848        let ptr = unsafe { ffi::am_texture_new_view(self.as_ptr(), pixel_format) };
849        if ptr.is_null() {
850            None
851        } else {
852            Some(unsafe { Self::from_raw(ptr) })
853        }
854    }
855
856    fn transfer_metadata(&self) -> TextureTransferMetadata {
857        TextureTransferMetadata {
858            width: self.width(),
859            height: self.height(),
860            depth: self.depth(),
861            mipmap_level_count: self.mipmap_level_count(),
862            array_length: self.array_length(),
863            pixel_format: self.pixel_format(),
864            texture_type: self.texture_type(),
865            storage_mode: self.storage_mode(),
866        }
867    }
868}
869
870#[allow(clippy::too_many_lines)]
871fn validate_texture_transfer(
872    metadata: TextureTransferMetadata,
873    byte_length: usize,
874    bytes_per_row: usize,
875    origin: (usize, usize),
876    size: (usize, usize),
877    mipmap_level: usize,
878    slice: usize,
879) -> Result<(), TextureTransferError> {
880    if !matches!(
881        metadata.storage_mode,
882        crate::storage_mode::SHARED | crate::storage_mode::MANAGED
883    ) {
884        return Err(TextureTransferError::CpuInaccessibleStorage {
885            storage_mode: metadata.storage_mode,
886        });
887    }
888    if metadata.depth != 1 {
889        return Err(TextureTransferError::UnsupportedDepth {
890            depth: metadata.depth,
891        });
892    }
893    if mipmap_level >= metadata.mipmap_level_count {
894        return Err(TextureTransferError::InvalidMipmapLevel {
895            mipmap_level,
896            mipmap_level_count: metadata.mipmap_level_count,
897        });
898    }
899    let array_length = texture_slice_count(metadata)?;
900    if slice >= array_length {
901        return Err(TextureTransferError::InvalidSlice {
902            slice,
903            array_length,
904        });
905    }
906    if size.0 == 0 || size.1 == 0 {
907        return Err(TextureTransferError::EmptyRegion);
908    }
909    for (field, value) in [
910        ("bytes_per_row", bytes_per_row),
911        ("origin.x", origin.0),
912        ("origin.y", origin.1),
913        ("size.width", size.0),
914        ("size.height", size.1),
915        ("mipmap_level", mipmap_level),
916        ("slice", slice),
917        ("byte_length", byte_length),
918    ] {
919        ensure_texture_native_int(value, field)?;
920    }
921
922    let mip_size = (
923        mip_dimension(metadata.width, mipmap_level),
924        mip_dimension(metadata.height, mipmap_level),
925    );
926    let end_x = origin
927        .0
928        .checked_add(size.0)
929        .ok_or(TextureTransferError::RegionOutOfBounds {
930            origin,
931            size,
932            mip_size,
933        })?;
934    let end_y = origin
935        .1
936        .checked_add(size.1)
937        .ok_or(TextureTransferError::RegionOutOfBounds {
938            origin,
939            size,
940            mip_size,
941        })?;
942    if end_x > mip_size.0 || end_y > mip_size.1 {
943        return Err(TextureTransferError::RegionOutOfBounds {
944            origin,
945            size,
946            mip_size,
947        });
948    }
949
950    let layout = pixel_format_layout(metadata.pixel_format).ok_or(
951        TextureTransferError::UnsupportedPixelFormat {
952            pixel_format: metadata.pixel_format,
953        },
954    )?;
955    validate_block_alignment("origin.x", origin.0, layout.block_width)?;
956    validate_block_alignment("origin.y", origin.1, layout.block_height)?;
957    if end_x != mip_size.0 {
958        validate_block_alignment("size.width", size.0, layout.block_width)?;
959    }
960    if end_y != mip_size.1 {
961        validate_block_alignment("size.height", size.1, layout.block_height)?;
962    }
963
964    let blocks_per_row = checked_div_ceil(size.0, layout.block_width)?;
965    let block_rows = checked_div_ceil(size.1, layout.block_height)?;
966    let minimum_row_bytes = blocks_per_row
967        .checked_mul(layout.bytes_per_block)
968        .ok_or(TextureTransferError::LayoutOverflow)?;
969    if bytes_per_row < minimum_row_bytes {
970        return Err(TextureTransferError::BytesPerRowTooSmall {
971            bytes_per_row,
972            minimum: minimum_row_bytes,
973        });
974    }
975    if bytes_per_row % layout.bytes_per_block != 0 {
976        return Err(TextureTransferError::BytesPerRowMisaligned {
977            bytes_per_row,
978            bytes_per_block: layout.bytes_per_block,
979        });
980    }
981    let preceding_rows = bytes_per_row
982        .checked_mul(block_rows - 1)
983        .ok_or(TextureTransferError::LayoutOverflow)?;
984    let required = preceding_rows
985        .checked_add(minimum_row_bytes)
986        .ok_or(TextureTransferError::LayoutOverflow)?;
987    ensure_texture_native_int(required, "required byte length")?;
988    if byte_length < required {
989        return Err(TextureTransferError::BufferTooShort {
990            actual: byte_length,
991            required,
992        });
993    }
994    Ok(())
995}
996
997fn pixel_format_layout(pixel_format: usize) -> Option<PixelFormatLayout> {
998    use crate::pixel_format;
999
1000    let bytes_per_block = match pixel_format {
1001        pixel_format::A8UNORM
1002        | pixel_format::R8UNORM
1003        | pixel_format::R8SNORM
1004        | pixel_format::R8UINT
1005        | pixel_format::R8SINT => 1,
1006        pixel_format::R16UNORM
1007        | pixel_format::R16SNORM
1008        | pixel_format::R16UINT
1009        | pixel_format::R16SINT
1010        | pixel_format::R16FLOAT
1011        | pixel_format::RG8UNORM
1012        | pixel_format::RG8SNORM
1013        | pixel_format::RG8UINT
1014        | pixel_format::RG8SINT => 2,
1015        pixel_format::R32FLOAT
1016        | pixel_format::RG16FLOAT
1017        | pixel_format::RGBA8UNORM
1018        | pixel_format::RGBA8UNORM_SRGB
1019        | pixel_format::RGBA8SNORM
1020        | pixel_format::RGBA8UINT
1021        | pixel_format::RGBA8SINT
1022        | pixel_format::BGRA8UNORM
1023        | pixel_format::BGRA8UNORM_SRGB
1024        | pixel_format::BGRA10_XR
1025        | pixel_format::BGR10_XR => 4,
1026        pixel_format::RGBA16FLOAT => 8,
1027        pixel_format::RGBA32FLOAT => 16,
1028        _ => return None,
1029    };
1030    Some(PixelFormatLayout {
1031        block_width: 1,
1032        block_height: 1,
1033        bytes_per_block,
1034    })
1035}
1036
1037fn texture_slice_count(metadata: TextureTransferMetadata) -> Result<usize, TextureTransferError> {
1038    match metadata.texture_type {
1039        crate::texture_type::TYPE_2D => Ok(1),
1040        crate::texture_type::TYPE_2D_ARRAY => Ok(metadata.array_length.max(1)),
1041        crate::texture_type::CUBE => Ok(6),
1042        crate::texture_type::CUBE_ARRAY => metadata
1043            .array_length
1044            .max(1)
1045            .checked_mul(6)
1046            .ok_or(TextureTransferError::LayoutOverflow),
1047        texture_type => Err(TextureTransferError::UnsupportedTextureType { texture_type }),
1048    }
1049}
1050
1051fn mip_dimension(base: usize, mipmap_level: usize) -> usize {
1052    base.checked_shr(u32::try_from(mipmap_level).unwrap_or(u32::MAX))
1053        .unwrap_or(0)
1054        .max(1)
1055}
1056
1057fn checked_div_ceil(value: usize, divisor: usize) -> Result<usize, TextureTransferError> {
1058    value
1059        .checked_add(divisor - 1)
1060        .map(|adjusted| adjusted / divisor)
1061        .ok_or(TextureTransferError::LayoutOverflow)
1062}
1063
1064fn validate_block_alignment(
1065    field: &'static str,
1066    value: usize,
1067    block_size: usize,
1068) -> Result<(), TextureTransferError> {
1069    if value % block_size == 0 {
1070        Ok(())
1071    } else {
1072        Err(TextureTransferError::BlockMisaligned {
1073            field,
1074            value,
1075            block_size,
1076        })
1077    }
1078}
1079
1080fn ensure_texture_native_int(
1081    value: usize,
1082    field: &'static str,
1083) -> Result<(), TextureTransferError> {
1084    if isize::try_from(value).is_ok() {
1085        Ok(())
1086    } else {
1087        Err(TextureTransferError::IntegerOutOfRange { field, value })
1088    }
1089}
1090
1091impl ComputePipelineState {
1092    /// Thread execution width for this compute pipeline.
1093    #[must_use]
1094    pub fn thread_execution_width(&self) -> usize {
1095        unsafe { ffi::am_compute_pipeline_state_thread_execution_width(self.as_ptr()) }
1096    }
1097
1098    /// Maximum threads per threadgroup.
1099    #[must_use]
1100    pub fn max_total_threads_per_threadgroup(&self) -> usize {
1101        unsafe { ffi::am_compute_pipeline_state_max_total_threads_per_threadgroup(self.as_ptr()) }
1102    }
1103
1104    /// Allocate a visible function table for this pipeline.
1105    #[must_use]
1106    pub fn new_visible_function_table(
1107        &self,
1108        function_count: usize,
1109    ) -> Option<VisibleFunctionTable> {
1110        VisibleFunctionTable::wrap(unsafe {
1111            ffi::am_compute_pipeline_state_new_visible_function_table(self.as_ptr(), function_count)
1112        })
1113    }
1114
1115    /// Allocate an intersection function table for this pipeline.
1116    #[must_use]
1117    pub fn new_intersection_function_table(
1118        &self,
1119        function_count: usize,
1120    ) -> Option<IntersectionFunctionTable> {
1121        IntersectionFunctionTable::wrap(unsafe {
1122            ffi::am_compute_pipeline_state_new_intersection_function_table(
1123                self.as_ptr(),
1124                function_count,
1125            )
1126        })
1127    }
1128}
1129
1130impl MetalFunction {
1131    /// Create an argument encoder for the argument buffer bound at `buffer_index`.
1132    #[must_use]
1133    pub fn new_argument_encoder(&self, buffer_index: usize) -> Option<ArgumentEncoder> {
1134        let ptr = unsafe { ffi::am_function_new_argument_encoder(self.as_ptr(), buffer_index) };
1135        if ptr.is_null() {
1136            None
1137        } else {
1138            Some(unsafe { ArgumentEncoder::from_function_ptr(ptr) })
1139        }
1140    }
1141}
1142
1143impl Heap {
1144    /// Heap size in bytes.
1145    #[must_use]
1146    pub fn size(&self) -> usize {
1147        unsafe { ffi::am_heap_size(self.as_ptr()) }
1148    }
1149
1150    /// Bytes currently used by heap-backed resources.
1151    #[must_use]
1152    pub fn used_size(&self) -> usize {
1153        unsafe { ffi::am_heap_used_size(self.as_ptr()) }
1154    }
1155
1156    /// Current heap allocation size in bytes.
1157    #[must_use]
1158    pub fn current_allocated_size(&self) -> usize {
1159        unsafe { ffi::am_heap_current_allocated_size(self.as_ptr()) }
1160    }
1161
1162    /// Largest allocatable block in the heap for the given alignment.
1163    #[must_use]
1164    pub fn max_available_size(&self, alignment: usize) -> usize {
1165        unsafe { ffi::am_heap_max_available_size(self.as_ptr(), alignment) }
1166    }
1167
1168    /// Allocate a buffer from this heap.
1169    #[must_use]
1170    pub fn new_buffer(&self, length: usize, options: usize) -> Option<MetalBuffer> {
1171        let ptr = unsafe { ffi::am_heap_new_buffer(self.as_ptr(), length, options) };
1172        if ptr.is_null() {
1173            None
1174        } else {
1175            Some(unsafe { MetalBuffer::from_retained_ptr(ptr) })
1176        }
1177    }
1178
1179    /// Allocate a texture from this heap.
1180    #[must_use]
1181    pub fn new_texture(&self, descriptor: TextureDescriptor) -> Option<MetalTexture> {
1182        let ptr = unsafe {
1183            ffi::am_heap_new_texture_2d(
1184                self.as_ptr(),
1185                descriptor.pixel_format,
1186                descriptor.width,
1187                descriptor.height,
1188                descriptor.mipmapped,
1189                descriptor.usage,
1190                descriptor.storage_mode,
1191            )
1192        };
1193        if ptr.is_null() {
1194            None
1195        } else {
1196            Some(unsafe { MetalTexture::from_raw(ptr) })
1197        }
1198    }
1199
1200    /// Allocate acceleration-structure storage from this heap.
1201    #[must_use]
1202    pub fn new_acceleration_structure_with_size(
1203        &self,
1204        size: usize,
1205    ) -> Option<AccelerationStructure> {
1206        AccelerationStructure::wrap(unsafe {
1207            ffi::am_heap_new_acceleration_structure_with_size(self.as_ptr(), size)
1208        })
1209    }
1210
1211    /// Set the heap purgeable state.
1212    #[must_use]
1213    pub fn set_purgeable_state(&self, state: usize) -> usize {
1214        unsafe { ffi::am_heap_set_purgeable_state(self.as_ptr(), state) }
1215    }
1216}
1217
1218impl Event {
1219    /// Current `signaledValue`.
1220    #[must_use]
1221    pub fn signaled_value(&self) -> u64 {
1222        unsafe { ffi::am_event_signaled_value(self.as_ptr()) }
1223    }
1224
1225    /// Update the event's `signaledValue`.
1226    pub fn set_signaled_value(&self, value: u64) {
1227        unsafe { ffi::am_event_set_signaled_value(self.as_ptr(), value) };
1228    }
1229
1230    /// Wait until the event reaches at least `value`.
1231    #[must_use]
1232    pub fn wait_until_signaled_value(&self, value: u64, timeout_ms: u64) -> bool {
1233        unsafe { ffi::am_event_wait_until_signaled_value(self.as_ptr(), value, timeout_ms) }
1234    }
1235}
1236
1237impl DynamicLibrary {
1238    /// Install name embedded into the dynamic library.
1239    #[must_use]
1240    pub fn install_name(&self) -> String {
1241        unsafe { take_string(ffi::am_dynamic_library_install_name(self.as_ptr())) }
1242    }
1243
1244    /// Serialize this dynamic library to `path`.
1245    ///
1246    /// # Errors
1247    ///
1248    /// Returns Metal's localized serialization error on failure.
1249    pub fn serialize_to_file(&self, path: &Path) -> Result<(), String> {
1250        let path = c_string(path.to_string_lossy().as_ref())?;
1251        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
1252        let ok = unsafe {
1253            ffi::am_dynamic_library_serialize_to_url(self.as_ptr(), path.as_ptr(), &mut err)
1254        };
1255        if ok {
1256            Ok(())
1257        } else {
1258            Err(unsafe {
1259                take_optional_string(err)
1260                    .unwrap_or_else(|| "MTLDynamicLibrary.serialize(to:) failed".to_string())
1261            })
1262        }
1263    }
1264}
1265
1266impl BinaryArchive {
1267    /// Add a compute pipeline descriptor built from `function` to the archive.
1268    ///
1269    /// # Errors
1270    ///
1271    /// Returns Metal's localized archive error on failure.
1272    pub fn add_compute_function(&self, function: &MetalFunction) -> Result<(), String> {
1273        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
1274        let ok = unsafe {
1275            ffi::am_binary_archive_add_compute_function(self.as_ptr(), function.as_ptr(), &mut err)
1276        };
1277        if ok {
1278            Ok(())
1279        } else {
1280            Err(unsafe {
1281                take_optional_string(err).unwrap_or_else(|| {
1282                    "MTLBinaryArchive.addComputePipelineFunctions failed".to_string()
1283                })
1284            })
1285        }
1286    }
1287
1288    /// Add a render pipeline descriptor built from `vertex` and `fragment` to the archive.
1289    ///
1290    /// # Errors
1291    ///
1292    /// Returns Metal's localized archive error on failure.
1293    pub fn add_render_functions(
1294        &self,
1295        vertex: &MetalFunction,
1296        fragment: &MetalFunction,
1297        color_pixel_format: usize,
1298        sample_count: usize,
1299    ) -> Result<(), String> {
1300        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
1301        let ok = unsafe {
1302            ffi::am_binary_archive_add_render_functions(
1303                self.as_ptr(),
1304                vertex.as_ptr(),
1305                fragment.as_ptr(),
1306                color_pixel_format,
1307                sample_count,
1308                &mut err,
1309            )
1310        };
1311        if ok {
1312            Ok(())
1313        } else {
1314            Err(unsafe {
1315                take_optional_string(err).unwrap_or_else(|| {
1316                    "MTLBinaryArchive.addRenderPipelineFunctions failed".to_string()
1317                })
1318            })
1319        }
1320    }
1321
1322    /// Serialize the archive to `path`.
1323    ///
1324    /// # Errors
1325    ///
1326    /// Returns Metal's localized serialization error on failure.
1327    pub fn serialize_to_file(&self, path: &Path) -> Result<(), String> {
1328        let path = c_string(path.to_string_lossy().as_ref())?;
1329        let mut err: *mut core::ffi::c_char = core::ptr::null_mut();
1330        let ok = unsafe {
1331            ffi::am_binary_archive_serialize_to_url(self.as_ptr(), path.as_ptr(), &mut err)
1332        };
1333        if ok {
1334            Ok(())
1335        } else {
1336            Err(unsafe {
1337                take_optional_string(err)
1338                    .unwrap_or_else(|| "MTLBinaryArchive.serialize(to:) failed".to_string())
1339            })
1340        }
1341    }
1342}
1343
1344impl IndirectCommandBuffer {
1345    /// Size of the indirect command buffer in bytes.
1346    #[must_use]
1347    pub fn size(&self) -> usize {
1348        unsafe { ffi::am_indirect_command_buffer_size(self.as_ptr()) }
1349    }
1350
1351    /// Reset commands in `range` back to empty state.
1352    pub fn reset_range(&self, range: Range<usize>) {
1353        unsafe {
1354            ffi::am_indirect_command_buffer_reset_range(
1355                self.as_ptr(),
1356                range.start,
1357                range.end.saturating_sub(range.start),
1358            );
1359        };
1360    }
1361}
1362
1363impl AccelerationStructure {
1364    /// Allocated storage size in bytes.
1365    #[must_use]
1366    pub fn size(&self) -> usize {
1367        unsafe { ffi::am_acceleration_structure_size(self.as_ptr()) }
1368    }
1369}
1370
1371impl IntersectionFunctionTable {
1372    /// Populate `index` with the built-in opaque triangle intersection function.
1373    pub fn set_opaque_triangle_intersection_function(&self, signature: usize, index: usize) {
1374        unsafe {
1375            ffi::am_intersection_function_table_set_opaque_triangle(
1376                self.as_ptr(),
1377                signature,
1378                index,
1379            );
1380        };
1381    }
1382}
1383
1384impl CounterSampleBuffer {
1385    /// Number of samples available in the buffer.
1386    #[must_use]
1387    pub fn sample_count(&self) -> usize {
1388        unsafe { ffi::am_counter_sample_buffer_sample_count(self.as_ptr()) }
1389    }
1390
1391    /// Resolve raw counter bytes for `range`.
1392    #[must_use]
1393    pub fn resolve_range(&self, range: Range<usize>) -> Option<Vec<u8>> {
1394        let mut out_len = 0usize;
1395        let ptr = unsafe {
1396            ffi::am_counter_sample_buffer_resolve_range(
1397                self.as_ptr(),
1398                range.start,
1399                range.end.saturating_sub(range.start),
1400                &mut out_len,
1401            )
1402        };
1403        if ptr.is_null() {
1404            None
1405        } else {
1406            let bytes = unsafe { core::slice::from_raw_parts(ptr.cast::<u8>(), out_len) }.to_vec();
1407            unsafe { libc::free(ptr.cast()) };
1408            Some(bytes)
1409        }
1410    }
1411}
1412
1413impl ResidencySet {
1414    /// Add `buffer` to the set.
1415    pub fn add_buffer(&self, buffer: &MetalBuffer) {
1416        unsafe { ffi::am_residency_set_add_buffer(self.as_ptr(), buffer.as_ptr()) };
1417    }
1418
1419    /// Add `texture` to the set.
1420    pub fn add_texture(&self, texture: &MetalTexture) {
1421        unsafe { ffi::am_residency_set_add_texture(self.as_ptr(), texture.as_ptr()) };
1422    }
1423
1424    /// Add `heap` to the set.
1425    pub fn add_heap(&self, heap: &Heap) {
1426        unsafe { ffi::am_residency_set_add_heap(self.as_ptr(), heap.as_ptr()) };
1427    }
1428
1429    /// Remove `buffer` from the set.
1430    pub fn remove_buffer(&self, buffer: &MetalBuffer) {
1431        unsafe { ffi::am_residency_set_remove_buffer(self.as_ptr(), buffer.as_ptr()) };
1432    }
1433
1434    /// Remove `texture` from the set.
1435    pub fn remove_texture(&self, texture: &MetalTexture) {
1436        unsafe { ffi::am_residency_set_remove_texture(self.as_ptr(), texture.as_ptr()) };
1437    }
1438
1439    /// Remove `heap` from the set.
1440    pub fn remove_heap(&self, heap: &Heap) {
1441        unsafe { ffi::am_residency_set_remove_heap(self.as_ptr(), heap.as_ptr()) };
1442    }
1443
1444    /// Remove all pending and committed allocations.
1445    pub fn remove_all_allocations(&self) {
1446        unsafe { ffi::am_residency_set_remove_all_allocations(self.as_ptr()) };
1447    }
1448
1449    /// Whether the set currently contains `buffer`.
1450    #[must_use]
1451    pub fn contains_buffer(&self, buffer: &MetalBuffer) -> bool {
1452        unsafe { ffi::am_residency_set_contains_buffer(self.as_ptr(), buffer.as_ptr()) }
1453    }
1454
1455    /// Whether the set currently contains `texture`.
1456    #[must_use]
1457    pub fn contains_texture(&self, texture: &MetalTexture) -> bool {
1458        unsafe { ffi::am_residency_set_contains_texture(self.as_ptr(), texture.as_ptr()) }
1459    }
1460
1461    /// Number of allocations in the set.
1462    #[must_use]
1463    pub fn allocation_count(&self) -> usize {
1464        unsafe { ffi::am_residency_set_allocation_count(self.as_ptr()) }
1465    }
1466
1467    /// Commit pending add/remove changes.
1468    pub fn commit(&self) {
1469        unsafe { ffi::am_residency_set_commit(self.as_ptr()) };
1470    }
1471
1472    /// Request residency for all committed allocations.
1473    pub fn request_residency(&self) {
1474        unsafe { ffi::am_residency_set_request_residency(self.as_ptr()) };
1475    }
1476
1477    /// End residency for all committed allocations.
1478    pub fn end_residency(&self) {
1479        unsafe { ffi::am_residency_set_end_residency(self.as_ptr()) };
1480    }
1481}
1482
1483impl CaptureManager {
1484    /// Retrieve the process-global capture manager.
1485    #[must_use]
1486    pub fn shared() -> Option<Self> {
1487        Self::wrap(unsafe { ffi::am_capture_manager_shared() })
1488    }
1489
1490    /// Query whether the given capture destination is supported.
1491    #[must_use]
1492    pub fn supports_destination(&self, destination: usize) -> bool {
1493        unsafe { ffi::am_capture_manager_supports_destination(self.as_ptr(), destination) }
1494    }
1495
1496    /// Whether a capture is currently in progress.
1497    #[must_use]
1498    pub fn is_capturing(&self) -> bool {
1499        unsafe { ffi::am_capture_manager_is_capturing(self.as_ptr()) }
1500    }
1501
1502    /// Create a capture scope that captures all command queues on `device`.
1503    #[must_use]
1504    pub fn new_capture_scope_with_device(&self, device: &MetalDevice) -> Option<CaptureScope> {
1505        CaptureScope::wrap(unsafe {
1506            ffi::am_capture_manager_new_scope_with_device(self.as_ptr(), device.as_ptr())
1507        })
1508    }
1509
1510    /// Create a capture scope restricted to `command_queue`.
1511    #[must_use]
1512    pub fn new_capture_scope_with_command_queue(
1513        &self,
1514        command_queue: &CommandQueue,
1515    ) -> Option<CaptureScope> {
1516        CaptureScope::wrap(unsafe {
1517            ffi::am_capture_manager_new_scope_with_command_queue(
1518                self.as_ptr(),
1519                command_queue.as_ptr(),
1520            )
1521        })
1522    }
1523}
1524
1525impl CaptureScope {
1526    /// Mark the start of the capture scope.
1527    pub fn begin(&self) {
1528        unsafe { ffi::am_capture_scope_begin(self.as_ptr()) };
1529    }
1530
1531    /// Mark the end of the capture scope.
1532    pub fn end(&self) {
1533        unsafe { ffi::am_capture_scope_end(self.as_ptr()) };
1534    }
1535}
1536
1537#[cfg(test)]
1538mod texture_transfer_tests {
1539    use super::*;
1540
1541    fn rgba8_metadata() -> TextureTransferMetadata {
1542        TextureTransferMetadata {
1543            width: 4,
1544            height: 4,
1545            depth: 1,
1546            mipmap_level_count: 1,
1547            array_length: 1,
1548            pixel_format: crate::pixel_format::RGBA8UNORM,
1549            texture_type: crate::texture_type::TYPE_2D,
1550            storage_mode: crate::storage_mode::SHARED,
1551        }
1552    }
1553
1554    #[test]
1555    fn rejects_short_rgba_row() {
1556        assert!(matches!(
1557            validate_texture_transfer(rgba8_metadata(), 64, 15, (0, 0), (4, 4), 0, 0),
1558            Err(TextureTransferError::BytesPerRowTooSmall { minimum: 16, .. })
1559        ));
1560    }
1561
1562    #[test]
1563    fn rejects_stride_larger_than_native_int() {
1564        assert!(matches!(
1565            validate_texture_transfer(
1566                rgba8_metadata(),
1567                usize::MAX,
1568                usize::MAX,
1569                (0, 0),
1570                (4, 4),
1571                0,
1572                0,
1573            ),
1574            Err(TextureTransferError::IntegerOutOfRange {
1575                field: "bytes_per_row",
1576                ..
1577            })
1578        ));
1579    }
1580
1581    #[test]
1582    fn rejects_unknown_pixel_format() {
1583        let mut metadata = rgba8_metadata();
1584        metadata.pixel_format = usize::MAX;
1585        assert!(matches!(
1586            validate_texture_transfer(metadata, 64, 16, (0, 0), (4, 4), 0, 0),
1587            Err(TextureTransferError::UnsupportedPixelFormat { .. })
1588        ));
1589    }
1590
1591    #[test]
1592    fn accepts_final_row_without_trailing_stride_padding() {
1593        assert!(validate_texture_transfer(rgba8_metadata(), 80, 32, (0, 0), (4, 3), 0, 0).is_ok());
1594    }
1595}