use std::sync::Arc;
use ffmpeg_next::{self as ffmpeg, ffi};
use thiserror::Error as ThisError;
use crate::pp_log::{PpLog, pp_error, pp_info};
use crate::{
buffer::MediaBuffer,
control::ControlMsg,
element::{Element, ElementType, Sink, element_pp_log},
elements::sink::renderer::SubmitError,
platform::{cuda::CudaDevice, ffmpeg::AvBufferRef},
};
pub trait CudaFrameRenderer: Send {
unsafe fn submit_nv12(
&self,
y: *const u8,
y_pitch: usize,
uv: *const u8,
uv_pitch: usize,
width: u32,
height: u32,
) -> std::result::Result<(), SubmitError>;
fn resize(&self, width: u32, height: u32) -> std::result::Result<(), SubmitError>;
}
#[derive(Debug, ThisError)]
pub enum CudaRendererError {
#[error("CudaRenderer only accepts CUDA frames, got {0:?}")]
UnsupportedFormat(ffmpeg::format::Pixel),
#[error("CUDA frame has no hardware frames context")]
MissingFramesContext,
#[error("CUDA frame belongs to a different CUDA context than this renderer")]
ForeignContext,
#[error("CudaRenderer only presents NV12 CUDA frames, got {0:?}")]
UnsupportedSoftwareFormat(ffmpeg::format::Pixel),
#[error("frame has no CUDA device pointer")]
MissingPlane,
#[error("submit failed: {0:?}")]
Submit(SubmitError),
#[error("resize failed: {0:?}")]
Resize(SubmitError),
}
pub struct CudaRenderer {
pp_log: PpLog,
name: Arc<str>,
inner: Box<dyn CudaFrameRenderer>,
device_ctx: *const ffi::AVHWDeviceContext,
_hw_device_ctx: Arc<AvBufferRef>,
}
unsafe impl Send for CudaRenderer {}
impl CudaRenderer {
pub fn new(
name: impl Into<String>,
device: &CudaDevice,
renderer: Box<dyn CudaFrameRenderer>,
) -> Self {
let name: Arc<str> = name.into().into();
let pp_log = element_pp_log(ElementType::CudaRenderer, &name, None);
let hw_device_ctx = device.retain();
let device_ctx = unsafe { (*hw_device_ctx.as_ptr()).data as *const ffi::AVHWDeviceContext };
pp_info!(pp_log: &pp_log, "created");
Self {
name,
pp_log,
inner: renderer,
device_ctx,
_hw_device_ctx: hw_device_ctx,
}
}
pub fn resize(&self, width: u32, height: u32) -> crate::error::Result<()> {
self.inner
.resize(width, height)
.inspect_err(|error| pp_error!(self, "resize failed: {error:?}"))
.map_err(CudaRendererError::Resize)?;
pp_info!(self, "resized: {width}x{height}");
Ok(())
}
fn submit(&self, frame: &ffmpeg::frame::Video) -> Result<(), CudaRendererError> {
let (y, y_pitch, uv, uv_pitch, width, height) = unsafe {
let ptr = frame.as_ptr();
let frames_ref = (*ptr).hw_frames_ctx;
if frames_ref.is_null() {
return Err(CudaRendererError::MissingFramesContext);
}
let frames_ctx = (*frames_ref).data as *const ffi::AVHWFramesContext;
if !std::ptr::eq((*frames_ctx).device_ctx, self.device_ctx) {
return Err(CudaRendererError::ForeignContext);
}
let sw_format = ffmpeg::format::Pixel::from((*frames_ctx).sw_format);
if sw_format != ffmpeg::format::Pixel::NV12 {
return Err(CudaRendererError::UnsupportedSoftwareFormat(sw_format));
}
let y = (*ptr).data[0];
let uv = (*ptr).data[1];
if y.is_null() || uv.is_null() {
return Err(CudaRendererError::MissingPlane);
}
(
y as *const u8,
(*ptr).linesize[0] as usize,
uv as *const u8,
(*ptr).linesize[1] as usize,
(*ptr).width as u32,
(*ptr).height as u32,
)
};
unsafe {
self.inner
.submit_nv12(y, y_pitch, uv, uv_pitch, width, height)
}
.map_err(CudaRendererError::Submit)
}
}
impl Element for CudaRenderer {
fn name(&self) -> Arc<str> {
self.name.clone()
}
fn element_type(&self) -> ElementType {
ElementType::CudaRenderer
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl Sink for CudaRenderer {
fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
let MediaBuffer::Video(frame) = buf else {
return Ok(());
};
if frame.format() != ffmpeg::format::Pixel::CUDA {
let format = frame.format();
pp_error!(self, "unsupported pixel format: {format:?}");
return Err(CudaRendererError::UnsupportedFormat(format).into());
}
self.submit(&frame)
.inspect_err(|error| pp_error!(self, "submit failed: {error}"))?;
Ok(())
}
fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
Ok(())
}
}
impl Drop for CudaRenderer {
fn drop(&mut self) {
pp_info!(self, "dropped: releasing hw_device_ctx");
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
use super::*;
use crate::{
element::Source,
elements::CudaDecoder,
pool::UnboundObjectPool,
test_support::{try_cuda_device, try_test_video},
};
#[derive(Default)]
struct RecordingRenderer {
submits: AtomicUsize,
last: Mutex<Option<(usize, usize, u32, u32)>>,
}
impl CudaFrameRenderer for RecordingRenderer {
unsafe fn submit_nv12(
&self,
y: *const u8,
y_pitch: usize,
uv: *const u8,
uv_pitch: usize,
width: u32,
height: u32,
) -> std::result::Result<(), SubmitError> {
assert!(!y.is_null() && !uv.is_null());
self.submits.fetch_add(1, Ordering::Relaxed);
*self.last.lock().unwrap() = Some((y_pitch, uv_pitch, width, height));
Ok(())
}
fn resize(&self, _width: u32, _height: u32) -> std::result::Result<(), SubmitError> {
Ok(())
}
}
#[test]
fn a_cpu_frame_is_rejected_as_a_typed_error() {
let Some((device, _cuda_lock)) = try_cuda_device() else {
return;
};
let mut renderer = CudaRenderer::new(
"cuda-renderer",
&device,
Box::new(RecordingRenderer::default()),
);
let frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::NV12, 64, 64);
let pool = UnboundObjectPool::new(0, ffmpeg::frame::Video::empty, |_| {});
let mut pooled = pool.get();
*pooled = frame;
let error = renderer
.consume(MediaBuffer::Video(Arc::new(pooled)))
.expect_err("a CPU frame must not be presented");
assert!(
error.to_string().contains("only accepts CUDA frames"),
"expected UnsupportedFormat, got {error}"
);
}
#[test]
fn a_frame_from_another_cuda_context_is_rejected() {
let Some((decode_device, _cuda_lock)) = try_cuda_device() else {
return;
};
let render_device = CudaDevice::new().expect("a second CUDA device");
let Some(path) = try_test_video() else {
return;
};
let frames = decode_some_frames(&decode_device, &path);
let Some(frame) = frames.into_iter().next() else {
eprintln!("skipping: NVDEC produced no frames for the configured fixture");
return;
};
let mut renderer = CudaRenderer::new(
"cuda-renderer",
&render_device,
Box::new(RecordingRenderer::default()),
);
let error = renderer
.consume(frame)
.expect_err("a frame from a foreign CUDA context must not be presented");
assert!(
error.to_string().contains("different CUDA context"),
"expected ForeignContext, got {error}"
);
}
#[test]
fn a_decoded_frame_reaches_the_graphics_side() {
let Some((device, _cuda_lock)) = try_cuda_device() else {
return;
};
let Some(path) = try_test_video() else {
return;
};
let frames = decode_some_frames(&device, &path);
if frames.is_empty() {
eprintln!("skipping: NVDEC produced no frames for the configured fixture");
return;
}
let expected = frames.len();
let inner = Arc::new(RecordingRenderer::default());
let mut renderer = CudaRenderer::new("cuda-renderer", &device, Box::new(inner.clone()));
for frame in frames {
renderer.consume(frame).expect("submit failed");
}
assert_eq!(inner.submits.load(Ordering::Relaxed), expected);
let (y_pitch, uv_pitch, width, height) =
inner.last.lock().unwrap().expect("nothing was submitted");
assert!(width > 0 && height > 0, "frame had no dimensions");
assert!(
y_pitch >= width as usize,
"y pitch {y_pitch} is narrower than width {width}"
);
assert!(
uv_pitch >= width as usize,
"uv pitch {uv_pitch} is narrower than width {width}"
);
}
impl CudaFrameRenderer for Arc<RecordingRenderer> {
unsafe fn submit_nv12(
&self,
y: *const u8,
y_pitch: usize,
uv: *const u8,
uv_pitch: usize,
width: u32,
height: u32,
) -> std::result::Result<(), SubmitError> {
unsafe { RecordingRenderer::submit_nv12(self, y, y_pitch, uv, uv_pitch, width, height) }
}
fn resize(&self, width: u32, height: u32) -> std::result::Result<(), SubmitError> {
RecordingRenderer::resize(self, width, height)
}
}
fn decode_some_frames(device: &CudaDevice, path: &str) -> Vec<MediaBuffer> {
let mut input = ffmpeg::format::input(path).expect("failed to open the test video");
let stream = input
.streams()
.best(ffmpeg::media::Type::Video)
.expect("the test video has no video stream");
let stream_index = stream.index();
let params = stream.parameters();
let mut decoder =
CudaDecoder::new("cuda-decoder", params, device, 4).expect("failed to open NVDEC");
let received = Arc::new(Mutex::new(Vec::new()));
decoder.src_pads()[0].link(Box::new(Collector {
received: received.clone(),
pp_log: element_pp_log(ElementType::Other, "collect", None),
}));
for (stream, packet) in input.packets() {
if stream.index() != stream_index {
continue;
}
decoder
.consume(MediaBuffer::Packet(Arc::new(packet)))
.expect("decode failed");
if received.lock().unwrap().len() >= 3 {
break;
}
}
std::mem::take(&mut *received.lock().unwrap())
}
struct Collector {
pp_log: PpLog,
received: Arc<Mutex<Vec<MediaBuffer>>>,
}
impl Element for Collector {
fn name(&self) -> Arc<str> {
"collect".into()
}
fn element_type(&self) -> ElementType {
ElementType::Other
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl Sink for Collector {
fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
if matches!(buf, MediaBuffer::Video(_)) {
self.received.lock().unwrap().push(buf);
}
Ok(())
}
fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
Ok(())
}
}
}