Skip to main content

apple_metal/
command.rs

1#![allow(clippy::missing_errors_doc)]
2
3use crate::{
4    ffi, storage_mode, util::take_optional_string, CommandBuffer, CommandBufferPhase, CommandQueue,
5    ComputePipelineState, CounterSampleBuffer, DepthStencilState, Event, Fence, MetalBuffer,
6    MetalTexture, RenderPipelineState, SamplerState,
7};
8use core::ffi::c_void;
9use core::ops::Range;
10use std::collections::HashSet;
11
12const MAX_BUFFER_BINDINGS: usize = 31;
13const MAX_TEXTURE_BINDINGS: usize = 128;
14const MAX_SAMPLER_BINDINGS: usize = 16;
15
16/// `MTLCommandBufferStatus` enum values.
17pub mod command_buffer_status {
18    /// Mirrors the `Metal` framework constant `NOT_ENQUEUED`.
19    pub const NOT_ENQUEUED: usize = 0;
20    /// Mirrors the `Metal` framework constant `ENQUEUED`.
21    pub const ENQUEUED: usize = 1;
22    /// Mirrors the `Metal` framework constant `COMMITTED`.
23    pub const COMMITTED: usize = 2;
24    /// Mirrors the `Metal` framework constant `SCHEDULED`.
25    pub const SCHEDULED: usize = 3;
26    /// Mirrors the `Metal` framework constant `COMPLETED`.
27    pub const COMPLETED: usize = 4;
28    /// Mirrors the `Metal` framework constant `ERROR`.
29    pub const ERROR: usize = 5;
30}
31
32/// Errors returned for invalid command-buffer or encoder operations.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum CommandBufferError {
35    /// The shared lifecycle lock was poisoned.
36    StateLockPoisoned,
37    /// The operation is not valid in the command buffer's current state.
38    InvalidState {
39        operation: &'static str,
40        state: &'static str,
41    },
42    /// A command encoder is still active on this command buffer.
43    ActiveEncoder,
44    /// The encoder has already ended.
45    EncoderEnded,
46    /// The native command encoder could not be created.
47    EncoderCreationFailed { encoder: &'static str },
48    /// A resource byte range is invalid.
49    RangeOutOfBounds {
50        resource: &'static str,
51        offset: usize,
52        length: usize,
53        resource_length: usize,
54    },
55    /// A range end precedes its start.
56    InvalidRange,
57    /// A binding index exceeds the supported table.
58    InvalidBindingIndex {
59        binding: &'static str,
60        index: usize,
61        limit: usize,
62    },
63    /// A dimension or offset cannot be represented by the native API.
64    IntegerOutOfRange { field: &'static str, value: usize },
65    /// A dispatch dimension must be non-zero.
66    EmptyDispatch { field: &'static str },
67    /// A synchronization operation requires managed storage.
68    ManagedStorageRequired { storage_mode: usize },
69    /// Waiting after updating the same fence in one encoder is illegal.
70    FenceWaitAfterUpdate,
71    /// The native bridge rejected a validated operation.
72    NativeRejected { operation: &'static str },
73    /// GPU execution completed with an error.
74    ExecutionFailed(String),
75}
76
77impl core::fmt::Display for CommandBufferError {
78    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        match self {
80            Self::StateLockPoisoned => formatter.write_str("command-buffer state lock is poisoned"),
81            Self::InvalidState { operation, state } => {
82                write!(
83                    formatter,
84                    "{operation} is invalid while command buffer is {state}"
85                )
86            }
87            Self::ActiveEncoder => formatter.write_str("a command encoder is still active"),
88            Self::EncoderEnded => formatter.write_str("the command encoder has already ended"),
89            Self::EncoderCreationFailed { encoder } => {
90                write!(
91                    formatter,
92                    "Metal could not create a {encoder} command encoder"
93                )
94            }
95            Self::RangeOutOfBounds {
96                resource,
97                offset,
98                length,
99                resource_length,
100            } => write!(
101                formatter,
102                "{resource} range {offset}..{} exceeds length {resource_length}",
103                offset.saturating_add(*length)
104            ),
105            Self::InvalidRange => formatter.write_str("range end precedes range start"),
106            Self::InvalidBindingIndex {
107                binding,
108                index,
109                limit,
110            } => write!(
111                formatter,
112                "{binding} binding index {index} is outside 0..{limit}"
113            ),
114            Self::IntegerOutOfRange { field, value } => {
115                write!(formatter, "{field} value {value} exceeds native Int")
116            }
117            Self::EmptyDispatch { field } => {
118                write!(formatter, "dispatch dimension {field} must be non-zero")
119            }
120            Self::ManagedStorageRequired { storage_mode } => {
121                write!(
122                    formatter,
123                    "managed storage required, got mode {storage_mode}"
124                )
125            }
126            Self::FenceWaitAfterUpdate => {
127                formatter.write_str("cannot wait for a fence after updating it in the same encoder")
128            }
129            Self::NativeRejected { operation } => {
130                write!(formatter, "Metal rejected {operation}")
131            }
132            Self::ExecutionFailed(message) => write!(formatter, "GPU execution failed: {message}"),
133        }
134    }
135}
136
137impl std::error::Error for CommandBufferError {}
138
139struct EncoderCore {
140    ptr: *mut c_void,
141    command_buffer: CommandBuffer,
142    ended: bool,
143    updated_fences: HashSet<usize>,
144}
145
146impl EncoderCore {
147    fn new(ptr: *mut c_void, command_buffer: CommandBuffer) -> Self {
148        Self {
149            ptr,
150            command_buffer,
151            ended: false,
152            updated_fences: HashSet::new(),
153        }
154    }
155
156    fn with_active<T>(
157        &self,
158        operation: &'static str,
159        encode: impl FnOnce(*mut c_void) -> T,
160    ) -> Result<T, CommandBufferError> {
161        if self.ended {
162            return Err(CommandBufferError::EncoderEnded);
163        }
164        let state = self
165            .command_buffer
166            .inner
167            .state
168            .lock()
169            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
170        ensure_recording(state.phase, operation)?;
171        if !state.active_encoder {
172            return Err(CommandBufferError::EncoderEnded);
173        }
174        drop(state);
175        Ok(encode(self.ptr))
176    }
177
178    fn finish(&mut self) -> Result<(), CommandBufferError> {
179        if self.ended {
180            return Err(CommandBufferError::EncoderEnded);
181        }
182        let mut state = self
183            .command_buffer
184            .inner
185            .state
186            .lock()
187            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
188        ensure_recording(state.phase, "end_encoding")?;
189        if !state.active_encoder {
190            return Err(CommandBufferError::EncoderEnded);
191        }
192        unsafe { ffi::am_command_encoder_end_encoding(self.ptr) };
193        state.active_encoder = false;
194        drop(state);
195        self.ended = true;
196        Ok(())
197    }
198
199    fn finish_on_drop(&mut self) {
200        if self.ended {
201            return;
202        }
203        let mut state = self
204            .command_buffer
205            .inner
206            .state
207            .lock()
208            .unwrap_or_else(std::sync::PoisonError::into_inner);
209        if matches!(
210            state.phase,
211            CommandBufferPhase::Recording | CommandBufferPhase::Enqueued
212        ) && state.active_encoder
213        {
214            unsafe { ffi::am_command_encoder_end_encoding(self.ptr) };
215            state.active_encoder = false;
216        }
217        drop(state);
218        self.ended = true;
219    }
220
221    fn record_fence_update(&mut self, fence: &Fence) {
222        self.updated_fences.insert(fence.as_ptr() as usize);
223    }
224
225    fn ensure_fence_wait_allowed(&self, fence: &Fence) -> Result<(), CommandBufferError> {
226        if self.updated_fences.contains(&(fence.as_ptr() as usize)) {
227            Err(CommandBufferError::FenceWaitAfterUpdate)
228        } else {
229            Ok(())
230        }
231    }
232}
233
234impl Drop for EncoderCore {
235    fn drop(&mut self) {
236        self.finish_on_drop();
237        if !self.ptr.is_null() {
238            unsafe { ffi::am_object_release(self.ptr) };
239            self.ptr = core::ptr::null_mut();
240        }
241    }
242}
243
244macro_rules! command_encoder {
245    ($(#[$meta:meta])* pub struct $name:ident;) => {
246        $(#[$meta])*
247        pub struct $name {
248            core: EncoderCore,
249        }
250
251        impl $name {
252            fn new(ptr: *mut c_void, command_buffer: CommandBuffer) -> Self {
253                Self {
254                    core: EncoderCore::new(ptr, command_buffer),
255                }
256            }
257
258            /// Borrowed raw native command-encoder pointer.
259            ///
260            /// The pointer is valid only until this wrapper is dropped. Calling
261            /// `endEncoding` through it bypasses lifecycle tracking.
262            #[must_use]
263            pub fn as_ptr(&self) -> *mut c_void {
264                self.core.ptr
265            }
266
267            /// Finish encoding. Dropping an active encoder performs this once
268            /// automatically.
269            pub fn end_encoding(mut self) -> Result<(), CommandBufferError> {
270                self.core.finish()
271            }
272        }
273    };
274}
275
276command_encoder!(
277    /// Apple's `id<MTLBlitCommandEncoder>` — encodes buffer and texture copy work.
278    pub struct BlitCommandEncoder;
279);
280command_encoder!(
281    /// Apple's `id<MTLComputeCommandEncoder>` — encodes compute dispatches.
282    pub struct ComputeCommandEncoder;
283);
284command_encoder!(
285    /// Apple's `id<MTLRenderCommandEncoder>` — encodes render passes.
286    pub struct RenderCommandEncoder;
287);
288
289impl CommandQueue {
290    /// Create a command buffer whose native object does not retain references.
291    ///
292    /// # Safety
293    ///
294    /// Every object referenced directly or indirectly by encoded commands must
295    /// remain alive until the command buffer reaches `COMPLETED` or `ERROR`.
296    /// Prefer [`Self::new_command_buffer`] unless the caller owns that lifetime
297    /// protocol.
298    #[must_use]
299    pub unsafe fn new_command_buffer_with_unretained_references(&self) -> Option<CommandBuffer> {
300        let ptr =
301            ffi::am_command_queue_new_command_buffer_with_unretained_references(self.as_ptr());
302        if ptr.is_null() {
303            None
304        } else {
305            Some(CommandBuffer::from_retained_ptr(ptr))
306        }
307    }
308}
309
310impl CommandBuffer {
311    /// Enqueue the command buffer on its queue without committing it.
312    pub fn enqueue(&self) -> Result<(), CommandBufferError> {
313        let mut state = self
314            .inner
315            .state
316            .lock()
317            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
318        if state.phase != CommandBufferPhase::Recording {
319            return Err(invalid_state("enqueue", state.phase));
320        }
321        if state.active_encoder {
322            return Err(CommandBufferError::ActiveEncoder);
323        }
324        unsafe { ffi::am_command_buffer_enqueue(self.as_ptr()) };
325        state.phase = CommandBufferPhase::Enqueued;
326        drop(state);
327        Ok(())
328    }
329
330    /// Submit the recorded commands for execution.
331    pub fn commit(&self) -> Result<(), CommandBufferError> {
332        let mut state = self
333            .inner
334            .state
335            .lock()
336            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
337        ensure_recording(state.phase, "commit")?;
338        if state.active_encoder {
339            return Err(CommandBufferError::ActiveEncoder);
340        }
341        unsafe { ffi::am_command_buffer_commit(self.as_ptr()) };
342        state.phase = CommandBufferPhase::Committed;
343        drop(state);
344        Ok(())
345    }
346
347    /// Block until Metal schedules this committed command buffer.
348    pub fn wait_until_scheduled(&self) -> Result<(), CommandBufferError> {
349        let state = self
350            .inner
351            .state
352            .lock()
353            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
354        match state.phase {
355            CommandBufferPhase::Completed => return Ok(()),
356            CommandBufferPhase::Error => return Err(self.execution_error()),
357            CommandBufferPhase::Committed => {}
358            phase => return Err(invalid_state("wait_until_scheduled", phase)),
359        }
360        drop(state);
361        unsafe { ffi::am_command_buffer_wait_until_scheduled(self.as_ptr()) };
362        Ok(())
363    }
364
365    /// Block until all submitted commands finish.
366    pub fn wait_until_completed(&self) -> Result<(), CommandBufferError> {
367        {
368            let state = self
369                .inner
370                .state
371                .lock()
372                .map_err(|_| CommandBufferError::StateLockPoisoned)?;
373            match state.phase {
374                CommandBufferPhase::Completed => return Ok(()),
375                CommandBufferPhase::Error => return Err(self.execution_error()),
376                CommandBufferPhase::Committed => {}
377                phase => return Err(invalid_state("wait_until_completed", phase)),
378            }
379        }
380        unsafe { ffi::am_command_buffer_wait_until_completed(self.as_ptr()) };
381        let status = unsafe { ffi::am_command_buffer_status(self.as_ptr()) };
382        let mut state = self
383            .inner
384            .state
385            .lock()
386            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
387        if status == command_buffer_status::ERROR {
388            state.phase = CommandBufferPhase::Error;
389            drop(state);
390            Err(self.execution_error())
391        } else {
392            state.phase = CommandBufferPhase::Completed;
393            drop(state);
394            Ok(())
395        }
396    }
397
398    /// Current `MTLCommandBufferStatus` value.
399    #[must_use]
400    pub fn status(&self) -> usize {
401        let status = unsafe { ffi::am_command_buffer_status(self.as_ptr()) };
402        let mut state = self
403            .inner
404            .state
405            .lock()
406            .unwrap_or_else(std::sync::PoisonError::into_inner);
407        match status {
408            command_buffer_status::COMPLETED => state.phase = CommandBufferPhase::Completed,
409            command_buffer_status::ERROR => state.phase = CommandBufferPhase::Error,
410            _ => {}
411        }
412        status
413    }
414
415    /// Localized Metal error string for a failed command buffer.
416    #[must_use]
417    pub fn error(&self) -> Option<String> {
418        unsafe { take_optional_string(ffi::am_command_buffer_error_message(self.as_ptr())) }
419    }
420
421    /// Create a standalone blit command encoder.
422    pub fn new_blit_command_encoder(&self) -> Result<BlitCommandEncoder, CommandBufferError> {
423        let core = self.begin_encoder("blit", || unsafe {
424            ffi::am_command_buffer_new_blit_command_encoder(self.as_ptr())
425        })?;
426        Ok(BlitCommandEncoder::new(core, self.clone()))
427    }
428
429    /// Create a standalone compute command encoder.
430    pub fn new_compute_command_encoder(&self) -> Result<ComputeCommandEncoder, CommandBufferError> {
431        let core = self.begin_encoder("compute", || unsafe {
432            ffi::am_command_buffer_new_compute_command_encoder(self.as_ptr())
433        })?;
434        Ok(ComputeCommandEncoder::new(core, self.clone()))
435    }
436
437    /// Create a render command encoder that renders into `texture`.
438    pub fn new_render_command_encoder(
439        &self,
440        texture: &MetalTexture,
441        load_action: usize,
442        store_action: usize,
443        clear_color: [f64; 4],
444    ) -> Result<RenderCommandEncoder, CommandBufferError> {
445        ensure_native_int(load_action, "load_action")?;
446        ensure_native_int(store_action, "store_action")?;
447        let core = self.begin_encoder("render", || unsafe {
448            ffi::am_command_buffer_new_render_command_encoder(
449                self.as_ptr(),
450                texture.as_ptr(),
451                load_action,
452                store_action,
453                clear_color[0],
454                clear_color[1],
455                clear_color[2],
456                clear_color[3],
457            )
458        })?;
459        Ok(RenderCommandEncoder::new(core, self.clone()))
460    }
461
462    /// Encode a wait until `event` reaches at least `value`.
463    pub fn encode_wait_for_event(
464        &self,
465        event: &Event,
466        value: u64,
467    ) -> Result<(), CommandBufferError> {
468        self.encode_without_encoder("encode_wait_for_event", || unsafe {
469            ffi::am_command_buffer_encode_wait_for_event(self.as_ptr(), event.as_ptr(), value);
470        })
471    }
472
473    /// Encode a signal that updates `event` to `value`.
474    pub fn encode_signal_event(&self, event: &Event, value: u64) -> Result<(), CommandBufferError> {
475        self.encode_without_encoder("encode_signal_event", || unsafe {
476            ffi::am_command_buffer_encode_signal_event(self.as_ptr(), event.as_ptr(), value);
477        })
478    }
479
480    /// Record a blit copy from `src` into `dst`.
481    pub fn blit_copy_buffer(
482        &self,
483        src: &MetalBuffer,
484        src_offset: usize,
485        dst: &MetalBuffer,
486        dst_offset: usize,
487        size: usize,
488    ) -> Result<(), CommandBufferError> {
489        let mut encoder = self.new_blit_command_encoder()?;
490        encoder.copy_buffer(src, src_offset, dst, dst_offset, size)?;
491        encoder.end_encoding()
492    }
493
494    /// Record a one-dimensional compute dispatch.
495    pub fn dispatch_compute_1d(
496        &self,
497        pipeline: &ComputePipelineState,
498        buffers: &[&MetalBuffer],
499        threadgroups: usize,
500        threads_per_group: usize,
501    ) -> Result<(), CommandBufferError> {
502        let mut encoder = self.new_compute_command_encoder()?;
503        encoder.set_compute_pipeline_state(pipeline)?;
504        for (index, buffer) in buffers.iter().enumerate() {
505            encoder.set_buffer(buffer, 0, index)?;
506        }
507        encoder.dispatch_threadgroups((threadgroups, 1, 1), (threads_per_group, 1, 1))?;
508        encoder.end_encoding()
509    }
510
511    pub(crate) fn encode_without_encoder(
512        &self,
513        operation: &'static str,
514        encode: impl FnOnce(),
515    ) -> Result<(), CommandBufferError> {
516        let state = self
517            .inner
518            .state
519            .lock()
520            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
521        ensure_recording(state.phase, operation)?;
522        if state.active_encoder {
523            return Err(CommandBufferError::ActiveEncoder);
524        }
525        encode();
526        drop(state);
527        Ok(())
528    }
529
530    fn begin_encoder(
531        &self,
532        encoder: &'static str,
533        create: impl FnOnce() -> *mut c_void,
534    ) -> Result<*mut c_void, CommandBufferError> {
535        let mut state = self
536            .inner
537            .state
538            .lock()
539            .map_err(|_| CommandBufferError::StateLockPoisoned)?;
540        ensure_recording(state.phase, "create command encoder")?;
541        if state.active_encoder {
542            return Err(CommandBufferError::ActiveEncoder);
543        }
544        let pointer = create();
545        if pointer.is_null() {
546            return Err(CommandBufferError::EncoderCreationFailed { encoder });
547        }
548        state.active_encoder = true;
549        drop(state);
550        Ok(pointer)
551    }
552
553    fn execution_error(&self) -> CommandBufferError {
554        let message =
555            unsafe { take_optional_string(ffi::am_command_buffer_error_message(self.as_ptr())) }
556                .unwrap_or_else(|| {
557                    "Metal reported an unspecified command-buffer error".to_string()
558                });
559        CommandBufferError::ExecutionFailed(message)
560    }
561}
562
563impl BlitCommandEncoder {
564    /// Copy `size` bytes from `src` into `dst`.
565    pub fn copy_buffer(
566        &mut self,
567        src: &MetalBuffer,
568        src_offset: usize,
569        dst: &MetalBuffer,
570        dst_offset: usize,
571        size: usize,
572    ) -> Result<(), CommandBufferError> {
573        checked_resource_range("source buffer", src_offset, size, src.length())?;
574        checked_resource_range("destination buffer", dst_offset, size, dst.length())?;
575        ensure_native_int(src_offset, "source offset")?;
576        ensure_native_int(dst_offset, "destination offset")?;
577        ensure_native_int(size, "copy size")?;
578        let accepted = self.core.with_active("copy_buffer", |encoder| unsafe {
579            ffi::am_blit_command_encoder_copy_buffer(
580                encoder,
581                src.as_ptr(),
582                src_offset,
583                dst.as_ptr(),
584                dst_offset,
585                size,
586            )
587        })?;
588        if accepted {
589            Ok(())
590        } else {
591            Err(CommandBufferError::NativeRejected {
592                operation: "buffer copy",
593            })
594        }
595    }
596
597    /// Fill a byte range of `buffer` with `value`.
598    pub fn fill_buffer(
599        &mut self,
600        buffer: &MetalBuffer,
601        range: Range<usize>,
602        value: u8,
603    ) -> Result<(), CommandBufferError> {
604        if range.start > range.end {
605            return Err(CommandBufferError::InvalidRange);
606        }
607        let length = range.end - range.start;
608        checked_resource_range("buffer", range.start, length, buffer.length())?;
609        ensure_native_int(range.start, "fill offset")?;
610        ensure_native_int(length, "fill length")?;
611        let accepted = self.core.with_active("fill_buffer", |encoder| unsafe {
612            ffi::am_blit_command_encoder_fill_buffer(
613                encoder,
614                buffer.as_ptr(),
615                range.start,
616                length,
617                value,
618            )
619        })?;
620        if accepted {
621            Ok(())
622        } else {
623            Err(CommandBufferError::NativeRejected {
624                operation: "buffer fill",
625            })
626        }
627    }
628
629    /// Sample hardware counters into `sample_buffer`.
630    pub fn sample_counters(
631        &mut self,
632        sample_buffer: &CounterSampleBuffer,
633        sample_index: usize,
634        barrier: bool,
635    ) -> Result<(), CommandBufferError> {
636        if sample_index >= sample_buffer.sample_count() {
637            return Err(CommandBufferError::InvalidBindingIndex {
638                binding: "counter sample",
639                index: sample_index,
640                limit: sample_buffer.sample_count(),
641            });
642        }
643        ensure_native_int(sample_index, "sample index")?;
644        let accepted = self.core.with_active("sample_counters", |encoder| unsafe {
645            ffi::am_blit_command_encoder_sample_counters(
646                encoder,
647                sample_buffer.as_ptr(),
648                sample_index,
649                barrier,
650            )
651        })?;
652        if accepted {
653            Ok(())
654        } else {
655            Err(CommandBufferError::NativeRejected {
656                operation: "counter sampling",
657            })
658        }
659    }
660
661    /// Make managed resource writes visible to the CPU after completion.
662    pub fn synchronize_resource(&mut self, buffer: &MetalBuffer) -> Result<(), CommandBufferError> {
663        synchronize_resource(&self.core, buffer.as_ptr(), buffer.storage_mode())
664    }
665
666    /// Make managed texture writes visible to the CPU after completion.
667    pub fn synchronize_texture(
668        &mut self,
669        texture: &MetalTexture,
670    ) -> Result<(), CommandBufferError> {
671        synchronize_resource(&self.core, texture.as_ptr(), texture.storage_mode())
672    }
673
674    /// Update `fence` with work encoded so far.
675    pub fn update_fence(&mut self, fence: &Fence) -> Result<(), CommandBufferError> {
676        self.core.with_active("update_fence", |encoder| unsafe {
677            ffi::am_blit_command_encoder_update_fence(encoder, fence.as_ptr());
678        })?;
679        self.core.record_fence_update(fence);
680        Ok(())
681    }
682
683    /// Wait for `fence` before executing subsequent work.
684    pub fn wait_for_fence(&mut self, fence: &Fence) -> Result<(), CommandBufferError> {
685        self.core.ensure_fence_wait_allowed(fence)?;
686        self.core.with_active("wait_for_fence", |encoder| unsafe {
687            ffi::am_blit_command_encoder_wait_for_fence(encoder, fence.as_ptr());
688        })
689    }
690}
691
692impl ComputeCommandEncoder {
693    /// Bind a compute pipeline state.
694    pub fn set_compute_pipeline_state(
695        &mut self,
696        pipeline: &ComputePipelineState,
697    ) -> Result<(), CommandBufferError> {
698        self.core
699            .with_active("set_compute_pipeline_state", |encoder| unsafe {
700                ffi::am_compute_command_encoder_set_pipeline_state(encoder, pipeline.as_ptr());
701            })
702    }
703
704    /// Bind a buffer at `index`.
705    pub fn set_buffer(
706        &mut self,
707        buffer: &MetalBuffer,
708        offset: usize,
709        index: usize,
710    ) -> Result<(), CommandBufferError> {
711        validate_binding_index("buffer", index, MAX_BUFFER_BINDINGS)?;
712        checked_resource_range("buffer", offset, 0, buffer.length())?;
713        ensure_native_int(offset, "buffer offset")?;
714        self.core.with_active("set_buffer", |encoder| unsafe {
715            ffi::am_compute_command_encoder_set_buffer(encoder, buffer.as_ptr(), offset, index);
716        })
717    }
718
719    /// Bind a texture at `index`.
720    pub fn set_texture(
721        &mut self,
722        texture: &MetalTexture,
723        index: usize,
724    ) -> Result<(), CommandBufferError> {
725        validate_binding_index("texture", index, MAX_TEXTURE_BINDINGS)?;
726        self.core.with_active("set_texture", |encoder| unsafe {
727            ffi::am_compute_command_encoder_set_texture(encoder, texture.as_ptr(), index);
728        })
729    }
730
731    /// Bind a sampler state at `index`.
732    pub fn set_sampler_state(
733        &mut self,
734        sampler: &SamplerState,
735        index: usize,
736    ) -> Result<(), CommandBufferError> {
737        validate_binding_index("sampler", index, MAX_SAMPLER_BINDINGS)?;
738        self.core
739            .with_active("set_sampler_state", |encoder| unsafe {
740                ffi::am_compute_command_encoder_set_sampler_state(encoder, sampler.as_ptr(), index);
741            })
742    }
743
744    /// Bind a visible function table at `index`.
745    pub fn set_visible_function_table(
746        &mut self,
747        table: &crate::VisibleFunctionTable,
748        index: usize,
749    ) -> Result<(), CommandBufferError> {
750        validate_binding_index("visible function table", index, MAX_BUFFER_BINDINGS)?;
751        self.core
752            .with_active("set_visible_function_table", |encoder| unsafe {
753                ffi::am_compute_command_encoder_set_visible_function_table(
754                    encoder,
755                    table.as_ptr(),
756                    index,
757                );
758            })
759    }
760
761    /// Bind an intersection function table at `index`.
762    pub fn set_intersection_function_table(
763        &mut self,
764        table: &crate::IntersectionFunctionTable,
765        index: usize,
766    ) -> Result<(), CommandBufferError> {
767        validate_binding_index("intersection function table", index, MAX_BUFFER_BINDINGS)?;
768        self.core
769            .with_active("set_intersection_function_table", |encoder| unsafe {
770                ffi::am_compute_command_encoder_set_intersection_function_table(
771                    encoder,
772                    table.as_ptr(),
773                    index,
774                );
775            })
776    }
777
778    /// Bind an acceleration structure at `index`.
779    pub fn set_acceleration_structure(
780        &mut self,
781        acceleration_structure: &crate::AccelerationStructure,
782        index: usize,
783    ) -> Result<(), CommandBufferError> {
784        validate_binding_index("acceleration structure", index, MAX_BUFFER_BINDINGS)?;
785        self.core
786            .with_active("set_acceleration_structure", |encoder| unsafe {
787                ffi::am_compute_command_encoder_set_acceleration_structure(
788                    encoder,
789                    acceleration_structure.as_ptr(),
790                    index,
791                );
792            })
793    }
794
795    /// Dispatch threadgroups of fixed size.
796    pub fn dispatch_threadgroups(
797        &mut self,
798        threadgroups: (usize, usize, usize),
799        threads_per_threadgroup: (usize, usize, usize),
800    ) -> Result<(), CommandBufferError> {
801        validate_size(threadgroups, "threadgroup")?;
802        validate_size(threads_per_threadgroup, "threads-per-threadgroup")?;
803        self.core
804            .with_active("dispatch_threadgroups", |encoder| unsafe {
805                ffi::am_compute_command_encoder_dispatch_threadgroups(
806                    encoder,
807                    threadgroups.0,
808                    threadgroups.1,
809                    threadgroups.2,
810                    threads_per_threadgroup.0,
811                    threads_per_threadgroup.1,
812                    threads_per_threadgroup.2,
813                );
814            })
815    }
816
817    /// Dispatch an arbitrary thread grid.
818    pub fn dispatch_threads(
819        &mut self,
820        threads: (usize, usize, usize),
821        threads_per_threadgroup: (usize, usize, usize),
822    ) -> Result<(), CommandBufferError> {
823        validate_size(threads, "thread grid")?;
824        validate_size(threads_per_threadgroup, "threads-per-threadgroup")?;
825        self.core.with_active("dispatch_threads", |encoder| unsafe {
826            ffi::am_compute_command_encoder_dispatch_threads(
827                encoder,
828                threads.0,
829                threads.1,
830                threads.2,
831                threads_per_threadgroup.0,
832                threads_per_threadgroup.1,
833                threads_per_threadgroup.2,
834            );
835        })
836    }
837
838    /// Update `fence` with work encoded so far.
839    pub fn update_fence(&mut self, fence: &Fence) -> Result<(), CommandBufferError> {
840        self.core.with_active("update_fence", |encoder| unsafe {
841            ffi::am_compute_command_encoder_update_fence(encoder, fence.as_ptr());
842        })?;
843        self.core.record_fence_update(fence);
844        Ok(())
845    }
846
847    /// Wait for `fence` before executing subsequent work.
848    pub fn wait_for_fence(&mut self, fence: &Fence) -> Result<(), CommandBufferError> {
849        self.core.ensure_fence_wait_allowed(fence)?;
850        self.core.with_active("wait_for_fence", |encoder| unsafe {
851            ffi::am_compute_command_encoder_wait_for_fence(encoder, fence.as_ptr());
852        })
853    }
854}
855
856impl RenderCommandEncoder {
857    /// Bind a render pipeline state.
858    pub fn set_render_pipeline_state(
859        &mut self,
860        pipeline: &RenderPipelineState,
861    ) -> Result<(), CommandBufferError> {
862        self.core
863            .with_active("set_render_pipeline_state", |encoder| unsafe {
864                ffi::am_render_command_encoder_set_render_pipeline_state(
865                    encoder,
866                    pipeline.as_ptr(),
867                );
868            })
869    }
870
871    /// Bind a vertex buffer at `index`.
872    pub fn set_vertex_buffer(
873        &mut self,
874        buffer: &MetalBuffer,
875        offset: usize,
876        index: usize,
877    ) -> Result<(), CommandBufferError> {
878        validate_binding_index("vertex buffer", index, MAX_BUFFER_BINDINGS)?;
879        checked_resource_range("vertex buffer", offset, 0, buffer.length())?;
880        ensure_native_int(offset, "vertex buffer offset")?;
881        self.core
882            .with_active("set_vertex_buffer", |encoder| unsafe {
883                ffi::am_render_command_encoder_set_vertex_buffer(
884                    encoder,
885                    buffer.as_ptr(),
886                    offset,
887                    index,
888                );
889            })
890    }
891
892    /// Bind a fragment sampler state at `index`.
893    pub fn set_fragment_sampler_state(
894        &mut self,
895        sampler: &SamplerState,
896        index: usize,
897    ) -> Result<(), CommandBufferError> {
898        validate_binding_index("fragment sampler", index, MAX_SAMPLER_BINDINGS)?;
899        self.core
900            .with_active("set_fragment_sampler_state", |encoder| unsafe {
901                ffi::am_render_command_encoder_set_fragment_sampler_state(
902                    encoder,
903                    sampler.as_ptr(),
904                    index,
905                );
906            })
907    }
908
909    /// Bind a depth/stencil state object.
910    pub fn set_depth_stencil_state(
911        &mut self,
912        state: &DepthStencilState,
913    ) -> Result<(), CommandBufferError> {
914        self.core
915            .with_active("set_depth_stencil_state", |encoder| unsafe {
916                ffi::am_render_command_encoder_set_depth_stencil_state(encoder, state.as_ptr());
917            })
918    }
919
920    /// Draw a non-indexed primitive range.
921    pub fn draw_primitives(
922        &mut self,
923        primitive_type: usize,
924        vertex_start: usize,
925        vertex_count: usize,
926    ) -> Result<(), CommandBufferError> {
927        ensure_native_int(primitive_type, "primitive type")?;
928        ensure_native_int(vertex_start, "vertex start")?;
929        ensure_native_int(vertex_count, "vertex count")?;
930        vertex_start
931            .checked_add(vertex_count)
932            .filter(|end| isize::try_from(*end).is_ok())
933            .ok_or_else(|| CommandBufferError::IntegerOutOfRange {
934                field: "vertex range end",
935                value: vertex_start.saturating_add(vertex_count),
936            })?;
937        self.core.with_active("draw_primitives", |encoder| unsafe {
938            ffi::am_render_command_encoder_draw_primitives(
939                encoder,
940                primitive_type,
941                vertex_start,
942                vertex_count,
943            );
944        })
945    }
946
947    /// Update `fence` with work encoded so far.
948    pub fn update_fence(&mut self, fence: &Fence) -> Result<(), CommandBufferError> {
949        self.core.with_active("update_fence", |encoder| unsafe {
950            ffi::am_render_command_encoder_update_fence(encoder, fence.as_ptr());
951        })?;
952        self.core.record_fence_update(fence);
953        Ok(())
954    }
955
956    /// Wait for `fence` before executing subsequent work.
957    pub fn wait_for_fence(&mut self, fence: &Fence) -> Result<(), CommandBufferError> {
958        self.core.ensure_fence_wait_allowed(fence)?;
959        self.core.with_active("wait_for_fence", |encoder| unsafe {
960            ffi::am_render_command_encoder_wait_for_fence(encoder, fence.as_ptr());
961        })
962    }
963}
964
965fn ensure_recording(
966    phase: CommandBufferPhase,
967    operation: &'static str,
968) -> Result<(), CommandBufferError> {
969    if matches!(
970        phase,
971        CommandBufferPhase::Recording | CommandBufferPhase::Enqueued
972    ) {
973        Ok(())
974    } else {
975        Err(invalid_state(operation, phase))
976    }
977}
978
979fn invalid_state(operation: &'static str, phase: CommandBufferPhase) -> CommandBufferError {
980    CommandBufferError::InvalidState {
981        operation,
982        state: match phase {
983            CommandBufferPhase::Recording => "recording",
984            CommandBufferPhase::Enqueued => "enqueued",
985            CommandBufferPhase::Committed => "committed",
986            CommandBufferPhase::Completed => "completed",
987            CommandBufferPhase::Error => "failed",
988        },
989    }
990}
991
992fn checked_resource_range(
993    resource: &'static str,
994    offset: usize,
995    length: usize,
996    resource_length: usize,
997) -> Result<(), CommandBufferError> {
998    let end = offset
999        .checked_add(length)
1000        .ok_or(CommandBufferError::RangeOutOfBounds {
1001            resource,
1002            offset,
1003            length,
1004            resource_length,
1005        })?;
1006    if end > resource_length {
1007        Err(CommandBufferError::RangeOutOfBounds {
1008            resource,
1009            offset,
1010            length,
1011            resource_length,
1012        })
1013    } else {
1014        Ok(())
1015    }
1016}
1017
1018fn validate_binding_index(
1019    binding: &'static str,
1020    index: usize,
1021    limit: usize,
1022) -> Result<(), CommandBufferError> {
1023    if index < limit {
1024        Ok(())
1025    } else {
1026        Err(CommandBufferError::InvalidBindingIndex {
1027            binding,
1028            index,
1029            limit,
1030        })
1031    }
1032}
1033
1034fn ensure_native_int(value: usize, field: &'static str) -> Result<(), CommandBufferError> {
1035    if isize::try_from(value).is_ok() {
1036        Ok(())
1037    } else {
1038        Err(CommandBufferError::IntegerOutOfRange { field, value })
1039    }
1040}
1041
1042fn validate_size(
1043    size: (usize, usize, usize),
1044    field: &'static str,
1045) -> Result<(), CommandBufferError> {
1046    for (axis, value) in [("width", size.0), ("height", size.1), ("depth", size.2)] {
1047        if value == 0 {
1048            return Err(CommandBufferError::EmptyDispatch { field: axis });
1049        }
1050        ensure_native_int(value, field)?;
1051    }
1052    Ok(())
1053}
1054
1055fn synchronize_resource(
1056    core: &EncoderCore,
1057    resource: *mut c_void,
1058    resource_storage_mode: usize,
1059) -> Result<(), CommandBufferError> {
1060    if resource_storage_mode != storage_mode::MANAGED {
1061        return Err(CommandBufferError::ManagedStorageRequired {
1062            storage_mode: resource_storage_mode,
1063        });
1064    }
1065    let accepted = core.with_active("synchronize_resource", |encoder| unsafe {
1066        ffi::am_blit_command_encoder_synchronize_resource(encoder, resource)
1067    })?;
1068    if accepted {
1069        Ok(())
1070    } else {
1071        Err(CommandBufferError::NativeRejected {
1072            operation: "managed resource synchronization",
1073        })
1074    }
1075}