port: H3 closes with every named cause dead, and the verify-screen rows are ADDITIVE, not gamma
Three answers landed from the Decoder in one iteration and two of them refuted
things -- one theirs, one mine.
H3 -- CLOSED, and deliberately not closed green. The rate is measured at 56.8
units per guest second (control 1.15 %, two elements agreeing at one clock).
30 and 120 are both excluded. At 56.8 the plate's t=236 lands at 4.15 s against
this port's 3.93 s, so the port is fractionally EARLY. With rest.t, the clock
origin and the anchor already eliminated, ALL FOUR named causes are now dead and
the human's observation is unattributed. Written down that way rather than
letting H3 go quiet: the strongest remaining candidate is that A was unbound on
the play-test build, so that human could not skip the 137 s intro, and the run
they judged is not the run any of these measurements describes.
`units = 2 x frames` is dead as a route -- the same animation takes 21 labels in
one capture and 33 in another. Audited: this port never used it. boot.gd
integrates `delta * units_per_second`, so the retirement cost authored/timing.json
a JUSTIFICATION and not a behaviour, and I have recorded which leg died and
which survived. The surviving leg has no frames in the chain: 12 declared units
against a 0.14-0.30 s measured black plateau = 40-86 units/s, which excludes 120
on its own. 60 is KEPT -- 56.8 is 5.6 % away against ~5 % quantisation.
H4 -- ANSWERED, and MY PREMISE WAS WRONG. I reported the verify-screen rows as a
blend-SPACE divergence. The transfer curve I built for that was a mean per
reference-value bucket over a BIMODAL population -- 53 % of pixels within 1 level
and 16 844 differing by 40+ -- and the gamma shape was an artefact of the
binning. A mean over a mixed population is not a transfer function.
All three renderers blend in the encoded space:
game k_8_8_8_8 on 2402/2402 and 33779/33791 draws, _GAMMA zero times
Godot measured with a pre-registered control -- alphas 64/128/192 render
64/128/192 exactly; linear-then-re-encode excluded by 33-74 levels
reference integer math straight on 8-bit values, no linearisation
The real cause is ADDITIVE. The port draws 5 elements additive on main_menu and
9 on extras, transcribed from the Decoder's per-draw RB_BLENDCONTROL0 log off the
running game; ui_layout.rs has exactly two blend sites, both alpha-over, and line
1169 records that it tried additive and refuted it from its own composite
metrics. The two renderers disagree on purpose, and the disagreement tracks the
set size: 9 -> 6.74, 5 -> 3.94, 0 -> ~0.7. That ordering was not fitted; the
additive map predates this comparison.
check-all: the allowance is now DERIVED from authored/rendering.json's additive
set plus the two legacy rows, instead of a literal list. A screen is excused
BECAUSE it has additive elements the reference cannot draw; one that differs
without them still fails, which a list could not express. Verified against the
last run's set: main_menu and extras pass, main_menu_jp, extras_jp, build_12 and
build_15 still FAIL. check-all stays red on those four and they are H5 -- not
diagnosed, not excused. build_12 and build_15 report byte-identical statistics,
which points at one shared element rather than two coincidences.
Refutation attempts: the hit this iteration is against my own page again -- the
blend-space diagnosis, killed by a control I ran on my own renderer. The
Decoder's encoded-space finding and their 56.8 both survived the only checks I
could make: my control reproduces their space independently, and their rate is
consistent with authored/timing.json's frame-free leg.
Not settled: H5's four rows; whether the splashes tick at the title's rate (they
are a different GamePart); H1 (key repeat); what the human actually saw.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AHUQvXGyNcKonSEWsgWcX
This commit is contained in:
@@ -28,10 +28,45 @@
|
||||
"running game as HANDOFF Q1 (a declared t=30 landing on the linear value at",
|
||||
"every one of seven sampled frames). The port renders at its own frame rate and",
|
||||
"converts through this constant, so guest pacing cannot reach it. The two",
|
||||
"numbers are not comparable and one is not evidence about the other."
|
||||
"numbers are not comparable and one is not evidence about the other.",
|
||||
"",
|
||||
"🔴 2026-09-01 — THE FIRST LEG ABOVE IS RETIRED. THE VALUE IS NOT.",
|
||||
"",
|
||||
"'2 units per rendered frame ... a 30 Hz game, giving 60 units per second' is a",
|
||||
"FRAME-COUNT derivation, and the Decoder retired that mechanism the same day",
|
||||
"(docs/re/units-per-second-measured.md): the same animation takes 21 frame",
|
||||
"labels in one capture and 33 in another, and one splash logo steps +136,+34 in",
|
||||
"one run and +17,+51,+34,+34,+17,+17 in the other. A fixed per-frame increment",
|
||||
"cannot do that. The clock is TIME-INTEGRATED, not frame-counted, so `units =",
|
||||
"2 x frames` computes an emulator artefact. The 2 was that run's frame pacing.",
|
||||
"",
|
||||
"✅ The port's RUNTIME was already right: `boot.gd` advances",
|
||||
"`time_units += delta * units_per_second`, off delta time. Nothing in this port",
|
||||
"derives a unit from a frame count. Audited 2026-09-01, and it is why the",
|
||||
"retirement cost a justification and not a behaviour.",
|
||||
"",
|
||||
"✅ AND THE SECOND LEG NEVER TOUCHED A FRAME COUNT, which is why 60 survives:",
|
||||
"the transition quad is declared black for 12 units and the capture bracketed",
|
||||
"the pure-black plateau at 0.14-0.30 s (title-plate-delay-measured.md, at a",
|
||||
"0.125 s sampling resolution). 12 units in 0.14-0.30 s is 40-86 units/s. That",
|
||||
"is a declared unit count against a wall-clock duration, with no frames in the",
|
||||
"chain -- and it EXCLUDES 120 units/s, which would need 0.10 s.",
|
||||
"",
|
||||
"✅ MEASURED DIRECTLY 2026-09-01: 56.8 units per guest second, control passing",
|
||||
"at 1.15 %, from two elements agreeing at one clock (`ptbtn00` 657.9 alpha/s,",
|
||||
"`ptcopyright` 650.4 alpha/s, which puts ptcopyright's segment at T = 22.25 --",
|
||||
"a rate agreement AND a round declared length). 30 and 120 are both excluded.",
|
||||
"",
|
||||
"60 IS KEPT. 56.8 is 5.6 % away against a ~5 % quantisation resolution, so it",
|
||||
"does not refute 60, and the Decoder explicitly did not ask for a change. The",
|
||||
"reach is the TITLE: the splashes are a different GamePart and nothing yet shows",
|
||||
"they tick at the same rate.",
|
||||
"",
|
||||
"⚠️ If anyone re-fits this from alpha: DROP THE LAST STEP of a ramp. It clamps",
|
||||
"at 255 and reports more elapsed time than it consumed -- worth 4 % on the plate."
|
||||
],
|
||||
"kind": "measured",
|
||||
"source": "/reborn docs/port/HANDOFF.md Q1, docs/re/ui-keyframe-time-unit.md",
|
||||
"source": "/reborn docs/port/HANDOFF.md Q1, docs/re/ui-keyframe-time-unit.md, docs/re/units-per-second-measured.md",
|
||||
"ramp": "linear",
|
||||
"ramp_why": [
|
||||
"Also HANDOFF Q1, and part of the same measurement: the fade lands on the",
|
||||
|
||||
@@ -106,7 +106,7 @@ method finding that applies to the Decoder's harness as much as this one:
|
||||
|---|---|---|
|
||||
| **H1** | **Does a held direction REPEAT in the menus, and at what rate?** Initial delay and repeat interval, for the d-pad and for the left stick separately if they differ. | Behavioural. The port now does **one step per deflection** — `authored`, and deliberately the conservative half: it cannot run away and it invents no rate. If the game repeats, a human will feel it as *"I have to flick it again"*. |
|
||||
| **H2** | ✅ **ANSWERED 2026-09-01, and the port's own premise was wrong.** The Decoder found **no post-process pass** on either splash (`auto/frame-blend-draw-path` `3cc1b51`). The blur is **in the asset**: every logo ships a pre-blurred copy ~21×20 px larger and concentric, drawn alone first and crossfaded out as the sharp logo fades in — which is the human's *"blurred to clear/sharp, in about a second"*, with no shader. | 🔴 **"The port applies no blur at all" was FALSE and this page said it.** The port draws all seven quads of the developer splash, the three companions included, and always has. Verified by a frozen sweep, 3 units a step: companions alone at u=3–15, all seven at u=18–42, sharp only from u=45. Writing down what the renderer was *believed* to do instead of running it is what produced the claim. What remains is the **extent**, and it is the same unit question as H3 — asked there, once. See [`plate-arrival-halves.md`](plate-arrival-halves.md). |
|
||||
| **H3** | ✅ **ANSWERED IN BOTH HALVES 2026-09-01** by `h3-units-per-frame-measured.md` (`origin/auto/frame-blend-draw-path` at `a482d9a`), against a pre-registration committed before the capture was read. **units/frame = 2** (three consecutive gap-free steps of exactly 23 on `ptbtn00`'s own ramp; `255×2/22 = 23.18`). **The anchor is t=160** — `ptcopyright`, calibrating to t≈168–176, 8–16 units from B and 50–58 from A. | 🔴 **Two of the port's own conclusions were refuted by this and are struck in [`plate-arrival-halves.md`](plate-arrival-halves.md), not deleted.** (1) My **5 units/frame** was an artefact of two errors: an alpha step is not a clock rate (`Δα/frame = 255×(units/frame)/T`, and I read steps off elements with different `T`), and my "onsets" were first-submissions already at α=85. The three intervals agreed because they shared the bias, not because they corroborated. (2) My claim that the t=160 anchor **collapses `clock: "shared"`** is **withdrawn** — I falsified it using the 2.135 s figure, the one number the same capture puts in dispute. Against the new capture's own labels the declared 54 and 76 units reconstruct at 44 and 66, short by 13–19 % in the same direction, with empty labels a named candidate for the residual. `clock: "shared"` stands. **Still open, and the only thing left: `units/second = 2 × guest fps`, and nobody has established whether the guest is 30 Hz or 60 Hz.** 2×30 = 60 (what the port ships); 2×60 = 120, which puts the plate at **1.97 s** instead of 3.93 s — the size of what the human reported. The Decoder asked the port not to move off 60 on their account yet, and it has not. |
|
||||
| **H3** | ✅ **CLOSED 2026-09-01. ALL FOUR NAMED CAUSES ARE DEAD AND THE HUMAN'S OBSERVATION IS NOT — recorded that way deliberately rather than left green.** The rate is **56.8 units per guest second**, measured, control at 1.15 %, two elements agreeing at one clock (`units-per-second-measured.md`). 30 and 120 both excluded. At 56.8 the plate's t=236 lands at **4.15 s** against the port's 3.93 s — the port is fractionally **early**. | Eliminated in order: `rest.t` (the arrival is a declared keyframe), the clock origin (85/85 filmed frames share one clock), the anchor (answered t=160, and `clock: "shared"` survives it), the unit constant (56.8). 🔴 **`units = 2 × frames` is dead as a route** — the same animation takes 21 labels in one capture and 33 in another. ✅ Audited: this port never used it; `boot.gd` integrates `delta * units_per_second`, so the retirement cost a *justification* in `authored/timing.json` and not a behaviour, and that file's second leg (12 declared units against a 0.14–0.30 s black plateau = 40–86 units/s) has no frames in the chain. **60 units/s is KEPT** — 56.8 is 5.6 % away against ~5 % quantisation and the Decoder did not ask for a move. ⚠️ Reach is the **title**; the splashes are a different `GamePart`. What the human saw is now unattributed — see [`plate-arrival-halves.md`](plate-arrival-halves.md) for the two remaining candidates, of which the strongest is that **Ⓐ was unbound on the play-test build so that human could not skip the 137 s intro**, and the run they judged is not the run any of these measurements describes. |
|
||||
|
||||
**On H2, three things the port can say that narrow it**, none of which settle it:
|
||||
|
||||
@@ -133,7 +133,8 @@ submitted per frame.
|
||||
|
||||
| # | ask | why the port cannot answer it |
|
||||
|---|---|---|
|
||||
| **H4** | **In what colour space is the UI alpha blend evaluated?** You decoded the *equation* from the pixel shader's ucode on 2026-09-01 — source-over, shader premultiplies, `ONE/INV_SRC_ALPHA`. This asks the space, not the equation: is the blend done on sRGB-encoded values, or linearised and re-encoded? | Behavioural / GPU state. `verify-screen` newly reports `DIFFERS` on six screens, and the divergence is **exactly zero on 18 081 unblended pixels** and gamma-shaped on every blended one — the signature of two renderers blending in different spaces. Ours is 16 % closer to `live-main-menu.png` than `sylpheed-cli` is, which is a direction and not a verdict. `check-all` is **left red** rather than the allowance widened. Derived from port `HEAD` `6eccfa8`; HANDOFF at `9ca1eb5`. See [`verify-screen-blend-divergence.md`](verify-screen-blend-divergence.md). |
|
||||
| **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 33–74 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** | **Four `verify-screen` rows have no diagnosis**: `main_menu_jp` (mean 0.79), `extras_jp` (0.66), `build_12` and `build_15` (both exactly 0.0368, over3 462, diff bbox `464x266+60+454`). None is in the additive map. `build_12` and `build_15` producing **byte-identical statistics** points at one shared element rather than two coincidences. | Not blocking — `check-all` is **left red** on these four and the allowance was not widened to cover them. It is now *derived* from `authored/rendering.json`'s additive set plus the two legacy rows, so a screen is excused because it has additive elements the reference cannot draw, and one that differs without them still fails. Port `HEAD` `da7864e`; HANDOFF `9ca1eb5`. |
|
||||
|
||||
## Still open — these block work
|
||||
|
||||
|
||||
@@ -313,7 +313,53 @@ arrives. *"The title has settled"* can only ever mean **the build-in elements
|
||||
have finished**, never *the screen has stopped changing* — which is the same
|
||||
distinction that `rest.t` keeps getting wrong.
|
||||
|
||||
### What is still open, and it is the only thing left
|
||||
### 🔴 ALL FOUR NAMED CAUSES ARE NOW DEAD, AND THE HUMAN'S OBSERVATION IS NOT
|
||||
|
||||
**The rate is measured: 56.8 units per guest second** (`units-per-second-measured.md`,
|
||||
control passing at 1.15 %, two elements agreeing at one clock — `ptbtn00` at
|
||||
657.9 α/s and `ptcopyright` at 650.4 α/s, which puts `ptcopyright`'s segment at
|
||||
`T = 22.25`, a round declared length nobody fitted). **30 and 120 are both
|
||||
excluded.**
|
||||
|
||||
At 56.8 units/s the plate's `t = 236` lands at **4.15 s** after clock zero,
|
||||
against the port's **3.93 s**. The port is fractionally **early**. So:
|
||||
|
||||
| candidate | verdict |
|
||||
|---|---|
|
||||
| `rest.t` | eliminated — the arrival is a declared keyframe |
|
||||
| clock origin | eliminated — 85/85 frames share one clock |
|
||||
| the anchor (t=118 vs t=160) | answered: t=160, and `clock: "shared"` survives it |
|
||||
| **the unit→seconds constant** | **eliminated — 56.8 measured; the port is early, not late** |
|
||||
|
||||
**Nothing named in the play-test explains what the human saw, and this page says
|
||||
so rather than quietly closing H3 green.** The observation stands and is now
|
||||
unattributed. Two things it could still be, neither of them the plate's own
|
||||
timing and neither established here:
|
||||
|
||||
* **what "late" was measured against.** Every number above is relative to the
|
||||
title's clock zero. A player experiences the plate relative to the *boot*, and
|
||||
the port's boot reaches the title at 7.86 s only because `--skip-at` presses Ⓐ.
|
||||
🔴 **On the play-test build Ⓐ was not bound to the pad at all**, so that human
|
||||
could not skip the 137 s intro — the run they judged is not the run any of
|
||||
these measurements describe.
|
||||
* **the splash dwells.** The rate's reach is the **title**. The splashes are a
|
||||
different `GamePart` and nothing yet shows they tick at 56.8; the Decoder has
|
||||
reading their `T` off the disc as a next item.
|
||||
|
||||
### The route that is dead regardless
|
||||
|
||||
🔴 **`units = 2 × frames` must not be used anywhere.** The same animation takes
|
||||
21 frame labels in one capture and 33 in another, and a splash logo steps
|
||||
`+136,+34` in one run and `+17,+51,+34,+34,+17,+17` in the other; a fixed
|
||||
per-frame increment cannot do that. The 2 was one run's frame pacing.
|
||||
|
||||
✅ **Audited, and the port never did this.** `boot.gd` advances
|
||||
`time_units += delta * units_per_second` off delta time. The retirement cost this
|
||||
port a *justification* in `authored/timing.json`, not a behaviour — and the
|
||||
justification's second leg (12 declared units against a 0.14–0.30 s measured
|
||||
black plateau, 40–86 units/s, no frames in the chain) never depended on it.
|
||||
|
||||
### What was still open, before the rate landed
|
||||
|
||||
`units/second = units/frame × guest fps`. The first factor is now **2**. The
|
||||
second is not established: **2 × 30 = 60** (what this port uses) and
|
||||
|
||||
@@ -1,107 +1,151 @@
|
||||
# `verify-screen` DIFFERS on six more screens — measured, cause not settled
|
||||
# `verify-screen` DIFFERS on six more screens — it is ADDITIVE, and the port is ahead of the reference
|
||||
|
||||
**Status:** 🟡 **measured, not diagnosed.** `check-all` is **red** on this and is
|
||||
deliberately left red. Written 2026-09-01 by the Port at `6eccfa8` + this commit.
|
||||
**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`.
|
||||
|
||||
## What changed
|
||||
> 🔴 **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.
|
||||
|
||||
`tools/port/check-all` allows `verify-screen` to report `DIFFERS` on exactly two
|
||||
screens, `title` and `title_jp`, each with a per-screen reason. It now reports
|
||||
eight:
|
||||
## The measurement that settles it
|
||||
|
||||
| screen | max | mean | over3 | allowed? |
|
||||
|---|---|---|---|---|
|
||||
| `title` | 41 | 0.4431 | 4 500 | ✅ sweep phase |
|
||||
| `title_jp` | 233 | 2.7715 | 65 329 | ✅ rest-pose sparkles |
|
||||
| **`main_menu`** | 97 | **3.9363** | 236 149 | 🔴 |
|
||||
| **`extras`** | 113 | **6.7422** | 387 419 | 🔴 |
|
||||
| **`main_menu_jp`** | 26 | 0.7885 | 3 248 | 🔴 |
|
||||
| **`extras_jp`** | 26 | 0.6592 | 3 163 | 🔴 |
|
||||
| **`build_12`** | 17 | 0.0368 | 462 | 🔴 |
|
||||
| **`build_15`** | 17 | 0.0368 | 462 | 🔴 |
|
||||
|
||||
## It is not this iteration's changes, and that was checked rather than assumed
|
||||
|
||||
The commit before this one touched `authored/flow.json` (comment fields only),
|
||||
`docs/`, comments in `boot.gd`, `_film_capture`, and `check-all`. The one
|
||||
plausible mechanism was the new `godot --headless --path port --import` step
|
||||
regenerating `port/.godot/`.
|
||||
|
||||
**It cannot be that.** `ExportTree.texture()` reads each sprite as bytes and
|
||||
builds it with `Image.load_png_from_buffer` + `ImageTexture.create_from_image` —
|
||||
the textures live outside `res://` and **never enter Godot's import pipeline at
|
||||
all**, so the import cache has no path to the drawn pixels. The mechanism is
|
||||
excluded structurally, not by a passing test.
|
||||
|
||||
## What the difference actually is
|
||||
|
||||
Not a moved or missing element. The `main_menu` diff covers the **whole frame**
|
||||
(bounding box `1280x720+0+0`), and the transfer curve from the reference's value
|
||||
to Godot's, over all 921 600 pixels and all three channels, is monotone,
|
||||
**zero at both ends, and largest in the mid-to-upper range**:
|
||||
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**:
|
||||
|
||||
```
|
||||
ref 3 → 3.33 (+0.33) ref 100 → 111.38 (+11.38)
|
||||
ref 20 → 21.12 (+1.12) ref 130 → 161.43 (+31.43)
|
||||
ref 50 → 55.74 (+5.74) ref 158 → 227.14 (+69.14)
|
||||
ref 80 → 86.60 (+6.60) ref 199 → 199.01 (+0.01) n = 8 425
|
||||
ref 204 → 204.16 (+0.16) n = 1 276
|
||||
ref 255 → 255.00 (+0.00) n = 8 380
|
||||
|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 rows that matter are the last three.** At 199, 204 and 255 — 18 081
|
||||
pixels — the two renderers agree to **a hundredth of a level**. Those are the
|
||||
opaque, unblended pixels. Everywhere a blend happens they diverge, and the
|
||||
divergence has the shape of a gamma curve.
|
||||
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.
|
||||
|
||||
**So this is a difference in what space the alpha blend is evaluated in**, not in
|
||||
geometry, not in content, not in which elements are drawn. Godot comes out
|
||||
brighter on every blended pixel and identical on every unblended one.
|
||||
## What it is
|
||||
|
||||
## Which renderer moved: the port is **closer to the game**, by 16 %
|
||||
**The port draws some elements ADDITIVE. The reference has no additive path at
|
||||
all.**
|
||||
|
||||
Both frames scored against `docs/re/captures/title-builds/live-main-menu.png`,
|
||||
cropped to the capture's 1279×675:
|
||||
`crates/sylpheed-formats/src/ui_layout.rs` has exactly two blend sites, lines
|
||||
1072 and 1174, and both are alpha-over:
|
||||
|
||||
| | RMSE vs the live capture |
|
||||
|---|---|
|
||||
| Godot (`main_menu.godot.png`) | **3151.96** (0.0481) |
|
||||
| `sylpheed-cli screen render` (`main_menu.ref.png`) | 3769.61 (0.0575) |
|
||||
```rust
|
||||
canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8;
|
||||
```
|
||||
|
||||
⚠️ **Read this as an ordering and nothing more.** `verify-screen` poses
|
||||
`--pose=rest`, and its own header is emphatic that a `--pose=rest` frame must
|
||||
never be scored against a capture — that mistake produced a published finding
|
||||
once already. The absolute numbers here are contaminated by exactly that. What
|
||||
survives is the *comparison*: **both sides carry the same pose contamination**,
|
||||
so which of the two is nearer the capture is still meaningful, while "how near"
|
||||
is not. Anyone quoting 0.0481 as the port's fidelity is repeating the mistake
|
||||
the header warns about.
|
||||
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.
|
||||
|
||||
## Why it is left red
|
||||
`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.
|
||||
|
||||
The `check-all` allowance carries its own warning — it was once *"a count with an
|
||||
excuse"*, and the excuse was measurably false twice running. Widening a named set
|
||||
because six rows appeared, on a cause I have measured but not diagnosed, is that
|
||||
same move a third time. The rows are real, they are new, and nobody has said
|
||||
which renderer is right.
|
||||
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.
|
||||
|
||||
Two facts point at a live answer rather than a stale allowance:
|
||||
## The prediction this makes, and it holds
|
||||
|
||||
* **Thirteen commits touched `crates/` since the allowance was written**
|
||||
(`ecd5e56`), so the reference has moved and the allowance has not been
|
||||
re-derived against it.
|
||||
* The Decoder decoded the splash blend from the **pixel shader's ucode** on
|
||||
2026-09-01 (`origin/auto/frame-blend-draw-path`): *straight source-over, the
|
||||
shader premultiplies, `ONE/INV_SRC_ALPHA` in the blend register*. That is a
|
||||
statement about the equation. **What space it is evaluated in is the question
|
||||
this page is asking**, and it is next door to what they already have open.
|
||||
If the divergence is the additive set, its size should scale with how many
|
||||
elements are in that set. It does:
|
||||
|
||||
Asked in [`BLOCKED.md`](BLOCKED.md).
|
||||
| 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.
|
||||
|
||||
## Still open — 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. It is *further from one capture on one screen at
|
||||
one pose*, which is a direction, not a verdict.
|
||||
* That the port's blend is right. The port being closer here is consistent with
|
||||
it being closer by accident.
|
||||
* Anything about `title` or `title_jp`, whose existing reasons are unaffected.
|
||||
* 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.
|
||||
|
||||
@@ -135,8 +135,35 @@ echo
|
||||
echo "consistency (expected to differ, for a stated reason):"
|
||||
rc=0; env OUT="$OUT/screens" tools/port/verify-screen >"$OUT/verify-screen.log" 2>&1 || rc=$?
|
||||
differs=$(grep -c DIFFERS "$OUT/verify-screen.log" || true)
|
||||
# 🔴 THE ALLOWANCE IS DERIVED NOW, NOT LISTED, and that is strictly stronger.
|
||||
#
|
||||
# Six screens joined this set on 2026-09-01 and the cause is diagnosed for two of
|
||||
# them: the port draws some elements ADDITIVE -- transcribed from the Decoder's
|
||||
# per-draw RB_BLENDCONTROL0 log off the running game -- and the reference has no
|
||||
# additive path at all (ui_layout.rs has exactly two blend sites, both
|
||||
# alpha-over, and line 1169 records that it tried additive and refuted it from
|
||||
# its own composite metrics). So the two renderers disagree ON PURPOSE, and the
|
||||
# size of the disagreement tracks the size of the additive set: extras has 9
|
||||
# elements and a mean of 6.74, main_menu has 5 and 3.94, and the screens with
|
||||
# none sit an order of magnitude below.
|
||||
#
|
||||
# Computing the allowance from `authored/rendering.json` rather than listing it
|
||||
# means a screen is excused BECAUSE it has additive elements the reference
|
||||
# cannot draw, and a screen that differs WITHOUT them still fails -- which a
|
||||
# literal list could not express, and which keeps this from going stale against
|
||||
# the map it is derived from. main_menu_jp, extras_jp, build_12 and build_15 are
|
||||
# NOT in that map, are NOT diagnosed, and still fail.
|
||||
# docs/port/verify-screen-blend-divergence.md
|
||||
additive_screens=$(python3 -c "
|
||||
import json
|
||||
d=json.load(open('authored/rendering.json')).get('additive_elements') or {}
|
||||
print('\n'.join(k for k,v in d.items() if v))" 2>/dev/null)
|
||||
allow_args=(-e title -e title_jp)
|
||||
for sc in $additive_screens; do allow_args+=(-e "$sc"); done
|
||||
printf ' %-24s allowing %s (additive set + 2 legacy)\n' verify-screen \
|
||||
"$(echo $additive_screens | tr '\n' ' ')"
|
||||
unexpected=$(grep DIFFERS "$OUT/verify-screen.log" | awk '{print $1}' \
|
||||
| grep -vx -e title -e title_jp || true)
|
||||
| grep -vx "${allow_args[@]}" || true)
|
||||
|
||||
# 🔴 THE OLD ALLOWANCE WAS FALSE, AND MY FIRST REPLACEMENT REASON WAS ALSO
|
||||
# WRONG. Both are recorded because the second error is the more instructive.
|
||||
|
||||
Reference in New Issue
Block a user