open-standards/wai/wai-pack/src/fixture.rs
Transaction Science abc75ec98c wai-pack: staged packages — refinement layers, stills, a piece's ladder, live windows and simulated delivery (W4-4)
The packer writes refinement layers, chooses each piece's representation by
its ladder, packs live, and simulates delivery over MoQ; the corpus gains a
staged golden that the independent verifier re-derives byte for byte.

- Refinements (packaging §5.4): for each refinement rung of a piece's ladder,
  a wai.video.int_refine object over the int_motion base, written by wai-rs's
  refinement encoder (staged::wir1::encode) with the manifest `wai stage
  refine` writes; every layer applied to the decoded base by the reference
  sink before it is kept. The index states each prefix's tier, digest, SSE
  and deadline.
- A piece's ladder and stills (§5.7, plan::piece_ladder): a rule's
  then.ladder, or the component's first base rung, its refinement chain and
  its floors. §3.4 item 10 now requires a rule's ladder to name exactly one
  base, every refinement over a rung it names and every floor. A
  wai.image.png base is a still, the piece's first frame held for the piece,
  written as a PNG whose every byte is stated (png.rs; the verifier's reader
  inflates with miniz_oxide, the one new dependency).
- One coder (pack::Coder) for the on-demand and the live packer; the
  verifier holds each piece to its ladder and its plan's deadline, and
  decodes every prefix (a still held, each layer through the sink).
- Catalogs: the MoQ catalog is a stage set of moq-streaming-format §4.1,
  written by staged::moq (wai_staged, a receipts track and an alias per
  media track, depends on a track of every layer below); `priority` (MOQT's,
  §8.5) and `wai_layer` leave it, and the registry drops `wai_layer`. HTTP
  views carry the component's default base capability only: a still's piece
  plays from the floor there (http-delivery §4.2), and its stage view and
  MoQ tracks list it.
- Live (§9.2, live.rs, `wai-pack live`): frames pushed one at a time, pieces
  coded as the segmenter closes them, windows sealed by version-2 receipts
  chained live-window; its plan, bases and refinements are the on-demand
  package's byte for byte (`wai-pack listing --primary`).
- Delivery (§8.9, delivery.rs, `wai-pack sim`): a thin consumer of
  stage_measure::sim — per-track timeouts, §8.5's priorities, the
  subscriber's cap, group receipts on each track's chain; delivered objects
  accepted by Merkle proof, dropped ones held by inclusion only.
- Corpus: the golden video-staged-01 (fixture, policy, supplied floor and
  supplied_floor.py, which states how it was written), staged-mutations/,
  delivery/; video-base-01 keeps its objects, index and receipt (its MoQ
  catalog and LISTING change).
- pack_verify.py re-derives every layer and still from the source (its own
  rANS coder and deflate), decodes every prefix (staged_verify.py's WIR1 and
  PNG readers), checks the MoQ catalog with staged_verify.py's §4.1 check,
  renders every catalog, and re-derives each delivery case through
  sim_verify.py's simulator. stage_diff.py runs layers, stills, piece plans
  and deliveries differentially.
