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(mut self, on_complete: impl FnOnce(Result<()>) + Send + 'static) -> Pending {
471        self.close();
472        let guards = self.guards.clone();
473        let finish = Arc::new(Finish {
474            completion: Mutex::new(Some(Completion {
475                on_complete: Box::new(on_complete),
476                own: self.own.clone(),
477                kernels: std::mem::take(&mut self.kernels),
478                unfenced: self.own.is_none(),
479                guards: std::mem::take(&mut self.guards),
480                _retained: std::mem::take(&mut self.retained),
481                device: self.device.clone(),
482            })),
483            result: Mutex::new(None),
484            done: Condvar::new(),
485        });
486        let shared = finish.clone();
487        let handler = RcBlock::new(move |raw: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
488            // A panic must not unwind into Metal's Objective-C caller, which aborts the
489            // process: report it instead (the result is published either way).
490            let completed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
491                // SAFETY: Metal passes the completed command buffer, valid for this call.
492                complete(&shared, unsafe { raw.as_ref() });
493            }));
494            if completed.is_err() {
495                eprintln!("nuvec-gpu: a command buffer's completion handler panicked");
496            }
497        });
498        autoreleasepool(|_| {
499            // SAFETY: the block is valid, and Metal copies it before this call returns. It
500            // is called at most once, on any thread: its captures are `Send` (the buffers
501            // are only retained or released, and the guards are atomics).
502            unsafe { self.raw.addCompletedHandler(RcBlock::as_ptr(&handler)) };
503            self.raw.commit();
504        });
505        let sequence = super::buffer::commit_sequence();
506        for (guard, write) in guards {
507            guard.committed_as(sequence);
508            guard.submitted(write);
509            // With a fence, these uses now order work committed later; without one, they
510            // count as committed once complete.
511            if self.own.is_some() {
512                guard.committed();
513            }
514        }
515        Pending {
516            raw: self.raw.clone(),
517            finish,
518        }
519    }
520
521    /// Whether a dispatch with `args` depends on dispatches since the last barrier:
522    /// read-after-write, write-after-write or write-after-read.
523    fn conflicts(&self, args: &[Arg<'_>]) -> bool {
524        args.iter().filter_map(Arg::buffer).any(|(b, _, write)| {
525            let id = b.id();
526            self.writes.contains(&id) || (write && self.reads.contains(&id))
527        })
528    }
529
530    fn encoder(&mut self) -> Result<&Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>> {
531        if self.encoder.is_none() {
532            let encoder = self
533                .raw
534                .computeCommandEncoderWithDispatchType(MTLDispatchType::Concurrent)
535                .ok_or_else(|| GpuError::CommandBuffer("cannot create a compute encoder".into()))?;
536            if let Some((fence, true)) = &self.fence {
537                encoder.waitForFence(fence);
538            }
539            if std::mem::take(&mut self.after_wait) {
540                if let Some((fence, _)) = &self.fence {
541                    encoder.waitForFence(fence);
542                }
543                if let Some(own) = &self.own {
544                    encoder.waitForFence(own);
545                }
546                self.reads.clear();
547                self.writes.clear();
548            }
549            self.encoder = Some(encoder);
550        }
551        Ok(self.encoder.as_ref().expect("just created"))
552    }
553}
554
555impl Drop for CommandBuffer {
556    /// Abandoned work is never submitted; release the buffers' guards. (Metal objects are
557    /// released in an autorelease pool: their deallocation may autorelease others.)
558    fn drop(&mut self) {
559        autoreleasepool(|_| {
560            if let Some(encoder) = self.encoder.take() {
561                encoder.endEncoding();
562            }
563            for (guard, write) in self.guards.drain(..) {
564                guard.committed();
565                guard.submitted(write);
566                guard.release(write, None); // abandoned before its fences were recorded
567            }
568            self.retained.clear();
569        });
570    }
571}
572
573fn size([width, height, depth]: [usize; 3]) -> MTLSize {
574    MTLSize {
575        width,
576        height,
577        depth,
578    }
579}
580
581fn validate(args: &[Arg<'_>]) -> Result<()> {
582    if args.len() > MAX_ARGS {
583        return Err(GpuError::InvalidArgument(format!(
584            "{} arguments; Metal allows {MAX_ARGS}",
585            args.len()
586        )));
587    }
588    for (index, arg) in args.iter().enumerate() {
589        match arg {
590            Arg::Bytes(bytes) if bytes.len() > MAX_INLINE_BYTES => {
591                return Err(GpuError::InvalidArgument(format!(
592                    "argument {index}: {} inline bytes; Metal allows {MAX_INLINE_BYTES}",
593                    bytes.len()
594                )));
595            }
596            Arg::Write(b, _) if b.is_mapped() => return Err(GpuError::ReadOnly),
597            Arg::Same(earlier, offset) => match args.get(*earlier) {
598                Some(Arg::Write(b, _)) if *offset <= b.len() => {}
599                _ => {
600                    return Err(GpuError::InvalidArgument(format!(
601                        "argument {index}: `Same({earlier}, {offset})` needs a written \
602                         (`Arg::Write`) argument and an offset within it"
603                    )));
604                }
605            },
606            Arg::Read(..) | Arg::Write(..)
607                if arg.buffer().is_some_and(|(b, offset, _)| offset > b.len()) =>
608            {
609                let (b, offset, _) = arg.buffer().expect("buffer argument");
610                return Err(GpuError::InvalidArgument(format!(
611                    "argument {index}: offset {offset} is past the end of a {}-byte buffer",
612                    b.len()
613                )));
614            }
615            _ => {}
616        }
617    }
618    Ok(())
619}
620
621/// What remains to do when a command buffer committed with
622/// [`CommandBuffer::commit_then`] completes.
623struct Completion {
624    on_complete: Box<dyn FnOnce(Result<()>) + Send>,
625    /// The command buffer's fence, recorded on the buffers it uses.
626    own: Option<Fence>,
627    /// The kernels dispatched, while profiling.
628    kernels: Vec<Arc<str>>,
629    guards: Vec<(Arc<Guard>, bool)>,
630    /// The command buffer had no timeline fence: its uses count as committed only now.
631    unfenced: bool,
632    _retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
633    device: Device,
634}
635
636// SAFETY: the retained Metal buffers are never accessed, only released (Metal objects may be
637// released from any thread); the guards are atomics, the callback is `Send`, and `Device` is
638// shared across threads by design (`Device::shared`).
639unsafe impl Send for Completion {}
640
641/// The completion of a command buffer committed with [`CommandBuffer::commit_then`] (run
642/// once, by whoever gets there first: a waiter or Metal's handler), and its result.
643struct Finish {
644    completion: Mutex<Option<Completion>>,
645    /// The command buffer's result, set once the completion has run.
646    result: Mutex<Option<Result<()>>>,
647    done: Condvar,
648}
649
650impl Finish {
651    /// Blocks until the completion has run (on any thread), and returns the result.
652    fn wait(&self) -> Result<()> {
653        let mut result = self.result.lock().unwrap_or_else(PoisonError::into_inner);
654        loop {
655            if let Some(r) = &*result {
656                return r.clone();
657            }
658            result = self
659                .done
660                .wait(result)
661                .unwrap_or_else(PoisonError::into_inner);
662        }
663    }
664}
665
666/// Publishes the result when dropped, so that waiters wake up even if `on_complete` panics.
667struct Publish<'a>(&'a Finish, Option<Result<()>>);
668
669impl Drop for Publish<'_> {
670    fn drop(&mut self) {
671        let mut result = self.0.result.lock().unwrap_or_else(PoisonError::into_inner);
672        *result = self.1.take();
673        self.0.done.notify_all();
674    }
675}
676
677/// Runs the completion once, whoever gets there first (a waiter or Metal's handler):
678/// releases the buffers' uses, ends the flight, runs `on_complete`, then publishes the
679/// result. This is the only place where submitted uses are released.
680fn complete(finish: &Finish, raw: &ProtocolObject<dyn MTLCommandBuffer>) {
681    let Some(c) = finish
682        .completion
683        .lock()
684        .unwrap_or_else(PoisonError::into_inner)
685        .take()
686    else {
687        return;
688    };
689    let result = status(raw);
690    let _publish = Publish(finish, Some(result.clone()));
691    super::profile::record(&c.kernels, raw.GPUEndTime() - raw.GPUStartTime());
692    for (guard, write) in c.guards {
693        if c.unfenced {
694            guard.committed();
695        }
696        guard.release(write, c.own.as_ref());
697    }
698    drop(c._retained);
699    c.device.flights().end();
700    (c.on_complete)(result);
701}
702
703/// A command buffer committed with [`CommandBuffer::commit_then`]. Dropping it does not
704/// wait; [`wait`](Self::wait) blocks until the GPU has finished and the completion has run
705/// (on this thread, if Metal's handler has not started it yet).
706#[must_use = "dropping `Pending` does not wait; keep it to `wait`, or drop it explicitly"]
707#[derive(Clone)]
708pub struct Pending {
709    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
710    finish: Arc<Finish>,
711}
712
713// SAFETY: a committed command buffer is only waited on (`waitUntilCompleted`, which Metal
714// allows from any thread) and retained or released; the completion state is behind a mutex.
715unsafe impl Send for Pending {}
716// SAFETY: as above; every method takes `&self` and waiting is thread-safe.
717unsafe impl Sync for Pending {}
718
719impl Pending {
720    /// Blocks until the GPU has finished; the completion has run when this returns (its
721    /// buffers are released and [`Device::in_flight`] no longer counts it).
722    pub fn wait(&self) {
723        let _ = self.result();
724    }
725
726    /// Waits as [`wait`](Self::wait) does and returns the command buffer's result.
727    pub fn wait_result(&self) -> Result<()> {
728        self.result()
729    }
730
731    fn result(&self) -> Result<()> {
732        autoreleasepool(|_| {
733            self.raw.waitUntilCompleted();
734            complete(&self.finish, &self.raw);
735        });
736        self.finish.wait()
737    }
738}
739
740impl std::fmt::Debug for Pending {
741    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
742        f.debug_struct("Pending").finish_non_exhaustive()
743    }
744}
745
746/// Submitted GPU work. [`wait`](Self::wait) for it to finish; dropping it also waits (and
747/// discards a GPU error).
748#[must_use = "dropping `Submitted` waits for the GPU and discards its errors; call `wait`"]
749pub struct Submitted {
750    pending: Pending,
751    waited: bool,
752}
753
754impl Submitted {
755    /// Blocks until the GPU has finished and the buffers are released, then reports any
756    /// GPU error.
757    pub fn wait(mut self) -> Result<()> {
758        self.waited = true;
759        self.pending.result()
760    }
761}
762
763impl std::fmt::Debug for Submitted {
764    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765        f.debug_struct("Submitted").finish_non_exhaustive()
766    }
767}
768
769impl Drop for Submitted {
770    fn drop(&mut self) {
771        if !self.waited {
772            self.pending.wait();
773        }
774    }
775}