mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-07 13:39:55 +00:00
fix(tools): report an unreadable last frame and lock a title's work directory
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
39b52ea6c0
commit
e5b4e22aba
3 files changed
+51
-3
No files matched your search
+1
-1
@@ -79,4 +79,4 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and
|
||||
|
||||
## Running titles repeatably
|
||||
|
||||
`python tools/title_run.py --games <directory> [ID ...]` relinks and runs extracted title packages found under `--games` (a directory holding `eboot.bin` and `sce_sys/param.json`) and writes `summary.json` and a fixed-format `summary.md` under `--out` (default `build/title-runs/<commit>-<time>/`). Each title runs `--runs` times (default 2): the first with empty AnyPS5 and Mesa or NVIDIA shader caches, the next ones keeping them. AMD's Windows `VkCache` is not isolated. On Windows the title's windows open behind every other window, without focus and without being minimized, and can be brought forward from the taskbar; elsewhere run it under a virtual display if that matters. A run ends as `relink-failed`, `crashed`, `exited`, `stalled` (no present in the last 20 s, or too few presents for a frame to be dumped), `blank` (the last frame is one colour) or `rendering`; the first, middle and last dumped frames are kept. A screen that presents only when its view changes stops after a few presents and is reported `stalled` although the title still answers input; pressing a button through `tools/pad_input.py` with `APS5_PAD_INPUT_FILE` set tells it apart from a stuck title. `--compare <summary.json>` lists regressions, improvements and missing titles against an earlier run, limited to the IDs given when there are any. Titles are prepared in `<out>/work/<ID>/`, the same path on every run, so a firewall prompt for a title that opens a port appears once; answering Cancel does not affect the run. Per-title relink flags, run length and environment live in `tools/titles.json`.
|
||||
`python tools/title_run.py --games <directory> [ID ...]` relinks and runs extracted title packages found under `--games` (a directory holding `eboot.bin` and `sce_sys/param.json`) and writes `summary.json` and a fixed-format `summary.md` under `--out` (default `build/title-runs/<commit>-<time>/`). Each title runs `--runs` times (default 2): the first with empty AnyPS5 and Mesa or NVIDIA shader caches, the next ones keeping them. AMD's Windows `VkCache` is not isolated. On Windows the title's windows open behind every other window, without focus and without being minimized, and can be brought forward from the taskbar; elsewhere run it under a virtual display if that matters. A run ends as `relink-failed`, `crashed`, `exited`, `stalled` (no present in the last 20 s, or too few presents for a frame to be dumped), `frame-unreadable` (the last dumped frame is truncated or not a 24- or 32-bit BMP), `blank` (the last frame is one colour) or `rendering`; the first, middle and last dumped frames are kept. A screen that presents only when its view changes stops after a few presents and is reported `stalled` although the title still answers input; pressing a button through `tools/pad_input.py` with `APS5_PAD_INPUT_FILE` set tells it apart from a stuck title. `--compare <summary.json>` lists regressions, improvements and missing titles against an earlier run, limited to the IDs given when there are any. Titles are prepared in `<out>/work/<ID>/`, the same path on every run, so a firewall prompt for a title that opens a port appears once; answering Cancel does not affect the run. An invocation locks `<out>/work/<ID>.lock` for each title it runs before touching anything, and refuses to start while another invocation with the same `--out` holds one of them. Per-title relink flags, run length and environment live in `tools/titles.json`.
|
||||
@@ -116,6 +116,14 @@ class AnalysisTests(unittest.TestCase):
|
||||
cut.write_bytes(noisy.read_bytes()[:-20])
|
||||
self.assertIsNone(title_run.frame_is_uniform(cut))
|
||||
|
||||
def test_classify_reads_the_last_frame(self):
|
||||
uniform = bmp(self.root / "uniform.bmp", 8, 4, lambda x, y: (9, 9, 9))
|
||||
varied = bmp(self.root / "varied.bmp", 8, 4, lambda x, y: (x * 30, y * 60, 0))
|
||||
truncated = self.root / "truncated.bmp"
|
||||
truncated.write_bytes(varied.read_bytes()[:-20])
|
||||
for frame, outcome in ((uniform, "blank"), (varied, "rendering"), (truncated, "frame-unreadable")):
|
||||
self.assertEqual(title_run.classify(True, None, 5.0, True, title_run.frame_is_uniform(frame)), outcome, frame.name)
|
||||
|
||||
def test_classify(self):
|
||||
cases = [
|
||||
((False, None, None, False, None), "relink-failed"),
|
||||
@@ -126,7 +134,7 @@ class AnalysisTests(unittest.TestCase):
|
||||
((True, None, 5.0, False, None), "stalled"),
|
||||
((True, None, 5.0, True, True), "blank"),
|
||||
((True, None, 5.0, True, False), "rendering"),
|
||||
((True, None, 5.0, True, None), "rendering"),
|
||||
((True, None, 5.0, True, None), "frame-unreadable"),
|
||||
]
|
||||
for arguments, outcome in cases:
|
||||
self.assertEqual(title_run.classify(*arguments), outcome, arguments)
|
||||
@@ -199,6 +207,23 @@ class PreparationTests(unittest.TestCase):
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.root = Path(directory.name)
|
||||
|
||||
def test_a_running_title_is_left_alone(self):
|
||||
package(self.root / "games/PPSA00001", "PPSA00001")
|
||||
out = self.root / "out"
|
||||
marker = out / "work/PPSA00001/app0/eboot.bin"
|
||||
marker.parent.mkdir(parents=True)
|
||||
marker.write_bytes(b"running")
|
||||
held = title_run.lock_work(out / "work/PPSA00001.lock")
|
||||
self.addCleanup(held.close)
|
||||
self.assertIsNotNone(held)
|
||||
with self.assertRaisesRegex(SystemExit, "already running in .*: PPSA00001"):
|
||||
title_run.main(["--games", str(self.root / "games"), "--out", str(out), "PPSA00001"])
|
||||
self.assertEqual(marker.read_bytes(), b"running")
|
||||
held.close()
|
||||
again = title_run.lock_work(out / "work/PPSA00001.lock")
|
||||
self.assertIsNotNone(again)
|
||||
again.close()
|
||||
|
||||
def test_discover(self):
|
||||
package(self.root / "a/src/PPSA00001", "PPSA00001", "01.000.110", "Light")
|
||||
package(self.root / "b/PPSA00002", "PPSA00002")
|
||||
|
||||
+24
-1
@@ -17,7 +17,7 @@ sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
import unwrap_self
|
||||
from package_release import windows_runtime
|
||||
OUTCOMES = ("relink-failed", "crashed", "exited", "stalled", "blank", "rendering")
|
||||
OUTCOMES = ("relink-failed", "crashed", "exited", "stalled", "frame-unreadable", "blank", "rendering")
|
||||
TITLE_ID = re.compile(r"[A-Z]{4}[0-9]{5}")
|
||||
TITLE_FIELDS = {"relink": list, "fixed_base": str, "executables": list, "seconds": int, "env": dict}
|
||||
FIXED_BASE_FLAGS = {"windows": "--windows-fixed-base", "linux": "--linux-fixed-base"}
|
||||
@@ -107,6 +107,8 @@ def classify(relinked, exit_code, summary_age, framed, last_frame_uniform):
|
||||
return "crashed" if exit_code != 0 else "exited"
|
||||
if summary_age is None or summary_age > STALL_SECONDS or not framed:
|
||||
return "stalled"
|
||||
if last_frame_uniform is None:
|
||||
return "frame-unreadable"
|
||||
if last_frame_uniform:
|
||||
return "blank"
|
||||
return "rendering"
|
||||
@@ -218,6 +220,23 @@ def link_tree(source, target, skip):
|
||||
link_file(path, target / relative)
|
||||
|
||||
|
||||
def lock_work(path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
handle = path.open("a+b")
|
||||
try:
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
handle.seek(0)
|
||||
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
else:
|
||||
import fcntl
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
handle.close()
|
||||
return None
|
||||
return handle
|
||||
|
||||
|
||||
def prepare(title_id, found, entry, platform, build, work):
|
||||
package = found["package"]
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
@@ -504,6 +523,10 @@ def main(argv=None):
|
||||
missing = [title_id for title_id in ids if title_id not in found]
|
||||
if missing:
|
||||
raise SystemExit(f"not found under --games: {', '.join(missing)}")
|
||||
locks = {title_id: lock_work(args.out.resolve() / "work" / f"{title_id}.lock") for title_id in ids}
|
||||
busy = [title_id for title_id, handle in locks.items() if handle is None]
|
||||
if busy:
|
||||
raise SystemExit(f"already running in {args.out.resolve() / 'work'}: {', '.join(busy)}")
|
||||
if previous and args.ids:
|
||||
previous["titles"] = {title_id: title for title_id, title in previous["titles"].items() if title_id in ids}
|
||||
source = source_root(args.build)
|
||||
|
||||
Reference in new issue
Block a user