use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use crate::dmabuf::{rt_format, DmaBuf};
use crate::{
bindings, BufferType, Color, Config as VaConfig, Context, Display, Picture, ProcColorProperties,
ProcPipelineParameterBuffer, Surface, SurfaceMemoryDescriptor, UsageHint, VAConfigAttrib, VAConfigAttribType,
VAEntrypoint, VAProfile,
};
pub fn probe(display: &Display) -> anyhow::Result<()> {
let entrypoints = display
.query_config_entrypoints(VAProfile::VAProfileNone)
.map_err(|e| anyhow::anyhow!("query video processing entrypoints: {e:?}"))?;
match entrypoints.contains(&VAEntrypoint::VAEntrypointVideoProc) {
true => Ok(()),
false => anyhow::bail!("the driver has no video post-processing entrypoint"),
}
}
pub struct Processor {
display: Arc<Display>,
contexts: RefCell<HashMap<(u32, u32), Rc<Context>>>,
config: VaConfig,
}
impl Processor {
pub fn new<P: AsRef<Path>>(device: P) -> anyhow::Result<Self> {
let device = device.as_ref();
let display =
Display::open_drm_display(device).map_err(|e| anyhow::anyhow!("open DRM display {device:?}: {e}"))?;
Self::with_display(display)
}
pub fn open() -> anyhow::Result<Self> {
let (_, display) =
crate::display::open_first(probe).map_err(|e| e.context("find a render node that post-processes video"))?;
Self::with_display(display)
}
pub fn with_display(display: Arc<Display>) -> anyhow::Result<Self> {
let mut attrs = [VAConfigAttrib {
type_: VAConfigAttribType::VAConfigAttribRTFormat,
value: 0,
}];
display
.get_config_attributes(
VAProfile::VAProfileNone,
VAEntrypoint::VAEntrypointVideoProc,
&mut attrs,
)
.map_err(|e| anyhow::anyhow!("query VPP config attributes: {e:?}"))?;
let config = display
.create_config(
attrs.to_vec(),
VAProfile::VAProfileNone,
VAEntrypoint::VAEntrypointVideoProc,
)
.map_err(|e| anyhow::anyhow!("create VPP config: {e:?}"))?;
Ok(Self {
display,
contexts: RefCell::new(HashMap::new()),
config,
})
}
pub fn display(&self) -> &Arc<Display> {
&self.display
}
pub fn import(&self, buffer: DmaBuf) -> anyhow::Result<Surface<DmaBuf>> {
buffer.import(&self.display, Some(UsageHint::USAGE_HINT_VPP_READ))
}
pub fn blit<D, E>(
&self,
input: &Surface<D>,
output: &Surface<E>,
input_color: Option<Color>,
output_color: Option<Color>,
) -> anyhow::Result<()>
where
D: SurfaceMemoryDescriptor,
E: SurfaceMemoryDescriptor,
{
if !Arc::ptr_eq(input.display(), &self.display) || !Arc::ptr_eq(output.display(), &self.display) {
anyhow::bail!("a VPP blit between surfaces of another display");
}
let size = output.size();
let context = self.context(size)?;
let result = run_blit(&context, input, output, input_color, output_color);
if result.is_err() {
self.contexts.borrow_mut().remove(&size);
}
result
}
pub fn process<D: SurfaceMemoryDescriptor>(
&self,
input: &Surface<D>,
fourcc: u32,
(width, height): (u32, u32),
input_color: Option<Color>,
output_color: Option<Color>,
) -> anyhow::Result<Surface<()>> {
let rt_format = rt_format(fourcc).ok_or_else(|| anyhow::anyhow!("no RT format for fourcc {fourcc:#x}"))?;
let output = self
.display
.create_surfaces(
rt_format,
Some(fourcc),
width,
height,
Some(UsageHint::USAGE_HINT_VPP_WRITE | UsageHint::USAGE_HINT_EXPORT),
vec![()],
)
.map_err(|e| anyhow::anyhow!("allocate a {width}x{height} VPP output surface: {e:?}"))?
.pop()
.ok_or_else(|| anyhow::anyhow!("vaCreateSurfaces returned no surface"))?;
self.blit(input, &output, input_color, output_color)?;
Ok(output)
}
pub fn retile(&self, buffer: DmaBuf) -> anyhow::Result<Surface<()>> {
let fourcc = buffer
.fourcc()
.ok_or_else(|| anyhow::anyhow!("no VA-API format for DRM format {:#010x}", buffer.drm_format))?;
let input = self.import(buffer)?;
self.process(&input, fourcc, input.size(), None, None)
}
fn context(&self, (width, height): (u32, u32)) -> anyhow::Result<Rc<Context>> {
let mut contexts = self.contexts.borrow_mut();
if let Some(context) = contexts.get(&(width, height)) {
return Ok(Rc::clone(context));
}
let context = self
.display
.create_context::<()>(&self.config, width, height, None, true)
.map_err(|e| anyhow::anyhow!("create a {width}x{height} VPP context: {e:?}"))?;
contexts.insert((width, height), Rc::clone(&context));
Ok(context)
}
}
fn run_blit<D, E>(
context: &Rc<Context>,
input: &Surface<D>,
output: &Surface<E>,
input_color: Option<Color>,
output_color: Option<Color>,
) -> anyhow::Result<()>
where
D: SurfaceMemoryDescriptor,
E: SurfaceMemoryDescriptor,
{
let parameters = pipeline(input.id(), input_color, output_color);
let buffer = context
.create_buffer(BufferType::ProcPipelineParameter(parameters))
.map_err(|e| anyhow::anyhow!("create VPP pipeline buffer: {e:?}"))?;
let mut picture = Picture::new(0, Rc::clone(context), output);
picture.add_buffer(buffer);
picture
.begin()
.map_err(|e| anyhow::anyhow!("vaBeginPicture: {e:?}"))?
.render()
.map_err(|e| anyhow::anyhow!("vaRenderPicture: {e:?}"))?
.end()
.map_err(|e| anyhow::anyhow!("vaEndPicture: {e:?}"))?
.sync()
.map_err(|(e, _)| anyhow::anyhow!("vaSyncSurface: {e:?}"))?;
Ok(())
}
fn pipeline(
surface: bindings::VASurfaceID,
input_color: Option<Color>,
output_color: Option<Color>,
) -> ProcPipelineParameterBuffer {
let side = |color: Option<Color>| match color {
Some(color) => (
color.va_standard(),
ProcColorProperties::new(0, color.va_range(), 0, 0, 0),
),
None => (0, ProcColorProperties::default()),
};
let (input_standard, input_properties) = side(input_color);
let (output_standard, output_properties) = side(output_color);
ProcPipelineParameterBuffer::new(
surface,
None,
input_standard,
None,
0,
output_standard,
0,
0,
None,
None,
None,
0,
None,
0,
None,
0,
0,
input_properties,
output_properties,
0,
None,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{VA_FOURCC_NV12, VA_RT_FORMAT_YUV420};
#[test]
fn retile_round_trip() {
let Some(display) = Display::open() else {
eprintln!("skipping: no VA-API display");
return;
};
let Ok(processor) = Processor::with_display(display) else {
eprintln!("skipping: device has no video post-processing entrypoint");
return;
};
let mut surfaces = processor
.display()
.create_surfaces(
VA_RT_FORMAT_YUV420,
Some(VA_FOURCC_NV12),
640,
480,
Some(UsageHint::USAGE_HINT_DECODER | UsageHint::USAGE_HINT_EXPORT),
vec![()],
)
.expect("allocate a decode surface");
let decoded = surfaces.pop().expect("one surface");
let exported = decoded.export_prime().expect("export the decode surface");
let source_modifier = exported.objects[0].drm_format_modifier;
let (fourcc, size, planes) = (
exported.fourcc,
(exported.width, exported.height),
exported.layers[0].num_planes,
);
let buffer = DmaBuf::from_prime(exported).expect("a composed export is one object");
let retiled = processor.retile(buffer).expect("re-tile the decode surface");
let output = retiled.export_prime().expect("export the re-tiled surface");
let target_modifier = output.objects[0].drm_format_modifier;
eprintln!("modifier {source_modifier:#x} -> {target_modifier:#x}");
assert_eq!(output.fourcc, fourcc, "the blit preserves the pixel format");
assert_eq!((output.width, output.height), size);
assert_eq!(output.objects.len(), 1, "a composed export is one object");
assert_eq!(
output.layers[0].num_planes, planes,
"the blit preserves the plane count"
);
}
#[test]
fn a_scaled_surface_reads_back_at_its_new_size() {
let Some(display) = Display::open() else {
eprintln!("skipping: no VA-API display");
return;
};
let Ok(processor) = Processor::with_display(display) else {
eprintln!("skipping: device has no video post-processing entrypoint");
return;
};
let (width, height) = (640u32, 480u32);
let source = processor
.display()
.create_surfaces(
VA_RT_FORMAT_YUV420,
Some(VA_FOURCC_NV12),
width,
height,
Some(UsageHint::USAGE_HINT_EXPORT),
vec![()],
)
.expect("allocate a surface")
.pop()
.expect("one surface");
let format = processor
.display()
.query_image_formats()
.expect("query image formats")
.into_iter()
.find(|f| f.fourcc == VA_FOURCC_NV12)
.expect("an NV12 image format");
{
let mut image =
crate::Image::create_from(&source, format, (width, height), (width, height)).expect("create an image");
let va_image = *image.image();
let data = image.as_mut();
for y in 0..height as usize {
let row = va_image.offsets[0] as usize + y * va_image.pitches[0] as usize;
for x in 0..width as usize {
data[row + x] = if x < width as usize / 2 { 40 } else { 200 };
}
}
for y in 0..height as usize / 2 {
let row = va_image.offsets[1] as usize + y * va_image.pitches[1] as usize;
data[row..row + width as usize].fill(128);
}
}
let scaled = processor
.process(&source, VA_FOURCC_NV12, (width / 2, height / 2), None, None)
.expect("scale the surface");
let exported = crate::decode::ExportedFrame::from_surface(scaled, 7).expect("export the scaled surface");
assert_eq!((exported.width, exported.height), (width / 2, height / 2));
assert_eq!(exported.timestamp, 7);
let frame = exported.download().expect("read the scaled surface back");
let (w, h) = ((width / 2) as usize, (height / 2) as usize);
assert_eq!(frame.data.len(), w * h * 3 / 2);
let (left, right) = (frame.data[h / 2 * w + w / 8], frame.data[h / 2 * w + w * 7 / 8]);
assert!(
left.abs_diff(40) < 4 && right.abs_diff(200) < 4,
"read back {left} and {right}"
);
}
}