`the_derived_order_puts_primitives_last_and_that_wipes_screens` asserted that switching primitives on still flattened at least one screen (`wiped_on > 0`), and said to turn `include_primitives` on by default the day it stopped. `cfcda55` made that day arrive — a keyless primitive that would hide the screen is now forced to paint first — and the wire had been red since 2026-08-29, invisible to CI because the runner has no corpus and the suite self-skips. #38 chose option A: keep the default off, and pin the fix instead. The flatness proxy stood in for "paint order is solved", and the fix's own record says that is only partly true — "This does **not** make `include_primitives` safe by default" (docs/re/structures/ui-prm-primitives.md). So: * renamed to `no_build_is_wiped_with_primitives_on` — the old name asserted the bug; it survives in the doc comment for anyone searching from #38; * `wiped_on > 0` -> `assert_eq!(wiped_on, 0)`, with a message saying what a failure now means (`forced_backdrop` regressed) and that it must not be "fixed" by turning primitives off in the test; * `include_primitives` stays `false`; no library code changes. It is the only end-to-end check of the rule: the four tests in `ui_forced_backdrop_disc.rs` pin its mechanics, and none composes with primitives on. Verified with the corpus present, including that it can fail: fix 3 passed 125 builds draw a primitive, 0 wiped forced_backdrop disabled FAILED 36 of 125 wiped — the new message fired restored 3 passed 125 builds, 0 wiped The control's 36 is exactly the "36 builds ... wiped by our own sort" that `cfcda55` and `ui-forced-backdrop.md` report — the guard independently reproduces the fix's own number, so it measures precisely what was repaired. The control was a one-line `return false;` in a scratch copy; the working tree was restored and `git diff` showed only this test file before committing. Closes #38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
259 lines
11 KiB
Rust
259 lines
11 KiB
Rust
//! `.prm` elements are untextured quads, and the declaration table says so.
|
|
//!
|
|
//! Every composite the port draws is missing them: they carry no sprite, so
|
|
//! `compose` skips them. What they are was settled statically on 2026-08-19 —
|
|
//! see `docs/re/structures/ui-prm-primitives.md`. This pins the three facts the
|
|
//! decoding rests on, and is disc-gated like the rest of the corpus.
|
|
|
|
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<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();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// **Bit `0x10` of `kind` means "untextured primitive", and it is exact.**
|
|
///
|
|
/// This is the point of the whole item: the primitive-ness is a *decoded field*,
|
|
/// not a guess from the `.prm` file extension. Over every screen build on the
|
|
/// disc, `kind & 0x10` and a `.prm` name agree with **zero** exceptions in
|
|
/// either direction — so a port can classify without parsing names.
|
|
///
|
|
/// Also pinned here because it is the same sweep: no `.prm` element has a RATC
|
|
/// child of its own name. All 369 of them are payload-less, which is what
|
|
/// "untextured" means concretely — there is nothing in the bundle to draw.
|
|
#[test]
|
|
fn kind_bit_0x10_means_prm_exactly_and_prm_carries_no_payload() {
|
|
let Some(root) = disc_root() else {
|
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
|
return;
|
|
};
|
|
let (mut prm, mut prm_without_bit, mut bit_without_prm, mut with_child) = (0, 0, 0, 0);
|
|
let (mut full_screen, mut sized) = (0, 0);
|
|
for_each_build(&root, |pak, bytes| {
|
|
let Some(build) = ui_layout::parse_build(bytes) else {
|
|
return;
|
|
};
|
|
if build.from_fallback {
|
|
return;
|
|
}
|
|
let kids = ratc::parse(bytes).unwrap_or_default();
|
|
for el in &build.elements {
|
|
let is_prm = el.name.to_ascii_lowercase().ends_with(".prm");
|
|
let has_bit = el.kind & 0x10 != 0;
|
|
if is_prm && !has_bit {
|
|
prm_without_bit += 1;
|
|
eprintln!("{pak}: {} is .prm without bit 0x10", el.name);
|
|
}
|
|
if has_bit && !is_prm {
|
|
bit_without_prm += 1;
|
|
eprintln!("{pak}: {} has bit 0x10 but is not .prm", el.name);
|
|
}
|
|
if !is_prm {
|
|
continue;
|
|
}
|
|
prm += 1;
|
|
if kids.iter().any(|c| c.name == el.name) {
|
|
with_child += 1;
|
|
eprintln!("{pak}: {} has a payload child", el.name);
|
|
}
|
|
sized += 1;
|
|
if (el.pivot_x * 2, el.pivot_y * 2) == (1280, 720) {
|
|
full_screen += 1;
|
|
}
|
|
}
|
|
});
|
|
assert!(
|
|
prm > 300,
|
|
"only {prm} `.prm` elements — the sweep did not run"
|
|
);
|
|
assert_eq!(prm_without_bit, 0, "a `.prm` element without kind bit 0x10");
|
|
assert_eq!(
|
|
bit_without_prm, 0,
|
|
"kind bit 0x10 on something that is not `.prm`"
|
|
);
|
|
assert_eq!(with_child, 0, "a `.prm` element with a payload child");
|
|
// 361 of 369 are exactly the design space; the handful that are not are
|
|
// small coloured quads (844x600, and two degenerate 0x720).
|
|
assert!(
|
|
full_screen * 10 > sized * 9,
|
|
"only {full_screen}/{sized} `.prm` elements are full-screen"
|
|
);
|
|
eprintln!(
|
|
"{prm} `.prm` elements: all kind&0x10, none with a payload, {full_screen} full-screen"
|
|
);
|
|
}
|
|
|
|
/// **A `.prm` fade quad rests at its transparent plateau**, which is what makes
|
|
/// it drawable at all.
|
|
///
|
|
/// The title's `pteff00.prm` is a screen transition: opaque black at t=12,
|
|
/// transparent by t=64, held transparent to t=74, opaque black again on the way
|
|
/// out. What the title screen sits at is the transparent plateau — and this
|
|
/// element is painted **last** in the measured order, so getting it wrong means
|
|
/// a full-screen opaque black quad over everything.
|
|
///
|
|
/// It *was* wrong: the old longest-dwell resting rule picked the opaque
|
|
/// endpoint. This test asserted that defect deliberately until the plateau rule
|
|
/// replaced it (`docs/re/structures/ui-resting-pose.md`); now it guards the fix.
|
|
#[test]
|
|
fn the_title_fade_quad_rests_transparent() {
|
|
let Some(root) = disc_root() else {
|
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
|
return;
|
|
};
|
|
let arc = PakArchive::open(root.join("dat").join("GP_TITLE.pak")).expect("GP_TITLE.pak");
|
|
let mut checked = 0usize;
|
|
for e in arc.entries() {
|
|
let Ok(bytes) = arc.read(e) else { continue };
|
|
let Some(build) = ui_layout::parse_build(&bytes) else {
|
|
continue;
|
|
};
|
|
for el in &build.elements {
|
|
if el.name != "pteff00.prm" || el.keyframes.len() != 4 {
|
|
continue;
|
|
}
|
|
checked += 1;
|
|
let a: Vec<u32> = el.keyframes.iter().map(|k| k.fade >> 24).collect();
|
|
assert_eq!(
|
|
a,
|
|
vec![0xff, 0x00, 0x00, 0xff],
|
|
"the fade group is opaque → transparent → transparent → opaque"
|
|
);
|
|
assert_eq!(
|
|
el.rest().map(|k| k.fade),
|
|
Some(0x0000_0000),
|
|
"the fade quad must rest at its transparent plateau — an opaque \
|
|
answer here paints a full-screen black quad over the title"
|
|
);
|
|
}
|
|
}
|
|
assert!(
|
|
checked >= 3,
|
|
"found {checked} title fade quads, expected several"
|
|
);
|
|
}
|
|
|
|
/// **Drawing the primitives with the derived order swallows screens** — which
|
|
/// is why `ComposeOptions::include_primitives` is off by default.
|
|
///
|
|
/// A primitive has no `T8aD` header, so it has no layer key, and
|
|
/// `derived_paint_order` sorts the keyless to the very end. That is not a
|
|
/// harmless default: `GP_DIALOG`'s `pzeff00.prm` is a **single** keyframe of
|
|
/// opaque black at full screen, and painted last it wipes the build.
|
|
///
|
|
/// The ground truth contradicts *both* simple defaults. On the developer-logo
|
|
/// splash the game paints `palogo_eff0.prm` **first** — it is the black
|
|
/// backdrop. On the title it paints `pteff02.prm` at slot 4, beneath the
|
|
/// wordmark, and `pteff00.prm` **last**, as the fade-out. So the order is real,
|
|
/// per-element, and not derivable from anything decoded so far.
|
|
///
|
|
/// This test measures the damage rather than asserting the feature works, so the
|
|
/// number stays honest and moves when the ordering is solved.
|
|
/// With primitives drawn, no build comes out wiped.
|
|
///
|
|
/// 🔴 THIS USED TO ASSERT THE OPPOSITE. Until issue #38 it was a tripwire named
|
|
/// `the_derived_order_puts_primitives_last_and_that_wipes_screens`, asserting
|
|
/// `wiped_on > 0` — that switching primitives on still flattened at least one
|
|
/// screen — with the instruction to turn `include_primitives` on by default the
|
|
/// day it stopped. `53f8345` made that day arrive: a keyless primitive that
|
|
/// would hide the screen is now forced to paint first (`forced_backdrop`,
|
|
/// `docs/re/structures/ui-forced-backdrop.md`), and the 36 one-colour builds
|
|
/// were "wiped by our own sort". The wire tripped on 2026-08-29 and sat red for
|
|
/// 15 days, because CI has no corpus and the suite self-skips there.
|
|
///
|
|
/// #38 chose to keep the default **off** and pin the fix instead. The flatness
|
|
/// proxy stood in for "paint order is solved", and the fix's own record says
|
|
/// it is only partly solved: *"This does **not** make `include_primitives` safe
|
|
/// by default"* (`docs/re/structures/ui-prm-primitives.md`). An unlisted
|
|
/// primitive that does not hide the screen still sorts last.
|
|
///
|
|
/// So this is now the one end-to-end check of the rule: the four tests in
|
|
/// `ui_forced_backdrop_disc.rs` pin its mechanics, and none composes with
|
|
/// primitives on.
|
|
#[test]
|
|
fn no_build_is_wiped_with_primitives_on() {
|
|
let Some(root) = disc_root() else {
|
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
|
return;
|
|
};
|
|
let opts = |primitives| ui_layout::ComposeOptions {
|
|
backdrop: [0, 0, 0, 255],
|
|
include_primitives: primitives,
|
|
..Default::default()
|
|
};
|
|
let flatness = |c: &ui_layout::ComposedScreen| {
|
|
let mut hist = std::collections::HashMap::<[u8; 3], usize>::new();
|
|
for p in c.rgba.as_chunks::<4>().0 {
|
|
*hist.entry([p[0], p[1], p[2]]).or_default() += 1;
|
|
}
|
|
*hist.values().max().unwrap_or(&0) as f64 / (c.width * c.height) as f64
|
|
};
|
|
let (mut with_prm, mut wiped_on, mut wiped_off) = (0usize, 0usize, 0usize);
|
|
for_each_build(&root, |_pak, bytes| {
|
|
if !ui_layout::is_build(bytes) {
|
|
return;
|
|
}
|
|
let Some(b) = ui_layout::parse_build(bytes) else {
|
|
return;
|
|
};
|
|
if b.from_fallback
|
|
|| !b.elements.iter().any(|e| {
|
|
e.kind & 0x10 != 0
|
|
&& e.sprite.is_none()
|
|
&& e.rest().map(|k| (k.fade >> 24) & 0xff).unwrap_or(0) != 0
|
|
})
|
|
{
|
|
return;
|
|
}
|
|
with_prm += 1;
|
|
if flatness(&ui_layout::compose(&b, bytes, opts(true), None)) > 0.99 {
|
|
wiped_on += 1;
|
|
}
|
|
if flatness(&ui_layout::compose(&b, bytes, opts(false), None)) > 0.99 {
|
|
wiped_off += 1;
|
|
}
|
|
});
|
|
assert!(with_prm > 50, "only {with_prm} builds draw a primitive");
|
|
assert_eq!(
|
|
wiped_off, 0,
|
|
"the DEFAULT composite wipes {wiped_off} builds — primitives are supposed \
|
|
to be off unless asked for"
|
|
);
|
|
assert_eq!(
|
|
wiped_on, 0,
|
|
"{wiped_on} of {with_prm} builds come out >99% one colour with primitives on — \
|
|
a primitive that hides the screen is painting on top again. That is \
|
|
`forced_backdrop` failing to force it first; see \
|
|
docs/re/structures/ui-forced-backdrop.md and issue #38. Do NOT resolve \
|
|
this by turning `include_primitives` off in the test: the default is \
|
|
already off, and this test is the only end-to-end check of the rule."
|
|
);
|
|
eprintln!(
|
|
"{with_prm} builds draw a visible primitive; with primitives on \
|
|
{wiped_on} come out >99% one colour, {wiped_off} by default"
|
|
);
|
|
}
|