From 45d41c687a2132bed8a016371c3ded7e30959b19 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 19:00:07 +0900 Subject: [PATCH 1/6] Make EU1.1 bootstrap coverage optional --- config/mph_rom_profiles.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 169bc4e..fdafb9e 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -145,7 +145,7 @@ "rom_size": 67108864, "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", "program_id": "mph_amhp1", - "coverage": "coverage/eu11-bootstrap-entry-points.json", + "coverage": "", "game_config": "game.toml", "fmv_runtime": false, "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", From ecf2fe89e3bddba5e53d9e8106dfbf8bf98093b9 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 19:00:40 +0900 Subject: [PATCH 2/6] Allow exact profiles without static coverage seeds --- CMakeLists.txt | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dc2345..c0c6e13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,11 +77,24 @@ else() endif() set(MPH_INPUT_DIR "${MPH_GENERATED_DIR}/inputs") set(MPH_RECOMP_DIR "${MPH_GENERATED_DIR}/recomp") -set(MPH_COVERAGE_SEEDS - "${CMAKE_CURRENT_SOURCE_DIR}/${MPH_PROFILE_COVERAGE}") -if(NOT EXISTS "${MPH_COVERAGE_SEEDS}") - message(FATAL_ERROR - "Coverage seed file for ${MPH_VERSION} not found: ${MPH_COVERAGE_SEEDS}") + +set(MPH_COVERAGE_SEEDS "") +set(MPH_PREP_COVERAGE_ARGS) +set(MPH_PREP_COVERAGE_DEPENDS) +if(NOT MPH_PROFILE_COVERAGE STREQUAL "") + set(MPH_COVERAGE_SEEDS + "${CMAKE_CURRENT_SOURCE_DIR}/${MPH_PROFILE_COVERAGE}") + if(NOT EXISTS "${MPH_COVERAGE_SEEDS}") + message(FATAL_ERROR + "Coverage seed file for ${MPH_VERSION} not found: ${MPH_COVERAGE_SEEDS}") + endif() + list(APPEND MPH_PREP_COVERAGE_ARGS + --coverage "${MPH_COVERAGE_SEEDS}") + list(APPEND MPH_PREP_COVERAGE_DEPENDS "${MPH_COVERAGE_SEEDS}") +else() + message(STATUS + "MPH profile ${MPH_VERSION}: no static coverage seed; " + "using ROM-header ARM9/ARM7 bootstrap roots only") endif() message(STATUS @@ -110,11 +123,11 @@ add_custom_command( COMMAND "${MPH_PYTHON}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/prepare_mph.py" --rom "${MPH_ROM}" --out "${MPH_INPUT_DIR}" - --coverage "${MPH_COVERAGE_SEEDS}" + ${MPH_PREP_COVERAGE_ARGS} --version "${MPH_VERSION}" --profiles "${MPH_PROFILE_FILE}" DEPENDS "${MPH_ROM}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/prepare_mph.py" - "${MPH_PROFILE_FILE}" "${MPH_COVERAGE_SEEDS}" + "${MPH_PROFILE_FILE}" ${MPH_PREP_COVERAGE_DEPENDS} COMMENT "Verifying ${MPH_VERSION} and preparing main/overlay code images") add_custom_target(mph_prepare DEPENDS ${MPH_PREP_OUTPUTS}) From dc8e1a738a5e9bc68e8ba828bec153a99650e812 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 19:01:15 +0900 Subject: [PATCH 3/6] Support bootstrap-only exact profile preparation --- tools/prepare_mph.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/tools/prepare_mph.py b/tools/prepare_mph.py index e16f1da..49ab5e5 100644 --- a/tools/prepare_mph.py +++ b/tools/prepare_mph.py @@ -186,7 +186,14 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--rom", type=Path, required=True) parser.add_argument("--out", type=Path, required=True) - parser.add_argument("--coverage", type=Path, required=True) + parser.add_argument( + "--coverage", + type=Path, + help=( + "optional static coverage seed manifest; omit to bootstrap from " + "the ROM-header ARM9/ARM7 entry points only" + ), + ) parser.add_argument( "--version", default=DEFAULT_VERSION, @@ -232,16 +239,20 @@ def main() -> int: ) rom = ndspy.rom.NintendoDSRom(rom_bytes) - coverage = json.loads(args.coverage.read_text(encoding="utf-8")) - if coverage.get("game_sha1") != expected_sha1: - raise SystemExit( - f"static coverage seed identity does not match {args.version}" - ) - coverage_entries = coverage.get("entry_points", {}) - if not isinstance(coverage_entries, dict): - raise SystemExit("coverage entry_points must be an object") - arm9_coverage = list(coverage_entries.get("arm9", [])) - arm7_coverage = list(coverage_entries.get("arm7", [])) + arm9_coverage: list[dict[str, object]] = [] + arm7_coverage: list[dict[str, object]] = [] + if args.coverage is not None: + coverage = json.loads(args.coverage.read_text(encoding="utf-8")) + if coverage.get("game_sha1") != expected_sha1: + raise SystemExit( + f"static coverage seed identity does not match {args.version}" + ) + coverage_entries = coverage.get("entry_points", {}) + if not isinstance(coverage_entries, dict): + raise SystemExit("coverage entry_points must be an object") + arm9_coverage = list(coverage_entries.get("arm9", [])) + arm7_coverage = list(coverage_entries.get("arm7", [])) + arm9_compressed = bytes(rom.arm9) arm9 = code_compression.decompress(arm9_compressed) if len(arm9) < len(arm9_compressed): @@ -326,6 +337,13 @@ def main() -> int: f"{expected_game_code.decode('ascii')} revision={expected_revision} " f"SHA-1 {digest}" ) + if args.coverage is None: + print("Static coverage: none; using ROM-header ARM9/ARM7 bootstrap roots only") + else: + print( + f"Static coverage: {len(arm9_coverage)} ARM9 + " + f"{len(arm7_coverage)} ARM7 seed entries from {args.coverage}" + ) print( f"ARM9: {len(arm9_compressed):,} compressed bytes -> " f"{len(arm9):,} bytes at 0x{int(rom.arm9RamAddress):08X}" @@ -340,4 +358,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From 1883e57ee4f970cdc9ec1365bb03f4f4fbc3ec6a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 19:01:46 +0900 Subject: [PATCH 4/6] Validate optional exact-content coverage seeds --- tools/check_mph_multirom_profiles.py | 56 +++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index efaada2..589860b 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Validate the current multi-ROM schema, then run the legacy deep checks. -The legacy checker predates the all-version Adaptive Widescreen address table -and the shared runtime-generic frontend config. Preserve its broad coverage on -a temporary compatibility view while validating the current scale fields and -generic config contract against the real registry. +The legacy checker predates the all-version Adaptive Widescreen address table, +the shared runtime-generic frontend config, and optional bootstrap-only coverage. +Preserve its broad coverage on a temporary compatibility view while validating +the current scale fields and generic config contract against the real registry. """ from __future__ import annotations @@ -105,6 +105,25 @@ def validate_scale_registry(repo: Path, table: Path | None) -> None: if profile.get("game_config") != "game.toml": die(f"{key}: content profiles must share the generic game.toml frontend config") + coverage = profile.get("coverage") + if not isinstance(coverage, str): + die(f"{key}.coverage must be a string; use an empty string for bootstrap-only") + if coverage: + coverage_path = repo / coverage + if not coverage_path.is_file(): + die(f"{key}.coverage does not exist: {coverage_path}") + coverage_doc = json.loads(coverage_path.read_text(encoding="utf-8")) + if coverage_doc.get("game_sha1") != profile.get("sha1"): + die(f"{key}.coverage game_sha1 does not match profile SHA-1") + + # EU1.1 never had promoted coverage: the removed file contained zero ARM9 + # and zero ARM7 entries and existed only to satisfy the old required-file + # shape. Keep the source tree honest and represent that state as no seed. + if profiles.get("EU1_1", {}).get("coverage") != "": + die("EU1_1 should remain bootstrap-only until real coverage is promoted") + if (repo / "coverage" / "eu11-bootstrap-entry-points.json").exists(): + die("obsolete empty EU1.1 bootstrap coverage placeholder still exists") + if table: text = table.read_text(encoding="utf-8") rows = { @@ -126,7 +145,7 @@ def validate_scale_registry(repo: Path, table: Path | None) -> None: def legacy_compat_view(repo: Path, destination: Path) -> Path: - """Synthesize the old per-profile config shape only for the legacy checker.""" + """Synthesize old required-file/profile shapes only for the legacy checker.""" target = destination / "repo" shutil.copytree( repo, target, @@ -139,11 +158,30 @@ def legacy_compat_view(repo: Path, destination: Path) -> Path: for field in SCALE_FIELDS: runtime.pop(field, None) - # The old checker expects exact identity duplicated into each game config - # and also expects the historical EU1.1 widescreen-disable state. Generate - # those files only inside the temporary compatibility copy so production - # source keeps one generic game.toml. + # The old checker requires a concrete coverage file for every profile. + # Generate an empty identity-only manifest in the temporary compatibility + # copy when the production profile intentionally has no promoted coverage. for key, profile in registry["profiles"].items(): + if not profile.get("coverage"): + legacy_coverage_rel = f"coverage/.legacy-{key}-bootstrap.json" + legacy_coverage_path = target / legacy_coverage_rel + legacy_coverage_path.parent.mkdir(parents=True, exist_ok=True) + legacy_coverage_path.write_text( + json.dumps( + { + "schema": 1, + "game_sha1": profile["sha1"], + "scenario": None, + "selection": "legacy checker bootstrap compatibility view", + "entry_points": {"arm9": [], "arm7": []}, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + profile["coverage"] = legacy_coverage_rel + legacy_rel = f"config/.legacy-{key}.toml" profile["game_config"] = legacy_rel adaptive = profile.get("adaptive_widescreen") is True From 9329764409cdcbfea93174a7c20b6248b84aa4a8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 19:01:55 +0900 Subject: [PATCH 5/6] Remove empty EU1.1 bootstrap coverage placeholder --- coverage/eu11-bootstrap-entry-points.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 coverage/eu11-bootstrap-entry-points.json diff --git a/coverage/eu11-bootstrap-entry-points.json b/coverage/eu11-bootstrap-entry-points.json deleted file mode 100644 index efb59c2..0000000 --- a/coverage/eu11-bootstrap-entry-points.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": 1, - "game_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", - "scenario": null, - "selection": "EU1.1 bootstrap only: ROM-header ARM9/ARM7 roots; no USA coverage reused", - "entry_points": { - "arm9": [], - "arm7": [] - } -} From e60bac604c0b846fd8834ceb52230e3b129d3bd9 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 19:05:47 +0900 Subject: [PATCH 6/6] Document bootstrap-only EU1.1 coverage state --- docs/EU1_1_BRINGUP.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index fc36f3c..b613223 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -262,6 +262,8 @@ Current clean EU1.1 profile: - whole-ROM SHA-1: `bdcd1dea293e24c98d4c481430e90d21198985a5` - default launcher ROM: `Metroid Prime Hunters (Europe Rev 1).nds` - shared frontend config: `game.toml` +- static coverage: none currently promoted; exact AOT preparation bootstraps + from the ROM-header ARM9/ARM7 entry points only - Adaptive Widescreen: exposed and revision-aware - FMV runtime bank: disabled until an EU1.1-specific capture is validated @@ -319,23 +321,29 @@ during the build. The preparation path intentionally remains exact-content gated. This protects generated code provenance; it is not the runtime selector. -Example EU1.1 preparation: +EU1.1 currently has no promoted static coverage, so preparation intentionally +uses only the ROM-header ARM9/ARM7 entry points: ```bash python tools/prepare_mph.py \ --version EU1_1 \ --rom "/path/to/Metroid Prime Hunters (Europe Rev 1).nds" \ - --coverage coverage/eu11-bootstrap-entry-points.json \ --out generated/EU1_1/inputs ``` -`prepare_mph.py` checks: +`prepare_mph.py` always checks: - exact whole-ROM SHA-1 - expected ROM size - game code at `0x0C` - revision at `0x1E` -- coverage `game_sha1` + +When `--coverage ` is supplied, it additionally verifies the +coverage `game_sha1` and adds those ARM9/ARM7 entry points. Omitting +`--coverage` is a deliberate bootstrap-only mode and still emits the ROM-header +entry roots. The old `coverage/eu11-bootstrap-entry-points.json` placeholder was +removed because it contained zero ARM9 and zero ARM7 coverage entries and was +therefore behaviorally identical to omitting coverage. It then extracts ARM9, ARM7 and overlays and emits content-specific seed configs. @@ -388,8 +396,8 @@ produce the canonical melonPrimeDS executable checksums. It verifies: - upstream launcher/tests and the imported overlay QA tools remain pinned to the audited upstream title commit -`MPH Multi-ROM Static Checks` #123 passed after the upstream `905ffab` / ndsrecomp -`302404ad...` integration. +The multi-ROM static workflow is required to pass after every profile/schema +change, including bootstrap-only profiles with no promoted coverage file. ## 13. Remaining optimization work