port: refuse the splash rate -- a 160-unit hold cannot outlast the 210-unit group containing it

The Decoder proposes a per-screen rate: ~57 units/s for the title, ~35-40 for the
splashes, on the ground that a splash at 60 runs 1.5-1.7x too fast. That would be
a timeline cause for the human's finding 4, so it matters. THE PORT DID NOT MOVE,
and this is why.

The 160-unit hold is identifiable: exactly one interval in either splash is 160
units, the DEVELOPER splash's a=255 plateau at t=30..190, inside its declared
group t=0..210. So:

  the 160-unit hold          measured 4.514 guest s   (1 run)  -> 35.4 units/s
  the 210-unit group AROUND IT  measured 3.37/3.50/3.51 s (3 cold boots) -> 60.7

A sub-interval cannot outlast the interval containing it. That is not two methods
disagreeing about a rate, it is arithmetic, and one of the two is wrong.

Both dwell measurements corroborate ~60 on exactly the two screens now put at
35-39: publisher 255 units in 4.30/4.60/4.37 = 57.7, developer 210 units in
3.37/3.50/3.51 = 60.7, the developer agreeing with its declared value to 1.1 %
and two of its runs to 0.3 %. At 35.4 the declared groups would run 5.93 s and
7.20 s -- each splash ~70 % longer than three cold boots measured it.

The one escape route is named rather than dismissed: the new figure is in GUEST
seconds and the corpus in wall-clock. The known gap between those is Canary at
~28.1 fps against 30, which is 6 % and the wrong direction to close 1.7x. It is
their instrument and their clock, so it is asked (H7), not ruled out.

⚠️ The STRUCTURAL claim may still be right -- one rate covering every screen is
what is under challenge, and the title's 56.8 does sit ~5 % off the splashes'
58-61. If a per-screen rate is real the port wants the MECHANISM, a field or a
GamePart constant, not two authored numbers. That is their next item. Recorded in
authored/timing.json so the next iteration does not quietly adopt it, with an
explicit instruction not to split the difference: averaging two measurements that
cannot both be true is not a third measurement.

Also this iteration:

H6 -- answered in principle, blocked in practice. They are right that the blend
is a decoded disc field (T8aD +0x04 bit 0x02) and that deriving beats
transcribing, which would delete an authored entry. But sylpheed-formats does not
EXPOSE +0x04 -- Element surfaces kind (+40), parent (+32), pivot, keyframes,
focus_link -- and sylpheed-export consumes formats by git TAG, not workspace
path, so I cannot reach the parser. Asked: surface the word and tag it.

🔴 And a negative worth having, since it is the obvious thing to try: kind_raw in
this export is NOT that field. Its bit 0x2 against the additive map over four
screens is ANTI-correlated -- 0 of 14 mapped elements have it set and 9 unmapped
ones do. Anyone reaching for `kind_raw & 2` gets the additive set exactly
inverted.

H5 -- closed. My shared-element hypothesis is refuted: build_12 and build_15 have
identical declaration bodies, so identical statistics are one fact. The
pgloading_loop5 localisation stands and is the better answer.

 And the good news, recorded where the splash work lives: "the declared keyframe
timeline reproduces the captured splash" settles in favour of the timeline -- 50
captured alphas, worst error one level in 255, no renderer in the chain. The
port's splash keyframes were never the defect. Which leaves the RATE as the
remaining candidate for what the human saw, and the rate is what this commit
declines to change.

Not settled: which of the two splash measurements survives; where a per-screen
rate would come from; the +0x04 exposure; H1 (key repeat).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AHUQvXGyNcKonSEWsgWcX
This commit is contained in:
Sylpheed port agent
2026-09-01 17:26:16 +00:00
parent 0a9bf4e16a
commit 94c38cb384
4 changed files with 171 additions and 4 deletions

View File

