//! `.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 = 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 = 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. #[test] fn the_derived_order_puts_primitives_last_and_that_wipes_screens() { 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.chunks_exact(4) { *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!( wiped_on > 0, "no build is wiped with primitives on — the ordering problem this flag \ exists for may be solved, in which case turn it on by default" ); eprintln!( "{with_prm} builds draw a visible primitive; with the derived order \ {wiped_on} of them come out >99% one colour, {wiped_off} by default" ); }