use std::ptr::NonNull;
use std::sync::{Arc, Mutex, PoisonError};
use block2::RcBlock;
use objc2::rc::Retained;
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, 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<usize>,
encoder: Option<Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>>,
reads: Vec<usize>,
writes: Vec<usize>,
barriers: usize,
guards: Vec<(Arc<Guard>, bool)>,
retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
kernels: Vec<Arc<str>>,
}
impl Device {
pub fn command_buffer(&self) -> Result<CommandBuffer> {
let raw = 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(),
retained: Vec::new(),
kernels: Vec::new(),
})
}
}
impl Device {
pub fn timeline(&self) -> Result<Timeline> {
let fence = 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 = 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 fn dispatch(
&mut self,
pipeline: &Pipeline,
args: &[Arg<'_>],
threads: usize,
) -> Result<()> {
if threads == 0 {
return validate(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 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 validate(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 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 validate(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<()> {
if super::profile::enabled() {
self.kernels.push(match super::profile::shapes() {
true => format!("{} {extent:?}", pipeline.name()).into(),
false => pipeline.name.clone(),
});
}
validate(args)?;
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;
if key != own && !self.waited.contains(&key) {
encoder.waitForFence(&fence);
self.waited.push(key);
}
}
}
}
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 guard = buffer.guard();
guard.acquire(write);
self.guards.push((guard, write));
self.retained.extend(buffer.retained());
}
dispatch(&encoder);
Ok(())
}
pub fn barriers(&self) -> usize {
self.barriers
}
fn close(&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();
}
self.device.flights().begin();
}
pub fn commit(mut self) -> Submitted {
self.close();
self.raw.commit();
self.mark_committed();
Submitted {
device: self.device.clone(),
raw: self.raw.clone(),
unfenced: self.own.is_none(),
own: self.own.clone(),
guards: std::mem::take(&mut self.guards),
_retained: std::mem::take(&mut self.retained),
done: false,
}
}
pub fn commit_then(mut self, on_complete: impl FnOnce(Result<()>) + Send + 'static) -> Pending {
self.close();
let guards: Vec<Arc<Guard>> = match self.own {
Some(_) => self.guards.iter().map(|(g, _)| g.clone()).collect(),
None => Vec::new(),
};
let finish: Finish = Arc::new(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(),
})));
let shared = finish.clone();
let handler = RcBlock::new(move |raw: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
complete(&shared, unsafe { raw.as_ref() });
});
unsafe { self.raw.addCompletedHandler(RcBlock::as_ptr(&handler)) };
self.raw.commit();
for guard in guards {
guard.committed();
}
Pending {
raw: self.raw.clone(),
finish,
}
}
fn mark_committed(&self) {
if self.own.is_some() {
for (guard, _) in &self.guards {
guard.committed();
}
}
}
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);
}
self.encoder = Some(encoder);
}
Ok(self.encoder.as_ref().expect("just created"))
}
}
impl Drop for CommandBuffer {
fn drop(&mut self) {
if let Some(encoder) = self.encoder.take() {
encoder.endEncoding();
}
for (guard, write) in self.guards.drain(..) {
guard.committed();
guard.release(write, None); }
}
}
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) => {
let target = args.get(*earlier).and_then(Arg::buffer);
match target {
Some((b, ..)) if *offset <= b.len() => {}
_ => {
return Err(GpuError::InvalidArgument(format!(
"argument {index}: `Same({earlier}, {offset})` needs another \
buffer 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 {}
type Finish = Arc<Mutex<Option<Completion>>>;
fn complete(finish: &Finish, raw: &ProtocolObject<dyn MTLCommandBuffer>) {
let Some(c) = finish.lock().unwrap_or_else(PoisonError::into_inner).take() else {
return;
};
let result = status(raw);
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);
}
pub struct Pending {
raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
finish: Finish,
}
impl Pending {
pub fn wait(&self) {
self.raw.waitUntilCompleted();
complete(&self.finish, &self.raw);
}
}
impl std::fmt::Debug for Pending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pending").finish_non_exhaustive()
}
}
pub struct Submitted {
device: Device,
raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
unfenced: bool,
own: Option<Fence>,
guards: Vec<(Arc<Guard>, bool)>,
_retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
done: bool,
}
impl Submitted {
pub fn wait(mut self) -> Result<()> {
self.finish()
}
fn finish(&mut self) -> Result<()> {
if self.done {
return Ok(());
}
self.raw.waitUntilCompleted();
self.done = true;
for (guard, write) in self.guards.drain(..) {
if self.unfenced {
guard.committed();
}
guard.release(write, self.own.as_ref());
}
self.device.flights().end();
status(&self.raw)
}
}
impl Drop for Submitted {
fn drop(&mut self) {
let _ = self.finish();
}
}