use std::os::fd::{AsFd, BorrowedFd};
use wgpu::hal::MemoryFlags;
use super::source::{Layout, Source};
use crate::{Color, DmaBuf, DmaBufPlane, DrmFormat, Error, Size};
fn err(message: impl std::fmt::Display) -> Error {
Error::Render(anyhow::anyhow!("{message}"))
}
pub(super) fn import(device: &wgpu::Device, buffer: &DmaBuf) -> Result<Option<Source>, Error> {
if !device
.features()
.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF)
{
return Ok(None);
}
if unsafe { device.as_hal::<wgpu::hal::api::Vulkan>() }.is_none() {
return Ok(None);
}
let size = Size::new(buffer.width(), buffer.height());
let shape = Shape::of(buffer.format(), size)?;
let planes = shape.planes(buffer.planes())?;
let color = match shape.layout {
Layout::Rgba => None,
_ => Some(buffer.color().unwrap_or_else(|| Color::infer(size))),
};
let export = buffer
.export()
.map_err(|e| Error::Render(anyhow::Error::new(e).context("export DMA-BUF")))?;
let (fd, lease) = export.into_parts();
let source = unsafe {
adopt(
device,
fd.as_fd(),
buffer.modifier(),
shape.layout,
color,
&planes,
Some(Box::new(lease)),
)
}?;
Ok(Some(source))
}
#[derive(Clone, Copy)]
struct Shape {
layout: Layout,
plane0: (wgpu::TextureFormat, Size),
plane1: Option<(wgpu::TextureFormat, Size)>,
plane2: Option<(wgpu::TextureFormat, Size)>,
}
impl Shape {
fn of(format: DrmFormat, size: Size) -> Result<Self, Error> {
let packed = |format| Self {
layout: Layout::Rgba,
plane0: (format, size),
plane1: None,
plane2: None,
};
let subsampled = || -> Result<Size, Error> {
size.validate("4:2:0 DMA-BUF")?;
Ok(Size::new(size.width / 2, size.height / 2))
};
Ok(match format {
DrmFormat::XRGB8888 | DrmFormat::ARGB8888 => packed(wgpu::TextureFormat::Bgra8Unorm),
DrmFormat::XBGR8888 | DrmFormat::ABGR8888 => packed(wgpu::TextureFormat::Rgba8Unorm),
DrmFormat::NV12 => Self {
layout: Layout::Nv12,
plane0: (wgpu::TextureFormat::R8Unorm, size),
plane1: Some((wgpu::TextureFormat::Rg8Unorm, subsampled()?)),
plane2: None,
},
DrmFormat::YUV420 => {
let half = subsampled()?;
Self {
layout: Layout::I420,
plane0: (wgpu::TextureFormat::R8Unorm, size),
plane1: Some((wgpu::TextureFormat::R8Unorm, half)),
plane2: Some((wgpu::TextureFormat::R8Unorm, half)),
}
}
format => return Err(err(format!("cannot import DMA-BUF format {:#x}", format.as_raw()))),
})
}
fn planes(&self, described: &[DmaBufPlane]) -> Result<Vec<Plane>, Error> {
let wanted = [Some(self.plane0), self.plane1, self.plane2];
let wanted = wanted.iter().flatten();
let count = 1 + usize::from(self.plane1.is_some()) + usize::from(self.plane2.is_some());
if described.len() != count {
return Err(err(format!(
"DMA-BUF describes {} planes, but its format has {count}",
described.len()
)));
}
Ok(wanted
.zip(described)
.map(|(&(format, size), plane)| Plane {
format,
size,
stride: plane.stride(),
offset: plane.offset(),
})
.collect())
}
}
struct Plane {
format: wgpu::TextureFormat,
size: Size,
stride: u32,
offset: u32,
}
unsafe fn adopt(
device: &wgpu::Device,
fd: BorrowedFd<'_>,
modifier: u64,
layout: Layout,
color: Option<Color>,
planes: &[Plane],
keepalive: Option<Box<dyn Send + Sync>>,
) -> Result<Source, Error> {
let mut views = Vec::with_capacity(planes.len());
for plane in planes {
let dup = fd
.try_clone_to_owned()
.map_err(|e| Error::Render(anyhow::Error::new(e).context("duplicate DMA-BUF")))?;
views.push(unsafe { adopt_plane(device, dup, modifier, plane) }?);
}
let filler = views.last().expect("a format has at least one plane").clone();
let mut views = views.into_iter();
let plane0 = views.next().expect("a format has at least one plane");
let plane1 = views.next().unwrap_or_else(|| filler.clone());
let plane2 = views.next().unwrap_or(filler);
Ok(Source {
layout,
color,
plane0,
plane1,
plane2,
keepalive,
})
}
unsafe fn adopt_plane(
device: &wgpu::Device,
fd: std::os::fd::OwnedFd,
modifier: u64,
plane: &Plane,
) -> Result<wgpu::TextureView, Error> {
let extent = wgpu::Extent3d {
width: plane.size.width,
height: plane.size.height,
depth_or_array_layers: 1,
};
let descriptor = wgpu::TextureDescriptor {
label: Some("moq-video imported DMA-BUF"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: plane.format,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let hal_descriptor = wgpu::hal::TextureDescriptor {
label: descriptor.label,
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: plane.format,
usage: wgpu::TextureUses::RESOURCE,
memory_flags: MemoryFlags::empty(),
view_formats: Vec::new(),
};
let hal = (unsafe { device.as_hal::<wgpu::hal::api::Vulkan>() })
.ok_or_else(|| err("wgpu device is not a Vulkan device"))?;
let texture =
unsafe { hal.texture_from_dmabuf_fd(fd, &hal_descriptor, modifier, plane.stride as u64, plane.offset as u64) }
.map_err(|e| {
err(format!(
"Vulkan DMA-BUF import of a {:?} {} plane at modifier {modifier:#x}: {e:?}",
plane.format, plane.size
))
})?;
drop(hal);
let texture = unsafe {
device.create_texture_from_hal::<wgpu::hal::api::Vulkan>(texture, &descriptor, wgpu::TextureUses::RESOURCE)
};
Ok(texture.create_view(&Default::default()))
}
#[cfg(all(test, feature = "vaapi"))]
pub(super) mod fixture {
use std::os::fd::OwnedFd;
use std::sync::Arc;
use super::*;
struct Exported(OwnedFd);
impl crate::frame::DmaBufFrame for Exported {
fn export(&self) -> std::io::Result<OwnedFd> {
self.0.try_clone()
}
fn download_i420(&self) -> Result<crate::frame::I420, Error> {
Err(err("the DMA-BUF fixture does not implement downloading"))
}
}
fn va_fourcc(format: DrmFormat) -> Result<u32, Error> {
match format {
DrmFormat::XRGB8888 => Ok(moq_vaapi::VA_FOURCC_BGRX),
DrmFormat::ARGB8888 => Ok(moq_vaapi::VA_FOURCC_BGRA),
DrmFormat::XBGR8888 => Ok(moq_vaapi::VA_FOURCC_RGBX),
DrmFormat::ABGR8888 => Ok(moq_vaapi::VA_FOURCC_RGBA),
DrmFormat::NV12 => Ok(moq_vaapi::VA_FOURCC_NV12),
DrmFormat::YUV420 => Ok(moq_vaapi::VA_FOURCC_I420),
format => Err(err(format!(
"no VA-API format for DMA-BUF format {:#x}",
format.as_raw()
))),
}
}
fn rows(format: DrmFormat, size: Size) -> Vec<(usize, usize)> {
let (width, height) = (size.width as usize, size.height as usize);
match format {
DrmFormat::NV12 => vec![(height, width), (height / 2, width)],
DrmFormat::YUV420 => vec![(height, width), (height / 2, width / 2), (height / 2, width / 2)],
format => panic!("the fixture cannot build DMA-BUF format {:#x}", format.as_raw()),
}
}
pub(in crate::render) fn surface(format: DrmFormat, size: Size, pixels: &[u8]) -> Option<DmaBuf> {
let fourcc = va_fourcc(format).expect("a VA-API format");
let display = moq_vaapi::Display::open()?;
let surface = display
.create_surfaces::<()>(
moq_vaapi::VA_RT_FORMAT_YUV420,
Some(fourcc),
size.width,
size.height,
Some(moq_vaapi::UsageHint::USAGE_HINT_DECODER | moq_vaapi::UsageHint::USAGE_HINT_EXPORT),
vec![()],
)
.map_err(|e| eprintln!("no {fourcc:#x} surface on this driver: {e}"))
.ok()?
.pop()
.expect("one surface");
upload(&display, &surface, format, size, pixels)?;
let exported = surface
.export_prime()
.map_err(|e| eprintln!("this driver will not export a {fourcc:#x} surface: {e}"))
.ok()?;
Some(adopt(&exported, format, size))
}
pub(in crate::render) fn adopt(
exported: &moq_vaapi::DrmPrimeSurfaceDescriptor,
format: DrmFormat,
size: Size,
) -> DmaBuf {
let [object] = exported.objects.as_slice() else {
panic!(
"the fixture needs one object holding every plane, got {}",
exported.objects.len()
);
};
let layer = exported.layers.first().expect("an exported layer");
let planes = (0..layer.num_planes as usize)
.map(|index| DmaBufPlane::new(layer.offset[index], layer.pitch[index]))
.collect();
let fd = object.fd.try_clone().expect("duplicate the exported descriptor");
DmaBuf::new(
format,
object.drm_format_modifier,
size.width,
size.height,
planes,
None,
Arc::new(Exported(fd)),
)
.expect("a well-formed DMA-BUF")
}
fn upload(
display: &Arc<moq_vaapi::Display>,
surface: &moq_vaapi::Surface<()>,
format: DrmFormat,
size: Size,
pixels: &[u8],
) -> Option<()> {
let fourcc = va_fourcc(format).expect("a VA-API format");
let image_format = display
.query_image_formats()
.expect("query image formats")
.into_iter()
.find(|image| image.fourcc == fourcc)
.or_else(|| {
eprintln!("this driver has no {fourcc:#x} image format");
None
})?;
let mut image = moq_vaapi::Image::create_from(surface, image_format, surface.size(), surface.size())
.map_err(|e| eprintln!("this driver will not create a {fourcc:#x} image: {e}"))
.ok()?;
let va_image = *image.image();
let destination: &mut [u8] = image.as_mut();
let mut source = pixels;
for (plane, (rows, length)) in rows(format, size).into_iter().enumerate() {
let (pitch, offset) = (va_image.pitches[plane] as usize, va_image.offsets[plane] as usize);
for row in 0..rows {
let start = offset + row * pitch;
destination[start..start + length].copy_from_slice(&source[row * length..(row + 1) * length]);
}
source = &source[rows * length..];
}
drop(image);
surface.sync().expect("sync the uploaded surface");
Some(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_buffer_short_a_plane_is_refused() {
let size = Size::new(64, 64);
let shape = Shape::of(DrmFormat::NV12, size).expect("NV12 is importable");
let luma_only = [DmaBufPlane::new(0, 64)];
assert!(shape.planes(&luma_only).is_err());
let both = [DmaBufPlane::new(0, 64), DmaBufPlane::new(64 * 64, 64)];
let planes = shape.planes(&both).expect("a complete NV12 buffer");
assert_eq!(planes.len(), 2);
assert_eq!(planes[0].format, wgpu::TextureFormat::R8Unorm);
assert_eq!(planes[0].size, size);
assert_eq!(planes[1].format, wgpu::TextureFormat::Rg8Unorm);
assert_eq!(planes[1].size, Size::new(32, 32));
assert_eq!(planes[1].offset, 64 * 64);
}
#[test]
fn the_producers_pitch_reaches_each_plane() {
let shape = Shape::of(DrmFormat::NV12, Size::new(60, 40)).expect("NV12 is importable");
let planes = shape
.planes(&[DmaBufPlane::new(0, 64), DmaBufPlane::new(64 * 48, 64)])
.expect("a complete NV12 buffer");
assert_eq!(planes[0].stride, 64, "luma keeps the padded pitch, not the width");
assert_eq!(planes[1].stride, 64, "chroma is half as wide but two bytes a texel");
}
#[test]
fn planar_420_imports_as_three_single_component_planes() {
let size = Size::new(64, 64);
let shape = Shape::of(DrmFormat::YUV420, size).expect("YU12 is importable");
assert_eq!(shape.layout, Layout::I420);
assert!(
shape
.planes(&[DmaBufPlane::new(0, 64), DmaBufPlane::new(64 * 64, 32)])
.is_err(),
"two planes is NV12's count, not this one's"
);
let planes = shape
.planes(&[
DmaBufPlane::new(0, 64),
DmaBufPlane::new(64 * 64, 32),
DmaBufPlane::new(64 * 64 + 32 * 32, 32),
])
.expect("a complete YU12 buffer");
assert_eq!(planes.len(), 3);
for plane in &planes {
assert_eq!(plane.format, wgpu::TextureFormat::R8Unorm);
}
assert_eq!(planes[0].size, size);
assert_eq!(planes[1].size, Size::new(32, 32));
assert_eq!(planes[2].size, Size::new(32, 32));
assert_eq!(planes[2].offset, 64 * 64 + 32 * 32);
assert_eq!(planes[2].stride, 32);
}
#[test]
fn packed_formats_import_as_a_single_plane() {
let size = Size::new(64, 64);
for (format, expected) in [
(DrmFormat::XRGB8888, wgpu::TextureFormat::Bgra8Unorm),
(DrmFormat::ABGR8888, wgpu::TextureFormat::Rgba8Unorm),
] {
let shape = Shape::of(format, size).expect("a packed format is importable");
assert_eq!(shape.layout, Layout::Rgba);
let planes = shape.planes(&[DmaBufPlane::new(0, 64 * 4)]).expect("one plane");
assert_eq!(planes.len(), 1);
assert_eq!(planes[0].format, expected);
}
}
#[test]
fn odd_dimensions_are_refused_for_subsampled_formats() {
assert!(Shape::of(DrmFormat::NV12, Size::new(65, 64)).is_err());
assert!(Shape::of(DrmFormat::YUV420, Size::new(64, 65)).is_err());
assert!(Shape::of(DrmFormat::XRGB8888, Size::new(65, 65)).is_ok());
}
}