The census bounded WHERE a wrong tie-break could show and said outright that nobody had measured how many of those pairs change a pixel. Measured. `compose_with_order` renders a bundle in a caller-supplied order; the new example renders each screen twice, once derived and once with one tied pair swapped, and diffs. Same-key elements are contiguous under a stable sort on (key, i), so a swap paints nothing else in between. Controlled per entry: swapping an OVERLAPPING pair with DIFFERENT keys moves 36 305 to 771 479 px (max delta 254). Where no such pair is drawn the output says so rather than reporting an uninterpretable zero. * EXTRAS (entries 6/9) and the main menu (5/8): 0 px. The tied ptframe pairs ink ~3 600 px each and share NONE of them -- the 102x132 rect overlap was an artefact of approximating an element as pivot x 2. Blend-independent. * Across all 31 drawable overlapping tied pairs in GP_TITLE, the largest change any of them makes to any channel is 3/255. * Withdrawn: "a wrong tie-break can be wrong by a whole layer". That rested on ptlogo_back2eff5 geometrically containing two other glows. Rendered, the swap moves 6 390 px by max delta 2. Containment is not occlusion when the container is a near-transparent glow, and nobody had rendered it before asserting it. Reach: this measures our compositor's sensitivity to order, not the game's. The zero-shared-ink results hold under any per-pixel blend; the delta<=3 figures assume ours. 13 disc-gated ui_paint_order_disc tests and 114 lib tests pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
280 lines
11 KiB
Rust
280 lines
11 KiB
Rust
//! What does the unknown paint-order TIE-BREAK actually cost, in pixels?
|
|
//!
|
|
//! `ui-paint-order-derived-check.md` bounds *where* a wrong tie-break could
|
|
//! show — 24 overlapping tied pairs across `GP_TITLE` — and says outright that
|
|
//! nobody has measured how many of them change a pixel. Overlap is an upper
|
|
//! bound: two elements can overlap and still composite identically in either
|
|
//! order, if either is transparent where they meet.
|
|
//!
|
|
//! This renders each screen twice — once in the order `compose` derives, once
|
|
//! with one tied pair swapped — and counts the pixels that differ. Same-key
|
|
//! elements are contiguous in the derived order (a stable sort by `(key, i)`),
|
|
//! so swapping two of them paints nothing else in between: the diff is the
|
|
//! tie-break's cost and nothing else.
|
|
//!
|
|
//! Every entry also runs a CONTROL: a swap of two OVERLAPPING elements with
|
|
//! DIFFERENT keys, i.e. a pair whose order the game is known to care about. If
|
|
//! the control diff is zero the instrument cannot see a reorder on this screen
|
|
//! and its zeros mean nothing.
|
|
//!
|
|
//! cargo run -p sylpheed-formats --example tie_break_pixel_cost -- <GP_TITLE.pak>
|
|
use sylpheed_formats::{pak, ui_layout};
|
|
use ui_layout::{ComposeOptions, UiBuild};
|
|
|
|
/// Pixels that differ, and the largest per-channel difference.
|
|
fn diff(a: &[u8], b: &[u8]) -> (usize, u8) {
|
|
let (mut n, mut worst) = (0usize, 0u8);
|
|
for (pa, pb) in a.chunks_exact(4).zip(b.chunks_exact(4)) {
|
|
if pa != pb {
|
|
n += 1;
|
|
for k in 0..4 {
|
|
worst = worst.max(pa[k].abs_diff(pb[k]));
|
|
}
|
|
}
|
|
}
|
|
(n, worst)
|
|
}
|
|
|
|
/// Where does element `ei` actually put ink? Render with it and without it;
|
|
/// the pixels that move are the ones it paints. This is what turns a bare
|
|
/// "0 px differ" into an explained one: bounding boxes can overlap while the
|
|
/// sprites inside them never touch the same pixel.
|
|
fn ink_mask(
|
|
build: &UiBuild,
|
|
bundle: &[u8],
|
|
opts: ComposeOptions,
|
|
order: &[usize],
|
|
base: &[u8],
|
|
ei: usize,
|
|
) -> Vec<bool> {
|
|
let n_el = build.elements.iter().map(|e| e.index).max().unwrap_or(0) + 1;
|
|
let mut vis = vec![true; n_el.max(build.elements.len())];
|
|
vis[build.elements[ei].index] = false;
|
|
let without = ui_layout::compose_with_order(build, bundle, opts, Some(&vis), Some(order));
|
|
base.chunks_exact(4)
|
|
.zip(without.rgba.chunks_exact(4))
|
|
.map(|(x, y)| x != y)
|
|
.collect()
|
|
}
|
|
|
|
fn swapped(order: &[usize], a: usize, b: usize) -> Vec<usize> {
|
|
let mut o = order.to_vec();
|
|
let (pa, pb) = (
|
|
o.iter().position(|&e| e == a).unwrap(),
|
|
o.iter().position(|&e| e == b).unwrap(),
|
|
);
|
|
o.swap(pa, pb);
|
|
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()?;
|
|
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 overlaps(a: &ui_layout::Element, b: &ui_layout::Element) -> bool {
|
|
let (Some(ra), Some(rb)) = (rect(a), rect(b)) else {
|
|
return false;
|
|
};
|
|
(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
|
|
}
|
|
|
|
struct Case {
|
|
name: &'static str,
|
|
opts: ComposeOptions,
|
|
}
|
|
|
|
fn cases() -> Vec<Case> {
|
|
vec![
|
|
Case {
|
|
name: "default (what `screen render` draws)",
|
|
opts: ComposeOptions {
|
|
backdrop: [0, 0, 0, 255],
|
|
..Default::default()
|
|
},
|
|
},
|
|
Case {
|
|
name: "everything on (focus+animated+primitives)",
|
|
opts: ComposeOptions {
|
|
include_focus: true,
|
|
include_animated: true,
|
|
include_primitives: true,
|
|
backdrop: [0, 0, 0, 255],
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
fn tied_overlapping_pairs(build: &UiBuild, bytes: &[u8]) -> Vec<(usize, usize, u32)> {
|
|
let keys: Vec<u32> = build
|
|
.elements
|
|
.iter()
|
|
.map(|e| ui_layout::sprite_layer_key(build, bytes, e).unwrap_or(u32::MAX))
|
|
.collect();
|
|
let mut out = Vec::new();
|
|
for a in 0..keys.len() {
|
|
for b in (a + 1)..keys.len() {
|
|
if keys[a] != keys[b] || keys[a] == u32::MAX {
|
|
continue;
|
|
}
|
|
if overlaps(&build.elements[a], &build.elements[b]) {
|
|
out.push((a, b, keys[a]));
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// 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)> {
|
|
let keys: Vec<u32> = build
|
|
.elements
|
|
.iter()
|
|
.map(|e| ui_layout::sprite_layer_key(build, bytes, e).unwrap_or(u32::MAX))
|
|
.collect();
|
|
let mut best: Option<(i64, usize, usize)> = None;
|
|
for a in 0..keys.len() {
|
|
for b in (a + 1)..keys.len() {
|
|
if keys[a] == keys[b] || keys[a] == u32::MAX || keys[b] == u32::MAX {
|
|
continue;
|
|
}
|
|
if !overlaps(&build.elements[a], &build.elements[b]) {
|
|
continue;
|
|
}
|
|
let (ra, rb) = (rect(&build.elements[a])?, rect(&build.elements[b])?);
|
|
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;
|
|
if best.map_or(true, |(x, _, _)| area > x) {
|
|
best = Some((area, a, b));
|
|
}
|
|
}
|
|
}
|
|
best.map(|(_, a, b)| (a, b))
|
|
}
|
|
|
|
fn main() {
|
|
let path = std::env::args()
|
|
.nth(1)
|
|
.expect("usage: tie_break_pixel_cost <pak>");
|
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
|
let entries: Vec<_> = ar.entries().to_vec();
|
|
println!("# tie-break pixel cost — {path}\n");
|
|
let mut totals = (0usize, 0usize, 0usize); // pairs, changed-a-pixel, controls-dead
|
|
for (i, e) in entries.iter().enumerate() {
|
|
let Ok(bytes) = ar.read(e) else { continue };
|
|
let Some(build) = ui_layout::parse_build(&bytes) else {
|
|
continue;
|
|
};
|
|
let pairs = tied_overlapping_pairs(&build, &bytes);
|
|
if pairs.is_empty() {
|
|
continue;
|
|
}
|
|
println!(
|
|
"entry {i:2} {} elements {} overlapping tied pair(s)",
|
|
build.elements.len(),
|
|
pairs.len()
|
|
);
|
|
let derived = ui_layout::derived_paint_order(&build, &bytes);
|
|
for c in cases() {
|
|
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) {
|
|
Some((a, b)) if drawn.contains(&a) && drawn.contains(&b) => {
|
|
let alt = ui_layout::compose_with_order(
|
|
&build,
|
|
&bytes,
|
|
c.opts,
|
|
None,
|
|
Some(&swapped(&derived, a, b)),
|
|
);
|
|
let (n, w) = diff(&base.rgba, &alt.rgba);
|
|
Some((a, b, n, w))
|
|
}
|
|
_ => None,
|
|
};
|
|
match ctrl {
|
|
Some((a, b, n, w)) if n > 0 => println!(
|
|
" [{}] CONTROL ok: swapping [{a}] {} x [{b}] {} moves {n} px (max Δ {w})",
|
|
c.name, build.elements[a].name, build.elements[b].name
|
|
),
|
|
Some((a, b, _, _)) => {
|
|
totals.2 += 1;
|
|
println!(
|
|
" [{}] CONTROL DEAD: swapping [{a}] {} x [{b}] {} changes NOTHING — \
|
|
zeros below are uninterpretable",
|
|
c.name, build.elements[a].name, build.elements[b].name
|
|
)
|
|
}
|
|
None => {
|
|
totals.2 += 1;
|
|
println!(
|
|
" [{}] CONTROL UNAVAILABLE: no overlapping different-key pair is drawn",
|
|
c.name
|
|
)
|
|
}
|
|
}
|
|
for &(a, b, key) in &pairs {
|
|
let both_drawn = drawn.contains(&a) && drawn.contains(&b);
|
|
if !both_drawn {
|
|
println!(
|
|
" [{}] [{a}] {} x [{b}] {} (key {key}): NOT BOTH DRAWN — unreachable here",
|
|
c.name, build.elements[a].name, build.elements[b].name
|
|
);
|
|
continue;
|
|
}
|
|
let alt = ui_layout::compose_with_order(
|
|
&build,
|
|
&bytes,
|
|
c.opts,
|
|
None,
|
|
Some(&swapped(&derived, a, b)),
|
|
);
|
|
let (n, w) = diff(&base.rgba, &alt.rgba);
|
|
let total = (base.width as usize) * (base.height as usize);
|
|
// Explain the number: how many pixels do the two BOTH paint on?
|
|
// A zero with a large shared-ink count is a real "order does
|
|
// not matter here"; a zero with no shared ink means the
|
|
// bounding boxes overlapped and the sprites did not.
|
|
let ma = ink_mask(&build, &bytes, c.opts, &derived, &base.rgba, a);
|
|
let mb = ink_mask(&build, &bytes, c.opts, &derived, &base.rgba, b);
|
|
let shared = ma.iter().zip(&mb).filter(|(x, y)| **x && **y).count();
|
|
let (ia, ib) = (
|
|
ma.iter().filter(|x| **x).count(),
|
|
mb.iter().filter(|x| **x).count(),
|
|
);
|
|
println!(
|
|
" [{}] [{a}] {} x [{b}] {} (key {key}): {n} px differ ({:.4}% of frame), \
|
|
max Δ {w} | ink {ia} / {ib} px, shared {shared} px",
|
|
c.name,
|
|
build.elements[a].name,
|
|
build.elements[b].name,
|
|
100.0 * n as f64 / total as f64
|
|
);
|
|
if c.name.starts_with("default") {
|
|
totals.0 += 1;
|
|
if n > 0 {
|
|
totals.1 += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
println!();
|
|
}
|
|
println!(
|
|
"default-options summary: {} of {} overlapping tied pairs change at least one pixel; \
|
|
{} dead/unavailable controls",
|
|
totals.1, totals.0, totals.2
|
|
);
|
|
}
|