mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
6.7 KiB
Python
137 lines
6.7 KiB
Python
"""Read a TOTK_FLIGHT dump (runtime/flight.c): the frames around the worst interval, the frames whose counts stand out,
|
|
and a contact sheet of the thumbnails with frame number and interval on each.
|
|
|
|
python tools/flight_view.py build/flight/<YYYYMMDD-HHMMSS>-f<frame> [--around FRAME] [--span 48] [--cols 12]
|
|
|
|
The sheet goes to <dump>/contact.png (--out to change): --span frames each side of the trigger (or of --around),
|
|
the worst frame boxed red, frames over 1.5x the median interval orange.
|
|
"""
|
|
import argparse
|
|
import re
|
|
import statistics
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
BLOCKED = ['pace_ms', 'fence_ms', 'drain_ms', 'pipe_ms', 'xlate_ms', 'tex_ms', 'astc_ms']
|
|
# Counts that are 0 in a steady frame: any non-zero value is listed.
|
|
RARE = ['shaders_xlated', 'pipe_built', 'pipe_waited', 'oneshot_waited', 'pipe_bg_used', 'draws_skip_pipe',
|
|
'draws_skip_shader', 'tex_new', 'tex_alias', 'tex_up', 'tex_reup', 'wb_done', 'wb_skip']
|
|
# Counts with a steady level: frames off the median by more than a quarter are listed.
|
|
LEVEL = ['submits', 'passes', 'draws', 'draws_seen', 'clears', 'copies', 'dispatches']
|
|
SHORT = {'interval_ms': 'ms', 'pace_ms': 'pace', 'fence_ms': 'fence', 'drain_ms': 'drain', 'pipe_ms': 'pipe',
|
|
'xlate_ms': 'xlate', 'tex_ms': 'tex', 'astc_ms': 'astc'}
|
|
|
|
|
|
def load(dump):
|
|
lines = (dump / 'frames.tsv').read_text(encoding='utf-8').splitlines()
|
|
head = lines[0] if lines and lines[0].startswith('#') else ''
|
|
rows = [ln.split('\t') for ln in lines if ln and not ln.startswith('#')]
|
|
cols = rows[0]
|
|
frames = []
|
|
for r in rows[1:]:
|
|
d = {c: float(v) for c, v in zip(cols, r)}
|
|
d['frame'] = int(d['frame'])
|
|
frames.append(d)
|
|
return head, cols, frames
|
|
|
|
|
|
def describe(f, cols):
|
|
"""One line per frame: interval, blocked times over 1 ms, then every non-zero rare count."""
|
|
parts = [f"f{f['frame']:<6} {f['interval_ms']:7.1f} ms"]
|
|
blocked = [f"{SHORT[c]} {f[c]:.1f}" for c in BLOCKED if f.get(c, 0) >= 1]
|
|
parts.append(('blocked: ' + ' '.join(blocked)) if blocked else '')
|
|
parts.append(f"draws {int(f['draws'])} passes {int(f['passes'])}")
|
|
rare = [f"{c} {int(f[c])}" for c in RARE if c in cols and f[c]]
|
|
parts.append(' '.join(rare))
|
|
return ' '.join(p for p in parts if p)
|
|
|
|
|
|
def deviations(frames, cols):
|
|
out = []
|
|
for c in RARE:
|
|
if c not in cols:
|
|
continue
|
|
hits = [(f['frame'], int(f[c])) for f in frames if f[c]]
|
|
if hits:
|
|
total = sum(v for _, v in hits)
|
|
shown = ' '.join(f'f{fr}={v}' for fr, v in sorted(hits, key=lambda h: -h[1])[:10])
|
|
out.append(f' {c:18} {len(hits):4} frames, total {total:6}: {shown}{" ..." if len(hits) > 10 else ""}')
|
|
for c in LEVEL:
|
|
vals = [f[c] for f in frames]
|
|
med = statistics.median(vals)
|
|
tol = max(2.0, 0.25 * med)
|
|
hits = [(f['frame'], int(f[c])) for f in frames if abs(f[c] - med) > tol]
|
|
if hits:
|
|
shown = ' '.join(f'f{fr}={v}' for fr, v in sorted(hits, key=lambda h: -abs(h[1] - med))[:10])
|
|
out.append(f' {c:18} {len(hits):4} frames off median {med:g}: {shown}{" ..." if len(hits) > 10 else ""}')
|
|
return out
|
|
|
|
|
|
def sheet(dump, frames, centre, span, cols_n, out, worst, slow):
|
|
sel = [f for f in frames if abs(f['frame'] - centre) <= span]
|
|
tiles = [(f, dump / f"f{f['frame']:06d}.png") for f in sel]
|
|
tiles = [(f, p) for f, p in tiles if p.exists()]
|
|
if not tiles:
|
|
return 0
|
|
tw, th, label = 240, 135, 16
|
|
rows = (len(tiles) + cols_n - 1) // cols_n
|
|
img = Image.new('RGB', (cols_n * tw, rows * (th + label)), (24, 24, 24))
|
|
draw = ImageDraw.Draw(img)
|
|
for i, (f, p) in enumerate(tiles):
|
|
x, y = (i % cols_n) * tw, (i // cols_n) * (th + label)
|
|
img.paste(Image.open(p).convert('RGB').resize((tw, th), Image.BILINEAR), (x, y + label))
|
|
colour = (255, 64, 64) if f['frame'] == worst else (255, 170, 0) if f['interval_ms'] > slow else (220, 220, 220)
|
|
draw.text((x + 3, y + 2), f"f{f['frame']} {f['interval_ms']:.1f} ms", fill=colour)
|
|
if colour != (220, 220, 220):
|
|
draw.rectangle([x, y + label, x + tw - 1, y + label + th - 1], outline=colour, width=3)
|
|
img.save(out)
|
|
return len(tiles)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('dump', type=Path)
|
|
ap.add_argument('--around', type=int, help='centre of the sheet and a second listing (default: the trigger)')
|
|
ap.add_argument('--span', type=int, default=48, help='frames each side on the contact sheet (default 48)')
|
|
ap.add_argument('--list', type=int, default=8, help='frames each side in the listing (default 8)')
|
|
ap.add_argument('--cols', type=int, default=12, help='contact sheet columns')
|
|
ap.add_argument('--out', type=Path, help='contact sheet path (default <dump>/contact.png)')
|
|
a = ap.parse_args()
|
|
if not (a.dump / 'frames.tsv').exists():
|
|
sys.exit(f'{a.dump}: no frames.tsv')
|
|
head, cols, frames = load(a.dump)
|
|
if not frames:
|
|
sys.exit(f'{a.dump}: no frames')
|
|
iv = sorted(f['interval_ms'] for f in frames[1:] or frames)
|
|
med = statistics.median(iv)
|
|
worst = max(frames[1:] or frames, key=lambda f: f['interval_ms'])
|
|
print(head.lstrip('# ') or a.dump.name)
|
|
print(f"frames {frames[0]['frame']}-{frames[-1]['frame']} ({len(frames)}), interval median {med:.1f} ms, "
|
|
f"p95 {iv[int(0.95 * (len(iv) - 1))]:.1f} ms, max {worst['interval_ms']:.1f} ms at f{worst['frame']}; "
|
|
f"thumbnails {sum(int(f.get('thumb', 0)) for f in frames)}")
|
|
m = re.search(r'at frame (\d+)', head)
|
|
centre = a.around if a.around is not None else int(m[1]) if m else worst['frame']
|
|
for c in dict.fromkeys([worst['frame'], centre]):
|
|
what = 'worst interval' if c == worst['frame'] else 'trigger' if a.around is None else '--around'
|
|
print(f'\nframes around f{c} ({what}):')
|
|
for f in frames:
|
|
if abs(f['frame'] - c) <= a.list:
|
|
print(('> ' if f['frame'] == c else ' ') + describe(f, cols))
|
|
slow = 1.5 * med
|
|
spikes = [f for f in frames if f['interval_ms'] > slow]
|
|
print(f'\nslow frames (> {slow:.1f} ms): {len(spikes)}')
|
|
for f in sorted(spikes, key=lambda f: -f['interval_ms'])[:12]:
|
|
print(' ' + describe(f, cols))
|
|
dev = deviations(frames, cols)
|
|
print('\ncounts that stand out:' if dev else '\ncounts that stand out: none')
|
|
print('\n'.join(dev))
|
|
out = a.out or a.dump / 'contact.png'
|
|
n = sheet(a.dump, frames, centre, a.span, a.cols, out, worst['frame'], slow)
|
|
print(f'\ncontact sheet: {out} ({n} thumbnails)' if n else '\ncontact sheet: no thumbnails in range')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|