80 findings, not the 14 the first run showed -- clippy stops at the first failing compilation unit, so `--keep-going` is what makes the list complete. 60 were machine-applicable (`cargo clippy --fix`). The rest by hand: * five descending `sort_by` -> `sort_by_key(Reverse(..))` * `chunks_exact(4)` on both sides of four zips, so the compared items stay `[u8; 4]` rather than one array against one slice * three `type` aliases for the census maps and the captured-quad tuple * `&PathBuf` -> `&Path` in two disc tests * two range loops; one of them keeps `#[allow(needless_range_loop)]` with the reason -- the index is into a map's value, which changes each iteration * the module doc list in `invert_capture` re-indented to markdown's rules * `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed. The sibling test at :162 is the one that asserts an offset; if this one was meant to as well, that is a test change and not a lint fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
339 lines
13 KiB
Rust
339 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.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0.iter()) {
|
|
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.as_chunks::<4>()
|
|
.0
|
|
.iter()
|
|
.zip(without.rgba.as_chunks::<4>().0.iter())
|
|
.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()?,
|
|
};
|
|
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.is_none_or(|(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
|
|
);
|
|
}
|