#![cfg(feature = "cuda")]
use crate::error::{Result, VisionError};
use oxicuda_dnn::conv::conv_forward;
use oxicuda_dnn::types::{ConvolutionDescriptor, TensorDesc, TensorDescMut};
use oxicuda_dnn::{DnnError, DnnHandle};
use oxicuda_memory::DeviceBuffer;
use scirs2_core::ndarray::{s, Array2, ArrayView2};
pub fn cuda_is_available() -> bool {
oxicuda_driver::init().is_ok()
&& oxicuda_driver::device::Device::count()
.map(|c| c > 0)
.unwrap_or(false)
}
fn dnn_err(e: DnnError) -> VisionError {
VisionError::GpuError(format!("oxicuda-dnn: {e}"))
}
fn cuda_err(e: oxicuda_driver::CudaError) -> VisionError {
VisionError::GpuError(format!("oxicuda CUDA driver: {e}"))
}
fn build_context() -> Result<std::sync::Arc<oxicuda_driver::Context>> {
oxicuda_driver::init().map_err(|e| VisionError::GpuError(format!("CUDA unavailable: {e}")))?;
let count = oxicuda_driver::device::Device::count()
.map_err(|e| VisionError::GpuError(format!("device count: {e}")))?;
if count <= 0 {
return Err(VisionError::GpuError(
"no NVIDIA CUDA device available".into(),
));
}
let dev = oxicuda_driver::device::Device::get(0).map_err(cuda_err)?;
Ok(std::sync::Arc::new(
oxicuda_driver::Context::new(&dev).map_err(cuda_err)?,
))
}
pub fn cuda_convolve_2d(image: &ArrayView2<f64>, kernel: &ArrayView2<f64>) -> Result<Array2<f64>> {
let (height, width) = image.dim();
let (k_height, k_width) = kernel.dim();
if k_height % 2 == 0 || k_width % 2 == 0 {
return Err(VisionError::InvalidInput(
"Kernel must have odd dimensions".to_string(),
));
}
if height == 0 || width == 0 {
return Err(VisionError::InvalidInput(
"Image must be non-empty".to_string(),
));
}
if height < k_height || width < k_width {
return Err(VisionError::InvalidInput(format!(
"Kernel ({k_height}x{k_width}) larger than image ({height}x{width})"
)));
}
let k_half_h = k_height / 2;
let k_half_w = k_width / 2;
let out_h = height - k_height + 1; let out_w = width - k_width + 1;
let image_std = image.as_standard_layout();
let image_slice = image_std
.as_slice()
.ok_or_else(|| VisionError::GpuError("cuda_convolve_2d: image not contiguous".into()))?;
let kernel_std = kernel.as_standard_layout();
let kernel_slice = kernel_std
.as_slice()
.ok_or_else(|| VisionError::GpuError("cuda_convolve_2d: kernel not contiguous".into()))?;
let ctx = build_context()?;
let handle = DnnHandle::new(&ctx).map_err(dnn_err)?;
let d_input = DeviceBuffer::from_host(image_slice).map_err(cuda_err)?;
let d_filter = DeviceBuffer::from_host(kernel_slice).map_err(cuda_err)?;
let mut d_output = DeviceBuffer::<f64>::alloc(out_h * out_w).map_err(cuda_err)?;
let input_desc =
TensorDesc::<f64>::nchw(&d_input, 1, 1, height as u32, width as u32).map_err(dnn_err)?;
let filter_desc = TensorDesc::<f64>::nchw(&d_filter, 1, 1, k_height as u32, k_width as u32)
.map_err(dnn_err)?;
let mut output_desc =
TensorDescMut::<f64>::nchw(&mut d_output, 1, 1, out_h as u32, out_w as u32)
.map_err(dnn_err)?;
let conv_desc = ConvolutionDescriptor::conv2d(0, 0, 1, 1, 1, 1, 1).map_err(dnn_err)?;
match conv_forward::<f64>(
&handle,
&input_desc,
&filter_desc,
&mut output_desc,
&conv_desc,
None,
) {
Ok(()) => {}
Err(DnnError::WorkspaceRequired(bytes)) => {
let mut workspace = DeviceBuffer::<u8>::alloc(bytes).map_err(cuda_err)?;
conv_forward::<f64>(
&handle,
&input_desc,
&filter_desc,
&mut output_desc,
&conv_desc,
Some(&mut workspace),
)
.map_err(dnn_err)?;
}
Err(e) => return Err(dnn_err(e)),
}
let mut host_out = vec![0.0f64; out_h * out_w];
d_output.copy_to_host(&mut host_out).map_err(cuda_err)?;
let valid = Array2::from_shape_vec((out_h, out_w), host_out)
.map_err(|e| VisionError::GpuError(format!("output reshape: {e}")))?;
let mut output = Array2::zeros((height, width));
output
.slice_mut(s![k_half_h..k_half_h + out_h, k_half_w..k_half_w + out_w])
.assign(&valid);
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn cpu_reference(image: &ArrayView2<f64>, kernel: &ArrayView2<f64>) -> Array2<f64> {
let (height, width) = image.dim();
let (k_height, k_width) = kernel.dim();
let k_half_h = k_height / 2;
let k_half_w = k_width / 2;
let mut out = Array2::zeros((height, width));
for y in k_half_h..(height - k_half_h) {
for x in k_half_w..(width - k_half_w) {
let mut acc = 0.0;
for ky in 0..k_height {
for kx in 0..k_width {
acc += image[[y + ky - k_half_h, x + kx - k_half_w]] * kernel[[ky, kx]];
}
}
out[[y, x]] = acc;
}
}
out
}
#[test]
fn cuda_convolve_2d_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let image = Array2::from_shape_vec((5, 5), (1..=25).map(|v| v as f64).collect())
.expect("valid 5x5 image");
let kernel =
Array2::from_shape_vec((3, 3), vec![0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0])
.expect("valid 3x3 kernel");
let got = cuda_convolve_2d(&image.view(), &kernel.view()).expect("cuda_convolve_2d failed");
let expected = cpu_reference(&image.view(), &kernel.view());
let max_diff = got
.iter()
.zip(expected.iter())
.map(|(g, e)| (g - e).abs())
.fold(0.0f64, f64::max);
assert!(max_diff < 1e-9, "max abs diff {max_diff} exceeds 1e-9");
}
#[test]
fn cuda_convolve_2d_rejects_even_kernel() {
let image = Array2::<f64>::zeros((5, 5));
let kernel = Array2::<f64>::zeros((2, 2));
assert!(cuda_convolve_2d(&image.view(), &kernel.view()).is_err());
}
#[test]
fn cuda_convolve_2d_rejects_empty_image() {
let image = Array2::<f64>::zeros((0, 0));
let kernel = Array2::<f64>::zeros((3, 3));
assert!(cuda_convolve_2d(&image.view(), &kernel.view()).is_err());
}
#[test]
fn cuda_convolve_2d_asymmetric_kernel_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let image = Array2::from_shape_vec((5, 7), (1..=35).map(|v| v as f64).collect::<Vec<_>>())
.expect("valid 5×7 image");
let kernel =
Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("valid 3×3 kernel");
let got = cuda_convolve_2d(&image.view(), &kernel.view()).expect("cuda_convolve_2d failed");
let expected = cpu_reference(&image.view(), &kernel.view());
let max_diff = got
.iter()
.zip(expected.iter())
.map(|(g, e)| (g - e).abs())
.fold(0.0f64, f64::max);
assert!(
max_diff < 1e-9,
"non-symmetric kernel max abs diff {max_diff:.3e} exceeds 1e-9 \
(image 5×7, kernel [[1..9]])"
);
let (height, width) = got.dim(); let k_half = 1_usize;
for row_idx in [0, height - 1] {
for col_idx in 0..width {
let val = got[[row_idx, col_idx]];
assert!(
val == 0.0,
"border pixel [{row_idx},{col_idx}] is {val} (expected exactly 0.0)"
);
}
}
for row_idx in k_half..(height - k_half) {
for col_idx in [0, width - 1] {
let val = got[[row_idx, col_idx]];
assert!(
val == 0.0,
"border pixel [{row_idx},{col_idx}] is {val} (expected exactly 0.0)"
);
}
}
}
}