The four elements the port measured as the worst on EXTRAS, and which appeared in no draw, were in a draw all along: the 24-index additive batch holds six quads and Canary printed the first two. Cap raised to 64, screen re-captured, all six named. Same draw as ptframe3, whose state was already measured -- the one-way implication doing real work. pteff10 is identified too, and it needed the resting SCALE: it ships as 409x144 and is drawn at 200 % x 500 % = 816x720. The matcher's 'try 1x and 2x' rule could not name it at any scale and reported a near miss against something else, which is a failure wearing the clothes of an answer. Candidates are now the declaration's pivot*2 scaled by the resting keyframe as well as the texture at 1x and 2x, and the tolerance is the log's own NDC print quantisation rather than a chosen number. Flagged rather than buried: pteff10 measuring additive is in tension with the port measuring it nearly exact under alpha-over. Both can be true for a dim semi-transparent glow over a dark background, and it is the one row a rendering check does not corroborate. Also stated: the three full-screen alpha-over draws are NOT individually identified -- four elements declare 1280x720 -- so the label on those rows is a candidate, not an identification. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
137 lines
6.2 KiB
Python
Executable File
137 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Which UI element is drawn with which BLEND state — from a Canary UI draw log.
|
|
|
|
ui_blend_map.py <xenia_re_ui_draws_NN.log> [--build 5]
|
|
|
|
Canary's `CaptureUiDrawForRE` (patched 2026-08-31) logs RB_BLENDCONTROL0 per
|
|
draw. The log does not name elements, so a draw is identified by the PIXEL SIZE
|
|
of its quad: the vertices are in NDC, and (x_max-x_min)/2*1280 by
|
|
(y_max-y_min)/2*720 is the on-screen size, which is matched against the sprite
|
|
dimensions read straight off the disc.
|
|
|
|
CONTROL, and it is not optional: GP_TITLE build 5 draws two rotated sweep
|
|
strips whose heights were measured independently in
|
|
docs/re/data/title-sweep-drawn-at-rest.txt as 1134 and 1303 px. If this script's
|
|
NDC->pixel conversion does not reproduce those two numbers, its sizes are wrong
|
|
and every identification below them is worthless. It prints the check.
|
|
|
|
Blend states seen on this title's UI, decoded from the Xenos enum
|
|
(xenos.h: kZero=0 kOne=1 kSrcAlpha=6 kOneMinusSrcAlpha=7; BlendOp kAdd=0):
|
|
|
|
0x00010001 src=ONE dst=ZERO opaque, blending effectively off
|
|
0x07010701 src=ONE dst=ONE_MINUS_SRC_A premultiplied alpha-over
|
|
0x01010101 src=ONE dst=ONE ADDITIVE
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
W, H = 1280.0, 720.0
|
|
FACTOR = {0: "ZERO", 1: "ONE", 4: "SRC_COLOR", 5: "1-SRC_COLOR", 6: "SRC_ALPHA",
|
|
7: "1-SRC_ALPHA", 8: "DST_COLOR", 9: "1-DST_COLOR", 10: "DST_ALPHA",
|
|
11: "1-DST_ALPHA"}
|
|
|
|
|
|
def blend_name(raw):
|
|
src, op, dst = raw & 0x1F, (raw >> 5) & 7, (raw >> 8) & 0x1F
|
|
if (src, op, dst) == (1, 0, 0):
|
|
return "opaque"
|
|
if (src, op, dst) == (1, 0, 7):
|
|
return "alpha-over(premul)"
|
|
if (src, op, dst) == (1, 0, 1):
|
|
return "ADDITIVE"
|
|
if (src, op, dst) == (6, 0, 7):
|
|
return "alpha-over(straight)"
|
|
return "%s+%s" % (FACTOR.get(src, src), FACTOR.get(dst, dst))
|
|
|
|
|
|
def candidates(builds):
|
|
"""Every size a UI draw could legitimately have, with a label and a basis.
|
|
|
|
Two bases, because neither alone names every element:
|
|
|
|
declared the declaration's `pivot * 2` scaled by the RESTING keyframe's
|
|
scale_x/scale_y. This is the only thing that names `pteff10`,
|
|
which ships as 409x144 and is drawn at 200 % x 500 % = 816x720 --
|
|
a scale-guessing matcher called it "no match" and offered a near
|
|
miss against something else instead.
|
|
texture the decoded `.t32`'s own pixel size, at 1x and 2x. Needed because
|
|
the pivot is NOT always half the texture (`ptmsg2` declares
|
|
384x38 for a 354x38 sprite) and because a button's focused `f`
|
|
variant has a texture and no declaration of its own.
|
|
|
|
`builds` is a list: the live title is TWO builds composited, 4 for the art
|
|
and 2 for the `PRESS (A)` plate.
|
|
"""
|
|
env = {**__import__("os").environ, "SYLPHEED_DISC": "/disc"}
|
|
def run(example):
|
|
return subprocess.run(
|
|
["cargo", "run", "--release", "-q", "-p", "sylpheed-formats",
|
|
"--example", example, "--"] + [str(b) for b in builds],
|
|
capture_output=True, text=True, cwd="/work", env=env).stdout
|
|
out = []
|
|
for line in run("rest_scale_of").splitlines():
|
|
m = re.match(r"(\S+\.(?:t32|prm|rat))\s+\d+,\d+\s+\d+\s+\d+\s+([\d.]+)x([\d.]+)", line)
|
|
if m:
|
|
out.append((m.group(1), "declared", float(m.group(2)), float(m.group(3))))
|
|
for line in run("frame_alpha_census").splitlines():
|
|
m = re.match(r"(\S+\.t32)\s+(\d+)x(\d+)", line)
|
|
if m:
|
|
w, h = float(m.group(2)), float(m.group(3))
|
|
out.append((m.group(1), "texture", w, h))
|
|
out.append((m.group(1), "texture@2x", w * 2, h * 2))
|
|
return out
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1]
|
|
spec = sys.argv[sys.argv.index("--build") + 1] if "--build" in sys.argv else "5"
|
|
builds = [int(b) for b in spec.split(",")]
|
|
cands = candidates(builds)
|
|
lines = open(path).read().splitlines()
|
|
rows = []
|
|
for i, line in enumerate(lines):
|
|
m = re.search(r"^\s*(\d+) prim=(\d+) indices=(\d+).* blend=0x([0-9A-F]+)", line)
|
|
if not m:
|
|
continue
|
|
idx, prim, nidx, raw = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4), 16)
|
|
tex = re.search(r"tex\[base=0x([0-9A-F]+) (\d+)x(\d+)", line)
|
|
verts = []
|
|
if i + 1 < len(lines):
|
|
for vm in re.finditer(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=", lines[i + 1]):
|
|
verts.append((float(vm.group(1)), float(vm.group(2))))
|
|
quads = [verts[k:k + 4] for k in range(0, len(verts) - 3, 4)]
|
|
rows.append((idx, prim, nidx, raw, tex.group(1) if tex else None, quads))
|
|
|
|
print("draw prim idx blend state quad px (w x h) best name match")
|
|
heights = []
|
|
for idx, prim, nidx, raw, tex, quads in rows:
|
|
for q in quads:
|
|
xs = [p[0] for p in q]
|
|
ys = [p[1] for p in q]
|
|
w = (max(xs) - min(xs)) / 2 * W
|
|
h = (max(ys) - min(ys)) / 2 * H
|
|
heights.append(h)
|
|
# The log prints NDC to TWO DECIMALS, so a width is quantised to
|
|
# 0.01 * 1280 / 2 = 6.4 px and a height to 3.6 px. The tolerance
|
|
# below is that quantisation, not a fudge factor: a match inside it
|
|
# is as close as this instrument can report.
|
|
best, bd, bb = "-", 1e9, ""
|
|
for n, basis, sw, sh in cands:
|
|
d = abs(sw - w) / 6.4 + abs(sh - h) / 3.6
|
|
if d < bd:
|
|
best, bd, bb = n, d, basis
|
|
tag = ("%s [%s]" % (best, bb)) if bd <= 2.0 else \
|
|
"(no match, nearest %s [%s] off %.1f quanta)" % (best, bb, bd)
|
|
print("%4d %4d %4d 0x%08X %-20s %7.1f x %7.1f %s"
|
|
% (idx, prim, nidx, raw, blend_name(raw), w, h, tag))
|
|
tall = sorted(h for h in heights if h > 900)
|
|
print("\nCONTROL — the two rotated sweep strips measured independently at 1134 and 1303 px:")
|
|
print(" tall quads found: %s" % ["%.0f" % t for t in sorted(set(round(t) for t in tall))])
|
|
print(" PASS" if any(abs(t - 1134) < 3 for t in tall) and any(abs(t - 1303) < 4 for t in tall)
|
|
else " FAIL — the NDC->pixel conversion is wrong; ignore every size above")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|