use rusty_h264_common::{Profile, YuvFrame};
use rusty_h264_decoder::Decoder;
use rusty_h264_encoder::{Encoder, EncoderConfig};
fn static_frame(w: usize, h: usize, f: u64) -> YuvFrame {
let mut fr = YuvFrame::black(w, h);
for y in 0..h {
for x in 0..w {
fr.y[y * w + x] = ((x as u64 / 8 * 9 + y as u64 / 8 * 5) & 0xff) as u8;
}
}
for y in h / 2..h {
for x in 0..w {
let jitter = ((x as u64 + y as u64 * 3 + f * 7) % 3) as u8;
fr.y[y * w + x] = fr.y[y * w + x].saturating_add(jitter);
}
}
let ox = (f as usize * 3) % (w / 2);
for y in h / 4..h / 4 + 24 {
for x in ox..ox + 24 {
fr.y[y * w + x] = ((x as u64 * 7 ^ y as u64 * 13 ^ f * 31) & 0xff) as u8;
}
}
let (cw, ch) = (w / 2, h / 2);
for y in 0..ch {
for x in 0..cw {
fr.u[y * cw + x] = 110;
fr.v[y * cw + x] = 140;
}
}
fr
}
fn busy_frame(w: usize, h: usize, f: u64) -> YuvFrame {
let mut fr = YuvFrame::black(w, h);
for y in 0..h {
for x in 0..w {
fr.y[y * w + x] = ((x as u64 * 3 + y as u64 * 5 + f * 11)
^ ((x as u64 >> 2) * (y as u64 >> 1))) as u8;
}
}
let (cw, ch) = (w / 2, h / 2);
for y in 0..ch {
for x in 0..cw {
let v = (128 + (x as i64 * 2 - y as i64 * 3 + f as i64 * 7)) as u8;
fr.u[y * cw + x] = v;
fr.v[y * cw + x] = v.wrapping_add(17);
}
}
fr
}
fn encode(
w: usize,
h: usize,
qp: u8,
cabac: bool,
rd_skip: bool,
nframes: u64,
gen: fn(usize, usize, u64) -> YuvFrame,
) -> Vec<u8> {
let mut cfg = EncoderConfig::new(w, h);
cfg.qp = qp;
cfg.gop_size = 30; cfg.cabac = cabac;
cfg.tune_rd_skip = rd_skip;
if cabac {
cfg.profile = Profile::Main;
}
let mut enc = Encoder::new(cfg).expect("encoder");
let mut out = Vec::new();
for f in 0..nframes {
out.extend_from_slice(&enc.encode(&gen(w, h, f)));
}
out.extend_from_slice(&enc.flush());
out
}
fn decode_all(stream: &[u8]) -> Vec<YuvFrame> {
let mut dec = Decoder::new();
dec.decode_stream(stream).expect("stream must decode cleanly")
}
#[test]
fn rd_skip_streams_decode() {
let (w, h) = (352, 288);
for &qp in &[22u8, 27, 32, 37] {
for &cabac in &[false, true] {
for (name, gen) in [
("static", static_frame as fn(usize, usize, u64) -> YuvFrame),
("busy", busy_frame),
] {
let s = encode(w, h, qp, cabac, true, 12, gen);
let frames = decode_all(&s);
assert_eq!(
frames.len(),
12,
"qp{qp} cabac={cabac} {name}: decoded frame count"
);
}
}
}
}
#[test]
fn gate_fires_on_static_and_blocks_on_busy() {
let (w, h) = (352, 288);
let static_off = encode(w, h, 32, false, false, 12, static_frame).len();
let static_on = encode(w, h, 32, false, true, 12, static_frame).len();
assert!(
static_on < static_off,
"gate must engage on high-free-skip content: {static_on} !< {static_off}"
);
let busy_off = encode(w, h, 32, false, false, 12, busy_frame);
let busy_on = encode(w, h, 32, false, true, 12, busy_frame);
assert_eq!(
busy_off, busy_on,
"gate must stay off on low-free-skip content — byte-identical to the default path"
);
}
#[test]
fn rd_skip_no_reconstruction_drift() {
let (w, h) = (352, 288);
for &cabac in &[false, true] {
let s = encode(w, h, 27, cabac, true, 16, static_frame);
let frames = decode_all(&s);
let last = &frames[frames.len() - 1];
let src = static_frame(w, h, 15);
let mut sse = 0u64;
for (a, b) in last.y.iter().zip(src.y.iter()) {
let d = *a as i64 - *b as i64;
sse += (d * d) as u64;
}
let mse = sse as f64 / (w * h) as f64;
let psnr = 10.0 * (255.0f64 * 255.0 / mse.max(1e-9)).log10();
assert!(
psnr > 30.0,
"cabac={cabac}: reconstruction drifted — final-frame PSNR {psnr:.2} dB"
);
}
}
#[test]
fn greedy_skip_dispatch_streams_decode() {
use rusty_h264_encoder::Preset;
let (w, h) = (352, 288);
for &cabac in &[false, true] {
for &gate in &[Some(0u32), Some(85), Some(101), None] {
for (name, gen) in [
("static", static_frame as fn(usize, usize, u64) -> YuvFrame),
("busy", busy_frame),
] {
let mut cfg = EncoderConfig::new(w, h);
cfg.qp = 27;
cfg.gop_size = 30;
cfg.cabac = cabac;
cfg.preset = Preset::Quality;
cfg.tune_greedy_skip_min_free = gate;
if cabac {
cfg.profile = Profile::Main;
}
let mut enc = Encoder::new(cfg).expect("encoder");
let mut out = Vec::new();
for f in 0..10 {
out.extend_from_slice(&enc.encode(&gen(w, h, f)));
}
out.extend_from_slice(&enc.flush());
let frames = decode_all(&out);
assert_eq!(
frames.len(),
10,
"cabac={cabac} gate={gate:?} {name}: decoded frame count"
);
}
}
}
}
fn near_static_frame(w: usize, h: usize, f: u64) -> YuvFrame {
let mut fr = YuvFrame::black(w, h);
for y in 0..h {
for x in 0..w {
let base = ((x as u64 / 8 * 9 + y as u64 / 8 * 5) & 0xff) as u8;
let d = ((x as u64 * 7 + y as u64 * 13 + f * 5) % 7) as u8;
fr.y[y * w + x] = base.saturating_add(d);
}
}
let (cw, ch) = (w / 2, h / 2);
for y in 0..ch {
for x in 0..cw {
fr.u[y * cw + x] = 110;
fr.v[y * cw + x] = 140;
}
}
fr
}
#[test]
fn greedy_skip_gate_changes_the_stream() {
use rusty_h264_encoder::Preset;
let (w, h) = (352, 288);
let enc_with = |gate: Option<u32>| {
let mut cfg = EncoderConfig::new(w, h);
cfg.qp = 37;
cfg.gop_size = 30;
cfg.preset = Preset::Quality;
cfg.tune_greedy_skip_min_free = gate;
let mut enc = Encoder::new(cfg).expect("encoder");
let mut out = Vec::new();
for f in 0..12 {
out.extend_from_slice(&enc.encode(&near_static_frame(w, h, f)));
}
out.extend_from_slice(&enc.flush());
out.len()
};
let ungated = enc_with(Some(0));
let disabled = enc_with(Some(101));
assert!(
ungated < disabled,
"the greedy skip must shrink the stream when ungated: {ungated} !< {disabled}"
);
}