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]>
202 lines
8.9 KiB
Rust
202 lines
8.9 KiB
Rust
//! 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());
|
||
}
|
||
}
|