//! Is the bundle header's `+0x08` really an animation **duration in frames**? //! //! The header sweep found `+0x04` takes only `0x3C0000` and `0x1E0000` — exactly //! `60.0` and `30.0` in 16.16 — and `+0x08` takes 30 / 1200 / 120 / 60, which //! *looks* like a frame rate and a length. That reading came from the values //! alone, and this checks it against something the file states independently: //! the **keyframe times** in the placement region. //! //! If `+0x08` is the length of the bundle's animation, the largest keyframe time //! in the bundle should never exceed it, and should reach it on bundles that //! animate all the way through. If instead the times run past it, the reading is //! wrong and the amber has to come off the other way. use std::collections::HashMap; use std::path::{Path, PathBuf}; use sylpheed_formats::{pak::PakArchive, ratc, ui_layout}; mod common; use common::disc_root; fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { let mut paks: Vec = std::fs::read_dir(root.join("dat")) .expect("dat/") .flatten() .map(|e| e.path()) .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")) .collect(); paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); let Ok(arc) = PakArchive::open(p) else { continue; }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ratc::is_ratc(&bytes) { f(&name, &bytes); } } } } fn be32(b: &[u8], at: usize) -> u32 { u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]) } #[test] fn header_0x08_against_the_keyframe_times() { let Some(root) = disc_root() else { eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)"); return; }; let (mut animated, mut within, mut exact, mut over) = (0usize, 0, 0, 0); let mut worst: Vec = Vec::new(); // How the ratio max_time / header_0x08 is distributed, in tenths. let mut ratio: HashMap = HashMap::new(); // Does the rate word co-vary with anything? let mut by_rate: HashMap = HashMap::new(); // rate -> (bundles, max seen 0x08) // The 16.16 frame-rate reading rests on twelve bundles at 30.0. If those are // VARIANTS of 60.0 bundles - same elements, different rate - the reading // gains a real discriminator; if they are unrelated one-offs it does not. let mut odd_rate: Vec = Vec::new(); for_each_build(&root, |pak, bytes| { if bytes.len() < 0x20 { return; } let Some(build) = ui_layout::parse_build(bytes) else { return; }; if build.from_fallback { return; } let dur = be32(bytes, 0x08); let rate = be32(bytes, 0x04); let e = by_rate.entry(rate).or_default(); e.0 += 1; e.1 = e.1.max(dur); if rate != 0x3C_0000 && odd_rate.len() < 20 { let names: Vec<&str> = build .elements .iter() .map(|e| e.name.as_str()) .take(6) .collect(); odd_rate.push(format!( "{pak}: rate {rate:#x} dur {dur} elements {} {:?}", build.elements.len(), names )); } let max_time = build .elements .iter() .flat_map(|el| el.keyframes.iter()) .filter_map(|k| k.time) .max(); let Some(max_time) = max_time else { return }; if dur == 0 { return; } animated += 1; if max_time <= dur { within += 1; if max_time == dur { exact += 1; } } else { over += 1; if worst.len() < 10 { worst.push(format!("{pak}: max keyframe {max_time} > header {dur}")); } } let r = ((max_time as f64 / dur as f64) * 10.0).round() as u32; *ratio.entry(r.min(30)).or_default() += 1; }); eprintln!("bundles with keyframe times and a non-zero +0x08: {animated}"); eprintln!(" max keyframe time <= +0x08: {within} of which EXACTLY equal: {exact}"); eprintln!(" max keyframe time > +0x08: {over}"); let mut r: Vec<_> = ratio.iter().collect(); r.sort(); eprintln!(" ratio max_time/+0x08 (tenths -> bundles): {r:?}"); let mut br: Vec<_> = by_rate.iter().collect(); br.sort(); eprintln!(" +0x04 rate word -> (bundles, largest +0x08 seen): {br:?}"); for w in &worst { eprintln!(" over: {w}"); } for o in &odd_rate { eprintln!(" non-60 rate: {o}"); } assert!(animated > 0, "no animated bundles — the sweep is broken"); // MEASURED 2026-08-24. +0x08 bounds the keyframe times in EVERY one of the // 2313 bundles that have both, and 444 of them reach it exactly. The // spread-out ratio histogram is what rules out the boring explanation: a // large unrelated constant would bound everything too, but then the ratios // would pile up near zero instead of peaking at 1.0. assert_eq!(over, 0, "a keyframe time runs past the header's +0x08"); assert!( exact > 400, "the bound is never attained — it may be unrelated" ); let near_one = ratio.get(&10).copied().unwrap_or(0); let near_zero = ratio.get(&0).copied().unwrap_or(0); assert!( near_one > near_zero * 4, "the max_time/+0x08 ratio does not peak at 1.0 — the bound may be vacuous" ); }