Skip to main content

nuvec_gpu/metal/
buffer.rs

1//! Shared-memory buffers.
2
3use std::ptr::NonNull;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7use objc2::runtime::ProtocolObject;
8use objc2_metal::MTLBuffer;
9
10use super::Device;
11use super::allocator::{Pool, Raw};
12use crate::{GpuError, Result};
13
14/// GPU memory in unified (shared) storage: the CPU reads and writes it in place.
15///
16/// Buffers come from the device's caching allocator and return to it when dropped. Hazard
17/// tracking is disabled at the Metal level (as in MLX); nuvec orders accesses itself. CPU
18/// access is refused while submitted GPU work uses the buffer, and GPU writes require
19/// `&mut Buffer`, so the CPU and the GPU never race on it.
20pub struct Buffer {
21    /// `None` for zero-length buffers, which own no Metal memory.
22    raw: Option<Raw>,
23    /// `raw.contents()`, fixed for the buffer's lifetime (cached: each lookup is an
24    /// Objective-C message).
25    ptr: NonNull<u8>,
26    len: usize,
27    /// Accounted (rounded) size in the pool.
28    size: usize,
29    pool: Arc<Pool>,
30    /// Number of submitted-but-unwaited command buffers that use this buffer.
31    in_flight: Arc<AtomicUsize>,
32}
33
34// SAFETY: Metal buffer objects may be retained, released and bound from any thread. The only
35// non-thread-safe part is the memory behind `contents()`, and access to it is synchronised by
36// Rust borrows (`contents_mut` needs `&mut self`; GPU writes need `&mut Buffer`) plus the
37// `in_flight` guard, which refuses CPU access while GPU work is pending.
38unsafe impl Send for Buffer {}
39// SAFETY: as above; `&Buffer` only allows shared reads of memory no GPU work is writing.
40unsafe impl Sync for Buffer {}
41
42impl Device {
43    /// A buffer of `len` bytes from the caching allocator. Its contents are unspecified
44    /// (a reused buffer holds stale bytes); fails with [`GpuError::OutOfMemory`] beyond the
45    /// memory limit.
46    pub fn new_buffer(&self, len: usize) -> Result<Buffer> {
47        let pool = self.pool().clone();
48        let (raw, size) = match pool.alloc(self.raw(), len)? {
49            Some((raw, size)) => (Some(raw), size),
50            None => (None, 0),
51        };
52        let ptr = raw
53            .as_ref()
54            .map_or(NonNull::dangling(), |r| r.contents().cast());
55        Ok(Buffer {
56            raw,
57            ptr,
58            len,
59            size,
60            pool,
61            in_flight: Arc::new(AtomicUsize::new(0)),
62        })
63    }
64
65    /// A buffer holding a copy of `bytes`.
66    pub fn buffer_from(&self, bytes: &[u8]) -> Result<Buffer> {
67        let mut buffer = self.new_buffer(bytes.len())?;
68        buffer.contents_mut()?.copy_from_slice(bytes);
69        Ok(buffer)
70    }
71}
72
73impl Buffer {
74    /// Size in bytes.
75    pub fn len(&self) -> usize {
76        self.len
77    }
78
79    /// Whether the buffer holds no bytes.
80    pub fn is_empty(&self) -> bool {
81        self.len == 0
82    }
83
84    /// Whether submitted GPU work still uses the buffer.
85    pub fn in_use(&self) -> bool {
86        self.in_flight.load(Ordering::Acquire) != 0
87    }
88
89    /// The bytes, if no submitted GPU work uses the buffer.
90    pub fn contents(&self) -> Result<&[u8]> {
91        if self.in_use() {
92            return Err(GpuError::BufferInUse);
93        }
94        if self.raw.is_none() {
95            return Ok(&[]);
96        }
97        // SAFETY: `ptr` is `raw.contents()`, at least `len` bytes (the allocation is at least the
98        // rounded size) of CPU-visible shared storage that lives as long as `raw`, which this
99        // borrow keeps alive. No GPU work uses the buffer (checked above), and new GPU writes
100        // need `&mut self`, which this shared borrow excludes.
101        Ok(unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) })
102    }
103
104    /// The bytes, mutably, if no submitted GPU work uses the buffer.
105    pub fn contents_mut(&mut self) -> Result<&mut [u8]> {
106        if self.in_use() {
107            return Err(GpuError::BufferInUse);
108        }
109        if self.raw.is_none() {
110            return Ok(&mut []);
111        }
112        // SAFETY: as in `contents`; additionally `&mut self` is exclusive, and `Buffer` is not
113        // `Clone`, so no other CPU reference to these bytes exists.
114        Ok(unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) })
115    }
116
117    pub(crate) fn raw(&self) -> Option<&ProtocolObject<dyn MTLBuffer>> {
118        self.raw.as_deref()
119    }
120
121    pub(crate) fn retained(&self) -> Option<Raw> {
122        self.raw.clone()
123    }
124
125    pub(crate) fn guard(&self) -> Arc<AtomicUsize> {
126        self.in_flight.clone()
127    }
128
129    /// Identity for hazard tracking (unique per live buffer, including empty ones).
130    pub(crate) fn id(&self) -> usize {
131        Arc::as_ptr(&self.in_flight) as usize
132    }
133}
134
135impl Drop for Buffer {
136    /// Returns the memory to the allocator's cache, unless GPU work still uses it (then it
137    /// is released when the GPU finishes, never reused early).
138    fn drop(&mut self) {
139        if let Some(raw) = self.raw.take() {
140            let reusable = !self.in_use();
141            self.pool.free(raw, self.size, reusable);
142        }
143    }
144}
145
146impl std::fmt::Debug for Buffer {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("Buffer")
149            .field("len", &self.len)
150            .field("in_use", &self.in_use())
151            .finish()
152    }
153}