diff --git a/crates/sylpheed-formats/examples/forced_backdrop_necessity.rs b/crates/sylpheed-formats/examples/forced_backdrop_necessity.rs new file mode 100644 index 00000000..3239dc20 --- /dev/null +++ b/crates/sylpheed-formats/examples/forced_backdrop_necessity.rs @@ -0,0 +1,88 @@ +//! Which screens does `forced_backdrop` DECIDE, and which does it merely agree with? +//! +//! Every check this corpus has run on the rule measured its **stability** — that +//! no verdict moved when something else changed. That is a different property +//! from **necessity**: an element whose position is already fixed by a read or an +//! implied key is confirmed by the rule, not decided by it. +//! +//! So: compute `derived_paint_order` with the rule, and again with the +//! `forced_backdrop` fallback removed, and report every entry whose order moves. +//! Where nothing moves, the rule is decorative on that screen; where it moves, +//! the rule is the only thing holding the order up. +//! +//! Raised by the port agent 2026-08-30. Reach note in +//! `docs/re/structures/ui-forced-backdrop.md`. + +use std::path::PathBuf; + +use sylpheed_formats::{pak::PakArchive, ui_layout}; + +fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec { + let mut idx: Vec = (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 path = std::env::args() + .nth(1) + .unwrap_or_else(|| root.join("dat/GP_TITLE.pak").to_string_lossy().into()); + let ar = PakArchive::open(&path).expect("pak"); + println!("# {path}"); + println!("# entry forced decides elements note"); + + let mut decides = Vec::new(); + let mut agrees = Vec::new(); + 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); + + // Which elements does the rule fire on, and of those, which have no key + // of their own to fall back on? + let mut forced = Vec::new(); + let mut keyless = Vec::new(); + for el in &b.elements { + if !ui_layout::forced_backdrop(&b, el) { + continue; + } + forced.push(el.name.clone()); + let own = ui_layout::sprite_layer_key(&b, &by, el) + .or_else(|| ui_layout::implied_layer_key(&el.name)); + if own.is_none() { + keyless.push(el.name.clone()); + } + } + if forced.is_empty() { + continue; + } + let moved = with != without; + if moved { + decides.push(i); + } else { + agrees.push(i); + } + println!( + " {i:>5} {:>6} {:>7} {:>8} forced=[{}] keyless=[{}]", + forced.len(), + if moved { "YES" } else { "no" }, + b.elements.len(), + forced.join(","), + keyless.join(","), + ); + } + println!("\n# rule DECIDES the order on entries {decides:?}"); + println!("# rule merely AGREES on entries {agrees:?}"); +} diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 3ecd12b1..a00b12db 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2529,6 +2529,38 @@ whatever it returns — the same reason a truncated log and a t=0 render both lo fine from inside. Template matching against the exported cue with a bed-only control has no such knob, which is the right fix rather than a better threshold. +## ✅ 2026-08-30 — your forced-backdrop correction is RIGHT, and disc-wide it is bigger than you said + +You told me "stability is not necessity" and that removing `forced_backdrop` +leaves the four splashes byte-identical while `build_12`/`build_15` go black. +**Confirmed independently — from my crate, not yours.** I recomputed +`derived_paint_order` with the fallback removed and diffed the orders: + +| `GP_TITLE` entry | rule decides? | forced element | +|---|---|---| +| 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 | + +Two renderers, same answer. Your point about the agreement not being independent +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 +reproducing my crate. + +🔴 **Disc-wide it is not 2 of 6, it is 62 of 80.** Over every `dat/*.pak`, the rule +**decides** the order on **62** forced instances and merely agrees on 18. Every one +of the 62 is keyless; no keyed element is ever moved. The 80 reproduces this +corpus's own earlier census exactly, which is the check that the probe sees the +same set. + +**What that means for you:** nothing on your five screens beyond the two you +already identified — but it does mean the rule is not a decoration anywhere, and if +the blend-mode assumption under it ever fails, 38 `.prm` instances go with it (the +other 24 are `.tbm`, whose pixels this corpus cannot locate, so those are +"correct or inert" either way). + +[`structures/ui-forced-backdrop.md`](../re/structures/ui-forced-backdrop.md) · +[census](../re/data/forced-backdrop-necessity.txt) + ## 2026-08-30 — the Ⓐ blocker is SOLVED, and it was the emulator, not the game **This supersedes the section below, which stands as the record of the wrong diff --git a/docs/re/data/forced-backdrop-necessity.txt b/docs/re/data/forced-backdrop-necessity.txt new file mode 100644 index 00000000..60cc2032 --- /dev/null +++ b/docs/re/data/forced-backdrop-necessity.txt @@ -0,0 +1,253 @@ +# Does forced_backdrop DECIDE a screen's order, or merely AGREE with it? +# +# Produced by: cargo run -p sylpheed-formats --example forced_backdrop_necessity -- +# over every dat/*.pak, 2026-08-30. SYLPHEED_DISC=/disc. +# +# 'decides' = derived_paint_order() differs from the same sort with the +# forced_backdrop fallback removed. 'no' = the element's own read or implied +# key already puts it there, OR every element on the screen is forced so the +# declaration-index tie-break gives the same order either way. +# +# TOTALS: 80 forced instances = 62 decides + 18 agrees. +# The 80 reproduces the census in ui-forced-backdrop.md exactly. +# Every one of the 62 deciders is keyless; no keyed element is ever moved. +# +# /disc/dat/GP_BUNK.pak +# entry forced decides elements note + 0 1 YES 21 forced=[px_bunk_base.tbm] keyless=[px_bunk_base.tbm] + 2 1 YES 21 forced=[px_bunk_base.tbm] keyless=[px_bunk_base.tbm] + 4 1 YES 19 forced=[pvbase.tbm] keyless=[pvbase.tbm] + 6 1 YES 19 forced=[pvbase.tbm] keyless=[pvbase.tbm] + +# rule DECIDES the order on entries [0, 2, 4, 6] +# rule merely AGREES on entries [] +# /disc/dat/GP_CHALLENGE.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_DEBRIEFING_PILOTLOG.pak +# entry forced decides elements note + 118 1 YES 6 forced=[px_deb_base.tbm] keyless=[px_deb_base.tbm] + 130 1 YES 6 forced=[px_deb_base.tbm] keyless=[px_deb_base.tbm] + 131 1 YES 13 forced=[pjbgbase2.tbm] keyless=[pjbgbase2.tbm] + 134 1 YES 10 forced=[px_deb_base.tbm] keyless=[px_deb_base.tbm] + 150 1 YES 13 forced=[pjbgbase2.tbm] keyless=[pjbgbase2.tbm] + 165 1 YES 10 forced=[px_deb_base.tbm] keyless=[px_deb_base.tbm] + +# rule DECIDES the order on entries [118, 130, 131, 134, 150, 165] +# rule merely AGREES on entries [] +# /disc/dat/GP_DIALOG.pak +# entry forced decides elements note + 2 1 YES 15 forced=[pcbase.tbm] keyless=[pcbase.tbm] + 3 1 YES 15 forced=[pcbase.tbm] keyless=[pcbase.tbm] + 9 1 YES 34 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 10 1 YES 46 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 11 1 YES 38 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 12 1 YES 32 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 13 1 YES 32 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 14 1 YES 34 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 15 1 YES 26 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 16 1 YES 20 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 17 1 YES 30 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 18 1 YES 38 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 19 1 YES 30 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 20 1 YES 36 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 21 1 YES 38 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 22 1 YES 46 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 23 1 YES 34 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 24 1 YES 36 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 25 1 YES 14 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 26 1 YES 16 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 28 1 YES 14 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 29 1 YES 16 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 30 1 YES 18 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 31 1 YES 18 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 32 1 YES 10 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 33 1 YES 16 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 34 1 YES 14 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 35 1 YES 18 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 36 1 YES 16 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 37 1 YES 18 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 38 1 YES 26 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 39 1 YES 20 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 40 1 YES 18 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 41 1 YES 20 forced=[pzeff00.prm] keyless=[pzeff00.prm] + 86 1 YES 2 forced=[esrb_base.prm] keyless=[esrb_base.prm] + 130 1 YES 2 forced=[esrb_base.prm] keyless=[esrb_base.prm] + +# rule DECIDES the order on entries [2, 3, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 86, 130] +# rule merely AGREES on entries [] +# /disc/dat/GP_GAMEOVER.pak +# entry forced decides elements note + 4 1 YES 13 forced=[pnbase.tbm] keyless=[pnbase.tbm] + 7 1 YES 13 forced=[pnbase.tbm] keyless=[pnbase.tbm] + +# rule DECIDES the order on entries [4, 7] +# rule merely AGREES on entries [] +# /disc/dat/GP_HANGAR_ARSENAL.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_LEADERBOARD.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_D2D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_E.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_E2D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_F.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_F2D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_I.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_I2D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_J.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_J2D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_S.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MAIN_GAME_S2D.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MISSION_LOG.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_MISSION_SELECT.pak +# entry forced decides elements note + 3 1 YES 15 forced=[px_mission_base.tbm] keyless=[px_mission_base.tbm] + 5 1 YES 15 forced=[px_mission_base.tbm] keyless=[px_mission_base.tbm] + +# rule DECIDES the order on entries [3, 5] +# rule merely AGREES on entries [] +# /disc/dat/GP_MOVIE_THEATER.pak +# entry forced decides elements note + 0 1 YES 12 forced=[px_movie_base.tbm] keyless=[px_movie_base.tbm] + 1 1 YES 12 forced=[px_movie_base.tbm] keyless=[px_movie_base.tbm] + +# rule DECIDES the order on entries [0, 1] +# rule merely AGREES on entries [] +# /disc/dat/GP_OPTIONS.pak +# entry forced decides elements note + 0 2 no 2 forced=[po_menu_base.tbm,po_menu_base.tbm] keyless=[po_menu_base.tbm,po_menu_base.tbm] + 2 2 no 2 forced=[po_menu_base.tbm,po_menu_base.tbm] keyless=[po_menu_base.tbm,po_menu_base.tbm] + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [0, 2] +# /disc/dat/GP_PAUSE_MENU.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_READY_ROOM.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_SAVE_LOAD.pak +# entry forced decides elements note + 37 2 no 13 forced=[pfbase.tbm,pfbase.tbm] keyless=[] + 40 2 no 13 forced=[pfbase.tbm,pfbase.tbm] keyless=[] + 46 1 YES 10 forced=[pgloading_eff00.prm] keyless=[pgloading_eff00.prm] + 58 1 no 9 forced=[pfbase.tbm] keyless=[] + 69 1 YES 10 forced=[pgloading_eff00.prm] keyless=[pgloading_eff00.prm] + 78 1 no 9 forced=[pfbase.tbm] keyless=[] + 89 1 YES 7 forced=[px_replay_base.tbm] keyless=[px_replay_base.tbm] + 93 2 no 13 forced=[pfbase.tbm,pfbase.tbm] keyless=[] + 98 1 YES 7 forced=[px_replay_base.tbm] keyless=[px_replay_base.tbm] + 99 2 no 13 forced=[pfbase.tbm,pfbase.tbm] keyless=[] + +# rule DECIDES the order on entries [46, 69, 89, 98] +# rule merely AGREES on entries [37, 40, 58, 78, 93, 99] +# /disc/dat/GP_STAGE_CLEAR.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/GP_SYSTEM.pak +# entry forced decides elements note + 0 1 YES 21 forced=[pqbase.tbm] keyless=[pqbase.tbm] + 1 1 YES 21 forced=[pqbase.tbm] keyless=[pqbase.tbm] + +# rule DECIDES the order on entries [0, 1] +# rule merely AGREES on entries [] +# /disc/dat/GP_TITLE.pak +# entry forced decides elements note + 10 1 no 3 forced=[palogo_eff0.prm] keyless=[] + 11 1 no 7 forced=[palogo_eff0.prm] keyless=[] + 12 1 YES 10 forced=[pgloading_eff00.prm] keyless=[pgloading_eff00.prm] + 13 1 no 3 forced=[palogo_eff0.prm] keyless=[] + 14 1 no 7 forced=[palogo_eff0.prm] keyless=[] + 15 1 YES 10 forced=[pgloading_eff00.prm] keyless=[pgloading_eff00.prm] + +# rule DECIDES the order on entries [12, 15] +# rule merely AGREES on entries [10, 11, 13, 14] +# /disc/dat/GP_TUTORIAL.pak +# entry forced decides elements note + 0 1 YES 18 forced=[pubase.tbm] keyless=[pubase.tbm] + 1 1 YES 18 forced=[pubase.tbm] keyless=[pubase.tbm] + +# rule DECIDES the order on entries [0, 1] +# rule merely AGREES on entries [] +# /disc/dat/fonts.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/sound.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] +# /disc/dat/tables.pak +# entry forced decides elements note + +# rule DECIDES the order on entries [] +# rule merely AGREES on entries [] diff --git a/docs/re/structures/ui-forced-backdrop.md b/docs/re/structures/ui-forced-backdrop.md index bb8ab864..4f1df18e 100644 --- a/docs/re/structures/ui-forced-backdrop.md +++ b/docs/re/structures/ui-forced-backdrop.md @@ -190,6 +190,81 @@ claim with the same consequence. Distinguishing them needs a capture of a screen that carries one, which is behind the Ⓐ fault ([`ui-clock-freezes-at-settle.md`](ui-clock-freezes-at-settle.md)). +## 🔴 Stability is not necessity — 62 of the 80 are DECIDED by this rule (2026-08-30) + +**Raised by the port agent, and it is right.** Every check this page has run — and +every re-check after a change elsewhere — measured whether a verdict *moved*. That +is the rule's **stability**. It never measured its **necessity**: an element whose +position is already fixed by a read `T8aD` key or an `implied_layer_key` is +*confirmed* by the rule, not *decided* by it, and on those screens removing the +rule entirely costs nothing. + +So the question was asked directly. `derived_paint_order` was recomputed with the +`forced_backdrop` fallback removed and the two orders compared, over every +`dat/*.pak`: + +| | instances | +|---|---| +| the rule **DECIDES** the order — it moves without it | **62** | +| the rule merely **AGREES** — order unchanged | 18 | +| **total forced** | **80** | + +The 80 reproduces this page's own census exactly, which is the check that the probe +is looking at the same set. Data: +[`../data/forced-backdrop-necessity.txt`](../data/forced-backdrop-necessity.txt) · +instrument: `crates/sylpheed-formats/examples/forced_backdrop_necessity.rs`. + +**Every one of the 62 deciders is keyless — no keyed element is ever moved.** That +is the rule behaving as designed: it is a fallback, and it only ever fires where +nothing else can speak. + +The 18 that merely agree split two ways, and the distinction matters: + +* **14 have a key of their own** — `pfbase.tbm` (10) and `palogo_eff0.prm` (4). Here + the sort already had the answer and the rule reproduces it. ⚠️ **This is the part + that is not independent support.** `palogo_eff0.prm` being forced first agrees + with its measured order, but its `implied_layer_key` is `0x00000000` and would + have put it first anyway — so that agreement is the rule reproducing the crate, + not the game confirming the rule. +* **4 are keyless but inert** — `po_menu_base.tbm`, twice in each of two 2-element + builds. *Every* element on those screens is forced, so all keys collapse to the + same value and the declaration-index tie-break gives the identical order either + way. + +### What actually holds the 62 up + +Not a key — there is none — and not the `palogo_eff0.prm` control, which as above +is only decisive under a convention its own key already satisfies. **It is the +impossibility argument alone**: a full-screen quad that is opaque at some instant +cannot paint above everything visible then, or the screen is blank at that instant. +That argument is doing all the work on 62 instances, and its assumptions are +exactly the ones listed under *Reach* below — which is now a larger exposure than +this page previously implied. + +By element, the 62: + +| | count | kind | +|---|---|---| +| `pzeff00.prm` | 32 | `.prm`, colour census says pure black | +| `pgloading_eff00.prm` | 4 | `.prm` — 2 in `GP_TITLE` (entries 12/15), 2 in `GP_SAVE_LOAD` | +| `esrb_base.prm` | 2 | `.prm` | +| ten `*base*.tbm` families | 24 | `.tbm`, ❔ pixels never located | + +⚠️ **24 of the 62 are `.tbm`**, and this page already records that a `.tbm`'s +pixels cannot be found anywhere on the disc. For those the two readings — "the rule +places it correctly" and "the element draws nothing, so its position is inert" — +remain indistinguishable, and both leave the composite right. The 38 `.prm` +deciders have no such escape: those are real quads with real colour, and the rule is +load-bearing on them in the full sense. + +### For the port specifically + +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 +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 +answer. + ## Reach ⚠️ **Assumes straight alpha-over blending.** Blend mode is ❔ on