mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 07:33:45 -06:00
feat: name the BIOS each run used
Record version 5: every repetition and variant carries bios_used, staged:<dest> when a load line of the core names the file, builtin when a fallback line says it ran its own BIOS, unknown otherwise. An opened file is no proof. The oracle gains fallback_patterns and load_patterns per core, and a record judged again carries judged_at and judged_by.
This commit is contained in:
1 parent
7c3e6ced09
commit
5c8da8a424
5 files changed
+194
-14
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://abdess.github.io/retrobios/api/v1/schemas/core-probe-record.schema.json",
|
||||
"title": "RetroBIOS core probe record",
|
||||
"description": "One line of measurements/<emulator>.jsonl: one run of scripts/core_probe.py, a libretro core executed headless, in full, with what is needed to judge and reproduce it. Every repetition, its core logs and per-frame hashes, and the raw timing values are in the line. From record_version 4 every repetition and every variant carries the boot oracle's verdict (boots, bios_error, crash, hang, unknown) with the evidence that decided it.",
|
||||
"description": "One line of measurements/<emulator>.jsonl: one run of scripts/core_probe.py, a libretro core executed headless, in full, with what is needed to judge and reproduce it. Every repetition, its core logs and per-frame hashes, and the raw timing values are in the line. From record_version 4 every repetition and every variant carries the boot oracle's verdict (boots, bios_error, crash, hang, unknown) with the evidence that decided it. From record_version 5 each also names the BIOS the core ran (`bios_used`), and a record judged again from an earlier one's runs carries `judged_at` and `judged_by`.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
@@ -24,9 +24,10 @@
|
||||
"record_version": {
|
||||
"enum": [
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"description": "3: records before the boot oracle. 4: every repetition and variant carries `verdict` and `verdict_evidence`, and `inputs.oracle` names what the verdicts were judged against."
|
||||
"description": "3: records before the boot oracle. 4: every repetition and variant carries `verdict` and `verdict_evidence`, and `inputs.oracle` names what the verdicts were judged against. 5: they also carry `bios_used`."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
@@ -416,6 +417,9 @@
|
||||
},
|
||||
"verdict_evidence": {
|
||||
"$ref": "#/$defs/verdict_evidence"
|
||||
},
|
||||
"bios_used": {
|
||||
"$ref": "#/$defs/bios_used"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,6 +525,36 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"judged_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the runs of the record it supersedes were judged again, offline, against the current oracle. recorded_at stays the time the runs were made."
|
||||
},
|
||||
"judged_by": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"script",
|
||||
"commit",
|
||||
"script_sha256"
|
||||
],
|
||||
"properties": {
|
||||
"script": {
|
||||
"const": "scripts/core_probe.py"
|
||||
},
|
||||
"commit": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"pattern": "^[0-9a-f]{40}$"
|
||||
},
|
||||
"script_sha256": {
|
||||
"$ref": "#/$defs/sha256"
|
||||
}
|
||||
},
|
||||
"description": "The core_probe that judged the runs again: frontend names the one that ran them."
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
@@ -565,7 +599,10 @@
|
||||
"if": {
|
||||
"properties": {
|
||||
"record_version": {
|
||||
"const": 4
|
||||
"enum": [
|
||||
4,
|
||||
5
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -605,6 +642,35 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"record_version": {
|
||||
"const": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"variants": {
|
||||
"items": {
|
||||
"required": [
|
||||
"bios_used"
|
||||
],
|
||||
"properties": {
|
||||
"repetitions": {
|
||||
"items": {
|
||||
"required": [
|
||||
"bios_used"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
@@ -850,6 +916,9 @@
|
||||
}
|
||||
},
|
||||
"description": "How a worker that did not finish ended: its exit status or the signal that killed it, or the timeout after which the probe killed it."
|
||||
},
|
||||
"bios_used": {
|
||||
"$ref": "#/$defs/bios_used"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -925,6 +994,10 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/verdict"
|
||||
}
|
||||
},
|
||||
"bios_log": {
|
||||
"type": "string",
|
||||
"description": "The log line that names the BIOS the core ran."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1011,11 +1084,30 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Log lines by which the core says it loaded the console's own BIOS: they add the bios stage when its screens are hidden."
|
||||
},
|
||||
"fallback_patterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Log lines after which the core runs its built-in BIOS."
|
||||
},
|
||||
"load_patterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Log lines naming the BIOS file the core loaded, in a group named path."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "What the verdicts were judged against: the reference frames of this content and core, the core's error patterns, error screens and BIOS load patterns, from the oracle file at its sha256. Null when the run had no oracle entry."
|
||||
},
|
||||
"bios_used": {
|
||||
"type": "string",
|
||||
"pattern": "^(staged:.+|builtin|unknown)$",
|
||||
"description": "The BIOS the core ran, from its log: staged:<dest> for the staged file a load line names, builtin when a fallback line says the core ran its own BIOS (OpenBIOS, HLE), unknown when the log says neither. An opened file is no proof."
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-7
@@ -1007,7 +1007,7 @@ def run_variant(args: argparse.Namespace, base: dict[str, Path], variant: Varian
|
||||
state_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
rep = run_once(args, system_dir, scratch, state_out, png_dir)
|
||||
oracle = oracle_for(args.oracle_data, args.content_sha1, rep.get("library_name"))
|
||||
rep["verdict"], rep["verdict_evidence"] = judge(rep, oracle)
|
||||
judge_repetition(rep, oracle)
|
||||
if rep.get("exit"):
|
||||
flags.append(f"repetition {index}: {rep['verdict_evidence']['detail']}")
|
||||
if not rep.get("loaded"):
|
||||
@@ -1023,15 +1023,14 @@ def run_variant(args: argparse.Namespace, base: dict[str, Path], variant: Varian
|
||||
if rep.get("observables") != reference:
|
||||
differing = sorted(k for k in (reference or {}) if (rep.get("observables") or {}).get(k) != reference[k])
|
||||
flags.append(f"repetition {index} disagrees with repetition 0 on {', '.join(differing)}")
|
||||
verdict, evidence = variant_verdict(reps)
|
||||
return {
|
||||
out = {
|
||||
"label": variant.label,
|
||||
"system_files": {dest: sha for dest, sha in sorted(staged.items())},
|
||||
"repetitions": reps,
|
||||
"flags": flags,
|
||||
"verdict": verdict,
|
||||
"verdict_evidence": evidence,
|
||||
}
|
||||
judge_variant(out)
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -1274,7 +1273,9 @@ def oracle_for(data: dict, content_sha1: str | None, library_name: str | None) -
|
||||
screens = [{"sha1": s["sha1"], "shows": s["shows"]} for s in core.get("error_screens") or ()]
|
||||
loaded = [p["pattern"] for p in core.get("bios_patterns") or ()]
|
||||
return {"references": references, "error_patterns": patterns, "error_screens": screens,
|
||||
"bios_patterns": loaded}
|
||||
"bios_patterns": loaded,
|
||||
"fallback_patterns": [p["pattern"] for p in core.get("fallback_patterns") or ()],
|
||||
"load_patterns": [p["pattern"] for p in core.get("load_patterns") or ()]}
|
||||
|
||||
|
||||
def sampled(rep: dict) -> list[tuple[int, str]]:
|
||||
@@ -1355,6 +1356,52 @@ def _without_the_game(rep: dict, oracle: dict, frames: list[tuple[int, str]]) ->
|
||||
"and no error screen"}
|
||||
|
||||
|
||||
BUILTIN = "builtin"
|
||||
SYSTEM_PLACE = "<system>/"
|
||||
|
||||
|
||||
def bios_used(rep: dict, oracle: dict) -> tuple[str, str | None]:
|
||||
"""Which BIOS the core ran, from its log, and the line that says so.
|
||||
|
||||
`staged:<dest>` when a load pattern names the file under the system
|
||||
directory, `builtin` when a fallback pattern says the core went to its
|
||||
own BIOS (OpenBIOS, HLE), `unknown` otherwise. The last such line wins:
|
||||
a core that tries a file and then falls back logs both, in that order.
|
||||
A file the core opened is no proof: Beetle PSX hashes every file of the
|
||||
folder before it decides.
|
||||
"""
|
||||
found: tuple[str, str | None] = ("unknown", None)
|
||||
for line in rep.get("logs") or ():
|
||||
text = line["text"]
|
||||
if any(re.search(pattern, text) for pattern in oracle.get("fallback_patterns") or ()):
|
||||
found = (BUILTIN, text)
|
||||
for pattern in oracle.get("load_patterns") or ():
|
||||
match = re.search(pattern, text)
|
||||
path = match.groupdict().get("path") if match else None
|
||||
if path:
|
||||
found = (f"staged:{path.removeprefix(SYSTEM_PLACE)}", text)
|
||||
return found
|
||||
|
||||
|
||||
def judge_repetition(rep: dict, oracle: dict) -> None:
|
||||
"""The verdict, its evidence and the BIOS used, written into the repetition."""
|
||||
verdict, evidence = judge(rep, oracle)
|
||||
used, line = bios_used(rep, oracle)
|
||||
if line:
|
||||
evidence["bios_log"] = line
|
||||
if verdict == BOOTS and used == BUILTIN:
|
||||
evidence["detail"] += ", on the core's built-in BIOS"
|
||||
rep["verdict"], rep["verdict_evidence"], rep["bios_used"] = verdict, evidence, used
|
||||
|
||||
|
||||
def judge_variant(variant: dict) -> None:
|
||||
"""The variant's verdict and BIOS: what every repetition agrees on."""
|
||||
reps = variant["repetitions"]
|
||||
variant["verdict"], variant["verdict_evidence"] = variant_verdict(reps)
|
||||
used = {rep.get("bios_used", "unknown") for rep in reps}
|
||||
variant["bios_used"] = used.pop() if len(used) == 1 else "unknown"
|
||||
|
||||
|
||||
def variant_verdict(reps: Sequence[dict]) -> tuple[str, dict]:
|
||||
"""The verdict all repetitions agree on, else unknown."""
|
||||
verdicts = [rep["verdict"] for rep in reps]
|
||||
@@ -1363,7 +1410,7 @@ def variant_verdict(reps: Sequence[dict]) -> tuple[str, dict]:
|
||||
evidence = {"rule": "repetitions_agree",
|
||||
"detail": f"{len(reps)} of {len(reps)} repetitions: {first['detail']}",
|
||||
"repetitions": verdicts}
|
||||
for key in ("frame", "sha1", "stages", "pattern", "log", "exit_status", "signal"):
|
||||
for key in ("frame", "sha1", "stages", "pattern", "log", "exit_status", "signal", "bios_log"):
|
||||
if key in first:
|
||||
evidence[key] = first[key]
|
||||
return verdicts[0], evidence
|
||||
|
||||
@@ -27,9 +27,10 @@ from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
RECORD_VERSION = 4
|
||||
RECORD_VERSION = 5
|
||||
# 3: before the boot oracle. 4: every repetition and variant carries a verdict.
|
||||
RECORD_VERSIONS = (3, 4)
|
||||
# 5: and the BIOS the core ran.
|
||||
RECORD_VERSIONS = (3, 4, 5)
|
||||
RECORDS_DIR = "measurements"
|
||||
ID_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}-[0-9a-f]{12}$")
|
||||
SLUG_PATTERN = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$")
|
||||
|
||||
@@ -218,6 +218,27 @@ class OracleTests(unittest.TestCase):
|
||||
self.assertEqual((verdict, evidence["stages"]), ("boots", ["bios", "game"]))
|
||||
self.assertEqual(core_probe.judge(self.rep(frames((0, self.TITLE))), oracle)[1]["stages"], ["game"])
|
||||
|
||||
def test_the_bios_used_comes_from_the_log(self) -> None:
|
||||
oracle = {**self.ORACLE, "fallback_patterns": [r"^Firmware is missing: "],
|
||||
"load_patterns": [r"^Firmware found: (?P<path>.+)$"]}
|
||||
cases = [
|
||||
(["Searching for firmware checksum: 0555", "Firmware is missing: scph5501.bin"], "builtin"),
|
||||
(["Firmware found: <system>/scph5501.bin", "Firmware SHA1: 0555"], "staged:scph5501.bin"),
|
||||
# A file tried, then the fallback: the last line decides.
|
||||
(["Firmware found: <system>/a.bin", "Firmware is missing: b.bin"], "builtin"),
|
||||
(["nothing about the BIOS"], "unknown"),
|
||||
]
|
||||
for logs, expected in cases:
|
||||
self.assertEqual(core_probe.bios_used(self.rep([], logs=logs), oracle)[0], expected, logs)
|
||||
rep = self.rep(frames((10, self.TITLE)), logs=["Firmware is missing: scph5501.bin"])
|
||||
core_probe.judge_repetition(rep, oracle)
|
||||
self.assertEqual((rep["verdict"], rep["bios_used"]), ("boots", "builtin"))
|
||||
self.assertTrue(rep["verdict_evidence"]["detail"].endswith("on the core's built-in BIOS"))
|
||||
self.assertEqual(rep["verdict_evidence"]["bios_log"], "Firmware is missing: scph5501.bin")
|
||||
variant = {"repetitions": [rep, {**rep, "bios_used": "staged:a.bin"}]}
|
||||
core_probe.judge_variant(variant)
|
||||
self.assertEqual(variant["bios_used"], "unknown")
|
||||
|
||||
def test_the_bios_screens_alone_are_not_a_boot(self) -> None:
|
||||
# A console that shows its logo and then refuses the disc never reached the game.
|
||||
verdict, evidence = core_probe.judge(
|
||||
@@ -516,7 +537,8 @@ class FakeCoreTests(unittest.TestCase):
|
||||
self.assertEqual(record["variants"][3]["system_files"],
|
||||
{"card.bin": hashlib.sha1(b"\x00").hexdigest()})
|
||||
self.assertEqual(record["inputs"]["oracle"]["error_patterns"], ["BIOS not found"])
|
||||
self.assertEqual(record["record_version"], 4)
|
||||
self.assertEqual(record["record_version"], 5)
|
||||
self.assertEqual({v["bios_used"] for v in record["variants"]}, {"unknown"})
|
||||
self.assert_schema(record)
|
||||
|
||||
def test_a_crash_and_a_hang_are_verdicts(self) -> None:
|
||||
|
||||
@@ -331,6 +331,24 @@ class ReadAndIndex(Workspace):
|
||||
self.assertTrue(any("measurements/beta.jsonl:1" in e and "kind" in e for e in errors), errors)
|
||||
self.assertIsNotNone(Draft202012Validator)
|
||||
|
||||
def test_a_version_5_record_names_the_bios_it_ran(self) -> None:
|
||||
try:
|
||||
from jsonschema import Draft202012Validator
|
||||
except ImportError:
|
||||
self.skipTest("jsonschema is not installed")
|
||||
schema = json.loads((REPO_ROOT / "schemas" / "core-probe-record.schema.json").read_text())
|
||||
record = mr.seal({**full_record("alpha", "card", "2026-10-28T10:00:00+00:00"), "record_version": 5})
|
||||
errors = [e.message for e in Draft202012Validator(schema).iter_errors(record)]
|
||||
self.assertIn("'bios_used' is a required property", errors)
|
||||
for variant in record["variants"]:
|
||||
variant["bios_used"] = "builtin"
|
||||
for rep in variant["repetitions"]:
|
||||
rep["bios_used"] = "staged:card.bin"
|
||||
self.assertEqual([e.message for e in Draft202012Validator(schema).iter_errors(record)
|
||||
if "bios_used" in e.message], [])
|
||||
record["variants"][0]["bios_used"] = "the other region's"
|
||||
self.assertTrue(list(Draft202012Validator(schema).iter_errors(record)))
|
||||
|
||||
def test_a_version_4_record_needs_its_verdicts(self) -> None:
|
||||
try:
|
||||
from jsonschema import Draft202012Validator # noqa: F401
|
||||
|
||||
Reference in new issue
Block a user