Files
Sylpheed/tools/re-capture/quads_per_frame.py
sylph-decoder 47faeaa7a9 re: RETRACT "the game never draws eff3" -- a batched draw merged two quads
The console draws all five title flashes. My claim that ptlogo_back2eff3 is
never drawn was an instrument artefact, and I had reported it to the port with
three alternative explanations "ruled out".

A GPU draw can batch several quads -- indices=4 is one, indices=8 two,
indices=24 six -- and the UI draw log dumps only the first 8 vertices. Taking
min/max over a line's whole vertex list merges quads into one box.

eff3 is batched with eff4, and because the wipe family is right-aligned, eff3
(788..1196) lies ENTIRELY INSIDE eff4 (447..1196). The union is exactly eff4's
own extent, so the merged box matched eff4 to 1 px, eff3 vanished, and nothing
looked wrong.

Parsed per quad, all five fire in both title entries in the declared stagger:
eff1 130-131, eff2 133, eff3 133-134, eff4 133-135, eff/eff5 134+, back2 136+;
and 5953-5955 / 5955-5957 / 5957-5958 / 5957-5959 / 5958+ / 5962+ in entry 2.
Frames 133 and 134 are t=60.1 and 62.3, inside eff3's declared t in (58,64).

Also retracts "the developer splash is one composited quad" -- the same bug,
which the port refuted by arithmetic first (a 259-tall box cannot contain three
logos spanning y 164..585). It draws three logos and three glows as separate
quads in one indices=24 call; the 525x259 was gamearts_eff merged with
seta_eff. The 9-unit black hold is unaffected: those glows are the developer
splash's first draw.

The three "ruled out" explanations were all aimed at the wrong failure. In
particular the invisible-draw check counted draws with NO geometry line, when
the hiding place was draws with PARTIAL geometry. Refuting three wrong
hypotheses is not evidence for a fourth, and a list of failure modes written by
whoever built the instrument is the least likely to contain its blind spot.
Recorded in METHOD.md, along with the tell that was present and explained away:
a merged box carries the first quad's colour, which made one element's alpha
read 255/127/254 on consecutive frames.

New tool: tools/re-capture/quads_per_frame.py parses vertices in groups of four
and warns when the logged quad count falls short of indices/4.

Also guards a double-A-tap in ui_draw_capture.sh: the movie branch ignored that
TARGET=menu had already tapped, so a run tapped A on the title at t=23s and
again at t=27s on the transition; the guest faulted and Xenia dumped registers
to stdout until the file reached 519 MB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 20:49:27 +00:00

64 lines
2.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Parse a `xenia_re_ui_draws_NN.log` into ONE ROW PER QUAD.
🔴 The reason this file exists. A draw can BATCH several quads — `indices=8` is
two, `indices=24` is six — and the log dumps only the first 8 vertices. Taking
min/max over a line's whole vertex list therefore merges quads into one box.
That is not a theoretical hazard. It silently produced two wrong findings on
2026-08-29:
* `ptlogo_back2eff3` (408x203 @ 788,117) is batched with `ptlogo_back2eff4`
(749x203 @ 447,117), and eff3 sits ENTIRELY INSIDE eff4's x-range, so the
union equals eff4 exactly. The merged box matched eff4 to 1 px and eff3
"was never drawn" — reported, with three other explanations ruled out.
* the developer splash's `gamearts_eff` + `seta_eff` merged into a 525x259
box that was read as "the three logos composited into one quad".
Vertices come in groups of four, one per quad. Read them that way.
"""
import re, sys, json
def quads(path, lo=None, hi=None):
"""Yield (frame, index_count, logged_quads, expected_quads, x, y, w, h, alpha)."""
frame = None
pend = None
for line in open(path, errors="replace"):
m = re.match(r"--- frame (\d+) ---", line)
if m:
frame = int(m.group(1)); pend = None; continue
if frame is None:
continue
if lo is not None and not (lo <= frame <= hi):
continue
mm = re.match(r"^\s*\d+ prim=(\d+) indices=(\d+)", line)
if mm:
pend = int(mm.group(2)); continue
vs = re.findall(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=[-\d.]+(?:,col=([0-9A-F]{8}))?\]", line)
if not vs or pend is None:
continue
exp = max(1, pend // 4)
got = len(vs) // 4
for k in range(got):
g = vs[k*4:(k+1)*4]
xs = [(float(a) + 1) / 2 * 1280 for a, b, _ in g]
ys = [(1 - float(b)) / 2 * 720 for a, b, _ in g]
col = next((c for _, _, c in g if c), None)
yield (frame, pend, got, exp,
round(min(xs)), round(min(ys)),
round(max(xs) - min(xs)), round(max(ys) - min(ys)),
int(col[:2], 16) if col else -1)
pend = None
if __name__ == "__main__":
path = sys.argv[1]
lo, hi = (int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else (None, None)
unlogged = 0
for q in quads(path, lo, hi):
if q[2] < q[3]:
unlogged += q[3] - q[2]
print(",".join(str(v) for v in q))
if unlogged:
print(f"# WARNING: {unlogged} quads were batched but NOT logged (8-vertex cap)",
file=sys.stderr)