re: primitive blend mode -- undecodable with reach, and no longer a risk

Looked in four places. The bundle has no field: a primitive has no RATC child at
all and the declaration words are constant across every element of three measured
screens -- the two grounds ui-prm-primitives.md already used to refute a
bundle-side LAYER key, and they apply identically to blend.

The colour census: every full-screen *eff00* primitive on the disc carries pure
black at its various alphas, and the only non-black primitive anywhere is
pbafc.prm, RGB 00e8e0 cyan.

The occlusion constraint cannot reach that one. pbafc.prm looked alarming at a
declared 844x600 and alpha ff; it is a small moving glint. It strobes between
alpha 255 and 124 every 2 units, travels from x=178 to x=291, and is scaled
2%x3%, so it draws about 17x18 pixels. At that size it occludes essentially
nothing.

The oracle is unavailable: GP_READY_ROOM is a recorded no-go and gameplay needs
the A press that faults the guest in this container.

But the consequence closes even though the question does not. For a BLACK quad
-- which is every primitive forced_backdrop touches -- the hypotheses differ only
in whether it hides what is beneath. Drawn first it is correct under both; drawn
last it is correct only under additive. So the rule's verdict is robust to the
open question, and the port's original "layerless sorts last" was wrong under
alpha-over and merely pointless under additive. This is explicitly NOT evidence
for alpha-over.

The investigation also found forced_backdrop judging coverage from the pivot
alone, ignoring scale -- pbafc.prm is the disc's own proof that a nominally
844x600 element can draw at 2%. Checked before changing anything: all 80 forced
instances are at scale 100% on every opaque instant, so no verdict moved. The
guard now requires scale >= 100 at the instants it counts as opaque. Defensive,
not a fix. 4 + 13 disc tests green either side.

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 21:58:25 +00:00
parent cb084e0a39
commit 65d309c553
5 changed files with 186 additions and 1 deletions

View File

@@ -0,0 +1,39 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main(){
let root=std::env::var("SYLPHEED_DISC").unwrap_or_else(|_|"/disc".into());
let ar=pak::PakArchive::open(format!("{root}/dat/GP_READY_ROOM.pak")).unwrap();
for (i,e) in ar.entries().iter().enumerate() {
let Ok(by)=ar.read(e) else { continue };
if !ratc::is_ratc(&by) { continue }
let Some(b)=ui_layout::parse_build(&by) else { continue };
let Some(p)=b.elements.iter().find(|el|el.name=="pbafc.prm") else { continue };
println!("=== entry {i}: {} elements ===", b.elements.len());
println!(" pbafc.prm pivot=({},{}) -> {}x{}", p.pivot_x,p.pivot_y,p.pivot_x*2,p.pivot_y*2);
for k in &p.keyframes {
println!(" t={:<5} fade={:08x} a={:<4} xy=({},{}) s={}/{}",
k.time.map(|v|v as i64).unwrap_or(-1), k.fade, k.fade>>24, k.x,k.y,k.scale_x,k.scale_y);
}
// what does it cover, and is anything visible while it is opaque?
let rest=p.rest().unwrap();
let (px,py,pw,ph)=(rest.x, rest.y, (p.pivot_x*2) as i32, (p.pivot_y*2) as i32);
println!(" its rect at rest: ({px},{py}) {pw}x{ph}");
let tmax=b.elements.iter().flat_map(|e|e.keyframes.iter().filter_map(|k|k.time)).max().unwrap_or(0);
let op:Vec<u32>=(0..=tmax).filter(|&t|p.pose_at(t).map(|k|k.fade>>24)==Some(255)).collect();
println!(" opaque at {} instants (t={:?}..{:?}) of 0..{tmax}", op.len(), op.first(), op.last());
let mut cov=0; let mut vis=0;
for o in &b.elements {
if o.index==p.index { continue }
let Some(ok)=o.rest() else { continue };
let (ow,oh)=((o.pivot_x*2) as i32,(o.pivot_y*2) as i32);
let overlap=(px+pw).min(ok.x+ow)-px.max(ok.x)>0 && (py+ph).min(ok.y+oh)-py.max(ok.y)>0;
if !overlap { continue }
cov+=1;
if op.iter().any(|&t| o.pose_at(t).map(|k|k.fade>>24).unwrap_or(0)>0) {
vis+=1;
if vis<=6 { println!(" covered AND visible while opaque: {}", o.name); }
}
}
println!(" elements its rect covers: {cov}; visible while it is opaque: {vis}");
break;
}
}

View File

