re(ui): measure the paint-order hedge -- exact on 4 of 5, and bound the rest

`compose` claimed the derived paint order "reproduces both measured
orders up to ties". That sentence was never measured and was stale by
one: there are three measured orders, not two. examples/paint_order_audit.rs
checks it.

  main menu (entries 5, 8)      derived == measured   0 inverted pairs
  developer splash (11, 14)     derived == measured   0 inverted pairs
  title (entry 4)               DIFFERS               8, all same-key ties

So the claim holds and the exception is entirely ties -- but two of those
ties are total occlusions, not near-misses. The tied family is the five
ptlogo_back2eff glows (key 32899); back2eff5 is 1133x280 and FULLY
CONTAINS back2eff3 (82,824 px^2 = 100% of the smaller) and back2eff4
(152,047 px^2 = 100%). Derived paints it on top of two glows it entirely
covers; the game paints it underneath. A tie-break by declaration index
can therefore be wrong by a whole layer. The title itself is unaffected --
it has a measured order.

The port's actual exposure, per screen: title, main menu and developer
splash all use MEASURED orders; the publisher splash is derived but has
ZERO ties, so it is fully determined; EXTRAS is derived with 15 tied
pairs of which only 2 OVERLAP. Two element pairs on one screen is the
whole risk, and that is what HANDOFF now says -- not the raw 15, which
would have overstated it 7x.

Reach stated: this compares the derived order against orders measured
from the game, not an independent derivation, so where no measured order
exists only the tie exposure can be checked. Overlap uses pivot*2 as the
element size at its resting placement.

Stale comment in compose corrected. METHOD: a hedge in a code comment is
an unmeasured claim; and count the cases that can bite, not the ones that
match the pattern.
This commit is contained in:
Sylpheed RE agent
2026-08-29 02:22:01 +00:00
parent c3cf3c2e81
commit ba47bdebe8
6 changed files with 271 additions and 2 deletions

View File

@@ -0,0 +1,122 @@
//! Does the DERIVED paint order reproduce the ones measured from the game?
//!
//! `compose` uses a measured order for the three builds that have one and falls
//! back to `derived_paint_order` (a sort on each sprite's layer key) everywhere
//! else. The doc comment claims the derived order "reproduces both measured
//! orders up to ties" — this checks that claim against all three, and says what
//! the ties actually cost.
//!
//! cargo run -p sylpheed-formats --example paint_order_audit -- <GP_TITLE.pak>
use sylpheed_formats::{pak, ui_layout};
fn measured(names: &[&str]) -> Option<(&'static str, Vec<usize>)> {
const TITLE: [&str; 24] = [
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
"ptlogoall_eff2.t32",
];
const SPLASH: [&str; 7] = [
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
"palogo_anima_eff.t32",
];
const MENU: [&str; 16] = [
"pteff00.prm", "ptbase.t32", "pteff05.t32", "ptloop01.rat",
"ptloop02.rat", "pteff02.prm", "ptframe1.t32", "ptframe2.t32",
"pteff10.t32", "pteff12.t32", "ptbtn01.rat", "ptbtn02.rat",
"ptbtn03.rat", "ptbtn04.rat", "ptbtn05.rat", "ptmsg.t32",
];
if names == TITLE {
return Some(("title", vec![9,11,12,10,13,6,20,19,14,15,18,16,17,0,2,4,7,1,3,5,22,23,21,8]));
}
if names == SPLASH { return Some(("splash", vec![0,2,4,6,1,3,5])); }
if names == MENU {
return Some(("main menu", vec![1,3,4,2,5,8,9,6,7,15,10,11,12,13,14,0]));
}
None
}
fn main() {
let path = std::env::args().nth(1).expect("usage: paint_order_audit <pak>");
let ar = pak::PakArchive::open(&path).expect("open pak");
let mut checked = 0;
let entries: Vec<_> = ar.entries().to_vec();
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 names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
// Every build: how exposed is it to tie-breaking? A tie between
// OVERLAPPING elements is where a derived order can go visibly wrong.
let keys_all: Vec<u32> = build.elements.iter()
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
.collect();
let mut tie_pairs = 0;
for a in 0..keys_all.len() {
for b in (a + 1)..keys_all.len() {
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX { tie_pairs += 1; }
}
}
// Of the tied pairs, how many OVERLAP? Only those can paint visibly
// differently under an arbitrary tie-break. Rect from the declared
// pivot (= half the sprite for a .t32) at the resting placement.
let 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))
};
let mut tie_overlap = 0;
for a in 0..keys_all.len() {
for b in (a + 1)..keys_all.len() {
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
else { continue };
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
if ox > 0 && oy > 0 { tie_overlap += 1; }
}
}
let Some((label, want)) = measured(&names) else {
println!("entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
{tie_overlap} of them OVERLAPPING", build.elements.len());
continue;
};
checked += 1;
let got = ui_layout::derived_paint_order(&build, &bytes);
let keys: Vec<u32> = build.elements.iter()
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
.collect();
let exact = got == want;
// How many adjacent pairs in the MEASURED order does derived get wrong,
// and of those, how many are between elements sharing a layer key (a
// tie the sort cannot resolve) versus a genuine key-order conflict?
let pos_got: Vec<usize> = {
let mut p = vec![0; got.len()];
for (r, &e) in got.iter().enumerate() { p[e] = r; }
p
};
let (mut inv, mut tied) = (0, 0);
for a in 0..want.len() {
for b in (a + 1)..want.len() {
let (x, y) = (want[a], want[b]);
if pos_got[x] > pos_got[y] {
inv += 1;
if keys[x] == keys[y] { tied += 1; }
}
}
}
println!("entry {i:2} {label:10} {} elements", want.len());
println!(" derived == measured : {}", if exact { "YES" } else { "NO" });
println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})");
if !exact {
println!(" measured: {want:?}");
println!(" derived : {got:?}");
println!(" keys : {keys:?}");
}
}
println!("\n{checked} build(s) with a measured order were checked");
}

