1#![allow(clippy::missing_errors_doc)]
2
3use crate::{ffi, storage_mode, MetalBuffer, MetalDevice, MetalTexture, SamplerState};
4use core::ffi::c_void;
5use std::sync::MutexGuard;
6
7const DATA_TYPE_TEXTURE: usize = 58;
8const DATA_TYPE_SAMPLER: usize = 59;
9const DATA_TYPE_POINTER: usize = 60;
10const DESCRIPTOR_WORD_COUNT: usize = 6;
11const FIRST_CONSTANT_DATA_TYPE: usize = 3;
12const LAST_CONSTANT_DATA_TYPE: usize = 56;
13const FIRST_LONG_DATA_TYPE: usize = 81;
14const LAST_LONG_DATA_TYPE: usize = 88;
15const FIRST_BFLOAT_DATA_TYPE: usize = 121;
16const LAST_BFLOAT_DATA_TYPE: usize = 124;
17
18pub mod argument_buffers_tier {
20 pub const TIER1: usize = 0;
22 pub const TIER2: usize = 1;
24}
25
26pub mod binding_access {
28 pub const READ_ONLY: usize = 0;
30 pub const READ_WRITE: usize = 1;
32 pub const WRITE_ONLY: usize = 2;
34}
35
36pub mod texture_type {
38 pub const TYPE_1D: usize = 0;
40 pub const TYPE_1D_ARRAY: usize = 1;
42 pub const TYPE_2D: usize = 2;
44 pub const TYPE_2D_ARRAY: usize = 3;
46 pub const TYPE_2D_MULTISAMPLE: usize = 4;
48 pub const CUBE: usize = 5;
50 pub const CUBE_ARRAY: usize = 6;
52 pub const TYPE_3D: usize = 7;
54 pub const TYPE_2D_MULTISAMPLE_ARRAY: usize = 8;
56 pub const TEXTURE_BUFFER: usize = 9;
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ArgumentBindingType {
63 Buffer,
65 Texture,
67 Sampler,
69 Constant,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum ArgumentEncoderError {
76 DescriptorCountOutOfRange,
78 EmptyDescriptorSet,
80 UnsupportedDataType { data_type: usize },
82 InvalidAccess { access: usize },
84 InvalidTextureType { texture_type: usize },
86 BufferArrayUnsupported { array_length: usize },
88 InvalidConstantBlockAlignment { alignment: usize },
90 BindingRangeOutOfBounds { index: usize, array_length: usize },
92 DuplicateBinding { index: usize },
94 NativeCreationFailed,
96 MappingLockPoisoned,
98 LayoutUnavailable,
100 InvalidBindingIndex { index: usize },
102 BindingTypeMismatch {
104 index: usize,
105 expected: ArgumentBindingType,
106 actual: ArgumentBindingType,
107 },
108 MisalignedArgumentBufferOffset { offset: usize, alignment: usize },
110 ArgumentBufferRangeOutOfBounds {
112 offset: usize,
113 encoded_length: usize,
114 buffer_length: usize,
115 },
116 CpuInaccessibleArgumentBuffer { storage_mode: usize },
118 BufferOffsetOutOfBounds { offset: usize, buffer_length: usize },
120 IntegerOutOfRange { field: &'static str, value: usize },
122 NativeRejected { operation: &'static str },
124}
125
126impl core::fmt::Display for ArgumentEncoderError {
127 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128 match self {
129 Self::DescriptorCountOutOfRange => {
130 formatter.write_str("argument descriptor count exceeds native Int")
131 }
132 Self::EmptyDescriptorSet => {
133 formatter.write_str("at least one argument descriptor is required")
134 }
135 Self::UnsupportedDataType { data_type } => {
136 write!(
137 formatter,
138 "data type {data_type} is not valid for an argument descriptor"
139 )
140 }
141 Self::InvalidAccess { access } => {
142 write!(formatter, "binding access value {access} is invalid")
143 }
144 Self::InvalidTextureType { texture_type } => {
145 write!(formatter, "texture type value {texture_type} is invalid")
146 }
147 Self::BufferArrayUnsupported { array_length } => write!(
148 formatter,
149 "buffer-pointer descriptor array length {array_length} is unsupported"
150 ),
151 Self::InvalidConstantBlockAlignment { alignment } => {
152 write!(formatter, "constant-block alignment {alignment} is invalid")
153 }
154 Self::BindingRangeOutOfBounds {
155 index,
156 array_length,
157 } => write!(
158 formatter,
159 "argument binding range {index}..{} is out of bounds",
160 index.saturating_add(*array_length)
161 ),
162 Self::DuplicateBinding { index } => {
163 write!(formatter, "argument binding index {index} is duplicated")
164 }
165 Self::NativeCreationFailed => {
166 formatter.write_str("Metal could not create argument encoder")
167 }
168 Self::MappingLockPoisoned => {
169 formatter.write_str("argument-buffer mapping lock is poisoned")
170 }
171 Self::LayoutUnavailable => formatter.write_str(
172 "function-derived argument layout is unavailable; use an explicit unsafe setter",
173 ),
174 Self::InvalidBindingIndex { index } => {
175 write!(formatter, "argument binding index {index} is not present")
176 }
177 Self::BindingTypeMismatch {
178 index,
179 expected,
180 actual,
181 } => write!(
182 formatter,
183 "argument binding {index} expects {expected:?}, not {actual:?}"
184 ),
185 Self::MisalignedArgumentBufferOffset { offset, alignment } => write!(
186 formatter,
187 "argument-buffer offset {offset} is not aligned to {alignment}"
188 ),
189 Self::ArgumentBufferRangeOutOfBounds {
190 offset,
191 encoded_length,
192 buffer_length,
193 } => write!(
194 formatter,
195 "encoded argument range {offset}..{} exceeds buffer length {buffer_length}",
196 offset.saturating_add(*encoded_length)
197 ),
198 Self::CpuInaccessibleArgumentBuffer { storage_mode } => write!(
199 formatter,
200 "storage mode {storage_mode} cannot be used as an argument-encoder destination"
201 ),
202 Self::BufferOffsetOutOfBounds {
203 offset,
204 buffer_length,
205 } => write!(
206 formatter,
207 "buffer offset {offset} exceeds buffer length {buffer_length}"
208 ),
209 Self::IntegerOutOfRange { field, value } => {
210 write!(formatter, "{field} value {value} exceeds native Int")
211 }
212 Self::NativeRejected { operation } => write!(formatter, "Metal rejected {operation}"),
213 }
214 }
215}
216
217impl std::error::Error for ArgumentEncoderError {}
218
219#[derive(Debug, Clone, Copy)]
221pub struct ArgumentDescriptor {
222 data_type: usize,
223 index: usize,
224 array_length: usize,
225 access: usize,
226 texture_type: usize,
227 constant_block_alignment: usize,
228}
229
230impl ArgumentDescriptor {
231 #[must_use]
233 pub const fn buffer(index: usize, access: usize) -> Self {
234 Self {
235 data_type: DATA_TYPE_POINTER,
236 index,
237 array_length: 0,
238 access,
239 texture_type: texture_type::TYPE_2D,
240 constant_block_alignment: 0,
241 }
242 }
243
244 #[must_use]
246 pub const fn texture(index: usize, texture_type: usize, access: usize) -> Self {
247 Self {
248 data_type: DATA_TYPE_TEXTURE,
249 index,
250 array_length: 0,
251 access,
252 texture_type,
253 constant_block_alignment: 0,
254 }
255 }
256
257 #[must_use]
259 pub const fn sampler(index: usize) -> Self {
260 Self {
261 data_type: DATA_TYPE_SAMPLER,
262 index,
263 array_length: 0,
264 access: binding_access::READ_ONLY,
265 texture_type: texture_type::TYPE_2D,
266 constant_block_alignment: 0,
267 }
268 }
269
270 #[must_use]
272 pub const fn constant(data_type: usize, index: usize, array_length: usize) -> Self {
273 Self {
274 data_type,
275 index,
276 array_length,
277 access: binding_access::READ_ONLY,
278 texture_type: texture_type::TYPE_2D,
279 constant_block_alignment: 0,
280 }
281 }
282
283 #[must_use]
285 pub fn with_array_length(mut self, array_length: usize) -> Self {
286 self.array_length = array_length;
287 self
288 }
289
290 #[must_use]
292 pub fn with_constant_block_alignment(mut self, alignment: usize) -> Self {
293 self.constant_block_alignment = alignment;
294 self
295 }
296
297 const fn as_words(self) -> [usize; DESCRIPTOR_WORD_COUNT] {
298 [
299 self.data_type,
300 self.index,
301 self.array_length,
302 self.access,
303 self.texture_type,
304 self.constant_block_alignment,
305 ]
306 }
307
308 const fn binding_type(self) -> ArgumentBindingType {
309 match self.data_type {
310 DATA_TYPE_POINTER => ArgumentBindingType::Buffer,
311 DATA_TYPE_TEXTURE => ArgumentBindingType::Texture,
312 DATA_TYPE_SAMPLER => ArgumentBindingType::Sampler,
313 _ => ArgumentBindingType::Constant,
314 }
315 }
316}
317
318pub struct ArgumentEncoder {
320 ptr: *mut c_void,
321 layout: Option<Vec<ArgumentBindingRange>>,
322}
323
324#[derive(Clone, Copy)]
325struct ArgumentBindingRange {
326 start: usize,
327 last: usize,
328 binding_type: ArgumentBindingType,
329}
330
331pub struct ArgumentBufferBinding<'a> {
333 encoder: &'a mut ArgumentEncoder,
334 buffer: &'a MetalBuffer,
335 offset: usize,
336 encoded_length: usize,
337 storage_mode: usize,
338 _mapping_lock: MutexGuard<'a, ()>,
339}
340
341unsafe impl Send for ArgumentEncoder {}
344
345impl Drop for ArgumentEncoder {
346 fn drop(&mut self) {
347 if !self.ptr.is_null() {
348 unsafe { ffi::am_object_release(self.ptr) };
349 self.ptr = core::ptr::null_mut();
350 }
351 }
352}
353
354impl MetalDevice {
355 pub fn new_argument_encoder_with_descriptors(
361 &self,
362 descriptors: &[ArgumentDescriptor],
363 ) -> Result<ArgumentEncoder, ArgumentEncoderError> {
364 let layout = build_layout(descriptors)?;
365 let word_count = descriptors
366 .len()
367 .checked_mul(DESCRIPTOR_WORD_COUNT)
368 .filter(|count| isize::try_from(*count).is_ok())
369 .ok_or(ArgumentEncoderError::DescriptorCountOutOfRange)?;
370 let mut words = Vec::with_capacity(word_count);
371 for descriptor in descriptors {
372 words.extend_from_slice(&descriptor.as_words());
373 }
374 let ptr = unsafe {
375 ffi::am_device_new_argument_encoder_with_descriptors(
376 self.as_ptr(),
377 words.as_ptr(),
378 descriptors.len(),
379 )
380 };
381 if ptr.is_null() {
382 Err(ArgumentEncoderError::NativeCreationFailed)
383 } else {
384 Ok(unsafe { ArgumentEncoder::from_descriptor_ptr(ptr, layout) })
385 }
386 }
387}
388
389impl ArgumentEncoder {
390 #[must_use]
392 pub fn encoded_length(&self) -> usize {
393 unsafe { ffi::am_argument_encoder_encoded_length(self.as_ptr()) }
394 }
395
396 #[must_use]
398 pub fn alignment(&self) -> usize {
399 unsafe { ffi::am_argument_encoder_alignment(self.as_ptr()) }
400 }
401
402 pub unsafe fn bind_argument_buffer<'a>(
415 &'a mut self,
416 buffer: &'a MetalBuffer,
417 offset: usize,
418 ) -> Result<ArgumentBufferBinding<'a>, ArgumentEncoderError> {
419 ensure_native_int(offset, "argument-buffer offset")?;
420 let alignment = self.alignment();
421 if alignment == 0 || offset % alignment != 0 {
422 return Err(ArgumentEncoderError::MisalignedArgumentBufferOffset { offset, alignment });
423 }
424 let encoded_length = self.encoded_length();
425 let end = offset.checked_add(encoded_length).ok_or_else(|| {
426 ArgumentEncoderError::ArgumentBufferRangeOutOfBounds {
427 offset,
428 encoded_length,
429 buffer_length: buffer.length(),
430 }
431 })?;
432 if end > buffer.length() {
433 return Err(ArgumentEncoderError::ArgumentBufferRangeOutOfBounds {
434 offset,
435 encoded_length,
436 buffer_length: buffer.length(),
437 });
438 }
439 let storage_mode = buffer.storage_mode();
440 if !matches!(storage_mode, storage_mode::SHARED | storage_mode::MANAGED) {
441 return Err(ArgumentEncoderError::CpuInaccessibleArgumentBuffer { storage_mode });
442 }
443 let mapping_lock = buffer
444 .lock_mapping()
445 .map_err(|_| ArgumentEncoderError::MappingLockPoisoned)?;
446 let accepted = unsafe {
447 ffi::am_argument_encoder_set_argument_buffer(self.as_ptr(), buffer.as_ptr(), offset)
448 };
449 if !accepted {
450 return Err(ArgumentEncoderError::NativeRejected {
451 operation: "argument-buffer binding",
452 });
453 }
454 Ok(ArgumentBufferBinding {
455 encoder: self,
456 buffer,
457 offset,
458 encoded_length,
459 storage_mode,
460 _mapping_lock: mapping_lock,
461 })
462 }
463
464 #[must_use]
469 pub const fn as_ptr(&self) -> *mut c_void {
470 self.ptr
471 }
472
473 pub(crate) unsafe fn from_function_ptr(ptr: *mut c_void) -> Self {
474 Self { ptr, layout: None }
475 }
476
477 unsafe fn from_descriptor_ptr(ptr: *mut c_void, layout: Vec<ArgumentBindingRange>) -> Self {
478 Self {
479 ptr,
480 layout: Some(layout),
481 }
482 }
483
484 fn validate_binding(
485 &self,
486 index: usize,
487 actual: ArgumentBindingType,
488 ) -> Result<(), ArgumentEncoderError> {
489 validate_index(index)?;
490 let layout = self
491 .layout
492 .as_ref()
493 .ok_or(ArgumentEncoderError::LayoutUnavailable)?;
494 let expected = layout
495 .iter()
496 .find(|range| index >= range.start && index <= range.last)
497 .map(|range| range.binding_type)
498 .ok_or(ArgumentEncoderError::InvalidBindingIndex { index })?;
499 if expected == actual {
500 Ok(())
501 } else {
502 Err(ArgumentEncoderError::BindingTypeMismatch {
503 index,
504 expected,
505 actual,
506 })
507 }
508 }
509}
510
511impl ArgumentBufferBinding<'_> {
512 pub fn set_buffer(
514 &mut self,
515 buffer: &MetalBuffer,
516 offset: usize,
517 index: usize,
518 ) -> Result<(), ArgumentEncoderError> {
519 self.encoder
520 .validate_binding(index, ArgumentBindingType::Buffer)?;
521 unsafe { self.set_buffer_unchecked(buffer, offset, index) }
522 }
523
524 pub fn set_texture(
526 &mut self,
527 texture: &MetalTexture,
528 index: usize,
529 ) -> Result<(), ArgumentEncoderError> {
530 self.encoder
531 .validate_binding(index, ArgumentBindingType::Texture)?;
532 unsafe { self.set_texture_unchecked(texture, index) }
533 }
534
535 pub fn set_sampler_state(
537 &mut self,
538 sampler: &SamplerState,
539 index: usize,
540 ) -> Result<(), ArgumentEncoderError> {
541 self.encoder
542 .validate_binding(index, ArgumentBindingType::Sampler)?;
543 unsafe { self.set_sampler_state_unchecked(sampler, index) }
544 }
545
546 pub unsafe fn set_buffer_unchecked(
553 &mut self,
554 buffer: &MetalBuffer,
555 offset: usize,
556 index: usize,
557 ) -> Result<(), ArgumentEncoderError> {
558 validate_index(index)?;
559 ensure_native_int(offset, "buffer offset")?;
560 if offset > buffer.length() {
561 return Err(ArgumentEncoderError::BufferOffsetOutOfBounds {
562 offset,
563 buffer_length: buffer.length(),
564 });
565 }
566 if ffi::am_argument_encoder_set_buffer(
567 self.encoder.as_ptr(),
568 buffer.as_ptr(),
569 offset,
570 index,
571 ) {
572 Ok(())
573 } else {
574 Err(ArgumentEncoderError::NativeRejected {
575 operation: "argument buffer resource binding",
576 })
577 }
578 }
579
580 pub unsafe fn set_texture_unchecked(
587 &mut self,
588 texture: &MetalTexture,
589 index: usize,
590 ) -> Result<(), ArgumentEncoderError> {
591 validate_index(index)?;
592 if ffi::am_argument_encoder_set_texture(self.encoder.as_ptr(), texture.as_ptr(), index) {
593 Ok(())
594 } else {
595 Err(ArgumentEncoderError::NativeRejected {
596 operation: "argument texture binding",
597 })
598 }
599 }
600
601 pub unsafe fn set_sampler_state_unchecked(
608 &mut self,
609 sampler: &SamplerState,
610 index: usize,
611 ) -> Result<(), ArgumentEncoderError> {
612 validate_index(index)?;
613 if ffi::am_argument_encoder_set_sampler_state(
614 self.encoder.as_ptr(),
615 sampler.as_ptr(),
616 index,
617 ) {
618 Ok(())
619 } else {
620 Err(ArgumentEncoderError::NativeRejected {
621 operation: "argument sampler binding",
622 })
623 }
624 }
625}
626
627impl Drop for ArgumentBufferBinding<'_> {
628 fn drop(&mut self) {
629 if self.storage_mode == storage_mode::MANAGED {
630 unsafe {
631 ffi::am_buffer_did_modify_range(
632 self.buffer.as_ptr(),
633 self.offset,
634 self.encoded_length,
635 );
636 }
637 }
638 }
639}
640
641fn build_layout(
642 descriptors: &[ArgumentDescriptor],
643) -> Result<Vec<ArgumentBindingRange>, ArgumentEncoderError> {
644 if descriptors.is_empty() {
645 return Err(ArgumentEncoderError::EmptyDescriptorSet);
646 }
647 if descriptors.len() > isize::MAX as usize {
648 return Err(ArgumentEncoderError::DescriptorCountOutOfRange);
649 }
650 let mut layout = Vec::with_capacity(descriptors.len());
651 for descriptor in descriptors {
652 validate_descriptor(*descriptor)?;
653 let occupied_binding_count = descriptor.array_length.max(1);
654 let last = descriptor
655 .index
656 .checked_add(occupied_binding_count - 1)
657 .filter(|last| isize::try_from(*last).is_ok())
658 .ok_or(ArgumentEncoderError::BindingRangeOutOfBounds {
659 index: descriptor.index,
660 array_length: descriptor.array_length,
661 })?;
662 layout.push(ArgumentBindingRange {
663 start: descriptor.index,
664 last,
665 binding_type: descriptor.binding_type(),
666 });
667 }
668 layout.sort_unstable_by_key(|range| range.start);
669 for ranges in layout.windows(2) {
670 if ranges[1].start <= ranges[0].last {
671 return Err(ArgumentEncoderError::DuplicateBinding {
672 index: ranges[1].start,
673 });
674 }
675 }
676 Ok(layout)
677}
678
679fn validate_index(index: usize) -> Result<(), ArgumentEncoderError> {
680 if isize::try_from(index).is_ok() {
681 Ok(())
682 } else {
683 Err(ArgumentEncoderError::InvalidBindingIndex { index })
684 }
685}
686
687fn validate_descriptor(descriptor: ArgumentDescriptor) -> Result<(), ArgumentEncoderError> {
688 validate_index(descriptor.index)?;
689 if descriptor.access > binding_access::WRITE_ONLY {
690 return Err(ArgumentEncoderError::InvalidAccess {
691 access: descriptor.access,
692 });
693 }
694 match descriptor.binding_type() {
695 ArgumentBindingType::Buffer => {
696 if descriptor.array_length != 0 {
697 return Err(ArgumentEncoderError::BufferArrayUnsupported {
698 array_length: descriptor.array_length,
699 });
700 }
701 if descriptor.constant_block_alignment != 0 {
702 return Err(ArgumentEncoderError::InvalidConstantBlockAlignment {
703 alignment: descriptor.constant_block_alignment,
704 });
705 }
706 }
707 ArgumentBindingType::Texture => {
708 if descriptor.texture_type > texture_type::TEXTURE_BUFFER {
709 return Err(ArgumentEncoderError::InvalidTextureType {
710 texture_type: descriptor.texture_type,
711 });
712 }
713 if descriptor.constant_block_alignment != 0 {
714 return Err(ArgumentEncoderError::InvalidConstantBlockAlignment {
715 alignment: descriptor.constant_block_alignment,
716 });
717 }
718 }
719 ArgumentBindingType::Sampler => {
720 if descriptor.access != binding_access::READ_ONLY {
721 return Err(ArgumentEncoderError::InvalidAccess {
722 access: descriptor.access,
723 });
724 }
725 if descriptor.constant_block_alignment != 0 {
726 return Err(ArgumentEncoderError::InvalidConstantBlockAlignment {
727 alignment: descriptor.constant_block_alignment,
728 });
729 }
730 }
731 ArgumentBindingType::Constant => {
732 if !is_supported_constant_data_type(descriptor.data_type) {
733 return Err(ArgumentEncoderError::UnsupportedDataType {
734 data_type: descriptor.data_type,
735 });
736 }
737 if descriptor.access != binding_access::READ_ONLY {
738 return Err(ArgumentEncoderError::InvalidAccess {
739 access: descriptor.access,
740 });
741 }
742 let alignment = descriptor.constant_block_alignment;
743 if alignment != 0
744 && (!alignment.is_power_of_two() || isize::try_from(alignment).is_err())
745 {
746 return Err(ArgumentEncoderError::InvalidConstantBlockAlignment { alignment });
747 }
748 }
749 }
750 Ok(())
751}
752
753fn is_supported_constant_data_type(data_type: usize) -> bool {
754 (FIRST_CONSTANT_DATA_TYPE..=LAST_CONSTANT_DATA_TYPE).contains(&data_type)
755 || (FIRST_LONG_DATA_TYPE..=LAST_LONG_DATA_TYPE).contains(&data_type)
756 || (FIRST_BFLOAT_DATA_TYPE..=LAST_BFLOAT_DATA_TYPE).contains(&data_type)
757}
758
759fn ensure_native_int(value: usize, field: &'static str) -> Result<(), ArgumentEncoderError> {
760 if isize::try_from(value).is_ok() {
761 Ok(())
762 } else {
763 Err(ArgumentEncoderError::IntegerOutOfRange { field, value })
764 }
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770
771 #[test]
772 fn modern_scalar_vector_types_are_supported() {
773 for data_type in [81, 88, 121, 124] {
774 assert!(is_supported_constant_data_type(data_type));
775 }
776 }
777}