Two things, and both are about a hidden parameter nobody was recording. 1. THE STICK THRESHOLD IS DECODED NOW, and it replaces an authored value. The Decoder measured that the game digitises the left stick to four direction bits at 61 % deflection, so it never sees a velocity. Gamepad.ENTER moves 0.5 -> 0.61. The 0.5 was never a chosen value: it was a FLOOR, because Godot's `ui_*` action deadzone is 0.50 and the latch must not arm below it. Between 0.50 and 0.61 Godot reports a direction the real game does not, and at 0.5 this port stepped there. The mechanism also corroborates the human's latch fix rather than merely agreeing with it: a control that digitises to bits cannot express a rate, so "one step per deflection" is what the hardware layer CAN produce. ⚠️ The 0.11 hysteresis gap stays AUTHORED -- nothing says the game has hysteresis at all. And a human chose 0.5, so this changes feel: revert the one constant if 0.61 reads as needing too much push. 🔴 AND THE CONTROL CAUGHT MY FIRST ATTEMPT AT ASSERTING IT. I added the new device-level row as subject "latch", and `verify-input --control` failed immediately with "a check did not invert -- it is not testing what it claims to test". It was right: removing the latch does not remove the THRESHOLD, the unlatched path tests `>= Gamepad.ENTER` too, so 0.55 counts 0 either way and the row could never invert. It is a NEGATIVE, and its positive control is the 0.70 row on the same shape. Reclassified. That also exposed a smaller thing: ok()'s negative branch HARDCODED "positive control is the stick row", so a second negative would have borrowed someone else's green line. It now takes the control's name, defaulting to the original text so the d-pad row is unchanged. ✅ I never consumed the pad bit table they have just corrected -- checked by grep over port/, authored/ and tools/port/, not remembered. 2. MY verify-screen NUMBERS WERE llvmpipe-SPECIFIC, and the prediction failed. Pre-registered: both renderers blend in encoded 8-bit space, so the diffs should be identical or within 1 level on the GPU. They are not -- every mean rose 3-35 %: title 0.4431 -> 0.5936, main_menu 3.9363 -> 4.1449, extras 6.7422 -> 6.9757, title_jp 2.7715 -> 2.9448, main_menu_jp 0.7885 -> 1.0157, extras_jp 0.6592 -> 0.8906, build_12/15 0.0368 -> 0.0454. But the MAXIMA are unchanged -- 41, 97, 113, 233, 17 identical, 26 -> 27 on one row. That is a rounding population growing, not content moving: two rasterisers round the last bit of a blend differently while the elements that genuinely differ do not move. Survives: the additive diagnosis, because it rests on an ORDERING and the ordering holds (9 elements > 5 > 0); the pgloading_loop5 localisation; the build_00/01 agreement; the derived allowance, same four failing rows. Does not, and is now labelled: the histogram (53 % within 1 level, 16 844 over 40); every absolute mean; and the RMSE-vs-capture pair 3151.96 / 3769.61 -- that ORDERING claim is not re-derived on the GPU and is not claimed until it is. The rule this earns: a renderer comparison carries its RASTERISER as a hidden parameter. Nothing here recorded which one produced a diff, and for months there was only one so it never mattered. Same discipline TEMPORAL-VERIFICATION already demands for capture rate, applied to what rasterises rather than what clocks. Not settled: H1's repeat half; H6's +0x04 exposure; the four red verify-screen rows; whether the port is still nearer the capture than the reference on the GPU. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AHUQvXGyNcKonSEWsgWcX
15 KiB
verify-screen DIFFERS on six more screens — it is ADDITIVE, and the port is ahead of the reference
Status: ✅ diagnosed for main_menu and extras, 🟡 open for four
others. check-all stays red, on the four. 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:
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:
extras6.98 >main_menu4.14 >extras_jp0.89 andmain_menu_jp1.02 >title0.59 — nine additive elements, then five, then none; - the
pgloading_loop5localisation, which is a bounding box; - the
build_00/build_01agree 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.