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:
49
tools/re-capture/eightax_detail_test.py
Executable file
49
tools/re-capture/eightax_detail_test.py
Executable 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()
|
||||
Reference in New Issue
Block a user