mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 15:00:30 +02:00
headless_bench.py compares the speed of headless binaries on savestates or frame dumps. It alternates the binaries between rounds and reports medians of wall time, CPU time, and instructions and cycles where the OS counts them. Instructions vary by well under 0.1% between identical runs, where wall time swings by several percent. It checks for mains power first, and stops on a run that failed or didn't get where it should. dump_compare.py replays every frame dump in a directory with two binaries and reports the dumps that render differently, or differently from run to run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
101 lines
4.5 KiB
Python
Executable File
101 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2012- PPSSPP Project.
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License 2.0 for more details.
|
|
|
|
# A copy of the GPL 2.0 should have been included with the program.
|
|
# If not, see http://www.gnu.org/licenses/
|
|
|
|
# Official git repository and contact information can be found at
|
|
# https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
"""Check that two PPSSPPHeadless binaries render every GE frame dump in a directory the same.
|
|
|
|
Frame dumps replay deterministically, so they show a rendering change exactly, where a game run from a savestate
|
|
can land on another frame. Each dump is replayed with the baseline binary once and with the new one --runs times;
|
|
a dump whose new screenshots differ from each other is reported as nondeterministic (usually a threading race).
|
|
|
|
Example:
|
|
python3 Tools/dump_compare.py ~/PSP/dumps old/PPSSPPHeadless build-release/PPSSPPHeadless --runs 2
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
|
|
def screenshot(binary, dump, out, graphics, timeout):
|
|
if os.path.exists(out):
|
|
os.remove(out)
|
|
try:
|
|
subprocess.run([binary, '--graphics=' + graphics, '--screenshot-save=' + out, dump], capture_output=True, timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
return 'timeout'
|
|
if not os.path.exists(out):
|
|
return 'none'
|
|
return hashlib.md5(open(out, 'rb').read()).hexdigest()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('dumps', help='a directory of .ppdmp (or zipped) frame dumps, searched recursively')
|
|
parser.add_argument('baseline', help='the PPSSPPHeadless to compare against')
|
|
parser.add_argument('new', help='the PPSSPPHeadless to check')
|
|
parser.add_argument('--runs', type=int, default=1, help='replays with the new binary per dump (default 1)')
|
|
parser.add_argument('--only', action='append', default=[], help='only dumps whose path contains this (repeatable)')
|
|
parser.add_argument('--graphics', default='software', help='backend (default software)')
|
|
parser.add_argument('--jobs', type=int, default=4, help='dumps replayed at once (default 4)')
|
|
parser.add_argument('--timeout', type=int, default=120, help='seconds per replay (default 120)')
|
|
parser.add_argument('--keep', help='keep the screenshots of differing dumps in this directory')
|
|
args = parser.parse_args()
|
|
for b in (args.baseline, args.new):
|
|
if not os.access(b, os.X_OK):
|
|
parser.error('not an executable: ' + b)
|
|
|
|
root = os.path.expanduser(args.dumps)
|
|
dumps = []
|
|
for d, _, files in os.walk(root):
|
|
for f in files:
|
|
if f.lower().endswith(('.ppdmp', '.zip')):
|
|
dumps.append(os.path.join(d, f))
|
|
dumps = sorted(d for d in dumps if not args.only or any(o in d for o in args.only))
|
|
work = tempfile.mkdtemp(prefix='dump_compare_')
|
|
|
|
def one(dump):
|
|
key = hashlib.md5(dump.encode()).hexdigest()[:12]
|
|
base = screenshot(args.baseline, dump, os.path.join(work, key + '_base.png'), args.graphics, args.timeout)
|
|
news = [screenshot(args.new, dump, os.path.join(work, '%s_new%d.png' % (key, i)), args.graphics, args.timeout) for i in range(args.runs)]
|
|
if all(n == base for n in news):
|
|
status = 'same'
|
|
elif len(set(news)) == 1:
|
|
status = 'DIFF'
|
|
else:
|
|
status = 'NONDET'
|
|
return status, dump, key, base, news
|
|
|
|
with ThreadPoolExecutor(args.jobs) as ex:
|
|
results = list(ex.map(one, dumps))
|
|
differing = [r for r in results if r[0] != 'same']
|
|
for status, dump, key, base, news in differing:
|
|
print('%-6s %s base %s new %s' % (status, os.path.relpath(dump, root), base[:8], ' '.join(n[:8] for n in news)))
|
|
if args.keep:
|
|
os.makedirs(args.keep, exist_ok=True)
|
|
for f in os.listdir(work):
|
|
if f.startswith(key):
|
|
os.replace(os.path.join(work, f), os.path.join(args.keep, f.replace(key, os.path.basename(dump))))
|
|
print('%d dumps, %d differ' % (len(results), len(differing)))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|