use std::sync::atomic::{AtomicU8, Ordering};
#[cfg(feature = "gpu")]
pub(crate) mod gpu;
#[cfg(feature = "gpu")]
pub(crate) mod sim;
#[cfg(feature = "gpu")]
pub(crate) type GpuDispatch = gpu::Dispatch;
#[cfg(not(feature = "gpu"))]
#[allow(dead_code)]
pub(crate) struct GpuDispatch {
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,
}
#[cfg(feature = "gpu")]
pub(crate) const FIXED_ONE: f32 = gpu::FIXED_ONE;
#[cfg(not(feature = "gpu"))]
pub(crate) const FIXED_ONE: f32 = 65536.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Device {
#[default]
Cpu,
Gpu,
}
impl Device {
pub fn parse(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"cpu" => Some(Self::Cpu),
"gpu" | "cuda" | "metal" => Some(Self::Gpu),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Cpu => "cpu",
Self::Gpu => "gpu",
}
}
}
static DEFAULT_DEVICE: AtomicU8 = AtomicU8::new(u8::MAX);
pub fn default_device() -> Device {
match DEFAULT_DEVICE.load(Ordering::Relaxed) {
0 => Device::Cpu,
1 => Device::Gpu,
_ => {
let device = std::env::var("EVENTCV_DEVICE")
.ok()
.and_then(|name| Device::parse(&name))
.unwrap_or_default();
set_default_device(device);
device
}
}
}
pub fn set_default_device(device: Device) {
DEFAULT_DEVICE.store(device as u8, Ordering::Relaxed);
}
pub fn gpu_available() -> bool {
#[cfg(feature = "gpu")]
{
gpu::with_context(|_| ()).is_some()
}
#[cfg(not(feature = "gpu"))]
{
false
}
}
pub fn shutdown() {
#[cfg(feature = "gpu")]
{
gpu::shutdown();
}
}
pub fn unavailable_reason_public() -> String {
unavailable_reason()
}
pub(crate) fn unavailable_reason() -> String {
#[cfg(feature = "gpu")]
{
"device=\"gpu\" was requested but no compatible adapter was found (wgpu could not open a \
Vulkan, Metal or DX12 device here); use device=\"cpu\""
.to_owned()
}
#[cfg(not(feature = "gpu"))]
{
"device=\"gpu\" was requested but this build has no GPU support; rebuild with \
--features gpu, or use device=\"cpu\""
.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::{default_device, set_default_device, Device};
#[test]
fn device_names_round_trip() {
for device in [Device::Cpu, Device::Gpu] {
assert_eq!(Device::parse(device.as_str()), Some(device));
}
assert_eq!(Device::parse("CUDA"), Some(Device::Gpu));
assert_eq!(Device::parse(" Metal "), Some(Device::Gpu));
assert_eq!(Device::parse("tpu"), None);
}
#[test]
fn the_default_is_the_cpu_and_can_be_moved() {
set_default_device(Device::Cpu);
assert_eq!(default_device(), Device::Cpu);
set_default_device(Device::Gpu);
assert_eq!(default_device(), Device::Gpu);
set_default_device(Device::Cpu);
}
}
#[cfg(test)]
mod gpu_tests {
use super::{gpu_available, Device};
use crate::representation::{
AveragedTimeSurface, CountMask, EventCount, EventFrameData, Polarity, Representation,
TimeSurface, VoxelGrid,
};
use crate::{EventStream, EventStreamBuilder};
fn skip_without_gpu() -> bool {
if gpu_available() {
return false;
}
assert!(
std::env::var("EVENTCV_REQUIRE_GPU").is_err(),
"EVENTCV_REQUIRE_GPU is set but no adapter was found"
);
true
}
fn stream() -> EventStream {
let mut builder = EventStreamBuilder::new(64, 48, 0.001);
for index in 0..20_000i64 {
let x = ((index * 37) % 64) as u16;
let y = ((index * 11) % 48) as u16;
builder.push(x, y, index * 3, index % 3 != 0);
}
builder.build()
}
fn floats(frame: &crate::representation::EventFrame) -> Vec<f32> {
match frame.data() {
EventFrameData::F32(values) => values.clone(),
other => panic!("expected float data, got {other:?}"),
}
}
#[test]
fn integer_kernels_match_the_cpu_exactly() {
if skip_without_gpu() {
return;
}
let stream = stream();
for normalize in [false, true] {
let counter = EventCount::new(normalize);
assert_eq!(
counter.generate_on(&stream, Device::Gpu).unwrap().data(),
counter.generate(&stream).unwrap().data(),
"event counts are integer sums and must be identical, not merely close"
);
let polarity = Polarity::new(normalize);
assert_eq!(
polarity.generate_on(&stream, Device::Gpu).unwrap().data(),
polarity.generate(&stream).unwrap().data()
);
}
let mask = CountMask::new(99.0, false);
assert_eq!(
mask.generate_on(&stream, Device::Gpu).unwrap().data(),
mask.generate(&stream).unwrap().data()
);
}
#[test]
fn float_kernels_match_the_cpu_within_the_fixed_point_quantum() {
if skip_without_gpu() {
return;
}
let stream = stream();
let tolerance = 1e-3;
for (name, cpu, gpu) in [
(
"voxel",
floats(&VoxelGrid::new(5, 30.0).generate(&stream).unwrap()),
floats(
&VoxelGrid::new(5, 30.0)
.generate_on(&stream, Device::Gpu)
.unwrap(),
),
),
(
"tsurf",
floats(&TimeSurface::new(30.0).generate(&stream).unwrap()),
floats(
&TimeSurface::new(30.0)
.generate_on(&stream, Device::Gpu)
.unwrap(),
),
),
(
"atsurf",
floats(&AveragedTimeSurface::new(30.0).generate(&stream).unwrap()),
floats(
&AveragedTimeSurface::new(30.0)
.generate_on(&stream, Device::Gpu)
.unwrap(),
),
),
] {
assert_eq!(cpu.len(), gpu.len(), "{name}: shape");
let worst = cpu
.iter()
.zip(&gpu)
.map(|(cpu, gpu)| (cpu - gpu).abs())
.fold(0.0_f32, f32::max);
assert!(worst <= tolerance, "{name}: worst cell differs by {worst}");
}
}
#[test]
fn a_kernel_gives_the_same_answer_every_run() {
if skip_without_gpu() {
return;
}
let stream = stream();
let first = floats(
&VoxelGrid::new(5, 30.0)
.generate_on(&stream, Device::Gpu)
.unwrap(),
);
for _ in 0..3 {
assert_eq!(
first,
floats(
&VoxelGrid::new(5, 30.0)
.generate_on(&stream, Device::Gpu)
.unwrap()
),
"integer accumulation must make repeated runs bit-identical"
);
}
}
}