diff --git a/docs/re/data/jp-title-at-rest.txt b/docs/re/data/jp-title-at-rest.txt new file mode 100644 index 00000000..587c3d70 --- /dev/null +++ b/docs/re/data/jp-title-at-rest.txt @@ -0,0 +1,45 @@ +# The JAPANESE title (GP_TITLE build 7) at rest -- captured 2026-08-30. +# +# Asked for by the port agent: its title_jp row drifted (155/20498 -> +# 233/61208), localized to a 350x396 block at (405,74) -- the logo stack -- +# and with NO capture of the JP title in the corpus it could say the two +# renderers had moved apart but not which one moved. +# +# MISSION.md has carried this as 'needs one more run' since 2026-08-29. +# Three earlier attempts failed to reach the interactive title in either +# locale. The reason is now known and was not the locale: A at the title +# needs a signed-in profile (title-a-press-fault.md), and no run had one. +# +# LOCALE: tools/re-capture/set_console_language.py ja -- canary's own +# persisted XConfig, offset located from struct landmarks. Restored to en +# afterwards; verified back at language=1. +# +# ✅ INDEPENDENT CONFIRMATION THE LOCALE TOOK: the XMA probe logged a +# DIFFERENT voice-context set from every English run -- +# ja: 1112064, 1150976, 1177600 +# en: 1294336, 1118208, 1171456 +# The Japanese voice region is a different set of streams, so the switch +# reached the guest and is not just a menu-language cosmetic. +# +# 'AT REST' IS DEMONSTRATED, NOT ASSUMED. Five frames ~1.5 s apart after the +# plate pulse says the screen has settled: +# +# the port's ROI, 350x396 at (405,74) design space: +# frame 1 vs 0: max |d| 0, pixels differing >8: 0 / 138600 +# frame 2 vs 0: max |d| 0, pixels differing >8: 0 / 138600 +# frame 3 vs 0: max |d| 0, pixels differing >8: 0 / 138600 +# frame 4 vs 0: max |d| 0, pixels differing >8: 0 / 138600 +# +# the whole frame, as a CONTRAST CONTROL (the plate pulses, so this must +# move or the instrument is blind): +# frame 1 vs 0: 39584 px frame 3 vs 0: 71927 px +# frame 2 vs 0: 58303 px frame 4 vs 0: 69604 px +# +# So the logo stack is byte-identical across 6 s while 5-8 % of the frame is +# moving. The ROI is at rest and the instrument can see motion. +# +# WHAT THE CAPTURE SHOWS that the English title does not: the katakana +# subtitle under the wordmark, and a crystalline burst BEHIND the wordmark -- +# the ptlogo3a/b/c + ptlogo_back2eff* stack, which ui-forced-backdrop and +# the tie-break work record as TRANSPARENT at rest on the English title. +# That is exactly the region the port's drift is localized to. diff --git a/tools/re-capture/jp_title_capture.py b/tools/re-capture/jp_title_capture.py new file mode 100755 index 00000000..67b85b9e --- /dev/null +++ b/tools/re-capture/jp_title_capture.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Capture the JAPANESE title (`GP_TITLE` build 7) at rest. + +Asked for by the port agent: its `title_jp` row drifted and it can say the two +renderers moved apart but not which moved, because **no capture of the JP title +exists in this corpus**. `MISSION.md` has carried this as "needs one more run" — +the locale route is `set_console_language.py`, and the reason the earlier runs +failed (Ⓐ needs a signed-in profile) is now known. + +"At rest" is **demonstrated, not assumed**: after the plate pulse says the title +has settled, five frames are taken ~1.5 s apart and the port's own region of +interest — 350×396 at (405, 74) in design space — is compared across them. If the +logo stack is still moving, the frames will say so. + +⚠️ The frames are display-space 1280×720 with the game surface at y=45 +(`tbm-submenu-reached.txt`), so the design-space box is offset by that. + + jp_title_capture.py OUTDIR [wait_s] +""" +import subprocess +import sys +import time + +import numpy as np +from PIL import Image + +OUT = sys.argv[1] +WAIT = float(sys.argv[2]) if len(sys.argv) > 2 else 480 +W, H = 1280, 720 +NEED, CEIL, HOLD = 500, 2500, 12 +SURF_Y = 45 +ROI = (405, 74, 350, 396) # design-space x, y, w, h — the port's drifting block + + +def _open(): + return subprocess.Popen( + ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", + "-video_size", f"{W}x{H}", "-i", ":98", "-r", "4", + "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], + stdout=subprocess.PIPE, bufsize=W * H * 3 * 2) + + +def glyph(a): + r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2] + return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum()) + + +T0 = time.time() +p, n, seg, streak = _open(), W * H * 3, time.time(), 0 +frames = [] +while True: + el = time.time() - T0 + if el > WAIT: + print("TIMEOUT — the title never settled", flush=True); break + if time.time() - seg > 30: + p.kill(); p = _open(); seg = time.time() + buf = p.stdout.read(n) + if len(buf) < n: + p.kill(); p = _open(); seg = time.time(); continue + a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(np.uint8) + c = glyph(a.astype(int)) + streak = streak + 1 if NEED <= c <= CEIL else 0 + if streak >= HOLD: + print(f"[{el:7.1f}s] TITLE SETTLED (plate pulse, glyph {c}) — taking 5 frames", flush=True) + for k in range(5): + t0 = time.time() + while time.time() - t0 < 1.5: + b2 = p.stdout.read(n) + if len(b2) < n: + p.kill(); p = _open(); break + a = np.frombuffer(b2, np.uint8).reshape(H, W, 3).astype(np.uint8) + Image.fromarray(a).save(f"{OUT}/jp-title-{k}.png") + frames.append(a.astype(int)) + print(f" frame {k}: glyph {glyph(a.astype(int))}", flush=True) + break +p.kill() +if len(frames) == 5: + x, y, w, h = ROI + rois = [f[SURF_Y + y:SURF_Y + y + h, x:x + w] for f in frames] + print("\nROI stability — the port's 350x396 block at (405,74), design space:") + for i in range(1, 5): + d = np.abs(rois[i] - rois[0]) + print(f" frame {i} vs 0: max |Δ| {d.max():3d}, pixels differing >8: " + f"{int((d.max(axis=2) > 8).sum()):6d} / {d.shape[0]*d.shape[1]}", flush=True) + full = [np.abs(frames[i] - frames[0]).max(axis=2) for i in range(1, 5)] + print(" whole frame, for contrast (the plate pulses, so this SHOULD move):") + for i, d in enumerate(full, 1): + print(f" frame {i} vs 0: pixels differing >8: {int((d > 8).sum()):7d}", flush=True)