re(ui): settle 8AX vs ptbase statically -- the game draws the full-res one

I had parked this as "needs a per-draw capture recording texture base
addresses". It did not. 8AX (1280x720) and ptbase (640x360 at 200%) are
the SAME artwork at two resolutions, which is exactly why comparing
either against a capture is inconclusive -- and why comparing their
DIFFERENCE is not. Compute 8AX - upscale(ptbase), the detail only 8AX
has, and ask whether the capture contains it.

Both candidates are first mapped into the capture's tone domain with the
measured gamma; without that the residual is dominated by the tone
difference and the test is blind.

  main menu  corr +0.0475   controls +0.0032 shift, -0.0075 flip   68% of ceiling
  title      corr +0.0634   controls +0.0095 shift, +0.0086 flip   68% of ceiling

Two independent screens, both at 68% of the theoretical ceiling
(sd of the 8AX-only detail over sd of the capture residual), 7-15x their
matched controls. The controls preserve spatial correlation and destroy
only alignment, so they are what "no signal" looks like.

So the recommendation changes: resolve the name and draw 8AX at 1:1.
Upscaling ptbase 2x is wrong, not merely softer. Still do not draw both --
an opaque layer over an identical one costs fill and hides later changes,
and ptbase's element is the one carrying the keyframes, so a consumer
needs its timing with 8AX's pixels.

Also recorded and withdrawn: a cruder pixel-pair test gave 0.00-0.72 for
upscales, 0.98 native and 1.01 for the capture -- apparently decisive.
Additive noise raises both terms of that ratio equally and drives any
value toward 1; fitting a noise term, both "native + noise" and "bilinear
+ noise" reproduce the observed numbers. The conclusion is right, that
test does not establish it, and it is in REFUTED because the number looks
conclusive and is not.

Not shown: whether ptbase is also drawn underneath. 8AX is ~86% opaque
and carries the same art, so it would hide it either way.
This commit is contained in:
Sylpheed RE agent
2026-08-29 02:40:53 +00:00
parent 6c8ee2ee6a
commit 6c4e5d3d71
6 changed files with 140 additions and 15 deletions

View File

