This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/kf_time_probe.py
Sylpheed RE agent 69291a315f
Some checks failed
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — macos-latest (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
port: add Q10 -- what a music bank's sub-waves actually are
BGM_001.slb is three sub-waves (10 KB, 4.47 MB, 4.67 MB) and the decoder
concatenates them into one 347 s track. That is a default nobody chose, not a
decision: two near-equal halves could be intro + loop, two variations, or two
halves of one piece, and a menu that loops its music needs to know which.

Found while wiring the Audio Library up to the shared banks. Recorded in
HANDOFF.md as a trap too, so the port does not build looping on top of the
concatenated track before the question is answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:43:25 +02:00

90 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Per-frame quad geometry and vertex colour from a `log_ui_draws` capture.
`ui_draw_order.py` answers "in what order were these painted"; this answers
"what did this quad look like on frame N, and on N+1". That is what a keyframe
TIME unit has to be measured against: the bundle says an element ramps from
t=31 to t=34, and the only way to learn what a `t` is worth is to count the
rendered frames the same ramp takes in the running game.
kf_time_probe.py <capture.log> [--csv out.csv]
Emits one row per quad per frame: frame, draw index, pixel rect, whether the
quad is axis-aligned, and the per-vertex colour word (whose alpha byte is the
element's fade). Frame numbers are the emulator's VdSwap count, so they are
SUBMITTED FRAMES, not wall-clock — which is the point: an emulator that runs
at half speed does not move them.
"""
import re
import sys
W, H = 1280, 720
VERT = re.compile(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=(-?\d+\.\d+)(?:,col=([0-9A-F]{8}))?\]")
def quads(log):
"""Yield (frame, draw, x0, y0, w, h, rot, col) for every quad in the log."""
frame, pending = None, None
for line in open(log):
if line.startswith("# every draw"):
frame = int(line.rstrip().split()[-1].split("..")[0])
continue
if line.startswith("--- frame"):
frame = int(line.split()[2])
continue
m = re.match(r"\s*(\d+) (prim=.*)", line)
if m:
pending = (int(m.group(1)), m.group(2))
continue
if "vb=0x" in line and pending:
hits = VERT.findall(line)
verts = [(float(a), float(b), c) for a, b, _z, c in hits]
if verts:
# Two conventions in one log: the UI sprite shader emits NDC,
# the full-screen pass emits pixels already.
if max(abs(v) for x, y, _c in verts for v in (x, y)) > 4.0:
pts = [(x, y, c) for x, y, c in verts]
else:
pts = [((x + 1) / 2 * W, (1 - y) / 2 * H, c) for x, y, c in verts]
per = 4 if "prim=13" in pending[1] else len(pts)
for q in range(0, len(pts), per):
chunk = pts[q:q + per]
if not chunk:
continue
xs = [p[0] for p in chunk]
ys = [p[1] for p in chunk]
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
rot = all(
(abs(x - x0) < 1.0 or abs(x - x1) < 1.0)
and (abs(y - y0) < 1.0 or abs(y - y1) < 1.0)
for x, y in zip(xs, ys)
)
cols = {p[2] for p in chunk if p[2]}
col = sorted(cols)[0] if len(cols) == 1 else (
"/".join(sorted(cols)) if cols else "")
yield (frame, pending[0], round(x0), round(y0),
round(x1 - x0), round(y1 - y0), "" if rot else "ROT",
col)
pending = None
def main():
log = sys.argv[1]
out = None
if "--csv" in sys.argv:
out = open(sys.argv[sys.argv.index("--csv") + 1], "w")
out.write("frame,draw,x,y,w,h,rot,col\n")
for row in quads(log):
line = "%d,%d,%d,%d,%d,%d,%s,%s" % row
if out:
out.write(line + "\n")
else:
print(line)
if out:
out.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())