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:
Abdessamad Derraz committed 2026-10-27 17:58:48 +01:00
1 parent a17406edae
commit 70c3bbd853
8 files changed
+111 -21

No files matched your search

+8 -1
View File
@@ -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
View File
@@ -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)
+14 -3
View File
@@ -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
View File
@@ -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]
+8 -1
View File
@@ -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":
+21
View File
@@ -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)
+9 -1
View File
@@ -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
View File
@@ -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.