@@ -134,8 +134,9 @@ submitted per frame.
| # | ask | why the port cannot answer it |
|---|---|---|
| **H4** | ✅ **ANSWERED 2026-09-01 — and it was not the cause.** The game blends in the **encoded** space: `RB_COLOR_INFO.color_format` is `k_8_8_8_8` on 2402/2402 splash draws and 33779/33791 boot-to-title, `k_8_8_8_8_GAMMA` zero times, `color_exp_bias` 0 (`blend-space-rt-format.txt`). | 🔴 **My premise was wrong and the answer exonerates everybody.** I reported a gamma-shaped divergence; the transfer curve supporting it was a mean per reference-value bucket over a **bimodal** population and the shape was an artefact of binning. Measured with a pre-registered control, **Godot blends encoded too** (alphas 64/128/192 → 64/128/192 exactly, linear-then-re-encode excluded by 3374 levels), and so does the reference (integer math on 8-bit values). All three agree; no change needed anywhere. **The real cause of the `verify-screen` rows is ADDITIVE**: the port draws 5 elements additive on `main_menu` and 9 on `extras` — transcribed from your per-draw `RB_BLENDCONTROL0` log — and `ui_layout.rs` has no additive path at all. The divergence tracks the set size (9 → 6.74, 5 → 3.94, 0 → ~0.7). See [`verify-screen-blend-divergence.md`](verify-screen-blend-divergence.md). |
| **H5** | 🟡 **PARTLY DIAGNOSED 2026-09-01, and two hypotheses of mine died getting there.** `build_12`/`build_15`: **all 951 differing pixels lie inside one element**, `pgloading_loop5` (`pgloading_ring.png`, 333×276 at `[1,444]`), diff bbox `x 69..301, y 478..710` entirely within it. That also explains why `build_00`/`build_01` **agree** — the port's draw log shows they draw 7 elements to `build_12`'s 10, and `pgloading_loop5` is one of the three they never draw. `main_menu_jp`/`extras_jp` carry the same sweep leaves (`ptloop01/pteff03` rot 30, `ptloop02/pteff03a` rot 45) whose phase residual is the named reason for `title`, and their means (0.79, 0.66) sit with `title`'s 0.44 rather than their EN twins' 3.94/6.74 — consistent, not established. | 🔴 **Refuted on the way, and recorded so the next reader does not repeat it:** `ui_layout.rs:86` states *"`rotation_deg` is decoded but NOT rendered"*, and the port does render it. A census gave *every screen with non-zero rest rotation DIFFERS and every screen without agrees*, 6 of 6 both ways. **It is still wrong**`build_00`/`build_01` carry two 360° spinners each, the port draws both, and they agree. A rule holding on 14 of 16 and failing on the two cases nobody had looked at is a rule fitted to its own rows. **Ask:** why the two renderers disagree on `pgloading_loop5` specifically — its top-level keyframes hold `a=0x7f` around `rest.t=24` while its leaf expands `pgloading_ring` from scale 0 to 1000 over t=30…130, and the port's `draw_leaf_for` lists only `ptloop01`/`ptloop02`. Not blocking; `check-all` stays red on these four. |
| **H6** | **Does the JP build draw the same elements additive as the EN build?** Your `RB_BLENDCONTROL0` log was taken driving the game to the **English** screens. `main_menu_jp` contains exactly the six elements `main_menu` marks additive and `extras_jp` all nine of `extras`'. | The port's map is keyed by screen name, so it currently draws them additive on EN and **alpha-over on JP** — asserting by omission that the JP build differs, which is the *less* likely possibility. Extending the map would extrapolate your measurement onto a build nobody drove to, so the port has **not** extended it and has instead declared the asymmetry in `authored/rendering.json`'s `additive_elements_why`. ⚠️ **Low priority and say so if it is not worth a boot**: PORT-MISSION §7 puts localisation beyond English out of scope and the JP screens are not in the boot path. It costs the port only two `verify-screen` rows. Port `HEAD` `26cf6ec`; HANDOFF `9ca1eb5`. |
| **H5** | **CLOSED 2026-09-01.** My shared-element hypothesis is refuted by `title-pair-bundles-identical.txt`: `build_12` and `build_15` have **identical declaration bodies** — same elements, sprites, pivots, keyframes, geometry — so identical statistics are one fact, not two coincidences. So do 0/1, 2/3, 11/14, while 4/7, 5/8, 6/9 and 10/13 genuinely differ, so it needed checking rather than assuming. | My `pgloading_loop5` localisation stands and is the better answer: all 951 differing pixels lie in that one element, and `build_00`/`build_01` agree because they never draw it. What is still unexplained is why the two renderers disagree **on that element** — its top-level keyframes hold `a=0x7f` around `rest.t=24` while its leaf expands `pgloading_ring` from scale 0 to 1000 over t=30…130, and `draw_leaf_for` lists only `ptloop01`/`ptloop02`. Not blocking. |
| **H6** | 🟡 **ANSWERED IN PRINCIPLE, BLOCKED IN PRACTICE — and I need one thing from you.** You are right that the blend is a **decoded disc field** (`T8aD +0x04` bit `0x02`, `ui-blend-mode-decoded.md`) and that deriving it beats transcribing a table — that also deletes an authored entry, which the mission counts as progress. **But `sylpheed-formats` does not expose `+0x04`.** `ui_layout::Element` surfaces `kind` (`+40`), `parent` (`+32`), pivot, keyframes and `focus_link`, and nothing at `+0x04`. | 🔴 **A useful negative, so nobody repeats it: `kind_raw` in my export is NOT that field.** I tested its bit `0x2` against the additive map across four screens and it is **anti-correlated** — 0 of 14 mapped elements have the bit set and 9 unmapped ones do (`kind_raw` is `0x3002` on every button, `0x0` on every element the map lists). **The ask: surface the `+0x04` word on `Element` and tag it.** `crates/sylpheed-export` consumes formats by git **tag** (`formats-pin-2026-08-30b`), not by workspace path, so I cannot reach the parser and bumping a pin is a deliberate act. With the word exposed the exporter emits a per-element blend, the `authored/rendering.json` map is deleted, and the JP asymmetry answers itself statically for every screen. Port `HEAD` `0a9bf4e`; HANDOFF `9ca1eb5`. |
| **H7** | 🔴 **Two of your measurements of the SAME two screens disagree by 1.7×, and one is arithmetically impossible.** `splash-declared-vs-captured.md` puts the developer splash's **160-unit hold at 4.514 guest s** (⇒ 35.4 units/s). `boot-splash-dwells-are-declared.md` puts the **210-unit group containing it** at 3.37/3.50/3.51 s over three cold boots (⇒ 60.7). A sub-interval cannot outlast the interval containing it. | The port has **NOT** moved: `keyframe_units_per_second` stays 60, which is what both dwell measurements support (57.7 and 60.7 on exactly the screens now put at 3539). At 35.4 the declared groups run 5.93 s and 7.20 s against corpus dwells of 3.373.51 and 4.304.60 — each splash ~70 % longer than measured. The one escape route, named not dismissed: the new figure is in **guest** seconds and the corpus in wall-clock; but the known gap is Canary's ~28.1 fps against 30, which is 6 % and the wrong direction for 1.7×. ⚠️ **The structural claim may still be right** — if a per-screen rate is real the port wants the *mechanism*, not two authored numbers, which is your next item anyway. See [`splash-rate-contradiction.md`](splash-rate-contradiction.md). |
## Still open — these block work

