#![forbid(unsafe_code)]
use thiserror::Error;
pub(super) const H264_MAX_DPB_SLOTS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct DpbSlot {
pub(super) frame_num: u32,
pub(super) frame_num_wrap: i32,
pub(super) pic_order_cnt: i32,
pub(super) used_for_reference: bool,
}
impl DpbSlot {
#[must_use]
pub(super) 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(super) 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(super) 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(super) struct Dpb {
slots: Vec<Option<DpbSlot>>,
outstanding: Vec<bool>,
}
impl Dpb {
#[must_use]
pub(super) 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]
#[allow(
dead_code,
reason = "exercised by dpb_tests.rs (is_outstanding_defaults_false_and_out_of_range_is_false); \
a plain `cargo check` without --tests never sees that call site — kept for API \
parity with vulkan/dpb.rs's identical accessor, mirrors this file's own \
Dpb::capacity precedent"
)]
pub(super) fn is_outstanding(&self, index: usize) -> bool {
self.outstanding.get(index).copied().unwrap_or(false)
}
pub(super) fn mark_outstanding(&mut self, index: usize) -> Result<(), DpbError> {
self.check_index(index)?;
self.outstanding[index] = true;
Ok(())
}
pub(super) fn clear_outstanding(&mut self, index: usize) -> Result<(), DpbError> {
self.check_index(index)?;
self.outstanding[index] = false;
Ok(())
}
#[must_use]
#[allow(
dead_code,
reason = "exercised by dpb_tests.rs (new_clamps_capacity_to_*); a plain `cargo check` \
without --tests never sees that call site — kept for API parity with \
vulkan/dpb.rs's identical accessor, not because a non-test caller needs it yet"
)]
pub(super) const fn capacity(&self) -> usize {
self.slots.len()
}
#[must_use]
pub(super) fn slot(&self, index: usize) -> Option<&DpbSlot> {
self.slots.get(index).and_then(Option::as_ref)
}
#[must_use]
pub(super) fn free_slot_index(&self) -> Option<usize> {
self.slots
.iter()
.zip(self.outstanding.iter())
.position(|(slot, outstanding)| slot.is_none() && !outstanding)
}
pub(super) fn occupied_slots(&self) -> impl Iterator<Item = (usize, &DpbSlot)> {
self.slots
.iter()
.enumerate()
.filter_map(|(index, slot)| slot.as_ref().map(|slot| (index, slot)))
}
const fn check_index(&self, index: usize) -> Result<(), DpbError> {
if index >= self.slots.len() {
Err(DpbError::InvalidSlotIndex {
index,
capacity: self.slots.len(),
})
} else {
Ok(())
}
}
pub(super) 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(super) 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(super) 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(super) 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(super) 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(super) 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)
}
}
#[must_use]
#[allow(
clippy::similar_names,
reason = "prev_msb/prev_lsb name the two halves of one ITU-T H.264 § 8.2.1.1 state pair \
(PicOrderCntMsb, pic_order_cnt_lsb) — matching, not confusable, names"
)]
#[allow(
clippy::cast_possible_wrap,
reason = "pic_order_cnt_lsb/prev_lsb/max_pic_order_cnt_lsb are H.264 POC-LSB values, bounded \
by log2_max_pic_order_cnt_lsb (<= 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; \
mirrors compute_frame_num_wrap's identical allow above"
)]
pub(super) const fn derive_pic_order_cnt_msb(
pic_order_cnt_lsb: u32,
prev_msb: i32,
prev_lsb: u32,
max_pic_order_cnt_lsb: u32,
) -> i32 {
let half = (max_pic_order_cnt_lsb / 2) as i32;
let lsb = pic_order_cnt_lsb as i32;
let prev_lsb = prev_lsb as i32;
if lsb < prev_lsb && prev_lsb - lsb >= half {
prev_msb + max_pic_order_cnt_lsb as i32
} else if lsb > prev_lsb && lsb - prev_lsb > half {
prev_msb - max_pic_order_cnt_lsb as i32
} else {
prev_msb
}
}
#[must_use]
pub(super) fn default_ref_pic_list0(dpb: &Dpb) -> Vec<usize> {
let mut refs: Vec<(usize, i32)> = dpb
.occupied_slots()
.filter(|(_, slot)| slot.used_for_reference)
.map(|(index, slot)| (index, slot.frame_num_wrap))
.collect();
refs.sort_by_key(|&(_, frame_num_wrap)| std::cmp::Reverse(frame_num_wrap));
refs.into_iter().map(|(index, _)| index).collect()
}
#[cfg(test)]
#[path = "dpb_tests.rs"]
mod tests;