re: the title's build-in measured in the guest's draw stream -- the flashes are real

The settle-time decode was confirmed only against a SETTLED frame, which shows
the end state is right and says nothing about whether the five flashes ever
happen. This runs the oracle: a draw capture armed before the title exists,
so the window contains the frames in which the screen is built.

The flashes fire in a six-frame window and are absent from all 155 other
sampled frames. `ptlogo_back2eff1` is drawn in exactly two frames at t = 54.0
against a decoded peak of t54-56; `ptlogo1` first appears at t = 42.2 against
a decoded t42. Units-per-frame was taken from the GLOW's period alone, a
different element, so the timings are not circular. The two holders are
continuous from frame 134.

The plate glow's quad carries a per-vertex colour whose alpha IS the element's
fade alpha, so the ramp is read straight out of the guest: observed range
0..80 against a decoded peak of 80, exact and unfitted; period 51.158
presented frames over 20 cycle starts. Fitting the decoded ramp gives RMS
13.16 alpha levels against 38.18 for the same ramp REVERSED -- if the shape
carried no information those would be equal, so the asymmetry is real and
correctly directed. Further controls: symmetric triangle 15.73, flat 31.13.

`ptlogo_back2eff3` was never drawn, and that is expected rather than a miss: a
2-unit flash peak is 0.85 of a presented frame, so catching one is a matter of
phase. A port drawing all five every time shows more sweep than the console.

METHOD.md gains the trap this cost: a 2D draw's identity is its vertex
geometry, not its bound texture. These sprites sample shared pages, and
matching texture dimensions produced a false negative (no flash is ever drawn)
and a false positive (the intro movie's 640x360 YUV planes read as `ptbase2`)
in the same pass.

Also records the top-level restriction on the settle window, which the port
raised and which is verified here: top-level [160,236] width 76, including the
`ptloop` leaves [269,540] width 271 -- an instant past the end of every
top-level element's timeline.

Evidence committed as a derived per-frame series, not the 7 MB raw log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
sylph-decoder
2026-08-29 19:56:34 +00:00
parent 4c74e579a0
commit d98c8214cc
9 changed files with 3511 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""When does each title sprite get DRAWN, frame by frame, in the real game?
Reads a `xenia_re_ui_draws_NN.log` armed early (so the window contains the frames
in which the screen is BUILT, not just its steady state) and reports, per texture
size, the first and last frame it is bound in.
The point is a falsifiable prediction. `docs/re/structures/ui-settle-time.md`
decodes `ptlogo_back2eff1`..`eff5` as five staggered two-frame flashes that sweep
across the logo once and are extinguished by keyframe t110, while
`ptlogo_back2eff` and `ptlogo_back2` hold for the rest of the screen. Four of the
five have UNIQUE decoded dimensions, so the log can confirm or refute that
directly:
eff1 167x126 eff2 258x203 eff3 408x203 eff4 749x203
If they appear in a short contiguous run of early frames and never again, the
decode is right. If they are bound every frame, or never, it is wrong.
buildin_timeline.py <log> [--dims WxH,...]
"""
import re, sys, collections
# GP_TITLE build 4, from `sylpheed-cli`/`sprite_dims`. Two share 1133x280, which
# is why the capture also records per-vertex colour alpha.
KNOWN = {
(167,126): "ptlogo_back2eff1 FLASH t54-58",
(258,203): "ptlogo_back2eff2 FLASH t58-62",
(408,203): "ptlogo_back2eff3 FLASH t62-66",
(749,203): "ptlogo_back2eff4 FLASH t~64",
(1133,280): "ptlogo_back2eff / eff5 (AMBIGUOUS: same size)",
(1118,262): "ptlogo_back2 holds t80-243",
(919,113): "ptlogo1 holds",
(992,104): "ptlogo2 holds",
(1280,720): "pteff04 full-screen",
(640,360): "ptbase2",
(694,20): "ptcopyright",
(399,180): "pteff03 / pteff03a (sweeps)",
(517,131): "ptlogoall_eff",
(235,180): "ptlogoall_eff2",
(640,319): "pteff01",
(37,17): "ptlogo_tm",
}
def main():
path = sys.argv[1]
frame = None
seen = collections.defaultdict(list) # dims -> [frames]
per_frame = collections.Counter()
frames = []
for line in open(path, errors="replace"):
m = re.match(r"--- frame (\d+) ---", line)
if m:
frame = int(m.group(1)); frames.append(frame); continue
if frame is None:
continue
for w, h in re.findall(r"tex\[base=0x[0-9A-F]+ (\d+)x(\d+) fmt=\d+\]", line):
seen[(int(w), int(h))].append(frame)
if line.startswith(("0","1","2","3","4","5","6","7","8","9")) or re.match(r"^\s*\d+ prim=", line):
per_frame[frame] += 1
if not frames:
print("no frames in the log"); return
lo, hi = min(frames), max(frames)
print(f"frames {lo}..{hi} ({len(set(frames))} distinct) draws {sum(per_frame.values())}\n")
rows = []
for dims, fl in seen.items():
s = sorted(set(fl))
rows.append((s[0], dims, s[-1], len(s), len(fl)))
rows.sort()
print(f" {'first':>7} {'last':>7} {'frames':>7} {'draws':>7} {'size':>10} what")
for first, dims, last, nf, nd in rows:
name = KNOWN.get(dims, "")
span = last - first
tag = " ⟵ TRANSIENT" if nf <= 12 and span <= 20 else (" (every frame)" if nf > 0.8*len(set(frames)) else "")
print(f" {first:>7} {last:>7} {nf:>7} {nd:>7} {dims[0]:>4}x{dims[1]:<4} {name}{tag}")
if __name__ == "__main__":
main()