Skip to main content

moq_vaapi/
encode.rs

1// Copyright 2022 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4//
5// Adapted from discord/cros-codecs (encoder/stateless/h264/vaapi.rs), itself
6// BSD-3-Clause / Copyright The ChromiumOS Authors. See LICENSE.cros-codecs.
7
8//! Thin VA-API H.264 encoder built on the vendored libva binding.
9//!
10//! Reuses the backend-agnostic bitstream layer ([`crate::codec::h264`]) from
11//! discord/cros-codecs (BSD-3-Clause) for SPS/PPS/slice synthesis, and drives
12//! libva directly for surface upload + slice submission, rather than vendoring
13//! cros-codecs's generic multi-backend encoder framework. The per-frame VA
14//! buffer population is ported from cros-codecs's `encoder/stateless/h264/vaapi.rs`.
15//!
16//! Low-latency only: IPPP (no B-frames), one reference frame, matching the
17//! VideoToolbox / Media Foundation / NVENC backends in moq-video.
18//!
19//! Three ways in, cheapest first:
20//! - [`Encoder::encode_dmabuf`] with an NV12 DMA-BUF at the encoder's size
21//!   encodes the producer's memory in place.
22//! - [`Encoder::encode_dmabuf`] with any other importable DMA-BUF (packed RGB
23//!   from a screen capture, YUYV from a camera, NV12 at another size) converts
24//!   and scales it into the encoder's own surface with the video processor, on
25//!   the GPU.
26//! - [`Encoder::encode_nv12`] uploads CPU pixels.
27//!
28//! Checked on Intel Meteor Lake with the iHD driver by decoding with ffmpeg's
29//! software decoder: CPU and DMA-BUF input come back above 35 dB luma PSNR,
30//! and BGRX converts to BT.601 and BT.709 with every channel within 6 code
31//! values of the reference. The low-power entrypoint is untested, since that
32//! device exposes the full one.
33
34use std::path::PathBuf;
35use std::rc::Rc;
36use std::sync::Arc;
37
38use crate::codec::h264::parser::SpsBuilder;
39use crate::codec::h264::parser::{Level, Pps, PpsBuilder, Profile, SliceHeader, SliceHeaderBuilder, SliceType, Sps};
40use crate::codec::h264::synthesizer::Synthesizer;
41use crate::dmabuf::DmaBuf;
42use crate::vpp::Processor;
43use crate::{
44	BufferType, Color, Config as VaConfig, Context, Display, EncCodedBuffer, EncMiscParameter,
45	EncMiscParameterFrameRate, EncMiscParameterRateControl, EncPackedHeaderParameter, EncPackedHeaderType,
46	EncPictureParameter, EncPictureParameterBufferH264, EncSequenceParameter, EncSequenceParameterBufferH264,
47	EncSliceParameter, EncSliceParameterBufferH264, H264EncFrameCropOffsets, H264EncPicFields, H264EncSeqFields,
48	H264VuiFields, Image, MappedCodedBuffer, Picture, PictureH264, RcFlags, Surface, SurfaceMemoryDescriptor,
49	UsageHint, VAConfigAttrib, VAConfigAttribType, VAEntrypoint, VAProfile, VA_ATTRIB_NOT_SUPPORTED,
50	VA_ENC_PACKED_HEADER_PICTURE, VA_ENC_PACKED_HEADER_SEQUENCE, VA_FOURCC_ARGB, VA_FOURCC_BGRA, VA_FOURCC_BGRX,
51	VA_FOURCC_NV12, VA_FOURCC_RGBA, VA_FOURCC_RGBX, VA_FOURCC_XRGB, VA_INVALID_ID, VA_PICTURE_H264_INVALID,
52	VA_PICTURE_H264_SHORT_TERM_REFERENCE, VA_RC_CBR, VA_RT_FORMAT_YUV420,
53};
54
55/// Whether a frame is used as a reference, and for how long.
56/// (Vendored from discord/cros-codecs, BSD-3-Clause; used by the slice synthesizer.)
57#[derive(Copy, Clone, PartialEq, Eq, Debug)]
58pub enum IsReference {
59	No,
60	ShortTerm,
61	LongTerm,
62}
63
64/// Quantizer bounds carried over from the cros-codecs encoder.
65///
66/// The full H.264 range lets Intel's CBR controller spend too much of a GOP on
67/// its IDR and then raise the P-frame quantizer enough to produce a visible
68/// quality pulse. These bounds trade strict CBR at the extremes for stable
69/// picture quality.
70const MIN_QP: u8 = 18;
71const MAX_QP: u8 = 36;
72/// Window over which libva's rate controller targets the configured bitrate.
73///
74/// This matches cros-codecs. The old `framerate * 1000` value confused frames
75/// per second with seconds and gave a 30 fps encoder a 30-second window.
76pub const RATE_CONTROL_WINDOW_MS: u32 = 1_500;
77/// `max_frame_num` upper bound for the low-delay GOP; matches cros-codecs.
78const LIMIT: u32 = 2048;
79
80/// H.264 encoder configuration. Dimensions must be even (4:2:0 chroma).
81#[derive(Clone, Debug)]
82pub struct Config {
83	pub width: u32,
84	pub height: u32,
85	pub framerate: u32,
86	/// Target bitrate in bits per second (CBR).
87	pub bitrate: u32,
88	/// Keyframe interval in frames.
89	pub gop: u32,
90	/// DRM render node to open (e.g. `/dev/dri/renderD128`).
91	///
92	/// `None`, the default, opens the first node whose driver can encode H.264,
93	/// and [`Encoder::config`] then names the node it picked.
94	pub device: Option<PathBuf>,
95	/// Color space written into the SPS, and the target of a conversion from RGB.
96	///
97	/// Defaults to [`Color::infer`] for the height, which is what a decoder
98	/// assumes for an unlabelled stream.
99	pub color: Color,
100}
101
102impl Config {
103	pub fn new(width: u32, height: u32, framerate: u32, bitrate: u32, gop: u32) -> Self {
104		Self {
105			width,
106			height,
107			framerate,
108			bitrate,
109			gop,
110			device: None,
111			color: Color::infer(height),
112		}
113	}
114}
115
116/// Per-frame reference metadata. (Mirrors cros-codecs `DpbEntryMeta`.)
117#[derive(Copy, Clone)]
118struct FrameMeta {
119	poc: u32,
120	frame_num: u32,
121}
122
123/// A VA-API H.264 encoder. Built once, fed NV12 frames, emits Annex-B H.264.
124pub struct Encoder {
125	config: Config,
126	sps: Rc<Sps>,
127	pps: Rc<Pps>,
128	width_mbs: u16,
129	height_mbs: u16,
130
131	// Source surface (reused; NV12 re-uploaded per frame) and a 2-deep
132	// reconstructed surface ring (current + previous reference for IPPP).
133	input: Option<Surface<()>>,
134	recon: Vec<Surface<()>>,
135	/// Reference from the previous encoded frame: (recon index, metadata).
136	reference: Option<(usize, FrameMeta)>,
137
138	counter: u32,
139
140	/// The video processor converting and scaling DMA-BUF input, opened on the
141	/// first frame that needs it.
142	processor: Option<Processor>,
143	/// Set by a bitrate change, and cleared by the next frame, which asks the
144	/// driver to restart its rate controller at the new target.
145	reset_rate_control: bool,
146
147	// Drop order: encode resources before the display they belong to.
148	coded: EncCodedBuffer,
149	coded_size: usize,
150	context: Rc<Context>,
151	_va_config: VaConfig,
152	display: Arc<Display>,
153}
154
155impl Encoder {
156	/// Opens [`Config::device`], or the first render node that encodes H.264, and sets up an encode session for `config`.
157	///
158	/// # Errors
159	///
160	/// Fails when libva cannot be loaded, when the named node cannot be opened
161	/// or does not encode H.264 (a named node is never swapped for another), when
162	/// no node encodes H.264, and when the driver refuses the session.
163	pub fn new(mut config: Config) -> anyhow::Result<Self> {
164		let (device, display) = match config.device.take() {
165			Some(device) => {
166				let display = Display::open_drm_display(&device)
167					.map_err(|e| anyhow::anyhow!("open DRM display {device:?}: {e:?}"))?;
168				(device, display)
169			}
170			None => {
171				crate::display::open_first(probe).map_err(|e| e.context("find a render node that encodes H.264"))?
172			}
173		};
174
175		let entrypoint = encode_entrypoint(&display)?;
176		let mut attrs = vec![
177			VAConfigAttrib {
178				type_: VAConfigAttribType::VAConfigAttribRTFormat,
179				value: VA_RT_FORMAT_YUV420,
180			},
181			VAConfigAttrib {
182				type_: VAConfigAttribType::VAConfigAttribRateControl,
183				value: VA_RC_CBR,
184			},
185		];
186		// The SPS and PPS go out as packed headers, so the VUI (color, frame
187		// buffering) and the level are ours. iHD takes them without being told;
188		// a driver that follows the spec writes its own unless the config asks.
189		let packed = packed_headers(&display, entrypoint);
190		if packed != 0 {
191			attrs.push(VAConfigAttrib {
192				type_: VAConfigAttribType::VAConfigAttribEncPackedHeaders,
193				value: packed,
194			});
195		} else {
196			log::warn!("the driver takes no packed SPS/PPS; the stream carries the driver's own headers");
197		}
198		let va_config = display
199			.create_config(attrs, VAProfile::VAProfileH264Main, entrypoint)
200			.map_err(|e| anyhow::anyhow!("create VA config: {e:?}"))?;
201
202		let context = display
203			.create_context::<()>(&va_config, config.width, config.height, None, true)
204			.map_err(|e| anyhow::anyhow!("create VA context: {e:?}"))?;
205
206		let coded_size = coded_size(config.bitrate);
207		let coded = context
208			.create_enc_coded(coded_size)
209			.map_err(|e| anyhow::anyhow!("create coded buffer: {e:?}"))?;
210
211		let input = make_surface(&display, &config)?;
212		let recon = vec![make_surface(&display, &config)?, make_surface(&display, &config)?];
213
214		let (sps, pps) = build_sps_pps(&config);
215		let width_mbs = sps.pic_width_in_mbs_minus1 + 1;
216		let height_mbs = sps.pic_height_in_map_units_minus1 + 1;
217
218		log::info!(
219			"opened VA-API H.264 encoder on {:?}: {}x{} @ {}fps, {} bps, low power: {}",
220			device,
221			config.width,
222			config.height,
223			config.framerate,
224			config.bitrate,
225			entrypoint == VAEntrypoint::VAEntrypointEncSliceLP,
226		);
227		config.device = Some(device);
228		Ok(Self {
229			config,
230			sps,
231			pps,
232			width_mbs,
233			height_mbs,
234			input: Some(input),
235			recon,
236			reference: None,
237			counter: 0,
238			processor: None,
239			reset_rate_control: false,
240			coded,
241			coded_size,
242			context,
243			_va_config: va_config,
244			display,
245		})
246	}
247
248	/// Encodes one tightly-packed NV12 frame, returning Annex-B H.264 (with inline
249	/// SPS/PPS on each IDR). `keyframe` forces an IDR.
250	///
251	/// # Errors
252	///
253	/// Fails when `nv12` is shorter than a frame at the encoder's size, and when
254	/// the upload or the encode fails.
255	pub fn encode_nv12(&mut self, nv12: &[u8], keyframe: bool) -> anyhow::Result<Vec<u8>> {
256		let (width, height) = (self.config.width as usize, self.config.height as usize);
257		let expected = width * height + width * height.div_ceil(2);
258		if nv12.len() < expected {
259			anyhow::bail!("NV12 frame is {} bytes, {width}x{height} needs {expected}", nv12.len());
260		}
261		let input = self.input.take().expect("input surface present");
262		let result = upload_nv12(&self.display, &input, self.config.width, self.config.height, nv12)
263			.and_then(|()| self.encode_surface(&input, keyframe));
264		self.input = Some(input);
265		result
266	}
267
268	/// Encodes one frame held in a DMA-BUF, returning Annex-B H.264 like
269	/// [`encode_nv12`](Self::encode_nv12).
270	///
271	/// An NV12 buffer at the encoder's size, whose size is a whole number of
272	/// macroblocks and whose [`color`](DmaBuf::color) is the stream's or
273	/// unknown, is imported and encoded in place. Any other mapped format or
274	/// size goes through the video processor into the encoder's own surface
275	/// first: scaled to the encoder's size and converted to [`Config::color`].
276	/// A YUV buffer with no color of its own is taken to be in that space
277	/// already. Nothing reaches the CPU on either path.
278	///
279	/// The macroblock condition is not a formality: the encoder reads whole
280	/// macroblocks, so it reads 1088 rows of a 1080-row picture, and a producer
281	/// that places the chroma plane right after the last visible luma row
282	/// (PipeWire does) has nothing there to read.
283	///
284	/// The producer must have finished writing the buffer; this does not wait
285	/// on its fence.
286	///
287	/// # Errors
288	///
289	/// Fails when the driver refuses to import the buffer (an unsupported
290	/// modifier or layout), when the device has no video processor and the
291	/// buffer needs one, and when the encode itself fails. The encoder stays
292	/// usable after a refused import, so a caller can fall back to
293	/// [`encode_nv12`](Self::encode_nv12) for the same frame.
294	pub fn encode_dmabuf(&mut self, buffer: DmaBuf, keyframe: bool) -> anyhow::Result<Vec<u8>> {
295		let fourcc = buffer
296			.fourcc()
297			.ok_or_else(|| anyhow::anyhow!("no VA-API format for DRM format {:#010x}", buffer.drm_format))?;
298		let color = self.config.color;
299		let in_place = fourcc == VA_FOURCC_NV12
300			&& (buffer.width, buffer.height) == (self.config.width, self.config.height)
301			&& buffer.width % 16 == 0
302			&& buffer.height % 16 == 0
303			&& buffer.color.is_none_or(|own| own == color);
304		if in_place {
305			let source = buffer.import(&self.display, Some(UsageHint::USAGE_HINT_ENCODER))?;
306			return self.encode_surface(&source, keyframe);
307		}
308
309		let input_color = match is_rgb(fourcc) {
310			true => None,
311			false => Some(buffer.color.unwrap_or(color)),
312		};
313		let source = buffer.import(&self.display, Some(UsageHint::USAGE_HINT_VPP_READ))?;
314		let input = self.input.take().expect("input surface present");
315		let result = self
316			.processor()
317			.and_then(|processor| processor.blit(&source, &input, input_color, Some(color)))
318			.and_then(|()| self.encode_surface(&input, keyframe));
319		self.input = Some(input);
320		result
321	}
322
323	/// Changes the target bitrate, in bits per second, from the next frame on.
324	///
325	/// The rate control parameters go out with every frame, so this neither
326	/// forces an IDR nor rebuilds the session; the next frame also asks the
327	/// driver to reset its rate controller. It then converges over its window,
328	/// [`RATE_CONTROL_WINDOW_MS`].
329	///
330	/// The level in the SPS is not revisited, so a rate far above the one the
331	/// stream opened with can exceed what that level allows.
332	///
333	/// # Errors
334	///
335	/// Fails for a bitrate of zero, and when a larger coded buffer for the new
336	/// rate cannot be allocated, in which case the old rate stays.
337	pub fn set_bitrate(&mut self, bitrate: u32) -> anyhow::Result<()> {
338		if bitrate == 0 {
339			anyhow::bail!("a bitrate of zero");
340		}
341		let size = coded_size(bitrate);
342		if size > self.coded_size {
343			// A higher rate can produce a picture larger than the buffer sized for
344			// the old one, and a truncated picture is corrupt rather than short.
345			self.coded = self
346				.context
347				.create_enc_coded(size)
348				.map_err(|e| anyhow::anyhow!("create coded buffer: {e:?}"))?;
349			self.coded_size = size;
350		}
351		if bitrate != self.config.bitrate {
352			self.reset_rate_control = true;
353		}
354		self.config.bitrate = bitrate;
355		Ok(())
356	}
357
358	/// Returns the configuration in effect, including a bitrate changed since opening and the render node picked when none was named.
359	pub fn config(&self) -> &Config {
360		&self.config
361	}
362
363	/// Returns the video processor, opening it on first use.
364	fn processor(&mut self) -> anyhow::Result<&Processor> {
365		if self.processor.is_none() {
366			self.processor = Some(Processor::with_display(Arc::clone(&self.display))?);
367		}
368		Ok(self.processor.as_ref().expect("opened above"))
369	}
370
371	/// Encodes the picture in `source`, which must be NV12 at the encoder's size.
372	fn encode_surface<D: SurfaceMemoryDescriptor>(
373		&mut self,
374		source: &Surface<D>,
375		keyframe: bool,
376	) -> anyhow::Result<Vec<u8>> {
377		let is_idr = keyframe || self.counter % self.config.gop == 0;
378		if is_idr {
379			// IDR resets the H.264 frame numbering and the reference list.
380			self.counter = 0;
381			self.reference = None;
382		}
383
384		// frame_num counts modulo MaxFrameNum, and the slice header carries only
385		// the low bits of the picture order count; the full count goes to the
386		// driver in the picture parameters.
387		let meta = FrameMeta {
388			poc: self.counter.wrapping_mul(2),
389			frame_num: self.counter % LIMIT,
390		};
391		let recon_idx = (self.counter as usize) % self.recon.len();
392
393		let slice_type = if is_idr { SliceType::I } else { SliceType::P };
394		let header = SliceHeaderBuilder::new(&self.pps)
395			.slice_type(slice_type)
396			.first_mb_in_slice(0)
397			.frame_num(meta.frame_num as u16)
398			.pic_order_cnt_lsb((meta.poc % (LIMIT * 2)) as u16)
399			.build();
400
401		let num_macroblocks = self.width_mbs as u32 * self.height_mbs as u32;
402		let bits_per_second = self.config.bitrate;
403
404		// Reference (previous reconstructed frame) for P slices: surface id + meta.
405		let reference = match (is_idr, self.reference) {
406			(false, Some((ref_idx, ref_meta))) => Some((self.recon[ref_idx].id(), ref_meta)),
407			_ => None,
408		};
409
410		let seq_param = build_enc_seq_param(&self.sps, bits_per_second, LIMIT, 0);
411		let pic_param = build_enc_pic_param(
412			&self.pps,
413			&self.coded,
414			self.recon[recon_idx].id(),
415			meta,
416			is_idr,
417			reference,
418		);
419		let slice_param = build_enc_slice_param(&self.pps, &header, reference, num_macroblocks);
420
421		let mut picture = Picture::new(meta.frame_num as u64, Rc::clone(&self.context), source);
422
423		// VA-API spec buffer order: sequence, picture, slice, then packed headers,
424		// then rate-control misc params.
425		picture.add_buffer(self.create(seq_param)?);
426		picture.add_buffer(self.create(pic_param)?);
427		picture.add_buffer(self.create(slice_param)?);
428
429		if is_idr {
430			let (sps_param, sps_data) = packed_header(EncPackedHeaderType::Sequence, &self.packed_sps()?);
431			let (pps_param, pps_data) = packed_header(EncPackedHeaderType::Picture, &self.packed_pps()?);
432			picture.add_buffer(self.create(sps_param)?);
433			picture.add_buffer(self.create(sps_data)?);
434			picture.add_buffer(self.create(pps_param)?);
435			picture.add_buffer(self.create(pps_data)?);
436		}
437
438		let rc = EncMiscParameterRateControl::new(
439			bits_per_second,
440			100,                    // target_percentage (CBR)
441			RATE_CONTROL_WINDOW_MS, // window_size (ms)
442			u32::from((MIN_QP + MAX_QP) / 2),
443			MIN_QP as u32,
444			0, // basic_unit_size
445			// Do not let rate control drop a frame to meet the bitrate. A live
446			// encoder has already admitted this frame into the media timeline.
447			RcFlags::new(u32::from(self.reset_rate_control), 1, 0, 0, 0, 0, 0, 0, 0),
448			0, // icq_quality_factor
449			MAX_QP as u32,
450			0, // quality_factor
451			0, // target_frame_size
452		);
453		picture.add_buffer(self.create(BufferType::EncMiscParameter(EncMiscParameter::RateControl(rc)))?);
454
455		let framerate = EncMiscParameterFrameRate::new(self.config.framerate, 0);
456		picture.add_buffer(self.create(BufferType::EncMiscParameter(EncMiscParameter::FrameRate(framerate)))?);
457
458		let picture = picture.begin().map_err(|e| anyhow::anyhow!("picture begin: {e:?}"))?;
459		let picture = picture.render().map_err(|e| anyhow::anyhow!("picture render: {e:?}"))?;
460		let picture = picture.end().map_err(|e| anyhow::anyhow!("picture end: {e:?}"))?;
461		// Sync (PictureEnd -> PictureSync) so the source is no longer read and
462		// the coded buffer is ready.
463		picture
464			.sync()
465			.map_err(|(e, _)| anyhow::anyhow!("picture sync: {e:?}"))?;
466
467		// The reconstructed surface syncs implicitly; read the coded bitstream.
468		let bitstream = self.read_coded()?;
469
470		self.reference = Some((recon_idx, meta));
471		self.counter += 1;
472		self.reset_rate_control = false;
473		Ok(bitstream)
474	}
475
476	fn create(&self, buffer: BufferType) -> anyhow::Result<crate::Buffer> {
477		self.context
478			.create_buffer(buffer)
479			.map_err(|e| anyhow::anyhow!("create VA buffer: {e:?}"))
480	}
481
482	fn packed_sps(&self) -> anyhow::Result<Vec<u8>> {
483		let mut buf = Vec::new();
484		Synthesizer::<'_, Sps, _>::synthesize(3, &self.sps, &mut buf, true)
485			.map_err(|e| anyhow::anyhow!("synthesize SPS: {e:?}"))?;
486		Ok(buf)
487	}
488
489	fn packed_pps(&self) -> anyhow::Result<Vec<u8>> {
490		let mut buf = Vec::new();
491		Synthesizer::<'_, Pps, _>::synthesize(3, &self.pps, &mut buf, true)
492			.map_err(|e| anyhow::anyhow!("synthesize PPS: {e:?}"))?;
493		Ok(buf)
494	}
495
496	fn read_coded(&self) -> anyhow::Result<Vec<u8>> {
497		let mapped = MappedCodedBuffer::new(&self.coded).map_err(|e| anyhow::anyhow!("map coded buffer: {e:?}"))?;
498		let mut out = Vec::new();
499		for segment in mapped.iter() {
500			out.extend_from_slice(segment.buf);
501		}
502		Ok(out)
503	}
504}
505
506/// Checks that `display`'s driver can encode H.264, which is what [`Encoder::new`] needs of a render node.
507///
508/// # Errors
509///
510/// Fails when the driver offers neither the full nor the low-power H.264 Main
511/// encode entrypoint, or cannot be asked.
512pub fn probe(display: &Display) -> anyhow::Result<()> {
513	encode_entrypoint(display).map(drop)
514}
515
516/// Returns the entrypoint to encode H.264 Main with: the full one, or the low-power one where it is all there is.
517///
518/// Some Intel parts expose H.264 encode only through `VAEntrypointEncSliceLP`.
519fn encode_entrypoint(display: &Display) -> anyhow::Result<VAEntrypoint::Type> {
520	let entrypoints = display
521		.query_config_entrypoints(VAProfile::VAProfileH264Main)
522		.map_err(|e| anyhow::anyhow!("query H.264 Main entrypoints: {e:?}"))?;
523	pick_entrypoint(&entrypoints).ok_or_else(|| anyhow::anyhow!("the device has no H.264 Main encode entrypoint"))
524}
525
526/// Returns the encode entrypoint to use out of those a device offers: the full one when present.
527fn pick_entrypoint(offered: &[VAEntrypoint::Type]) -> Option<VAEntrypoint::Type> {
528	[VAEntrypoint::VAEntrypointEncSlice, VAEntrypoint::VAEntrypointEncSliceLP]
529		.into_iter()
530		.find(|entrypoint| offered.contains(entrypoint))
531}
532
533/// Returns the packed header kinds to ask for: sequence and picture, where the driver takes them.
534///
535/// Zero when the driver takes neither, or when asking fails.
536fn packed_headers(display: &Display, entrypoint: VAEntrypoint::Type) -> u32 {
537	let mut attrs = [VAConfigAttrib {
538		type_: VAConfigAttribType::VAConfigAttribEncPackedHeaders,
539		value: 0,
540	}];
541	if display
542		.get_config_attributes(VAProfile::VAProfileH264Main, entrypoint, &mut attrs)
543		.is_err()
544		|| attrs[0].value & VA_ATTRIB_NOT_SUPPORTED != 0
545	{
546		return 0;
547	}
548	attrs[0].value & (VA_ENC_PACKED_HEADER_SEQUENCE | VA_ENC_PACKED_HEADER_PICTURE)
549}
550
551/// Returns the lowest H.264 level, from 4 up, whose frame size and macroblock rate cover the stream.
552///
553/// Only those two limits: the level's maximum bitrate is not checked.
554///
555/// Level 4 is the floor because it was the fixed level before, and decoders
556/// take the DPB size from `max_dec_frame_buffering` rather than from the level,
557/// so a lower one would buy nothing. Above 1080p (a 1440p or 4K screen share) a
558/// fixed level 4 would declare a stream its decoder may refuse.
559fn level(width: u32, height: u32, framerate: u32) -> Level {
560	// H.264 Table A-1: (level, MaxMBPS, MaxFS).
561	const LEVELS: [(Level, u64, u64); 8] = [
562		(Level::L4, 245_760, 8_192),
563		(Level::L4_2, 522_240, 8_704),
564		(Level::L5, 589_824, 22_080),
565		(Level::L5_1, 983_040, 36_864),
566		(Level::L5_2, 2_073_600, 36_864),
567		(Level::L6, 4_177_920, 139_264),
568		(Level::L6_1, 8_355_840, 139_264),
569		(Level::L6_2, 16_711_680, 139_264),
570	];
571	let frame = u64::from(width.div_ceil(16)) * u64::from(height.div_ceil(16));
572	let rate = frame * u64::from(framerate.max(1));
573	LEVELS
574		.into_iter()
575		.find(|&(_, max_rate, max_frame)| frame <= max_frame && rate <= max_rate)
576		.map(|(level, _, _)| level)
577		.unwrap_or(Level::L6_2)
578}
579
580/// Returns the coded buffer size in bytes for `bitrate`: two seconds of it, and at least 1.5 MB.
581fn coded_size(bitrate: u32) -> usize {
582	(bitrate as usize / 4).max(1_500_000)
583}
584
585/// Returns whether the VA fourcc `fourcc` is packed RGB, which has no YUV matrix of its own.
586fn is_rgb(fourcc: u32) -> bool {
587	matches!(
588		fourcc,
589		VA_FOURCC_BGRX | VA_FOURCC_BGRA | VA_FOURCC_RGBX | VA_FOURCC_RGBA | VA_FOURCC_XRGB | VA_FOURCC_ARGB
590	)
591}
592
593/// Allocate one NV12 encode surface.
594fn make_surface(display: &Arc<Display>, config: &Config) -> anyhow::Result<Surface<()>> {
595	let mut surfaces = display
596		.create_surfaces::<()>(
597			VA_RT_FORMAT_YUV420,
598			Some(VA_FOURCC_NV12),
599			config.width,
600			config.height,
601			Some(UsageHint::USAGE_HINT_ENCODER),
602			vec![()],
603		)
604		.map_err(|e| anyhow::anyhow!("create surface: {e:?}"))?;
605	surfaces.pop().ok_or_else(|| anyhow::anyhow!("no surface created"))
606}
607
608/// Upload tightly-packed NV12 into a surface. Ported from cros-codecs
609/// `upload_nv12_img` (honors the image's per-plane offsets + pitches).
610fn upload_nv12(
611	display: &Arc<Display>,
612	surface: &Surface<()>,
613	width: u32,
614	height: u32,
615	data: &[u8],
616) -> anyhow::Result<()> {
617	let formats = display
618		.query_image_formats()
619		.map_err(|e| anyhow::anyhow!("query image formats: {e:?}"))?;
620	let format = formats
621		.into_iter()
622		.find(|f| f.fourcc == VA_FOURCC_NV12)
623		.ok_or_else(|| anyhow::anyhow!("driver has no NV12 image format"))?;
624
625	let mut image = Image::create_from(surface, format, surface.size(), surface.size())
626		.map_err(|e| anyhow::anyhow!("create image: {e:?}"))?;
627	let va_image = *image.image();
628	let dest: &mut [u8] = image.as_mut();
629	let (w, h) = (width as usize, height as usize);
630
631	// Luma plane.
632	let mut src = data;
633	let mut dst = &mut dest[va_image.offsets[0] as usize..];
634	for _ in 0..h {
635		dst[..w].copy_from_slice(&src[..w]);
636		dst = &mut dst[va_image.pitches[0] as usize..];
637		src = &src[w..];
638	}
639	// Interleaved chroma plane (h/2 rows of w bytes).
640	let mut src = &data[w * h..];
641	let mut dst = &mut dest[va_image.offsets[1] as usize..];
642	for _ in 0..h / 2 {
643		dst[..w].copy_from_slice(&src[..w]);
644		dst = &mut dst[va_image.pitches[1] as usize..];
645		src = &src[w..];
646	}
647
648	drop(image);
649	surface.sync().map_err(|e| anyhow::anyhow!("surface sync: {e:?}"))?;
650	Ok(())
651}
652
653/// Build the SPS/PPS for a low-latency IPPP stream. Ported from
654/// discord/cros-codecs `LowDelayH264Delegate::new_sequence` (BSD-3-Clause).
655fn build_sps_pps(config: &Config) -> (Rc<Sps>, Rc<Pps>) {
656	let level = level(config.width, config.height, config.framerate);
657	let (primaries, transfer, matrix) = config.color.vui();
658	let sps = SpsBuilder::new()
659		.seq_parameter_set_id(0)
660		.profile_idc(Profile::Main)
661		.chroma_format_idc(1)
662		.level_idc(level)
663		.max_frame_num(LIMIT)
664		.pic_order_cnt_type(0)
665		.max_pic_order_cnt_lsb(LIMIT * 2)
666		.max_num_ref_frames(1)
667		.frame_mbs_only_flag(true)
668		.direct_8x8_inference_flag(level >= Level::L3)
669		.resolution(config.width, config.height)
670		.bit_depth_luma(8)
671		.bit_depth_chroma(8)
672		.aspect_ratio(1, 1)
673		.timing_info(1, config.framerate * 2, false)
674		.video_signal_type(config.color.full_range, primaries, transfer, matrix)
675		.max_num_reorder_frames(0)
676		.max_dec_frame_buffering(1)
677		.build();
678
679	let pps = PpsBuilder::new(Rc::clone(&sps))
680		.pic_parameter_set_id(0)
681		.pic_init_qp(26)
682		.entropy_coding_mode_flag(true)
683		.transform_8x8_mode_flag(false)
684		.deblocking_filter_control_present_flag(true)
685		.num_ref_idx_l0_default_active(1)
686		.num_ref_idx_l1_default_active_minus1(0)
687		.build();
688
689	(sps, pps)
690}
691
692fn build_invalid_pic() -> PictureH264 {
693	PictureH264::new(VA_INVALID_ID, 0, VA_PICTURE_H264_INVALID, 0, 0)
694}
695
696fn build_h264_pic(surface_id: u32, meta: FrameMeta) -> PictureH264 {
697	PictureH264::new(
698		surface_id,
699		meta.frame_num,
700		VA_PICTURE_H264_SHORT_TERM_REFERENCE,
701		meta.poc as i32,
702		meta.poc as i32,
703	)
704}
705
706/// Ported from cros-codecs `build_enc_seq_param` (BSD-3-Clause).
707fn build_enc_seq_param(sps: &Sps, bits_per_second: u32, intra_period: u32, ip_period: u32) -> BufferType {
708	let seq_fields = H264EncSeqFields::new(
709		sps.chroma_format_idc as u32,
710		sps.frame_mbs_only_flag as u32,
711		sps.mb_adaptive_frame_field_flag as u32,
712		sps.seq_scaling_matrix_present_flag as u32,
713		sps.direct_8x8_inference_flag as u32,
714		sps.log2_max_frame_num_minus4 as u32,
715		sps.pic_order_cnt_type as u32,
716		sps.log2_max_pic_order_cnt_lsb_minus4 as u32,
717		sps.delta_pic_order_always_zero_flag as u32,
718	);
719
720	let frame_crop = if sps.frame_cropping_flag {
721		Some(H264EncFrameCropOffsets::new(
722			sps.frame_crop_left_offset,
723			sps.frame_crop_right_offset,
724			sps.frame_crop_top_offset,
725			sps.frame_crop_bottom_offset,
726		))
727	} else {
728		None
729	};
730
731	let vui_fields = if sps.vui_parameters_present_flag {
732		Some(H264VuiFields::new(
733			sps.vui_parameters.aspect_ratio_idc as u32,
734			sps.vui_parameters.timing_info_present_flag as u32,
735			sps.vui_parameters.bitstream_restriction_flag as u32,
736			sps.vui_parameters.log2_max_mv_length_horizontal,
737			sps.vui_parameters.log2_max_mv_length_vertical,
738			sps.vui_parameters.fixed_frame_rate_flag as u32,
739			sps.vui_parameters.low_delay_hrd_flag as u32,
740			sps.vui_parameters.motion_vectors_over_pic_boundaries_flag as u32,
741		))
742	} else {
743		None
744	};
745
746	let mut offset_for_ref_frame = [0i32; 256];
747	offset_for_ref_frame[..255].copy_from_slice(&sps.offset_for_ref_frame[..]);
748
749	BufferType::EncSequenceParameter(EncSequenceParameter::H264(EncSequenceParameterBufferH264::new(
750		sps.seq_parameter_set_id,
751		sps.level_idc as u8,
752		intra_period,
753		intra_period,
754		ip_period,
755		bits_per_second,
756		sps.max_num_ref_frames as u32,
757		sps.pic_width_in_mbs_minus1 + 1,
758		sps.pic_height_in_map_units_minus1 + 1,
759		&seq_fields,
760		sps.bit_depth_luma_minus8,
761		sps.bit_depth_chroma_minus8,
762		sps.num_ref_frames_in_pic_order_cnt_cycle,
763		sps.offset_for_non_ref_pic,
764		sps.offset_for_top_to_bottom_field,
765		offset_for_ref_frame,
766		frame_crop,
767		vui_fields,
768		sps.vui_parameters.aspect_ratio_idc,
769		sps.vui_parameters.sar_width as u32,
770		sps.vui_parameters.sar_height as u32,
771		sps.vui_parameters.num_units_in_tick,
772		sps.vui_parameters.time_scale,
773	)))
774}
775
776/// Ported from cros-codecs `build_enc_pic_param` (BSD-3-Clause).
777fn build_enc_pic_param(
778	pps: &Pps,
779	coded: &EncCodedBuffer,
780	recon_id: u32,
781	meta: FrameMeta,
782	is_idr: bool,
783	reference: Option<(u32, FrameMeta)>,
784) -> BufferType {
785	let pic_fields = H264EncPicFields::new(
786		is_idr as u32,
787		1, // reference_pic_flag: this frame is a short-term reference
788		pps.entropy_coding_mode_flag as u32,
789		pps.weighted_pred_flag as u32,
790		pps.weighted_bipred_idc as u32,
791		pps.constrained_intra_pred_flag as u32,
792		pps.transform_8x8_mode_flag as u32,
793		pps.deblocking_filter_control_present_flag as u32,
794		pps.redundant_pic_cnt_present_flag as u32,
795		0,
796		pps.pic_scaling_matrix_present_flag as u32,
797	);
798
799	let curr_pic = build_h264_pic(recon_id, meta);
800	let mut reference_frames: [PictureH264; 16] = std::array::from_fn(|_| build_invalid_pic());
801	if let Some((id, m)) = reference {
802		reference_frames[0] = build_h264_pic(id, m);
803	}
804
805	BufferType::EncPictureParameter(EncPictureParameter::H264(EncPictureParameterBufferH264::new(
806		curr_pic,
807		reference_frames,
808		coded.id(),
809		pps.pic_parameter_set_id,
810		pps.seq_parameter_set_id,
811		0,
812		meta.frame_num as u16,
813		(pps.pic_init_qp_minus26 + 26) as u8,
814		pps.num_ref_idx_l0_default_active_minus1,
815		pps.num_ref_idx_l1_default_active_minus1,
816		pps.chroma_qp_index_offset,
817		pps.second_chroma_qp_index_offset,
818		&pic_fields,
819	)))
820}
821
822/// Ported from cros-codecs `build_enc_slice_param` (BSD-3-Clause), simplified for
823/// IPPP (no weighted prediction, single reference in list 0).
824fn build_enc_slice_param(
825	pps: &Pps,
826	header: &SliceHeader,
827	reference: Option<(u32, FrameMeta)>,
828	num_macroblocks: u32,
829) -> BufferType {
830	let mut ref_pic_list_0: [PictureH264; 32] = std::array::from_fn(|_| build_invalid_pic());
831	if let Some((id, m)) = reference {
832		ref_pic_list_0[0] = build_h264_pic(id, m);
833	}
834	let ref_pic_list_1: [PictureH264; 32] = std::array::from_fn(|_| build_invalid_pic());
835
836	let (num_ref_idx_l0_active_minus1, num_ref_idx_l1_active_minus1) = if header.num_ref_idx_active_override_flag {
837		(header.num_ref_idx_l0_active_minus1, header.num_ref_idx_l1_active_minus1)
838	} else {
839		(
840			pps.num_ref_idx_l0_default_active_minus1,
841			pps.num_ref_idx_l1_default_active_minus1,
842		)
843	};
844
845	BufferType::EncSliceParameter(EncSliceParameter::H264(EncSliceParameterBufferH264::new(
846		header.first_mb_in_slice,
847		num_macroblocks,
848		VA_INVALID_ID,
849		header.slice_type as u8,
850		pps.pic_parameter_set_id,
851		header.idr_pic_id,
852		header.pic_order_cnt_lsb,
853		header.delta_pic_order_cnt_bottom,
854		header.delta_pic_order_cnt,
855		header.direct_spatial_mv_pred_flag as u8,
856		header.num_ref_idx_active_override_flag as u8,
857		num_ref_idx_l0_active_minus1,
858		num_ref_idx_l1_active_minus1,
859		ref_pic_list_0,
860		ref_pic_list_1,
861		header.pred_weight_table.luma_log2_weight_denom,
862		header.pred_weight_table.chroma_log2_weight_denom,
863		0,
864		header.pred_weight_table.luma_weight_l0,
865		[0i16; 32],
866		0,
867		header.pred_weight_table.chroma_weight_l0,
868		[[0i16; 2]; 32],
869		0,
870		header.pred_weight_table.luma_weight_l1,
871		[0i16; 32],
872		0,
873		header.pred_weight_table.chroma_weight_l1,
874		[[0i16; 2]; 32],
875		header.cabac_init_idc,
876		header.slice_qp_delta,
877		header.disable_deblocking_filter_idc,
878		header.slice_alpha_c0_offset_div2,
879		header.slice_beta_offset_div2,
880	)))
881}
882
883/// A packed-header parameter + data buffer pair for a synthesized NAL.
884fn packed_header(kind: EncPackedHeaderType, data: &[u8]) -> (BufferType, BufferType) {
885	let param =
886		BufferType::EncPackedHeaderParameter(EncPackedHeaderParameter::new(kind, (data.len() * 8) as u32, true));
887	let payload = BufferType::EncPackedHeaderData(data.to_vec());
888	(param, payload)
889}
890
891#[cfg(test)]
892mod tests {
893	use std::path::PathBuf;
894	use std::process::Command;
895	use std::sync::atomic::{AtomicU32, Ordering};
896
897	use super::*;
898	use crate::dmabuf::DmaBuf;
899	use crate::{VA_RT_FORMAT_RGB32, VA_RT_FORMAT_YUV420};
900
901	const WIDTH: u32 = 320;
902	const HEIGHT: u32 = 240;
903
904	fn config() -> Config {
905		Config::new(WIDTH, HEIGHT, 30, 2_000_000, 30)
906	}
907
908	/// Opens an encoder, or `None` to skip on a machine without VA-API H.264 encode.
909	fn encoder(config: Config) -> Option<Encoder> {
910		match Encoder::new(config) {
911			Ok(encoder) => Some(encoder),
912			Err(err) => {
913				eprintln!("skipping: no VA-API H.264 encoder: {err:#}");
914				None
915			}
916		}
917	}
918
919	/// A diagonal gradient with a block moving across it, so a stale or
920	/// misplaced reference shows up as the block in the wrong place.
921	fn nv12_frame(width: u32, height: u32, step: u32) -> Vec<u8> {
922		let (w, h) = (width as usize, height as usize);
923		let mut data = vec![128u8; w * h + w * h.div_ceil(2)];
924		for y in 0..h {
925			for x in 0..w {
926				data[y * w + x] = (32 + (x + y) * 160 / (w + h)) as u8;
927			}
928		}
929		let x0 = (step as usize * 8) % (w / 2);
930		for y in h / 4..h / 2 {
931			for x in x0..x0 + w / 4 {
932				data[y * w + x] = 220;
933			}
934		}
935		data
936	}
937
938	/// Returns a temporary file path unique to this call.
939	///
940	/// The tests run as threads of one process, so the process id alone would
941	/// let two of them share a file.
942	fn temp_path(label: &str) -> PathBuf {
943		static NEXT: AtomicU32 = AtomicU32::new(0);
944		let index = NEXT.fetch_add(1, Ordering::Relaxed);
945		std::env::temp_dir().join(format!("moq-vaapi-{label}-{}-{index}.264", std::process::id()))
946	}
947
948	/// Decodes an Annex-B stream with ffmpeg's software decoder into NV12
949	/// pictures, or `None` to skip without ffmpeg.
950	fn ffmpeg_decode(stream: &[u8], name: &str) -> Option<Vec<u8>> {
951		// ffmpeg reads the elementary stream from a file rather than a pipe, so
952		// the test never has to drain its output while still writing its input.
953		let path = temp_path(name);
954		std::fs::write(&path, stream).expect("write the elementary stream");
955		let output = Command::new("ffmpeg")
956			.args(["-v", "error", "-i"])
957			.arg(&path)
958			.args(["-pix_fmt", "nv12", "-f", "rawvideo", "-"])
959			.output();
960		let _ = std::fs::remove_file(&path);
961		match output {
962			Ok(output) if output.status.success() => Some(output.stdout),
963			Ok(output) => panic!("ffmpeg refused the stream: {}", String::from_utf8_lossy(&output.stderr)),
964			Err(_) => {
965				eprintln!("skipping: no ffmpeg");
966				None
967			}
968		}
969	}
970
971	/// Asks ffprobe for the stream's `color_range,color_space`.
972	fn ffprobe_color(stream: &[u8]) -> Option<String> {
973		let path = temp_path("probe");
974		std::fs::write(&path, stream).expect("write the elementary stream");
975		let output = Command::new("ffprobe")
976			.args([
977				"-v",
978				"error",
979				"-show_entries",
980				"stream=color_range,color_space",
981				"-of",
982				"csv=p=0",
983			])
984			.arg(&path)
985			.output()
986			.ok();
987		let _ = std::fs::remove_file(&path);
988		Some(String::from_utf8(output?.stdout).ok()?.trim().to_string())
989	}
990
991	/// Returns the peak signal-to-noise ratio of the luma planes, in dB.
992	fn luma_psnr(a: &[u8], b: &[u8], width: u32, height: u32) -> f64 {
993		let len = (width * height) as usize;
994		let mse = a[..len]
995			.iter()
996			.zip(&b[..len])
997			.map(|(&x, &y)| (x as f64 - y as f64).powi(2))
998			.sum::<f64>()
999			/ len as f64;
1000		10.0 * (255.0f64.powi(2) / mse.max(1e-9)).log10()
1001	}
1002
1003	/// Returns the mean of the samples in a rectangle of one plane.
1004	fn mean(
1005		plane: &[u8],
1006		stride: usize,
1007		(x0, y0, x1, y1): (usize, usize, usize, usize),
1008		step: usize,
1009		at: usize,
1010	) -> f64 {
1011		let mut sum = 0.0;
1012		let mut count = 0.0;
1013		for y in y0..y1 {
1014			for x in x0..x1 {
1015				sum += plane[y * stride + x * step + at] as f64;
1016				count += 1.0;
1017			}
1018		}
1019		sum / count
1020	}
1021
1022	/// Returns the NAL unit types in an Annex-B access unit.
1023	fn nal_types(unit: &[u8]) -> Vec<u8> {
1024		let mut types = Vec::new();
1025		let mut i = 0;
1026		while i + 3 < unit.len() {
1027			if unit[i..i + 3] == [0, 0, 1] {
1028				types.push(unit[i + 3] & 0x1f);
1029				i += 3;
1030			}
1031			i += 1;
1032		}
1033		types
1034	}
1035
1036	/// Returns a driver surface of `fourcc` on `display`, filled through an image.
1037	fn surface_with(
1038		display: &Arc<Display>,
1039		fourcc: u32,
1040		rt_format: u32,
1041		(width, height): (u32, u32),
1042		fill: impl FnOnce(&mut [u8], &crate::bindings::VAImage),
1043	) -> Surface<()> {
1044		let surface = display
1045			.create_surfaces(
1046				rt_format,
1047				Some(fourcc),
1048				width,
1049				height,
1050				Some(UsageHint::USAGE_HINT_EXPORT),
1051				vec![()],
1052			)
1053			.expect("allocate a surface")
1054			.pop()
1055			.expect("one surface");
1056		let format = display
1057			.query_image_formats()
1058			.expect("query image formats")
1059			.into_iter()
1060			.find(|f| f.fourcc == fourcc)
1061			.expect("the driver has an image format for the surface format");
1062		let mut image =
1063			Image::create_from(&surface, format, (width, height), (width, height)).expect("create an image");
1064		let va_image = *image.image();
1065		fill(image.as_mut(), &va_image);
1066		drop(image);
1067		surface.sync().expect("sync the filled surface");
1068		surface
1069	}
1070
1071	/// Returns an NV12 surface holding `nv12_frame`, exported as a DMA-BUF.
1072	fn nv12_dmabuf(display: &Arc<Display>, (width, height): (u32, u32), step: u32) -> DmaBuf {
1073		let surface = surface_with(display, VA_FOURCC_NV12, VA_RT_FORMAT_YUV420, (width, height), |_, _| {});
1074		upload_nv12(display, &surface, width, height, &nv12_frame(width, height, step)).expect("upload");
1075		DmaBuf::from_prime(surface.export_prime().expect("export the surface")).expect("one object")
1076	}
1077
1078	/// With no node named, the encoder opens one that encodes H.264 and records
1079	/// which. A named node is used or refused, never swapped for another.
1080	#[test]
1081	fn the_render_node_is_found_or_named() {
1082		let Some(encoder) = encoder(config()) else { return };
1083		let device = encoder.config().device.clone().expect("the picked node is recorded");
1084		let display = Display::open_drm_display(&device).expect("reopen the picked node");
1085		probe(&display).expect("the picked node encodes H.264");
1086
1087		let named = Config {
1088			device: Some(PathBuf::from("/dev/null")),
1089			..config()
1090		};
1091		assert!(
1092			Encoder::new(named).is_err(),
1093			"a named node that is not a render node was swapped for one that is"
1094		);
1095	}
1096
1097	/// CPU NV12 in, H.264 out that an independent decoder reads back as the same picture.
1098	#[test]
1099	fn nv12_round_trips_through_a_software_decoder() {
1100		let Some(mut encoder) = encoder(config()) else { return };
1101		let mut stream = Vec::new();
1102		let mut frames = Vec::new();
1103		for step in 0..10 {
1104			let frame = nv12_frame(WIDTH, HEIGHT, step);
1105			stream.extend(encoder.encode_nv12(&frame, false).expect("encode"));
1106			frames.push(frame);
1107		}
1108		let Some(decoded) = ffmpeg_decode(&stream, "nv12") else {
1109			return;
1110		};
1111		let len = frames[0].len();
1112		assert_eq!(decoded.len(), len * frames.len(), "ffmpeg decoded a different count");
1113		for (index, frame) in frames.iter().enumerate() {
1114			let psnr = luma_psnr(frame, &decoded[index * len..], WIDTH, HEIGHT);
1115			assert!(psnr > 35.0, "picture {index} decoded at {psnr:.1} dB");
1116		}
1117	}
1118
1119	/// The SPS names the color space the encoder was configured with.
1120	#[test]
1121	fn the_stream_is_labelled_with_its_color_space() {
1122		for (config, expected) in [
1123			(config(), "tv,smpte170m"),
1124			(
1125				Config {
1126					color: Color::BT709,
1127					..config()
1128				},
1129				"tv,bt709",
1130			),
1131		] {
1132			let Some(mut encoder) = encoder(config) else { return };
1133			let unit = encoder
1134				.encode_nv12(&nv12_frame(WIDTH, HEIGHT, 0), true)
1135				.expect("encode");
1136			let Some(color) = ffprobe_color(&unit) else {
1137				eprintln!("skipping: no ffprobe");
1138				return;
1139			};
1140			assert_eq!(color, expected);
1141		}
1142	}
1143
1144	/// An IDR opens every group, and only there.
1145	#[test]
1146	fn keyframes_follow_the_group_length() {
1147		let Some(mut encoder) = encoder(Config { gop: 10, ..config() }) else {
1148			return;
1149		};
1150		for step in 0..25 {
1151			let unit = encoder
1152				.encode_nv12(&nv12_frame(WIDTH, HEIGHT, step), false)
1153				.expect("encode");
1154			let types = nal_types(&unit);
1155			let idr = types.contains(&5);
1156			assert_eq!(idr, step % 10 == 0, "frame {step} has NAL types {types:?}");
1157			if idr {
1158				assert!(
1159					types.contains(&7) && types.contains(&8),
1160					"an IDR carries its SPS and PPS"
1161				);
1162			}
1163		}
1164		// A forced keyframe restarts the count: the next IDR is a group later.
1165		for step in 0..=10 {
1166			let unit = encoder
1167				.encode_nv12(&nv12_frame(WIDTH, HEIGHT, step), step == 0)
1168				.expect("encode");
1169			assert_eq!(
1170				nal_types(&unit).contains(&5),
1171				step % 10 == 0,
1172				"frame {step} after the forced keyframe"
1173			);
1174		}
1175	}
1176
1177	#[test]
1178	fn the_full_entrypoint_wins_and_low_power_is_the_fallback() {
1179		use VAEntrypoint::{
1180			VAEntrypointEncSlice as Full, VAEntrypointEncSliceLP as LowPower, VAEntrypointVLD as Decode,
1181		};
1182		assert_eq!(pick_entrypoint(&[Decode, LowPower, Full]), Some(Full));
1183		assert_eq!(pick_entrypoint(&[Decode, LowPower]), Some(LowPower));
1184		assert_eq!(pick_entrypoint(&[Decode]), None);
1185	}
1186
1187	/// A buffer the driver refuses leaves the encoder usable, so the caller's
1188	/// CPU fallback can encode the same frame.
1189	#[test]
1190	fn a_refused_import_leaves_the_encoder_usable() {
1191		let Some(mut encoder) = encoder(config()) else { return };
1192		let Some(display) = Display::open() else {
1193			eprintln!("skipping: no VA-API display");
1194			return;
1195		};
1196		// A format with no mapping, and a real buffer under a modifier no driver has.
1197		let unmapped = DmaBuf {
1198			drm_format: u32::from_le_bytes(*b"ZZZZ"),
1199			..nv12_dmabuf(&display, (WIDTH, HEIGHT), 0)
1200		};
1201		assert!(encoder.encode_dmabuf(unmapped, true).is_err());
1202		let unknown_modifier = DmaBuf {
1203			modifier: 0x00ff_ffff_ffff_fffe,
1204			..nv12_dmabuf(&display, (WIDTH, HEIGHT), 0)
1205		};
1206		assert!(encoder.encode_dmabuf(unknown_modifier, true).is_err());
1207
1208		let unit = encoder
1209			.encode_nv12(&nv12_frame(WIDTH, HEIGHT, 0), true)
1210			.expect("the CPU path still encodes");
1211		let Some(decoded) = ffmpeg_decode(&unit, "refused") else {
1212			return;
1213		};
1214		let psnr = luma_psnr(&nv12_frame(WIDTH, HEIGHT, 0), &decoded, WIDTH, HEIGHT);
1215		assert!(psnr > 35.0, "decoded at {psnr:.1} dB");
1216	}
1217
1218	/// A 1080-row buffer is not encoded in place: the encoder reads 1088 rows,
1219	/// so it goes through the video processor into the padded input surface.
1220	#[test]
1221	fn a_buffer_short_of_whole_macroblocks_is_blitted() {
1222		let (width, height) = (1920, 1080);
1223		let Some(mut encoder) = encoder(Config::new(width, height, 30, 4_000_000, 30)) else {
1224			return;
1225		};
1226		let Some(display) = Display::open() else {
1227			eprintln!("skipping: no VA-API display");
1228			return;
1229		};
1230		let unit = encoder
1231			.encode_dmabuf(nv12_dmabuf(&display, (width, height), 0), true)
1232			.expect("encode the DMA-BUF");
1233		let Some(decoded) = ffmpeg_decode(&unit, "1080") else {
1234			return;
1235		};
1236		let psnr = luma_psnr(&nv12_frame(width, height, 0), &decoded, width, height);
1237		assert!(psnr > 35.0, "decoded at {psnr:.1} dB");
1238	}
1239
1240	/// A YUV buffer labelled with another color space is converted into the
1241	/// stream's, not relabelled.
1242	#[test]
1243	fn a_yuv_dmabuf_in_another_space_is_converted() {
1244		let Some(mut encoder) = encoder(Config {
1245			color: Color::BT601,
1246			..config()
1247		}) else {
1248			return;
1249		};
1250		let Some(display) = Display::open() else {
1251			eprintln!("skipping: no VA-API display");
1252			return;
1253		};
1254		// Flat red, written in BT.709.
1255		let [y, u, v] = limited([255, 0, 0], Color::BT709).map(|value| value.round() as u8);
1256		let surface = surface_with(
1257			&display,
1258			VA_FOURCC_NV12,
1259			VA_RT_FORMAT_YUV420,
1260			(WIDTH, HEIGHT),
1261			|data, image| {
1262				for row in 0..HEIGHT as usize {
1263					let at = image.offsets[0] as usize + row * image.pitches[0] as usize;
1264					data[at..at + WIDTH as usize].fill(y);
1265				}
1266				for row in 0..HEIGHT as usize / 2 {
1267					let at = image.offsets[1] as usize + row * image.pitches[1] as usize;
1268					for pair in data[at..at + WIDTH as usize].chunks_exact_mut(2) {
1269						pair.copy_from_slice(&[u, v]);
1270					}
1271				}
1272			},
1273		);
1274		let buffer = DmaBuf {
1275			color: Some(Color::BT709),
1276			..DmaBuf::from_prime(surface.export_prime().expect("export")).expect("one object")
1277		};
1278		let unit = encoder.encode_dmabuf(buffer, true).expect("encode");
1279		let Some(decoded) = ffmpeg_decode(&unit, "yuv-color") else {
1280			return;
1281		};
1282
1283		let (w, h) = (WIDTH as usize, HEIGHT as usize);
1284		let (luma, chroma) = decoded.split_at(w * h);
1285		let actual = [
1286			mean(luma, w, (w / 4, h / 4, w * 3 / 4, h * 3 / 4), 1, 0),
1287			mean(chroma, w, (w / 8, h / 8, w * 3 / 8, h * 3 / 8), 2, 0),
1288			mean(chroma, w, (w / 8, h / 8, w * 3 / 8, h * 3 / 8), 2, 1),
1289		];
1290		let expected = limited([255, 0, 0], Color::BT601);
1291		for (channel, (a, e)) in actual.iter().zip(expected).enumerate() {
1292			assert!(
1293				(a - e).abs() < 6.0,
1294				"channel {channel}: decoded {a:.1}, expected BT.601 {e:.1}"
1295			);
1296		}
1297	}
1298
1299	/// An NV12 DMA-BUF at the encoder's size is encoded from the producer's memory.
1300	#[test]
1301	fn an_nv12_dmabuf_encodes_in_place() {
1302		let Some(mut encoder) = encoder(config()) else { return };
1303		let Some(display) = Display::open() else {
1304			eprintln!("skipping: no VA-API display");
1305			return;
1306		};
1307		let mut stream = Vec::new();
1308		for step in 0..5 {
1309			let buffer = nv12_dmabuf(&display, (WIDTH, HEIGHT), step);
1310			stream.extend(encoder.encode_dmabuf(buffer, false).expect("encode the DMA-BUF"));
1311		}
1312		let Some(decoded) = ffmpeg_decode(&stream, "dmabuf") else {
1313			return;
1314		};
1315		let len = nv12_frame(WIDTH, HEIGHT, 0).len();
1316		assert_eq!(decoded.len(), 5 * len);
1317		for step in 0..5 {
1318			let psnr = luma_psnr(
1319				&nv12_frame(WIDTH, HEIGHT, step),
1320				&decoded[step as usize * len..],
1321				WIDTH,
1322				HEIGHT,
1323			);
1324			assert!(psnr > 35.0, "picture {step} decoded at {psnr:.1} dB");
1325		}
1326	}
1327
1328	/// An NV12 DMA-BUF at another size is scaled to the encoder's on the GPU.
1329	#[test]
1330	fn a_dmabuf_at_another_size_is_scaled() {
1331		let Some(mut encoder) = encoder(config()) else { return };
1332		let Some(display) = Display::open() else {
1333			eprintln!("skipping: no VA-API display");
1334			return;
1335		};
1336		let mut stream = Vec::new();
1337		for step in 0..3 {
1338			let buffer = nv12_dmabuf(&display, (WIDTH * 2, HEIGHT * 2), step);
1339			stream.extend(encoder.encode_dmabuf(buffer, false).expect("encode the DMA-BUF"));
1340		}
1341		let Some(decoded) = ffmpeg_decode(&stream, "scaled") else {
1342			return;
1343		};
1344		let len = nv12_frame(WIDTH, HEIGHT, 0).len();
1345		assert_eq!(decoded.len(), 3 * len, "the output is at the encoder's size");
1346		// Compare against the source scaled on the CPU by point sampling, which
1347		// differs from the GPU filter only at edges. 25 dB separates a scale
1348		// from a crop, which scores about 16 dB on this gradient.
1349		for step in 0..3u32 {
1350			let source = nv12_frame(WIDTH * 2, HEIGHT * 2, step);
1351			let mut expected = vec![0u8; (WIDTH * HEIGHT) as usize];
1352			for y in 0..HEIGHT as usize {
1353				for x in 0..WIDTH as usize {
1354					expected[y * WIDTH as usize + x] = source[(y * 2) * (WIDTH as usize * 2) + x * 2];
1355				}
1356			}
1357			let psnr = luma_psnr(&expected, &decoded[step as usize * len..], WIDTH, HEIGHT);
1358			assert!(psnr > 25.0, "picture {step} decoded at {psnr:.1} dB");
1359		}
1360	}
1361
1362	/// A packed RGB DMA-BUF is converted on the GPU into the configured color space.
1363	#[test]
1364	fn an_rgb_dmabuf_is_converted_to_the_configured_space() {
1365		// Four flat quadrants, so the check reads colors rather than edges.
1366		const QUADRANTS: [[u8; 3]; 4] = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255]];
1367		for color in [Color::BT601, Color::BT709] {
1368			let Some(mut encoder) = encoder(Config { color, ..config() }) else {
1369				return;
1370			};
1371			let Some(display) = Display::open() else {
1372				eprintln!("skipping: no VA-API display");
1373				return;
1374			};
1375			let surface = surface_with(
1376				&display,
1377				VA_FOURCC_BGRX,
1378				VA_RT_FORMAT_RGB32,
1379				(WIDTH, HEIGHT),
1380				|data, image| {
1381					let pitch = image.pitches[0] as usize;
1382					let offset = image.offsets[0] as usize;
1383					for y in 0..HEIGHT as usize {
1384						for x in 0..WIDTH as usize {
1385							let quadrant = (y >= HEIGHT as usize / 2) as usize * 2 + (x >= WIDTH as usize / 2) as usize;
1386							let [r, g, b] = QUADRANTS[quadrant];
1387							let at = offset + y * pitch + x * 4;
1388							data[at..at + 4].copy_from_slice(&[b, g, r, 255]);
1389						}
1390					}
1391				},
1392			);
1393			let exported = surface.export_prime().expect("export");
1394			let buffer = DmaBuf::from_prime(exported).expect("one object");
1395			assert_eq!(buffer.drm_format, crate::dmabuf::drm::XRGB8888);
1396			let unit = encoder.encode_dmabuf(buffer, false).expect("encode the RGB DMA-BUF");
1397			let Some(decoded) = ffmpeg_decode(&unit, "rgb") else {
1398				return;
1399			};
1400
1401			let (w, h) = (WIDTH as usize, HEIGHT as usize);
1402			let (luma, chroma) = decoded.split_at(w * h);
1403			for (quadrant, rgb) in QUADRANTS.iter().enumerate() {
1404				let (qx, qy) = (quadrant % 2, quadrant / 2);
1405				// The middle half of each quadrant, clear of the chroma bleed at its edges.
1406				let area = |scale: usize| {
1407					let (qw, qh) = (w / 2 / scale, h / 2 / scale);
1408					(
1409						qx * qw + qw / 4,
1410						qy * qh + qh / 4,
1411						qx * qw + qw * 3 / 4,
1412						qy * qh + qh * 3 / 4,
1413					)
1414				};
1415				let actual = [
1416					mean(luma, w, area(1), 1, 0),
1417					mean(chroma, w, area(2), 2, 0),
1418					mean(chroma, w, area(2), 2, 1),
1419				];
1420				let expected = limited(*rgb, color);
1421				for (channel, (a, e)) in actual.iter().zip(expected).enumerate() {
1422					assert!(
1423						(a - e).abs() < 6.0,
1424						"{color:?} quadrant {quadrant} channel {channel}: decoded {a:.1}, expected {e:.1}"
1425					);
1426				}
1427			}
1428		}
1429	}
1430
1431	#[test]
1432	fn the_level_covers_the_frame_size_and_rate() {
1433		assert_eq!(level(320, 240, 30), Level::L4);
1434		assert_eq!(level(1920, 1080, 30), Level::L4);
1435		assert_eq!(level(1920, 1080, 60), Level::L4_2);
1436		assert_eq!(level(2560, 1440, 30), Level::L5);
1437		assert_eq!(level(3840, 2160, 30), Level::L5_1);
1438		assert_eq!(level(3840, 2160, 60), Level::L5_2);
1439	}
1440
1441	/// Returns the limited-range Y'CbCr of an RGB color, per BT.601 or BT.709.
1442	fn limited([r, g, b]: [u8; 3], color: Color) -> [f64; 3] {
1443		let (kr, kb) = match color.matrix {
1444			crate::Matrix::Bt601 => (0.299, 0.114),
1445			crate::Matrix::Bt709 => (0.2126, 0.0722),
1446		};
1447		let (r, g, b) = (r as f64, g as f64, b as f64);
1448		let y = kr * r + (1.0 - kr - kb) * g + kb * b;
1449		[
1450			16.0 + y * 219.0 / 255.0,
1451			128.0 + (b - y) / (2.0 * (1.0 - kb)) * 224.0 / 255.0,
1452			128.0 + (r - y) / (2.0 * (1.0 - kr)) * 224.0 / 255.0,
1453		]
1454	}
1455
1456	/// Lowering the bitrate mid-stream shrinks the pictures without an IDR.
1457	#[test]
1458	fn a_bitrate_change_takes_effect_without_a_keyframe() {
1459		let Some(mut encoder) = encoder(Config::new(WIDTH, HEIGHT, 30, 2_000_000, 1_000)) else {
1460			return;
1461		};
1462		// The moving block over light noise: enough detail that 2 Mbps is
1463		// spent, not so much that the quantizer bound decides the size instead
1464		// of the rate controller.
1465		let mut state = 0x2545_f491_u32;
1466		let mut frame = |step: u32| {
1467			let mut frame = nv12_frame(WIDTH, HEIGHT, step);
1468			for sample in &mut frame[..(WIDTH * HEIGHT) as usize] {
1469				state ^= state << 13;
1470				state ^= state >> 17;
1471				state ^= state << 5;
1472				*sample = sample.saturating_add((state >> 28) as u8);
1473			}
1474			frame
1475		};
1476		// The mean picture size over the second half of 60 frames, once the
1477		// rate controller has settled.
1478		// Also asserts that no picture after the first is an IDR.
1479		let mut average = |encoder: &mut Encoder, from: u32| {
1480			let sizes: Vec<usize> = (from..from + 60)
1481				.map(|step| {
1482					let unit = encoder.encode_nv12(&frame(step), false).expect("encode");
1483					if step > 0 {
1484						assert!(!nal_types(&unit).contains(&5), "picture {step} is an IDR");
1485					}
1486					unit.len()
1487				})
1488				.collect();
1489			sizes[30..].iter().sum::<usize>() / 30
1490		};
1491
1492		let high = average(&mut encoder, 0);
1493		encoder.set_bitrate(250_000).expect("set the bitrate");
1494		assert_eq!(encoder.config().bitrate, 250_000);
1495		assert!(encoder.set_bitrate(0).is_err(), "a zero bitrate is refused");
1496		let low = average(&mut encoder, 60);
1497		eprintln!("mean picture at 2 Mbps: {high} bytes, at 250 kbps: {low} bytes");
1498		assert!(
1499			low * 3 < high,
1500			"the rate did not drop: {high} -> {low} bytes per picture"
1501		);
1502	}
1503}