mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-18 23:02: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
|
# Severity mapping per platform
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def compute_severity(status: str, required: bool, mode: str) -> str:
|
def compute_severity(
|
||||||
"""Map (status, required, verification_mode) → 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
|
- RetroArch (existence): required+missing = warning, optional+missing = info
|
||||||
- Batocera (md5): no required distinction — all equal (batocera-systems has no mandatory field)
|
- Batocera (md5): no required distinction (batocera-systems has no mandatory field)
|
||||||
- Recalbox (md5): mandatory+missing = critical, optional+missing = warning (Bios.cpp:109-130)
|
- 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:
|
if status == Status.OK:
|
||||||
return Severity.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 mode == "existence":
|
||||||
if status == Status.MISSING:
|
if status == Status.MISSING:
|
||||||
return Severity.WARNING if required else Severity.INFO
|
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:
|
if status == Status.MISSING:
|
||||||
return Severity.CRITICAL if required else Severity.WARNING
|
return Severity.CRITICAL if required else Severity.WARNING
|
||||||
if status == Status.UNTESTED:
|
if status == Status.UNTESTED:
|
||||||
return Severity.WARNING if required else Severity.WARNING
|
return Severity.WARNING
|
||||||
return Severity.OK
|
return Severity.OK
|
||||||
|
|
||||||
|
|
||||||
@@ -213,6 +220,9 @@ def find_undeclared_files(
|
|||||||
undeclared = []
|
undeclared = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for emu_name, profile in sorted(profiles.items()):
|
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", []))
|
emu_systems = set(profile.get("systems", []))
|
||||||
# Only check emulators whose systems overlap with this platform
|
# Only check emulators whose systems overlap with this platform
|
||||||
if not emu_systems & platform_systems:
|
if not emu_systems & platform_systems:
|
||||||
@@ -240,6 +250,7 @@ def find_undeclared_files(
|
|||||||
"emulator": profile.get("emulator", emu_name),
|
"emulator": profile.get("emulator", emu_name),
|
||||||
"name": fname,
|
"name": fname,
|
||||||
"required": f.get("required", False),
|
"required": f.get("required", False),
|
||||||
|
"hle_fallback": f.get("hle_fallback", False),
|
||||||
"in_repo": in_repo,
|
"in_repo": in_repo,
|
||||||
"note": f.get("note", ""),
|
"note": f.get("note", ""),
|
||||||
})
|
})
|
||||||
@@ -267,6 +278,14 @@ def verify_platform(
|
|||||||
)
|
)
|
||||||
zip_contents = build_zip_contents_index(db) if has_zipped else {}
|
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
|
# Per-entry results
|
||||||
details = []
|
details = []
|
||||||
# Per-destination aggregation
|
# Per-destination aggregation
|
||||||
@@ -284,6 +303,7 @@ def verify_platform(
|
|||||||
else:
|
else:
|
||||||
result = verify_entry_md5(file_entry, local_path, resolve_status)
|
result = verify_entry_md5(file_entry, local_path, resolve_status)
|
||||||
result["system"] = sys_id
|
result["system"] = sys_id
|
||||||
|
result["hle_fallback"] = hle_index.get(file_entry.get("name", ""), False)
|
||||||
details.append(result)
|
details.append(result)
|
||||||
|
|
||||||
# Aggregate by destination
|
# 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):
|
if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get(prev, 0):
|
||||||
file_status[dest] = cur
|
file_status[dest] = cur
|
||||||
file_required[dest] = required
|
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)
|
prev_sev = file_severity.get(dest)
|
||||||
if prev_sev is None or _SEVERITY_ORDER.get(sev, 0) > _SEVERITY_ORDER.get(prev_sev, 0):
|
if prev_sev is None or _SEVERITY_ORDER.get(sev, 0) > _SEVERITY_ORDER.get(prev_sev, 0):
|
||||||
file_severity[dest] = sev
|
file_severity[dest] = sev
|
||||||
@@ -362,8 +383,9 @@ def print_platform_result(result: dict, group: list[str]) -> None:
|
|||||||
continue
|
continue
|
||||||
seen_details.add(key)
|
seen_details.add(key)
|
||||||
req = "required" if d.get("required", True) else "optional"
|
req = "required" if d.get("required", True) else "optional"
|
||||||
|
hle = ", HLE available" if d.get("hle_fallback") else ""
|
||||||
reason = d.get("reason", "")
|
reason = d.get("reason", "")
|
||||||
print(f" UNTESTED ({req}): {key} — {reason}")
|
print(f" UNTESTED ({req}{hle}): {key} — {reason}")
|
||||||
for d in result["details"]:
|
for d in result["details"]:
|
||||||
if d["status"] == Status.MISSING:
|
if d["status"] == Status.MISSING:
|
||||||
key = f"{d['system']}/{d['name']}"
|
key = f"{d['system']}/{d['name']}"
|
||||||
@@ -371,7 +393,8 @@ def print_platform_result(result: dict, group: list[str]) -> None:
|
|||||||
continue
|
continue
|
||||||
seen_details.add(key)
|
seen_details.add(key)
|
||||||
req = "required" if d.get("required", True) else "optional"
|
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
|
# Cross-reference: undeclared files used by cores
|
||||||
undeclared = result.get("undeclared_files", [])
|
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:
|
with open(os.path.join(self.emulators_dir, "test_emu.yml"), "w") as fh:
|
||||||
yaml.dump(emu, 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 profile (should be skipped)
|
||||||
alias = {"emulator": "TestAlias", "type": "alias", "alias_of": "test_emu", "files": []}
|
alias = {"emulator": "TestAlias", "type": "alias", "alias_of": "test_emu", "files": []}
|
||||||
with open(os.path.join(self.emulators_dir, "test_alias.yml"), "w") as fh:
|
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)
|
# dd_covered.bin from TestEmuDD should NOT appear (data_dir match)
|
||||||
self.assertNotIn("dd_covered.bin", names)
|
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):
|
def test_50_platform_grouping_identical(self):
|
||||||
groups = group_identical_platforms(
|
groups = group_identical_platforms(
|
||||||
["test_existence", "test_inherited"], self.platforms_dir
|
["test_existence", "test_inherited"], self.platforms_dir
|
||||||
|
|||||||
Reference in New Issue
Block a user