Files
punktfunk/crates/pf-bitstream/src/lib.rs
T
enricobuehler 7f83ec6c2f feat(bitstream): M7 begins — the AV1 planner, and it plans frames the stream hides
The third planner in this crate, and the foundation every AV1 rung will
consume. Same contract as its H.264 and H.265 siblings: an access unit in, a
plan out, with the vendored cros-codecs parser reading the bitstream and this
module owning the reference ledger, the output bookkeeping and the
concealment posture.

AV1's reference model is simpler than H.264's and entirely explicit — eight
numbered slots, `ref_frame_idx` naming what a frame reads and
`refresh_frame_flags` naming what it writes — so the planner is bookkeeping
rather than derivation, and a frame naming an empty slot is a lost reference
with no spec process that might legitimately have emptied it.

Two things measurement changed, both before a line of backend code depends on
them.

`plan_au` returns a VECTOR. An AV1 temporal unit may carry several frames,
and the vendored vector does: 250 units, 274 frames, 24 units carrying two.
Measured, those 24 extras are not `show_existing_frame` (there are none in
this vector) but HIDDEN frames — decoded, never displayed, referenced later.
A planner that took the last header in each unit would have decoded 250
frames and silently dropped 24 REFERENCES, and the damage would have
surfaced as missing-reference concealment on frames that were never damaged.

A picture is not removed until its LAST slot goes. One picture routinely
occupies several slots at once — a key frame refreshes all eight — so a slot
being overwritten does not mean its picture is gone. Reporting it removed
would free a surface under a live reference, which is precisely the shape
this program exists to catch. Tested directly, and asserted to report once
rather than once per slot.

What this does not cover is written down rather than left to be assumed: the
vector uses `show_existing_frame` zero times, so the display-only path and
its key-frame slot reset are exercised by no test here, and the test asserts
that count is zero so the day it changes the claim gets revisited.

Per-backend conversions are deliberately absent. Vulkan, DXVA and libva
disagree about what a reference list indexes — the disagreement that made
HEVC unplayable on every driver — so each belongs beside its siblings in
pf-vkdecode / pf-dxvadec / pf-vaadec, where its own convention is written
down and tested.

Gates: macOS fmt/clippy/344 tests, container clippy -D warnings over six
crates, 799 tests, workspace check.
2026-08-06 17:18:34 +02:00

158 lines
6.4 KiB
Rust

