forked from unom/punktfunk
H265Planner mirrors the H.264 layer's contract exactly: plan_au -> AuPlan
{ picture, slices with ref lists by stable PicId, DpbUpdate, warnings },
same concealment posture (warnings never abort, in-place reference
substitution preserving ref_idx positions, outputs survive failed AUs,
flush gates on AwaitingIdr, any IRAP resumes). Ported logic: RPS 8.3.2
(short-term AND long-term incl. PocLsbLt/MSB-cycle - the hosts' RFI
recovery rides long-term refs), ref lists 8.3.3/8.3.4, DPB C.5.2.2/C.5.2.3
via the vendored dpb; POC 8.3.1 from the vendored PictureData. Written
fresh: the plan surface, AU walk, envelope gates (multilayer, interlaced,
SCC self-reference, DPB>16, conf-window overflow - checked at EVERY
activation, not just parse), HEVC recovery-point SEI (prefix NALU 39,
se(v) recovery_poc_cnt), VUI colour with E.3.1 inference, and a test-only
HEVC bitstream synthesizer (upstream has none).
Upstream deviations worth naming (all in-code with spec anchors): the
empty-RPS inter slice cannot infinite-loop (upstream bug); RASL behind a
joined CRA refuses BEFORE any state change (PlanError::RaslSkipped - the
WP-2 wiring must map it to skip, not reanchor; module docs carry the
contract note); MaxPicOrderCntLsb reads from the ACTIVATING SPS (upstream
latches at parse - a latent multi-SPS bug); C.5.2.2's exemption is
picture 0 of the BITSTREAM (EobNut), never first-after-EOS.
Vendored parser gained PROVENANCE deviation 7 (report upstream): hostile
slice headers with num_long_term_sps+num_long_term_pics > 16 indexed out
of bounds of SliceHeader's [_;16] arrays - a production panic on exactly
the long-term-reference path, now a parse error.
Port review round 7: 10 findings (3 blocking: the vendor panic, an
EOS-boundary output interleave, an envelope bypass through PPS-only SPS
rebind reaching wrapping crop arithmetic) - 9 fixed with a regression
test each, 1 documented as the WP-2 contract note. Known follow-up: the
h264 AU-tail truncation detector shares h265's dead-arm shape (its arm
also cuts reserved NALU types, so the fix is not identical - deferred).
Tests: 29 h265 planner + 2 HEVC SEI + full test-25fps.h265/bear/bbb clip
walks with real invariants (every stored id output exactly once,
ascending POC per IRAP period). Gates: fmt clean; clippy -D warnings zero
(mac + pf-lxcheck2 incl. pf-client-core/pf-presenter); tests 45+69 mac,
69+121+53 container.
157 lines
6.4 KiB
Rust
157 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 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);
|
|
}
|
|
}
|