Skip to main content

moq_vaapi/
dmabuf.rs

1//! Importing a Linux DMA-BUF as a VA surface, without copying it.
2//!
3//! A [`DmaBuf`] describes an allocation some other API made (a PipeWire
4//! producer, a camera, a V4L2 device, another VA-API context that exported
5//! its surface) well enough for the driver to wrap it as a [`Surface`]. The
6//! surface reads the producer's memory directly: an encoder given one encodes
7//! the producer's pixels, and a post-processor given one converts them, without
8//! the frame passing through the CPU.
9//!
10//! The format is named the DRM way, which is what every producer of a DMA-BUF
11//! speaks. [`va_fourcc`] maps it to the VA-API name, which differs for packed
12//! RGB (DRM names the channels in little-endian word order, VA-API in byte
13//! order) and for planar 4:2:0 (`YU12` against `I420`).
14
15use std::io::{Seek, SeekFrom};
16use std::os::fd::{AsRawFd, OwnedFd};
17use std::sync::Arc;
18
19use crate::{
20	Color, Display, DrmPrimeSurfaceDescriptor, ExternalBufferDescriptor, MemoryType, Surface, UsageHint,
21	VADRMPRIMESurfaceDescriptor, VA_FOURCC_ARGB, VA_FOURCC_BGRA, VA_FOURCC_BGRX, VA_FOURCC_I420, VA_FOURCC_NV12,
22	VA_FOURCC_P010, VA_FOURCC_RGBA, VA_FOURCC_RGBX, VA_FOURCC_XRGB, VA_FOURCC_YUY2, VA_FOURCC_YV12, VA_RT_FORMAT_RGB32,
23	VA_RT_FORMAT_YUV420, VA_RT_FORMAT_YUV420_10, VA_RT_FORMAT_YUV422,
24};
25
26/// DRM fourcc codes for the formats [`va_fourcc`] maps.
27///
28/// These are the `DRM_FORMAT_*` values from `drm_fourcc.h`, which the VA-API
29/// headers do not carry.
30pub mod drm {
31	const fn fourcc(code: &[u8; 4]) -> u32 {
32		u32::from_le_bytes(*code)
33	}
34
35	/// Semi-planar 4:2:0, a luma plane and an interleaved Cb/Cr plane.
36	pub const NV12: u32 = fourcc(b"NV12");
37	/// Semi-planar 4:2:0 with 10 bits per sample in 16-bit words.
38	pub const P010: u32 = fourcc(b"P010");
39	/// Planar 4:2:0: luma, then Cb, then Cr.
40	pub const YUV420: u32 = fourcc(b"YU12");
41	/// Planar 4:2:0: luma, then Cr, then Cb.
42	pub const YVU420: u32 = fourcc(b"YV12");
43	/// Packed 4:2:2, `Y0 Cb Y1 Cr` in memory.
44	pub const YUYV: u32 = fourcc(b"YUYV");
45	/// Packed 32-bit, `B G R X` in memory.
46	pub const XRGB8888: u32 = fourcc(b"XR24");
47	/// Packed 32-bit, `B G R A` in memory.
48	pub const ARGB8888: u32 = fourcc(b"AR24");
49	/// Packed 32-bit, `R G B X` in memory.
50	pub const XBGR8888: u32 = fourcc(b"XB24");
51	/// Packed 32-bit, `R G B A` in memory.
52	pub const ABGR8888: u32 = fourcc(b"AB24");
53	/// Packed 32-bit, `X R G B` in memory.
54	pub const BGRX8888: u32 = fourcc(b"BX24");
55	/// Packed 32-bit, `A R G B` in memory.
56	pub const BGRA8888: u32 = fourcc(b"BA24");
57}
58
59/// Returns the `VA_FOURCC_*` naming the same layout as the DRM format `drm_format`.
60///
61/// Returns `None` for a format this crate has no mapping for.
62pub fn va_fourcc(drm_format: u32) -> Option<u32> {
63	Some(match drm_format {
64		drm::NV12 => VA_FOURCC_NV12,
65		drm::P010 => VA_FOURCC_P010,
66		drm::YUV420 => VA_FOURCC_I420,
67		drm::YVU420 => VA_FOURCC_YV12,
68		drm::YUYV => VA_FOURCC_YUY2,
69		drm::XRGB8888 => VA_FOURCC_BGRX,
70		drm::ARGB8888 => VA_FOURCC_BGRA,
71		drm::XBGR8888 => VA_FOURCC_RGBX,
72		drm::ABGR8888 => VA_FOURCC_RGBA,
73		drm::BGRX8888 => VA_FOURCC_XRGB,
74		drm::BGRA8888 => VA_FOURCC_ARGB,
75		_ => return None,
76	})
77}
78
79/// Returns the `VA_RT_FORMAT_*` a surface of the VA fourcc `fourcc` is allocated as.
80///
81/// Returns `None` for a format this crate has no mapping for. Guessing the
82/// render-target format wrong makes `vaCreateSurfaces` fail with an error that
83/// names neither, so an unknown format is refused up front instead.
84pub fn rt_format(fourcc: u32) -> Option<u32> {
85	match fourcc {
86		VA_FOURCC_NV12 | VA_FOURCC_I420 | VA_FOURCC_YV12 => Some(VA_RT_FORMAT_YUV420),
87		VA_FOURCC_P010 => Some(VA_RT_FORMAT_YUV420_10),
88		VA_FOURCC_YUY2 => Some(VA_RT_FORMAT_YUV422),
89		VA_FOURCC_BGRA | VA_FOURCC_BGRX | VA_FOURCC_RGBA | VA_FOURCC_RGBX | VA_FOURCC_ARGB | VA_FOURCC_XRGB => {
90			Some(VA_RT_FORMAT_RGB32)
91		}
92		_ => None,
93	}
94}
95
96/// One plane of a DMA-BUF, within the single object holding them all.
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct Plane {
99	/// Byte offset of the plane from the start of the object.
100	pub offset: u32,
101	/// Bytes between adjacent rows of the plane.
102	pub pitch: u32,
103}
104
105/// An externally allocated DMA-BUF, described well enough to import as a surface.
106///
107/// One object holding every plane, which is what a VA-API decoder exports and
108/// what a PipeWire producer negotiating a single modifier hands out. A buffer
109/// split across several objects is not describable here.
110///
111/// The descriptor owns the file descriptor and keeps it open for the life of the
112/// surface built from it. libva does not take ownership (the driver resolves the
113/// buffer during `vaCreateSurfaces` and leaves the fd alone), so holding it
114/// costs one descriptor and removes the question of whether closing it early is
115/// safe.
116#[derive(Debug)]
117pub struct DmaBuf {
118	/// Pixel format, a `DRM_FORMAT_*` code; see [`drm`].
119	pub drm_format: u32,
120	/// Format modifier describing the tiling of the object.
121	pub modifier: u64,
122	/// Width of the picture in pixels.
123	///
124	/// The visible width, not the allocated one: the padding lives in each
125	/// plane's pitch and offset.
126	pub width: u32,
127	/// Height of the picture in pixels, likewise the visible height.
128	pub height: u32,
129	/// Plane offsets and pitches, in format order. One to four.
130	pub planes: Vec<Plane>,
131	/// The exported descriptor, held open until the surface is destroyed.
132	pub fd: OwnedFd,
133	/// The YUV color space of the pixels, where the producer knows it.
134	///
135	/// Ignored for packed RGB. The encoder converts a YUV buffer whose space
136	/// differs from the stream's, and takes one with `None` to be in the
137	/// stream's space already.
138	pub color: Option<Color>,
139}
140
141impl DmaBuf {
142	/// Describes a surface exported by [`Surface::export_prime`] for import elsewhere.
143	///
144	/// The color is left unknown; set [`color`](Self::color) when the
145	/// producer knows it.
146	///
147	/// # Errors
148	///
149	/// Fails unless the export is one object holding one composed layer, which
150	/// is what `export_prime` asks the driver for.
151	pub fn from_prime(mut exported: DrmPrimeSurfaceDescriptor) -> anyhow::Result<Self> {
152		if exported.objects.len() != 1 || exported.layers.len() != 1 {
153			anyhow::bail!(
154				"expected one object and one layer, the export has {} and {}",
155				exported.objects.len(),
156				exported.layers.len()
157			);
158		}
159		let object = exported.objects.remove(0);
160		let layer = &exported.layers[0];
161		Ok(Self {
162			drm_format: layer.drm_format,
163			modifier: object.drm_format_modifier,
164			width: exported.width,
165			height: exported.height,
166			planes: (0..(layer.num_planes as usize).min(4))
167				.map(|i| Plane {
168					offset: layer.offset[i],
169					pitch: layer.pitch[i],
170				})
171				.collect(),
172			fd: object.fd,
173			color: None,
174		})
175	}
176
177	/// Returns the `VA_FOURCC_*` of this buffer's format, if it has one.
178	pub fn fourcc(&self) -> Option<u32> {
179		va_fourcc(self.drm_format)
180	}
181
182	/// Imports this buffer as a surface on `display`, without copying it.
183	///
184	/// The returned surface owns the buffer, so the descriptor stays open
185	/// exactly as long as the surface referring to it. `usage_hint` tells the
186	/// driver what the surface is for; some drivers only accept an import as an
187	/// encoder source when it says so.
188	///
189	/// # Errors
190	///
191	/// Fails when the format has no VA-API mapping, when the plane count is not
192	/// one to four, and when the driver refuses the import, which it does for a
193	/// modifier or pitch it cannot address.
194	pub fn import(self, display: &Arc<Display>, usage_hint: Option<UsageHint>) -> anyhow::Result<Surface<DmaBuf>> {
195		let fourcc = self
196			.fourcc()
197			.ok_or_else(|| anyhow::anyhow!("no VA-API format for DRM format {:#010x}", self.drm_format))?;
198		let rt_format =
199			rt_format(fourcc).ok_or_else(|| anyhow::anyhow!("no VA-API render target format for {fourcc:#010x}"))?;
200		if self.planes.is_empty() || self.planes.len() > 4 {
201			anyhow::bail!("a DMA-BUF import takes one to four planes, got {}", self.planes.len());
202		}
203		let (width, height) = (self.width, self.height);
204
205		display
206			.create_surfaces(rt_format, Some(fourcc), width, height, usage_hint, vec![self])
207			.map_err(|e| anyhow::anyhow!("import a {width}x{height} DMA-BUF as a surface: {e:?}"))?
208			.pop()
209			.ok_or_else(|| anyhow::anyhow!("vaCreateSurfaces returned no surface"))
210	}
211}
212
213impl ExternalBufferDescriptor for DmaBuf {
214	const MEMORY_TYPE: MemoryType = MemoryType::DrmPrime2;
215	type DescriptorAttribute = VADRMPRIMESurfaceDescriptor;
216
217	fn va_surface_attribute(&mut self) -> Self::DescriptorAttribute {
218		// `import` has checked the format and the plane count; an unmapped format
219		// reaching here anyway goes out as zero, which the driver refuses.
220		let mut descriptor = VADRMPRIMESurfaceDescriptor {
221			fourcc: self.fourcc().unwrap_or(0),
222			width: self.width,
223			height: self.height,
224			num_objects: 1,
225			num_layers: 1,
226			..Default::default()
227		};
228
229		descriptor.objects[0].fd = self.fd.as_raw_fd();
230		descriptor.objects[0].size = buffer_size(&self.fd);
231		descriptor.objects[0].drm_format_modifier = self.modifier;
232
233		let layer = &mut descriptor.layers[0];
234		layer.drm_format = self.drm_format;
235		layer.num_planes = self.planes.len().min(4) as u32;
236		for (index, plane) in self.planes.iter().take(4).enumerate() {
237			layer.object_index[index] = 0;
238			layer.offset[index] = plane.offset;
239			layer.pitch[index] = plane.pitch;
240		}
241
242		descriptor
243	}
244}
245
246/// Returns the size of the DMA-BUF behind `fd`, or zero when the kernel will not say.
247///
248/// A DMA-BUF reports its size through `lseek(SEEK_END)`. Intel's iHD driver
249/// refuses to import packed RGB with a size of zero, where for YUV formats it
250/// works the size out from the layout; zero stays the fallback because it is
251/// what the other drivers expect when the size is unknown. The seek moves the
252/// file offset shared with the producer's descriptor. The kernel accepts only
253/// the start and the end as a DMA-BUF offset (not even `SEEK_CUR`), so the
254/// offset was at the start before, and seeking back there restores it.
255fn buffer_size(fd: &OwnedFd) -> u32 {
256	let Ok(clone) = fd.try_clone() else {
257		return 0;
258	};
259	let mut file = std::fs::File::from(clone);
260	let size = file
261		.seek(SeekFrom::End(0))
262		.ok()
263		.and_then(|size| u32::try_from(size).ok());
264	let _ = file.seek(SeekFrom::Start(0));
265	size.unwrap_or(0)
266}
267
268#[cfg(test)]
269mod tests {
270	use super::*;
271
272	#[test]
273	fn every_mapped_format_has_a_render_target() {
274		for format in [
275			drm::NV12,
276			drm::P010,
277			drm::YUV420,
278			drm::YVU420,
279			drm::YUYV,
280			drm::XRGB8888,
281			drm::ARGB8888,
282			drm::XBGR8888,
283			drm::ABGR8888,
284			drm::BGRX8888,
285			drm::BGRA8888,
286		] {
287			let fourcc = va_fourcc(format).expect("mapped");
288			assert!(rt_format(fourcc).is_some(), "{format:#010x} has no render target");
289		}
290	}
291
292	#[test]
293	fn packed_rgb_maps_to_the_byte_order_name() {
294		// DRM XRGB8888 is a little-endian word, so memory holds B G R X, which
295		// VA-API calls BGRX.
296		assert_eq!(va_fourcc(drm::XRGB8888), Some(VA_FOURCC_BGRX));
297		assert_eq!(va_fourcc(drm::ABGR8888), Some(VA_FOURCC_RGBA));
298		assert_eq!(va_fourcc(drm::YUV420), Some(VA_FOURCC_I420));
299		assert_eq!(va_fourcc(u32::from_le_bytes(*b"ZZZZ")), None);
300	}
301}