# `verify-screen` DIFFERS on six more screens β€” it is ADDITIVE, and the port is ahead of the reference **Status:** βœ… **RESOLVED.** The reference gained an additive path (`formats-pin-2026-09-01b`) and the divergence this page is about collapses **6Γ—**. Everything below stands; the last section is the measurement that closes it. Port `HEAD` `da7864e` + this commit; HANDOFF at `9ca1eb5`. > πŸ”΄ **This page said, in its first version, that the cause was a blend-SPACE > divergence β€” one renderer linearising and the other not. That was wrong, and > the way it was wrong is the useful part.** Both renderers demonstrably blend in > the encoded space. The transfer curve I built to support it averaged Godot's > value per *reference value* bucket, which collapsed a **bimodal** population β€” > a large majority differing by ≀1 level and a minority differing by 40+ β€” into a > smooth-looking curve that resembled gamma and was an artefact of the binning. > A mean over a mixed population is not a transfer function. ## The measurement that settles it 53 % of pixels agree to within **1 level** and 69 % to within 3. The rest are not spread over a curve; they are **concentrated in the middle of the frame**: ``` |delta| 0 : 134 969 (cum 14.6 %) > 3 : 282 166 px = 30.6 % |delta| 1 : 355 261 (cum 53.2 %) bbox: the whole frame, but |delta| 2 : 103 348 (cum 64.4 %) occupancy is ~zero in columns |delta| 3 : 45 856 (cum 69.4 %) 0,1,6,7 of 8 and heavy in 2..5 |delta| 40+: 16 844 (cum 100 %) ``` The ≀1 tier is integer truncation against float rounding β€” the reference computes `(sc*sa + dc*(255-sa)) / 255` in `u32` and truncates; Godot rounds. The 30.6 % is something else, and it is in the columns where the menu's content sits. ## What it is **The port draws some elements ADDITIVE. The reference has no additive path at all.** `crates/sylpheed-formats/src/ui_layout.rs` has exactly two blend sites, lines 1072 and 1174, and both are alpha-over: ```rust canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8; ``` and line 1169 carries the reason β€” an *"ADDITIVE selector and REFUTED β€” it moved every metric against the …"*. The reference tried additive, refuted it against its own composite metrics, and does not do it. `authored/rendering.json` gives the port an additive set per screen, and its `why` records that this is **transcribed, not authored**: the port proposed additive from a two-background composite solve, kept it a proposal because nothing on the disc selects a blend mode, and adopted it only when the Decoder logged **`RB_BLENDCONTROL0` per draw in Canary** and drove the game to both screens. So the two renderers disagree **on purpose**: one implements a measurement of the game's blend register, the other implements a refutation made from composite metrics before that measurement existed. ## The prediction this makes, and it holds If the divergence is the additive set, its size should scale with how many elements are in that set. It does: | screen | additive elements in `authored/rendering.json` | mean diff | |---|---|---| | `extras` | **9** β€” `pteff10 pteff20 ptframe3 ptframe4 pteff21 pteff22 pteff23 ptloop01 ptloop02` | **6.7422** | | `main_menu` | **5** β€” `pteff12 ptframe1 ptframe2 ptloop01 ptloop02` | **3.9363** | | `main_menu_jp` | **0** β€” not in the map | 0.7885 | | `extras_jp` | **0** β€” not in the map | 0.6592 | | `title` | **0** β€” present, deliberately empty | 0.4431 (the known sweep residual) | Nine beats five beats zero, in order, with the two zero-rows an order of magnitude below the two non-zero ones. That ordering was not fitted; the additive map was written before this comparison existed. ## And which side is right: the port, on the evidence there is * the additive set is a **measurement off the running game** β€” the blend control register, per draw β€” and the reference's alpha-over is a refutation from *renderer metrics*, which the protocol ranks below a capture; * scored against `docs/re/captures/title-builds/live-main-menu.png`, Godot is **RMSE 3151.96** and the reference **3769.61** β€” the port is 16 % closer. ⚠️ **That second line is an ordering and nothing more.** `verify-screen` poses `--pose=rest` and its own header is emphatic that such a frame must never be scored against a capture β€” that mistake produced a published finding once already. Both sides carry the same pose contamination, so *which* is nearer is still meaningful; *how near* is not. Nobody should quote 0.0481 as the port's fidelity. ## The blend SPACE, separately: both are encoded, and so is the game Worth keeping even though it turned out not to be the cause, because it closes `BLOCKED.md` H4 and it was measured rather than recalled. **Godot, measured with a control** β€” a white quad over black at three known modulate alphas, through the port's own texture path (`load_png_from_buffer` β†’ `ImageTexture`), with the port's `[rendering]` settings: | declared alpha | encoded-space prediction | linear-then-re-encode prediction | **measured** | |---|---|---|---| | 64 | 64 | 138 | **64** | | 128 | 128 | 188 | **128** | | 192 | 192 | 225 | **192** | Pre-registered before running; exact on all three, with the alternative excluded by 33–74 levels. **The reference:** integer arithmetic straight on 8-bit values, no linearisation anywhere in the file. **The game:** the Decoder's `blend-space-rt-format.txt` β€” `RB_COLOR_INFO` `color_format` is `k_8_8_8_8` on 2402/2402 splash draws and 33779/33791 of the boot-to-title capture, `k_8_8_8_8_GAMMA` appears **zero** times, and `color_exp_bias` is 0 throughout. `k_8_8_8_8_GAMMA` is the only format around which Canary applies a gamma↔linear conversion. **All three agree. Blend space is not a difference between anybody here**, and the port needs no change for it. ## The other four rows β€” localised, and two hypotheses died getting there `check-all` still fails on these four, and the allowance was not widened to cover them. What follows is where they are, not yet why. ### πŸ”΄ Hypothesis 1, refuted: a blend-space divergence Covered above. Killed by a control on my own renderer. ### πŸ”΄ Hypothesis 2, refuted: the port renders `rotation_deg` and the reference does not This one looked strong. `ui_layout.rs:86` says so in its own words β€” *"`rotation_deg` is decoded but NOT rendered"* β€” and the port does render it. A census over all sixteen exported screens gave: > **Every screen with a non-zero rotation at rest DIFFERS, and every screen with > none agrees** β€” 6 of 6 either way, including both legacy rows. **It is still wrong.** Widening the rule to *any* rotating element β€” a non-zero rest rotation **or** a two-keyframe 360Β° spinner at any depth β€” breaks it: `build_00` and `build_01` carry two spinners each and **agree** with the reference. Asked directly, the port's own draw log says it draws both spinners on those screens. So rotation is present, rendered, and produces no difference. A rule that holds on 14 of 16 and fails on the two cases nobody had looked at is the shape of a rule fitted to the rows it was built from. Recorded because the next person will find the `ui_layout.rs:86` comment and reach for it too. ### βœ… What is actually established: `build_12` and `build_15` are ONE element All **951** differing pixels lie inside `pgloading_loop5` (`pgloading_ring.png`, 333Γ—276 at `[1,444]`), and the diff's own bounding box β€” `x 69..301, y 478..710` β€” sits entirely within it. No other element's rectangle contains a differing pixel that this one does not. And that closes the `build_00` / `build_01` question without any rule about rotation: the port's draw log shows those two screens draw **7** elements and `build_12` draws **10**. `pgloading_loop5` is one of the three extra. The loading screens that agree are the ones that never draw the element the disagreement is in. Why the two renderers disagree *on that element* is not settled. It is small β€” max 17, mean 0.0368 β€” and the element is unusual: its top-level keyframes hold `a=0x7f` for eight units around `rest.t = 24`, while its **leaf** record expands `pgloading_ring` from `scale 0` to `1000` over t=30…130. The port's `draw_leaf_for` lists only `ptloop01` and `ptloop02`, so the port draws the element and not that leaf. Whether the reference does the same is the open half. ### 🟑 `main_menu_jp` and `extras_jp` β€” consistent with the sweep leaves, not established Both carry `ptloop01/pteff03` (rot 30, at x=1521) and `ptloop02/pteff03a` (rot βˆ’45, at x=βˆ’839) β€” the **same sweep leaves** whose phase residual is the already-named reason for `title`. Their means sit with `title`'s and nowhere near their own EN twins': | | mean | |---|---| | `title` (sweep residual, named since P1) | 0.4431 | | `extras_jp` | 0.6592 | | `main_menu_jp` | 0.7885 | | `main_menu` (5 additive) | 3.9363 | | `extras` (9 additive) | 6.7422 | That is consistent with the JP rows being the sweep residual alone, with no additive contribution β€” which is what the port's own map implies, since it lists no additive set for them. **Consistent with, not established:** nobody has isolated the sweep leaves on those two screens. ## βœ… 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 all nine of `extras`'. The port draws them **alpha-over** on the JP screens and **additive** on the EN ones, purely because `authored/rendering.json` is keyed by screen name and the Decoder's `RB_BLENDCONTROL0` log was taken on the EN screens. **The port is therefore asserting, by omission, that the JP build blends the same elements differently β€” and that is the less likely of the two possibilities.** Extending the map would extrapolate a measurement onto a build nobody drove to, which is not the port's to do; leaving it silent asserts the surprising thing by default. So it is made explicit in `authored/rendering.json` and asked in `BLOCKED.md`, and the map is **not** extended. ⚠️ It does not affect the deliverable: MISSION Β§7 puts localisation beyond English out of scope, and the JP screens are not in the boot path. ## Why `check-all` stays red Four rows are **not** explained by the additive set: | screen | mean | over3 | note | |---|---|---|---| | `main_menu_jp` | 0.7885 | 3 248 | not in the additive map, yet differs | | `extras_jp` | 0.6592 | 3 163 | same | | `build_12` | 0.0368 | 462 | tiny, and localised β€” diff bbox `464x266+60+454` | | `build_15` | 0.0368 | 462 | identical figures to `build_12` | `build_12` and `build_15` producing byte-identical statistics suggests one shared element rather than two coincidences. None of the four is diagnosed and none is excused. ## What changed in `check-all` The allowance is now **derived** rather than listed: a screen may differ if it has a non-empty additive set in `authored/rendering.json`, plus the two named legacy rows. That is strictly stronger than the hard-coded list β€” a screen that differs *without* additive elements now fails, which a literal list could not express, and the allowance cannot go stale against the map it is computed from. ## What this does not claim * That the reference is wrong to have refuted additive from its own metrics. It is superseded by a capture, which is a different thing. * That the port's additive set is complete. It covers three screens; nobody has logged the register on the rest. * Anything about the four undiagnosed rows. --- # πŸ”΄ Re-validated on the GPU, and my prediction failed: these numbers are rasteriser-specific The human activated a hardware GPU on 2026-09-01. **Every number on this page above was measured under `llvmpipe`**, so they needed re-deriving before anyone builds on them. ## Pre-registered (R2) > Both renderers blend in encoded 8-bit space, so if the port's drawing is > rasteriser-independent the diffs should be **identical, or within 1 level**. > Anything materially different means a published conclusion here was > GPU-specific. ## It failed | screen | llvmpipe mean | **GPU mean** | change | max (llvmpipe β†’ GPU) | |---|---|---|---|---| | `title` | 0.4431 | **0.5936** | **+34 %** | 41 β†’ 41 | | `main_menu` | 3.9363 | **4.1449** | +5.3 % | 97 β†’ 97 | | `extras` | 6.7422 | **6.9757** | +3.5 % | 113 β†’ 113 | | `title_jp` | 2.7715 | **2.9448** | +6.3 % | 233 β†’ 233 | | `main_menu_jp` | 0.7885 | **1.0157** | **+29 %** | 26 β†’ 27 | | `extras_jp` | 0.6592 | **0.8906** | **+35 %** | 26 β†’ 26 | | `build_12` / `build_15` | 0.0368 | **0.0454** | +23 % | 17 β†’ 17 | **Every mean rose, by 3–35 %.** So the diffs are *not* rasteriser-independent and the prediction was wrong. ## What survives, and what does not πŸ”΄ **The maxima are unchanged** β€” 41, 97, 113, 233, 17 identical, and 26 β†’ 27 on one row. The large differences are exactly where they were. That is the shape of a **rounding population growing, not content moving**: llvmpipe and the NVIDIA rasteriser round the last bit of a blend differently, so the ≀1-level tier grows while the elements that genuinely differ do not move at all. Consistent with both still blending in encoded space β€” which the control on this page established for Godot generally, not for one rasteriser. **What survives:** * the **additive diagnosis**, because it rests on an *ordering*, and the ordering holds on the GPU: `extras` 6.98 > `main_menu` 4.14 > `extras_jp` 0.89 and `main_menu_jp` 1.02 > `title` 0.59 β€” nine additive elements, then five, then none; * the **`pgloading_loop5` localisation**, which is a bounding box; * the **`build_00`/`build_01` agree** result β€” still 0 pixels over the bar; * the **derived allowance**: the failing set is the same four rows. **What does not, and is now labelled:** * the histogram (*53 % within 1 level, 16 844 over 40*) was llvmpipe-specific and the ≀1 tier is larger on the GPU; * every absolute **mean** quoted above this section; * the **RMSE-vs-capture** pair (3151.96 / 3769.61) was llvmpipe. The *ordering* claim β€” the port is nearer than the reference β€” has not been re-derived on the GPU and is not claimed here until it is. ## The rule this earns **A renderer comparison carries its rasteriser as a hidden parameter.** Nothing in this corpus recorded which one produced a diff, and for eight months there was only one so it never mattered. Any diff quoted from here on should say what drew it β€” the same discipline `TEMPORAL-VERIFICATION.md` already demands for capture rate, applied to the thing that rasterises rather than the thing that clocks. --- # βœ… Closed: the reference can draw additive now, and the divergence collapses 6Γ— The Decoder taught `ui_layout::blit` the additive blend at `formats-pin-2026-09-01b`, on the argument this page made β€” that the comparison was **structurally incapable**, and that the refutation which had kept additive out was `⟨render-vs-capture⟩`, i.e. that renderer disagreeing with itself while it had a stale keyframe association, no leaf geometry and no rotation. **Measured without pulling their branch into mine**: a detached worktree at the tag, `sylpheed-cli` built there, and `verify-screen` pointed at it through `SYLPHEED_CLI`. My branch is untouched and the reference on `main` is unchanged. ## Pre-registered (R2) > With the reference finally able to draw additive, the diffs caused by that gap > should collapse. `main_menu`/`extras` and their JP twins should fall sharply > from ~7, and whatever remains is a *different* cause. | screen | alpha-over reference | **additive reference** | factor | |---|---|---|---| | `main_menu` | 7.2580, max 105 | **1.2068, max 28** | **6.0Γ—** | | `main_menu_jp` | 7.3440, max 108 | **1.2111, max 31** | **6.1Γ—** | | `extras` | 6.9757, max 113 | **1.0229, max 28** | **6.8Γ—** | | `extras_jp` | 7.0734, max 115 | **1.0255, max 30** | **6.9Γ—** | | `title` | 1.0335, max 88 | **0.5685, max 41** | 1.8Γ— | | `title_jp` | 4.4944, max 233 | **2.8225, max 233** | 1.6Γ— | | `build_12` / `build_15` | 0.0772, max 60 | **0.0463, max 17** | 1.7Γ— | | `build_00` / `build_01` | 0.0676, max 60 | **0.0366, max 4** | **DIFFERS β†’ OK** | **`build_00` and `build_01` stop differing entirely** β€” `over3` 3 422 β†’ **0**. ## And the twins agree to a third of a percent | | | |---|---| | `main_menu` 1.2068 vs `main_menu_jp` 1.2111 | **0.36 % apart** | | `extras` 1.0229 vs `extras_jp` 1.0255 | **0.25 % apart** | Under the name-keyed map these pairs were 4.1Γ— and 7.8Γ— apart. Nothing was fitted to make that happen β€” the locale twins converged first when the *port* took the decoded field, and again now that the *reference* draws the same blend. ## The residual is smaller and its causes are the documented ones * `title_jp` 2.82 at max 233 remains the largest, and its named reason β€” `--pose=rest` sparkle handling β€” is untouched by any of this. * `title` 0.57 at max 41 is the `ptloop` sweep-phase residual, also untouched. * `main_menu`/`extras` and twins now sit at ~1.0–1.2, max ~28–31. **New, small, and undiagnosed** β€” additive was the dominant cause and not the only one. * `build_12`/`build_15` do **not** return to their pre-change 0.0368: they land at 0.0463 with both renderers drawing `pgloading_loop5` additive. So that element carries a small residual of its own beyond the blend. ## πŸ”΄ What must happen to `check-all`, and why it has NOT happened yet The allowance widened when the reference could not draw additive. **That justification is gone**, so the allowance should be narrowed back and the check should regain its teeth. **It is not narrowed in this commit, deliberately.** `check-all` builds the reference from the **workspace** `crates/sylpheed-formats`, and the additive path is at a tag that has not landed on `main`. Tightening now would turn `check-all` red against a reference that still cannot draw additive β€” a wall of failures meaning one thing, which is the exact defect the display guard was added for. **The trigger is mechanical**: when the additive path is on `main`, drop the export-derived clause and leave the two named legacy rows. The set that should then differ is measured above and is `title`, `title_jp`, `main_menu`, `extras`, `main_menu_jp`, `extras_jp`, `build_12`, `build_15` β€” with `build_00`/`build_01` expected to pass.