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])*
14pub struct $name {
16 ptr: *mut c_void,
17 }
18
19 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 unsafe { ffi::am_object_release(self.ptr) };
32 self.ptr = core::ptr::null_mut();
33 }
34 }
35 }
36
37 impl $name {
38#[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
55pub mod indirect_command_type {
57 pub const DRAW: usize = 1 << 0;
59 pub const DRAW_INDEXED: usize = 1 << 1;
61 pub const CONCURRENT_DISPATCH: usize = 1 << 5;
63 pub const CONCURRENT_DISPATCH_THREADS: usize = 1 << 6;
65}
66
67pub mod counter_sampling_point {
69 pub const AT_STAGE_BOUNDARY: usize = 0;
71 pub const AT_DRAW_BOUNDARY: usize = 1;
73 pub const AT_DISPATCH_BOUNDARY: usize = 2;
75 pub const AT_TILE_DISPATCH_BOUNDARY: usize = 3;
77 pub const AT_BLIT_BOUNDARY: usize = 4;
79}
80
81pub mod log_level {
83 pub const UNDEFINED: usize = 0;
85 pub const DEBUG: usize = 1;
87 pub const INFO: usize = 2;
89 pub const NOTICE: usize = 3;
91 pub const ERROR: usize = 4;
93 pub const FAULT: usize = 5;
95}
96
97pub mod purgeable_state {
99 pub const KEEP_CURRENT: usize = 1;
101 pub const NON_VOLATILE: usize = 2;
103 pub const VOLATILE: usize = 3;
105 pub const EMPTY: usize = 4;
107}
108
109pub mod capture_destination {
111 pub const DEVELOPER_TOOLS: usize = 1;
113 pub const GPU_TRACE_DOCUMENT: usize = 2;
115}
116
117pub mod intersection_function_signature {
119 pub const NONE: usize = 0;
121 pub const INSTANCING: usize = 1 << 0;
123 pub const TRIANGLE_DATA: usize = 1 << 1;
125 pub const WORLD_SPACE_DATA: usize = 1 << 2;
127}
128
129opaque_handle!(
130 pub struct Heap;
132);
133opaque_handle!(
134 pub struct Event;
136);
137opaque_handle!(
138 pub struct Fence;
140);
141opaque_handle!(
142 pub struct DynamicLibrary;
144);
145opaque_handle!(
146 pub struct BinaryArchive;
148);
149opaque_handle!(
150 pub struct IndirectCommandBuffer;
152);
153opaque_handle!(
154 pub struct AccelerationStructure;
156);
157opaque_handle!(
158 pub struct IntersectionFunctionTable;
160);
161opaque_handle!(
162 pub struct VisibleFunctionTable;
164);
165opaque_handle!(
166 pub struct CounterSampleBuffer;
168);
169opaque_handle!(
170 pub struct LogState;
172);
173opaque_handle!(
174 pub struct ResidencySet;
176);
177opaque_handle!(
178 pub struct CaptureManager;
180);
181opaque_handle!(
182 pub struct CaptureScope;
184);
185
186impl MetalDevice {
187 #[must_use]
189 pub fn name(&self) -> String {
190 unsafe { take_string(ffi::am_device_name(self.as_ptr())) }
191 }
192
193 #[must_use]
195 pub fn registry_id(&self) -> u64 {
196 unsafe { ffi::am_device_registry_id(self.as_ptr()) }
197 }
198
199 #[must_use]
201 pub fn supports_dynamic_libraries(&self) -> bool {
202 unsafe { ffi::am_device_supports_dynamic_libraries(self.as_ptr()) }
203 }
204
205 #[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 #[must_use]
213 pub fn supports_raytracing(&self) -> bool {
214 unsafe { ffi::am_device_supports_raytracing(self.as_ptr()) }
215 }
216
217 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 #[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 #[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 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 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 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 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 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 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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
531pub enum TextureTransferError {
532 UnsupportedPixelFormat { pixel_format: usize },
534 CpuInaccessibleStorage { storage_mode: usize },
536 InvalidMipmapLevel {
538 mipmap_level: usize,
539 mipmap_level_count: usize,
540 },
541 InvalidSlice { slice: usize, array_length: usize },
543 EmptyRegion,
545 UnsupportedDepth { depth: usize },
547 UnsupportedTextureType { texture_type: usize },
549 RegionOutOfBounds {
551 origin: (usize, usize),
552 size: (usize, usize),
553 mip_size: (usize, usize),
554 },
555 BlockMisaligned {
557 field: &'static str,
558 value: usize,
559 block_size: usize,
560 },
561 BytesPerRowTooSmall {
563 bytes_per_row: usize,
564 minimum: usize,
565 },
566 BytesPerRowMisaligned {
568 bytes_per_row: usize,
569 bytes_per_block: usize,
570 },
571 LayoutOverflow,
573 IntegerOutOfRange { field: &'static str, value: usize },
575 BufferTooShort { actual: usize, required: usize },
577 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 #[must_use]
691 pub fn depth(&self) -> usize {
692 unsafe { ffi::am_texture_depth(self.as_ptr()) }
693 }
694
695 #[must_use]
697 pub fn mipmap_level_count(&self) -> usize {
698 unsafe { ffi::am_texture_mipmap_level_count(self.as_ptr()) }
699 }
700
701 #[must_use]
703 pub fn array_length(&self) -> usize {
704 unsafe { ffi::am_texture_array_length(self.as_ptr()) }
705 }
706
707 #[must_use]
709 pub fn usage(&self) -> usize {
710 unsafe { ffi::am_texture_usage(self.as_ptr()) }
711 }
712
713 #[must_use]
715 pub fn storage_mode(&self) -> usize {
716 unsafe { ffi::am_texture_storage_mode(self.as_ptr()) }
717 }
718
719 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
1146 pub fn size(&self) -> usize {
1147 unsafe { ffi::am_heap_size(self.as_ptr()) }
1148 }
1149
1150 #[must_use]
1152 pub fn used_size(&self) -> usize {
1153 unsafe { ffi::am_heap_used_size(self.as_ptr()) }
1154 }
1155
1156 #[must_use]
1158 pub fn current_allocated_size(&self) -> usize {
1159 unsafe { ffi::am_heap_current_allocated_size(self.as_ptr()) }
1160 }
1161
1162 #[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 #[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 #[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 #[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 #[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 #[must_use]
1221 pub fn signaled_value(&self) -> u64 {
1222 unsafe { ffi::am_event_signaled_value(self.as_ptr()) }
1223 }
1224
1225 pub fn set_signaled_value(&self, value: u64) {
1227 unsafe { ffi::am_event_set_signaled_value(self.as_ptr(), value) };
1228 }
1229
1230 #[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 #[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 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 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 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 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 #[must_use]
1347 pub fn size(&self) -> usize {
1348 unsafe { ffi::am_indirect_command_buffer_size(self.as_ptr()) }
1349 }
1350
1351 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 #[must_use]
1366 pub fn size(&self) -> usize {
1367 unsafe { ffi::am_acceleration_structure_size(self.as_ptr()) }
1368 }
1369}
1370
1371impl IntersectionFunctionTable {
1372 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 #[must_use]
1387 pub fn sample_count(&self) -> usize {
1388 unsafe { ffi::am_counter_sample_buffer_sample_count(self.as_ptr()) }
1389 }
1390
1391 #[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 pub fn add_buffer(&self, buffer: &MetalBuffer) {
1416 unsafe { ffi::am_residency_set_add_buffer(self.as_ptr(), buffer.as_ptr()) };
1417 }
1418
1419 pub fn add_texture(&self, texture: &MetalTexture) {
1421 unsafe { ffi::am_residency_set_add_texture(self.as_ptr(), texture.as_ptr()) };
1422 }
1423
1424 pub fn add_heap(&self, heap: &Heap) {
1426 unsafe { ffi::am_residency_set_add_heap(self.as_ptr(), heap.as_ptr()) };
1427 }
1428
1429 pub fn remove_buffer(&self, buffer: &MetalBuffer) {
1431 unsafe { ffi::am_residency_set_remove_buffer(self.as_ptr(), buffer.as_ptr()) };
1432 }
1433
1434 pub fn remove_texture(&self, texture: &MetalTexture) {
1436 unsafe { ffi::am_residency_set_remove_texture(self.as_ptr(), texture.as_ptr()) };
1437 }
1438
1439 pub fn remove_heap(&self, heap: &Heap) {
1441 unsafe { ffi::am_residency_set_remove_heap(self.as_ptr(), heap.as_ptr()) };
1442 }
1443
1444 pub fn remove_all_allocations(&self) {
1446 unsafe { ffi::am_residency_set_remove_all_allocations(self.as_ptr()) };
1447 }
1448
1449 #[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 #[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 #[must_use]
1463 pub fn allocation_count(&self) -> usize {
1464 unsafe { ffi::am_residency_set_allocation_count(self.as_ptr()) }
1465 }
1466
1467 pub fn commit(&self) {
1469 unsafe { ffi::am_residency_set_commit(self.as_ptr()) };
1470 }
1471
1472 pub fn request_residency(&self) {
1474 unsafe { ffi::am_residency_set_request_residency(self.as_ptr()) };
1475 }
1476
1477 pub fn end_residency(&self) {
1479 unsafe { ffi::am_residency_set_end_residency(self.as_ptr()) };
1480 }
1481}
1482
1483impl CaptureManager {
1484 #[must_use]
1486 pub fn shared() -> Option<Self> {
1487 Self::wrap(unsafe { ffi::am_capture_manager_shared() })
1488 }
1489
1490 #[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 #[must_use]
1498 pub fn is_capturing(&self) -> bool {
1499 unsafe { ffi::am_capture_manager_is_capturing(self.as_ptr()) }
1500 }
1501
1502 #[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 #[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 pub fn begin(&self) {
1528 unsafe { ffi::am_capture_scope_begin(self.as_ptr()) };
1529 }
1530
1531 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}