feat(client): M6's conversion half — one AuPlan into libva's buffers
The second half of pf-vaadec: picture parameters, inverse-quantization matrices and one slice-parameter record per slice, over the same transaction discipline pf-dxvadec uses — validate, resolve references against the PRE-removal slot map, then apply removals and assign the setup slot last. A half-applied DPB update is the shape of a corrupt reference, so nothing mutates until every fallible step has passed. Three things VAAPI wants that neither other backend does, all of which the existing plan already carries: A bit offset. slice_data_bit_offset is where slice_data() begins, counted from and including the NAL header byte with emulation-prevention bytes removed — DXVA takes a byte offset, Vulkan takes nothing. It costs no new parsing: the vendored parser records exactly that as SliceHeader::header_bit_size, because cros-codecs' own production backend is VAAPI. The slice data without its start code, since that offset is relative to the NAL header byte. SlicePlan::data is start-code-inclusive and the prefix is three OR four bytes — the host emits four on every access unit — so it is measured per slice rather than assumed. Assuming it is the defect that made HEVC unplayable on every driver. The per-slice reference lists. DXVA's short-format slice control expresses no lists at all; VAAPI wants RefPicList0/1 in 8.2.4.2 order, which is what the plan's derived lists already are. And the distinction that cost M5 a defect, now written down in a third place: reference_frames is documented "in DPB", the same statement DXVA's RefFrameList makes and the opposite of Vulkan's pReferenceSlots. It is filled from the marked-DPB snapshot; the per-slice lists come from the slice's own. Getting that backwards loses a long-term reference no slice happens to name. Weight tables follow 7.3.3's presence rule rather than being copied unconditionally: flagged only where the PPS actually enables explicit weighting for that slice type and list. Flagging them otherwise hands the driver defaults as though the stream had coded them. The vendored PredWeightTable stores luma_offset_l0 as [i8; 32] but luma_offset_l1 as [i16; 32] — an upstream inconsistency, not a semantic one — so the narrow side widens. Envelope refusals are errors, never silent narrowings: slice groups, separate colour planes, a capacity mismatch, a reference holding no slot, lists past their array bounds, a slice range outside its access unit. Tests: 15. The one that matters walks all 250 access units of the vendored conformance vector through H264Planner and this conversion, asserting per slice that the range lies inside its access unit, that the declared size matches it, that the start code really was trimmed, and that the header neither is zero bits nor outruns the slice — plus that reference_frames carries exactly as many valid entries as the marked DPB and every entry past it is invalidated. It also asserts it saw a multi-slice picture and a non-empty reference set, so a splitter bug cannot make it vacuous. Gates: rustfmt, clippy, cargo doc with no unresolved links, and the container's clippy -D warnings, tests and workspace check.
This commit is contained in:
+18
-12
@@ -11,12 +11,15 @@
|
||||
//! - [`va`]: the libva decode buffer layouts, **hand-declared**, with every size and
|
||||
//! offset measured off the real headers and pinned as compile-time assertions.
|
||||
//! - [`config`]: profile, render-target format and surface-count decisions.
|
||||
//! - [`pic`]: one `AuPlan` into picture parameters, IQ matrices and slice records.
|
||||
//!
|
||||
//! # Status
|
||||
//!
|
||||
//! **Layouts and configuration only.** The `AuPlan` → picture/slice/IQ-matrix
|
||||
//! conversion is the next work package; the groundwork it needs is established and
|
||||
//! recorded here so it starts from facts rather than from a reading of the spec:
|
||||
//! **H.264 conversion complete; no libva calls yet.** What remains for the rung is
|
||||
//! the Linux-only plumbing (config/context/surface creation, `vaRenderPicture`,
|
||||
//! sync, dmabuf export) and the H.265 twin of [`pic`].
|
||||
//!
|
||||
//! Four things this crate settled that a reader would otherwise have to re-derive:
|
||||
//!
|
||||
//! * **`slice_data_bit_offset` costs no new parsing.** VAAPI is the only one of the
|
||||
//! three backends that wants a bit position — DXVA takes a byte offset, Vulkan
|
||||
@@ -26,10 +29,10 @@
|
||||
//! `(nalu.size - emulation_prevention_bytes) * 8 - bits_left`, it counts from and
|
||||
//! including the NAL header byte with emulation-prevention bytes removed, which is
|
||||
//! what `VASliceParameterBufferH264` documents.
|
||||
//! * **The slice data buffer starts at the NAL header byte**, so the start code must
|
||||
//! be skipped — `SlicePlan::data` is start-code-inclusive, and the prefix is three
|
||||
//! OR four bytes (the real host emits four on 100% of access units). The same
|
||||
//! normalisation the Vulkan ring layer performs, and the reason that layer exists.
|
||||
//! * **The slice data buffer starts at the NAL header byte**, so the start code is
|
||||
//! skipped — `SlicePlan::data` is start-code-inclusive, and the prefix is three
|
||||
//! OR four bytes (the real host emits four on 100% of access units), so it is
|
||||
//! measured per slice rather than assumed.
|
||||
//! * **`VAPictureParameterBufferH264::reference_frames` is the MARKED DPB**, not the
|
||||
//! access unit's own lists — the same statement DXVA's `RefFrameList` makes, so it
|
||||
//! is filled from pf-bitstream's per-AU `dpb_refs` snapshot. Vulkan's
|
||||
@@ -37,11 +40,10 @@
|
||||
//! conventions now have a written home.
|
||||
//! * **Unlike DXVA's short-format slice control, VAAPI wants the per-slice reference
|
||||
//! lists themselves** (`RefPicList0`/`RefPicList1`, 32 entries each, in 8.2.4.2
|
||||
//! order) and the full prediction weight tables. `SlicePlan::ref_list0`/`ref_list1`
|
||||
//! and `SliceHeader::pred_weight_table` supply both — with one wrinkle to handle
|
||||
//! rather than discover later: the vendored `PredWeightTable` stores
|
||||
//! `luma_offset_l0` as `[i8; 32]` but `luma_offset_l1` as `[i16; 32]`, an upstream
|
||||
//! inconsistency, while libva wants `i16` for both.
|
||||
//! order) and the prediction weight tables. One wrinkle handled in [`pic`]: the
|
||||
//! vendored `PredWeightTable` stores `luma_offset_l0` as `[i8; 32]` but
|
||||
//! `luma_offset_l1` as `[i16; 32]`, an upstream inconsistency, while libva wants
|
||||
//! `i16` for both.
|
||||
//!
|
||||
//! # Why the slot ledger is borrowed
|
||||
//!
|
||||
@@ -54,6 +56,7 @@
|
||||
//! a parameter and stay pure.
|
||||
|
||||
pub mod config;
|
||||
pub mod pic;
|
||||
pub mod va;
|
||||
|
||||
/// The DPB slot ledger — borrowed, not redefined (crate docs).
|
||||
@@ -83,6 +86,9 @@ pub use config::Codec;
|
||||
pub use config::ConfigError;
|
||||
pub use config::VaProfile;
|
||||
pub use config::VA_ENTRYPOINT_VLD;
|
||||
pub use pic::plan_to_va;
|
||||
pub use pic::DecodePlanVa;
|
||||
pub use pic::PlanToVaError;
|
||||
pub use va::PicFieldsH264;
|
||||
pub use va::SeqFieldsH264;
|
||||
pub use va::VaIqMatrixBufferH264;
|
||||
|
||||
@@ -0,0 +1,703 @@
|
||||
//! One [`AuPlan`] into the libva buffers a `vaRenderPicture` call carries: the
|
||||
//! picture parameters, the inverse-quantization matrices and one slice-parameter
|
||||
//! record per slice.
|
||||
//!
|
||||
//! The counterpart of `pf-dxvadec`'s `pic` module, and it follows the same
|
||||
//! transaction discipline for the same reason — a half-applied DPB update is the
|
||||
//! shape of a corrupt reference:
|
||||
//!
|
||||
//! 1. envelope and capacity are validated (read-only);
|
||||
//! 2. references resolve against the PRE-removal state (read-only) — this access
|
||||
//! unit's own end-of-picture marking can evict a picture its slices legitimately
|
||||
//! reference;
|
||||
//! 3. `removed` is applied, then the setup slot is assigned last.
|
||||
//!
|
||||
//! # Three things VAAPI wants that the other two backends do not
|
||||
//!
|
||||
//! **A bit offset.** `slice_data_bit_offset` is the position where `slice_data()`
|
||||
//! begins, counted from and including the NAL header byte with emulation-prevention
|
||||
//! bytes removed. DXVA takes a byte offset and Vulkan takes nothing. It costs no new
|
||||
//! parsing: the vendored parser records exactly this as
|
||||
//! `SliceHeader::header_bit_size` — `(nalu.size - epb) * 8 - bits_left` — because
|
||||
//! cros-codecs' own production backend is VAAPI.
|
||||
//!
|
||||
//! **The slice data without its start code.** That bit offset is relative to the NAL
|
||||
//! header byte, so the buffer must begin there. `SlicePlan::data` is
|
||||
//! start-code-INCLUSIVE and the prefix is three OR four bytes (the real host emits
|
||||
//! four on every access unit), so the prefix is measured per slice rather than
|
||||
//! assumed — the same normalisation the Vulkan ring layer performs, and the same
|
||||
//! defect class that made HEVC unplayable when it was skipped.
|
||||
//!
|
||||
//! **The per-slice reference lists.** DXVA's short-format slice control expresses no
|
||||
//! lists at all — the hardware re-parses the slice header — but VAAPI wants
|
||||
//! `RefPicList0`/`RefPicList1` in 8.2.4.2 order, which is precisely what
|
||||
//! `SlicePlan::ref_list0`/`ref_list1` already carry.
|
||||
//!
|
||||
//! # Two reference sets, and they are not the same set
|
||||
//!
|
||||
//! `VAPictureParameterBufferH264::reference_frames` is documented as "in DPB": a
|
||||
//! statement about the decoded picture buffer, exactly like DXVA's `RefFrameList`
|
||||
//! and exactly UNLIKE Vulkan's `pReferenceSlots` (the slots THIS operation uses).
|
||||
//! It is therefore filled from the planner's per-AU `dpb_refs` snapshot — the marked
|
||||
//! DPB — while the per-slice lists come from the slice's own derived lists. Getting
|
||||
//! that backwards loses a long-term reference no slice happens to name, which is the
|
||||
//! failure this program has already paid for on the DXVA side.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use pf_bitstream::h264::AuPlan;
|
||||
use pf_bitstream::h264::PicId;
|
||||
use pf_bitstream::h264::RefPic;
|
||||
|
||||
use crate::va::PicFieldsH264;
|
||||
use crate::va::SeqFieldsH264;
|
||||
use crate::va::VaIqMatrixBufferH264;
|
||||
use crate::va::VaPictureH264;
|
||||
use crate::va::VaPictureParameterBufferH264;
|
||||
use crate::va::VaSliceParameterBufferH264;
|
||||
use crate::va::VA_PICTURE_H264_LONG_TERM_REFERENCE;
|
||||
use crate::va::VA_PICTURE_H264_SHORT_TERM_REFERENCE;
|
||||
use crate::va::VA_SLICE_DATA_FLAG_ALL;
|
||||
use crate::SlotError;
|
||||
use crate::SlotMap;
|
||||
|
||||
/// `VAPictureParameterBufferH264::reference_frames` length, and the H.264 DPB
|
||||
/// ceiling — the two coincide, which is why an overflow here means a malformed plan
|
||||
/// rather than an expressiveness limit.
|
||||
pub const REFERENCE_FRAMES_LEN: usize = 16;
|
||||
|
||||
/// `RefPicList0`/`RefPicList1` length.
|
||||
pub const REF_PIC_LIST_LEN: usize = 32;
|
||||
|
||||
/// Everything one `vaBeginPicture`/`vaRenderPicture`/`vaEndPicture` sequence needs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DecodePlanVa {
|
||||
pub pic_params: VaPictureParameterBufferH264,
|
||||
pub iq_matrix: VaIqMatrixBufferH264,
|
||||
/// One record per slice, in bitstream order.
|
||||
pub slices: Vec<VaSliceParameterBufferH264>,
|
||||
/// Each slice's data range in the access unit, **start code excluded** — what
|
||||
/// the matching `VASliceDataBuffer` carries. Parallel to [`Self::slices`].
|
||||
pub slice_data: Vec<Range<usize>>,
|
||||
/// The DPB slot this picture decodes into; index it into the caller's surface
|
||||
/// table to get the `VASurfaceID`.
|
||||
pub setup_slot: u8,
|
||||
}
|
||||
|
||||
/// Why a plan cannot be expressed as VAAPI buffers.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PlanToVaError {
|
||||
NoSlices,
|
||||
NoStoredId,
|
||||
/// FMO. Refused rather than ignored: the deprecated fields exist in the struct
|
||||
/// but no driver implements slice groups.
|
||||
SliceGroups {
|
||||
count: u32,
|
||||
},
|
||||
SeparateColourPlanes,
|
||||
CapacityMismatch {
|
||||
required: usize,
|
||||
capacity: usize,
|
||||
},
|
||||
/// A slice named a reference the slot map does not hold.
|
||||
UnresolvedReference(PicId),
|
||||
/// More marked references than `reference_frames` can express.
|
||||
TooManyReferences(usize),
|
||||
/// A slice's derived list is longer than `RefPicList0`/`1`.
|
||||
RefListTooLong {
|
||||
slice: usize,
|
||||
len: usize,
|
||||
},
|
||||
/// The slot map's slot has no entry in the caller's surface table.
|
||||
SurfaceOutOfRange {
|
||||
slot: u8,
|
||||
surfaces: usize,
|
||||
},
|
||||
/// The picture is larger than the macroblock counters can express.
|
||||
DimensionOverflow {
|
||||
width_mbs: u32,
|
||||
height_mbs: u32,
|
||||
},
|
||||
/// A slice's byte range is not inside the access unit, or carries no Annex-B
|
||||
/// start code where one is required.
|
||||
SliceRange {
|
||||
slice: usize,
|
||||
},
|
||||
/// `header_bit_size` does not fit `slice_data_bit_offset`'s 16 bits. Only
|
||||
/// reachable from an absurd slice header, and an error rather than a truncation
|
||||
/// because a wrong bit offset decodes garbage.
|
||||
SliceBitOffsetOverflow {
|
||||
slice: usize,
|
||||
bits: usize,
|
||||
},
|
||||
Slot(SlotError),
|
||||
}
|
||||
|
||||
impl From<SlotError> for PlanToVaError {
|
||||
fn from(e: SlotError) -> Self {
|
||||
PlanToVaError::Slot(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PlanToVaError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PlanToVaError::NoSlices => write!(f, "the access unit planned no slices"),
|
||||
PlanToVaError::NoStoredId => write!(f, "the plan stored no picture id"),
|
||||
PlanToVaError::SliceGroups { count } => {
|
||||
write!(f, "slice groups (FMO) are outside the envelope: {count}")
|
||||
}
|
||||
PlanToVaError::SeparateColourPlanes => {
|
||||
write!(f, "separate colour planes are outside the envelope")
|
||||
}
|
||||
PlanToVaError::CapacityMismatch { required, capacity } => write!(
|
||||
f,
|
||||
"the slot map holds {capacity} slots, this stream needs {required}"
|
||||
),
|
||||
PlanToVaError::UnresolvedReference(id) => {
|
||||
write!(f, "reference picture {id} holds no DPB slot")
|
||||
}
|
||||
PlanToVaError::TooManyReferences(n) => {
|
||||
write!(f, "{n} marked references exceed reference_frames[16]")
|
||||
}
|
||||
PlanToVaError::RefListTooLong { slice, len } => {
|
||||
write!(f, "slice {slice}: reference list of {len} exceeds 32")
|
||||
}
|
||||
PlanToVaError::SurfaceOutOfRange { slot, surfaces } => {
|
||||
write!(f, "DPB slot {slot} has no surface in a table of {surfaces}")
|
||||
}
|
||||
PlanToVaError::DimensionOverflow {
|
||||
width_mbs,
|
||||
height_mbs,
|
||||
} => write!(
|
||||
f,
|
||||
"picture of {width_mbs}x{height_mbs} macroblocks is too large"
|
||||
),
|
||||
PlanToVaError::SliceRange { slice } => {
|
||||
write!(
|
||||
f,
|
||||
"slice {slice}: byte range is not a start-code-prefixed NAL"
|
||||
)
|
||||
}
|
||||
PlanToVaError::SliceBitOffsetOverflow { slice, bits } => {
|
||||
write!(
|
||||
f,
|
||||
"slice {slice}: header of {bits} bits exceeds 16-bit offset"
|
||||
)
|
||||
}
|
||||
PlanToVaError::Slot(e) => write!(f, "DPB slot map: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PlanToVaError {}
|
||||
|
||||
/// The Annex-B start-code length at the front of `bytes` (3 or 4), or `None`.
|
||||
fn start_code_len(bytes: &[u8]) -> Option<usize> {
|
||||
if bytes.starts_with(&[0x00, 0x00, 0x00, 0x01]) {
|
||||
Some(4)
|
||||
} else if bytes.starts_with(&[0x00, 0x00, 0x01]) {
|
||||
Some(3)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// One `VAPictureH264` for a reference picture already resolved to a slot.
|
||||
fn va_ref(rp: &RefPic, surface: u32) -> VaPictureH264 {
|
||||
VaPictureH264 {
|
||||
picture_id: surface,
|
||||
frame_idx: u32::from(rp.frame_num_or_lt_idx),
|
||||
flags: if rp.is_long_term {
|
||||
VA_PICTURE_H264_LONG_TERM_REFERENCE
|
||||
} else {
|
||||
VA_PICTURE_H264_SHORT_TERM_REFERENCE
|
||||
},
|
||||
top_field_order_cnt: rp.top_field_order_cnt,
|
||||
bottom_field_order_cnt: rp.bottom_field_order_cnt,
|
||||
va_reserved: [0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `id` to its surface, or say which id could not be resolved.
|
||||
fn surface_of(slots: &SlotMap, surfaces: &[u32], id: PicId) -> Result<(u8, u32), PlanToVaError> {
|
||||
let slot = slots
|
||||
.slot_of(id)
|
||||
.ok_or(PlanToVaError::UnresolvedReference(id))?;
|
||||
let surface = *surfaces
|
||||
.get(usize::from(slot))
|
||||
.ok_or(PlanToVaError::SurfaceOutOfRange {
|
||||
slot,
|
||||
surfaces: surfaces.len(),
|
||||
})?;
|
||||
Ok((slot, surface))
|
||||
}
|
||||
|
||||
/// Convert one planned access unit.
|
||||
///
|
||||
/// `au` is the access unit the plan was built from — needed because the slice data
|
||||
/// buffer must start at the NAL header byte, and the start-code prefix is three or
|
||||
/// four bytes depending on the encoder. `surfaces` maps DPB slot to `VASurfaceID`;
|
||||
/// this crate never allocates one.
|
||||
///
|
||||
/// Nothing mutates `slots` until every fallible step has passed.
|
||||
pub fn plan_to_va(
|
||||
plan: &AuPlan,
|
||||
au: &[u8],
|
||||
slots: &mut SlotMap,
|
||||
surfaces: &[u32],
|
||||
) -> Result<DecodePlanVa, PlanToVaError> {
|
||||
if plan.slices.is_empty() {
|
||||
return Err(PlanToVaError::NoSlices);
|
||||
}
|
||||
let setup_id = plan.dpb.stored.ok_or(PlanToVaError::NoStoredId)?;
|
||||
let sps = &plan.sps;
|
||||
let pps = &plan.pps;
|
||||
let pic = &plan.picture;
|
||||
|
||||
if pps.num_slice_groups_minus1 != 0 {
|
||||
return Err(PlanToVaError::SliceGroups {
|
||||
count: pps.num_slice_groups_minus1 + 1,
|
||||
});
|
||||
}
|
||||
if sps.separate_colour_plane_flag {
|
||||
return Err(PlanToVaError::SeparateColourPlanes);
|
||||
}
|
||||
|
||||
let required = pic.max_dpb_frames + 1;
|
||||
if slots.capacity() != required {
|
||||
return Err(PlanToVaError::CapacityMismatch {
|
||||
required,
|
||||
capacity: slots.capacity(),
|
||||
});
|
||||
}
|
||||
|
||||
// Height is expressed in FRAME macroblocks, so the map-units count doubles for a
|
||||
// non-frame-only SPS — unreachable inside pf-bitstream's progressive envelope,
|
||||
// written out so the expression says what the spec says.
|
||||
let width_mbs = u32::from(sps.pic_width_in_mbs_minus1) + 1;
|
||||
let height_mbs = (u32::from(sps.pic_height_in_map_units_minus1) + 1)
|
||||
* (2 - u32::from(sps.frame_mbs_only_flag));
|
||||
let (Ok(width_minus1), Ok(height_minus1)) = (
|
||||
u16::try_from(width_mbs.saturating_sub(1)),
|
||||
u16::try_from(height_mbs.saturating_sub(1)),
|
||||
) else {
|
||||
return Err(PlanToVaError::DimensionOverflow {
|
||||
width_mbs,
|
||||
height_mbs,
|
||||
});
|
||||
};
|
||||
|
||||
// --- read-only resolution, against the PRE-removal slot map -------------
|
||||
|
||||
// The marked DPB, in the planner's order — `reference_frames` is a statement
|
||||
// about the DPB, not about this access unit (module docs).
|
||||
if plan.dpb_refs.len() > REFERENCE_FRAMES_LEN {
|
||||
return Err(PlanToVaError::TooManyReferences(plan.dpb_refs.len()));
|
||||
}
|
||||
let mut reference_frames = [VaPictureH264::invalid(); REFERENCE_FRAMES_LEN];
|
||||
for (slot_out, rp) in reference_frames.iter_mut().zip(&plan.dpb_refs) {
|
||||
let (_, surface) = surface_of(slots, surfaces, rp.id)?;
|
||||
*slot_out = va_ref(rp, surface);
|
||||
}
|
||||
|
||||
// Per-slice derived lists, in 8.2.4.2 order.
|
||||
let mut slices = Vec::with_capacity(plan.slices.len());
|
||||
let mut slice_data = Vec::with_capacity(plan.slices.len());
|
||||
for (index, sp) in plan.slices.iter().enumerate() {
|
||||
let hdr = &sp.header;
|
||||
let mut rec = VaSliceParameterBufferH264::zeroed();
|
||||
|
||||
let bytes = au
|
||||
.get(sp.data.clone())
|
||||
.ok_or(PlanToVaError::SliceRange { slice: index })?;
|
||||
let prefix = start_code_len(bytes).ok_or(PlanToVaError::SliceRange { slice: index })?;
|
||||
let payload = sp.data.start + prefix..sp.data.end;
|
||||
rec.slice_data_size = (payload.end - payload.start) as u32;
|
||||
rec.slice_data_offset = 0;
|
||||
rec.slice_data_flag = VA_SLICE_DATA_FLAG_ALL;
|
||||
rec.slice_data_bit_offset = u16::try_from(hdr.header_bit_size).map_err(|_| {
|
||||
PlanToVaError::SliceBitOffsetOverflow {
|
||||
slice: index,
|
||||
bits: hdr.header_bit_size,
|
||||
}
|
||||
})?;
|
||||
slice_data.push(payload);
|
||||
|
||||
rec.first_mb_in_slice = hdr.first_mb_in_slice as u16;
|
||||
rec.slice_type = hdr.slice_type as u8;
|
||||
rec.direct_spatial_mv_pred_flag = u8::from(hdr.direct_spatial_mv_pred_flag);
|
||||
rec.num_ref_idx_l0_active_minus1 = hdr.num_ref_idx_l0_active_minus1;
|
||||
rec.num_ref_idx_l1_active_minus1 = hdr.num_ref_idx_l1_active_minus1;
|
||||
rec.cabac_init_idc = hdr.cabac_init_idc;
|
||||
rec.slice_qp_delta = hdr.slice_qp_delta;
|
||||
rec.disable_deblocking_filter_idc = hdr.disable_deblocking_filter_idc;
|
||||
rec.slice_alpha_c0_offset_div2 = hdr.slice_alpha_c0_offset_div2;
|
||||
rec.slice_beta_offset_div2 = hdr.slice_beta_offset_div2;
|
||||
|
||||
for (list_out, list_in) in [
|
||||
(&mut rec.ref_pic_list0, &sp.ref_list0),
|
||||
(&mut rec.ref_pic_list1, &sp.ref_list1),
|
||||
] {
|
||||
if list_in.len() > REF_PIC_LIST_LEN {
|
||||
return Err(PlanToVaError::RefListTooLong {
|
||||
slice: index,
|
||||
len: list_in.len(),
|
||||
});
|
||||
}
|
||||
for (entry, rp) in list_out.iter_mut().zip(list_in) {
|
||||
// The marked snapshot is the authority for the marking and the
|
||||
// pair-key: a list entry may be a concealment substitute relabelled
|
||||
// short-term. Falling back to the entry's own copy is honest if the
|
||||
// DPB does not hold it.
|
||||
let marked = plan.dpb_refs.iter().find(|d| d.id == rp.id);
|
||||
let (_, surface) = surface_of(slots, surfaces, rp.id)?;
|
||||
*entry = va_ref(marked.unwrap_or(rp), surface);
|
||||
}
|
||||
}
|
||||
|
||||
// 7.3.3: an explicit weight table is parsed for L0 when the PPS enables
|
||||
// weighted P prediction on a P/SP slice, and for both lists when
|
||||
// weighted_bipred_idc == 1 on a B slice. Anywhere else the arrays are not
|
||||
// meaningful, and flagging them would hand the driver defaults as if the
|
||||
// stream had coded them.
|
||||
let pwt = &hdr.pred_weight_table;
|
||||
let explicit_l0 = (pps.weighted_pred_flag
|
||||
&& (hdr.slice_type.is_p() || hdr.slice_type.is_sp()))
|
||||
|| (pps.weighted_bipred_idc == 1 && hdr.slice_type.is_b());
|
||||
let explicit_l1 = pps.weighted_bipred_idc == 1 && hdr.slice_type.is_b();
|
||||
if explicit_l0 || explicit_l1 {
|
||||
rec.luma_log2_weight_denom = pwt.luma_log2_weight_denom;
|
||||
rec.chroma_log2_weight_denom = pwt.chroma_log2_weight_denom;
|
||||
}
|
||||
if explicit_l0 {
|
||||
rec.luma_weight_l0_flag = 1;
|
||||
rec.chroma_weight_l0_flag = 1;
|
||||
rec.luma_weight_l0 = pwt.luma_weight_l0;
|
||||
// The vendored table stores L0 offsets as i8 and L1 offsets as i16 — an
|
||||
// upstream inconsistency, not a semantic difference; libva wants i16 for
|
||||
// both, so the narrow side widens.
|
||||
for (out, v) in rec.luma_offset_l0.iter_mut().zip(pwt.luma_offset_l0) {
|
||||
*out = i16::from(v);
|
||||
}
|
||||
rec.chroma_weight_l0 = pwt.chroma_weight_l0;
|
||||
for (out, v) in rec.chroma_offset_l0.iter_mut().zip(pwt.chroma_offset_l0) {
|
||||
*out = [i16::from(v[0]), i16::from(v[1])];
|
||||
}
|
||||
}
|
||||
if explicit_l1 {
|
||||
rec.luma_weight_l1_flag = 1;
|
||||
rec.chroma_weight_l1_flag = 1;
|
||||
rec.luma_weight_l1 = pwt.luma_weight_l1;
|
||||
rec.luma_offset_l1 = pwt.luma_offset_l1;
|
||||
rec.chroma_weight_l1 = pwt.chroma_weight_l1;
|
||||
for (out, v) in rec.chroma_offset_l1.iter_mut().zip(pwt.chroma_offset_l1) {
|
||||
*out = [i16::from(v[0]), i16::from(v[1])];
|
||||
}
|
||||
}
|
||||
|
||||
slices.push(rec);
|
||||
}
|
||||
|
||||
// --- mutations, after every fallible step -------------------------------
|
||||
|
||||
// The AU's own picture can appear in `removed`: a non-reference picture with no
|
||||
// free frame buffer is stored and evicted within one plan. Its surface must
|
||||
// still exist for the decode, so it is assigned here and released right after.
|
||||
let setup_evicted = plan.dpb.removed.contains(&setup_id);
|
||||
for &id in &plan.dpb.removed {
|
||||
if id == setup_id {
|
||||
continue;
|
||||
}
|
||||
let _ = slots.release(id);
|
||||
}
|
||||
let setup_slot = slots.assign(setup_id)?;
|
||||
if setup_evicted {
|
||||
slots.release(setup_id);
|
||||
}
|
||||
let curr_surface =
|
||||
*surfaces
|
||||
.get(usize::from(setup_slot))
|
||||
.ok_or(PlanToVaError::SurfaceOutOfRange {
|
||||
slot: setup_slot,
|
||||
surfaces: surfaces.len(),
|
||||
})?;
|
||||
|
||||
let curr_pic = VaPictureH264 {
|
||||
picture_id: curr_surface,
|
||||
// For the current picture this is `frame_num`, not a long-term index.
|
||||
frame_idx: u32::from(pic.frame_num),
|
||||
flags: if pic.is_reference {
|
||||
VA_PICTURE_H264_SHORT_TERM_REFERENCE
|
||||
} else {
|
||||
0
|
||||
},
|
||||
top_field_order_cnt: pic.top_field_order_cnt,
|
||||
bottom_field_order_cnt: pic.bottom_field_order_cnt,
|
||||
va_reserved: [0; 4],
|
||||
};
|
||||
|
||||
let seq_fields = SeqFieldsH264 {
|
||||
chroma_format_idc: sps.chroma_format_idc,
|
||||
separate_colour_plane_flag: sps.separate_colour_plane_flag,
|
||||
gaps_in_frame_num_value_allowed_flag: sps.gaps_in_frame_num_value_allowed_flag,
|
||||
frame_mbs_only_flag: sps.frame_mbs_only_flag,
|
||||
mb_adaptive_frame_field_flag: sps.mb_adaptive_frame_field_flag,
|
||||
direct_8x8_inference_flag: sps.direct_8x8_inference_flag,
|
||||
// A.3.3.2 is a level-derived constraint, and libavcodec's VAAPI backend
|
||||
// leaves it 0 for every stream it sends; matching the path drivers are
|
||||
// validated against beats deriving a value nobody consumes.
|
||||
min_luma_bi_pred_size8x8: false,
|
||||
log2_max_frame_num_minus4: sps.log2_max_frame_num_minus4,
|
||||
pic_order_cnt_type: sps.pic_order_cnt_type,
|
||||
log2_max_pic_order_cnt_lsb_minus4: sps.log2_max_pic_order_cnt_lsb_minus4,
|
||||
delta_pic_order_always_zero_flag: sps.delta_pic_order_always_zero_flag,
|
||||
};
|
||||
let pic_fields = PicFieldsH264 {
|
||||
entropy_coding_mode_flag: pps.entropy_coding_mode_flag,
|
||||
weighted_pred_flag: pps.weighted_pred_flag,
|
||||
weighted_bipred_idc: pps.weighted_bipred_idc,
|
||||
transform_8x8_mode_flag: pps.transform_8x8_mode_flag,
|
||||
// Progressive envelope: pf-bitstream rejects field coding before a plan
|
||||
// exists, so this is a constant rather than a read.
|
||||
field_pic_flag: false,
|
||||
constrained_intra_pred_flag: pps.constrained_intra_pred_flag,
|
||||
pic_order_present_flag: pps.bottom_field_pic_order_in_frame_present_flag,
|
||||
deblocking_filter_control_present_flag: pps.deblocking_filter_control_present_flag,
|
||||
redundant_pic_cnt_present_flag: pps.redundant_pic_cnt_present_flag,
|
||||
reference_pic_flag: pic.is_reference,
|
||||
};
|
||||
|
||||
let pic_params = VaPictureParameterBufferH264 {
|
||||
curr_pic,
|
||||
reference_frames,
|
||||
picture_width_in_mbs_minus1: width_minus1,
|
||||
picture_height_in_mbs_minus1: height_minus1,
|
||||
bit_depth_luma_minus8: pic.bit_depth_luma_minus8,
|
||||
bit_depth_chroma_minus8: pic.bit_depth_chroma_minus8,
|
||||
num_ref_frames: sps.max_num_ref_frames,
|
||||
seq_fields: seq_fields.pack(),
|
||||
num_slice_groups_minus1: 0,
|
||||
slice_group_map_type: 0,
|
||||
slice_group_change_rate_minus1: 0,
|
||||
pic_init_qp_minus26: pps.pic_init_qp_minus26,
|
||||
pic_init_qs_minus26: pps.pic_init_qs_minus26,
|
||||
chroma_qp_index_offset: pps.chroma_qp_index_offset,
|
||||
second_chroma_qp_index_offset: pps.second_chroma_qp_index_offset,
|
||||
pic_fields: pic_fields.pack(),
|
||||
frame_num: pic.frame_num,
|
||||
va_reserved: [0; 8],
|
||||
};
|
||||
|
||||
// The PPS lists are the EFFECTIVE ones: the parser has already applied Table 7-2's
|
||||
// fallback rules, so no SPS/PPS merge happens here.
|
||||
let iq_matrix = VaIqMatrixBufferH264 {
|
||||
scaling_list4x4: pps.scaling_lists_4x4,
|
||||
// libva carries only the two 8x8 lists a 4:2:0 stream uses; the parser keeps
|
||||
// six (the 4:4:4 set).
|
||||
scaling_list8x8: [pps.scaling_lists_8x8[0], pps.scaling_lists_8x8[1]],
|
||||
va_reserved: [0; 4],
|
||||
};
|
||||
|
||||
Ok(DecodePlanVa {
|
||||
pic_params,
|
||||
iq_matrix,
|
||||
slices,
|
||||
slice_data,
|
||||
setup_slot,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::va::VA_PICTURE_H264_INVALID;
|
||||
|
||||
/// The vendored conformance vector every other rung's parity legs decode: 250
|
||||
/// access units, two slice NALUs per picture, four IDRs, real reordering.
|
||||
const TEST_25FPS_H264: &[u8] = include_bytes!(
|
||||
"../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"
|
||||
);
|
||||
|
||||
/// Minimal H.264 access-unit splitter. The production wire delivers whole access
|
||||
/// units, so pf-bitstream keeps its splitter test-only; this is the same rule —
|
||||
/// a new AU begins at a non-VCL NALU following slices, or at a slice declaring
|
||||
/// itself first-in-picture — and the access-unit count asserted below is what
|
||||
/// keeps it honest.
|
||||
fn split_aus(stream: &[u8]) -> Vec<&[u8]> {
|
||||
let mut aus = Vec::new();
|
||||
let (mut au_start, mut au_has_slice) = (0usize, false);
|
||||
let mut i = 0usize;
|
||||
while i + 3 <= stream.len() {
|
||||
if stream[i..i + 3] != [0x00, 0x00, 0x01] {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let header = i + 3;
|
||||
let mut start = i;
|
||||
if start > 0 && stream[start - 1] == 0x00 {
|
||||
start -= 1;
|
||||
}
|
||||
let is_slice = matches!(stream[header] & 0x1f, 1 | 5);
|
||||
let first = is_slice && stream.get(header + 1).is_some_and(|b| b & 0x80 != 0);
|
||||
if au_has_slice && (!is_slice || first) {
|
||||
aus.push(&stream[au_start..start]);
|
||||
au_start = start;
|
||||
au_has_slice = false;
|
||||
}
|
||||
au_has_slice |= is_slice;
|
||||
i += 3;
|
||||
}
|
||||
aus.push(&stream[au_start..]);
|
||||
aus
|
||||
}
|
||||
|
||||
/// Every access unit of a real stream converts, and the parts a driver reads are
|
||||
/// self-consistent.
|
||||
///
|
||||
/// The unit tests above check one field at a time; this is the one that would
|
||||
/// notice a transaction ordering mistake, a slot exhausted mid-stream, or a
|
||||
/// slice range that walked off its access unit — none of which a synthetic
|
||||
/// single-picture case reaches.
|
||||
#[test]
|
||||
fn the_whole_vendored_vector_converts() {
|
||||
use pf_bitstream::h264::H264Planner;
|
||||
|
||||
let aus = split_aus(TEST_25FPS_H264);
|
||||
assert_eq!(aus.len(), 250, "the vendored vector is 250 access units");
|
||||
|
||||
let mut planner = H264Planner::new();
|
||||
// One surface per slot, with ids that are distinguishable from slot indices
|
||||
// so a mix-up shows up as a value, not as an off-by-one that still looks
|
||||
// plausible.
|
||||
let surfaces: Vec<u32> = (0..32u32).map(|i| 0x9000 + i).collect();
|
||||
let mut slots: Option<SlotMap> = None;
|
||||
let mut converted = 0usize;
|
||||
let mut saw_multi_slice = false;
|
||||
let mut saw_references = false;
|
||||
|
||||
for (index, au) in aus.iter().enumerate() {
|
||||
let plan = planner
|
||||
.plan_au(au)
|
||||
.unwrap_or_else(|e| panic!("AU {index}: the clean vector must plan, got {e:?}"));
|
||||
let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames));
|
||||
let out = plan_to_va(&plan, au, map, &surfaces)
|
||||
.unwrap_or_else(|e| panic!("AU {index}: conversion failed: {e}"));
|
||||
|
||||
assert_eq!(
|
||||
out.slices.len(),
|
||||
plan.slices.len(),
|
||||
"AU {index}: one record per slice"
|
||||
);
|
||||
assert_eq!(out.slice_data.len(), out.slices.len());
|
||||
saw_multi_slice |= out.slices.len() > 1;
|
||||
|
||||
for (n, (rec, range)) in out.slices.iter().zip(&out.slice_data).enumerate() {
|
||||
assert!(
|
||||
range.end <= au.len() && range.start < range.end,
|
||||
"AU {index} slice {n}: range {range:?} is not inside a {}-byte AU",
|
||||
au.len()
|
||||
);
|
||||
assert_eq!(
|
||||
rec.slice_data_size as usize,
|
||||
range.end - range.start,
|
||||
"AU {index} slice {n}: declared size must match the range"
|
||||
);
|
||||
// The payload begins at the NAL header byte: no start code left.
|
||||
assert_ne!(
|
||||
&au[range.start..range.start + 3.min(range.end - range.start)],
|
||||
&[0x00, 0x00, 0x01][..],
|
||||
"AU {index} slice {n}: the start code was not trimmed"
|
||||
);
|
||||
assert!(
|
||||
rec.slice_data_bit_offset > 0,
|
||||
"AU {index} slice {n}: a slice header cannot be zero bits"
|
||||
);
|
||||
assert!(
|
||||
usize::from(rec.slice_data_bit_offset) < (range.end - range.start) * 8,
|
||||
"AU {index} slice {n}: the header cannot outrun the slice"
|
||||
);
|
||||
}
|
||||
|
||||
// `reference_frames` mirrors the marked DPB exactly: as many valid
|
||||
// entries as the snapshot has, and every entry past it invalidated.
|
||||
let valid = out
|
||||
.pic_params
|
||||
.reference_frames
|
||||
.iter()
|
||||
.filter(|e| e.flags & VA_PICTURE_H264_INVALID == 0)
|
||||
.count();
|
||||
assert_eq!(
|
||||
valid,
|
||||
plan.dpb_refs.len(),
|
||||
"AU {index}: reference_frames must carry the marked DPB and nothing else"
|
||||
);
|
||||
saw_references |= valid > 0;
|
||||
for e in out.pic_params.reference_frames.iter().take(valid) {
|
||||
assert!(
|
||||
surfaces.contains(&e.picture_id),
|
||||
"AU {index}: a reference names a surface outside the table"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(out.pic_params.frame_num, plan.picture.frame_num);
|
||||
assert!(usize::from(out.setup_slot) < surfaces.len());
|
||||
converted += 1;
|
||||
}
|
||||
|
||||
assert_eq!(converted, 250);
|
||||
assert!(
|
||||
saw_multi_slice,
|
||||
"this vector is two slices per picture — a run that never saw one is \
|
||||
splitting access units wrong"
|
||||
);
|
||||
assert!(
|
||||
saw_references,
|
||||
"a 250-frame vector must reference something"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_code_len_reads_both_prefix_forms() {
|
||||
assert_eq!(start_code_len(&[0, 0, 1, 0x65]), Some(3));
|
||||
assert_eq!(start_code_len(&[0, 0, 0, 1, 0x65]), Some(4));
|
||||
// A NAL handed over WITHOUT its prefix must not be mistaken for one: the
|
||||
// bit offset is relative to the header byte, so trimming the wrong number
|
||||
// of bytes shifts every slice.
|
||||
assert_eq!(start_code_len(&[0x65, 0x88]), None);
|
||||
assert_eq!(start_code_len(&[0, 0, 2, 1]), None);
|
||||
assert_eq!(start_code_len(&[0, 0]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_term_reference_is_flagged_long_term() {
|
||||
let rp = RefPic {
|
||||
id: 7,
|
||||
top_field_order_cnt: 4,
|
||||
bottom_field_order_cnt: 4,
|
||||
is_long_term: true,
|
||||
frame_num_or_lt_idx: 2,
|
||||
};
|
||||
let e = va_ref(&rp, 0x1234);
|
||||
assert_eq!(e.flags, VA_PICTURE_H264_LONG_TERM_REFERENCE);
|
||||
assert_eq!(e.picture_id, 0x1234);
|
||||
// For a long-term picture this field carries LongTermFrameIdx, not frame_num.
|
||||
assert_eq!(e.frame_idx, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_short_term_reference_carries_its_frame_num() {
|
||||
let rp = RefPic {
|
||||
id: 3,
|
||||
top_field_order_cnt: -2,
|
||||
bottom_field_order_cnt: -2,
|
||||
is_long_term: false,
|
||||
frame_num_or_lt_idx: 9,
|
||||
};
|
||||
let e = va_ref(&rp, 5);
|
||||
assert_eq!(e.flags, VA_PICTURE_H264_SHORT_TERM_REFERENCE);
|
||||
assert_eq!(e.frame_idx, 9);
|
||||
assert_eq!(e.top_field_order_cnt, -2);
|
||||
assert_ne!(e.flags & VA_PICTURE_H264_INVALID, VA_PICTURE_H264_INVALID);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user