use metal::{CompileOptions, Device, MTLResourceOptions, MTLSize};
use ndarray::Array4;
use std::ffi::c_void;
const MSL: &str = r#"
#include <metal_stdlib>
using namespace metal;
kernel void scatter_k(
device const long* t [[buffer(0)]],
device const int* x [[buffer(1)]],
device const int* y [[buffer(2)]],
device const int* p [[buffer(3)]],
device const long* starts [[buffer(4)]],
device const long* prefix [[buffer(5)]],
device const long* t0_arr [[buffer(6)]],
device const long* denom [[buffer(7)]],
device const long* row_map [[buffer(8)]],
device const long* col_map [[buffer(9)]],
device atomic_uint* accum [[buffer(10)]],
constant int& n_windows [[buffer(11)]],
constant long& n_memb [[buffer(12)]],
constant int& nbins [[buffer(13)]],
constant int& width [[buffer(14)]],
constant int& height [[buffer(15)]],
constant int& out_h [[buffer(16)]],
constant int& out_w [[buffer(17)]],
uint gid [[thread_position_in_grid]])
{
long m = (long)gid;
if (m >= n_memb) return;
// largest w with prefix[w] <= m (binary search over prefix[0..n_windows])
int lo = 0, hi = n_windows + 1;
while (lo < hi) { int mid = (lo + hi) >> 1; if (prefix[mid] <= m) lo = mid + 1; else hi = mid; }
int w = lo - 1;
long i = starts[w] + (m - prefix[w]);
int yi = y[i], xi = x[i];
if (yi < 0 || yi >= height || xi < 0 || xi >= width) return;
long yo = row_map[yi]; if (yo < 0) return;
long xo = col_map[xi]; if (xo < 0) return;
long num = (t[i] - t0_arr[w]) * (long)nbins;
long tidx = num / denom[w]; // integer floor (operands non-negative)
if (tidx > nbins - 1) tidx = nbins - 1;
long pol = p[i] > 0 ? p[i] : 0;
long chan = pol * (long)nbins + tidx;
long plane = (long)out_h * out_w;
long channels = 2 * (long)nbins;
long flat = w * channels * plane + chan * plane + yo * out_w + xo;
atomic_fetch_add_explicit(&accum[flat], 1u, memory_order_relaxed);
}
kernel void clip_cast_k(
device const uint* accum [[buffer(0)]],
device uchar* out [[buffer(1)]],
constant long& n [[buffer(2)]],
constant uint& cutoff [[buffer(3)]],
uint gid [[thread_position_in_grid]])
{
long i = (long)gid;
if (i >= n) return;
uint v = accum[i];
out[i] = (uchar)(v < cutoff ? v : cutoff);
}
"#;
#[allow(clippy::too_many_arguments)]
pub fn stacked_histogram_dense_metal(
t: &[i64],
x: &[i32],
y: &[i32],
p: &[i32],
grid: &[i64],
delta_t_us: i64,
nbins: usize,
count_cutoff: u32,
row_map: &[i64],
col_map: &[i64],
out_h: usize,
out_w: usize,
) -> Result<Array4<u8>, String> {
let n_windows = grid.len();
let channels = 2 * nbins;
let plane = out_h * out_w;
let buf_len = n_windows * channels * plane;
let n_events = t.len();
let mut t0 = vec![0i64; n_windows];
let mut denom = vec![1i64; n_windows];
let mut starts = vec![0i64; n_windows];
let mut prefix = vec![0i64; n_windows + 1];
for w in 0..n_windows {
let s = t.partition_point(|&v| v < grid[w] - delta_t_us); let e = t.partition_point(|&v| v <= grid[w]); if e <= s {
prefix[w + 1] = prefix[w];
continue;
}
starts[w] = s as i64;
t0[w] = t[s];
let span = t[e - 1] - t[s];
denom[w] = if span > 1 { span } else { 1 };
prefix[w + 1] = prefix[w] + (e - s) as i64;
}
let n_memb = prefix[n_windows];
let device = Device::system_default().ok_or("no Metal device available")?;
let lib = device
.new_library_with_source(MSL, &CompileOptions::new())
.map_err(|e| format!("MSL compile failed: {e}"))?;
let pso_scatter = {
let f = lib
.get_function("scatter_k", None)
.map_err(|e| e.to_string())?;
device
.new_compute_pipeline_state_with_function(&f)
.map_err(|e| e.to_string())?
};
let pso_clip = {
let f = lib
.get_function("clip_cast_k", None)
.map_err(|e| e.to_string())?;
device
.new_compute_pipeline_state_with_function(&f)
.map_err(|e| e.to_string())?
};
let queue = device.new_command_queue();
let shared = MTLResourceOptions::StorageModeShared;
let i64buf = |v: &[i64]| {
let n = v.len().max(1);
device.new_buffer_with_data(v.as_ptr() as *const c_void, (n * 8) as u64, shared)
};
let i32buf = |v: &[i32]| {
let n = v.len().max(1);
device.new_buffer_with_data(v.as_ptr() as *const c_void, (n * 4) as u64, shared)
};
let b_t = i64buf(t);
let b_x = i32buf(x);
let b_y = i32buf(y);
let b_p = i32buf(p);
let b_starts = i64buf(&starts);
let b_prefix = i64buf(&prefix);
let b_t0 = i64buf(&t0);
let b_denom = i64buf(&denom);
let b_row = i64buf(row_map);
let b_col = i64buf(col_map);
let b_accum = device.new_buffer((buf_len * 4) as u64, shared);
let b_out = device.new_buffer(buf_len as u64, shared);
unsafe {
std::ptr::write_bytes(b_accum.contents() as *mut u8, 0, buf_len * 4);
}
let nw = n_windows as i32;
let nb = nbins as i32;
let width = col_map.len() as i32;
let height = row_map.len() as i32;
let oh = out_h as i32;
let ow = out_w as i32;
let buf_len_i64 = buf_len as i64;
let set_i32 = |enc: &metal::ComputeCommandEncoderRef, idx: u64, v: &i32| {
enc.set_bytes(idx, 4, v as *const i32 as *const c_void);
};
let cmd = queue.new_command_buffer();
if n_memb > 0 && n_events > 0 {
let enc = cmd.new_compute_command_encoder();
enc.set_compute_pipeline_state(&pso_scatter);
for (idx, b) in [
&b_t, &b_x, &b_y, &b_p, &b_starts, &b_prefix, &b_t0, &b_denom, &b_row, &b_col, &b_accum,
]
.iter()
.enumerate()
{
enc.set_buffer(idx as u64, Some(b), 0);
}
set_i32(enc, 11, &nw);
enc.set_bytes(12, 8, &n_memb as *const i64 as *const c_void);
set_i32(enc, 13, &nb);
set_i32(enc, 14, &width);
set_i32(enc, 15, &height);
set_i32(enc, 16, &oh);
set_i32(enc, 17, &ow);
let tg = pso_scatter.max_total_threads_per_threadgroup().min(256);
enc.dispatch_threads(MTLSize::new(n_memb as u64, 1, 1), MTLSize::new(tg, 1, 1));
enc.end_encoding();
}
{
let enc = cmd.new_compute_command_encoder();
enc.set_compute_pipeline_state(&pso_clip);
enc.set_buffer(0, Some(&b_accum), 0);
enc.set_buffer(1, Some(&b_out), 0);
enc.set_bytes(2, 8, &buf_len_i64 as *const i64 as *const c_void);
enc.set_bytes(3, 4, &count_cutoff as *const u32 as *const c_void);
let tg = pso_clip.max_total_threads_per_threadgroup().min(256);
enc.dispatch_threads(MTLSize::new(buf_len as u64, 1, 1), MTLSize::new(tg, 1, 1));
enc.end_encoding();
}
cmd.commit();
cmd.wait_until_completed();
let out_slice = unsafe { std::slice::from_raw_parts(b_out.contents() as *const u8, buf_len) };
Array4::from_shape_vec((n_windows, channels, out_h, out_w), out_slice.to_vec())
.map_err(|e| format!("failed to shape Metal output: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ev_representations::stacked_histogram_dense::stacked_histogram_dense;
#[test]
fn metal_matches_cpu_dense() {
let row_map: Vec<i64> = (0..4).collect();
let col_map: Vec<i64> = (0..4).collect();
let t: Vec<i64> = vec![0, 10, 20, 50_000, 50_000, 60_000, 99_999, 100_000];
let x: Vec<i32> = vec![0, 1, 2, 3, 0, 1, 2, 9]; let y: Vec<i32> = vec![0, 1, 2, 3, 0, 1, 2, 3];
let p: Vec<i32> = vec![1, 0, 1, 0, 1, 0, 1, 1];
let grid: Vec<i64> = vec![50_000, 100_000];
let nbins = 2usize;
let cutoff = 10u32;
let cpu = stacked_histogram_dense(
&t,
&x.iter().map(|&v| v as i64).collect::<Vec<_>>(),
&y.iter().map(|&v| v as i64).collect::<Vec<_>>(),
&p.iter().map(|&v| v as i64).collect::<Vec<_>>(),
&grid,
50_000,
nbins,
cutoff,
&row_map,
&col_map,
4,
4,
);
let metal = stacked_histogram_dense_metal(
&t, &x, &y, &p, &grid, 50_000, nbins, cutoff, &row_map, &col_map, 4, 4,
)
.expect("metal backend");
assert_eq!(cpu.shape(), metal.shape());
assert_eq!(
cpu, metal,
"Metal output must be bit-identical to CPU dense"
);
}
}