use std::collections::HashMap;
use std::ptr::NonNull;
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use block2::RcBlock;
use objc2::rc::{Retained, autoreleasepool};
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBarrierScope, MTLBuffer, MTLCommandBuffer, MTLCommandBufferStatus, MTLCommandEncoder,
MTLCommandQueue, MTLComputeCommandEncoder, MTLDevice, MTLDispatchType, MTLFence, MTLSize,
};
use super::buffer::Guard;
use super::{Buffer, Device, Event, Pipeline};
use crate::{GpuError, Result};
const MAX_ARGS: usize = 31;
const MAX_INLINE_BYTES: usize = 4096;
#[derive(Debug)]
pub enum Arg<'a> {
Read(&'a Buffer, usize),
Write(&'a mut Buffer, usize),
Bytes(&'a [u8]),
Same(usize, usize),
}
impl<'a> Arg<'a> {
pub fn read(buffer: &'a Buffer) -> Self {
Self::Read(buffer, 0)
}
pub fn write(buffer: &'a mut Buffer) -> Self {
Self::Write(buffer, 0)
}
pub fn bytes(bytes: &'a [u8]) -> Self {
Self::Bytes(bytes)
}
fn buffer(&self) -> Option<(&Buffer, usize, bool)> {
match self {
Self::Read(b, offset) => Some((b, *offset, false)),
Self::Write(b, offset) => Some((b, *offset, true)),
Self::Bytes(_) | Self::Same(..) => None,
}
}
}
pub(crate) type Fence = Retained<ProtocolObject<dyn MTLFence>>;
pub struct Timeline {
fence: Fence,
started: bool,
}
impl std::fmt::Debug for Timeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Timeline").finish_non_exhaustive()
}
}
pub struct CommandBuffer {
device: Device,
raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
fence: Option<(Fence, bool)>,
own: Option<Fence>,
waited: Vec<Fence>,
encoder: Option<Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>>,
reads: Vec<usize>,
writes: Vec<usize>,
barriers: usize,
guards: Vec<(Arc<Guard>, bool)>,
uses: HashMap<usize, (usize, usize)>,
retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
after_wait: bool,
kernels: Vec<Arc<str>>,
}
impl Device {
pub fn command_buffer(&self) -> Result<CommandBuffer> {
let raw = autoreleasepool(|_| self.queue().commandBuffer())
.ok_or_else(|| GpuError::CommandBuffer("cannot create a command buffer".into()))?;
Ok(CommandBuffer {
device: self.clone(),
raw,
fence: None,
own: None,
waited: Vec::new(),
encoder: None,
reads: Vec::new(),
writes: Vec::new(),
barriers: 0,
guards: Vec::new(),
uses: HashMap::new(),
retained: Vec::new(),
after_wait: false,
kernels: Vec::new(),
})
}
}
impl Device {
pub fn timeline(&self) -> Result<Timeline> {
let fence = autoreleasepool(|_| self.raw().newFence())
.ok_or_else(|| GpuError::CommandBuffer("cannot create a fence".into()))?;
Ok(Timeline {
fence,
started: false,
})
}
pub fn ordered_command_buffer(&self) -> Result<CommandBuffer> {
let fence = autoreleasepool(|_| self.raw().newFence())
.ok_or_else(|| GpuError::CommandBuffer("cannot create a fence".into()))?;
let mut cmd = self.command_buffer()?;
cmd.own = Some(fence);
Ok(cmd)
}
pub fn command_buffer_on(&self, timeline: &mut Timeline) -> Result<CommandBuffer> {
let mut cmd = self.command_buffer()?;
cmd.fence = Some((timeline.fence.clone(), timeline.started));
timeline.started = true;
Ok(cmd)
}
}
fn status(raw: &ProtocolObject<dyn MTLCommandBuffer>) -> Result<()> {
if raw.status() == MTLCommandBufferStatus::Error {
let message = raw.error().map_or_else(
|| "unknown error".to_owned(),
|e| e.localizedDescription().to_string(),
);
return Err(GpuError::CommandBuffer(message));
}
Ok(())
}
impl CommandBuffer {
pub unsafe fn dispatch(
&mut self,
pipeline: &Pipeline,
args: &[Arg<'_>],
threads: usize,
) -> Result<()> {
if threads == 0 {
return self.check(args);
}
let width = pipeline.execution_width().max(1);
let mut group = pipeline.max_threads_per_threadgroup().min(threads).max(1);
if group > width {
group -= group % width;
}
self.encode(pipeline, args, [threads, 1, 1], |encoder| {
encoder
.dispatchThreads_threadsPerThreadgroup(size([threads, 1, 1]), size([group, 1, 1]));
})
}
pub unsafe fn dispatch_groups(
&mut self,
pipeline: &Pipeline,
args: &[Arg<'_>],
groups: [usize; 3],
threads: [usize; 3],
) -> Result<()> {
let per_group: usize = threads.iter().product();
if per_group > pipeline.max_threads_per_threadgroup() {
return Err(GpuError::InvalidArgument(format!(
"{per_group} threads per threadgroup; `{}` allows {}",
pipeline.name(),
pipeline.max_threads_per_threadgroup()
)));
}
if groups.contains(&0) || per_group == 0 {
return self.check(args);
}
let extent = [0, 1, 2].map(|d| groups[d] * threads[d]);
self.encode(pipeline, args, extent, |encoder| {
encoder.dispatchThreadgroups_threadsPerThreadgroup(size(groups), size(threads));
})
}
pub unsafe fn dispatch_threads(
&mut self,
pipeline: &Pipeline,
args: &[Arg<'_>],
grid: [usize; 3],
group: [usize; 3],
) -> Result<()> {
let per_group: usize = group.iter().product();
if per_group > pipeline.max_threads_per_threadgroup() {
return Err(GpuError::InvalidArgument(format!(
"{per_group} threads per threadgroup; `{}` allows {}",
pipeline.name(),
pipeline.max_threads_per_threadgroup()
)));
}
if grid.contains(&0) || per_group == 0 {
return self.check(args);
}
self.encode(pipeline, args, grid, |encoder| {
encoder.dispatchThreads_threadsPerThreadgroup(size(grid), size(group));
})
}
fn encode(
&mut self,
pipeline: &Pipeline,
args: &[Arg<'_>],
extent: [usize; 3],
dispatch: impl FnOnce(&ProtocolObject<dyn MTLComputeCommandEncoder>),
) -> Result<()> {
autoreleasepool(|_| self.encode_now(pipeline, args, extent, dispatch))
}
fn encode_now(
&mut self,
pipeline: &Pipeline,
args: &[Arg<'_>],
extent: [usize; 3],
dispatch: impl FnOnce(&ProtocolObject<dyn MTLComputeCommandEncoder>),
) -> Result<()> {
self.check(args)?;
if super::profile::enabled() {
self.kernels.push(match super::profile::shapes() {
true => format!("{} {extent:?}", pipeline.name()).into(),
false => pipeline.name.clone(),
});
}
if self.conflicts(args) {
self.encoder()?
.memoryBarrierWithScope(MTLBarrierScope::Buffers);
self.barriers += 1;
self.reads.clear();
self.writes.clear();
}
let encoder = self.encoder()?.clone();
if let Some(own) = &self.own {
let own = Retained::as_ptr(own) as usize;
for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
for fence in buffer.pending_fences(write) {
let key = Retained::as_ptr(&fence) as usize;
let seen = self
.waited
.iter()
.any(|f| Retained::as_ptr(f) as usize == key);
if key != own && !seen {
encoder.waitForFence(&fence);
self.waited.push(fence);
}
}
}
}
encoder.setComputePipelineState(pipeline.raw());
for (index, arg) in args.iter().enumerate() {
match arg {
Arg::Bytes(bytes) => {
let data = NonNull::new(bytes.as_ptr().cast_mut())
.unwrap_or(NonNull::dangling())
.cast();
unsafe { encoder.setBytes_length_atIndex(data, bytes.len(), index) };
}
Arg::Read(..) | Arg::Write(..) => {
let (buffer, offset, _) = arg.buffer().expect("buffer argument");
unsafe { encoder.setBuffer_offset_atIndex(buffer.raw(), offset, index) };
}
Arg::Same(earlier, offset) => {
let (buffer, _, _) = args[*earlier].buffer().expect("validated");
unsafe { encoder.setBuffer_offset_atIndex(buffer.raw(), *offset, index) };
}
}
}
for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
(if write {
&mut self.writes
} else {
&mut self.reads
})
.push(buffer.id());
let uses = self.uses.entry(buffer.id()).or_default();
uses.0 += 1;
uses.1 += usize::from(write);
let guard = buffer.guard();
guard.acquire(write);
self.guards.push((guard, write));
self.retained.extend(buffer.retained());
}
dispatch(&encoder);
Ok(())
}
fn check(&self, args: &[Arg<'_>]) -> Result<()> {
validate(args)?;
for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
if self.own.is_none() && buffer.previous_busy() {
return Err(GpuError::BufferInUse);
}
let (uses, writes) = buffer.unsubmitted();
let (own_uses, own_writes) = self.uses.get(&buffer.id()).copied().unwrap_or_default();
if writes > own_writes || (write && uses > own_uses) {
return Err(GpuError::BufferInUse);
}
}
Ok(())
}
pub fn barriers(&self) -> usize {
self.barriers
}
fn close(&mut self) {
autoreleasepool(|_| self.end_encoding());
self.device.flights().begin();
}
fn end_encoding(&mut self) {
if let Some(encoder) = self.encoder.take() {
if let Some((fence, _)) = &self.fence {
encoder.updateFence(fence);
}
if let Some(own) = &self.own {
encoder.updateFence(own);
for (guard, write) in &self.guards {
guard.record(own, *write);
}
}
encoder.endEncoding();
}
}
pub fn signal_event(&mut self, event: &Event, value: u64) {
autoreleasepool(|_| self.end_encoding());
self.raw
.encodeSignalEvent_value(ProtocolObject::from_ref(event.raw()), value);
}
pub fn wait_event(&mut self, event: &Event, value: u64) {
autoreleasepool(|_| self.end_encoding());
self.raw
.encodeWaitForEvent_value(ProtocolObject::from_ref(event.raw()), value);
self.after_wait = true;
}
#[must_use = "dropping `Submitted` waits for the GPU and discards its errors; call `wait`"]
pub fn commit(self) -> Submitted {
Submitted {
pending: self.commit_then(|_| {}),
waited: false,
}
}
#[must_use = "dropping `Pending` does not wait; keep it to `wait`, or drop it explicitly"]
pub fn commit_then(mut self, on_complete: impl FnOnce(Result<()>) + Send + 'static) -> Pending {
self.close();
let guards = self.guards.clone();
let finish = Arc::new(Finish {
completion: Mutex::new(Some(Completion {
on_complete: Box::new(on_complete),
own: self.own.clone(),
kernels: std::mem::take(&mut self.kernels),
unfenced: self.own.is_none(),
guards: std::mem::take(&mut self.guards),
_retained: std::mem::take(&mut self.retained),
device: self.device.clone(),
})),
result: Mutex::new(None),
done: Condvar::new(),
});
let shared = finish.clone();
let handler = RcBlock::new(move |raw: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
let completed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
complete(&shared, unsafe { raw.as_ref() });
}));
if completed.is_err() {
eprintln!("nuvec-gpu: a command buffer's completion handler panicked");
}
});
autoreleasepool(|_| {
unsafe { self.raw.addCompletedHandler(RcBlock::as_ptr(&handler)) };
self.raw.commit();
});
let sequence = super::buffer::commit_sequence();
for (guard, write) in guards {
guard.committed_as(sequence);
guard.submitted(write);
if self.own.is_some() {
guard.committed();
}
}
Pending {
raw: self.raw.clone(),
finish,
}
}
fn conflicts(&self, args: &[Arg<'_>]) -> bool {
args.iter().filter_map(Arg::buffer).any(|(b, _, write)| {
let id = b.id();
self.writes.contains(&id) || (write && self.reads.contains(&id))
})
}
fn encoder(&mut self) -> Result<&Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>> {
if self.encoder.is_none() {
let encoder = self
.raw
.computeCommandEncoderWithDispatchType(MTLDispatchType::Concurrent)
.ok_or_else(|| GpuError::CommandBuffer("cannot create a compute encoder".into()))?;
if let Some((fence, true)) = &self.fence {
encoder.waitForFence(fence);
}
if std::mem::take(&mut self.after_wait) {
if let Some((fence, _)) = &self.fence {
encoder.waitForFence(fence);
}
if let Some(own) = &self.own {
encoder.waitForFence(own);
}
self.reads.clear();
self.writes.clear();
}
self.encoder = Some(encoder);
}
Ok(self.encoder.as_ref().expect("just created"))
}
}
impl Drop for CommandBuffer {
fn drop(&mut self) {
autoreleasepool(|_| {
if let Some(encoder) = self.encoder.take() {
encoder.endEncoding();
}
for (guard, write) in self.guards.drain(..) {
guard.committed();
guard.submitted(write);
guard.release(write, None); }
self.retained.clear();
});
}
}
fn size([width, height, depth]: [usize; 3]) -> MTLSize {
MTLSize {
width,
height,
depth,
}
}
fn validate(args: &[Arg<'_>]) -> Result<()> {
if args.len() > MAX_ARGS {
return Err(GpuError::InvalidArgument(format!(
"{} arguments; Metal allows {MAX_ARGS}",
args.len()
)));
}
for (index, arg) in args.iter().enumerate() {
match arg {
Arg::Bytes(bytes) if bytes.len() > MAX_INLINE_BYTES => {
return Err(GpuError::InvalidArgument(format!(
"argument {index}: {} inline bytes; Metal allows {MAX_INLINE_BYTES}",
bytes.len()
)));
}
Arg::Write(b, _) if b.is_mapped() => return Err(GpuError::ReadOnly),
Arg::Same(earlier, offset) => match args.get(*earlier) {
Some(Arg::Write(b, _)) if *offset <= b.len() => {}
_ => {
return Err(GpuError::InvalidArgument(format!(
"argument {index}: `Same({earlier}, {offset})` needs a written \
(`Arg::Write`) argument and an offset within it"
)));
}
},
Arg::Read(..) | Arg::Write(..)
if arg.buffer().is_some_and(|(b, offset, _)| offset > b.len()) =>
{
let (b, offset, _) = arg.buffer().expect("buffer argument");
return Err(GpuError::InvalidArgument(format!(
"argument {index}: offset {offset} is past the end of a {}-byte buffer",
b.len()
)));
}
_ => {}
}
}
Ok(())
}
struct Completion {
on_complete: Box<dyn FnOnce(Result<()>) + Send>,
own: Option<Fence>,
kernels: Vec<Arc<str>>,
guards: Vec<(Arc<Guard>, bool)>,
unfenced: bool,
_retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
device: Device,
}
unsafe impl Send for Completion {}
struct Finish {
completion: Mutex<Option<Completion>>,
result: Mutex<Option<Result<()>>>,
done: Condvar,
}
impl Finish {
fn wait(&self) -> Result<()> {
let mut result = self.result.lock().unwrap_or_else(PoisonError::into_inner);
loop {
if let Some(r) = &*result {
return r.clone();
}
result = self
.done
.wait(result)
.unwrap_or_else(PoisonError::into_inner);
}
}
}
struct Publish<'a>(&'a Finish, Option<Result<()>>);
impl Drop for Publish<'_> {
fn drop(&mut self) {
let mut result = self.0.result.lock().unwrap_or_else(PoisonError::into_inner);
*result = self.1.take();
self.0.done.notify_all();
}
}
fn complete(finish: &Finish, raw: &ProtocolObject<dyn MTLCommandBuffer>) {
let Some(c) = finish
.completion
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
else {
return;
};
let result = status(raw);
let _publish = Publish(finish, Some(result.clone()));
super::profile::record(&c.kernels, raw.GPUEndTime() - raw.GPUStartTime());
for (guard, write) in c.guards {
if c.unfenced {
guard.committed();
}
guard.release(write, c.own.as_ref());
}
drop(c._retained);
c.device.flights().end();
(c.on_complete)(result);
}
#[must_use = "dropping `Pending` does not wait; keep it to `wait`, or drop it explicitly"]
#[derive(Clone)]
pub struct Pending {
raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
finish: Arc<Finish>,
}
unsafe impl Send for Pending {}
unsafe impl Sync for Pending {}
impl Pending {
pub fn wait(&self) {
let _ = self.result();
}
pub fn wait_result(&self) -> Result<()> {
self.result()
}
fn result(&self) -> Result<()> {
autoreleasepool(|_| {
self.raw.waitUntilCompleted();
complete(&self.finish, &self.raw);
});
self.finish.wait()
}
}
impl std::fmt::Debug for Pending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pending").finish_non_exhaustive()
}
}
#[must_use = "dropping `Submitted` waits for the GPU and discards its errors; call `wait`"]
pub struct Submitted {
pending: Pending,
waited: bool,
}
impl Submitted {
pub fn wait(mut self) -> Result<()> {
self.waited = true;
self.pending.result()
}
}
impl std::fmt::Debug for Submitted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Submitted").finish_non_exhaustive()
}
}
impl Drop for Submitted {
fn drop(&mut self) {
if !self.waited {
self.pending.wait();
}
}
}