`ui_draw_capture.sh` grows three knobs the second iteration needed: * ARM=early presses F10 before the title exists, so a long window contains the frames in which a screen is BUILT (it turns out none are — the title screen submits the same 11 draws every frame and never rebuilds); * TARGET=menu taps A once on the title and arms on the main menu, skipping attract movies on the way — it does not get there, but the blocker it hits is documented rather than worked around; * EXTRA_FLAGS passes emulator cvars through (--create_profile_if_none, --mem_watch=false, --log_level). `ui_draw_order.py` follows the capture's new vertex format (x, y, z) and reports the Z it now has.
117 lines
4.5 KiB
Python
Executable File
117 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Turn a `log_ui_draws` capture into a per-frame PAINT ORDER, named by sprite.
|
|
|
|
The capture records every draw of a frame in submission order with, for the UI
|
|
shader, the quad's vertex positions. Those positions are NDC, so a quad's pixel
|
|
rect is recoverable exactly; and a screen's sprites decode to near-unique sizes,
|
|
so a rect's SIZE names the sprite it draws — no texture identity needed.
|
|
|
|
ui_draw_order.py <capture.log> <texture-dir> [frame]
|
|
|
|
`texture-dir` is the output of `sylpheed-cli pak textures`, whose filenames
|
|
carry `<name>_<W>x<H>.png`. Matching is on size within a pixel of slack, so a
|
|
size shared by two sprites reports BOTH rather than guessing.
|
|
"""
|
|
import re
|
|
import sys
|
|
import os
|
|
import glob
|
|
from collections import defaultdict
|
|
|
|
W, H = 1280, 720
|
|
VERT = re.compile(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=(-?\d+\.\d+)\]")
|
|
|
|
|
|
def sprite_index(texdir):
|
|
idx = defaultdict(list)
|
|
for f in glob.glob(os.path.join(texdir, "*.png")):
|
|
m = re.match(r"[0-9a-f]{8}_(.+?)_(\d+)x(\d+)\.png$", os.path.basename(f))
|
|
if m:
|
|
idx[(int(m.group(2)), int(m.group(3)))].append(m.group(1))
|
|
return idx
|
|
|
|
|
|
# Slack, and why it is not 0: a quad is a few pixels smaller than the sprite it
|
|
# draws (ptlogo1 919x113 comes back as 915x115, ptcopyright 694x20 as 691x18).
|
|
# The cause is unmeasured — a scale just under 1, or a UV inset — so match on
|
|
# nearest size within a few pixels and print the distance rather than pretend to
|
|
# an exact hit.
|
|
SLACK = 6
|
|
|
|
|
|
def name_for(idx, w, h):
|
|
hits = []
|
|
for (sw, sh), names in idx.items():
|
|
d = max(abs(sw - w), abs(sh - h))
|
|
if d <= SLACK:
|
|
for n in names:
|
|
hits.append((d, f"{n} {sw}x{sh}" + (f" (±{d})" if d else "")))
|
|
hits.sort()
|
|
# The same sprite appears once per language build of the pak, under a
|
|
# different hash prefix — one name is one candidate.
|
|
seen, out = set(), []
|
|
for _, t in hits:
|
|
if t not in seen:
|
|
seen.add(t)
|
|
out.append(t)
|
|
return out
|
|
|
|
|
|
def main():
|
|
log, texdir = sys.argv[1], sys.argv[2]
|
|
want = sys.argv[3] if len(sys.argv) > 3 else None
|
|
idx = sprite_index(texdir)
|
|
frame = None
|
|
pending = None
|
|
first = None
|
|
for line in open(log):
|
|
if line.startswith("# every draw"):
|
|
first = line.rstrip().split()[-1].split("..")[0]
|
|
frame = first
|
|
continue
|
|
if line.startswith("--- frame"):
|
|
frame = line.split()[2]
|
|
continue
|
|
m = re.match(r"\s*(\d+) (prim=.*)", line)
|
|
if m:
|
|
pending = (m.group(1), m.group(2))
|
|
continue
|
|
if "vb=0x" in line and pending:
|
|
if want is None or frame == want or frame is None:
|
|
verts = [(float(a), float(b)) for a, b, _z in VERT.findall(line)]
|
|
if verts:
|
|
# Two conventions in one log: the UI sprite shader emits NDC,
|
|
# the full-screen pass emits pixels already (-0.5 .. 1279.5).
|
|
if max(abs(v) for xy in verts for v in xy) > 4.0:
|
|
xs = [x for x, _ in verts]
|
|
ys = [y for _, y in verts]
|
|
else:
|
|
xs = [(x + 1) / 2 * W for x, _ in verts]
|
|
ys = [(1 - y) / 2 * H for _, y in verts]
|
|
n = len(verts)
|
|
# A quad list stores 4 verts per quad; report each quad.
|
|
per = 4 if "prim=13" in pending[1] else n
|
|
for q in range(0, n, per):
|
|
qx, qy = xs[q:q + per], ys[q:q + per]
|
|
if not qx:
|
|
continue
|
|
x0, x1 = min(qx), max(qx)
|
|
y0, y1 = min(qy), max(qy)
|
|
w, h = round(x1 - x0), round(y1 - y0)
|
|
# Axis-aligned iff every vertex sits on the bbox edge.
|
|
rot = "" if 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(qx, qy)
|
|
) else " ROT"
|
|
names = name_for(idx, w, h)
|
|
print(f"frame {frame} draw {pending[0]:>4} "
|
|
f"({round(x0):4},{round(y0):4}) {w:4}x{h:<4}{rot} "
|
|
f"→ {', '.join(names) if names else '?'}")
|
|
pending = None
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|