use std::sync::Mutex;
use rayon::prelude::*;
use wgpu::util::DeviceExt;
use crate::EventStream;
const SHADER: &str = include_str!("kernels.wgsl");
const WORKGROUP: u32 = 256;
pub(crate) const FIXED_ONE: f32 = 65536.0;
pub(crate) struct Context {
pub(crate) device: wgpu::Device,
pub(crate) queue: wgpu::Queue,
module: wgpu::ShaderModule,
layout: wgpu::BindGroupLayout,
pipeline_layout: wgpu::PipelineLayout,
pipelines: std::sync::Mutex<std::collections::HashMap<&'static str, wgpu::ComputePipeline>>,
pub(crate) sim: std::sync::OnceLock<super::sim::SimPipeline>,
}
fn layout_entry(binding: u32, uniform: bool, read_only: bool) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: if uniform {
wgpu::BufferBindingType::Uniform
} else {
wgpu::BufferBindingType::Storage { read_only }
},
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
static CONTEXT: Mutex<Option<Option<Context>>> = Mutex::new(None);
pub(crate) fn with_context<T>(body: impl FnOnce(&Context) -> T) -> Option<T> {
let mut guard = CONTEXT.lock().unwrap_or_else(|error| error.into_inner());
body(guard.get_or_insert_with(open).as_ref()?).into()
}
pub(crate) fn shutdown() {
let mut guard = CONTEXT.lock().unwrap_or_else(|error| error.into_inner());
if let Some(Some(context)) = guard.take() {
context.device.poll(wgpu::Maintain::Wait);
drop(context);
}
}
fn open() -> Option<Context> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
}))?;
let (device, queue) = pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("eventcv"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits {
max_storage_buffers_per_shader_stage: 6,
..wgpu::Limits::downlevel_defaults()
},
memory_hints: wgpu::MemoryHints::Performance,
},
None,
))
.ok()?;
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("eventcv representations"),
source: wgpu::ShaderSource::Wgsl(SHADER.into()),
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("eventcv kernels"),
entries: &[
layout_entry(0, true, true), layout_entry(1, false, true), layout_entry(2, false, true), layout_entry(3, false, false), layout_entry(4, false, false), ],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("eventcv kernels"),
bind_group_layouts: &[&layout],
push_constant_ranges: &[],
});
Some(Context {
device,
queue,
module,
layout,
pipeline_layout,
pipelines: std::sync::Mutex::new(std::collections::HashMap::new()),
sim: std::sync::OnceLock::new(),
})
}
impl Context {
fn with_pipeline<T>(&self, entry: &'static str, body: impl FnOnce(&wgpu::ComputePipeline) -> T) -> T {
let mut pipelines = self
.pipelines
.lock()
.unwrap_or_else(|error| error.into_inner());
let pipeline = pipelines.entry(entry).or_insert_with(|| {
self.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(entry),
layout: Some(&self.pipeline_layout),
module: &self.module,
entry_point: entry,
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
})
});
body(pipeline)
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct Params {
width: u32,
height: u32,
n_events: u32,
bins: u32,
scale_ms: f32,
span_ms: f32,
fixed_one: f32,
max_age_ticks: u32,
}
unsafe impl bytemuck::Zeroable for Params {}
unsafe impl bytemuck::Pod for Params {}
pub(crate) struct Dispatch {
pub(crate) entry: &'static str,
pub(crate) cells: usize,
pub(crate) initial: i32,
pub(crate) bins: u32,
pub(crate) span_ms: f32,
pub(crate) fixed_one: f32,
pub(crate) window_ms: Option<f64>,
pub(crate) needs_ages: bool,
}
pub(crate) enum GpuError {
Saturated,
Driver(String),
}
pub(crate) fn run(
context: &Context,
stream: &EventStream,
dispatch: &Dispatch,
) -> Result<Vec<i32>, GpuError> {
let (width, height) = stream.sensor_size();
let (xs, ys, ts, ps) = (stream.xs(), stream.ys(), stream.ts(), stream.ps());
let newest = ts.iter().copied().max().unwrap_or(0);
let mut coords: Vec<u32> = (0..stream.len())
.into_par_iter()
.map(|index| {
u32::from(xs[index])
| ((u32::from(ys[index]) & 0x7fff) << 16)
| ((ps[index] as u32) << 31)
})
.collect();
if coords.is_empty() {
coords.push(0);
}
let ages: Vec<u32> = if dispatch.needs_ages {
ts.par_iter()
.map(|t| newest.saturating_sub(*t).try_into().unwrap_or(u32::MAX))
.collect()
} else {
Vec::new()
};
let ages = if ages.is_empty() { vec![0] } else { ages };
let params = Params {
width: width as u32,
height: height as u32,
n_events: stream.len() as u32,
bins: dispatch.bins,
scale_ms: stream.timestamp_scale_ms() as f32,
span_ms: dispatch.span_ms,
fixed_one: dispatch.fixed_one,
max_age_ticks: dispatch
.window_ms
.map_or(u32::MAX, |window| max_age_ticks(window, stream.timestamp_scale_ms())),
};
let device = &context.device;
let storage = wgpu::BufferUsages::STORAGE;
let uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("params"),
contents: bytemuck::bytes_of(¶ms),
usage: wgpu::BufferUsages::UNIFORM,
});
let coords = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("coords"),
contents: bytemuck::cast_slice(&coords),
usage: storage,
});
let ages = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("ages"),
contents: bytemuck::cast_slice(&ages),
usage: storage,
});
let cells = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cells"),
contents: bytemuck::cast_slice(&vec![dispatch.initial; dispatch.cells]),
usage: storage | wgpu::BufferUsages::COPY_SRC,
});
let saturated = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("saturated"),
contents: bytemuck::bytes_of(&0u32),
usage: storage | wgpu::BufferUsages::COPY_SRC,
});
let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &context.layout,
entries: &[
binding(0, &uniform),
binding(1, &coords),
binding(2, &ages),
binding(3, &cells),
binding(4, &saturated),
],
});
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback"),
size: (dispatch.cells * 4 + 4) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
context.with_pipeline(dispatch.entry, |pipeline| {
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &bindings, &[]);
pass.dispatch_workgroups((stream.len() as u32).div_ceil(WORKGROUP).max(1), 1, 1);
});
encoder.copy_buffer_to_buffer(&cells, 0, &readback, 0, (dispatch.cells * 4) as u64);
encoder.copy_buffer_to_buffer(&saturated, 0, &readback, (dispatch.cells * 4) as u64, 4);
context.queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = sender.send(result);
});
device.poll(wgpu::Maintain::Wait);
receiver
.recv()
.map_err(|error| GpuError::Driver(error.to_string()))?
.map_err(|error| GpuError::Driver(error.to_string()))?;
let mapped = slice.get_mapped_range();
let values: Vec<i32> = bytemuck::cast_slice::<u8, i32>(&mapped[..dispatch.cells * 4]).to_vec();
let saturated = mapped[dispatch.cells * 4..].iter().any(|byte| *byte != 0);
drop(mapped);
readback.unmap();
if saturated {
return Err(GpuError::Saturated);
}
Ok(values)
}
fn max_age_ticks(window_ms: f64, scale_ms: f64) -> u32 {
if scale_ms <= 0.0 || !scale_ms.is_finite() {
return u32::MAX;
}
let inside = |ticks: i64| ticks >= 0 && (ticks as f64 * scale_ms) <= window_ms;
let mut ticks = (window_ms / scale_ms).floor() as i64;
while ticks > 0 && !inside(ticks) {
ticks -= 1;
}
while inside(ticks + 1) {
ticks += 1;
}
ticks.clamp(0, i64::from(u32::MAX)) as u32
}
fn binding(index: u32, buffer: &wgpu::Buffer) -> wgpu::BindGroupEntry<'_> {
wgpu::BindGroupEntry {
binding: index,
resource: buffer.as_entire_binding(),
}
}