re: F5 survives the full-quad reader, measured rather than asserted

Last iteration I asserted F5 was unaffected by the truncating-reader bug
because it compared like with like. Asserting that is the move that produced
the bug, so this measures it. The new reader sees 9.7 quads/frame vs ~7.5.

Scalar that needs no element identification: quads mid-ramp (0<a<250) per
frame goes 6,4,4,4,2,1,4,3 -> 0 at f436, while the control never reaches 0
anywhere in 48 frames of build-in. One frame with nothing part-way through a
ramp is the cut.

Bonus the old reader could not show: both sweeps enter at f436-438 at their
declared opening alphas -- pteff03 at 255, pteff03a at 1,2,3,4,6,11,17 from
its declared 0.

Refutation attempt on the port's "clock jumps to 236.0": tried and failed.
My bound is [100,238), which contains 236 -- consistent, not independent
confirmation.

Adds tools/re-capture/read_draws.py so the truncating regex is not re-rolled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
This commit is contained in:
sylph-decoder
2026-09-02 21:06:55 +00:00
parent 1acdb004b6
commit e3d76fe397
2 changed files with 130 additions and 0 deletions

67
tools/re-capture/read_draws.py Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Read a Xenia UI draw log -- EVERY quad, not the first vertex of each draw.
⚠️ THIS EXISTS BECAUSE THE OBVIOUS READER IS WRONG. A draw line carries
`indices=N` vertices on ONE `v:` line, and N is routinely 8 -- two quads batched
into a single draw. A reader that takes the first `v: [...]` match per line sees
one of them and silently drops the rest.
That produced a clean, complete-looking negative twice in this corpus
(`f6-unit5-pteff03a-never-drawn.md`, `f6-unit6-...`), both refuted by
`f6-unit11-pteff03a-IS-drawn.md`. `REFUTED.md` L170 had already recorded a draw
carrying two rotated parallelograms. Use this reader; do not re-roll the regex.
from read_draws import read
frames = read(path) # {frame: [Quad, ...]} Quad = (page, verts, alpha, blend, cx)
"""
import re, collections
_F = re.compile(r'^--- frame (\d+) ')
_TEX = re.compile(r'tex\[base=(0x[0-9A-F]+) (\d+)x(\d+) fmt=(\d+)(?: h=([0-9A-F]+))?\]')
_IDX = re.compile(r'indices=(\d+)')
_V = re.compile(r'\[(-?\d+\.\d+),(-?\d+\.\d+),z=[-\d.]+,col=([0-9A-F]{8})\]')
class Quad(tuple):
__slots__ = ()
def __new__(cls, page, verts, alpha, blend, cx):
return tuple.__new__(cls, (page, verts, alpha, blend, cx))
page = property(lambda s: s[0])
verts = property(lambda s: s[1])
alpha = property(lambda s: s[2])
blend = property(lambda s: s[3])
cx = property(lambda s: s[4])
def read(path):
frames = collections.defaultdict(list)
frame = page = blend = None
idx = 0
for line in open(path, errors='replace'):
m = _F.match(line)
if m:
frame = int(m.group(1)); continue
if frame is None:
continue
mt = _TEX.search(line)
if mt:
page = mt.group(5) or mt.group(1)
mi = _IDX.search(line); idx = int(mi.group(1)) if mi else 0
mb = re.search(r'blend=(0x[0-9A-F]+)', line); blend = mb.group(1) if mb else None
continue
if page is not None and ' v: ' in line:
vs = _V.findall(line)
# every group of 4 vertices is one quad; a partial tail is dropped
for q in range(len(vs) // 4):
quad = vs[q*4:(q+1)*4]
cx = sum(float(v[0]) for v in quad) / 4
frames[frame].append(Quad(page, [(float(a), float(b)) for a, b, _ in quad],
int(quad[0][2][:2], 16), blend, round(cx, 3)))
page = None
return dict(frames)
if __name__ == '__main__':
import sys
fr = read(sys.argv[1])
ks = sorted(fr)
tot = sum(len(v) for v in fr.values())
print(f"{len(ks)} frames, {tot} quads, frames {ks[0]}..{ks[-1]}")
print(f"mean quads/frame {tot/len(ks):.2f}")