use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use super::sys::*;
use crate::hw::select::HwCodec;
use crate::{Error, Result};
pub struct Device {
api: &'static Api,
ctx: CUcontext,
caps: Mutex<HashMap<(cudaVideoCodec, u32), Option<Caps>>>,
}
unsafe impl Send for Device {}
unsafe impl Sync for Device {}
#[derive(Clone, Copy, Debug)]
pub struct Caps {
pub max: (u32, u32),
pub min: (u32, u32),
}
static DEVICE: OnceLock<Option<Device>> = OnceLock::new();
pub fn get() -> Option<&'static Device> {
DEVICE.get_or_init(Device::open).as_ref()
}
fn device() -> Option<(&'static Api, CUdevice)> {
static DEV: OnceLock<Option<CUdevice>> = OnceLock::new();
let api = Api::get()?;
let dev = DEV.get_or_init(|| {
let mut dev = 0;
unsafe { ((api.cuInit)(0) == CUDA_SUCCESS && (api.cuDeviceGet)(&mut dev, 0) == CUDA_SUCCESS).then_some(dev) }
});
Some((api, (*dev)?))
}
pub fn present() -> bool {
device().is_some()
}
pub fn created() -> bool {
DEVICE.get().is_some_and(|d| d.is_some())
}
pub fn cuda_codec(codec: HwCodec) -> Option<cudaVideoCodec> {
Some(match codec {
HwCodec::H264 => CODEC_H264,
HwCodec::Hevc => CODEC_HEVC,
HwCodec::Vp8 => CODEC_VP8,
HwCodec::Vp9 => CODEC_VP9,
HwCodec::Av1 => CODEC_AV1,
_ => return None,
})
}
pub fn check(r: CUresult, what: &str) -> Result<()> {
if r == CUDA_SUCCESS { Ok(()) } else { Err(Error::Decode(format!("NVDEC {what}: CUDA error {r}"))) }
}
impl Device {
fn open() -> Option<Device> {
let (api, dev) = device()?;
let mut ctx = std::ptr::null_mut();
if unsafe { (api.cuDevicePrimaryCtxRetain)(&mut ctx, dev) } != CUDA_SUCCESS {
return None;
}
Some(Device { api, ctx, caps: Mutex::default() })
}
pub fn api(&self) -> &'static Api {
self.api
}
pub fn push(&self) -> Result<Current<'_>> {
check(unsafe { (self.api.cuCtxPushCurrent)(self.ctx) }, "context")?;
Ok(Current(self))
}
pub fn caps(&self, codec: cudaVideoCodec, bit_depth: u32) -> Option<Caps> {
if let Some(c) = self.caps.lock().unwrap().get(&(codec, bit_depth)) {
return *c;
}
let found = self.query(codec, bit_depth);
self.caps.lock().unwrap().insert((codec, bit_depth), found);
found
}
fn query(&self, codec: cudaVideoCodec, bit_depth: u32) -> Option<Caps> {
let _current = self.push().ok()?;
let mut c: CUVIDDECODECAPS = zeroed();
c.eCodecType = codec;
c.eChromaFormat = CHROMA_420;
c.nBitDepthMinus8 = bit_depth.checked_sub(8)?;
let ok = unsafe { (self.api.cuvidGetDecoderCaps)(&mut c) } == CUDA_SUCCESS;
(ok && c.bIsSupported != 0)
.then_some(Caps { max: (c.nMaxWidth, c.nMaxHeight), min: (c.nMinWidth as u32, c.nMinHeight as u32) })
}
}
pub struct Current<'a>(&'a Device);
impl Drop for Current<'_> {
fn drop(&mut self) {
let mut popped = std::ptr::null_mut();
unsafe { (self.0.api.cuCtxPopCurrent)(&mut popped) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hw::select::HwCodec;
#[test]
fn maps_only_video_codecs() {
assert_eq!(cuda_codec(HwCodec::H264), Some(CODEC_H264));
assert_eq!(cuda_codec(HwCodec::Av1), Some(CODEC_AV1));
assert_eq!(cuda_codec(HwCodec::Aac), None);
}
#[test]
fn reports_the_gpus_decoders() {
let Some(dev) = get() else {
eprintln!("skipped: no NVDEC");
return;
};
for codec in [CODEC_H264, CODEC_HEVC, CODEC_VP8, CODEC_VP9, CODEC_AV1] {
let caps = dev.caps(codec, 8);
eprintln!("codec {codec} 8-bit: {caps:?}");
assert!(caps.is_some_and(|c| c.max.0 >= 1920 && c.max.1 >= 1080), "codec {codec}");
}
for codec in [CODEC_HEVC, CODEC_VP9, CODEC_AV1] {
assert!(dev.caps(codec, 10).is_some(), "codec {codec} 10-bit");
}
assert!(dev.caps(CODEC_H264, 12).is_none(), "no 12-bit H.264");
}
#[test]
fn the_context_can_be_pushed_from_any_thread() {
let Some(dev) = get() else { return };
let handles: Vec<_> = (0..4).map(|_| std::thread::spawn(move || dev.push().map(|_| ()).is_ok())).collect();
assert!(handles.into_iter().all(|h| h.join().unwrap()));
}
}