diff --git a/crates/sylpheed-formats/examples/palogo_eff_check.rs b/crates/sylpheed-formats/examples/palogo_eff_check.rs new file mode 100644 index 00000000..7d4c6c16 --- /dev/null +++ b/crates/sylpheed-formats/examples/palogo_eff_check.rs @@ -0,0 +1,40 @@ +//! Are `palogo_gamearts_eff` / `palogo_seta_eff` dwell-FALLBACK cases, or PLATEAU +//! cases? The port agent lists them among `GP_TITLE`'s four visible fallback +//! fires; this census listed only `palogo_sqex_eff` and `palogo_anima_eff`. +//! +//! It matters because the two are different defects. A plateau is a pose the +//! element genuinely HOLDS, and `rest_plateau()` returning it is correct. Only the +//! fallback is the unsound path. +//! cargo run -p sylpheed-formats --example palogo_eff_check +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE"); + for (i, e) in ar.entries().iter().enumerate() { + let Ok(by) = ar.read(e) else { continue }; + let Some(b) = ui_layout::parse_build(&by) else { continue }; + for el in &b.elements { + if !el.name.starts_with("palogo") || !el.name.contains("eff") { continue } + // A single-keyframe element has no gap to maximise, so neither path + // applies and `rest()` trivially returns the only pose. Excluding it + // here matches the census, which filters `len < 2`. + if el.keyframes.len() < 2 { continue } + let plateau = el.keyframes.windows(2).position(|w| { + w[0].x == w[1].x && w[0].y == w[1].y && w[0].scale_x == w[1].scale_x + && w[0].scale_y == w[1].scale_y && w[0].fade == w[1].fade + }); + let ks: Vec = el.keyframes.iter().map(|k| format!( + "{}:a{} {},{} {}%", + k.time.map(|v| v.to_string()).unwrap_or("-".into()), + (k.fade >> 24) & 0xff, k.x, k.y, k.scale_x)).collect(); + let r = el.rest(); + println!("e{i:<3} {:24} kf=[{}]", el.name, ks.join(" ")); + println!(" plateau at pair {:?} -> path: {} rest a={} t={:?}", + plateau, + if plateau.is_some() { "PLATEAU (sound: the pose is held)" } else { "DWELL FALLBACK (unsound)" }, + r.map(|k| (k.fade >> 24) & 0xff).unwrap_or(0), + r.and_then(|k| k.time)); + } + } +} diff --git a/docs/re/data/palogo-eff-plateau-vs-fallback.txt b/docs/re/data/palogo-eff-plateau-vs-fallback.txt new file mode 100644 index 00000000..c407ac83 --- /dev/null +++ b/docs/re/data/palogo-eff-plateau-vs-fallback.txt @@ -0,0 +1,40 @@ +# Are palogo_gamearts_eff / palogo_seta_eff dwell-FALLBACK cases? No -- PLATEAU. +# +# 2026-08-30. The port agent listed them among GP_TITLE's four visible +# fallback fires. This census listed only palogo_sqex_eff and +# palogo_anima_eff, so one of us was wrong. +# +# The distinction is not cosmetic: a plateau is a pose the element genuinely +# HOLDS, and rest_plateau() returning it is CORRECT. Only the dwell fallback +# is the unsound path. +# +e10 palogo_sqex_eff.t32 kf=[0:a0 299,319 100% 15:a255 299,319 100% 30:a212 299,319 100% 45:a0 299,319 100%] + plateau at pair None -> path: DWELL FALLBACK (unsound) rest a=212 t=Some(30) +e11 palogo_gamearts_eff.t32 kf=[0:a0 379,154 100% 15:a255 379,154 100% 30:a255 379,154 100% 45:a0 379,154 100%] + plateau at pair Some(1) -> path: PLATEAU (sound: the pose is held) rest a=255 t=Some(15) +e11 palogo_seta_eff.t32 kf=[0:a0 511,305 100% 15:a255 511,305 100% 30:a255 511,305 100% 45:a0 511,305 100%] + plateau at pair Some(1) -> path: PLATEAU (sound: the pose is held) rest a=255 t=Some(15) +e11 palogo_anima_eff.t32 kf=[0:a0 435,440 100% 15:a255 435,440 100% 30:a212 435,440 100% 45:a0 435,440 100%] + plateau at pair None -> path: DWELL FALLBACK (unsound) rest a=212 t=Some(30) +e13 palogo_sqex_eff.t32 kf=[0:a0 299,319 100% 15:a255 299,319 100% 30:a212 299,319 100% 45:a0 299,319 100%] + plateau at pair None -> path: DWELL FALLBACK (unsound) rest a=212 t=Some(30) +e14 palogo_gamearts_eff.t32 kf=[0:a0 379,154 100% 15:a255 379,154 100% 30:a255 379,154 100% 45:a0 379,154 100%] + plateau at pair Some(1) -> path: PLATEAU (sound: the pose is held) rest a=255 t=Some(15) +e14 palogo_seta_eff.t32 kf=[0:a0 511,305 100% 15:a255 511,305 100% 30:a255 511,305 100% 45:a0 511,305 100%] + plateau at pair Some(1) -> path: PLATEAU (sound: the pose is held) rest a=255 t=Some(15) +e14 palogo_anima_eff.t32 kf=[0:a0 435,440 100% 15:a255 435,440 100% 30:a212 435,440 100% 45:a0 435,440 100%] + plateau at pair None -> path: DWELL FALLBACK (unsound) rest a=212 t=Some(30) +# +# βœ… REFUTATION SUCCEEDS. gamearts_eff and seta_eff hold a=255 at the SAME +# x, y and scale from t=15 to t=30 -- that is a plateau at pair index 1, so +# rest_plateau() handles them and returns t=15, a=255 correctly. They are not +# fallback cases. The census's four (sqex_eff x2, anima_eff x2) stand. +# +# πŸ”΄ BUT THE PORT'S UNDERLYING POINT SURVIVES AND GETS BIGGER. Its rest pose +# for those two really is a=255, the flash's peak -- reached by the SOUND +# path. So 'rest is not a frame to score against a capture' is NOT a +# consequence of the fallback being unsound. A plateau can itself be the held +# peak of a transient, and here four elements hold full-alpha flashes. +# +# The rule therefore covers BOTH paths, and the fallback census understates +# the exposure rather than bounding it. diff --git a/docs/re/structures/ui-resting-pose.md b/docs/re/structures/ui-resting-pose.md index e2852ec2..b103a570 100644 --- a/docs/re/structures/ui-resting-pose.md +++ b/docs/re/structures/ui-resting-pose.md @@ -345,6 +345,34 @@ a frame the game never shows. render posed at `rest` is a legitimate **common reference for comparing two decoders**, and is **not** a frame to score against a capture of the game. +βœ… **Now an oracle number rather than an argument.** The port measured its publisher +splash against the committed capture in both poses: **timeline RMSE 2.17 / 0.01 % +differing**, against **`--pose=rest` RMSE 9.05 / 0.75 %** β€” **75Γ— the differing +area**, on a screen it ships. Nothing it ships is wrong; its settled pose evaluates +`pose_at(hold)` and skips the flashes. + +### πŸ”΄ …and the rule is NOT a consequence of the fallback being unsound + +The port also listed `palogo_gamearts_eff` and `palogo_seta_eff` among the four +visible fallback fires. **They are not** β€” refutation attempt, and it succeeds +([`../data/palogo-eff-plateau-vs-fallback.txt`](../data/palogo-eff-plateau-vs-fallback.txt)): + +| element | keyframes | path | `rest` | +|---|---|---|---| +| `palogo_sqex_eff`, `palogo_anima_eff` | `0:a0 15:a255 30:a212 45:a0` | **dwell fallback** (unsound) | t=30, a=212 | +| `palogo_gamearts_eff`, `palogo_seta_eff` | `0:a0 15:a255 **30:a255** 45:a0` | **plateau** (sound β€” the pose is held) | t=15, **a=255** | + +The second pair holds `a=255` at identical x, y and scale from t=15 to t=30. That +**is** a plateau, `rest_plateau()` handles it, and t=15 is the *correct* answer. The +census's four stand. + +πŸ”΄ **But that makes the port's point stronger, not weaker.** Its rest pose for those +two is the flash's **peak**, reached by the **sound** path. So "a rest render is not +a frame to score against a capture" does **not** follow from the fallback being +unsound β€” **a plateau can itself be the held peak of a transient.** The rule covers +both paths, and the fallback census (2 305 / 1 697) *understates* the exposure +rather than bounding it. + ### πŸ”΄ What this retracts Last iteration I reported the build 7 render difference as evidence **against**