//! A settled screen is one INSTANT, not one hold per element. //! //! `Element::rest()` returns an element's last *hold* keyframe, picked for that //! element alone. For anything that ends the screen settled that is right. For a //! **transient** it is exactly wrong: a two-frame flash's last hold is the flash //! *peak*, so `rest()` leaves it burning for the whole screen. //! //! `GP_TITLE` build 4 is the case that found this. `ptlogo_back2eff1` … `eff5` //! are five staggered flashes — `a=0` until t52, `255` for two frames, `0` again //! two frames later — that sweep left to right across the logo once and are gone //! by t110. Two elements, `ptlogo_back2eff` (t66–238) and `ptlogo_back2` //! (t80–243), then hold for the rest of the screen. `rest()` draws all seven at //! `a=255` simultaneously, and stacking five extra white glows blows the light //! arc out to saturation: against the console capture the arc's mean error is //! 33.22 and 8 581 pixels sit at the clipping level, where the console has 1 459. //! //! `UiBuild::settle_time()` recovers the right instant from the disc alone — the //! midpoint of the longest keyframe-free interval — with no reference to any //! capture. For this build that is t=198, and posing there takes the arc error to //! 11.79 and the clipped count to 1 452 against the console's 1 459. //! //! Argument, controls and the disc-wide census: `docs/re/structures/ui-settle-time.md`. use std::path::PathBuf; use sylpheed_formats::{pak::PakArchive, ratc, ui_layout}; mod common; use common::disc_root; /// The case that found the bug, asserted end to end. #[test] fn a_flash_is_transparent_at_the_settle_time_and_opaque_at_rest() { let Some(root) = disc_root() else { eprintln!("SYLPHEED_DISC unset — skipping"); return; }; let arc = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak"); let bytes = arc.read(&arc.entries()[4]).expect("build 4"); let b = ui_layout::parse_build(&bytes).expect("parse"); let (lo, hi) = b.settle_window().expect("a settle window"); let t = b.settle_time().expect("a settle time"); assert!( hi - lo >= 60, "title's settle window should be a second or more, got {lo}..{hi}" ); assert!( lo < t && t < hi, "settle time {t} must lie inside {lo}..{hi}" ); let alpha = |k: &ui_layout::Keyframe| k.fade >> 24; let mut flashes = 0; for el in &b.elements { let Some(name) = el.name.strip_prefix("ptlogo_back2eff") else { continue; }; // `ptlogo_back2eff.t32` itself holds; only the numbered ones flash. if !name.starts_with(|c: char| c.is_ascii_digit()) { continue; } flashes += 1; let rest = el.rest().expect("a rest pose"); let posed = el.pose_at(t).expect("a posed keyframe"); assert_eq!( alpha(rest), 255, "{}: rest() is expected to report the FLASH PEAK — that is the bug", el.name ); assert_eq!( alpha(&posed), 0, "{} flashes once before t110 and must be gone at the settle time {t}", el.name ); } assert_eq!( flashes, 5, "GP_TITLE build 4 has five numbered back2 flashes" ); // …while the two that genuinely hold are still opaque there. for want in ["ptlogo_back2eff.t32", "ptlogo_back2.t32"] { let el = b.elements.iter().find(|e| e.name == want).expect(want); assert_eq!( alpha(&el.pose_at(t).expect("posed")), 255, "{want} holds across the settle time and must stay opaque" ); } } /// The window is a property of the data, so it must be computable disc-wide /// without panicking, and must be self-consistent wherever it exists. #[test] fn settle_windows_are_self_consistent_disc_wide() { let Some(root) = disc_root() else { eprintln!("SYLPHEED_DISC unset — skipping"); return; }; 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(); let (mut with, mut wide) = (0usize, 0usize); for p in &paks { let Ok(a) = PakArchive::open(p) else { continue }; for e in a.entries() { let Ok(by) = a.read(e) else { continue }; if !ratc::is_ratc(&by) { continue; } let Some(b) = ui_layout::parse_build(&by) else { continue; }; let Some((lo, hi)) = b.settle_window() else { continue; }; with += 1; assert!(lo < hi, "an empty window is not a window: {lo}..{hi}"); let t = b.settle_time().expect("a window implies a time"); assert!((lo..=hi).contains(&t), "settle time {t} outside {lo}..{hi}"); // No element may have a keyframe strictly inside the window — that // is the whole definition, so it is worth asserting rather than // trusting. for el in &b.elements { for k in &el.keyframes { if let Some(kt) = k.time { assert!( kt <= lo || kt >= hi, "{}: keyframe t={kt} lies inside the settle window {lo}..{hi}", el.name ); } } } if hi - lo >= 30 { wide += 1; } } } assert!( with > 1000, "expected >1000 bundles with a settle window, got {with}" ); eprintln!("{with} bundles have a settle window; {wide} are at least 30 units wide"); }