Closes the open half of Q3. `ui-paint-order-derived-check.md` bounded WHERE a wrong tie-break could show -- overlapping same-key pairs -- and said outright that nobody had measured how many change a pixel. At the instant the player sees, the answer is: at most 1 px at max channel difference 1, on the JAPANESE title only (`ptlogo2` x `ptlogo_tm`, 5 px of shared ink). Exactly 0 px on all five port screens. The earlier 24-pair bound was counted at `rest()`, and 10 of the title's 11 overlapping tied pairs are between `ptlogo_back2eff1`..`eff5` -- the five transient flashes from the settle-time finding, transparent on the settled screen. A tie between two invisible elements cannot cost a pixel. Not a knife-edge. Sweeping every keyframe time and every midpoint between keyframe times, the live-pair count is flat across the ENTIRE settle window: 1 on the EN title, 2 on the JP title, 0 on all four loading bundles -- whose tie is live only at t17..t33, during the build-in, which matters because their settle windows are narrow enough to deserve little trust otherwise. Controls: every entry reporting zero also swaps an overlapping DIFFERENT-key pair, which must and does move pixels (25 310 / 268 698 / ~765 000 px). Zeros are explained by shared-ink counts rather than asserted -- the `ptframe` pairs overlap by bounding box and share 0 px of ink. Entries 0/1/12/15 have NO live control and their zeros rest on keyframe data rather than a render; recorded as the weaker claim it is. Refutation attempt on the corpus's "24 overlapping pairs": it SURVIVES as a rest-pose count -- an independent recount reproduces entry 7's 16 exactly. What is overturned is its interpretation as the risk surface. `tie_break_pixel_cost` gains a settle-time case and an alpha/scale filter on its rect test; `tie_cost_over_time` is new. Also strips 611 bytes of captured cargo warnings from the head of the committed tie census. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
77 lines
3.8 KiB
Rust
77 lines
3.8 KiB
Rust
//! How many tied pairs can cost a pixel, as a function of TIME?
|
|
//!
|
|
//! `tie_break_pixel_cost` answers "at one pose". That leaves the answer looking
|
|
//! like it might be a knife-edge: pick a different instant and the count could
|
|
//! jump. This sweeps every keyframe time in the bundle and reports, per entry,
|
|
//! how many same-key pairs are simultaneously **opaque, non-collapsed and
|
|
//! overlapping** — the pairs whose order could possibly matter at that instant.
|
|
//!
|
|
//! The instrument's control is built in: the count at t=0 (nothing has faded in)
|
|
//! and the count at rest must bracket it, and an entry whose count is flat at
|
|
//! zero for the whole sweep would be suspicious rather than reassuring — so the
|
|
//! peak is printed too.
|
|
use sylpheed_formats::{pak, ui_layout};
|
|
use ui_layout::UiBuild;
|
|
|
|
fn rect(e: &ui_layout::Element, t: u32) -> Option<(i32, i32, i32, i32)> {
|
|
let kf = e.pose_at(t)?;
|
|
if kf.fade >> 24 == 0 || kf.scale_x == 0 || kf.scale_y == 0 {
|
|
return None;
|
|
}
|
|
let (w, h) = ((e.pivot_x * 2) as i32, (e.pivot_y * 2) as i32);
|
|
if w == 0 || h == 0 { return None; }
|
|
Some((kf.x, kf.y, w, h))
|
|
}
|
|
|
|
fn live_pairs(b: &UiBuild, bytes: &[u8], keys: &[u32], t: u32) -> usize {
|
|
let mut n = 0;
|
|
for a in 0..keys.len() {
|
|
for c in (a + 1)..keys.len() {
|
|
if keys[a] != keys[c] || keys[a] == u32::MAX { continue }
|
|
let (Some(ra), Some(rb)) = (rect(&b.elements[a], t), rect(&b.elements[c], t)) else { continue };
|
|
if (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0) > 0
|
|
&& (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1) > 0 { n += 1 }
|
|
}
|
|
}
|
|
n
|
|
}
|
|
|
|
fn main() {
|
|
let path = std::env::args().nth(1).expect("usage: tie_cost_over_time <pak>");
|
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
|
println!("# tied pairs that could cost a pixel, over time — {path}\n");
|
|
for (i, e) in ar.entries().to_vec().iter().enumerate() {
|
|
let Ok(by) = ar.read(e) else { continue };
|
|
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
|
let keys: Vec<u32> = b.elements.iter()
|
|
.map(|el| ui_layout::sprite_layer_key(&b, &by, el).unwrap_or(u32::MAX)).collect();
|
|
let mut ts: Vec<u32> = b.elements.iter()
|
|
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time)).collect();
|
|
ts.sort_unstable(); ts.dedup();
|
|
if ts.len() < 2 { continue }
|
|
let last = *ts.last().unwrap();
|
|
// sample every keyframe time AND every midpoint between them
|
|
let mut samples: Vec<u32> = ts.clone();
|
|
for w in ts.windows(2) { samples.push(w[0] + (w[1] - w[0]) / 2); }
|
|
samples.sort_unstable(); samples.dedup();
|
|
let counts: Vec<(u32, usize)> =
|
|
samples.iter().map(|&t| (t, live_pairs(&b, &by, &keys, t))).collect();
|
|
let peak = counts.iter().map(|&(_, n)| n).max().unwrap_or(0);
|
|
if peak == 0 { continue }
|
|
let Some((lo, hi)) = b.settle_window() else { continue };
|
|
let st = b.settle_time().unwrap();
|
|
let at_settle = live_pairs(&b, &by, &keys, st);
|
|
// the whole plateau, not just its midpoint
|
|
let plateau: Vec<usize> =
|
|
(lo..=hi).step_by(((hi - lo).max(1) / 8).max(1) as usize)
|
|
.map(|t| live_pairs(&b, &by, &keys, t)).collect();
|
|
let pmax = plateau.iter().copied().max().unwrap_or(0);
|
|
println!("entry {i:2} peak {peak} live pair(s) over t=0..{last} \
|
|
settle window [{lo},{hi}] at t={st}: {at_settle} ACROSS THE WHOLE WINDOW: max {pmax}");
|
|
let busy: Vec<String> = counts.iter().filter(|&&(_, n)| n > 0)
|
|
.map(|&(t, n)| format!("t{t}:{n}")).collect();
|
|
if busy.len() <= 24 { println!(" live only at {}", busy.join(" ")); }
|
|
else { println!(" live at {} of {} sampled instants", busy.len(), counts.len()); }
|
|
}
|
|
}
|