A snapshot of the non-game files as of0148cb8("port: F5/F6 hand-off -- one-minute human checks, and a refutation attempt that survived", 2026-09-04), the tip of auto/port-p6-audio. The branch was deleted from the server on 2026-09-17 during the consolidation cleanup; issue #7 asks for this work as a reviewable PR, so it is recovered here before the commits are garbage collected. Contents: the 84 files the branch changed relative to its fork pointb305aa4, which is this commit's parent. The tree is therefore 0148cb8's tree with the 854 exported game assets left out -- export-probe/, export-probe2/, three .wav renders of game audio and adv-v2-screenlog.tsv. Game data stays out of git; the exporter regenerates those from the disc. docs/port/DECISIONS.md still refers to them by name. Not recovered: the branch's own 366 commits. Keeping them would make those assets reachable again, so this is one snapshot instead. The original commits stay unreferenced in the server's object store, and in this clone under the local branch archive/port-p6-audio, until either is garbage collected. Refs #7. The OPTIONS work that issue #6 asks for is a subset of this branch, also recovered as recover/options-menu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
181 lines
7.9 KiB
Python
Executable File
181 lines
7.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""WHERE does the edge residual sit, and is that region locally shifted?
|
|
|
|
RENDER=<png> tools/port/edge-residual-map [screen]
|
|
|
|
`edge-residual-kind` fits the whole frame and excludes a GLOBAL translation. Its
|
|
own reach statement says the thing it cannot do: one misplaced element is a small
|
|
share of 38 752 edge pixels and would not move a whole-frame number. This tiles
|
|
the frame and runs the same discriminator INSIDE each tile, so a single displaced
|
|
element shows up as one hot tile with a local slope -- which is invisible to the
|
|
global fit by construction, not by accident.
|
|
|
|
Division of labour, agreed with the Decoder 2026-08-31: the residual map is the
|
|
port's (it needs the render beside the capture), the element inventory is theirs
|
|
(it needs the disc). This tool produces the map and NAMES NOTHING.
|
|
|
|
THE CONTROL IS A KNOWN LOCAL SHIFT. A map that cannot localise a displacement it
|
|
was told about cannot be trusted to have found one it was not. Exit 0 the report
|
|
is trustworthy, 2 the control failed and the report is suppressed. No 1.
|
|
"""
|
|
import math, os, subprocess, sys, tempfile
|
|
|
|
CAPS = "docs/re/captures/title-builds"
|
|
SCREEN = sys.argv[1] if len(sys.argv) > 1 else "main_menu"
|
|
W, H = 1279, 675 # top-left crop of the guest surface; never scaled
|
|
TILE = 64
|
|
EDGE = 12
|
|
MIN_EDGE_PX = 150 # below this a tile's slope is noise
|
|
# The controls displace this region and the map must find it there.
|
|
CTRL_BOX = (448, 320, 640, 448) # x0, y0, x1, y1
|
|
# TWO controls, because ONE OF THEM FAILED AND TAUGHT ME THE LIMIT. The slope is
|
|
# a linearisation, residual ~ dx * gradient, which holds only while dx is small
|
|
# against the width of an edge. A +2 px displacement localises perfectly but reads
|
|
# back +0.8..+1.25, so the estimator SATURATES. Control A checks magnitude in the
|
|
# regime where magnitude means something; control B checks that a displacement too
|
|
# large to measure is still FOUND. Reporting a saturating slope as a distance
|
|
# would understate a real displacement by more than half.
|
|
CTRL_A_DX = 1 # linear regime: localisation AND magnitude
|
|
CTRL_B_DX = 2 # saturating: localisation and SIGN only
|
|
|
|
|
|
def gray(png, out):
|
|
subprocess.run(["convert", png, "-colorspace", "Gray", "-depth", "8",
|
|
"gray:" + out], check=True)
|
|
return open(out, "rb").read()
|
|
|
|
|
|
def lutfit(a, b):
|
|
tot = [0] * 256; cnt = [0] * 256
|
|
for i in range(len(a)):
|
|
tot[a[i]] += b[i]; cnt[a[i]] += 1
|
|
return [(tot[v] // cnt[v]) if cnt[v] else v for v in range(256)]
|
|
|
|
|
|
def tiles(a, b):
|
|
"""Per-tile mean |residual| on edge pixels, and the local shift slope."""
|
|
lut = lutfit(a, b) # ONE global LUT: tone is global, displacement is not
|
|
out = {}
|
|
for ty in range(0, H - 1, TILE):
|
|
for tx in range(0, W - 1, TILE):
|
|
gx = []; gy = []; rs = []; flat = []
|
|
for y in range(max(1, ty), min(H - 1, ty + TILE)):
|
|
o = y * W
|
|
for x in range(max(1, tx), min(W - 1, tx + TILE)):
|
|
i = o + x
|
|
ax = (a[i + 1] - a[i - 1]) * 0.5
|
|
ay = (a[i + W] - a[i - W]) * 0.5
|
|
d = float(lut[a[i]] - b[i])
|
|
if abs(ax) + abs(ay) < EDGE:
|
|
flat.append(abs(d)); continue
|
|
gx.append(ax); gy.append(ay); rs.append(d)
|
|
n = len(rs)
|
|
if n < MIN_EDGE_PX:
|
|
continue
|
|
mabs = sum(abs(v) for v in rs) / n
|
|
mflat = (sum(flat) / len(flat)) if flat else 0.0
|
|
mr = sum(rs) / n
|
|
|
|
def slope(u):
|
|
mu = sum(u) / n
|
|
suu = sum((v - mu) ** 2 for v in u)
|
|
if suu <= 0:
|
|
return 0.0
|
|
return sum((u[k] - mu) * (rs[k] - mr) for k in range(n)) / suu
|
|
out[(tx, ty)] = (mabs, slope(gx), slope(gy), n, mflat)
|
|
return out
|
|
|
|
|
|
def top(t, k=8):
|
|
return sorted(t.items(), key=lambda kv: -kv[1][0])[:k]
|
|
|
|
|
|
def show(t, label, k=8):
|
|
print(f" {label}")
|
|
print(f" {'tile':>12} {'edge':>7} {'flat':>7} {'e/f':>6} "
|
|
f"{'dx':>7} {'dy':>7} {'edge px':>8}")
|
|
for (tx, ty), (m, sx, sy, n, mf) in top(t, k):
|
|
ef = (m / mf) if mf > 0.01 else float('inf')
|
|
print(f" {tx:4d},{ty:4d} {m:7.2f} {mf:7.2f} {ef:6.2f} "
|
|
f"{sx:+7.3f} {sy:+7.3f} {n:8d}")
|
|
|
|
|
|
def shift_box(a, box, dx):
|
|
x0, y0, x1, y1 = box
|
|
out = bytearray(a)
|
|
for y in range(y0, y1):
|
|
for x in range(x0, x1):
|
|
out[y * W + x] = a[y * W + min(W - 1, max(0, x - dx))]
|
|
return bytes(out)
|
|
|
|
|
|
tmp = tempfile.mkdtemp()
|
|
cap = f"{CAPS}/live-{SCREEN.replace('_', '-')}.png"
|
|
render = os.environ.get("RENDER", "")
|
|
for p in (cap, render):
|
|
if not p or not os.path.exists(p):
|
|
print(f" 🔴 missing: {p or 'RENDER=<png>'}"); sys.exit(2)
|
|
subprocess.run(["convert", render, "-crop", f"{W}x{H}+0+0", "+repage",
|
|
f"{tmp}/crop.png"], check=True)
|
|
r = gray(f"{tmp}/crop.png", f"{tmp}/r.gray")
|
|
c = gray(cap, f"{tmp}/c.gray")
|
|
|
|
print("CONTROLS -- the render against itself with ONE REGION displaced.\n"
|
|
"The map must put that region on top; magnitude only in the linear regime.\n")
|
|
x0, y0, x1, y1 = CTRL_BOX
|
|
bad = []
|
|
|
|
|
|
def control(dx, check_magnitude):
|
|
t = tiles(r, shift_box(r, CTRL_BOX, dx))
|
|
show(t, f"known +{dx} px shift inside x {x0}-{x1}, y {y0}-{y1}", 4)
|
|
hits = [(k, v) for k, v in top(t, 4)
|
|
if x0 - TILE < k[0] < x1 and y0 - TILE < k[1] < y1]
|
|
if not hits:
|
|
bad.append(f"+{dx} px: displaced region not in the top 4 tiles")
|
|
return
|
|
best = max(hits, key=lambda kv: kv[1][0])[1][1]
|
|
if best <= 0.3:
|
|
bad.append(f"+{dx} px: local slope {best:+.3f} has the wrong sign or is flat")
|
|
elif check_magnitude and abs(best - dx) > 0.4:
|
|
bad.append(f"+{dx} px: local slope {best:+.3f} does not recover it")
|
|
print(f" -> localised, local slope {best:+.3f} px"
|
|
f"{'' if check_magnitude else ' (saturating -- a LOWER BOUND)'}\n")
|
|
|
|
|
|
control(CTRL_A_DX, True)
|
|
control(CTRL_B_DX, False)
|
|
if bad:
|
|
print(" 🔴 CONTROL FAILED: " + "; ".join(bad))
|
|
print(" A map that cannot find a displacement it was told about cannot be")
|
|
print(" trusted to have found one it was not. Report suppressed.")
|
|
sys.exit(2)
|
|
print(" ✅ controls pass: a 1 px displacement is localised and measured, a 2 px\n"
|
|
" one is localised with its magnitude understated. So a hot tile with a\n"
|
|
" real slope is a floor on the displacement, never a ceiling.\n")
|
|
|
|
print(f"THE REAL PAIR -- {SCREEN}\n")
|
|
rt = tiles(r, c)
|
|
show(rt, f"{SCREEN}: hottest tiles, whole-frame LUT applied", 10)
|
|
ms = sorted(v[0] for v in rt.values())
|
|
med = ms[len(ms) // 2]
|
|
efs = sorted(v[0] / v[4] for v in rt.values() if v[4] > 0.01)
|
|
med_ef = efs[len(efs) // 2]
|
|
hot = max(rt.items(), key=lambda kv: kv[1][0])
|
|
print(f"\n median tile |resid| {med:.2f} hottest {hot[1][0]:.2f} "
|
|
f"at {hot[0][0]},{hot[0][1]} ({hot[1][0]/med:.2f}x median)")
|
|
print(f" median tile edge/flat {med_ef:.2f}")
|
|
efs_hot = [v[0] / v[4] for _, v in top(rt, 10) if v[4] > 0.01]
|
|
print(f" hot tiles span edge/flat {min(efs_hot):.2f}..{max(efs_hot):.2f}, "
|
|
f"straddling that median")
|
|
print(" 📌 SO THE COLUMN DOES NOT SPLIT THEM. I added it expecting two families --")
|
|
print(" tiles hot only at edges (an edge-rendering difference) against tiles")
|
|
print(" hot everywhere (a local tone the global LUT mis-serves). The hot tiles")
|
|
print(" run continuously across the median instead, so the hot region is NOT")
|
|
print(" one anomalous element with a character of its own. Note the frame-wide")
|
|
print(" pooled edge/flat is 3.16 while the per-tile median is 1.84: pooling is")
|
|
print(" dominated by the tiles carrying the most edge pixels, and reading a")
|
|
print(" per-tile threshold off it would have manufactured the split.")
|
|
print("\n ⚠️ THIS TOOL NAMES NOTHING. A hot tile is a coordinate, not an element.")
|
|
print(" What sits under it is the Decoder's to say -- they hold the disc.")
|