re: the forced-backdrop rule's pixel cost -- 38 screens go black without it

Follows the necessity census. 'The order moves' is a property of the sort; the
tie-break work already found reorders costing zero pixels, so the picture
moving is a separate claim. Rendered each of the 62 deciding builds twice and
diffed.

  38 .prm deciders: changed_px == ink_px in ALL 38. Without the rule the
                    primitive sorts last, paints over everything, and the
                    screen composites to pure black. The port's original
                    contradiction argument, measured on 38 builds across seven
                    archives instead of argued on two.
  24 .tbm deciders: zero -- and that is MY INSTRUMENT, not a finding.

The control asked whether the composite had ink; it always does. The question
was whether the reordered ELEMENT has ink, and compose draws no pixels at all
for a .tbm. So those 24 zeros measure our renderer's blindness by construction.
tie_break_pixel_cost.rs already had the per-element ink_mask this needed.
Reported rather than quietly patched: a control that cannot fail is the shape
this corpus keeps paying for.

Also corrects two things the port agent caught:

  - 'Two renderers, same answer' was true of the six GP_TITLE instances and not
    of the other 74. The port's re-run of my probe is my code executed twice;
    its independent leg was removing its own exporter post-pass, which covers
    GP_TITLE only. The disc-wide 62 has one witness and the page now says so.
  - forced_backdrop_necessity.rs defaulted to GP_TITLE with no argument, so a
    bare run printed 6 instances in the same format as 80. It now walks every
    dat/*.pak and reports the archive count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 07:04:05 +00:00
parent 304ce9efaa
commit aa9b7ef340
5 changed files with 307 additions and 10 deletions

View File

@@ -12,6 +12,15 @@
//! //!
//! Raised by the port agent 2026-08-30. Reach note in //! Raised by the port agent 2026-08-30. Reach note in
//! `docs/re/structures/ui-forced-backdrop.md`. //! `docs/re/structures/ui-forced-backdrop.md`.
//!
//! cargo run -p sylpheed-formats --example forced_backdrop_necessity -- [pak...]
//!
//! 🔴 With no argument this used to default to `GP_TITLE` alone, so a bare run
//! reported **6 instances, not 80** — a thirteenth of the census, printed in the
//! same format and reading like the whole thing. The port agent hit it and nearly
//! filed the discrepancy back at me. It now walks every `dat/*.pak` by default and
//! says on stderr how many archives it opened, because "I ran your instrument" has
//! to mean the same thing to both of us.
use std::path::PathBuf; use std::path::PathBuf;
@@ -33,15 +42,27 @@ fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
fn main() { fn main() {
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
let path = std::env::args() let (mut total_decides, mut total_agrees) = (0usize, 0usize);
.nth(1) let mut paks: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
.unwrap_or_else(|| root.join("dat/GP_TITLE.pak").to_string_lossy().into()); if paks.is_empty() {
let ar = PakArchive::open(&path).expect("pak"); let mut all: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
println!("# {path}"); .expect("dat/")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
.collect();
all.sort();
paks = all;
}
eprintln!("# scanning {} archive(s)", paks.len());
for path in &paks {
let ar = PakArchive::open(path).expect("pak");
println!("# {}", path.display());
println!("# entry forced decides elements note"); println!("# entry forced decides elements note");
let mut decides = Vec::new(); let mut decides = Vec::new();
let mut agrees = Vec::new(); let mut agrees = Vec::new();
#[allow(unused)]
let _ = (&decides, &agrees);
for (i, e) in ar.entries().iter().enumerate() { for (i, e) in ar.entries().iter().enumerate() {
let Ok(by) = ar.read(e) else { continue }; let Ok(by) = ar.read(e) else { continue };
let Some(b) = ui_layout::parse_build(&by) else { let Some(b) = ui_layout::parse_build(&by) else {
@@ -83,6 +104,11 @@ fn main() {
keyless.join(","), keyless.join(","),
); );
} }
println!("\n# rule DECIDES the order on entries {decides:?}"); println!("# rule DECIDES the order on entries {decides:?}");
println!("# rule merely AGREES on entries {agrees:?}"); println!("# rule merely AGREES on entries {agrees:?}\n");
total_decides += decides.len();
total_agrees += agrees.len();
}
println!("# TOTAL over {} archive(s): {total_decides} deciding entries, \
{total_agrees} agreeing", paks.len());
} }

View File

@@ -0,0 +1,119 @@
//! What does `forced_backdrop` cost IN PIXELS on the screens it decides?
//!
//! `forced_backdrop_necessity.rs` answers "does the derived ORDER move", which is
//! a property of the sort. The port agent then pointed out — correctly — that its
//! re-run of that probe was **my code executed twice**, not a second witness, so
//! the disc-wide 62 has one measurement behind it and only `GP_TITLE` has two.
//!
//! This does not fix that (it is still this crate), but it moves the question to a
//! **different layer**: render each deciding build twice, once in the order
//! `compose` derives and once with the `forced_backdrop` fallback removed, and
//! count the pixels that differ. "The order moved" and "the picture moved" are not
//! the same claim, and the second is the one anybody cares about — the tie-break
//! work already found overlapping reorders that cost exactly zero pixels.
//!
//! Each entry carries its own CONTROL: the pixel count of the composite itself.
//! If a build renders empty, its zero means the instrument saw nothing, not that
//! the rule is free.
//!
//! cargo run -p sylpheed-formats --example forced_backdrop_pixel_cost -- [pak...]
//!
//! With no argument it walks **every `dat/*.pak`** — the necessity probe defaulted
//! to `GP_TITLE`, which made a bare run report a thirteenth of the census and read
//! like the whole thing.
use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, ui_layout};
use ui_layout::ComposeOptions;
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
idx.sort_by_key(|&i| {
let el = &build.elements[i];
(
ui_layout::sprite_layer_key(build, bundle, el)
.or_else(|| ui_layout::implied_layer_key(&el.name))
.unwrap_or(u32::MAX),
i,
)
});
idx
}
fn main() {
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
let mut paks: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
if paks.is_empty() {
let mut all: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
.expect("dat/")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
.collect();
all.sort();
paks = all;
}
eprintln!("# scanning {} archive(s)", paks.len());
let opts = ComposeOptions {
include_primitives: true,
backdrop: [0, 0, 0, 255],
..Default::default()
};
println!("# archive entry element changed_px total_px ink_px(control) pct");
let (mut decided, mut zero_cost, mut blind) = (0usize, 0usize, 0usize);
for pak in &paks {
let Ok(ar) = PakArchive::open(pak) else { continue };
let name = pak.file_name().unwrap().to_string_lossy().to_string();
for (i, e) in ar.entries().iter().enumerate() {
let Ok(by) = ar.read(e) else { continue };
let Some(b) = ui_layout::parse_build(&by) else {
continue;
};
let with = ui_layout::derived_paint_order(&b, &by);
let without = order_without_rule(&b, &by);
if with == without {
continue;
}
let forced: Vec<&str> = b
.elements
.iter()
.filter(|el| ui_layout::forced_backdrop(&b, el))
.map(|el| el.name.as_str())
.collect();
let a = ui_layout::compose_with_order(&b, &by, opts.clone(), None, Some(&with));
let c = ui_layout::compose_with_order(&b, &by, opts.clone(), None, Some(&without));
let n = a
.rgba
.chunks_exact(4)
.zip(c.rgba.chunks_exact(4))
.filter(|(x, y)| x != y)
.count();
// Control: does this build put any ink down at all, against the bare
// backdrop? A build that renders to nothing cannot show a reorder.
let ink = a
.rgba
.chunks_exact(4)
.filter(|p| p[..3] != [0, 0, 0])
.count();
let total = a.rgba.len() / 4;
decided += 1;
if ink == 0 {
blind += 1;
} else if n == 0 {
zero_cost += 1;
}
println!(
" {name} {i} {} {n} {total} {ink} {:.2}%",
forced.join(","),
100.0 * n as f64 / total as f64
);
}
}
println!("\n# builds whose ORDER the rule decides: {decided}");
println!("# of those, costing ZERO pixels: {zero_cost}");
println!("# of those, BLIND (build renders no ink, control fails): {blind}");
}

View File

@@ -2541,7 +2541,13 @@ leaves the four splashes byte-identical while `build_12`/`build_15` go black.
| 10, 11, 13, 14 (splashes) | **no** — order unchanged | `palogo_eff0.prm`, which has its own implied key `0x00000000` | | 10, 11, 13, 14 (splashes) | **no** — order unchanged | `palogo_eff0.prm`, which has its own implied key `0x00000000` |
| **12, 15** (loading) | **YES** | `pgloading_eff00.prm` — no read key, no implied key | | **12, 15** (loading) | **YES** | `pgloading_eff00.prm` — no read key, no implied key |
Two renderers, same answer. Your point about the agreement not being independent 🔴 **But "two renderers, same answer" is true of these six and not of the other
74**, and you were right to say so. Your independent leg is the one that came
first: you removed *your own* post-pass and diffed your export — different code,
different layer. Your re-run of my probe is my code executed twice, and I have
corrected the page to say that rather than let the matching table imply otherwise.
Your point about the agreement not being independent
support is also right and I have written it into the page: `palogo_eff0.prm` would support is also right and I have written it into the page: `palogo_eff0.prm` would
sort first from its implied key anyway, so the rule reproducing it is the rule sort first from its implied key anyway, so the rule reproducing it is the rule
reproducing my crate. reproducing my crate.

View File

@@ -0,0 +1,83 @@
# What forced_backdrop costs IN PIXELS on the 62 builds whose order it decides.
#
# Produced by: cargo run -p sylpheed-formats --example forced_backdrop_pixel_cost
# (no argument = every dat/*.pak), 2026-08-30, SYLPHEED_DISC=/disc.
# Each build rendered twice at 1280x720 with include_primitives=true and a
# black backdrop: once in derived_paint_order(), once with the
# forced_backdrop fallback removed. changed_px is the diff.
#
# RESULT, and it splits perfectly along the element kind:
# 38 .prm deciders: changed_px > 0, and changed_px == ink_px in ALL 38.
# Without the rule the screen composites to PURE BLACK.
# 24 .tbm deciders: changed_px == 0 in all 24 -- but see the caveat, this
# is our compositor drawing no pixels for a .tbm at all,
# NOT the rule being free. The control below did not
# catch it and was the wrong control.
#
# archive entry element changed_px total_px ink_px(control) pct
GP_BUNK.pak 0 px_bunk_base.tbm 0 921600 75487 0.00%
GP_BUNK.pak 2 px_bunk_base.tbm 0 921600 70372 0.00%
GP_BUNK.pak 4 pvbase.tbm 0 921600 41137 0.00%
GP_BUNK.pak 6 pvbase.tbm 0 921600 34773 0.00%
GP_DEBRIEFING_PILOTLOG.pak 118 px_deb_base.tbm 0 921600 25812 0.00%
GP_DEBRIEFING_PILOTLOG.pak 130 px_deb_base.tbm 0 921600 25812 0.00%
GP_DEBRIEFING_PILOTLOG.pak 131 pjbgbase2.tbm 0 921600 33329 0.00%
GP_DEBRIEFING_PILOTLOG.pak 134 px_deb_base.tbm 0 921600 44147 0.00%
GP_DEBRIEFING_PILOTLOG.pak 150 pjbgbase2.tbm 0 921600 34401 0.00%
GP_DEBRIEFING_PILOTLOG.pak 165 px_deb_base.tbm 0 921600 42487 0.00%
GP_DIALOG.pak 2 pcbase.tbm 0 921600 646626 0.00%
GP_DIALOG.pak 3 pcbase.tbm 0 921600 646626 0.00%
GP_DIALOG.pak 9 pzeff00.prm 867195 921600 867195 94.10%
GP_DIALOG.pak 10 pzeff00.prm 869598 921600 869598 94.36%
GP_DIALOG.pak 11 pzeff00.prm 868329 921600 868329 94.22%
GP_DIALOG.pak 12 pzeff00.prm 867629 921600 867629 94.14%
GP_DIALOG.pak 13 pzeff00.prm 867713 921600 867713 94.15%
GP_DIALOG.pak 14 pzeff00.prm 868023 921600 868023 94.19%
GP_DIALOG.pak 15 pzeff00.prm 867294 921600 867294 94.11%
GP_DIALOG.pak 16 pzeff00.prm 865326 921600 865326 93.89%
GP_DIALOG.pak 17 pzeff00.prm 867276 921600 867276 94.11%
GP_DIALOG.pak 18 pzeff00.prm 868899 921600 868899 94.28%
GP_DIALOG.pak 19 pzeff00.prm 866752 921600 866752 94.05%
GP_DIALOG.pak 20 pzeff00.prm 868266 921600 868266 94.21%
GP_DIALOG.pak 21 pzeff00.prm 868388 921600 868388 94.23%
GP_DIALOG.pak 22 pzeff00.prm 870053 921600 870053 94.41%
GP_DIALOG.pak 23 pzeff00.prm 867982 921600 867982 94.18%
GP_DIALOG.pak 24 pzeff00.prm 868386 921600 868386 94.23%
GP_DIALOG.pak 25 pzeff00.prm 865233 921600 865233 93.88%
GP_DIALOG.pak 26 pzeff00.prm 865525 921600 865525 93.92%
GP_DIALOG.pak 28 pzeff00.prm 865233 921600 865233 93.88%
GP_DIALOG.pak 29 pzeff00.prm 865500 921600 865500 93.91%
GP_DIALOG.pak 30 pzeff00.prm 866183 921600 866183 93.99%
GP_DIALOG.pak 31 pzeff00.prm 866233 921600 866233 93.99%
GP_DIALOG.pak 32 pzeff00.prm 865192 921600 865192 93.88%
GP_DIALOG.pak 33 pzeff00.prm 865538 921600 865538 93.92%
GP_DIALOG.pak 34 pzeff00.prm 865233 921600 865233 93.88%
GP_DIALOG.pak 35 pzeff00.prm 866079 921600 866079 93.98%
GP_DIALOG.pak 36 pzeff00.prm 865508 921600 865508 93.91%
GP_DIALOG.pak 37 pzeff00.prm 866218 921600 866218 93.99%
GP_DIALOG.pak 38 pzeff00.prm 868259 921600 868259 94.21%
GP_DIALOG.pak 39 pzeff00.prm 866804 921600 866804 94.05%
GP_DIALOG.pak 40 pzeff00.prm 866062 921600 866062 93.97%
GP_DIALOG.pak 41 pzeff00.prm 866792 921600 866792 94.05%
GP_DIALOG.pak 86 esrb_base.prm 6388 921600 6388 0.69%
GP_DIALOG.pak 130 esrb_base.prm 6388 921600 6388 0.69%
GP_GAMEOVER.pak 4 pnbase.tbm 0 921600 921600 0.00%
GP_GAMEOVER.pak 7 pnbase.tbm 0 921600 921600 0.00%
GP_MISSION_SELECT.pak 3 px_mission_base.tbm 0 921600 661678 0.00%
GP_MISSION_SELECT.pak 5 px_mission_base.tbm 0 921600 659464 0.00%
GP_MOVIE_THEATER.pak 0 px_movie_base.tbm 0 921600 32871 0.00%
GP_MOVIE_THEATER.pak 1 px_movie_base.tbm 0 921600 27726 0.00%
GP_SAVE_LOAD.pak 46 pgloading_eff00.prm 49771 921600 49771 5.40%
GP_SAVE_LOAD.pak 69 pgloading_eff00.prm 49771 921600 49771 5.40%
GP_SAVE_LOAD.pak 89 px_replay_base.tbm 0 921600 269421 0.00%
GP_SAVE_LOAD.pak 98 px_replay_base.tbm 0 921600 269421 0.00%
GP_SYSTEM.pak 0 pqbase.tbm 0 921600 828253 0.00%
GP_SYSTEM.pak 1 pqbase.tbm 0 921600 828199 0.00%
GP_TITLE.pak 12 pgloading_eff00.prm 49771 921600 49771 5.40%
GP_TITLE.pak 15 pgloading_eff00.prm 49771 921600 49771 5.40%
GP_TUTORIAL.pak 0 pubase.tbm 0 921600 92946 0.00%
GP_TUTORIAL.pak 1 pubase.tbm 0 921600 92879 0.00%
# builds whose ORDER the rule decides: 62
# of those, costing ZERO pixels: 24
# of those, BLIND (build renders no ink, control fails): 0

View File

@@ -262,8 +262,71 @@ load-bearing on them in the full sense.
On the five menu screens the exposure is **two**: `GP_TITLE` entries **12 and 15**, On the five menu screens the exposure is **two**: `GP_TITLE` entries **12 and 15**,
the dressed loading bundles, where `pgloading_eff00.prm` has neither a read nor an the dressed loading bundles, where `pgloading_eff00.prm` has neither a read nor an
implied key. Entries 10, 11, 13 and 14 — the four splashes — are unchanged with the implied key. Entries 10, 11, 13 and 14 — the four splashes — are unchanged with the
rule removed, exactly as the port measured on its own side. Two renderers, same rule removed.
answer.
### The cost in pixels: 38 of the 62 turn the screen black without it
"The order moves" is a property of the sort. The tie-break work already found
overlapping reorders costing **zero** pixels, so it does not follow that the picture
moves. Each of the 62 deciding builds was therefore rendered twice — once in
`derived_paint_order`, once with the fallback removed — and diffed
([`../data/forced-backdrop-pixel-cost.txt`](../data/forced-backdrop-pixel-cost.txt),
instrument `examples/forced_backdrop_pixel_cost.rs`).
The split is perfect, and it falls exactly along the element kind:
| | builds | changed pixels |
|---|---|---|
| `.prm` deciders | **38** | **0.69 % … 94.41 %** of the frame |
| `.tbm` deciders | 24 | **0** — but see below |
**On all 38 `.prm` builds, `changed_px` equals the composite's total ink exactly.**
Not approximately — identically, 38 times out of 38. Without the rule the primitive
sorts last, paints over everything, and the screen composites to **pure black**.
That is the port's original contradiction argument, and it is now measured on 38
builds across seven archives rather than argued on two.
For the port's two: `GP_TITLE` entries 12 and 15 each move **49 771 px = 5.40 %** of
the frame, which is their entire ink.
### 🔴 The zero on the 24 `.tbm` builds is my instrument, not a finding
**The control I wrote was the wrong control and it passed anyway.** It asked
whether the *composite* had ink — it always does, the rest of the screen draws —
when the question is whether *the element being reordered* has ink. This page
already records that `compose` draws **no pixels at all** for a `.tbm`, because it
has no resolvable sprite. So a `.tbm`'s paint position cannot change a pixel in our
renderer **by construction**, and those 24 zeros measure that and nothing else.
`tie_break_pixel_cost.rs` got this right and has the per-element `ink_mask` this
one needed. Reported rather than quietly patched, because the shape — a control
that cannot fail — is the one this corpus keeps paying for.
⚠️ So the `.tbm` half of the necessity result stands where it stood: **"correct or
inert", indistinguishable**, and no closer to being distinguished than before.
The 38 `.prm` are the part this measurement moves.
### 🔴 "Two renderers, same answer" was true of six instances, not eighty
That sentence stood here and it overstated the evidence. **The port agent caught it
and it is worth stating precisely, because the failure it guards against is the one
that started this whole thread** — `verify-screen` scoring two blank frames `OK`.
| | witnesses |
|---|---|
| the six `GP_TITLE` instances | **two, genuinely independent** — the port removed *its own* post-pass in its exporter and diffed its export; different code, different language, different layer. My crate-side run agrees |
| the other **74** | **one measurement, executed twice.** The port re-ran *this crate's* probe. A fault in the instrument reproduces identically for both of us |
So the disc-wide 62 is **not** independently confirmed and this page will not claim
it is. What the port's re-run does establish is that the probe is deterministic and
that I transcribed its output correctly — worth having, and much less than
agreement.
⚠️ **And the instrument had a real trap.** `forced_backdrop_necessity.rs` defaulted
to `GP_TITLE` when given no argument, so a bare run printed **6 instances in the
same format as 80**. The port hit it and nearly filed the discrepancy back at me. It
now walks every `dat/*.pak` by default and reports the archive count on stderr.
"I ran your instrument" has to mean the same thing to both of us.
## Reach ## Reach