From 6c4e5d3d717908a734e64f1610cf24b2edc99fcc Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Sat, 29 Aug 2026 02:40:53 +0000 Subject: [PATCH] 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. --- docs/port/HANDOFF.md | 21 +++++-- docs/re/METHOD.md | 13 +++++ docs/re/REFUTED.md | 13 +++++ docs/re/data/eightax-detail-test.txt | 2 + .../structures/ui-8ax-fullres-background.md | 57 ++++++++++++++++--- tools/re-capture/eightax_detail_test.py | 49 ++++++++++++++++ 6 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 docs/re/data/eightax-detail-test.txt create mode 100755 tools/re-capture/eightax_detail_test.py diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 0a529a01..185fc50a 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -248,12 +248,21 @@ authored version can be deleted. 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 patch sampled. - ⚠️ **So resolving the name and drawing it as well would double-draw an opaque - full-screen layer** — invisible as a doubling, which is worse. If you want the - sharper background, use `8AX` at 1:1 and *drop* `ptbase`; don't add it. - ❔ Which of the two the game actually draws is not established — both carry the - same art, and separating them needs a per-draw capture recording texture base - addresses (the two differ in size). + ✅ **SETTLED 2026-08-29 — the game draws the full-res `8AX`, so use it.** + Previously parked as "needs a per-draw capture"; it did not. The two carry the + same art at two resolutions, so what separates them is the detail `8AX` has + that an upscale cannot. Correlating the capture's departure-from-upscale + against the 8AX-only detail (both first mapped through the measured gamma): + 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, 7–15× + 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) * ✅ **Paint order: your exposure is two element pairs, on one screen.** We use diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index eb07535d..78afb281 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -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 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. +* **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.00–0.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. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index f5d4cd8a..3684a490 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -469,3 +469,16 @@ neighbourhood, not just the line. 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; `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.00–0.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. diff --git a/docs/re/data/eightax-detail-test.txt b/docs/re/data/eightax-detail-test.txt new file mode 100644 index 00000000..20774112 --- /dev/null +++ b/docs/re/data/eightax-detail-test.txt @@ -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 diff --git a/docs/re/structures/ui-8ax-fullres-background.md b/docs/re/structures/ui-8ax-fullres-background.md index 7db77327..95fa4c09 100644 --- a/docs/re/structures/ui-8ax-fullres-background.md +++ b/docs/re/structures/ui-8ax-fullres-background.md @@ -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 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`, -rather than upscaling a 640×360 copy 2×. That is a sharper background from a -texture already in the bundle. Not done here — it is a rendering choice, and -`ptbase`'s element carries the keyframes. +✅ **And that is now the recommendation, not just an option:** use `8AX` at 1:1. +The detail test above says the game's background carries 8AX's full-resolution +detail, so upscaling the 640×360 `ptbase` 2× is *wrong*, not merely softer. +⚠️ 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.9–4.1) against +everything else that differs (sd 27–43), 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, 7–15× 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.60–0.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 - -* **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 not by a constant ratio. That is a separate colour/gamma question, untouched here. diff --git a/tools/re-capture/eightax_detail_test.py b/tools/re-capture/eightax_detail_test.py new file mode 100755 index 00000000..3f81df11 --- /dev/null +++ b/tools/re-capture/eightax_detail_test.py @@ -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()