mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-13 12:22:33 -05:00
feat: hle_fallback field + launcher filtering in verify
Added hle_fallback: true/false per file in emulator profiles. When a core has HLE and the file is missing, severity downgrades to INFO instead of CRITICAL — core works without it. verify.py builds an HLE index from emulator profiles and applies it during severity computation. Cross-reference now skips launcher profiles (type: launcher) and includes hle_fallback in undeclared file reports. 33 E2E tests (4 new: HLE severity, HLE index, launcher skip, cross-ref HLE). 0 regressions. Based on source code analysis: - RetroArch core_info.c:2233 — existence check only, no blocking - PCSX ReARMed psxbios.c:28 — full HLE BIOS replacement - Dolphin CommonPaths.h — all files optional with HLE - snes9x — DSP HLE built-in, coprocessor files optional
This commit is contained in:
@@ -150,17 +150,24 @@ def verify_entry_md5(
|
||||
# Severity mapping per platform
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def compute_severity(status: str, required: bool, mode: str) -> str:
|
||||
"""Map (status, required, verification_mode) → severity.
|
||||
def compute_severity(
|
||||
status: str, required: bool, mode: str, hle_fallback: bool = False,
|
||||
) -> str:
|
||||
"""Map (status, required, verification_mode, hle_fallback) → severity.
|
||||
|
||||
Based on native platform behavior:
|
||||
Based on native platform behavior + emulator HLE capability:
|
||||
- RetroArch (existence): required+missing = warning, optional+missing = info
|
||||
- Batocera (md5): no required distinction — all equal (batocera-systems has no mandatory field)
|
||||
- Recalbox (md5): mandatory+missing = critical, optional+missing = warning (Bios.cpp:109-130)
|
||||
- Batocera (md5): no required distinction (batocera-systems has no mandatory field)
|
||||
- Recalbox (md5): mandatory+missing = critical, optional+missing = warning
|
||||
- hle_fallback: core works without this file via HLE → always INFO when missing
|
||||
"""
|
||||
if status == Status.OK:
|
||||
return Severity.OK
|
||||
|
||||
# HLE fallback: core works without this file regardless of platform requirement
|
||||
if hle_fallback and status == Status.MISSING:
|
||||
return Severity.INFO
|
||||
|
||||
if mode == "existence":
|
||||
if status == Status.MISSING:
|
||||
return Severity.WARNING if required else Severity.INFO
|
||||
@@ -170,7 +177,7 @@ def compute_severity(status: str, required: bool, mode: str) -> str:
|
||||
if status == Status.MISSING:
|
||||
return Severity.CRITICAL if required else Severity.WARNING
|
||||
if status == Status.UNTESTED:
|
||||
return Severity.WARNING if required else Severity.WARNING
|
||||
return Severity.WARNING
|
||||
return Severity.OK
|
||||
|
||||
|
||||
@@ -213,6 +220,9 @@ def find_undeclared_files(
|
||||
undeclared = []
|
||||
seen = set()
|
||||
for emu_name, profile in sorted(profiles.items()):
|
||||
# Skip launchers — they don't use system_dir for BIOS
|
||||
if profile.get("type") == "launcher":
|
||||
continue
|
||||
emu_systems = set(profile.get("systems", []))
|
||||
# Only check emulators whose systems overlap with this platform
|
||||
if not emu_systems & platform_systems:
|
||||
@@ -240,6 +250,7 @@ def find_undeclared_files(
|
||||
"emulator": profile.get("emulator", emu_name),
|
||||
"name": fname,
|
||||
"required": f.get("required", False),
|
||||
"hle_fallback": f.get("hle_fallback", False),
|
||||
"in_repo": in_repo,
|
||||
"note": f.get("note", ""),
|
||||
})
|
||||
@@ -267,6 +278,14 @@ def verify_platform(
|
||||
)
|
||||
zip_contents = build_zip_contents_index(db) if has_zipped else {}
|
||||
|
||||
# Build HLE index from emulator profiles: {filename: True} if any core has HLE for it
|
||||
profiles = emu_profiles if emu_profiles is not None else load_emulator_profiles(emulators_dir)
|
||||
hle_index: dict[str, bool] = {}
|
||||
for profile in profiles.values():
|
||||
for f in profile.get("files", []):
|
||||
if f.get("hle_fallback"):
|
||||
hle_index[f.get("name", "")] = True
|
||||
|
||||
# Per-entry results
|
||||
details = []
|
||||
# Per-destination aggregation
|
||||
@@ -284,6 +303,7 @@ def verify_platform(
|
||||
else:
|
||||
result = verify_entry_md5(file_entry, local_path, resolve_status)
|
||||
result["system"] = sys_id
|
||||
result["hle_fallback"] = hle_index.get(file_entry.get("name", ""), False)
|
||||
details.append(result)
|
||||
|
||||
# Aggregate by destination
|
||||
@@ -296,7 +316,8 @@ def verify_platform(
|
||||
if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get(prev, 0):
|
||||
file_status[dest] = cur
|
||||
file_required[dest] = required
|
||||
sev = compute_severity(cur, required, mode)
|
||||
hle = hle_index.get(file_entry.get("name", ""), False)
|
||||
sev = compute_severity(cur, required, mode, hle)
|
||||
prev_sev = file_severity.get(dest)
|
||||
if prev_sev is None or _SEVERITY_ORDER.get(sev, 0) > _SEVERITY_ORDER.get(prev_sev, 0):
|
||||
file_severity[dest] = sev
|
||||
@@ -362,8 +383,9 @@ def print_platform_result(result: dict, group: list[str]) -> None:
|
||||
continue
|
||||
seen_details.add(key)
|
||||
req = "required" if d.get("required", True) else "optional"
|
||||
hle = ", HLE available" if d.get("hle_fallback") else ""
|
||||
reason = d.get("reason", "")
|
||||
print(f" UNTESTED ({req}): {key} — {reason}")
|
||||
print(f" UNTESTED ({req}{hle}): {key} — {reason}")
|
||||
for d in result["details"]:
|
||||
if d["status"] == Status.MISSING:
|
||||
key = f"{d['system']}/{d['name']}"
|
||||
@@ -371,7 +393,8 @@ def print_platform_result(result: dict, group: list[str]) -> None:
|
||||
continue
|
||||
seen_details.add(key)
|
||||
req = "required" if d.get("required", True) else "optional"
|
||||
print(f" MISSING ({req}): {key}")
|
||||
hle = ", HLE available" if d.get("hle_fallback") else ""
|
||||
print(f" MISSING ({req}{hle}): {key}")
|
||||
|
||||
# Cross-reference: undeclared files used by cores
|
||||
undeclared = result.get("undeclared_files", [])
|
||||
|
||||
@@ -280,6 +280,30 @@ class TestE2E(unittest.TestCase):
|
||||
with open(os.path.join(self.emulators_dir, "test_emu.yml"), "w") as fh:
|
||||
yaml.dump(emu, fh)
|
||||
|
||||
# Emulator with HLE fallback
|
||||
emu_hle = {
|
||||
"emulator": "TestHLE",
|
||||
"type": "libretro",
|
||||
"systems": ["console-a"],
|
||||
"files": [
|
||||
{"name": "present_req.bin", "required": True, "hle_fallback": True},
|
||||
{"name": "hle_missing.bin", "required": True, "hle_fallback": True},
|
||||
{"name": "no_hle_missing.bin", "required": True, "hle_fallback": False},
|
||||
],
|
||||
}
|
||||
with open(os.path.join(self.emulators_dir, "test_hle.yml"), "w") as fh:
|
||||
yaml.dump(emu_hle, fh)
|
||||
|
||||
# Launcher profile (should be excluded from cross-reference)
|
||||
launcher = {
|
||||
"emulator": "TestLauncher",
|
||||
"type": "launcher",
|
||||
"systems": ["console-a"],
|
||||
"files": [{"name": "launcher_bios.bin", "required": True}],
|
||||
}
|
||||
with open(os.path.join(self.emulators_dir, "test_launcher.yml"), "w") as fh:
|
||||
yaml.dump(launcher, fh)
|
||||
|
||||
# Alias profile (should be skipped)
|
||||
alias = {"emulator": "TestAlias", "type": "alias", "alias_of": "test_emu", "files": []}
|
||||
with open(os.path.join(self.emulators_dir, "test_alias.yml"), "w") as fh:
|
||||
@@ -455,6 +479,46 @@ class TestE2E(unittest.TestCase):
|
||||
# dd_covered.bin from TestEmuDD should NOT appear (data_dir match)
|
||||
self.assertNotIn("dd_covered.bin", names)
|
||||
|
||||
def test_44_cross_ref_skips_launchers(self):
|
||||
config = load_platform_config("test_existence", self.platforms_dir)
|
||||
profiles = load_emulator_profiles(self.emulators_dir)
|
||||
undeclared = find_undeclared_files(config, self.emulators_dir, self.db, profiles)
|
||||
names = {u["name"] for u in undeclared}
|
||||
# launcher_bios.bin from TestLauncher should NOT appear
|
||||
self.assertNotIn("launcher_bios.bin", names)
|
||||
|
||||
def test_45_hle_fallback_downgrades_severity(self):
|
||||
"""Missing file with hle_fallback=true → INFO severity, not CRITICAL."""
|
||||
from verify import compute_severity, Severity
|
||||
# required + missing + NO HLE = CRITICAL
|
||||
sev = compute_severity("missing", True, "md5", hle_fallback=False)
|
||||
self.assertEqual(sev, Severity.CRITICAL)
|
||||
# required + missing + HLE = INFO
|
||||
sev = compute_severity("missing", True, "md5", hle_fallback=True)
|
||||
self.assertEqual(sev, Severity.INFO)
|
||||
# required + missing + HLE + existence mode = INFO
|
||||
sev = compute_severity("missing", True, "existence", hle_fallback=True)
|
||||
self.assertEqual(sev, Severity.INFO)
|
||||
|
||||
def test_46_hle_index_built_from_emulator_profiles(self):
|
||||
"""verify_platform reads hle_fallback from emulator profiles."""
|
||||
config = load_platform_config("test_existence", self.platforms_dir)
|
||||
profiles = load_emulator_profiles(self.emulators_dir)
|
||||
result = verify_platform(config, self.db, self.emulators_dir, profiles)
|
||||
# present_req.bin has hle_fallback: true in TestHLE profile
|
||||
for d in result["details"]:
|
||||
if d["name"] == "present_req.bin":
|
||||
self.assertTrue(d.get("hle_fallback", False))
|
||||
break
|
||||
|
||||
def test_47_cross_ref_shows_hle_on_undeclared(self):
|
||||
"""Undeclared files include hle_fallback from emulator profile."""
|
||||
config = load_platform_config("test_existence", self.platforms_dir)
|
||||
profiles = load_emulator_profiles(self.emulators_dir)
|
||||
undeclared = find_undeclared_files(config, self.emulators_dir, self.db, profiles)
|
||||
hle_files = {u["name"] for u in undeclared if u.get("hle_fallback")}
|
||||
self.assertIn("hle_missing.bin", hle_files)
|
||||
|
||||
def test_50_platform_grouping_identical(self):
|
||||
groups = group_identical_platforms(
|
||||
["test_existence", "test_inherited"], self.platforms_dir
|
||||
|
||||
Reference in New Issue
Block a user