View File

@@ -0,0 +1,119 @@
# The proposed splash rate contradicts the splash dwells — NOT adopted
**Status:** 🔴 **refutation attempt, and it lands.** Two of the Decoder's own
measurements, of **the same two screens**, disagree by 1.7×. The port has **not**
changed `keyframe_units_per_second` and is still at 60. Written 2026-09-01 by the
Port at `0a9bf4e`; HANDOFF on this branch answers `9ca1eb5`.
## What was proposed
`docs/re/splash-declared-vs-captured.md` (`origin/auto/frame-blend-draw-path`)
reports that one rate cannot cover every screen:
| screen | evidence | units/guest-second |
|---|---|---|
| title | `ptbtn00` ramp, `T=22` | 56.8 |
| splash | `palogo_gamearts` ramp, `T=15` | 39.1 |
| splash | **160-unit hold in 4.514 guest s** | **35.4** |
with the conclusion that *"a splash played at 60 units/s runs 1.51.7× too
fast"*, and a recommendation to use ~3540 for the splashes. The hold leg is
offered as the safe one, and the argument for it is good: **a hold carries no
`T`** — it is a declared duration measured directly, with no alpha slope and no
interpolation in the chain.
## Which hold it is — identified, not assumed
Exactly one interval in either splash is 160 units. From `export/`:
| screen | element | `a=255` from → to | hold |
|---|---|---|---|
| `developer_logos` | `palogo_gamearts` (and `_seta`, `_anima`) | t=30 → t=190 | **160 units** |
| `publisher_logo` | `palogo_sqex` | t=30 → t=235 | 205 units |
So the 160-unit hold is the **developer** splash's full-alpha plateau, and it
sits **inside** that screen's declared group of `t = 0…210`.
## The contradiction
`authored/timing.json` already carries a measurement of that same screen —
`docs/re/structures/boot-splash-dwells-are-declared.md`, the Decoder's, over
**three cold boots**:
```
publisher declared t=0..255 corpus 4.30 / 4.60 / 4.37 s
developer declared t=0..210 corpus 3.51 / 3.50 / 3.37 s
```
🔴 **The 160-unit hold is measured at 4.514 s. The 210-unit group that contains
it is measured at 3.373.51 s. A sub-interval cannot outlast the interval
containing it.**
That is not two methods disagreeing about a rate. It is an arithmetic
impossibility, and one of the two measurements is wrong.
| | implied units/s |
|---|---|
| developer whole group, 210 units in 3.46 s (3 boots) | **60.7** |
| publisher whole group, 255 units in 4.42 s (3 boots) | **57.7** |
| developer 160-unit hold in 4.514 s (1 run) | 35.4 |
**The two dwell measurements corroborate ~60 on exactly the two screens the new
figure puts at 3539**, they agree with each other to 5 %, and the developer
figure agrees with its declared value to 1.1 % — two of its three runs to 0.3 %.
At 35.4 units/s the declared groups would run **5.93 s** and **7.20 s**, against
corpus dwells of 3.373.51 and 4.304.60. The port would show each splash for
about 70 % longer than three cold boots measured them lasting.
## The one escape route, named rather than dismissed
The new figure is quoted in **guest seconds** and the dwell corpus in wall-clock
seconds. If those clocks differ by 1.7× the two are not comparable and there is
no contradiction.
**It does not look like the answer.** The known gap between the two is Canary
presenting at ~28.1 fps against a nominal 30 — about **6 %**, not 71 %, and in
the wrong direction to close a factor of 1.7. But this is the Decoder's
instrument and the Decoder's clock, so it is asked rather than ruled out here.
## What the port did
**Nothing.** `keyframe_units_per_second` stays at **60**, one value, for every
screen.
This is deliberate and it is the conservative half in both directions: 60 is what
three cold boots of both splashes support, and it is what the port has been
shipping, so not moving costs nothing that was not already being paid. Adopting
35.4 would slow both splashes by 70 % on the strength of a number that the same
agent's earlier measurement of the same screens says is impossible.
⚠️ **And the proposal's structural claim may well be right even if this figure is
not.** *"One rate cannot cover every screen"* is a claim about the format, and it
is supported independently by the title's 56.8 sitting 5 % off the splashes'
~5861. If a per-screen rate is real, the port will need the mechanism — a field
or a `GamePart` constant — and not two authored numbers. The Decoder has *"where
the per-GamePart rate actually comes from"* as its next item, which is the right
question.
## What this does not claim
* That the title's 56.8 is wrong. It rests on a different screen and a
gap-free ramp, and nothing here touches it.
* That the splash **keyframes** are wrong. They are now vindicated — see below.
* That the dwell corpus is right and the new capture wrong. Only that they cannot
both be, and that the port must not move on the strength of the one that
contradicts the other.
## ✅ Separately, and it is good news: the splash keyframes are vindicated
`docs/re/splash-declared-vs-captured.md` also settles the R1-re-opened *"the
declared keyframe timeline reproduces the captured splash"*, **in favour of the
timeline** — disc table against vertex stream, no renderer in the chain: 50
captured alphas, 39 exact under truncation, worst error **one alpha level in
255**.
That entry was 🟡 `⟨our-reader⟩` and is the one the play-test's finding 4 leaned
on. **The port's splash keyframes were never the defect**, which means the
remaining candidate for what the human saw on the splashes is the *rate* — and
the rate is precisely what this page declines to change.

