Finishes #16 in the three places its earlier remedies missed. `tests/`: the last four local `disc_root()` copies now use `tests/common`, and with them goes the one real hardcoded fallback — `ui_keyframe_record_disc.rs` fell back to an absolute path on one machine, which made `unset SYLPHEED_DISC` a no-op there. Control: with the corpus absent that suite now finishes in 0.00s instead of 57.55s, so it skips rather than finding a disc of its own. `examples/`: seventeen examples defaulted to `/disc`, the mount point inside the CI container. Redundant there — `docker/ci/run` sets `SYLPHEED_DISC=/disc` — and wrong everywhere else, where a missing corpus turned into a file-not-found against a path that has never existed on the host. They now name the variable to set, like the other hundred examples already did. `docker/ci/run`: mount `$SYLPHEED_RES3D` and `$SYLPHEED_ISO` alongside the disc. Only the disc was mounted, so an in-container run sat out the res3d and iso suites while looking like a full one — the defect this issue is about, in the runner itself. Measured in the container on this desktop with all three corpora present: 45 suites / 377 passed / 0 failed / 14 ignored, and `sylpheed-corpus-report.txt` now reports PRESENT for all three rather than for the disc alone. Refs #16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
154 lines
5.8 KiB
Rust
154 lines
5.8 KiB
Rust
//! 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<PathBuf> = 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");
|
||
}
|