View File

@@ -874,8 +874,16 @@ pub fn compose(
// Measured paint order when one exists for this build, declaration order // Measured paint order when one exists for this build, declaration order
// otherwise — see `measured_paint_order`. // otherwise — see `measured_paint_order`.
// Measured order when this build is one of the two read off the running // Measured order when this build is one of the two read off the running
// game; otherwise the order DERIVED from the sprites' layer keys, which // game; otherwise the order DERIVED from the sprites' layer keys.
// reproduces both measured orders up to ties. //
// ✅ Checked 2026-08-29 (`examples/paint_order_audit.rs`), because the
// previous wording here — "reproduces both measured orders up to ties" —
// was unmeasured and stale by one: there are THREE measured orders. The
// derived order reproduces the main menu and the developer splash EXACTLY
// (0 inverted pairs each) and differs on the title by 8 pairs, all of them
// 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<usize> = let order: Vec<usize> =
measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle)); measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle));
for &ei in &order { for &ei in &order {

View File

@@ -256,6 +256,23 @@ authored version can be deleted.
addresses (the two differ in size). addresses (the two differ in size).
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md) [`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
***Paint order: your exposure is two element pairs, on one screen.** We use
an order *measured from the running game* where one exists and a derived order
(a sort on each sprite's layer key) elsewhere. Checked, rather than assumed:
the derived order reproduces the measured one **exactly** on the main menu
(0 inverted pairs) and the developer splash (0). On the **title** it differs by
8 pairs — **all same-layer-key ties** — and two of those are total occlusions
(`back2eff5` is 1133×280 and *fully contains* `back2eff3` and `back2eff4`;
derived puts it on top, the game puts it underneath). The title is unaffected
in practice because it has a measured order.
Per screen: title **measured**, main menu **measured**, developer splash
**measured**, publisher splash derived but with **0 ties** (fully determined),
and **`EXTRAS` derived with 15 tied pairs of which only 2 overlap**. That pair
count is the whole risk — a tie-break by declaration index can be wrong by a
whole layer where it is wrong, so if `EXTRAS` ever looks off, those two pairs
are where to look.
[`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md)
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is * **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the ✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
disc's real navigation structure is unknown, and `opt ` is *not* a focus link disc's real navigation structure is unknown, and `opt ` is *not* a focus link

View File

@@ -551,3 +551,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
within 2 % — so the negative arrives with its own evidence that the frames were within 2 % — so the negative arrives with its own evidence that the frames were
live. Building the cross-check into the tool costs a few lines and is what live. Building the cross-check into the tool costs a few lines and is what
separates a measurement from a silence. separates a measurement from a silence.
* **A hedge in a code comment is an unmeasured claim.** `compose` said the
derived paint order "reproduces both measured orders up to ties" — a sentence
that sounds like a result and was neither measured nor kept current: there were
three measured orders by then, not two. Measuring it took one example program
and turned a hedge into a bounded number (exact on 4 of 5 bundles; the 5th off
by 8 pairs, all ties). Grep your own comments for "up to", "roughly",
"essentially" — each one is a claim nobody has checked.
* **Count the cases that can actually bite, not the cases that match the
pattern.** `EXTRAS` has 15 tied pairs in its derived paint order, which reads
alarming. Only **2** of them overlap, and a tie between non-overlapping
elements paints identically either way. Reporting 15 would have overstated the
risk by 7×; the useful number is the one filtered by whether the difference can
reach a pixel.

View File

@@ -0,0 +1,35 @@
# cargo run -p sylpheed-formats --example paint_order_audit -- dat/GP_TITLE.pak
# 2026-08-29
1682 | let decl = &decls[0];
290 | let mut flush = |base: u32,
entry 0 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
entry 1 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
entry 2 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 3 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 4 title 24 elements
derived == measured : NO
inverted pairs : 8 (of which same-layer-key ties: 8)
measured: [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8]
derived : [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, 22, 23, 21, 8]
keys : [32928, 32928, 32928, 32928, 32928, 32928, 32832, 32928, 4294967295, 32768, 4294967295, 32784, 32784, 4294967295, 32899, 32899, 32899, 32899, 32899, 32898, 32897, 33024, 32936, 32937]
entry 5 main menu 16 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 6 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
entry 7 (no measured order) 30 elements, 37 tied pairs, 16 of them OVERLAPPING
entry 8 main menu 16 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 9 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
entry 10 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 11 splash 7 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 12 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
entry 13 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 14 splash 7 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 15 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
5 build(s) with a measured order were checked

View File

@@ -0,0 +1,74 @@
# ✅ Does the derived paint order reproduce the measured ones? Mostly — and the gap is bounded
**Status:****checked, with numbers.** `compose` uses a paint order *measured
from the running game* for the builds that have one and falls back to
`derived_paint_order` — a sort on each sprite's layer key — everywhere else. The
code's doc comment claimed the derived order "reproduces both measured orders up
to ties". That was a hedge with no measurement behind it, and it was stale:
there are **three** measured orders now, not two.
Tool: `cargo run -p sylpheed-formats --example paint_order_audit -- dat/GP_TITLE.pak`.
Output committed at [`data/paint-order-audit.txt`](../data/paint-order-audit.txt).
## The claim holds, and the exception is all ties
| build | derived == measured | inverted pairs | of which same-key ties |
|---|---|---|---|
| main menu (entries 5, 8) | **YES** | 0 | — |
| developer splash (11, 14) | **YES** | 0 | — |
| title (entry 4) | **NO** | 8 | **8 — every one** |
Four of the five measured bundles reproduce **exactly**. The title's eight
disagreements are all between elements that share a layer key, which the sort
cannot order and breaks by declaration index.
## ⚠️ Two of those ties are total occlusions
The tied family is the five `ptlogo_back2eff` glows, all key `32899`:
```
measured paints: 14, 15, 18, 16, 17
derived paints: 14, 15, 16, 17, 18
```
Two pairs flip, and they are not near-misses:
| pair | overlap | |
|---|---|---|
| `back2eff5` vs `back2eff3` | 82 824 px² | **100 % of the smaller** |
| `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.
## ✅ The port's actual exposure is two element pairs
Per build, counting tied pairs and how many of them **overlap** (only those can
paint visibly differently):
| entry | the port's screen | order used | tied pairs | overlapping |
|---|---|---|---|---|
| 4 | title | **measured** | — | — |
| 5 | main menu | **measured** (derived agrees exactly) | 0 | 0 |
| **6** | **`EXTRAS`** | **derived** | 15 | **2** |
| 10 | publisher splash | derived | **0** | **0** |
| 11 | developer splash | **measured** | — | — |
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).
🟡 For completeness, outside the port's set: entry 7 (the Japanese title) is the
worst on the disc at 37 tied pairs, 16 overlapping.
## Reach
* This checks the derived order against the orders **measured from the running
game**; it is not an independent derivation of what the game does. Where no
measured order exists, agreement cannot be checked at all — only the *tie
exposure* can, which is what the table above reports.
* Overlap uses `pivot × 2` as the element's size (documented as the sprite's own
dimensions for a `.t32`) at its resting placement, so scaled or rotated
elements are approximated.