use std::collections::VecDeque;
use std::rc::Rc;
use crate::{EncodeError, VideoEncoder, VideoEncoderConfig, VideoInputPreference};
use mediaway_common::{
Bytes, CodecKind, Packet, PixelFormat, StreamInfo, VideoFrame, VideoFrameStorage, VideoGeometry,
};
use cros_libva::{
BufferType, Config, Context, Display, EncPictureParameter, EncSequenceParameter, Image,
MappedCodedBuffer, Picture, Surface, UsageHint, VA_FOURCC_NV12, VA_INVALID_ID, VA_LSB_FIRST,
VA_RC_CQP, VA_RT_FORMAT_YUV420, VAConfigAttrib, VAConfigAttribType, VAEntrypoint,
VAImageFormat, VP9EncPicFlags, VP9EncRefFlags,
};
use super::codec::video_profile;
use super::vp9_gop::{FrameDecision, FrameRequest, GopState, WORKSPACE_PING_PONG_SLOTS};
const ENTRYPOINT_PROBE_ORDER: [cros_libva::VAEntrypoint::Type; 3] = [
VAEntrypoint::VAEntrypointEncSlice,
VAEntrypoint::VAEntrypointEncPicture,
VAEntrypoint::VAEntrypointEncSliceLP,
];
const VP9_MAX_TILE_WIDTH: u32 = 4096;
const FIXED_QINDEX_KEY: u8 = 60;
const FIXED_QINDEX_INTER: u8 = 80;
pub(crate) struct VaapiVp9Encoder {
context: Rc<Context>,
_config: Config,
info: StreamInfo,
width: u32,
height: u32,
#[allow(
dead_code,
reason = "read only at open_cpu time to build the Config; kept for future diagnostics \
parity with video.rs's supports_p_frames field"
)]
entrypoint: cros_libva::VAEntrypoint::Type,
nv12_bytes: usize,
surfaces: [Option<Surface<()>>; WORKSPACE_PING_PONG_SLOTS],
gop: GopState,
effective_gop_size: u32,
seq_sent: bool,
pending: VecDeque<Packet>,
flushed: bool,
}
impl VaapiVp9Encoder {
pub(crate) fn open(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
validate(config)?;
match config.input {
VideoInputPreference::CpuUploadOk => Self::open_cpu(config),
_ => Err(EncodeError::Unsupported),
}
}
fn open_cpu(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
let display: Rc<Display> = Display::open().ok_or(EncodeError::Backend)?;
let profile = video_profile(config.codec)?;
let supported = display
.query_config_profiles()
.map_err(|_| EncodeError::Backend)?;
if !supported.contains(&profile) {
return Err(EncodeError::Unsupported);
}
let entrypoints = display
.query_config_entrypoints(profile)
.map_err(|_| EncodeError::Backend)?;
let entrypoint = ENTRYPOINT_PROBE_ORDER
.into_iter()
.find(|candidate| entrypoints.contains(candidate))
.ok_or(EncodeError::Unsupported)?;
let effective_gop_size = if config.gop_size > 1 {
config.gop_size
} else {
1
};
let attrs = vec![VAConfigAttrib {
type_: VAConfigAttribType::VAConfigAttribRateControl,
value: VA_RC_CQP,
}];
let vaconfig = display
.create_config(attrs, profile, entrypoint)
.map_err(|_| EncodeError::Backend)?;
let surfaces = display
.create_surfaces(
VA_RT_FORMAT_YUV420,
Some(VA_FOURCC_NV12),
config.width,
config.height,
Some(UsageHint::USAGE_HINT_ENCODER),
vec![(); WORKSPACE_PING_PONG_SLOTS],
)
.map_err(|_| EncodeError::Backend)?;
let context = display
.create_context(
&vaconfig,
config.width,
config.height,
Some(&surfaces),
true,
)
.map_err(|_| EncodeError::Backend)?;
let nv12_bytes = nv12_size(config.width, config.height)?;
let mut surfaces_iter = surfaces.into_iter();
let surfaces_array: [Option<Surface<()>>; WORKSPACE_PING_PONG_SLOTS] =
std::array::from_fn(|_| surfaces_iter.next());
Ok(Self {
context,
_config: vaconfig,
info: stream_info_from(config),
width: config.width,
height: config.height,
entrypoint,
nv12_bytes,
surfaces: surfaces_array,
gop: GopState::new(effective_gop_size),
effective_gop_size,
seq_sent: false,
pending: VecDeque::new(),
flushed: false,
})
}
fn encode_one(
&mut self,
surface: Surface<()>,
frame: &VideoFrame,
decision: &FrameDecision,
reference: Option<cros_libva::VASurfaceID>,
) -> (Option<Surface<()>>, Result<Packet, EncodeError>) {
let coded_size = self.nv12_bytes.saturating_mul(2).max(4096);
let Ok(coded_buf) = self.context.create_enc_coded(coded_size) else {
return (Some(surface), Err(EncodeError::Backend));
};
let surface_id = surface.id();
let seq_buf = if self.seq_sent {
None
} else {
let seq_params = build_seq_params(self.width, self.height, self.effective_gop_size);
match self.context.create_buffer(BufferType::EncSequenceParameter(
EncSequenceParameter::VP9(seq_params),
)) {
Ok(buf) => Some(buf),
Err(_) => return (Some(surface), Err(EncodeError::Backend)),
}
};
let pic_params = build_pic_params(
surface_id,
coded_buf.id(),
decision,
reference,
self.width,
self.height,
);
let Ok(pic_buf) =
self.context
.create_buffer(BufferType::EncPictureParameter(EncPictureParameter::VP9(
pic_params,
)))
else {
return (Some(surface), Err(EncodeError::Backend));
};
let timestamp = u64::try_from(frame.pts).unwrap_or(0);
let mut picture = Picture::new::<()>(timestamp, Rc::clone(&self.context), surface);
if let Some(seq_buf) = seq_buf {
picture.add_buffer(seq_buf);
}
picture.add_buffer(pic_buf);
let Ok(picture) = picture.begin::<()>() else {
return (None, Err(EncodeError::Backend));
};
let Ok(picture) = picture.render() else {
return (None, Err(EncodeError::Backend));
};
let Ok(picture) = picture.end() else {
return (None, Err(EncodeError::Backend));
};
let Ok(picture) = picture.sync::<()>() else {
return (None, Err(EncodeError::Backend));
};
let bytes = match MappedCodedBuffer::new(&coded_buf) {
Ok(mapped) => {
let mut bytes = Vec::new();
for segment in mapped.iter() {
bytes.extend_from_slice(segment.buf);
}
bytes
}
Err(_) => return (picture.take_surface().ok(), Err(EncodeError::Backend)),
};
let surface = picture.take_surface().ok();
self.seq_sent = true;
let packet = Packet {
stream_id: 0,
pts: frame.pts,
dts: frame.pts,
duration: frame.duration,
is_keyframe: decision.is_key,
is_discard: false,
payload: Bytes::from(bytes),
};
(surface, Ok(packet))
}
}
impl VideoEncoder for VaapiVp9Encoder {
fn stream_info(&self) -> &StreamInfo {
&self.info
}
fn push_frame(&mut self, frame: &VideoFrame) -> Result<(), EncodeError> {
if self.flushed {
return Err(EncodeError::Closed);
}
let VideoFrameStorage::Cpu { data } = &frame.storage else {
return Err(EncodeError::Unsupported);
};
if frame.width != self.width || frame.height != self.height {
return Err(EncodeError::InvalidInput);
}
if data.len() < self.nv12_bytes {
return Err(EncodeError::InvalidInput);
}
let decision = self.gop.decide(FrameRequest::Auto);
let reference = match decision.reference_slot {
Some(ref_slot) => {
let ref_surface = self.surfaces[ref_slot]
.as_ref()
.ok_or(EncodeError::Backend)?;
Some(ref_surface.id())
}
None => None,
};
let slot = decision.setup_slot;
let surface = self.surfaces[slot].take().ok_or(EncodeError::Backend)?;
let surface = match upload_cpu_nv12(&surface, data, self.width, self.height) {
Ok(()) => surface,
Err(e) => {
self.surfaces[slot] = Some(surface);
return Err(e);
}
};
let (returned_surface, result) = self.encode_one(surface, frame, &decision, reference);
self.surfaces[slot] = returned_surface;
let packet = result?;
self.pending.push_back(packet);
Ok(())
}
fn poll_packet(&mut self) -> Result<Option<Packet>, EncodeError> {
Ok(self.pending.pop_front())
}
fn flush(&mut self) -> Result<(), EncodeError> {
self.flushed = true;
Ok(())
}
}
fn upload_cpu_nv12(
surface: &Surface<()>,
data: &[u8],
width: u32,
height: u32,
) -> Result<(), EncodeError> {
let format = VAImageFormat {
fourcc: VA_FOURCC_NV12,
byte_order: VA_LSB_FIRST,
bits_per_pixel: 12,
depth: 0,
red_mask: 0,
green_mask: 0,
blue_mask: 0,
alpha_mask: 0,
va_reserved: Default::default(),
};
let mut image = Image::create_from(surface, format, (width, height), (width, height))
.map_err(|_| EncodeError::Backend)?;
let va_image = *image.image();
let offsets = va_image.offsets;
let pitches = va_image.pitches;
let w = width as usize;
let h = height as usize;
let y_pitch = pitches[0] as usize;
let y_offset = offsets[0] as usize;
let uv_pitch = pitches[1] as usize;
let uv_offset = offsets[1] as usize;
let y_plane_bytes = w * h;
let uv_rows = h / 2;
let dst = image.as_mut();
if dst.len() < y_offset + y_pitch * h || dst.len() < uv_offset + uv_pitch * uv_rows {
return Err(EncodeError::Backend);
}
for row in 0..h {
let src = row * w;
let dst_off = y_offset + row * y_pitch;
dst[dst_off..dst_off + w].copy_from_slice(&data[src..src + w]);
}
for row in 0..uv_rows {
let src = y_plane_bytes + row * w;
let dst_off = uv_offset + row * uv_pitch;
dst[dst_off..dst_off + w].copy_from_slice(&data[src..src + w]);
}
Ok(())
}
fn build_seq_params(
width: u32,
height: u32,
effective_gop_size: u32,
) -> cros_libva::EncSequenceParameterBufferVP9 {
cros_libva::EncSequenceParameterBufferVP9::new(
width,
height,
0, effective_gop_size,
effective_gop_size,
0, effective_gop_size,
)
}
fn build_pic_params(
surface_id: cros_libva::VASurfaceID,
coded_buf_id: cros_libva::VABufferID,
decision: &FrameDecision,
reference: Option<cros_libva::VASurfaceID>,
width: u32,
height: u32,
) -> cros_libva::EncPictureParameterBufferVP9 {
let mut reference_frames = [VA_INVALID_ID; 8];
if let Some(ref_surface_id) = reference {
reference_frames[0] = ref_surface_id;
}
let ref_last_idx = 0u32;
let ref_flags = if decision.is_key {
VP9EncRefFlags::new(
1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
)
} else {
VP9EncRefFlags::new(
0, 1, 0, ref_last_idx, 1, 0,
0,
0,
0,
0, )
};
let reset_frame_context = u32::from(decision.is_key) * 3;
let pic_flags = VP9EncPicFlags::new(
u32::from(!decision.is_key), 1, 1, 0, 0, 0, 1, reset_frame_context,
0, 0, 0, 0, 0, 0, 0, 0, 0, );
cros_libva::EncPictureParameterBufferVP9::new(
width, height, width, height, surface_id, reference_frames,
coded_buf_id,
&ref_flags,
&pic_flags,
decision.refresh_frame_flags,
if decision.is_key {
FIXED_QINDEX_KEY
} else {
FIXED_QINDEX_INTER
},
0, 0, 0, 0, 0, [0i8; 4], [0i8; 2], 0,
0,
0,
0,
0,
0,
0, 0, log2_tile_columns(width),
0, 0, 0, )
}
fn validate(config: &VideoEncoderConfig) -> Result<(), EncodeError> {
if config.codec != CodecKind::Vp9 {
return Err(EncodeError::Unsupported);
}
if config.width == 0 || config.height == 0 {
return Err(EncodeError::InvalidInput);
}
if config.pixel_format != PixelFormat::Nv12 {
return Err(EncodeError::Unsupported);
}
if config.time_base.den == 0 {
return Err(EncodeError::InvalidInput);
}
if config.gop_size == 0 {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
fn nv12_size(width: u32, height: u32) -> Result<usize, EncodeError> {
let w = usize::try_from(width).map_err(|_| EncodeError::InvalidInput)?;
let h = usize::try_from(height).map_err(|_| EncodeError::InvalidInput)?;
w.checked_mul(h)
.and_then(|y| y.checked_mul(3))
.and_then(|v| v.checked_div(2))
.ok_or(EncodeError::InvalidInput)
}
fn log2_tile_columns(frame_width_src: u32) -> u8 {
let num_tile_columns = frame_width_src.div_ceil(VP9_MAX_TILE_WIDTH).max(1);
if num_tile_columns == 1 {
0
} else {
let log2 = u32::BITS - (num_tile_columns - 1).leading_zeros();
u8::try_from(log2).unwrap_or(u8::MAX)
}
}
#[allow(clippy::missing_const_for_fn, reason = "StreamInfo holds Bytes")]
fn stream_info_from(config: &VideoEncoderConfig) -> StreamInfo {
StreamInfo::Video {
id: 0,
codec: CodecKind::Vp9,
time_base: config.time_base,
geometry: VideoGeometry {
width: config.width,
height: config.height,
},
extra_data: Bytes::new(),
}
}
#[cfg(test)]
#[path = "vp9_tests.rs"]
mod tests;