Files
Sylpheed/crates/sylpheed-formats/examples/tie_break_pixel_cost.rs
sylph-decoder 7b5a4aa041 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
2026-08-29 19:35:29 +00:00

333 lines
13 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, 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;
}
Some((kf.x, kf.y, w, h))
}
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
&& (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],
..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], at: Option<u32>) -> 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], at) {
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], at: Option<u32>) -> 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], at) {
continue;
}
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;
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;
};
// 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) at rest{}",
build.elements.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 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, c.opts.at) {
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
);
}