From cb084e0a3913648124aa65e3cab1d2e616880609 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 21:51:06 +0000 Subject: [PATCH] re: the primitive colour census -- and it refutes 38 of my own 80 forced verdicts A disc-wide census of the ARGB that keyless elements carry. Every full-screen *eff00* PRIMITIVE is pure black at its various alphas (ff000000, 7f000000, 40000000, b2000000, cc000000, d4000000, 00000000). Black at alpha a over content is exactly an alpha-over dim or fade, and an additive black quad would be a no-op nobody would author -- so this narrows the open blend question a long way. The only non-black primitive on the disc is pbafc.prm, RGB 00e8e0 cyan at alphas up to ff, and it is 844x600, NOT full-screen, so it sits outside forced_backdrop's geometry guard. It is now the sole additive candidate. The census also refutes my own argument for nearly half its verdicts. Of the 80 forced-first instances only 42 are .prm; 38 are .tbm carrying fade ffffffff. A SOLID white quad at alpha 255 painted first would make the screen white, and no screen is white -- so a .tbm is a white modulation on a texture, and element alpha does not establish its coverage. That is the .t32 error one file extension further out. I guarded that with el.sprite.is_some(), which fixed the symptom and not the cause: an element's alpha is not its texture's opacity, and only an untextured primitive makes the two the same fact. So 42 verdicts stay decoded and 38 drop to inferred -- still almost certainly right, since all are named *base*, all are full-screen, and pfbase.tbm's first position is measured in the running game, but that is a name-and-role argument which this page elsewhere calls the weaker kind. The code is deliberately unchanged. Restricting forced_backdrop to .prm would send eleven screens' backgrounds back to u32::MAX -- last -- which is the blank-screen bug the rule was written to fix. Downgrading the status is honest; reverting the position would be wrong. The 42/38 split is pinned by a test so anyone tightening the rule sees what it costs. Separately, on the port's black_hold_units ask: four more no-input boots yielded one usable log, which armed late and missed the publisher splash, so the sample is still two runs spanning 3 and 4 frames. Their 6.5-9.2 range stands. And a reason it may not be resolvable this way: the draw log DROPS frame numbers -- in the 3-frame run, frames 121 and 124 are absent entirely, so "frames with no sprite" and "span of frame numbers" are different quantities. Their statistical correction is taken: at n=3 the sample SD (3.893) is the estimator, not the population SD (3.179), making my run 1.88 sigma from the corpus mean rather than 2.31. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd --- .../examples/prm_colour_census.rs | 37 +++++++++++++ .../tests/ui_forced_backdrop_disc.rs | 15 ++++- docs/port/HANDOFF.md | 55 +++++++++++++++++++ docs/re/INDEX.md | 1 + docs/re/structures/ui-forced-backdrop.md | 44 +++++++++++++++ 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 crates/sylpheed-formats/examples/prm_colour_census.rs diff --git a/crates/sylpheed-formats/examples/prm_colour_census.rs b/crates/sylpheed-formats/examples/prm_colour_census.rs new file mode 100644 index 00000000..133b89d2 --- /dev/null +++ b/crates/sylpheed-formats/examples/prm_colour_census.rs @@ -0,0 +1,37 @@ +//! What COLOUR is a primitive, and does any of them only make sense additively? +//! +//! `ui-prm-primitives.md` leaves blend mode open, and `forced_backdrop` assumes +//! straight alpha-over. A quad whose ARGB would tint the whole screen a colour no +//! screen shows is evidence against alpha-over for that quad. +use sylpheed_formats::{pak, ratc, ui_layout}; +use std::collections::BTreeMap; +fn main(){ + let root=std::env::var("SYLPHEED_DISC").unwrap_or_else(|_|"/disc".into()); + let mut paks:Vec<_>=std::fs::read_dir(format!("{root}/dat")).expect("dat/") + .flatten().map(|e|e.path()) + .filter(|p|p.extension().and_then(|s|s.to_str())==Some("pak")).collect(); + paks.sort(); + let mut m:BTreeMap>=BTreeMap::new(); + for p in &paks { + let Ok(ar)=pak::PakArchive::open(p) else { continue }; + for e in ar.entries() { + let Ok(by)=ar.read(e) else { continue }; + if !ratc::is_ratc(&by) { continue } + let Some(b)=ui_layout::parse_build(&by) else { continue }; + for el in &b.elements { + if el.sprite.is_some() { continue } + for k in &el.keyframes { + *m.entry(el.name.clone()).or_default() + .entry(format!("{:08x}", k.fade)).or_default()+=1; + } + } + } + } + println!("{:>26} fade ARGB values (count)", "keyless element"); + for (n,v) in &m { + let tot:usize=v.values().sum(); + if tot<4 { continue } + let s:Vec=v.iter().map(|(k,c)|format!("{k}x{c}")).collect(); + println!(" {n:>24} {}", s.join(" ")); + } +} diff --git a/crates/sylpheed-formats/tests/ui_forced_backdrop_disc.rs b/crates/sylpheed-formats/tests/ui_forced_backdrop_disc.rs index d2007d0f..ca4e9b36 100644 --- a/crates/sylpheed-formats/tests/ui_forced_backdrop_disc.rs +++ b/crates/sylpheed-formats/tests/ui_forced_backdrop_disc.rs @@ -147,7 +147,7 @@ fn forced_backdrops_are_full_screen_and_plentiful() { .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")) .collect(); paks.sort(); - let mut forced = 0usize; + let (mut forced, mut prm, mut tbm) = (0usize, 0usize, 0usize); for p in &paks { let Ok(ar) = PakArchive::open(p) else { continue }; for e in ar.entries() { @@ -161,6 +161,8 @@ fn forced_backdrops_are_full_screen_and_plentiful() { continue; } forced += 1; + if element.name.ends_with(".prm") { prm += 1 } + else if element.name.ends_with(".tbm") { tbm += 1 } // πŸ”΄ Untextured only. This assertion caught the rule's real // limit: applied to `.t32` sprites it claimed 22 of them must // sort first, against their own layer keys β€” a sprite's element @@ -181,4 +183,15 @@ fn forced_backdrops_are_full_screen_and_plentiful() { } assert!(forced > 50, "expected a real population, got {forced}"); eprintln!("{forced} keyless primitives have their position forced to first"); + + // πŸ”΄ Pin the split, so anyone tightening this rule sees what it would cost. + // Only the `.prm` half is DECODED: a solid colour quad's fade IS its pixel, so + // opacity and coverage are the same fact. Every `.tbm` in the set carries fade + // `ffffffff` β€” a white SOLID quad painted first would make the screen white, so + // they are textured, and element alpha does not establish their coverage. + // Their verdicts are kept because restricting to `.prm` would send eleven + // screens' backgrounds back to last, which is the bug this rule fixed. + assert!(prm >= 40 && tbm >= 30, + "expected roughly 42 .prm / 38 .tbm forced instances, got {prm} / {tbm} β€” \ +if this moved, re-read the self-refutation section of ui-forced-backdrop.md"); } diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 33e8a808..92f78f5f 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2426,3 +2426,58 @@ previous iteration now agree on which one it is. βœ… Untouched: the declared 255 and 210, your 4.400 / 3.650. +## 2026-08-29 β€” half the forced-backdrop verdicts are weaker than I told you + +πŸ”΄ **A census of what colour these elements carry refutes my own argument for 38 of +its 80 verdicts.** Nothing you have shipped needs to move, but the *status* does. + +| the 80 forced-first instances | count | fade ARGB | +|---|---|---| +| `.prm` β€” untextured solid quads | **42** | pure black | +| `.tbm` | **38** | **`ffffffff`** β€” white at full alpha | + +**A solid white quad at alpha 255 painted first would make the screen white.** No +screen is white β€” so a `.tbm` is not a solid quad; `ffffffff` is a white +*modulation on a texture*. Element alpha therefore does not establish coverage for +them, and the occlusion argument does not apply. + +That is the `.t32` mistake one file extension further out. I guarded it with +`el.sprite.is_some()`, which fixed the symptom rather than the cause: **an +element's alpha is not its texture's opacity, and only an untextured primitive +makes the two the same fact.** + +* βœ… **42 `.prm` verdicts stay decoded** β€” for a solid colour quad the fade *is* the + pixel. +* 🟑 **38 `.tbm` verdicts drop to inferred.** Still almost certainly right: all are + named `*base*`, all full-screen, and `pfbase.tbm`'s first position is **measured + in the running game**. But that is a name-and-role argument, which is the weaker + kind β€” if you implemented this rule, that half of it is not decoded. +* ⚠️ **I did not change the code**, and would not: restricting to `.prm` sends + `pcbase`, `pnbase`, `pqbase`, `pubase`, `pvbase`, `pjbgbase2`, `po_menu_base` and + the four `px_*_base` back to last β€” the blank-screen bug the rule was written to + fix. Downgrading the status is honest; reverting the position would be wrong. + +βœ… **And the blend question is much narrower now.** Every full-screen `*eff00*` +primitive on the disc is **pure black** β€” alpha-over dim/fade behaviour, and an +additive black quad would be a no-op nobody authors. The **only** non-black +primitive is `pbafc.prm` (cyan `00e8e0`), and it is **844Γ—600, not full-screen**, so +outside the rule entirely. ❔ It is now the sole additive candidate. + +### On `black_hold_units = 9` β€” I could not narrow it, and here is why + +πŸ”΄ **Your range is right and stands: ~6.5–9.2 units, with 9 at the top.** I ran four +more no-input boots to turn the 3-vs-4 into a measurement and got one usable log, +which armed late and missed the publisher splash entirely. So I still have **two** +runs, spanning 3 and 4 frames. + +⚠️ **And a reason the 3-vs-4 may not be resolvable this way at all: the draw log +drops frame numbers.** In the run with a 3-frame span, frames 121 and 124 are absent +from the log entirely β€” so "frames with no sprite" (2) and "span of frame numbers" +(3) are different quantities, and neither is certainly the guest's frame count. One +frame is a third of this value, exactly as you said. + +βœ… **Your statistical correction is right and I have taken it.** At n=3 the sample SD +is 3.893, not the population 3.179, so my run is **1.88 Οƒ** from the corpus mean, not +2.31 β€” less of an outlier than I credited myself with. Your t = 3.27 on 2 df, p β‰ˆ 0.08 +reproduces. + diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 854f0c0b..20b2650b 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -177,3 +177,4 @@ files, which is how the same ground got covered twice. | [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) *(span sensitivity)* | How much of the forced-backdrop rule rests on the timeline convention | βœ… **decoded**: the span is `0..=max keyframe time over every element`, and an element **holds** its final pose β€” decoded, not assumed ([`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md), [`ui-record-loop-length.md`](structures/ui-record-loop-length.md)). Sensitivity over the 130 keyless full-screen primitives with an opaque interval: using the header's declared **`+0x08`** instead changes **0** verdicts (interchangeable); using the primitive's **own** last keyframe changes **72**; counting elements **gone** after their last keyframe changes **72**. πŸ”΄ So the hold decides **55 %** of verdicts β€” and dropping it is **refuted by a measured order**: `palogo_eff0.prm` is a single keyframe at t=0, so without the hold it is opaque for one instant, nothing else is up, and the rule calls it *free* against a game measured painting it first. βœ… The verdicts that matter are convention-independent β€” `pgloading_eff00.prm` is FIRST under all four, `pteff00.prm` FREE under all four. ⚠️ The port's **256 vs 211** was a **bundle mismatch, not a definitional one**: `palogo_eff0.prm` runs to t=255 on the publisher splash (entries 10/13) and t=210 on the developer (11/14) | | [`structures/ui-clock-freezes-at-settle.md`](structures/ui-clock-freezes-at-settle.md) | The top-level clock stops at the settle point β€” observed in the running game | βœ… **measured**: `GP_TITLE` build 4 declares `t = 0…269`, about 120 presented frames at this run's pacing, and the dwell lasted **~1 100**. `ptcopyright` declares alphaβ‰₯1 for **106 units** (t=138…244) and is **drawn for 1 050 frames**; `ptlogo1` declares an exit at t=264 and is drawn for 1 095. Both vanish within three frames of the dwell ending. **The clock advances through the build-in, stops inside the settle window `[160,236]`, and holds; the exit ramp plays when the screen leaves, not on a timer** β€” [`ui-settle-time.md`](structures/ui-settle-time.md)'s decode observed from the other side. A nested record keeps looping on its own clock throughout. πŸ”΄ **This closes the 114-vs-120 gap, and it was my arithmetic**: 2.231 units/frame was regressed over *build-in* events (the only stretch the top-level clock advances) and applied to a period measured during the freeze β€” two different clocks. The declared **120** was never in doubt from the calibration-free dark-fraction test. βœ… The 51.158-frame period is now confirmed by a **second independent estimator** (autocorrelation, lag 51 with harmonics at 102/154) β€” ⚠️ whose first version **failed its control**, returning 48, because it indexed by sample position where the log's frame numbers have gaps. ❔ The **sweeps'** period stays unmeasured: the same validated estimator disagrees between two dwells of one screen (515 vs 452 frames). πŸ”΄ **Blocker: a single β’Ά on the title faults the guest** β€” 3 attempts, 2 register dumps of 223 MB and 519 MB, against 3 no-input runs that all completed; bounds menu-side dynamic RE here, and any scripted button press needs a `canary.stdout` size guard | | [`structures/boot-splash-dwells-are-declared.md`](structures/boot-splash-dwells-are-declared.md) | How long each boot splash is shown | βœ… **decoded**: the dwells are the bundles' own declared timelines β€” publisher **t=0…255 = 4.250 s**, developer **t=0…210 = 3.500 s** at 60 units/s. The corpus's independent screenshot timing over 3 cold boots gives 4.30/4.60/4.37 and **3.51/3.50/3.37** β€” the developer agreeing to **1.1 %**, two of its three runs to 0.3 %. πŸ”΄ **Wall clock is the wrong unit to author**: a fresh no-input boot measured the same two dwells at **5.10–5.61 s** and 3.83–4.30 s, 15–20 % longer than both the declared values and the corpus's runs, on the same disc β€” so a seconds figure is one run's emulator pacing. Boundaries from the draw stream: publisher wordmark frames 6–119, **3 frames with no sprite drawn**, developer glows 123, wordmarks 140–209, intro video 216. πŸ”΄ **The frameβ†’wall-clock instrument resolves to one BUFFER FLUSH, not one frame** β€” 69 of 125 samples showed no advance and the rest jumped 7–15 frames, making the apparent rate swing 0.0164–0.0316 s/frame; frames 119 and 123 fall in one burst, so the inter-splash gap is **not separable** by it. Quoted as brackets; sub-flush estimates withdrawn before reporting. ⚠️ `palogo_anima` never appears β€” almost certainly the 8-vertex cap (7 elements batched, 2 logged), the same trap as the `eff3` false negative, so it is named not reported. ❔ the publisher's 4.1 % error vs the developer's 1.1 % is unexplained | +| [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) *(colour census + self-refutation)* | What colour a keyless element is, and which forced verdicts the argument actually supports | βœ… **decoded, disc-wide**: every full-screen `*eff00*` **primitive** is **pure black** at its various alphas (`ff000000`, `7f000000`, `40000000`, `b2000000`, `cc000000`, `d4000000`, `00000000`) β€” exactly an alpha-over dim or fade, and an *additive* black quad would be a no-op nobody would author. The **only** non-black primitive on the disc is `pbafc.prm`, RGB `00e8e0` cyan at alphas to `ff`, and it is **844Γ—600, not full-screen**, so outside the backdrop rule's geometry guard β€” ❔ it is now the sole additive candidate. πŸ”΄ **Self-refutation: of the 80 forced-first instances only 42 are `.prm`; 38 are `.tbm` carrying fade `ffffffff`.** A *solid* white quad painted first would make the screen white and no screen is white, so a `.tbm` is a white **modulation on a texture** β€” and element alpha does not establish its coverage. That is the `.t32` error one extension further out: I had fixed the symptom (`el.sprite.is_some()`) not the cause, **an element's alpha is not its texture's opacity, and only an untextured primitive makes the two the same fact**. So 42 verdicts stay **decoded**, 38 drop to 🟑 (still almost certainly right β€” all named `*base*`, full-screen, and `pfbase.tbm`'s first position is *measured* β€” but on a name-and-role argument this page elsewhere calls the weaker kind). ⚠️ Code deliberately unchanged: restricting to `.prm` would send eleven screens' backgrounds back to last, the blank-screen bug the rule fixed. Split pinned by a test | diff --git a/docs/re/structures/ui-forced-backdrop.md b/docs/re/structures/ui-forced-backdrop.md index 8e49f6ad..99d5c6fb 100644 --- a/docs/re/structures/ui-forced-backdrop.md +++ b/docs/re/structures/ui-forced-backdrop.md @@ -129,6 +129,40 @@ being wrong. primitives, which are solid quads and do occlude what they cover. That is also the only case `derived_paint_order` consults it for. +## πŸ”΄ Self-refutation: the argument is sound for only 42 of the 80 + +A disc-wide census of the *colour* these elements carry breaks the rule's premise +for nearly half its verdicts. + +| the 80 forced-first instances | count | fade ARGB | +|---|---|---| +| `.prm` β€” untextured solid quads | **42** | pure black (`ff000000`, `7f000000`, `40000000`, …) | +| `.tbm` | **38** | **`ffffffff`** β€” white at full alpha | + +**A solid white quad at alpha 255 painted first would make the screen white.** No +screen is white. So a `.tbm` is not a solid quad: `ffffffff` is a white +*modulation* on a texture, which is exactly what a background bitmap carries. + +πŸ”΄ **And that means element alpha does not establish coverage for them** β€” the same +error the `.t32` guard already caught, one file extension further out. I fixed that +symptom (`el.sprite.is_some()`) rather than its cause: **an element's alpha is not +its texture's opacity, and only an untextured primitive makes the two the same +thing.** + +**What this does and does not change:** + +* βœ… The **42 `.prm`** verdicts stand as decoded. For a solid colour quad the fade + *is* the pixel, so opacity and coverage are the same fact. +* 🟑 The **38 `.tbm`** verdicts are **not** decoded. They are almost certainly still + right β€” every one is named `*base*`, is full-screen, and one of them + (`pfbase.tbm`) has its first position **measured in the running game** β€” but that + is a name-and-role argument, which this page elsewhere argues is the weaker kind. +* ⚠️ **The code is deliberately unchanged.** Restricting `forced_backdrop` to + `.prm` would send `pcbase`, `pnbase`, `pqbase`, `pubase`, `pvbase`, `pjbgbase2`, + `po_menu_base` and the four `px_*_base` back to `u32::MAX` β€” last β€” which is the + blank-screen bug this rule was written to fix. Downgrading their *status* is + honest; reverting their *position* would be wrong. + ## Reach ⚠️ **Assumes straight alpha-over blending.** Blend mode is ❔ on @@ -136,6 +170,16 @@ only case `derived_paint_order` consults it for. would not occlude, and the rule would then be placing it wrongly. The `palogo_eff0.prm` control is evidence the assumption holds at least there. +βœ… **The colour census narrows this a long way.** Every full-screen `*eff00*` +primitive on the disc is **pure black** at its various alphas β€” `ff000000`, +`7f000000`, `40000000`, `b2000000`, `cc000000`, `d4000000`, `00000000`. Black at +alpha *a* over content is exactly what an alpha-over dim or fade looks like, and an +*additive* black quad would be a no-op, so a designer would not author one. The +single non-black primitive on the disc is **`pbafc.prm`**, RGB `00e8e0` (cyan) at +alphas up to `ff` β€” and it is **844Γ—600, not full-screen**, so it is outside this +rule's geometry guard entirely. ❔ Whether *it* is additive is still open, and it is +now the only candidate. + ⚠️ **It gives a lower bound, not an ordering.** It settles the 80 instances where occlusion forces the position, and says nothing about the 50 where the primitive is opaque only part of the time β€” including `pteff00.prm`, whose place on top is