Skip to main content

nuvec_gpu/metal/
command.rs

1//! Command encoding with minimal barriers, and submission.
2
3use std::collections::HashMap;
4use std::ptr::NonNull;
5use std::sync::{Arc, Condvar, Mutex, PoisonError};
6
7use block2::RcBlock;
8use objc2::rc::{Retained, autoreleasepool};
9use objc2::runtime::ProtocolObject;
10use objc2_metal::{
11    MTLBarrierScope, MTLBuffer, MTLCommandBuffer, MTLCommandBufferStatus, MTLCommandEncoder,
12    MTLCommandQueue, MTLComputeCommandEncoder, MTLDevice, MTLDispatchType, MTLFence, MTLSize,
13};
14
15use super::buffer::Guard;
16use super::{Buffer, Device, Event, Pipeline};
17use crate::{GpuError, Result};
18
19/// Metal's limit on buffer argument slots per kernel.
20const MAX_ARGS: usize = 31;
21/// Metal's limit for inline bytes (`setBytes`).
22const MAX_INLINE_BYTES: usize = 4096;
23
24/// A kernel argument, bound to the slot of its position in the argument list.
25#[derive(Debug)]
26pub enum Arg<'a> {
27    /// A buffer the kernel only reads, from byte `offset` on.
28    Read(&'a Buffer, usize),
29    /// A buffer the kernel writes (and may read), from byte `offset` on.
30    Write(&'a mut Buffer, usize),
31    /// Small constant data copied into the command stream (at most 4 KiB).
32    Bytes(&'a [u8]),
33    /// The buffer of the argument at this index bound again, from byte `offset` on: an
34    /// in-place kernel reading and writing one buffer through two slots. That argument must
35    /// be an [`Arg::Write`] (whose hazards cover both slots); anything else is an
36    /// [`GpuError::InvalidArgument`].
37    Same(usize, usize),
38}
39
40impl<'a> Arg<'a> {
41    /// The whole buffer, read-only.
42    pub fn read(buffer: &'a Buffer) -> Self {
43        Self::Read(buffer, 0)
44    }
45
46    /// The whole buffer, written.
47    pub fn write(buffer: &'a mut Buffer) -> Self {
48        Self::Write(buffer, 0)
49    }
50
51    /// Inline constant bytes.
52    pub fn bytes(bytes: &'a [u8]) -> Self {
53        Self::Bytes(bytes)
54    }
55
56    fn buffer(&self) -> Option<(&Buffer, usize, bool)> {
57        match self {
58            Self::Read(b, offset) => Some((b, *offset, false)),
59            Self::Write(b, offset) => Some((b, *offset, true)),
60            Self::Bytes(_) | Self::Same(..) => None,
61        }
62    }
63}
64
65pub(crate) type Fence = Retained<ProtocolObject<dyn MTLFence>>;
66
67/// Orders command buffers that are committed without waiting: each command buffer created
68/// on a timeline starts its work only after the previous one's work has finished. Buffers
69/// are untracked, so consecutive command buffers would otherwise overlap on the GPU (as
70/// [`Device::ordered_command_buffer`]s do when they are independent).
71///
72/// Implemented with one fence (as in MLX): a command buffer's compute pass waits for the
73/// fence and updates it at the end. Command buffers without work leave it untouched, so
74/// empty or abandoned ones need no special handling.
75pub struct Timeline {
76    fence: Fence,
77    started: bool,
78}
79
80impl std::fmt::Debug for Timeline {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("Timeline").finish_non_exhaustive()
83    }
84}
85
86/// Work being recorded for the GPU: one concurrent compute encoder, with memory barriers
87/// inserted only where a dispatch depends on an earlier one.
88pub struct CommandBuffer {
89    device: Device,
90    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
91    /// The timeline fence, and whether the compute pass must wait for it.
92    fence: Option<(Fence, bool)>,
93    /// For ordered command buffers: this command buffer's fence (updated at the end of its
94    /// work and recorded on the buffers it writes), and the fences already waited for.
95    own: Option<Fence>,
96    /// Held (not their addresses): a dropped fence's address could be reused by a new one.
97    waited: Vec<Fence>,
98    encoder: Option<Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>>,
99    /// Buffers read and written since the last barrier.
100    reads: Vec<usize>,
101    writes: Vec<usize>,
102    barriers: usize,
103    /// In-flight guards of the buffers used, and the buffers themselves (kept alive).
104    guards: Vec<(Arc<Guard>, bool)>,
105    /// Per buffer (by id): its uses in this command buffer, all and the writing ones.
106    uses: HashMap<usize, (usize, usize)>,
107    retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
108    /// An event wait ended the last encoder: the next one waits for this buffer's fences.
109    after_wait: bool,
110    /// The kernels dispatched, while profiling.
111    kernels: Vec<Arc<str>>,
112}
113
114impl Device {
115    /// A new command buffer on this device's queue. It is *unordered*: its work does not
116    /// wait for other command buffers, so binding reused memory whose previous owner's GPU
117    /// work is still running ([`Device::new_gpu_buffer`]) is refused with
118    /// [`GpuError::BufferInUse`]; use an
119    /// [`ordered_command_buffer`](Self::ordered_command_buffer) for that.
120    pub fn command_buffer(&self) -> Result<CommandBuffer> {
121        let raw = autoreleasepool(|_| self.queue().commandBuffer())
122            .ok_or_else(|| GpuError::CommandBuffer("cannot create a command buffer".into()))?;
123        Ok(CommandBuffer {
124            device: self.clone(),
125            raw,
126            fence: None,
127            own: None,
128            waited: Vec::new(),
129            encoder: None,
130            reads: Vec::new(),
131            writes: Vec::new(),
132            barriers: 0,
133            guards: Vec::new(),
134            uses: HashMap::new(),
135            retained: Vec::new(),
136            after_wait: false,
137            kernels: Vec::new(),
138        })
139    }
140}
141
142impl Device {
143    /// A new, empty timeline.
144    pub fn timeline(&self) -> Result<Timeline> {
145        let fence = autoreleasepool(|_| self.raw().newFence())
146            .ok_or_else(|| GpuError::CommandBuffer("cannot create a fence".into()))?;
147        Ok(Timeline {
148            fence,
149            started: false,
150        })
151    }
152
153    /// A new command buffer whose work reads the outputs of earlier *ordered* command
154    /// buffers (committed on this device from any thread) safely while they are in flight:
155    /// before a dispatch reads or writes a buffer with a pending GPU write, it waits for the
156    /// writing command buffer's fence. Independent command buffers overlap on the GPU, as
157    /// in MLX.
158    ///
159    /// Reads wait for the pending writer; writes wait for every pending user (writing a
160    /// buffer that earlier work still reads). Other command buffers do not wait.
161    pub fn ordered_command_buffer(&self) -> Result<CommandBuffer> {
162        let fence = autoreleasepool(|_| self.raw().newFence())
163            .ok_or_else(|| GpuError::CommandBuffer("cannot create a fence".into()))?;
164        let mut cmd = self.command_buffer()?;
165        cmd.own = Some(fence);
166        Ok(cmd)
167    }
168
169    /// A new command buffer whose work runs after the work of every command buffer created
170    /// earlier on `timeline` (commit them in creation order). Like
171    /// [`command_buffer`](Self::command_buffer), it refuses reused memory whose previous
172    /// owner's GPU work is still running.
173    pub fn command_buffer_on(&self, timeline: &mut Timeline) -> Result<CommandBuffer> {
174        let mut cmd = self.command_buffer()?;
175        cmd.fence = Some((timeline.fence.clone(), timeline.started));
176        timeline.started = true;
177        Ok(cmd)
178    }
179}
180
181/// Reports a finished command buffer's GPU error, if any.
182fn status(raw: &ProtocolObject<dyn MTLCommandBuffer>) -> Result<()> {
183    if raw.status() == MTLCommandBufferStatus::Error {
184        let message = raw.error().map_or_else(
185            || "unknown error".to_owned(),
186            |e| e.localizedDescription().to_string(),
187        );
188        return Err(GpuError::CommandBuffer(message));
189    }
190    Ok(())
191}
192
193impl CommandBuffer {
194    /// Encodes `pipeline` over `threads` threads (a 1-D grid) with `args` bound to slots
195    /// `0..args.len()`. The threadgroup size is derived from the pipeline's limits.
196    ///
197    /// Arguments are validated (slot count, inline size, offsets within their buffers,
198    /// [`Arg::Same`] targets, ordering with other command buffers), but what the kernel does
199    /// with them is not: nuvec cannot see which elements a kernel touches.
200    ///
201    /// # Safety
202    ///
203    /// The kernel, run over this grid, must access each bound buffer only within its length
204    /// from the bound offset (and write only the buffers bound with [`Arg::Write`] or
205    /// [`Arg::Same`]). An out-of-bounds access reads or writes memory that other buffers,
206    /// possibly referenced by safe code, own.
207    pub unsafe fn dispatch(
208        &mut self,
209        pipeline: &Pipeline,
210        args: &[Arg<'_>],
211        threads: usize,
212    ) -> Result<()> {
213        if threads == 0 {
214            return self.check(args);
215        }
216        let width = pipeline.execution_width().max(1);
217        let mut group = pipeline.max_threads_per_threadgroup().min(threads).max(1);
218        if group > width {
219            group -= group % width;
220        }
221        self.encode(pipeline, args, [threads, 1, 1], |encoder| {
222            encoder
223                .dispatchThreads_threadsPerThreadgroup(size([threads, 1, 1]), size([group, 1, 1]));
224        })
225    }
226
227    /// Encodes `pipeline` over a grid of `groups` threadgroups of `threads` threads each, for
228    /// kernels that cooperate within a threadgroup (reductions, tiled matrix products).
229    ///
230    /// # Safety
231    ///
232    /// As for [`dispatch`](Self::dispatch): the kernel must access each bound buffer only
233    /// within its length from the bound offset.
234    pub unsafe fn dispatch_groups(
235        &mut self,
236        pipeline: &Pipeline,
237        args: &[Arg<'_>],
238        groups: [usize; 3],
239        threads: [usize; 3],
240    ) -> Result<()> {
241        let per_group: usize = threads.iter().product();
242        if per_group > pipeline.max_threads_per_threadgroup() {
243            return Err(GpuError::InvalidArgument(format!(
244                "{per_group} threads per threadgroup; `{}` allows {}",
245                pipeline.name(),
246                pipeline.max_threads_per_threadgroup()
247            )));
248        }
249        if groups.contains(&0) || per_group == 0 {
250            return self.check(args);
251        }
252        let extent = [0, 1, 2].map(|d| groups[d] * threads[d]);
253        self.encode(pipeline, args, extent, |encoder| {
254            encoder.dispatchThreadgroups_threadsPerThreadgroup(size(groups), size(threads));
255        })
256    }
257
258    /// Encodes `pipeline` over exactly `grid` threads in up to three dimensions, in
259    /// threadgroups of `group` threads (the edge threadgroups are partial).
260    ///
261    /// # Safety
262    ///
263    /// As for [`dispatch`](Self::dispatch): the kernel must access each bound buffer only
264    /// within its length from the bound offset.
265    pub unsafe fn dispatch_threads(
266        &mut self,
267        pipeline: &Pipeline,
268        args: &[Arg<'_>],
269        grid: [usize; 3],
270        group: [usize; 3],
271    ) -> Result<()> {
272        let per_group: usize = group.iter().product();
273        if per_group > pipeline.max_threads_per_threadgroup() {
274            return Err(GpuError::InvalidArgument(format!(
275                "{per_group} threads per threadgroup; `{}` allows {}",
276                pipeline.name(),
277                pipeline.max_threads_per_threadgroup()
278            )));
279        }
280        if grid.contains(&0) || per_group == 0 {
281            return self.check(args);
282        }
283        self.encode(pipeline, args, grid, |encoder| {
284            encoder.dispatchThreads_threadsPerThreadgroup(size(grid), size(group));
285        })
286    }
287
288    /// Validates and binds `args`, inserts a barrier if needed, then lets `dispatch` encode
289    /// over `extent` threads (for profiling). Runs in an autorelease pool: Metal
290    /// autoreleases objects while encoding (the compute encoder's context among them), and
291    /// a thread other than the main one drains none of them otherwise.
292    fn encode(
293        &mut self,
294        pipeline: &Pipeline,
295        args: &[Arg<'_>],
296        extent: [usize; 3],
297        dispatch: impl FnOnce(&ProtocolObject<dyn MTLComputeCommandEncoder>),
298    ) -> Result<()> {
299        autoreleasepool(|_| self.encode_now(pipeline, args, extent, dispatch))
300    }
301
302    fn encode_now(
303        &mut self,
304        pipeline: &Pipeline,
305        args: &[Arg<'_>],
306        extent: [usize; 3],
307        dispatch: impl FnOnce(&ProtocolObject<dyn MTLComputeCommandEncoder>),
308    ) -> Result<()> {
309        self.check(args)?;
310        if super::profile::enabled() {
311            self.kernels.push(match super::profile::shapes() {
312                true => format!("{} {extent:?}", pipeline.name()).into(),
313                false => pipeline.name.clone(),
314            });
315        }
316        if self.conflicts(args) {
317            self.encoder()?
318                .memoryBarrierWithScope(MTLBarrierScope::Buffers);
319            self.barriers += 1;
320            self.reads.clear();
321            self.writes.clear();
322        }
323        let encoder = self.encoder()?.clone();
324        if let Some(own) = &self.own {
325            let own = Retained::as_ptr(own) as usize;
326            for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
327                for fence in buffer.pending_fences(write) {
328                    let key = Retained::as_ptr(&fence) as usize;
329                    let seen = self
330                        .waited
331                        .iter()
332                        .any(|f| Retained::as_ptr(f) as usize == key);
333                    if key != own && !seen {
334                        encoder.waitForFence(&fence);
335                        self.waited.push(fence);
336                    }
337                }
338            }
339        }
340        encoder.setComputePipelineState(pipeline.raw());
341        for (index, arg) in args.iter().enumerate() {
342            match arg {
343                Arg::Bytes(bytes) => {
344                    let data = NonNull::new(bytes.as_ptr().cast_mut())
345                        .unwrap_or(NonNull::dangling())
346                        .cast();
347                    // SAFETY: `validate` checked `index < 31` and `bytes.len() <= 4096`;
348                    // Metal copies the bytes during this call, so the pointer need only be
349                    // valid for `bytes.len()` bytes now, which the slice guarantees.
350                    unsafe { encoder.setBytes_length_atIndex(data, bytes.len(), index) };
351                }
352                Arg::Read(..) | Arg::Write(..) => {
353                    let (buffer, offset, _) = arg.buffer().expect("buffer argument");
354                    // SAFETY: `validate` checked `index < 31` and `offset <= buffer.len()`, so
355                    // the binding stays within the buffer; the buffer is retained by this
356                    // command buffer until the GPU has finished.
357                    unsafe { encoder.setBuffer_offset_atIndex(buffer.raw(), offset, index) };
358                }
359                Arg::Same(earlier, offset) => {
360                    let (buffer, _, _) = args[*earlier].buffer().expect("validated");
361                    // SAFETY: as above: `validate` checked that `earlier` names another
362                    // buffer argument and `offset <= buffer.len()`.
363                    unsafe { encoder.setBuffer_offset_atIndex(buffer.raw(), *offset, index) };
364                }
365            }
366        }
367        for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
368            (if write {
369                &mut self.writes
370            } else {
371                &mut self.reads
372            })
373            .push(buffer.id());
374            let uses = self.uses.entry(buffer.id()).or_default();
375            uses.0 += 1;
376            uses.1 += usize::from(write);
377            let guard = buffer.guard();
378            guard.acquire(write);
379            self.guards.push((guard, write));
380            self.retained.extend(buffer.retained());
381        }
382        dispatch(&encoder);
383        Ok(())
384    }
385
386    /// Validates `args`, then refuses uses that nothing would order after earlier GPU work:
387    /// - reused memory whose previous owner's work is pending, in a command buffer without
388    ///   per-buffer fences (only ordered command buffers wait for that work);
389    /// - a buffer that another command buffer, not submitted yet, writes (or, for a write
390    ///   here, uses at all): its fence orders later work only once it is submitted.
391    fn check(&self, args: &[Arg<'_>]) -> Result<()> {
392        validate(args)?;
393        for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
394            if self.own.is_none() && buffer.previous_busy() {
395                return Err(GpuError::BufferInUse);
396            }
397            let (uses, writes) = buffer.unsubmitted();
398            let (own_uses, own_writes) = self.uses.get(&buffer.id()).copied().unwrap_or_default();
399            if writes > own_writes || (write && uses > own_uses) {
400                return Err(GpuError::BufferInUse);
401            }
402        }
403        Ok(())
404    }
405
406    /// Memory barriers inserted so far.
407    pub fn barriers(&self) -> usize {
408        self.barriers
409    }
410
411    /// Ends encoding, updating the timeline fence after this command buffer's work.
412    fn close(&mut self) {
413        autoreleasepool(|_| self.end_encoding());
414        self.device.flights().begin();
415    }
416
417    fn end_encoding(&mut self) {
418        if let Some(encoder) = self.encoder.take() {
419            if let Some((fence, _)) = &self.fence {
420                encoder.updateFence(fence);
421            }
422            if let Some(own) = &self.own {
423                encoder.updateFence(own);
424                for (guard, write) in &self.guards {
425                    guard.record(own, *write);
426                }
427            }
428            encoder.endEncoding();
429        }
430    }
431
432    /// Raises `event` to `value` once the GPU has finished the work encoded so far in this
433    /// command buffer ([`Event::wait`] waits for it on the CPU).
434    pub fn signal_event(&mut self, event: &Event, value: u64) {
435        autoreleasepool(|_| self.end_encoding());
436        self.raw
437            .encodeSignalEvent_value(ProtocolObject::from_ref(event.raw()), value);
438    }
439
440    /// Makes the work encoded after this call wait on the GPU until `event` reaches `value`
441    /// (the CPU raises it with [`Event::signal`]).
442    pub fn wait_event(&mut self, event: &Event, value: u64) {
443        autoreleasepool(|_| self.end_encoding());
444        self.raw
445            .encodeWaitForEvent_value(ProtocolObject::from_ref(event.raw()), value);
446        // The next encoder also orders after this buffer's earlier encoders.
447        self.after_wait = true;
448    }
449
450    /// Submits the work to the GPU. Dropping the returned [`Submitted`] waits for it and
451    /// discards its result; [`Submitted::wait`] reports GPU errors.
452    #[must_use = "dropping `Submitted` waits for the GPU and discards its errors; call `wait`"]
453    pub fn commit(self) -> Submitted {
454        Submitted {
455            pending: self.commit_then(|_| {}),
456            waited: false,
457        }
458    }
459
460    /// Submits the work and returns at once. `on_complete` runs exactly once with the
461    /// command buffer's result: on a Metal thread when the GPU finishes, or earlier on the
462    /// thread that calls [`Pending::wait`].
463    ///
464    /// `on_complete` must not block, in particular not on other GPU work: Metal runs a
465    /// queue's completion handlers one at a time, so waiting there for another command
466    /// buffer's completion can deadlock.
467    /// A panic in `on_complete` on Metal's thread is caught and reported on standard error,
468    /// so it never aborts the process; the result is still published to [`Pending::wait`].
469    #[must_use = "dropping `Pending` does not wait; keep it to `wait`, or drop it explicitly"]
470    pub fn commit_then(self, on_complete: impl FnOnce(Result<()>) + Send + 'static) -> Pending {
471        self.submit(on_complete, None)
472    }
473
474    /// [`commit_then`](Self::commit_then), but the GPU gets the work only once
475    /// [`Deferred::commit`] is called (or the [`Deferred`] is dropped). The command buffer
476    /// takes its place in the queue now: command buffers committed later run after it. Work
477    /// that waits on the GPU for an event the CPU may raise late is committed this way once
478    /// the event is raised, so the GPU never waits for it (Metal fails a command buffer that
479    /// waits for seconds).
480    #[must_use = "dropping `Deferred` commits at once"]
481    pub fn enqueue_then(
482        self,
483        on_complete: impl FnOnce(Result<()>) + Send + 'static,
484    ) -> (Pending, Deferred) {
485        let gate = Arc::new(Gate::default());
486        let pending = self.submit(on_complete, Some(gate.clone()));
487        let deferred = Deferred {
488            raw: pending.raw.clone(),
489            gate,
490        };
491        (pending, deferred)
492    }
493
494    fn submit(
495        mut self,
496        on_complete: impl FnOnce(Result<()>) + Send + 'static,
497        gate: Option<Arc<Gate>>,
498    ) -> Pending {
499        self.close();
500        let guards = self.guards.clone();
501        let finish = Arc::new(Finish {
502            completion: Mutex::new(Some(Completion {
503                on_complete: Box::new(on_complete),
504                own: self.own.clone(),
505                kernels: std::mem::take(&mut self.kernels),
506                unfenced: self.own.is_none(),
507                guards: std::mem::take(&mut self.guards),
508                _retained: std::mem::take(&mut self.retained),
509                device: self.device.clone(),
510            })),
511            result: Mutex::new(None),
512            done: Condvar::new(),
513        });
514        let shared = finish.clone();
515        let handler = RcBlock::new(move |raw: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
516            // A panic must not unwind into Metal's Objective-C caller, which aborts the
517            // process: report it instead (the result is published either way).
518            let completed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
519                // SAFETY: Metal passes the completed command buffer, valid for this call.
520                complete(&shared, unsafe { raw.as_ref() });
521            }));
522            if completed.is_err() {
523                eprintln!("nuvec-gpu: a command buffer's completion handler panicked");
524            }
525        });
526        autoreleasepool(|_| {
527            // SAFETY: the block is valid, and Metal copies it before this call returns. It
528            // is called at most once, on any thread: its captures are `Send` (the buffers
529            // are only retained or released, and the guards are atomics).
530            unsafe { self.raw.addCompletedHandler(RcBlock::as_ptr(&handler)) };
531            match gate {
532                Some(_) => self.raw.enqueue(),
533                None => self.raw.commit(),
534            }
535        });
536        let sequence = super::buffer::commit_sequence();
537        for (guard, write) in guards {
538            guard.committed_as(sequence);
539            guard.submitted(write);
540            // With a fence, these uses now order work committed later; without one, they
541            // count as committed once complete.
542            if self.own.is_some() {
543                guard.committed();
544            }
545        }
546        Pending {
547            raw: self.raw.clone(),
548            finish,
549            gate,
550        }
551    }
552
553    /// Whether a dispatch with `args` depends on dispatches since the last barrier:
554    /// read-after-write, write-after-write or write-after-read.
555    fn conflicts(&self, args: &[Arg<'_>]) -> bool {
556        args.iter().filter_map(Arg::buffer).any(|(b, _, write)| {
557            let id = b.id();
558            self.writes.contains(&id) || (write && self.reads.contains(&id))
559        })
560    }
561
562    fn encoder(&mut self) -> Result<&Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>> {
563        if self.encoder.is_none() {
564            let encoder = self
565                .raw
566                .computeCommandEncoderWithDispatchType(MTLDispatchType::Concurrent)
567                .ok_or_else(|| GpuError::CommandBuffer("cannot create a compute encoder".into()))?;
568            if let Some((fence, true)) = &self.fence {
569                encoder.waitForFence(fence);
570            }
571            if std::mem::take(&mut self.after_wait) {
572                if let Some((fence, _)) = &self.fence {
573                    encoder.waitForFence(fence);
574                }
575                if let Some(own) = &self.own {
576                    encoder.waitForFence(own);
577                }
578                self.reads.clear();
579                self.writes.clear();
580            }
581            self.encoder = Some(encoder);
582        }
583        Ok(self.encoder.as_ref().expect("just created"))
584    }
585}
586
587impl Drop for CommandBuffer {
588    /// Abandoned work is never submitted; release the buffers' guards. (Metal objects are
589    /// released in an autorelease pool: their deallocation may autorelease others.)
590    fn drop(&mut self) {
591        autoreleasepool(|_| {
592            if let Some(encoder) = self.encoder.take() {
593                encoder.endEncoding();
594            }
595            for (guard, write) in self.guards.drain(..) {
596                guard.committed();
597                guard.submitted(write);
598                guard.release(write, None); // abandoned before its fences were recorded
599            }
600            self.retained.clear();
601        });
602    }
603}
604
605fn size([width, height, depth]: [usize; 3]) -> MTLSize {
606    MTLSize {
607        width,
608        height,
609        depth,
610    }
611}
612
613fn validate(args: &[Arg<'_>]) -> Result<()> {
614    if args.len() > MAX_ARGS {
615        return Err(GpuError::InvalidArgument(format!(
616            "{} arguments; Metal allows {MAX_ARGS}",
617            args.len()
618        )));
619    }
620    for (index, arg) in args.iter().enumerate() {
621        match arg {
622            Arg::Bytes(bytes) if bytes.len() > MAX_INLINE_BYTES => {
623                return Err(GpuError::InvalidArgument(format!(
624                    "argument {index}: {} inline bytes; Metal allows {MAX_INLINE_BYTES}",
625                    bytes.len()
626                )));
627            }
628            Arg::Write(b, _) if b.is_mapped() => return Err(GpuError::ReadOnly),
629            Arg::Same(earlier, offset) => match args.get(*earlier) {
630                Some(Arg::Write(b, _)) if *offset <= b.len() => {}
631                _ => {
632                    return Err(GpuError::InvalidArgument(format!(
633                        "argument {index}: `Same({earlier}, {offset})` needs a written \
634                         (`Arg::Write`) argument and an offset within it"
635                    )));
636                }
637            },
638            Arg::Read(..) | Arg::Write(..)
639                if arg.buffer().is_some_and(|(b, offset, _)| offset > b.len()) =>
640            {
641                let (b, offset, _) = arg.buffer().expect("buffer argument");
642                return Err(GpuError::InvalidArgument(format!(
643                    "argument {index}: offset {offset} is past the end of a {}-byte buffer",
644                    b.len()
645                )));
646            }
647            _ => {}
648        }
649    }
650    Ok(())
651}
652
653/// What remains to do when a command buffer committed with
654/// [`CommandBuffer::commit_then`] completes.
655struct Completion {
656    on_complete: Box<dyn FnOnce(Result<()>) + Send>,
657    /// The command buffer's fence, recorded on the buffers it uses.
658    own: Option<Fence>,
659    /// The kernels dispatched, while profiling.
660    kernels: Vec<Arc<str>>,
661    guards: Vec<(Arc<Guard>, bool)>,
662    /// The command buffer had no timeline fence: its uses count as committed only now.
663    unfenced: bool,
664    _retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
665    device: Device,
666}
667
668// SAFETY: the retained Metal buffers are never accessed, only released (Metal objects may be
669// released from any thread); the guards are atomics, the callback is `Send`, and `Device` is
670// shared across threads by design (`Device::shared`).
671unsafe impl Send for Completion {}
672
673/// The completion of a command buffer committed with [`CommandBuffer::commit_then`] (run
674/// once, by whoever gets there first: a waiter or Metal's handler), and its result.
675struct Finish {
676    completion: Mutex<Option<Completion>>,
677    /// The command buffer's result, set once the completion has run.
678    result: Mutex<Option<Result<()>>>,
679    done: Condvar,
680}
681
682impl Finish {
683    /// Blocks until the completion has run (on any thread), and returns the result.
684    fn wait(&self) -> Result<()> {
685        let mut result = self.result.lock().unwrap_or_else(PoisonError::into_inner);
686        loop {
687            if let Some(r) = &*result {
688                return r.clone();
689            }
690            result = self
691                .done
692                .wait(result)
693                .unwrap_or_else(PoisonError::into_inner);
694        }
695    }
696}
697
698/// Publishes the result when dropped, so that waiters wake up even if `on_complete` panics.
699struct Publish<'a>(&'a Finish, Option<Result<()>>);
700
701impl Drop for Publish<'_> {
702    fn drop(&mut self) {
703        let mut result = self.0.result.lock().unwrap_or_else(PoisonError::into_inner);
704        *result = self.1.take();
705        self.0.done.notify_all();
706    }
707}
708
709/// Runs the completion once, whoever gets there first (a waiter or Metal's handler):
710/// releases the buffers' uses, ends the flight, runs `on_complete`, then publishes the
711/// result. This is the only place where submitted uses are released.
712fn complete(finish: &Finish, raw: &ProtocolObject<dyn MTLCommandBuffer>) {
713    let Some(c) = finish
714        .completion
715        .lock()
716        .unwrap_or_else(PoisonError::into_inner)
717        .take()
718    else {
719        return;
720    };
721    let result = status(raw);
722    let _publish = Publish(finish, Some(result.clone()));
723    super::profile::record(&c.kernels, raw.GPUEndTime() - raw.GPUStartTime());
724    for (guard, write) in c.guards {
725        if c.unfenced {
726            guard.committed();
727        }
728        guard.release(write, c.own.as_ref());
729    }
730    drop(c._retained);
731    c.device.flights().end();
732    (c.on_complete)(result);
733}
734
735/// A command buffer committed with [`CommandBuffer::commit_then`]. Dropping it does not
736/// wait; [`wait`](Self::wait) blocks until the GPU has finished and the completion has run
737/// (on this thread, if Metal's handler has not started it yet).
738#[must_use = "dropping `Pending` does not wait; keep it to `wait`, or drop it explicitly"]
739#[derive(Clone)]
740pub struct Pending {
741    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
742    finish: Arc<Finish>,
743    /// For [`CommandBuffer::enqueue_then`]: set once the work is committed.
744    gate: Option<Arc<Gate>>,
745}
746
747// SAFETY: a committed command buffer is only waited on (`waitUntilCompleted`, which Metal
748// allows from any thread) and retained or released; the completion state is behind a mutex.
749unsafe impl Send for Pending {}
750// SAFETY: as above; every method takes `&self` and waiting is thread-safe.
751unsafe impl Sync for Pending {}
752
753impl Pending {
754    /// Blocks until the GPU has finished; the completion has run when this returns (its
755    /// buffers are released and [`Device::in_flight`] no longer counts it).
756    pub fn wait(&self) {
757        let _ = self.result();
758    }
759
760    /// Waits as [`wait`](Self::wait) does and returns the command buffer's result.
761    pub fn wait_result(&self) -> Result<()> {
762        self.result()
763    }
764
765    fn result(&self) -> Result<()> {
766        if let Some(gate) = &self.gate {
767            gate.wait();
768        }
769        autoreleasepool(|_| {
770            self.raw.waitUntilCompleted();
771            complete(&self.finish, &self.raw);
772        });
773        self.finish.wait()
774    }
775}
776
777impl std::fmt::Debug for Pending {
778    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
779        f.debug_struct("Pending").finish_non_exhaustive()
780    }
781}
782
783/// Whether an enqueued command buffer has been committed.
784#[derive(Default)]
785struct Gate {
786    committed: Mutex<bool>,
787    changed: Condvar,
788}
789
790impl Gate {
791    fn wait(&self) {
792        let mut committed = self
793            .committed
794            .lock()
795            .unwrap_or_else(PoisonError::into_inner);
796        while !*committed {
797            committed = self
798                .changed
799                .wait(committed)
800                .unwrap_or_else(PoisonError::into_inner);
801        }
802    }
803}
804
805/// The commit of a command buffer queued with [`CommandBuffer::enqueue_then`]: call
806/// [`commit`](Self::commit), from any thread, to hand the work to the GPU. Dropping it
807/// commits as well.
808pub struct Deferred {
809    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
810    gate: Arc<Gate>,
811}
812
813// SAFETY: an enqueued command buffer is only committed (which Metal allows from any thread)
814// and retained or released.
815unsafe impl Send for Deferred {}
816
817impl Deferred {
818    /// Hands the work to the GPU.
819    pub fn commit(self) {}
820}
821
822impl Drop for Deferred {
823    fn drop(&mut self) {
824        autoreleasepool(|_| self.raw.commit());
825        *self
826            .gate
827            .committed
828            .lock()
829            .unwrap_or_else(PoisonError::into_inner) = true;
830        self.gate.changed.notify_all();
831    }
832}
833
834impl std::fmt::Debug for Deferred {
835    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
836        f.debug_struct("Deferred").finish_non_exhaustive()
837    }
838}
839
840/// Submitted GPU work. [`wait`](Self::wait) for it to finish; dropping it also waits (and
841/// discards a GPU error).
842#[must_use = "dropping `Submitted` waits for the GPU and discards its errors; call `wait`"]
843pub struct Submitted {
844    pending: Pending,
845    waited: bool,
846}
847
848impl Submitted {
849    /// Blocks until the GPU has finished and the buffers are released, then reports any
850    /// GPU error.
851    pub fn wait(mut self) -> Result<()> {
852        self.waited = true;
853        self.pending.result()
854    }
855}
856
857impl std::fmt::Debug for Submitted {
858    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
859        f.debug_struct("Submitted").finish_non_exhaustive()
860    }
861}
862
863impl Drop for Submitted {
864    fn drop(&mut self) {
865        if !self.waited {
866            self.pending.wait();
867        }
868    }
869}