use std::ptr::NonNull;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice};
use super::Device;
use super::allocator::{Pool, Raw};
use super::command::Fence;
use crate::{GpuError, Result};
pub struct Buffer {
raw: Option<Raw>,
ptr: NonNull<u8>,
len: usize,
size: usize,
pool: Arc<Pool>,
in_flight: Arc<Guard>,
previous: Option<Arc<Guard>>,
mapped: bool,
}
unsafe impl Send for Buffer {}
unsafe impl Sync for Buffer {}
impl Device {
pub fn new_buffer(&self, len: usize) -> Result<Buffer> {
self.alloc_buffer(len, false)
}
pub fn new_gpu_buffer(&self, len: usize) -> Result<Buffer> {
self.alloc_buffer(len, true)
}
fn alloc_buffer(&self, len: usize, gpu: bool) -> Result<Buffer> {
let pool = self.pool().clone();
let (raw, size, busy) = match pool.alloc(self.raw(), len, gpu)? {
Some((raw, size, busy)) => (Some(raw), size, busy),
None => (None, 0, None),
};
let ptr = raw
.as_ref()
.map_or(NonNull::dangling(), |r| r.contents().cast());
Ok(Buffer {
raw,
ptr,
len,
size,
pool,
in_flight: Arc::default(),
previous: busy,
mapped: false,
})
}
pub fn map_file(&self, path: &std::path::Path) -> Result<Buffer> {
use std::os::fd::AsRawFd;
let io = |e: std::io::Error| GpuError::Io(format!("{}: {e}", path.display()));
let file = std::fs::File::open(path).map_err(io)?;
let len = usize::try_from(file.metadata().map_err(io)?.len())
.map_err(|_| GpuError::Io(format!("{}: too large", path.display())))?;
if len == 0 {
return self.new_buffer(0);
}
let page = usize::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }).unwrap_or(16384);
let map_len = len.div_ceil(page) * page;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
map_len,
libc::PROT_READ,
libc::MAP_SHARED,
file.as_raw_fd(),
0,
)
};
if ptr == libc::MAP_FAILED {
return Err(io(std::io::Error::last_os_error()));
}
let start = NonNull::new(ptr.cast::<u8>()).expect("mmap returned a non-null address");
let unmap = block2::RcBlock::new(move |p: NonNull<std::ffi::c_void>, n: usize| {
unsafe { libc::munmap(p.as_ptr(), n) };
});
let options = objc2_metal::MTLResourceOptions::StorageModeShared
| objc2_metal::MTLResourceOptions::HazardTrackingModeUntracked;
let raw = unsafe {
self.raw()
.newBufferWithBytesNoCopy_length_options_deallocator(
start.cast(),
map_len,
options,
Some(&unmap),
)
};
let Some(raw) = raw else {
unsafe { libc::munmap(ptr, map_len) };
return Err(GpuError::OutOfMemory(len));
};
self.pool().track(&raw);
Ok(Buffer {
raw: Some(raw),
ptr: start,
len,
size: 0,
pool: self.pool().clone(),
in_flight: Arc::default(),
previous: None,
mapped: true,
})
}
pub fn buffer_from(&self, bytes: &[u8]) -> Result<Buffer> {
let mut buffer = self.new_buffer(bytes.len())?;
buffer.contents_mut()?.copy_from_slice(bytes);
Ok(buffer)
}
}
impl Buffer {
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn in_use(&self) -> bool {
self.in_flight.users.load(Ordering::Acquire) != 0
}
fn wait_for_previous(&self) {
if let Some(previous) = &self.previous {
while !previous.idle() {
std::thread::yield_now();
}
}
}
pub fn contents(&self) -> Result<&[u8]> {
self.wait_for_previous();
if self.in_flight.writers.load(Ordering::Acquire) != 0 {
return Err(GpuError::BufferInUse);
}
if self.raw.is_none() {
return Ok(&[]);
}
Ok(unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) })
}
pub fn contents_mut(&mut self) -> Result<&mut [u8]> {
if self.mapped {
return Err(GpuError::ReadOnly);
}
if self.in_use() {
return Err(GpuError::BufferInUse);
}
self.wait_for_previous();
self.previous = None;
if self.raw.is_none() {
return Ok(&mut []);
}
Ok(unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) })
}
pub(crate) fn raw(&self) -> Option<&ProtocolObject<dyn MTLBuffer>> {
self.raw
.as_deref()
.or_else(|| self.pool.placeholder().map(|r| &**r))
}
pub(crate) fn retained(&self) -> Option<Raw> {
self.raw
.clone()
.or_else(|| self.pool.placeholder().cloned())
}
pub fn is_mapped(&self) -> bool {
self.mapped
}
pub(crate) fn guard(&self) -> Arc<Guard> {
self.in_flight.clone()
}
pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
let mut fences = self.in_flight.pending_fences(write);
if let Some(previous) = &self.previous {
fences.extend(previous.pending_fences(true));
}
fences
}
pub(crate) fn id(&self) -> usize {
Arc::as_ptr(&self.in_flight) as usize
}
}
#[derive(Default)]
pub(crate) struct Guard {
users: AtomicUsize,
writers: AtomicUsize,
uncommitted: AtomicUsize,
fences: Mutex<Fences>,
}
impl std::fmt::Debug for Guard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Guard")
.field("users", &self.users)
.field("writers", &self.writers)
.finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
struct Fences {
writer: Option<Fence>,
users: Vec<Fence>,
}
impl Guard {
pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
if self.users.load(Ordering::Acquire) == 0 {
return Vec::new();
}
let fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
if write {
fences.users.clone()
} else if self.writers.load(Ordering::Acquire) != 0 {
fences.writer.iter().cloned().collect()
} else {
Vec::new()
}
}
pub(crate) fn record(&self, fence: &Fence, write: bool) {
let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
if write {
fences.writer = Some(fence.clone());
}
fences.users.push(fence.clone());
}
pub(crate) fn idle(&self) -> bool {
self.users.load(Ordering::Acquire) == 0
}
pub(crate) fn committed_uses(&self) -> bool {
self.uncommitted.load(Ordering::Acquire) == 0
}
pub(crate) fn committed(&self) {
self.uncommitted.fetch_sub(1, Ordering::AcqRel);
}
pub(crate) fn acquire(&self, write: bool) {
self.uncommitted.fetch_add(1, Ordering::AcqRel);
self.users.fetch_add(1, Ordering::AcqRel);
if write {
self.writers.fetch_add(1, Ordering::AcqRel);
}
}
pub(crate) fn release(&self, write: bool, fence: Option<&Fence>) {
if let Some(fence) = fence {
let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
let same = |f: &Fence| Retained::as_ptr(f) == Retained::as_ptr(fence);
if let Some(i) = fences.users.iter().position(same) {
fences.users.swap_remove(i);
}
if fences.writer.as_ref().is_some_and(same) {
fences.writer = None;
}
}
if write {
self.writers.fetch_sub(1, Ordering::AcqRel);
}
if self.users.fetch_sub(1, Ordering::AcqRel) == 1 {
let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
if self.users.load(Ordering::Acquire) == 0 {
*fences = Fences::default();
}
drop(fences);
}
}
}
impl Drop for Buffer {
fn drop(&mut self) {
if let Some(raw) = self.raw.take() {
if self.mapped {
self.pool.untrack(&raw);
drop(raw);
return;
}
let busy = if self.in_use() {
Some(self.in_flight.clone())
} else {
self.previous.take().filter(|p| !p.idle())
};
self.pool.free(raw, self.size, busy);
}
}
}
impl std::fmt::Debug for Buffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Buffer")
.field("len", &self.len)
.field("in_use", &self.in_use())
.finish()
}
}
#[cfg(test)]
mod tests {
use objc2_metal::MTLDevice;
use super::*;
#[test]
fn completed_fences_leave_buffers_that_stay_in_use() {
let Some(device) = Device::system_default() else {
return;
};
let new_fence = || device.raw().newFence().expect("a fence");
let guard = Guard::default();
let long = new_fence();
guard.acquire(false);
guard.record(&long, false);
guard.committed();
for _ in 0..1000 {
let fence = new_fence();
guard.acquire(true);
guard.record(&fence, true);
guard.committed();
guard.release(true, Some(&fence));
}
let fences = guard.fences.lock().unwrap();
assert_eq!(fences.users.len(), 1);
assert!(fences.writer.is_none());
drop(fences);
guard.release(false, Some(&long));
assert!(guard.idle());
}
}