Files
Sylpheed/crates/sylpheed-formats/tests/ui_forced_backdrop_disc.rs
sim cc9392bde4 test: one disc resolver, no machine-specific defaults, all three corpora in the container
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>
2026-09-17 22:18:59 +02:00

226 lines
8.9 KiB
Rust

//! An opaque full-screen primitive cannot paint above what it would hide.
//!
//! A keyless primitive has no layer key, and `implied_layer_key` records the
//! handful whose position was measured in the running game. For one class the
//! file settles it without a measurement: an element covering the screen and
//! fully opaque at some instant cannot paint above anything visible then, or the
//! screen is blank. Where that set is *every* other element, the position is
//! forced to first.
//!
//! The port found this by contradiction on `build_12`/`build_15`, which its
//! renderer composited to solid black at every instant of their declared life.
//!
//! Argument, census and reach: `docs/re/structures/ui-forced-backdrop.md`.
use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
mod common;
use common::disc_root;
fn build(ar: &PakArchive, i: usize) -> (Vec<u8>, ui_layout::UiBuild) {
let by = ar.read(&ar.entries()[i]).expect("entry");
let b = ui_layout::parse_build(&by).expect("parse");
(by, b)
}
fn el<'a>(b: &'a ui_layout::UiBuild, name: &str) -> &'a ui_layout::Element {
b.elements.iter().find(|e| e.name == name).expect(name)
}
/// The two controls are measured orders from the running game. The rule has to
/// reproduce one and permit the other, or it is not measuring occlusion.
#[test]
fn the_rule_reproduces_both_measured_primitives() {
let Some(root) = disc_root() else {
eprintln!("SYLPHEED_DISC unset — skipping");
return;
};
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
// `palogo_eff0.prm` is MEASURED painting first. Named like an overlay, so a
// name-based rule gets it wrong; occlusion gets it right.
let (_, splash) = build(&ar, 11);
assert!(
ui_layout::forced_backdrop(&splash, el(&splash, "palogo_eff0.prm")),
"the developer splash's backdrop is measured FIRST and must come out forced"
);
// `pteff00.prm` is MEASURED painting last. It is opaque only at its screen's
// entry and exit, so the rule must NOT force it down.
for entry in [4usize, 5] {
let (_, b) = build(&ar, entry);
assert!(
!ui_layout::forced_backdrop(&b, el(&b, "pteff00.prm")),
"entry {entry}: pteff00.prm is measured painting LAST and must stay permitted on top"
);
}
}
/// The case that prompted it: the loading screens.
#[test]
fn the_loading_screens_backdrop_sorts_first() {
let Some(root) = disc_root() else {
eprintln!("SYLPHEED_DISC unset — skipping");
return;
};
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
for entry in [12usize, 15] {
let (by, b) = build(&ar, entry);
let prim = el(&b, "pgloading_eff00.prm");
assert!(ui_layout::forced_backdrop(&b, prim), "entry {entry}");
let order = ui_layout::derived_paint_order(&b, &by);
assert_eq!(
order.first().copied(),
Some(prim.index),
"entry {entry}: the backdrop must be painted first, not last"
);
}
}
/// 🔴 The rule quantifies over "every instant the primitive is opaque" and "every
/// element visible then", so both halves depend on where the timeline ends and on
/// what an element does after its own last keyframe. **The hold is not a
/// convenience: a measured order requires it.**
///
/// `palogo_eff0.prm` is a SINGLE keyframe at t=0. If an element counted as *gone*
/// after its last keyframe, the splash's backdrop would exist for one instant, no
/// other element would be up yet, and the rule would call it free — against the
/// order measured in the running game, which paints it first.
///
/// Disc-wide the choice decides **72 of 130** verdicts, so this is the load-bearing
/// half of the rule. (Using the header's declared `+0x08` as the span instead of
/// the elements' maximum changes **0**.)
#[test]
fn the_hold_after_a_final_keyframe_is_required_by_a_measured_order() {
let Some(root) = disc_root() else {
eprintln!("SYLPHEED_DISC unset — skipping");
return;
};
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
for entry in [10usize, 11] {
let (_, b) = build(&ar, entry);
let prim = el(&b, "palogo_eff0.prm");
assert_eq!(
prim.keyframes.len(),
1,
"entry {entry}: the case rests on it being static"
);
// With the hold — what `pose_at` does, and what the game does.
assert!(
ui_layout::forced_backdrop(&b, prim),
"entry {entry}: measured painting FIRST, so the rule must force it"
);
// Without it, spelled out here rather than imported, so the test states
// the counterfactual it is pinning.
let tmax = b
.elements
.iter()
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
.max()
.unwrap_or(0);
let last =
|e: &ui_layout::Element| e.keyframes.iter().filter_map(|k| k.time).max().unwrap_or(0);
let alpha_no_hold = |e: &ui_layout::Element, t: u32| -> u32 {
if t > last(e) {
0
} else {
e.pose_at(t).map(|k| k.fade >> 24).unwrap_or(0)
}
};
let opaque: Vec<u32> = (0..=tmax)
.filter(|&t| alpha_no_hold(prim, t) == 255)
.collect();
let others: Vec<_> = b
.elements
.iter()
.filter(|o| o.index != prim.index)
.collect();
let below = others
.iter()
.filter(|o| opaque.iter().any(|&t| alpha_no_hold(o, t) > 0))
.count();
assert_ne!(
below,
others.len(),
"entry {entry}: without the hold this element would come out FREE — which is \
why the hold is load-bearing rather than incidental"
);
}
}
/// Disc-wide: the rule must fire on a real population and never on something it
/// cannot occlude.
#[test]
fn forced_backdrops_are_full_screen_and_plentiful() {
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 forced, mut prm, mut tbm) = (0usize, 0usize, 0usize);
for p in &paks {
let Ok(ar) = PakArchive::open(p) else {
continue;
};
for e in ar.entries() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) {
continue;
}
let Some(b) = ui_layout::parse_build(&by) else {
continue;
};
for element in &b.elements {
if !ui_layout::forced_backdrop(&b, element) {
continue;
}
forced += 1;
if element.name.ends_with(".prm") {
prm += 1
} else if element.name.ends_with(".tbm") {
tbm += 1
}
// 🔴 Untextured only. This assertion caught the rule's real
// limit: applied to `.t32` sprites it claimed 22 of them must
// sort first, against their own layer keys — a sprite's element
// alpha says nothing about its texture's coverage.
assert!(
element.sprite.is_none(),
"{}: a textured sprite cannot be judged to occlude by element alpha",
element.name
);
assert!(
element.pivot_x * 2 >= b.design_w && element.pivot_y * 2 >= b.design_h,
"{}: a quad that does not cover the screen cannot occlude it",
element.name
);
}
}
}
assert!(forced > 50, "expected a real population, got {forced}");
eprintln!("{forced} keyless primitives have their position forced to first");
// 🔴 Pin the split, so anyone tightening this rule sees what it would cost.
// Only the `.prm` half is DECODED: a solid colour quad's fade IS its pixel, so
// opacity and coverage are the same fact. Every `.tbm` in the set carries fade
// `ffffffff` — a white SOLID quad painted first would make the screen white, so
// they are textured, and element alpha does not establish their coverage.
// Their verdicts are kept because restricting to `.prm` would send eleven
// screens' backgrounds back to last, which is the bug this rule fixed.
assert!(
prm >= 40 && tbm >= 30,
"expected roughly 42 .prm / 38 .tbm forced instances, got {prm} / {tbm} — \
if this moved, re-read the self-refutation section of ui-forced-backdrop.md"
);
}