use std::ptr::NonNull;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use objc2::rc::{Retained, autoreleasepool};
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, ptr) = autoreleasepool(|_| {
Ok::<_, GpuError>(match pool.alloc(self.raw(), len, gpu)? {
Some((raw, size, busy)) => {
let ptr = raw.contents().cast();
(Some(raw), size, busy, ptr)
}
None => (None, 0, None, NonNull::dangling()),
})
})?;
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> {
autoreleasepool(|_| self.map_file_now(path))
}
fn map_file_now(&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 map_len = crate::memory::mapping_size(len);
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 {
previous.wait_idle();
}
}
pub(crate) fn previous_busy(&self) -> bool {
self.previous.as_ref().is_some_and(|p| !p.idle())
}
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 unsubmitted(&self) -> (usize, usize) {
self.in_flight.unsubmitted()
}
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 {
last_commit: AtomicU64,
users: AtomicUsize,
writers: AtomicUsize,
uncommitted: AtomicUsize,
unsubmitted: AtomicUsize,
unsubmitted_writers: AtomicUsize,
fences: Mutex<Fences>,
idle: Condvar,
}
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>,
}
static COMMITS: AtomicU64 = AtomicU64::new(0);
pub(crate) fn commit_sequence() -> u64 {
COMMITS.fetch_add(1, Ordering::AcqRel) + 1
}
impl Guard {
pub(crate) fn committed_as(&self, sequence: u64) {
self.last_commit.fetch_max(sequence, Ordering::AcqRel);
}
pub(crate) fn in_latest_commit(&self) -> bool {
self.last_commit.load(Ordering::Acquire) >= COMMITS.load(Ordering::Acquire)
}
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 wait_idle(&self) {
if self.idle() {
return;
}
let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
while !self.idle() {
fences = self
.idle
.wait(fences)
.unwrap_or_else(PoisonError::into_inner);
}
}
pub(crate) fn unsubmitted(&self) -> (usize, usize) {
(
self.unsubmitted.load(Ordering::Acquire),
self.unsubmitted_writers.load(Ordering::Acquire),
)
}
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 submitted(&self, write: bool) {
if write {
self.unsubmitted_writers.fetch_sub(1, Ordering::AcqRel);
}
self.unsubmitted.fetch_sub(1, Ordering::AcqRel);
}
pub(crate) fn acquire(&self, write: bool) {
self.uncommitted.fetch_add(1, Ordering::AcqRel);
self.unsubmitted.fetch_add(1, Ordering::AcqRel);
self.users.fetch_add(1, Ordering::AcqRel);
if write {
self.unsubmitted_writers.fetch_add(1, Ordering::AcqRel);
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();
self.idle.notify_all();
}
drop(fences);
}
}
}
impl Drop for Buffer {
fn drop(&mut self) {
let Some(raw) = self.raw.take() else {
return;
};
autoreleasepool(|_| {
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();
guard.submitted(false);
for _ in 0..1000 {
let fence = new_fence();
guard.acquire(true);
guard.record(&fence, true);
guard.committed();
guard.submitted(true);
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());
}
}