//! The client's bitstream layer for native decode (design/client-native-decode.md §3.1):
//! everything a stateless hardware decoder needs to know about an AU before submission —
//! parsed headers, POC, DPB state, reference lists (including MMCO/LTR, which the hosts'
//! RFI recovery actively uses), recovery-point SEI — derived once here and consumed by
//! every backend (Vulkan `StdVideo*`, DXVA picparams, libva buffers).
//!
//! Parsing primitives come from the vendored cros-codecs parser layer
//! (`vendor/cros-codecs`, see its PROVENANCE.md); this crate owns what upstream keeps in
//! its Linux-only `decoder::stateless` half — the per-AU orchestration — plus the pieces
//! upstream lacks (SEI payload parsing: their parsers classify SEI NALUs but never read
//! them).
//!
//! Scope discipline: punktfunk clients decode punktfunk hosts — zero-reorder, no
//! B-frames, progressive, parameter sets from encoders we control. Implement to spec
//! where cheap; reject-with-log outside that envelope rather than half-decode.
//!
//! Nothing in this crate may touch a GPU API, an OS handle, or the network: CPU-only by
//! construction, so its tests run on every CI leg including macOS. And no `unsafe`,
//! compiler-enforced — this layer exists to replace C parsers; it does not get to
//! reintroduce their failure mode.
#![forbid(unsafe_code)]
pub mod av1;
pub mod h264;
pub mod h265;
pub mod sei;
// The vendor-pinning smoke tests below assert against byte counts and golden values from
// the vendored snapshot's own test vectors; a cros-codecs re-sync that shifts parser
// behavior must trip HERE, in our tree, not in a decode session.
#[cfg(test)]
mod vendor_smoke {
use std::io::Cursor;
use cros_codecs::bitstream_utils::IvfIterator;
use cros_codecs::codec::av1::parser::ObuAction;
use cros_codecs::codec::av1::parser::ParsedObu;
use cros_codecs::codec::h264::parser::Nalu as H264Nalu;
use cros_codecs::codec::h264::parser::Parser as H264Parser;
use cros_codecs::codec::h265::parser::Nalu as H265Nalu;
use cros_codecs::codec::h265::parser::Parser as H265Parser;
const H264_25FPS: &[u8] =
include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264");
const H265_25FPS: &[u8] =
include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265");
const AV1_25FPS: &[u8] =
include_bytes!("../vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1");
const VP9_25FPS: &[u8] =
include_bytes!("../vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9");
#[test]
fn h264_parses_the_vendored_vector_to_its_goldens() {
let mut cursor = Cursor::new(H264_25FPS);
let mut parser = H264Parser::default();
let (mut nalus, mut sps, mut slices) = (0u32, 0u32, 0u32);
let mut coded = (0u32, 0u32);
while let Ok(nalu) = H264Nalu::next(&mut cursor) {
nalus += 1;
if let Ok(s) = parser.parse_sps(&nalu) {
sps += 1;
coded = (
(s.pic_width_in_mbs_minus1 as u32 + 1) * 16,
(s.pic_height_in_map_units_minus1 as u32 + 1) * 16,
);
continue;
}
if parser.parse_pps(&nalu).is_ok() {
continue;
}
if parser.parse_slice_header(nalu).is_ok() {
slices += 1;
}
}
// 759 is upstream's own golden for this stream (chromium h264_parser_unittest lineage).
assert_eq!(nalus, 759);
assert_eq!(sps, 4);
assert_eq!(slices, 500);
assert_eq!(coded, (320, 240));
}
#[test]
fn h265_parses_the_vendored_vector() {
let mut cursor = Cursor::new(H265_25FPS);
let mut parser = H265Parser::default();
let (mut nalus, mut sps, mut slices) = (0u32, 0u32, 0u32);
while let Ok(nalu) = H265Nalu::next(&mut cursor) {
nalus += 1;
if parser.parse_sps(&nalu).is_ok() {
sps += 1;
continue;
}
if parser.parse_pps(&nalu).is_ok() {
continue;
}
if parser.parse_slice_header(nalu).is_ok() {
slices += 1;
}
}
assert_eq!(nalus, 254);
assert_eq!(sps, 1);
assert_eq!(slices, 250);
}
#[test]
fn av1_walks_obus_and_maintains_ref_slots_across_the_stream() {
let mut parser = cros_codecs::codec::av1::parser::Parser::default();
let (mut obus, mut frames) = (0u32, 0u32);
for packet in IvfIterator::new(AV1_25FPS) {
let mut consumed = 0;
while let Ok(action) = parser.read_obu(&packet[consumed..]) {
let obu = match action {
ObuAction::Process(obu) => obu,
ObuAction::Drop(n) => {
consumed += n as usize;
continue;
}
};
consumed += obu.bytes_used;
obus += 1;
// `ref_frame_update` is the parser's ref-slot bookkeeping; without it,
// inter frames fail with "Reference is invalid" — the parser validates
// reference integrity rather than trusting the stream.
match parser.parse_obu(obu).expect("parse_obu") {
ParsedObu::FrameHeader(fh) => {
frames += 1;
parser.ref_frame_update(&fh).expect("ref slot update");
}
ParsedObu::Frame(f) => {
frames += 1;
parser.ref_frame_update(&f.header).expect("ref slot update");
}
_ => {}
}
}
}
// 525 is upstream's own golden (cross-checked against GStreamer's OBU walk).
assert_eq!(obus, 525);
assert_eq!(frames, 274);
}
#[test]
fn vp9_splits_superframes_and_parses_headers() {
let mut parser = cros_codecs::codec::vp9::parser::Parser::default();
let (mut chunks, mut frames) = (0u32, 0u32);
for packet in IvfIterator::new(VP9_25FPS) {
chunks += 1;
frames += parser
.parse_chunk(packet.as_ref())
.expect("vp9 chunk")
.len() as u32;
}
assert_eq!(chunks, 250);
// > chunks proves superframe splitting engaged.
assert_eq!(frames, 269);
}
}