mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
feat: settle a file only when the run uses it
hle_fallback needs the file to change the run: Beetle PSX hashes every file of its folder, so opened is not used. PCSX-ReARMed hides the BIOS logo by default: its BIOS load line stands for the bios stage. A check runs again only when the oracle entries of its content and core change, and the rules run on every pass from the kept records.
This commit is contained in:
1 parent
a17406edae
commit
70c3bbd853
8 files changed
+111
-21
No files matched your search
@@ -1004,11 +1004,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bios_patterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "What the verdicts were judged against: the reference frames of this content and core, the core's error patterns and error screens, from the oracle file at its sha256. Null when the run had no oracle entry."
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-6
@@ -903,6 +903,7 @@ class Campaign:
|
||||
self.store = ContentStore(self)
|
||||
self.content = load_content(paths.content_file)
|
||||
self.oracle_sha = file_sha256(paths.oracle) if paths.oracle.is_file() else ""
|
||||
self.oracle_data = core_probe.load_oracle(paths.oracle)[0]
|
||||
self.deadline = 0.0
|
||||
self.current: Pass | None = None
|
||||
self.dirty_at_start: set[str] = set()
|
||||
@@ -1055,9 +1056,14 @@ class Campaign:
|
||||
|
||||
def remember(self, emulator: str, key: str, result: ProbeResult, core: Core | None,
|
||||
digest: str) -> None:
|
||||
judged = None
|
||||
if result.record and result.record["inputs"].get("content"):
|
||||
judged = [result.record["inputs"]["content"]["sha1"], result.record["core"]["library_name"]]
|
||||
entry = {"status": result.status, "at": now_iso(), "profile_digest": digest,
|
||||
"core_sha256": core.sha256 if core else None,
|
||||
"record_version": mr.RECORD_VERSION, "oracle_sha256": self.oracle_sha}
|
||||
"record_version": mr.RECORD_VERSION, "oracle_sha256": self.oracle_digest(judged)}
|
||||
if judged:
|
||||
entry["oracle_key"] = judged
|
||||
if result.reason:
|
||||
entry["reason"] = result.reason
|
||||
if result.log:
|
||||
@@ -1073,9 +1079,27 @@ class Campaign:
|
||||
self.state["checks"].setdefault(emulator, {})[key] = {
|
||||
"status": PENDING, "reason": reason, "at": now_iso(), "profile_digest": digest}
|
||||
|
||||
def oracle_digest(self, key: list[str] | None) -> str:
|
||||
"""What a check's verdicts depend on in the oracle: the entries of its
|
||||
content and core, else the whole file. Another core's references
|
||||
never make a check run again."""
|
||||
if not key:
|
||||
return self.oracle_sha
|
||||
return mr.value_sha1(core_probe.oracle_for(self.oracle_data, *key))
|
||||
|
||||
def needed(self, emulator: str, key: str, core: Core, digest: str) -> bool:
|
||||
return should_run(self.state["checks"].get(emulator, {}).get(key), core.sha256, digest,
|
||||
self.oracle_sha)
|
||||
previous = self.state["checks"].get(emulator, {}).get(key)
|
||||
judged = (previous or {}).get("oracle_key")
|
||||
if previous and not judged and previous.get("record"):
|
||||
record = self.read_record(emulator, previous["record"]) or {}
|
||||
if (record.get("inputs") or {}).get("content"):
|
||||
judged = [record["inputs"]["content"]["sha1"], record["core"]["library_name"]]
|
||||
previous["oracle_key"] = judged
|
||||
# Judged against the entries the record kept, not the file it named.
|
||||
kept = {k: v for k, v in (record["inputs"].get("oracle") or {}).items()
|
||||
if k not in ("file", "sha256")}
|
||||
previous["oracle_sha256"] = mr.value_sha1({"bios_patterns": [], **kept})
|
||||
return should_run(previous, core.sha256, digest, self.oracle_digest(judged))
|
||||
|
||||
def check(self, emulator: str, key: str, core: Core, digest: str, argv: list[str],
|
||||
log_name: str) -> dict | None:
|
||||
@@ -1134,8 +1158,8 @@ class Campaign:
|
||||
keys[f"negative:{dest}"] = self.check_key(core, content, f"negative:{dest}")
|
||||
if self.settings.bench:
|
||||
keys["bench"] = self.check_key(core, content, "bench")
|
||||
if not any(self.needed(emulator, key, core, digest) for key in keys.values()):
|
||||
return
|
||||
# The rules run on every pass, from the records kept when nothing
|
||||
# needs to run again: a change of the table applies without a run.
|
||||
try:
|
||||
folder = self.store.get(content)
|
||||
load = load_file(folder, content.load)
|
||||
@@ -1174,7 +1198,7 @@ class Campaign:
|
||||
for label in labels[3:]:
|
||||
self.log(f"{emulator}: {rules.rule_region(rules.run_of(record, label), present, entry, content.region)}")
|
||||
if dest in self.opened(files_record, 0) or dest in self.opened(record, 0):
|
||||
verdicts = rules.rule_need(none, present, entry)
|
||||
verdicts = rules.rule_need(none, present, entry, rules.read(none, present, zeros, half))
|
||||
validation = rules.rule_validation(zeros, half, none, present, entry)
|
||||
if validation:
|
||||
verdicts.append(validation)
|
||||
|
||||
@@ -11,7 +11,7 @@ BIOS at all, `present` with the file alone, and wrong files staged under its
|
||||
name (zeros of its size, its first half, another region's dump).
|
||||
|
||||
runs field settled
|
||||
none boots hle_fallback true
|
||||
none boots, the file changes the run hle_fallback true
|
||||
none fails, present boots required true
|
||||
hle_fallback false
|
||||
zeros refused, present boots validation a content check
|
||||
@@ -123,9 +123,20 @@ def agrees(entry: dict, verdict: Verdict) -> bool:
|
||||
return current(entry, verdict.field) == verdict.value
|
||||
|
||||
|
||||
def rule_need(none: Run, present: Run, entry: dict) -> list[Verdict]:
|
||||
"""No BIOS at all against the file alone: required and hle_fallback."""
|
||||
def read(none: Run, *runs: Run) -> bool:
|
||||
"""True when the file changes the run: alone or as a wrong file, one
|
||||
variant ends otherwise than the run with no BIOS, or reaches other
|
||||
stages. A core can open a file without using it for this content (a
|
||||
scan that hashes every file of the folder)."""
|
||||
return any(run.verdict != none.verdict or run.stages != none.stages for run in runs if run)
|
||||
|
||||
|
||||
def rule_need(none: Run, present: Run, entry: dict, used: bool = True) -> list[Verdict]:
|
||||
"""No BIOS at all against the file alone: required and hle_fallback. A
|
||||
file the content never uses settles nothing."""
|
||||
name = entry["name"]
|
||||
if none.verdict == BOOTS and not used:
|
||||
return []
|
||||
if none.verdict == BOOTS:
|
||||
out = [Verdict("boots_without_file", "hle_fallback", True,
|
||||
f"the core boots the content with no BIOS at all, {name} included",
|
||||
|
||||
+11
-3
@@ -1173,8 +1173,9 @@ DEFAULT_ORACLE = ROOT / "tests" / "campaign" / "oracle.yml"
|
||||
|
||||
def oracle_for(data: dict, content_sha1: str | None, library_name: str | None) -> dict:
|
||||
"""The oracle entries that judge one content on one core: its reference
|
||||
frames, the error patterns every core shares and the core's own, and the
|
||||
core's known error screens."""
|
||||
frames, the error patterns every core shares and the core's own, the
|
||||
core's known error screens, and the log lines by which it says it loaded
|
||||
the console's own BIOS."""
|
||||
core = (data.get("cores") or {}).get(library_name or "") or {}
|
||||
references: list[dict] = []
|
||||
for item in data.get("content") or ():
|
||||
@@ -1185,7 +1186,9 @@ def oracle_for(data: dict, content_sha1: str | None, library_name: str | None) -
|
||||
patterns = [p["pattern"] for p in (data.get("error_patterns") or ())]
|
||||
patterns += [p["pattern"] for p in core.get("error_patterns") or ()]
|
||||
screens = [{"sha1": s["sha1"], "shows": s["shows"]} for s in core.get("error_screens") or ()]
|
||||
return {"references": references, "error_patterns": patterns, "error_screens": screens}
|
||||
loaded = [p["pattern"] for p in core.get("bios_patterns") or ()]
|
||||
return {"references": references, "error_patterns": patterns, "error_screens": screens,
|
||||
"bios_patterns": loaded}
|
||||
|
||||
|
||||
def sampled(rep: dict) -> list[tuple[int, str]]:
|
||||
@@ -1218,6 +1221,11 @@ def judge(rep: dict, oracle: dict) -> tuple[str, dict]:
|
||||
references = {r["sha1"]: r for r in oracle.get("references") or ()}
|
||||
reached = [(frame, digest) for frame, digest in frames if digest in references]
|
||||
stages = list(dict.fromkeys(references[d]["stage"] for _, d in reached))
|
||||
# A core that hides the BIOS screens by default still says it loaded one.
|
||||
if "bios" not in stages and any(re.search(pattern, line["text"])
|
||||
for pattern in oracle.get("bios_patterns") or ()
|
||||
for line in rep.get("logs") or ()):
|
||||
stages.insert(0, "bios")
|
||||
game = [(frame, digest) for frame, digest in reached if references[digest]["stage"] == "game"]
|
||||
if game:
|
||||
frame, digest = game[0]
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
# cores:. A line a core logs and then carries on (a fallback to a
|
||||
# built-in BIOS) is not an error pattern.
|
||||
# error_screens per core, the sha1 of a frame that shows its BIOS error message.
|
||||
# bios_patterns per core, log lines by which it says it loaded the console's
|
||||
# own BIOS: they stand for the bios-stage screens of a core
|
||||
# that hides them by default.
|
||||
#
|
||||
# A run that reaches no reference frame, matches no pattern or screen and
|
||||
# does not end on one colour is `unknown`: frames that differ from another
|
||||
@@ -47,7 +50,11 @@ cores:
|
||||
Without a BIOS, or with one of the wrong size, it logs "Using HLE BIOS"
|
||||
and runs its own (frontend/libretro.c:3893-3899 at c8816799). The BIOS
|
||||
boot logo is off by default (pcsx_rearmed_show_bios_bootlogo), so its
|
||||
runs reach no bios-stage screen.
|
||||
runs reach no bios-stage screen: the line it logs once a file passes
|
||||
its signature check and is kept stands for them.
|
||||
bios_patterns:
|
||||
- pattern: '^found (US|EU|JP) BIOS file, crc32 '
|
||||
source_ref: frontend/libretro.c:3646-3669 at c8816799
|
||||
error_patterns: []
|
||||
error_screens: []
|
||||
"PCSX1":
|
||||
|
||||
@@ -155,6 +155,14 @@ class RuleTable(unittest.TestCase):
|
||||
optional = rules.rule_need(NONE_BOOTS, PRESENT, {"name": "a", "required": False})
|
||||
self.assertEqual([v.field for v in optional], ["hle_fallback"])
|
||||
|
||||
def test_a_file_the_content_never_uses_settles_nothing(self) -> None:
|
||||
unused = run(rules.BOOTS, "bios.bin", ("game",))
|
||||
self.assertFalse(rules.read(NONE_BOOTS, unused, unused, unused))
|
||||
self.assertEqual(rules.rule_need(NONE_BOOTS, unused, self.ENTRY, used=False), [])
|
||||
# A wrong file that hangs where no file boots: the core reads it.
|
||||
self.assertTrue(rules.read(NONE_BOOTS, unused, run(rules.HANG), unused))
|
||||
self.assertTrue(rules.read(NONE_BOOTS, PRESENT, None, None))
|
||||
|
||||
def test_fails_without_and_boots_alone_settles_required(self) -> None:
|
||||
for failure in rules.FAILS:
|
||||
verdicts = rules.rule_need(run(failure, "none"), PRESENT, self.ENTRY)
|
||||
@@ -397,6 +405,19 @@ class StateAndResume(Workspace):
|
||||
self.assertTrue(campaign.should_run({**done, "status": "pending"}, "a", "d"))
|
||||
self.assertFalse(campaign.should_run({**done, "status": "failed"}, "a", "d"))
|
||||
|
||||
def test_only_its_own_oracle_entries_make_a_check_run_again(self) -> None:
|
||||
key = [hashlib.sha1(b"GAME").hexdigest(), "Fake"]
|
||||
before = self.make().oracle_digest(key)
|
||||
data = yaml.safe_load(self.oracle.read_text())
|
||||
data["content"][0]["references"]["Other"] = {"recorded_from": "2026-10-27-000000000000",
|
||||
"frames": [{"sha1": "a" * 40, "stage": "game",
|
||||
"shows": "x"}]}
|
||||
self.oracle.write_text(yaml.safe_dump(data), encoding="utf-8")
|
||||
self.assertEqual(self.make().oracle_digest(key), before)
|
||||
data["cores"] = {"Fake": {"error_patterns": [{"pattern": "BIOS", "reason": "r"}]}}
|
||||
self.oracle.write_text(yaml.safe_dump(data), encoding="utf-8")
|
||||
self.assertNotEqual(self.make().oracle_digest(key), before)
|
||||
|
||||
def test_evidence_does_not_change_the_digest(self) -> None:
|
||||
profile = yaml.safe_load(PROFILE)
|
||||
before = campaign.profile_digest(profile)
|
||||
|
||||
@@ -211,6 +211,13 @@ class OracleTests(unittest.TestCase):
|
||||
self.assertEqual((evidence["rule"], evidence["frame"], evidence["stages"]),
|
||||
("reference_frame", 20, ["bios", "game"]))
|
||||
|
||||
def test_a_bios_load_line_stands_for_hidden_bios_screens(self) -> None:
|
||||
oracle = {**self.ORACLE, "bios_patterns": [r"^found US BIOS file"]}
|
||||
verdict, evidence = core_probe.judge(
|
||||
self.rep(frames((0, self.TITLE)), logs=["found US BIOS file, crc32 1: x"]), oracle)
|
||||
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_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(
|
||||
@@ -275,6 +282,7 @@ class OracleTests(unittest.TestCase):
|
||||
picked = core_probe.oracle_for(data, "a" * 40, "Fake")
|
||||
self.assertEqual(picked["references"], [{"sha1": self.TITLE, "stage": "game", "shows": "t"}])
|
||||
self.assertEqual(picked["error_patterns"], ["shared", "own"])
|
||||
self.assertEqual(picked["bios_patterns"], [])
|
||||
self.assertEqual(core_probe.oracle_for(data, "b" * 40, "Fake")["references"], [])
|
||||
self.assertEqual(core_probe.oracle_for(data, "a" * 40, "Nobody")["error_patterns"], ["shared"])
|
||||
|
||||
@@ -283,7 +291,7 @@ class OracleTests(unittest.TestCase):
|
||||
data = yaml.safe_load(core_probe.DEFAULT_ORACLE.read_text(encoding="utf-8"))
|
||||
patterns = list(data.get("error_patterns") or ())
|
||||
for core in (data.get("cores") or {}).values():
|
||||
patterns += core.get("error_patterns") or []
|
||||
patterns += (core.get("error_patterns") or []) + (core.get("bios_patterns") or [])
|
||||
for screen in core.get("error_screens") or ():
|
||||
self.assertRegex(screen["sha1"], r"^[0-9a-f]{40}$")
|
||||
for pattern in patterns:
|
||||
|
||||
+10
-6
@@ -891,7 +891,9 @@ content by the sha1 of the file handed to the core and a core by its library
|
||||
name. A reference frame is recorded once with the full BIOS set and reviewed
|
||||
by looking at its PNG. An error pattern is a log line after which the core
|
||||
stops, read from its source: a line a core logs before it falls back to a
|
||||
built-in BIOS is not one. Frames that differ from another run never make a
|
||||
built-in BIOS is not one. A BIOS load pattern is the line a core logs once
|
||||
it keeps the console's BIOS: it adds the `bios` stage for a core that hides
|
||||
the BIOS screens by default (PCSX-ReARMed). Frames that differ from another run never make a
|
||||
boot.
|
||||
|
||||
| Option | Effect |
|
||||
@@ -1000,7 +1002,7 @@ python scripts/campaign.py install-timer # opt-in: writes the autorun units,
|
||||
|
||||
| Verdicts | Field | Written |
|
||||
|----------|-------|---------|
|
||||
| `none` boots | `hle_fallback` | `true` |
|
||||
| `none` boots, and the file changes the run: alone or as a wrong file, another verdict or other stages than with no BIOS | `hle_fallback` | `true`. A file the game never uses settles nothing: a core can open every file of the folder to hash it |
|
||||
| `none` fails (`bios_error`, `crash`, `hang`) and the file alone boots | `required`, `hle_fallback` | `true`, `false` |
|
||||
| Zeros of the file's size refused while the file boots | `validation` | a content check is proven. Agreeing adds evidence, a profile with no md5, sha1, sha256, crc32 or adler32 is an open finding |
|
||||
| Zeros used while the file boots | `validation` | no content check. A profile that declares one is an open finding |
|
||||
@@ -1026,16 +1028,18 @@ prints the record id with the screens that stay on for 3 samples or more in
|
||||
every repetition. A reviewer opens those PNGs and writes the chosen frames
|
||||
into `tests/campaign/oracle.yml` with their stage and what they show.
|
||||
|
||||
A run that agrees with the profile adds its record id to `evidence:` and
|
||||
The rules run on every pass, from the kept records when no check needs to run
|
||||
again, so a change of the table applies without a run. A run that agrees with
|
||||
the profile adds its record id to `evidence:` and
|
||||
resolves an open finding of the same field. The profile is edited as text and
|
||||
the edit is refused unless the result parses to the same profile with only
|
||||
those fields changed.
|
||||
|
||||
State lives in `.cache/campaign/state.json` of the main checkout: each check
|
||||
is `done`, `failed` (with the reason and its log) or `pending` (with the
|
||||
reason), with the core sha256, the profile digest, the oracle sha256 and the
|
||||
record version it ran against. A pass skips a check that is done or failed
|
||||
against the same four, so it
|
||||
reason), with the core sha256, the profile digest, the oracle entries of its
|
||||
content and core (by sha1) and the record version it ran against. A pass
|
||||
skips a check that is done or failed against the same four, so it
|
||||
takes new nightlies, changed profiles, newly fetched content and pending
|
||||
checks. The campaign's own `evidence:` writes do not count as a profile change.
|
||||
|
||||
|
||||
Reference in new issue
Block a user