use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex, OnceLock};
use cudarc::driver::{CudaContext, CudaFunction, LaunchConfig, PushKernelArg, result};
use super::pool::{self, Pool};
use super::{I420, vulkan};
use crate::{Color, Error, Size};
const RESIZE_PTX: &str = include_str!("nv12_resize.ptx");
struct Kernels {
luma: CudaFunction,
chroma: CudaFunction,
}
type Loaded = HashMap<usize, Result<Arc<Kernels>, String>>;
fn kernels(ctx: &Arc<CudaContext>) -> Result<Arc<Kernels>, Error> {
static KERNELS: OnceLock<Mutex<Loaded>> = OnceLock::new();
let mut loaded = KERNELS
.get_or_init(Default::default)
.lock()
.expect("CUDA resize kernels poisoned");
loaded
.entry(ctx.ordinal())
.or_insert_with(|| {
let module = ctx
.load_module(cudarc::nvrtc::Ptx::from_src(RESIZE_PTX))
.map_err(|e| format!("load nv12_resize PTX: {e:?}"))?;
Ok(Arc::new(Kernels {
luma: module
.load_function("resize_luma")
.map_err(|e| format!("load resize_luma: {e:?}"))?,
chroma: module
.load_function("resize_chroma")
.map_err(|e| format!("load resize_chroma: {e:?}"))?,
}))
})
.clone()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize unavailable: {e}")))
}
struct Raw {
ctx: Arc<CudaContext>,
ptr: cudarc::driver::sys::CUdeviceptr,
len: usize,
}
impl Raw {
fn alloc(ctx: &Arc<CudaContext>, len: usize) -> Result<Self, Error> {
ctx.bind_to_thread()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
let ptr = unsafe { result::malloc_sync(len) }
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA alloc of {len} bytes: {e:?}")))?;
Ok(Self {
ctx: ctx.clone(),
ptr,
len,
})
}
}
impl Drop for Raw {
fn drop(&mut self) {
if self.ctx.bind_to_thread().is_ok() {
let _ = unsafe { result::free_sync(self.ptr) };
}
}
}
struct Device(Arc<CudaContext>);
impl pool::Alloc for Device {
type Buffer = Raw;
fn alloc(&self, len: usize) -> Result<Raw, Error> {
Raw::alloc(&self.0, len)
}
}
struct Buffer {
raw: std::mem::ManuallyDrop<Raw>,
pool: Option<Arc<Pool<Device>>>,
}
impl Buffer {
fn take(pool: &Arc<Pool<Device>>, len: usize) -> Result<Self, Error> {
Ok(Self {
raw: std::mem::ManuallyDrop::new(pool.take(len)?),
pool: Some(pool.clone()),
})
}
}
impl std::ops::Deref for Buffer {
type Target = Raw;
fn deref(&self) -> &Raw {
&self.raw
}
}
impl Drop for Buffer {
fn drop(&mut self) {
let raw = unsafe { std::mem::ManuallyDrop::take(&mut self.raw) };
match &self.pool {
Some(pool) => pool.put(raw.len, raw),
None => drop(raw),
}
}
}
#[derive(Clone)]
pub struct Frame {
buf: Arc<Buffer>,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) pitch: u32,
color: Option<Color>,
}
impl std::fmt::Debug for Frame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Frame")
.field("size", &self.size())
.field("pitch", &self.pitch)
.field("color", &self.color)
.field("pooled", &self.buf.pool.is_some())
.finish()
}
}
fn nv12_len(height: u32, pitch: u32) -> Result<usize, Error> {
(pitch as usize)
.checked_mul(height as usize)
.and_then(|luma| luma.checked_mul(3))
.map(|bytes| bytes / 2)
.ok_or_else(|| {
Error::Codec(anyhow::anyhow!(
"NV12 frame of {height} rows at pitch {pitch} is too large"
))
})
}
fn aligned_pitch(width: u32) -> Result<u32, Error> {
width
.checked_next_multiple_of(256)
.ok_or_else(|| Error::Codec(anyhow::anyhow!("frame width {width} is too wide for a CUDA NV12 pitch")))
}
impl Frame {
pub(crate) fn alloc(ctx: &Arc<CudaContext>, width: u32, height: u32, pitch: u32) -> Result<Self, Error> {
debug_assert!(pitch >= width && width.is_multiple_of(2) && height.is_multiple_of(2));
let raw = Raw::alloc(ctx, nv12_len(height, pitch)?)?;
Ok(Self {
buf: Arc::new(Buffer {
raw: std::mem::ManuallyDrop::new(raw),
pool: None,
}),
width,
height,
pitch,
color: None,
})
}
fn pooled(pool: &Arc<Pool<Device>>, size: Size, color: Option<Color>) -> Result<Self, Error> {
let pitch = aligned_pitch(size.width)?;
Ok(Self {
buf: Arc::new(Buffer::take(pool, nv12_len(size.height, pitch)?)?),
width: size.width,
height: size.height,
pitch,
color,
})
}
pub fn size(&self) -> Size {
Size::new(self.width, self.height)
}
pub fn color(&self) -> Option<Color> {
self.color
}
pub(crate) fn device_ptr(&self) -> u64 {
self.buf.ptr
}
pub(crate) fn download_i420(&self) -> Result<I420, Error> {
self.buf
.ctx
.bind_to_thread()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
let mut host = vec![0u8; self.buf.len];
unsafe { result::memcpy_dtoh_sync(&mut host, self.buf.ptr) }
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA download: {e:?}")))?;
let (w, h) = (self.width as usize, self.height as usize);
let (cw, ch) = (w / 2, h / 2);
let pitch = self.pitch as usize;
let mut data = vec![0u8; I420::len(self.size())?];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
for row in 0..h {
luma[row * w..row * w + w].copy_from_slice(&host[row * pitch..row * pitch + w]);
}
let uv_base = pitch * h;
for row in 0..ch {
let src = &host[uv_base + row * pitch..uv_base + row * pitch + w];
for col in 0..cw {
u_dst[row * cw + col] = src[col * 2];
v_dst[row * cw + col] = src[col * 2 + 1];
}
}
Ok(I420 {
width: self.width,
height: self.height,
data,
color: self.color,
})
}
pub fn resize(&self, size: Size) -> Result<Self, Error> {
size.validate("resize to")?;
let Size { width, height } = size;
let ctx = &self.buf.ctx;
let kernels = kernels(ctx)?;
let dst = match &self.buf.pool {
Some(pool) => Self::pooled(pool, size, self.color)?,
None => {
let mut dst = Self::alloc(ctx, width, height, aligned_pitch(width)?)?;
dst.color = self.color;
dst
}
};
let pitch = dst.pitch;
let stream = ctx.default_stream();
let block = (16u32, 16, 1);
let grid = |w: u32, h: u32| (w.div_ceil(16), h.div_ceil(16), 1);
let launch_err = |plane: &str, e| Error::Codec(anyhow::anyhow!("CUDA resize {plane}: {e:?}"));
unsafe {
stream
.launch_builder(&kernels.luma)
.arg(&self.buf.ptr)
.arg(&self.pitch)
.arg(&self.width)
.arg(&self.height)
.arg(&dst.buf.ptr)
.arg(&pitch)
.arg(&width)
.arg(&height)
.launch(LaunchConfig {
grid_dim: grid(width, height),
block_dim: block,
shared_mem_bytes: 0,
})
}
.map_err(|e| launch_err("luma", e))?;
let src_uv = self.buf.ptr + u64::from(self.pitch) * u64::from(self.height);
let dst_uv = dst.buf.ptr + u64::from(pitch) * u64::from(height);
let (src_pw, src_ph) = (self.width / 2, self.height / 2);
let (dst_pw, dst_ph) = (width / 2, height / 2);
unsafe {
stream
.launch_builder(&kernels.chroma)
.arg(&src_uv)
.arg(&self.pitch)
.arg(&src_pw)
.arg(&src_ph)
.arg(&dst_uv)
.arg(&pitch)
.arg(&dst_pw)
.arg(&dst_ph)
.launch(LaunchConfig {
grid_dim: grid(dst_pw, dst_ph),
block_dim: block,
shared_mem_bytes: 0,
})
}
.map_err(|e| launch_err("chroma", e))?;
stream
.synchronize()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize sync: {e:?}")))?;
Ok(dst)
}
}
const CONVERT_PTX: &str = include_str!("rgba_to_nv12.ptx");
pub struct Converter {
ctx: Arc<CudaContext>,
color: Color,
kernel: CudaFunction,
pool: Arc<Pool<Device>>,
}
impl std::fmt::Debug for Converter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Converter")
.field("device", &self.ctx.ordinal())
.field("color", &self.color)
.field("capacity", &self.pool.capacity())
.finish_non_exhaustive()
}
}
impl Converter {
pub fn new(ordinal: usize, color: Color, capacity: NonZeroUsize) -> Result<Self, Error> {
let ctx = CudaContext::new(ordinal)
.map_err(|e| Error::Unsupported(format!("CUDA device {ordinal} is unavailable: {e:?}")))?;
let kernel = ctx
.load_module(cudarc::nvrtc::Ptx::from_src(CONVERT_PTX))
.and_then(|module| module.load_function("rgba_to_nv12"))
.map_err(|e| Error::Unsupported(format!("CUDA color conversion unavailable: {e:?}")))?;
Ok(Self {
pool: Arc::new(Pool::new(Device(ctx.clone()), capacity)),
ctx,
color,
kernel,
})
}
pub fn color(&self) -> Color {
self.color
}
pub fn convert(&self, frame: &vulkan::Frame) -> Result<Frame, Error> {
if frame.cuda_context().ordinal() != self.ctx.ordinal() {
return Err(Error::Unsupported(format!(
"Vulkan image on CUDA device {} cannot be converted on device {}",
frame.cuda_context().ordinal(),
self.ctx.ordinal()
)));
}
let size = frame.size();
size.validate("Vulkan/CUDA conversion of")?;
let dst = Frame::pooled(&self.pool, size, Some(self.color))?;
let weights = self.color.coefficients();
let bgra = u32::from(frame.channels() == vulkan::Channels::Bgra);
let stream = frame.cuda_stream();
let (blocks_w, blocks_h) = (size.width / 2, size.height / 2);
unsafe {
stream
.launch_builder(&self.kernel)
.arg(&frame.cuda_surface())
.arg(&size.width)
.arg(&size.height)
.arg(&bgra)
.arg(&weights.y)
.arg(&weights.u)
.arg(&weights.v)
.arg(&dst.buf.ptr)
.arg(&dst.pitch)
.launch(LaunchConfig {
grid_dim: (blocks_w.div_ceil(16), blocks_h.div_ceil(16), 1),
block_dim: (16, 16, 1),
shared_mem_bytes: 0,
})
}
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA color conversion: {e:?}")))?;
stream
.synchronize()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA color conversion sync: {e:?}")))?;
Ok(dst)
}
}
#[cfg(test)]
#[path = "cuda_test.rs"]
mod tests;