Skip to main content

nuvec_gpu/metal/
command.rs

1//! Command encoding with minimal barriers, and submission.
2
3use std::ptr::NonNull;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7use objc2::rc::Retained;
8use objc2::runtime::ProtocolObject;
9use objc2_metal::{
10    MTLBarrierScope, MTLBuffer, MTLCommandBuffer, MTLCommandBufferStatus, MTLCommandEncoder,
11    MTLCommandQueue, MTLComputeCommandEncoder, MTLDispatchType, MTLSize,
12};
13
14use super::{Buffer, Device, Pipeline};
15use crate::{GpuError, Result};
16
17/// Metal's limit on buffer argument slots per kernel.
18const MAX_ARGS: usize = 31;
19/// Metal's limit for inline bytes (`setBytes`).
20const MAX_INLINE_BYTES: usize = 4096;
21
22/// A kernel argument, bound to the slot of its position in the argument list.
23#[derive(Debug)]
24pub enum Arg<'a> {
25    /// A buffer the kernel only reads, from byte `offset` on.
26    Read(&'a Buffer, usize),
27    /// A buffer the kernel writes (and may read), from byte `offset` on.
28    Write(&'a mut Buffer, usize),
29    /// Small constant data copied into the command stream (at most 4 KiB).
30    Bytes(&'a [u8]),
31}
32
33impl<'a> Arg<'a> {
34    /// The whole buffer, read-only.
35    pub fn read(buffer: &'a Buffer) -> Self {
36        Self::Read(buffer, 0)
37    }
38
39    /// The whole buffer, written.
40    pub fn write(buffer: &'a mut Buffer) -> Self {
41        Self::Write(buffer, 0)
42    }
43
44    /// Inline constant bytes.
45    pub fn bytes(bytes: &'a [u8]) -> Self {
46        Self::Bytes(bytes)
47    }
48
49    fn buffer(&self) -> Option<(&Buffer, usize, bool)> {
50        match self {
51            Self::Read(b, offset) => Some((b, *offset, false)),
52            Self::Write(b, offset) => Some((b, *offset, true)),
53            Self::Bytes(_) => None,
54        }
55    }
56}
57
58/// Work being recorded for the GPU: one concurrent compute encoder, with memory barriers
59/// inserted only where a dispatch depends on an earlier one.
60pub struct CommandBuffer {
61    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
62    encoder: Option<Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>>,
63    /// Buffers read and written since the last barrier.
64    reads: Vec<usize>,
65    writes: Vec<usize>,
66    barriers: usize,
67    /// In-flight guards of the buffers used, and the buffers themselves (kept alive).
68    guards: Vec<Arc<AtomicUsize>>,
69    retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
70}
71
72impl Device {
73    /// A new command buffer on this device's queue.
74    pub fn command_buffer(&self) -> Result<CommandBuffer> {
75        let raw = self
76            .queue()
77            .commandBuffer()
78            .ok_or_else(|| GpuError::CommandBuffer("cannot create a command buffer".into()))?;
79        Ok(CommandBuffer {
80            raw,
81            encoder: None,
82            reads: Vec::new(),
83            writes: Vec::new(),
84            barriers: 0,
85            guards: Vec::new(),
86            retained: Vec::new(),
87        })
88    }
89}
90
91impl CommandBuffer {
92    /// Encodes `pipeline` over `threads` threads (a 1-D grid) with `args` bound to slots
93    /// `0..args.len()`.
94    pub fn dispatch(
95        &mut self,
96        pipeline: &Pipeline,
97        args: &[Arg<'_>],
98        threads: usize,
99    ) -> Result<()> {
100        validate(args)?;
101        if threads == 0 {
102            return Ok(());
103        }
104        if self.conflicts(args) {
105            self.encoder()?
106                .memoryBarrierWithScope(MTLBarrierScope::Buffers);
107            self.barriers += 1;
108            self.reads.clear();
109            self.writes.clear();
110        }
111        let encoder = self.encoder()?.clone();
112        encoder.setComputePipelineState(pipeline.raw());
113        for (index, arg) in args.iter().enumerate() {
114            match arg {
115                Arg::Bytes(bytes) => {
116                    let data = NonNull::new(bytes.as_ptr().cast_mut())
117                        .unwrap_or(NonNull::dangling())
118                        .cast();
119                    // SAFETY: `validate` checked `index < 31` and `bytes.len() <= 4096`;
120                    // Metal copies the bytes during this call, so the pointer need only be
121                    // valid for `bytes.len()` bytes now, which the slice guarantees.
122                    unsafe { encoder.setBytes_length_atIndex(data, bytes.len(), index) };
123                }
124                Arg::Read(..) | Arg::Write(..) => {
125                    let (buffer, offset, _) = arg.buffer().expect("buffer argument");
126                    // SAFETY: `validate` checked `index < 31` and `offset <= buffer.len()`, so
127                    // the binding stays within the buffer; the buffer is retained by this
128                    // command buffer until the GPU has finished.
129                    unsafe { encoder.setBuffer_offset_atIndex(buffer.raw(), offset, index) };
130                }
131            }
132        }
133        for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
134            (if write {
135                &mut self.writes
136            } else {
137                &mut self.reads
138            })
139            .push(buffer.id());
140            let guard = buffer.guard();
141            guard.fetch_add(1, Ordering::AcqRel);
142            self.guards.push(guard);
143            self.retained.extend(buffer.retained());
144        }
145        let width = pipeline.execution_width().max(1);
146        let mut group = pipeline.max_threads_per_threadgroup().min(threads).max(1);
147        if group > width {
148            group -= group % width;
149        }
150        let size = |width| MTLSize {
151            width,
152            height: 1,
153            depth: 1,
154        };
155        encoder.dispatchThreads_threadsPerThreadgroup(size(threads), size(group));
156        Ok(())
157    }
158
159    /// Memory barriers inserted so far.
160    pub fn barriers(&self) -> usize {
161        self.barriers
162    }
163
164    /// Submits the work to the GPU.
165    pub fn commit(mut self) -> Submitted {
166        if let Some(encoder) = self.encoder.take() {
167            encoder.endEncoding();
168        }
169        self.raw.commit();
170        Submitted {
171            raw: self.raw.clone(),
172            guards: std::mem::take(&mut self.guards),
173            _retained: std::mem::take(&mut self.retained),
174            done: false,
175        }
176    }
177
178    /// Whether a dispatch with `args` depends on dispatches since the last barrier:
179    /// read-after-write, write-after-write or write-after-read.
180    fn conflicts(&self, args: &[Arg<'_>]) -> bool {
181        args.iter().filter_map(Arg::buffer).any(|(b, _, write)| {
182            let id = b.id();
183            self.writes.contains(&id) || (write && self.reads.contains(&id))
184        })
185    }
186
187    fn encoder(&mut self) -> Result<&Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>> {
188        if self.encoder.is_none() {
189            let encoder = self
190                .raw
191                .computeCommandEncoderWithDispatchType(MTLDispatchType::Concurrent)
192                .ok_or_else(|| GpuError::CommandBuffer("cannot create a compute encoder".into()))?;
193            self.encoder = Some(encoder);
194        }
195        Ok(self.encoder.as_ref().expect("just created"))
196    }
197}
198
199impl Drop for CommandBuffer {
200    /// Abandoned work is never submitted; release the buffers' guards.
201    fn drop(&mut self) {
202        if let Some(encoder) = self.encoder.take() {
203            encoder.endEncoding();
204        }
205        for guard in self.guards.drain(..) {
206            guard.fetch_sub(1, Ordering::AcqRel);
207        }
208    }
209}
210
211fn validate(args: &[Arg<'_>]) -> Result<()> {
212    if args.len() > MAX_ARGS {
213        return Err(GpuError::InvalidArgument(format!(
214            "{} arguments; Metal allows {MAX_ARGS}",
215            args.len()
216        )));
217    }
218    for (index, arg) in args.iter().enumerate() {
219        match arg {
220            Arg::Bytes(bytes) if bytes.len() > MAX_INLINE_BYTES => {
221                return Err(GpuError::InvalidArgument(format!(
222                    "argument {index}: {} inline bytes; Metal allows {MAX_INLINE_BYTES}",
223                    bytes.len()
224                )));
225            }
226            Arg::Read(..) | Arg::Write(..)
227                if arg.buffer().is_some_and(|(b, offset, _)| offset > b.len()) =>
228            {
229                let (b, offset, _) = arg.buffer().expect("buffer argument");
230                return Err(GpuError::InvalidArgument(format!(
231                    "argument {index}: offset {offset} is past the end of a {}-byte buffer",
232                    b.len()
233                )));
234            }
235            _ => {}
236        }
237    }
238    Ok(())
239}
240
241/// Submitted GPU work. [`wait`](Self::wait) for it to finish; dropping it also waits.
242pub struct Submitted {
243    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
244    guards: Vec<Arc<AtomicUsize>>,
245    _retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
246    done: bool,
247}
248
249impl Submitted {
250    /// Blocks until the GPU has finished, then reports any GPU error.
251    pub fn wait(mut self) -> Result<()> {
252        self.finish()
253    }
254
255    fn finish(&mut self) -> Result<()> {
256        if self.done {
257            return Ok(());
258        }
259        self.raw.waitUntilCompleted();
260        self.done = true;
261        for guard in self.guards.drain(..) {
262            guard.fetch_sub(1, Ordering::AcqRel);
263        }
264        if self.raw.status() == MTLCommandBufferStatus::Error {
265            let message = self.raw.error().map_or_else(
266                || "unknown error".to_owned(),
267                |e| e.localizedDescription().to_string(),
268            );
269            return Err(GpuError::CommandBuffer(message));
270        }
271        Ok(())
272    }
273}
274
275impl Drop for Submitted {
276    fn drop(&mut self) {
277        let _ = self.finish();
278    }
279}