From 871caf9cae79e3af30122c2d6e93ac5b03511f88 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:16:25 +0100 Subject: [PATCH 1/6] fix: scan placeholder BIOS folders in packs LRPS2 and PCEE2 name their BIOS .bin, which resolves to no file, so the platform pack's agnostic scan never anchored pcsx2/bios/ for them. On RetroArch yaps2 anchored it and the files carried only yaps2 as their core. On Batocera and RetroBat, where yaps2 does not run, pcsx2/bios/ stayed empty or held only the platform's own list. The scan now takes the folders where the system's other profiles resolve a hashed dump of that shape, the evidence the emulator pack already used, and a companion (.nvm) looks where its image is proven. --- scripts/packextras.py | 67 ++++++++++++++++++++++++++++-------- tests/test_agnostic_scope.py | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/scripts/packextras.py b/scripts/packextras.py index 056eaedd0..df9d22557 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -513,6 +513,7 @@ def _agnostic_scan_extras( extras_prefix: str, standalone_set: set[str] | None = None, taken: dict[str, dict] | None = None, + all_profiles: dict | None = None, ) -> list[dict]: """Every interchangeable candidate a filename-agnostic core accepts. @@ -521,6 +522,8 @@ def _agnostic_scan_extras( The directory is the delicate part: anchoring it on a single filename walked into another emulator's tree, so the profile's own files have to agree before the scan follows an ambiguous name. + ``all_profiles`` are the profiles a placeholder's directory is proven + from, ``profiles`` when not given. """ extras: list[dict] = [] files_db = db.get("files", {}) @@ -617,7 +620,21 @@ def _agnostic_scan_extras( resolved_dirs.get(id(image), "") if image else resolved_dirs.get(id(f), "") ) - if not path_prefix: + # A placeholder (.bin) resolves to nothing and anchors no + # directory. The folders where the system's other profiles + # resolve hashed files of that shape do, those folders only: + # LRPS2 and PCEE2 are made of placeholders, and on Batocera, + # where no other libretro PS2 core anchors pcsx2/bios/, the + # pack held nothing there. A companion (.nvm) looks + # where its image is proven: RetroBat's pcsx2/bios/ had + # scph39001.bin and its .nvm only at the BIOS root. + exact_dirs: frozenset[str] = frozenset() + anchor = image if image else f + if not path_prefix and "<" in str(anchor.get("name", "")): + exact_dirs = placeholder_directories( + anchor, emu_name, profile, db, all_profiles or profiles + ) + if not path_prefix and not exact_dirs: continue # A name matching one file identifies it. A name matching several # identifies nothing on its own, so the profile's other files have @@ -629,7 +646,11 @@ def _agnostic_scan_extras( # Scan DB for all files under this prefix matching size for sha1, entry in files_db.items(): path = entry.get("path", "") - if not path.startswith(path_prefix): + if exact_dirs: + if ("/" not in path or path.rsplit("/", 1)[0] not in exact_dirs + or "/.variants/" in path): + continue + elif not path.startswith(path_prefix): continue if not companion_ext and not size_fits(f, entry.get("size", 0)): continue @@ -686,6 +707,31 @@ def _agnostic_scan_extras( return extras +def placeholder_directories( + seed: dict, emu_name: str, profile: dict, db: dict, all_profiles: dict, +) -> frozenset[str]: + """The folders that hold the dumps a placeholder entry accepts. + + Each folder where another profile of the same system resolves a hashed + file of the seed's shape by content: PCSX2 proves bios/Sony/PlayStation 2/. + Only that folder, not the ones below it (its DVD firmware). + """ + systems = set(profile.get("systems", [])) + directories: set[str] = set() + for other_name, other in all_profiles.items(): + if other_name == emu_name or not systems & set(other.get("systems", [])): + continue + for f in other.get("files", []): + if not isinstance(f, dict) or not any(f.get(h) for h in ("sha1", "md5", "sha256", "crc32")): + continue + if f.get("size") and not size_fits(seed, f["size"]): + continue + local, status = resolve_local_file({**f, "source_profile": other_name}, db) + if local and "/" in local and resolution_is_hash_exact(status): + directories.add(local.rsplit("/", 1)[0]) + return frozenset(directories) + + def _placeholder_scan( files: list[dict], emu_name: str, profile: dict, db: dict, standalone: bool, all_profiles: dict | None = None, @@ -699,7 +745,6 @@ def _placeholder_scan( (its DVD firmware). Nothing proven, nothing scanned. """ files_db = db.get("files", {}) - systems = set(profile.get("systems", [])) extras: list[dict] = [] for seed in files: name = str(seed.get("name", "")) @@ -707,18 +752,10 @@ def _placeholder_scan( continue if not (seed.get("size") or seed.get("min_size") or seed.get("max_size")): continue - directories: set[str] = set() - for other_name, other in (all_profiles or load_emulator_profiles("emulators")).items(): - if other_name == emu_name or not systems & set(other.get("systems", [])): - continue - for f in other.get("files", []): - if not any(f.get(h) for h in ("sha1", "md5", "sha256", "crc32")): - continue - if f.get("size") and not size_fits(seed, f["size"]): - continue - local, status = resolve_local_file({**f, "source_profile": other_name}, db) - if local and "/" in local and resolution_is_hash_exact(status): - directories.add(local.rsplit("/", 1)[0]) + directories = placeholder_directories( + seed, emu_name, profile, db, + all_profiles or load_emulator_profiles("emulators"), + ) layout = (seed.get("standalone_path") if standalone else None) or seed.get("path") or "" folder = layout.rsplit("/", 1)[0] if "/" in layout else "" for sha1, entry in sorted(files_db.items(), key=lambda kv: kv[1].get("path", "")): diff --git a/tests/test_agnostic_scope.py b/tests/test_agnostic_scope.py index 3a1d3d27a..1326f8242 100644 --- a/tests/test_agnostic_scope.py +++ b/tests/test_agnostic_scope.py @@ -55,6 +55,66 @@ class AgnosticScan(unittest.TestCase): ["pcsx2/bios/scph1.bin", "pcsx2/bios/scph2.bin"]) + def test_a_placeholder_scans_the_folder_another_profile_proves(self): + """LRPS2 and PCEE2 name their BIOS .bin, which resolves nowhere. + + On RetroArch yaps2 anchored pcsx2/bios/ for them; on Batocera no + other libretro PS2 core runs, and the pack held nothing there. The + folder where another profile of the system resolves a hashed dump of + that shape is the evidence, that folder only. + """ + from packextras import _agnostic_scan_extras + + with tempfile.TemporaryDirectory() as tmp: + previous = os.getcwd() + os.chdir(tmp) + self.addCleanup(os.chdir, previous) + files, by_name = {}, {} + for path, data in ( + ("bios/PS2/scph1.bin", b"a" * 64), + ("bios/PS2/scph2.bin", b"b" * 64), + ("bios/PS2/scph2.bin.rom1", b"r" * 8), + ("bios/PS2/scph1.nvm", b"n" * 8), + ("bios/PS2/.variants/scph3.bin", b"e" * 64), + ("bios/PS2/DVD/dvd.bin", b"f" * 64), + ): + Path(path).parent.mkdir(parents=True, exist_ok=True) + Path(path).write_bytes(data) + sha1 = hashlib.sha1(data).hexdigest() + name = path.rsplit("/", 1)[1] + files[sha1] = {"path": path, "name": name, "size": len(data), "sha1": sha1} + by_name.setdefault(name, []).append(sha1) + db = {"files": files, "indexes": {"by_name": by_name, "by_md5": {}, "by_crc32": {}, + "by_path_suffix": {}}} + placeholders = {"emulator": "L", "type": "libretro", "bios_mode": "agnostic", + "bios_directory": "pcsx2/bios/", "systems": ["ps2"], "files": [ + {"name": ".bin", "found_by": "content", + "path": "pcsx2/bios/.bin", "min_size": 32, "max_size": 64}, + {"name": ".rom1", "path": "pcsx2/bios/.rom1"}, + {"name": ".nvm", "path": "pcsx2/bios/.nvm", "size": 8}, + ]} + pinned = {"emulator": "S", "type": "standalone", "systems": ["ps2"], "files": [ + {"name": "scph1.bin", "size": 64, + "sha1": hashlib.sha1(b"a" * 64).hexdigest()}, + ]} + extras = _agnostic_scan_extras( + {"l": placeholders, "s": pinned}, {"l"}, db, by_name, set(), "") + self.assertEqual(sorted(e["destination"] for e in extras), [ + "pcsx2/bios/scph1.bin", "pcsx2/bios/scph1.nvm", "pcsx2/bios/scph2.bin", + "pcsx2/bios/scph2.bin.rom1", + ]) + self.assertTrue(all(e["source_profile"] == "l" for e in extras)) + # An image the platform already ships there still gets its + # companion: RetroBat lists pcsx2/bios/scph39001.bin itself. + shipped = {"pcsx2/bios/scph1.bin"} + extras = _agnostic_scan_extras( + {"l": placeholders, "s": pinned}, {"l"}, db, by_name, shipped, "") + self.assertIn("pcsx2/bios/scph1.nvm", {e["destination"] for e in extras}) + # Nothing proves the folder: nothing is scanned. + self.assertEqual( + _agnostic_scan_extras({"l": placeholders}, {"l"}, db, by_name, set(), ""), []) + + class BundledHashIsProof(unittest.TestCase): """A bundled file with a declared hash is served by those bytes or flagged. From d457948143cfcf429f65b52d61c09cef12bf5f39 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:16:30 +0100 Subject: [PATCH 2/6] fix: keep standalone copies of a twice-held file A profile standalone copy was dropped when the manifest held its file under two destinations. With the libretro PS2 cores' pcsx2/bios/ scan, DuckStation's ps2_scph50006.bin and ps2_scph50008.bin copies left the Batocera, Recalbox and RetroBat manifests. Same bytes at every path are one source: the copy whose cores name the emulator, else the shortest path. --- scripts/generate_pack.py | 24 ++++++++++++++++++++++-- tests/test_agnostic_scope.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 8c1018563..fcda2e74d 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -4091,8 +4091,20 @@ def _data_root_target(system: str, text: str) -> str | None: return None +def _one_content(held: list[str], entries: dict[str, dict], emulator: str) -> list[str]: + """The one dest a copy reads when every held dest carries the same bytes. + + Unchanged when the contents differ: the name then designates nothing. + """ + if len({entries[dest].get("sha1") for dest in held}) != 1: + return held + own = [dest for dest in held if emulator in (entries[dest].get("cores") or ())] + return [min(own or held, key=lambda dest: (dest.count("/"), len(dest), dest))] + + def _profile_copies( profiles: dict[str, dict], dests: list[str], declared: list[dict], + entries: dict[str, dict] | None = None, ) -> list[dict]: """The standalone copies the emulator profiles document: each file with a standalone_path, into the data_root of each system that names a place. @@ -4105,6 +4117,10 @@ def _profile_copies( file found by content (found_by) sits wherever the folder the user set for the scan points: Amiberry reads its ROMs from rom_path, which amiberry.conf moves, so its name under data_root proves nothing. + ``entries`` maps each dest to its manifest entry: one file carried at + two paths with the same bytes is one source, the copy whose cores name + the emulator, else the shortest path. DuckStation's ps2_scph50006.bin + sits at the BIOS root and under pcsx2/bios/ for the libretro PS2 cores. """ from fnmatch import fnmatch # noqa: PLC0415 @@ -4143,6 +4159,8 @@ def _profile_copies( or entry.get("type") == "directory" or entry.get("found_by") == "content"): continue held = by_tail.get(entry.get("path") or entry["name"], []) + if len(held) > 1 and entries is not None: + held = _one_content(held, entries, profile_name) if len(held) != 1: continue source = held[0] @@ -4171,8 +4189,10 @@ def _standalone_copies( whose registry entry declares none gets none.""" if not declared: return [] - dests = [entry["dest"] for entry in manifest_files if entry.get("root") != "save"] - return _placed_copies(declared, dests) + _profile_copies(profiles, dests, declared) + kept = [entry for entry in manifest_files if entry.get("root") != "save"] + dests = [entry["dest"] for entry in kept] + entries = {entry["dest"]: entry for entry in kept} + return _placed_copies(declared, dests) + _profile_copies(profiles, dests, declared, entries) def _stable_hashes(local_path: str, db: dict) -> dict: diff --git a/tests/test_agnostic_scope.py b/tests/test_agnostic_scope.py index 1326f8242..bb54e0332 100644 --- a/tests/test_agnostic_scope.py +++ b/tests/test_agnostic_scope.py @@ -115,6 +115,36 @@ class AgnosticScan(unittest.TestCase): _agnostic_scan_extras({"l": placeholders}, {"l"}, db, by_name, set(), ""), []) +class OneContentTwoPaths(unittest.TestCase): + """A standalone copy reads one file the manifest carries at two paths. + + The libretro PS2 cores put DuckStation's ps2_scph50006.bin under + pcsx2/bios/ beside the BIOS root copy, and the copy into DuckStation's + folder was dropped as ambiguous on Batocera, Recalbox and RetroBat. + """ + + DUCK = {"data_root": {"linux": {"path": "~/.local/share/duckstation", "source_ref": "x"}}, + "files": [{"name": "ps2_scph50006.bin", "standalone_path": "bios/ps2_scph50006.bin"}]} + + def _copies(self, entries): + from generate_pack import _profile_copies + + return _profile_copies({"duckstation": self.DUCK}, list(entries), [], entries) + + def test_same_bytes_take_the_copy_that_serves_the_emulator(self): + (copy,) = self._copies({ + "pcsx2/bios/ps2_scph50006.bin": {"sha1": "a", "cores": ["lrps2"]}, + "ps2_scph50006.bin": {"sha1": "a", "cores": ["duckstation", "pcsx2"]}, + }) + self.assertEqual(copy["file"], "ps2_scph50006.bin") + + def test_other_bytes_under_one_name_designate_nothing(self): + self.assertEqual(self._copies({ + "pcsx2/bios/ps2_scph50006.bin": {"sha1": "a", "cores": ["lrps2"]}, + "ps2_scph50006.bin": {"sha1": "b", "cores": ["duckstation"]}, + }), []) + + class BundledHashIsProof(unittest.TestCase): """A bundled file with a declared hash is served by those bytes or flagged. From 7cb0e67eb6a02a58f05c0f78cc9c94d4e5b40361 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:38:03 +0100 Subject: [PATCH 3/6] feat: profile the Batocera and Recalbox LRPS2 Batocera 43.1 builds libretro/ps2 at 416291a with 001.bios-dir.patch and Recalbox 10.1.1 builds it at 9485a53 with 0001-recalbox-paths.patch. Both patches move the BIOS scan from pcsx2/bios to ps2, and Recalbox also moves the data root to system/ps2. Each build gets its own built_by profile: lrps2-416291a under Batocera's pcsx2 name, lrps2-9485a53 under Recalbox's ps2 name. A platform's own build now answers for its own build mode only on a dual_mode_cores name, so Batocera keeps standalone PCSX2 beside its libretro build. --- emulators/lrps2-416291a.yml | 152 +++++++++++++++++++++++++++++++++++ emulators/lrps2-9485a53.yml | 151 ++++++++++++++++++++++++++++++++++ scripts/common.py | 22 ++++- tests/test_batocera_cores.py | 45 +++++++++++ wiki/adding-a-platform.md | 5 +- 5 files changed, 371 insertions(+), 4 deletions(-) create mode 100644 emulators/lrps2-416291a.yml create mode 100644 emulators/lrps2-9485a53.yml diff --git a/emulators/lrps2-416291a.yml b/emulators/lrps2-416291a.yml new file mode 100644 index 000000000..5b4a7919f --- /dev/null +++ b/emulators/lrps2-416291a.yml @@ -0,0 +1,152 @@ +# LRPS2 at 416291a with Batocera's BIOS directory patch +# Source: https://github.com/libretro/parallel-ps2 +# Upstream: https://github.com/PCSX2/pcsx2 + +emulator: "LRPS2 (416291a, Batocera)" +type: libretro +core_classification: community_fork +bios_mode: agnostic +source: "https://github.com/libretro/parallel-ps2" +upstream: "https://github.com/PCSX2/pcsx2" +profiled_date: "2026-11-10" +source_commit: "416291ad7dc3caf5df4501c9249cbbe30cbef811" +pin_frozen: "Batocera, the only platform that ships it, builds this revision with package/batocera/emulators/retroarch/libretro/libretro-ps2/001.bios-dir.patch (Batocera package/batocera/emulators/retroarch/libretro/libretro-ps2/libretro-ps2.mk:7 at batocera-43.1). Later commits are not part of that build." +pin_build: + - platform: batocera + repo: "https://github.com/batocera-linux/batocera.linux" + revision: "batocera-43.1" + path: "package/batocera/emulators/retroarch/libretro/libretro-ps2/libretro-ps2.mk" + variable: LIBRETRO_PS2_VERSION + value: "416291ad7dc3caf5df4501c9249cbbe30cbef811" +core_version: null +version_basis: + kind: none +display_name: "Sony - PlayStation 2 (LRPS2 416291a for Batocera)" +cores: [pcsx2] +built_by: [batocera] +systems: [sony-playstation-2] + +bios_directory: "ps2/" +resources_directory: "pcsx2/resources/" + +notes: | + Batocera installs the build as pcsx2_libretro.so (Batocera + package/batocera/emulators/retroarch/libretro/libretro-ps2/libretro-ps2.mk:35-36) + and launches it as the libretro core pcsx2 of the ps2 system (Batocera + package/batocera/emulationstation/batocera-es-system/es_systems.yml:2293-2294). + The library version is the short commit hash git reports at build time + (cmake/BuildParameters.cmake:4-9, libretro/main.cpp:1423-1427). + + Its patch moves the BIOS scan from pcsx2/bios to ps2 below the system + directory (libretro/main.cpp:1806) and the BIOS folder default to + /userdata/bios/ps2, the same folder on Batocera (pcsx2/Pcsx2Config.cpp:1042). + The data root stays system/pcsx2, so resources are read from + pcsx2/resources and memory cards are written to pcsx2/memcards + (libretro/main.cpp:1910-1912). + + BIOS detection is filename-agnostic: ps2/ is scanned for any file between + 4 and 8 MB with a RESET and a ROMVER romdir entry, no hash is checked, and + the list fills the pcsx2_bios core option whose default is the first BIOS + found (libretro/main.cpp:1800-1838). retro_load_game refuses to start when + no BIOS was found (libretro/main.cpp:1927-1931). The companion files take + their path from the selected BIOS. + + The DEV9 hard disk image is user storage the core creates, not a file to + obtain. + +files: + - name: ".bin" + found_by: content + path: "ps2/.bin" + required: true + description: "PS2 BIOS binary" + min_size: 4194304 + max_size: 8388608 + validation: [size] + config_key: "pcsx2_bios" + source_ref: "source libretro/main.cpp:1800-1838, source libretro/main.cpp:360-365, source libretro/main.cpp:1927-1931, source pcsx2/ps2/BiosTools.cpp:31-32, 60-187, 231-323, source pcsx2/Pcsx2Config.cpp:994-1000, 1042, 1051-1061" + note: > + Any file of 4 to 8 MB in ps2/ whose romdir holds RESET and ROMVER. The + pcsx2_bios core option picks one, and the first valid image found is used + when the configured one is missing. + + - name: ".rom1" + path: "ps2/.rom1" + required: false + max_size: 4194304 + description: "DVD player ROM" + source_ref: "source pcsx2/ps2/BiosTools.cpp:189-213, 314, source pcsx2/MemoryTypes.h:24" + note: "Tries .rom1, then the BIOS path with its extension replaced by rom1. Skipped when neither exists." + + - name: ".rom2" + path: "ps2/.rom2" + required: false + max_size: 524288 + description: "Chinese ROM extension" + source_ref: "source pcsx2/ps2/BiosTools.cpp:189-213, 315, source pcsx2/MemoryTypes.h:25" + note: "Same naming as rom1. Present only on Chinese consoles." + + - name: ".nvm" + path: "ps2/.nvm" + required: false + hle_fallback: true + size: 1024 + description: "NVRAM / EEPROM data" + source_ref: "source pcsx2/CDVD/CDVD.cpp:59-60, source pcsx2/CDVD/CDVD.cpp:160-184" + note: > + Read at the BIOS path with the extension replaced by nvm. Region and + language defaults are filled in when the file is missing, short, or has + no language or region parameters. + + - name: ".mec" + path: "ps2/.mec" + required: false + hle_fallback: true + size: 4 + description: "Mechacon version" + source_ref: "source pcsx2/CDVD/CDVD.cpp:62, source pcsx2/CDVD/CDVD.cpp:186-196" + note: "Read at the BIOS path with the extension replaced by mec. Written as 0x00020603 when missing." + + - name: "GameIndex.yaml" + path: "pcsx2/resources/GameIndex.yaml" + required: false + installed_on: [batocera] + description: "Per-game fixes and settings database" + source_ref: "source pcsx2/GameDatabase.cpp:48, source pcsx2/GameDatabase.cpp:880-885, source libretro/main.cpp:2324-2361" + note: > + Read from pcsx2/resources at the first game lookup. Without it no game + fix applies. Batocera installs the copy of the built tree in + bios/pcsx2/resources (Batocera + package/batocera/emulators/retroarch/libretro/libretro-ps2/libretro-ps2.mk:37-39). + + - name: "cheats_ws.zip" + path: "pcsx2/resources/cheats_ws.zip" + required: false + description: "widescreen patches archive" + source_ref: "source pcsx2/VMManager.cpp:333-348, source libretro/main.cpp:2315-2322" + note: "Read when widescreen patches are enabled and pcsx2/cheats_ws holds no patch for the game." + + - name: "cheats_ni.zip" + path: "pcsx2/resources/cheats_ni.zip" + required: false + description: "no-interlacing patches archive" + source_ref: "source pcsx2/VMManager.cpp:366-383, source libretro/main.cpp:2315-2322" + note: "Read when no-interlacing patches are enabled and pcsx2/cheats_ni holds no patch for the game." + + - name: "eeprom.dat" + load_from: working_directory + required: false + hle_fallback: true + size: 64 + description: "DEV9 EEPROM" + source_ref: "source pcsx2/DEV9/DEV9.cpp:107-116, source pcsx2/DEV9/DEV9.cpp:152-167" + note: "Opened from the working directory when DEV9 starts and mapped over 64 bytes. A compiled-in image stands in when the file is missing." + + - name: "flash.dat" + load_from: working_directory + required: false + hle_fallback: true + size: 8650752 + description: "DEV9 flash memory with ECC" + source_ref: "source pcsx2/DEV9/flash.cpp:21-28, source pcsx2/DEV9/flash.cpp:74-101" + note: "Read from the working directory when DEV9 starts. The flash is blank (0xFF) when the file is missing." diff --git a/emulators/lrps2-9485a53.yml b/emulators/lrps2-9485a53.yml new file mode 100644 index 000000000..7ffef6c18 --- /dev/null +++ b/emulators/lrps2-9485a53.yml @@ -0,0 +1,151 @@ +# LRPS2 at 9485a53 with Recalbox's path patch +# Source: https://github.com/libretro/parallel-ps2 +# Upstream: https://github.com/PCSX2/pcsx2 + +emulator: "LRPS2 (9485a53, Recalbox)" +type: libretro +core_classification: community_fork +bios_mode: agnostic +source: "https://github.com/libretro/parallel-ps2" +upstream: "https://github.com/PCSX2/pcsx2" +profiled_date: "2026-11-10" +source_commit: "9485a53fa5aa2bff17e04518116107f81a8c82e3" +pin_frozen: "Recalbox, the only platform that ships it, builds this revision with package/libretro-ps2/0001-recalbox-paths.patch (Recalbox package/libretro-ps2/libretro-ps2.mk:9 at tag 10.1.1). Later commits are not part of that build." +pin_build: + - platform: recalbox + repo: "https://gitlab.com/recalbox/recalbox" + revision: "10.1.1" + path: "package/libretro-ps2/libretro-ps2.mk" + variable: LIBRETRO_PS2_VERSION + value: "9485a53fa5aa2bff17e04518116107f81a8c82e3" +core_version: null +version_basis: + kind: none +display_name: "Sony - PlayStation 2 (LRPS2 9485a53 for Recalbox)" +cores: [ps2] +built_by: [recalbox] +systems: [sony-playstation-2] + +bios_directory: "ps2/" +resources_directory: "ps2/resources/" + +notes: | + Recalbox installs the build as ps2_libretro.so (Recalbox + package/libretro-ps2/libretro-ps2.mk:31-33) and lists it as the libretro + core ps2 of the ps2 system (Recalbox + package/recalbox-romfs2/systems/ps2/system.ini:65-69). + The library version is the short commit hash git reports at build time + (cmake/BuildParameters.cmake:4-9, libretro/main.cpp:1424-1428). + + Its patch moves the BIOS scan from pcsx2/bios to ps2 below the system + directory (libretro/main.cpp:1807), the data root from system/pcsx2 to + system/ps2, so resources are read from ps2/resources + (libretro/main.cpp:1911-1913), the BIOS folder default to + /recalbox/share/bios/ps2, the same folder on Recalbox, and memory cards to + /recalbox/share/saves/ps2 (pcsx2/Pcsx2Config.cpp:1042-1043, 1061-1062). + + BIOS detection is filename-agnostic: ps2/ is scanned for any file between + 4 and 8 MB with a RESET and a ROMVER romdir entry, no hash is checked, and + the list fills the pcsx2_bios core option whose default is the first BIOS + found (libretro/main.cpp:1801-1839). retro_load_game refuses to start when + no BIOS was found (libretro/main.cpp:1928-1932). The companion files take + their path from the selected BIOS. + + The DEV9 hard disk image is user storage the core creates, not a file to + obtain. + +files: + - name: ".bin" + found_by: content + path: "ps2/.bin" + required: true + description: "PS2 BIOS binary" + min_size: 4194304 + max_size: 8388608 + validation: [size] + config_key: "pcsx2_bios" + source_ref: "source libretro/main.cpp:1801-1839, source libretro/main.cpp:360-365, source libretro/main.cpp:1928-1932, source pcsx2/ps2/BiosTools.cpp:31-32, 60-187, 231-323, source pcsx2/Pcsx2Config.cpp:994-1000, 1042, 1051-1061" + note: > + Any file of 4 to 8 MB in ps2/ whose romdir holds RESET and ROMVER. The + pcsx2_bios core option picks one, and the first valid image found is used + when the configured one is missing. + + - name: ".rom1" + path: "ps2/.rom1" + required: false + max_size: 4194304 + description: "DVD player ROM" + source_ref: "source pcsx2/ps2/BiosTools.cpp:189-213, 314, source pcsx2/MemoryTypes.h:24" + note: "Tries .rom1, then the BIOS path with its extension replaced by rom1. Skipped when neither exists." + + - name: ".rom2" + path: "ps2/.rom2" + required: false + max_size: 524288 + description: "Chinese ROM extension" + source_ref: "source pcsx2/ps2/BiosTools.cpp:189-213, 315, source pcsx2/MemoryTypes.h:25" + note: "Same naming as rom1. Present only on Chinese consoles." + + - name: ".nvm" + path: "ps2/.nvm" + required: false + hle_fallback: true + size: 1024 + description: "NVRAM / EEPROM data" + source_ref: "source pcsx2/CDVD/CDVD.cpp:59-60, source pcsx2/CDVD/CDVD.cpp:160-184" + note: > + Read at the BIOS path with the extension replaced by nvm. Region and + language defaults are filled in when the file is missing, short, or has + no language or region parameters. + + - name: ".mec" + path: "ps2/.mec" + required: false + hle_fallback: true + size: 4 + description: "Mechacon version" + source_ref: "source pcsx2/CDVD/CDVD.cpp:62, source pcsx2/CDVD/CDVD.cpp:186-196" + note: "Read at the BIOS path with the extension replaced by mec. Written as 0x00020603 when missing." + + - name: "GameIndex.yaml" + path: "ps2/resources/GameIndex.yaml" + required: false + installed_on: [recalbox] + description: "Per-game fixes and settings database" + source_ref: "source pcsx2/GameDatabase.cpp:48, source pcsx2/GameDatabase.cpp:880-885, source libretro/main.cpp:2265-2302" + note: > + Read from ps2/resources at the first game lookup. Without it no game fix + applies. Recalbox installs the copy of the built tree in + bios/ps2/resources (Recalbox package/libretro-ps2/libretro-ps2.mk:34-37). + + - name: "cheats_ws.zip" + path: "ps2/resources/cheats_ws.zip" + required: false + description: "widescreen patches archive" + source_ref: "source pcsx2/VMManager.cpp:333-348, source libretro/main.cpp:2256-2263" + note: "Read when widescreen patches are enabled and ps2/cheats_ws holds no patch for the game." + + - name: "cheats_ni.zip" + path: "ps2/resources/cheats_ni.zip" + required: false + description: "no-interlacing patches archive" + source_ref: "source pcsx2/VMManager.cpp:366-383, source libretro/main.cpp:2256-2263" + note: "Read when no-interlacing patches are enabled and ps2/cheats_ni holds no patch for the game." + + - name: "eeprom.dat" + load_from: working_directory + required: false + hle_fallback: true + size: 64 + description: "DEV9 EEPROM" + source_ref: "source pcsx2/DEV9/DEV9.cpp:107-116, source pcsx2/DEV9/DEV9.cpp:152-167" + note: "Opened from the working directory when DEV9 starts and mapped over 64 bytes. A compiled-in image stands in when the file is missing." + + - name: "flash.dat" + load_from: working_directory + required: false + hle_fallback: true + size: 8650752 + description: "DEV9 flash memory with ECC" + source_ref: "source pcsx2/DEV9/flash.cpp:21-28, source pcsx2/DEV9/flash.cpp:74-101" + note: "Read from the working directory when DEV9 starts. The flash is blank (0xFF) when the file is missing." diff --git a/scripts/common.py b/scripts/common.py index 09d2900e2..7fc2bdf04 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -3308,6 +3308,11 @@ def _platform_profiles( } index = upstream_profile_index(profiles) platform = str(config.get("_key") or "") + dual_names = {str(c) for c in config.get("dual_mode_cores") or []} + + def build_modes(p: dict) -> set[str]: + kind = str(p.get("type", "")) + return {mode for mode in ("standalone", "libretro") if mode in kind} def designated(name: str) -> set[str]: # A build the platform compiles itself (built_by) is what its @@ -3319,10 +3324,21 @@ def _platform_profiles( p for p in index.get(name, ()) if platform and platform in (profiles[p].get("built_by") or []) } - if own: - return own preferred = preferred_profile(index, name) - return {preferred} if preferred is not None else set() + if not own: + return {preferred} if preferred is not None else set() + # The own builds stand for the modes they are built in only. + # Batocera builds LRPS2 as pcsx2_libretro.so and launches + # standalone PCSX2 under the same name. + own_modes = set().union(*(build_modes(profiles[p]) for p in own)) + if ( + name in dual_names + and preferred is not None + and preferred not in own + and not build_modes(profiles[preferred]) & own_modes + ): + own.add(preferred) + return own result = {profile for c in core_set for profile in designated(c)} # A name the platform launches both ways designates its libretro diff --git a/tests/test_batocera_cores.py b/tests/test_batocera_cores.py index eeeff5203..890cf67b3 100644 --- a/tests/test_batocera_cores.py +++ b/tests/test_batocera_cores.py @@ -160,5 +160,50 @@ class APlatformsOwnBuildWinsItsName(unittest.TestCase): self.assertEqual(resolve_platform_cores(retropie, profiles), {"uae4all"}) +class APatchedLibretroBuildTakesItsNameOnly(unittest.TestCase): + """Batocera and Recalbox compile LRPS2 with a patch that reads the BIOS + from ps2/ instead of pcsx2/bios/. Their own build replaces the buildbot + LRPS2 under the name they ship, and Batocera's standalone PCSX2, under + the same name, stays.""" + + def test_own_libretro_build_keeps_the_standalone_of_a_dual_name(self): + from common import resolve_platform_cores + + profiles = { + "pcsx2": {"type": "standalone", "source": "s", "cores": ["pcsx2"]}, + "lrps2": {"type": "libretro", "source": "l", "cores": ["lrps2", "pcsx2"]}, + "lrps2-own": {"type": "libretro", "source": "l", "cores": ["pcsx2"], + "built_by": ["x"]}, + } + config = {"_key": "x", "cores": ["pcsx2"], "standalone_cores": ["pcsx2"], + "dual_mode_cores": ["pcsx2"]} + self.assertEqual(resolve_platform_cores(config, profiles), {"pcsx2", "lrps2-own"}) + del config["dual_mode_cores"] + self.assertEqual(resolve_platform_cores(config, profiles), {"lrps2-own"}) + + def test_the_registry_resolves_each_platform_build(self): + from common import load_emulator_profiles, load_platform_config, resolve_platform_cores + + profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) + platforms = str(REPO_ROOT / "platforms") + + def ps2_cores(platform: str) -> set[str]: + config = load_platform_config(platform, platforms) + return { + c for c in resolve_platform_cores(config, profiles) + if c.startswith(("lrps2", "pcsx2", "pcee2")) + } + + self.assertEqual(ps2_cores("batocera"), {"lrps2-416291a", "pcsx2", "pcee2"}) + self.assertEqual(ps2_cores("recalbox"), {"lrps2-9485a53", "pcsx2"}) + for platform in ("retroarch", "retrobat", "emudeck", "retrodeck"): + with self.subTest(platform=platform): + cores = ps2_cores(platform) + self.assertIn("lrps2", cores) + self.assertFalse({"lrps2-416291a", "lrps2-9485a53"} & cores) + self.assertEqual(profiles["lrps2-416291a"]["files"][0]["path"], "ps2/.bin") + self.assertEqual(profiles["lrps2-9485a53"]["files"][0]["path"], "ps2/.bin") + + if __name__ == "__main__": unittest.main() diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index 1154990e3..446d020bf 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -195,7 +195,10 @@ resolved. Three strategies exist: - **Explicit list**: `cores: [beetle_psx, dolphin, ...]` - match by profile key name or by any name in a profile's `cores:`. When a profile's `built_by` names this - platform, that profile answers for the name (ROCKNIX's `yabadabadoo`). Used by + platform, that profile answers for the name (ROCKNIX's `yabadabadoo`). On a + `dual_mode_cores` name it answers for its own build mode only: Batocera's + patched LRPS2 (`lrps2-416291a`) takes the libretro `pcsx2` and the standalone + PCSX2 stays. Used by Batocera, Recalbox, RetroBat, RomM, KNULLI, AmberELEC, EmuELEC, MinUI, NextUI, MyMinUI, ROCKNIX, RetroPie, EmuDeck, RetroDECK. A list that names `retroarch` (the last three) also takes every libretro profile, as `all_libretro` does, From 3d53a6744cf8a9e46753ee96facdbfd6e80eb710 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:38:07 +0100 Subject: [PATCH 4/6] feat: list the DEV9 eeprom.dat of lrps2 DEV9init opens eeprom.dat from the working directory and maps 64 bytes over the compiled-in image (pcsx2/DEV9/DEV9.cpp:159-176). The profile did not list it. --- emulators/lrps2.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/emulators/lrps2.yml b/emulators/lrps2.yml index b9675af82..870f6f956 100644 --- a/emulators/lrps2.yml +++ b/emulators/lrps2.yml @@ -140,6 +140,15 @@ files: flash is blank (0xFF) when neither exists. Written back only after the emulated system programs it. + - name: "eeprom.dat" + load_from: working_directory + required: false + hle_fallback: true + size: 64 + description: "DEV9 EEPROM" + source_ref: "source pcsx2/DEV9/DEV9.cpp:111-123, source pcsx2/DEV9/DEV9.cpp:159-176" + note: "Opened from the working directory when DEV9 starts and mapped over 64 bytes. A compiled-in image stands in when the file is missing." + - name: "mg_keystore.bin" path: "pcsx2/bios/mg_keystore.bin" required: false From 8fff4d6909459a8bf909b4d20bf1ed5bc2c64fb1 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:02:27 +0100 Subject: [PATCH 5/6] fix: pack companions beside a shared BIOS image Two filename-agnostic cores reading one BIOS folder each scan the same images. The second scan stopped at an image the first had placed, so a companion only the second core opens never reached the pack: Recalbox's libretro PCSX2 reads .erom beside the images LRPS2 placed in ps2/. --- scripts/packextras.py | 13 ++++++--- tests/test_bios_companions.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/scripts/packextras.py b/scripts/packextras.py index df9d22557..559014fd9 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -697,9 +697,16 @@ def _agnostic_scan_extras( "agnostic_scan": True, } extra["claim_order"] = (4, 0, emu_name) - if not _take(extra, full_dest, seen_dests, taken): - continue - extras.append(extra) + if _take(extra, full_dest, seen_dests, taken): + extras.append(extra) + else: + # The image another core's scan placed is this core's + # too, and so are the companions it reads beside it: + # Recalbox's libretro PCSX2 opens .erom next to + # the images LRPS2 placed, which LRPS2 never reads. + held = taken.get(full_dest) if taken is not None else None + if held is None or held.get("sha1") != sha1: + continue extras.extend(_companion_extras( _companions(path, scan_name, _companion_extensions(profile, fname), files_db), folder, extras_prefix, seen_dests, profile, emu_name, f, taken, diff --git a/tests/test_bios_companions.py b/tests/test_bios_companions.py index de9d9c45b..d0b8ba4c0 100644 --- a/tests/test_bios_companions.py +++ b/tests/test_bios_companions.py @@ -108,6 +108,56 @@ class Companions(unittest.TestCase): self.assertIn("SCPH-70004.rom1", names) self.assertFalse({"SCPH-70004.ROM1", "cheats_ws.zip", "GameIndex.yaml"} & names) + def test_a_shared_image_brings_each_cores_companions(self): + """Recalbox runs LRPS2 and the old libretro PCSX2 on one ps2/ folder. + LRPS2's scan placed SCPH-70004.BIN and its rom1, and the second core's + scan stopped at the taken image, so the .erom only it reads stayed out.""" + import os + import tempfile + + from packextras import _agnostic_scan_extras + + def profile(name: str, ext: str) -> dict: + return {"emulator": name, "bios_mode": "agnostic", "files": [ + {"name": "ps2-0230a-20080220.bin", "found_by": "content", + "path": "ps2/ps2-0230a-20080220.bin", "min_size": 8, "max_size": 16}, + {"name": f"ps2-0230a-20080220.{ext}", "path": f"ps2/ps2-0230a-20080220.{ext}"}, + ]} + + stored = { + "ps2-0230a-20080220.bin": b"I" * 8, + "ps2-0230a-20080220.rom1": b"R", + "ps2-0230a-20080220.erom": b"E", + } + with tempfile.TemporaryDirectory() as tmp: + cwd = os.getcwd() + os.chdir(tmp) + try: + files, by_name = {}, {} + for name, payload in stored.items(): + path = f"bios/PS2/{name}" + os.makedirs("bios/PS2", exist_ok=True) + Path(path).write_bytes(payload) + sha1 = f"{len(files):040d}" + files[sha1] = {"path": path, "name": name, "size": len(payload), + "sha1": sha1} + by_name[name] = [sha1] + db = {"files": files, "indexes": { + "by_name": by_name, "by_md5": {}, "by_crc32": {}, "by_path_suffix": {}, + }} + profiles = {"a": profile("A", "rom1"), "b": profile("B", "erom")} + extras = _agnostic_scan_extras( + profiles, {"a", "b"}, db, by_name, set(), "", taken={}, + ) + finally: + os.chdir(cwd) + placed = [(e["destination"], e["source_profile"]) for e in extras] + self.assertEqual(sorted(placed), [ + ("ps2/ps2-0230a-20080220.bin", "a"), + ("ps2/ps2-0230a-20080220.erom", "b"), + ("ps2/ps2-0230a-20080220.rom1", "a"), + ]) + def test_pcsx2_profiles_name_no_free_standing_companion(self): for name in ("pcsx2", "pcsx2-legacy"): profile = yaml.safe_load((REPO_ROOT / "emulators" / f"{name}.yml").read_text()) From f3f125d9455acf1236e64b46cf50573abaaec463 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:02:27 +0100 Subject: [PATCH 6/6] feat: profile Recalbox's libretro PCSX2 Recalbox 10.1.1 builds the wxWidgets-era libretro PCSX2 for x86_64 at 408e3b2 with 0003-bios-path.patch, which moves the BIOS scan from pcsx2/bios to ps2, and lists it as the libretro core pcsx2 of the ps2 system. Since lrps2-9485a53 took the ps2 name no profile described it. pcsx2-libretro-408e3b2 is built_by recalbox under the name pcsx2, beside standalone PCSX2 and LRPS2. It reads .rom1, rom2, erom, nvm and mec beside the selected image. GameIndex.yaml and the patch archives are compiled in. --- emulators/pcsx2-libretro-408e3b2.yml | 118 +++++++++++++++++++++++++++ tests/test_batocera_cores.py | 14 +++- 2 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 emulators/pcsx2-libretro-408e3b2.yml diff --git a/emulators/pcsx2-libretro-408e3b2.yml b/emulators/pcsx2-libretro-408e3b2.yml new file mode 100644 index 000000000..b90a4468f --- /dev/null +++ b/emulators/pcsx2-libretro-408e3b2.yml @@ -0,0 +1,118 @@ +# libretro PCSX2 at 408e3b2 with Recalbox's BIOS path patch +# Source: https://gitlab.com/recalbox/packages/libretro/libretro-pcsx2 +# Upstream: https://github.com/PCSX2/pcsx2 + +emulator: "PCSX2 libretro (408e3b2, Recalbox)" +type: libretro +core_classification: community_fork +bios_mode: agnostic +source: "https://gitlab.com/recalbox/packages/libretro/libretro-pcsx2" +upstream: "https://github.com/PCSX2/pcsx2" +profiled_date: "2026-11-10" +source_commit: "408e3b253847b25536ad77e4ab0c3aff6b8b896f" +pin_frozen: "Recalbox, the only platform that ships it, builds this revision with package/libretro-pcsx2/0003-bios-path.patch (Recalbox package/libretro-pcsx2/libretro-pcsx2.mk:8 at tag 10.1.1). Later commits are not part of that build." +pin_build: + - platform: recalbox + repo: "https://gitlab.com/recalbox/recalbox" + revision: "10.1.1" + path: "package/libretro-pcsx2/libretro-pcsx2.mk" + variable: LIBRETRO_PCSX2_VERSION + value: "408e3b253847b25536ad77e4ab0c3aff6b8b896f" +core_version: "1.7.0" +version_basis: + kind: file + path: "pcsx2/SysForwardDefs.h" + pattern: 'PCSX2_VersionHi\s*=\s*(\d+);\s*\n.*PCSX2_VersionMid\s*=\s*(\d+);\s*\n.*PCSX2_VersionLo\s*=\s*(\d+);' + format: "{1}.{2}.{3}" +display_name: "Sony - PlayStation 2 (PCSX2 libretro 408e3b2 for Recalbox)" +cores: [pcsx2] +built_by: [recalbox] +systems: [sony-playstation-2] + +bios_directory: "ps2/" + +notes: | + The wxWidgets-era libretro port of PCSX2 1.7.0. Recalbox builds it for + x86_64 only (Recalbox package/libretro-pcsx2/Config.in:3, Recalbox + configs/recalbox-x86_64_defconfig:352), installs it as pcsx2_libretro.so + (Recalbox package/libretro-pcsx2/libretro-pcsx2.mk:30-31) and lists it as + the libretro core pcsx2 of the ps2 system (Recalbox + package/recalbox-romfs2/systems/ps2/system.ini:54-58). + The library version is git describe of the build tree, empty when the tree + has no .git (cmake/Pcsx2Utils.cmake:38-52, 70-72, libretro/main.cpp:447-448). + + Its patch moves the BIOS scan from pcsx2/bios to ps2 below the system + directory (libretro/main.cpp:264), /recalbox/share/bios/ps2 on Recalbox. + + BIOS detection is filename-agnostic: every file in ps2/ whose romdir holds + a RESET and a ROMVER entry is listed, no size or hash is checked, and the + list fills the pcsx2_bios core option whose default is the first BIOS found + (libretro/main.cpp:262-303). Without one, retro_init returns before it + creates the emulator and no game starts (libretro/main.cpp:282-287, + 325-328). The companion files take their path from the selected BIOS. + + GameIndex.yaml and the widescreen, no-interlacing and 60 fps patch archives + are compiled in (pcsx2/CMakeLists.txt:338-367, + pcsx2/gui/AppGameDatabase.cpp:20-24, pcsx2/Patch.cpp:30-32). DEV9 is the + null device (pcsx2/DEV9/DEV9.cpp:59-64), so no hard disk, EEPROM or flash + file is read. Memory cards and pnach patches under system/pcsx2 are user + files. + +files: + - name: ".bin" + found_by: content + path: "ps2/.bin" + required: true + description: "PS2 BIOS binary" + min_size: 4194304 + max_size: 8388608 + config_key: "pcsx2_bios" + source_ref: "source libretro/main.cpp:262-303, source libretro/main.cpp:317-328, source libretro/main.cpp:653-654, source pcsx2/ps2/BiosTools.cpp:74-167, 261-319, 340-356, source pcsx2/MemoryTypes.h:21" + note: > + Any file in ps2/ whose romdir holds RESET and ROMVER, picked by the + pcsx2_bios core option. No size is checked: RESET is looked for in the + first 8 MB (pcsx2/ps2/BiosTools.cpp:79-84) and the first 4 MB are + mapped. + + - name: ".rom1" + path: "ps2/.rom1" + required: false + description: "DVD player ROM" + source_ref: "source pcsx2/ps2/BiosTools.cpp:191-229, 294, source pcsx2/MemoryTypes.h:22" + note: "Tries .rom1, then the BIOS path with its extension replaced by rom1. Skipped when neither exists. The first 256 KB are read." + + - name: ".rom2" + path: "ps2/.rom2" + required: false + description: "Chinese ROM extension" + source_ref: "source pcsx2/ps2/BiosTools.cpp:191-229, 295, source pcsx2/MemoryTypes.h:23" + note: "Same naming as rom1. Present only on Chinese consoles. The first 512 KB are read." + + - name: ".erom" + path: "ps2/.erom" + required: false + description: "DVD player extension ROM" + source_ref: "source pcsx2/ps2/BiosTools.cpp:191-229, 296, source pcsx2/MemoryTypes.h:24" + note: "Same naming as rom1. The first 0x1C0000 bytes are read." + + - name: ".nvm" + path: "ps2/.nvm" + required: false + hle_fallback: true + size: 1024 + description: "NVRAM / EEPROM data" + source_ref: "source pcsx2/CDVD/CDVD.cpp:142-187, source libretro/main.cpp:634-642, source libretro/language_injector.cpp:231-262" + note: > + Read at the BIOS path with the extension replaced by nvm. A file shorter + than 1024 bytes is replaced by a blank one with a dummy i.LINK ID. + Without it the fast boot option is ignored. When it exists and the BIOS + is in the language table, the system language option is written into it. + + - name: ".mec" + path: "ps2/.mec" + required: false + hle_fallback: true + size: 4 + description: "Mechacon version" + source_ref: "source pcsx2/CDVD/CDVD.cpp:96-126" + note: "Read at the BIOS path with the extension replaced by mec. Written as bytes 03 06 02 00 when missing." diff --git a/tests/test_batocera_cores.py b/tests/test_batocera_cores.py index 890cf67b3..271bd276c 100644 --- a/tests/test_batocera_cores.py +++ b/tests/test_batocera_cores.py @@ -195,14 +195,20 @@ class APatchedLibretroBuildTakesItsNameOnly(unittest.TestCase): } self.assertEqual(ps2_cores("batocera"), {"lrps2-416291a", "pcsx2", "pcee2"}) - self.assertEqual(ps2_cores("recalbox"), {"lrps2-9485a53", "pcsx2"}) + # Recalbox ships two libretro PS2 builds on x86_64: LRPS2 as ps2 and + # the wxWidgets-era libretro PCSX2 as pcsx2, beside standalone PCSX2. + self.assertEqual( + ps2_cores("recalbox"), {"lrps2-9485a53", "pcsx2-libretro-408e3b2", "pcsx2"} + ) + own_builds = {"lrps2-416291a", "lrps2-9485a53", "pcsx2-libretro-408e3b2"} for platform in ("retroarch", "retrobat", "emudeck", "retrodeck"): with self.subTest(platform=platform): cores = ps2_cores(platform) self.assertIn("lrps2", cores) - self.assertFalse({"lrps2-416291a", "lrps2-9485a53"} & cores) - self.assertEqual(profiles["lrps2-416291a"]["files"][0]["path"], "ps2/.bin") - self.assertEqual(profiles["lrps2-9485a53"]["files"][0]["path"], "ps2/.bin") + self.assertFalse(own_builds & cores) + self.assertNotIn("pcsx2-libretro-408e3b2", ps2_cores("batocera")) + for name in sorted(own_builds): + self.assertEqual(profiles[name]["files"][0]["path"], "ps2/.bin") if __name__ == "__main__":