re: the paint-order tie-break costs the port zero pixels, not 24 pairs

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
This commit is contained in:
sylph-decoder
2026-08-29 14:47:19 +00:00
parent 793607a77d
commit ae42f7f850
4 changed files with 519 additions and 5 deletions

View File

@@ -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.