use crate::{Error, Result};
use crate::lossy::bool_dec::{BoolDecoder, BoolState};
use crate::lossy::decode::Frame;
use crate::lossy::frame_header::{FrameHeader, KEY_FRAME_HEADER_LEN};
use crate::lossy::loop_filter::FInfo;
use crate::lossy::prelude::*;
use crate::lossy::reconstruct::{self, Planes};
use crate::lossy::yuv::{Yuv420Ref, upsample_output_row};
pub(crate) enum Step {
NeedMore,
Header {
width: u32,
height: u32,
},
Rows {
first_row: u32,
count: u32,
},
Done,
}
pub(crate) struct Vp8Stream {
state: Phase,
}
enum Phase {
ParsingHeaders,
Streaming(Box<StreamState>),
Done(Vec<u8>),
}
struct StreamState {
frame: Frame,
planes: Planes,
fstrengths: [[FInfo; 2]; 4],
filter_type: u8,
use_skip: bool,
width: usize,
height: usize,
part0_range: (usize, usize),
part0_state: BoolState,
part_start: Vec<usize>,
part_declared_end: Vec<Option<usize>>,
token_state: Vec<Option<BoolState>>,
mb_y: usize,
mb_x: usize,
intra_done: bool,
finfo_prev: Vec<FInfo>,
finfo_cur: Vec<FInfo>,
filtered_through: Option<usize>,
ready: Vec<u8>,
out_rows_done: u32,
}
impl StreamState {
const fn chroma_height(&self) -> usize {
self.height.div_ceil(2)
}
}
impl Vp8Stream {
pub(crate) const fn new() -> Self {
Self {
state: Phase::ParsingHeaders,
}
}
pub(crate) fn advance(&mut self, payload: &[u8], final_input: bool) -> Result<Step> {
match core::mem::replace(&mut self.state, Phase::ParsingHeaders) {
Phase::ParsingHeaders => self.advance_parsing(payload, final_input),
Phase::Streaming(ss) => self.advance_streaming(ss, payload, final_input),
Phase::Done(pixels) => {
self.state = Phase::Done(pixels);
Ok(Step::Done)
},
}
}
pub(crate) fn ready(&self) -> &[u8] {
match &self.state {
Phase::Streaming(ss) => &ss.ready,
Phase::Done(pixels) => pixels,
Phase::ParsingHeaders => &[],
}
}
#[cfg(test)]
pub(crate) fn into_pixels(self) -> Option<Vec<u8>> {
match self.state {
Phase::Done(pixels) => Some(pixels),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn token_layout(&self) -> Option<(usize, Vec<usize>, Vec<Option<usize>>)> {
match &self.state {
Phase::Streaming(ss) => Some((
ss.frame.num_parts,
ss.part_start.clone(),
ss.part_declared_end.clone(),
)),
_ => None,
}
}
fn advance_parsing(&mut self, payload: &[u8], final_input: bool) -> Result<Step> {
if payload.len() < KEY_FRAME_HEADER_LEN {
return short_or_truncated(final_input);
}
let fh = FrameHeader::parse_key_frame(payload)?;
let part0_len = usize::try_from(fh.first_partition_size).unwrap_or(usize::MAX);
let Some(part0_end) = KEY_FRAME_HEADER_LEN
.checked_add(part0_len)
.filter(|&e| payload.len() >= e)
else {
return short_or_truncated(final_input);
};
let part0 = &payload[KEY_FRAME_HEADER_LEN..part0_end];
let mut frame = Frame::new(fh)?;
let mut br = BoolDecoder::new(part0);
frame.parse_headers(&mut br);
let num_parts = 1usize << br.read_literal(2);
frame.num_parts = num_parts;
let last = num_parts - 1;
let base = part0_end;
let Some(table_end) = base.checked_add(3 * last).filter(|&e| payload.len() >= e) else {
return short_or_truncated(final_input);
};
let mut part_start = Vec::with_capacity(num_parts);
let mut part_declared_end = Vec::with_capacity(num_parts);
let mut cur = table_end;
for p in 0..last {
let sz = usize::from(payload[base + 3 * p])
| usize::from(payload[base + 3 * p + 1]) << 8
| usize::from(payload[base + 3 * p + 2]) << 16;
part_start.push(cur);
let end = cur.saturating_add(sz);
part_declared_end.push(Some(end));
cur = end;
}
part_start.push(cur);
part_declared_end.push(None);
frame.parse_quant(&mut br);
let _update_proba = br.read_flag();
frame.parse_proba(&mut br);
let planes = Planes::new(frame.mb_w, frame.mb_h);
let fstrengths = reconstruct::compute_fstrengths(&frame.segment, &frame.filter);
let (use_skip, filter_type, mb_w) = (frame.proba.use_skip, frame.filter_type, frame.mb_w);
let ss = StreamState {
part0_range: (KEY_FRAME_HEADER_LEN, part0_end),
part0_state: br.state(),
part_start,
part_declared_end,
token_state: vec![None; num_parts],
mb_y: 0,
mb_x: 0,
intra_done: false,
finfo_prev: vec![FInfo::default(); mb_w],
finfo_cur: vec![FInfo::default(); mb_w],
filtered_through: None,
ready: Vec::new(),
out_rows_done: 0,
filter_type,
use_skip,
width: usize::from(fh.width),
height: usize::from(fh.height),
fstrengths,
planes,
frame,
};
self.state = Phase::Streaming(Box::new(ss));
Ok(Step::Header {
width: u32::from(fh.width),
height: u32::from(fh.height),
})
}
fn advance_streaming(
&mut self,
mut ss: Box<StreamState>,
payload: &[u8],
final_input: bool,
) -> Result<Step> {
let mb_w = ss.frame.mb_w;
let mb_h = ss.frame.mb_h;
let mask = ss.frame.num_parts - 1;
let rows_before = ss.out_rows_done;
loop {
if ss.mb_y == mb_h {
if ss.filter_type != 0 && ss.filtered_through != Some(mb_h - 1) {
reconstruct::filter_mb_row(
&mut ss.planes,
&ss.finfo_prev,
mb_h - 1,
ss.filter_type,
);
ss.filtered_through = Some(mb_h - 1);
}
emit_finalized_rows(&mut ss);
let new = ss.out_rows_done - rows_before;
if new > 0 {
self.state = Phase::Streaming(ss);
return Ok(Step::Rows {
first_row: rows_before,
count: new,
});
}
let pixels = core::mem::take(&mut ss.ready);
self.state = Phase::Done(pixels);
return Ok(Step::Done);
}
let mb_y = ss.mb_y;
if !ss.intra_done {
let (a, b) = ss.part0_range;
let mut br0 = BoolDecoder::resume(&payload[a..b], ss.part0_state);
ss.frame.parse_intra_mode_row(&mut br0);
ss.part0_state = br0.state();
ss.intra_done = true;
}
let p = mb_y & mask;
let start = ss.part_start[p].min(payload.len());
let end = ss.part_declared_end[p].map_or(payload.len(), |e| e.min(payload.len()));
let slice = &payload[start..end];
let window_final =
final_input || ss.part_declared_end[p].map_or(final_input, |e| payload.len() >= e);
let mut token_br = match ss.token_state[p] {
Some(st) => BoolDecoder::resume(slice, st),
None => {
if slice.len() >= 2 || window_final {
BoolDecoder::new(slice)
} else {
let new = ss.out_rows_done - rows_before;
self.state = Phase::Streaming(ss);
return Ok(rows_or_needmore(rows_before, new));
}
},
};
let mut suspended = false;
while ss.mb_x < mb_w {
let mb_x = ss.mb_x;
if ss.frame.mb_data[mb_x].skip {
ss.frame.skip_residuals(mb_x);
ss.mb_x += 1;
continue;
}
let pre_br = token_br.state();
let pre_top = ss.frame.mb_info[mb_x + 1];
let pre_left = ss.frame.mb_info[0];
ss.frame.parse_residuals(&mut token_br, mb_x);
if token_br.is_exhausted() && !window_final {
ss.frame.mb_info[mb_x + 1] = pre_top;
ss.frame.mb_info[0] = pre_left;
ss.token_state[p] = Some(pre_br);
suspended = true;
break;
}
ss.mb_x += 1;
}
if suspended {
let new = ss.out_rows_done - rows_before;
self.state = Phase::Streaming(ss);
return Ok(rows_or_needmore(rows_before, new));
}
ss.token_state[p] = Some(token_br.state());
reconstruct_and_filter_row(&mut ss, mb_y, mb_w);
ss.frame.init_scanline();
ss.mb_y += 1;
ss.mb_x = 0;
ss.intra_done = false;
emit_finalized_rows(&mut ss);
}
}
}
const fn rows_or_needmore(first_row: u32, new_rows: u32) -> Step {
if new_rows > 0 {
Step::Rows {
first_row,
count: new_rows,
}
} else {
Step::NeedMore
}
}
const fn short_or_truncated(final_input: bool) -> Result<Step> {
if final_input {
Err(Error::Truncated)
} else {
Ok(Step::NeedMore)
}
}
fn reconstruct_and_filter_row(ss: &mut StreamState, mb_y: usize, mb_w: usize) {
for mb_x in 0..mb_w {
reconstruct::reconstruct_mb(&mut ss.planes, &ss.frame.mb_data[mb_x], mb_x, mb_y, mb_w);
ss.finfo_cur[mb_x] =
reconstruct::resolve_finfo(ss.fstrengths, &ss.frame.mb_data[mb_x], ss.use_skip);
}
if ss.filter_type != 0 && mb_y >= 1 {
reconstruct::filter_mb_row(&mut ss.planes, &ss.finfo_prev, mb_y - 1, ss.filter_type);
ss.filtered_through = Some(mb_y - 1);
}
core::mem::swap(&mut ss.finfo_prev, &mut ss.finfo_cur);
}
fn emit_finalized_rows(ss: &mut StreamState) {
let (luma_end, chroma_end) = finalized_plane_rows(
ss.filter_type,
ss.filtered_through,
ss.mb_y,
ss.frame.mb_h,
ss.height,
ss.chroma_height(),
);
let (width, height, chroma_height) = (ss.width, ss.height, ss.chroma_height());
let row_bytes = width * 4;
while (ss.out_rows_done as usize) < height {
let y = ss.out_rows_done as usize;
if y >= luma_end {
break;
}
let cmax = y.div_ceil(2).min(chroma_height - 1);
if cmax >= chroma_end {
break;
}
ss.ready.resize((y + 1) * row_bytes, 0);
let y0 = ss.planes.y_stride + 1;
let uv0 = ss.planes.uv_stride + 1;
let src = Yuv420Ref {
y: &ss.planes.y[y0..],
y_stride: ss.planes.y_stride,
u: &ss.planes.u[uv0..],
v: &ss.planes.v[uv0..],
uv_stride: ss.planes.uv_stride,
};
upsample_output_row(
&src,
width,
height,
y,
&mut ss.ready[y * row_bytes..(y + 1) * row_bytes],
);
ss.out_rows_done += 1;
}
}
fn finalized_plane_rows(
filter_type: u8,
f_idx: Option<usize>,
r: usize,
mb_h: usize,
height: usize,
chroma_height: usize,
) -> (usize, usize) {
if filter_type == 0 {
return ((16 * r).min(height), (8 * r).min(chroma_height));
}
let extra = if filter_type == 1 { 2 } else { 8 };
match f_idx {
None => (0, 0),
Some(k) if k == mb_h - 1 => (height, chroma_height),
Some(k) => {
let luma = (16 * (k + 1)).saturating_sub(extra).min(height);
let chroma = if filter_type == 1 {
(8 * r).min(chroma_height)
} else {
(8 * (k + 1)).saturating_sub(4).min(chroma_height)
};
(luma, chroma)
},
}
}
#[cfg(test)]
fn stream_over_splits(payload: &[u8], cuts: &[usize]) -> Result<Vec<u8>> {
let len = payload.len();
let mut stream = Vp8Stream::new();
let mut rows_reported = 0u32;
let boundaries = cuts.iter().copied().chain(core::iter::once(len));
'outer: for cut in boundaries {
let cut = cut.min(len);
let final_input = cut == len;
loop {
match stream.advance(&payload[..cut], final_input)? {
Step::Header { .. } => {},
Step::Rows { first_row, count } => {
assert_eq!(first_row, rows_reported, "Rows payout is not contiguous");
rows_reported += count;
},
Step::NeedMore => break,
Step::Done => break 'outer,
}
}
}
stream.into_pixels().ok_or(Error::Truncated)
}
#[cfg(test)]
fn split_patterns(len: usize) -> Vec<Vec<usize>> {
let mut pats = vec![Vec::new()];
if len > 1 {
pats.push((1..len).collect());
pats.push(vec![len / 2]);
pats.push(vec![len / 3, 2 * len / 3]);
}
pats
}
#[cfg(test)]
mod tests {
use super::{Step, Vp8Stream, finalized_plane_rows, split_patterns, stream_over_splits};
use crate::lossy::decode;
use crate::lossy::prelude::*;
use crate::lossy::{Dimensions, Effort, ImageRef, LossyConfig, PixelLayout, encode_vp8};
fn byte(v: u32) -> u8 {
u8::try_from(v & 0xff).unwrap_or(0)
}
fn rgba_image(width: u32, height: u32, f: impl Fn(u32, u32) -> [u8; 3]) -> Vec<u8> {
let mut buf = Vec::new();
for y in 0..height {
for x in 0..width {
let [r, g, b] = f(x, y);
buf.extend_from_slice(&[r, g, b, 0xff]);
}
}
buf
}
fn encode_payload(
rgba: &[u8],
width: u32,
height: u32,
effort: Effort,
quality: u8,
) -> Vec<u8> {
let dims = Dimensions::new(width, height).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, rgba).unwrap();
let cfg = LossyConfig::new().with_quality(quality).with_effort(effort);
encode_vp8(img, &cfg).unwrap().1
}
const FIXTURES: &[&[u8]] = &[
include_bytes!("../../tests/fixtures/checker_16x16_q20.vp8"),
include_bytes!("../../tests/fixtures/gradient_17x13_q80.vp8"),
include_bytes!("../../tests/fixtures/noise_32x24_q30.vp8"),
include_bytes!("../../tests/fixtures/noise_5x9_q50.vp8"),
];
fn one_shot(payload: &[u8]) -> crate::Result<Vec<u8>> {
decode::decode_frame(payload).map(|img| img.as_bytes().to_vec())
}
fn assert_stream_equivalence(payload: &[u8]) {
let expected = one_shot(payload);
for cuts in split_patterns(payload.len()) {
let streamed = stream_over_splits(payload, &cuts);
match (&expected, &streamed) {
(Ok(a), Ok(b)) => assert_eq!(a, b, "pixels differ for cuts {cuts:?}"),
(Err(e), Err(f)) => assert_eq!(e, f, "errors differ for cuts {cuts:?}"),
(a, b) => panic!("stream/one-shot disagree (cuts {cuts:?}): {a:?} vs {b:?}"),
}
}
}
fn assert_cuts_match(payload: &[u8], cuts: &[usize]) {
match (one_shot(payload), stream_over_splits(payload, cuts)) {
(Ok(a), Ok(b)) => assert_eq!(a, b, "pixels differ for cuts {cuts:?}"),
(Err(e), Err(f)) => assert_eq!(e, f, "errors differ for cuts {cuts:?}"),
(a, b) => panic!("stream/one-shot disagree (cuts {cuts:?}): {a:?} vs {b:?}"),
}
}
#[test]
fn stream_equals_one_shot_on_fixtures() {
for fixture in FIXTURES {
assert_stream_equivalence(fixture);
}
}
proptest::proptest! {
#[test]
fn stream_equals_one_shot_over_random_splits(
idx in 0usize..FIXTURES.len(),
raw_cuts in proptest::collection::vec(0usize..4096, 0..10),
) {
let mut cuts = raw_cuts;
cuts.sort_unstable();
assert_cuts_match(FIXTURES[idx], &cuts);
}
#[test]
fn stream_never_disagrees_on_arbitrary_bytes(
data in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..600),
) {
match (one_shot(&data), stream_over_splits(&data, &[])) {
(Ok(a), Ok(b)) => proptest::prop_assert_eq!(a, b),
(Err(_), Err(_)) => {}
(a, b) => proptest::prop_assert!(
false, "disagree: one_shot_ok={} streamed_ok={}", a.is_ok(), b.is_ok()
),
}
}
}
#[test]
fn stream_equals_one_shot_on_minimal_key_frame() {
let header = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0];
assert_stream_equivalence(&header);
}
#[test]
fn finalized_plane_rows_reports_exact_finalized_counts() {
assert_eq!(finalized_plane_rows(0, None, 3, 5, 100, 100), (48, 24));
assert_eq!(finalized_plane_rows(1, Some(2), 4, 5, 200, 200), (46, 32));
assert_eq!(finalized_plane_rows(2, Some(2), 4, 5, 200, 200), (40, 20));
}
#[test]
fn stream_matches_one_shot_on_a_filter_off_multi_row_frame() {
let (w, h) = (32u32, 48u32);
let rgba = rgba_image(w, h, |x, y| [byte(y * 5), byte(x * 3), byte((x + y) * 2)]);
let payload = encode_payload(&rgba, w, h, Effort::Fast, 80);
assert_stream_equivalence(&payload);
}
#[test]
fn truncating_a_fixture_matches_one_shot() {
for fixture in FIXTURES {
for keep in [10, fixture.len() / 2, fixture.len() - 1] {
if keep < 10 || keep >= fixture.len() {
continue;
}
let truncated = &fixture[..keep];
assert_stream_equivalence(truncated);
}
}
}
fn control_partition(count_log2: u32) -> Vec<u8> {
let mut enc = crate::lossy::bool_enc::BoolEncoder::new();
enc.put_flag(false); enc.put_flag(false); enc.put_flag(false); enc.put_flag(false); enc.put_literal(6, 0); enc.put_literal(3, 0); enc.put_flag(false); enc.put_literal(2, count_log2); enc.finish()
}
fn size_entry(sz: usize) -> [u8; 3] {
[
u8::try_from(sz & 0xff).unwrap(),
u8::try_from((sz >> 8) & 0xff).unwrap(),
u8::try_from((sz >> 16) & 0xff).unwrap(),
]
}
fn build_multipart(
width: u16,
height: u16,
count_log2: u32,
sizes: &[usize],
final_len: usize,
) -> (Vec<u8>, Vec<usize>) {
let num_parts = 1usize << count_log2;
assert_eq!(
sizes.len(),
num_parts - 1,
"one size per non-final partition"
);
let mut p0 = control_partition(count_log2);
p0.resize(p0.len() + 16, 0x5Au8);
let mut payload = crate::lossy::enc_header::frame_header_bytes(
u32::try_from(p0.len()).unwrap(),
width,
height,
)
.to_vec();
payload.extend_from_slice(&p0);
for &sz in sizes {
payload.extend_from_slice(&size_entry(sz));
}
let table_end = payload.len(); let token_total = sizes.iter().sum::<usize>() + final_len;
for i in 0..token_total {
let h = (i as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.rotate_left(29);
payload.push(u8::try_from((h >> 32) & 0xff).unwrap());
}
let mut starts = Vec::with_capacity(num_parts);
let mut cur = table_end;
for &sz in sizes {
starts.push(cur);
cur += sz;
}
starts.push(cur);
(payload, starts)
}
#[test]
fn multipart_size_table_offsets_are_pinned_exactly() {
let sizes = [86945usize, 91571, 82375]; let (payload, starts) = build_multipart(16, 64, 2, &sizes, 512);
assert!(
starts[3] < payload.len(),
"final partition must stay in bounds"
);
let mut stream = Vp8Stream::new();
assert!(
matches!(
stream.advance(&payload, false).unwrap(),
Step::Header { .. }
),
"the full header must parse in one feed"
);
let (num_parts, part_start, part_end) = stream
.token_layout()
.expect("layout is known after the header");
assert_eq!(num_parts, 4, "1 << read_literal(2) == 4 token partitions");
assert_eq!(part_start, starts, "cumulative partition starts");
assert_eq!(
part_end,
vec![Some(starts[1]), Some(starts[2]), Some(starts[3]), None],
"each non-final partition's declared end is its successor's start"
);
assert!(
one_shot(&payload).is_ok(),
"the crafted frame must decode (non-vacuous)"
);
assert_cuts_match(&payload, &[]);
}
#[test]
fn streaming_a_skipped_macroblock_advances_the_column() {
let (w, h) = (16u32, 48u32);
let rgba = rgba_image(w, h, |_, _| [128, 128, 128]);
let payload = encode_payload(&rgba, w, h, Effort::Balanced, 90);
assert_stream_equivalence(&payload);
}
#[test]
fn restarved_partition_reports_no_phantom_rows() {
let (payload, starts) = build_multipart(16, 32, 1, &[8], 64);
assert_cuts_match(&payload, &[starts[1]]);
}
#[test]
fn a_fully_buffered_nonfinal_partition_commits_its_row() {
let (payload, starts) = build_multipart(16, 16, 1, &[0], 32);
let prefix = &payload[..starts[1]]; let mut stream = Vp8Stream::new();
let mut committed = false;
loop {
match stream.advance(prefix, false).unwrap() {
Step::Header { .. } => {},
Step::Rows { .. } | Step::Done => {
committed = true;
break;
},
Step::NeedMore => break,
}
}
assert!(
committed,
"a fully-buffered non-final partition must commit its row"
);
assert!(!stream.ready().is_empty(), "row 0 output must be available");
}
#[test]
fn a_truncated_final_partition_commits_padding_not_an_error() {
let (payload, starts) = build_multipart(16, 16, 1, &[1000], 0);
let truncated = &payload[..starts[0]]; assert_cuts_match(truncated, &[]);
}
}