use std::path::{Path, PathBuf};
use std::process::Command;
const ITU_BASE: &str =
"https://www.itu.int/wftp3/av-arch/jctvc-site/bitstream_exchange/draft_conformance/HEVC_v1";
const DEC265: &str = "/home/lilith/work/heic/libde265-src/build/dec265/dec265";
fn vectors_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("conformance/vectors")
}
fn ensure_vector(name: &str) -> Option<PathBuf> {
let dir = vectors_dir().join(name);
let bitstream = find_bitstream(&dir);
if bitstream.is_some() {
return bitstream;
}
let url = format!("{ITU_BASE}/{name}.zip");
let zip_path = format!("/tmp/{name}.zip");
eprintln!("Downloading {name}...");
let status = Command::new("curl")
.args(["-sSfL", &url, "-o", &zip_path])
.status()
.ok()?;
if !status.success() {
eprintln!(" FAILED to download {name}");
return None;
}
std::fs::create_dir_all(&dir).ok()?;
let status = Command::new("unzip")
.args(["-o", "-q", &zip_path, "-d"])
.arg(&dir)
.status()
.ok()?;
std::fs::remove_file(&zip_path).ok();
if !status.success() {
eprintln!(" FAILED to extract {name}");
return None;
}
find_bitstream(&dir)
}
fn find_bitstream(dir: &Path) -> Option<PathBuf> {
if !dir.exists() {
return None;
}
for entry in walkdir(dir) {
if let Some(ext) = entry.extension()
&& (ext == "bit" || ext == "bin" || ext == "265")
{
return Some(entry);
}
}
None
}
fn walkdir(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
result.extend(walkdir(&path));
} else {
result.push(path);
}
}
}
result
}
fn ensure_reference_yuv(name: &str, bitstream: &Path) -> Option<PathBuf> {
let _dir = bitstream.parent()?;
let top_dir = vectors_dir().join(name);
let ref_path = top_dir.join("reference.yuv");
if ref_path.exists() && std::fs::metadata(&ref_path).ok()?.len() > 0 {
return Some(ref_path);
}
for entry in walkdir(&top_dir) {
if entry.extension().is_some_and(|e| e == "yuv") && !entry.to_string_lossy().contains("md5")
{
return Some(entry);
}
}
eprintln!(" Generating reference YUV with ffmpeg (display order)...");
let status = Command::new("ffmpeg")
.args(["-i"])
.arg(bitstream)
.args(["-pix_fmt", "yuv420p", "-f", "rawvideo", "-y"])
.arg(&ref_path)
.args(["-loglevel", "error"])
.status()
.ok()?;
if status.success() && ref_path.exists() {
Some(ref_path)
} else {
if Path::new(DEC265).exists() {
eprintln!(" ffmpeg failed, trying dec265 (WARNING: decode order)...");
let status = Command::new(DEC265)
.arg(bitstream)
.args(["-o", ref_path.to_str()?])
.arg("--quiet")
.status()
.ok()?;
if status.success() && ref_path.exists() {
return Some(ref_path);
}
}
eprintln!(" Failed to generate reference for {name}");
None
}
}
fn get_dimensions(bitstream: &Path) -> Option<(u32, u32)> {
let output = Command::new("ffprobe")
.args(["-v", "error", "-select_streams", "v:0"])
.args(["-show_entries", "stream=width,height"])
.args(["-of", "csv=p=0"])
.arg(bitstream)
.output()
.ok()?;
let s = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = s.trim().split(',').collect();
if parts.len() >= 2 {
Some((parts[0].parse().ok()?, parts[1].parse().ok()?))
} else {
None
}
}
fn count_frame_types(bitstream: &Path) -> String {
let output = Command::new("ffprobe")
.args(["-v", "error", "-show_frames", "-select_streams", "v:0"])
.args(["-show_entries", "frame=pict_type"])
.args(["-of", "csv=p=0"])
.arg(bitstream)
.output()
.ok();
match output {
Some(o) => {
let s = String::from_utf8_lossy(&o.stdout);
let types: Vec<&str> = s.trim().split('\n').collect();
let i_count = types.iter().filter(|&&t| t == "I").count();
let p_count = types.iter().filter(|&&t| t == "P").count();
let b_count = types.iter().filter(|&&t| t == "B").count();
format!(
"{} frames ({}I {}P {}B)",
types.len(),
i_count,
p_count,
b_count
)
}
None => "unknown".to_string(),
}
}
fn decode_with_ours(bitstream: &Path) -> Result<Vec<Vec<u16>>, String> {
let data = std::fs::read(bitstream).map_err(|e| format!("read: {e}"))?;
let mut decoder = heic::VideoDecoder::new(16);
let frames = decoder
.decode_annex_b(&data)
.map_err(|e| format!("decode: {e}"))?;
Ok(frames
.into_iter()
.map(|f| {
let cw = f.cropped_width();
let ch = f.cropped_height();
let stride = f.width as usize;
let mut cropped = Vec::with_capacity((cw * ch) as usize);
for y in f.crop_top..(f.height - f.crop_bottom) {
let row_start = y as usize * stride + f.crop_left as usize;
let row_end = row_start + cw as usize;
cropped.extend_from_slice(&f.y_plane[row_start..row_end]);
}
cropped
})
.collect())
}
fn load_reference_yuv(path: &Path, width: u32, height: u32) -> Vec<Vec<u16>> {
let data = std::fs::read(path).unwrap_or_default();
let luma_size = (width * height) as usize;
let chroma_size = (width / 2 * height / 2) as usize;
let frame_size = luma_size + 2 * chroma_size;
let num_frames = data.len().checked_div(frame_size).unwrap_or(0);
let mut frames = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let offset = i * frame_size;
let y_bytes = &data[offset..offset + luma_size];
let y_plane: Vec<u16> = y_bytes.iter().map(|&b| b as u16).collect();
frames.push(y_plane);
}
frames
}
fn compare_y_planes(ours: &[u16], reference: &[u16], width: u32, height: u32) -> (f64, usize, u16) {
let len = (width * height) as usize;
let ours = &ours[..len.min(ours.len())];
let reference = &reference[..len.min(reference.len())];
let mut sse = 0u64;
let mut num_diff = 0usize;
let mut max_diff = 0u16;
for (&a, &b) in ours.iter().zip(reference.iter()) {
let diff = (a as i32 - b as i32).unsigned_abs() as u16;
if diff > 0 {
num_diff += 1;
max_diff = max_diff.max(diff);
sse += (diff as u64) * (diff as u64);
}
}
let mse = if !ours.is_empty() {
sse as f64 / ours.len() as f64
} else {
0.0
};
let psnr = if mse > 0.0 {
10.0 * (255.0f64 * 255.0 / mse).log10()
} else {
f64::INFINITY
};
(psnr, num_diff, max_diff)
}
fn run_conformance_test(name: &str) {
eprintln!("\n=== {name} ===");
let bitstream = match ensure_vector(name) {
Some(b) => b,
None => {
eprintln!(" SKIP: could not obtain bitstream");
return;
}
};
let (width, height) = match get_dimensions(&bitstream) {
Some(d) => d,
None => {
eprintln!(" SKIP: could not determine dimensions");
return;
}
};
let frame_types = count_frame_types(&bitstream);
eprintln!(" {width}x{height}, {frame_types}");
let ref_yuv = match ensure_reference_yuv(name, &bitstream) {
Some(r) => r,
None => {
eprintln!(" SKIP: no reference YUV");
return;
}
};
let our_frames = match decode_with_ours(&bitstream) {
Ok(f) => f,
Err(e) => {
eprintln!(" FAIL: {e}");
return;
}
};
let ref_frames = load_reference_yuv(&ref_yuv, width, height);
eprintln!(
" Decoded {} frames (ref: {} frames)",
our_frames.len(),
ref_frames.len()
);
let compare_count = our_frames.len().min(ref_frames.len());
let mut all_exact = true;
let mut worst_psnr = f64::INFINITY;
let mut total_diff_pixels = 0usize;
for i in 0..compare_count {
let (psnr, num_diff, max_diff) =
compare_y_planes(&our_frames[i], &ref_frames[i], width, height);
worst_psnr = worst_psnr.min(psnr);
total_diff_pixels += num_diff;
if num_diff > 0 {
all_exact = false;
if i < 5 || psnr < 40.0 {
eprintln!(
" Frame {i}: PSNR={psnr:.1}dB, {num_diff} diff pixels, max_diff={max_diff}"
);
}
}
}
if compare_count > 0 {
if all_exact {
eprintln!(" PASS: all {compare_count} frames pixel-exact");
} else {
eprintln!(
" RESULT: worst PSNR={worst_psnr:.1}dB, total diff pixels={total_diff_pixels}"
);
}
}
if our_frames.len() < ref_frames.len() {
eprintln!(
" WARNING: decoded only {} of {} reference frames",
our_frames.len(),
ref_frames.len()
);
}
}
#[test]
fn girlshy() {
let bitstream = Path::new("/home/lilith/work/heic/libde265-src/testdata/girlshy.h265");
if !bitstream.exists() {
eprintln!("SKIP: girlshy.h265 not found");
return;
}
let (width, height) = get_dimensions(bitstream).unwrap_or((316, 240));
let frame_types = count_frame_types(bitstream);
eprintln!("\n=== girlshy ===");
eprintln!(" {width}x{height}, {frame_types}");
let ref_path = PathBuf::from("/tmp/girlshy_ref_display.yuv");
if !ref_path.exists() {
let _ = Command::new("ffmpeg")
.args(["-i"])
.arg(bitstream)
.args(["-pix_fmt", "yuv420p", "-f", "rawvideo", "-y"])
.arg(&ref_path)
.args(["-loglevel", "error"])
.status();
}
match decode_with_ours(bitstream) {
Ok(our_frames) => {
let ref_frames = load_reference_yuv(&ref_path, width, height);
eprintln!(
" Decoded {} frames (ref: {} frames)",
our_frames.len(),
ref_frames.len()
);
let n = our_frames.len().min(ref_frames.len());
for i in 0..n.min(15) {
let (psnr, num_diff, max_diff) =
compare_y_planes(&our_frames[i], &ref_frames[i], width, height);
let uninit = our_frames[i].iter().filter(|&&v| v == u16::MAX).count();
let total_px = width * height;
eprintln!(
" Frame {i}: PSNR={psnr:.1}dB, diff={num_diff}/{total_px} max={max_diff}, uninit={uninit}"
);
if i == 1 && num_diff > 0 {
for j in 0..our_frames[i].len().min(ref_frames[i].len()) {
let a = our_frames[i][j];
let b = ref_frames[i][j];
if a != b {
let px = j as u32 % width;
let py = j as u32 / width;
eprintln!(
" First diff at ({px},{py}): ours={a}, ref={b}, diff={}",
(a as i32 - b as i32).abs()
);
break;
}
}
if !our_frames[i].is_empty() && !ref_frames[i].is_empty() {
eprintln!(
" (0,0): ours={}, ref={}",
our_frames[i][0], ref_frames[i][0]
);
eprintln!(
" (100,100): ours={}, ref={}",
our_frames[i][(100 * width + 100) as usize],
ref_frames[i][(100 * width + 100) as usize]
);
}
}
}
}
Err(e) => eprintln!(" FAIL: {e}"),
}
}
#[test]
fn amvp_a() {
run_conformance_test("AMVP_A_MTK_4");
}
#[test]
fn amvp_b() {
run_conformance_test("AMVP_B_MTK_4");
}
#[test]
fn amvp_c() {
run_conformance_test("AMVP_C_Samsung_7");
}
#[test]
fn merge_a() {
run_conformance_test("MERGE_A_TI_3");
}
#[test]
fn merge_b() {
run_conformance_test("MERGE_B_TI_3");
}
#[test]
fn merge_c() {
run_conformance_test("MERGE_C_TI_3");
}
#[test]
fn merge_d() {
run_conformance_test("MERGE_D_TI_3");
}
#[test]
fn merge_e() {
run_conformance_test("MERGE_E_TI_3");
}
#[test]
fn merge_f() {
run_conformance_test("MERGE_F_MTK_4");
}
#[test]
fn tmvp_a() {
run_conformance_test("TMVP_A_MS_3");
}
#[test]
fn amp_a() {
run_conformance_test("AMP_A_Samsung_7");
}
#[test]
fn amp_b() {
run_conformance_test("AMP_B_Samsung_7");
}
#[test]
fn amp_d() {
run_conformance_test("AMP_D_Hisilicon_3");
}
#[test]
fn pmerge_a() {
run_conformance_test("PMERGE_A_TI_3");
}
#[test]
fn pmerge_b() {
run_conformance_test("PMERGE_B_TI_3");
}
#[test]
fn pmerge_c() {
run_conformance_test("PMERGE_C_TI_3");
}
#[test]
fn mvclip_a() {
run_conformance_test("MVCLIP_A_qualcomm_3");
}
#[test]
fn mvdl1zero() {
run_conformance_test("MVDL1ZERO_A_docomo_4");
}
#[test]
fn rps_a() {
run_conformance_test("RPS_A_docomo_5");
}
#[test]
fn rps_b() {
run_conformance_test("RPS_B_qualcomm_5");
}
#[test]
fn rps_c() {
run_conformance_test("RPS_C_ericsson_5");
}
#[test]
fn rap_a() {
run_conformance_test("RAP_A_docomo_6");
}
#[test]
fn rap_b() {
run_conformance_test("RAP_B_Bossen_2");
}
#[test]
fn dblk_a() {
run_conformance_test("DBLK_A_SONY_3");
}
#[test]
fn dblk_b() {
run_conformance_test("DBLK_B_SONY_3");
}
#[test]
fn dblk_d() {
run_conformance_test("DBLK_D_VIXS_2");
}
#[test]
fn sao_a() {
run_conformance_test("SAO_A_MediaTek_4");
}
#[test]
fn sao_b() {
run_conformance_test("SAO_B_MediaTek_5");
}
#[test]
fn wp_a() {
run_conformance_test("WP_A_Toshiba_3");
}
#[test]
fn wp_b() {
run_conformance_test("WP_B_Toshiba_3");
}
#[test]
fn ipred_a() {
run_conformance_test("IPRED_A_Qualcomm_3");
}
#[test]
fn ipred_b() {
run_conformance_test("IPRED_B_Nokia_3");
}
#[test]
fn rqt_a() {
run_conformance_test("RQT_A_HHI_4");
}
#[test]
fn rqt_b() {
run_conformance_test("RQT_B_HHI_4");
}
#[test]
fn sdh_a() {
run_conformance_test("SDH_A_Orange_4");
}
#[test]
fn slist_a() {
run_conformance_test("SLIST_A_Sony_5");
}
#[test]
fn cainit_a() {
run_conformance_test("CAINIT_A_SHARP_4");
}
#[test]
fn cainit_b() {
run_conformance_test("CAINIT_B_SHARP_4");
}
#[test]
fn struct_a() {
run_conformance_test("STRUCT_A_Samsung_7");
}
#[test]
fn struct_b() {
run_conformance_test("STRUCT_B_Samsung_7");
}
#[test]
fn confwin_a() {
run_conformance_test("CONFWIN_A_Sony_1");
}
#[test]
fn deltaqp_a() {
run_conformance_test("DELTAQP_A_BRCM_4");
}
#[test]
fn slices_a() {
run_conformance_test("SLICES_A_Rovi_3");
}
#[test]
fn tiles_a() {
run_conformance_test("TILES_A_Cisco_2");
}
#[test]
fn tiles_b() {
run_conformance_test("TILES_B_Cisco_1");
}
#[test]
fn poc_a() {
run_conformance_test("POC_A_Bossen_3");
}
#[test]
fn wpp_a() {
run_conformance_test("WPP_A_ericsson_MAIN_2");
}
#[test]
fn merge_a_deblock_debug() {
let name = "MERGE_A_TI_3";
let bitstream = match ensure_vector(name) {
Some(b) => b,
None => {
eprintln!("SKIP");
return;
}
};
let (width, height) = get_dimensions(&bitstream).unwrap();
let ref_yuv = match ensure_reference_yuv(name, &bitstream) {
Some(r) => r,
None => {
eprintln!("SKIP: no ref");
return;
}
};
let data = std::fs::read(&bitstream).unwrap();
let mut decoder = heic::VideoDecoder::new(16);
let frames = decoder.decode_annex_b(&data).unwrap();
let ref_frames = load_reference_yuv(&ref_yuv, width, height);
for frame_idx in 0..frames.len().min(ref_frames.len()) {
let f = &frames[frame_idx];
let cw = f.cropped_width();
let _ch = f.cropped_height();
let stride = f.width as usize;
let mut our_y = Vec::new();
for y in f.crop_top..(f.height - f.crop_bottom) {
let start = y as usize * stride + f.crop_left as usize;
our_y.extend_from_slice(&f.y_plane[start..start + cw as usize]);
}
let ref_y = &ref_frames[frame_idx];
let mut diffs: Vec<(u32, u32, i32)> = Vec::new();
let len = (width * height) as usize;
for i in 0..len.min(our_y.len()).min(ref_y.len()) {
let d = our_y[i] as i32 - ref_y[i] as i32;
if d != 0 {
diffs.push((i as u32 % width, i as u32 / width, d));
}
}
if diffs.is_empty() {
eprintln!("Frame {frame_idx}: EXACT");
continue;
}
let max_abs = diffs.iter().map(|d| d.2.abs()).max().unwrap();
eprintln!(
"Frame {frame_idx}: {} diffs, max_abs={max_abs}",
diffs.len()
);
for (x, y, d) in &diffs {
let vert_edge = (*x / 8) * 8; let vert_edge_hi = vert_edge + 8; let horiz_edge = (*y / 8) * 8;
let horiz_edge_hi = horiz_edge + 8;
let near_vert = if (*x as i32 - vert_edge as i32).abs() <= 3 {
Some(vert_edge)
} else if (vert_edge_hi as i32 - *x as i32).abs() <= 3 {
Some(vert_edge_hi)
} else {
None
};
let near_horiz = if (*y as i32 - horiz_edge as i32).abs() <= 3 {
Some(horiz_edge)
} else if (horiz_edge_hi as i32 - *y as i32).abs() <= 3 {
Some(horiz_edge_hi)
} else {
None
};
eprintln!(" ({x},{y}) diff={d:+} vert_edge={near_vert:?} horiz_edge={near_horiz:?}");
}
}
}