#![forbid(unsafe_code)]
use thiserror::Error;
pub const H264_MAX_DPB_SLOTS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DpbSlot {
pub frame_num: u32,
pub frame_num_wrap: i32,
pub pic_order_cnt: i32,
pub used_for_reference: bool,
}
impl DpbSlot {
#[must_use]
pub const fn new_reference(frame_num: u32, frame_num_wrap: i32, pic_order_cnt: i32) -> Self {
Self {
frame_num,
frame_num_wrap,
pic_order_cnt,
used_for_reference: true,
}
}
}
#[must_use]
#[allow(
clippy::cast_possible_wrap,
reason = "frame_num/max_frame_num are H.264 frame_num values, bounded by \
log2_max_frame_num (<= 16 bits per the spec's own field width) — never close to \
i32::MAX, so the wrap this lint warns about is unreachable in practice"
)]
pub const fn compute_frame_num_wrap(
frame_num: u32,
current_frame_num: u32,
max_frame_num: u32,
) -> i32 {
if frame_num > current_frame_num {
frame_num as i32 - max_frame_num as i32
} else {
frame_num as i32
}
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DpbError {
#[error("DPB slot {index} still has an outstanding Zero-Copy handle")]
SlotOutstanding {
index: usize,
},
#[error("no free DPB slot available (capacity {capacity})")]
NoFreeSlot {
capacity: usize,
},
#[error("DPB slot index {index} out of range (capacity {capacity})")]
InvalidSlotIndex {
index: usize,
capacity: usize,
},
}
pub struct Dpb {
slots: Vec<Option<DpbSlot>>,
outstanding: Vec<bool>,
}
impl Dpb {
#[must_use]
pub fn new(capacity: usize) -> Self {
let capacity = capacity.clamp(1, H264_MAX_DPB_SLOTS);
Self {
slots: vec![None; capacity],
outstanding: vec![false; capacity],
}
}
#[must_use]
pub fn capacity(&self) -> usize {
self.slots.len()
}
#[must_use]
pub fn slot(&self, index: usize) -> Option<&DpbSlot> {
self.slots.get(index).and_then(Option::as_ref)
}
#[must_use]
pub fn is_outstanding(&self, index: usize) -> bool {
self.outstanding.get(index).copied().unwrap_or(false)
}
#[must_use]
pub fn free_slot_index(&self) -> Option<usize> {
self.slots.iter().position(Option::is_none)
}
pub fn occupied_slots(&self) -> impl Iterator<Item = (usize, &DpbSlot)> {
self.slots
.iter()
.enumerate()
.filter_map(|(index, slot)| slot.as_ref().map(|slot| (index, slot)))
}
fn check_index(&self, index: usize) -> Result<(), DpbError> {
if index >= self.slots.len() {
Err(DpbError::InvalidSlotIndex {
index,
capacity: self.slots.len(),
})
} else {
Ok(())
}
}
pub fn mark_outstanding(&mut self, index: usize) -> Result<(), DpbError> {
self.check_index(index)?;
self.outstanding[index] = true;
Ok(())
}
pub fn clear_outstanding(&mut self, index: usize) -> Result<(), DpbError> {
self.check_index(index)?;
self.outstanding[index] = false;
Ok(())
}
pub fn insert(&mut self, index: usize, slot: DpbSlot) -> Result<(), DpbError> {
self.check_index(index)?;
if self.outstanding[index] {
return Err(DpbError::SlotOutstanding { index });
}
self.slots[index] = Some(slot);
Ok(())
}
pub fn evict(&mut self, index: usize) -> Result<(), DpbError> {
self.check_index(index)?;
if self.outstanding[index] {
return Err(DpbError::SlotOutstanding { index });
}
self.slots[index] = None;
Ok(())
}
#[must_use]
pub fn sliding_window_evict_target(&self) -> Option<usize> {
self.occupied_slots()
.filter(|(_, slot)| slot.used_for_reference)
.min_by_key(|(_, slot)| slot.frame_num_wrap)
.map(|(index, _)| index)
}
pub fn refresh_frame_num_wraps(&mut self, current_frame_num: u32, max_frame_num: u32) {
for slot in self.slots.iter_mut().flatten() {
slot.frame_num_wrap =
compute_frame_num_wrap(slot.frame_num, current_frame_num, max_frame_num);
}
}
pub fn clear_all(&mut self) -> Result<(), DpbError> {
let occupied: Vec<usize> = self.occupied_slots().map(|(index, _)| index).collect();
for index in occupied {
self.evict(index)?;
}
Ok(())
}
pub fn allocate_slot(&mut self) -> Result<usize, DpbError> {
if let Some(index) = self.free_slot_index() {
return Ok(index);
}
let index = self
.sliding_window_evict_target()
.ok_or(DpbError::NoFreeSlot {
capacity: self.slots.len(),
})?;
self.evict(index)?;
Ok(index)
}
}
#[cfg(test)]
#[path = "dpb_tests.rs"]
mod tests;