use std::borrow::Cow;
use yuv::{YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, YuvRange, YuvStandardMatrix, rgba_to_yuv420};
use crate::Error;
pub(crate) enum Frame {
#[cfg(target_os = "macos")]
Surface(macos::Surface),
#[cfg(target_os = "windows")]
Texture(d3d11::Texture),
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Cuda(cuda::Frame),
I420(I420),
}
impl Frame {
pub(crate) fn width(&self) -> u32 {
match self {
#[cfg(target_os = "macos")]
Frame::Surface(s) => s.width,
#[cfg(target_os = "windows")]
Frame::Texture(t) => t.width,
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Frame::Cuda(c) => c.width,
Frame::I420(i) => i.width,
}
}
pub(crate) fn height(&self) -> u32 {
match self {
#[cfg(target_os = "macos")]
Frame::Surface(s) => s.height,
#[cfg(target_os = "windows")]
Frame::Texture(t) => t.height,
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Frame::Cuda(c) => c.height,
Frame::I420(i) => i.height,
}
}
pub(crate) fn to_i420(&self) -> Result<Cow<'_, I420>, Error> {
match self {
#[cfg(target_os = "macos")]
Frame::Surface(s) => Ok(Cow::Owned(s.download_i420()?)),
#[cfg(target_os = "windows")]
Frame::Texture(t) => Ok(Cow::Owned(t.download_i420()?)),
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Frame::Cuda(c) => Ok(Cow::Owned(c.download_i420()?)),
Frame::I420(i) => Ok(Cow::Borrowed(i)),
}
}
}
#[derive(Clone)]
pub(crate) struct I420 {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl I420 {
pub(crate) fn len(width: u32, height: u32) -> usize {
let luma = width as usize * height as usize;
luma + luma / 2
}
pub(crate) fn from_rgba(rgba: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
rgba_to_yuv420(
&mut planar,
rgba,
stride,
YuvRange::Limited,
YuvStandardMatrix::Bt601,
YuvConversionMode::Balanced,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("rgba_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
pub(crate) fn from_bgra(bgra: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
use yuv::bgra_to_yuv420;
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
bgra_to_yuv420(
&mut planar,
bgra,
stride,
YuvRange::Limited,
YuvStandardMatrix::Bt601,
YuvConversionMode::Balanced,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("bgra_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
pub(crate) fn from_planes(
y: &[u8],
u: &[u8],
v: &[u8],
y_stride: usize,
uv_stride: usize,
width: u32,
height: u32,
) -> Self {
let (w, h) = (width as usize, height as usize);
let (cw, ch) = (w / 2, h / 2);
let mut data = vec![0u8; Self::len(width, height)];
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(&y[row * y_stride..row * y_stride + w]);
}
for row in 0..ch {
u_dst[row * cw..row * cw + cw].copy_from_slice(&u[row * uv_stride..row * uv_stride + cw]);
v_dst[row * cw..row * cw + cw].copy_from_slice(&v[row * uv_stride..row * uv_stride + cw]);
}
Self { width, height, data }
}
#[cfg(target_os = "linux")]
pub(crate) fn from_rgb(rgb: &[u8], width: u32, height: u32) -> Result<Self, Error> {
use yuv::rgb_to_yuv420;
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
rgb_to_yuv420(
&mut planar,
rgb,
width * 3,
YuvRange::Limited,
YuvStandardMatrix::Bt601,
YuvConversionMode::Balanced,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("rgb_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
#[cfg(target_os = "linux")]
pub(crate) fn from_yuyv(yuyv: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
use yuv::{YuvPackedImage, yuyv422_to_yuv420};
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
let packed = YuvPackedImage {
yuy: yuyv,
yuy_stride: stride,
width,
height,
};
yuyv422_to_yuv420(&mut planar, &packed)
.map_err(|e| Error::Codec(anyhow::anyhow!("yuyv422_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
#[cfg(target_os = "windows")]
pub(crate) fn from_nv12(nv12: &[u8], width: u32, height: u32) -> Result<Self, Error> {
let (w, h) = (width as usize, height as usize);
let luma = w * h;
let chroma = luma / 4;
let need = luma + 2 * chroma;
if nv12.len() < need {
return Err(Error::Codec(anyhow::anyhow!(
"NV12 buffer too small: {} < {need} for {width}x{height}",
nv12.len()
)));
}
let mut data = vec![0u8; Self::len(width, height)];
data[..luma].copy_from_slice(&nv12[..luma]);
let (u_dst, v_dst) = data[luma..].split_at_mut(chroma);
deinterleave_uv(&nv12[luma..need], u_dst, v_dst);
Ok(Self { width, height, data })
}
pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
use std::cell::RefCell;
use fast_image_resize::images::{Image, ImageRef};
use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
thread_local! {
static RESIZER: RefCell<Resizer> = RefCell::new(Resizer::new());
}
let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Bilinear));
let plane = |resizer: &mut Resizer,
src: &[u8],
sw: u32,
sh: u32,
dst: &mut [u8],
dw: u32,
dh: u32|
-> Result<(), Error> {
let src = ImageRef::new(sw, sh, src, PixelType::U8)
.map_err(|e| Error::Codec(anyhow::anyhow!("resize source: {e}")))?;
let mut dst = Image::from_slice_u8(dw, dh, dst, PixelType::U8)
.map_err(|e| Error::Codec(anyhow::anyhow!("resize destination: {e}")))?;
resizer
.resize(&src, &mut dst, &options)
.map_err(|e| Error::Codec(anyhow::anyhow!("resize: {e}")))
};
let luma = width as usize * height as usize;
let mut data = vec![0u8; Self::len(width, height)];
let (y_dst, chroma) = data.split_at_mut(luma);
let (u_dst, v_dst) = chroma.split_at_mut(luma / 4);
RESIZER.with_borrow_mut(|resizer| {
plane(resizer, self.y(), self.width, self.height, y_dst, width, height)?;
let (sw2, sh2) = (self.width / 2, self.height / 2);
let (dw2, dh2) = (width / 2, height / 2);
plane(resizer, self.u(), sw2, sh2, u_dst, dw2, dh2)?;
plane(resizer, self.v(), sw2, sh2, v_dst, dw2, dh2)
})?;
Ok(Self { width, height, data })
}
fn pack(planar: &YuvPlanarImageMut<u8>, width: u32, height: u32) -> Self {
let mut data = Vec::with_capacity(Self::len(width, height));
data.extend_from_slice(planar.y_plane.borrow());
data.extend_from_slice(planar.u_plane.borrow());
data.extend_from_slice(planar.v_plane.borrow());
Self { width, height, data }
}
fn luma_len(&self) -> usize {
self.width as usize * self.height as usize
}
fn chroma_len(&self) -> usize {
self.luma_len() / 4
}
pub(crate) fn y(&self) -> &[u8] {
&self.data[..self.luma_len()]
}
pub(crate) fn u(&self) -> &[u8] {
let start = self.luma_len();
&self.data[start..start + self.chroma_len()]
}
pub(crate) fn v(&self) -> &[u8] {
let start = self.luma_len() + self.chroma_len();
&self.data[start..start + self.chroma_len()]
}
}
#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "nvenc")))]
pub(crate) fn interleave_uv(u: &[u8], v: &[u8], uv: &mut [u8]) {
for (pair, (u, v)) in uv.chunks_exact_mut(2).zip(u.iter().zip(v)) {
pair[0] = *u;
pair[1] = *v;
}
}
#[cfg(target_os = "windows")]
pub(crate) fn deinterleave_uv(uv: &[u8], u: &mut [u8], v: &mut [u8]) {
for (pair, (u, v)) in uv.chunks_exact(2).zip(u.iter_mut().zip(v)) {
*u = pair[0];
*v = pair[1];
}
}
#[cfg(target_os = "macos")]
pub(crate) mod macos {
use std::ptr;
use objc2_core_foundation::CFRetained;
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
CVPixelBufferGetPixelFormatType, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags,
CVPixelBufferUnlockBaseAddress, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
};
use super::I420;
use crate::Error;
const LOCK_READ_ONLY: CVPixelBufferLockFlags = CVPixelBufferLockFlags(1);
pub(crate) struct Surface {
pub(crate) buffer: CFRetained<CVPixelBuffer>,
pub(crate) width: u32,
pub(crate) height: u32,
}
unsafe impl Send for Surface {}
unsafe impl Sync for Surface {}
impl Surface {
pub(crate) fn new(buffer: CFRetained<CVPixelBuffer>, width: u32, height: u32) -> Self {
Self { buffer, width, height }
}
pub(crate) fn download_i420(&self) -> Result<I420, Error> {
let format = CVPixelBufferGetPixelFormatType(&self.buffer);
if format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
&& format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
{
return Err(Error::Codec(anyhow::anyhow!(
"cannot download pixel format {format:#x}; expected NV12"
)));
}
let (w, h) = (self.width as usize, self.height as usize);
let (cw, ch) = (w / 2, h / 2);
let status = unsafe { CVPixelBufferLockBaseAddress(&self.buffer, LOCK_READ_ONLY) };
if status != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"CVPixelBufferLockBaseAddress failed: {status}"
)));
}
let _guard = UnlockGuard(&self.buffer);
let mut data = vec![0u8; I420::len(self.width, self.height)];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
let y_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 0) as *const u8;
let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 0);
for row in 0..h {
unsafe {
ptr::copy_nonoverlapping(y_base.add(row * y_stride), luma[row * w..].as_mut_ptr(), w);
}
}
let uv_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 1) as *const u8;
let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 1);
for row in 0..ch {
let src = unsafe { uv_base.add(row * uv_stride) };
for col in 0..cw {
unsafe {
u_plane[row * cw + col] = *src.add(col * 2);
v_plane[row * cw + col] = *src.add(col * 2 + 1);
}
}
}
Ok(I420 {
width: self.width,
height: self.height,
data,
})
}
}
struct UnlockGuard<'a>(&'a CVPixelBuffer);
impl Drop for UnlockGuard<'_> {
fn drop(&mut self) {
unsafe { CVPixelBufferUnlockBaseAddress(self.0, LOCK_READ_ONLY) };
}
}
}
#[cfg(all(target_os = "linux", feature = "nvdec"))]
pub(crate) mod cuda {
use std::sync::{Arc, OnceLock};
use cudarc::driver::{CudaContext, CudaFunction, LaunchConfig, PushKernelArg, result};
use super::I420;
use crate::Error;
const RESIZE_PTX: &str = include_str!("frame/nv12_resize.ptx");
struct Kernels {
luma: CudaFunction,
chroma: CudaFunction,
}
fn kernels(ctx: &Arc<CudaContext>) -> Result<&'static Kernels, Error> {
static KERNELS: OnceLock<Result<Kernels, String>> = OnceLock::new();
KERNELS
.get_or_init(|| {
let module = ctx
.load_module(cudarc::nvrtc::Ptx::from_src(RESIZE_PTX))
.map_err(|e| format!("load nv12_resize PTX: {e:?}"))?;
Ok(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:?}"))?,
})
})
.as_ref()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize unavailable: {e}")))
}
struct Buffer {
ctx: Arc<CudaContext>,
ptr: cudarc::driver::sys::CUdeviceptr,
len: usize,
}
impl Drop for Buffer {
fn drop(&mut self) {
if self.ctx.bind_to_thread().is_ok() {
let _ = unsafe { result::free_sync(self.ptr) };
}
}
}
#[derive(Clone)]
pub(crate) struct Frame {
buf: Arc<Buffer>,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) pitch: u32,
}
impl Frame {
pub(crate) fn alloc(ctx: &Arc<CudaContext>, width: u32, height: u32, pitch: u32) -> Result<Self, Error> {
debug_assert!(pitch >= width && width % 2 == 0 && height % 2 == 0);
let len = pitch as usize * height as usize * 3 / 2;
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 {
buf: Arc::new(Buffer {
ctx: ctx.clone(),
ptr,
len,
}),
width,
height,
pitch,
})
}
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.width, self.height)];
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,
})
}
pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
let ctx = &self.buf.ctx;
let kernels = kernels(ctx)?;
let pitch = width.next_multiple_of(256);
let dst = Self::alloc(ctx, width, height, 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)
}
}
}
#[cfg(target_os = "windows")]
pub(crate) mod d3d11 {
use std::ptr;
use windows::Win32::Foundation::HMODULE;
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
use windows::Win32::Graphics::Direct3D10::ID3D10Multithread;
use windows::Win32::Graphics::Direct3D11::{
D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_MAP_READ,
D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, D3D11CreateDevice,
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
};
use windows::core::Interface;
use super::I420;
use crate::Error;
fn err(ctx: &str, e: windows::core::Error) -> Error {
Error::Codec(anyhow::anyhow!("{ctx}: {e}"))
}
pub(crate) fn create_device() -> Result<ID3D11Device, Error> {
let mut device: Option<ID3D11Device> = None;
unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
None,
)
.map_err(|e| err("D3D11CreateDevice", e))?;
}
let device = device.ok_or_else(|| Error::Codec(anyhow::anyhow!("D3D11CreateDevice returned null")))?;
let multithread = device
.cast::<ID3D10Multithread>()
.map_err(|e| err("query ID3D10Multithread", e))?;
unsafe {
let _ = multithread.SetMultithreadProtected(true);
}
Ok(device)
}
pub(crate) struct Texture {
pub(crate) device: ID3D11Device,
pub(crate) texture: ID3D11Texture2D,
pub(crate) subresource: u32,
pub(crate) width: u32,
pub(crate) height: u32,
}
impl Texture {
pub(crate) fn new(
device: ID3D11Device,
texture: ID3D11Texture2D,
subresource: u32,
width: u32,
height: u32,
) -> Self {
Self {
device,
texture,
subresource,
width,
height,
}
}
pub(crate) fn download_i420(&self) -> Result<I420, Error> {
let context = unsafe { self.device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { self.texture.GetDesc(&mut desc) };
desc.ArraySize = 1;
desc.MipLevels = 1;
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
desc.MiscFlags = 0;
let mut staging: Option<ID3D11Texture2D> = None;
unsafe {
self.device
.CreateTexture2D(&desc, None, Some(&mut staging))
.map_err(|e| err("CreateTexture2D (staging)", e))?;
}
let staging = staging.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))?;
unsafe {
context.CopySubresourceRegion(&staging, 0, 0, 0, 0, &self.texture, self.subresource, None);
}
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
unsafe {
context
.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
.map_err(|e| err("Map (staging)", e))?;
}
let _guard = UnmapGuard {
context: &context,
resource: &staging,
};
let (w, h) = (self.width as usize, self.height as usize);
let (cw, ch) = (w / 2, h / 2);
let pitch = mapped.RowPitch as usize;
let base = mapped.pData as *const u8;
let tex_height = desc.Height as usize;
let mut data = vec![0u8; I420::len(self.width, self.height)];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
for row in 0..h {
unsafe {
ptr::copy_nonoverlapping(base.add(row * pitch), luma[row * w..].as_mut_ptr(), w);
}
}
let uv_base = unsafe { base.add(pitch * tex_height) };
for row in 0..ch {
let src = unsafe { uv_base.add(row * pitch) };
for col in 0..cw {
unsafe {
u_plane[row * cw + col] = *src.add(col * 2);
v_plane[row * cw + col] = *src.add(col * 2 + 1);
}
}
}
Ok(I420 {
width: self.width,
height: self.height,
data,
})
}
}
struct UnmapGuard<'a> {
context: &'a ID3D11DeviceContext,
resource: &'a ID3D11Texture2D,
}
impl Drop for UnmapGuard<'_> {
fn drop(&mut self) {
unsafe { self.context.Unmap(self.resource, 0) };
}
}
}
#[cfg(test)]
mod tests {
use super::I420;
fn gradient_i420(width: u32, height: u32) -> I420 {
let (w, h) = (width as usize, height as usize);
let (cw, ch) = (w / 2, h / 2);
let mut data = vec![0u8; I420::len(width, height)];
let (y, chroma) = data.split_at_mut(w * h);
let (u, v) = chroma.split_at_mut(cw * ch);
for row in 0..h {
for col in 0..w {
y[row * w + col] = ((col * 255) / w) as u8;
}
}
for row in 0..ch {
for col in 0..cw {
u[row * cw + col] = ((row * 255) / ch) as u8;
v[row * cw + col] = (((row + col) * 255) / (ch + cw)) as u8;
}
}
I420 { width, height, data }
}
fn mae(a: &[u8], b: &[u8]) -> u64 {
assert_eq!(a.len(), b.len());
a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64
}
#[test]
fn i420_resize_follows_gradients() {
let src = gradient_i420(320, 240);
let dst = src.resize(128, 96).unwrap();
assert_eq!((dst.width, dst.height), (128, 96));
let expected = gradient_i420(128, 96);
assert!(mae(dst.y(), expected.y()) < 4, "luma ramp drifted");
assert!(mae(dst.u(), expected.u()) < 4, "u ramp drifted");
assert!(mae(dst.v(), expected.v()) < 4, "v ramp drifted");
}
#[cfg(all(target_os = "linux", feature = "nvdec"))]
#[test]
fn cuda_resize_matches_cpu() {
use std::sync::Arc;
use cudarc::driver::{CudaContext, result};
use super::cuda;
if unsafe { libloading::Library::new("libcuda.so.1") }.is_err() {
return;
}
let Ok(ctx): Result<Arc<CudaContext>, _> = CudaContext::new(0) else {
return;
};
let (w, h) = (322u32, 242u32); let src_i420 = gradient_i420(w, h);
let pitch = 512u32;
let frame = cuda::Frame::alloc(&ctx, w, h, pitch).unwrap();
let mut host = vec![0u8; pitch as usize * h as usize * 3 / 2];
for row in 0..h as usize {
let dst = row * pitch as usize;
host[dst..dst + w as usize].copy_from_slice(&src_i420.y()[row * w as usize..(row + 1) * w as usize]);
}
let (cw, ch) = (w as usize / 2, h as usize / 2);
for row in 0..ch {
let dst = (h as usize + row) * pitch as usize;
for col in 0..cw {
host[dst + 2 * col] = src_i420.u()[row * cw + col];
host[dst + 2 * col + 1] = src_i420.v()[row * cw + col];
}
}
unsafe { result::memcpy_htod_sync(frame.device_ptr(), &host) }.unwrap();
let scaled = frame.resize(160, 120).unwrap();
let gpu = scaled.download_i420().unwrap();
let cpu = src_i420.resize(160, 120).unwrap();
assert_eq!((gpu.width, gpu.height), (160, 120));
assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
}
}