mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-06 13:30:00 +00:00
fix(tools): keep title names out of the catalog and refuse a malformed param.json title ID
titles.json no longer takes a name: a run is labelled with the package's own param.json title name. discover() checks the param.json titleId with the same pattern as the catalog keys, since it names the work directory that prepare() empties. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0a4d3a908e
commit
5e4007fd30
4 files changed
+39
-26
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, such as OpenNOW's sign-in page, 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), `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`.
|
||||
@@ -42,8 +42,12 @@ class TitlesTests(unittest.TestCase):
|
||||
title_run.load_titles(self.write({"PPSA00001": {"relink": [1]}}))
|
||||
|
||||
def test_bad_id_is_refused(self):
|
||||
with self.assertRaisesRegex(ValueError, "VLC is not a title ID"):
|
||||
title_run.load_titles(self.write({"VLC": {}}))
|
||||
with self.assertRaisesRegex(ValueError, "NOT-AN-ID is not a title ID"):
|
||||
title_run.load_titles(self.write({"NOT-AN-ID": {}}))
|
||||
|
||||
def test_name_is_refused(self):
|
||||
with self.assertRaisesRegex(ValueError, "PPSA00001: unknown field name"):
|
||||
title_run.load_titles(self.write({"PPSA00001": {"name": "Light"}}))
|
||||
|
||||
def test_default_entry(self):
|
||||
self.assertEqual(title_run.relink_args({}, "windows"), ["--windows"])
|
||||
@@ -147,26 +151,26 @@ class ReportTests(unittest.TestCase):
|
||||
self.assertEqual(title_run.cell([run("rendering")], "warm"), "-")
|
||||
|
||||
def test_worst_and_compare(self):
|
||||
now = summary({"PPSA85300": {"name": "VLC", "version": "0.4", "runs": [run("crashed"), run("crashed", "warm")]},
|
||||
now = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("crashed"), run("crashed", "warm")]},
|
||||
"PPSA97358": {"name": "XPSemu", "version": "A2.1", "runs": [run("rendering"), run("rendering", "warm")]}})
|
||||
before = summary({"PPSA85300": {"name": "VLC", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]},
|
||||
before = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]},
|
||||
"PPSA97358": {"name": "XPSemu", "version": "A2.1", "runs": [run("stalled"), run("stalled", "warm")]}})
|
||||
self.assertEqual(title_run.worst(now["titles"]["PPSA85300"]["runs"]), "crashed")
|
||||
self.assertEqual(title_run.compare(now, before),
|
||||
([("PPSA85300", "rendering", "crashed")], [("PPSA97358", "stalled", "rendering")]))
|
||||
|
||||
def test_render_markdown(self):
|
||||
now = summary({"PPSA85300": {"name": "VLC", "version": "0.4",
|
||||
now = summary({"PPSA85300": {"name": "Light", "version": "0.4",
|
||||
"runs": [run("crashed", error="what(): unsupported MIB 1.1 | x"), run("rendering", "warm", 31.0)]}})
|
||||
before = summary({"PPSA85300": {"name": "VLC", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]}})
|
||||
before = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]}})
|
||||
self.assertEqual(title_run.render_markdown(now, before), "\n".join([
|
||||
"AnyPS5 `abc1234`, main + #6814. Windows 11, Radeon 860M, driver 32.0.13058.8001, CPU Ryzen AI 7 350. 2 runs per title, 60 s each.",
|
||||
"",
|
||||
"Regressions: VLC (PPSA85300) rendering -> crashed",
|
||||
"Regressions: Light (PPSA85300) rendering -> crashed",
|
||||
"",
|
||||
"| Title | ID | Version | Cold | Warm | FPS | First error | Before |",
|
||||
"|---|---|---|---|---|---|---|---|",
|
||||
"| VLC | PPSA85300 | 0.4 | crashed | rendering | 31.0 | what(): unsupported MIB 1.1 \\| x | rendering |",
|
||||
"| Light | PPSA85300 | 0.4 | crashed | rendering | 31.0 | what(): unsupported MIB 1.1 \\| x | rendering |",
|
||||
"",
|
||||
]))
|
||||
self.assertNotIn("Before", title_run.render_markdown(now))
|
||||
@@ -174,8 +178,8 @@ class ReportTests(unittest.TestCase):
|
||||
self.assertTrue(title_run.render_markdown(now).startswith("AnyPS5 `abc1234`, main + #6814. CPU Ryzen AI 7 350. 2 runs"))
|
||||
|
||||
def test_render_markdown_lists_missing_titles(self):
|
||||
now = summary({"PPSA85300": {"name": "VLC", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]}})
|
||||
before = summary({"PPSA85300": {"name": "VLC", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]},
|
||||
now = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]}})
|
||||
before = summary({"PPSA85300": {"name": "Light", "version": "0.4", "runs": [run("rendering"), run("rendering", "warm")]},
|
||||
"PPSA97358": {"name": "XPSemu", "version": "A2.1", "runs": [run("stalled"), run("stalled", "warm")]}})
|
||||
self.assertIn("Missing: XPSemu (PPSA97358)", title_run.render_markdown(now, before))
|
||||
|
||||
@@ -223,6 +227,12 @@ class PreparationTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "param.json: no titleId"):
|
||||
title_run.discover([self.root])
|
||||
|
||||
def test_discover_refuses_a_title_id_that_is_not_one(self):
|
||||
for index, title_id in enumerate(("../../outside", str(self.root / "elsewhere"), "PPSA0001")):
|
||||
package(self.root / str(index) / "PPSA00001", title_id)
|
||||
with self.assertRaisesRegex(ValueError, "is not a title ID"):
|
||||
title_run.discover([self.root / str(index)])
|
||||
|
||||
def test_unwrap_or_copy_keeps_an_elf(self):
|
||||
source = self.root / "eboot.bin"
|
||||
source.write_bytes(b"\x7fELF" + bytes(60))
|
||||
|
||||
+6
-3
@@ -18,14 +18,15 @@ sys.path.insert(0, str(ROOT / "tools"))
|
||||
import unwrap_self
|
||||
from package_release import windows_runtime
|
||||
OUTCOMES = ("relink-failed", "crashed", "exited", "stalled", "blank", "rendering")
|
||||
TITLE_FIELDS = {"name": str, "relink": list, "fixed_base": str, "executables": list, "seconds": int, "env": dict}
|
||||
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"}
|
||||
|
||||
|
||||
def load_titles(path):
|
||||
titles = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
for title_id, entry in titles.items():
|
||||
if not re.fullmatch(r"[A-Z]{4}[0-9]{5}", title_id):
|
||||
if not TITLE_ID.fullmatch(title_id):
|
||||
raise ValueError(f"{path}: {title_id} is not a title ID")
|
||||
for key, value in entry.items():
|
||||
if key not in TITLE_FIELDS:
|
||||
@@ -180,6 +181,8 @@ def discover(directories):
|
||||
title_id = data.get("titleId")
|
||||
if not isinstance(title_id, str):
|
||||
raise ValueError(f"{param}: no titleId")
|
||||
if not TITLE_ID.fullmatch(title_id):
|
||||
raise ValueError(f"{param}: titleId {title_id!r} is not a title ID")
|
||||
if title_id in found:
|
||||
raise ValueError(f"{title_id} found more than once: {found[title_id]['package']} and {package}")
|
||||
localized = data.get("localizedParameters", {})
|
||||
@@ -535,7 +538,7 @@ def main(argv=None):
|
||||
summary["machine"][key] = summary["machine"][key] or value
|
||||
runs.append({"run": number, "cache": "cold" if number == 1 else "warm", **result})
|
||||
print(f"{title_id} run{number}: {result['outcome']}", flush=True)
|
||||
summary["titles"][title_id] = {"name": entry.get("name", found[title_id]["name"]), "version": found[title_id]["version"], "seconds": seconds, "runs": runs}
|
||||
summary["titles"][title_id] = {"name": found[title_id]["name"], "version": found[title_id]["version"], "seconds": seconds, "runs": runs}
|
||||
write_summary(output, summary)
|
||||
write_summary(output, summary)
|
||||
report = render_markdown(summary, previous)
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"PPSA01153": {"name": "OliSe Player", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA68091": {"name": "Ruffle Flash", "relink": ["--registry"], "fixed_base": "0x400000"},
|
||||
"PPSA69421": {"name": "SuperTuxKart", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA85300": {"name": "VLC", "relink": ["--registry"], "fixed_base": "0x400000"},
|
||||
"PPSA97358": {"name": "XPSemu", "relink": ["--registry"], "fixed_base": "0x400000"},
|
||||
"PPSA99004": {"name": "ProsperoAI", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99008": {"name": "ProsperoEden", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99011": {"name": "PS5 FSR4 Showcase", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99169": {"name": "RetroArch", "executables": ["picker/picker.bin"]},
|
||||
"PPSA99203": {"name": "PS5SX2", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99288": {"name": "Nuvio Max'd", "relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99360": {"name": "PS5CEMU-HAR", "relink": ["--trap-sdk-syscall-stubs"]}
|
||||
"PPSA01153": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA68091": {"relink": ["--registry"], "fixed_base": "0x400000"},
|
||||
"PPSA69421": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA85300": {"relink": ["--registry"], "fixed_base": "0x400000"},
|
||||
"PPSA97358": {"relink": ["--registry"], "fixed_base": "0x400000"},
|
||||
"PPSA99004": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99008": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99011": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99169": {"executables": ["picker/picker.bin"]},
|
||||
"PPSA99203": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99288": {"relink": ["--trap-sdk-syscall-stubs"]},
|
||||
"PPSA99360": {"relink": ["--trap-sdk-syscall-stubs"]}
|
||||
}
|
||||
Reference in new issue
Block a user