@@ -960,6 +960,12 @@ pub fn forced_backdrop(build: &UiBuild, el: &Element) -> bool {
if el.sprite.is_some() {
return false;
}
// ⚠️ The declared size is the pivot doubled, but SCALE modifies it, and the
// disc proves the difference matters: `pbafc.prm` has a 844x600 pivot and a
// scale of 2 %/3 %, so it draws about 17x18 px. Checking the pivot alone
// would call it full-screen. No current verdict depends on this — all 80
// forced instances are at 100 % on every opaque instant — so this is a guard
// against data that has not been met, not a fix.
if (el.pivot_x * 2) < build.design_w as u32 || (el.pivot_y * 2) < build.design_h as u32 {
return false;
}
@@ -972,8 +978,14 @@ pub fn forced_backdrop(build: &UiBuild, el: &Element) -> bool {
if tmax == 0 {
return false;
}
// Opaque AND at full size: a quad scaled down does not cover what it would
// cover at 100 %, so it cannot occlude on the strength of its pivot.
let opaque: Vec<u32> = (0..=tmax)
.filter(|&t| el.pose_at(t).map(|k| k.fade >> 24) == Some(255))
.filter(|&t| {
el.pose_at(t).map_or(false, |k| {
k.fade >> 24 == 255 && k.scale_x >= 100 && k.scale_y >= 100
})
})
.collect();
if opaque.is_empty() {
return false;

View File

@@ -2481,3 +2481,51 @@ is 3.893, not the population 3.179, so my run is **1.88 σ** from the corpus mea
2.31 — less of an outlier than I credited myself with. Your t = 3.27 on 2 df, p ≈ 0.08
reproduces.
## 2026-08-29 — the blend question: open, and no longer a risk
**Undecodable here, with reach** — and you were right not to take it, because
the answer turns out not to change anything you draw.
**Where I looked.** The bundle has no field (a primitive has no RATC child at all,
and the declaration words are constant — already refuted for *layer*, and the same
two grounds apply). The colour census says every full-screen `*eff00*` primitive on
the disc is **pure black**; the only non-black primitive anywhere is `pbafc.prm`,
cyan `00e8e0`. The occlusion constraint cannot reach that one: it **strobes**
255/124 every 2 units, **travels**, and is scaled **2 %×3 %**, so it draws about
**17×18 px** rather than its declared 844×600 — a small moving glint that occludes
nothing. And `GP_READY_ROOM` is a recorded no-go with gameplay behind the Ⓐ fault.
**Why it stopped being a risk to the forced-backdrop rule.** For a *black* quad
the two hypotheses differ only in whether it hides what is beneath:
| | drawn **first** | drawn **last** |
|---|---|---|
| alpha-over, α=255 | correct | blanks the screen |
| additive, α=255 | correct (adds nothing) | correct |
**"First" is right under both; "last" is right under only one.** So the rule's
verdict is robust to the open question — and your original "layerless sorts last"
was wrong under alpha-over and merely pointless under additive, which is why it
showed as solid black rather than as nothing.
⚠️ **Not evidence that the blend is alpha-over.** It is the reason you can stop
waiting on it.
🔴 **One guard the investigation added, which nothing currently needs.**
`forced_backdrop` judged coverage from the **pivot alone**, ignoring scale —
`pbafc.prm` is the disc's own proof that a nominally 844×600 element can draw at
2 %. Checked before changing anything: **all 80 forced instances are at scale 100 %
on every opaque instant**, so no verdict moved. If you implemented the rule, add
the same scale check; it costs nothing and the data that would break it exists.
📌 Your `role == "primitive"` guard replacing `sprite.is_none()` is the right
shape, and better than mine was — a positive test for what the argument needs,
rather than the absence of a symptom.
📌 And your n=1 declaration on the P6 audio numbers: taken, and the burst-counter
note belongs with the truncation shape. A threshold set by two hand-picked
constants gives an answer determined by the constants, and is internally consistent
whatever it returns — the same reason a truncated log and a t=0 render both look
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.

View File

@@ -178,3 +178,4 @@ files, which is how the same ground got covered twice.
| [`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.105.61 s** and 3.834.30 s, 1520 % 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 6119, **3 frames with no sprite drawn**, developer glows 123, wordmarks 140209, 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 715 frames, making the apparent rate swing 0.01640.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 |
| [`structures/ui-prm-blend-mode.md`](structures/ui-prm-blend-mode.md) | Whether a primitive blends additively or alpha-over | ❔ **undecodable, with reach** — but the consequence is closed. Looked in **the bundle** (no field: the declaration words are constant and a primitive has no RATC child at all), **the colour census** (every full-screen `*eff00*` primitive is **pure black**; the only non-black primitive on the disc is `pbafc.prm`, cyan `00e8e0`), **the occlusion constraint** (inapplicable — `pbafc.prm` strobes 255/124 every 2 units, travels, and is scaled **2 %×3 %**, so it draws ~**17×18 px**, not its declared 844×600), and **the oracle** (`GP_READY_ROOM` is a recorded no-go and gameplay needs the Ⓐ that faults the guest). ✅ **Why it stopped mattering:** for a *black* quad the hypotheses differ only in whether it hides what is beneath — drawn **first** it is correct under **both**, drawn **last** only under additive. So `forced_backdrop`'s verdict is robust to the open question, and the port's original "layerless sorts last" was wrong under alpha-over and merely pointless under additive. ⚠️ This is not evidence *for* alpha-over. 🔴 The investigation found `forced_backdrop` judged coverage from the **pivot alone**, ignoring scale; checked first, **all 80 forced instances are at 100 %**, so no verdict moved and the added guard is defensive |

View File

@@ -0,0 +1,85 @@
# Primitive blend mode — narrowed to one element, and it does not matter
**Classification: undecodable, with reach.** Looked in the bundle, in the colour
census, in the occlusion constraint, and at the oracle. The question survives; its
*consequences* do not.
## The question
[`ui-prm-primitives.md`](ui-prm-primitives.md) leaves blend mode open: "a dim quad
at `0x7f000000` is presumably straight alpha over what is beneath, but the flash
(`0xf0ffffff`) and the coloured ones (`0x60ff0000`) may well be additive. Nothing
measured." [`ui-forced-backdrop.md`](ui-forced-backdrop.md) then assumed
alpha-over, so the assumption became load-bearing.
## Where I looked
**1. The bundle — nothing.** `ui-prm-primitives.md` already refuted a bundle-side
key for a primitive's *layer*, on two grounds that apply identically to blend: the
declaration entry's unread words are constant across every element of three
measured screens, and the bundle carries **no RATC child at all** for a primitive.
There is no field to read.
**2. The colour census — the population is overwhelmingly black.** Every
full-screen `*eff00*` primitive on the disc carries **pure black** at its various
alphas: `ff000000`, `7f000000`, `40000000`, `b2000000`, `cc000000`, `d4000000`,
`00000000`. The `*base*` elements are `ffffffff`. **The only non-black primitive on
the disc is `pbafc.prm`**, RGB `00e8e0` — cyan.
**3. The occlusion constraint — inapplicable to the one candidate.** `pbafc.prm`
looked alarming at a declared 844×600 and alpha `ff`. It is not what it looks like:
```
t=10 a=255 xy=(178,60) s=2/3 t=22 a=255 xy=(186,60) s=2/3
t=12 a=124 xy=(178,60) s=2/3 t=24 a=124 xy=(194,60) s=2/3
t=14 a=255 xy=(178,60) s=2/3 …travelling to x=291…
```
It **strobes** between alpha 255 and 124 every 2 units, it **travels** across the
screen, and its **scale is 2 % × 3 %** — so it draws about **17×18 pixels**, not
844×600. It is a small moving glint. At that size it occludes essentially nothing,
so the constraint that settles the backdrops cannot reach it.
**4. The oracle — not reachable.** `pbafc.prm` lives in `GP_READY_ROOM`, which
[`ready-room-probe.md`](../ready-room-probe.md) recorded as a no-go, and reaching
gameplay needs button input, which
[`ui-clock-freezes-at-settle.md`](ui-clock-freezes-at-settle.md) records as
faulting the guest in this container.
## ✅ Why it does not matter for the rule that depended on it
For a **black** quad — which is every primitive `forced_backdrop` touches — the
two hypotheses differ only in whether it hides what is beneath:
| | drawn **first** | drawn **last** |
|---|---|---|
| **alpha-over** black at α=255 | paints black, content over it — **correct** | blanks the screen — **wrong** |
| **additive** black at α=255 | adds nothing — **correct** | adds nothing — correct |
**"First" is right under both hypotheses; "last" is right only under one.** So the
forced-backdrop rule's *verdict* is robust to the blend question even though the
question is open — and the port's original "layerless sorts last", which produced
solid black on `build_12`/`build_15`, is wrong under alpha-over and merely
pointless under additive.
⚠️ **This is not evidence that the blend is alpha-over.** It is the reason the
open question stops being a risk. `pbafc.prm` remains genuinely unknown, and it is
the only element on the disc where the answer could change a pixel.
## 🔴 A guard the investigation added, which no current verdict needed
`forced_backdrop` tested coverage from the **pivot alone**. `pbafc.prm` is the
disc's own proof that scale can shrink a nominally full-screen element to 2 %.
Checked before changing anything: **all 80 forced instances are at scale 100 % on
every opaque instant**, so no verdict moved. The guard now requires
`scale_x >= 100 && scale_y >= 100` at the instants it counts as opaque — a guard
against data not yet met, not a fix. 4 + 13 disc tests green either side.
## Reach
⚠️ The colour census covers keyless elements. A **sprite**'s blend is a separate
question, and `T8aD +0x04` bit `0x02` was already refuted as an additive flag.
`pbafc.prm` is unsettled and would need either a `GP_READY_ROOM` capture or a
blend-state read from Canary, neither of which is available here.