use std::collections::VecDeque;
use std::rc::Rc;
use crate::{EncodeError, VideoEncoder, VideoEncoderConfig, VideoInputPreference};
use mediaway_common::{
Bytes, CodecKind, GpuBufferHandle, Packet, PixelFormat, StreamInfo, VideoFrame,
VideoFrameStorage, VideoGeometry,
};
use cros_libva::{
BufferType, Config, Context, Display, EncPictureParameter, EncSequenceParameter,
EncSliceParameter, H264EncPicFields, H264EncSeqFields, Image, MappedCodedBuffer, Picture,
PictureH264, Surface, SurfaceMemoryDescriptor, UsageHint, VA_ATTRIB_NOT_SUPPORTED,
VA_FOURCC_NV12, VA_INVALID_ID, VA_LSB_FIRST, VA_PICTURE_H264_SHORT_TERM_REFERENCE, VA_RC_CQP,
VA_RT_FORMAT_YUV420, VAConfigAttrib, VAConfigAttribType, VAEntrypoint, VAImageFormat,
};
use super::codec::video_profile;
use super::dmabuf;
use super::gop::{DpbSlot, FrameDecision, FrameRequest, GopState, LOG2_MAX_FRAME_NUM_MINUS4};
const SURFACE_POOL_SIZE: usize = super::gop::WORKSPACE_DPB_CAP;
const FIXED_QP: u8 = 26;
const LEVEL_IDC: u8 = 30;
const IDR_ONLY_LOG2_MAX_FRAME_NUM_MINUS4: u8 = 4;
pub(crate) struct VaapiH264Encoder {
context: Rc<Context>,
_config: Config,
info: StreamInfo,
width: u32,
height: u32,
mb_width: u16,
mb_height: u16,
nv12_bytes: usize,
bits_per_second: u32,
surfaces: Vec<Option<Surface<()>>>,
input: VideoInputPreference,
gop: GopState,
effective_gop_size: u32,
#[allow(
dead_code,
reason = "read only by video_tests.rs's hardware-gated tests; a plain `cargo check` \
without --tests never sees that call site (mirrors mediaway-decoder's \
dpb.rs::Dpb::capacity precedent)"
)]
supports_p_frames: bool,
pending: VecDeque<Packet>,
flushed: bool,
}
impl VaapiH264Encoder {
pub(crate) fn open(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
validate(config)?;
match config.input {
VideoInputPreference::CpuUploadOk | VideoInputPreference::ZeroCopyGpu => {
Self::open_cpu(config)
}
}
}
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 supports_p_frames = probe_supports_p_frames(&display, profile);
let effective_gop_size = if config.gop_size > 1
&& supports_p_frames
&& config.input != VideoInputPreference::ZeroCopyGpu
{
config.gop_size
} else {
1
};
let attrs = vec![VAConfigAttrib {
type_: VAConfigAttribType::VAConfigAttribRateControl,
value: VA_RC_CQP,
}];
let vaconfig = display
.create_config(attrs, profile, VAEntrypoint::VAEntrypointEncSlice)
.map_err(|_| EncodeError::Backend)?;
let mb_width = mb_count(config.width)?;
let mb_height = mb_count(config.height)?;
let surfaces = display
.create_surfaces(
VA_RT_FORMAT_YUV420,
Some(VA_FOURCC_NV12),
config.width,
config.height,
Some(UsageHint::USAGE_HINT_ENCODER),
vec![(); SURFACE_POOL_SIZE],
)
.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)?;
Ok(Self {
context,
_config: vaconfig,
info: stream_info_from(config),
width: config.width,
height: config.height,
mb_width,
mb_height,
nv12_bytes,
bits_per_second: config.bitrate_bps,
surfaces: surfaces.into_iter().map(Some).collect(),
input: config.input,
gop: GopState::new(effective_gop_size),
effective_gop_size,
supports_p_frames,
pending: VecDeque::new(),
flushed: false,
})
}
fn encode_one<D: SurfaceMemoryDescriptor>(
&self,
surface: Surface<D>,
frame: &VideoFrame,
decision: &FrameDecision,
reference: Option<(cros_libva::VASurfaceID, DpbSlot)>,
) -> (Option<Surface<D>>, Result<Packet, EncodeError>) {
let num_macroblocks = u32::from(self.mb_width) * u32::from(self.mb_height);
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 gop_active = self.effective_gop_size > 1;
let seq_buf = if decision.is_idr {
let seq_params = build_seq_params(
self.mb_width,
self.mb_height,
self.bits_per_second,
self.effective_gop_size,
);
match self.context.create_buffer(BufferType::EncSequenceParameter(
EncSequenceParameter::H264(seq_params),
)) {
Ok(buf) => Some(buf),
Err(_) => return (Some(surface), Err(EncodeError::Backend)),
}
} else {
None
};
let pic_params =
build_pic_params(surface_id, coded_buf.id(), decision, reference, gop_active);
let slice_params = build_slice_params(num_macroblocks, decision, reference);
let Ok(pic_buf) =
self.context
.create_buffer(BufferType::EncPictureParameter(EncPictureParameter::H264(
pic_params,
)))
else {
return (Some(surface), Err(EncodeError::Backend));
};
let Ok(slice_buf) =
self.context
.create_buffer(BufferType::EncSliceParameter(EncSliceParameter::H264(
slice_params,
)))
else {
return (Some(surface), Err(EncodeError::Backend));
};
let timestamp = u64::try_from(frame.pts).unwrap_or(0);
let mut picture = Picture::new::<D>(timestamp, Rc::clone(&self.context), surface);
if let Some(seq_buf) = seq_buf {
picture.add_buffer(seq_buf);
}
picture.add_buffer(pic_buf);
picture.add_buffer(slice_buf);
let Ok(picture) = picture.begin::<D>() 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::<D>() 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();
let packet = Packet {
stream_id: 0,
pts: frame.pts,
dts: frame.pts,
duration: frame.duration,
is_keyframe: decision.is_idr,
is_discard: false,
payload: Bytes::from(bytes),
};
(surface, Ok(packet))
}
fn resolve_reference(
&self,
decision: &FrameDecision,
) -> Result<Option<(cros_libva::VASurfaceID, DpbSlot)>, EncodeError> {
match decision.reference {
Some((ref_slot, ref_dpb_slot)) => {
let ref_surface = self.surfaces[ref_slot]
.as_ref()
.ok_or(EncodeError::Backend)?;
Ok(Some((ref_surface.id(), ref_dpb_slot)))
}
None => Ok(None),
}
}
fn push_frame_cpu(&mut self, frame: &VideoFrame, data: &Bytes) -> Result<(), EncodeError> {
if data.len() < self.nv12_bytes {
return Err(EncodeError::InvalidInput);
}
let decision = self.gop.decide(FrameRequest::Auto);
let reference = self.resolve_reference(&decision)?;
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 push_frame_dmabuf(
&mut self,
frame: &VideoFrame,
desc: &mediaway_common::DmaBufDescriptor,
) -> Result<(), EncodeError> {
let decision = self.gop.decide(FrameRequest::Auto);
let reference = self.resolve_reference(&decision)?;
let surface =
dmabuf::import_surface(self.context.display(), desc, self.width, self.height)?;
let (_returned_surface, result) = self.encode_one(surface, frame, &decision, reference);
let packet = result?;
self.pending.push_back(packet);
Ok(())
}
}
impl VideoEncoder for VaapiH264Encoder {
fn stream_info(&self) -> &StreamInfo {
&self.info
}
fn push_frame(&mut self, frame: &VideoFrame) -> Result<(), EncodeError> {
if self.flushed {
return Err(EncodeError::Closed);
}
if frame.width != self.width || frame.height != self.height {
return Err(EncodeError::InvalidInput);
}
match (self.input, &frame.storage) {
(VideoInputPreference::CpuUploadOk, VideoFrameStorage::Cpu { data }) => {
self.push_frame_cpu(frame, data)
}
(
VideoInputPreference::ZeroCopyGpu,
VideoFrameStorage::Gpu(GpuBufferHandle::DmaBuf(desc)),
) => self.push_frame_dmabuf(frame, desc),
_ => Err(EncodeError::InvalidInput),
}
}
fn poll_packet(&mut self) -> Result<Option<Packet>, EncodeError> {
Ok(self.pending.pop_front())
}
fn flush(&mut self) -> Result<(), EncodeError> {
self.flushed = true;
Ok(())
}
}
pub(super) fn probe_supports_p_frames(
display: &Display,
profile: cros_libva::VAProfile::Type,
) -> bool {
let mut attribs = [VAConfigAttrib {
type_: VAConfigAttribType::VAConfigAttribEncMaxRefFrames,
value: 0,
}];
let Ok(()) =
display.get_config_attributes(profile, VAEntrypoint::VAEntrypointEncSlice, &mut attribs)
else {
return false;
};
attribs[0].value != VA_ATTRIB_NOT_SUPPORTED && attribs[0].value != 0
}
pub(super) 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(
mb_width: u16,
mb_height: u16,
bits_per_second: u32,
effective_gop_size: u32,
) -> cros_libva::EncSequenceParameterBufferH264 {
let gop_active = effective_gop_size > 1;
let log2_max_frame_num_minus4 = if gop_active {
LOG2_MAX_FRAME_NUM_MINUS4
} else {
IDR_ONLY_LOG2_MAX_FRAME_NUM_MINUS4
};
let (intra_period, intra_idr_period, ip_period) = if gop_active {
(effective_gop_size, 1, 1)
} else {
(1, 1, 0)
};
let seq_fields = H264EncSeqFields::new(
1, 1, 0, 0, 1, u32::from(log2_max_frame_num_minus4),
2, 0, 0, );
cros_libva::EncSequenceParameterBufferH264::new(
0, LEVEL_IDC,
intra_period,
intra_idr_period,
ip_period,
bits_per_second,
1, mb_width,
mb_height,
&seq_fields,
0, 0, 0, 0, 0, [0i32; 256], None, None, 0, 0, 0, 0, 0, )
}
fn build_pic_params(
surface_id: cros_libva::VASurfaceID,
coded_buf_id: cros_libva::VABufferID,
decision: &FrameDecision,
reference: Option<(cros_libva::VASurfaceID, DpbSlot)>,
gop_active: bool,
) -> cros_libva::EncPictureParameterBufferH264 {
let curr_pic = PictureH264::new(
surface_id,
decision.frame_num,
0,
decision.poc,
decision.poc,
);
let mut reference_frames: [PictureH264; 16] = std::array::from_fn(|_| invalid_picture_h264());
if let Some((ref_surface_id, ref_slot)) = reference {
reference_frames[0] = reference_picture_h264(ref_surface_id, ref_slot);
}
let pic_fields = H264EncPicFields::new(
u32::from(decision.is_idr), u32::from(gop_active), 0, 0, 0, 1, 0, 0, 0, 0, 0, );
#[allow(
clippy::cast_possible_truncation,
reason = "GopState::decide bounds frame_num < 1 << (LOG2_MAX_FRAME_NUM_MINUS4 + 4) == \
65536, always representable in u16 (ADR-0002 § VA-API-specific plumbing)"
)]
let frame_num = decision.frame_num as u16;
cros_libva::EncPictureParameterBufferH264::new(
curr_pic,
reference_frames,
coded_buf_id,
0, 0, 0, frame_num,
FIXED_QP,
0, 0, 0, 0, &pic_fields,
)
}
fn build_slice_params(
num_macroblocks: u32,
decision: &FrameDecision,
reference: Option<(cros_libva::VASurfaceID, DpbSlot)>,
) -> cros_libva::EncSliceParameterBufferH264 {
let mut ref_pic_list_0: [PictureH264; 32] = std::array::from_fn(|_| invalid_picture_h264());
let ref_pic_list_1: [PictureH264; 32] = std::array::from_fn(|_| invalid_picture_h264());
if let Some((ref_surface_id, ref_slot)) = reference {
ref_pic_list_0[0] = reference_picture_h264(ref_surface_id, ref_slot);
}
let slice_type: u8 = if decision.is_idr { 2 } else { 0 };
cros_libva::EncSliceParameterBufferH264::new(
0, num_macroblocks,
VA_INVALID_ID, slice_type,
0, decision.idr_pic_id,
0, 0, [0i32; 2], 0, 0, 0, 0, ref_pic_list_0,
ref_pic_list_1,
0, 0, 0, [0i16; 32],
[0i16; 32],
0, [[0i16; 2]; 32],
[[0i16; 2]; 32],
0, [0i16; 32],
[0i16; 32],
0, [[0i16; 2]; 32],
[[0i16; 2]; 32],
0, 0, 0, 0, 0, )
}
fn reference_picture_h264(surface_id: cros_libva::VASurfaceID, slot: DpbSlot) -> PictureH264 {
PictureH264::new(
surface_id,
slot.frame_num,
VA_PICTURE_H264_SHORT_TERM_REFERENCE,
slot.poc,
slot.poc,
)
}
fn invalid_picture_h264() -> PictureH264 {
PictureH264::new(
cros_libva::VA_INVALID_SURFACE,
0,
cros_libva::VA_PICTURE_H264_INVALID,
0,
0,
)
}
fn validate(config: &VideoEncoderConfig) -> Result<(), EncodeError> {
if config.codec != CodecKind::H264 {
return Err(EncodeError::Unsupported);
}
if config.width == 0 || config.height == 0 {
return Err(EncodeError::InvalidInput);
}
if !config.width.is_multiple_of(16) || !config.height.is_multiple_of(16) {
return Err(EncodeError::Unsupported);
}
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 mb_count(dim: u32) -> Result<u16, EncodeError> {
u16::try_from(dim / 16).map_err(|_| EncodeError::InvalidInput)
}
pub(super) 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)
}
#[allow(clippy::missing_const_for_fn, reason = "StreamInfo holds Bytes")]
fn stream_info_from(config: &VideoEncoderConfig) -> StreamInfo {
StreamInfo::Video {
id: 0,
codec: CodecKind::H264,
time_base: config.time_base,
geometry: VideoGeometry {
width: config.width,
height: config.height,
},
extra_data: Bytes::new(),
}
}
#[cfg(test)]
#[path = "video_tests.rs"]
mod tests;