@@ -248,12 +248,21 @@ authored version can be deleted.
200 %) is *the same artwork at half resolution* — its 2× upscale differs from 200 %) is *the same artwork at half resolution* — its 2× upscale differs from
`8AX` by mean 2.05, and our background is pixel-identical to `8AX` in every `8AX` by mean 2.05, and our background is pixel-identical to `8AX` in every
patch sampled. patch sampled.
⚠️ **So resolving the name and drawing it as well would double-draw an opaque **SETTLED 2026-08-29 — the game draws the full-res `8AX`, so use it.**
full-screen layer** — invisible as a doubling, which is worse. If you want the Previously parked as "needs a per-draw capture"; it did not. The two carry the
sharper background, use `8AX` at 1:1 and *drop* `ptbase`; don't add it. same art at two resolutions, so what separates them is the detail `8AX` has
❔ Which of the two the game actually draws is not established — both carry the that an upscale cannot. Correlating the capture's departure-from-upscale
same art, and separating them needs a per-draw capture recording texture base against the 8AX-only detail (both first mapped through the measured gamma):
addresses (the two differ in size). main menu **+0.0475** vs controls +0.0032 / 0.0075, title **+0.0634** vs
+0.0095 / +0.0086 — **two screens, both 68 % of the theoretical ceiling, 715×
their matched controls**.
**So: resolve the name and draw `8AX` at 1:1.** Upscaling the 640×360 `ptbase`
2× is *wrong*, not merely softer. ⚠️ Do not draw **both** — an opaque
full-screen layer over an identical one costs fill and hides later changes; and
note `ptbase`'s element is the one carrying the keyframes, so you need its
timing with `8AX`'s pixels.
⚠️ It does not show whether `ptbase` is *also* drawn underneath — `8AX` is
~86 % opaque and would hide it either way.
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md) [`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
***Paint order: your exposure is two element pairs, on one screen.** We use ***Paint order: your exposure is two element pairs, on one screen.** We use

View File

@@ -598,3 +598,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
checker went from 1 038 resolving / 16 missing to 1 049 / 5. +11 and 11 checker went from 1 038 resolving / 16 missing to 1 049 / 5. +11 and 11
against 11 edits is the confirmation that the pass did what it said and touched against 11 edits is the confirmation that the pass did what it said and touched
nothing else. A bulk edit without that arithmetic is a hope. nothing else. A bulk edit without that arithmetic is a hope.
* **When two candidates carry the same content, compare their DIFFERENCE against
the oracle, not themselves.** `8AX` (1280×720) and `ptbase` (640×360 at 200 %)
are the same artwork, so no comparison of either against a capture can separate
them — every such test had been read as "inconclusive, needs a per-draw
capture". What separates them is `8AX upscale(ptbase)`: the detail only one
of them has. Correlating the capture's residual against *that* answered it
statically, on two screens, with matched controls.
* **A ratio that saturates at 1 under noise is not a discriminator.** A pixel-pair
test cleanly separated upscales (0.000.72) from native (0.98) and put the
capture at 1.01 — apparently decisive. Additive noise raises both terms of the
ratio equally and drives *any* value toward 1, and fitting a noise term showed
both hypotheses reproduce the observed numbers. Before believing a ratio, ask
what it does as noise grows.

View File

@@ -469,3 +469,16 @@ neighbourhood, not just the line.
already retracted it and located three cues in `Static.slb` that decode to PCM. already retracted it and located three cues in `Static.slb` that decode to PCM.
The retraction never reached the row the port agent reads. Handoff row fixed; The retraction never reached the row the port agent reads. Handoff row fixed;
`tools/re-capture/handoff_lint.py` now checks for this class. `tools/re-capture/handoff_lint.py` now checks for this class.
* "which of `8AX` and `ptbase` the game draws needs a per-draw capture recording
texture base addresses" → **mine, and refuted — it is settled statically.** The
two carry the same art at two resolutions, so neither compares usefully against
a capture; their *difference* does. Correlating the capture's
departure-from-upscale against the 8AX-only detail gives +0.0475 (main menu)
and +0.0634 (title), both 68 % of ceiling against matched controls of ≤0.0095.
The game draws the full-res `8AX`.
[`ui-8ax-fullres-background.md`](structures/ui-8ax-fullres-background.md)
* "the pixel-pair ratio shows the capture is native, not an upscale" → **mine,
and withdrawn as evidence.** Upscales give 0.000.72, native 0.98, capture 1.01
— but additive noise pushes any such ratio toward 1, and both "native + noise"
and "bilinear upscale + noise" fit the observed values. The conclusion happens
to be right; this test does not establish it.

View File

@@ -0,0 +1,2 @@
main menu: corr +0.0475 controls +0.0032 (shift) -0.0075 (flip) ceiling 0.070 -> 68% of it
title: corr +0.0634 controls +0.0095 (shift) +0.0086 (flip) ceiling 0.094 -> 68% of it

View File

@@ -68,17 +68,56 @@ Resolving `pteff05 → 8AX` and drawing it *in addition to* `ptbase` would
one. It would not be visible as a doubling, which is worse: it would silently one. It would not be visible as a doubling, which is worse: it would silently
cost fill and hide any future change to either layer. cost fill and hide any future change to either layer.
🟡 **The free win, if a port wants it:** use `8AX` at 1:1 and *drop* `ptbase`, **And that is now the recommendation, not just an option:** use `8AX` at 1:1.
rather than upscaling a 640×360 copy 2×. That is a sharper background from a The detail test above says the game's background carries 8AX's full-resolution
texture already in the bundle. Not done here — it is a rendering choice, and detail, so upscaling the 640×360 `ptbase` 2× is *wrong*, not merely softer.
`ptbase`'s element carries the keyframes. ⚠️ Still do not draw **both** — an opaque full-screen layer over an identical one
costs fill and hides any later change to either. `ptbase`'s element carries the
keyframes, so a consumer needs its timing with `8AX`'s pixels.
## ✅ Settled statically: the game draws the FULL-RES `8AX`
This was parked as "needs a per-draw capture". It did not — the two candidates
carry the same art at two resolutions, so what separates them is **the detail
`8AX` has that an upscale cannot**. Compute `8AX upscale(ptbase)` and ask
whether the live capture contains it.
[`tools/re-capture/eightax_detail_test.py`](../../../tools/re-capture/eightax_detail_test.py),
output at [`data/eightax-detail-test.txt`](../data/eightax-detail-test.txt).
Both candidates are first mapped into the capture's tone domain with the measured
gamma ([tone curve](ui-render-tone-curve.md)); without that the capture's
residual is dominated by the tone difference and the test is blind.
| screen | corr | control (shift 7 px) | control (flip) | ceiling | % of ceiling |
|---|---|---|---|---|---|
| main menu | **+0.0475** | +0.0032 | 0.0075 | 0.070 | **68 %** |
| title | **+0.0634** | +0.0095 | +0.0086 | 0.094 | **68 %** |
The "ceiling" is what a clean *"8AX is drawn"* would give, `sd(8AX-only detail) /
sd(capture residual)` — the 8AX-only detail is small (sd 1.94.1) against
everything else that differs (sd 2743), which is why the absolute correlation is
small and why the controls matter more than the magnitude.
**Two independent screens both land at 68 % of ceiling, 715× their matched
controls.** The controls preserve the spatial correlation structure and destroy
only the alignment, so they are what "no signal" looks like here.
### 🔴 An earlier, cruder version of this test does NOT support the conclusion
A pixel-pair test (a 2×-nearest upscale has identical adjacent columns) gave
ratio 0.00 for nearest, 0.600.72 for bilinear/bicubic/lanczos, 0.98 for native
`8AX`, and **1.01 for the capture** — apparently decisive. It is not: additive
noise raises both terms equally and pushes *any* ratio toward 1. Fitting a noise
term, both "native + noise ≈ 1.2" and "bilinear upscale + noise ≈ 1.8" reproduce
the observed numbers. Recorded because the number looks conclusive and is not.
### ⚠️ What it still does not show
Whether `ptbase` is **also** drawn underneath. `8AX` is ~86 % opaque and carries
the same art, so it would hide it either way. The observable fact is that 8AX's
detail reaches the screen.
## ❔ Not established ## ❔ Not established
* **Whether the game draws one, the other, or both.** The pixel evidence says
our background matches `8AX`'s art, which `ptbase` also carries — it cannot
separate them. A per-draw capture of the main menu recording texture *base
addresses* would, since the two textures are different sizes.
* The capture is ~4× darker than the render in these patches (4.5 vs 17.7), and * The capture is ~4× darker than the render in these patches (4.5 vs 17.7), and
not by a constant ratio. That is a separate colour/gamma question, untouched not by a constant ratio. That is a separate colour/gamma question, untouched
here. here.

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Does the running game draw the full-res `8AX` or the 640x360 `ptbase` upscaled?
Both carry the SAME artwork at two resolutions, so no pixel comparison of the
backgrounds can separate them. What CAN: the detail 8AX has and an upscale does
not. Compute `8AX - upscale(ptbase)` -- the 8AX-only detail -- and ask whether
the live capture contains it.
Both candidates are first mapped into the capture's tone domain with the
measured gamma (docs/re/structures/ui-render-tone-curve.md); without that, the
capture's residual is dominated by the tone difference and the test is blind.
CONTROLS are the point: the same correlation with the 8AX residual shifted 7 px
and flipped. Those preserve the spatial correlation structure while destroying
the alignment, so they are what "no signal" looks like here.
"""
import sys
import numpy as np
from PIL import Image
CASES = [
("main menu", 1.491, "a715f485_8AX_1280x720.png", "a715f485_ptbase.t32_640x360.png",
"docs/re/captures/title-builds/live-main-menu.png"),
("title", 1.338, "a60fcb85_8AX_1280x720.png", "a60fcb85_ptbase2.t32_640x360.png",
"docs/re/captures/title-builds/live-title-press-a.png"),
]
TEX = sys.argv[1] if len(sys.argv) > 1 else "/tmp/tex"
Y0, Y1, X0, X1 = 40, 300, 20, 600 # background, away from menu text
def main():
for label, gamma, ax_png, pb_png, cap_png in CASES:
ax = np.asarray(Image.open(f"{TEX}/{ax_png}").convert("L")).astype(float)
pb = np.asarray(Image.open(f"{TEX}/{pb_png}").convert("L")
.resize((1280, 720), Image.BILINEAR)).astype(float)
cap = np.asarray(Image.open(cap_png).convert("L")).astype(float)
h, w = cap.shape
ax, pb = ax[:h, :w], pb[:h, :w]
g = lambda a: 255 * np.power(np.clip(a, 0, 255) / 255.0, gamma)
axg, pbg = g(ax), g(pb)
E = (axg - pbg)[Y0:Y1, X0:X1] # detail only 8AX has
C = (cap - pbg)[Y0:Y1, X0:X1] # how the capture departs from the upscale
cc = lambda a, b: float(np.corrcoef(a.ravel(), b.ravel())[0, 1])
ceiling = E.std() / C.std()
r = cc(C, E)
print(f"{label}: corr {r:+.4f} controls {cc(C[:, :-7], E[:, 7:]):+.4f} (shift) "
f"{cc(C, E[::-1]):+.4f} (flip) ceiling {ceiling:.3f} -> {100*r/ceiling:.0f}% of it")
if __name__ == "__main__":
main()