use std::sync::{Arc, Mutex};
use block2::RcBlock;
use objc2::runtime::ProtocolObject;
use objc2_metal::MTLCommandBuffer;
const RING_CAPACITY: usize = 240;
#[derive(Debug, Default)]
pub struct GpuTimingSink {
inner: Mutex<Ring>,
}
#[derive(Debug, Default)]
struct Ring {
samples: Vec<f32>,
next: usize,
}
impl GpuTimingSink {
pub fn new() -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(Ring {
samples: Vec::with_capacity(RING_CAPACITY),
next: 0,
}),
})
}
fn push(&self, gpu_ms: f32) {
if let Ok(mut ring) = self.inner.try_lock() {
if ring.samples.len() < RING_CAPACITY {
ring.samples.push(gpu_ms);
} else {
let i = ring.next;
ring.samples[i] = gpu_ms;
ring.next = (i + 1) % RING_CAPACITY;
}
}
}
pub fn snapshot(&self) -> Vec<f32> {
self.inner
.lock()
.map(|r| r.samples.clone())
.unwrap_or_default()
}
pub fn stats(&self) -> Option<GpuTimingStats> {
let mut s = self.snapshot();
if s.is_empty() {
return None;
}
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = s.len();
let mean = s.iter().sum::<f32>() / n as f32;
let p = |q: f32| s[((q * (n as f32 - 1.0)).round() as usize).min(n - 1)];
Some(GpuTimingStats {
count: n,
mean_ms: mean,
p50_ms: p(0.50),
p99_ms: p(0.99),
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct GpuTimingStats {
pub count: usize,
pub mean_ms: f32,
pub p50_ms: f32,
pub p99_ms: f32,
}
pub unsafe fn add_gpu_timing_handler(cmd_buf_ptr: *mut std::ffi::c_void, sink: Arc<GpuTimingSink>) {
if cmd_buf_ptr.is_null() {
return;
}
let cmd_buf: &ProtocolObject<dyn MTLCommandBuffer> =
unsafe { &*(cmd_buf_ptr as *const ProtocolObject<dyn MTLCommandBuffer>) };
let handler = RcBlock::new(
move |finished: std::ptr::NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
let fb = unsafe { finished.as_ref() };
let start = fb.GPUStartTime();
let end = fb.GPUEndTime();
let elapsed = end - start;
if start > 0.0 && elapsed.is_finite() && elapsed > 0.0 {
sink.push((elapsed * 1000.0) as f32);
}
},
);
cmd_buf.addCompletedHandler(&*handler as *const _ as *mut _);
}