- Tests 5, 7, 12 and 13 over the staged golden (tests/staged.rs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 14:21:54 -04:00

202 lines
8.9 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Synthetic sources for the conformance corpus, generated rather than committed.
//!
//! Each fixture is a Y4M stream (`C420jpeg`, progressive, 25 frames per second) whose
//! every byte is an integer function of the frame index and the pixel position, so an
//! independent implementation regenerates it byte for byte; the corpus pins each
//! fixture's SHA-256. A fixture is a list of spans; a span shows one shot, or fades from
//! one shot to another, for a number of frames. Shots read the global frame index `t`,
//! so a shot that resumes after an interruption continues its motion.
//!
//! - `ramp`: `Y = lo + v·(hi − lo) / (period − 1)`, `v = (x·dx + y·dy + t·speed) mod
//! period`; with `jitter` 1, `Y` moves by `noise mod 3 − 1`, clamped to 16..=235.
//! - `blocks`: `Y = levels[((x + t·speed) / bw + y / bh) mod 3]`.
//! - `noise`: `Y = lo + noise mod (hi − lo + 1)`.
//! - A fade of `n` frames from shot `a` to shot `b`, at its `i`-th frame (from 0), is
//! `(a·(n − i − 1) + b·(i + 1)) / n` per sample, chroma included.
//! - A span marked *local* reads `t` as the frame's index within the span, so the span
//! is the same frames wherever it appears: a bumper played twice.
//!
//! `noise(seed, x, y, t) = mix(seed ^ mix(x ^ mix(y ^ mix(t))))`, where `mix` is the
//! 32-bit finaliser `v ^= v >> 16; v *= 0x7feb352d; v ^= v >> 15; v *= 0x846ca68b;
//! v ^= v >> 16` (wrapping). Chroma is constant per shot.
/// The fixtures this module generates.
pub const NAMES: &[&str] = &["cuts-01", "kinds-01", "video-base-01", "video-staged-01"];
/// The fixtures the scene-cut, encode and step-choice corpora cover.
pub const SCENECUT_NAMES: &[&str] = &["cuts-01", "kinds-01"];
fn mix(mut v: u32) -> u32 {
v ^= v >> 16;
v = v.wrapping_mul(0x7feb_352d);
v ^= v >> 15;
v = v.wrapping_mul(0x846c_a68b);
v ^= v >> 16;
v
}
/// The fixtures' noise.
pub fn noise(seed: u32, x: u32, y: u32, t: u32) -> u32 {
mix(seed ^ mix(x ^ mix(y ^ mix(t))))
}
#[derive(Debug, Clone, Copy)]
enum Shot {
Ramp { lo: u32, hi: u32, dx: u32, dy: u32, speed: u32, period: u32, jitter: bool, seed: u32, cb: u8, cr: u8 },
Blocks { levels: [u8; 3], bw: u32, bh: u32, speed: u32, cb: u8, cr: u8 },
Noise { lo: u32, hi: u32, seed: u32, cb: u8, cr: u8 },
}
impl Shot {
fn y(&self, x: u32, y: u32, t: u32) -> u32 {
match *self {
Shot::Ramp { lo, hi, dx, dy, speed, period, jitter, seed, .. } => {
let v = (x * dx + y * dy + t * speed) % period;
let base = lo + v * (hi - lo) / (period - 1);
if jitter { (base + noise(seed, x, y, t) % 3).saturating_sub(1).clamp(16, 235) } else { base }
}
Shot::Blocks { levels, bw, bh, speed, .. } => levels[(((x + t * speed) / bw + y / bh) % 3) as usize] as u32,
Shot::Noise { lo, hi, seed, .. } => lo + noise(seed, x, y, t) % (hi - lo + 1),
}
}
fn chroma(&self) -> (u32, u32) {
match *self {
Shot::Ramp { cb, cr, .. } | Shot::Blocks { cb, cr, .. } | Shot::Noise { cb, cr, .. } => (cb as u32, cr as u32),
}
}
}
struct Span {
frames: u32,
shot: Shot,
fade_to: Option<Shot>,
local: bool,
}
#[allow(clippy::too_many_arguments)]
const fn ramp(lo: u32, hi: u32, dx: u32, dy: u32, speed: u32, jitter: bool, seed: u32, cb: u8, cr: u8) -> Shot {
Shot::Ramp { lo, hi, dx, dy, speed, period: 32, jitter, seed, cb, cr }
}
fn spans(name: &str) -> Option<(u32, u32, Vec<Span>)> {
let s = |frames, shot| Span { frames, shot, fade_to: None, local: false };
let local = |frames, shot| Span { frames, shot, fade_to: None, local: true };
Some(match name {
// Hard cuts; a flash of exactly 6 frames (the default lookahead) inside shot B;
// a 40-frame fade from C to D; and two shots cut against each other, E and F.
"cuts-01" => {
let a = ramp(20, 100, 1, 1, 1, true, 1, 110, 140);
let b = ramp(120, 220, 2, 1, 1, true, 2, 150, 120);
let flash = Shot::Noise { lo: 228, hi: 235, seed: 3, cb: 128, cr: 128 };
let c = ramp(30, 90, 1, 2, 1, true, 4, 120, 120);
let d = ramp(150, 230, 1, 0, 2, true, 5, 136, 136);
let e = ramp(40, 120, 0, 1, 1, true, 6, 100, 150);
let f = ramp(140, 200, 1, 0, 1, true, 7, 160, 110);
(32, 18, vec![
s(60, a), s(30, b), s(6, flash), s(44, b), s(20, c),
Span { frames: 40, shot: c, fade_to: Some(d), local: false },
s(30, d), s(30, e), s(30, f), s(30, e), s(30, f),
])
}
// One shot of each kind, cut against each other, then a static shot longer than
// the longest piece.
"kinds-01" => (32, 18, vec![
s(40, ramp(20, 200, 1, 1, 0, false, 0, 128, 128)),
s(40, Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 128, cr: 128 }),
s(40, Shot::Noise { lo: 16, hi: 235, seed: 7, cb: 128, cr: 128 }),
s(40, ramp(16, 80, 1, 1, 1, true, 8, 128, 128)),
s(130, ramp(60, 180, 2, 1, 0, false, 0, 140, 116)),
]),
// The base-only golden: three shots with a one-second bumper between them, the
// same frames both times, so its pieces are one object.
"video-base-01" => {
let bumper = Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 100, cr: 160 };
(64, 36, vec![
s(60, ramp(20, 200, 1, 1, 1, true, 11, 110, 140)),
local(25, bumper),
s(50, ramp(30, 220, 2, 1, 1, true, 12, 150, 120)),
local(25, bumper),
s(40, ramp(16, 160, 1, 2, 1, true, 13, 128, 128)),
])
}
// The staged golden: a moving shot, the bumper, a still shot (no motion, no
// noise), the bumper again (the same frames both times), and a second moving
// shot.
"video-staged-01" => {
let bumper = Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 100, cr: 160 };
(64, 36, vec![
s(50, ramp(20, 200, 1, 1, 1, true, 21, 110, 140)),
local(25, bumper),
s(40, ramp(60, 180, 2, 1, 0, false, 0, 140, 116)),
local(25, bumper),
s(60, ramp(30, 120, 1, 2, 1, true, 22, 150, 120)),
])
}
_ => return None,
})
}
/// The fixture `name` as Y4M bytes, or `None` for a name [`NAMES`] does not hold.
pub fn generate(name: &str) -> Option<Vec<u8>> {
let (w, h, spans) = spans(name)?;
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let mut out = format!("YUV4MPEG2 W{w} H{h} F25:1 Ip A1:1 C420jpeg\n").into_bytes();
let mut t = 0u32;
for span in &spans {
for i in 0..span.frames {
out.extend_from_slice(b"FRAME\n");
let blend = |a: u32, b: u32| (a * (span.frames - i - 1) + b * (i + 1)) / span.frames;
let st = if span.local { i } else { t };
for y in 0..h {
for x in 0..w {
let v = span.shot.y(x, y, st);
let v = span.fade_to.map_or(v, |to| blend(v, to.y(x, y, st)));
out.push(v as u8);
}
}
let (cb, cr) = span.shot.chroma();
let (cb, cr) = span.fade_to.map_or((cb, cr), |to| {
let (tb, tr) = to.chroma();
(blend(cb, tb), blend(cr, tr))
});
out.extend(std::iter::repeat_n(cb as u8, (cw * ch) as usize));
out.extend(std::iter::repeat_n(cr as u8, (cw * ch) as usize));
t += 1;
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
/// Known answers computed independently (Python, arbitrary-precision integers
/// masked to 32 bits).
#[test]
fn mix_and_noise_known_answers() {
assert_eq!(mix(0), 0);
assert_eq!(mix(1), 0x6889_90c0);
assert_eq!(mix(0xdead_beef), 0xe628_c683);
assert_eq!(noise(3, 5, 7, 11), 3_929_262_924);
}
#[test]
fn every_fixture_is_a_y4m_stream() {
for n in NAMES {
let b = generate(n).unwrap();
let (hd, frames) = crate::source::y4m::read_all(&b).unwrap();
let dims = if n.starts_with("video-") { (64, 36) } else { (32, 18) };
assert_eq!((hd.w, hd.h, hd.rate), (dims.0, dims.1, Some((25, 1))), "{n}");
assert!(frames.len() >= 200, "{n}");
}
// The bumper is the same frames both times.
let (_, f) = crate::source::y4m::read_all(&generate("video-base-01").unwrap()).unwrap();
assert_eq!(f[60..85], f[135..160]);
assert_ne!(f[60..85], f[85..110]);
let (_, f) = crate::source::y4m::read_all(&generate("video-staged-01").unwrap()).unwrap();
assert_eq!(f[50..75], f[115..140]);
assert!(f[75..115].iter().all(|x| *x == f[75]), "the still shot does not change");
assert!(generate("nope").is_none());
}
}