re: the paint-order tie-break costs one pixel, on one screen we do not ship
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
This commit is contained in:
@@ -67,10 +67,21 @@ fn swapped(order: &[usize], a: usize, b: usize) -> Vec<usize> {
|
||||
o
|
||||
}
|
||||
|
||||
/// The element's on-screen rect at rest, the same approximation the tie census
|
||||
/// uses: the declared pivot doubled, placed at the resting keyframe.
|
||||
fn rect(e: &ui_layout::Element) -> Option<(i32, i32, i32, i32)> {
|
||||
let kf = e.rest()?;
|
||||
/// The element's on-screen rect, the same approximation the tie census uses:
|
||||
/// the declared pivot doubled, placed at the keyframe. `at` selects the pose —
|
||||
/// `None` is `rest()`, which is where the original census was computed.
|
||||
///
|
||||
/// 🔴 An element that is TRANSPARENT at the chosen pose gets no rect at all. A
|
||||
/// tie involving something invisible cannot cost a pixel, and counting it as an
|
||||
/// overlap is what made the original census an upper bound rather than a cost.
|
||||
fn rect(e: &ui_layout::Element, at: Option<u32>) -> Option<(i32, i32, i32, i32)> {
|
||||
let kf = match at {
|
||||
Some(t) => e.pose_at(t)?,
|
||||
None => e.rest()?.clone(),
|
||||
};
|
||||
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;
|
||||
@@ -78,8 +89,8 @@ fn rect(e: &ui_layout::Element) -> Option<(i32, i32, i32, i32)> {
|
||||
Some((kf.x, kf.y, w, h))
|
||||
}
|
||||
|
||||
fn overlaps(a: &ui_layout::Element, b: &ui_layout::Element) -> bool {
|
||||
let (Some(ra), Some(rb)) = (rect(a), rect(b)) else {
|
||||
fn overlaps(a: &ui_layout::Element, b: &ui_layout::Element, at: Option<u32>) -> bool {
|
||||
let (Some(ra), Some(rb)) = (rect(a, at), rect(b, at)) else {
|
||||
return false;
|
||||
};
|
||||
(ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0) > 0
|
||||
@@ -107,12 +118,27 @@ fn cases() -> Vec<Case> {
|
||||
include_animated: true,
|
||||
include_primitives: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
// 🔴 The two cases above pose at `rest()`, which is each element's last
|
||||
// hold picked independently — so they draw transients that the settled
|
||||
// screen does not have (`docs/re/structures/ui-settle-time.md`). A tie
|
||||
// between two elements that are transparent at the settle time cannot
|
||||
// cost a pixel on the screen the player sees, however much their rects
|
||||
// overlap at rest. `at` is filled in per entry.
|
||||
Case {
|
||||
name: "AT THE SETTLE TIME (what the player sees)",
|
||||
opts: ComposeOptions {
|
||||
backdrop: [0, 0, 0, 255],
|
||||
at: Some(0), // replaced per entry
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn tied_overlapping_pairs(build: &UiBuild, bytes: &[u8]) -> Vec<(usize, usize, u32)> {
|
||||
fn tied_overlapping_pairs(build: &UiBuild, bytes: &[u8], at: Option<u32>) -> Vec<(usize, usize, u32)> {
|
||||
let keys: Vec<u32> = build
|
||||
.elements
|
||||
.iter()
|
||||
@@ -124,7 +150,7 @@ fn tied_overlapping_pairs(build: &UiBuild, bytes: &[u8]) -> Vec<(usize, usize, u
|
||||
if keys[a] != keys[b] || keys[a] == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
if overlaps(&build.elements[a], &build.elements[b]) {
|
||||
if overlaps(&build.elements[a], &build.elements[b], at) {
|
||||
out.push((a, b, keys[a]));
|
||||
}
|
||||
}
|
||||
@@ -134,7 +160,7 @@ fn tied_overlapping_pairs(build: &UiBuild, bytes: &[u8]) -> Vec<(usize, usize, u
|
||||
|
||||
/// A pair the game's own order DOES separate: overlapping, different keys.
|
||||
/// Used as the control — swapping it must move pixels.
|
||||
fn control_pair(build: &UiBuild, bytes: &[u8]) -> Option<(usize, usize)> {
|
||||
fn control_pair(build: &UiBuild, bytes: &[u8], at: Option<u32>) -> Option<(usize, usize)> {
|
||||
let keys: Vec<u32> = build
|
||||
.elements
|
||||
.iter()
|
||||
@@ -146,10 +172,10 @@ fn control_pair(build: &UiBuild, bytes: &[u8]) -> Option<(usize, usize)> {
|
||||
if keys[a] == keys[b] || keys[a] == u32::MAX || keys[b] == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
if !overlaps(&build.elements[a], &build.elements[b]) {
|
||||
if !overlaps(&build.elements[a], &build.elements[b], at) {
|
||||
continue;
|
||||
}
|
||||
let (ra, rb) = (rect(&build.elements[a])?, rect(&build.elements[b])?);
|
||||
let (ra, rb) = (rect(&build.elements[a], at)?, rect(&build.elements[b], at)?);
|
||||
let ox = ((ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0)) as i64;
|
||||
let oy = ((ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1)) as i64;
|
||||
let area = ox * oy;
|
||||
@@ -174,22 +200,49 @@ fn main() {
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let pairs = tied_overlapping_pairs(&build, &bytes);
|
||||
if pairs.is_empty() {
|
||||
// The census pairs are still computed at rest, so the report can say
|
||||
// how many of THOSE survive posing at the settle time.
|
||||
let pairs_at_rest = tied_overlapping_pairs(&build, &bytes, None);
|
||||
if pairs_at_rest.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let settle = build.settle_time();
|
||||
println!(
|
||||
"entry {i:2} {} elements {} overlapping tied pair(s)",
|
||||
"entry {i:2} {} elements {} overlapping tied pair(s) at rest{}",
|
||||
build.elements.len(),
|
||||
pairs.len()
|
||||
pairs_at_rest.len(),
|
||||
match (settle, build.settle_window()) {
|
||||
(Some(t), Some((lo, hi))) => format!(" settle t={t} (window {} units)", hi - lo),
|
||||
_ => " NO SETTLE WINDOW".to_string(),
|
||||
}
|
||||
);
|
||||
let derived = ui_layout::derived_paint_order(&build, &bytes);
|
||||
for c in cases() {
|
||||
for mut c in cases() {
|
||||
// The settle case is a no-op on a bundle that never settles.
|
||||
if c.opts.at.is_some() {
|
||||
match settle {
|
||||
Some(t) => c.opts.at = Some(t),
|
||||
None => {
|
||||
println!(" [{}] SKIPPED: no settle window", c.name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let pairs = tied_overlapping_pairs(&build, &bytes, c.opts.at);
|
||||
if pairs.len() != pairs_at_rest.len() {
|
||||
println!(
|
||||
" [{}] 🔴 {} of the {} tied pairs are GONE at this pose (an element is \
|
||||
transparent or collapsed there) — they cannot cost a pixel",
|
||||
c.name,
|
||||
pairs_at_rest.len() - pairs.len(),
|
||||
pairs_at_rest.len()
|
||||
);
|
||||
}
|
||||
let base = ui_layout::compose_with_order(&build, &bytes, c.opts, None, Some(&derived));
|
||||
let drawn: std::collections::HashSet<usize> = base.drawn.iter().copied().collect();
|
||||
// Control first. An instrument that cannot see a reorder it is
|
||||
// supposed to see makes every zero below meaningless.
|
||||
let ctrl = match control_pair(&build, &bytes) {
|
||||
let ctrl = match control_pair(&build, &bytes, c.opts.at) {
|
||||
Some((a, b)) if drawn.contains(&a) && drawn.contains(&b) => {
|
||||
let alt = ui_layout::compose_with_order(
|
||||
&build,
|
||||
|
||||
76
crates/sylpheed-formats/examples/tie_cost_over_time.rs
Normal file
76
crates/sylpheed-formats/examples/tie_cost_over_time.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! 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()); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user