Skip to main content

moq_vaapi/
vpp.rs

1//! Video post-processing: the `VAEntrypointVideoProc` pipeline.
2//!
3//! The one operation is a blit from one surface into another, which the video
4//! processor stretches to fit the destination and converts to the destination's
5//! format on the way. That covers three jobs without the pixels leaving the GPU:
6//!
7//! - Scaling: blit into a smaller or larger surface.
8//! - Color conversion: blit a packed RGB or YUYV surface into an NV12 one, the
9//!   format an encoder takes. [`Processor::blit`] takes the YUV space to write.
10//! - Re-tiling: blit into a surface of the same size and format. The destination
11//!   is one the driver allocated for itself, so the blit rewrites the pixels
12//!   into whatever tiling the driver prefers for a fresh allocation. That is how
13//!   a DMA-BUF carrying a format modifier some other API refuses to import
14//!   becomes one it accepts. Measured on Intel Meteor Lake with iHD 26.1.5 (the
15//!   `retile_round_trip` test prints it), a decode target exports at
16//!   `0x100000000000009` (`I915_FORMAT_MOD_4_TILED`), which Mesa's Vulkan driver
17//!   lists, so that combination does not need it. Older iHD releases wrote
18//!   `Y_TILED` (`0x100000000000002`), which Mesa does not list.
19//!
20//! ```no_run
21//! # fn example(buffer: moq_vaapi::dmabuf::DmaBuf) -> anyhow::Result<()> {
22//! use moq_vaapi::vpp::Processor;
23//!
24//! let processor = Processor::new("/dev/dri/renderD128")?;
25//! let retiled = processor.retile(buffer)?;
26//! let exported = retiled.export_prime().map_err(|e| anyhow::anyhow!("export: {e:?}"))?;
27//! # let _ = exported;
28//! # Ok(())
29//! # }
30//! ```
31
32use std::cell::RefCell;
33use std::collections::HashMap;
34use std::path::Path;
35use std::rc::Rc;
36use std::sync::Arc;
37
38use crate::dmabuf::{rt_format, DmaBuf};
39use crate::{
40	bindings, BufferType, Color, Config as VaConfig, Context, Display, Picture, ProcColorProperties,
41	ProcPipelineParameterBuffer, Surface, SurfaceMemoryDescriptor, UsageHint, VAConfigAttrib, VAConfigAttribType,
42	VAEntrypoint, VAProfile,
43};
44
45/// Checks that `display`'s driver offers video post-processing, which is what [`Processor::with_display`] needs of a render node.
46///
47/// # Errors
48///
49/// Fails when the driver has no `VAEntrypointVideoProc`, or cannot be asked.
50pub fn probe(display: &Display) -> anyhow::Result<()> {
51	let entrypoints = display
52		.query_config_entrypoints(VAProfile::VAProfileNone)
53		.map_err(|e| anyhow::anyhow!("query video processing entrypoints: {e:?}"))?;
54	match entrypoints.contains(&VAEntrypoint::VAEntrypointVideoProc) {
55		true => Ok(()),
56		false => anyhow::bail!("the driver has no video post-processing entrypoint"),
57	}
58}
59
60/// A video post-processor on one device.
61///
62/// Holds the display, the VPP configuration, and one processing context per
63/// destination size, which are what cost something to build. A caller
64/// converting every frame of a stream, or of a few renditions, pays for each
65/// context once.
66pub struct Processor {
67	display: Arc<Display>,
68	// Drop order: the contexts before the config they were created from.
69	contexts: RefCell<HashMap<(u32, u32), Rc<Context>>>,
70	config: VaConfig,
71}
72
73impl Processor {
74	/// Opens `device` and configures it for video post-processing.
75	///
76	/// `device` is a DRM render node, e.g. `/dev/dri/renderD128`. Pick the one
77	/// matching the device that will consume the result: the blit only stays on
78	/// the GPU when both ends are the same device.
79	pub fn new<P: AsRef<Path>>(device: P) -> anyhow::Result<Self> {
80		let device = device.as_ref();
81		let display =
82			Display::open_drm_display(device).map_err(|e| anyhow::anyhow!("open DRM display {device:?}: {e}"))?;
83		Self::with_display(display)
84	}
85
86	/// Opens the first device whose driver can post-process, if any.
87	///
88	/// Mirrors [`Display::open`], and is what a caller with no opinion about
89	/// which GPU should use. A machine with more than one wants
90	/// [`Processor::new`] or [`Processor::with_display`] instead.
91	pub fn open() -> anyhow::Result<Self> {
92		let (_, display) =
93			crate::display::open_first(probe).map_err(|e| e.context("find a render node that post-processes video"))?;
94		Self::with_display(display)
95	}
96
97	/// Configures an already open display for video post-processing.
98	///
99	/// Sharing the display with a decoder or an encoder is what lets a surface
100	/// move between them: a surface belongs to the display that created it.
101	///
102	/// # Errors
103	///
104	/// Fails when the device has no `VAEntrypointVideoProc`, which is how a
105	/// caller finds out before the first frame rather than during it.
106	pub fn with_display(display: Arc<Display>) -> anyhow::Result<Self> {
107		// Asking for the attribute is the probe: a device without the VPP
108		// entrypoint fails here instead of at vaCreateConfig, with an error that
109		// says which entrypoint was missing.
110		let mut attrs = [VAConfigAttrib {
111			type_: VAConfigAttribType::VAConfigAttribRTFormat,
112			value: 0,
113		}];
114		display
115			.get_config_attributes(
116				VAProfile::VAProfileNone,
117				VAEntrypoint::VAEntrypointVideoProc,
118				&mut attrs,
119			)
120			.map_err(|e| anyhow::anyhow!("query VPP config attributes: {e:?}"))?;
121
122		let config = display
123			.create_config(
124				attrs.to_vec(),
125				VAProfile::VAProfileNone,
126				VAEntrypoint::VAEntrypointVideoProc,
127			)
128			.map_err(|e| anyhow::anyhow!("create VPP config: {e:?}"))?;
129
130		Ok(Self {
131			display,
132			contexts: RefCell::new(HashMap::new()),
133			config,
134		})
135	}
136
137	/// Returns the display this processor runs on, to share with a decoder or an encoder.
138	pub fn display(&self) -> &Arc<Display> {
139		&self.display
140	}
141
142	/// Imports an exported DMA-BUF as a surface on this processor's display, without copying it.
143	///
144	/// The returned surface owns `buffer`, so the descriptor stays open exactly
145	/// as long as the surface referring to it.
146	///
147	/// # Errors
148	///
149	/// See [`DmaBuf::import`].
150	pub fn import(&self, buffer: DmaBuf) -> anyhow::Result<Surface<DmaBuf>> {
151		buffer.import(&self.display, Some(UsageHint::USAGE_HINT_VPP_READ))
152	}
153
154	/// Blits `input` into `output`, scaling to the size of `output` and converting to its format.
155	///
156	/// `input_color` and `output_color` name the YUV space on each side. Leave
157	/// the input side `None` for an RGB input, which has no matrix; set the
158	/// output side to the space a YUV destination should be written in. A side
159	/// left `None` is up to the driver, which is right for a blit that only
160	/// re-tiles, and for a scale that names neither side. Returns once the
161	/// destination holds the result.
162	///
163	/// # Errors
164	///
165	/// Fails when either surface belongs to another display, and when the
166	/// driver refuses the blit (a format pair or scale it cannot do).
167	pub fn blit<D, E>(
168		&self,
169		input: &Surface<D>,
170		output: &Surface<E>,
171		input_color: Option<Color>,
172		output_color: Option<Color>,
173	) -> anyhow::Result<()>
174	where
175		D: SurfaceMemoryDescriptor,
176		E: SurfaceMemoryDescriptor,
177	{
178		// A surface id means something only on the display that made it, and
179		// the driver would read another display's id as one of its own.
180		if !Arc::ptr_eq(input.display(), &self.display) || !Arc::ptr_eq(output.display(), &self.display) {
181			anyhow::bail!("a VPP blit between surfaces of another display");
182		}
183		let size = output.size();
184		let context = self.context(size)?;
185		let result = run_blit(&context, input, output, input_color, output_color);
186		if result.is_err() {
187			// A picture that failed half way can leave the context mid-picture,
188			// so the next blit of this size starts on a fresh one.
189			self.contexts.borrow_mut().remove(&size);
190		}
191		result
192	}
193
194	/// Blits `input` into a new driver-allocated surface of `fourcc`, `width` and `height`.
195	///
196	/// The destination is hinted as post-processing output that will be
197	/// exported, so [`Surface::export_prime`] works on it. See
198	/// [`blit`](Self::blit) for the colors.
199	///
200	/// # Errors
201	///
202	/// Fails for a format with no render target mapping, when the destination
203	/// cannot be allocated, and as [`blit`](Self::blit) does.
204	pub fn process<D: SurfaceMemoryDescriptor>(
205		&self,
206		input: &Surface<D>,
207		fourcc: u32,
208		(width, height): (u32, u32),
209		input_color: Option<Color>,
210		output_color: Option<Color>,
211	) -> anyhow::Result<Surface<()>> {
212		let rt_format = rt_format(fourcc).ok_or_else(|| anyhow::anyhow!("no RT format for fourcc {fourcc:#x}"))?;
213		let output = self
214			.display
215			.create_surfaces(
216				rt_format,
217				Some(fourcc),
218				width,
219				height,
220				Some(UsageHint::USAGE_HINT_VPP_WRITE | UsageHint::USAGE_HINT_EXPORT),
221				vec![()],
222			)
223			.map_err(|e| anyhow::anyhow!("allocate a {width}x{height} VPP output surface: {e:?}"))?
224			.pop()
225			.ok_or_else(|| anyhow::anyhow!("vaCreateSurfaces returned no surface"))?;
226
227		self.blit(input, &output, input_color, output_color)?;
228		Ok(output)
229	}
230
231	/// Imports `buffer` and blits it into a driver-allocated surface of the same format and size.
232	///
233	/// This is the whole re-tile. Export the result with
234	/// [`Surface::export_prime`] and read the modifier off the object to see what
235	/// the driver chose.
236	/// Destroying the returned surface does not invalidate an export taken from
237	/// it, since the exported descriptor holds its own reference on the
238	/// underlying buffer.
239	pub fn retile(&self, buffer: DmaBuf) -> anyhow::Result<Surface<()>> {
240		let fourcc = buffer
241			.fourcc()
242			.ok_or_else(|| anyhow::anyhow!("no VA-API format for DRM format {:#010x}", buffer.drm_format))?;
243		let input = self.import(buffer)?;
244		self.process(&input, fourcc, input.size(), None, None)
245	}
246
247	/// Returns the processing context for a destination of `size`, building it on first use.
248	fn context(&self, (width, height): (u32, u32)) -> anyhow::Result<Rc<Context>> {
249		let mut contexts = self.contexts.borrow_mut();
250		if let Some(context) = contexts.get(&(width, height)) {
251			return Ok(Rc::clone(context));
252		}
253		// No render targets: a VPP context takes its destination per picture, and
254		// binding it to one surface would make every other destination a rebuild.
255		let context = self
256			.display
257			.create_context::<()>(&self.config, width, height, None, true)
258			.map_err(|e| anyhow::anyhow!("create a {width}x{height} VPP context: {e:?}"))?;
259		contexts.insert((width, height), Rc::clone(&context));
260		Ok(context)
261	}
262}
263
264/// Runs one blit on `context` and waits for it to finish.
265fn run_blit<D, E>(
266	context: &Rc<Context>,
267	input: &Surface<D>,
268	output: &Surface<E>,
269	input_color: Option<Color>,
270	output_color: Option<Color>,
271) -> anyhow::Result<()>
272where
273	D: SurfaceMemoryDescriptor,
274	E: SurfaceMemoryDescriptor,
275{
276	let parameters = pipeline(input.id(), input_color, output_color);
277	let buffer = context
278		.create_buffer(BufferType::ProcPipelineParameter(parameters))
279		.map_err(|e| anyhow::anyhow!("create VPP pipeline buffer: {e:?}"))?;
280
281	let mut picture = Picture::new(0, Rc::clone(context), output);
282	picture.add_buffer(buffer);
283	picture
284		.begin()
285		.map_err(|e| anyhow::anyhow!("vaBeginPicture: {e:?}"))?
286		.render()
287		.map_err(|e| anyhow::anyhow!("vaRenderPicture: {e:?}"))?
288		.end()
289		.map_err(|e| anyhow::anyhow!("vaEndPicture: {e:?}"))?
290		.sync()
291		.map_err(|(e, _)| anyhow::anyhow!("vaSyncSurface: {e:?}"))?;
292	Ok(())
293}
294
295/// Returns a pipeline parameter buffer describing a plain blit of `surface`.
296///
297/// No regions (so the whole input stretched over the whole output), no
298/// filters, no references, no rotation or mirroring, and each side's color
299/// standard and range as given, or the driver's default where not.
300fn pipeline(
301	surface: bindings::VASurfaceID,
302	input_color: Option<Color>,
303	output_color: Option<Color>,
304) -> ProcPipelineParameterBuffer {
305	let side = |color: Option<Color>| match color {
306		Some(color) => (
307			color.va_standard(),
308			ProcColorProperties::new(0, color.va_range(), 0, 0, 0),
309		),
310		None => (0, ProcColorProperties::default()),
311	};
312	let (input_standard, input_properties) = side(input_color);
313	let (output_standard, output_properties) = side(output_color);
314	ProcPipelineParameterBuffer::new(
315		surface,
316		None,
317		input_standard,
318		None,
319		0,
320		output_standard,
321		0,
322		0,
323		None,
324		None,
325		None,
326		0,
327		None,
328		0,
329		None,
330		0,
331		0,
332		input_properties,
333		output_properties,
334		0,
335		None,
336	)
337}
338
339#[cfg(test)]
340mod tests {
341	use super::*;
342	use crate::{VA_FOURCC_NV12, VA_RT_FORMAT_YUV420};
343
344	/// A driver-allocated NV12 surface survives a round trip through the
345	/// post-processor, and the modifier the driver picked is printed.
346	///
347	/// Skips without a VA-API device, so it is a no-op on a builder and real
348	/// coverage on a machine with a GPU. What it asserts is that the whole
349	/// sequence runs (import an exported DMA-BUF, blit, export the result); the
350	/// modifier the driver picks is hardware policy, so it is printed rather
351	/// than asserted.
352	#[test]
353	fn retile_round_trip() {
354		let Some(display) = Display::open() else {
355			eprintln!("skipping: no VA-API display");
356			return;
357		};
358		let Ok(processor) = Processor::with_display(display) else {
359			eprintln!("skipping: device has no video post-processing entrypoint");
360			return;
361		};
362
363		// A decode-hinted surface, which is the allocation whose tiling is the
364		// problem a re-tile exists for.
365		let mut surfaces = processor
366			.display()
367			.create_surfaces(
368				VA_RT_FORMAT_YUV420,
369				Some(VA_FOURCC_NV12),
370				640,
371				480,
372				Some(UsageHint::USAGE_HINT_DECODER | UsageHint::USAGE_HINT_EXPORT),
373				vec![()],
374			)
375			.expect("allocate a decode surface");
376		let decoded = surfaces.pop().expect("one surface");
377		let exported = decoded.export_prime().expect("export the decode surface");
378		let source_modifier = exported.objects[0].drm_format_modifier;
379		let (fourcc, size, planes) = (
380			exported.fourcc,
381			(exported.width, exported.height),
382			exported.layers[0].num_planes,
383		);
384		let buffer = DmaBuf::from_prime(exported).expect("a composed export is one object");
385
386		let retiled = processor.retile(buffer).expect("re-tile the decode surface");
387		let output = retiled.export_prime().expect("export the re-tiled surface");
388		let target_modifier = output.objects[0].drm_format_modifier;
389
390		eprintln!("modifier {source_modifier:#x} -> {target_modifier:#x}");
391		assert_eq!(output.fourcc, fourcc, "the blit preserves the pixel format");
392		assert_eq!((output.width, output.height), size);
393		assert_eq!(output.objects.len(), 1, "a composed export is one object");
394		assert_eq!(
395			output.layers[0].num_planes, planes,
396			"the blit preserves the plane count"
397		);
398	}
399	/// A surface scaled by the processor exports and reads back at its new
400	/// size, with the picture scaled rather than cropped.
401	#[test]
402	fn a_scaled_surface_reads_back_at_its_new_size() {
403		let Some(display) = Display::open() else {
404			eprintln!("skipping: no VA-API display");
405			return;
406		};
407		let Ok(processor) = Processor::with_display(display) else {
408			eprintln!("skipping: device has no video post-processing entrypoint");
409			return;
410		};
411
412		// Left half dark, right half bright, so a crop and a scale differ.
413		let (width, height) = (640u32, 480u32);
414		let source = processor
415			.display()
416			.create_surfaces(
417				VA_RT_FORMAT_YUV420,
418				Some(VA_FOURCC_NV12),
419				width,
420				height,
421				Some(UsageHint::USAGE_HINT_EXPORT),
422				vec![()],
423			)
424			.expect("allocate a surface")
425			.pop()
426			.expect("one surface");
427		let format = processor
428			.display()
429			.query_image_formats()
430			.expect("query image formats")
431			.into_iter()
432			.find(|f| f.fourcc == VA_FOURCC_NV12)
433			.expect("an NV12 image format");
434		{
435			let mut image =
436				crate::Image::create_from(&source, format, (width, height), (width, height)).expect("create an image");
437			let va_image = *image.image();
438			let data = image.as_mut();
439			for y in 0..height as usize {
440				let row = va_image.offsets[0] as usize + y * va_image.pitches[0] as usize;
441				for x in 0..width as usize {
442					data[row + x] = if x < width as usize / 2 { 40 } else { 200 };
443				}
444			}
445			for y in 0..height as usize / 2 {
446				let row = va_image.offsets[1] as usize + y * va_image.pitches[1] as usize;
447				data[row..row + width as usize].fill(128);
448			}
449		}
450
451		let scaled = processor
452			.process(&source, VA_FOURCC_NV12, (width / 2, height / 2), None, None)
453			.expect("scale the surface");
454		let exported = crate::decode::ExportedFrame::from_surface(scaled, 7).expect("export the scaled surface");
455		assert_eq!((exported.width, exported.height), (width / 2, height / 2));
456		assert_eq!(exported.timestamp, 7);
457		let frame = exported.download().expect("read the scaled surface back");
458		let (w, h) = ((width / 2) as usize, (height / 2) as usize);
459		assert_eq!(frame.data.len(), w * h * 3 / 2);
460		let (left, right) = (frame.data[h / 2 * w + w / 8], frame.data[h / 2 * w + w * 7 / 8]);
461		assert!(
462			left.abs_diff(40) < 4 && right.abs_diff(200) < 4,
463			"read back {left} and {right}"
464		);
465	}
466}