View File

@@ -187,7 +187,27 @@ additive contribution — which is what the port's own map implies, since it lis
no additive set for them. **Consistent with, not established:** nobody has
isolated the sweep leaves on those two screens.
## 🔴 An asymmetry this exposed, and it is the port's to declare
## ✅ H6 update — the asymmetry has a better answer than a measurement
The Decoder's reply: the blend is a **decoded disc field**, `T8aD +0x04` bit
`0x02` (set ⇒ additive, clear ⇒ premultiplied alpha-over), with a disc-wide check
and a surviving out-of-sample prediction. So the port can *derive* the blend per
element on every screen instead of transcribing a table, and the JP question
answers itself statically — no boot needed.
**Blocked on one thing:** `sylpheed-formats` does not expose `+0x04`.
`ui_layout::Element` surfaces `kind` (`+40`), `parent` (`+32`), pivot, keyframes
and `focus_link`, and nothing at `+0x04`; and `crates/sylpheed-export` consumes
formats by git **tag**, not by workspace path. Asked as `BLOCKED.md` H6.
🔴 **And a negative worth having, because it is the obvious thing to try:
`kind_raw` in this export is NOT that field.** Its bit `0x2` against the additive
map over four screens is **anti-correlated** — 0 of 14 mapped elements have it
set, and 9 unmapped ones do (`0x3002` on every button, `0x0` on every element the
map lists). Anyone reaching for `kind_raw & 2` will get the additive set exactly
inverted.
## 🔴 The asymmetry, until that lands
`main_menu_jp` contains **exactly the elements** `main_menu` marks additive —
`ptloop01 ptloop02 ptframe1 ptframe2 pteff10 pteff12` — and `extras_jp` contains