diff --git a/crates/sylpheed-formats/examples/tie_break_pixel_cost.rs b/crates/sylpheed-formats/examples/tie_break_pixel_cost.rs new file mode 100644 index 00000000..a264b5be --- /dev/null +++ b/crates/sylpheed-formats/examples/tie_break_pixel_cost.rs @@ -0,0 +1,279 @@ +//! 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 -- +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 { + 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 { + 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 { + 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 = 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 = 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 "); + 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 = 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 + ); +} diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 3a000717..116e6ea1 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -924,6 +924,23 @@ pub fn compose( bundle: &[u8], opts: ComposeOptions, visible: Option<&[bool]>, +) -> ComposedScreen { + compose_with_order(build, bundle, opts, visible, None) +} + +/// `compose`, with the paint order supplied by the caller. +/// +/// The only reason this exists is to *measure* what a paint order costs: render +/// a screen twice, once with the order `compose` would pick and once with two +/// elements swapped, and diff the pixels. `order` is a permutation of element +/// indices, first painted first; `None` means "whatever `compose` would use". +/// Nothing in the normal render path passes anything but `None`. +pub fn compose_with_order( + build: &UiBuild, + bundle: &[u8], + opts: ComposeOptions, + visible: Option<&[bool]>, + order_override: Option<&[usize]>, ) -> ComposedScreen { let (w, h) = (build.design_w, build.design_h); // A dim backdrop stands in for the PRMD dim-quad + the live 3D scene behind @@ -947,8 +964,10 @@ pub fn compose( // same-layer-key ties, two being total occlusions. Of the port's five // screens only `EXTRAS` rests on a derived order with ties: 15 tied pairs, // 2 overlapping. See docs/re/structures/ui-paint-order-derived-check.md. - let order: Vec = - measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle)); + let order: Vec = match order_override { + Some(o) => o.to_vec(), + None => measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle)), + }; for &ei in &order { let Some(el) = build.elements.get(ei) else { continue; diff --git a/docs/re/data/paint-order-tie-pixel-cost.txt b/docs/re/data/paint-order-tie-pixel-cost.txt new file mode 100644 index 00000000..716ff617 --- /dev/null +++ b/docs/re/data/paint-order-tie-pixel-cost.txt @@ -0,0 +1,125 @@ +# tie-break pixel cost — /disc/dat/GP_TITLE.pak + +entry 0 7 elements 1 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL UNAVAILABLE: no overlapping different-key pair is drawn + [default (what `screen render` draws)] [5] pgloading_eff01.t32 x [6] pgloading_eff02.t32 (key 49408): 1761 px differ (0.1911% of frame), max Δ 1 | ink 30864 / 21163 px, shared 3298 px + [everything on (focus+animated+primitives)] CONTROL ok: swapping [4] pgloading_loop4.rat x [6] pgloading_eff02.t32 moves 3654 px (max Δ 23) + [everything on (focus+animated+primitives)] [5] pgloading_eff01.t32 x [6] pgloading_eff02.t32 (key 49408): 1610 px differ (0.1747% of frame), max Δ 1 | ink 29173 / 21017 px, shared 3139 px + +entry 1 7 elements 1 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL UNAVAILABLE: no overlapping different-key pair is drawn + [default (what `screen render` draws)] [5] pgloading_eff01.t32 x [6] pgloading_eff02.t32 (key 49408): 1761 px differ (0.1911% of frame), max Δ 1 | ink 30864 / 21163 px, shared 3298 px + [everything on (focus+animated+primitives)] CONTROL ok: swapping [4] pgloading_loop4.rat x [6] pgloading_eff02.t32 moves 3654 px (max Δ 23) + [everything on (focus+animated+primitives)] [5] pgloading_eff01.t32 x [6] pgloading_eff02.t32 (key 49408): 1610 px differ (0.1747% of frame), max Δ 1 | ink 29173 / 21017 px, shared 3139 px + +entry 4 24 elements 13 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL ok: swapping [10] pteff04.t32 x [18] ptlogo_back2eff5.t32 moves 36305 px (max Δ 254) + [default (what `screen render` draws)] [2] ptlogo1.t32 x [4] ptlogo1.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [3] ptlogo2.t32 x [5] ptlogo2.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [11] ptloop01.rat x [12] ptloop02.rat (key 32784): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [14] ptlogo_back2eff1.t32 x [15] ptlogo_back2eff2.t32 (key 32899): 295 px differ (0.0320% of frame), max Δ 1 | ink 2483 / 6547 px, shared 2483 px + [default (what `screen render` draws)] [14] ptlogo_back2eff1.t32 x [16] ptlogo_back2eff3.t32 (key 32899): 861 px differ (0.0934% of frame), max Δ 2 | ink 2483 / 9698 px, shared 2483 px + [default (what `screen render` draws)] [14] ptlogo_back2eff1.t32 x [17] ptlogo_back2eff4.t32 (key 32899): 1555 px differ (0.1687% of frame), max Δ 2 | ink 2483 / 13926 px, shared 2483 px + [default (what `screen render` draws)] [14] ptlogo_back2eff1.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 6645 px differ (0.7210% of frame), max Δ 3 | ink 2483 / 22834 px, shared 2398 px + [default (what `screen render` draws)] [15] ptlogo_back2eff2.t32 x [16] ptlogo_back2eff3.t32 (key 32899): 420 px differ (0.0456% of frame), max Δ 1 | ink 6547 / 9698 px, shared 6547 px + [default (what `screen render` draws)] [15] ptlogo_back2eff2.t32 x [17] ptlogo_back2eff4.t32 (key 32899): 1209 px differ (0.1312% of frame), max Δ 2 | ink 6547 / 13926 px, shared 6547 px + [default (what `screen render` draws)] [15] ptlogo_back2eff2.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 6641 px differ (0.7206% of frame), max Δ 2 | ink 6547 / 22834 px, shared 6360 px + [default (what `screen render` draws)] [16] ptlogo_back2eff3.t32 x [17] ptlogo_back2eff4.t32 (key 32899): 584 px differ (0.0634% of frame), max Δ 1 | ink 9698 / 13926 px, shared 9698 px + [default (what `screen render` draws)] [16] ptlogo_back2eff3.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 6390 px differ (0.6934% of frame), max Δ 2 | ink 9698 / 22834 px, shared 9462 px + [default (what `screen render` draws)] [17] ptlogo_back2eff4.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 5516 px differ (0.5985% of frame), max Δ 1 | ink 13926 / 22834 px, shared 13480 px + [everything on (focus+animated+primitives)] CONTROL ok: swapping [10] pteff04.t32 x [18] ptlogo_back2eff5.t32 moves 860461 px (max Δ 254) + [everything on (focus+animated+primitives)] [2] ptlogo1.t32 x [4] ptlogo1.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [everything on (focus+animated+primitives)] [3] ptlogo2.t32 x [5] ptlogo2.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [everything on (focus+animated+primitives)] [11] ptloop01.rat x [12] ptloop02.rat (key 32784): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + [everything on (focus+animated+primitives)] [14] ptlogo_back2eff1.t32 x [15] ptlogo_back2eff2.t32 (key 32899): 280 px differ (0.0304% of frame), max Δ 1 | ink 2516 / 6589 px, shared 2516 px + [everything on (focus+animated+primitives)] [14] ptlogo_back2eff1.t32 x [16] ptlogo_back2eff3.t32 (key 32899): 811 px differ (0.0880% of frame), max Δ 2 | ink 2516 / 9754 px, shared 2516 px + [everything on (focus+animated+primitives)] [14] ptlogo_back2eff1.t32 x [17] ptlogo_back2eff4.t32 (key 32899): 1527 px differ (0.1657% of frame), max Δ 2 | ink 2516 / 14072 px, shared 2516 px + [everything on (focus+animated+primitives)] [14] ptlogo_back2eff1.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 6586 px differ (0.7146% of frame), max Δ 3 | ink 2516 / 22970 px, shared 2419 px + [everything on (focus+animated+primitives)] [15] ptlogo_back2eff2.t32 x [16] ptlogo_back2eff3.t32 (key 32899): 395 px differ (0.0429% of frame), max Δ 1 | ink 6589 / 9754 px, shared 6589 px + [everything on (focus+animated+primitives)] [15] ptlogo_back2eff2.t32 x [17] ptlogo_back2eff4.t32 (key 32899): 1204 px differ (0.1306% of frame), max Δ 2 | ink 6589 / 14072 px, shared 6589 px + [everything on (focus+animated+primitives)] [15] ptlogo_back2eff2.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 6567 px differ (0.7126% of frame), max Δ 2 | ink 6589 / 22970 px, shared 6397 px + [everything on (focus+animated+primitives)] [16] ptlogo_back2eff3.t32 x [17] ptlogo_back2eff4.t32 (key 32899): 599 px differ (0.0650% of frame), max Δ 1 | ink 9754 / 14072 px, shared 9754 px + [everything on (focus+animated+primitives)] [16] ptlogo_back2eff3.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 6333 px differ (0.6872% of frame), max Δ 2 | ink 9754 / 22970 px, shared 9512 px + [everything on (focus+animated+primitives)] [17] ptlogo_back2eff4.t32 x [18] ptlogo_back2eff5.t32 (key 32899): 5427 px differ (0.5889% of frame), max Δ 1 | ink 14072 / 22970 px, shared 13620 px + +entry 5 16 elements 2 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL ok: swapping [1] ptbase.t32 x [2] pteff05.t32 moves 764030 px (max Δ 67) + [default (what `screen render` draws)] [3] ptloop01.rat x [4] ptloop02.rat (key 32784): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [6] ptframe1.t32 x [7] ptframe2.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 4783 / 5297 px, shared 0 px + [everything on (focus+animated+primitives)] CONTROL ok: swapping [1] ptbase.t32 x [2] pteff05.t32 moves 725164 px (max Δ 50) + [everything on (focus+animated+primitives)] [3] ptloop01.rat x [4] ptloop02.rat (key 32784): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + [everything on (focus+animated+primitives)] [6] ptframe1.t32 x [7] ptframe2.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 4781 / 5305 px, shared 0 px + +entry 6 18 elements 2 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL ok: swapping [11] ptbase.t32 x [12] pteff05.t32 moves 761600 px (max Δ 67) + [default (what `screen render` draws)] [0] ptframe3.t32 x [1] ptframe4.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 3646 / 3584 px, shared 0 px + [default (what `screen render` draws)] [14] ptloop01.rat x [15] ptloop02.rat (key 32784): NOT BOTH DRAWN — unreachable here + [everything on (focus+animated+primitives)] CONTROL ok: swapping [11] ptbase.t32 x [12] pteff05.t32 moves 721144 px (max Δ 50) + [everything on (focus+animated+primitives)] [0] ptframe3.t32 x [1] ptframe4.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 3646 / 3584 px, shared 0 px + [everything on (focus+animated+primitives)] [14] ptloop01.rat x [15] ptloop02.rat (key 32784): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + +entry 7 30 elements 16 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL ok: swapping [8] ptlogo_eff3.t32 x [14] pteff04.t32 moves 240308 px (max Δ 225) + [default (what `screen render` draws)] [1] ptlogo2.t32 x [11] ptlogo_tm.t32 (key 32928): 1 px differ (0.0001% of frame), max Δ 1 | ink 58790 / 1062 px, shared 5 px + [default (what `screen render` draws)] [2] ptlogo1.t32 x [4] ptlogo1.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [3] ptlogo2.t32 x [5] ptlogo2.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [7] ptlogo_eff2.rat x [23] ptlogo_back2.t32 (key 32898): 67 px differ (0.0073% of frame), max Δ 1 | ink 74167 / 723 px, shared 8 px + [default (what `screen render` draws)] [8] ptlogo_eff3.t32 x [24] ptlogo_back2eff.t32 (key 32897): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 14507 px, shared 0 px + [default (what `screen render` draws)] [15] ptloop01.rat x [16] ptloop02.rat (key 32784): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [18] ptlogo_back2eff1.t32 x [19] ptlogo_back2eff2.t32 (key 32899): 211 px differ (0.0229% of frame), max Δ 1 | ink 2124 / 4909 px, shared 2124 px + [default (what `screen render` draws)] [18] ptlogo_back2eff1.t32 x [20] ptlogo_back2eff3.t32 (key 32899): 538 px differ (0.0584% of frame), max Δ 2 | ink 2124 / 7538 px, shared 2124 px + [default (what `screen render` draws)] [18] ptlogo_back2eff1.t32 x [21] ptlogo_back2eff4.t32 (key 32899): 941 px differ (0.1021% of frame), max Δ 2 | ink 2124 / 9413 px, shared 2124 px + [default (what `screen render` draws)] [18] ptlogo_back2eff1.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 1061 px differ (0.1151% of frame), max Δ 2 | ink 2124 / 16198 px, shared 2124 px + [default (what `screen render` draws)] [19] ptlogo_back2eff2.t32 x [20] ptlogo_back2eff3.t32 (key 32899): 216 px differ (0.0234% of frame), max Δ 1 | ink 4909 / 7538 px, shared 4909 px + [default (what `screen render` draws)] [19] ptlogo_back2eff2.t32 x [21] ptlogo_back2eff4.t32 (key 32899): 663 px differ (0.0719% of frame), max Δ 2 | ink 4909 / 9413 px, shared 4909 px + [default (what `screen render` draws)] [19] ptlogo_back2eff2.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 847 px differ (0.0919% of frame), max Δ 2 | ink 4909 / 16198 px, shared 4909 px + [default (what `screen render` draws)] [20] ptlogo_back2eff3.t32 x [21] ptlogo_back2eff4.t32 (key 32899): 330 px differ (0.0358% of frame), max Δ 1 | ink 7538 / 9413 px, shared 7538 px + [default (what `screen render` draws)] [20] ptlogo_back2eff3.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 526 px differ (0.0571% of frame), max Δ 2 | ink 7538 / 16198 px, shared 7538 px + [default (what `screen render` draws)] [21] ptlogo_back2eff4.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 9 px differ (0.0010% of frame), max Δ 1 | ink 9413 / 16198 px, shared 9413 px + [everything on (focus+animated+primitives)] CONTROL ok: swapping [8] ptlogo_eff3.t32 x [14] pteff04.t32 moves 825048 px (max Δ 225) + [everything on (focus+animated+primitives)] [1] ptlogo2.t32 x [11] ptlogo_tm.t32 (key 32928): 3 px differ (0.0003% of frame), max Δ 1 | ink 58742 / 1062 px, shared 5 px + [everything on (focus+animated+primitives)] [2] ptlogo1.t32 x [4] ptlogo1.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [everything on (focus+animated+primitives)] [3] ptlogo2.t32 x [5] ptlogo2.t32 (key 32928): NOT BOTH DRAWN — unreachable here + [everything on (focus+animated+primitives)] [7] ptlogo_eff2.rat x [23] ptlogo_back2.t32 (key 32898): 67 px differ (0.0073% of frame), max Δ 1 | ink 73690 / 729 px, shared 7 px + [everything on (focus+animated+primitives)] [8] ptlogo_eff3.t32 x [24] ptlogo_back2eff.t32 (key 32897): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 14531 px, shared 0 px + [everything on (focus+animated+primitives)] [15] ptloop01.rat x [16] ptloop02.rat (key 32784): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + [everything on (focus+animated+primitives)] [18] ptlogo_back2eff1.t32 x [19] ptlogo_back2eff2.t32 (key 32899): 193 px differ (0.0209% of frame), max Δ 1 | ink 2137 / 4917 px, shared 2137 px + [everything on (focus+animated+primitives)] [18] ptlogo_back2eff1.t32 x [20] ptlogo_back2eff3.t32 (key 32899): 511 px differ (0.0554% of frame), max Δ 2 | ink 2137 / 7552 px, shared 2137 px + [everything on (focus+animated+primitives)] [18] ptlogo_back2eff1.t32 x [21] ptlogo_back2eff4.t32 (key 32899): 931 px differ (0.1010% of frame), max Δ 2 | ink 2137 / 9422 px, shared 2137 px + [everything on (focus+animated+primitives)] [18] ptlogo_back2eff1.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 1034 px differ (0.1122% of frame), max Δ 3 | ink 2137 / 16223 px, shared 2137 px + [everything on (focus+animated+primitives)] [19] ptlogo_back2eff2.t32 x [20] ptlogo_back2eff3.t32 (key 32899): 233 px differ (0.0253% of frame), max Δ 1 | ink 4917 / 7552 px, shared 4917 px + [everything on (focus+animated+primitives)] [19] ptlogo_back2eff2.t32 x [21] ptlogo_back2eff4.t32 (key 32899): 668 px differ (0.0725% of frame), max Δ 2 | ink 4917 / 9422 px, shared 4917 px + [everything on (focus+animated+primitives)] [19] ptlogo_back2eff2.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 841 px differ (0.0913% of frame), max Δ 2 | ink 4917 / 16223 px, shared 4917 px + [everything on (focus+animated+primitives)] [20] ptlogo_back2eff3.t32 x [21] ptlogo_back2eff4.t32 (key 32899): 319 px differ (0.0346% of frame), max Δ 1 | ink 7552 / 9422 px, shared 7552 px + [everything on (focus+animated+primitives)] [20] ptlogo_back2eff3.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 539 px differ (0.0585% of frame), max Δ 2 | ink 7552 / 16223 px, shared 7552 px + [everything on (focus+animated+primitives)] [21] ptlogo_back2eff4.t32 x [22] ptlogo_back2eff5.t32 (key 32899): 6 px differ (0.0007% of frame), max Δ 1 | ink 9422 / 16223 px, shared 9422 px + +entry 8 16 elements 2 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL ok: swapping [1] ptbase.t32 x [2] pteff05.t32 moves 771479 px (max Δ 66) + [default (what `screen render` draws)] [3] ptloop01.rat x [4] ptloop02.rat (key 32784): NOT BOTH DRAWN — unreachable here + [default (what `screen render` draws)] [6] ptframe1.t32 x [7] ptframe2.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 4778 / 5302 px, shared 0 px + [everything on (focus+animated+primitives)] CONTROL ok: swapping [1] ptbase.t32 x [2] pteff05.t32 moves 733320 px (max Δ 50) + [everything on (focus+animated+primitives)] [3] ptloop01.rat x [4] ptloop02.rat (key 32784): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + [everything on (focus+animated+primitives)] [6] ptframe1.t32 x [7] ptframe2.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 4782 / 5309 px, shared 0 px + +entry 9 18 elements 2 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL ok: swapping [11] ptbase.t32 x [12] pteff05.t32 moves 768159 px (max Δ 66) + [default (what `screen render` draws)] [0] ptframe3.t32 x [1] ptframe4.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 3646 / 3584 px, shared 0 px + [default (what `screen render` draws)] [14] ptloop01.rat x [15] ptloop02.rat (key 32784): NOT BOTH DRAWN — unreachable here + [everything on (focus+animated+primitives)] CONTROL ok: swapping [11] ptbase.t32 x [12] pteff05.t32 moves 729480 px (max Δ 50) + [everything on (focus+animated+primitives)] [0] ptframe3.t32 x [1] ptframe4.t32 (key 32848): 0 px differ (0.0000% of frame), max Δ 0 | ink 3646 / 3584 px, shared 0 px + [everything on (focus+animated+primitives)] [14] ptloop01.rat x [15] ptloop02.rat (key 32784): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + +entry 12 10 elements 1 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL UNAVAILABLE: no overlapping different-key pair is drawn + [default (what `screen render` draws)] [8] pgloading_eff01.t32 x [9] pgloading_eff02.t32 (key 49408): 1761 px differ (0.1911% of frame), max Δ 1 | ink 30864 / 21163 px, shared 3298 px + [everything on (focus+animated+primitives)] CONTROL DEAD: swapping [6] pgloading_loop5.rat x [7] pgloading_baseeff.t32 changes NOTHING — zeros below are uninterpretable + [everything on (focus+animated+primitives)] [8] pgloading_eff01.t32 x [9] pgloading_eff02.t32 (key 49408): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + +entry 15 10 elements 1 overlapping tied pair(s) + [default (what `screen render` draws)] CONTROL UNAVAILABLE: no overlapping different-key pair is drawn + [default (what `screen render` draws)] [8] pgloading_eff01.t32 x [9] pgloading_eff02.t32 (key 49408): 1761 px differ (0.1911% of frame), max Δ 1 | ink 30864 / 21163 px, shared 3298 px + [everything on (focus+animated+primitives)] CONTROL DEAD: swapping [6] pgloading_loop5.rat x [7] pgloading_baseeff.t32 changes NOTHING — zeros below are uninterpretable + [everything on (focus+animated+primitives)] [8] pgloading_eff01.t32 x [9] pgloading_eff02.t32 (key 49408): 0 px differ (0.0000% of frame), max Δ 0 | ink 0 / 0 px, shared 0 px + +default-options summary: 26 of 31 overlapping tied pairs change at least one pixel; 6 dead/unavailable controls diff --git a/docs/re/structures/ui-paint-order-derived-check.md b/docs/re/structures/ui-paint-order-derived-check.md index 623858d3..2e0947c8 100644 --- a/docs/re/structures/ui-paint-order-derived-check.md +++ b/docs/re/structures/ui-paint-order-derived-check.md @@ -77,9 +77,15 @@ Two pairs flip, and they are not near-misses: | `back2eff5` vs `back2eff4` | 152 047 px² | **100 % of the smaller** | `back2eff5` is 1133×280 and **fully contains** both. Derived paints it on top of -two glows it completely covers; the game paints it underneath. So a tie-break by -declaration index is not cosmetic — where it is wrong, it can be wrong by a whole -layer. ✅ The title is unaffected in practice, because it has a measured order. +two glows it completely covers; the game paints it underneath. ✅ The title is +unaffected in practice, because it has a measured order. + +🔴 **"Not cosmetic — it can be wrong by a whole layer" is WITHDRAWN (2026-08-29).** +That sentence stood here on a geometric argument and nobody had rendered it. The +swap was then measured: `back2eff3` × `back2eff5` moves **6 390 px by a maximum +of Δ2 out of 255**, and `back2eff4` × `back2eff5` moves 5 516 px by **Δ1**. A +total occlusion by a near-transparent additive glow occludes nothing. See +[the pixel cost](#-what-the-tie-break-actually-costs-in-pixels-2026-08-29). ## ✅ The port's actual exposure is two element pairs @@ -98,6 +104,10 @@ So of the five screens, **one** rests on an unverified derived order, and its risk is **two overlapping tied pairs** — not the 15 the raw tie count suggests. The publisher splash's derived order is fully determined (no ties at all). +✅ **And that risk is now measured at zero pixels** — both pairs turn out to +share no ink at all. See +[the pixel cost](#-what-the-tie-break-actually-costs-in-pixels-2026-08-29). + 🟡 For completeness, outside the port's set: entry 7 (the Japanese title) is the worst on the disc at 37 tied pairs, 16 overlapping. @@ -205,3 +215,84 @@ frames happen to be locally similar. But it is a real check, and it removes the **So the chain is: 15 tied pairs → 2 overlapping → 1 drawable → consistent with the capture.** That is the whole paint-order risk on the port's five screens. + +--- + +## ✅ What the tie-break actually costs, in pixels (2026-08-29) + +The section above bounds *where* a wrong tie-break could show and says outright +that "nobody has measured how many of those actually change a pixel". Measured +now, and the answer is **zero on every screen the port ships**. + +Tool: `cargo run -p sylpheed-formats --example tie_break_pixel_cost -- dat/GP_TITLE.pak`, +output committed at [`data/paint-order-tie-pixel-cost.txt`](../data/paint-order-tie-pixel-cost.txt). +It renders each bundle twice — once in the derived order, once with one tied +pair swapped — and diffs. Elements sharing a key are contiguous in the derived +order (a stable sort on `(key, i)`), so a swap paints nothing else in between: +the diff is the tie-break's cost and nothing else. + +### The instrument was controlled first, per entry + +Every entry also swaps an **overlapping pair with different keys** — a pair whose +order the game demonstrably cares about. If that swap moves nothing, the +instrument cannot see a reorder on that screen and its zeros are worthless. + +| entry | control swap | pixels moved | +|---|---|---| +| 4 | `pteff04` × `ptlogo_back2eff5` | 36 305 (max Δ **254**) | +| 5 / 8 | `ptbase` × `pteff05` | 764 030 / 771 479 (max Δ 67) | +| 6 / 9 | `ptbase` × `pteff05` | 761 600 / 768 159 (max Δ 67) | +| 7 | `ptlogo_eff3` × `pteff04` | 240 308 (max Δ 225) | +| 0, 1, 12, 15 | — | ⚠️ **no control**: on the loading bundles no overlapping different-key pair is drawn under default options. Their numbers below are non-zero, so they do not rest on a control; a *zero* there would have been uninterpretable | + +### And every result explains itself + +A bare "0 px differ" is ambiguous: it can mean the order genuinely does not +matter, or that the two elements never painted on the same pixel and the +"overlap" was an artefact of the rect approximation (pivot × 2 at the resting +placement). So the tool also reports each element's **ink** — the pixels that +move when that element alone is removed — and the **shared ink** between the +pair. That distinction is what makes the zeros below trustworthy. + +### The port's screens: zero, and blend-independent + +| entry | screen | tied pair | ink | **shared ink** | pixels changed | +|---|---|---|---|---|---| +| 5, 8 | main menu EN / JP | `ptframe1` × `ptframe2` | 4 783 / 5 297 px | **0** | **0** | +| **6, 9** | **`EXTRAS` EN / JP** | `ptframe3` × `ptframe4` | 3 646 / 3 584 px | **0** | **0** | +| 10 | publisher splash | — (no ties) | | | **0** | +| 4, 11 | title, developer splash | measured order used | | | **0** | + +`ptframe3` and `ptframe4` each put ink on ~3 600 pixels and **share none of +them**. Their 102 × 132 bounding-box overlap is a rect artefact: the sprites +inside it are disjoint. ✅ This is stronger than the correlation check below it, +because it does not depend on the blend at all — when two layers never touch the +same pixel, their order cannot matter under *any* per-pixel compositing rule. + +**So the whole Q3 tie-break risk on the five menu screens is zero pixels, not +"one drawable pair, consistent with a capture".** + +### Where it is non-zero, it is invisible + +| entry | screen | worst tied pair | px changed | **max Δ** | +|---|---|---|---|---| +| 4 | title EN (derived order forced) | `back2eff1` × `back2eff5` | 6 645 (0.72 %) | **3** | +| 7 | title JP | `back2eff1` × `back2eff5` | 1 061 (0.12 %) | **3** | +| 0, 1, 12, 15 | loading ×4 | `pgloading_eff01` × `eff02` | 1 761 (0.19 %) | **1** | + +Across **31 drawable overlapping tied pairs on the whole of `GP_TITLE`**, 26 +change at least one pixel and the largest change any of them makes to any +channel is **3/255**. The tied families are additive glows; they are nearly +transparent where they meet, which is why containment does not equal occlusion. + +⚠️ **Reach.** This measures *our compositor's* sensitivity to the order, not the +game's. For the ptframe pairs that does not matter (zero shared ink is +blend-independent). For the glow pairs it does: the Δ ≤ 3 figures assume our +alpha blend, and a game using additive or premultiplied blending for these could +differ. What the numbers rule out is a *structural* error — a layer appearing or +disappearing — not a shading one. + +⚠️ Entries 12 and 15 under `--focus --animated --primitives` report ink 0/0 for +the tied pair: with the primitives drawn, those two elements contribute no +visible pixels at all. That is why their control is dead in that configuration, +and it is self-consistent rather than a failure.