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
80 lines
3.2 KiB
Python
Executable File
80 lines
3.2 KiB
Python
Executable File
#!/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()
|