From 4e8d3750741abe28df37ce4f81dbcb833af5cce1 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:20:00 +0200 Subject: [PATCH 1/3] fix: serve australian mega cd discs their pal bios BlastEm and Kega Fusion pick the European Mega CD BIOS for every disc whose region code is the PAL one, Australian discs included: BlastEm reads E or bit 8 of the header region (romdb.c:176-215), Fusion the security byte 0x64 at 0x20B. Both now carry oceania, and a repo-wide test holds every European Mega CD BIOS to it. Fusion picks its export Master System BIOS from the CountryOrder setting or a CRC32 list, never from the cart, so bios_U.sms and bios_E.sms carry no region. --- emulators/blastem.yml | 4 ++-- emulators/kega-fusion.yml | 16 +++++++--------- tests/test_region.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/emulators/blastem.yml b/emulators/blastem.yml index 9487baf7..10a0f005 100644 --- a/emulators/blastem.yml +++ b/emulators/blastem.yml @@ -80,10 +80,10 @@ files: - name: bios_CD_E.bin system: sega-megacd - region: [europe] + region: [europe, oceania] required: true mode: libretro - note: "Mega CD BIOS for discs whose region resolves to E (segacd.c:1587-1588). Same load rules as bios_CD_U.bin." + note: "Mega CD BIOS for discs whose region resolves to E (segacd.c:1587-1588). The header code E, or bit 8 of a hex code, is the only PAL value the code reads, so Australian discs take this branch too (romdb.c:176-215). Same load rules as bios_CD_U.bin." source_ref: "source libblastem.c:502, source segacd.c:1573-1615" - name: bios_CD_J.bin diff --git a/emulators/kega-fusion.yml b/emulators/kega-fusion.yml index a2c60d45..b24f4358 100644 --- a/emulators/kega-fusion.yml +++ b/emulators/kega-fusion.yml @@ -71,11 +71,11 @@ files: - name: eu_mcd1_9210.bin aliases: [bios_CD_E.bin] system: sega-megacd - region: [europe] + region: [europe, oceania] required: true description: "Mega CD (Europe) boot ROM" - note: "Opened from SCDEURBIOS for the European region, same 128 KB read and same lock when it is missing, with the EUR message." - source_ref: "Fusion.exe 0x44b68a (default path), 0x478979-0x4789d6 (open, message, fread 0x20000)" + note: "Opened from SCDEURBIOS for the European region, same 128 KB read and same lock when it is missing, with the EUR message. With country autodetect on (the default), the disc's security byte at 0x20B picks the region: 0x7A USA, 0xA1 JAP, 0x64 EUR. EUR is the only branch that sets the PAL version bits (0xC0) and 50 Hz, so Australian discs take it too." + source_ref: "Fusion.exe 0x44b68a (default path), 0x478979-0x4789d6 (open, message, fread 0x20000), 0x44756b-0x44758a and 0x44cfc0-0x44cfe3 (security byte), 0x478963-0x478970 (PAL branch)" - name: 32X_G_BIOS.bin system: sega-32x @@ -110,11 +110,10 @@ files: - name: bios_U.sms system: sega-mastersystem - region: [north-america] required: false description: "Master System (USA) boot ROM" - note: "Read from SMSUSABIOS, up to 512 KB, for the USA region unless SMSBIOSDisable is set. A missing file leaves the cartridge to start directly. No file name is fixed by the code." - source_ref: "Fusion.exe 0x47d541-0x47d5b9" + note: "Read from SMSUSABIOS, up to 512 KB, for the USA region unless SMSBIOSDisable is set. A missing file leaves the cartridge to start directly. No file name is fixed by the code. The header nibble at 0x7FFF tells Japan from export only: an export cart gets USA or EUR from the CountryOrder setting (USA by default), or from a CRC32 list held in the executable." + source_ref: "Fusion.exe 0x47d541-0x47d5b9, 0x45c173-0x45c1f4 (header nibble, CountryOrder), 0x47ce20-0x47ced1 (CRC32 list)" - name: bios_J.sms system: sega-mastersystem @@ -126,11 +125,10 @@ files: - name: bios_E.sms system: sega-mastersystem - region: [europe] required: false description: "Master System (Europe) boot ROM" - note: "Read from SMSEURBIOS for the European region, same rules as the USA image." - source_ref: "Fusion.exe 0x47d60c-0x47d659" + note: "Read from SMSEURBIOS for the European region, same rules as the USA image. EUR comes from the CountryOrder setting or the CRC32 list, never from the header." + source_ref: "Fusion.exe 0x47d60c-0x47d659, 0x45c1a4-0x45c1f4 (CountryOrder), 0x47cea1-0x47ced1 (CRC32 list)" - name: bios.gg system: sega-gamegear diff --git a/tests/test_region.py b/tests/test_region.py index cf0771ff..9ed35589 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -889,6 +889,8 @@ class RegionalRequestsKeepWhatTheCodeLoads(unittest.TestCase): ("ace-dl", ["france"], "private/firmware/Amstrad_s4.cpr"), ("duckstation", ["brazil"], "scph5501.bin"), ("duckstation", ["south-korea"], "scph5500.bin"), + ("blastem", ["australia", "north-america"], "bios_CD_E.bin"), + ("kega-fusion", ["australia", "north-america"], "eu_mcd1_9210.bin"), ) def test_each_request_keeps_its_file(self): @@ -1094,6 +1096,42 @@ class MegaCdAmericasIsOneCode(unittest.TestCase): self.assertEqual(narrow, []) +class MegaCdEuropeServesOceania(unittest.TestCase): + """A Mega CD disc carries one PAL code for Europe and Australia, so every + emulator picking the European BIOS by that code picks it for an + Australian disc too. blastem and kega-fusion tagged it europe alone, and a + --region au,us pack dropped the BIOS those discs boot with.""" + + def test_a_european_mega_cd_bios_includes_oceania(self): + import yaml + + root = Path(__file__).resolve().parents[1] + narrow = [] + for path in sorted((root / "emulators").glob("*.yml")): + profile = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + for entry in profile.get("files") or []: + system = str(entry.get("system", "")) + regions = entry.get("region") or [] + if system in ("sega-megacd", "mega-cd") and "europe" in regions \ + and "oceania" not in regions: + narrow.append(f"{path.stem}: {entry.get('name')}") + self.assertEqual(narrow, []) + + +class FusionSmsExportBiosFollowsTheSetting(unittest.TestCase): + """Kega Fusion tells a Japanese Master System cart from an export one by + the header, then picks USA or EUR from its CountryOrder setting or a CRC32 + list. Neither export BIOS is chosen for a territory the cart names.""" + + def test_the_export_bioses_are_unregioned(self): + from common import load_emulator_profiles + + files = {f["name"]: f for f in load_emulator_profiles("emulators")["kega-fusion"]["files"]} + for name in ("bios_U.sms", "bios_E.sms"): + with self.subTest(name=name): + self.assertNotIn("region", files[name]) + + class NoMarketStopsAtAustralia(unittest.TestCase): """simple64 tagged its PAL PIF [europe, australia]. New Zealand is not under australia, so --region new-zealand kept only the NTSC PIF, though From e318bb45035ae20adde6f8d7bb563e4333dc6739 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:20:03 +0200 Subject: [PATCH 2/3] fix: read pcsx_rearmed us slot as the default CheckCdrom sets PSX_REGION_US before it reads the licence, and only "Inc." or "Euro pe" move it (libpcsxcore/misc.c:422-453). A Latin American disc boots the US slot, so its four BIOS images carry latin-america and --region br,jp keeps them. --- emulators/pcsx_rearmed.yml | 10 +++++----- tests/test_region.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/emulators/pcsx_rearmed.yml b/emulators/pcsx_rearmed.yml index 6852e0d3..5494ba14 100644 --- a/emulators/pcsx_rearmed.yml +++ b/emulators/pcsx_rearmed.yml @@ -36,13 +36,13 @@ files: - name: "scph5501.bin" description: "SCPH-5501 (v3.0 11-18-96 A)" - region: [north-america] + region: [north-america, latin-america] size: 524288 required: false hle_fallback: true validation: [size] source_ref: "source frontend/libretro.c:3864-3867 (listed_bios[1])" - note: "Preferred US BIOS, searched first by exact filename." + note: "Preferred US BIOS, searched first by exact filename. The US slot is the detector's default: a disc takes it for both America licence strings and for any licence that is neither \"Inc.\" (JP) nor \"Euro pe\" (EU), and content without a disc boots it too (libpcsxcore/misc.c:422-453, frontend/libretro.c:2167-2171)." # -- Region: Europe (PSX_REGION_EU = 2) -- @@ -70,7 +70,7 @@ files: - name: "scph101.bin" description: "SCPH-101 (v4.4 03-24-00 A) - PSone US" - region: [north-america] + region: [north-america, latin-america] size: 524288 required: false hle_fallback: true @@ -80,7 +80,7 @@ files: - name: "scph7001.bin" description: "SCPH-7001 (v4.1 12-16-97 A)" - region: [north-america] + region: [north-america, latin-america] size: 524288 required: false hle_fallback: true @@ -89,7 +89,7 @@ files: - name: "scph1001.bin" description: "SCPH-1001 (v2.2 12-04-95 A)" - region: [north-america] + region: [north-america, latin-america] size: 524288 required: false hle_fallback: true diff --git a/tests/test_region.py b/tests/test_region.py index 9ed35589..3c544724 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -889,6 +889,8 @@ class RegionalRequestsKeepWhatTheCodeLoads(unittest.TestCase): ("ace-dl", ["france"], "private/firmware/Amstrad_s4.cpr"), ("duckstation", ["brazil"], "scph5501.bin"), ("duckstation", ["south-korea"], "scph5500.bin"), + ("pcsx_rearmed", ["brazil", "japan"], "scph5501.bin"), + ("pcsx_rearmed", ["brazil", "japan"], "scph1001.bin"), ("blastem", ["australia", "north-america"], "bios_CD_E.bin"), ("kega-fusion", ["australia", "north-america"], "eu_mcd1_9210.bin"), ) @@ -1118,6 +1120,24 @@ class MegaCdEuropeServesOceania(unittest.TestCase): self.assertEqual(narrow, []) +class PcsxRearmedUsSlotIsTheDefault(unittest.TestCase): + """CheckCdrom sets PSX_REGION_US before reading the licence and only + "Inc." or "Euro pe" move it (libpcsxcore/misc.c:422-453): a disc of any + other market boots the US slot.""" + + def test_every_us_slot_bios_includes_latin_america(self): + from common import load_emulator_profiles + + tagged = [ + f for f in load_emulator_profiles("emulators")["pcsx_rearmed"]["files"] + if "north-america" in (f.get("region") or []) + ] + self.assertEqual(len(tagged), 4) + for entry in tagged: + with self.subTest(name=entry["name"]): + self.assertIn("latin-america", entry["region"]) + + class FusionSmsExportBiosFollowsTheSetting(unittest.TestCase): """Kega Fusion tells a Japanese Master System cart from an export one by the header, then picks USA or EUR from its CountryOrder setting or a CRC32 From 7bcd41e0c40dea63fe22be24e7467f940607dd4c Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:20:08 +0200 Subject: [PATCH 3/3] fix: let taiwan and asia-ntsc answer each other kronos tags the ST-V BIOS epr-19854.ic8 taiwan, mednafen_stv tags it asia-ntsc, the SMPC area Mednafen names for Taiwan (ss.cpp:1119, 2363-2364). As siblings under asia they never matched, so a request in one spelling dropped the file tagged in the other, and --region taiwan,us dropped the Saturn sega_101.bin. AREA_TERRITORIES relates each area to the territory it serves, china for asia-pal included, in region.py and its installer copy. --- install.ps1 | 2 +- install.py | 12 +++++++++++- install.sh | 2 +- scripts/region.py | 22 ++++++++++++++++++++-- tests/test_install_selection.py | 7 +++++-- tests/test_region.py | 14 ++++++++++++++ wiki/profiling.md | 5 ++++- 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/install.ps1 b/install.ps1 index 52ac1e68..33b617d8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "22737fbb470a63a8d3630f52ffee1535e57f96a2f1fea969e58368e06ca35f06" +$defaultInstallSha256 = "c1f70fb54ba84b75f434a04cdb41998e342210bb804276dda286a333efd80733" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 3cde59b4..bcad60d7 100644 --- a/install.py +++ b/install.py @@ -137,6 +137,10 @@ REGION_ALIASES: dict[str, str] = { "auto": WORLD_REGION, "gb": "uk", } +AREA_TERRITORIES: dict[str, frozenset] = { + "asia-ntsc": frozenset({"taiwan"}), + "asia-pal": frozenset({"china"}), +} _REGION_PARENT = { member: parent for parent, members in REGION_TREE.items() for member in members } @@ -2116,7 +2120,13 @@ def canonical_region(raw: str) -> str: def _region_comparable(a: str, b: str) -> bool: - return a == b or _REGION_PARENT.get(a) == b or _REGION_PARENT.get(b) == a + return ( + a == b + or _REGION_PARENT.get(a) == b + or _REGION_PARENT.get(b) == a + or a in AREA_TERRITORIES.get(b, ()) + or b in AREA_TERRITORIES.get(a, ()) + ) def _region_rank(file_regions: set[str], requested: list[str]) -> int: diff --git a/install.sh b/install.sh index 360630eb..431418a0 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="22737fbb470a63a8d3630f52ffee1535e57f96a2f1fea969e58368e06ca35f06" +DEFAULT_INSTALL_SHA256="c1f70fb54ba84b75f434a04cdb41998e342210bb804276dda286a333efd80733" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/scripts/region.py b/scripts/region.py index 7c6f9257..a299a8a0 100644 --- a/scripts/region.py +++ b/scripts/region.py @@ -47,6 +47,17 @@ REGION_TREE: dict[str, frozenset[str]] = { "oceania": frozenset({"australia", "new-zealand"}), } +# Hardware areas that fuse territory and signal standard, with the territories +# the code serves through them. Mednafen's Saturn SMPC area 0x2 is "For TAIWAN +# and PHILIPINES" and its area option offers it as "Taiwan", area 0xA as +# "China" (mednafen/ss/ss.cpp:1119, 2363-2364 in the mednafen_stv port). +# kronos tags the ST-V BIOS epr-19854.ic8 taiwan and mednafen_stv tags it +# asia-ntsc: a request in either spelling has to reach the other. +AREA_TERRITORIES: dict[str, frozenset[str]] = { + "asia-ntsc": frozenset({"taiwan"}), + "asia-pal": frozenset({"china"}), +} + _PARENT: dict[str, str] = { member: parent for parent, members in REGION_TREE.items() for member in members } @@ -96,8 +107,15 @@ def _canonical(raw: str) -> str: def comparable(a: str, b: str) -> bool: - """True when two regions are equal or in a parent/child relation.""" - return a == b or _PARENT.get(a) == b or _PARENT.get(b) == a + """True when two regions are equal, parent and child, or an area and a + territory it serves.""" + return ( + a == b + or _PARENT.get(a) == b + or _PARENT.get(b) == a + or a in AREA_TERRITORIES.get(b, ()) + or b in AREA_TERRITORIES.get(a, ()) + ) def normalize_declared(raw: str | list | None) -> set[str]: diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index 66c8d9dc..a379e1a3 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -40,6 +40,7 @@ class OneRegionVocabulary(unittest.TestCase): def test_the_copy_matches(self): self.assertEqual(install.REGION_TREE, dict(region.REGION_TREE)) + self.assertEqual(install.AREA_TERRITORIES, dict(region.AREA_TERRITORIES)) self.assertEqual(install.REGION_ALIASES, region.ALIASES) self.assertEqual(install.WORLD_REGION, region.WORLD) self.assertEqual(install.REGIONS, region.REGIONS) @@ -49,7 +50,8 @@ class TheRegionRuleIsTheBuilders(unittest.TestCase): """Random groups, both implementations, one answer.""" POOL = ["north-america", "europe", "japan", "uk", "canada", "brazil", - "south-korea", "world", "asia", "oceania"] + "south-korea", "world", "asia", "oceania", "taiwan", "asia-ntsc", + "china", "asia-pal"] def _case(self, rng: random.Random): groups: dict[str, list[tuple[str, str]]] = {} @@ -79,7 +81,8 @@ class TheRegionRuleIsTheBuilders(unittest.TestCase): if not entry["regions"]: del entry["regions"], entry["region_groups"] entry.pop("priority", None) - requested = rng.sample(["north-america", "europe", "japan", "uk", "canada"], + requested = rng.sample(["north-america", "europe", "japan", "uk", "canada", + "taiwan", "asia-ntsc", "china", "asia-pal"], rng.randint(1, 3)) return groups, index, list(entries.values()), requested diff --git a/tests/test_region.py b/tests/test_region.py index 3c544724..a2e853bc 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -58,6 +58,17 @@ class TestComparable(unittest.TestCase): def test_saturn_fused_slug_is_under_asia(self): self.assertTrue(region.comparable("asia", "asia-ntsc")) + def test_an_area_matches_the_territory_it_serves(self): + """kronos tags the ST-V BIOS epr-19854.ic8 taiwan, mednafen_stv tags + it asia-ntsc, the SMPC area Mednafen names for Taiwan.""" + self.assertTrue(region.comparable("taiwan", "asia-ntsc")) + self.assertTrue(region.comparable("asia-ntsc", "taiwan")) + self.assertTrue(region.comparable("china", "asia-pal")) + + def test_an_area_does_not_match_other_territories(self): + self.assertFalse(region.comparable("japan", "asia-ntsc")) + self.assertFalse(region.comparable("taiwan", "asia-pal")) + class TestNormalizeDeclared(unittest.TestCase): def test_none_is_empty(self): @@ -889,6 +900,9 @@ class RegionalRequestsKeepWhatTheCodeLoads(unittest.TestCase): ("ace-dl", ["france"], "private/firmware/Amstrad_s4.cpr"), ("duckstation", ["brazil"], "scph5501.bin"), ("duckstation", ["south-korea"], "scph5500.bin"), + ("mednafen_stv", ["taiwan", "north-america"], "epr-19854.ic8"), + ("kronos", ["asia-ntsc", "north-america"], "epr-19854.ic8"), + ("beetle_saturn", ["taiwan", "north-america"], "sega_101.bin"), ("pcsx_rearmed", ["brazil", "japan"], "scph5501.bin"), ("pcsx_rearmed", ["brazil", "japan"], "scph1001.bin"), ("blastem", ["australia", "north-america"], "bios_CD_E.bin"), diff --git a/wiki/profiling.md b/wiki/profiling.md index d0d17163..d920f642 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -186,7 +186,10 @@ What does not, and why each trap is easy to fall into: `env.Loader.ReqSpec == "PAL"`, a video specification, and the Atari 2600 has no region lock. There is no signal field: drop `region:` rather than map it. The exception is hardware that fuses both axes in one register, such as the - Saturn SMPC area codes behind `asia-ntsc` and `asia-pal`. + Saturn SMPC area codes behind `asia-ntsc` and `asia-pal`. Mednafen names + them for Taiwan and China, so a `--region taiwan` request reaches an + `asia-ntsc` file and a `--region china` request an `asia-pal` one, and the + other way round (`AREA_TERRITORIES` in `scripts/region.py`). - **A single-region system.** When every file of a system shares one region, the pack filter keeps them all anyway, so annotating adds nothing. - **A language without a territory.** Arabic MSX variants, CJK fonts. The