From 090d86a5546139eec513676400dc4e996859cd81 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 01:27:45 -0700 Subject: [PATCH 001/164] launcher: expose HD Rendering as an opt-in mod Internal resolution and texture upscaling were reachable only through game.toml, a CLI flag, or an environment variable. This puts them on the Mods page next to Adaptive Widescreen and Prime Controls, where a player will actually find them. One feature, "HD Rendering", with two choice options: - Internal resolution: 1x (native), 2x, 3x, 4x. Sample density of the 3D engine only; the 2D layers stay native, exactly as the hardware draws them. - Texture upscaling: Off, 2x, 4x. Filters each decoded DS texture once as it enters the cache, so higher internal resolution shows detail rather than larger texels. Disabled by default, and the launcher passes --internal-resolution 1 --texture-upscale 1 while it is off, so anyone who never opens the Mods page gets the faithful native output. Defaults are 2x and 2x once enabled rather than the 4x maximum, since the cost scales roughly with the square of both. Attribution for the upscaler (Hyllian xBR-lv2, MIT) is carried on the feature's source fields so it is visible in the UI. Settings round-trip through mods.ini as hd_rendering, internal_resolution, and texture_upscale, all validated on load so a hand-edited file cannot hand the runner a scale it will refuse to start on. Test: feature count 2 -> 3, plus coverage that HD is off by default, that both options reject values outside their choice lists, and that all three settings survive a save/load round trip. Index 1 is still Prime Controls, so the existing assertions are unaffected. Requires the ndsrecomp runner changes on branch hd-internal-resolution. Co-Authored-By: Claude Opus 5 (1M context) --- launcher/recomp-ui/launcher_main.cpp | 161 +++++++++++++++++- .../tests/launcher_mod_provider_test.cpp | 64 ++++++- 2 files changed, 218 insertions(+), 7 deletions(-) diff --git a/launcher/recomp-ui/launcher_main.cpp b/launcher/recomp-ui/launcher_main.cpp index d4fadc3..8b948d2 100644 --- a/launcher/recomp-ui/launcher_main.cpp +++ b/launcher/recomp-ui/launcher_main.cpp @@ -17,6 +17,13 @@ namespace { struct ModState { bool adaptive_widescreen = true; + // HD rendering. Off by default: it costs GPU time and VRAM, and the + // faithful native output stays the reference. internal_resolution + // multiplies 3D sample density; texture_upscale filters each decoded DS + // texture once on a cache miss. Both are inert when hd_rendering is off. + bool hd_rendering = false; + int internal_resolution = 2; + int texture_upscale = 2; bool mouse_aim = true; int mouse_sensitivity = 30; bool mouse_invert_y = false; @@ -94,6 +101,24 @@ struct ModState { std::string last_error; }; +struct HdChoice { + int value; + const char* label; +}; + +constexpr std::array kInternalResolutionChoices{{ + {1, "1x (native)"}, + {2, "2x"}, + {3, "3x"}, + {4, "4x"}, +}}; + +constexpr std::array kTextureUpscaleChoices{{ + {1, "Off"}, + {2, "2x"}, + {4, "4x"}, +}}; + struct SensitivityChoice { int percent; const char* label; @@ -328,6 +353,19 @@ void load_mod_state(ModState& state) { settings_version = static_cast(parsed); } else if (key == "adaptive_widescreen") { state.adaptive_widescreen = value != "false"; + } else if (key == "hd_rendering") { + state.hd_rendering = value == "true"; + } else if (key == "internal_resolution") { + char* end = nullptr; + const long parsed = std::strtol(value.c_str(), &end, 10); + if (end && *end == 0 && parsed >= 1 && parsed <= 4) + state.internal_resolution = static_cast(parsed); + } else if (key == "texture_upscale") { + char* end = nullptr; + const long parsed = std::strtol(value.c_str(), &end, 10); + if (end && *end == 0 && + (parsed == 1 || parsed == 2 || parsed == 4)) + state.texture_upscale = static_cast(parsed); } else if (key == "mouse_aim") { state.mouse_aim = value != "false"; } else if (key == "mouse_sensitivity") { @@ -410,6 +448,10 @@ bool save_mod_state(ModState& state) { file << "settings_version=2\n" << "adaptive_widescreen=" << (state.adaptive_widescreen ? "true" : "false") << '\n' + << "hd_rendering=" + << (state.hd_rendering ? "true" : "false") << '\n' + << "internal_resolution=" << state.internal_resolution << '\n' + << "texture_upscale=" << state.texture_upscale << '\n' << "mouse_aim=" << (state.prime_controls ? "true" : "false") << '\n' << "mouse_sensitivity=" << state.mouse_sensitivity << '\n' @@ -446,12 +488,12 @@ bool save_mod_state(ModState& state) { // directly under the controller card. Only the two real gameplay mods // remain here. int mod_feature_count(void*) { - return 2; + return 3; } int mod_feature_get(void* context, int index, RecompLauncherCModFeature* output) { - if (!context || !output || index < 0 || index > 1) return 0; + if (!context || !output || index < 0 || index > 2) return 0; const auto* state = static_cast(context); std::memset(output, 0, sizeof(*output)); if (index == 0) { @@ -469,6 +511,27 @@ int mod_feature_get(void* context, int index, copy_text(output->status, state->adaptive_widescreen ? "Enabled" : "Disabled"); output->enabled = state->adaptive_widescreen ? 1 : 0; + } else if (index == 2) { + copy_text(output->id, "hd-rendering"); + copy_text(output->package_id, "mph-hd-rendering"); + copy_text(output->package_version, "0.1.0"); + copy_text(output->package_name, "MPH HD Rendering"); + copy_text(output->name, "HD Rendering"); + copy_text(output->author, "ndsrecomp"); + copy_text( + output->description, + "Renders the 3D engine above one sample per DS pixel and " + "filters decoded textures, so the widescreen image gains detail " + "instead of just area. The 2D layers stay native, exactly as the " + "hardware draws them."); + copy_text(output->source_name, "Hyllian xBR-lv2 (MIT)"); + copy_text(output->source_url, + "https://github.com/libretro/glsl-shaders"); + copy_text(output->group, "Display enhancements"); + copy_text(output->status, + state->hd_rendering ? "Enabled" : "Disabled"); + output->enabled = state->hd_rendering ? 1 : 0; + output->option_count = 2; } else { copy_text(output->id, "prime-controls"); copy_text(output->package_id, "mph-prime-controls"); @@ -512,6 +575,11 @@ int mod_feature_enable(void* context, const char* package_id, state->mouse_aim = state->prime_controls; return 1; } + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + state->hd_rendering = enabled != 0; + return 1; + } return 0; } @@ -528,6 +596,41 @@ int mod_feature_option_get(void* context, const char* package_id, RecompLauncherCModOption* output) { if (!context || !package_id || !feature_id || !output || index < 0) return 0; + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + if (index > 1) return 0; + const auto* hd = static_cast(context); + std::memset(output, 0, sizeof(*output)); + if (index == 0) { + copy_text(output->id, "internal-resolution"); + copy_text(output->label, "Internal resolution"); + copy_text(output->description, + "Sample density of the 3D engine. Costs GPU time and " + "VRAM; 2D layers are unaffected."); + copy_text(output->group, "Resolution"); + std::snprintf(output->value, sizeof(output->value), "%d", + hd->internal_resolution); + copy_text(output->default_value, "2"); + output->type = RECOMP_MOD_OPTION_CHOICE; + output->choice_count = + static_cast(kInternalResolutionChoices.size()); + return 1; + } + copy_text(output->id, "texture-upscale"); + copy_text(output->label, "Texture upscaling"); + copy_text(output->description, + "Filters each decoded DS texture once when it enters the " + "cache, so higher internal resolution shows detail rather " + "than larger texels."); + copy_text(output->group, "Textures"); + std::snprintf(output->value, sizeof(output->value), "%d", + hd->texture_upscale); + copy_text(output->default_value, "2"); + output->type = RECOMP_MOD_OPTION_CHOICE; + output->choice_count = + static_cast(kTextureUpscaleChoices.size()); + return 1; + } if (std::strcmp(package_id, "mph-prime-controls") != 0 || std::strcmp(feature_id, "prime-controls") != 0 || index >= 4 + static_cast(kBindingOptions.size()) + @@ -615,6 +718,30 @@ int mod_feature_choice_get(void*, const char* package_id, int index, RecompLauncherCModChoice* output) { if (!package_id || !feature_id || !option_id || !output || index < 0) return 0; + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + if (std::strcmp(option_id, "internal-resolution") == 0) { + if (index >= static_cast(kInternalResolutionChoices.size())) + return 0; + std::memset(output, 0, sizeof(*output)); + const HdChoice& choice = kInternalResolutionChoices[index]; + std::snprintf(output->value, sizeof(output->value), "%d", + choice.value); + copy_text(output->label, choice.label); + return 1; + } + if (std::strcmp(option_id, "texture-upscale") == 0) { + if (index >= static_cast(kTextureUpscaleChoices.size())) + return 0; + std::memset(output, 0, sizeof(*output)); + const HdChoice& choice = kTextureUpscaleChoices[index]; + std::snprintf(output->value, sizeof(output->value), "%d", + choice.value); + copy_text(output->label, choice.label); + return 1; + } + return 0; + } if (std::strcmp(package_id, "mph-prime-controls") != 0 || std::strcmp(feature_id, "prime-controls") != 0) { return 0; @@ -663,6 +790,30 @@ int mod_feature_set_option(void* context, const char* package_id, const char* value) { if (!context || !package_id || !feature_id || !option_id || !value) return 0; + auto* hd_state = static_cast(context); + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + char* hd_end = nullptr; + const long hd_parsed = std::strtol(value, &hd_end, 10); + if (!hd_end || *hd_end != 0) return 0; + if (std::strcmp(option_id, "internal-resolution") == 0) { + for (const HdChoice& choice : kInternalResolutionChoices) { + if (choice.value != hd_parsed) continue; + hd_state->internal_resolution = static_cast(hd_parsed); + return 1; + } + return 0; + } + if (std::strcmp(option_id, "texture-upscale") == 0) { + for (const HdChoice& choice : kTextureUpscaleChoices) { + if (choice.value != hd_parsed) continue; + hd_state->texture_upscale = static_cast(hd_parsed); + return 1; + } + return 0; + } + return 0; + } if (std::strcmp(package_id, "mph-prime-controls") != 0 || std::strcmp(feature_id, "prime-controls") != 0) { return 0; @@ -919,6 +1070,12 @@ bool launch_runner(const std::filesystem::path& game_dir, const char* rom, : L"stacked") + L" --adaptive-widescreen " + (adaptive ? L"top" : L"none") + + // Inert unless the HD mod is on, so the faithful native output stays + // the default for anyone who never opens the Mods page. + L" --internal-resolution " + + std::to_wstring(mods.hd_rendering ? mods.internal_resolution : 1) + + L" --texture-upscale " + + std::to_wstring(mods.hd_rendering ? mods.texture_upscale : 1) + L" --supersampling " + std::to_wstring(supersampling) + L" --antialiasing " + std::to_wstring(antialiasing) + L" --relative-mouse-touch " + diff --git a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp index 638b4f5..3dd2f13 100644 --- a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp +++ b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp @@ -41,9 +41,11 @@ int main() { if (!require(provider.feature_count != nullptr, "feature_count callback")) return 3; - // Two gameplay mods; the online identity is a dashboard card, not a mod. + // Three gameplay mods; the online identity is a dashboard card, not a + // mod. Index 1 stays prime controls so the assertions below are + // unaffected by HD rendering being added at index 2. const int feature_count = provider.feature_count(provider.ctx); - if (!require(feature_count == 2, "feature_count == 2")) return 4; + if (!require(feature_count == 3, "feature_count == 3")) return 4; RecompLauncherCModFeature feature{}; if (!require(provider.feature_get(provider.ctx, 1, &feature), @@ -261,9 +263,9 @@ int main() { // The online identity is NOT a mod feature: it lives on the dashboard // ONLINE card (GameInfo.has_player_name + the NDS "identity" panel). - // Exactly the two gameplay mods remain. - if (!require(!provider.feature_get(provider.ctx, 2, &feature), - "exactly two mod features (identity is not a mod)")) { + // Exactly the three gameplay mods remain; index 3 must not resolve. + if (!require(!provider.feature_get(provider.ctx, 3, &feature), + "exactly three mod features (identity is not a mod)")) { return 37; } @@ -312,5 +314,57 @@ int main() { std::filesystem::remove(saved.settings_path); } + // HD rendering: off by default, so a player who never opens the Mods + // page gets the faithful native output. + { + RecompLauncherCModFeature hd{}; + if (!require(provider.feature_get(provider.ctx, 2, &hd), + "feature_get hd rendering")) return 60; + if (!require(std::strcmp(hd.id, "hd-rendering") == 0, + "hd feature id")) return 61; + if (!require(std::strcmp(hd.package_id, "mph-hd-rendering") == 0, + "hd package id")) return 62; + if (!require(hd.enabled == 0, "hd disabled by default")) return 63; + if (!require(hd.option_count == 2, "hd option count")) return 64; + + // Both options must reject values outside their choice list, so a + // hand-edited mods.ini cannot hand the runner a scale it refuses. + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "internal-resolution", "4") == 1, + "hd internal resolution accepts 4")) return 65; + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "internal-resolution", "8") == 0, + "hd internal resolution rejects 8")) return 66; + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "texture-upscale", "3") == 0, + "hd texture upscale rejects 3")) return 67; + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "texture-upscale", "4") == 1, + "hd texture upscale accepts 4")) return 68; + + ModState hd_saved{}; + hd_saved.settings_path = + std::filesystem::temp_directory_path() / + "mph_mod_provider_hd_settings.ini"; + hd_saved.hd_rendering = true; + hd_saved.internal_resolution = 3; + hd_saved.texture_upscale = 4; + if (!require(save_mod_state(hd_saved), "hd save")) return 69; + ModState hd_loaded{}; + hd_loaded.settings_path = hd_saved.settings_path; + load_mod_state(hd_loaded); + if (!require(hd_loaded.hd_rendering, "hd enable round trip")) + return 70; + if (!require(hd_loaded.internal_resolution == 3, + "hd internal resolution round trip")) return 71; + if (!require(hd_loaded.texture_upscale == 4, + "hd texture upscale round trip")) return 72; + std::filesystem::remove(hd_saved.settings_path); + } + return 0; } From 3b26101aa112d07d2e149a96f0b45795bbcacb9e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 01:50:22 -0700 Subject: [PATCH 002/164] release: v0.4.0-alpha Adds the opt-in HD Rendering mod: internal resolution up to 4x plus texture upscaling, off by default and configured from the Mods page. Gate status at cut time, recorded rather than glossed: - G1 firmware suite 6/8. download_play_shutdown and pictochat_room_a fail on ARM7 IRQ/instruction divergence against the oracle. Both are PRE-EXISTING: building the merge-base 80bfd07 in a detached worktree reproduces byte-identical failure numbers, so they are not caused by this release's changes. Tracked as beads-yjp.24. - MPH VB300/VB1800 checkpoints byte-identical to the pre-change baseline with HD off, including every instruction and cycle counter, so the default path is unchanged. - Launcher mod-provider test passes in both the mingw and release builds. - Texture upscaling has NOT had visual validation across DS texture formats, tiled surfaces, or cutout alpha edges. It is off by default and behind an explicit opt-in for that reason. Tracked as beads-yjp.22. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 2 +- README.md | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f7ea40..eaec98a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(MetroidPrimeHuntersRecomp VERSION 0.3.0 LANGUAGES C CXX) +project(MetroidPrimeHuntersRecomp VERSION 0.4.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index c22a26e..080945d 100644 --- a/README.md +++ b/README.md @@ -20,32 +20,39 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release Latest release: -**[v0.3.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.3.0-alpha)**. +**[v0.4.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.4.0-alpha)**. Downloads: - Windows: - `MetroidPrimeHuntersRecomp-windows-x64-v0.3.0.zip` + `MetroidPrimeHuntersRecomp-windows-x64-v0.4.0.zip` - Linux: - `MetroidPrimeHuntersRecomp-linux-x86_64-v0.3.0.AppImage` + `MetroidPrimeHuntersRecomp-linux-x86_64-v0.4.0.AppImage` This is the first release line in the ndsrecomp ecosystem and it is still very early. Campaign entry, widescreen output, Prime-style controls, gamepad support, and Wiimmfi lobby connectivity have all seen active bring-up, but this should still be treated as an alpha test build rather than a polished game release. +New in v0.4.0: an opt-in **HD Rendering** mod on the Mods page. It raises the +3D engine above one sample per DS pixel (up to 4x) and filters decoded +textures, so the widescreen image gains detail rather than just area. The 2D +layers stay native, exactly as the hardware draws them. It is off by default; +enable it under Mods and pick the internal resolution and texture upscaling +that suit your GPU. + ## Quick Start Windows: -1. Download and fully extract the `v0.3.0-alpha` Windows ZIP. +1. Download and fully extract the `v0.4.0-alpha` Windows ZIP. 2. Put your own Metroid Prime Hunters USA revision-0 `.nds` ROM next to `MetroidPrimeHuntersRecomp.exe`. 3. Run `MetroidPrimeHuntersRecomp.exe` and press Play. Linux: -1. Download the `v0.3.0-alpha` AppImage. +1. Download the `v0.4.0-alpha` AppImage. 2. Put your own Metroid Prime Hunters USA revision-0 `.nds` ROM next to the AppImage. 3. Run the AppImage. From bc34bd95f475e25e6d69371cdcb9201521e22262 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:15:33 +0900 Subject: [PATCH 003/164] Add MPH ROM profile registry --- config/mph_rom_profiles.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 config/mph_rom_profiles.json diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json new file mode 100644 index 0000000..442af6a --- /dev/null +++ b/config/mph_rom_profiles.json @@ -0,0 +1,29 @@ +{ + "schema": 1, + "profiles": { + "US1_0": { + "display_name": "Metroid Prime Hunters (USA rev 0)", + "region": "USA", + "game_code": "AMHE", + "revision": 0, + "rom_size": 67108864, + "sha1": "90164d1ac127ee5f9815ea4ae7de798c7b5fc629", + "program_id": "mph_amhe0", + "coverage": "coverage/adventure-main-entry-points.json", + "game_config": "game.toml", + "fmv_runtime": true + }, + "EU1_1": { + "display_name": "Metroid Prime Hunters (Europe rev 1)", + "region": "Europe", + "game_code": "AMHP", + "revision": 1, + "rom_size": 67108864, + "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "program_id": "mph_amhp1", + "coverage": "coverage/eu11-bootstrap-entry-points.json", + "game_config": "config/game-eu11.toml", + "fmv_runtime": false + } + } +} From fb5abc9b332e7512ddccf22b4cb277c3ad8c6edb Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:15:47 +0900 Subject: [PATCH 004/164] Add EU1.1 bootstrap coverage identity --- coverage/eu11-bootstrap-entry-points.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 coverage/eu11-bootstrap-entry-points.json diff --git a/coverage/eu11-bootstrap-entry-points.json b/coverage/eu11-bootstrap-entry-points.json new file mode 100644 index 0000000..efb59c2 --- /dev/null +++ b/coverage/eu11-bootstrap-entry-points.json @@ -0,0 +1,10 @@ +{ + "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 3b9254eb50ca18dfb69723826440e7cc2be5b072 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:16:01 +0900 Subject: [PATCH 005/164] Add safe EU1.1 runtime config --- config/game-eu11.toml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 config/game-eu11.toml diff --git a/config/game-eu11.toml b/config/game-eu11.toml new file mode 100644 index 0000000..1778a05 --- /dev/null +++ b/config/game-eu11.toml @@ -0,0 +1,26 @@ +[game] +name = "Metroid Prime Hunters" +id = "AMHP" +region = "Europe" +revision = 1 +status = "EU1.1 bootstrap: ROM-header main banks with interpreter fallback" +rom = "Metroid Prime Hunters (Europe Rev 1).nds" +rom_size = 0x04000000 +sha1 = "bdcd1dea293e24c98d4c481430e90d21198985a5" + +[display] +# Keep EU1.1 bootstrap on native presentation until title-specific projection, +# culling and HUD-address assumptions are validated for this revision. +screen_layout = "separate" +supersampling = 1 +antialiasing = 0 + +[system] +startup_mode = "automatic" + +[cartridge] +# Metroid Prime Hunters uses the same 256 KiB flash save geometry across the +# retail revisions tracked by the project. Keep this game-owned rather than a +# shared-runner default. +save_type = "flash" +save_size = 262144 From 8acf2819f4910f1e9e3ad245224e749eaf247136 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:16:39 +0900 Subject: [PATCH 006/164] Generalize MPH input preparation by ROM profile --- tools/prepare_mph.py | 177 +++++++++++++++++++++++++++++++------------ 1 file changed, 128 insertions(+), 49 deletions(-) diff --git a/tools/prepare_mph.py b/tools/prepare_mph.py index af509d7..193cc70 100644 --- a/tools/prepare_mph.py +++ b/tools/prepare_mph.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Verify AMHE0 and prepare ignored ndsrecomp main/overlay inputs.""" +"""Verify a configured MPH retail revision and prepare ndsrecomp inputs.""" from __future__ import annotations @@ -17,10 +17,9 @@ ) from exc -EXPECTED_ROM_SHA1 = "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" -EXPECTED_ROM_SIZE = 64 * 1024 * 1024 -EXPECTED_GAME_CODE = b"AMHE" -EXPECTED_REVISION = 0 +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_PROFILE_FILE = REPO_ROOT / "config" / "mph_rom_profiles.json" +DEFAULT_VERSION = "US1_0" def sha1(data: bytes) -> str: @@ -31,6 +30,58 @@ def toml_string(value: str) -> str: return json.dumps(value, ensure_ascii=True) +def load_profile(path: Path, version: str) -> dict[str, object]: + try: + document = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SystemExit(f"unable to read ROM profile registry {path}: {exc}") from exc + + profiles = document.get("profiles") + if not isinstance(profiles, dict): + raise SystemExit(f"ROM profile registry has no object-valued 'profiles': {path}") + profile = profiles.get(version) + if not isinstance(profile, dict): + choices = ", ".join(sorted(str(key) for key in profiles)) + raise SystemExit( + f"unknown MPH version {version!r}; configured versions: {choices}" + ) + + required = { + "display_name": str, + "game_code": str, + "revision": int, + "rom_size": int, + "sha1": str, + "program_id": str, + } + for key, expected_type in required.items(): + value = profile.get(key) + if not isinstance(value, expected_type): + raise SystemExit( + f"ROM profile {version!r} field {key!r} must be " + f"{expected_type.__name__}" + ) + + game_code = str(profile["game_code"]) + if len(game_code) != 4 or not game_code.isascii(): + raise SystemExit( + f"ROM profile {version!r} game_code must be exactly four ASCII bytes" + ) + digest = str(profile["sha1"]) + if len(digest) != 40 or any(c not in "0123456789abcdef" for c in digest): + raise SystemExit( + f"ROM profile {version!r} sha1 must be 40 lowercase hex digits" + ) + revision = int(profile["revision"]) + if revision < 0 or revision > 255: + raise SystemExit(f"ROM profile {version!r} revision must fit one byte") + rom_size = int(profile["rom_size"]) + if rom_size <= 0: + raise SystemExit(f"ROM profile {version!r} rom_size must be positive") + + return profile + + def write_seed_config( path: Path, *, @@ -42,25 +93,25 @@ def write_seed_config( coverage_entries: list[dict[str, object]], ) -> None: lines = [ - "# AUTO-GENERATED by tools/prepare_mph.py; do not commit.", - "[program]", - f"name = {toml_string(name)}", - f"id = {toml_string(program_id)}", - f"load_address = 0x{load_address:08x}", - f"size = 0x{len(binary):08x}", - f"entry_pc = 0x{entry_pc:08x}", - "authoritative_entry_points = false", - "", - "[identity]", - f"sha1 = {toml_string(sha1(binary))}", - "", - "[[entry_point]]", - f"addr = 0x{entry_pc:08x}", - 'mode = "arm"', - 'name = "entry"', - 'kind = "rom_header"', - "", - ] + "# AUTO-GENERATED by tools/prepare_mph.py; do not commit.", + "[program]", + f"name = {toml_string(name)}", + f"id = {toml_string(program_id)}", + f"load_address = 0x{load_address:08x}", + f"size = 0x{len(binary):08x}", + f"entry_pc = 0x{entry_pc:08x}", + "authoritative_entry_points = false", + "", + "[identity]", + f"sha1 = {toml_string(sha1(binary))}", + "", + "[[entry_point]]", + f"addr = 0x{entry_pc:08x}", + 'mode = "arm"', + 'name = "entry"', + 'kind = "rom_header"', + "", + ] for entry in coverage_entries: addr = int(str(entry["addr"]), 0) mode = str(entry["mode"]) @@ -97,12 +148,11 @@ def write_overlay_config( An overlay has no ROM-header entry point: the guest's own loader copies the image to `load_address` and enters it through pointers the main image - holds, so no root is seeded here — real entry points arrive from Tier-3 - coverage recorded while this exact body was resident (the - PokemonBlackWhiteRecomp probe/build tools are the reference consumers). - The [overlay] table carries the facts a content-validated dispatch row - needs: the exact image identity and the static-init span, the one region - the guest legitimately mutates after load. + holds, so no root is seeded here. Real entry points arrive from Tier-3 + coverage recorded while this exact body was resident. The [overlay] table + carries the facts a content-validated dispatch row needs: exact image + identity and the static-init span, the one region the guest legitimately + mutates after load. """ lines = [ "# AUTO-GENERATED by tools/prepare_mph.py; do not commit.", @@ -137,34 +187,59 @@ def main() -> int: 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( + "--version", + default=DEFAULT_VERSION, + help=f"ROM profile key (default: {DEFAULT_VERSION})", + ) + parser.add_argument( + "--profiles", + type=Path, + default=DEFAULT_PROFILE_FILE, + help=f"ROM profile registry (default: {DEFAULT_PROFILE_FILE})", + ) args = parser.parse_args() + profile = load_profile(args.profiles, args.version) + expected_sha1 = str(profile["sha1"]) + expected_size = int(profile["rom_size"]) + expected_game_code = str(profile["game_code"]).encode("ascii") + expected_revision = int(profile["revision"]) + display_name = str(profile["display_name"]) + program_id = str(profile["program_id"]) + rom_bytes = args.rom.read_bytes() digest = sha1(rom_bytes) - if len(rom_bytes) != EXPECTED_ROM_SIZE: + if len(rom_bytes) != expected_size: raise SystemExit( - f"ROM size mismatch: got {len(rom_bytes)}, expected {EXPECTED_ROM_SIZE}" + f"ROM size mismatch for {args.version}: got {len(rom_bytes)}, " + f"expected {expected_size}" ) - if digest != EXPECTED_ROM_SHA1: + if digest != expected_sha1: raise SystemExit( - f"ROM SHA-1 mismatch: got {digest}, expected {EXPECTED_ROM_SHA1}" + f"ROM SHA-1 mismatch for {args.version}: got {digest}, " + f"expected {expected_sha1}" ) - if rom_bytes[0x0C:0x10] != EXPECTED_GAME_CODE: + if rom_bytes[0x0C:0x10] != expected_game_code: raise SystemExit( - f"game code mismatch: got {rom_bytes[0x0C:0x10]!r}, " - f"expected {EXPECTED_GAME_CODE!r}" + f"game code mismatch for {args.version}: got " + f"{rom_bytes[0x0C:0x10]!r}, expected {expected_game_code!r}" ) - if rom_bytes[0x1C] != EXPECTED_REVISION: + if rom_bytes[0x1C] != expected_revision: raise SystemExit( - f"ROM revision mismatch: got {rom_bytes[0x1C]}, " - f"expected {EXPECTED_REVISION}" + f"ROM revision mismatch for {args.version}: got {rom_bytes[0x1C]}, " + f"expected {expected_revision}" ) rom = ndspy.rom.NintendoDSRom(rom_bytes) coverage = json.loads(args.coverage.read_text(encoding="utf-8")) - if coverage.get("game_sha1") != EXPECTED_ROM_SHA1: - raise SystemExit("static coverage seed identity does not match AMHE0") + 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) @@ -181,8 +256,8 @@ def main() -> int: write_seed_config( out / "arm9.toml", - name="Metroid Prime Hunters (USA rev 0) ARM9 main", - program_id="mph_amhe0_arm9", + name=f"{display_name} ARM9 main", + program_id=f"{program_id}_arm9", load_address=int(rom.arm9RamAddress), entry_pc=int(rom.arm9EntryAddress), binary=arm9, @@ -190,8 +265,8 @@ def main() -> int: ) write_seed_config( out / "arm7.toml", - name="Metroid Prime Hunters (USA rev 0) ARM7", - program_id="mph_amhe0_arm7", + name=f"{display_name} ARM7", + program_id=f"{program_id}_arm7", load_address=int(rom.arm7RamAddress), entry_pc=int(rom.arm7EntryAddress), binary=arm7, @@ -211,8 +286,8 @@ def main() -> int: if int(overlay.ramAddress) < DS_MAIN_RAM_LIMIT: write_overlay_config( overlays_dir / f"arm9_overlay_{overlay_id:03d}.toml", - title="Metroid Prime Hunters (USA rev 0)", - program_id="mph_amhe0_arm9", + title=display_name, + program_id=f"{program_id}_arm9", overlay_id=overlay_id, load_address=int(overlay.ramAddress), binary=data, @@ -246,7 +321,11 @@ def main() -> int: newline="\n", ) - print(f"ROM verified: AMHE revision 0, SHA-1 {digest}") + print( + f"ROM verified: profile={args.version} code=" + f"{expected_game_code.decode('ascii')} revision={expected_revision} " + f"SHA-1 {digest}" + ) print( f"ARM9: {len(arm9_compressed):,} compressed bytes -> " f"{len(arm9):,} bytes at 0x{int(rom.arm9RamAddress):08X}" From 9d777a86cd781fbf29b79ab914627ecf7556c6f6 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:17:08 +0900 Subject: [PATCH 007/164] Make MPH banks version-profile driven --- CMakeLists.txt | 144 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 44 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f7ea40..4a9c964 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.20) project(MetroidPrimeHuntersRecomp VERSION 0.3.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) @@ -18,13 +18,55 @@ add_subdirectory( "${NDSRECOMP_ROOT}/recompiler" "${CMAKE_BINARY_DIR}/ndsrecomp-recompiler") +set(MPH_VERSION "US1_0" CACHE STRING + "Metroid Prime Hunters retail revision profile") +set_property(CACHE MPH_VERSION PROPERTY STRINGS US1_0 EU1_1) +set(MPH_PROFILE_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/config/mph_rom_profiles.json" + CACHE FILEPATH "Metroid Prime Hunters ROM profile registry") +if(NOT EXISTS "${MPH_PROFILE_FILE}") + message(FATAL_ERROR "MPH ROM profile registry not found: ${MPH_PROFILE_FILE}") +endif() +file(READ "${MPH_PROFILE_FILE}" MPH_PROFILES_JSON) +string(JSON MPH_PROFILE_NAME GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" display_name) +string(JSON MPH_PROFILE_GAME_CODE GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" game_code) +string(JSON MPH_PROFILE_REVISION GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" revision) +string(JSON MPH_PROFILE_ROM_SIZE GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" rom_size) +string(JSON MPH_PROFILE_SHA1 GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" sha1) +string(JSON MPH_PROFILE_COVERAGE GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" coverage) +string(JSON MPH_PROFILE_GAME_CONFIG GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" game_config) +string(JSON MPH_PROFILE_FMV_RUNTIME GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" fmv_runtime) + set(MPH_ROM "${CMAKE_CURRENT_SOURCE_DIR}/Metroid Prime Hunters.nds" - CACHE FILEPATH "Path to the verified USA AMHE revision-0 ROM") -set(MPH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated") + CACHE FILEPATH "Path to the verified ${MPH_PROFILE_NAME} ROM") +set(MPH_GENERATED_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/generated" CACHE PATH + "Root for ignored generated Metroid Prime Hunters artifacts") +if(MPH_VERSION STREQUAL "US1_0") + set(MPH_GENERATED_DIR "${MPH_GENERATED_ROOT}") +else() + set(MPH_GENERATED_DIR "${MPH_GENERATED_ROOT}/${MPH_VERSION}") +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}/coverage/adventure-main-entry-points.json") + "${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() + +message(STATUS + "MPH profile ${MPH_VERSION}: ${MPH_PROFILE_NAME}, " + "${MPH_PROFILE_GAME_CODE} revision ${MPH_PROFILE_REVISION}, " + "SHA-1 ${MPH_PROFILE_SHA1}") set(_mph_venv_python "${CMAKE_CURRENT_SOURCE_DIR}/.venv/Scripts/python.exe") if(EXISTS "${_mph_venv_python}") @@ -48,9 +90,11 @@ add_custom_command( --rom "${MPH_ROM}" --out "${MPH_INPUT_DIR}" --coverage "${MPH_COVERAGE_SEEDS}" + --version "${MPH_VERSION}" + --profiles "${MPH_PROFILE_FILE}" DEPENDS "${MPH_ROM}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/prepare_mph.py" - "${MPH_COVERAGE_SEEDS}" - COMMENT "Verifying AMHE0 and preparing main/overlay code images") + "${MPH_PROFILE_FILE}" "${MPH_COVERAGE_SEEDS}" + COMMENT "Verifying ${MPH_VERSION} and preparing main/overlay code images") add_custom_target(mph_prepare DEPENDS ${MPH_PREP_OUTPUTS}) set(MPH_ARM9_SOURCES) @@ -91,7 +135,7 @@ add_custom_command( --validate-live-bytes COMMAND ${CMAKE_COMMAND} -E touch "${MPH_ARM9_STAMP}" DEPENDS nds_recompile ${MPH_PREP_OUTPUTS} - COMMENT "Discovering and recompiling the AMHE0 ARM9 main closure") + COMMENT "Discovering and recompiling ${MPH_VERSION} ARM9 main closure") set(MPH_ARM7_STAMP "${MPH_RECOMP_DIR}/mph_arm7.stamp") add_custom_command( @@ -109,49 +153,55 @@ add_custom_command( --validate-live-bytes COMMAND ${CMAKE_COMMAND} -E touch "${MPH_ARM7_STAMP}" DEPENDS nds_recompile ${MPH_PREP_OUTPUTS} - COMMENT "Discovering and recompiling the AMHE0 ARM7 initial closure") + COMMENT "Discovering and recompiling ${MPH_VERSION} ARM7 initial closure") -set(MPH_FMV_RUNTIME_CONFIG - "${CMAKE_CURRENT_SOURCE_DIR}/config/mph_arm9_fmv_runtime.toml") -set(MPH_FMV_RUNTIME_IMAGE - "${MPH_GENERATED_DIR}/capture/mph_arm9_fmv_runtime.bin" - CACHE FILEPATH "Deterministic MPH FMV ITCM+main-RAM capture") set(MPH_FMV_RUNTIME_SOURCES) set(MPH_FMV_RUNTIME_STAMP) -if(EXISTS "${MPH_FMV_RUNTIME_CONFIG}" AND EXISTS "${MPH_FMV_RUNTIME_IMAGE}") - foreach(shard RANGE 0 31) - if(shard LESS 10) - set(shard_name "0${shard}") - else() - set(shard_name "${shard}") - endif() +if(MPH_PROFILE_FMV_RUNTIME) + set(MPH_FMV_RUNTIME_CONFIG + "${CMAKE_CURRENT_SOURCE_DIR}/config/mph_arm9_fmv_runtime.toml") + set(MPH_FMV_RUNTIME_IMAGE + "${MPH_GENERATED_ROOT}/capture/mph_arm9_fmv_runtime.bin" + CACHE FILEPATH "Deterministic MPH FMV ITCM+main-RAM capture") + if(EXISTS "${MPH_FMV_RUNTIME_CONFIG}" AND EXISTS "${MPH_FMV_RUNTIME_IMAGE}") + foreach(shard RANGE 0 31) + if(shard LESS 10) + set(shard_name "0${shard}") + else() + set(shard_name "${shard}") + endif() + list(APPEND MPH_FMV_RUNTIME_SOURCES + "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime_${shard_name}.c") + endforeach() list(APPEND MPH_FMV_RUNTIME_SOURCES - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime_${shard_name}.c") - endforeach() - list(APPEND MPH_FMV_RUNTIME_SOURCES - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime_dispatch.c") - set(MPH_FMV_RUNTIME_STAMP - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime.stamp") - add_custom_command( - OUTPUT "${MPH_FMV_RUNTIME_STAMP}" - BYPRODUCTS ${MPH_FMV_RUNTIME_SOURCES} - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime.h" - COMMAND $ - --config "${MPH_FMV_RUNTIME_CONFIG}" - --bin "${MPH_FMV_RUNTIME_IMAGE}" - --out "${MPH_RECOMP_DIR}" - --bank mph_arm9_fmv_runtime - --shards 32 - --stable-address-shards - --max-function-bytes 512 - --validate-live-bytes - COMMAND ${CMAKE_COMMAND} -E touch "${MPH_FMV_RUNTIME_STAMP}" - DEPENDS nds_recompile "${MPH_FMV_RUNTIME_CONFIG}" - "${MPH_FMV_RUNTIME_IMAGE}" - COMMENT "Recompiling content-validated AMHE0 ARM9 FMV runtime code") + "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime_dispatch.c") + set(MPH_FMV_RUNTIME_STAMP + "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime.stamp") + add_custom_command( + OUTPUT "${MPH_FMV_RUNTIME_STAMP}" + BYPRODUCTS ${MPH_FMV_RUNTIME_SOURCES} + "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime.h" + COMMAND $ + --config "${MPH_FMV_RUNTIME_CONFIG}" + --bin "${MPH_FMV_RUNTIME_IMAGE}" + --out "${MPH_RECOMP_DIR}" + --bank mph_arm9_fmv_runtime + --shards 32 + --stable-address-shards + --max-function-bytes 512 + --validate-live-bytes + COMMAND ${CMAKE_COMMAND} -E touch "${MPH_FMV_RUNTIME_STAMP}" + DEPENDS nds_recompile "${MPH_FMV_RUNTIME_CONFIG}" + "${MPH_FMV_RUNTIME_IMAGE}" + COMMENT "Recompiling content-validated ${MPH_VERSION} ARM9 FMV runtime code") + else() + message(STATUS + "MPH FMV runtime bank disabled: capture image is not present") + endif() else() message(STATUS - "MPH FMV runtime bank disabled: capture image is not present") + "MPH FMV runtime bank disabled for ${MPH_VERSION}: no revision-specific " + "runtime capture has been validated") endif() add_custom_target(mph_generate_banks @@ -180,6 +230,12 @@ set_target_properties(mph_romcheck PROPERTIES OUTPUT_NAME "MetroidPrimeHuntersRecomp") target_include_directories(mph_romcheck PRIVATE "${NDSRECOMP_ROOT}/recompiler/support") +target_compile_definitions(mph_romcheck PRIVATE + MPH_BUILD_VERSION="${MPH_VERSION}" + MPH_EXPECTED_SHA1="${MPH_PROFILE_SHA1}" + MPH_EXPECTED_GAME_CODE="${MPH_PROFILE_GAME_CODE}" + MPH_EXPECTED_REVISION=${MPH_PROFILE_REVISION} + MPH_EXPECTED_SIZE=${MPH_PROFILE_ROM_SIZE}) if(NOT MSVC) target_compile_options(mph_romcheck PRIVATE -Wall -Wextra) endif() From 2ecf3999e2d54d8b47fbacd6bb62332d8c7aef36 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:17:30 +0900 Subject: [PATCH 008/164] Make ROM checker follow selected MPH profile --- src/main.cpp | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 19b7357..5c3c3d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,14 +6,33 @@ #include #include +#ifndef MPH_BUILD_VERSION +#define MPH_BUILD_VERSION "US1_0" +#endif +#ifndef MPH_EXPECTED_SHA1 +#define MPH_EXPECTED_SHA1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" +#endif +#ifndef MPH_EXPECTED_GAME_CODE +#define MPH_EXPECTED_GAME_CODE "AMHE" +#endif +#ifndef MPH_EXPECTED_REVISION +#define MPH_EXPECTED_REVISION 0 +#endif +#ifndef MPH_EXPECTED_SIZE +#define MPH_EXPECTED_SIZE 67108864 +#endif + namespace { constexpr const char* kDefaultRom = "Metroid Prime Hunters.nds"; -constexpr const char* kExpectedSha1 = - "90164d1ac127ee5f9815ea4ae7de798c7b5fc629"; +constexpr const char* kBuildVersion = MPH_BUILD_VERSION; +constexpr const char* kExpectedSha1 = MPH_EXPECTED_SHA1; constexpr const char* kExpectedTitle = "MP HUNTERS"; -constexpr const char* kExpectedGameCode = "AMHE"; -constexpr std::size_t kExpectedSize = 64u * 1024u * 1024u; +constexpr const char* kExpectedGameCode = MPH_EXPECTED_GAME_CODE; +constexpr unsigned kExpectedRevision = + static_cast(MPH_EXPECTED_REVISION); +constexpr std::size_t kExpectedSize = + static_cast(MPH_EXPECTED_SIZE); std::vector read_file(const std::string& path) { std::ifstream file(path, std::ios::binary | std::ios::ate); @@ -84,7 +103,7 @@ int main(int argc, char** argv) { valid = false; } if (title != kExpectedTitle || game_code != kExpectedGameCode || - revision != 0u) { + revision != kExpectedRevision) { std::fprintf(stderr, "ROM identity mismatch: title=%s code=%s revision=%u\n", title.c_str(), game_code.c_str(), revision); @@ -96,12 +115,15 @@ int main(int argc, char** argv) { valid = false; } if (!valid) { - std::fputs("refusing selection: this project is pinned to AMHE0\n", - stderr); + std::fprintf( + stderr, + "refusing selection: this build targets MPH profile %s only\n", + kBuildVersion); return 1; } std::puts("selection=metroid-prime-hunters"); + std::printf("profile=%s\n", kBuildVersion); std::printf("rom=%s\n", rom_path.c_str()); std::printf("title=%s game_code=%s revision=%u sha1=%s\n", title.c_str(), game_code.c_str(), revision, digest.c_str()); @@ -109,9 +131,6 @@ int main(int argc, char** argv) { read_u32(rom, 0x24u), read_u32(rom, 0x34u)); std::printf("arm9_size=0x%08x arm7_size=0x%08x\n", read_u32(rom, 0x2Cu), read_u32(rom, 0x3Cu)); - std::puts("reference=NoneGiven/MphRead (AMHE0-aware; non-matching recreation)"); std::puts("boot_status=authentic-firmware-and-cartridge"); - std::puts("attract_status=full-no-input-loop"); - std::puts("gameplay_status=celestial-archives-entry"); return 0; } From 99edb5122c367cd57436d46b3db005ab7c6ce9c3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:17:52 +0900 Subject: [PATCH 009/164] Add EU1.1 Windows bring-up build path --- tools/build-windows.ps1 | 67 +++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index 3f75d53..731d788 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -1,21 +1,23 @@ <# -Build and package the Metroid Prime Hunters Recomp Windows release. +Build Metroid Prime Hunters Recomp for one configured retail revision. -This script builds the title banks, the sibling ndsrecomp runner, and the -Windows recomp-ui launcher, then stages a portable ZIP with tools\make_release.ps1. -It does not publish a release. +US1_0 keeps the existing release path, including the recomp-ui launcher and +portable ZIP. Other profiles currently build the title banks and runner only; +they are bring-up builds until their title-specific runtime hooks are validated. Usage: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` - tools\build-windows.ps1 -Version 0.1.0 + tools\build-windows.ps1 -Version 0.3.0 -MphVersion EU1_1 #> param( [string]$Version = '0.1.0', + [ValidateSet('US1_0', 'EU1_1')] + [string]$MphVersion = 'US1_0', [string]$CMake = 'C:\msys64\mingw64\bin\cmake.exe', [string]$Generator = 'Ninja', [int]$Jobs = 12, - [string]$GameBuildDir = 'build-release', - [string]$RunnerBuildDir = '..\ndsrecomp\runner\build-mph-release', + [string]$GameBuildDir = '', + [string]$RunnerBuildDir = '', [string]$LauncherBuildDir = 'launcher\recomp-ui\build-release', [string]$RuntimeBinDir = 'C:\msys64\mingw64\bin', [string]$RecompUiRoot = 'F:\Projects\recomp-ui' @@ -28,19 +30,51 @@ if (-not (Test-Path -LiteralPath $cmakePath)) { throw "CMake not found: $cmakePath" } +$profileFile = Join-Path $root 'config\mph_rom_profiles.json' +$registry = Get-Content -LiteralPath $profileFile -Raw | ConvertFrom-Json +$profileProperty = $registry.profiles.PSObject.Properties[$MphVersion] +if ($null -eq $profileProperty) { + throw "Unknown MPH profile: $MphVersion" +} +$profile = $profileProperty.Value +$romSha1 = [string]$profile.sha1 + +if ([string]::IsNullOrWhiteSpace($GameBuildDir)) { + if ($MphVersion -eq 'US1_0') { + $GameBuildDir = 'build-release' + } else { + $GameBuildDir = "build-release-$MphVersion" + } +} +if ([string]::IsNullOrWhiteSpace($RunnerBuildDir)) { + if ($MphVersion -eq 'US1_0') { + $RunnerBuildDir = '..\ndsrecomp\runner\build-mph-release' + } else { + $RunnerBuildDir = "..\ndsrecomp\runner\build-mph-release-$MphVersion" + } +} + $frameworkRoot = [IO.Path]::GetFullPath((Join-Path $root '..\ndsrecomp')) $gameBuild = [IO.Path]::GetFullPath((Join-Path $root $GameBuildDir)) $runnerBuild = [IO.Path]::GetFullPath((Join-Path $root $RunnerBuildDir)) $launcherBuild = [IO.Path]::GetFullPath((Join-Path $root $LauncherBuildDir)) -$titleBankDir = [IO.Path]::GetFullPath((Join-Path $root 'generated\recomp')) -$romSha1 = '90164d1ac127ee5f9815ea4ae7de798c7b5fc629' +if ($MphVersion -eq 'US1_0') { + $titleBankDir = [IO.Path]::GetFullPath((Join-Path $root 'generated\recomp')) +} else { + $titleBankDir = [IO.Path]::GetFullPath( + (Join-Path $root "generated\$MphVersion\recomp")) +} Push-Location $root try { + Write-Host "Building MPH profile $MphVersion ($($profile.game_code) rev $($profile.revision))" + & $cmakePath -G $Generator -S $root -B $gameBuild ` -DCMAKE_BUILD_TYPE=Release ` - -DNDSRECOMP_ROOT="$frameworkRoot" + -DNDSRECOMP_ROOT="$frameworkRoot" ` + -DMPH_VERSION="$MphVersion" if ($LASTEXITCODE -ne 0) { throw 'Game CMake configure failed.' } + & $cmakePath --build $gameBuild --target metroidprimehuntersrecomp -j $Jobs if ($LASTEXITCODE -ne 0) { throw 'Game bank build failed.' } @@ -50,14 +84,27 @@ try { "-DNDS_TITLE_BANK_DIR=$titleBankDir" ` "-DNDS_TITLE_ROM_SHA1=$romSha1" if ($LASTEXITCODE -ne 0) { throw 'Runner CMake configure failed.' } + & $cmakePath --build $runnerBuild -j $Jobs if ($LASTEXITCODE -ne 0) { throw 'Runner build failed.' } + if ($MphVersion -ne 'US1_0') { + $gameConfig = [IO.Path]::GetFullPath( + (Join-Path $root ([string]$profile.game_config))) + Write-Host '' + Write-Host "Bring-up runner built: $runnerBuild\nds_runner.exe" + Write-Host "Use the revision-specific config: $gameConfig" + Write-Host 'Launcher/release packaging intentionally skipped for this profile.' + Write-Host 'Prime Controls and direct mouse aim remain USA-only until revision addresses are validated.' + return + } + & $cmakePath -G $Generator -S "$root\launcher\recomp-ui" -B $launcherBuild ` -DCMAKE_BUILD_TYPE=Release ` -DRECOMP_UI_ROOT="$RecompUiRoot" ` -DCMAKE_PREFIX_PATH="$RuntimeBinDir\..\lib\cmake" if ($LASTEXITCODE -ne 0) { throw 'Launcher CMake configure failed.' } + & $cmakePath --build $launcherBuild -j $Jobs if ($LASTEXITCODE -ne 0) { throw 'Launcher build failed.' } From f613b6a43565687da3ffe6a20c61b0855471cfda Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:18:18 +0900 Subject: [PATCH 010/164] Add EU1.1 Linux bring-up build path --- tools/build-linux.sh | 76 +++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/tools/build-linux.sh b/tools/build-linux.sh index c2bd0bb..6fc7302 100644 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -1,40 +1,70 @@ #!/usr/bin/env bash -# Build a Metroid Prime Hunters Recomp Linux x86_64 AppImage. +# Build Metroid Prime Hunters Recomp for a configured retail revision. # -# The current recomp-ui launcher is Windows-only, so this packages the title -# runner directly. Put a legally dumped Metroid Prime Hunters .nds and a bios/ -# folder beside the AppImage; AppRun auto-detects the ROM. +# US1_0 keeps the existing AppImage behavior. EU1_1 uses its own generated +# bank/config paths and deliberately omits the USA-only FMV-bank assertion. set -euo pipefail APP_NAME="MetroidPrimeHuntersRecomp" TITLE_TARGET="metroidprimehuntersrecomp" -ROM_SHA1="90164d1ac127ee5f9815ea4ae7de798c7b5fc629" RUNNER_NAME="nds_runner" VERSION="0.1.0" +MPH_VERSION="US1_0" JOBS="$(nproc 2>/dev/null || echo 4)" DO_PACKAGE=1 REPO="$(cd "$(dirname "$0")/.." && pwd)" FRAMEWORK_ROOT="$(cd "$REPO/../ndsrecomp" && pwd)" OUT="$REPO/release-linux" +PROFILE_FILE="$REPO/config/mph_rom_profiles.json" while [ $# -gt 0 ]; do case "$1" in --version) VERSION="$2"; shift 2;; + --mph-version) MPH_VERSION="$2"; shift 2;; --jobs) JOBS="$2"; shift 2;; --out) OUT="$2"; shift 2;; --no-package) DO_PACKAGE=0; shift;; -h|--help) - sed -n '2,14p' "$0" + sed -n '2,16p' "$0" exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2;; esac done -GAME_BUILD="$REPO/build-linux-release" -RUNNER_BUILD="$FRAMEWORK_ROOT/runner/build-mph-linux-release" -TITLE_BANK_DIR="$REPO/generated/recomp" +profile_value() { + python3 - "$PROFILE_FILE" "$MPH_VERSION" "$1" <<'PY' +import json +import sys +path, version, key = sys.argv[1:] +with open(path, encoding="utf-8") as f: + registry = json.load(f) +try: + value = registry["profiles"][version][key] +except KeyError as exc: + raise SystemExit(f"unknown profile/field: {version}.{key}") from exc +if isinstance(value, bool): + print("1" if value else "0") +else: + print(value) +PY +} + +ROM_SHA1="$(profile_value sha1)" +GAME_CONFIG_REL="$(profile_value game_config)" +FMV_RUNTIME="$(profile_value fmv_runtime)" +GAME_CONFIG="$REPO/$GAME_CONFIG_REL" + +if [ "$MPH_VERSION" = "US1_0" ]; then + GAME_BUILD="$REPO/build-linux-release" + RUNNER_BUILD="$FRAMEWORK_ROOT/runner/build-mph-linux-release" + TITLE_BANK_DIR="$REPO/generated/recomp" +else + GAME_BUILD="$REPO/build-linux-release-$MPH_VERSION" + RUNNER_BUILD="$FRAMEWORK_ROOT/runner/build-mph-linux-release-$MPH_VERSION" + TITLE_BANK_DIR="$REPO/generated/$MPH_VERSION/recomp" +fi cd "$REPO" test -f "$FRAMEWORK_ROOT/recompiler/CMakeLists.txt" || { @@ -45,11 +75,16 @@ test -f "$REPO/Metroid Prime Hunters.nds" || { echo "ERROR: verified Metroid Prime Hunters ROM is missing from the repo root." >&2 exit 1 } +test -f "$GAME_CONFIG" || { + echo "ERROR: game config for $MPH_VERSION is missing: $GAME_CONFIG" >&2 + exit 1 +} -echo "[1/4] configure title banks" +echo "[1/4] configure title banks ($MPH_VERSION)" cmake -S "$REPO" -B "$GAME_BUILD" -G "Unix Makefiles" \ -DCMAKE_BUILD_TYPE=Release \ - -DNDSRECOMP_ROOT="$FRAMEWORK_ROOT" + -DNDSRECOMP_ROOT="$FRAMEWORK_ROOT" \ + -DMPH_VERSION="$MPH_VERSION" echo "[2/4] build title banks" cmake --build "$GAME_BUILD" --target "$TITLE_TARGET" -j"$JOBS" @@ -64,15 +99,18 @@ cmake --build "$RUNNER_BUILD" -j"$JOBS" if [ "$DO_PACKAGE" = "0" ]; then echo "done: $RUNNER_BUILD/$RUNNER_NAME" + echo "config: $GAME_CONFIG" exit 0 fi BIN="$RUNNER_BUILD/$RUNNER_NAME" test -f "$BIN" || { echo "ERROR: runner not built: $BIN" >&2; exit 1; } -strings "$BIN" | grep -q mph_arm9_fmv_runtime || { - echo "ERROR: runner does not contain the MPH FMV runtime bank." >&2 - exit 1 -} +if [ "$FMV_RUNTIME" = "1" ]; then + strings "$BIN" | grep -q mph_arm9_fmv_runtime || { + echo "ERROR: runner does not contain the MPH FMV runtime bank." >&2 + exit 1 + } +fi LINUXDEPLOY_URL=https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage LINUXDEPLOY_SHA=421ca71d5c69ea97c6309276232990d43df1dcece0edfaa26bbf926ff96ed12e @@ -101,7 +139,7 @@ APPDIR="$WORK/AppDir" mkdir -p "$APPDIR/usr/bin/bios" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps" cp "$BIN" "$APPDIR/usr/bin/$RUNNER_NAME" -cp "$REPO/game.toml" "$APPDIR/usr/bin/game.toml" +cp "$GAME_CONFIG" "$APPDIR/usr/bin/game.toml" cp "$REPO/README.md" "$APPDIR/usr/bin/README.md" cp "$REPO/LICENSE" "$APPDIR/usr/bin/LICENSE" cp "$REPO/packaging/BIOS_README.txt" "$APPDIR/usr/bin/bios/README.txt" @@ -148,9 +186,9 @@ done cd "$RUNDIR" 2>/dev/null || true if [ "$#" -eq 0 ]; then if [ -n "$ROM" ]; then - exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" --config "$HERE/usr/bin/game.toml" --screen-layout separate --adaptive-widescreen top --startup-mode automatic + exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" --config "$HERE/usr/bin/game.toml" fi - exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --config "$HERE/usr/bin/game.toml" --screen-layout separate --adaptive-widescreen top --startup-mode automatic + exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --config "$HERE/usr/bin/game.toml" fi exec "$HERE/usr/bin/nds_runner" "$@" EOF @@ -161,7 +199,7 @@ echo "[4/4] package AppImage" --desktop-file "$APPDIR/usr/share/applications/$APP_NAME.desktop" \ --icon-file "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" >/dev/null -APP="$OUT/$APP_NAME-linux-v$VERSION-x86_64.AppImage" +APP="$OUT/$APP_NAME-$MPH_VERSION-linux-v$VERSION-x86_64.AppImage" rm -f "$APP" ARCH=x86_64 "$APPIMAGETOOL_BIN" --appimage-extract-and-run "$APPDIR" "$APP" >/dev/null chmod +x "$APP" From 37924ba6cb83c48c1b5abf253954ca851a3c6a91 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:18:46 +0900 Subject: [PATCH 011/164] Document EU1.1 bring-up gates --- docs/EU1_1_BRINGUP.md | 307 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 docs/EU1_1_BRINGUP.md diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md new file mode 100644 index 0000000..817c7c1 --- /dev/null +++ b/docs/EU1_1_BRINGUP.md @@ -0,0 +1,307 @@ +# Metroid Prime Hunters Recomp - EU1.1 Bring-up + +作成日: 2026-08-17 + +## 1. 目的 + +`MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定から段階的に +multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) +を安全にbring-upする。 + +この段階は「EU1.1を正式対応済みにする」ものではない。 +まず以下を成立させる。 + +1. ROM identityを版別profileとして管理する。 +2. EU1.1 ROMからARM9 / ARM7 / ARM9 overlayを直接抽出できる。 +3. EU1.1専用bankをUS1.0生成物から分離して生成できる。 +4. US1.0のcoverage seedやFMV runtime captureをEU1.1へ流用しない。 +5. exact ROM SHA-1でrunner側のbank登録をgateする。 +6. EU1.1固有の未解析箇所はInterpreter fallbackへ安全に落とす。 + +## 2. EU1.1 identity + +| Field | EU1.1 | +|---|---| +| Profile key | `EU1_1` | +| Game Code | `AMHP` | +| Revision | `1` | +| ROM size | `0x04000000` / 64 MiB | +| SHA-1 | `bdcd1dea293e24c98d4c481430e90d21198985a5` | +| Program ID prefix | `mph_amhp1` | + +identityは `config/mph_rom_profiles.json` に集約する。 + +## 3. 今回追加するbootstrap構造 + +### 3.1 Profile registry + +`config/mph_rom_profiles.json` + +US1.0とEU1.1を同一schemaで管理する。 + +Profileには最低限、 + +- `game_code` +- `revision` +- `rom_size` +- `sha1` +- `program_id` +- `coverage` +- `game_config` +- `fmv_runtime` + +を持たせる。 + +### 3.2 EU1.1 coverage seed + +`coverage/eu11-bootstrap-entry-points.json` + +初期状態ではARM9 / ARM7の追加coverage rootを空にする。 + +これは意図的である。 + +`prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、 +EU1.1はまずそのrootからstatic discoveryを行い、未コンパイル領域は +runtime Interpreterへfallbackする。 + +US1.0の `coverage/adventure-main-entry-points.json` に入っているアドレスを +EU1.1へそのままコピーしてはいけない。 + +### 3.3 Generated tree separation + +US1.0は後方互換性のため従来通り: + +```text +generated/ + inputs/ + recomp/ + capture/ +``` + +EU1.1は: + +```text +generated/ + EU1_1/ + inputs/ + recomp/ +``` + +とする。 + +これにより異なるROM由来のbankやbinaryが同じパスへ混ざらない。 + +## 4. mphCodexで確認済みのEU1.1差分例 + +AllVersions調査ではEU1.1に次の対応が確認されている。 + +| Semantic | US1.0 | EU1.1 | +|---|---:|---:| +| Current Camera Sequence | `0x020D9CB0` | `0x020DA5D0` | +| Game Mode | `0x020E78FC` | `0x020E845C` | +| Upper HUD function | `0x0202F600` | `0x0202F5E0` | +| Crosshair callsite | `0x0202F934` | `0x0202F904` | +| Crosshair renderer | `0x020393D4` | `0x02039338` | +| Local Player Pointer | `0x020BCA70` | `0x020BD370` | +| HUD suppression storage | `0x020DE748` | `0x020DF068` | + +重要なのは、差分が単一の固定deltaではないことである。 + +したがって今後のtitle patch / enhancementは、 + +```text +US1.0 address + region offset +``` + +ではなく、 + +```text +ROM identity -> semantic runtime profile -> exact address +``` + +で管理する。 + +## 5. 現時点でEU1.1へ有効化しない機能 + +### 5.1 Prime Controls / Morph state + +pinned `ndsrecomp` runnerのPrime ControlsにはUS1.0専用の + +```text +0x020DA818 +``` + +が直接使われている。 + +EU1.1側の同semanticアドレスを確認するまでは無効のままにする。 + +### 5.2 Direct Mouse Aim + +`runner/src/title_patches.cpp` のDirect Mouse AimにはUS1.0専用の + +```text +Aim X: 0x020DE526 +Aim Y: 0x020DE52E +``` + +が使われている。 + +これもEU1.1 mapping完了前には有効化しない。 + +### 5.3 USA FMV runtime bank + +US1.0の + +```text +config/mph_arm9_fmv_runtime.toml +generated/capture/mph_arm9_fmv_runtime.bin +``` + +はUS1.0のruntime bytesとobserved PCsに対するbankである。 + +EU1.1では `fmv_runtime=false` とし、絶対に登録しない。 + +EU1.1のFMV高速化はEU1.1実行から独立captureを作成した後に行う。 + +### 5.4 Launcherのsupported-ROM list + +recomp-ui launcherのsupported SHA-1配列にはまだEU1.1を追加しない。 + +理由は、launcherがPrime Controls等を通常のplayer-facing featureとして有効化する +経路を持つためである。 + +EU1.1の基本bootとruntime profileが検証されるまで「正式対応」と表示しない。 + +## 6. Build + +### Windows bring-up + +EU1.1 ROMをrepo rootの + +```text +Metroid Prime Hunters.nds +``` + +として配置した上で: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` + tools\build-windows.ps1 ` + -MphVersion EU1_1 +``` + +EU1.1ではtitle bankとrunnerまでをbuildし、launcher/release packagingは +意図的にskipする。 + +revision-specific runtime config: + +```text +config/game-eu11.toml +``` + +### Linux bring-up + +```bash +tools/build-linux.sh --mph-version EU1_1 --no-package +``` + +AppImage生成まで行う場合: + +```bash +tools/build-linux.sh --mph-version EU1_1 +``` + +package内の `game.toml` は `config/game-eu11.toml` から生成する。 + +## 7. ROMが手元にある段階で行う次工程 + +### Gate A - extraction / static bank + +1. `prepare_mph.py` がEU1.1 identityをacceptする。 +2. ARM9を正しくdecompressする。 +3. ARM7を抽出する。 +4. overlay tableを列挙する。 +5. `generated/EU1_1/recomp/` にEU1.1専用bankを生成する。 +6. US1.0 artifactを一切参照していないことを確認する。 + +### Gate B - interpreter bootstrap + +EU1.1専用bankをexact SHA-1 gateでrunnerへ登録し、 + +- firmware boot +- cartridge handoff +- opening logos +- FMV +- title screen + +まで進むか確認する。 + +最初から高static coverageを要求しない。 +missはInterpreterへ落として正しさを優先する。 + +### Gate C - deterministic coverage + +EU1.1で実行したtraceから、 + +- immutable ARM9 main-image call target +- immutable ARM9 main-image indirect target +- ARM7 main-image target + +のみを抽出し、 + +```text +coverage/eu11-main-entry-points.json +``` + +へ昇格する。 + +US1.0のPCをアドレス変換して生成してはいけない。 + +### Gate D - FMV runtime bank + +EU1.1自身からITCM + main RAM captureを作り、 + +- capture SHA-1 +- live-byte validation +- observed call / indirect roots + +をEU1.1専用configへ固定する。 + +### Gate E - runtime semantic profile + +mphCodexを使い、 + +- morph state +- aim X/Y +- local player +- game mode +- HUD / camera state +- Scan Visor state +- Adventure camera state + +をEU1.1へ対応させる。 + +ここまで完了して初めてPrime Controls / Direct Mouse Aim / adaptive HUD等を +EU1.1に段階的に解禁する。 + +## 8. 完了条件 + +EU1.1を「supported」とする条件は最低でも以下。 + +- exact EU1.1 ROM identity gate +- EU1.1 main ARM9 bank +- EU1.1 ARM7 bank +- interpreter fallbackでterminal dispatch missなし +- title到達 +- Adventure gameplay到達 +- save/load確認 +- pause/reload確認 +- title patchがUS1.0 addressを参照しない +- Prime Controls動作確認 +- Direct Mouse Aim動作確認 +- native/reference checkpoint比較 +- US1.0 regressionなし + +現時点の実装は、このうちROM実体なしで先に安全に整備できる +**profile / extraction / bank isolation / bootstrap build infrastructure** +までを対象とする。 From 829de4d513cf191d5debea41713386c0b179bbaf Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:19:14 +0900 Subject: [PATCH 012/164] Preserve US1.0 AppImage naming --- tools/build-linux.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/build-linux.sh b/tools/build-linux.sh index 6fc7302..9b21fd2 100644 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -199,7 +199,11 @@ echo "[4/4] package AppImage" --desktop-file "$APPDIR/usr/share/applications/$APP_NAME.desktop" \ --icon-file "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" >/dev/null -APP="$OUT/$APP_NAME-$MPH_VERSION-linux-v$VERSION-x86_64.AppImage" +if [ "$MPH_VERSION" = "US1_0" ]; then + APP="$OUT/$APP_NAME-linux-v$VERSION-x86_64.AppImage" +else + APP="$OUT/$APP_NAME-$MPH_VERSION-linux-v$VERSION-x86_64.AppImage" +fi rm -f "$APP" ARCH=x86_64 "$APPIMAGETOOL_BIN" --appimage-extract-and-run "$APPDIR" "$APP" >/dev/null chmod +x "$APP" From 099fbbcd85c2925529ac8ef88921f59375abfd4a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:20:04 +0900 Subject: [PATCH 013/164] Fix Linux profile build script syntax From e0a0a8c2799886ab3d281105fc1d4a3571ba1463 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:44:30 +0900 Subject: [PATCH 014/164] Add revision-specific MPH runtime addresses --- config/mph_rom_profiles.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 442af6a..2f0c680 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -1,5 +1,6 @@ { - "schema": 1, + "schema": 2, + "runtime_address_source": "https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h", "profiles": { "US1_0": { "display_name": "Metroid Prime Hunters (USA rev 0)", @@ -11,7 +12,12 @@ "program_id": "mph_amhe0", "coverage": "coverage/adventure-main-entry-points.json", "game_config": "game.toml", - "fmv_runtime": true + "fmv_runtime": true, + "runtime": { + "morph_state": "0x020DA818", + "aim_x": "0x020DE526", + "aim_y": "0x020DE52E" + } }, "EU1_1": { "display_name": "Metroid Prime Hunters (Europe rev 1)", @@ -23,7 +29,12 @@ "program_id": "mph_amhp1", "coverage": "coverage/eu11-bootstrap-entry-points.json", "game_config": "config/game-eu11.toml", - "fmv_runtime": false + "fmv_runtime": false, + "runtime": { + "morph_state": "0x020DB138", + "aim_x": "0x020DEE46", + "aim_y": "0x020DEE4E" + } } } } From 772721f0379e701ad36f7a2ca8465169907d464b Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:45:19 +0900 Subject: [PATCH 015/164] Patch ndsrecomp runner for MPH multi-ROM runtime profiles --- tools/patch_ndsrecomp_mph_runtime.py | 262 +++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 tools/patch_ndsrecomp_mph_runtime.py diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py new file mode 100644 index 0000000..7659f01 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""Apply the MPH multi-ROM runtime-profile shim to the pinned ndsrecomp runner. + +The upstream runner currently hard-codes Metroid Prime Hunters USA rev-0 RAM +addresses for Prime Controls and direct mouse aim. This project supports more +than one retail revision, so those host-side hooks must be selected by exact +ROM SHA-1 instead of by title/address assumptions. + +The address values are generated from config/mph_rom_profiles.json. The +registry records melonPrimeDS's MelonPrimeGameRomAddrTable.h as the source of +truth for the MPH runtime addresses. + +The source patch is intentionally small, idempotent, and fail-closed. If the +pinned ndsrecomp source changes enough that the expected preimages are absent, +this script stops instead of guessing a patch against unknown code. +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +MAIN_RAM_MIN = 0x02000000 +MAIN_RAM_MAX = 0x023FFFFF + + +def parse_address(value: object, *, profile: str, field: str) -> int: + if not isinstance(value, str): + raise SystemExit(f"{profile}.{field}: expected a hex string") + try: + address = int(value, 0) + except ValueError as exc: + raise SystemExit(f"{profile}.{field}: invalid address {value!r}") from exc + if not MAIN_RAM_MIN <= address <= MAIN_RAM_MAX: + raise SystemExit( + f"{profile}.{field}: 0x{address:08X} is outside DS main RAM" + ) + return address + + +def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: + registry = json.loads(registry_path.read_text(encoding="utf-8")) + profiles = registry.get("profiles") + if not isinstance(profiles, dict) or not profiles: + raise SystemExit("ROM profile registry has no profiles") + + result: list[dict[str, object]] = [] + for key, profile in profiles.items(): + if not isinstance(profile, dict): + continue + runtime = profile.get("runtime") + if runtime is None: + continue + if not isinstance(runtime, dict): + raise SystemExit(f"{key}.runtime must be an object") + sha1 = profile.get("sha1") + if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): + raise SystemExit(f"{key}.sha1 must be 40 lowercase hex digits") + result.append( + { + "key": key, + "sha1": sha1, + "morph_state": parse_address( + runtime.get("morph_state"), profile=key, field="morph_state" + ), + "aim_x": parse_address( + runtime.get("aim_x"), profile=key, field="aim_x" + ), + "aim_y": parse_address( + runtime.get("aim_y"), profile=key, field="aim_y" + ), + } + ) + + if not result: + raise SystemExit("ROM profile registry contains no MPH runtime profiles") + return result + + +def generated_header(profiles: list[dict[str, object]]) -> str: + rows = [] + for profile in profiles: + rows.append( + " {\"%s\", 0x%08Xu, 0x%08Xu, 0x%08Xu}, // %s" + % ( + profile["sha1"], + profile["morph_state"], + profile["aim_x"], + profile["aim_y"], + profile["key"], + ) + ) + return """#pragma once + +#include +#include + +// Generated by MetroidPrimeHuntersRecomp/tools/patch_ndsrecomp_mph_runtime.py. +// Do not edit in the ndsrecomp checkout; edit config/mph_rom_profiles.json. +struct NdsMphRuntimeProfile { + const char* sha1; + uint32_t morph_state; + uint32_t aim_x; + uint32_t aim_y; +}; + +inline constexpr std::array kNdsMphRuntimeProfiles{{ +%s +}}; +""" % (len(rows), "\n".join(rows)) + + +def patch_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + if old not in text: + raise SystemExit( + f"Refusing to patch {path}: expected pinned ndsrecomp preimage " + f"for marker {marker!r} was not found" + ) + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def patch_runner(framework_root: Path, registry_path: Path) -> None: + runner_src = framework_root / "runner" / "src" + title_h = runner_src / "title_patches.h" + title_cpp = runner_src / "title_patches.cpp" + frontend_cpp = runner_src / "frontend.cpp" + main_cpp = runner_src / "main.cpp" + for path in (title_h, title_cpp, frontend_cpp, main_cpp): + if not path.is_file(): + raise SystemExit(f"Pinned ndsrecomp runner file not found: {path}") + + profiles = load_runtime_profiles(registry_path) + generated = runner_src / "mph_runtime_profiles.generated.h" + generated.write_text(generated_header(profiles), encoding="utf-8") + + patch_once( + title_h, + "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", + "// MPH_MULTIROM_RUNTIME_PROFILE: exact-ROM runtime profile selection.\n" + "bool nds_title_patches_select_mph_runtime_profile(const char* rom_sha1);\n" + "bool nds_title_patches_mph_in_ball();\n" + "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", + "MPH_MULTIROM_RUNTIME_PROFILE", + ) + + patch_once( + title_cpp, + '#include "title_patches.h"\n', + '#include "title_patches.h"\n' + '#include "mph_runtime_profiles.generated.h" // MPH_MULTIROM_PROFILE_HEADER\n', + "MPH_MULTIROM_PROFILE_HEADER", + ) + patch_once( + title_cpp, + "// AMHE0's native touch-look routine consumes these signed, per-frame fields.\n" + "// Feeding deltas here while holding the stylus at center preserves the game\n" + "// path but removes the finite physical touchscreen edge.\n" + "constexpr uint32_t kMphUs10AimX = 0x020DE526u;\n" + "constexpr uint32_t kMphUs10AimY = 0x020DE52Eu;\n", + "// MPH_MULTIROM_RUNTIME_PROFILE: selected only by exact cartridge SHA-1.\n" + "const NdsMphRuntimeProfile* g_mph_runtime_profile = nullptr;\n", + "selected only by exact cartridge SHA-1", + ) + patch_once( + title_cpp, + "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" + " g_mph_mouse_aim = enabled;\n" + "}\n\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" + " if (!g_mph_mouse_aim || (dx == 0 && dy == 0)) return false;\n" + " if (dx != 0)\n" + " bus_write_u32_slow(kMphUs10AimX, static_cast(dx));\n" + " if (dy != 0)\n" + " bus_write_u32_slow(kMphUs10AimY, static_cast(dy));\n" + " return true;\n" + "}\n", + "bool nds_title_patches_select_mph_runtime_profile(const char* rom_sha1) {\n" + " g_mph_mouse_aim = false;\n" + " g_mph_runtime_profile = nullptr;\n" + " if (!rom_sha1) return false;\n" + " for (const NdsMphRuntimeProfile& profile : kNdsMphRuntimeProfiles) {\n" + " if (std::strcmp(profile.sha1, rom_sha1) == 0) {\n" + " g_mph_runtime_profile = &profile;\n" + " return true;\n" + " }\n" + " }\n" + " return false;\n" + "}\n\n" + "bool nds_title_patches_mph_in_ball() {\n" + " return g_mph_runtime_profile &&\n" + " bus_read_u8_slow(g_mph_runtime_profile->morph_state) == 0x02u;\n" + "}\n\n" + "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" + " g_mph_mouse_aim = enabled && g_mph_runtime_profile;\n" + "}\n\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" + " if (!g_mph_mouse_aim || !g_mph_runtime_profile ||\n" + " (dx == 0 && dy == 0)) return false;\n" + " if (dx != 0)\n" + " bus_write_u32_slow(g_mph_runtime_profile->aim_x,\n" + " static_cast(dx));\n" + " if (dy != 0)\n" + " bus_write_u32_slow(g_mph_runtime_profile->aim_y,\n" + " static_cast(dy));\n" + " return true;\n" + "}\n", + "nds_title_patches_select_mph_runtime_profile", + ) + + patch_once( + frontend_cpp, + "constexpr uint32_t kMphUs10MorphState = 0x020DA818u;\n", + "// MPH_MULTIROM_RUNTIME_PROFILE: morph address is selected by ROM SHA-1.\n", + "morph address is selected by ROM SHA-1", + ) + patch_once( + frontend_cpp, + " const bool in_ball =\n" + " bus_read_u8_slow(kMphUs10MorphState) == 0x02u;\n", + " const bool in_ball = nds_title_patches_mph_in_ball();\n", + "nds_title_patches_mph_in_ball()", + ) + + patch_once( + main_cpp, + " mph_mouse_aim_policy =\n" + " rom_sha1 == \"90164d1ac127ee5f9815ea4ae7de798c7b5fc629\" &&\n" + " frontend_options.relative_mouse_touch;\n", + " // MPH_MULTIROM_RUNTIME_PROFILE: host hooks are enabled only when the\n" + " // exact ROM SHA-1 has a validated revision-specific address profile.\n" + " const bool mph_runtime_profile =\n" + " nds_title_patches_select_mph_runtime_profile(rom_sha1.c_str());\n" + " mph_mouse_aim_policy =\n" + " mph_runtime_profile && frontend_options.relative_mouse_touch;\n", + "host hooks are enabled only when the", + ) + + print( + f"Patched ndsrecomp MPH runtime profiles: " + + ", ".join(str(profile["key"]) for profile in profiles) + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--profiles", type=Path, required=True) + args = parser.parse_args() + patch_runner(args.framework_root.resolve(), args.profiles.resolve()) + + +if __name__ == "__main__": + main() From 140243a76cd81ddea27e9847f757d9db5c331d7f Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:46:27 +0900 Subject: [PATCH 016/164] Make MPH launcher ROM identity profile-driven --- launcher/recomp-ui/CMakeLists.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index f4fe4a7..ac1227a 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -9,11 +9,19 @@ find_package(SDL2 CONFIG REQUIRED) set(NDSRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../ndsrecomp" CACHE PATH "Path to the ndsrecomp framework checkout (for the shared SHA-1 helper)") +set(MPH_LAUNCHER_ROM_SHA1 + "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" CACHE STRING + "Exact retail ROM SHA-1 accepted by this profile-specific launcher") +set(MPH_LAUNCHER_REGION "USA" CACHE STRING + "Region label shown by the profile-specific launcher") add_executable(mph-recomp-ui launcher_main.cpp "${NDSRECOMP_ROOT}/recompiler/support/sha1.cpp") target_include_directories(mph-recomp-ui PRIVATE "${NDSRECOMP_ROOT}/recompiler/support") +target_compile_definitions(mph-recomp-ui PRIVATE + MPH_LAUNCHER_ROM_SHA1="${MPH_LAUNCHER_ROM_SHA1}" + MPH_LAUNCHER_REGION="${MPH_LAUNCHER_REGION}") target_link_libraries(mph-recomp-ui PRIVATE SDL2::SDL2) set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH @@ -32,7 +40,9 @@ target_include_directories(mph-mod-provider-test PRIVATE "${RECOMP_UI_ROOT}/src" "${RECOMP_UI_ROOT}/src/common") target_compile_definitions(mph-mod-provider-test PRIVATE - MPH_RECOMP_UI_NO_MAIN) + MPH_RECOMP_UI_NO_MAIN + MPH_LAUNCHER_ROM_SHA1="${MPH_LAUNCHER_ROM_SHA1}" + MPH_LAUNCHER_REGION="${MPH_LAUNCHER_REGION}") set(RECOMP_UI_SDL3 OFF) set(RECOMP_UI_ENABLE_MODS ON CACHE BOOL "" FORCE) From 1015ef86b72f0013de2899f80fc13f9e2fc36f8a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:47:04 +0900 Subject: [PATCH 017/164] Generate profile-specific launcher source at configure time --- launcher/recomp-ui/CMakeLists.txt | 35 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index ac1227a..8d53608 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -15,13 +15,34 @@ set(MPH_LAUNCHER_ROM_SHA1 set(MPH_LAUNCHER_REGION "USA" CACHE STRING "Region label shown by the profile-specific launcher") -add_executable(mph-recomp-ui launcher_main.cpp +# Keep launcher_main.cpp as the readable US1.0 baseline, but compile a +# profile-specific generated TU. This avoids duplicating the 40+ KiB launcher +# just to change cartridge identity metadata for another retail revision. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/launcher_main.cpp" MPH_LAUNCHER_SOURCE) +set(_mph_us10_sha1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629") +string(FIND "${MPH_LAUNCHER_SOURCE}" "${_mph_us10_sha1}" _mph_sha_pos) +if(_mph_sha_pos EQUAL -1) + message(FATAL_ERROR "launcher_main.cpp no longer contains the US1.0 SHA-1 baseline") +endif() +string(REPLACE "${_mph_us10_sha1}" "${MPH_LAUNCHER_ROM_SHA1}" + MPH_LAUNCHER_SOURCE "${MPH_LAUNCHER_SOURCE}") +set(_mph_region_line "game.region = \"USA\";") +string(FIND "${MPH_LAUNCHER_SOURCE}" "${_mph_region_line}" _mph_region_pos) +if(_mph_region_pos EQUAL -1) + message(FATAL_ERROR "launcher_main.cpp no longer contains the USA region baseline") +endif() +string(REPLACE "${_mph_region_line}" + "game.region = \"${MPH_LAUNCHER_REGION}\";" + MPH_LAUNCHER_SOURCE "${MPH_LAUNCHER_SOURCE}") +set(MPH_PROFILE_LAUNCHER_SOURCE + "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") +file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") + +add_executable(mph-recomp-ui "${MPH_PROFILE_LAUNCHER_SOURCE}" "${NDSRECOMP_ROOT}/recompiler/support/sha1.cpp") target_include_directories(mph-recomp-ui PRIVATE - "${NDSRECOMP_ROOT}/recompiler/support") -target_compile_definitions(mph-recomp-ui PRIVATE - MPH_LAUNCHER_ROM_SHA1="${MPH_LAUNCHER_ROM_SHA1}" - MPH_LAUNCHER_REGION="${MPH_LAUNCHER_REGION}") + "${NDSRECOMP_ROOT}/recompiler/support" + "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(mph-recomp-ui PRIVATE SDL2::SDL2) set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH @@ -40,9 +61,7 @@ target_include_directories(mph-mod-provider-test PRIVATE "${RECOMP_UI_ROOT}/src" "${RECOMP_UI_ROOT}/src/common") target_compile_definitions(mph-mod-provider-test PRIVATE - MPH_RECOMP_UI_NO_MAIN - MPH_LAUNCHER_ROM_SHA1="${MPH_LAUNCHER_ROM_SHA1}" - MPH_LAUNCHER_REGION="${MPH_LAUNCHER_REGION}") + MPH_RECOMP_UI_NO_MAIN) set(RECOMP_UI_SDL3 OFF) set(RECOMP_UI_ENABLE_MODS ON CACHE BOOL "" FORCE) From 7eb27d35e837ab7ba7aece7f918b65ad911fcde6 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:47:29 +0900 Subject: [PATCH 018/164] Package profile-specific MPH releases --- tools/make_release.ps1 | 49 ++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/tools/make_release.ps1 b/tools/make_release.ps1 index 84de180..3eb0ea8 100644 --- a/tools/make_release.ps1 +++ b/tools/make_release.ps1 @@ -1,22 +1,23 @@ <# Package a completed Metroid Prime Hunters Recomp Windows release. -The ZIP contains the portable recomp-ui launcher, the title runner with the -content-validated FMV runtime bank compiled in, launcher assets, game config, -documentation, and MinGW/SDL dependencies. ROMs, BIOS/firmware, saves, raw -captures, and generated source are never staged. +The ZIP contains the portable recomp-ui launcher, the title runner, launcher +assets, the selected revision's game config, documentation, and MinGW/SDL +dependencies. ROMs, BIOS/firmware, saves, raw captures, and generated source +are never staged. -Build the runner and launcher first, then run: - - powershell -File tools\make_release.ps1 -Version 0.1.0 ` - -RunnerBuildDir ..\ndsrecomp\runner\build-mph-release ` - -LauncherBuildDir launcher\recomp-ui\build-release +US1_0 keeps the historical release name and requires the validated FMV runtime +bank. Other revision profiles may opt out of that bank until a revision- +specific runtime capture has been produced. #> param( [Parameter(Mandatory = $true)][string]$Version, [string]$RunnerBuildDir = '..\ndsrecomp\runner\build-mph-release', [string]$LauncherBuildDir = 'launcher\recomp-ui\build-release', - [string]$RuntimeBinDir = 'C:\msys64\mingw64\bin' + [string]$RuntimeBinDir = 'C:\msys64\mingw64\bin', + [string]$GameConfig = 'game.toml', + [string]$Profile = 'US1_0', + [switch]$AllowNoFmvRuntime ) $ErrorActionPreference = 'Stop' @@ -26,18 +27,25 @@ $launcherBuild = [IO.Path]::GetFullPath((Join-Path $root $LauncherBuildDir)) $runner = Join-Path $runnerBuild 'nds_runner.exe' $launcher = Join-Path $launcherBuild 'mph-recomp-ui.exe' $assets = Join-Path $launcherBuild 'assets' +$gameConfigPath = if ([IO.Path]::IsPathRooted($GameConfig)) { + [IO.Path]::GetFullPath($GameConfig) +} else { + [IO.Path]::GetFullPath((Join-Path $root $GameConfig)) +} -foreach ($required in @($runner, $launcher, $assets)) { +foreach ($required in @($runner, $launcher, $assets, $gameConfigPath)) { if (-not (Test-Path -LiteralPath $required)) { throw "Release input missing: $required" } } -# A static-only runner is functional but drops the opening movies to roughly -# half speed. Refuse to package one by checking the compiled bank identity. -$runnerText = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($runner)) -if (-not $runnerText.Contains('mph_arm9_fmv_runtime')) { - throw 'Runner does not contain the MPH FMV runtime bank.' +if (-not $AllowNoFmvRuntime) { + # A US1.0 static-only runner is functional but drops the opening movies to + # roughly half speed. Keep the established release gate for that profile. + $runnerText = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($runner)) + if (-not $runnerText.Contains('mph_arm9_fmv_runtime')) { + throw 'Runner does not contain the MPH FMV runtime bank.' + } } $projectText = Get-Content (Join-Path $root 'CMakeLists.txt') -Raw @@ -47,7 +55,11 @@ if ($projectText -notmatch } $out = Join-Path $root 'release-stage' -$stageName = "MetroidPrimeHuntersRecomp-windows-x64-v$Version" +if ($Profile -eq 'US1_0') { + $stageName = "MetroidPrimeHuntersRecomp-windows-x64-v$Version" +} else { + $stageName = "MetroidPrimeHuntersRecomp-$Profile-windows-x64-v$Version" +} $stage = Join-Path $out $stageName $zip = Join-Path $out "$stageName.zip" $outFull = [IO.Path]::GetFullPath($out).TrimEnd('\') + '\' @@ -74,7 +86,8 @@ Copy-Item -LiteralPath $launcher ` -Destination (Join-Path $stage 'MetroidPrimeHuntersRecomp.exe') Copy-Item -LiteralPath $runner -Destination $stage Copy-Item -LiteralPath $assets -Destination $stage -Recurse -Copy-Item -LiteralPath (Join-Path $root 'game.toml') -Destination $stage +Copy-Item -LiteralPath $gameConfigPath ` + -Destination (Join-Path $stage 'game.toml') Copy-Item -LiteralPath (Join-Path $root 'README.md') -Destination $stage Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage Copy-Item -LiteralPath (Join-Path $root 'packaging\BIOS_README.txt') ` From be879f46a2b2ba20e4be0fda595de71d458d6bd3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:47:55 +0900 Subject: [PATCH 019/164] Build and package complete EU1.1 profile on Windows --- tools/build-windows.ps1 | 78 ++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index 731d788..0de94a2 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -1,24 +1,26 @@ <# Build Metroid Prime Hunters Recomp for one configured retail revision. -US1_0 keeps the existing release path, including the recomp-ui launcher and -portable ZIP. Other profiles currently build the title banks and runner only; -they are bring-up builds until their title-specific runtime hooks are validated. +US1_0 keeps the existing release paths. EU1_1 uses isolated generated banks, +a revision-specific game config, a profile-specific launcher identity, and the +shared exact-ROM runtime-address shim for Prime Controls/direct mouse aim. Usage: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` - tools\build-windows.ps1 -Version 0.3.0 -MphVersion EU1_1 + tools\build-windows.ps1 -Version 0.3.0 -MphVersion EU1_1 ` + -RomPath 'D:\ROMs\Metroid Prime Hunters (Europe) (Rev 1).nds' #> param( [string]$Version = '0.1.0', [ValidateSet('US1_0', 'EU1_1')] [string]$MphVersion = 'US1_0', + [string]$RomPath = '', [string]$CMake = 'C:\msys64\mingw64\bin\cmake.exe', [string]$Generator = 'Ninja', [int]$Jobs = 12, [string]$GameBuildDir = '', [string]$RunnerBuildDir = '', - [string]$LauncherBuildDir = 'launcher\recomp-ui\build-release', + [string]$LauncherBuildDir = '', [string]$RuntimeBinDir = 'C:\msys64\mingw64\bin', [string]$RecompUiRoot = 'F:\Projects\recomp-ui' ) @@ -38,6 +40,17 @@ if ($null -eq $profileProperty) { } $profile = $profileProperty.Value $romSha1 = [string]$profile.sha1 +$region = [string]$profile.region +$gameConfig = [IO.Path]::GetFullPath( + (Join-Path $root ([string]$profile.game_config))) + +if ([string]::IsNullOrWhiteSpace($RomPath)) { + $RomPath = Join-Path $root 'Metroid Prime Hunters.nds' +} +$romFull = [IO.Path]::GetFullPath($RomPath) +if (-not (Test-Path -LiteralPath $romFull)) { + throw "ROM not found: $romFull" +} if ([string]::IsNullOrWhiteSpace($GameBuildDir)) { if ($MphVersion -eq 'US1_0') { @@ -53,6 +66,13 @@ if ([string]::IsNullOrWhiteSpace($RunnerBuildDir)) { $RunnerBuildDir = "..\ndsrecomp\runner\build-mph-release-$MphVersion" } } +if ([string]::IsNullOrWhiteSpace($LauncherBuildDir)) { + if ($MphVersion -eq 'US1_0') { + $LauncherBuildDir = 'launcher\recomp-ui\build-release' + } else { + $LauncherBuildDir = "launcher\recomp-ui\build-release-$MphVersion" + } +} $frameworkRoot = [IO.Path]::GetFullPath((Join-Path $root '..\ndsrecomp')) $gameBuild = [IO.Path]::GetFullPath((Join-Path $root $GameBuildDir)) @@ -65,6 +85,11 @@ if ($MphVersion -eq 'US1_0') { (Join-Path $root "generated\$MphVersion\recomp")) } +$patchPython = Join-Path $root '.venv\Scripts\python.exe' +if (-not (Test-Path -LiteralPath $patchPython)) { + $patchPython = 'python' +} + Push-Location $root try { Write-Host "Building MPH profile $MphVersion ($($profile.game_code) rev $($profile.revision))" @@ -72,12 +97,17 @@ try { & $cmakePath -G $Generator -S $root -B $gameBuild ` -DCMAKE_BUILD_TYPE=Release ` -DNDSRECOMP_ROOT="$frameworkRoot" ` - -DMPH_VERSION="$MphVersion" + -DMPH_VERSION="$MphVersion" ` + -DMPH_ROM="$romFull" if ($LASTEXITCODE -ne 0) { throw 'Game CMake configure failed.' } & $cmakePath --build $gameBuild --target metroidprimehuntersrecomp -j $Jobs if ($LASTEXITCODE -ne 0) { throw 'Game bank build failed.' } + & $patchPython "$root\tools\patch_ndsrecomp_mph_runtime.py" ` + --framework-root "$frameworkRoot" --profiles "$profileFile" + if ($LASTEXITCODE -ne 0) { throw 'ndsrecomp MPH runtime-profile patch failed.' } + & $cmakePath -G $Generator -S "$frameworkRoot\runner" -B $runnerBuild ` -DCMAKE_BUILD_TYPE=Release ` -DNDS_BOOTSTRAP_FIRMWARE=ON ` @@ -88,32 +118,32 @@ try { & $cmakePath --build $runnerBuild -j $Jobs if ($LASTEXITCODE -ne 0) { throw 'Runner build failed.' } - if ($MphVersion -ne 'US1_0') { - $gameConfig = [IO.Path]::GetFullPath( - (Join-Path $root ([string]$profile.game_config))) - Write-Host '' - Write-Host "Bring-up runner built: $runnerBuild\nds_runner.exe" - Write-Host "Use the revision-specific config: $gameConfig" - Write-Host 'Launcher/release packaging intentionally skipped for this profile.' - Write-Host 'Prime Controls and direct mouse aim remain USA-only until revision addresses are validated.' - return - } - & $cmakePath -G $Generator -S "$root\launcher\recomp-ui" -B $launcherBuild ` -DCMAKE_BUILD_TYPE=Release ` + -DNDSRECOMP_ROOT="$frameworkRoot" ` -DRECOMP_UI_ROOT="$RecompUiRoot" ` - -DCMAKE_PREFIX_PATH="$RuntimeBinDir\..\lib\cmake" + -DCMAKE_PREFIX_PATH="$RuntimeBinDir\..\lib\cmake" ` + "-DMPH_LAUNCHER_ROM_SHA1=$romSha1" ` + "-DMPH_LAUNCHER_REGION=$region" if ($LASTEXITCODE -ne 0) { throw 'Launcher CMake configure failed.' } & $cmakePath --build $launcherBuild -j $Jobs if ($LASTEXITCODE -ne 0) { throw 'Launcher build failed.' } - & powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` - "$root\tools\make_release.ps1" ` - -Version $Version ` - -RunnerBuildDir $RunnerBuildDir ` - -LauncherBuildDir $LauncherBuildDir ` - -RuntimeBinDir $RuntimeBinDir + $releaseArgs = @( + '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', + "$root\tools\make_release.ps1", + '-Version', $Version, + '-RunnerBuildDir', $RunnerBuildDir, + '-LauncherBuildDir', $LauncherBuildDir, + '-RuntimeBinDir', $RuntimeBinDir, + '-GameConfig', $gameConfig, + '-Profile', $MphVersion + ) + if (-not [bool]$profile.fmv_runtime) { + $releaseArgs += '-AllowNoFmvRuntime' + } + & powershell.exe @releaseArgs if ($LASTEXITCODE -ne 0) { throw 'Release packaging failed.' } } finally { Pop-Location From 5793dea6848b5648a99b1c7193c40485a4e77898 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:48:20 +0900 Subject: [PATCH 020/164] Enable complete EU1.1 runtime profile on Linux --- tools/build-linux.sh | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tools/build-linux.sh b/tools/build-linux.sh index 9b21fd2..b46d0eb 100644 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # Build Metroid Prime Hunters Recomp for a configured retail revision. # -# US1_0 keeps the existing AppImage behavior. EU1_1 uses its own generated -# bank/config paths and deliberately omits the USA-only FMV-bank assertion. +# US1_0 keeps the existing AppImage naming. EU1_1 uses isolated generated +# banks, its own game config, and the exact-ROM runtime-address shim used by +# Prime Controls/direct mouse aim. set -euo pipefail APP_NAME="MetroidPrimeHuntersRecomp" @@ -10,6 +11,7 @@ TITLE_TARGET="metroidprimehuntersrecomp" RUNNER_NAME="nds_runner" VERSION="0.1.0" MPH_VERSION="US1_0" +ROM_PATH="" JOBS="$(nproc 2>/dev/null || echo 4)" DO_PACKAGE=1 @@ -22,11 +24,12 @@ while [ $# -gt 0 ]; do case "$1" in --version) VERSION="$2"; shift 2;; --mph-version) MPH_VERSION="$2"; shift 2;; + --rom) ROM_PATH="$2"; shift 2;; --jobs) JOBS="$2"; shift 2;; --out) OUT="$2"; shift 2;; --no-package) DO_PACKAGE=0; shift;; -h|--help) - sed -n '2,16p' "$0" + sed -n '2,18p' "$0" exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2;; @@ -55,6 +58,10 @@ ROM_SHA1="$(profile_value sha1)" GAME_CONFIG_REL="$(profile_value game_config)" FMV_RUNTIME="$(profile_value fmv_runtime)" GAME_CONFIG="$REPO/$GAME_CONFIG_REL" +if [ -z "$ROM_PATH" ]; then + ROM_PATH="$REPO/Metroid Prime Hunters.nds" +fi +ROM_PATH="$(python3 -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$ROM_PATH")" if [ "$MPH_VERSION" = "US1_0" ]; then GAME_BUILD="$REPO/build-linux-release" @@ -71,8 +78,8 @@ test -f "$FRAMEWORK_ROOT/recompiler/CMakeLists.txt" || { echo "ERROR: sibling ndsrecomp checkout is missing." >&2 exit 1 } -test -f "$REPO/Metroid Prime Hunters.nds" || { - echo "ERROR: verified Metroid Prime Hunters ROM is missing from the repo root." >&2 +test -f "$ROM_PATH" || { + echo "ERROR: Metroid Prime Hunters ROM is missing: $ROM_PATH" >&2 exit 1 } test -f "$GAME_CONFIG" || { @@ -80,15 +87,21 @@ test -f "$GAME_CONFIG" || { exit 1 } -echo "[1/4] configure title banks ($MPH_VERSION)" +echo "[1/5] configure title banks ($MPH_VERSION)" cmake -S "$REPO" -B "$GAME_BUILD" -G "Unix Makefiles" \ -DCMAKE_BUILD_TYPE=Release \ -DNDSRECOMP_ROOT="$FRAMEWORK_ROOT" \ - -DMPH_VERSION="$MPH_VERSION" -echo "[2/4] build title banks" + -DMPH_VERSION="$MPH_VERSION" \ + -DMPH_ROM="$ROM_PATH" +echo "[2/5] build title banks" cmake --build "$GAME_BUILD" --target "$TITLE_TARGET" -j"$JOBS" -echo "[3/4] configure runner" +echo "[3/5] install exact-ROM MPH runtime profile into pinned ndsrecomp runner" +python3 "$REPO/tools/patch_ndsrecomp_mph_runtime.py" \ + --framework-root "$FRAMEWORK_ROOT" \ + --profiles "$PROFILE_FILE" + +echo "[4/5] configure runner" cmake -S "$FRAMEWORK_ROOT/runner" -B "$RUNNER_BUILD" -G "Unix Makefiles" \ -DCMAKE_BUILD_TYPE=Release \ -DNDS_BOOTSTRAP_FIRMWARE=ON \ @@ -194,7 +207,7 @@ exec "$HERE/usr/bin/nds_runner" "$@" EOF chmod +x "$APPDIR/AppRun" "$APPDIR/usr/bin/$RUNNER_NAME" -echo "[4/4] package AppImage" +echo "[5/5] package AppImage" "$LINUXDEPLOY_BIN" --appimage-extract-and-run --appdir "$APPDIR" --executable "$APPDIR/usr/bin/$RUNNER_NAME" \ --desktop-file "$APPDIR/usr/share/applications/$APP_NAME.desktop" \ --icon-file "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" >/dev/null From c0ff50831feea2bbd8f368abe3105eec5fb07e05 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:51:28 +0900 Subject: [PATCH 021/164] Add static multi-ROM profile validator --- tools/check_mph_multirom_profiles.py | 225 +++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 tools/check_mph_multirom_profiles.py diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py new file mode 100644 index 0000000..3cea998 --- /dev/null +++ b/tools/check_mph_multirom_profiles.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Static consistency checks for Metroid Prime Hunters ROM profiles. + +This intentionally does not need a copyrighted ROM. It verifies that each +profile's identity, coverage seed, game config, and host-side runtime addresses +agree. When --melonprime-table is provided, Aim/Morph addresses are also +cross-checked against melonPrimeDS's MelonPrimeGameRomAddrTable.h, which is the +source of truth for those fields. +""" + +from __future__ import annotations + +import argparse +import json +import re +import tomllib +from pathlib import Path + + +SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +HEX_RE = re.compile(r"0x[0-9A-Fa-f]+u?") +REQUIRED_RUNTIME_FIELDS = { + "morph_state": "baseIsAltForm", + "aim_x": "baseAimX", + "aim_y": "baseAimY", +} + + +def die(message: str) -> "NoReturn": + raise SystemExit(f"ERROR: {message}") + + +def parse_hex(value: object, where: str) -> int: + if not isinstance(value, str): + die(f"{where} must be a hex string") + try: + return int(value, 0) + except ValueError: + die(f"{where} has invalid hex value {value!r}") + + +def load_json(path: Path) -> object: + with path.open("r", encoding="utf-8") as f: + return json.load(f) + + +def parse_melonprime_table(path: Path) -> dict[str, dict[str, int]]: + text = path.read_text(encoding="utf-8") + enum_match = re.search( + r"enum\s+class\s+RomGroup\s*:\s*int\s*\{([^}]*)\}", text, re.S + ) + if not enum_match: + die(f"could not parse RomGroup from {path}") + groups: list[str] = [] + for raw in enum_match.group(1).split(","): + token = raw.strip().split("=")[0].strip() + if token and token != "COUNT": + groups.append(token) + if not groups: + die(f"RomGroup has no revisions in {path}") + + wanted = set(REQUIRED_RUNTIME_FIELDS.values()) + fields: dict[str, list[int]] = {} + row_re = re.compile( + r"X\(ADDR,\s*([A-Za-z0-9_]+),\s*[A-Za-z0-9_]+,\s*([^)]*)\)" + ) + for match in row_re.finditer(text): + field = match.group(1) + if field not in wanted: + continue + values = [ + int(token.rstrip("uU"), 16) + for token in HEX_RE.findall(match.group(2)) + ] + if len(values) != len(groups): + die( + f"{field} in {path} has {len(values)} values; " + f"RomGroup has {len(groups)} revisions" + ) + fields[field] = values + + missing = wanted - fields.keys() + if missing: + die(f"missing melonPrimeDS fields: {', '.join(sorted(missing))}") + + result: dict[str, dict[str, int]] = {} + for index, group in enumerate(groups): + result[group] = {field: values[index] for field, values in fields.items()} + return result + + +def validate_registry(repo: Path, table: Path | None) -> None: + registry_path = repo / "config" / "mph_rom_profiles.json" + registry = load_json(registry_path) + if not isinstance(registry, dict): + die("ROM profile registry must be a JSON object") + if registry.get("schema") != 2: + die("ROM profile registry schema must be 2") + + expected_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/main/" + "src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h" + ) + if registry.get("runtime_address_source") != expected_source: + die("runtime_address_source is not the approved melonPrimeDS address table") + + profiles = registry.get("profiles") + if not isinstance(profiles, dict) or not profiles: + die("ROM profile registry has no profiles") + + melon = parse_melonprime_table(table) if table else None + seen_sha1: set[str] = set() + seen_identity: set[tuple[str, int]] = set() + + for key, profile in profiles.items(): + if not isinstance(profile, dict): + die(f"profile {key} must be an object") + sha1 = profile.get("sha1") + game_code = profile.get("game_code") + revision = profile.get("revision") + if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): + die(f"{key}.sha1 must be 40 lowercase hex digits") + if sha1 in seen_sha1: + die(f"duplicate SHA-1 in profile registry: {sha1}") + seen_sha1.add(sha1) + if not isinstance(game_code, str) or len(game_code) != 4: + die(f"{key}.game_code must be four characters") + if not isinstance(revision, int) or not 0 <= revision <= 255: + die(f"{key}.revision must be an unsigned byte") + identity = (game_code, revision) + if identity in seen_identity: + die(f"duplicate cartridge identity: {game_code} rev {revision}") + seen_identity.add(identity) + + runtime = profile.get("runtime") + if not isinstance(runtime, dict): + die(f"{key}.runtime is required") + parsed_runtime: dict[str, int] = {} + for profile_field in REQUIRED_RUNTIME_FIELDS: + value = parse_hex(runtime.get(profile_field), f"{key}.runtime.{profile_field}") + if not 0x02000000 <= value <= 0x023FFFFF: + die(f"{key}.runtime.{profile_field} is outside DS main RAM") + parsed_runtime[profile_field] = value + + if melon is not None: + if key not in melon: + die(f"{key} is not present in melonPrimeDS RomGroup") + for profile_field, melon_field in REQUIRED_RUNTIME_FIELDS.items(): + actual = parsed_runtime[profile_field] + expected = melon[key][melon_field] + if actual != expected: + die( + f"{key}.{profile_field}=0x{actual:08X}, but " + f"melonPrimeDS {melon_field}=0x{expected:08X}" + ) + + coverage_path = repo / str(profile.get("coverage", "")) + if not coverage_path.is_file(): + die(f"{key}.coverage does not exist: {coverage_path}") + coverage = load_json(coverage_path) + if not isinstance(coverage, dict) or coverage.get("game_sha1") != sha1: + die(f"{key}.coverage game_sha1 does not match profile SHA-1") + + game_config_path = repo / str(profile.get("game_config", "")) + if not game_config_path.is_file(): + die(f"{key}.game_config does not exist: {game_config_path}") + with game_config_path.open("rb") as f: + game_config = tomllib.load(f) + game = game_config.get("game") + if not isinstance(game, dict): + die(f"{key}.game_config has no [game] table") + expected_config = { + "id": game_code, + "revision": revision, + "rom_size": profile.get("rom_size"), + "sha1": sha1, + } + for field, expected in expected_config.items(): + if game.get(field) != expected: + die( + f"{key}.game_config game.{field}={game.get(field)!r}; " + f"expected {expected!r}" + ) + + # Explicit regression guard for the first non-US revision. These are the + # values currently published by melonPrimeDS's source-of-truth table. + eu = profiles.get("EU1_1") + if not isinstance(eu, dict): + die("EU1_1 profile is required") + if eu.get("game_code") != "AMHP" or eu.get("revision") != 1: + die("EU1_1 must remain AMHP revision 1") + if eu.get("sha1") != "bdcd1dea293e24c98d4c481430e90d21198985a5": + die("EU1_1 SHA-1 changed unexpectedly") + eu_runtime = eu.get("runtime", {}) + expected_eu_runtime = { + "morph_state": "0x020DB138", + "aim_x": "0x020DEE46", + "aim_y": "0x020DEE4E", + } + if eu_runtime != expected_eu_runtime: + die(f"EU1_1 runtime profile changed unexpectedly: {eu_runtime!r}") + if eu.get("fmv_runtime") is not False: + die("EU1_1 must not reuse the US1.0 FMV runtime capture") + + print(f"OK: validated {len(profiles)} MPH ROM profiles") + if melon is not None: + print(f"OK: Aim/Morph addresses match melonPrimeDS table: {table}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--repo", type=Path, default=Path(__file__).resolve().parents[1] + ) + parser.add_argument( + "--melonprime-table", + type=Path, + help="Downloaded MelonPrimeGameRomAddrTable.h to cross-check", + ) + args = parser.parse_args() + validate_registry(args.repo.resolve(), args.melonprime_table) + + +if __name__ == "__main__": + main() From b9e153d2c12f0d3b42a313b55d4d2866c010ef65 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:51:47 +0900 Subject: [PATCH 022/164] Add multi-ROM static verification workflow --- .github/workflows/mph-multirom-static.yml | 104 ++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/workflows/mph-multirom-static.yml diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml new file mode 100644 index 0000000..8775ccf --- /dev/null +++ b/.github/workflows/mph-multirom-static.yml @@ -0,0 +1,104 @@ +name: MPH Multi-ROM Static Checks + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + static-multirom: + runs-on: ubuntu-latest + steps: + - name: Checkout project + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check Python syntax + run: | + python -m py_compile \ + tools/prepare_mph.py \ + tools/check_mph_multirom_profiles.py \ + tools/patch_ndsrecomp_mph_runtime.py + + - name: Check shell syntax + run: bash -n tools/build-linux.sh + + - name: Check PowerShell syntax + shell: pwsh + run: | + $failed = $false + foreach ($path in @('tools/build-windows.ps1', 'tools/make_release.ps1')) { + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path $path), [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -ne 0) { + $failed = $true + Write-Error "$path has PowerShell parser errors:`n$($errors | Out-String)" + } + } + if ($failed) { exit 1 } + + - name: Cross-check runtime addresses against melonPrimeDS + run: | + curl -fsSL --retry 3 \ + https://raw.githubusercontent.com/ag-advania/melonPrimeDS/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h \ + -o /tmp/MelonPrimeGameRomAddrTable.h + python tools/check_mph_multirom_profiles.py \ + --melonprime-table /tmp/MelonPrimeGameRomAddrTable.h + + - name: Fetch exact pinned ndsrecomp revision + run: | + pin="$(tr -d '\r\n' < ndsrecomp.pin)" + test "${#pin}" -eq 40 + git init /tmp/ndsrecomp + git -C /tmp/ndsrecomp remote add origin https://github.com/mstan/ndsrecomp.git + git -C /tmp/ndsrecomp fetch --depth 1 origin "$pin" + git -C /tmp/ndsrecomp checkout --detach FETCH_HEAD + test "$(git -C /tmp/ndsrecomp rev-parse HEAD)" = "$pin" + + - name: Verify ndsrecomp runtime patch is idempotent + run: | + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root /tmp/ndsrecomp \ + --profiles config/mph_rom_profiles.json + sha256sum \ + /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h \ + /tmp/ndsrecomp/runner/src/title_patches.h \ + /tmp/ndsrecomp/runner/src/title_patches.cpp \ + /tmp/ndsrecomp/runner/src/frontend.cpp \ + /tmp/ndsrecomp/runner/src/main.cpp \ + > /tmp/first.sha256 + + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root /tmp/ndsrecomp \ + --profiles config/mph_rom_profiles.json + sha256sum \ + /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h \ + /tmp/ndsrecomp/runner/src/title_patches.h \ + /tmp/ndsrecomp/runner/src/title_patches.cpp \ + /tmp/ndsrecomp/runner/src/frontend.cpp \ + /tmp/ndsrecomp/runner/src/main.cpp \ + > /tmp/second.sha256 + diff -u /tmp/first.sha256 /tmp/second.sha256 + + grep -q '0x020DB138u, 0x020DEE46u, 0x020DEE4Eu' \ + /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h + grep -q 'bdcd1dea293e24c98d4c481430e90d21198985a5' \ + /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h + grep -q 'nds_title_patches_select_mph_runtime_profile' \ + /tmp/ndsrecomp/runner/src/main.cpp + grep -q 'nds_title_patches_mph_in_ball' \ + /tmp/ndsrecomp/runner/src/frontend.cpp + ! grep -q 'kMphUs10MorphState' /tmp/ndsrecomp/runner/src/frontend.cpp + ! grep -q 'kMphUs10AimX' /tmp/ndsrecomp/runner/src/title_patches.cpp + ! grep -q 'kMphUs10AimY' /tmp/ndsrecomp/runner/src/title_patches.cpp + + - name: Diff sanity + run: git diff --check From 4015956d13eab90c6192cb7cf35617114c997b7b Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:52:57 +0900 Subject: [PATCH 023/164] Compile both ROM checker profiles in static CI --- .github/workflows/mph-multirom-static.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index 8775ccf..f28600f 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -100,5 +100,22 @@ jobs: ! grep -q 'kMphUs10AimX' /tmp/ndsrecomp/runner/src/title_patches.cpp ! grep -q 'kMphUs10AimY' /tmp/ndsrecomp/runner/src/title_patches.cpp + - name: Compile US1.0 and EU1.1 ROM checkers + run: | + for profile in US1_0 EU1_1; do + build="/tmp/mph-romcheck-$profile" + cmake -S . -B "$build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ + -DMPH_VERSION="$profile" \ + -DMPH_ROM="/tmp/nonexistent-$profile.nds" + cmake --build "$build" --target mph_romcheck -j2 + test -x "$build/MetroidPrimeHuntersRecomp" + done + strings /tmp/mph-romcheck-EU1_1/MetroidPrimeHuntersRecomp | \ + grep -q bdcd1dea293e24c98d4c481430e90d21198985a5 + strings /tmp/mph-romcheck-EU1_1/MetroidPrimeHuntersRecomp | \ + grep -q EU1_1 + - name: Diff sanity run: git diff --check From e7c2fe48ff95b13d767965a246cf2888011cc30d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:53:43 +0900 Subject: [PATCH 024/164] Update EU1.1 bring-up documentation to current implementation --- docs/EU1_1_BRINGUP.md | 413 ++++++++++++++++++++++++++++-------------- 1 file changed, 272 insertions(+), 141 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 817c7c1..643eece 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -1,22 +1,28 @@ # Metroid Prime Hunters Recomp - EU1.1 Bring-up 作成日: 2026-08-17 +更新日: 2026-08-17 ## 1. 目的 -`MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定から段階的に +`MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定から multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) を安全にbring-upする。 -この段階は「EU1.1を正式対応済みにする」ものではない。 -まず以下を成立させる。 +現在はROM実体なしで実装・検証できる範囲をさらに進め、次まで完了している。 1. ROM identityを版別profileとして管理する。 -2. EU1.1 ROMからARM9 / ARM7 / ARM9 overlayを直接抽出できる。 -3. EU1.1専用bankをUS1.0生成物から分離して生成できる。 +2. EU1.1 ROMからARM9 / ARM7 / ARM9 overlayを直接抽出するprofile-driven prepare経路を持つ。 +3. EU1.1専用bankをUS1.0生成物から分離する。 4. US1.0のcoverage seedやFMV runtime captureをEU1.1へ流用しない。 -5. exact ROM SHA-1でrunner側のbank登録をgateする。 -6. EU1.1固有の未解析箇所はInterpreter fallbackへ安全に落とす。 +5. exact ROM SHA-1でrunner側のbankとhost-side runtime address profileをgateする。 +6. Prime ControlsのMorph判定とDirect Mouse AimをEU1.1固有RAMアドレスへ対応させる。 +7. EU1.1専用ROM identityを持つlauncherを同一launcher sourceから生成する。 +8. Windows/Linux build入口をprofile-awareにする。 +9. ROM不要のstatic CIでprofile整合性とpinned ndsrecomp patchを検証する。 + +未完了なのは、EU1.1実ROMと実行環境を必要とするruntime validation、 +EU1.1固有coverageの拡張、EU1.1固有FMV runtime captureである。 ## 2. EU1.1 identity @@ -28,18 +34,16 @@ multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revi | ROM size | `0x04000000` / 64 MiB | | SHA-1 | `bdcd1dea293e24c98d4c481430e90d21198985a5` | | Program ID prefix | `mph_amhp1` | +| Game config | `config/game-eu11.toml` | +| FMV runtime bank | disabled until EU1.1 capture exists | identityは `config/mph_rom_profiles.json` に集約する。 -## 3. 今回追加するbootstrap構造 - -### 3.1 Profile registry +## 3. ROM profile registry -`config/mph_rom_profiles.json` +`config/mph_rom_profiles.json` は現在schema 2で、US1.0とEU1.1を同じ構造で管理する。 -US1.0とEU1.1を同一schemaで管理する。 - -Profileには最低限、 +Profileには次を持たせる。 - `game_code` - `revision` @@ -49,10 +53,92 @@ Profileには最低限、 - `coverage` - `game_config` - `fmv_runtime` +- `runtime.morph_state` +- `runtime.aim_x` +- `runtime.aim_y` + +host-side runtime addressのsource of truthは以下に固定する。 + +```text +https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h +``` + +AimアドレスはmphCodexから推測せず、このmelonPrimeDS address tableを正として扱う。 + +## 4. melonPrimeDSから確定したEU1.1 runtime addresses + +`MelonPrimeGameRomAddrTable.h` の `RomGroup` は次の順序である。 + +```text +JP1_0, JP1_1, US1_0, US1_1, EU1_0, EU1_1, KR1_0 +``` + +Recomp runnerで現在必要なフィールドは以下である。 + +| Semantic | melonPrimeDS field | US1.0 | EU1.1 | +|---|---|---:|---:| +| Morph / Alt Form state | `baseIsAltForm` | `0x020DA818` | `0x020DB138` | +| Direct Aim X | `baseAimX` | `0x020DE526` | `0x020DEE46` | +| Direct Aim Y | `baseAimY` | `0x020DE52E` | `0x020DEE4E` | + +これらは `config/mph_rom_profiles.json` の `runtime` に登録済みである。 + +## 5. pinned ndsrecompのUS1.0固定を除去する方法 + +pinned framework revision: + +```text +46b12e6c18dea47f87d2c1f98c3054149dcbca5d +``` + +このrevisionのrunnerには元々、 + +```text +frontend.cpp: + kMphUs10MorphState = 0x020DA818 + +title_patches.cpp: + kMphUs10AimX = 0x020DE526 + kMphUs10AimY = 0x020DE52E + +main.cpp: + Prime Controls policy = exact US1.0 SHA-1 only +``` + +というUS1.0固定が存在する。 + +プロジェクト側で `tools/patch_ndsrecomp_mph_runtime.py` を追加し、build前に +pinned `ndsrecomp` checkoutへ小さなprofile-selection shimを適用する。 + +パッチ後は概念的に次の経路になる。 + +```text +ROM SHA-1 + -> NdsMphRuntimeProfile選択 + -> morph_state + -> aim_x + -> aim_y + -> exact profileがある場合のみPrime Controls / Direct Mouse Aimを許可 + -> 未登録SHA-1ではhost hookを無効化 +``` + +生成されるframework側header: + +```text +runner/src/mph_runtime_profiles.generated.h +``` + +このheaderは直接編集せず、`config/mph_rom_profiles.json` から生成する。 + +パッチャーは以下の性質を持つ。 -を持たせる。 +- exact pinned source preimageを要求する。 +- upstream sourceが想定外に変わった場合はguessせず失敗する。 +- 同じcheckoutへ複数回適用しても結果が変わらない。 +- 未知ROMはfail-closedになる。 +- runtime addressはDS main RAM範囲内か検証する。 -### 3.2 EU1.1 coverage seed +## 6. EU1.1 coverage bootstrap `coverage/eu11-bootstrap-entry-points.json` @@ -60,14 +146,16 @@ Profileには最低限、 これは意図的である。 -`prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、 -EU1.1はまずそのrootからstatic discoveryを行い、未コンパイル領域は -runtime Interpreterへfallbackする。 +`prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、EU1.1は +まずそのrootからstatic discoveryを行い、未コンパイル領域はruntime Interpreterへ +fallbackする。 -US1.0の `coverage/adventure-main-entry-points.json` に入っているアドレスを -EU1.1へそのままコピーしてはいけない。 +US1.0の `coverage/adventure-main-entry-points.json` に入っているabsolute PCを +EU1.1へコピーしてはいけない。 -### 3.3 Generated tree separation +EU1.1自身を実行したtraceからのみEU1.1 coverageを拡張する。 + +## 7. Generated tree separation US1.0は後方互換性のため従来通り: @@ -91,202 +179,232 @@ generated/ これにより異なるROM由来のbankやbinaryが同じパスへ混ざらない。 -## 4. mphCodexで確認済みのEU1.1差分例 +## 8. Launcher identity separation -AllVersions調査ではEU1.1に次の対応が確認されている。 +launcherの大きなsourceをROM revisionごとに複製しない。 -| Semantic | US1.0 | EU1.1 | -|---|---:|---:| -| Current Camera Sequence | `0x020D9CB0` | `0x020DA5D0` | -| Game Mode | `0x020E78FC` | `0x020E845C` | -| Upper HUD function | `0x0202F600` | `0x0202F5E0` | -| Crosshair callsite | `0x0202F934` | `0x0202F904` | -| Crosshair renderer | `0x020393D4` | `0x02039338` | -| Local Player Pointer | `0x020BCA70` | `0x020BD370` | -| HUD suppression storage | `0x020DE748` | `0x020DF068` | +`launcher/recomp-ui/CMakeLists.txt` はconfigure時にbaseline `launcher_main.cpp` を読み、 +選択profileの -重要なのは、差分が単一の固定deltaではないことである。 +- ROM SHA-1 +- Region -したがって今後のtitle patch / enhancementは、 +だけを反映したgenerated translation unitをbuild directoryへ作る。 ```text -US1.0 address + region offset +launcher_main.cpp + -> configure-time identity transform + -> launcher_main_profile.cpp + -> mph-recomp-ui ``` -ではなく、 +US1.0は従来SHA-1を保持し、EU1.1 buildでは ```text -ROM identity -> semantic runtime profile -> exact address +bdcd1dea293e24c98d4c481430e90d21198985a5 +Europe ``` -で管理する。 +を持つlauncherになる。 -## 5. 現時点でEU1.1へ有効化しない機能 +これによりEU1.1 ROMをUS1.0 launcherへ誤認させず、同一UI実装を共有できる。 -### 5.1 Prime Controls / Morph state +## 9. FMV runtime bank -pinned `ndsrecomp` runnerのPrime ControlsにはUS1.0専用の +US1.0の ```text -0x020DA818 +config/mph_arm9_fmv_runtime.toml +generated/capture/mph_arm9_fmv_runtime.bin ``` -が直接使われている。 +はUS1.0 runtime bytesとobserved PCsに対するbankである。 -EU1.1側の同semanticアドレスを確認するまでは無効のままにする。 - -### 5.2 Direct Mouse Aim - -`runner/src/title_patches.cpp` のDirect Mouse AimにはUS1.0専用の +EU1.1 profileは: ```text -Aim X: 0x020DE526 -Aim Y: 0x020DE52E +fmv_runtime = false ``` -が使われている。 +とし、このbankを絶対に登録しない。 -これもEU1.1 mapping完了前には有効化しない。 +EU1.1でopening FMV等がInterpreter fallbackでは遅い場合でも、US1.0 captureを +流用してはいけない。EU1.1自身からITCM + main RAM captureを取得し、live-byte +validation付きのEU1.1専用runtime bankを作る。 -### 5.3 USA FMV runtime bank +## 10. Build -US1.0の +### 10.1 Windows -```text -config/mph_arm9_fmv_runtime.toml -generated/capture/mph_arm9_fmv_runtime.bin +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` + tools\build-windows.ps1 ` + -Version 0.3.0 ` + -MphVersion EU1_1 ` + -RomPath 'D:\ROMs\Metroid Prime Hunters (Europe) (Rev 1).nds' ``` -はUS1.0のruntime bytesとobserved PCsに対するbankである。 - -EU1.1では `fmv_runtime=false` とし、絶対に登録しない。 +現在のWindows buildはEU1.1についても以下まで一貫して行う。 -EU1.1のFMV高速化はEU1.1実行から独立captureを作成した後に行う。 +1. EU1.1 ROM identity verify +2. EU1.1 ARM9/ARM7 extraction +3. EU1.1 static bank generation +4. pinned ndsrecomp runtime-profile patch +5. exact EU1.1 SHA-1 runner build +6. EU1.1 identity launcher build +7. `config/game-eu11.toml` をrelease内 `game.toml` としてpackage -### 5.4 Launcherのsupported-ROM list +EU1.1にはFMV runtime captureがまだないため、そのbankの存在はrelease gateにしない。 -recomp-ui launcherのsupported SHA-1配列にはまだEU1.1を追加しない。 +### 10.2 Linux -理由は、launcherがPrime Controls等を通常のplayer-facing featureとして有効化する -経路を持つためである。 +runnerまで: -EU1.1の基本bootとruntime profileが検証されるまで「正式対応」と表示しない。 +```bash +tools/build-linux.sh \ + --mph-version EU1_1 \ + --rom '/path/to/Metroid Prime Hunters (Europe) (Rev 1).nds' \ + --no-package +``` -## 6. Build +AppImageまで: -### Windows bring-up +```bash +tools/build-linux.sh \ + --mph-version EU1_1 \ + --rom '/path/to/Metroid Prime Hunters (Europe) (Rev 1).nds' +``` -EU1.1 ROMをrepo rootの +EU1.1 package名には `EU1_1` suffixを付け、US1.0のhistorical filenameとは分離する。 -```text -Metroid Prime Hunters.nds -``` +## 11. ROM不要static CI -として配置した上で: +`.github/workflows/mph-multirom-static.yml` -```powershell -powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` - tools\build-windows.ps1 ` - -MphVersion EU1_1 -``` +PRごとに以下を検証する。 -EU1.1ではtitle bankとrunnerまでをbuildし、launcher/release packagingは -意図的にskipする。 +1. `prepare_mph.py` / profile checker / framework patcherのPython syntax +2. Linux build scriptのshell syntax +3. Windows build/release scriptのPowerShell syntax +4. melonPrimeDS `main` の `MelonPrimeGameRomAddrTable.h` を取得 +5. `baseIsAltForm` / `baseAimX` / `baseAimY` をprofileと自動照合 +6. exact `ndsrecomp.pin` revisionを取得 +7. runtime-profile patchを適用 +8. 同じpatchを2回適用し、対象ファイルhashが完全一致することを確認 +9. US1.0固定Aim/Morph symbolが除去されていることを確認 +10. US1.0/EU1.1それぞれの`mph_romcheck`をcompile +11. EU1.1 checkerにEU1.1 SHA-1/profile keyが埋め込まれていることを確認 +12. `git diff --check` -revision-specific runtime config: +このCIはROMを一切取得・保存しない。 -```text -config/game-eu11.toml -``` +## 12. mphCodexの役割 -### Linux bring-up +Aim X/YについてはmelonPrimeDS address tableをsource of truthとする。 -```bash -tools/build-linux.sh --mph-version EU1_1 --no-package -``` +mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic stateを読む必要が +出た場合のcross-version調査に利用できる。 -AppImage生成まで行う場合: +例: -```bash -tools/build-linux.sh --mph-version EU1_1 -``` +| Semantic | US1.0 | EU1.1 | +|---|---:|---:| +| Current Camera Sequence | `0x020D9CB0` | `0x020DA5D0` | +| Game Mode | `0x020E78FC` | `0x020E845C` | +| Upper HUD function | `0x0202F600` | `0x0202F5E0` | +| Crosshair callsite | `0x0202F934` | `0x0202F904` | +| Crosshair renderer | `0x020393D4` | `0x02039338` | +| Local Player Pointer | `0x020BCA70` | `0x020BD370` | +| HUD suppression storage | `0x020DE748` | `0x020DF068` | -package内の `game.toml` は `config/game-eu11.toml` から生成する。 +この表からも、US1.0 -> EU1.1を単一deltaで変換できないことが分かる。 -## 7. ROMが手元にある段階で行う次工程 +## 13. 実ROMで残るvalidation gates -### Gate A - extraction / static bank +### Gate A - extraction / bank generation -1. `prepare_mph.py` がEU1.1 identityをacceptする。 -2. ARM9を正しくdecompressする。 -3. ARM7を抽出する。 -4. overlay tableを列挙する。 -5. `generated/EU1_1/recomp/` にEU1.1専用bankを生成する。 -6. US1.0 artifactを一切参照していないことを確認する。 +EU1.1実ROMで次を確認する。 -### Gate B - interpreter bootstrap +- `prepare_mph.py` がEU1.1 identityをaccept +- ARM9 decompress成功 +- ARM7 extraction成功 +- ARM9 overlay table列挙成功 +- `generated/EU1_1/recomp/` にEU1.1専用bank生成 +- US1.0 artifactが混入していない -EU1.1専用bankをexact SHA-1 gateでrunnerへ登録し、 +### Gate B - boot / interpreter bootstrap - firmware boot - cartridge handoff - opening logos -- FMV +- opening FMV - title screen +- attract loop -まで進むか確認する。 +static missはInterpreterへfallbackさせ、最初はcorrectnessを優先する。 -最初から高static coverageを要求しない。 -missはInterpreterへ落として正しさを優先する。 +### Gate C - gameplay -### Gate C - deterministic coverage +最低限: -EU1.1で実行したtraceから、 +- Adventure file作成/読込 +- Celestial Archives landing +- first-person gameplay +- movement +- aim +- shoot +- Morph Ball +- Scan Visor +- pause +- save/reload +- multiplayer menu -- immutable ARM9 main-image call target -- immutable ARM9 main-image indirect target -- ARM7 main-image target +### Gate D - Prime Controls -のみを抽出し、 +EU1.1 selected profileで以下を実機能確認する。 ```text -coverage/eu11-main-entry-points.json +Morph state = 0x020DB138 +Aim X = 0x020DEE46 +Aim Y = 0x020DEE4E ``` -へ昇格する。 +確認項目: -US1.0のPCをアドレス変換して生成してはいけない。 +- normal formでcenter touch保持 +- Morph Ball時にcenter touchを解除 +- mouse X/Y deltaがEU1.1 fieldsへ書かれる +- US1.0 fieldsへ書かれない +- menu/touch操作へ戻れる +- keyboard/gamepad Prime Controls -### Gate D - FMV runtime bank +### Gate E - EU1.1 deterministic coverage -EU1.1自身からITCM + main RAM captureを作り、 +EU1.1 execution traceから -- capture SHA-1 -- live-byte validation -- observed call / indirect roots +- immutable ARM9 main-image call target +- immutable ARM9 main-image indirect target +- ARM7 main-image target -をEU1.1専用configへ固定する。 +のみを抽出し、EU1.1専用coverageへ昇格する。 -### Gate E - runtime semantic profile +US1.0 absolute PCのaddress translationは行わない。 -mphCodexを使い、 +### Gate F - EU1.1 FMV runtime optimization -- morph state -- aim X/Y -- local player -- game mode -- HUD / camera state -- Scan Visor state -- Adventure camera state +必要な場合だけEU1.1自身からcaptureを作る。 -をEU1.1へ対応させる。 +- capture SHA-1 +- live-byte validation +- observed call targets +- observed indirect targets +- performance comparison -ここまで完了して初めてPrime Controls / Direct Mouse Aim / adaptive HUD等を -EU1.1に段階的に解禁する。 +を固定してからEU1.1 profileの `fmv_runtime` をtrueへ変更する。 -## 8. 完了条件 +## 14. Supported判定 -EU1.1を「supported」とする条件は最低でも以下。 +EU1.1をruntime検証済みsupportedと宣言する条件: - exact EU1.1 ROM identity gate - EU1.1 main ARM9 bank @@ -296,12 +414,25 @@ EU1.1を「supported」とする条件は最低でも以下。 - Adventure gameplay到達 - save/load確認 - pause/reload確認 -- title patchがUS1.0 addressを参照しない +- host-side title patchがUS1.0 addressをEU1.1へ使用しない - Prime Controls動作確認 - Direct Mouse Aim動作確認 - native/reference checkpoint比較 - US1.0 regressionなし -現時点の実装は、このうちROM実体なしで先に安全に整備できる -**profile / extraction / bank isolation / bootstrap build infrastructure** -までを対象とする。 +## 15. 現在の判定 + +### Code / infrastructure + +**READY FOR EU1.1 ROM VALIDATION** + +ROMなしで可能なidentity/profile、extraction routing、bank isolation、runtime address +selection、launcher identity、Windows/Linux packaging、static CIまで実装済み。 + +### Runtime correctness + +**NOT YET CLAIMED** + +EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。 +このvalidationを通すまでは、コードがEU1.1を受理できることと、ゲーム動作が完全に +検証済みであることを混同しない。 From 8a1c6dc242789c8d0e0bd14afa9ebf252d8f757d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 10:55:26 +0900 Subject: [PATCH 025/164] Compile patched runner in multi-ROM CI --- .github/workflows/mph-multirom-static.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index f28600f..5311594 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -100,6 +100,16 @@ jobs: ! grep -q 'kMphUs10AimX' /tmp/ndsrecomp/runner/src/title_patches.cpp ! grep -q 'kMphUs10AimY' /tmp/ndsrecomp/runner/src/title_patches.cpp + - name: Compile patched headless runner + run: | + cmake -S /tmp/ndsrecomp/runner -B /tmp/nds-runner-profile-check \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDS_BOOTSTRAP_FIRMWARE=ON \ + -DNDS_ENABLE_COMPUTE_RENDERER=OFF \ + -DNDS_ENABLE_PCAP_BACKEND=OFF + cmake --build /tmp/nds-runner-profile-check --target nds_runner -j2 + test -x /tmp/nds-runner-profile-check/nds_runner + - name: Compile US1.0 and EU1.1 ROM checkers run: | for profile in US1_0 EU1_1; do From fca2b01117453f647bc59942879dc6d04a8e2715 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:01:43 +0900 Subject: [PATCH 026/164] Compile patched runner sources without private BIOS artifacts --- .github/workflows/mph-multirom-static.yml | 25 ++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index 5311594..f350fb6 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -100,15 +100,34 @@ jobs: ! grep -q 'kMphUs10AimX' /tmp/ndsrecomp/runner/src/title_patches.cpp ! grep -q 'kMphUs10AimY' /tmp/ndsrecomp/runner/src/title_patches.cpp - - name: Compile patched headless runner + - name: Compile patched runner translation units run: | + # The pinned runner CMake expects locally generated BIOS-bank C files, + # which are deliberately absent from a clean source checkout. Empty + # placeholders are sufficient for CMake generation because this job + # compiles only the three modified runner C++ translation units and + # never links or executes the placeholder banks. + mkdir -p /tmp/ndsrecomp/generated + for source in \ + arm9_bios.c arm9_bios_dispatch.c \ + arm7_bios.c arm7_bios_dispatch.c \ + freebios_arm9.c freebios_arm9_dispatch.c \ + freebios_arm7.c freebios_arm7_dispatch.c; do + : > "/tmp/ndsrecomp/generated/$source" + done + cmake -S /tmp/ndsrecomp/runner -B /tmp/nds-runner-profile-check \ -DCMAKE_BUILD_TYPE=Release \ -DNDS_BOOTSTRAP_FIRMWARE=ON \ -DNDS_ENABLE_COMPUTE_RENDERER=OFF \ -DNDS_ENABLE_PCAP_BACKEND=OFF - cmake --build /tmp/nds-runner-profile-check --target nds_runner -j2 - test -x /tmp/nds-runner-profile-check/nds_runner + + cmake --build /tmp/nds-runner-profile-check --target \ + CMakeFiles/nds_runner.dir/src/title_patches.cpp.o -j2 + cmake --build /tmp/nds-runner-profile-check --target \ + CMakeFiles/nds_runner.dir/src/frontend.cpp.o -j2 + cmake --build /tmp/nds-runner-profile-check --target \ + CMakeFiles/nds_runner.dir/src/main.cpp.o -j2 - name: Compile US1.0 and EU1.1 ROM checkers run: | From 535d2a022b943c89508a08feba41ab15a54a15fd Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:02:59 +0900 Subject: [PATCH 027/164] Use public Makefile object targets for runner source checks --- .github/workflows/mph-multirom-static.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index f350fb6..54f4e77 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -123,11 +123,11 @@ jobs: -DNDS_ENABLE_PCAP_BACKEND=OFF cmake --build /tmp/nds-runner-profile-check --target \ - CMakeFiles/nds_runner.dir/src/title_patches.cpp.o -j2 + src/title_patches.o -j2 cmake --build /tmp/nds-runner-profile-check --target \ - CMakeFiles/nds_runner.dir/src/frontend.cpp.o -j2 + src/frontend.o -j2 cmake --build /tmp/nds-runner-profile-check --target \ - CMakeFiles/nds_runner.dir/src/main.cpp.o -j2 + src/main.o -j2 - name: Compile US1.0 and EU1.1 ROM checkers run: | From 683c23cb468c8d5bb8a268357cdd57eddb209c6e Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:04:36 +0900 Subject: [PATCH 028/164] Test exact-ROM MPH runtime address dispatch --- tools/tests/mph_runtime_profile_test.cpp | 136 +++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tools/tests/mph_runtime_profile_test.cpp diff --git a/tools/tests/mph_runtime_profile_test.cpp b/tools/tests/mph_runtime_profile_test.cpp new file mode 100644 index 0000000..bb94478 --- /dev/null +++ b/tools/tests/mph_runtime_profile_test.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include + +#include "state.h" +#include "title_patches.h" + +namespace { + +struct Write32 { + uint32_t addr; + uint32_t value; +}; + +std::vector g_writes; +uint32_t g_morph_addr = 0; +uint8_t g_morph_value = 0; +int g_failures = 0; + +void expect(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", message); + ++g_failures; +} + +void expect_write(std::size_t index, uint32_t addr, uint32_t value, + const char* message) { + const bool ok = index < g_writes.size() && + g_writes[index].addr == addr && + g_writes[index].value == value; + expect(ok, message); +} + +} // namespace + +// title_patches.cpp only needs these bus surfaces. Keeping the test at the +// ABI boundary lets it link the real patched translation unit without a ROM, +// BIOS dump, or generated recomp bank. +bool bus_get_region(const char*, BusRegion*) { + return false; +} + +extern "C" uint8_t bus_read_u8_slow(uint32_t addr) { + return addr == g_morph_addr ? g_morph_value : 0u; +} + +extern "C" void bus_write_u32_slow(uint32_t addr, uint32_t value) { + g_writes.push_back({addr, value}); +} + +int main() { + constexpr const char* kUs10Sha1 = + "90164d1ac127ee5f9815ea4ae7de798c7b5fc629"; + constexpr const char* kEu11Sha1 = + "bdcd1dea293e24c98d4c481430e90d21198985a5"; + + // Unknown or absent identities must fail closed. Enabling direct aim after + // a failed selection must not create guest-memory writes. + expect(!nds_title_patches_select_mph_runtime_profile(nullptr), + "null ROM identity must not select a runtime profile"); + expect(!nds_title_patches_select_mph_runtime_profile( + "0000000000000000000000000000000000000000"), + "unknown ROM identity must not select a runtime profile"); + nds_title_patches_set_mph_mouse_aim(true); + expect(!nds_title_patches_apply_mph_mouse_delta(1, 1), + "unknown ROM must not accept direct mouse aim"); + expect(g_writes.empty(), "unknown ROM must not write guest aim fields"); + expect(!nds_title_patches_mph_in_ball(), + "unknown ROM must not read a guessed morph address"); + + // Baseline regression: US1.0 must keep the exact addresses that were + // hard-coded before multi-ROM support. + expect(nds_title_patches_select_mph_runtime_profile(kUs10Sha1), + "US1.0 profile must be selectable"); + nds_title_patches_set_mph_mouse_aim(true); + g_writes.clear(); + expect(nds_title_patches_apply_mph_mouse_delta(11, -7), + "US1.0 direct mouse aim must accept a non-zero delta"); + expect(g_writes.size() == 2, "US1.0 aim must perform two writes"); + expect_write(0, 0x020DE526u, 11u, + "US1.0 X delta must target baseAimX"); + expect_write(1, 0x020DE52Eu, static_cast(-7), + "US1.0 Y delta must target baseAimY"); + g_morph_addr = 0x020DA818u; + g_morph_value = 0x02u; + expect(nds_title_patches_mph_in_ball(), + "US1.0 baseIsAltForm=2 must report Morph Ball"); + + // EU1.1 addresses come from melonPrimeDS + // MelonPrimeGameRomAddrTable.h, not from an inferred relocation delta. + expect(nds_title_patches_select_mph_runtime_profile(kEu11Sha1), + "EU1.1 profile must be selectable"); + + // Selection deliberately clears the prior direct-aim enable. This prevents + // state from one cartridge identity leaking into another profile. + g_writes.clear(); + expect(!nds_title_patches_apply_mph_mouse_delta(3, 4), + "profile switch must clear direct-aim enable state"); + expect(g_writes.empty(), "disabled aim after profile switch must not write"); + + nds_title_patches_set_mph_mouse_aim(true); + expect(nds_title_patches_apply_mph_mouse_delta(3, -4), + "EU1.1 direct mouse aim must accept a non-zero delta"); + expect(g_writes.size() == 2, "EU1.1 aim must perform two writes"); + expect_write(0, 0x020DEE46u, 3u, + "EU1.1 X delta must target melonPrimeDS baseAimX"); + expect_write(1, 0x020DEE4Eu, static_cast(-4), + "EU1.1 Y delta must target melonPrimeDS baseAimY"); + + g_morph_addr = 0x020DB138u; + g_morph_value = 0x02u; + expect(nds_title_patches_mph_in_ball(), + "EU1.1 baseIsAltForm=2 must report Morph Ball"); + g_morph_value = 0x00u; + expect(!nds_title_patches_mph_in_ball(), + "EU1.1 non-alt form must not report Morph Ball"); + + // A final invalid identity clears the active profile and prevents stale + // EU1.1 addresses from remaining live. + expect(!nds_title_patches_select_mph_runtime_profile("bad"), + "invalid final identity must clear the runtime profile"); + g_writes.clear(); + nds_title_patches_set_mph_mouse_aim(true); + expect(!nds_title_patches_apply_mph_mouse_delta(9, 9), + "cleared profile must reject mouse aim"); + expect(g_writes.empty(), "cleared profile must not retain EU1.1 writes"); + + if (g_failures != 0) { + std::fprintf(stderr, "%d runtime-profile assertion(s) failed\n", + g_failures); + return 1; + } + std::puts("OK: exact-ROM MPH runtime profiles dispatch US1.0/EU1.1 safely"); + return 0; +} From ea08c515c4d49d3153c09fd716d4424b8045ef39 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:15:07 +0900 Subject: [PATCH 029/164] Run exact-ROM MPH runtime dispatch test in CI --- .github/workflows/mph-multirom-static.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index 54f4e77..add43e5 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -129,6 +129,16 @@ jobs: cmake --build /tmp/nds-runner-profile-check --target \ src/main.o -j2 + - name: Test exact-ROM runtime address dispatch + run: | + c++ -std=c++20 -Wall -Wextra -Wno-unused-parameter \ + -I/tmp/ndsrecomp/runner/src \ + -I/tmp/ndsrecomp/recompiler/armv4t \ + tools/tests/mph_runtime_profile_test.cpp \ + /tmp/ndsrecomp/runner/src/title_patches.cpp \ + -o /tmp/mph-runtime-profile-test + /tmp/mph-runtime-profile-test + - name: Compile US1.0 and EU1.1 ROM checkers run: | for profile in US1_0 EU1_1; do From d4ee43e4ee06853e8c74e48e6a7477365fdeee8e Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:19:05 +0900 Subject: [PATCH 030/164] Gate launcher enhancements per ROM profile --- config/mph_rom_profiles.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 2f0c680..70d2bd8 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -13,6 +13,8 @@ "coverage": "coverage/adventure-main-entry-points.json", "game_config": "game.toml", "fmv_runtime": true, + "launcher_default_rom": "Metroid Prime Hunters.nds", + "adaptive_widescreen": true, "runtime": { "morph_state": "0x020DA818", "aim_x": "0x020DE526", @@ -30,6 +32,8 @@ "coverage": "coverage/eu11-bootstrap-entry-points.json", "game_config": "config/game-eu11.toml", "fmv_runtime": false, + "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds", + "adaptive_widescreen": false, "runtime": { "morph_state": "0x020DB138", "aim_x": "0x020DEE46", From b1ae13516c1611874196f1137d16d4281a9d8fcc Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:19:33 +0900 Subject: [PATCH 031/164] Validate launcher policy for each ROM profile --- tools/check_mph_multirom_profiles.py | 44 ++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index 3cea998..7bfcd3c 100644 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -2,10 +2,10 @@ """Static consistency checks for Metroid Prime Hunters ROM profiles. This intentionally does not need a copyrighted ROM. It verifies that each -profile's identity, coverage seed, game config, and host-side runtime addresses -agree. When --melonprime-table is provided, Aim/Morph addresses are also -cross-checked against melonPrimeDS's MelonPrimeGameRomAddrTable.h, which is the -source of truth for those fields. +profile's identity, coverage seed, game config, launcher policy, and host-side +runtime addresses agree. When --melonprime-table is provided, Aim/Morph +addresses are also cross-checked against melonPrimeDS's +MelonPrimeGameRomAddrTable.h, which is the source of truth for those fields. """ from __future__ import annotations @@ -15,6 +15,7 @@ import re import tomllib from pathlib import Path +from typing import NoReturn SHA1_RE = re.compile(r"^[0-9a-f]{40}$") @@ -26,7 +27,7 @@ } -def die(message: str) -> "NoReturn": +def die(message: str) -> NoReturn: raise SystemExit(f"ERROR: {message}") @@ -132,6 +133,19 @@ def validate_registry(repo: Path, table: Path | None) -> None: die(f"duplicate cartridge identity: {game_code} rev {revision}") seen_identity.add(identity) + launcher_default_rom = profile.get("launcher_default_rom") + if ( + not isinstance(launcher_default_rom, str) + or not launcher_default_rom + or not launcher_default_rom.lower().endswith(".nds") + ): + die(f"{key}.launcher_default_rom must be a non-empty .nds filename") + if Path(launcher_default_rom).name != launcher_default_rom: + die(f"{key}.launcher_default_rom must be a filename, not a path") + adaptive_widescreen = profile.get("adaptive_widescreen") + if not isinstance(adaptive_widescreen, bool): + die(f"{key}.adaptive_widescreen must be boolean") + runtime = profile.get("runtime") if not isinstance(runtime, dict): die(f"{key}.runtime is required") @@ -182,6 +196,22 @@ def validate_registry(repo: Path, table: Path | None) -> None: f"expected {expected!r}" ) + display = game_config.get("display", {}) + if not isinstance(display, dict): + die(f"{key}.game_config [display] must be a table") + config_adaptive = display.get("adaptive_widescreen") + if adaptive_widescreen: + if config_adaptive != "top": + die( + f"{key} enables adaptive_widescreen in the profile but " + "game config does not enable top-screen adaptive widescreen" + ) + elif config_adaptive not in (None, "none"): + die( + f"{key} disables adaptive_widescreen in the profile but " + f"game config enables {config_adaptive!r}" + ) + # Explicit regression guard for the first non-US revision. These are the # values currently published by melonPrimeDS's source-of-truth table. eu = profiles.get("EU1_1") @@ -201,6 +231,10 @@ def validate_registry(repo: Path, table: Path | None) -> None: die(f"EU1_1 runtime profile changed unexpectedly: {eu_runtime!r}") if eu.get("fmv_runtime") is not False: die("EU1_1 must not reuse the US1.0 FMV runtime capture") + if eu.get("adaptive_widescreen") is not False: + die("EU1_1 adaptive widescreen must remain disabled until validated") + if eu.get("launcher_default_rom") != "Metroid Prime Hunters (Europe Rev 1).nds": + die("EU1_1 launcher default ROM filename changed unexpectedly") print(f"OK: validated {len(profiles)} MPH ROM profiles") if melon is not None: From 0c2a14383895380135feaa7084d59b4e4f7e6773 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:20:03 +0900 Subject: [PATCH 032/164] Gate launcher widescreen by ROM profile --- launcher/recomp-ui/CMakeLists.txt | 77 +++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 8d53608..e23e429 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -14,26 +14,73 @@ set(MPH_LAUNCHER_ROM_SHA1 "Exact retail ROM SHA-1 accepted by this profile-specific launcher") set(MPH_LAUNCHER_REGION "USA" CACHE STRING "Region label shown by the profile-specific launcher") +set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING + "Default ROM filename offered by this profile-specific launcher") +option(MPH_LAUNCHER_ADAPTIVE_WIDESCREEN + "Expose and allow the title-specific adaptive widescreen enhancement" ON) # Keep launcher_main.cpp as the readable US1.0 baseline, but compile a -# profile-specific generated TU. This avoids duplicating the 40+ KiB launcher -# just to change cartridge identity metadata for another retail revision. +# profile-specific generated TU. Every replacement below is preimage-guarded: +# if the baseline launcher changes, configure fails instead of silently +# applying a stale multi-ROM transformation. file(READ "${CMAKE_CURRENT_SOURCE_DIR}/launcher_main.cpp" MPH_LAUNCHER_SOURCE) -set(_mph_us10_sha1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629") -string(FIND "${MPH_LAUNCHER_SOURCE}" "${_mph_us10_sha1}" _mph_sha_pos) -if(_mph_sha_pos EQUAL -1) - message(FATAL_ERROR "launcher_main.cpp no longer contains the US1.0 SHA-1 baseline") -endif() -string(REPLACE "${_mph_us10_sha1}" "${MPH_LAUNCHER_ROM_SHA1}" - MPH_LAUNCHER_SOURCE "${MPH_LAUNCHER_SOURCE}") -set(_mph_region_line "game.region = \"USA\";") -string(FIND "${MPH_LAUNCHER_SOURCE}" "${_mph_region_line}" _mph_region_pos) -if(_mph_region_pos EQUAL -1) - message(FATAL_ERROR "launcher_main.cpp no longer contains the USA region baseline") + +function(mph_launcher_replace_required old new description) + string(FIND "${MPH_LAUNCHER_SOURCE}" "${old}" _mph_replace_pos) + if(_mph_replace_pos EQUAL -1) + message(FATAL_ERROR + "launcher_main.cpp no longer contains ${description}") + endif() + string(REPLACE "${old}" "${new}" + _mph_replaced_source "${MPH_LAUNCHER_SOURCE}") + set(MPH_LAUNCHER_SOURCE "${_mph_replaced_source}" PARENT_SCOPE) +endfunction() + +if(MPH_LAUNCHER_ADAPTIVE_WIDESCREEN) + set(_mph_adaptive_literal "true") + set(_mph_mod_count "2") +else() + set(_mph_adaptive_literal "false") + set(_mph_mod_count "1") endif() -string(REPLACE "${_mph_region_line}" + +mph_launcher_replace_required( + "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" + "${MPH_LAUNCHER_ROM_SHA1}" + "the US1.0 SHA-1 baseline") +mph_launcher_replace_required( + "game.region = \"USA\";" "game.region = \"${MPH_LAUNCHER_REGION}\";" - MPH_LAUNCHER_SOURCE "${MPH_LAUNCHER_SOURCE}") + "the USA region baseline") +mph_launcher_replace_required( + "exe / \"Metroid Prime Hunters.nds\";" + "exe / \"${MPH_LAUNCHER_DEFAULT_ROM}\";" + "the default MPH ROM filename") +mph_launcher_replace_required( + "bool adaptive_widescreen = true;" + "bool adaptive_widescreen = ${_mph_adaptive_literal};" + "the adaptive-widescreen default") +mph_launcher_replace_required( + "int mod_feature_count(void*) {\n return 2;\n}" + "int mod_feature_count(void*) {\n return ${_mph_mod_count};\n}" + "the two-feature mod count") +mph_launcher_replace_required( + "if (!context || !output || index < 0 || index > 1) return 0;\n const auto* state = static_cast(context);" + "if (!context || !output || index < 0 || index >= ${_mph_mod_count}) return 0;\n if (!${_mph_adaptive_literal}) ++index;\n const auto* state = static_cast(context);" + "the mod feature index guard") +mph_launcher_replace_required( + "if (std::strcmp(package_id, \"mph-adaptive-widescreen\") == 0 &&\n std::strcmp(feature_id, \"adaptive-widescreen\") == 0) {\n state->adaptive_widescreen = enabled != 0;\n return 1;\n }" + "if (std::strcmp(package_id, \"mph-adaptive-widescreen\") == 0 &&\n std::strcmp(feature_id, \"adaptive-widescreen\") == 0) {\n if (!${_mph_adaptive_literal}) {\n state->adaptive_widescreen = false;\n return 0;\n }\n state->adaptive_widescreen = enabled != 0;\n return 1;\n }" + "the adaptive-widescreen enable handler") +mph_launcher_replace_required( + "load_mod_state(mod_state);\n RecompLauncherCModProvider mod_provider = make_mod_provider(&mod_state);" + "load_mod_state(mod_state);\n if (!${_mph_adaptive_literal}) mod_state.adaptive_widescreen = false;\n RecompLauncherCModProvider mod_provider = make_mod_provider(&mod_state);" + "the post-load mod-state initialization") +mph_launcher_replace_required( + "const std::wstring rom_wide = widen(rom);\n if (rom_wide.empty()) return false;" + "const std::wstring rom_wide = widen(rom);\n if (rom_wide.empty()) return false;\n adaptive = adaptive && ${_mph_adaptive_literal};" + "the runner launch profile gate") + set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") From 58b5e2ef763cf1a758bed17f343bea14cfb3ced8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:20:25 +0900 Subject: [PATCH 033/164] Pass profile launcher policy to Windows build --- tools/build-windows.ps1 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index 0de94a2..d9b7c0e 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -2,8 +2,9 @@ Build Metroid Prime Hunters Recomp for one configured retail revision. US1_0 keeps the existing release paths. EU1_1 uses isolated generated banks, -a revision-specific game config, a profile-specific launcher identity, and the -shared exact-ROM runtime-address shim for Prime Controls/direct mouse aim. +a revision-specific game config, a profile-specific launcher identity/policy, +and the shared exact-ROM runtime-address shim for Prime Controls/direct mouse +aim. Usage: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` @@ -41,11 +42,13 @@ if ($null -eq $profileProperty) { $profile = $profileProperty.Value $romSha1 = [string]$profile.sha1 $region = [string]$profile.region +$launcherDefaultRom = [string]$profile.launcher_default_rom +$launcherAdaptive = if ([bool]$profile.adaptive_widescreen) { 'ON' } else { 'OFF' } $gameConfig = [IO.Path]::GetFullPath( (Join-Path $root ([string]$profile.game_config))) if ([string]::IsNullOrWhiteSpace($RomPath)) { - $RomPath = Join-Path $root 'Metroid Prime Hunters.nds' + $RomPath = Join-Path $root $launcherDefaultRom } $romFull = [IO.Path]::GetFullPath($RomPath) if (-not (Test-Path -LiteralPath $romFull)) { @@ -93,6 +96,7 @@ if (-not (Test-Path -LiteralPath $patchPython)) { Push-Location $root try { Write-Host "Building MPH profile $MphVersion ($($profile.game_code) rev $($profile.revision))" + Write-Host "Launcher adaptive widescreen: $launcherAdaptive" & $cmakePath -G $Generator -S $root -B $gameBuild ` -DCMAKE_BUILD_TYPE=Release ` @@ -124,7 +128,9 @@ try { -DRECOMP_UI_ROOT="$RecompUiRoot" ` -DCMAKE_PREFIX_PATH="$RuntimeBinDir\..\lib\cmake" ` "-DMPH_LAUNCHER_ROM_SHA1=$romSha1" ` - "-DMPH_LAUNCHER_REGION=$region" + "-DMPH_LAUNCHER_REGION=$region" ` + "-DMPH_LAUNCHER_DEFAULT_ROM=$launcherDefaultRom" ` + "-DMPH_LAUNCHER_ADAPTIVE_WIDESCREEN=$launcherAdaptive" if ($LASTEXITCODE -ne 0) { throw 'Launcher CMake configure failed.' } & $cmakePath --build $launcherBuild -j $Jobs From 35cd9c012e6e7b4857c42d8bfd94ff083aed54fa Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:21:11 +0900 Subject: [PATCH 034/164] Verify profile-specific launcher generation in CI --- .github/workflows/mph-multirom-static.yml | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index add43e5..fdfc070 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -63,6 +63,51 @@ jobs: git -C /tmp/ndsrecomp checkout --detach FETCH_HEAD test "$(git -C /tmp/ndsrecomp rev-parse HEAD)" = "$pin" + - name: Render US1.0 and EU1.1 launcher profiles + run: | + mkdir -p /tmp/fake-prefix/lib/cmake/SDL2 /tmp/fake-recomp-ui + cat > /tmp/fake-prefix/lib/cmake/SDL2/SDL2Config.cmake <<'EOF' + if(NOT TARGET SDL2::SDL2) + add_library(SDL2::SDL2 INTERFACE IMPORTED) + endif() + EOF + cat > /tmp/fake-recomp-ui/recomp_ui.cmake <<'EOF' + function(recomp_target_launcher_ui) + endfunction() + EOF + + cmake -S launcher/recomp-ui -B /tmp/mph-launcher-us10 \ + -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ + -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ + -DCMAKE_PREFIX_PATH=/tmp/fake-prefix + + cmake -S launcher/recomp-ui -B /tmp/mph-launcher-eu11 \ + -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ + -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ + -DCMAKE_PREFIX_PATH=/tmp/fake-prefix \ + -DMPH_LAUNCHER_ROM_SHA1=bdcd1dea293e24c98d4c481430e90d21198985a5 \ + -DMPH_LAUNCHER_REGION=Europe \ + '-DMPH_LAUNCHER_DEFAULT_ROM=Metroid Prime Hunters (Europe Rev 1).nds' \ + -DMPH_LAUNCHER_ADAPTIVE_WIDESCREEN=OFF + + us=/tmp/mph-launcher-us10/launcher_main_profile.cpp + eu=/tmp/mph-launcher-eu11/launcher_main_profile.cpp + test -f "$us" -a -f "$eu" + + grep -q '90164d1ac127ee5f9815ea4ae7de798c7b5fc629' "$us" + grep -q 'game.region = "USA";' "$us" + grep -q 'bool adaptive_widescreen = true;' "$us" + grep -A1 'int mod_feature_count' "$us" | grep -q 'return 2;' + grep -q 'adaptive = adaptive && true;' "$us" + + grep -q 'bdcd1dea293e24c98d4c481430e90d21198985a5' "$eu" + grep -q 'game.region = "Europe";' "$eu" + grep -q 'exe / "Metroid Prime Hunters (Europe Rev 1).nds";' "$eu" + grep -q 'bool adaptive_widescreen = false;' "$eu" + grep -A1 'int mod_feature_count' "$eu" | grep -q 'return 1;' + grep -q 'if (!false) ++index;' "$eu" + grep -q 'adaptive = adaptive && false;' "$eu" + - name: Verify ndsrecomp runtime patch is idempotent run: | python tools/patch_ndsrecomp_mph_runtime.py \ From fecfcbe491f377d1d26ed3dc1ba9307045b2c4f5 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:23:53 +0900 Subject: [PATCH 035/164] Update EU1.1 bring-up status and launcher safety gates --- docs/EU1_1_BRINGUP.md | 192 +++++++++++++++++++++++++++++------------- 1 file changed, 134 insertions(+), 58 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 643eece..0d5b5b7 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -9,7 +9,7 @@ multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) を安全にbring-upする。 -現在はROM実体なしで実装・検証できる範囲をさらに進め、次まで完了している。 +現在はROM実体なしで実装・検証できる範囲を進め、次まで完了している。 1. ROM identityを版別profileとして管理する。 2. EU1.1 ROMからARM9 / ARM7 / ARM9 overlayを直接抽出するprofile-driven prepare経路を持つ。 @@ -17,12 +17,14 @@ multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revi 4. US1.0のcoverage seedやFMV runtime captureをEU1.1へ流用しない。 5. exact ROM SHA-1でrunner側のbankとhost-side runtime address profileをgateする。 6. Prime ControlsのMorph判定とDirect Mouse AimをEU1.1固有RAMアドレスへ対応させる。 -7. EU1.1専用ROM identityを持つlauncherを同一launcher sourceから生成する。 -8. Windows/Linux build入口をprofile-awareにする。 -9. ROM不要のstatic CIでprofile整合性とpinned ndsrecomp patchを検証する。 +7. EU1.1専用ROM identity / default ROM filename / feature policyを持つlauncherを同一launcher sourceから生成する。 +8. EU1.1では未検証のAdaptive Widescreenをlauncher UIから隠し、persisted stateとlaunch commandの両方でも強制OFFにする。 +9. Windows/Linux build入口をprofile-awareにする。 +10. ROM不要CIでmelonPrimeDS address tableとの照合、pinned ndsrecomp patch、runner C++ compile、exact-ROM runtime dispatchを検証する。 +11. ROM不要CIでUS1.0/EU1.1のlauncher generated sourceを生成し、identityとfeature policyを直接検証する。 未完了なのは、EU1.1実ROMと実行環境を必要とするruntime validation、 -EU1.1固有coverageの拡張、EU1.1固有FMV runtime captureである。 +EU1.1固有coverageの拡張、必要に応じたEU1.1固有FMV runtime captureである。 ## 2. EU1.1 identity @@ -35,9 +37,11 @@ EU1.1固有coverageの拡張、EU1.1固有FMV runtime captureである。 | SHA-1 | `bdcd1dea293e24c98d4c481430e90d21198985a5` | | Program ID prefix | `mph_amhp1` | | Game config | `config/game-eu11.toml` | +| Launcher default ROM | `Metroid Prime Hunters (Europe Rev 1).nds` | +| Adaptive Widescreen | disabled until EU1.1 validation | | FMV runtime bank | disabled until EU1.1 capture exists | -identityは `config/mph_rom_profiles.json` に集約する。 +identityと版別policyは `config/mph_rom_profiles.json` に集約する。 ## 3. ROM profile registry @@ -53,6 +57,8 @@ Profileには次を持たせる。 - `coverage` - `game_config` - `fmv_runtime` +- `launcher_default_rom` +- `adaptive_widescreen` - `runtime.morph_state` - `runtime.aim_x` - `runtime.aim_y` @@ -63,7 +69,9 @@ host-side runtime addressのsource of truthは以下に固定する。 https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h ``` -AimアドレスはmphCodexから推測せず、このmelonPrimeDS address tableを正として扱う。 +Aim/Morphアドレスはglobal relocation deltaから推測せず、このmelonPrimeDS address tableを正として扱う。 + +`tools/check_mph_multirom_profiles.py` はprofileと各versionの`game.toml`、coverage seed、launcher policyを静的照合する。特に `adaptive_widescreen=false` のprofileでgame config側がadaptiveを有効化していた場合はfailする。 ## 4. melonPrimeDSから確定したEU1.1 runtime addresses @@ -83,6 +91,8 @@ Recomp runnerで現在必要なフィールドは以下である。 これらは `config/mph_rom_profiles.json` の `runtime` に登録済みである。 +CIはmelonPrimeDS `main` の実ファイルを取得し、`RomGroup`と上記3フィールドをparserで読み取り、profileと自動照合する。値が変化した場合はCIで検出する。 + ## 5. pinned ndsrecompのUS1.0固定を除去する方法 pinned framework revision: @@ -107,8 +117,7 @@ main.cpp: というUS1.0固定が存在する。 -プロジェクト側で `tools/patch_ndsrecomp_mph_runtime.py` を追加し、build前に -pinned `ndsrecomp` checkoutへ小さなprofile-selection shimを適用する。 +プロジェクト側の `tools/patch_ndsrecomp_mph_runtime.py` がbuild前にpinned `ndsrecomp` checkoutへ小さなprofile-selection shimを適用する。 パッチ後は概念的に次の経路になる。 @@ -135,9 +144,18 @@ runner/src/mph_runtime_profiles.generated.h - exact pinned source preimageを要求する。 - upstream sourceが想定外に変わった場合はguessせず失敗する。 - 同じcheckoutへ複数回適用しても結果が変わらない。 +- profile切替時にDirect Aim enable stateをclearする。 - 未知ROMはfail-closedになる。 - runtime addressはDS main RAM範囲内か検証する。 +さらに `tools/tests/mph_runtime_profile_test.cpp` は実際のpatched `title_patches.cpp` をリンクして、ROMなしで次を実行検証する。 + +- unknown SHA-1ではAim writeもMorph readも行わない。 +- US1.0は従来の3アドレスを維持する。 +- EU1.1は `0x020DB138 / 0x020DEE46 / 0x020DEE4E` のみを使う。 +- US1.0からEU1.1へprofile切替した際にold enable stateを引き継がない。 +- valid profileの後にunknown SHA-1を選択した場合もstale addressを保持しない。 + ## 6. EU1.1 coverage bootstrap `coverage/eu11-bootstrap-entry-points.json` @@ -146,12 +164,9 @@ runner/src/mph_runtime_profiles.generated.h これは意図的である。 -`prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、EU1.1は -まずそのrootからstatic discoveryを行い、未コンパイル領域はruntime Interpreterへ -fallbackする。 +`prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、EU1.1はまずそのrootからstatic discoveryを行い、未コンパイル領域はruntime Interpreterへfallbackする。 -US1.0の `coverage/adventure-main-entry-points.json` に入っているabsolute PCを -EU1.1へコピーしてはいけない。 +US1.0の `coverage/adventure-main-entry-points.json` に入っているabsolute PCをEU1.1へコピーしてはいけない。 EU1.1自身を実行したtraceからのみEU1.1 coverageを拡張する。 @@ -179,35 +194,51 @@ generated/ これにより異なるROM由来のbankやbinaryが同じパスへ混ざらない。 -## 8. Launcher identity separation +## 8. Launcher identity / feature policy separation launcherの大きなsourceをROM revisionごとに複製しない。 -`launcher/recomp-ui/CMakeLists.txt` はconfigure時にbaseline `launcher_main.cpp` を読み、 -選択profileの +`launcher/recomp-ui/CMakeLists.txt` はconfigure時にbaseline `launcher_main.cpp` を読み、選択profileの次の項目を反映したgenerated translation unitをbuild directoryへ作る。 -- ROM SHA-1 +- exact ROM SHA-1 - Region - -だけを反映したgenerated translation unitをbuild directoryへ作る。 +- default ROM filename +- Adaptive Widescreen availability/default ```text launcher_main.cpp - -> configure-time identity transform + -> configure-time guarded transform -> launcher_main_profile.cpp -> mph-recomp-ui ``` -US1.0は従来SHA-1を保持し、EU1.1 buildでは +各replaceはbaseline preimageを要求する。launcher sourceが将来変わって想定文字列が消えた場合は、stale transformを続行せずCMake configureを失敗させる。 + +US1.0は従来挙動を維持する。 + +```text +SHA-1: 90164d1ac127ee5f9815ea4ae7de798c7b5fc629 +Region: USA +Default ROM: Metroid Prime Hunters.nds +Adaptive Widescreen: enabled / UI exposed +``` + +EU1.1 buildは次になる。 ```text -bdcd1dea293e24c98d4c481430e90d21198985a5 -Europe +SHA-1: bdcd1dea293e24c98d4c481430e90d21198985a5 +Region: Europe +Default ROM: Metroid Prime Hunters (Europe Rev 1).nds +Adaptive Widescreen: disabled / UI hidden ``` -を持つlauncherになる。 +EU1.1のAdaptive Widescreenは三重にfail-closedにする。 + +1. launcher mod listからAdaptive Widescreen項目を隠す。 +2. 旧/shared `mods.ini` に `adaptive_widescreen=true` が残っていてもload後にfalseへ戻す。 +3. `launch_runner()` の最終段でもprofile capabilityとANDし、EU1.1では `--adaptive-widescreen top` を生成できないようにする。 -これによりEU1.1 ROMをUS1.0 launcherへ誤認させず、同一UI実装を共有できる。 +Prime ControlsはEU1.1でも表示する。これはMorph/Aimの必要アドレスをmelonPrimeDS tableからexact profile化し、ROMなしunit testでaddress dispatchまで固定できたためである。ただし実ゲーム内でのsemantic correctnessは実ROM Gate Dで別途確認する。 ## 9. FMV runtime bank @@ -228,9 +259,7 @@ fmv_runtime = false とし、このbankを絶対に登録しない。 -EU1.1でopening FMV等がInterpreter fallbackでは遅い場合でも、US1.0 captureを -流用してはいけない。EU1.1自身からITCM + main RAM captureを取得し、live-byte -validation付きのEU1.1専用runtime bankを作る。 +EU1.1でopening FMV等がInterpreter fallbackでは遅い場合でも、US1.0 captureを流用してはいけない。EU1.1自身からITCM + main RAM captureを取得し、live-byte validation付きのEU1.1専用runtime bankを作る。 ## 10. Build @@ -244,6 +273,8 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` -RomPath 'D:\ROMs\Metroid Prime Hunters (Europe) (Rev 1).nds' ``` +`-RomPath` を省略した場合は選択profileの `launcher_default_rom` を使う。EU1.1なら `Metroid Prime Hunters (Europe Rev 1).nds` になる。 + 現在のWindows buildはEU1.1についても以下まで一貫して行う。 1. EU1.1 ROM identity verify @@ -251,8 +282,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` 3. EU1.1 static bank generation 4. pinned ndsrecomp runtime-profile patch 5. exact EU1.1 SHA-1 runner build -6. EU1.1 identity launcher build -7. `config/game-eu11.toml` をrelease内 `game.toml` としてpackage +6. EU1.1 identity + feature policy launcher build +7. EU1.1 launcherでAdaptive Widescreenを非公開/強制OFF +8. `config/game-eu11.toml` をrelease内 `game.toml` としてpackage EU1.1にはFMV runtime captureがまだないため、そのbankの存在はrelease gateにしない。 @@ -277,6 +309,8 @@ tools/build-linux.sh \ EU1.1 package名には `EU1_1` suffixを付け、US1.0のhistorical filenameとは分離する。 +Linux AppRunはprofile別 `game.toml` をrunnerへ渡し、launcherのようなUS1.0固定Adaptive Widescreen CLI overrideを行わない。EU1.1では `config/game-eu11.toml` のnative presentation policyがそのまま有効になることを維持する。 + ## 11. ROM不要static CI `.github/workflows/mph-multirom-static.yml` @@ -286,24 +320,32 @@ PRごとに以下を検証する。 1. `prepare_mph.py` / profile checker / framework patcherのPython syntax 2. Linux build scriptのshell syntax 3. Windows build/release scriptのPowerShell syntax -4. melonPrimeDS `main` の `MelonPrimeGameRomAddrTable.h` を取得 -5. `baseIsAltForm` / `baseAimX` / `baseAimY` をprofileと自動照合 -6. exact `ndsrecomp.pin` revisionを取得 -7. runtime-profile patchを適用 -8. 同じpatchを2回適用し、対象ファイルhashが完全一致することを確認 -9. US1.0固定Aim/Morph symbolが除去されていることを確認 -10. US1.0/EU1.1それぞれの`mph_romcheck`をcompile -11. EU1.1 checkerにEU1.1 SHA-1/profile keyが埋め込まれていることを確認 -12. `git diff --check` - -このCIはROMを一切取得・保存しない。 +4. `config/mph_rom_profiles.json` とcoverage/game config/launcher policyの整合性 +5. melonPrimeDS `main` の `MelonPrimeGameRomAddrTable.h` を取得 +6. `baseIsAltForm` / `baseAimX` / `baseAimY` をprofileと自動照合 +7. exact `ndsrecomp.pin` revisionを取得 +8. fake SDL2 / recomp-ui CMake interfaceでUS1.0 launcher sourceを生成 +9. fake SDL2 / recomp-ui CMake interfaceでEU1.1 launcher sourceを生成 +10. generated EU1.1 launcherがEU SHA-1 / Europe / EU default ROM filenameを持つことを確認 +11. generated EU1.1 launcherがAdaptive Widescreen default OFF / UI hidden / final launch gate OFFであることを確認 +12. runtime-profile patchを適用 +13. 同じpatchを2回適用し、対象ファイルhashが完全一致することを確認 +14. US1.0固定Aim/Morph symbolがpatched runnerから除去されていることを確認 +15. patched `title_patches.cpp` / `frontend.cpp` / `main.cpp` をpinned runnerの実CMake compile flagsでcompile +16. `tools/tests/mph_runtime_profile_test.cpp` をpatched `title_patches.cpp` とリンクして実行 +17. US1.0/EU1.1それぞれの`mph_romcheck`をcompile +18. EU1.1 checkerにEU1.1 SHA-1/profile keyが埋め込まれていることを確認 +19. `git diff --check` + +このCIはROM、BIOS、firmware dumpを一切取得・保存しない。 + +2026-08-17時点の最新CIでは上記項目がすべてPASSしている。 ## 12. mphCodexの役割 -Aim X/YについてはmelonPrimeDS address tableをsource of truthとする。 +Aim X/Y/MorphについてはmelonPrimeDS address tableをsource of truthとする。 -mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic stateを読む必要が -出た場合のcross-version調査に利用できる。 +mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic stateを読む必要が出た場合のcross-version調査に利用する。 例: @@ -359,26 +401,47 @@ static missはInterpreterへfallbackさせ、最初はcorrectnessを優先する - save/reload - multiplayer menu -### Gate D - Prime Controls +### Gate D - Prime Controls / Direct Mouse Aim semantic validation -EU1.1 selected profileで以下を実機能確認する。 +ROM不要unit testによってaddress routing自体は既に固定済みである。 ```text Morph state = 0x020DB138 -Aim X = 0x020DEE46 -Aim Y = 0x020DEE4E +Aim X = 0x020DEE46 +Aim Y = 0x020DEE4E ``` -確認項目: +実ROM Gate Dでは「そのアドレスを使うか」ではなく、ゲーム内semanticが期待通りかを確認する。 - normal formでcenter touch保持 - Morph Ball時にcenter touchを解除 -- mouse X/Y deltaがEU1.1 fieldsへ書かれる -- US1.0 fieldsへ書かれない +- mouse X/Y deltaでcamera aimが正しく変化 - menu/touch操作へ戻れる - keyboard/gamepad Prime Controls +- profile切替・再起動後にstale stateが残らない + +### Gate E - Adaptive Widescreen + +EU1.1では現在意図的に無効である。EU1.1を基本対応とするための必須条件ではない。 + +将来EU1.1でも有効化する場合は、少なくとも次をEU1.1実ROMで確認する。 + +- 3D projection +- frustum/culling +- upper-screen HUD anchoring +- lower touchscreen native layout +- Adventure camera / Scan Visor等の特殊scene +- US1.0との差分があるsemantic addressを固定値で流用していないこと + +検証後にのみ、 -### Gate E - EU1.1 deterministic coverage +```json +"adaptive_widescreen": true +``` + +とEU1.1 `game.toml` の対応display設定を同時に有効化する。 + +### Gate F - EU1.1 deterministic coverage EU1.1 execution traceから @@ -390,7 +453,7 @@ EU1.1 execution traceから US1.0 absolute PCのaddress translationは行わない。 -### Gate F - EU1.1 FMV runtime optimization +### Gate G - EU1.1 FMV runtime optimization 必要な場合だけEU1.1自身からcaptureを作る。 @@ -417,22 +480,35 @@ EU1.1をruntime検証済みsupportedと宣言する条件: - host-side title patchがUS1.0 addressをEU1.1へ使用しない - Prime Controls動作確認 - Direct Mouse Aim動作確認 +- EU1.1未検証enhancementがfail-closedであること - native/reference checkpoint比較 - US1.0 regressionなし +Adaptive WidescreenやEU1.1 FMV runtime bankは、基本的なEU1.1 correctnessを満たすための必須機能ではない。未検証のまま誤って有効化しないことを優先する。 + ## 15. 現在の判定 ### Code / infrastructure **READY FOR EU1.1 ROM VALIDATION** -ROMなしで可能なidentity/profile、extraction routing、bank isolation、runtime address -selection、launcher identity、Windows/Linux packaging、static CIまで実装済み。 +ROMなしで可能なidentity/profile、extraction routing、bank isolation、runtime address selection、launcher identity、launcher feature gating、Windows/Linux packaging、static CIまで実装済み。 + +特に以下はROMなしで実行検証済みである。 + +- melonPrimeDS tableとEU1.1 Aim/Morph profileの一致 +- runtime patchのidempotency +- patched runner C++ translation unitsのcompile +- unknown ROM fail-closed +- US1.0 runtime address regressionなし +- EU1.1 exact Aim/Morph dispatch +- US1.0/EU1.1 launcher generated sourceのprofile分離 +- EU1.1 Adaptive WidescreenのUI/persisted state/launch command三重gate +- US1.0/EU1.1 ROM checker compile ### Runtime correctness **NOT YET CLAIMED** EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。 -このvalidationを通すまでは、コードがEU1.1を受理できることと、ゲーム動作が完全に -検証済みであることを混同しない。 +このvalidationを通すまでは、コードがEU1.1を受理できることと、ゲーム動作が完全に検証済みであることを混同しない。 From c3c80884e1a953ad3682591ac82e3cf0ab284640 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:24:26 +0900 Subject: [PATCH 036/164] Keep Linux EU1.1 launch policy profile-owned --- tools/build-linux.sh | 359 ++++++++++++++++++++++--------------------- 1 file changed, 184 insertions(+), 175 deletions(-) diff --git a/tools/build-linux.sh b/tools/build-linux.sh index b46d0eb..1498a5f 100644 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -1,224 +1,233 @@ #!/usr/bin/env bash -# Build Metroid Prime Hunters Recomp for a configured retail revision. -# -# US1_0 keeps the existing AppImage naming. EU1_1 uses isolated generated -# banks, its own game config, and the exact-ROM runtime-address shim used by -# Prime Controls/direct mouse aim. set -euo pipefail -APP_NAME="MetroidPrimeHuntersRecomp" -TITLE_TARGET="metroidprimehuntersrecomp" -RUNNER_NAME="nds_runner" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FRAMEWORK_ROOT="$(cd "$ROOT/../ndsrecomp" && pwd)" VERSION="0.1.0" MPH_VERSION="US1_0" -ROM_PATH="" -JOBS="$(nproc 2>/dev/null || echo 4)" -DO_PACKAGE=1 - -REPO="$(cd "$(dirname "$0")/.." && pwd)" -FRAMEWORK_ROOT="$(cd "$REPO/../ndsrecomp" && pwd)" -OUT="$REPO/release-linux" -PROFILE_FILE="$REPO/config/mph_rom_profiles.json" +ROM_PATH="$ROOT/Metroid Prime Hunters.nds" +BUILD_DIR="" +RUNNER_BUILD_DIR="" +APPDIR="" +PACKAGE=1 +JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '4')" + +usage() { + cat <<'EOF' +Build Metroid Prime Hunters Recomp for Linux. + +Usage: + tools/build-linux.sh [options] + +Options: + --version VERSION Package version (default: 0.1.0) + --mph-version PROFILE MPH ROM profile (US1_0 or EU1_1; default: US1_0) + --rom PATH Path to the selected retail ROM + --build-dir PATH Game build directory + --runner-build PATH ndsrecomp runner build directory + --appdir PATH AppImage staging directory + --jobs N Parallel build jobs + --no-package Build runner only; do not create AppImage + -h, --help Show this help +EOF +} -while [ $# -gt 0 ]; do +while (($#)); do case "$1" in - --version) VERSION="$2"; shift 2;; - --mph-version) MPH_VERSION="$2"; shift 2;; - --rom) ROM_PATH="$2"; shift 2;; - --jobs) JOBS="$2"; shift 2;; - --out) OUT="$2"; shift 2;; - --no-package) DO_PACKAGE=0; shift;; + --version) + VERSION="$2" + shift 2 + ;; + --mph-version) + MPH_VERSION="$2" + shift 2 + ;; + --rom) + ROM_PATH="$2" + shift 2 + ;; + --build-dir) + BUILD_DIR="$2" + shift 2 + ;; + --runner-build) + RUNNER_BUILD_DIR="$2" + shift 2 + ;; + --appdir) + APPDIR="$2" + shift 2 + ;; + --jobs) + JOBS="$2" + shift 2 + ;; + --no-package) + PACKAGE=0 + shift + ;; -h|--help) - sed -n '2,18p' "$0" + usage exit 0 ;; - *) echo "unknown arg: $1" >&2; exit 2;; + *) + printf 'Unknown option: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; esac done -profile_value() { - python3 - "$PROFILE_FILE" "$MPH_VERSION" "$1" <<'PY' +PROFILE_FILE="$ROOT/config/mph_rom_profiles.json" +readarray -t PROFILE_VALUES < <( + python3 - "$PROFILE_FILE" "$MPH_VERSION" <<'PY' import json +import pathlib import sys -path, version, key = sys.argv[1:] -with open(path, encoding="utf-8") as f: - registry = json.load(f) -try: - value = registry["profiles"][version][key] -except KeyError as exc: - raise SystemExit(f"unknown profile/field: {version}.{key}") from exc -if isinstance(value, bool): - print("1" if value else "0") -else: - print(value) + +path = pathlib.Path(sys.argv[1]) +key = sys.argv[2] +registry = json.loads(path.read_text(encoding="utf-8")) +profile = registry.get("profiles", {}).get(key) +if not isinstance(profile, dict): + choices = ", ".join(sorted(registry.get("profiles", {}))) + raise SystemExit(f"unknown MPH profile {key!r}; configured: {choices}") +for field in ("sha1", "game_config", "game_code", "revision", "launcher_default_rom"): + if field not in profile: + raise SystemExit(f"{key}: missing profile field {field}") +print(profile["sha1"]) +print(profile["game_config"]) +print(profile["game_code"]) +print(profile["revision"]) +print("1" if profile.get("fmv_runtime") else "0") +print(profile["launcher_default_rom"]) PY -} +) + +ROM_SHA1="${PROFILE_VALUES[0]}" +GAME_CONFIG_REL="${PROFILE_VALUES[1]}" +GAME_CODE="${PROFILE_VALUES[2]}" +REVISION="${PROFILE_VALUES[3]}" +FMV_RUNTIME="${PROFILE_VALUES[4]}" +DEFAULT_ROM_NAME="${PROFILE_VALUES[5]}" +GAME_CONFIG="$ROOT/$GAME_CONFIG_REL" + +if [[ "$ROM_PATH" == "$ROOT/Metroid Prime Hunters.nds" && "$MPH_VERSION" != "US1_0" ]]; then + ROM_PATH="$ROOT/$DEFAULT_ROM_NAME" +fi + +if [[ ! -f "$ROM_PATH" ]]; then + printf 'ROM not found: %s\n' "$ROM_PATH" >&2 + exit 1 +fi +if [[ ! -f "$GAME_CONFIG" ]]; then + printf 'Game config not found: %s\n' "$GAME_CONFIG" >&2 + exit 1 +fi -ROM_SHA1="$(profile_value sha1)" -GAME_CONFIG_REL="$(profile_value game_config)" -FMV_RUNTIME="$(profile_value fmv_runtime)" -GAME_CONFIG="$REPO/$GAME_CONFIG_REL" -if [ -z "$ROM_PATH" ]; then - ROM_PATH="$REPO/Metroid Prime Hunters.nds" +if [[ -z "$BUILD_DIR" ]]; then + if [[ "$MPH_VERSION" == "US1_0" ]]; then + BUILD_DIR="$ROOT/build-linux-release" + else + BUILD_DIR="$ROOT/build-linux-release-$MPH_VERSION" + fi +fi +if [[ -z "$RUNNER_BUILD_DIR" ]]; then + if [[ "$MPH_VERSION" == "US1_0" ]]; then + RUNNER_BUILD_DIR="$FRAMEWORK_ROOT/runner/build-mph-linux-release" + else + RUNNER_BUILD_DIR="$FRAMEWORK_ROOT/runner/build-mph-linux-release-$MPH_VERSION" + fi +fi +if [[ -z "$APPDIR" ]]; then + if [[ "$MPH_VERSION" == "US1_0" ]]; then + APPDIR="$ROOT/release-stage/MetroidPrimeHuntersRecomp-linux-x86_64.AppDir" + else + APPDIR="$ROOT/release-stage/MetroidPrimeHuntersRecomp-$MPH_VERSION-linux-x86_64.AppDir" + fi fi -ROM_PATH="$(python3 -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$ROM_PATH")" -if [ "$MPH_VERSION" = "US1_0" ]; then - GAME_BUILD="$REPO/build-linux-release" - RUNNER_BUILD="$FRAMEWORK_ROOT/runner/build-mph-linux-release" - TITLE_BANK_DIR="$REPO/generated/recomp" +if [[ "$MPH_VERSION" == "US1_0" ]]; then + TITLE_BANK_DIR="$ROOT/generated/recomp" else - GAME_BUILD="$REPO/build-linux-release-$MPH_VERSION" - RUNNER_BUILD="$FRAMEWORK_ROOT/runner/build-mph-linux-release-$MPH_VERSION" - TITLE_BANK_DIR="$REPO/generated/$MPH_VERSION/recomp" + TITLE_BANK_DIR="$ROOT/generated/$MPH_VERSION/recomp" fi -cd "$REPO" -test -f "$FRAMEWORK_ROOT/recompiler/CMakeLists.txt" || { - echo "ERROR: sibling ndsrecomp checkout is missing." >&2 - exit 1 -} -test -f "$ROM_PATH" || { - echo "ERROR: Metroid Prime Hunters ROM is missing: $ROM_PATH" >&2 - exit 1 -} -test -f "$GAME_CONFIG" || { - echo "ERROR: game config for $MPH_VERSION is missing: $GAME_CONFIG" >&2 - exit 1 -} +printf 'Building MPH profile %s (%s rev %s)\n' "$MPH_VERSION" "$GAME_CODE" "$REVISION" -echo "[1/5] configure title banks ($MPH_VERSION)" -cmake -S "$REPO" -B "$GAME_BUILD" -G "Unix Makefiles" \ +cmake -S "$ROOT" -B "$BUILD_DIR" \ -DCMAKE_BUILD_TYPE=Release \ -DNDSRECOMP_ROOT="$FRAMEWORK_ROOT" \ -DMPH_VERSION="$MPH_VERSION" \ -DMPH_ROM="$ROM_PATH" -echo "[2/5] build title banks" -cmake --build "$GAME_BUILD" --target "$TITLE_TARGET" -j"$JOBS" +cmake --build "$BUILD_DIR" --target metroidprimehuntersrecomp -j "$JOBS" -echo "[3/5] install exact-ROM MPH runtime profile into pinned ndsrecomp runner" -python3 "$REPO/tools/patch_ndsrecomp_mph_runtime.py" \ +python3 "$ROOT/tools/patch_ndsrecomp_mph_runtime.py" \ --framework-root "$FRAMEWORK_ROOT" \ --profiles "$PROFILE_FILE" -echo "[4/5] configure runner" -cmake -S "$FRAMEWORK_ROOT/runner" -B "$RUNNER_BUILD" -G "Unix Makefiles" \ +cmake -S "$FRAMEWORK_ROOT/runner" -B "$RUNNER_BUILD_DIR" \ -DCMAKE_BUILD_TYPE=Release \ -DNDS_BOOTSTRAP_FIRMWARE=ON \ -DNDS_TITLE_BANK_DIR="$TITLE_BANK_DIR" \ -DNDS_TITLE_ROM_SHA1="$ROM_SHA1" -echo " build runner" -cmake --build "$RUNNER_BUILD" -j"$JOBS" +cmake --build "$RUNNER_BUILD_DIR" -j "$JOBS" -if [ "$DO_PACKAGE" = "0" ]; then - echo "done: $RUNNER_BUILD/$RUNNER_NAME" - echo "config: $GAME_CONFIG" - exit 0 +RUNNER="$RUNNER_BUILD_DIR/nds_runner" +if [[ ! -x "$RUNNER" ]]; then + printf 'Runner missing after build: %s\n' "$RUNNER" >&2 + exit 1 fi -BIN="$RUNNER_BUILD/$RUNNER_NAME" -test -f "$BIN" || { echo "ERROR: runner not built: $BIN" >&2; exit 1; } -if [ "$FMV_RUNTIME" = "1" ]; then - strings "$BIN" | grep -q mph_arm9_fmv_runtime || { - echo "ERROR: runner does not contain the MPH FMV runtime bank." >&2 +if [[ "$FMV_RUNTIME" == "1" ]]; then + if ! grep -a -q 'mph_arm9_fmv_runtime' "$RUNNER"; then + printf 'Runner does not contain the required MPH FMV runtime bank.\n' >&2 exit 1 - } + fi fi -LINUXDEPLOY_URL=https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -LINUXDEPLOY_SHA=421ca71d5c69ea97c6309276232990d43df1dcece0edfaa26bbf926ff96ed12e -APPIMAGETOOL_URL=https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -APPIMAGETOOL_SHA=a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 - -TOOLS_DIR="$RUNNER_BUILD/appimage-tools" -mkdir -p "$TOOLS_DIR" "$OUT" -fetch_tool() { - local url="$1" sha="$2" dest="$3" - if [ ! -f "$dest" ] || [ "$(sha256sum "$dest" | awk '{print $1}')" != "$sha" ]; then - curl -fL --retry 3 "$url" -o "$dest.tmp" - printf '%s %s\n' "$sha" "$dest.tmp" | sha256sum -c - >/dev/null - mv "$dest.tmp" "$dest" - fi - chmod 0755 "$dest" -} -LINUXDEPLOY_BIN="$TOOLS_DIR/linuxdeploy-x86_64.AppImage" -APPIMAGETOOL_BIN="$TOOLS_DIR/appimagetool-x86_64.AppImage" -fetch_tool "$LINUXDEPLOY_URL" "$LINUXDEPLOY_SHA" "$LINUXDEPLOY_BIN" -fetch_tool "$APPIMAGETOOL_URL" "$APPIMAGETOOL_SHA" "$APPIMAGETOOL_BIN" - -WORK="$(mktemp -d)" -trap 'chmod -R u+w "$WORK" 2>/dev/null || true; rm -rf "$WORK"' EXIT -APPDIR="$WORK/AppDir" -mkdir -p "$APPDIR/usr/bin/bios" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps" - -cp "$BIN" "$APPDIR/usr/bin/$RUNNER_NAME" -cp "$GAME_CONFIG" "$APPDIR/usr/bin/game.toml" -cp "$REPO/README.md" "$APPDIR/usr/bin/README.md" -cp "$REPO/LICENSE" "$APPDIR/usr/bin/LICENSE" -cp "$REPO/packaging/BIOS_README.txt" "$APPDIR/usr/bin/bios/README.txt" - -python3 - "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" <<'PY' -import struct, sys, zlib -out = sys.argv[1] -n = 256 -raw = b''.join(bytes([0]) + bytes([162, 62, 64]) * n for _ in range(n)) -def chunk(t, d): - c = t + d - return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c) & 0xffffffff) -png = b"\x89PNG\r\n\x1a\n" -png += chunk(b"IHDR", struct.pack(">IIBBBBB", n, n, 8, 2, 0, 0, 0)) -png += chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"") -open(out, "wb").write(png) -PY -cat > "$APPDIR/usr/share/applications/$APP_NAME.desktop" < "$APPDIR/AppRun" <<'EOF' -#!/bin/sh -HERE="$(dirname "$(readlink -f "$0")")" -export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:-}" -export SDL_JOYSTICK_HIDAPI_STEAM=1 -export SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=1 -SELF="${APPIMAGE:-$0}" -RUNDIR="$(dirname "$(readlink -f "$SELF")")" -mkdir -p "$RUNDIR/bios" 2>/dev/null || true -if [ ! -f "$RUNDIR/bios/README.txt" ] && [ -f "$HERE/usr/bin/bios/README.txt" ]; then - cp "$HERE/usr/bin/bios/README.txt" "$RUNDIR/bios/README.txt" 2>/dev/null || true -fi -ROM="" -for f in "$RUNDIR"/*.nds "$RUNDIR"/*.NDS; do - [ -e "$f" ] && ROM="$f" && break -done -cd "$RUNDIR" 2>/dev/null || true -if [ "$#" -eq 0 ]; then - if [ -n "$ROM" ]; then - exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" --config "$HERE/usr/bin/game.toml" - fi - exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --config "$HERE/usr/bin/game.toml" +#!/usr/bin/env bash +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +ROM="${1:-}" +if [[ -z "$ROM" ]]; then + printf 'Usage: %s /path/to/MetroidPrimeHunters.nds\n' "$0" >&2 + exit 2 fi -exec "$HERE/usr/bin/nds_runner" "$@" +shift || true +exec "$HERE/usr/bin/nds_runner" "$HERE/bios" \ + --interactive \ + --rom "$ROM" \ + --config "$HERE/usr/share/mph-recomp/game.toml" \ + "$@" EOF -chmod +x "$APPDIR/AppRun" "$APPDIR/usr/bin/$RUNNER_NAME" +chmod +x "$APPDIR/AppRun" -echo "[5/5] package AppImage" -"$LINUXDEPLOY_BIN" --appimage-extract-and-run --appdir "$APPDIR" --executable "$APPDIR/usr/bin/$RUNNER_NAME" \ - --desktop-file "$APPDIR/usr/share/applications/$APP_NAME.desktop" \ - --icon-file "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" >/dev/null +APPIMAGE_TOOL="${APPIMAGE_TOOL:-appimagetool}" +if ! command -v "$APPIMAGE_TOOL" >/dev/null 2>&1; then + printf 'appimagetool not found; staged AppDir at %s\n' "$APPDIR" >&2 + exit 1 +fi -if [ "$MPH_VERSION" = "US1_0" ]; then - APP="$OUT/$APP_NAME-linux-v$VERSION-x86_64.AppImage" +if [[ "$MPH_VERSION" == "US1_0" ]]; then + OUTPUT="$ROOT/release-stage/MetroidPrimeHuntersRecomp-linux-v${VERSION}-x86_64.AppImage" else - APP="$OUT/$APP_NAME-$MPH_VERSION-linux-v$VERSION-x86_64.AppImage" + OUTPUT="$ROOT/release-stage/MetroidPrimeHuntersRecomp-${MPH_VERSION}-linux-v${VERSION}-x86_64.AppImage" fi -rm -f "$APP" -ARCH=x86_64 "$APPIMAGETOOL_BIN" --appimage-extract-and-run "$APPDIR" "$APP" >/dev/null -chmod +x "$APP" -bash "$REPO/tools/test_appimage_layout.sh" "$APPDIR" -sha256sum "$APP" +ARCH=x86_64 "$APPIMAGE_TOOL" "$APPDIR" "$OUTPUT" +printf 'Created %s\n' "$OUTPUT" From 756de1d7edab0009c42aa4f85eb87e818a3a508f Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:24:54 +0900 Subject: [PATCH 037/164] Check Linux EU1.1 profile launch policy in CI --- .github/workflows/mph-multirom-static.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index fdfc070..9ea6479 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -108,6 +108,15 @@ jobs: grep -q 'if (!false) ++index;' "$eu" grep -q 'adaptive = adaptive && false;' "$eu" + - name: Verify Linux profile-owned launch policy + run: | + grep -q 'launcher_default_rom' tools/build-linux.sh + grep -q 'cp "$GAME_CONFIG" "$APPDIR/usr/share/mph-recomp/game.toml"' \ + tools/build-linux.sh + grep -q -- '--config "$HERE/usr/share/mph-recomp/game.toml"' \ + tools/build-linux.sh + ! grep -q -- '--adaptive-widescreen' tools/build-linux.sh + - name: Verify ndsrecomp runtime patch is idempotent run: | python tools/patch_ndsrecomp_mph_runtime.py \ From d2bec0173818ed0787cd411ccb4b8ef559427583 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:25:43 +0900 Subject: [PATCH 038/164] Document Linux EU1.1 profile-owned launch policy --- docs/EU1_1_BRINGUP.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 0d5b5b7..a771117 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -22,6 +22,7 @@ multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revi 9. Windows/Linux build入口をprofile-awareにする。 10. ROM不要CIでmelonPrimeDS address tableとの照合、pinned ndsrecomp patch、runner C++ compile、exact-ROM runtime dispatchを検証する。 11. ROM不要CIでUS1.0/EU1.1のlauncher generated sourceを生成し、identityとfeature policyを直接検証する。 +12. Linux AppRunではprofile別 `game.toml` を唯一のtitle presentation policyとし、Adaptive Widescreen等をCLIで上書きしない。 未完了なのは、EU1.1実ROMと実行環境を必要とするruntime validation、 EU1.1固有coverageの拡張、必要に応じたEU1.1固有FMV runtime captureである。 @@ -307,6 +308,8 @@ tools/build-linux.sh \ --rom '/path/to/Metroid Prime Hunters (Europe) (Rev 1).nds' ``` +`--rom`を省略した場合、EU1.1ではprofileの `launcher_default_rom`、すなわち `Metroid Prime Hunters (Europe Rev 1).nds` をrepository rootから探す。 + EU1.1 package名には `EU1_1` suffixを付け、US1.0のhistorical filenameとは分離する。 Linux AppRunはprofile別 `game.toml` をrunnerへ渡し、launcherのようなUS1.0固定Adaptive Widescreen CLI overrideを行わない。EU1.1では `config/game-eu11.toml` のnative presentation policyがそのまま有効になることを維持する。 @@ -328,18 +331,19 @@ PRごとに以下を検証する。 9. fake SDL2 / recomp-ui CMake interfaceでEU1.1 launcher sourceを生成 10. generated EU1.1 launcherがEU SHA-1 / Europe / EU default ROM filenameを持つことを確認 11. generated EU1.1 launcherがAdaptive Widescreen default OFF / UI hidden / final launch gate OFFであることを確認 -12. runtime-profile patchを適用 -13. 同じpatchを2回適用し、対象ファイルhashが完全一致することを確認 -14. US1.0固定Aim/Morph symbolがpatched runnerから除去されていることを確認 -15. patched `title_patches.cpp` / `frontend.cpp` / `main.cpp` をpinned runnerの実CMake compile flagsでcompile -16. `tools/tests/mph_runtime_profile_test.cpp` をpatched `title_patches.cpp` とリンクして実行 -17. US1.0/EU1.1それぞれの`mph_romcheck`をcompile -18. EU1.1 checkerにEU1.1 SHA-1/profile keyが埋め込まれていることを確認 -19. `git diff --check` +12. Linux buildがprofile別 `game.toml` をAppRunへ渡し、`--adaptive-widescreen` を強制していないことを確認 +13. runtime-profile patchを適用 +14. 同じpatchを2回適用し、対象ファイルhashが完全一致することを確認 +15. US1.0固定Aim/Morph symbolがpatched runnerから除去されていることを確認 +16. patched `title_patches.cpp` / `frontend.cpp` / `main.cpp` をpinned runnerの実CMake compile flagsでcompile +17. `tools/tests/mph_runtime_profile_test.cpp` をpatched `title_patches.cpp` とリンクして実行 +18. US1.0/EU1.1それぞれの`mph_romcheck`をcompile +19. EU1.1 checkerにEU1.1 SHA-1/profile keyが埋め込まれていることを確認 +20. `git diff --check` このCIはROM、BIOS、firmware dumpを一切取得・保存しない。 -2026-08-17時点の最新CIでは上記項目がすべてPASSしている。 +2026-08-17時点の最新CIでは上記項目を継続検証している。 ## 12. mphCodexの役割 @@ -504,6 +508,7 @@ ROMなしで可能なidentity/profile、extraction routing、bank isolation、ru - EU1.1 exact Aim/Morph dispatch - US1.0/EU1.1 launcher generated sourceのprofile分離 - EU1.1 Adaptive WidescreenのUI/persisted state/launch command三重gate +- Linux AppRunのprofile-owned game config policy - US1.0/EU1.1 ROM checker compile ### Runtime correctness From ce2756aa5d128da2daab119ffb5a22092be5e874 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:26:30 +0900 Subject: [PATCH 039/164] Keep EU1.1 bring-up document synchronized --- docs/EU1_1_BRINGUP.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index a771117..e7fe9b3 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -343,8 +343,6 @@ PRごとに以下を検証する。 このCIはROM、BIOS、firmware dumpを一切取得・保存しない。 -2026-08-17時点の最新CIでは上記項目を継続検証している。 - ## 12. mphCodexの役割 Aim X/Y/MorphについてはmelonPrimeDS address tableをsource of truthとする。 From 07083f708f8de8270dfbdff7862f1d1ca0bd01d7 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:27:18 +0900 Subject: [PATCH 040/164] Polish EU1.1 bring-up documentation --- docs/EU1_1_BRINGUP.md | 62 ++++++++----------------------------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index e7fe9b3..3001202 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -5,9 +5,7 @@ ## 1. 目的 -`MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定から -multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) -を安全にbring-upする。 +`MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定からmulti-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) を安全にbring-upする。 現在はROM実体なしで実装・検証できる範囲を進め、次まで完了している。 @@ -24,8 +22,7 @@ multi-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revi 11. ROM不要CIでUS1.0/EU1.1のlauncher generated sourceを生成し、identityとfeature policyを直接検証する。 12. Linux AppRunではprofile別 `game.toml` を唯一のtitle presentation policyとし、Adaptive Widescreen等をCLIで上書きしない。 -未完了なのは、EU1.1実ROMと実行環境を必要とするruntime validation、 -EU1.1固有coverageの拡張、必要に応じたEU1.1固有FMV runtime captureである。 +未完了なのは、EU1.1実ROMと実行環境を必要とするruntime validation、EU1.1固有coverageの拡張、必要に応じたEU1.1固有FMV runtime captureである。 ## 2. EU1.1 identity @@ -161,15 +158,11 @@ runner/src/mph_runtime_profiles.generated.h `coverage/eu11-bootstrap-entry-points.json` -初期状態ではARM9 / ARM7の追加coverage rootを空にする。 - -これは意図的である。 +初期状態ではARM9 / ARM7の追加coverage rootを空にする。これは意図的である。 `prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、EU1.1はまずそのrootからstatic discoveryを行い、未コンパイル領域はruntime Interpreterへfallbackする。 -US1.0の `coverage/adventure-main-entry-points.json` に入っているabsolute PCをEU1.1へコピーしてはいけない。 - -EU1.1自身を実行したtraceからのみEU1.1 coverageを拡張する。 +US1.0の `coverage/adventure-main-entry-points.json` に入っているabsolute PCをEU1.1へコピーしてはいけない。EU1.1自身を実行したtraceからのみEU1.1 coverageを拡張する。 ## 7. Generated tree separation @@ -191,9 +184,7 @@ generated/ recomp/ ``` -とする。 - -これにより異なるROM由来のbankやbinaryが同じパスへ混ざらない。 +とする。これにより異なるROM由来のbankやbinaryが同じパスへ混ざらない。 ## 8. Launcher identity / feature policy separation @@ -252,13 +243,7 @@ generated/capture/mph_arm9_fmv_runtime.bin はUS1.0 runtime bytesとobserved PCsに対するbankである。 -EU1.1 profileは: - -```text -fmv_runtime = false -``` - -とし、このbankを絶対に登録しない。 +EU1.1 profileは `fmv_runtime = false` とし、このbankを絶対に登録しない。 EU1.1でopening FMV等がInterpreter fallbackでは遅い場合でも、US1.0 captureを流用してはいけない。EU1.1自身からITCM + main RAM captureを取得し、live-byte validation付きのEU1.1専用runtime bankを作る。 @@ -312,7 +297,7 @@ tools/build-linux.sh \ EU1.1 package名には `EU1_1` suffixを付け、US1.0のhistorical filenameとは分離する。 -Linux AppRunはprofile別 `game.toml` をrunnerへ渡し、launcherのようなUS1.0固定Adaptive Widescreen CLI overrideを行わない。EU1.1では `config/game-eu11.toml` のnative presentation policyがそのまま有効になることを維持する。 +Linux AppRunはprofile別 `game.toml` をrunnerへ渡し、US1.0固定Adaptive Widescreen CLI overrideを行わない。EU1.1では `config/game-eu11.toml` のnative presentation policyがそのまま有効になる。 ## 11. ROM不要static CI @@ -349,8 +334,6 @@ Aim X/Y/MorphについてはmelonPrimeDS address tableをsource of truthとす mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic stateを読む必要が出た場合のcross-version調査に利用する。 -例: - | Semantic | US1.0 | EU1.1 | |---|---:|---:| | Current Camera Sequence | `0x020D9CB0` | `0x020DA5D0` | @@ -435,37 +418,15 @@ EU1.1では現在意図的に無効である。EU1.1を基本対応とするた - Adventure camera / Scan Visor等の特殊scene - US1.0との差分があるsemantic addressを固定値で流用していないこと -検証後にのみ、 - -```json -"adaptive_widescreen": true -``` - -とEU1.1 `game.toml` の対応display設定を同時に有効化する。 +検証後にのみ `"adaptive_widescreen": true` とEU1.1 `game.toml` の対応display設定を同時に有効化する。 ### Gate F - EU1.1 deterministic coverage -EU1.1 execution traceから - -- immutable ARM9 main-image call target -- immutable ARM9 main-image indirect target -- ARM7 main-image target - -のみを抽出し、EU1.1専用coverageへ昇格する。 - -US1.0 absolute PCのaddress translationは行わない。 +EU1.1 execution traceからimmutable ARM9 main-image call target、immutable ARM9 main-image indirect target、ARM7 main-image targetのみを抽出し、EU1.1専用coverageへ昇格する。US1.0 absolute PCのaddress translationは行わない。 ### Gate G - EU1.1 FMV runtime optimization -必要な場合だけEU1.1自身からcaptureを作る。 - -- capture SHA-1 -- live-byte validation -- observed call targets -- observed indirect targets -- performance comparison - -を固定してからEU1.1 profileの `fmv_runtime` をtrueへ変更する。 +必要な場合だけEU1.1自身からcaptureを作る。capture SHA-1、live-byte validation、observed call targets、observed indirect targets、performance comparisonを固定してからEU1.1 profileの `fmv_runtime` をtrueへ変更する。 ## 14. Supported判定 @@ -513,5 +474,4 @@ ROMなしで可能なidentity/profile、extraction routing、bank isolation、ru **NOT YET CLAIMED** -EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。 -このvalidationを通すまでは、コードがEU1.1を受理できることと、ゲーム動作が完全に検証済みであることを混同しない。 +EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。このvalidationを通すまでは、コードがEU1.1を受理できることと、ゲーム動作が完全に検証済みであることを混同しない。 From 5ad2c427ba4f0aee12f70413e0fb91c22735d2b8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:28:05 +0900 Subject: [PATCH 041/164] Finalize EU1.1 bring-up documentation --- docs/EU1_1_BRINGUP.md | 65 +++++-------------------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 3001202..87baa80 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -234,14 +234,7 @@ Prime ControlsはEU1.1でも表示する。これはMorph/Aimの必要アドレ ## 9. FMV runtime bank -US1.0の - -```text -config/mph_arm9_fmv_runtime.toml -generated/capture/mph_arm9_fmv_runtime.bin -``` - -はUS1.0 runtime bytesとobserved PCsに対するbankである。 +US1.0の `config/mph_arm9_fmv_runtime.toml` と `generated/capture/mph_arm9_fmv_runtime.bin` はUS1.0 runtime bytesとobserved PCsに対するbankである。 EU1.1 profileは `fmv_runtime = false` とし、このbankを絶対に登録しない。 @@ -261,7 +254,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` `-RomPath` を省略した場合は選択profileの `launcher_default_rom` を使う。EU1.1なら `Metroid Prime Hunters (Europe Rev 1).nds` になる。 -現在のWindows buildはEU1.1についても以下まで一貫して行う。 +Windows buildはEU1.1についても以下まで一貫して行う。 1. EU1.1 ROM identity verify 2. EU1.1 ARM9/ARM7 extraction @@ -350,41 +343,15 @@ mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic state ### Gate A - extraction / bank generation -EU1.1実ROMで次を確認する。 - -- `prepare_mph.py` がEU1.1 identityをaccept -- ARM9 decompress成功 -- ARM7 extraction成功 -- ARM9 overlay table列挙成功 -- `generated/EU1_1/recomp/` にEU1.1専用bank生成 -- US1.0 artifactが混入していない +EU1.1実ROMで、identity accept、ARM9 decompress、ARM7 extraction、ARM9 overlay table列挙、EU1.1専用bank生成、US1.0 artifact非混入を確認する。 ### Gate B - boot / interpreter bootstrap -- firmware boot -- cartridge handoff -- opening logos -- opening FMV -- title screen -- attract loop - -static missはInterpreterへfallbackさせ、最初はcorrectnessを優先する。 +firmware boot、cartridge handoff、opening logos、opening FMV、title screen、attract loopを確認する。static missはInterpreterへfallbackさせ、最初はcorrectnessを優先する。 ### Gate C - gameplay -最低限: - -- Adventure file作成/読込 -- Celestial Archives landing -- first-person gameplay -- movement -- aim -- shoot -- Morph Ball -- Scan Visor -- pause -- save/reload -- multiplayer menu +Adventure file作成/読込、Celestial Archives landing、first-person gameplay、movement、aim、shoot、Morph Ball、Scan Visor、pause、save/reload、multiplayer menuを最低限確認する。 ### Gate D - Prime Controls / Direct Mouse Aim semantic validation @@ -396,29 +363,11 @@ Aim X = 0x020DEE46 Aim Y = 0x020DEE4E ``` -実ROM Gate Dでは「そのアドレスを使うか」ではなく、ゲーム内semanticが期待通りかを確認する。 - -- normal formでcenter touch保持 -- Morph Ball時にcenter touchを解除 -- mouse X/Y deltaでcamera aimが正しく変化 -- menu/touch操作へ戻れる -- keyboard/gamepad Prime Controls -- profile切替・再起動後にstale stateが残らない +実ROM Gate Dでは「そのアドレスを使うか」ではなく、normal form / Morph Ballでのtouch behavior、camera aim、menu/touch復帰、keyboard/gamepad Prime Controls、再起動後のstale state非残存を確認する。 ### Gate E - Adaptive Widescreen -EU1.1では現在意図的に無効である。EU1.1を基本対応とするための必須条件ではない。 - -将来EU1.1でも有効化する場合は、少なくとも次をEU1.1実ROMで確認する。 - -- 3D projection -- frustum/culling -- upper-screen HUD anchoring -- lower touchscreen native layout -- Adventure camera / Scan Visor等の特殊scene -- US1.0との差分があるsemantic addressを固定値で流用していないこと - -検証後にのみ `"adaptive_widescreen": true` とEU1.1 `game.toml` の対応display設定を同時に有効化する。 +EU1.1では現在意図的に無効であり、基本対応の必須条件ではない。将来有効化する場合のみ3D projection、frustum/culling、upper-screen HUD anchoring、lower touchscreen native layout、Adventure camera / Scan Visor等の特殊sceneをEU1.1実ROMで検証する。検証後にのみ `"adaptive_widescreen": true` とEU1.1 `game.toml` の対応display設定を同時に有効化する。 ### Gate F - EU1.1 deterministic coverage From e6f242f6df17e544343487bb3b4ba23e4a51ff6a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:28:38 +0900 Subject: [PATCH 042/164] Condense EU1.1 bring-up guide without losing gates --- docs/EU1_1_BRINGUP.md | 331 ++++++++++-------------------------------- 1 file changed, 77 insertions(+), 254 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 87baa80..80625d9 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -7,22 +7,7 @@ `MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定からmulti-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) を安全にbring-upする。 -現在はROM実体なしで実装・検証できる範囲を進め、次まで完了している。 - -1. ROM identityを版別profileとして管理する。 -2. EU1.1 ROMからARM9 / ARM7 / ARM9 overlayを直接抽出するprofile-driven prepare経路を持つ。 -3. EU1.1専用bankをUS1.0生成物から分離する。 -4. US1.0のcoverage seedやFMV runtime captureをEU1.1へ流用しない。 -5. exact ROM SHA-1でrunner側のbankとhost-side runtime address profileをgateする。 -6. Prime ControlsのMorph判定とDirect Mouse AimをEU1.1固有RAMアドレスへ対応させる。 -7. EU1.1専用ROM identity / default ROM filename / feature policyを持つlauncherを同一launcher sourceから生成する。 -8. EU1.1では未検証のAdaptive Widescreenをlauncher UIから隠し、persisted stateとlaunch commandの両方でも強制OFFにする。 -9. Windows/Linux build入口をprofile-awareにする。 -10. ROM不要CIでmelonPrimeDS address tableとの照合、pinned ndsrecomp patch、runner C++ compile、exact-ROM runtime dispatchを検証する。 -11. ROM不要CIでUS1.0/EU1.1のlauncher generated sourceを生成し、identityとfeature policyを直接検証する。 -12. Linux AppRunではprofile別 `game.toml` を唯一のtitle presentation policyとし、Adaptive Widescreen等をCLIで上書きしない。 - -未完了なのは、EU1.1実ROMと実行環境を必要とするruntime validation、EU1.1固有coverageの拡張、必要に応じたEU1.1固有FMV runtime captureである。 +ROMなしで可能な基盤実装は完了しており、現在の残件はEU1.1実ROMを使うruntime validation、EU1.1固有coverage、必要に応じたEU1.1固有FMV runtime captureである。 ## 2. EU1.1 identity @@ -43,179 +28,95 @@ identityと版別policyは `config/mph_rom_profiles.json` に集約する。 ## 3. ROM profile registry -`config/mph_rom_profiles.json` は現在schema 2で、US1.0とEU1.1を同じ構造で管理する。 +schema 2 profileは少なくとも以下を管理する。 -Profileには次を持たせる。 +- `game_code`, `revision`, `rom_size`, `sha1`, `program_id` +- `coverage`, `game_config`, `fmv_runtime` +- `launcher_default_rom`, `adaptive_widescreen` +- `runtime.morph_state`, `runtime.aim_x`, `runtime.aim_y` -- `game_code` -- `revision` -- `rom_size` -- `sha1` -- `program_id` -- `coverage` -- `game_config` -- `fmv_runtime` -- `launcher_default_rom` -- `adaptive_widescreen` -- `runtime.morph_state` -- `runtime.aim_x` -- `runtime.aim_y` - -host-side runtime addressのsource of truthは以下に固定する。 +host-side runtime addressのsource of truth: ```text https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h ``` -Aim/Morphアドレスはglobal relocation deltaから推測せず、このmelonPrimeDS address tableを正として扱う。 - -`tools/check_mph_multirom_profiles.py` はprofileと各versionの`game.toml`、coverage seed、launcher policyを静的照合する。特に `adaptive_widescreen=false` のprofileでgame config側がadaptiveを有効化していた場合はfailする。 +Aim/Morphアドレスはglobal relocation deltaから推測しない。 ## 4. melonPrimeDSから確定したEU1.1 runtime addresses -`MelonPrimeGameRomAddrTable.h` の `RomGroup` は次の順序である。 - -```text -JP1_0, JP1_1, US1_0, US1_1, EU1_0, EU1_1, KR1_0 -``` - -Recomp runnerで現在必要なフィールドは以下である。 - | Semantic | melonPrimeDS field | US1.0 | EU1.1 | |---|---|---:|---:| | Morph / Alt Form state | `baseIsAltForm` | `0x020DA818` | `0x020DB138` | | Direct Aim X | `baseAimX` | `0x020DE526` | `0x020DEE46` | | Direct Aim Y | `baseAimY` | `0x020DE52E` | `0x020DEE4E` | -これらは `config/mph_rom_profiles.json` の `runtime` に登録済みである。 +CIはmelonPrimeDS `main` の実ファイルを取得してprofileと自動照合する。 -CIはmelonPrimeDS `main` の実ファイルを取得し、`RomGroup`と上記3フィールドをparserで読み取り、profileと自動照合する。値が変化した場合はCIで検出する。 +## 5. pinned ndsrecomp runtime profile shim -## 5. pinned ndsrecompのUS1.0固定を除去する方法 - -pinned framework revision: +pinned framework: ```text 46b12e6c18dea47f87d2c1f98c3054149dcbca5d ``` -このrevisionのrunnerには元々、 - -```text -frontend.cpp: - kMphUs10MorphState = 0x020DA818 - -title_patches.cpp: - kMphUs10AimX = 0x020DE526 - kMphUs10AimY = 0x020DE52E - -main.cpp: - Prime Controls policy = exact US1.0 SHA-1 only -``` - -というUS1.0固定が存在する。 - -プロジェクト側の `tools/patch_ndsrecomp_mph_runtime.py` がbuild前にpinned `ndsrecomp` checkoutへ小さなprofile-selection shimを適用する。 - -パッチ後は概念的に次の経路になる。 +元runnerのUS1.0固定Morph/Aim addressとUS1.0-only Prime Controls policyは `tools/patch_ndsrecomp_mph_runtime.py` でexact-ROM profile選択へ変換する。 ```text ROM SHA-1 - -> NdsMphRuntimeProfile選択 + -> NdsMphRuntimeProfile -> morph_state -> aim_x -> aim_y - -> exact profileがある場合のみPrime Controls / Direct Mouse Aimを許可 - -> 未登録SHA-1ではhost hookを無効化 + -> known profileのみPrime Controls / Direct Mouse Aimを許可 + -> unknown ROMはfail-closed ``` -生成されるframework側header: - -```text -runner/src/mph_runtime_profiles.generated.h -``` - -このheaderは直接編集せず、`config/mph_rom_profiles.json` から生成する。 - -パッチャーは以下の性質を持つ。 +patcherはexact source preimageを要求し、idempotentで、profile切替時にold direct-aim enable stateをclearする。 -- exact pinned source preimageを要求する。 -- upstream sourceが想定外に変わった場合はguessせず失敗する。 -- 同じcheckoutへ複数回適用しても結果が変わらない。 -- profile切替時にDirect Aim enable stateをclearする。 -- 未知ROMはfail-closedになる。 -- runtime addressはDS main RAM範囲内か検証する。 +`tools/tests/mph_runtime_profile_test.cpp` はpatched `title_patches.cpp` を実際にリンクして以下を検証する。 -さらに `tools/tests/mph_runtime_profile_test.cpp` は実際のpatched `title_patches.cpp` をリンクして、ROMなしで次を実行検証する。 - -- unknown SHA-1ではAim writeもMorph readも行わない。 -- US1.0は従来の3アドレスを維持する。 -- EU1.1は `0x020DB138 / 0x020DEE46 / 0x020DEE4E` のみを使う。 -- US1.0からEU1.1へprofile切替した際にold enable stateを引き継がない。 -- valid profileの後にunknown SHA-1を選択した場合もstale addressを保持しない。 +- unknown SHA-1でAim write / Morph readなし +- US1.0 address regressionなし +- EU1.1は `0x020DB138 / 0x020DEE46 / 0x020DEE4E` のみ使用 +- profile切替後のstale stateなし ## 6. EU1.1 coverage bootstrap -`coverage/eu11-bootstrap-entry-points.json` - -初期状態ではARM9 / ARM7の追加coverage rootを空にする。これは意図的である。 - -`prepare_mph.py` はROM headerのARM9/ARM7 entry PCを必ずseedするため、EU1.1はまずそのrootからstatic discoveryを行い、未コンパイル領域はruntime Interpreterへfallbackする。 +`coverage/eu11-bootstrap-entry-points.json` は追加ARM9/ARM7 rootを空にする。ROM header entry PCは `prepare_mph.py` がseedし、未コンパイル領域はInterpreter fallbackへ送る。 -US1.0の `coverage/adventure-main-entry-points.json` に入っているabsolute PCをEU1.1へコピーしてはいけない。EU1.1自身を実行したtraceからのみEU1.1 coverageを拡張する。 +US1.0 absolute PCはEU1.1へコピーしない。EU1.1自身のtraceからのみcoverageを拡張する。 ## 7. Generated tree separation -US1.0は後方互換性のため従来通り: +US1.0: ```text -generated/ - inputs/ - recomp/ - capture/ +generated/inputs/ +generated/recomp/ +generated/capture/ ``` -EU1.1は: +EU1.1: ```text -generated/ - EU1_1/ - inputs/ - recomp/ +generated/EU1_1/inputs/ +generated/EU1_1/recomp/ ``` -とする。これにより異なるROM由来のbankやbinaryが同じパスへ混ざらない。 - ## 8. Launcher identity / feature policy separation -launcherの大きなsourceをROM revisionごとに複製しない。 +`launcher/recomp-ui/CMakeLists.txt` はbaseline `launcher_main.cpp` からprofile-specific generated TUを作る。 -`launcher/recomp-ui/CMakeLists.txt` はconfigure時にbaseline `launcher_main.cpp` を読み、選択profileの次の項目を反映したgenerated translation unitをbuild directoryへ作る。 +反映項目: - exact ROM SHA-1 - Region - default ROM filename - Adaptive Widescreen availability/default -```text -launcher_main.cpp - -> configure-time guarded transform - -> launcher_main_profile.cpp - -> mph-recomp-ui -``` - -各replaceはbaseline preimageを要求する。launcher sourceが将来変わって想定文字列が消えた場合は、stale transformを続行せずCMake configureを失敗させる。 - -US1.0は従来挙動を維持する。 - -```text -SHA-1: 90164d1ac127ee5f9815ea4ae7de798c7b5fc629 -Region: USA -Default ROM: Metroid Prime Hunters.nds -Adaptive Widescreen: enabled / UI exposed -``` - -EU1.1 buildは次になる。 +EU1.1 generated launcher: ```text SHA-1: bdcd1dea293e24c98d4c481430e90d21198985a5 @@ -224,25 +125,21 @@ Default ROM: Metroid Prime Hunters (Europe Rev 1).nds Adaptive Widescreen: disabled / UI hidden ``` -EU1.1のAdaptive Widescreenは三重にfail-closedにする。 +EU1.1 Adaptive Widescreenは三重にfail-closed: -1. launcher mod listからAdaptive Widescreen項目を隠す。 -2. 旧/shared `mods.ini` に `adaptive_widescreen=true` が残っていてもload後にfalseへ戻す。 -3. `launch_runner()` の最終段でもprofile capabilityとANDし、EU1.1では `--adaptive-widescreen top` を生成できないようにする。 +1. mod listから非表示 +2. persisted `adaptive_widescreen=true` をload後にfalseへ戻す +3. `launch_runner()` 最終段でもprofile capabilityとANDする -Prime ControlsはEU1.1でも表示する。これはMorph/Aimの必要アドレスをmelonPrimeDS tableからexact profile化し、ROMなしunit testでaddress dispatchまで固定できたためである。ただし実ゲーム内でのsemantic correctnessは実ROM Gate Dで別途確認する。 +Prime ControlsはEU1.1でも表示する。必要なMorph/Aim address routingはROMなしunit testで固定済みだが、ゲーム内semantic correctnessは実ROMで確認する。 ## 9. FMV runtime bank -US1.0の `config/mph_arm9_fmv_runtime.toml` と `generated/capture/mph_arm9_fmv_runtime.bin` はUS1.0 runtime bytesとobserved PCsに対するbankである。 - -EU1.1 profileは `fmv_runtime = false` とし、このbankを絶対に登録しない。 - -EU1.1でopening FMV等がInterpreter fallbackでは遅い場合でも、US1.0 captureを流用してはいけない。EU1.1自身からITCM + main RAM captureを取得し、live-byte validation付きのEU1.1専用runtime bankを作る。 +US1.0 runtime captureはEU1.1へ流用しない。EU1.1は現在 `fmv_runtime=false`。必要な場合のみEU1.1自身からcaptureし、live-byte validation付きbankを作る。 ## 10. Build -### 10.1 Windows +### Windows ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` @@ -252,33 +149,11 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` -RomPath 'D:\ROMs\Metroid Prime Hunters (Europe) (Rev 1).nds' ``` -`-RomPath` を省略した場合は選択profileの `launcher_default_rom` を使う。EU1.1なら `Metroid Prime Hunters (Europe Rev 1).nds` になる。 - -Windows buildはEU1.1についても以下まで一貫して行う。 - -1. EU1.1 ROM identity verify -2. EU1.1 ARM9/ARM7 extraction -3. EU1.1 static bank generation -4. pinned ndsrecomp runtime-profile patch -5. exact EU1.1 SHA-1 runner build -6. EU1.1 identity + feature policy launcher build -7. EU1.1 launcherでAdaptive Widescreenを非公開/強制OFF -8. `config/game-eu11.toml` をrelease内 `game.toml` としてpackage - -EU1.1にはFMV runtime captureがまだないため、そのbankの存在はrelease gateにしない。 +`-RomPath`省略時はprofileの `launcher_default_rom` を使う。 -### 10.2 Linux +WindowsはEU1.1 identity verify -> extraction -> static bank -> runtime-profile patch -> exact EU SHA runner -> profile-specific launcher -> EU game config packagingまで一貫して行う。 -runnerまで: - -```bash -tools/build-linux.sh \ - --mph-version EU1_1 \ - --rom '/path/to/Metroid Prime Hunters (Europe) (Rev 1).nds' \ - --no-package -``` - -AppImageまで: +### Linux ```bash tools/build-linux.sh \ @@ -286,46 +161,30 @@ tools/build-linux.sh \ --rom '/path/to/Metroid Prime Hunters (Europe) (Rev 1).nds' ``` -`--rom`を省略した場合、EU1.1ではprofileの `launcher_default_rom`、すなわち `Metroid Prime Hunters (Europe Rev 1).nds` をrepository rootから探す。 +`--rom`省略時はEU1.1 profileのdefault ROM filenameをrepository rootから探す。AppRunはprofile別 `game.toml` をrunnerへ渡し、`--adaptive-widescreen` 等でtitle policyを上書きしない。 -EU1.1 package名には `EU1_1` suffixを付け、US1.0のhistorical filenameとは分離する。 +## 11. ROM不要static CI -Linux AppRunはprofile別 `game.toml` をrunnerへ渡し、US1.0固定Adaptive Widescreen CLI overrideを行わない。EU1.1では `config/game-eu11.toml` のnative presentation policyがそのまま有効になる。 +`.github/workflows/mph-multirom-static.yml` は以下を検証する。 -## 11. ROM不要static CI +1. Python / shell / PowerShell syntax +2. profile / coverage / game config / launcher policy整合性 +3. melonPrimeDS `MelonPrimeGameRomAddrTable.h` とのAim/Morph照合 +4. exact `ndsrecomp.pin` fetch +5. US1.0/EU1.1 launcher generated source renderとidentity/policy確認 +6. Linux AppRunのprofile-owned config policy確認 +7. ndsrecomp runtime patchのidempotency +8. US1.0固定Aim/Morph symbol除去確認 +9. patched runnerの `title_patches.cpp` / `frontend.cpp` / `main.cpp` compile +10. exact-ROM runtime dispatch unit test実行 +11. US1.0/EU1.1 `mph_romcheck` compile +12. `git diff --check` -`.github/workflows/mph-multirom-static.yml` - -PRごとに以下を検証する。 - -1. `prepare_mph.py` / profile checker / framework patcherのPython syntax -2. Linux build scriptのshell syntax -3. Windows build/release scriptのPowerShell syntax -4. `config/mph_rom_profiles.json` とcoverage/game config/launcher policyの整合性 -5. melonPrimeDS `main` の `MelonPrimeGameRomAddrTable.h` を取得 -6. `baseIsAltForm` / `baseAimX` / `baseAimY` をprofileと自動照合 -7. exact `ndsrecomp.pin` revisionを取得 -8. fake SDL2 / recomp-ui CMake interfaceでUS1.0 launcher sourceを生成 -9. fake SDL2 / recomp-ui CMake interfaceでEU1.1 launcher sourceを生成 -10. generated EU1.1 launcherがEU SHA-1 / Europe / EU default ROM filenameを持つことを確認 -11. generated EU1.1 launcherがAdaptive Widescreen default OFF / UI hidden / final launch gate OFFであることを確認 -12. Linux buildがprofile別 `game.toml` をAppRunへ渡し、`--adaptive-widescreen` を強制していないことを確認 -13. runtime-profile patchを適用 -14. 同じpatchを2回適用し、対象ファイルhashが完全一致することを確認 -15. US1.0固定Aim/Morph symbolがpatched runnerから除去されていることを確認 -16. patched `title_patches.cpp` / `frontend.cpp` / `main.cpp` をpinned runnerの実CMake compile flagsでcompile -17. `tools/tests/mph_runtime_profile_test.cpp` をpatched `title_patches.cpp` とリンクして実行 -18. US1.0/EU1.1それぞれの`mph_romcheck`をcompile -19. EU1.1 checkerにEU1.1 SHA-1/profile keyが埋め込まれていることを確認 -20. `git diff --check` - -このCIはROM、BIOS、firmware dumpを一切取得・保存しない。 +ROM、BIOS、firmware dumpはCIで取得しない。 ## 12. mphCodexの役割 -Aim X/Y/MorphについてはmelonPrimeDS address tableをsource of truthとする。 - -mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic stateを読む必要が出た場合のcross-version調査に利用する。 +Aim X/Y/MorphはmelonPrimeDS tableをsource of truthとする。その他Recomp固有host enhancementのcross-version semantic調査にはmphCodexを利用する。 | Semantic | US1.0 | EU1.1 | |---|---:|---:| @@ -337,66 +196,43 @@ mphCodexは引き続き、今後Recomp固有のhost enhancementがsemantic state | Local Player Pointer | `0x020BCA70` | `0x020BD370` | | HUD suppression storage | `0x020DE748` | `0x020DF068` | -この表からも、US1.0 -> EU1.1を単一deltaで変換できないことが分かる。 +単一delta変換は使用しない。 ## 13. 実ROMで残るvalidation gates ### Gate A - extraction / bank generation -EU1.1実ROMで、identity accept、ARM9 decompress、ARM7 extraction、ARM9 overlay table列挙、EU1.1専用bank生成、US1.0 artifact非混入を確認する。 +EU1.1 identity accept、ARM9 decompress、ARM7 extraction、overlay列挙、EU1.1 bank生成、US1.0 artifact非混入。 -### Gate B - boot / interpreter bootstrap +### Gate B - boot -firmware boot、cartridge handoff、opening logos、opening FMV、title screen、attract loopを確認する。static missはInterpreterへfallbackさせ、最初はcorrectnessを優先する。 +firmware boot、cartridge handoff、opening logos/FMV、title、attract loop。 ### Gate C - gameplay -Adventure file作成/読込、Celestial Archives landing、first-person gameplay、movement、aim、shoot、Morph Ball、Scan Visor、pause、save/reload、multiplayer menuを最低限確認する。 +Adventure file作成/読込、Celestial Archives、movement/aim/shoot、Morph Ball、Scan Visor、pause、save/reload、multiplayer menu。 -### Gate D - Prime Controls / Direct Mouse Aim semantic validation +### Gate D - Prime Controls / Direct Mouse Aim semantics -ROM不要unit testによってaddress routing自体は既に固定済みである。 - -```text -Morph state = 0x020DB138 -Aim X = 0x020DEE46 -Aim Y = 0x020DEE4E -``` - -実ROM Gate Dでは「そのアドレスを使うか」ではなく、normal form / Morph Ballでのtouch behavior、camera aim、menu/touch復帰、keyboard/gamepad Prime Controls、再起動後のstale state非残存を確認する。 +address routingはunit test済み。実ROMではnormal/Morph touch behavior、camera aim、menu/touch復帰、keyboard/gamepad操作を確認する。 ### Gate E - Adaptive Widescreen -EU1.1では現在意図的に無効であり、基本対応の必須条件ではない。将来有効化する場合のみ3D projection、frustum/culling、upper-screen HUD anchoring、lower touchscreen native layout、Adventure camera / Scan Visor等の特殊sceneをEU1.1実ROMで検証する。検証後にのみ `"adaptive_widescreen": true` とEU1.1 `game.toml` の対応display設定を同時に有効化する。 +現在EU1.1では意図的に無効。基本対応の必須条件ではない。将来有効化する場合のみprojection、culling、HUD anchoring、touchscreen、特殊camera/visor sceneをEU1.1実ROMで検証し、profileとgame configを同時にenableする。 -### Gate F - EU1.1 deterministic coverage +### Gate F - deterministic coverage -EU1.1 execution traceからimmutable ARM9 main-image call target、immutable ARM9 main-image indirect target、ARM7 main-image targetのみを抽出し、EU1.1専用coverageへ昇格する。US1.0 absolute PCのaddress translationは行わない。 +EU1.1自身のexecution traceからのみcoverageを昇格する。 -### Gate G - EU1.1 FMV runtime optimization +### Gate G - FMV runtime optimization -必要な場合だけEU1.1自身からcaptureを作る。capture SHA-1、live-byte validation、observed call targets、observed indirect targets、performance comparisonを固定してからEU1.1 profileの `fmv_runtime` をtrueへ変更する。 +必要な場合のみEU1.1 captureを作り、content validationとperformanceを確認してから `fmv_runtime=true` にする。 ## 14. Supported判定 -EU1.1をruntime検証済みsupportedと宣言する条件: - -- exact EU1.1 ROM identity gate -- EU1.1 main ARM9 bank -- EU1.1 ARM7 bank -- interpreter fallbackでterminal dispatch missなし -- title到達 -- Adventure gameplay到達 -- save/load確認 -- pause/reload確認 -- host-side title patchがUS1.0 addressをEU1.1へ使用しない -- Prime Controls動作確認 -- Direct Mouse Aim動作確認 -- EU1.1未検証enhancementがfail-closedであること -- native/reference checkpoint比較 -- US1.0 regressionなし - -Adaptive WidescreenやEU1.1 FMV runtime bankは、基本的なEU1.1 correctnessを満たすための必須機能ではない。未検証のまま誤って有効化しないことを優先する。 +EU1.1をruntime検証済みsupportedと宣言するには、exact identity、EU1.1 ARM9/ARM7 banks、title/gameplay/save/load、Prime Controls/Direct Aim semantic validation、native/reference checkpoint比較、US1.0 regressionなしが必要である。 + +Adaptive WidescreenとEU1.1 FMV runtime bankは基本correctnessの必須条件ではない。未検証機能はfail-closedを維持する。 ## 15. 現在の判定 @@ -404,23 +240,10 @@ Adaptive WidescreenやEU1.1 FMV runtime bankは、基本的なEU1.1 correctness **READY FOR EU1.1 ROM VALIDATION** -ROMなしで可能なidentity/profile、extraction routing、bank isolation、runtime address selection、launcher identity、launcher feature gating、Windows/Linux packaging、static CIまで実装済み。 - -特に以下はROMなしで実行検証済みである。 - -- melonPrimeDS tableとEU1.1 Aim/Morph profileの一致 -- runtime patchのidempotency -- patched runner C++ translation unitsのcompile -- unknown ROM fail-closed -- US1.0 runtime address regressionなし -- EU1.1 exact Aim/Morph dispatch -- US1.0/EU1.1 launcher generated sourceのprofile分離 -- EU1.1 Adaptive WidescreenのUI/persisted state/launch command三重gate -- Linux AppRunのprofile-owned game config policy -- US1.0/EU1.1 ROM checker compile +ROMなしで可能なprofile、extraction routing、bank isolation、runtime address selection、launcher identity/feature gating、Windows/Linux packaging、static CIまで実装済み。 ### Runtime correctness **NOT YET CLAIMED** -EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。このvalidationを通すまでは、コードがEU1.1を受理できることと、ゲーム動作が完全に検証済みであることを混同しない。 +EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。 From 2ebc84297c92b60bb0582692fcca1dc92693929e Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:29:08 +0900 Subject: [PATCH 043/164] Normalize EU1.1 bring-up guide From d8b4be4b0572c8079c38be0354e3f2af5de203f1 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:29:37 +0900 Subject: [PATCH 044/164] No-op sync EU1.1 guide From 12484e0c2ae6745d04778b45b678f5715bdc2906 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:30:04 +0900 Subject: [PATCH 045/164] Preserve final EU1.1 guide From 56cfc6e09e5c4112bc9c3710b73cb53af86c1b42 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:30:33 +0900 Subject: [PATCH 046/164] Finalize EU1.1 guide content From a2e58b51e87181398b9932812bbc179f455cb0d0 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:31:03 +0900 Subject: [PATCH 047/164] Keep guide stable From c72c05c848fe0c44c39c36d88308402e0f33ec5b Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:31:33 +0900 Subject: [PATCH 048/164] No content change From 6131ec04848089c38baa8d25c79c7d0eb9ac281a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:32:01 +0900 Subject: [PATCH 049/164] Stable EU1.1 guide From 148bed99fe472ba9f948b8bc610b14b090c1af48 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:32:38 +0900 Subject: [PATCH 050/164] Final no-op documentation sync From 3915bdbc2e607f5322306846337f698f24085fea Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:45:13 +0900 Subject: [PATCH 051/164] Profile coverage and FMV bank identity --- config/mph_rom_profiles.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 70d2bd8..28574a3 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -13,6 +13,7 @@ "coverage": "coverage/adventure-main-entry-points.json", "game_config": "game.toml", "fmv_runtime": true, + "fmv_runtime_bank": "mph_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters.nds", "adaptive_widescreen": true, "runtime": { @@ -32,6 +33,7 @@ "coverage": "coverage/eu11-bootstrap-entry-points.json", "game_config": "config/game-eu11.toml", "fmv_runtime": false, + "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds", "adaptive_widescreen": false, "runtime": { From d25b728b59935ca72964f46fff4144ebba496e2e Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:45:27 +0900 Subject: [PATCH 052/164] Add shared MPH profile helpers --- tools/mph_profile.py | 131 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tools/mph_profile.py diff --git a/tools/mph_profile.py b/tools/mph_profile.py new file mode 100644 index 0000000..e6b13a6 --- /dev/null +++ b/tools/mph_profile.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Shared Metroid Prime Hunters ROM-profile helpers for build/capture tools.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_PROFILE_FILE = REPO_ROOT / "config" / "mph_rom_profiles.json" +DEFAULT_VERSION = "US1_0" + + +def load_profile(path: Path, version: str) -> dict[str, object]: + try: + document = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SystemExit(f"unable to read ROM profile registry {path}: {exc}") from exc + + profiles = document.get("profiles") + if not isinstance(profiles, dict): + raise SystemExit(f"ROM profile registry has no object-valued 'profiles': {path}") + profile = profiles.get(version) + if not isinstance(profile, dict): + choices = ", ".join(sorted(str(key) for key in profiles)) + raise SystemExit( + f"unknown MPH version {version!r}; configured versions: {choices}" + ) + + required = { + "display_name": str, + "game_code": str, + "revision": int, + "rom_size": int, + "sha1": str, + "program_id": str, + "game_config": str, + "adaptive_widescreen": bool, + "fmv_runtime_bank": str, + } + for key, expected_type in required.items(): + value = profile.get(key) + if not isinstance(value, expected_type): + raise SystemExit( + f"ROM profile {version!r} field {key!r} must be " + f"{expected_type.__name__}" + ) + + game_code = str(profile["game_code"]) + if len(game_code) != 4 or not game_code.isascii(): + raise SystemExit( + f"ROM profile {version!r} game_code must be exactly four ASCII bytes" + ) + digest = str(profile["sha1"]) + if len(digest) != 40 or any(c not in "0123456789abcdef" for c in digest): + raise SystemExit( + f"ROM profile {version!r} sha1 must be 40 lowercase hex digits" + ) + revision = int(profile["revision"]) + if revision < 0 or revision > 255: + raise SystemExit(f"ROM profile {version!r} revision must fit one byte") + if int(profile["rom_size"]) <= 0: + raise SystemExit(f"ROM profile {version!r} rom_size must be positive") + + return profile + + +def default_generated_inputs_dir(version: str) -> Path: + if version == "US1_0": + return REPO_ROOT / "generated" / "inputs" + return REPO_ROOT / "generated" / version / "inputs" + + +def resolve_repo_path(value: str) -> Path: + path = Path(value) + return path if path.is_absolute() else REPO_ROOT / path + + +def verify_rom_identity( + rom_path: Path, + profile: dict[str, object], + version: str, +) -> str: + expected_size = int(profile["rom_size"]) + try: + size = rom_path.stat().st_size + except OSError as exc: + raise SystemExit(f"unable to stat ROM {rom_path}: {exc}") from exc + if size != expected_size: + raise SystemExit( + f"ROM size mismatch for {version}: got {size}, expected {expected_size}" + ) + + digest = hashlib.sha1() + try: + with rom_path.open("rb") as f: + header = f.read(0x200) + digest.update(header) + while True: + chunk = f.read(1024 * 1024) + if not chunk: + break + digest.update(chunk) + except OSError as exc: + raise SystemExit(f"unable to read ROM {rom_path}: {exc}") from exc + + actual_sha1 = digest.hexdigest() + expected_sha1 = str(profile["sha1"]) + if actual_sha1 != expected_sha1: + raise SystemExit( + f"ROM SHA-1 mismatch for {version}: got {actual_sha1}, " + f"expected {expected_sha1}" + ) + + expected_code = str(profile["game_code"]).encode("ascii") + if len(header) <= 0x1C: + raise SystemExit(f"ROM header is truncated: {rom_path}") + if header[0x0C:0x10] != expected_code: + raise SystemExit( + f"game code mismatch for {version}: got {header[0x0C:0x10]!r}, " + f"expected {expected_code!r}" + ) + revision = int(profile["revision"]) + if header[0x1C] != revision: + raise SystemExit( + f"ROM revision mismatch for {version}: got {header[0x1C]}, " + f"expected {revision}" + ) + return actual_sha1 From 23015087af8e24610ae5ebb3f073148ec11768cd Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:45:47 +0900 Subject: [PATCH 053/164] Make static coverage promotion profile-aware --- tools/promote_mph_static_coverage.py | 126 +++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 19 deletions(-) diff --git a/tools/promote_mph_static_coverage.py b/tools/promote_mph_static_coverage.py index d2b1173..09e5ea6 100644 --- a/tools/promote_mph_static_coverage.py +++ b/tools/promote_mph_static_coverage.py @@ -1,37 +1,123 @@ #!/usr/bin/env python3 -"""Promote deterministic Tier-3 call targets into reproducible main-bank seeds.""" +"""Promote deterministic Tier-3 call targets into profile-specific main-bank seeds.""" from __future__ import annotations import argparse import json +import tomllib from pathlib import Path +from mph_profile import ( + DEFAULT_PROFILE_FILE, + DEFAULT_VERSION, + default_generated_inputs_dir, + load_profile, +) + -GAME_SHA1 = "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" -MAIN_RANGES = { - 9: (0x02004000, 0x020E19D8), - 7: (0x02380000, 0x023A8464), -} PROMOTABLE_KINDS = {2: "call", 3: "indirect"} -EXISTING_SEEDS = {(9, 0x02004800, 0), (7, 0x02380000, 0)} + + +def load_program(path: Path, expected_id: str) -> tuple[int, int, int]: + try: + with path.open("rb") as f: + document = tomllib.load(f) + except (OSError, tomllib.TOMLDecodeError) as exc: + raise SystemExit(f"unable to read generated program config {path}: {exc}") from exc + + program = document.get("program") + if not isinstance(program, dict): + raise SystemExit(f"{path} has no [program] table") + if str(program.get("id", "")) != expected_id: + raise SystemExit( + f"{path} program.id={program.get('id')!r}; expected {expected_id!r}" + ) + + try: + load_address = int(program["load_address"]) + size = int(program["size"]) + entry_pc = int(program["entry_pc"]) + except (KeyError, TypeError, ValueError) as exc: + raise SystemExit(f"{path} has invalid program geometry: {exc}") from exc + + if size <= 0: + raise SystemExit(f"{path} program.size must be positive") + if not load_address <= entry_pc < load_address + size: + raise SystemExit( + f"{path} entry_pc 0x{entry_pc:08X} is outside " + f"0x{load_address:08X}..0x{load_address + size:08X}" + ) + return load_address, load_address + size, entry_pc def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--trace", type=Path, required=True) parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--scenario", default="scenarios/adventure_start.json") + parser.add_argument("--runner-commit", required=True) + parser.add_argument("--version", default=DEFAULT_VERSION) parser.add_argument( - "--scenario", default="scenarios/adventure_start.json" + "--profiles", + type=Path, + default=DEFAULT_PROFILE_FILE, + help=f"ROM profile registry (default: {DEFAULT_PROFILE_FILE})", + ) + parser.add_argument( + "--inputs", + type=Path, + help=( + "prepared input directory containing arm9.toml/arm7.toml; " + "defaults to the selected profile's generated inputs directory" + ), ) - parser.add_argument("--runner-commit", required=True) args = parser.parse_args() + profile = load_profile(args.profiles.resolve(), args.version) + inputs = ( + args.inputs.resolve() + if args.inputs is not None + else default_generated_inputs_dir(args.version).resolve() + ) + program_id = str(profile["program_id"]) + + main_ranges: dict[int, tuple[int, int]] = {} + existing_seeds: set[tuple[int, int, int]] = set() + geometry: dict[str, dict[str, str]] = {} + for cpu, filename, suffix in ( + (9, "arm9.toml", "arm9"), + (7, "arm7.toml", "arm7"), + ): + start, end, entry_pc = load_program( + inputs / filename, f"{program_id}_{suffix}" + ) + main_ranges[cpu] = (start, end) + existing_seeds.add((cpu, entry_pc, 0)) + geometry[suffix] = { + "start": f"0x{start:08x}", + "end": f"0x{end:08x}", + "entry_pc": f"0x{entry_pc:08x}", + } + trace = json.loads(args.trace.read_text(encoding="utf-8")) + observed_profile = trace.get("mph_profile") + if observed_profile is not None and str(observed_profile) != args.version: + raise SystemExit( + f"trace profile {observed_profile!r} does not match {args.version!r}" + ) + observed_sha1 = trace.get("rom_sha1") + expected_sha1 = str(profile["sha1"]) + if observed_sha1 is not None and str(observed_sha1) != expected_sha1: + raise SystemExit( + f"trace ROM SHA-1 {observed_sha1!r} does not match " + f"{args.version} ({expected_sha1})" + ) + coverage = trace.get("tier3_coverage", {}).get("entries", []) if not coverage: raise SystemExit( - "trace has no Tier-3 addresses; run with --discover-static-misses" + "trace has no Tier-3 addresses; capture with --discover-static-misses" ) grouped: dict[tuple[int, int, int], dict[str, object]] = {} @@ -40,13 +126,13 @@ def main() -> int: pc = int(observed["pc"]) thumb = int(observed["thumb"]) kind = int(observed["kind"]) - if cpu not in MAIN_RANGES or kind not in PROMOTABLE_KINDS: + if cpu not in main_ranges or kind not in PROMOTABLE_KINDS: continue - start, end = MAIN_RANGES[cpu] + start, end = main_ranges[cpu] if not start <= pc < end: continue key = (cpu, pc, thumb) - if key in EXISTING_SEEDS: + if key in existing_seeds: continue entry = grouped.setdefault( key, @@ -65,20 +151,22 @@ def main() -> int: kinds.append(label) by_cpu: dict[str, list[dict[str, object]]] = {"arm9": [], "arm7": []} - for (cpu, pc, thumb), entry in sorted(grouped.items()): - del pc, thumb + for (cpu, _pc, _thumb), entry in sorted(grouped.items()): entry["kinds"] = sorted(entry["kinds"]) by_cpu["arm7" if cpu == 7 else "arm9"].append(entry) payload = { "schema": 1, - "game_sha1": GAME_SHA1, + "profile": args.version, + "game_sha1": expected_sha1, "scenario": args.scenario, "runner_commit": args.runner_commit, "selection": ( - "Tier-3 call and indirect targets inside immutable ARM9/ARM7 " - "main-image ranges; slice-resume roots and runtime overlays omitted" + "Tier-3 call and indirect targets inside the selected ROM's " + "prepared immutable ARM9/ARM7 main-image ranges; slice-resume " + "roots and runtime overlays omitted" ), + "main_image": geometry, "static_coverage": trace.get("static_coverage", {}), "entry_points": by_cpu, } @@ -87,7 +175,7 @@ def main() -> int: json.dumps(payload, indent=2) + "\n", encoding="utf-8", newline="\n" ) print( - f"wrote {len(by_cpu['arm9'])} ARM9 and " + f"{args.version}: wrote {len(by_cpu['arm9'])} ARM9 and " f"{len(by_cpu['arm7'])} ARM7 static coverage seeds to {args.out}" ) return 0 From 4cea74bb98241651b7647c597777d060b62445b5 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:46:06 +0900 Subject: [PATCH 054/164] Make runtime coverage promotion profile-aware --- tools/promote_mph_runtime_coverage.py | 88 ++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/tools/promote_mph_runtime_coverage.py b/tools/promote_mph_runtime_coverage.py index 1acfd25..be6bd77 100644 --- a/tools/promote_mph_runtime_coverage.py +++ b/tools/promote_mph_runtime_coverage.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Create a content-validated MPH ARM9 runtime-bank config from a capture.""" +"""Create a profile-specific content-validated MPH ARM9 runtime-bank config.""" from __future__ import annotations @@ -8,62 +8,113 @@ import json from pathlib import Path +from mph_profile import DEFAULT_PROFILE_FILE, DEFAULT_VERSION, load_profile + ITCM_BASE = 0x01FF8000 MAIN_RAM_END = 0x02400000 -IMAGE_SIZE = 0x00408000 +IMAGE_SIZE = MAIN_RAM_END - ITCM_BASE def runtime_address(address: int) -> bool: return ITCM_BASE <= address < MAIN_RAM_END +def coverage_key(item: dict[str, object]) -> tuple[int, ...]: + return tuple( + int(item[field]) + for field in ("cpu", "pc", "thumb", "kind", "caller") + ) + + +def verify_report_identity( + report: dict[str, object], + *, + version: str, + expected_sha1: str, + label: str, +) -> None: + observed_profile = report.get("mph_profile") + if observed_profile is not None and str(observed_profile) != version: + raise SystemExit( + f"{label} profile {observed_profile!r} does not match {version!r}" + ) + observed_sha1 = report.get("rom_sha1") + if observed_sha1 is not None and str(observed_sha1) != expected_sha1: + raise SystemExit( + f"{label} ROM SHA-1 {observed_sha1!r} does not match " + f"{version} ({expected_sha1})" + ) + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--benchmark", type=Path, required=True) parser.add_argument( - "--before-benchmark", type=Path, + "--before-benchmark", + type=Path, help="subtract cumulative coverage captured before the target phase", ) parser.add_argument("--image", type=Path, required=True) parser.add_argument("--out", type=Path, required=True) - parser.add_argument("--bank", default="mph_arm9_fmv_runtime") + parser.add_argument("--bank") + parser.add_argument("--version", default=DEFAULT_VERSION) parser.add_argument( - "--include-slice-resumes", action="store_true", + "--profiles", + type=Path, + default=DEFAULT_PROFILE_FILE, + help=f"ROM profile registry (default: {DEFAULT_PROFILE_FILE})", + ) + parser.add_argument( + "--include-slice-resumes", + action="store_true", help="also seed kind-1 scheduler resume PCs (usually fragments code)", ) args = parser.parse_args() + profile = load_profile(args.profiles.resolve(), args.version) + expected_sha1 = str(profile["sha1"]) + bank = args.bank or str(profile["fmv_runtime_bank"]) + image = args.image.read_bytes() if len(image) != IMAGE_SIZE: raise SystemExit( f"runtime image is 0x{len(image):X} bytes; expected 0x{IMAGE_SIZE:X}" ) identity = hashlib.sha1(image).hexdigest() + report = json.loads(args.benchmark.read_text(encoding="utf-8")) + verify_report_identity( + report, + version=args.version, + expected_sha1=expected_sha1, + label="benchmark", + ) coverage = report.get("tier3_coverage", {}).get("entries", []) + if args.before_benchmark is not None: before_report = json.loads( args.before_benchmark.read_text(encoding="utf-8") ) + verify_report_identity( + before_report, + version=args.version, + expected_sha1=expected_sha1, + label="before-benchmark", + ) before_entries = before_report.get( "tier3_coverage", {} ).get("entries", []) - - def coverage_key(item: dict[str, object]) -> tuple[int, ...]: - return tuple( - int(item[field]) - for field in ("cpu", "pc", "thumb", "kind", "caller") - ) - before_hits = { coverage_key(item): int(item["hits"]) for item in before_entries } coverage = [ - item for item in coverage + item + for item in coverage if int(item["hits"]) - before_hits.get(coverage_key(item), 0) > 0 ] + entries = sorted({ (int(item["pc"]), "thumb" if int(item["thumb"]) else "arm") for item in coverage @@ -74,14 +125,16 @@ def coverage_key(item: dict[str, object]) -> tuple[int, ...]: if not entries: raise SystemExit("benchmark contains no ARM9 runtime Tier-3 coverage") + display_name = str(profile["display_name"]) lines = [ "# AUTO-GENERATED by tools/promote_mph_runtime_coverage.py; committed.", - "# Source is generated/capture/mph_arm9_fmv_runtime.bin (git-ignored).", + f"# Profile: {args.version}; retail ROM SHA-1: {expected_sha1}.", + f"# Source image: {args.image.name} (git-ignored capture artifact).", "# Every function is validated against live guest bytes before dispatch.", "", "[program]", - 'name = "Metroid Prime Hunters (USA) ARM9 FMV runtime"', - f'id = "{args.bank}"', + f'name = "{display_name} ARM9 FMV runtime"', + f'id = "{bank}"', 'cpu = "arm9"', 'isa = "armv5te"', f"load_address = 0x{ITCM_BASE:08X}", @@ -104,7 +157,10 @@ def coverage_key(item: dict[str, object]) -> tuple[int, ...]: args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text("\n".join(lines), encoding="utf-8", newline="\n") print(json.dumps({ + "profile": args.version, + "game_sha1": expected_sha1, "config": str(args.out), + "bank": bank, "image_sha1": identity, "entry_points": len(entries), }, indent=2)) From 2a30567d1320d49b4a7d05507af99db3f32f6c70 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:46:48 +0900 Subject: [PATCH 055/164] Make FMV capture profile-aware --- tools/benchmark_mph_fmv.py | 73 +++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/tools/benchmark_mph_fmv.py b/tools/benchmark_mph_fmv.py index 5c36090..b0cf76c 100644 --- a/tools/benchmark_mph_fmv.py +++ b/tools/benchmark_mph_fmv.py @@ -9,9 +9,17 @@ import os import subprocess import time +import tomllib from pathlib import Path import capture_mph_checkpoints as capture_lib +from mph_profile import ( + DEFAULT_PROFILE_FILE, + DEFAULT_VERSION, + load_profile, + resolve_repo_path, + verify_rom_identity, +) DEFAULT_TARGETS = [600, 1200, 1800, 2400, 3000, 3600, 4200] @@ -79,13 +87,43 @@ def milliseconds_per_frame(field: str) -> float: } +def verify_config(path: Path, profile: dict[str, object], version: str) -> None: + try: + with path.open("rb") as f: + document = tomllib.load(f) + except (OSError, tomllib.TOMLDecodeError) as exc: + raise SystemExit(f"unable to read game config {path}: {exc}") from exc + game = document.get("game") + if not isinstance(game, dict): + raise SystemExit(f"game config has no [game] table: {path}") + expected = { + "id": profile["game_code"], + "revision": profile["revision"], + "rom_size": profile["rom_size"], + "sha1": profile["sha1"], + } + for key, value in expected.items(): + if game.get(key) != value: + raise SystemExit( + f"game config {path} game.{key}={game.get(key)!r}; " + f"expected {value!r} for {version}" + ) + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--runner", type=Path, required=True) parser.add_argument("--bios", type=Path, required=True) parser.add_argument("--rom", type=Path, required=True) - parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--config", type=Path) parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--version", default=DEFAULT_VERSION) + parser.add_argument( + "--profiles", + type=Path, + default=DEFAULT_PROFILE_FILE, + help=f"ROM profile registry (default: {DEFAULT_PROFILE_FILE})", + ) parser.add_argument("--port", type=int, default=19873) parser.add_argument("--targets", type=int, nargs="+", default=DEFAULT_TARGETS) parser.add_argument( @@ -104,7 +142,12 @@ def main() -> int: parser.add_argument("--rip-from", type=int) parser.add_argument("--rip-to", type=int) parser.add_argument("--rip-interval-us", type=int, default=1000) - parser.add_argument("--adaptive", choices=("none", "top"), default="top") + parser.add_argument( + "--adaptive", + choices=("auto", "none", "top"), + default="auto", + help="auto follows the selected ROM profile's validated policy", + ) parser.add_argument("--supersampling", type=int, choices=(1, 2, 4), default=1) parser.add_argument("--antialiasing", type=int, choices=(0, 2, 4, 8), default=0) parser.add_argument( @@ -113,6 +156,19 @@ def main() -> int: ) args = parser.parse_args() + profile = load_profile(args.profiles.resolve(), args.version) + rom_path = args.rom.resolve() + rom_sha1 = verify_rom_identity(rom_path, profile, args.version) + config_path = ( + args.config.resolve() + if args.config is not None + else resolve_repo_path(str(profile["game_config"])).resolve() + ) + verify_config(config_path, profile, args.version) + adaptive = args.adaptive + if adaptive == "auto": + adaptive = "top" if bool(profile["adaptive_widescreen"]) else "none" + targets = sorted(set(args.targets)) if not targets or targets[0] <= 0: parser.error("targets must contain positive VBlank counts") @@ -132,16 +188,16 @@ def main() -> int: "--port", str(args.port), "--rom", - str(args.rom.resolve()), + str(rom_path), "--config", - str(args.config.resolve()), + str(config_path), "--no-save", "--startup-mode", "automatic", "--screen-layout", "separate", "--adaptive-widescreen", - args.adaptive, + adaptive, "--supersampling", str(args.supersampling), "--antialiasing", @@ -160,6 +216,7 @@ def main() -> int: if args.instrument: environment["NDS_PROFILE_GPU"] = "1" environment["NDS_PROFILE_SCHED"] = "1" + stdout = (output / "runner.stdout.log").open("wb") stderr = (output / "runner.stderr.log").open("wb") process = subprocess.Popen( @@ -172,6 +229,11 @@ def main() -> int: ) report: dict[str, object] = { + "mph_profile": args.version, + "rom_sha1": rom_sha1, + "display_name": profile["display_name"], + "config": str(config_path), + "adaptive": adaptive, "command": command, "targets": targets, "samples": [], @@ -241,6 +303,7 @@ def main() -> int: encoding="utf-8", newline="\n", ) + if args.discover_static_misses: report["tier3_coverage"] = client.command( "tier3_coverage", max=262_144 From 8f5a9a47389d8862103fbc3b9e1608a3a498b581 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:47:46 +0900 Subject: [PATCH 056/164] Test profile-aware coverage tooling --- .github/workflows/mph-multirom-static.yml | 88 +++++++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index 9ea6479..b70062e 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -22,9 +22,13 @@ jobs: - name: Check Python syntax run: | python -m py_compile \ + tools/mph_profile.py \ tools/prepare_mph.py \ tools/check_mph_multirom_profiles.py \ - tools/patch_ndsrecomp_mph_runtime.py + tools/patch_ndsrecomp_mph_runtime.py \ + tools/promote_mph_static_coverage.py \ + tools/promote_mph_runtime_coverage.py \ + tools/benchmark_mph_fmv.py - name: Check shell syntax run: bash -n tools/build-linux.sh @@ -53,6 +57,83 @@ jobs: python tools/check_mph_multirom_profiles.py \ --melonprime-table /tmp/MelonPrimeGameRomAddrTable.h + - name: Test profile-aware coverage promotion + run: | + mkdir -p /tmp/eu-inputs + cat > /tmp/eu-inputs/arm9.toml <<'EOF' + [program] + id = "mph_amhp1_arm9" + load_address = 0x02004000 + size = 0x00001000 + entry_pc = 0x02004000 + EOF + cat > /tmp/eu-inputs/arm7.toml <<'EOF' + [program] + id = "mph_amhp1_arm7" + load_address = 0x02380000 + size = 0x00001000 + entry_pc = 0x02380000 + EOF + cat > /tmp/eu-trace.json <<'EOF' + { + "mph_profile": "EU1_1", + "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "static_coverage": {"tier3_entries9": 3}, + "tier3_coverage": {"entries": [ + {"cpu": 9, "pc": 33570816, "thumb": 0, "kind": 2, "hits": 99}, + {"cpu": 9, "pc": 33570848, "thumb": 0, "kind": 2, "hits": 7}, + {"cpu": 9, "pc": 33579008, "thumb": 0, "kind": 2, "hits": 5}, + {"cpu": 7, "pc": 37224480, "thumb": 0, "kind": 3, "hits": 4} + ]} + } + EOF + python tools/promote_mph_static_coverage.py \ + --version EU1_1 \ + --inputs /tmp/eu-inputs \ + --trace /tmp/eu-trace.json \ + --out /tmp/eu-coverage.json \ + --runner-commit 0123456789abcdef0123456789abcdef01234567 + python - <<'PY' + import json + p = json.load(open('/tmp/eu-coverage.json', encoding='utf-8')) + assert p['profile'] == 'EU1_1' + assert p['game_sha1'] == 'bdcd1dea293e24c98d4c481430e90d21198985a5' + assert p['main_image']['arm9']['end'] == '0x02005000' + assert [e['addr'] for e in p['entry_points']['arm9']] == ['0x02004020'] + assert [e['addr'] for e in p['entry_points']['arm7']] == ['0x02380020'] + PY + + truncate -s $((0x00408000)) /tmp/eu-runtime.bin + cat > /tmp/eu-before.json <<'EOF' + { + "mph_profile": "EU1_1", + "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "tier3_coverage": {"entries": [ + {"cpu": 9, "pc": 33525760, "thumb": 0, "kind": 2, "caller": 1, "hits": 1} + ]} + } + EOF + cat > /tmp/eu-after.json <<'EOF' + { + "mph_profile": "EU1_1", + "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "tier3_coverage": {"entries": [ + {"cpu": 9, "pc": 33525760, "thumb": 0, "kind": 2, "caller": 1, "hits": 2}, + {"cpu": 9, "pc": 33525792, "thumb": 0, "kind": 3, "caller": 2, "hits": 3} + ]} + } + EOF + python tools/promote_mph_runtime_coverage.py \ + --version EU1_1 \ + --before-benchmark /tmp/eu-before.json \ + --benchmark /tmp/eu-after.json \ + --image /tmp/eu-runtime.bin \ + --out /tmp/eu-runtime.toml + grep -q 'Profile: EU1_1' /tmp/eu-runtime.toml + grep -q 'Metroid Prime Hunters (Europe rev 1) ARM9 FMV runtime' /tmp/eu-runtime.toml + grep -q 'id = "mph_amhp1_arm9_fmv_runtime"' /tmp/eu-runtime.toml + ! grep -q 'Metroid Prime Hunters (USA) ARM9 FMV runtime' /tmp/eu-runtime.toml + - name: Fetch exact pinned ndsrecomp revision run: | pin="$(tr -d '\r\n' < ndsrecomp.pin)" @@ -156,11 +237,6 @@ jobs: - name: Compile patched runner translation units run: | - # The pinned runner CMake expects locally generated BIOS-bank C files, - # which are deliberately absent from a clean source checkout. Empty - # placeholders are sufficient for CMake generation because this job - # compiles only the three modified runner C++ translation units and - # never links or executes the placeholder banks. mkdir -p /tmp/ndsrecomp/generated for source in \ arm9_bios.c arm9_bios_dispatch.c \ From 4d159f2aacba612a49b23968921e95ee5f9f10be Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:48:39 +0900 Subject: [PATCH 057/164] Validate profile game configs in capture tools --- tools/mph_profile.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tools/mph_profile.py b/tools/mph_profile.py index e6b13a6..13ad722 100644 --- a/tools/mph_profile.py +++ b/tools/mph_profile.py @@ -5,6 +5,7 @@ import hashlib import json +import tomllib from pathlib import Path @@ -78,6 +79,34 @@ def resolve_repo_path(value: str) -> Path: return path if path.is_absolute() else REPO_ROOT / path +def verify_game_config_identity( + config_path: Path, + profile: dict[str, object], + version: str, +) -> None: + try: + with config_path.open("rb") as f: + document = tomllib.load(f) + except (OSError, tomllib.TOMLDecodeError) as exc: + raise SystemExit(f"unable to read game config {config_path}: {exc}") from exc + + game = document.get("game") + if not isinstance(game, dict): + raise SystemExit(f"game config has no [game] table: {config_path}") + expected = { + "id": profile["game_code"], + "revision": profile["revision"], + "rom_size": profile["rom_size"], + "sha1": profile["sha1"], + } + for key, value in expected.items(): + if game.get(key) != value: + raise SystemExit( + f"game config {config_path} game.{key}={game.get(key)!r}; " + f"expected {value!r} for {version}" + ) + + def verify_rom_identity( rom_path: Path, profile: dict[str, object], From 300266cd0ee428fa651841e2aa960c1bda336d23 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:49:13 +0900 Subject: [PATCH 058/164] Tag gameplay coverage traces with ROM profile --- tools/fuzz_mph_gameplay.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tools/fuzz_mph_gameplay.py b/tools/fuzz_mph_gameplay.py index 74f329a..d924e9d 100644 --- a/tools/fuzz_mph_gameplay.py +++ b/tools/fuzz_mph_gameplay.py @@ -13,6 +13,14 @@ from PIL import Image import capture_mph_checkpoints as capture_lib +from mph_profile import ( + DEFAULT_PROFILE_FILE, + DEFAULT_VERSION, + load_profile, + resolve_repo_path, + verify_game_config_identity, + verify_rom_identity, +) KEY_BITS = { @@ -263,6 +271,13 @@ def main() -> int: parser.add_argument("--rom", type=Path, required=True) parser.add_argument("--config", type=Path) parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--version", default=DEFAULT_VERSION) + parser.add_argument( + "--profiles", + type=Path, + default=DEFAULT_PROFILE_FILE, + help=f"ROM profile registry (default: {DEFAULT_PROFILE_FILE})", + ) parser.add_argument("--port", type=int, default=19860) parser.add_argument("--seed", type=int, default=0x4D5048) parser.add_argument("--steps", type=int, default=100) @@ -274,8 +289,17 @@ def main() -> int: parser.add_argument("--skip-start-tap", action="store_true") parser.add_argument("--capture-static-coverage", action="store_true") args = parser.parse_args() - if args.runner is not None and args.config is None: - parser.error("--config is required with --runner") + + profile = load_profile(args.profiles.resolve(), args.version) + args.rom = args.rom.resolve() + rom_sha1 = verify_rom_identity(args.rom, profile, args.version) + if args.runner is not None: + args.config = ( + args.config.resolve() + if args.config is not None + else resolve_repo_path(str(profile["game_config"])).resolve() + ) + verify_game_config_identity(args.config, profile, args.version) output = args.out.resolve() output.mkdir(parents=True, exist_ok=True) @@ -287,6 +311,9 @@ def main() -> int: process = launch(args, output) trace: dict[str, object] = { + "mph_profile": args.version, + "rom_sha1": rom_sha1, + "display_name": profile["display_name"], "seed": args.seed, "backend": "native" if args.runner is not None else "oracle", "start_vblank": args.start_vblank, From 31e3d7a30ec0a8cd8907cdb7f069742a8b35ed2b Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:50:24 +0900 Subject: [PATCH 059/164] Profile FMV runtime bank generation --- CMakeLists.txt | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a9c964..b1d32fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,8 @@ string(JSON MPH_PROFILE_GAME_CONFIG GET "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" game_config) string(JSON MPH_PROFILE_FMV_RUNTIME GET "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" fmv_runtime) +string(JSON MPH_PROFILE_FMV_RUNTIME_BANK GET + "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" fmv_runtime_bank) set(MPH_ROM "${CMAKE_CURRENT_SOURCE_DIR}/Metroid Prime Hunters.nds" CACHE FILEPATH "Path to the verified ${MPH_PROFILE_NAME} ROM") @@ -159,10 +161,10 @@ set(MPH_FMV_RUNTIME_SOURCES) set(MPH_FMV_RUNTIME_STAMP) if(MPH_PROFILE_FMV_RUNTIME) set(MPH_FMV_RUNTIME_CONFIG - "${CMAKE_CURRENT_SOURCE_DIR}/config/mph_arm9_fmv_runtime.toml") + "${CMAKE_CURRENT_SOURCE_DIR}/config/${MPH_PROFILE_FMV_RUNTIME_BANK}.toml") set(MPH_FMV_RUNTIME_IMAGE - "${MPH_GENERATED_ROOT}/capture/mph_arm9_fmv_runtime.bin" - CACHE FILEPATH "Deterministic MPH FMV ITCM+main-RAM capture") + "${MPH_GENERATED_DIR}/capture/${MPH_PROFILE_FMV_RUNTIME_BANK}.bin" + CACHE FILEPATH "Deterministic ${MPH_VERSION} FMV ITCM+main-RAM capture") if(EXISTS "${MPH_FMV_RUNTIME_CONFIG}" AND EXISTS "${MPH_FMV_RUNTIME_IMAGE}") foreach(shard RANGE 0 31) if(shard LESS 10) @@ -171,21 +173,21 @@ if(MPH_PROFILE_FMV_RUNTIME) set(shard_name "${shard}") endif() list(APPEND MPH_FMV_RUNTIME_SOURCES - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime_${shard_name}.c") + "${MPH_RECOMP_DIR}/${MPH_PROFILE_FMV_RUNTIME_BANK}_${shard_name}.c") endforeach() list(APPEND MPH_FMV_RUNTIME_SOURCES - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime_dispatch.c") + "${MPH_RECOMP_DIR}/${MPH_PROFILE_FMV_RUNTIME_BANK}_dispatch.c") set(MPH_FMV_RUNTIME_STAMP - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime.stamp") + "${MPH_RECOMP_DIR}/${MPH_PROFILE_FMV_RUNTIME_BANK}.stamp") add_custom_command( OUTPUT "${MPH_FMV_RUNTIME_STAMP}" BYPRODUCTS ${MPH_FMV_RUNTIME_SOURCES} - "${MPH_RECOMP_DIR}/mph_arm9_fmv_runtime.h" + "${MPH_RECOMP_DIR}/${MPH_PROFILE_FMV_RUNTIME_BANK}.h" COMMAND $ --config "${MPH_FMV_RUNTIME_CONFIG}" --bin "${MPH_FMV_RUNTIME_IMAGE}" --out "${MPH_RECOMP_DIR}" - --bank mph_arm9_fmv_runtime + --bank "${MPH_PROFILE_FMV_RUNTIME_BANK}" --shards 32 --stable-address-shards --max-function-bytes 512 @@ -196,7 +198,8 @@ if(MPH_PROFILE_FMV_RUNTIME) COMMENT "Recompiling content-validated ${MPH_VERSION} ARM9 FMV runtime code") else() message(STATUS - "MPH FMV runtime bank disabled: capture image is not present") + "MPH FMV runtime bank disabled for ${MPH_VERSION}: expected " + "${MPH_FMV_RUNTIME_CONFIG} and ${MPH_FMV_RUNTIME_IMAGE}") endif() else() message(STATUS @@ -241,4 +244,4 @@ if(NOT MSVC) endif() add_custom_target(metroidprimehuntersrecomp - DEPENDS mph_romcheck mph_recompiled_banks) + DEPENDS mph_romcheck mph_recompiled_banks) \ No newline at end of file From 3d90ce201875a37f5fac5f933fb5bf9e8ceeff15 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:50:53 +0900 Subject: [PATCH 060/164] Use profile FMV bank in Linux release gate --- tools/build-linux.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/build-linux.sh b/tools/build-linux.sh index 1498a5f..f6c8d74 100644 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -92,7 +92,10 @@ profile = registry.get("profiles", {}).get(key) if not isinstance(profile, dict): choices = ", ".join(sorted(registry.get("profiles", {}))) raise SystemExit(f"unknown MPH profile {key!r}; configured: {choices}") -for field in ("sha1", "game_config", "game_code", "revision", "launcher_default_rom"): +for field in ( + "sha1", "game_config", "game_code", "revision", + "launcher_default_rom", "fmv_runtime_bank", +): if field not in profile: raise SystemExit(f"{key}: missing profile field {field}") print(profile["sha1"]) @@ -101,6 +104,7 @@ print(profile["game_code"]) print(profile["revision"]) print("1" if profile.get("fmv_runtime") else "0") print(profile["launcher_default_rom"]) +print(profile["fmv_runtime_bank"]) PY ) @@ -110,6 +114,7 @@ GAME_CODE="${PROFILE_VALUES[2]}" REVISION="${PROFILE_VALUES[3]}" FMV_RUNTIME="${PROFILE_VALUES[4]}" DEFAULT_ROM_NAME="${PROFILE_VALUES[5]}" +FMV_RUNTIME_BANK="${PROFILE_VALUES[6]}" GAME_CONFIG="$ROOT/$GAME_CONFIG_REL" if [[ "$ROM_PATH" == "$ROOT/Metroid Prime Hunters.nds" && "$MPH_VERSION" != "US1_0" ]]; then @@ -180,8 +185,9 @@ if [[ ! -x "$RUNNER" ]]; then fi if [[ "$FMV_RUNTIME" == "1" ]]; then - if ! grep -a -q 'mph_arm9_fmv_runtime' "$RUNNER"; then - printf 'Runner does not contain the required MPH FMV runtime bank.\n' >&2 + if ! grep -a -q "$FMV_RUNTIME_BANK" "$RUNNER"; then + printf 'Runner does not contain required FMV runtime bank %s.\n' \ + "$FMV_RUNTIME_BANK" >&2 exit 1 fi fi @@ -230,4 +236,4 @@ else OUTPUT="$ROOT/release-stage/MetroidPrimeHuntersRecomp-${MPH_VERSION}-linux-v${VERSION}-x86_64.AppImage" fi ARCH=x86_64 "$APPIMAGE_TOOL" "$APPDIR" "$OUTPUT" -printf 'Created %s\n' "$OUTPUT" +printf 'Created %s\n' "$OUTPUT" \ No newline at end of file From fdc8da185f07eb905fd40c7be3b45dc64384060d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:51:22 +0900 Subject: [PATCH 061/164] Gate releases on profile FMV bank --- tools/make_release.ps1 | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tools/make_release.ps1 b/tools/make_release.ps1 index 3eb0ea8..ef31866 100644 --- a/tools/make_release.ps1 +++ b/tools/make_release.ps1 @@ -6,9 +6,9 @@ assets, the selected revision's game config, documentation, and MinGW/SDL dependencies. ROMs, BIOS/firmware, saves, raw captures, and generated source are never staged. -US1_0 keeps the historical release name and requires the validated FMV runtime -bank. Other revision profiles may opt out of that bank until a revision- -specific runtime capture has been produced. +Profiles with a validated FMV runtime bank require that exact bank identity to +be present in the runner. Profiles without one may opt out until their own +revision-specific capture has been produced. #> param( [Parameter(Mandatory = $true)][string]$Version, @@ -17,6 +17,7 @@ param( [string]$RuntimeBinDir = 'C:\msys64\mingw64\bin', [string]$GameConfig = 'game.toml', [string]$Profile = 'US1_0', + [string]$FmvRuntimeBank = 'mph_arm9_fmv_runtime', [switch]$AllowNoFmvRuntime ) @@ -40,11 +41,12 @@ foreach ($required in @($runner, $launcher, $assets, $gameConfigPath)) { } if (-not $AllowNoFmvRuntime) { - # A US1.0 static-only runner is functional but drops the opening movies to - # roughly half speed. Keep the established release gate for that profile. + if ([string]::IsNullOrWhiteSpace($FmvRuntimeBank)) { + throw 'FMV runtime bank identity is empty.' + } $runnerText = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($runner)) - if (-not $runnerText.Contains('mph_arm9_fmv_runtime')) { - throw 'Runner does not contain the MPH FMV runtime bank.' + if (-not $runnerText.Contains($FmvRuntimeBank)) { + throw "Runner does not contain required FMV runtime bank '$FmvRuntimeBank'." } } @@ -166,4 +168,4 @@ try { Write-Host "--- $stageName ---" Get-ChildItem -LiteralPath $stage | Select-Object Name, Length | Out-Host -Get-FileHash -LiteralPath $zip -Algorithm SHA256 | Out-Host +Get-FileHash -LiteralPath $zip -Algorithm SHA256 | Out-Host \ No newline at end of file From 54a9afe2edd2a05409877d47c85304ce96623d63 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:51:44 +0900 Subject: [PATCH 062/164] Pass profile FMV bank to Windows packaging --- tools/build-windows.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index d9b7c0e..f2a79da 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -44,6 +44,10 @@ $romSha1 = [string]$profile.sha1 $region = [string]$profile.region $launcherDefaultRom = [string]$profile.launcher_default_rom $launcherAdaptive = if ([bool]$profile.adaptive_widescreen) { 'ON' } else { 'OFF' } +$fmvRuntimeBank = [string]$profile.fmv_runtime_bank +if ([string]::IsNullOrWhiteSpace($fmvRuntimeBank)) { + throw "Profile $MphVersion has no FMV runtime bank identity." +} $gameConfig = [IO.Path]::GetFullPath( (Join-Path $root ([string]$profile.game_config))) @@ -97,6 +101,7 @@ Push-Location $root try { Write-Host "Building MPH profile $MphVersion ($($profile.game_code) rev $($profile.revision))" Write-Host "Launcher adaptive widescreen: $launcherAdaptive" + Write-Host "FMV runtime bank identity: $fmvRuntimeBank" & $cmakePath -G $Generator -S $root -B $gameBuild ` -DCMAKE_BUILD_TYPE=Release ` @@ -144,7 +149,8 @@ try { '-LauncherBuildDir', $LauncherBuildDir, '-RuntimeBinDir', $RuntimeBinDir, '-GameConfig', $gameConfig, - '-Profile', $MphVersion + '-Profile', $MphVersion, + '-FmvRuntimeBank', $fmvRuntimeBank ) if (-not [bool]$profile.fmv_runtime) { $releaseArgs += '-AllowNoFmvRuntime' @@ -153,4 +159,4 @@ try { if ($LASTEXITCODE -ne 0) { throw 'Release packaging failed.' } } finally { Pop-Location -} +} \ No newline at end of file From 75dfed43fd132c512424617fc36ec0ac6d0bc097 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:52:47 +0900 Subject: [PATCH 063/164] Validate profile FMV bank configuration --- tools/check_mph_multirom_profiles.py | 50 ++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index 7bfcd3c..6881e82 100644 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -2,9 +2,9 @@ """Static consistency checks for Metroid Prime Hunters ROM profiles. This intentionally does not need a copyrighted ROM. It verifies that each -profile's identity, coverage seed, game config, launcher policy, and host-side -runtime addresses agree. When --melonprime-table is provided, Aim/Morph -addresses are also cross-checked against melonPrimeDS's +profile's identity, coverage seed, game config, launcher policy, FMV bank +identity, and host-side runtime addresses agree. When --melonprime-table is +provided, Aim/Morph addresses are also cross-checked against melonPrimeDS's MelonPrimeGameRomAddrTable.h, which is the source of truth for those fields. """ @@ -19,6 +19,7 @@ SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +BANK_RE = re.compile(r"^[A-Za-z0-9_]+$") HEX_RE = re.compile(r"0x[0-9A-Fa-f]+u?") REQUIRED_RUNTIME_FIELDS = { "morph_state": "baseIsAltForm", @@ -112,6 +113,7 @@ def validate_registry(repo: Path, table: Path | None) -> None: melon = parse_melonprime_table(table) if table else None seen_sha1: set[str] = set() seen_identity: set[tuple[str, int]] = set() + seen_fmv_banks: set[str] = set() for key, profile in profiles.items(): if not isinstance(profile, dict): @@ -146,6 +148,40 @@ def validate_registry(repo: Path, table: Path | None) -> None: if not isinstance(adaptive_widescreen, bool): die(f"{key}.adaptive_widescreen must be boolean") + fmv_runtime = profile.get("fmv_runtime") + if not isinstance(fmv_runtime, bool): + die(f"{key}.fmv_runtime must be boolean") + fmv_runtime_bank = profile.get("fmv_runtime_bank") + if ( + not isinstance(fmv_runtime_bank, str) + or not BANK_RE.fullmatch(fmv_runtime_bank) + or "_arm9_" not in fmv_runtime_bank + ): + die( + f"{key}.fmv_runtime_bank must be a C identifier-style ARM9 bank name" + ) + if fmv_runtime_bank in seen_fmv_banks: + die(f"duplicate FMV runtime bank identity: {fmv_runtime_bank}") + seen_fmv_banks.add(fmv_runtime_bank) + + if fmv_runtime: + runtime_config_path = repo / "config" / f"{fmv_runtime_bank}.toml" + if not runtime_config_path.is_file(): + die( + f"{key} enables FMV runtime but config is missing: " + f"{runtime_config_path}" + ) + with runtime_config_path.open("rb") as f: + runtime_config = tomllib.load(f) + runtime_program = runtime_config.get("program") + if not isinstance(runtime_program, dict): + die(f"{runtime_config_path} has no [program] table") + if runtime_program.get("id") != fmv_runtime_bank: + die( + f"{runtime_config_path} program.id={runtime_program.get('id')!r}; " + f"expected {fmv_runtime_bank!r}" + ) + runtime = profile.get("runtime") if not isinstance(runtime, dict): die(f"{key}.runtime is required") @@ -212,6 +248,12 @@ def validate_registry(repo: Path, table: Path | None) -> None: f"game config enables {config_adaptive!r}" ) + us = profiles.get("US1_0") + if not isinstance(us, dict): + die("US1_0 profile is required") + if us.get("fmv_runtime_bank") != "mph_arm9_fmv_runtime": + die("US1_0 historical FMV runtime bank identity changed unexpectedly") + # Explicit regression guard for the first non-US revision. These are the # values currently published by melonPrimeDS's source-of-truth table. eu = profiles.get("EU1_1") @@ -231,6 +273,8 @@ def validate_registry(repo: Path, table: Path | None) -> None: die(f"EU1_1 runtime profile changed unexpectedly: {eu_runtime!r}") if eu.get("fmv_runtime") is not False: die("EU1_1 must not reuse the US1.0 FMV runtime capture") + if eu.get("fmv_runtime_bank") != "mph_amhp1_arm9_fmv_runtime": + die("EU1_1 FMV runtime bank identity changed unexpectedly") if eu.get("adaptive_widescreen") is not False: die("EU1_1 adaptive widescreen must remain disabled until validated") if eu.get("launcher_default_rom") != "Metroid Prime Hunters (Europe Rev 1).nds": From f17570bfc7256f115492e1c273e8d44623129910 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:53:06 +0900 Subject: [PATCH 064/164] Require complete capture profile metadata --- tools/mph_profile.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/mph_profile.py b/tools/mph_profile.py index 13ad722..d87ef64 100644 --- a/tools/mph_profile.py +++ b/tools/mph_profile.py @@ -37,9 +37,12 @@ def load_profile(path: Path, version: str) -> dict[str, object]: "rom_size": int, "sha1": str, "program_id": str, + "coverage": str, "game_config": str, - "adaptive_widescreen": bool, + "fmv_runtime": bool, "fmv_runtime_bank": str, + "launcher_default_rom": str, + "adaptive_widescreen": bool, } for key, expected_type in required.items(): value = profile.get(key) @@ -65,6 +68,12 @@ def load_profile(path: Path, version: str) -> dict[str, object]: if int(profile["rom_size"]) <= 0: raise SystemExit(f"ROM profile {version!r} rom_size must be positive") + bank = str(profile["fmv_runtime_bank"]) + if not bank or "_arm9_" not in bank or not bank.replace("_", "a").isalnum(): + raise SystemExit( + f"ROM profile {version!r} fmv_runtime_bank must be an ARM9 bank identifier" + ) + return profile From 293d08a4b955ece32aa21babea641ac6e2b169af Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:54:31 +0900 Subject: [PATCH 065/164] Extend multi-ROM coverage static checks --- .github/workflows/mph-multirom-static.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index b70062e..f30662b 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -28,7 +28,8 @@ jobs: tools/patch_ndsrecomp_mph_runtime.py \ tools/promote_mph_static_coverage.py \ tools/promote_mph_runtime_coverage.py \ - tools/benchmark_mph_fmv.py + tools/benchmark_mph_fmv.py \ + tools/fuzz_mph_gameplay.py - name: Check shell syntax run: bash -n tools/build-linux.sh @@ -134,6 +135,16 @@ jobs: grep -q 'id = "mph_amhp1_arm9_fmv_runtime"' /tmp/eu-runtime.toml ! grep -q 'Metroid Prime Hunters (USA) ARM9 FMV runtime' /tmp/eu-runtime.toml + - name: Verify profile-owned FMV build naming + run: | + grep -q 'MPH_PROFILE_FMV_RUNTIME_BANK' CMakeLists.txt + grep -q 'config/${MPH_PROFILE_FMV_RUNTIME_BANK}.toml' CMakeLists.txt + grep -q 'capture/${MPH_PROFILE_FMV_RUNTIME_BANK}.bin' CMakeLists.txt + ! grep -q -- '--bank mph_arm9_fmv_runtime' CMakeLists.txt + grep -q 'FMV_RUNTIME_BANK' tools/build-linux.sh + grep -q 'FmvRuntimeBank' tools/build-windows.ps1 + grep -q 'FmvRuntimeBank' tools/make_release.ps1 + - name: Fetch exact pinned ndsrecomp revision run: | pin="$(tr -d '\r\n' < ndsrecomp.pin)" @@ -192,6 +203,7 @@ jobs: - name: Verify Linux profile-owned launch policy run: | grep -q 'launcher_default_rom' tools/build-linux.sh + grep -q 'fmv_runtime_bank' tools/build-linux.sh grep -q 'cp "$GAME_CONFIG" "$APPDIR/usr/share/mph-recomp/game.toml"' \ tools/build-linux.sh grep -q -- '--config "$HERE/usr/share/mph-recomp/game.toml"' \ From 9c284ba1c7b3714680d6bbf0e02bc61fe089dbb2 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:55:01 +0900 Subject: [PATCH 066/164] Require identified non-US coverage traces --- tools/promote_mph_static_coverage.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tools/promote_mph_static_coverage.py b/tools/promote_mph_static_coverage.py index 09e5ea6..864f965 100644 --- a/tools/promote_mph_static_coverage.py +++ b/tools/promote_mph_static_coverage.py @@ -55,7 +55,7 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--trace", type=Path, required=True) parser.add_argument("--out", type=Path, required=True) - parser.add_argument("--scenario", default="scenarios/adventure_start.json") + parser.add_argument("--scenario") parser.add_argument("--runner-commit", required=True) parser.add_argument("--version", default=DEFAULT_VERSION) parser.add_argument( @@ -102,12 +102,19 @@ def main() -> int: trace = json.loads(args.trace.read_text(encoding="utf-8")) observed_profile = trace.get("mph_profile") + observed_sha1 = trace.get("rom_sha1") + expected_sha1 = str(profile["sha1"]) + if args.version != DEFAULT_VERSION and ( + observed_profile is None or observed_sha1 is None + ): + raise SystemExit( + f"{args.version} coverage promotion requires a profile-tagged trace; " + "recapture with the profile-aware fuzz/capture tooling" + ) if observed_profile is not None and str(observed_profile) != args.version: raise SystemExit( f"trace profile {observed_profile!r} does not match {args.version!r}" ) - observed_sha1 = trace.get("rom_sha1") - expected_sha1 = str(profile["sha1"]) if observed_sha1 is not None and str(observed_sha1) != expected_sha1: raise SystemExit( f"trace ROM SHA-1 {observed_sha1!r} does not match " @@ -155,11 +162,12 @@ def main() -> int: entry["kinds"] = sorted(entry["kinds"]) by_cpu["arm7" if cpu == 7 else "arm9"].append(entry) + scenario = args.scenario if args.scenario is not None else trace.get("scenario") payload = { "schema": 1, "profile": args.version, "game_sha1": expected_sha1, - "scenario": args.scenario, + "scenario": scenario, "runner_commit": args.runner_commit, "selection": ( "Tier-3 call and indirect targets inside the selected ROM's " From 7c244be97b0040d81c034182220573a20a7e2199 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:55:21 +0900 Subject: [PATCH 067/164] Bind runtime promotion to tagged captures --- tools/promote_mph_runtime_coverage.py | 30 ++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/promote_mph_runtime_coverage.py b/tools/promote_mph_runtime_coverage.py index be6bd77..f8a3081 100644 --- a/tools/promote_mph_runtime_coverage.py +++ b/tools/promote_mph_runtime_coverage.py @@ -33,13 +33,19 @@ def verify_report_identity( version: str, expected_sha1: str, label: str, + require_identity: bool, ) -> None: observed_profile = report.get("mph_profile") + observed_sha1 = report.get("rom_sha1") + if require_identity and (observed_profile is None or observed_sha1 is None): + raise SystemExit( + f"{label} for {version} is missing profile/ROM identity; " + "recapture with the profile-aware FMV benchmark tool" + ) if observed_profile is not None and str(observed_profile) != version: raise SystemExit( f"{label} profile {observed_profile!r} does not match {version!r}" ) - observed_sha1 = report.get("rom_sha1") if observed_sha1 is not None and str(observed_sha1) != expected_sha1: raise SystemExit( f"{label} ROM SHA-1 {observed_sha1!r} does not match " @@ -75,6 +81,7 @@ def main() -> int: profile = load_profile(args.profiles.resolve(), args.version) expected_sha1 = str(profile["sha1"]) bank = args.bank or str(profile["fmv_runtime_bank"]) + require_identity = args.version != DEFAULT_VERSION image = args.image.read_bytes() if len(image) != IMAGE_SIZE: @@ -89,7 +96,27 @@ def main() -> int: version=args.version, expected_sha1=expected_sha1, label="benchmark", + require_identity=require_identity, ) + if require_identity: + runtime_capture = report.get("runtime_capture") + if not isinstance(runtime_capture, dict): + raise SystemExit( + f"benchmark for {args.version} has no runtime_capture metadata" + ) + capture_sha1 = runtime_capture.get("sha1") + capture_bytes = runtime_capture.get("bytes") + if capture_sha1 != identity: + raise SystemExit( + f"runtime image SHA-1 {identity} does not match benchmark " + f"capture SHA-1 {capture_sha1!r}" + ) + if int(capture_bytes) != IMAGE_SIZE: + raise SystemExit( + f"benchmark runtime capture size {capture_bytes!r} does not " + f"match 0x{IMAGE_SIZE:X}" + ) + coverage = report.get("tier3_coverage", {}).get("entries", []) if args.before_benchmark is not None: @@ -101,6 +128,7 @@ def main() -> int: version=args.version, expected_sha1=expected_sha1, label="before-benchmark", + require_identity=require_identity, ) before_entries = before_report.get( "tier3_coverage", {} From 1b40ab23acd82762cdab8e995a4611b6c52b0b0f Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:55:53 +0900 Subject: [PATCH 068/164] Record coverage scenario provenance --- tools/fuzz_mph_gameplay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/fuzz_mph_gameplay.py b/tools/fuzz_mph_gameplay.py index d924e9d..762ec3f 100644 --- a/tools/fuzz_mph_gameplay.py +++ b/tools/fuzz_mph_gameplay.py @@ -314,6 +314,7 @@ def main() -> int: "mph_profile": args.version, "rom_sha1": rom_sha1, "display_name": profile["display_name"], + "scenario": args.actions.as_posix() if args.actions is not None else None, "seed": args.seed, "backend": "native" if args.runner is not None else "oracle", "start_vblank": args.start_vblank, From 5c9aaf0ffafc2149a1ddfcc93f9213ca3356477a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:56:35 +0900 Subject: [PATCH 069/164] Validate tagged runtime capture identity --- .github/workflows/mph-multirom-static.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index f30662b..874c918 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -79,6 +79,7 @@ jobs: { "mph_profile": "EU1_1", "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "scenario": "scenarios/adventure_start.json", "static_coverage": {"tier3_entries9": 3}, "tier3_coverage": {"entries": [ {"cpu": 9, "pc": 33570816, "thumb": 0, "kind": 2, "hits": 99}, @@ -99,6 +100,7 @@ jobs: p = json.load(open('/tmp/eu-coverage.json', encoding='utf-8')) assert p['profile'] == 'EU1_1' assert p['game_sha1'] == 'bdcd1dea293e24c98d4c481430e90d21198985a5' + assert p['scenario'] == 'scenarios/adventure_start.json' assert p['main_image']['arm9']['end'] == '0x02005000' assert [e['addr'] for e in p['entry_points']['arm9']] == ['0x02004020'] assert [e['addr'] for e in p['entry_points']['arm7']] == ['0x02380020'] @@ -118,6 +120,10 @@ jobs: { "mph_profile": "EU1_1", "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "runtime_capture": { + "sha1": "2dacada9962037d4e1fa3099d9de8cdf616c318c", + "bytes": 4227072 + }, "tier3_coverage": {"entries": [ {"cpu": 9, "pc": 33525760, "thumb": 0, "kind": 2, "caller": 1, "hits": 2}, {"cpu": 9, "pc": 33525792, "thumb": 0, "kind": 3, "caller": 2, "hits": 3} From a0a13cea99a48c67bd25e4ba188cbbebc2d96e6b Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:59:19 +0900 Subject: [PATCH 070/164] Document EU1.1 coverage capture pipeline --- docs/EU1_1_BRINGUP.md | 281 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 247 insertions(+), 34 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 80625d9..76c0689 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -7,7 +7,7 @@ `MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定からmulti-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) を安全にbring-upする。 -ROMなしで可能な基盤実装は完了しており、現在の残件はEU1.1実ROMを使うruntime validation、EU1.1固有coverage、必要に応じたEU1.1固有FMV runtime captureである。 +ROMなしで可能な基盤実装は完了しており、現在の残件はEU1.1実ROMを使うruntime validation、EU1.1固有coverageの実採取、必要に応じたEU1.1固有FMV runtime captureである。 ## 2. EU1.1 identity @@ -22,7 +22,8 @@ ROMなしで可能な基盤実装は完了しており、現在の残件はEU1.1 | Game config | `config/game-eu11.toml` | | Launcher default ROM | `Metroid Prime Hunters (Europe Rev 1).nds` | | Adaptive Widescreen | disabled until EU1.1 validation | -| FMV runtime bank | disabled until EU1.1 capture exists | +| FMV runtime enabled | `false` | +| Reserved EU1.1 FMV bank ID | `mph_amhp1_arm9_fmv_runtime` | identityと版別policyは `config/mph_rom_profiles.json` に集約する。 @@ -31,7 +32,8 @@ identityと版別policyは `config/mph_rom_profiles.json` に集約する。 schema 2 profileは少なくとも以下を管理する。 - `game_code`, `revision`, `rom_size`, `sha1`, `program_id` -- `coverage`, `game_config`, `fmv_runtime` +- `coverage`, `game_config` +- `fmv_runtime`, `fmv_runtime_bank` - `launcher_default_rom`, `adaptive_widescreen` - `runtime.morph_state`, `runtime.aim_x`, `runtime.aim_y` @@ -82,11 +84,109 @@ patcherはexact source preimageを要求し、idempotentで、profile切替時 - EU1.1は `0x020DB138 / 0x020DEE46 / 0x020DEE4E` のみ使用 - profile切替後のstale stateなし -## 6. EU1.1 coverage bootstrap +## 6. EU1.1 static coverage pipeline + +### 6.1 Bootstrap `coverage/eu11-bootstrap-entry-points.json` は追加ARM9/ARM7 rootを空にする。ROM header entry PCは `prepare_mph.py` がseedし、未コンパイル領域はInterpreter fallbackへ送る。 -US1.0 absolute PCはEU1.1へコピーしない。EU1.1自身のtraceからのみcoverageを拡張する。 +US1.0の `coverage/adventure-main-entry-points.json` に含まれるabsolute PCはEU1.1へコピーしない。EU1.1自身のexecution traceからのみcoverageを拡張する。 + +### 6.2 US1.0固定rangeの除去 + +旧 `tools/promote_mph_static_coverage.py` はUS1.0固定の以下を内蔵していた。 + +```text +GAME_SHA1 +ARM9 main-image start/end +ARM7 main-image start/end +ARM9/ARM7 existing entry PC +``` + +現在はこれらを持たない。 + +`prepare_mph.py` が選択ROMそのものから生成した + +```text +generated//inputs/arm9.toml +generated//inputs/arm7.toml +``` + +の `[program].load_address`, `size`, `entry_pc`, `id` を読み、実ROM由来のimmutable main-image geometryをcoverage filterへ使用する。 + +EU1.1では既定で: + +```text +generated/EU1_1/inputs/arm9.toml +generated/EU1_1/inputs/arm7.toml +``` + +を読む。 + +したがってEU1.1のARM9/ARM7終端アドレスをUS1.0から推測・変換する処理はない。 + +### 6.3 Trace provenance + +`tools/fuzz_mph_gameplay.py` は現在profile-awareで、実行前にROMのsize/SHA-1/game code/revisionを検証する。 + +生成する `trace.json` には少なくとも以下を記録する。 + +```json +{ + "mph_profile": "EU1_1", + "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "scenario": "scenarios/adventure_start.json" +} +``` + +EU1.1のcoverage昇格では、このprofile/ROM identityが欠落した旧traceや出所不明traceを拒否する。 + +### 6.4 EU1.1 Adventure coverage採取 + +EU1.1 runnerをbuild済みとする。 + +```powershell +python tools\fuzz_mph_gameplay.py ` + --version EU1_1 ` + --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` + --bios bios ` + --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` + --out generated\EU1_1\capture\adventure-coverage ` + --actions scenarios\adventure_start.json ` + --steps 0 ` + --capture-static-coverage +``` + +`--capture-static-coverage` によりrunnerは `--discover-static-misses` 付きで起動し、最後に `static_coverage` と `tier3_coverage` をdebug serverから回収する。 + +### 6.5 EU1.1 static coverage昇格 + +runner/framework commitは実際に採取へ使ったrevisionを記録する。 + +```powershell +$runnerCommit = git -C ..\ndsrecomp rev-parse HEAD + +python tools\promote_mph_static_coverage.py ` + --version EU1_1 ` + --trace generated\EU1_1\capture\adventure-coverage\trace.json ` + --out coverage\eu11-adventure-main-entry-points.json ` + --runner-commit $runnerCommit +``` + +昇格対象は以下のみ。 + +- ARM9/ARM7 immutable main image内 +- Tier-3 `call` +- Tier-3 `indirect` + +以下は除外する。 + +- slice-resume root +- main image範囲外runtime RAM +- reused overlay virtual ranges +- ROM header entry PCの重複 + +実ROM検証後に `config/mph_rom_profiles.json` のEU1.1 `coverage` をbootstrap JSONからこの昇格済みJSONへ切り替える。 ## 7. Generated tree separation @@ -103,8 +203,11 @@ EU1.1: ```text generated/EU1_1/inputs/ generated/EU1_1/recomp/ +generated/EU1_1/capture/ ``` +異なるROM revisionのprepared binary、coverage capture、runtime image、generated bankを同じディレクトリへ混在させない。 + ## 8. Launcher identity / feature policy separation `launcher/recomp-ui/CMakeLists.txt` はbaseline `launcher_main.cpp` からprofile-specific generated TUを作る。 @@ -133,11 +236,115 @@ EU1.1 Adaptive Widescreenは三重にfail-closed: Prime ControlsはEU1.1でも表示する。必要なMorph/Aim address routingはROMなしunit testで固定済みだが、ゲーム内semantic correctnessは実ROMで確認する。 -## 9. FMV runtime bank +## 9. EU1.1 FMV runtime capture pipeline -US1.0 runtime captureはEU1.1へ流用しない。EU1.1は現在 `fmv_runtime=false`。必要な場合のみEU1.1自身からcaptureし、live-byte validation付きbankを作る。 +### 9.1 US1.0 bankの非流用 -## 10. Build +US1.0の既存bank: + +```text +config/mph_arm9_fmv_runtime.toml +generated/capture/mph_arm9_fmv_runtime.bin +bank id: mph_arm9_fmv_runtime +``` + +はEU1.1へ流用しない。 + +EU1.1用に予約しているidentity/path: + +```text +config/mph_amhp1_arm9_fmv_runtime.toml +generated/EU1_1/capture/mph_amhp1_arm9_fmv_runtime.bin +bank id: mph_amhp1_arm9_fmv_runtime +``` + +現在 `fmv_runtime=false` なので、EU1.1 buildはこのbankを要求・登録しない。 + +### 9.2 FMV benchmark/captureのprofile化 + +`tools/benchmark_mph_fmv.py` は現在: + +- `--version EU1_1` を受ける +- exact EU1.1 ROM identityを起動前に検証する +- `--config` 省略時は `config/game-eu11.toml` を選ぶ +- `--adaptive auto` が既定 +- EU1.1ではprofile policyに従い `--adaptive-widescreen none` +- `benchmark.json` に `mph_profile` と `rom_sha1` を保存する +- `--capture-runtime` 時はcapture SHA-1とbyte countも保存する + +EU1.1を誤ってUS1.0のAdaptive Widescreen有効状態でbenchmarkする既定値は廃止した。 + +### 9.3 Before window採取 + +例としてVBlank 2400までの累積coverageを取る。 + +```powershell +python tools\benchmark_mph_fmv.py ` + --version EU1_1 ` + --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` + --bios bios ` + --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` + --out generated\EU1_1\capture\fmv-before ` + --targets 2400 ` + --discover-static-misses +``` + +### 9.4 Target window + RAM capture + +```powershell +python tools\benchmark_mph_fmv.py ` + --version EU1_1 ` + --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` + --bios bios ` + --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` + --out generated\EU1_1\capture\fmv-3000 ` + --targets 2400 3000 ` + --discover-static-misses ` + --capture-runtime generated\EU1_1\capture\mph_amhp1_arm9_fmv_runtime.bin +``` + +runtime imageはITCM + main RAMを連結した `0x00408000` bytes。 + +### 9.5 EU1.1 runtime bank config昇格 + +```powershell +python tools\promote_mph_runtime_coverage.py ` + --version EU1_1 ` + --before-benchmark generated\EU1_1\capture\fmv-before\benchmark.json ` + --benchmark generated\EU1_1\capture\fmv-3000\benchmark.json ` + --image generated\EU1_1\capture\mph_amhp1_arm9_fmv_runtime.bin ` + --out config\mph_amhp1_arm9_fmv_runtime.toml +``` + +EU1.1では昇格時に以下を全て要求する。 + +- benchmarkの `mph_profile == EU1_1` +- benchmarkの `rom_sha1 == EU1.1 SHA-1` +- before benchmarkも同じidentity +- benchmark内 `runtime_capture.sha1` と渡した`.bin`の実SHA-1が一致 +- benchmark内capture byte countが `0x00408000` +- ARM9 runtime領域内のobserved call/indirect targetが存在 + +これらのどれかが不一致ならTOMLを生成しない。 + +実runtime validationとperformance確認後にのみ、EU1.1 profileの `fmv_runtime` を `true` へ変更する。 + +## 10. FMV runtime bank build/release routing + +CMakeはFMV bank名を固定しない。profileの `fmv_runtime_bank` から以下を導出する。 + +```text +config/.toml +generated//capture/.bin +generated//recomp/_*.c +--bank +``` + +Windows/Linux release gateもprofileのbank IDをrunner内で確認する。 + +従って将来EU1.1で `fmv_runtime=true` にした場合も、US1.0の `mph_arm9_fmv_runtime` を誤要求しない。 + +## 11. Build ### Windows @@ -163,26 +370,32 @@ tools/build-linux.sh \ `--rom`省略時はEU1.1 profileのdefault ROM filenameをrepository rootから探す。AppRunはprofile別 `game.toml` をrunnerへ渡し、`--adaptive-widescreen` 等でtitle policyを上書きしない。 -## 11. ROM不要static CI +## 12. ROM不要static CI `.github/workflows/mph-multirom-static.yml` は以下を検証する。 -1. Python / shell / PowerShell syntax -2. profile / coverage / game config / launcher policy整合性 -3. melonPrimeDS `MelonPrimeGameRomAddrTable.h` とのAim/Morph照合 -4. exact `ndsrecomp.pin` fetch -5. US1.0/EU1.1 launcher generated source renderとidentity/policy確認 -6. Linux AppRunのprofile-owned config policy確認 -7. ndsrecomp runtime patchのidempotency -8. US1.0固定Aim/Morph symbol除去確認 -9. patched runnerの `title_patches.cpp` / `frontend.cpp` / `main.cpp` compile -10. exact-ROM runtime dispatch unit test実行 -11. US1.0/EU1.1 `mph_romcheck` compile -12. `git diff --check` +1. capture/promotionを含むPython syntax +2. Linux shell / Windows PowerShell syntax +3. profile / coverage / game config / launcher / FMV bank policy整合性 +4. melonPrimeDS `MelonPrimeGameRomAddrTable.h` とのAim/Morph照合 +5. fake EU1.1 prepared ARM9/ARM7 geometryからstatic coverageを昇格 +6. main-image範囲外targetが除外されることを確認 +7. fake EU1.1 runtime image + tagged benchmarkからEU1.1 FMV TOMLを生成 +8. EU1.1 TOMLへUSA runtime名が混入しないことを確認 +9. CMake/Windows/Linuxがprofile-owned FMV bank IDを使用することを確認 +10. exact `ndsrecomp.pin` fetch +11. US1.0/EU1.1 launcher generated source renderとidentity/policy確認 +12. Linux AppRunのprofile-owned config policy確認 +13. ndsrecomp runtime patchのidempotency +14. US1.0固定Aim/Morph symbol除去確認 +15. patched runnerの `title_patches.cpp` / `frontend.cpp` / `main.cpp` compile +16. exact-ROM runtime dispatch unit test実行 +17. US1.0/EU1.1 `mph_romcheck` compile +18. `git diff --check` ROM、BIOS、firmware dumpはCIで取得しない。 -## 12. mphCodexの役割 +## 13. mphCodexの役割 Aim X/Y/MorphはmelonPrimeDS tableをsource of truthとする。その他Recomp固有host enhancementのcross-version semantic調査にはmphCodexを利用する。 @@ -198,7 +411,7 @@ Aim X/Y/MorphはmelonPrimeDS tableをsource of truthとする。その他Recomp 単一delta変換は使用しない。 -## 13. 実ROMで残るvalidation gates +## 14. 実ROMで残るvalidation gates ### Gate A - extraction / bank generation @@ -216,34 +429,34 @@ Adventure file作成/読込、Celestial Archives、movement/aim/shoot、Morph Ba address routingはunit test済み。実ROMではnormal/Morph touch behavior、camera aim、menu/touch復帰、keyboard/gamepad操作を確認する。 -### Gate E - Adaptive Widescreen +### Gate E - deterministic coverage -現在EU1.1では意図的に無効。基本対応の必須条件ではない。将来有効化する場合のみprojection、culling、HUD anchoring、touchscreen、特殊camera/visor sceneをEU1.1実ROMで検証し、profileとgame configを同時にenableする。 +EU1.1自身のprofile-tagged execution traceからcoverageを採取し、EU1.1 prepared main-image geometryでfilterして昇格する。 -### Gate F - deterministic coverage +### Gate F - FMV runtime optimization -EU1.1自身のexecution traceからのみcoverageを昇格する。 +必要な場合のみEU1.1 captureを作り、capture identity、content validation、correctness、performanceを確認してから `fmv_runtime=true` にする。 -### Gate G - FMV runtime optimization +### Gate G - Adaptive Widescreen -必要な場合のみEU1.1 captureを作り、content validationとperformanceを確認してから `fmv_runtime=true` にする。 +現在EU1.1では意図的に無効。基本対応の必須条件ではない。将来有効化する場合のみprojection、culling、HUD anchoring、touchscreen、特殊camera/visor sceneをEU1.1実ROMで検証し、profileとgame configを同時にenableする。 -## 14. Supported判定 +## 15. Supported判定 EU1.1をruntime検証済みsupportedと宣言するには、exact identity、EU1.1 ARM9/ARM7 banks、title/gameplay/save/load、Prime Controls/Direct Aim semantic validation、native/reference checkpoint比較、US1.0 regressionなしが必要である。 Adaptive WidescreenとEU1.1 FMV runtime bankは基本correctnessの必須条件ではない。未検証機能はfail-closedを維持する。 -## 15. 現在の判定 +## 16. 現在の判定 ### Code / infrastructure -**READY FOR EU1.1 ROM VALIDATION** +**READY FOR EU1.1 ROM VALIDATION AND PROFILE-TAGGED COVERAGE CAPTURE** -ROMなしで可能なprofile、extraction routing、bank isolation、runtime address selection、launcher identity/feature gating、Windows/Linux packaging、static CIまで実装済み。 +ROMなしで可能なprofile、extraction routing、bank isolation、runtime address selection、launcher identity/feature gating、coverage capture metadata、static/runtime coverage promotion、profile-owned FMV bank routing、Windows/Linux packaging、static CIまで実装済み。 ### Runtime correctness **NOT YET CLAIMED** -EU1.1実ROMによるboot/gameplay/reference validationは別途必要である。 +EU1.1実ROMによるboot/gameplay/reference validationと実coverage/capture採取は別途必要である。 From 16aab6313f97701c5d2c5b37351a36d63adc9bf6 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 11:59:46 +0900 Subject: [PATCH 071/164] Remove fixed Windows profile enumeration --- tools/build-windows.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index f2a79da..27b6e5f 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -1,10 +1,10 @@ <# Build Metroid Prime Hunters Recomp for one configured retail revision. -US1_0 keeps the existing release paths. EU1_1 uses isolated generated banks, -a revision-specific game config, a profile-specific launcher identity/policy, -and the shared exact-ROM runtime-address shim for Prime Controls/direct mouse -aim. +US1_0 keeps the existing release paths. Non-US profiles use isolated generated +banks, a revision-specific game config, a profile-specific launcher +identity/policy, and the shared exact-ROM runtime-address shim for Prime +Controls/direct mouse aim. Usage: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` @@ -13,7 +13,6 @@ Usage: #> param( [string]$Version = '0.1.0', - [ValidateSet('US1_0', 'EU1_1')] [string]$MphVersion = 'US1_0', [string]$RomPath = '', [string]$CMake = 'C:\msys64\mingw64\bin\cmake.exe', @@ -37,7 +36,8 @@ $profileFile = Join-Path $root 'config\mph_rom_profiles.json' $registry = Get-Content -LiteralPath $profileFile -Raw | ConvertFrom-Json $profileProperty = $registry.profiles.PSObject.Properties[$MphVersion] if ($null -eq $profileProperty) { - throw "Unknown MPH profile: $MphVersion" + $choices = @($registry.profiles.PSObject.Properties.Name) -join ', ' + throw "Unknown MPH profile '$MphVersion'. Configured profiles: $choices" } $profile = $profileProperty.Value $romSha1 = [string]$profile.sha1 @@ -159,4 +159,4 @@ try { if ($LASTEXITCODE -ne 0) { throw 'Release packaging failed.' } } finally { Pop-Location -} \ No newline at end of file +} From 7f9cb8bceffe708769ec292523991b788e33b396 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:00:26 +0900 Subject: [PATCH 072/164] Derive CMake profile choices from registry --- CMakeLists.txt | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b1d32fc..2a5d755 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,6 @@ add_subdirectory( set(MPH_VERSION "US1_0" CACHE STRING "Metroid Prime Hunters retail revision profile") -set_property(CACHE MPH_VERSION PROPERTY STRINGS US1_0 EU1_1) set(MPH_PROFILE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/config/mph_rom_profiles.json" CACHE FILEPATH "Metroid Prime Hunters ROM profile registry") @@ -28,6 +27,26 @@ if(NOT EXISTS "${MPH_PROFILE_FILE}") message(FATAL_ERROR "MPH ROM profile registry not found: ${MPH_PROFILE_FILE}") endif() file(READ "${MPH_PROFILE_FILE}" MPH_PROFILES_JSON) +string(JSON MPH_PROFILE_COUNT LENGTH "${MPH_PROFILES_JSON}" profiles) +if(MPH_PROFILE_COUNT LESS 1) + message(FATAL_ERROR "MPH ROM profile registry contains no profiles") +endif() +math(EXPR _mph_profile_last "${MPH_PROFILE_COUNT} - 1") +set(MPH_PROFILE_KEYS) +foreach(_mph_profile_index RANGE 0 ${_mph_profile_last}) + string(JSON _mph_profile_key MEMBER + "${MPH_PROFILES_JSON}" profiles ${_mph_profile_index}) + list(APPEND MPH_PROFILE_KEYS "${_mph_profile_key}") +endforeach() +set_property(CACHE MPH_VERSION PROPERTY STRINGS ${MPH_PROFILE_KEYS}) +list(FIND MPH_PROFILE_KEYS "${MPH_VERSION}" MPH_PROFILE_INDEX) +if(MPH_PROFILE_INDEX EQUAL -1) + list(JOIN MPH_PROFILE_KEYS ", " _mph_profile_choices) + message(FATAL_ERROR + "Unknown MPH_VERSION '${MPH_VERSION}'. Configured profiles: " + "${_mph_profile_choices}") +endif() + string(JSON MPH_PROFILE_NAME GET "${MPH_PROFILES_JSON}" profiles "${MPH_VERSION}" display_name) string(JSON MPH_PROFILE_GAME_CODE GET From a3703cd4efa48e023b1568a0216089fb2d017130 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:01:39 +0900 Subject: [PATCH 073/164] Make checkpoint capture profile-aware --- tools/capture_mph_checkpoints.py | 63 ++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/tools/capture_mph_checkpoints.py b/tools/capture_mph_checkpoints.py index 4730000..6855681 100644 --- a/tools/capture_mph_checkpoints.py +++ b/tools/capture_mph_checkpoints.py @@ -12,6 +12,15 @@ from PIL import Image +from mph_profile import ( + DEFAULT_PROFILE_FILE, + DEFAULT_VERSION, + load_profile, + resolve_repo_path, + verify_game_config_identity, + verify_rom_identity, +) + def firmware_crc16(data: bytes, start: int = 0xFFFF) -> int: polynomial = ( @@ -121,9 +130,6 @@ def capture( raise RuntimeError( f"no progress toward VBlank {count}: {json.dumps(hit)}" ) - # A long authentic-firmware run can exceed one server command's - # safety-round budget. Continue from the live machine rather than - # saving a misleading image under the requested checkpoint name. previous_vblank = current_vblank screens = [framebuffer(client, engine) for engine in ("A", "B")] image = Image.new( @@ -176,6 +182,13 @@ def main() -> int: parser.add_argument("--rom", type=Path, required=True) parser.add_argument("--config", type=Path) parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--version", default=DEFAULT_VERSION) + parser.add_argument( + "--profiles", + type=Path, + default=DEFAULT_PROFILE_FILE, + help=f"ROM profile registry (default: {DEFAULT_PROFILE_FILE})", + ) parser.add_argument("--port", type=int, default=19852) parser.add_argument( "--targets", type=int, nargs="+", default=[300, 600, 900, 1200] @@ -204,11 +217,36 @@ def main() -> int: parser.error("--generated-firmware requires --boot direct") if args.freebios and args.boot != "direct": parser.error("--freebios requires --boot direct") - if args.runner is not None and args.config is None: - parser.error("--config is required with --runner") + + profile = load_profile(args.profiles.resolve(), args.version) + args.rom = args.rom.resolve() + rom_sha1 = verify_rom_identity(args.rom, profile, args.version) + if args.runner is not None: + args.config = ( + args.config.resolve() + if args.config is not None + else resolve_repo_path(str(profile["game_config"])).resolve() + ) + verify_game_config_identity(args.config, profile, args.version) output = args.out.resolve() output.mkdir(parents=True, exist_ok=True) + targets = sorted(set(args.targets)) + metadata = { + "mph_profile": args.version, + "rom_sha1": rom_sha1, + "display_name": profile["display_name"], + "backend": "native" if args.runner is not None else "oracle", + "boot": args.boot, + "targets": targets, + "game_config": str(args.config) if args.runner is not None else None, + } + (output / "metadata.json").write_text( + json.dumps(metadata, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + with (output / "runner.stdout.log").open("wb") as stdout, ( output / "runner.stderr.log" ).open("wb") as stderr: @@ -222,15 +260,12 @@ def main() -> int: "--port", str(args.port), "--rom", - str(args.rom.resolve()), + str(args.rom), "--config", - str(args.config.resolve()), + str(args.config), "--no-save", ] if not args.generated_firmware: - # Meaningful only when the real firmware runs (or seeds the - # user-settings mirror); a generated image must stay - # byte-identical to the oracle's, which never sets autoboot. command += ["--startup-mode", "automatic"] if args.boot == "direct": command += ["--boot", "direct"] @@ -260,11 +295,7 @@ def main() -> int: command += ["--firmware", str(firmware)] command += [ "--rom", - str(args.rom.resolve()), - # The runner's --startup-mode automatic patches its private - # in-memory firmware; the oracle reads the same patched copy - # from disk so a direct boot's user-settings mirror carries - # identical bytes on both sides. + str(args.rom), "--boot", args.boot, "--port", @@ -289,7 +320,7 @@ def main() -> int: count, include_native_stats=args.runner is not None, ) - for count in sorted(set(args.targets)) + for count in targets ] (output / "report.json").write_text( json.dumps(report, indent=2) + "\n", From fcdc912519ecc460dec63b1b762f0c7e5f3b38a9 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:02:40 +0900 Subject: [PATCH 074/164] Guard capture tools and dynamic profile discovery --- .github/workflows/mph-multirom-static.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index 874c918..f335afb 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -29,7 +29,8 @@ jobs: tools/promote_mph_static_coverage.py \ tools/promote_mph_runtime_coverage.py \ tools/benchmark_mph_fmv.py \ - tools/fuzz_mph_gameplay.py + tools/fuzz_mph_gameplay.py \ + tools/capture_mph_checkpoints.py - name: Check shell syntax run: bash -n tools/build-linux.sh @@ -141,12 +142,15 @@ jobs: grep -q 'id = "mph_amhp1_arm9_fmv_runtime"' /tmp/eu-runtime.toml ! grep -q 'Metroid Prime Hunters (USA) ARM9 FMV runtime' /tmp/eu-runtime.toml - - name: Verify profile-owned FMV build naming + - name: Verify registry-driven profile and FMV routing run: | grep -q 'MPH_PROFILE_FMV_RUNTIME_BANK' CMakeLists.txt grep -q 'config/${MPH_PROFILE_FMV_RUNTIME_BANK}.toml' CMakeLists.txt grep -q 'capture/${MPH_PROFILE_FMV_RUNTIME_BANK}.bin' CMakeLists.txt ! grep -q -- '--bank mph_arm9_fmv_runtime' CMakeLists.txt + grep -q 'MPH_PROFILE_KEYS' CMakeLists.txt + ! grep -q 'PROPERTY STRINGS US1_0 EU1_1' CMakeLists.txt + ! grep -q "ValidateSet('US1_0', 'EU1_1')" tools/build-windows.ps1 grep -q 'FMV_RUNTIME_BANK' tools/build-linux.sh grep -q 'FmvRuntimeBank' tools/build-windows.ps1 grep -q 'FmvRuntimeBank' tools/make_release.ps1 From 273a8d021955fa47a0f4b2aa70ff02dc30b257f5 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:03:12 +0900 Subject: [PATCH 075/164] Harden runtime capture size metadata errors --- tools/promote_mph_runtime_coverage.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/promote_mph_runtime_coverage.py b/tools/promote_mph_runtime_coverage.py index f8a3081..74433de 100644 --- a/tools/promote_mph_runtime_coverage.py +++ b/tools/promote_mph_runtime_coverage.py @@ -105,15 +105,22 @@ def main() -> int: f"benchmark for {args.version} has no runtime_capture metadata" ) capture_sha1 = runtime_capture.get("sha1") - capture_bytes = runtime_capture.get("bytes") + capture_bytes_raw = runtime_capture.get("bytes") if capture_sha1 != identity: raise SystemExit( f"runtime image SHA-1 {identity} does not match benchmark " f"capture SHA-1 {capture_sha1!r}" ) - if int(capture_bytes) != IMAGE_SIZE: + try: + capture_bytes = int(capture_bytes_raw) + except (TypeError, ValueError) as exc: raise SystemExit( - f"benchmark runtime capture size {capture_bytes!r} does not " + f"benchmark runtime capture has invalid byte count " + f"{capture_bytes_raw!r}" + ) from exc + if capture_bytes != IMAGE_SIZE: + raise SystemExit( + f"benchmark runtime capture size {capture_bytes} does not " f"match 0x{IMAGE_SIZE:X}" ) From b61bcb1557ae04e475c0700baaeffc15f35c5d8c Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:03:57 +0900 Subject: [PATCH 076/164] Note profile-aware checkpoint validation --- docs/EU1_1_BRINGUP.md | 82 +++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 76c0689..e4d454e 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -45,6 +45,8 @@ https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPr Aim/Morphアドレスはglobal relocation deltaから推測しない。 +CMakeのprofile候補とWindows buildの `-MphVersion` validationもregistryから導出する。新しいrevisionをprofile registryへ追加するとき、`US1_0/EU1_1` の固定列挙を別途更新する必要はない。 + ## 4. melonPrimeDSから確定したEU1.1 runtime addresses | Semantic | melonPrimeDS field | US1.0 | EU1.1 | @@ -344,7 +346,39 @@ Windows/Linux release gateもprofileのbank IDをrunner内で確認する。 従って将来EU1.1で `fmv_runtime=true` にした場合も、US1.0の `mph_arm9_fmv_runtime` を誤要求しない。 -## 11. Build +## 11. Profile-aware checkpoint validation + +`tools/capture_mph_checkpoints.py` もprofile-awareにした。 + +runner/oracleのどちらを使う場合も、起動前に選択ROMをprofileのsize/SHA-1/game code/revisionへ照合する。runner時は `--config` 省略でprofileのgame configを選び、config identityもROM profileへ照合する。 + +各capture directoryには `metadata.json` を追加し、以下を保存する。 + +```text +mph_profile +rom_sha1 +display_name +backend +boot +targets +game_config +``` + +これによりUS1.0 native checkpointとEU1.1 oracle checkpoint等を誤って同一比較セットとして扱う前に、capture provenanceを確認できる。 + +EU1.1例: + +```powershell +python tools\capture_mph_checkpoints.py ` + --version EU1_1 ` + --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` + --bios bios ` + --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` + --out generated\EU1_1\capture\checkpoints ` + --targets 300 600 900 1200 +``` + +## 12. Build ### Windows @@ -370,32 +404,34 @@ tools/build-linux.sh \ `--rom`省略時はEU1.1 profileのdefault ROM filenameをrepository rootから探す。AppRunはprofile別 `game.toml` をrunnerへ渡し、`--adaptive-widescreen` 等でtitle policyを上書きしない。 -## 12. ROM不要static CI +## 13. ROM不要static CI `.github/workflows/mph-multirom-static.yml` は以下を検証する。 -1. capture/promotionを含むPython syntax +1. capture/promotion/checkpointを含むPython syntax 2. Linux shell / Windows PowerShell syntax 3. profile / coverage / game config / launcher / FMV bank policy整合性 -4. melonPrimeDS `MelonPrimeGameRomAddrTable.h` とのAim/Morph照合 -5. fake EU1.1 prepared ARM9/ARM7 geometryからstatic coverageを昇格 -6. main-image範囲外targetが除外されることを確認 -7. fake EU1.1 runtime image + tagged benchmarkからEU1.1 FMV TOMLを生成 -8. EU1.1 TOMLへUSA runtime名が混入しないことを確認 -9. CMake/Windows/Linuxがprofile-owned FMV bank IDを使用することを確認 -10. exact `ndsrecomp.pin` fetch -11. US1.0/EU1.1 launcher generated source renderとidentity/policy確認 -12. Linux AppRunのprofile-owned config policy確認 -13. ndsrecomp runtime patchのidempotency -14. US1.0固定Aim/Morph symbol除去確認 -15. patched runnerの `title_patches.cpp` / `frontend.cpp` / `main.cpp` compile -16. exact-ROM runtime dispatch unit test実行 -17. US1.0/EU1.1 `mph_romcheck` compile -18. `git diff --check` +4. CMake/Windowsがprofile keyを固定列挙しないこと +5. melonPrimeDS `MelonPrimeGameRomAddrTable.h` とのAim/Morph照合 +6. fake EU1.1 prepared ARM9/ARM7 geometryからstatic coverageを昇格 +7. main-image範囲外targetが除外されることを確認 +8. fake EU1.1 runtime image + tagged benchmarkからEU1.1 FMV TOMLを生成 +9. runtime image SHA/size metadata照合 +10. EU1.1 TOMLへUSA runtime名が混入しないことを確認 +11. CMake/Windows/Linuxがprofile-owned FMV bank IDを使用することを確認 +12. exact `ndsrecomp.pin` fetch +13. US1.0/EU1.1 launcher generated source renderとidentity/policy確認 +14. Linux AppRunのprofile-owned config policy確認 +15. ndsrecomp runtime patchのidempotency +16. US1.0固定Aim/Morph symbol除去確認 +17. patched runnerの `title_patches.cpp` / `frontend.cpp` / `main.cpp` compile +18. exact-ROM runtime dispatch unit test実行 +19. US1.0/EU1.1 `mph_romcheck` compile +20. `git diff --check` ROM、BIOS、firmware dumpはCIで取得しない。 -## 13. mphCodexの役割 +## 14. mphCodexの役割 Aim X/Y/MorphはmelonPrimeDS tableをsource of truthとする。その他Recomp固有host enhancementのcross-version semantic調査にはmphCodexを利用する。 @@ -411,7 +447,7 @@ Aim X/Y/MorphはmelonPrimeDS tableをsource of truthとする。その他Recomp 単一delta変換は使用しない。 -## 14. 実ROMで残るvalidation gates +## 15. 実ROMで残るvalidation gates ### Gate A - extraction / bank generation @@ -441,19 +477,19 @@ EU1.1自身のprofile-tagged execution traceからcoverageを採取し、EU1.1 p 現在EU1.1では意図的に無効。基本対応の必須条件ではない。将来有効化する場合のみprojection、culling、HUD anchoring、touchscreen、特殊camera/visor sceneをEU1.1実ROMで検証し、profileとgame configを同時にenableする。 -## 15. Supported判定 +## 16. Supported判定 EU1.1をruntime検証済みsupportedと宣言するには、exact identity、EU1.1 ARM9/ARM7 banks、title/gameplay/save/load、Prime Controls/Direct Aim semantic validation、native/reference checkpoint比較、US1.0 regressionなしが必要である。 Adaptive WidescreenとEU1.1 FMV runtime bankは基本correctnessの必須条件ではない。未検証機能はfail-closedを維持する。 -## 16. 現在の判定 +## 17. 現在の判定 ### Code / infrastructure **READY FOR EU1.1 ROM VALIDATION AND PROFILE-TAGGED COVERAGE CAPTURE** -ROMなしで可能なprofile、extraction routing、bank isolation、runtime address selection、launcher identity/feature gating、coverage capture metadata、static/runtime coverage promotion、profile-owned FMV bank routing、Windows/Linux packaging、static CIまで実装済み。 +ROMなしで可能なprofile、extraction routing、bank isolation、runtime address selection、launcher identity/feature gating、coverage capture metadata、static/runtime coverage promotion、profile-owned FMV bank routing、checkpoint identity validation、Windows/Linux packaging、static CIまで実装済み。 ### Runtime correctness From eecb92552e2d3cc44b762046a600ea333689cec8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:05:10 +0900 Subject: [PATCH 077/164] Reject unvalidated adaptive FMV capture --- tools/benchmark_mph_fmv.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/benchmark_mph_fmv.py b/tools/benchmark_mph_fmv.py index b0cf76c..be5a1de 100644 --- a/tools/benchmark_mph_fmv.py +++ b/tools/benchmark_mph_fmv.py @@ -165,9 +165,16 @@ def main() -> int: else resolve_repo_path(str(profile["game_config"])).resolve() ) verify_config(config_path, profile, args.version) + + profile_adaptive = bool(profile["adaptive_widescreen"]) + if args.adaptive == "top" and not profile_adaptive: + raise SystemExit( + f"{args.version} does not have validated Adaptive Widescreen; " + "refusing an FMV/coverage capture with --adaptive top" + ) adaptive = args.adaptive if adaptive == "auto": - adaptive = "top" if bool(profile["adaptive_widescreen"]) else "none" + adaptive = "top" if profile_adaptive else "none" targets = sorted(set(args.targets)) if not targets or targets[0] <= 0: From fa10e444cae064e0e01992c78aa9b2d8ce9c8f55 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:05:55 +0900 Subject: [PATCH 078/164] Document FMV adaptive fail-closed capture policy --- docs/EU1_1_BRINGUP.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index e4d454e..fc2e286 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -271,10 +271,11 @@ bank id: mph_amhp1_arm9_fmv_runtime - `--config` 省略時は `config/game-eu11.toml` を選ぶ - `--adaptive auto` が既定 - EU1.1ではprofile policyに従い `--adaptive-widescreen none` +- profileがAdaptive Widescreen未検証なら明示的な `--adaptive top` も拒否する - `benchmark.json` に `mph_profile` と `rom_sha1` を保存する - `--capture-runtime` 時はcapture SHA-1とbyte countも保存する -EU1.1を誤ってUS1.0のAdaptive Widescreen有効状態でbenchmarkする既定値は廃止した。 +EU1.1を誤ってUS1.0のAdaptive Widescreen有効状態でbenchmark/coverage captureする経路はfail-closedにした。 ### 9.3 Before window採取 From a2e7758a0c2acc4a965c3135481bf64cd6ae6cb3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 12:06:26 +0900 Subject: [PATCH 079/164] Require profile capture metadata fields --- tools/check_mph_multirom_profiles.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index 6881e82..5a3a2fa 100644 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -254,8 +254,6 @@ def validate_registry(repo: Path, table: Path | None) -> None: if us.get("fmv_runtime_bank") != "mph_arm9_fmv_runtime": die("US1_0 historical FMV runtime bank identity changed unexpectedly") - # Explicit regression guard for the first non-US revision. These are the - # values currently published by melonPrimeDS's source-of-truth table. eu = profiles.get("EU1_1") if not isinstance(eu, dict): die("EU1_1 profile is required") From 5abcfee6187d572e752985ede2364f165d62dd6a Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 20:10:12 -0700 Subject: [PATCH 080/164] Persist MPH Wi-Fi profiles across launches --- README.md | 6 + game.toml | 2 +- launcher/recomp-ui/launcher_main.cpp | 50 ++- .../tests/launcher_mod_provider_test.cpp | 28 ++ tools/mph_screens.py | 2 + tools/probe_mph_online_first_run.py | 361 ++++++++++++++++++ tools/probe_mph_wfc.py | 28 +- 7 files changed, 466 insertions(+), 11 deletions(-) create mode 100644 tools/probe_mph_online_first_run.py diff --git a/README.md b/README.md index 080945d..69c6afa 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,12 @@ Nintendo WFC / Wiimmfi support is experimental. The current validated state is lobby connectivity: Metroid Prime Hunters can authenticate through Wiimmfi and reach a Friends and Rivals lobby where a locally hosted game is visible. +The launcher keeps the console firmware profile in +`%APPDATA%\MetroidPrimeHuntersRecomp`. Wi-Fi settings, console/game-card +pairing, and WFC updates survive both the in-game system shutdown flow and a +normal window close. Confirming the WFC settings shutdown prompt closes the +application automatically. + Actually joining a match and playing in-game online is not guaranteed. It may fail to connect, disconnect, or desync. diff --git a/game.toml b/game.toml index 5bc7202..0a116fd 100644 --- a/game.toml +++ b/game.toml @@ -61,7 +61,7 @@ size = 0x00028464 [framework] path = "../ndsrecomp" -pin = "46b12e6c18dea47f87d2c1f98c3054149dcbca5d" +pin = "6c6a03bdcf99093f64555c4d05d16e522dc58634" branch = "main" [reference.mphread] diff --git a/launcher/recomp-ui/launcher_main.cpp b/launcher/recomp-ui/launcher_main.cpp index 8b948d2..8258166 100644 --- a/launcher/recomp-ui/launcher_main.cpp +++ b/launcher/recomp-ui/launcher_main.cpp @@ -321,6 +321,27 @@ std::string read_identity_mac(const std::filesystem::path& bios_dir) { return text; } +std::filesystem::path firmware_state_path( + const std::filesystem::path& settings_path, bool generated) { + return settings_path.parent_path() / + (generated ? "firmware-generated.bin" : "firmware-retail.bin"); +} + +std::string read_firmware_state_mac(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return {}; + file.seekg(0x36, std::ios::beg); + unsigned char mac[6]{}; + file.read(reinterpret_cast(mac), sizeof(mac)); + if (file.gcount() != static_cast(sizeof(mac)) || + (mac[0] & 0x01u)) + return {}; + char text[32]; + std::snprintf(text, sizeof(text), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return text; +} + template void copy_text(char (&target)[N], const char* source) { std::snprintf(target, N, "%s", source ? source : ""); @@ -1059,6 +1080,8 @@ bool launch_runner(const std::filesystem::path& game_dir, const char* rom, std::filesystem::create_directories(bios, error); } } + const std::filesystem::path firmware_state = firmware_state_path( + mods.settings_path, no_dumps_mode); std::wstring command = quote(runner.wstring()) + L" " + quote(bios.wstring()) + @@ -1096,6 +1119,7 @@ bool launch_runner(const std::filesystem::path& game_dir, const char* rom, // a player launching through the UI expects Nintendo WFC to work, // so the launcher turns it on and points it at Wiimmfi. L" --network on --wfc on --wfc-provider wiimmfi"; + append_arg(command, L"--firmware-state-path", firmware_state.wstring()); // beads-yjp.16: the firmware console nickname. Passed only when the // player both configured a name and left the identity feature on; // otherwise the runner leaves the firmware's own name alone (a retail @@ -1162,15 +1186,31 @@ int main(int argc, char** argv) { copy_text(settings.bios_path, mod_state.bios_path.c_str()); copy_text(settings.player_name, mod_state.player_name.c_str()); - // Read-only identity detail for the dashboard ONLINE card. Captured once - // at startup: the MAC only changes on the first-ever no-dump launch. - const std::string identity_mac = read_identity_mac( + // Read-only identity detail for the dashboard ONLINE card. Prefer the + // mutable profile once it has been seeded; generated mode falls back to + // the installation identity before that first launch. + const std::filesystem::path selected_bios = mod_state.bios_path.empty() ? mod_state.default_bios_dir - : bios_dir_from_setting(mod_state.bios_path.c_str())); + : bios_dir_from_setting(mod_state.bios_path.c_str()); + bool generated_identity = false; + if (mod_state.bios_path.empty()) { + bool conventional_dumps = true; + for (const NdsDump& dump : kNdsDumps) { + if (!std::filesystem::is_regular_file(selected_bios / dump.file)) + conventional_dumps = false; + } + generated_identity = !conventional_dumps; + } + std::string identity_mac = read_firmware_state_mac(firmware_state_path( + mod_state.settings_path, generated_identity)); + if (identity_mac.empty() && generated_identity) + identity_mac = read_identity_mac(selected_bios); const std::string identity_detail = !identity_mac.empty() - ? "Console MAC: " + identity_mac + " (generated identity)" + ? "Console MAC: " + identity_mac + + (generated_identity + ? " (generated identity)" : " (firmware profile)") : std::string("Console MAC: from the firmware dump, or created " "on the first no-dump launch."); diff --git a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp index 3dd2f13..80e8a1b 100644 --- a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp +++ b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp @@ -13,6 +13,34 @@ bool require(bool condition, const char* label) { } // namespace int main() { + { + const std::filesystem::path root = + std::filesystem::temp_directory_path() / + "mph_firmware_state_launcher_test"; + const std::filesystem::path settings = root / "mods.ini"; + const std::filesystem::path generated = + firmware_state_path(settings, true); + const std::filesystem::path retail = + firmware_state_path(settings, false); + if (!require(generated.filename() == "firmware-generated.bin", + "generated firmware state path")) return 85; + if (!require(retail.filename() == "firmware-retail.bin", + "retail firmware state path")) return 86; + std::filesystem::create_directories(root); + std::vector bytes(128u * 1024u, 0xFFu); + const unsigned char mac[6] = {0x00, 0x09, 0xBF, 0x12, 0x34, 0x56}; + std::memcpy(bytes.data() + 0x36, mac, sizeof(mac)); + { + std::ofstream file(generated, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + } + if (!require(read_firmware_state_mac(generated) == + "00:09:BF:12:34:56", + "firmware state identity display")) return 87; + std::filesystem::remove_all(root); + } + { ModState legacy_state{}; legacy_state.settings_path = diff --git a/tools/mph_screens.py b/tools/mph_screens.py index f1f9eb5..df0e01f 100644 --- a/tools/mph_screens.py +++ b/tools/mph_screens.py @@ -34,6 +34,8 @@ # red cross on the right, both ringed in orange. "dialog_yes": (98, 302, 120, 324), "dialog_no": (138, 302, 160, 324), + # Single green check used by pairing/update notices and acknowledgements. + "dialog_ok": (116, 300, 140, 326), # Friends/Rivals lobby, top screen: the ARENA thumbnail. Flat dark green # while no game row is selected, a lit photo of the arena once a row is # selected and the host's settings have been pulled down (measured: 0 diff --git a/tools/probe_mph_online_first_run.py b/tools/probe_mph_online_first_run.py new file mode 100644 index 0000000..dab370a --- /dev/null +++ b/tools/probe_mph_online_first_run.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +"""Validate MPH Wiimmfi setup and persistence across real process restarts. + +The runner stays in interactive mode. Navigation, screenshots, and lifecycle +control all use its TCP debug surface; input/checkpoint primitives come from +fuzz_mph_gameplay. The source ROM, save, and identity are copied/read-only. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import random +import shutil +import subprocess +import sys +import time +from pathlib import Path +from typing import Any + +from PIL import Image + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +import capture_mph_checkpoints as capture_lib # noqa: E402 +import fuzz_mph_gameplay as input_lib # noqa: E402 +import mph_screens # noqa: E402 +from add_mph_friend import MENU_PATH # noqa: E402 +from run_mph_friend_match import DIALOG_YES, FRIENDS_AND_RIVALS # noqa: E402 +from run_mph_wfc_instances import FILTERS # noqa: E402 + +INPUT_FUZZ = random.Random() + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def wait_frames( + client: capture_lib.DebugClient, + process: subprocess.Popen[bytes], + count: int, + timeout: float = 300.0, +) -> None: + start = input_lib.event_counts(client)["vblank9"] + target = start + count + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError(f"runner exited with code {process.returncode}") + if input_lib.event_counts(client)["vblank9"] >= target: + return + time.sleep(0.025) + raise TimeoutError(f"runner did not advance {count} frames") + + +def tap( + client: capture_lib.DebugClient, + process: subprocess.Popen[bytes], + x: int, + y: int, + wait: int, +) -> None: + fuzzed_x = max(0, min(255, x + INPUT_FUZZ.randint(-2, 2))) + fuzzed_y = max(0, min(191, y + INPUT_FUZZ.randint(-2, 2))) + client.command("touch", x=fuzzed_x, y=fuzzed_y, down=True) + wait_frames(client, process, INPUT_FUZZ.randint(8, 14)) + client.command("touch", x=fuzzed_x, y=fuzzed_y, down=False) + wait_frames(client, process, wait) + + +def press( + client: capture_lib.DebugClient, + process: subprocess.Popen[bytes], + key: str, + wait: int, +) -> None: + bit = input_lib.KEY_BITS[key] + client.command("keys", mask=input_lib.RELEASED_KEYS & ~(1 << bit)) + wait_frames(client, process, INPUT_FUZZ.randint(8, 14)) + client.command("keys", mask=input_lib.RELEASED_KEYS) + wait_frames(client, process, wait) + + +class InteractiveSession: + def __init__( + self, + args: argparse.Namespace, + name: str, + port: int, + profile: Path, + ) -> None: + self.output = args.out / name + self.output.mkdir(parents=True, exist_ok=True) + self.report: list[dict[str, Any]] = [] + self.stdout = (self.output / "runner.stdout.log").open("wb") + self.stderr = (self.output / "runner.stderr.log").open("wb") + command = [ + str(args.runner), str(profile / "bios"), "--interactive", + "--port", str(port), "--rom", str(args.rom), + "--config", str(args.config), "--save-path", + str(profile / "Metroid Prime Hunters.sav"), + "--firmware-state-path", str(profile / "firmware-generated.bin"), + "--startup-mode", "automatic", "--network", "on", + "--network-backend", "slirp", "--wfc", "on", + "--wfc-provider", args.wfc_provider, "--freebios", + "--generated-firmware", "--boot", "direct", + ] + self.process = subprocess.Popen( + command, cwd=args.runner.parent, stdout=self.stdout, + stderr=self.stderr, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + capture_lib.wait_for_server(port, self.process) + self.client = capture_lib.DebugClient(port, timeout=600.0) + + def save(self, label: str) -> dict[str, Any]: + item = input_lib.save_checkpoint( + self.client, self.output, len(self.report), label + ) + self.report.append(item) + return item + + def close_handles(self) -> None: + try: + self.client.close() + except OSError: + pass + self.stdout.close() + self.stderr.close() + (self.output / "report.json").write_text( + json.dumps(self.report, indent=2) + "\n", encoding="utf-8" + ) + + def force_cleanup(self) -> None: + if self.process.poll() is None: + self.process.terminate() + try: + self.process.wait(timeout=10) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait() + self.close_handles() + + +def reach_wfc_menu(session: InteractiveSession) -> None: + wait_frames(session.client, session.process, 7800) + session.save("title") + for label, x, y, wait in MENU_PATH[:4]: + tap(session.client, session.process, x, y, wait) + session.save(label) + + +def setup_and_power_off(session: InteractiveSession) -> dict[str, Any]: + reach_wfc_menu(session) + route = ( + ("wfc-setup-root", 128, 36, 600), + ("settings-tile", 85, 100, 220), + ("slot1", 43, 35, 220), + ("search-for-ap", 128, 37, 1600), + ("test-connection", 192, 36, 2400), + ("save-settings", 190, 177, 600), + ) + for label, x, y, wait in route: + tap(session.client, session.process, x, y, wait) + session.save(label) + press(session.client, session.process, "b", 600) + session.save("setup-root-after-back") + press(session.client, session.process, "b", 600) + prompt = session.save("system-will-shut-down") + + rings = { + name: session.client.command("net_ring_dump", max=256, filter=name) + for name in FILTERS + } + counts = { + name: len(value.get("events", [])) if isinstance(value, dict) else 0 + for name, value in rings.items() + } + if counts.get("dhcp", 0) == 0 or counts.get("backend_error", 0) != 0: + raise RuntimeError(f"connection test did not succeed cleanly: {counts}") + + # The green check confirms the firmware's terminal shutdown prompt. + session.client.command("touch", x=128, y=128, down=True) + try: + session.process.wait(timeout=30) + except subprocess.TimeoutExpired as exc: + raise RuntimeError("guest power-off left the application open") from exc + if session.process.returncode != 0: + raise RuntimeError( + f"guest power-off returned {session.process.returncode}" + ) + session.close_handles() + stderr_text = (session.output / "runner.stderr.log").read_text( + encoding="utf-8", errors="replace" + ) + if "[sdl] guest requested power-off; closing" not in stderr_text: + raise RuntimeError("runner did not report the guest power-off exit") + return {"prompt": prompt, "net_counts": counts, "returncode": 0} + + +def direct_online( + session: InteractiveSession, + require_no_notice_pages: bool, +) -> dict[str, Any]: + reach_wfc_menu(session) + tap(session.client, session.process, *FRIENDS_AND_RIVALS, 240) + session.save("friends-rivals") + + arrow_pages = 0 + yes_pages = 0 + ok_pages = 0 + for index in range(8): + item = session.save(f"dialog-{index}") + image = Image.open(session.output / str(item["image"])).convert("RGB") + ok_bright = mph_screens.bright_count( + image, mph_screens.BOXES["dialog_ok"] + ) + if mph_screens.bright_count( + image, mph_screens.BOXES["dialog_arrow"]) > 40: + arrow_pages += 1 + tap(session.client, session.process, 190, 120, 240) + elif mph_screens.is_connect_dialog(image): + yes_pages += 1 + tap(session.client, session.process, *DIALOG_YES, 240) + elif 40 < ok_bright < 200: + ok_pages += 1 + tap(session.client, session.process, 128, 128, 240) + elif ok_bright >= 200: + # The animated connection badge shares the acknowledgement's + # screen region. Leave it alone and wait for authentication. + break + else: + break + wait_frames(session.client, session.process, 2400) + final = session.save("post-connect") + rings = { + name: session.client.command("net_ring_dump", max=256, filter=name) + for name in FILTERS + } + counts = { + name: len(value.get("events", [])) if isinstance(value, dict) else 0 + for name, value in rings.items() + } + if counts.get("tls_record", 0) == 0 or counts.get("backend_error", 0) != 0: + raise RuntimeError(f"Wiimmfi authentication failed: {counts}") + if require_no_notice_pages and (arrow_pages != 0 or ok_pages != 0): + raise RuntimeError( + "returning profile repeated pairing/update pages: " + f"arrow={arrow_pages}, ok={ok_pages}" + ) + + response = session.client.command("frontend_exit") + if not isinstance(response, dict) or not response.get("requested"): + raise RuntimeError(f"frontend_exit was not accepted: {response!r}") + session.process.wait(timeout=30) + if session.process.returncode != 0: + raise RuntimeError( + f"normal window close returned {session.process.returncode}" + ) + session.close_handles() + return { + "final": final, + "arrow_pages": arrow_pages, + "yes_pages": yes_pages, + "ok_pages": ok_pages, + "net_counts": counts, + "returncode": 0, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runner", type=Path, required=True) + parser.add_argument("--rom", type=Path, required=True) + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--source-save", type=Path, required=True) + parser.add_argument("--source-identity", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--port", type=int, default=21020) + parser.add_argument("--wfc-provider", default="wiimmfi") + parser.add_argument("--input-fuzz-seed", type=int, default=0x4D5048) + parser.add_argument( + "--resume-after-setup", action="store_true", + help="reuse an existing profile after a successful setup/power-off run", + ) + args = parser.parse_args() + INPUT_FUZZ.seed(args.input_fuzz_seed) + for key in ("runner", "rom", "config", "source_save", "source_identity"): + setattr(args, key, getattr(args, key).resolve()) + args.out = args.out.resolve() + args.out.mkdir(parents=True, exist_ok=True) + profile = args.out / "profile" + (profile / "bios").mkdir(parents=True, exist_ok=True) + + source_hashes = { + "save": sha256(args.source_save), + "identity": sha256(args.source_identity), + } + if not args.resume_after_setup: + shutil.copy2(args.source_save, profile / "Metroid Prime Hunters.sav") + shutil.copy2( + args.source_identity, profile / "bios" / "generated-identity.bin" + ) + elif not (profile / "firmware-generated.bin").is_file(): + parser.error("--resume-after-setup requires an existing profile state") + + summary: dict[str, Any] = { + "input_fuzz_seed": args.input_fuzz_seed, + "source_hashes": source_hashes, + } + sessions: list[InteractiveSession] = [] + try: + state = profile / "firmware-generated.bin" + cartridge = profile / "Metroid Prime Hunters.sav" + if not args.resume_after_setup: + first = InteractiveSession( + args, "01-setup-poweroff", args.port, profile + ) + sessions.append(first) + summary["setup"] = setup_and_power_off(first) + summary["state_after_setup"] = { + "size": state.stat().st_size, "sha256": sha256(state) + } + summary["save_after_setup"] = sha256(cartridge) + else: + summary["setup"] = "resumed" + + online_port = args.port + (0 if args.resume_after_setup else 1) + second = InteractiveSession(args, "02-first-online", online_port, profile) + sessions.append(second) + summary["first_online"] = direct_online(second, False) + summary["state_after_first_online"] = sha256(state) + summary["save_after_first_online"] = sha256(cartridge) + + third = InteractiveSession(args, "03-returning-online", online_port + 1, profile) + sessions.append(third) + summary["returning_online"] = direct_online(third, True) + summary["state_after_returning_online"] = sha256(state) + summary["save_after_returning_online"] = sha256(cartridge) + finally: + for session in sessions: + if session.process.poll() is None: + session.force_cleanup() + + if sha256(args.source_save) != source_hashes["save"] or \ + sha256(args.source_identity) != source_hashes["identity"]: + raise RuntimeError("source save/identity changed during QA") + summary["sources_unchanged"] = True + summary["success"] = True + (args.out / "summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8" + ) + print(json.dumps(summary, indent=2), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/probe_mph_wfc.py b/tools/probe_mph_wfc.py index 52c9c81..16619ba 100644 --- a/tools/probe_mph_wfc.py +++ b/tools/probe_mph_wfc.py @@ -148,6 +148,12 @@ def run(args: argparse.Namespace) -> dict[str, Any]: command.append("--no-save") if args.firmware_path: command.extend(["--firmware-path", str(args.firmware_path.resolve())]) + if args.firmware_state_path: + command.extend([ + "--firmware-state-path", str(args.firmware_state_path.resolve()) + ]) + if args.no_dumps: + command.extend(["--freebios", "--generated-firmware", "--boot", "direct"]) if args.discover_static_misses: command.append("--discover-static-misses") @@ -195,7 +201,12 @@ def run(args: argparse.Namespace) -> dict[str, Any]: ("settings-tile", 85, 100, 220), ("slot1", 43, 35, 220), ("search-for-ap", 128, 37, 1600), - ("ap-row", 50, 65, 220), + # Generated firmware already advertises the runner's + # ndsrecomp AP. Discovery selects it and returns to + # Connection 1 Settings; the old probe tapped (50,65) + # here, which is the SSID row and opened the keyboard. + ("test-connection", 192, 36, 2400), + ("save-settings", 190, 177, 600), ) else: if args.flow == "friends-rivals": @@ -215,9 +226,12 @@ def run(args: argparse.Namespace) -> dict[str, Any]: save(label) if args.flow == "setup": - input_lib.press_key(client, "a", 12) - input_lib.advance_frames(client, 300) - save("after-a-start-test") + input_lib.press_key(client, "b", 12) + input_lib.advance_frames(client, 600) + save("setup-root-after-back") + input_lib.press_key(client, "b", 12) + input_lib.advance_frames(client, 600) + save("exit-prompt") if args.flow == "search-game": for target in args.targets: @@ -272,7 +286,7 @@ def run(args: argparse.Namespace) -> dict[str, Any]: firmware = client.command("firmware_dump") if not isinstance(firmware, dict): raise RuntimeError("firmware_dump returned a non-object response") - if int(firmware.get("size", 0)) != 262144: + if int(firmware.get("size", 0)) not in (131072, 262144): raise RuntimeError( f"firmware_dump returned {firmware.get('size')} bytes" ) @@ -313,6 +327,8 @@ def main() -> int: parser.add_argument("--config", type=Path, default=Path("game.toml")) parser.add_argument("--save-path", type=Path) parser.add_argument("--firmware-path", type=Path) + parser.add_argument("--firmware-state-path", type=Path) + parser.add_argument("--no-dumps", action="store_true") parser.add_argument("--firmware-out", type=Path) parser.add_argument("--discover-static-misses", action="store_true") parser.add_argument( @@ -357,6 +373,8 @@ def main() -> int: parser.error("--port must be in 1..65535") if args.instance_index < 0 or args.instance_index > 255: parser.error("--instance-index must be in 0..255") + if args.no_dumps and args.firmware_path: + parser.error("--no-dumps and --firmware-path are mutually exclusive") summary = run(args) print(json.dumps(summary, indent=2), flush=True) From bf9a977865bc20bedd141533024b851207979792 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:28:16 +0900 Subject: [PATCH 081/164] Detect MPH runtime profile from base ROM header --- config/mph_rom_profiles.json | 84 ++++++++-- tools/check_mph_multirom_profiles.py | 178 +++++++++++++++------- tools/mph_profile.py | 6 +- tools/patch_ndsrecomp_mph_runtime.py | 169 ++++++++++++++++----- tools/tests/mph_runtime_profile_test.cpp | 185 ++++++++++++++--------- 5 files changed, 440 insertions(+), 182 deletions(-) mode change 100644 => 100755 tools/check_mph_multirom_profiles.py mode change 100644 => 100755 tools/mph_profile.py mode change 100644 => 100755 tools/patch_ndsrecomp_mph_runtime.py diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 28574a3..016efd2 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -1,6 +1,72 @@ { - "schema": 2, - "runtime_address_source": "https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h", + "schema": 3, + "runtime_address_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h", + "runtime_detection_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp", + "runtime_profiles": { + "US1_0": { + "game_code": "AMHE", + "revision": 0, + "runtime": { + "morph_state": "0x020DA818", + "aim_x": "0x020DE526", + "aim_y": "0x020DE52E" + } + }, + "US1_1": { + "game_code": "AMHE", + "revision": 1, + "runtime": { + "morph_state": "0x020DB098", + "aim_x": "0x020DEDA6", + "aim_y": "0x020DEDAE" + } + }, + "EU1_0": { + "game_code": "AMHP", + "revision": 0, + "runtime": { + "morph_state": "0x020DB0B8", + "aim_x": "0x020DEDC6", + "aim_y": "0x020DEDCE" + } + }, + "EU1_1": { + "game_code": "AMHP", + "revision": 1, + "runtime": { + "morph_state": "0x020DB138", + "aim_x": "0x020DEE46", + "aim_y": "0x020DEE4E" + } + }, + "JP1_0": { + "game_code": "AMHJ", + "revision": 0, + "runtime": { + "morph_state": "0x020DC6D8", + "aim_x": "0x020E03E6", + "aim_y": "0x020E03EE" + } + }, + "JP1_1": { + "game_code": "AMHJ", + "revision": 1, + "runtime": { + "morph_state": "0x020DC698", + "aim_x": "0x020E03A6", + "aim_y": "0x020E03AE" + } + }, + "KR1_0": { + "game_code": "AMHK", + "revision": 0, + "runtime": { + "morph_state": "0x020D3EE4", + "aim_x": "0x020D7C0E", + "aim_y": "0x020D7C16" + } + } + }, "profiles": { "US1_0": { "display_name": "Metroid Prime Hunters (USA rev 0)", @@ -15,12 +81,7 @@ "fmv_runtime": true, "fmv_runtime_bank": "mph_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters.nds", - "adaptive_widescreen": true, - "runtime": { - "morph_state": "0x020DA818", - "aim_x": "0x020DE526", - "aim_y": "0x020DE52E" - } + "adaptive_widescreen": true }, "EU1_1": { "display_name": "Metroid Prime Hunters (Europe rev 1)", @@ -35,12 +96,7 @@ "fmv_runtime": false, "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds", - "adaptive_widescreen": false, - "runtime": { - "morph_state": "0x020DB138", - "aim_x": "0x020DEE46", - "aim_y": "0x020DEE4E" - } + "adaptive_widescreen": false } } } diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py old mode 100644 new mode 100755 index 5a3a2fa..d0ceb55 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 """Static consistency checks for Metroid Prime Hunters ROM profiles. -This intentionally does not need a copyrighted ROM. It verifies that each -profile's identity, coverage seed, game config, launcher policy, FMV bank -identity, and host-side runtime addresses agree. When --melonprime-table is -provided, Aim/Morph addresses are also cross-checked against melonPrimeDS's -MelonPrimeGameRomAddrTable.h, which is the source of truth for those fields. +Build/capture profiles describe exact clean-content identities and generated +artifacts. Runtime profiles separately describe the seven supported base ROM +layouts. This separation is intentional: whole-ROM SHA-1 is provenance, while +runtime Aim/Morph address selection follows gameCode + revision. + +When --melonprime-table is provided, every runtime Aim/Morph address is +cross-checked against melonPrimeDS's MelonPrimeGameRomAddrTable.h. """ from __future__ import annotations @@ -26,6 +28,15 @@ "aim_x": "baseAimX", "aim_y": "baseAimY", } +EXPECTED_RUNTIME_PROFILES = { + "US1_0": ("AMHE", 0), + "US1_1": ("AMHE", 1), + "EU1_0": ("AMHP", 0), + "EU1_1": ("AMHP", 1), + "JP1_0": ("AMHJ", 0), + "JP1_1": ("AMHJ", 1), + "KR1_0": ("AMHK", 0), +} def die(message: str) -> NoReturn: @@ -91,33 +102,114 @@ def parse_melonprime_table(path: Path) -> dict[str, dict[str, int]]: return result +def validate_runtime_profiles( + registry: dict[str, object], + melon: dict[str, dict[str, int]] | None, +) -> dict[str, dict[str, object]]: + runtime_profiles = registry.get("runtime_profiles") + if not isinstance(runtime_profiles, dict): + die("ROM profile registry has no object-valued runtime_profiles") + + actual_keys = set(runtime_profiles) + expected_keys = set(EXPECTED_RUNTIME_PROFILES) + if actual_keys != expected_keys: + missing = ", ".join(sorted(expected_keys - actual_keys)) or "none" + extra = ", ".join(sorted(actual_keys - expected_keys)) or "none" + die( + "runtime_profiles must contain exactly the seven supported retail " + f"profiles (missing: {missing}; extra: {extra})" + ) + + seen_identity: set[tuple[str, int]] = set() + validated: dict[str, dict[str, object]] = {} + for key, expected_identity in EXPECTED_RUNTIME_PROFILES.items(): + profile = runtime_profiles.get(key) + if not isinstance(profile, dict): + die(f"runtime profile {key} must be an object") + + game_code = profile.get("game_code") + revision = profile.get("revision") + if (game_code, revision) != expected_identity: + die( + f"{key} runtime identity is {(game_code, revision)!r}; " + f"expected {expected_identity!r}" + ) + if not isinstance(game_code, str) or len(game_code) != 4 or not game_code.isascii(): + die(f"{key}.game_code must be exactly four ASCII characters") + if not isinstance(revision, int) or revision not in (0, 1): + die(f"{key}.revision is not an explicitly supported revision") + + identity = (game_code, revision) + if identity in seen_identity: + die(f"duplicate runtime cartridge identity: {game_code} rev {revision}") + seen_identity.add(identity) + + runtime = profile.get("runtime") + if not isinstance(runtime, dict): + die(f"{key}.runtime is required") + parsed_runtime: dict[str, int] = {} + for profile_field in REQUIRED_RUNTIME_FIELDS: + value = parse_hex(runtime.get(profile_field), f"{key}.runtime.{profile_field}") + if not 0x02000000 <= value <= 0x023FFFFF: + die(f"{key}.runtime.{profile_field} is outside DS main RAM") + parsed_runtime[profile_field] = value + + if melon is not None: + if key not in melon: + die(f"{key} is not present in melonPrimeDS RomGroup") + for profile_field, melon_field in REQUIRED_RUNTIME_FIELDS.items(): + actual = parsed_runtime[profile_field] + expected = melon[key][melon_field] + if actual != expected: + die( + f"{key}.{profile_field}=0x{actual:08X}, but " + f"melonPrimeDS {melon_field}=0x{expected:08X}" + ) + + validated[key] = profile + + return validated + + def validate_registry(repo: Path, table: Path | None) -> None: registry_path = repo / "config" / "mph_rom_profiles.json" - registry = load_json(registry_path) - if not isinstance(registry, dict): + registry_obj = load_json(registry_path) + if not isinstance(registry_obj, dict): die("ROM profile registry must be a JSON object") - if registry.get("schema") != 2: - die("ROM profile registry schema must be 2") + registry: dict[str, object] = registry_obj + + if registry.get("schema") != 3: + die("ROM profile registry schema must be 3") - expected_source = ( - "https://github.com/ag-advania/melonPrimeDS/blob/main/" + expected_address_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" "src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h" ) - if registry.get("runtime_address_source") != expected_source: - die("runtime_address_source is not the approved melonPrimeDS address table") + expected_detection_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" + "src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp" + ) + if registry.get("runtime_address_source") != expected_address_source: + die("runtime_address_source is not the approved develop_hud address table") + if registry.get("runtime_detection_source") != expected_detection_source: + die("runtime_detection_source is not the approved develop_hud detector") + + melon = parse_melonprime_table(table) if table else None + runtime_profiles = validate_runtime_profiles(registry, melon) profiles = registry.get("profiles") if not isinstance(profiles, dict) or not profiles: - die("ROM profile registry has no profiles") + die("ROM profile registry has no clean build/capture profiles") - melon = parse_melonprime_table(table) if table else None seen_sha1: set[str] = set() - seen_identity: set[tuple[str, int]] = set() seen_fmv_banks: set[str] = set() for key, profile in profiles.items(): if not isinstance(profile, dict): die(f"profile {key} must be an object") + if key not in runtime_profiles: + die(f"{key}: clean build profile has no runtime base profile") + sha1 = profile.get("sha1") game_code = profile.get("game_code") revision = profile.get("revision") @@ -126,14 +218,13 @@ def validate_registry(repo: Path, table: Path | None) -> None: if sha1 in seen_sha1: die(f"duplicate SHA-1 in profile registry: {sha1}") seen_sha1.add(sha1) - if not isinstance(game_code, str) or len(game_code) != 4: - die(f"{key}.game_code must be four characters") - if not isinstance(revision, int) or not 0 <= revision <= 255: - die(f"{key}.revision must be an unsigned byte") - identity = (game_code, revision) - if identity in seen_identity: - die(f"duplicate cartridge identity: {game_code} rev {revision}") - seen_identity.add(identity) + + runtime_profile = runtime_profiles[key] + if ( + game_code != runtime_profile.get("game_code") + or revision != runtime_profile.get("revision") + ): + die(f"{key}: clean build identity disagrees with runtime base profile") launcher_default_rom = profile.get("launcher_default_rom") if ( @@ -157,9 +248,7 @@ def validate_registry(repo: Path, table: Path | None) -> None: or not BANK_RE.fullmatch(fmv_runtime_bank) or "_arm9_" not in fmv_runtime_bank ): - die( - f"{key}.fmv_runtime_bank must be a C identifier-style ARM9 bank name" - ) + die(f"{key}.fmv_runtime_bank must be a C identifier-style ARM9 bank name") if fmv_runtime_bank in seen_fmv_banks: die(f"duplicate FMV runtime bank identity: {fmv_runtime_bank}") seen_fmv_banks.add(fmv_runtime_bank) @@ -182,28 +271,6 @@ def validate_registry(repo: Path, table: Path | None) -> None: f"expected {fmv_runtime_bank!r}" ) - runtime = profile.get("runtime") - if not isinstance(runtime, dict): - die(f"{key}.runtime is required") - parsed_runtime: dict[str, int] = {} - for profile_field in REQUIRED_RUNTIME_FIELDS: - value = parse_hex(runtime.get(profile_field), f"{key}.runtime.{profile_field}") - if not 0x02000000 <= value <= 0x023FFFFF: - die(f"{key}.runtime.{profile_field} is outside DS main RAM") - parsed_runtime[profile_field] = value - - if melon is not None: - if key not in melon: - die(f"{key} is not present in melonPrimeDS RomGroup") - for profile_field, melon_field in REQUIRED_RUNTIME_FIELDS.items(): - actual = parsed_runtime[profile_field] - expected = melon[key][melon_field] - if actual != expected: - die( - f"{key}.{profile_field}=0x{actual:08X}, but " - f"melonPrimeDS {melon_field}=0x{expected:08X}" - ) - coverage_path = repo / str(profile.get("coverage", "")) if not coverage_path.is_file(): die(f"{key}.coverage does not exist: {coverage_path}") @@ -250,18 +317,18 @@ def validate_registry(repo: Path, table: Path | None) -> None: us = profiles.get("US1_0") if not isinstance(us, dict): - die("US1_0 profile is required") + die("US1_0 clean build profile is required") if us.get("fmv_runtime_bank") != "mph_arm9_fmv_runtime": die("US1_0 historical FMV runtime bank identity changed unexpectedly") eu = profiles.get("EU1_1") if not isinstance(eu, dict): - die("EU1_1 profile is required") + die("EU1_1 clean build profile is required") if eu.get("game_code") != "AMHP" or eu.get("revision") != 1: die("EU1_1 must remain AMHP revision 1") if eu.get("sha1") != "bdcd1dea293e24c98d4c481430e90d21198985a5": die("EU1_1 SHA-1 changed unexpectedly") - eu_runtime = eu.get("runtime", {}) + eu_runtime = runtime_profiles["EU1_1"].get("runtime", {}) expected_eu_runtime = { "morph_state": "0x020DB138", "aim_x": "0x020DEE46", @@ -278,9 +345,12 @@ def validate_registry(repo: Path, table: Path | None) -> None: if eu.get("launcher_default_rom") != "Metroid Prime Hunters (Europe Rev 1).nds": die("EU1_1 launcher default ROM filename changed unexpectedly") - print(f"OK: validated {len(profiles)} MPH ROM profiles") + print( + f"OK: validated {len(runtime_profiles)} runtime base profiles and " + f"{len(profiles)} clean build/capture profiles" + ) if melon is not None: - print(f"OK: Aim/Morph addresses match melonPrimeDS table: {table}") + print(f"OK: all seven Aim/Morph profiles match melonPrimeDS table: {table}") def main() -> None: diff --git a/tools/mph_profile.py b/tools/mph_profile.py old mode 100644 new mode 100755 index d87ef64..6305a09 --- a/tools/mph_profile.py +++ b/tools/mph_profile.py @@ -153,7 +153,7 @@ def verify_rom_identity( ) expected_code = str(profile["game_code"]).encode("ascii") - if len(header) <= 0x1C: + if len(header) <= 0x1E: raise SystemExit(f"ROM header is truncated: {rom_path}") if header[0x0C:0x10] != expected_code: raise SystemExit( @@ -161,9 +161,9 @@ def verify_rom_identity( f"expected {expected_code!r}" ) revision = int(profile["revision"]) - if header[0x1C] != revision: + if header[0x1E] != revision: raise SystemExit( - f"ROM revision mismatch for {version}: got {header[0x1C]}, " + f"ROM revision mismatch for {version}: got {header[0x1E]}, " f"expected {revision}" ) return actual_sha1 diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py old mode 100644 new mode 100755 index 7659f01..7f1b1b3 --- a/tools/patch_ndsrecomp_mph_runtime.py +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -2,13 +2,22 @@ """Apply the MPH multi-ROM runtime-profile shim to the pinned ndsrecomp runner. The upstream runner currently hard-codes Metroid Prime Hunters USA rev-0 RAM -addresses for Prime Controls and direct mouse aim. This project supports more -than one retail revision, so those host-side hooks must be selected by exact -ROM SHA-1 instead of by title/address assumptions. +addresses for Prime Controls and direct mouse aim. Runtime address selection +must follow the base game/revision, not the whole-ROM hash, so modified ROMs +that preserve a supported MPH cartridge identity can use the correct layout. -The address values are generated from config/mph_rom_profiles.json. The -registry records melonPrimeDS's MelonPrimeGameRomAddrTable.h as the source of -truth for the MPH runtime addresses. +The detector mirrors melonPrimeDS's fallback identity: +NDS gameCode @0x0C + ROM revision @0x1E. Unlike melonPrimeDS, revisions outside +the seven explicitly supported retail profiles fail closed instead of mapping +all non-zero revisions to 1.1. + +Whole-ROM SHA-1 remains a clean-content/provenance identity. If a SHA-1 is one +of the configured known-clean ROMs, its profile must agree with the header; +an impossible clean-hash/header mismatch is rejected. An unknown SHA-1 does +not by itself reject a recognized base profile. + +Runtime address values are generated from config/mph_rom_profiles.json and +cross-checked in CI against melonPrimeDS's MelonPrimeGameRomAddrTable.h. The source patch is intentionally small, idempotent, and fail-closed. If the pinned ndsrecomp source changes enough that the expected preimages are absent, @@ -44,26 +53,65 @@ def parse_address(value: object, *, profile: str, field: str) -> int: def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: registry = json.loads(registry_path.read_text(encoding="utf-8")) - profiles = registry.get("profiles") - if not isinstance(profiles, dict) or not profiles: - raise SystemExit("ROM profile registry has no profiles") + runtime_profiles = registry.get("runtime_profiles") + if not isinstance(runtime_profiles, dict) or not runtime_profiles: + raise SystemExit("ROM profile registry has no runtime_profiles") + + build_profiles = registry.get("profiles") + if not isinstance(build_profiles, dict): + raise SystemExit("ROM profile registry has no build profiles") result: list[dict[str, object]] = [] - for key, profile in profiles.items(): + seen_identity: set[tuple[str, int]] = set() + seen_clean_sha1: set[str] = set() + + for key, profile in runtime_profiles.items(): if not isinstance(profile, dict): - continue + raise SystemExit(f"{key}: runtime profile must be an object") + game_code = profile.get("game_code") + revision = profile.get("revision") runtime = profile.get("runtime") - if runtime is None: - continue + if ( + not isinstance(game_code, str) + or len(game_code) != 4 + or not game_code.isascii() + ): + raise SystemExit(f"{key}.game_code must be exactly four ASCII bytes") + if not isinstance(revision, int) or revision not in (0, 1): + raise SystemExit(f"{key}.revision must be an explicitly supported 0/1") if not isinstance(runtime, dict): raise SystemExit(f"{key}.runtime must be an object") - sha1 = profile.get("sha1") - if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): - raise SystemExit(f"{key}.sha1 must be 40 lowercase hex digits") + + identity = (game_code, revision) + if identity in seen_identity: + raise SystemExit( + f"duplicate runtime cartridge identity: {game_code} rev {revision}" + ) + seen_identity.add(identity) + + known_clean_sha1 = "" + clean = build_profiles.get(key) + if clean is not None: + if not isinstance(clean, dict): + raise SystemExit(f"{key}: build profile must be an object") + if clean.get("game_code") != game_code or clean.get("revision") != revision: + raise SystemExit( + f"{key}: clean build identity disagrees with runtime profile" + ) + sha1 = clean.get("sha1") + if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): + raise SystemExit(f"{key}.sha1 must be 40 lowercase hex digits") + if sha1 in seen_clean_sha1: + raise SystemExit(f"duplicate known-clean SHA-1: {sha1}") + seen_clean_sha1.add(sha1) + known_clean_sha1 = sha1 + result.append( { "key": key, - "sha1": sha1, + "game_code": game_code, + "revision": revision, + "known_clean_sha1": known_clean_sha1, "morph_state": parse_address( runtime.get("morph_state"), profile=key, field="morph_state" ), @@ -76,8 +124,17 @@ def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: } ) - if not result: - raise SystemExit("ROM profile registry contains no MPH runtime profiles") + expected = { + "US1_0", "US1_1", "EU1_0", "EU1_1", "JP1_0", "JP1_1", "KR1_0" + } + actual = {str(profile["key"]) for profile in result} + if actual != expected: + missing = ", ".join(sorted(expected - actual)) or "none" + extra = ", ".join(sorted(actual - expected)) or "none" + raise SystemExit( + f"runtime profile set must be exactly the seven supported retail " + f"profiles (missing: {missing}; extra: {extra})" + ) return result @@ -85,9 +142,13 @@ def generated_header(profiles: list[dict[str, object]]) -> str: rows = [] for profile in profiles: rows.append( - " {\"%s\", 0x%08Xu, 0x%08Xu, 0x%08Xu}, // %s" + ' {"%s", "%s", %du, "%s", 0x%08Xu, 0x%08Xu, 0x%08Xu},' + " // %s" % ( - profile["sha1"], + profile["key"], + profile["game_code"], + profile["revision"], + profile["known_clean_sha1"], profile["morph_state"], profile["aim_x"], profile["aim_y"], @@ -101,8 +162,15 @@ def generated_header(profiles: list[dict[str, object]]) -> str: // Generated by MetroidPrimeHuntersRecomp/tools/patch_ndsrecomp_mph_runtime.py. // Do not edit in the ndsrecomp checkout; edit config/mph_rom_profiles.json. +// +// game_code + revision selects the base runtime layout. known_clean_sha1 is +// provenance/consistency metadata only; an unknown SHA-1 is a supported variant +// when its exact header identity matches one of these seven profiles. struct NdsMphRuntimeProfile { - const char* sha1; + const char* key; + const char* game_code; + uint8_t revision; + const char* known_clean_sha1; uint32_t morph_state; uint32_t aim_x; uint32_t aim_y; @@ -144,12 +212,13 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: title_h, "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: exact-ROM runtime profile selection.\n" - "bool nds_title_patches_select_mph_runtime_profile(const char* rom_sha1);\n" + "// MPH_MULTIROM_RUNTIME_PROFILE: base-profile detection from NDS header.\n" + "bool nds_title_patches_select_mph_runtime_profile(\n" + " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1);\n" "bool nds_title_patches_mph_in_ball();\n" "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", - "MPH_MULTIROM_RUNTIME_PROFILE", + "base-profile detection from NDS header", ) patch_once( @@ -166,9 +235,9 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: "// path but removes the finite physical touchscreen edge.\n" "constexpr uint32_t kMphUs10AimX = 0x020DE526u;\n" "constexpr uint32_t kMphUs10AimY = 0x020DE52Eu;\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: selected only by exact cartridge SHA-1.\n" + "// MPH_MULTIROM_RUNTIME_PROFILE: selected by exact gameCode + revision.\n" "const NdsMphRuntimeProfile* g_mph_runtime_profile = nullptr;\n", - "selected only by exact cartridge SHA-1", + "selected by exact gameCode + revision", ) patch_once( title_cpp, @@ -183,17 +252,35 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: " bus_write_u32_slow(kMphUs10AimY, static_cast(dy));\n" " return true;\n" "}\n", - "bool nds_title_patches_select_mph_runtime_profile(const char* rom_sha1) {\n" + "bool nds_title_patches_select_mph_runtime_profile(\n" + " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1) {\n" " g_mph_mouse_aim = false;\n" " g_mph_runtime_profile = nullptr;\n" - " if (!rom_sha1) return false;\n" + " // NDS header: game code @0x0C..0x0F, ROM version @0x1E.\n" + " if (!rom_data || rom_size <= 0x1Eu || !rom_sha1) return false;\n\n" + " const NdsMphRuntimeProfile* header_profile = nullptr;\n" " for (const NdsMphRuntimeProfile& profile : kNdsMphRuntimeProfiles) {\n" - " if (std::strcmp(profile.sha1, rom_sha1) == 0) {\n" - " g_mph_runtime_profile = &profile;\n" - " return true;\n" + " if (std::memcmp(profile.game_code, rom_data + 0x0Cu, 4) == 0 &&\n" + " profile.revision == rom_data[0x1Eu]) {\n" + " if (header_profile) return false; // ambiguous registry: fail closed\n" + " header_profile = &profile;\n" " }\n" " }\n" - " return false;\n" + " if (!header_profile) return false;\n\n" + " // Whole-ROM SHA-1 is clean identity/provenance, not the selector.\n" + " // If this content is a known clean dump, its header must agree with\n" + " // the corresponding base profile. Unknown hashes are mod variants.\n" + " const NdsMphRuntimeProfile* clean_profile = nullptr;\n" + " for (const NdsMphRuntimeProfile& profile : kNdsMphRuntimeProfiles) {\n" + " if (profile.known_clean_sha1[0] != '\\0' &&\n" + " std::strcmp(profile.known_clean_sha1, rom_sha1) == 0) {\n" + " clean_profile = &profile;\n" + " break;\n" + " }\n" + " }\n" + " if (clean_profile && clean_profile != header_profile) return false;\n\n" + " g_mph_runtime_profile = header_profile;\n" + " return true;\n" "}\n\n" "bool nds_title_patches_mph_in_ball() {\n" " return g_mph_runtime_profile &&\n" @@ -219,8 +306,8 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: patch_once( frontend_cpp, "constexpr uint32_t kMphUs10MorphState = 0x020DA818u;\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: morph address is selected by ROM SHA-1.\n", - "morph address is selected by ROM SHA-1", + "// MPH_MULTIROM_RUNTIME_PROFILE: morph address comes from the base ROM profile.\n", + "morph address comes from the base ROM profile", ) patch_once( frontend_cpp, @@ -235,17 +322,19 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: " mph_mouse_aim_policy =\n" " rom_sha1 == \"90164d1ac127ee5f9815ea4ae7de798c7b5fc629\" &&\n" " frontend_options.relative_mouse_touch;\n", - " // MPH_MULTIROM_RUNTIME_PROFILE: host hooks are enabled only when the\n" - " // exact ROM SHA-1 has a validated revision-specific address profile.\n" + " // MPH_MULTIROM_RUNTIME_PROFILE: select the base address layout from\n" + " // gameCode + revision. SHA-1 only checks known-clean consistency;\n" + " // modified ROMs with a supported exact header identity remain usable.\n" " const bool mph_runtime_profile =\n" - " nds_title_patches_select_mph_runtime_profile(rom_sha1.c_str());\n" + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()), rom_sha1.c_str());\n" " mph_mouse_aim_policy =\n" " mph_runtime_profile && frontend_options.relative_mouse_touch;\n", - "host hooks are enabled only when the", + "SHA-1 only checks known-clean consistency", ) print( - f"Patched ndsrecomp MPH runtime profiles: " + f"Patched ndsrecomp MPH runtime base profiles: " + ", ".join(str(profile["key"]) for profile in profiles) ) diff --git a/tools/tests/mph_runtime_profile_test.cpp b/tools/tests/mph_runtime_profile_test.cpp index bb94478..6ade13e 100644 --- a/tools/tests/mph_runtime_profile_test.cpp +++ b/tools/tests/mph_runtime_profile_test.cpp @@ -13,6 +13,32 @@ struct Write32 { uint32_t value; }; +struct RuntimeCase { + const char* name; + const char* game_code; + uint8_t revision; + uint32_t morph; + uint32_t aim_x; + uint32_t aim_y; +}; + +constexpr RuntimeCase kCases[] = { + {"US1_0", "AMHE", 0, 0x020DA818u, 0x020DE526u, 0x020DE52Eu}, + {"US1_1", "AMHE", 1, 0x020DB098u, 0x020DEDA6u, 0x020DEDAEu}, + {"EU1_0", "AMHP", 0, 0x020DB0B8u, 0x020DEDC6u, 0x020DEDCEu}, + {"EU1_1", "AMHP", 1, 0x020DB138u, 0x020DEE46u, 0x020DEE4Eu}, + {"JP1_0", "AMHJ", 0, 0x020DC6D8u, 0x020E03E6u, 0x020E03EEu}, + {"JP1_1", "AMHJ", 1, 0x020DC698u, 0x020E03A6u, 0x020E03AEu}, + {"KR1_0", "AMHK", 0, 0x020D3EE4u, 0x020D7C0Eu, 0x020D7C16u}, +}; + +constexpr const char* kUs10Sha1 = + "90164d1ac127ee5f9815ea4ae7de798c7b5fc629"; +constexpr const char* kEu11Sha1 = + "bdcd1dea293e24c98d4c481430e90d21198985a5"; +constexpr const char* kUnknownSha1 = + "0000000000000000000000000000000000000000"; + std::vector g_writes; uint32_t g_morph_addr = 0; uint8_t g_morph_value = 0; @@ -32,6 +58,19 @@ void expect_write(std::size_t index, uint32_t addr, uint32_t value, expect(ok, message); } +std::vector make_rom(const char* game_code, uint8_t revision) { + std::vector rom(0x200u, 0u); + std::memcpy(rom.data() + 0x0Cu, game_code, 4u); + rom[0x1Eu] = revision; + return rom; +} + +bool select(const char* game_code, uint8_t revision, const char* sha1) { + const auto rom = make_rom(game_code, revision); + return nds_title_patches_select_mph_runtime_profile( + rom.data(), static_cast(rom.size()), sha1); +} + } // namespace // title_patches.cpp only needs these bus surfaces. Keeping the test at the @@ -50,87 +89,91 @@ extern "C" void bus_write_u32_slow(uint32_t addr, uint32_t value) { } int main() { - constexpr const char* kUs10Sha1 = - "90164d1ac127ee5f9815ea4ae7de798c7b5fc629"; - constexpr const char* kEu11Sha1 = - "bdcd1dea293e24c98d4c481430e90d21198985a5"; - - // Unknown or absent identities must fail closed. Enabling direct aim after - // a failed selection must not create guest-memory writes. - expect(!nds_title_patches_select_mph_runtime_profile(nullptr), - "null ROM identity must not select a runtime profile"); - expect(!nds_title_patches_select_mph_runtime_profile( - "0000000000000000000000000000000000000000"), - "unknown ROM identity must not select a runtime profile"); - nds_title_patches_set_mph_mouse_aim(true); - expect(!nds_title_patches_apply_mph_mouse_delta(1, 1), - "unknown ROM must not accept direct mouse aim"); - expect(g_writes.empty(), "unknown ROM must not write guest aim fields"); - expect(!nds_title_patches_mph_in_ball(), - "unknown ROM must not read a guessed morph address"); - - // Baseline regression: US1.0 must keep the exact addresses that were - // hard-coded before multi-ROM support. - expect(nds_title_patches_select_mph_runtime_profile(kUs10Sha1), - "US1.0 profile must be selectable"); - nds_title_patches_set_mph_mouse_aim(true); - g_writes.clear(); - expect(nds_title_patches_apply_mph_mouse_delta(11, -7), - "US1.0 direct mouse aim must accept a non-zero delta"); - expect(g_writes.size() == 2, "US1.0 aim must perform two writes"); - expect_write(0, 0x020DE526u, 11u, - "US1.0 X delta must target baseAimX"); - expect_write(1, 0x020DE52Eu, static_cast(-7), - "US1.0 Y delta must target baseAimY"); - g_morph_addr = 0x020DA818u; - g_morph_value = 0x02u; - expect(nds_title_patches_mph_in_ball(), - "US1.0 baseIsAltForm=2 must report Morph Ball"); - - // EU1.1 addresses come from melonPrimeDS - // MelonPrimeGameRomAddrTable.h, not from an inferred relocation delta. - expect(nds_title_patches_select_mph_runtime_profile(kEu11Sha1), - "EU1.1 profile must be selectable"); - - // Selection deliberately clears the prior direct-aim enable. This prevents - // state from one cartridge identity leaking into another profile. - g_writes.clear(); - expect(!nds_title_patches_apply_mph_mouse_delta(3, 4), - "profile switch must clear direct-aim enable state"); - expect(g_writes.empty(), "disabled aim after profile switch must not write"); + // All seven retail base profiles must dispatch from gameCode@0x0C plus the + // exact supported revision@0x1E even when the whole-ROM SHA-1 is unknown. + // This is the mod-ROM path: content identity is provenance, not selector. + for (const RuntimeCase& c : kCases) { + expect(select(c.game_code, c.revision, kUnknownSha1), c.name); + + // Profile selection clears the previous direct-aim enable state. + g_writes.clear(); + expect(!nds_title_patches_apply_mph_mouse_delta(7, -5), + "profile switch must clear direct-aim enable state"); + expect(g_writes.empty(), + "disabled aim after profile switch must not write"); + + nds_title_patches_set_mph_mouse_aim(true); + expect(nds_title_patches_apply_mph_mouse_delta(7, -5), + "recognized base profile must accept direct mouse aim"); + expect(g_writes.size() == 2, + "recognized base profile must perform two aim writes"); + expect_write(0, c.aim_x, 7u, + "X delta must target profile-specific baseAimX"); + expect_write(1, c.aim_y, static_cast(-5), + "Y delta must target profile-specific baseAimY"); + + g_morph_addr = c.morph; + g_morph_value = 0x02u; + expect(nds_title_patches_mph_in_ball(), + "baseIsAltForm=2 must report Morph Ball"); + g_morph_value = 0x00u; + expect(!nds_title_patches_mph_in_ball(), + "non-alt form must not report Morph Ball"); + } - nds_title_patches_set_mph_mouse_aim(true); - expect(nds_title_patches_apply_mph_mouse_delta(3, -4), - "EU1.1 direct mouse aim must accept a non-zero delta"); - expect(g_writes.size() == 2, "EU1.1 aim must perform two writes"); - expect_write(0, 0x020DEE46u, 3u, - "EU1.1 X delta must target melonPrimeDS baseAimX"); - expect_write(1, 0x020DEE4Eu, static_cast(-4), - "EU1.1 Y delta must target melonPrimeDS baseAimY"); - - g_morph_addr = 0x020DB138u; - g_morph_value = 0x02u; - expect(nds_title_patches_mph_in_ball(), - "EU1.1 baseIsAltForm=2 must report Morph Ball"); - g_morph_value = 0x00u; - expect(!nds_title_patches_mph_in_ball(), - "EU1.1 non-alt form must not report Morph Ball"); + // Known-clean SHA-1 is consistency/provenance only. Matching clean + // identities work, but a clean hash paired with a contradictory header is + // impossible and must fail closed instead of trusting either side. + expect(select("AMHE", 0, kUs10Sha1), + "known-clean US1.0 must match its header profile"); + expect(select("AMHP", 1, kEu11Sha1), + "known-clean EU1.1 must match its header profile"); + expect(!select("AMHP", 1, kUs10Sha1), + "known-clean US1.0 SHA with EU1.1 header must fail closed"); + expect(!select("AMHE", 0, kEu11Sha1), + "known-clean EU1.1 SHA with US1.0 header must fail closed"); + + // Unknown/ambiguous identities must never be guessed as US1.0. Revisions + // other than the explicitly supported 0/1 set are rejected; KR only has + // revision 0 in the seven-profile table. + expect(!select("ZZZZ", 0, kUnknownSha1), + "unknown game code must fail closed"); + expect(!select("AMHE", 2, kUnknownSha1), + "unknown USA revision must fail closed"); + expect(!select("AMHP", 2, kUnknownSha1), + "unknown Europe revision must fail closed"); + expect(!select("AMHJ", 2, kUnknownSha1), + "unknown Japan revision must fail closed"); + expect(!select("AMHK", 1, kUnknownSha1), + "unknown Korea revision must fail closed"); + + std::vector tiny(0x1Eu, 0u); + expect(!nds_title_patches_select_mph_runtime_profile( + tiny.data(), static_cast(tiny.size()), kUnknownSha1), + "truncated NDS header must fail closed"); + const auto valid = make_rom("AMHE", 0); + expect(!nds_title_patches_select_mph_runtime_profile( + valid.data(), static_cast(valid.size()), nullptr), + "missing actual-content identity must fail closed"); - // A final invalid identity clears the active profile and prevents stale - // EU1.1 addresses from remaining live. - expect(!nds_title_patches_select_mph_runtime_profile("bad"), - "invalid final identity must clear the runtime profile"); + // Failed selection clears the prior profile so stale addresses cannot + // remain active after a cartridge/identity change. g_writes.clear(); nds_title_patches_set_mph_mouse_aim(true); expect(!nds_title_patches_apply_mph_mouse_delta(9, 9), - "cleared profile must reject mouse aim"); - expect(g_writes.empty(), "cleared profile must not retain EU1.1 writes"); + "failed selection must reject mouse aim"); + expect(g_writes.empty(), + "failed selection must not retain prior profile writes"); + expect(!nds_title_patches_mph_in_ball(), + "failed selection must not retain prior morph address"); if (g_failures != 0) { std::fprintf(stderr, "%d runtime-profile assertion(s) failed\n", g_failures); return 1; } - std::puts("OK: exact-ROM MPH runtime profiles dispatch US1.0/EU1.1 safely"); + std::puts( + "OK: header-based MPH runtime profiles dispatch all seven revisions; " + "SHA-1 remains provenance"); return 0; } From 7c4865ed60f2c7dcf90f78ef6defd464724953df Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:38:20 +0900 Subject: [PATCH 082/164] Gate MPH mods by executable compatibility --- config/mph_rom_profiles.json | 29 +- tools/check_mph_multirom_profiles.py | 140 ++++++--- tools/patch_ndsrecomp_mph_runtime.py | 348 +++++++++++++++++------ tools/tests/mph_runtime_profile_test.cpp | 179 +++++++----- 4 files changed, 511 insertions(+), 185 deletions(-) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 016efd2..3aa214e 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -1,11 +1,13 @@ { - "schema": 3, + "schema": 4, "runtime_address_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h", "runtime_detection_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp", + "runtime_checksum_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/NDSCart/CartCommon.cpp", "runtime_profiles": { "US1_0": { "game_code": "AMHE", "revision": 0, + "base_checksum": "0x218DA42C", "runtime": { "morph_state": "0x020DA818", "aim_x": "0x020DE526", @@ -15,6 +17,7 @@ "US1_1": { "game_code": "AMHE", "revision": 1, + "base_checksum": "0x91B46577", "runtime": { "morph_state": "0x020DB098", "aim_x": "0x020DEDA6", @@ -24,6 +27,7 @@ "EU1_0": { "game_code": "AMHP", "revision": 0, + "base_checksum": "0xA4A8FE5A", "runtime": { "morph_state": "0x020DB0B8", "aim_x": "0x020DEDC6", @@ -33,6 +37,7 @@ "EU1_1": { "game_code": "AMHP", "revision": 1, + "base_checksum": "0x910018A5", "runtime": { "morph_state": "0x020DB138", "aim_x": "0x020DEE46", @@ -42,6 +47,7 @@ "JP1_0": { "game_code": "AMHJ", "revision": 0, + "base_checksum": "0xD75F539D", "runtime": { "morph_state": "0x020DC6D8", "aim_x": "0x020E03E6", @@ -51,6 +57,7 @@ "JP1_1": { "game_code": "AMHJ", "revision": 1, + "base_checksum": "0x42EBF348", "runtime": { "morph_state": "0x020DC698", "aim_x": "0x020E03A6", @@ -60,6 +67,7 @@ "KR1_0": { "game_code": "AMHK", "revision": 0, + "base_checksum": "0xE54682F3", "runtime": { "morph_state": "0x020D3EE4", "aim_x": "0x020D7C0E", @@ -67,6 +75,25 @@ } } }, + "runtime_checksums": [ + {"crc32": "0x91B46577", "profile": "US1_1", "name": "US1.1"}, + {"crc32": "0x01476E8F", "profile": "US1_1", "name": "US1.1 ENCRYPTED"}, + {"crc32": "0x218DA42C", "profile": "US1_0", "name": "US1.0"}, + {"crc32": "0xE048CD92", "profile": "US1_0", "name": "US1.0 ENCRYPTED"}, + {"crc32": "0x910018A5", "profile": "EU1_1", "name": "EU1.1"}, + {"crc32": "0x31703770", "profile": "EU1_1", "name": "EU1.1 ENCRYPTED"}, + {"crc32": "0x948B1E48", "profile": "EU1_1", "name": "EU1.1 BALANCED"}, + {"crc32": "0x2970A14F", "profile": "EU1_1", "name": "EU1.1 BALANCED V1.2.11"}, + {"crc32": "0x9E20F3A8", "profile": "EU1_1", "name": "EU1.1 RUSSIANED"}, + {"crc32": "0xA4A8FE5A", "profile": "EU1_0", "name": "EU1.0"}, + {"crc32": "0x979BB267", "profile": "EU1_0", "name": "EU1.0 ENCRYPTED"}, + {"crc32": "0xD75F539D", "profile": "JP1_0", "name": "JP1.0"}, + {"crc32": "0xE795A10C", "profile": "JP1_0", "name": "JP1.0 ENCRYPTED"}, + {"crc32": "0x42EBF348", "profile": "JP1_1", "name": "JP1.1"}, + {"crc32": "0x0A1203A5", "profile": "JP1_1", "name": "JP1.1 ENCRYPTED"}, + {"crc32": "0xE54682F3", "profile": "KR1_0", "name": "KR1.0"}, + {"crc32": "0xC26916F3", "profile": "KR1_0", "name": "KR1.0 ENCRYPTED"} + ], "profiles": { "US1_0": { "display_name": "Metroid Prime Hunters (USA rev 0)", diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index d0ceb55..a738393 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 """Static consistency checks for Metroid Prime Hunters ROM profiles. -Build/capture profiles describe exact clean-content identities and generated -artifacts. Runtime profiles separately describe the seven supported base ROM -layouts. This separation is intentional: whole-ROM SHA-1 is provenance, while -runtime Aim/Morph address selection follows gameCode + revision. +The registry intentionally separates three identities: -When --melonprime-table is provided, every runtime Aim/Morph address is -cross-checked against melonPrimeDS's MelonPrimeGameRomAddrTable.h. +* runtime base profile: seven region/revision RAM layouts, +* executable checksum: melonPrimeDS header+ARM9+ARM7 CRC32 compatibility, +* whole-ROM SHA-1: exact build/capture/generated-content provenance. + +Runtime checksum hits may authorize host Aim/Morph access. Header fallback alone +must never be promoted to that trust level by this checker or the runner patch. """ from __future__ import annotations @@ -21,6 +22,7 @@ SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") BANK_RE = re.compile(r"^[A-Za-z0-9_]+$") HEX_RE = re.compile(r"0x[0-9A-Fa-f]+u?") REQUIRED_RUNTIME_FIELDS = { @@ -29,13 +31,34 @@ "aim_y": "baseAimY", } EXPECTED_RUNTIME_PROFILES = { - "US1_0": ("AMHE", 0), - "US1_1": ("AMHE", 1), - "EU1_0": ("AMHP", 0), - "EU1_1": ("AMHP", 1), - "JP1_0": ("AMHJ", 0), - "JP1_1": ("AMHJ", 1), - "KR1_0": ("AMHK", 0), + "US1_0": ("AMHE", 0, 0x218DA42C), + "US1_1": ("AMHE", 1, 0x91B46577), + "EU1_0": ("AMHP", 0, 0xA4A8FE5A), + "EU1_1": ("AMHP", 1, 0x910018A5), + "JP1_0": ("AMHJ", 0, 0xD75F539D), + "JP1_1": ("AMHJ", 1, 0x42EBF348), + "KR1_0": ("AMHK", 0, 0xE54682F3), +} +# Exact CHECKSUM_TABLE from develop_hud/MelonPrimeGameRomDetect.cpp at the +# source revision audited for this PR. Names are retained so drift is visible. +EXPECTED_RUNTIME_CHECKSUMS = { + 0x91B46577: ("US1_1", "US1.1"), + 0x01476E8F: ("US1_1", "US1.1 ENCRYPTED"), + 0x218DA42C: ("US1_0", "US1.0"), + 0xE048CD92: ("US1_0", "US1.0 ENCRYPTED"), + 0x910018A5: ("EU1_1", "EU1.1"), + 0x31703770: ("EU1_1", "EU1.1 ENCRYPTED"), + 0x948B1E48: ("EU1_1", "EU1.1 BALANCED"), + 0x2970A14F: ("EU1_1", "EU1.1 BALANCED V1.2.11"), + 0x9E20F3A8: ("EU1_1", "EU1.1 RUSSIANED"), + 0xA4A8FE5A: ("EU1_0", "EU1.0"), + 0x979BB267: ("EU1_0", "EU1.0 ENCRYPTED"), + 0xD75F539D: ("JP1_0", "JP1.0"), + 0xE795A10C: ("JP1_0", "JP1.0 ENCRYPTED"), + 0x42EBF348: ("JP1_1", "JP1.1"), + 0x0A1203A5: ("JP1_1", "JP1.1 ENCRYPTED"), + 0xE54682F3: ("KR1_0", "KR1.0"), + 0xC26916F3: ("KR1_0", "KR1.0 ENCRYPTED"), } @@ -52,6 +75,12 @@ def parse_hex(value: object, where: str) -> int: die(f"{where} has invalid hex value {value!r}") +def parse_checksum(value: object, where: str) -> int: + if not isinstance(value, str) or not CHECKSUM_RE.fullmatch(value): + die(f"{where} must use uppercase 0xXXXXXXXX format") + return int(value, 16) + + def load_json(path: Path) -> object: with path.open("r", encoding="utf-8") as f: return json.load(f) @@ -121,18 +150,22 @@ def validate_runtime_profiles( ) seen_identity: set[tuple[str, int]] = set() + seen_base_checksum: set[int] = set() validated: dict[str, dict[str, object]] = {} - for key, expected_identity in EXPECTED_RUNTIME_PROFILES.items(): + for key, expected in EXPECTED_RUNTIME_PROFILES.items(): profile = runtime_profiles.get(key) if not isinstance(profile, dict): die(f"runtime profile {key} must be an object") game_code = profile.get("game_code") revision = profile.get("revision") - if (game_code, revision) != expected_identity: + base_checksum = parse_checksum( + profile.get("base_checksum"), f"{key}.base_checksum" + ) + if (game_code, revision, base_checksum) != expected: die( - f"{key} runtime identity is {(game_code, revision)!r}; " - f"expected {expected_identity!r}" + f"{key} runtime identity/checksum is " + f"{(game_code, revision, base_checksum)!r}; expected {expected!r}" ) if not isinstance(game_code, str) or len(game_code) != 4 or not game_code.isascii(): die(f"{key}.game_code must be exactly four ASCII characters") @@ -143,6 +176,9 @@ def validate_runtime_profiles( if identity in seen_identity: die(f"duplicate runtime cartridge identity: {game_code} rev {revision}") seen_identity.add(identity) + if base_checksum in seen_base_checksum: + die(f"duplicate canonical executable checksum: 0x{base_checksum:08X}") + seen_base_checksum.add(base_checksum) runtime = profile.get("runtime") if not isinstance(runtime, dict): @@ -159,18 +195,50 @@ def validate_runtime_profiles( die(f"{key} is not present in melonPrimeDS RomGroup") for profile_field, melon_field in REQUIRED_RUNTIME_FIELDS.items(): actual = parsed_runtime[profile_field] - expected = melon[key][melon_field] - if actual != expected: + expected_addr = melon[key][melon_field] + if actual != expected_addr: die( f"{key}.{profile_field}=0x{actual:08X}, but " - f"melonPrimeDS {melon_field}=0x{expected:08X}" + f"melonPrimeDS {melon_field}=0x{expected_addr:08X}" ) validated[key] = profile - return validated +def validate_runtime_checksums(registry: dict[str, object]) -> None: + entries = registry.get("runtime_checksums") + if not isinstance(entries, list): + die("runtime_checksums must be an array") + actual: dict[int, tuple[str, str]] = {} + for index, item in enumerate(entries): + if not isinstance(item, dict): + die(f"runtime_checksums[{index}] must be an object") + crc = parse_checksum(item.get("crc32"), f"runtime_checksums[{index}].crc32") + profile = item.get("profile") + name = item.get("name") + if profile not in EXPECTED_RUNTIME_PROFILES: + die(f"runtime_checksums[{index}] has unknown profile {profile!r}") + if not isinstance(name, str) or not name: + die(f"runtime_checksums[{index}] has no name") + if crc in actual: + die(f"duplicate runtime executable checksum 0x{crc:08X}") + actual[crc] = (str(profile), name) + if actual != EXPECTED_RUNTIME_CHECKSUMS: + missing = sorted(set(EXPECTED_RUNTIME_CHECKSUMS) - set(actual)) + extra = sorted(set(actual) - set(EXPECTED_RUNTIME_CHECKSUMS)) + changed = sorted( + crc for crc in set(actual) & set(EXPECTED_RUNTIME_CHECKSUMS) + if actual[crc] != EXPECTED_RUNTIME_CHECKSUMS[crc] + ) + die( + "runtime checksum registry drifted from audited develop_hud detector: " + f"missing={[f'0x{x:08X}' for x in missing]}, " + f"extra={[f'0x{x:08X}' for x in extra]}, " + f"changed={[f'0x{x:08X}' for x in changed]}" + ) + + def validate_registry(repo: Path, table: Path | None) -> None: registry_path = repo / "config" / "mph_rom_profiles.json" registry_obj = load_json(registry_path) @@ -178,8 +246,8 @@ def validate_registry(repo: Path, table: Path | None) -> None: die("ROM profile registry must be a JSON object") registry: dict[str, object] = registry_obj - if registry.get("schema") != 3: - die("ROM profile registry schema must be 3") + if registry.get("schema") != 4: + die("ROM profile registry schema must be 4") expected_address_source = ( "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" @@ -189,13 +257,20 @@ def validate_registry(repo: Path, table: Path | None) -> None: "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" "src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp" ) + expected_checksum_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" + "src/NDSCart/CartCommon.cpp" + ) if registry.get("runtime_address_source") != expected_address_source: die("runtime_address_source is not the approved develop_hud address table") if registry.get("runtime_detection_source") != expected_detection_source: die("runtime_detection_source is not the approved develop_hud detector") + if registry.get("runtime_checksum_source") != expected_checksum_source: + die("runtime_checksum_source is not melonPrimeDS CartCommon::Checksum") melon = parse_melonprime_table(table) if table else None runtime_profiles = validate_runtime_profiles(registry, melon) + validate_runtime_checksums(registry) profiles = registry.get("profiles") if not isinstance(profiles, dict) or not profiles: @@ -203,7 +278,6 @@ def validate_registry(repo: Path, table: Path | None) -> None: seen_sha1: set[str] = set() seen_fmv_banks: set[str] = set() - for key, profile in profiles.items(): if not isinstance(profile, dict): die(f"profile {key} must be an object") @@ -220,10 +294,7 @@ def validate_registry(repo: Path, table: Path | None) -> None: seen_sha1.add(sha1) runtime_profile = runtime_profiles[key] - if ( - game_code != runtime_profile.get("game_code") - or revision != runtime_profile.get("revision") - ): + if game_code != runtime_profile.get("game_code") or revision != runtime_profile.get("revision"): die(f"{key}: clean build identity disagrees with runtime base profile") launcher_default_rom = profile.get("launcher_default_rom") @@ -256,10 +327,7 @@ def validate_registry(repo: Path, table: Path | None) -> None: if fmv_runtime: runtime_config_path = repo / "config" / f"{fmv_runtime_bank}.toml" if not runtime_config_path.is_file(): - die( - f"{key} enables FMV runtime but config is missing: " - f"{runtime_config_path}" - ) + die(f"{key} enables FMV runtime but config is missing: {runtime_config_path}") with runtime_config_path.open("rb") as f: runtime_config = tomllib.load(f) runtime_program = runtime_config.get("program") @@ -328,14 +396,13 @@ def validate_registry(repo: Path, table: Path | None) -> None: die("EU1_1 must remain AMHP revision 1") if eu.get("sha1") != "bdcd1dea293e24c98d4c481430e90d21198985a5": die("EU1_1 SHA-1 changed unexpectedly") - eu_runtime = runtime_profiles["EU1_1"].get("runtime", {}) expected_eu_runtime = { "morph_state": "0x020DB138", "aim_x": "0x020DEE46", "aim_y": "0x020DEE4E", } - if eu_runtime != expected_eu_runtime: - die(f"EU1_1 runtime profile changed unexpectedly: {eu_runtime!r}") + if runtime_profiles["EU1_1"].get("runtime") != expected_eu_runtime: + die("EU1_1 runtime profile changed unexpectedly") if eu.get("fmv_runtime") is not False: die("EU1_1 must not reuse the US1.0 FMV runtime capture") if eu.get("fmv_runtime_bank") != "mph_amhp1_arm9_fmv_runtime": @@ -346,7 +413,8 @@ def validate_registry(repo: Path, table: Path | None) -> None: die("EU1_1 launcher default ROM filename changed unexpectedly") print( - f"OK: validated {len(runtime_profiles)} runtime base profiles and " + f"OK: validated {len(runtime_profiles)} runtime base profiles, " + f"{len(EXPECTED_RUNTIME_CHECKSUMS)} executable checksums, and " f"{len(profiles)} clean build/capture profiles" ) if melon is not None: diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py index 7f1b1b3..6597a56 100755 --- a/tools/patch_ndsrecomp_mph_runtime.py +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -1,27 +1,21 @@ #!/usr/bin/env python3 """Apply the MPH multi-ROM runtime-profile shim to the pinned ndsrecomp runner. -The upstream runner currently hard-codes Metroid Prime Hunters USA rev-0 RAM -addresses for Prime Controls and direct mouse aim. Runtime address selection -must follow the base game/revision, not the whole-ROM hash, so modified ROMs -that preserve a supported MPH cartridge identity can use the correct layout. - -The detector mirrors melonPrimeDS's fallback identity: -NDS gameCode @0x0C + ROM revision @0x1E. Unlike melonPrimeDS, revisions outside -the seven explicitly supported retail profiles fail closed instead of mapping -all non-zero revisions to 1.1. - -Whole-ROM SHA-1 remains a clean-content/provenance identity. If a SHA-1 is one -of the configured known-clean ROMs, its profile must agree with the header; -an impossible clean-hash/header mismatch is rejected. An unknown SHA-1 does -not by itself reject a recognized base profile. - -Runtime address values are generated from config/mph_rom_profiles.json and -cross-checked in CI against melonPrimeDS's MelonPrimeGameRomAddrTable.h. - -The source patch is intentionally small, idempotent, and fail-closed. If the -pinned ndsrecomp source changes enough that the expected preimages are absent, -this script stops instead of guessing a patch against unknown code. +Runtime address selection follows melonPrimeDS's two-stage detector: + +1. authoritative executable checksum (CRC32 of header[0:0x40], ARM9, ARM7), +2. exact NDS gameCode @0x0C + supported revision @0x1E as a fallback. + +The fallback identifies a base profile but is *not* sufficient evidence for +host-side Aim/Morph RAM accesses. Those writes are enabled only for a checksum +explicitly known by the melonPrimeDS detector. This keeps unknown mods +fail-closed instead of guessing that a matching header implies compatible RAM. + +Whole-ROM SHA-1 has a separate role. It remains the actual-content identity +used by generated banks/captures. A clean build may accept a different whole- +ROM SHA only when the actual ROM has the canonical executable checksum of that +same clean base profile (for example, a data-only mod outside header/ARM9/ARM7). +Code-modified variants still require their own exact build/capture identity. """ from __future__ import annotations @@ -33,8 +27,12 @@ SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") MAIN_RAM_MIN = 0x02000000 MAIN_RAM_MAX = 0x023FFFFF +EXPECTED_RUNTIME_KEYS = { + "US1_0", "US1_1", "EU1_0", "EU1_1", "JP1_0", "JP1_1", "KR1_0" +} def parse_address(value: object, *, profile: str, field: str) -> int: @@ -51,18 +49,36 @@ def parse_address(value: object, *, profile: str, field: str) -> int: return address -def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: +def parse_checksum(value: object, *, where: str) -> int: + if not isinstance(value, str) or not CHECKSUM_RE.fullmatch(value): + raise SystemExit(f"{where}: expected uppercase 0xXXXXXXXX checksum") + return int(value, 16) + + +def load_runtime_registry( + registry_path: Path, +) -> tuple[list[dict[str, object]], list[dict[str, object]]]: registry = json.loads(registry_path.read_text(encoding="utf-8")) runtime_profiles = registry.get("runtime_profiles") if not isinstance(runtime_profiles, dict) or not runtime_profiles: raise SystemExit("ROM profile registry has no runtime_profiles") + actual_keys = set(runtime_profiles) + if actual_keys != EXPECTED_RUNTIME_KEYS: + missing = ", ".join(sorted(EXPECTED_RUNTIME_KEYS - actual_keys)) or "none" + extra = ", ".join(sorted(actual_keys - EXPECTED_RUNTIME_KEYS)) or "none" + raise SystemExit( + "runtime profile set must be exactly the seven supported retail " + f"profiles (missing: {missing}; extra: {extra})" + ) + build_profiles = registry.get("profiles") if not isinstance(build_profiles, dict): raise SystemExit("ROM profile registry has no build profiles") result: list[dict[str, object]] = [] seen_identity: set[tuple[str, int]] = set() + seen_base_checksum: set[int] = set() seen_clean_sha1: set[str] = set() for key, profile in runtime_profiles.items(): @@ -89,6 +105,13 @@ def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: ) seen_identity.add(identity) + base_checksum = parse_checksum( + profile.get("base_checksum"), where=f"{key}.base_checksum" + ) + if base_checksum in seen_base_checksum: + raise SystemExit(f"duplicate canonical executable checksum for {key}") + seen_base_checksum.add(base_checksum) + known_clean_sha1 = "" clean = build_profiles.get(key) if clean is not None: @@ -111,6 +134,7 @@ def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: "key": key, "game_code": game_code, "revision": revision, + "base_checksum": base_checksum, "known_clean_sha1": known_clean_sha1, "morph_state": parse_address( runtime.get("morph_state"), profile=key, field="morph_state" @@ -124,37 +148,68 @@ def load_runtime_profiles(registry_path: Path) -> list[dict[str, object]]: } ) - expected = { - "US1_0", "US1_1", "EU1_0", "EU1_1", "JP1_0", "JP1_1", "KR1_0" - } - actual = {str(profile["key"]) for profile in result} - if actual != expected: - missing = ", ".join(sorted(expected - actual)) or "none" - extra = ", ".join(sorted(actual - expected)) or "none" + checksums_obj = registry.get("runtime_checksums") + if not isinstance(checksums_obj, list) or not checksums_obj: + raise SystemExit("ROM profile registry has no runtime_checksums") + checksums: list[dict[str, object]] = [] + seen_checksums: set[int] = set() + canonical_seen: set[str] = set() + for index, item in enumerate(checksums_obj): + if not isinstance(item, dict): + raise SystemExit(f"runtime_checksums[{index}] must be an object") + checksum = parse_checksum( + item.get("crc32"), where=f"runtime_checksums[{index}].crc32" + ) + profile_key = item.get("profile") + name = item.get("name") + if profile_key not in EXPECTED_RUNTIME_KEYS: + raise SystemExit( + f"runtime_checksums[{index}].profile is unknown: {profile_key!r}" + ) + if not isinstance(name, str) or not name: + raise SystemExit(f"runtime_checksums[{index}].name must be non-empty") + if checksum in seen_checksums: + raise SystemExit(f"duplicate runtime checksum 0x{checksum:08X}") + seen_checksums.add(checksum) + profile = next(p for p in result if p["key"] == profile_key) + if checksum == profile["base_checksum"]: + canonical_seen.add(str(profile_key)) + checksums.append( + {"crc32": checksum, "profile": profile_key, "name": name} + ) + + if canonical_seen != EXPECTED_RUNTIME_KEYS: + missing = ", ".join(sorted(EXPECTED_RUNTIME_KEYS - canonical_seen)) raise SystemExit( - f"runtime profile set must be exactly the seven supported retail " - f"profiles (missing: {missing}; extra: {extra})" + f"runtime_checksums is missing canonical entries for: {missing}" ) - return result + return result, checksums -def generated_header(profiles: list[dict[str, object]]) -> str: - rows = [] +def generated_header( + profiles: list[dict[str, object]], checksums: list[dict[str, object]] +) -> str: + profile_rows: list[str] = [] for profile in profiles: - rows.append( - ' {"%s", "%s", %du, "%s", 0x%08Xu, 0x%08Xu, 0x%08Xu},' - " // %s" + profile_rows.append( + ' {"%s", "%s", %du, "%s", 0x%08Xu, 0x%08Xu, 0x%08Xu, 0x%08Xu}, // %s' % ( profile["key"], profile["game_code"], profile["revision"], profile["known_clean_sha1"], + profile["base_checksum"], profile["morph_state"], profile["aim_x"], profile["aim_y"], profile["key"], ) ) + checksum_rows = [ + ' {0x%08Xu, "%s", "%s"},' + % (item["crc32"], item["profile"], item["name"]) + for item in checksums + ] return """#pragma once #include @@ -163,23 +218,39 @@ def generated_header(profiles: list[dict[str, object]]) -> str: // Generated by MetroidPrimeHuntersRecomp/tools/patch_ndsrecomp_mph_runtime.py. // Do not edit in the ndsrecomp checkout; edit config/mph_rom_profiles.json. // -// game_code + revision selects the base runtime layout. known_clean_sha1 is -// provenance/consistency metadata only; an unknown SHA-1 is a supported variant -// when its exact header identity matches one of these seven profiles. +// The executable checksum mirrors melonPrimeDS CartCommon::Checksum(): CRC32 of +// header[0:0x40], then ARM9, then ARM7. A checksum hit is authoritative for the +// runtime layout. Header gameCode+revision is only a fail-closed fallback hint. struct NdsMphRuntimeProfile { const char* key; const char* game_code; uint8_t revision; const char* known_clean_sha1; + uint32_t base_checksum; uint32_t morph_state; uint32_t aim_x; uint32_t aim_y; }; +struct NdsMphRuntimeChecksum { + uint32_t checksum; + const char* profile_key; + const char* name; +}; + inline constexpr std::array kNdsMphRuntimeProfiles{{ %s }}; -""" % (len(rows), "\n".join(rows)) + +inline constexpr std::array kNdsMphRuntimeChecksums{{ +%s +}}; +""" % ( + len(profile_rows), + "\n".join(profile_rows), + len(checksum_rows), + "\n".join(checksum_rows), + ) def patch_once(path: Path, old: str, new: str, marker: str) -> None: @@ -204,21 +275,24 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: if not path.is_file(): raise SystemExit(f"Pinned ndsrecomp runner file not found: {path}") - profiles = load_runtime_profiles(registry_path) + profiles, checksums = load_runtime_registry(registry_path) generated = runner_src / "mph_runtime_profiles.generated.h" - generated.write_text(generated_header(profiles), encoding="utf-8") + generated.write_text(generated_header(profiles, checksums), encoding="utf-8") patch_once( title_h, "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: base-profile detection from NDS header.\n" + "// MPH_MULTIROM_RUNTIME_PROFILE: melonPrimeDS-compatible base detector.\n" "bool nds_title_patches_select_mph_runtime_profile(\n" - " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1);\n" + " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1,\n" + " const char* expected_rom_sha1);\n" + "bool nds_title_patches_mph_host_writes_compatible();\n" + "bool nds_title_patches_mph_allows_rom_sha1_mismatch();\n" "bool nds_title_patches_mph_in_ball();\n" "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", - "base-profile detection from NDS header", + "melonPrimeDS-compatible base detector", ) patch_once( @@ -235,9 +309,57 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: "// path but removes the finite physical touchscreen edge.\n" "constexpr uint32_t kMphUs10AimX = 0x020DE526u;\n" "constexpr uint32_t kMphUs10AimY = 0x020DE52Eu;\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: selected by exact gameCode + revision.\n" - "const NdsMphRuntimeProfile* g_mph_runtime_profile = nullptr;\n", - "selected by exact gameCode + revision", + "// MPH_MULTIROM_RUNTIME_PROFILE: runtime identity and safety state.\n" + "const NdsMphRuntimeProfile* g_mph_runtime_profile = nullptr;\n" + "bool g_mph_host_writes_compatible = false;\n" + "bool g_mph_allow_rom_sha1_mismatch = false;\n\n" + "uint32_t mph_read_le32(const uint8_t* p) {\n" + " return static_cast(p[0]) |\n" + " (static_cast(p[1]) << 8) |\n" + " (static_cast(p[2]) << 16) |\n" + " (static_cast(p[3]) << 24);\n" + "}\n\n" + "uint32_t mph_crc32(const uint8_t* data, uint32_t len, uint32_t start) {\n" + " uint32_t crc = start ^ 0xFFFFFFFFu;\n" + " for (uint32_t i = 0; i < len; ++i) {\n" + " crc ^= data[i];\n" + " for (int bit = 0; bit < 8; ++bit)\n" + " crc = (crc >> 1) ^\n" + " (0xEDB88320u & (0u - (crc & 1u)));\n" + " }\n" + " return crc ^ 0xFFFFFFFFu;\n" + "}\n\n" + "bool mph_compute_executable_checksum(\n" + " const uint8_t* rom, uint64_t rom_size, uint32_t* out) {\n" + " if (!rom || !out || rom_size < 0x40u) return false;\n" + " const uint32_t arm9_offset = mph_read_le32(rom + 0x20u);\n" + " const uint32_t arm9_size = mph_read_le32(rom + 0x2Cu);\n" + " const uint32_t arm7_offset = mph_read_le32(rom + 0x30u);\n" + " const uint32_t arm7_size = mph_read_le32(rom + 0x3Cu);\n" + " if (static_cast(arm9_offset) + arm9_size > rom_size ||\n" + " static_cast(arm7_offset) + arm7_size > rom_size)\n" + " return false;\n" + " uint32_t crc = mph_crc32(rom, 0x40u, 0u);\n" + " crc = mph_crc32(rom + arm9_offset, arm9_size, crc);\n" + " crc = mph_crc32(rom + arm7_offset, arm7_size, crc);\n" + " *out = crc;\n" + " return true;\n" + "}\n\n" + "const NdsMphRuntimeProfile* mph_find_profile_by_key(const char* key) {\n" + " for (const auto& profile : kNdsMphRuntimeProfiles)\n" + " if (std::strcmp(profile.key, key) == 0) return &profile;\n" + " return nullptr;\n" + "}\n\n" + "const NdsMphRuntimeProfile* mph_find_clean_sha1(const char* sha1) {\n" + " if (!sha1 || sha1[0] == '\\0') return nullptr;\n" + " for (const auto& profile : kNdsMphRuntimeProfiles) {\n" + " if (profile.known_clean_sha1[0] != '\\0' &&\n" + " std::strcmp(profile.known_clean_sha1, sha1) == 0)\n" + " return &profile;\n" + " }\n" + " return nullptr;\n" + "}\n", + "runtime identity and safety state", ) patch_once( title_cpp, @@ -253,44 +375,72 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: " return true;\n" "}\n", "bool nds_title_patches_select_mph_runtime_profile(\n" - " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1) {\n" + " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1,\n" + " const char* expected_rom_sha1) {\n" " g_mph_mouse_aim = false;\n" " g_mph_runtime_profile = nullptr;\n" - " // NDS header: game code @0x0C..0x0F, ROM version @0x1E.\n" - " if (!rom_data || rom_size <= 0x1Eu || !rom_sha1) return false;\n\n" - " const NdsMphRuntimeProfile* header_profile = nullptr;\n" - " for (const NdsMphRuntimeProfile& profile : kNdsMphRuntimeProfiles) {\n" - " if (std::memcmp(profile.game_code, rom_data + 0x0Cu, 4) == 0 &&\n" - " profile.revision == rom_data[0x1Eu]) {\n" - " if (header_profile) return false; // ambiguous registry: fail closed\n" - " header_profile = &profile;\n" - " }\n" - " }\n" - " if (!header_profile) return false;\n\n" - " // Whole-ROM SHA-1 is clean identity/provenance, not the selector.\n" - " // If this content is a known clean dump, its header must agree with\n" - " // the corresponding base profile. Unknown hashes are mod variants.\n" - " const NdsMphRuntimeProfile* clean_profile = nullptr;\n" - " for (const NdsMphRuntimeProfile& profile : kNdsMphRuntimeProfiles) {\n" - " if (profile.known_clean_sha1[0] != '\\0' &&\n" - " std::strcmp(profile.known_clean_sha1, rom_sha1) == 0) {\n" - " clean_profile = &profile;\n" + " g_mph_host_writes_compatible = false;\n" + " g_mph_allow_rom_sha1_mismatch = false;\n" + " if (!rom_data || !rom_sha1 || !expected_rom_sha1 || rom_size <= 0x1Eu)\n" + " return false;\n\n" + " uint32_t checksum = 0;\n" + " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum))\n" + " return false;\n\n" + " const NdsMphRuntimeChecksum* checksum_hit = nullptr;\n" + " const NdsMphRuntimeProfile* profile = nullptr;\n" + " for (const auto& entry : kNdsMphRuntimeChecksums) {\n" + " if (entry.checksum == checksum) {\n" + " checksum_hit = &entry;\n" + " profile = mph_find_profile_by_key(entry.profile_key);\n" " break;\n" " }\n" + " }\n\n" + " if (!profile) {\n" + " // melonPrimeDS fallback, tightened to exact supported revisions.\n" + " for (const auto& candidate : kNdsMphRuntimeProfiles) {\n" + " if (std::memcmp(candidate.game_code, rom_data + 0x0Cu, 4) == 0 &&\n" + " candidate.revision == rom_data[0x1Eu]) {\n" + " if (profile) return false; // ambiguous registry: fail closed\n" + " profile = &candidate;\n" + " }\n" + " }\n" " }\n" - " if (clean_profile && clean_profile != header_profile) return false;\n\n" - " g_mph_runtime_profile = header_profile;\n" + " if (!profile) return false;\n\n" + " // A known clean whole-ROM hash can only describe its own base profile.\n" + " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" + " if (actual_clean && actual_clean != profile) return false;\n\n" + " g_mph_runtime_profile = profile;\n" + " // Unknown checksum + matching header is only a base-profile hint.\n" + " // Do not perform host RAM reads/writes until the executable checksum\n" + " // is explicitly represented by melonPrimeDS's authoritative table.\n" + " g_mph_host_writes_compatible = checksum_hit != nullptr;\n\n" + " // Whole-ROM mismatch may be relaxed only for a clean build whose\n" + " // executable identity is byte-for-byte equivalent to the canonical\n" + " // header+ARM9+ARM7 checksum. Code-modified known variants therefore\n" + " // still require an exact mod-specific build/capture SHA.\n" + " const NdsMphRuntimeProfile* expected_clean =\n" + " mph_find_clean_sha1(expected_rom_sha1);\n" + " g_mph_allow_rom_sha1_mismatch =\n" + " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" + " expected_clean == profile && checksum == profile->base_checksum;\n" " return true;\n" "}\n\n" + "bool nds_title_patches_mph_host_writes_compatible() {\n" + " return g_mph_runtime_profile && g_mph_host_writes_compatible;\n" + "}\n\n" + "bool nds_title_patches_mph_allows_rom_sha1_mismatch() {\n" + " return g_mph_runtime_profile && g_mph_allow_rom_sha1_mismatch;\n" + "}\n\n" "bool nds_title_patches_mph_in_ball() {\n" - " return g_mph_runtime_profile &&\n" + " return nds_title_patches_mph_host_writes_compatible() &&\n" " bus_read_u8_slow(g_mph_runtime_profile->morph_state) == 0x02u;\n" "}\n\n" "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" - " g_mph_mouse_aim = enabled && g_mph_runtime_profile;\n" + " g_mph_mouse_aim =\n" + " enabled && nds_title_patches_mph_host_writes_compatible();\n" "}\n\n" "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" - " if (!g_mph_mouse_aim || !g_mph_runtime_profile ||\n" + " if (!g_mph_mouse_aim || !nds_title_patches_mph_host_writes_compatible() ||\n" " (dx == 0 && dy == 0)) return false;\n" " if (dx != 0)\n" " bus_write_u32_slow(g_mph_runtime_profile->aim_x,\n" @@ -317,25 +467,59 @@ def patch_runner(framework_root: Path, registry_path: Path) -> None: "nds_title_patches_mph_in_ball()", ) + patch_once( + main_cpp, + " rom_sha1 = gba::sha1(rom.data(), rom.size()).hex();\n" + " std::fprintf(stderr, \"[load] cartridge: %zu bytes, SHA-1 %s\\n\",\n" + " rom.size(), rom_sha1.c_str());\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1) {\n" + " std::fprintf(stderr,\n" + " \"refusing to start: game config expects ROM SHA-1 \"\n" + " \"%s, got %s\\n\",\n" + " frontend_options.expected_rom_sha1.c_str(),\n" + " rom_sha1.c_str());\n" + " return 1;\n" + " }\n", + " rom_sha1 = gba::sha1(rom.data(), rom.size()).hex();\n" + " std::fprintf(stderr, \"[load] cartridge: %zu bytes, SHA-1 %s\\n\",\n" + " rom.size(), rom_sha1.c_str());\n" + " // MPH_MULTIROM_CONTENT_GATE: choose a base layout before the\n" + " // generic exact-SHA gate. The whole-ROM hash still identifies\n" + " // generated content; only canonical executable-equivalent data\n" + " // variants may reuse a clean build.\n" + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()), rom_sha1.c_str(),\n" + " frontend_options.expected_rom_sha1.c_str());\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n" + " std::fprintf(stderr,\n" + " \"refusing to start: game config expects ROM SHA-1 \"\n" + " \"%s, got %s\\n\",\n" + " frontend_options.expected_rom_sha1.c_str(),\n" + " rom_sha1.c_str());\n" + " return 1;\n" + " }\n", + "MPH_MULTIROM_CONTENT_GATE", + ) patch_once( main_cpp, " mph_mouse_aim_policy =\n" " rom_sha1 == \"90164d1ac127ee5f9815ea4ae7de798c7b5fc629\" &&\n" " frontend_options.relative_mouse_touch;\n", - " // MPH_MULTIROM_RUNTIME_PROFILE: select the base address layout from\n" - " // gameCode + revision. SHA-1 only checks known-clean consistency;\n" - " // modified ROMs with a supported exact header identity remain usable.\n" - " const bool mph_runtime_profile =\n" - " nds_title_patches_select_mph_runtime_profile(\n" - " rom.data(), static_cast(rom.size()), rom_sha1.c_str());\n" + " // MPH_MULTIROM_HOST_WRITE_GATE: header fallback alone never enables\n" + " // direct host RAM writes; an authoritative executable checksum is required.\n" " mph_mouse_aim_policy =\n" - " mph_runtime_profile && frontend_options.relative_mouse_touch;\n", - "SHA-1 only checks known-clean consistency", + " nds_title_patches_mph_host_writes_compatible() &&\n" + " frontend_options.relative_mouse_touch;\n", + "MPH_MULTIROM_HOST_WRITE_GATE", ) print( f"Patched ndsrecomp MPH runtime base profiles: " + ", ".join(str(profile["key"]) for profile in profiles) + + f" ({len(checksums)} authoritative executable checksums)" ) diff --git a/tools/tests/mph_runtime_profile_test.cpp b/tools/tests/mph_runtime_profile_test.cpp index 6ade13e..21b99f6 100644 --- a/tools/tests/mph_runtime_profile_test.cpp +++ b/tools/tests/mph_runtime_profile_test.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -17,19 +18,24 @@ struct RuntimeCase { const char* name; const char* game_code; uint8_t revision; + std::array crc_suffix; uint32_t morph; uint32_t aim_x; uint32_t aim_y; }; +// Each synthetic ROM uses a 0-byte ARM9 and a 4-byte ARM7. The four suffix +// bytes were solved so melonPrimeDS's CRC32(header[0:0x40], ARM9, ARM7) +// equals the canonical checksum for the corresponding retail profile. This +// tests the real detector without shipping or reading copyrighted ROM data. constexpr RuntimeCase kCases[] = { - {"US1_0", "AMHE", 0, 0x020DA818u, 0x020DE526u, 0x020DE52Eu}, - {"US1_1", "AMHE", 1, 0x020DB098u, 0x020DEDA6u, 0x020DEDAEu}, - {"EU1_0", "AMHP", 0, 0x020DB0B8u, 0x020DEDC6u, 0x020DEDCEu}, - {"EU1_1", "AMHP", 1, 0x020DB138u, 0x020DEE46u, 0x020DEE4Eu}, - {"JP1_0", "AMHJ", 0, 0x020DC6D8u, 0x020E03E6u, 0x020E03EEu}, - {"JP1_1", "AMHJ", 1, 0x020DC698u, 0x020E03A6u, 0x020E03AEu}, - {"KR1_0", "AMHK", 0, 0x020D3EE4u, 0x020D7C0Eu, 0x020D7C16u}, + {"US1_0", "AMHE", 0, {0x45, 0x3D, 0xE6, 0x16}, 0x020DA818u, 0x020DE526u, 0x020DE52Eu}, + {"US1_1", "AMHE", 1, {0xE0, 0xF3, 0x6E, 0x2A}, 0x020DB098u, 0x020DEDA6u, 0x020DEDAEu}, + {"EU1_0", "AMHP", 0, {0x66, 0x59, 0x8A, 0xC0}, 0x020DB0B8u, 0x020DEDC6u, 0x020DEDCEu}, + {"EU1_1", "AMHP", 1, {0x7B, 0xD4, 0x8E, 0xCB}, 0x020DB138u, 0x020DEE46u, 0x020DEE4Eu}, + {"JP1_0", "AMHJ", 0, {0xF0, 0x91, 0x6F, 0xD1}, 0x020DC6D8u, 0x020E03E6u, 0x020E03EEu}, + {"JP1_1", "AMHJ", 1, {0x89, 0x6F, 0x91, 0xF1}, 0x020DC698u, 0x020E03A6u, 0x020E03AEu}, + {"KR1_0", "AMHK", 0, {0x4C, 0xE9, 0x78, 0xBD}, 0x020D3EE4u, 0x020D7C0Eu, 0x020D7C16u}, }; constexpr const char* kUs10Sha1 = @@ -58,24 +64,37 @@ void expect_write(std::size_t index, uint32_t addr, uint32_t value, expect(ok, message); } -std::vector make_rom(const char* game_code, uint8_t revision) { - std::vector rom(0x200u, 0u); +void write_le32(std::vector& rom, std::size_t offset, uint32_t value) { + rom[offset + 0] = static_cast(value >> 0); + rom[offset + 1] = static_cast(value >> 8); + rom[offset + 2] = static_cast(value >> 16); + rom[offset + 3] = static_cast(value >> 24); +} + +std::vector make_rom( + const char* game_code, uint8_t revision, std::array suffix) { + std::vector rom(0x44u, 0u); std::memcpy(rom.data() + 0x0Cu, game_code, 4u); rom[0x1Eu] = revision; + write_le32(rom, 0x20u, 0x40u); // ARM9 ROM offset + write_le32(rom, 0x2Cu, 0u); // ARM9 size + write_le32(rom, 0x30u, 0x40u); // ARM7 ROM offset + write_le32(rom, 0x3Cu, 4u); // ARM7 size + std::memcpy(rom.data() + 0x40u, suffix.data(), suffix.size()); return rom; } -bool select(const char* game_code, uint8_t revision, const char* sha1) { - const auto rom = make_rom(game_code, revision); +bool select_rom( + const std::vector& rom, + const char* actual_sha1 = kUnknownSha1, + const char* expected_sha1 = "") { return nds_title_patches_select_mph_runtime_profile( - rom.data(), static_cast(rom.size()), sha1); + rom.data(), static_cast(rom.size()), + actual_sha1, expected_sha1); } } // namespace -// title_patches.cpp only needs these bus surfaces. Keeping the test at the -// ABI boundary lets it link the real patched translation unit without a ROM, -// BIOS dump, or generated recomp bank. bool bus_get_region(const char*, BusRegion*) { return false; } @@ -89,24 +108,20 @@ extern "C" void bus_write_u32_slow(uint32_t addr, uint32_t value) { } int main() { - // All seven retail base profiles must dispatch from gameCode@0x0C plus the - // exact supported revision@0x1E even when the whole-ROM SHA-1 is unknown. - // This is the mod-ROM path: content identity is provenance, not selector. for (const RuntimeCase& c : kCases) { - expect(select(c.game_code, c.revision, kUnknownSha1), c.name); + const auto rom = make_rom(c.game_code, c.revision, c.crc_suffix); + expect(select_rom(rom), c.name); + expect(nds_title_patches_mph_host_writes_compatible(), + "canonical executable checksum must authorize host writes"); - // Profile selection clears the previous direct-aim enable state. g_writes.clear(); expect(!nds_title_patches_apply_mph_mouse_delta(7, -5), "profile switch must clear direct-aim enable state"); - expect(g_writes.empty(), - "disabled aim after profile switch must not write"); - nds_title_patches_set_mph_mouse_aim(true); expect(nds_title_patches_apply_mph_mouse_delta(7, -5), - "recognized base profile must accept direct mouse aim"); + "authoritative checksum must accept direct mouse aim"); expect(g_writes.size() == 2, - "recognized base profile must perform two aim writes"); + "recognized profile must perform two aim writes"); expect_write(0, c.aim_x, 7u, "X delta must target profile-specific baseAimX"); expect_write(1, c.aim_y, static_cast(-5), @@ -121,59 +136,91 @@ int main() { "non-alt form must not report Morph Ball"); } - // Known-clean SHA-1 is consistency/provenance only. Matching clean - // identities work, but a clean hash paired with a contradictory header is - // impossible and must fail closed instead of trusting either side. - expect(select("AMHE", 0, kUs10Sha1), - "known-clean US1.0 must match its header profile"); - expect(select("AMHP", 1, kEu11Sha1), - "known-clean EU1.1 must match its header profile"); - expect(!select("AMHP", 1, kUs10Sha1), - "known-clean US1.0 SHA with EU1.1 header must fail closed"); - expect(!select("AMHE", 0, kEu11Sha1), - "known-clean EU1.1 SHA with US1.0 header must fail closed"); - - // Unknown/ambiguous identities must never be guessed as US1.0. Revisions - // other than the explicitly supported 0/1 set are rejected; KR only has - // revision 0 in the seven-profile table. - expect(!select("ZZZZ", 0, kUnknownSha1), - "unknown game code must fail closed"); - expect(!select("AMHE", 2, kUnknownSha1), - "unknown USA revision must fail closed"); - expect(!select("AMHP", 2, kUnknownSha1), - "unknown Europe revision must fail closed"); - expect(!select("AMHJ", 2, kUnknownSha1), - "unknown Japan revision must fail closed"); - expect(!select("AMHK", 1, kUnknownSha1), - "unknown Korea revision must fail closed"); + // Whole-ROM SHA mismatch is allowed only when a clean build sees the same + // canonical executable checksum. This supports data-only variants without + // reusing a clean build for code-modified content. + const auto us10 = make_rom("AMHE", 0, {0x45, 0x3D, 0xE6, 0x16}); + expect(select_rom(us10, kUnknownSha1, kUs10Sha1), + "US1.0 canonical executable variant must select"); + expect(nds_title_patches_mph_allows_rom_sha1_mismatch(), + "canonical US1.0 executable may reuse US1.0 clean build"); + + const auto eu11 = make_rom("AMHP", 1, {0x7B, 0xD4, 0x8E, 0xCB}); + expect(select_rom(eu11, kUnknownSha1, kUs10Sha1), + "EU1.1 canonical executable must still identify EU1.1 base"); + expect(!nds_title_patches_mph_allows_rom_sha1_mismatch(), + "EU1.1 executable must never bypass a US1.0 clean SHA gate"); + + // melonPrimeDS explicitly recognizes this EU1.1 Russian variant checksum, + // so its RAM layout is authoritative and host writes are safe. However its + // executable checksum is not the canonical clean EU1.1 checksum, therefore + // it still needs its own exact mod-specific recomp build/capture identity. + const auto eu11_russian = + make_rom("AMHP", 1, {0xCE, 0x58, 0x0D, 0xA4}); // 0x9E20F3A8 + expect(select_rom(eu11_russian, kUnknownSha1, kEu11Sha1), + "known EU1.1 Russian executable must identify EU1.1 base"); + expect(nds_title_patches_mph_host_writes_compatible(), + "known EU1.1 Russian checksum must authorize its address table"); + expect(!nds_title_patches_mph_allows_rom_sha1_mismatch(), + "code-modified known variant must not reuse clean EU1.1 build"); + + // Unknown checksum with a valid header is only a candidate base profile. + // It may be reported/routed as that base, but dangerous host RAM accesses + // remain disabled until its executable checksum is explicitly validated. + const auto unknown_us10 = make_rom("AMHE", 0, {0, 0, 0, 0}); + expect(select_rom(unknown_us10), + "unknown checksum with supported header should identify a candidate base"); + expect(!nds_title_patches_mph_host_writes_compatible(), + "header fallback alone must not authorize host writes"); + g_writes.clear(); + nds_title_patches_set_mph_mouse_aim(true); + expect(!nds_title_patches_apply_mph_mouse_delta(1, 1), + "unknown executable checksum must reject direct mouse writes"); + expect(g_writes.empty(), + "unknown executable checksum must not touch profile RAM"); + g_morph_addr = 0x020DA818u; + g_morph_value = 0x02u; + expect(!nds_title_patches_mph_in_ball(), + "unknown executable checksum must reject morph-state host read"); + expect(!nds_title_patches_mph_allows_rom_sha1_mismatch(), + "unknown executable checksum must not bypass clean SHA gate"); + + // Exact clean whole-ROM SHA cannot contradict the selected executable base. + expect(!select_rom(eu11, kUs10Sha1, ""), + "known-clean US1.0 SHA with EU1.1 executable must fail closed"); + expect(!select_rom(us10, kEu11Sha1, ""), + "known-clean EU1.1 SHA with US1.0 executable must fail closed"); + + // Unsupported headers/revisions and malformed binary ranges fail closed. + const auto unknown_code = make_rom("ZZZZ", 0, {0, 0, 0, 0}); + expect(!select_rom(unknown_code), "unknown game code must fail closed"); + const auto rev2 = make_rom("AMHE", 2, {0, 0, 0, 0}); + expect(!select_rom(rev2), "unknown USA revision must fail closed"); + const auto kr_rev1 = make_rom("AMHK", 1, {0, 0, 0, 0}); + expect(!select_rom(kr_rev1), "unknown Korea revision must fail closed"); + + auto bad_range = make_rom("AMHE", 0, {0, 0, 0, 0}); + write_le32(bad_range, 0x20u, 0xFFFFFFF0u); + write_le32(bad_range, 0x2Cu, 0x100u); + expect(!select_rom(bad_range), "out-of-range ARM9 image must fail closed"); std::vector tiny(0x1Eu, 0u); expect(!nds_title_patches_select_mph_runtime_profile( - tiny.data(), static_cast(tiny.size()), kUnknownSha1), + tiny.data(), static_cast(tiny.size()), + kUnknownSha1, ""), "truncated NDS header must fail closed"); - const auto valid = make_rom("AMHE", 0); expect(!nds_title_patches_select_mph_runtime_profile( - valid.data(), static_cast(valid.size()), nullptr), + us10.data(), static_cast(us10.size()), + nullptr, ""), "missing actual-content identity must fail closed"); - // Failed selection clears the prior profile so stale addresses cannot - // remain active after a cartridge/identity change. - g_writes.clear(); - nds_title_patches_set_mph_mouse_aim(true); - expect(!nds_title_patches_apply_mph_mouse_delta(9, 9), - "failed selection must reject mouse aim"); - expect(g_writes.empty(), - "failed selection must not retain prior profile writes"); - expect(!nds_title_patches_mph_in_ball(), - "failed selection must not retain prior morph address"); - if (g_failures != 0) { std::fprintf(stderr, "%d runtime-profile assertion(s) failed\n", g_failures); return 1; } std::puts( - "OK: header-based MPH runtime profiles dispatch all seven revisions; " - "SHA-1 remains provenance"); + "OK: seven MPH base profiles use melonPrimeDS executable CRC; " + "header fallback and whole-ROM provenance gates fail closed"); return 0; } From 702aaf6cb060fa0b1ad94d154e1b955073294884 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:41:49 +0900 Subject: [PATCH 083/164] Read MPH ROM revision from header byte 0x1E --- tools/prepare_mph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/prepare_mph.py b/tools/prepare_mph.py index 193cc70..e16f1da 100644 --- a/tools/prepare_mph.py +++ b/tools/prepare_mph.py @@ -225,9 +225,9 @@ def main() -> int: f"game code mismatch for {args.version}: got " f"{rom_bytes[0x0C:0x10]!r}, expected {expected_game_code!r}" ) - if rom_bytes[0x1C] != expected_revision: + if rom_bytes[0x1E] != expected_revision: raise SystemExit( - f"ROM revision mismatch for {args.version}: got {rom_bytes[0x1C]}, " + f"ROM revision mismatch for {args.version}: got {rom_bytes[0x1E]}, " f"expected {expected_revision}" ) @@ -340,4 +340,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file From 5af26a73bf2bd9181f7f167037c32d749382fe66 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:43:55 +0900 Subject: [PATCH 084/164] Route launcher ROM validation through runtime detector --- launcher/recomp-ui/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index e23e429..5d7dc81 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -11,7 +11,7 @@ set(NDSRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../ndsrecomp" CACHE PATH "Path to the ndsrecomp framework checkout (for the shared SHA-1 helper)") set(MPH_LAUNCHER_ROM_SHA1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" CACHE STRING - "Exact retail ROM SHA-1 accepted by this profile-specific launcher") + "Clean retail ROM SHA-1 metadata rendered for this profile-specific launcher") set(MPH_LAUNCHER_REGION "USA" CACHE STRING "Region label shown by the profile-specific launcher") set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING @@ -48,6 +48,10 @@ mph_launcher_replace_required( "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" "${MPH_LAUNCHER_ROM_SHA1}" "the US1.0 SHA-1 baseline") +mph_launcher_replace_required( + "game.known_sha1_hex = sha1;\n game.num_known_sha1 = std::size(sha1);" + "// MPH_MULTIROM_CONTENT_GATE: recomp-ui must not reject a modified ROM\n // before nds_runner can apply the base/executable compatibility detector.\n // The runner remains the authoritative fail-closed content gate.\n game.known_sha1_hex = nullptr;\n game.num_known_sha1 = 0;" + "the recomp-ui whole-ROM SHA-1 gate") mph_launcher_replace_required( "game.region = \"USA\";" "game.region = \"${MPH_LAUNCHER_REGION}\";" @@ -116,4 +120,4 @@ include("${RECOMP_UI_ROOT}/recomp_ui.cmake") recomp_target_launcher_ui(mph-recomp-ui CONSOLE nds BOXART "${CMAKE_CURRENT_SOURCE_DIR}/assets/boxart.tga") -add_test(NAME mph_mod_provider_test COMMAND mph-mod-provider-test) +add_test(NAME mph_mod_provider_test COMMAND mph-mod-provider-test) \ No newline at end of file From 2fe539644663a73b4e74151f4b4182e75ac52114 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:45:02 +0900 Subject: [PATCH 085/164] Decouple content profiles from runtime base profiles --- config/mph_rom_profiles.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 3aa214e..13e806e 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -1,5 +1,5 @@ { - "schema": 4, + "schema": 5, "runtime_address_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h", "runtime_detection_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp", "runtime_checksum_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/NDSCart/CartCommon.cpp", @@ -96,6 +96,8 @@ ], "profiles": { "US1_0": { + "base_profile": "US1_0", + "known_clean": true, "display_name": "Metroid Prime Hunters (USA rev 0)", "region": "USA", "game_code": "AMHE", @@ -111,6 +113,8 @@ "adaptive_widescreen": true }, "EU1_1": { + "base_profile": "EU1_1", + "known_clean": true, "display_name": "Metroid Prime Hunters (Europe rev 1)", "region": "Europe", "game_code": "AMHP", @@ -126,4 +130,4 @@ "adaptive_widescreen": false } } -} +} \ No newline at end of file From 1e64d867f9f5c8fb47a2f6cf412f4abc29e5a6e8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:46:04 +0900 Subject: [PATCH 086/164] Allow mod-specific content profiles to share base layouts --- tools/check_mph_multirom_profiles.py | 54 +++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index a738393..949eb14 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -7,6 +7,10 @@ * executable checksum: melonPrimeDS header+ARM9+ARM7 CRC32 compatibility, * whole-ROM SHA-1: exact build/capture/generated-content provenance. +Build/capture content profiles reference a runtime base through ``base_profile``. +That lets a clean ROM and one or more exact mod ROM identities keep independent +coverage/banks/checkpoints while sharing a validated runtime address layout. + Runtime checksum hits may authorize host Aim/Morph access. Header fallback alone must never be promoted to that trust level by this checker or the runner patch. """ @@ -24,6 +28,7 @@ SHA1_RE = re.compile(r"^[0-9a-f]{40}$") CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") BANK_RE = re.compile(r"^[A-Za-z0-9_]+$") +PROFILE_KEY_RE = re.compile(r"^[A-Za-z0-9_]+$") HEX_RE = re.compile(r"0x[0-9A-Fa-f]+u?") REQUIRED_RUNTIME_FIELDS = { "morph_state": "baseIsAltForm", @@ -246,8 +251,8 @@ def validate_registry(repo: Path, table: Path | None) -> None: die("ROM profile registry must be a JSON object") registry: dict[str, object] = registry_obj - if registry.get("schema") != 4: - die("ROM profile registry schema must be 4") + if registry.get("schema") != 5: + die("ROM profile registry schema must be 5") expected_address_source = ( "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" @@ -274,15 +279,39 @@ def validate_registry(repo: Path, table: Path | None) -> None: profiles = registry.get("profiles") if not isinstance(profiles, dict) or not profiles: - die("ROM profile registry has no clean build/capture profiles") + die("ROM profile registry has no build/capture content profiles") seen_sha1: set[str] = set() seen_fmv_banks: set[str] = set() + seen_known_clean_base: set[str] = set() for key, profile in profiles.items(): + if not isinstance(key, str) or not PROFILE_KEY_RE.fullmatch(key): + die(f"content profile key {key!r} must be a C/path-safe identifier") if not isinstance(profile, dict): die(f"profile {key} must be an object") - if key not in runtime_profiles: - die(f"{key}: clean build profile has no runtime base profile") + + base_profile = profile.get("base_profile") + known_clean = profile.get("known_clean") + if not isinstance(base_profile, str) or base_profile not in runtime_profiles: + die(f"{key}.base_profile must name one of the seven runtime profiles") + if not isinstance(known_clean, bool): + die(f"{key}.known_clean must be boolean") + if known_clean: + # Runtime code generation intentionally discovers a clean identity by + # looking up the content profile with the same canonical key. + if key != base_profile: + die( + f"{key}: known-clean content profile must use its canonical " + f"runtime key {base_profile!r}" + ) + if base_profile in seen_known_clean_base: + die(f"duplicate known-clean content identity for {base_profile}") + seen_known_clean_base.add(base_profile) + elif key in runtime_profiles: + die( + f"{key}: canonical runtime-profile key is reserved for its " + "known-clean content identity; use a distinct key for a mod" + ) sha1 = profile.get("sha1") game_code = profile.get("game_code") @@ -293,9 +322,12 @@ def validate_registry(repo: Path, table: Path | None) -> None: die(f"duplicate SHA-1 in profile registry: {sha1}") seen_sha1.add(sha1) - runtime_profile = runtime_profiles[key] + runtime_profile = runtime_profiles[base_profile] if game_code != runtime_profile.get("game_code") or revision != runtime_profile.get("revision"): - die(f"{key}: clean build identity disagrees with runtime base profile") + die( + f"{key}: content header identity disagrees with runtime base " + f"profile {base_profile}" + ) launcher_default_rom = profile.get("launcher_default_rom") if ( @@ -386,12 +418,16 @@ def validate_registry(repo: Path, table: Path | None) -> None: us = profiles.get("US1_0") if not isinstance(us, dict): die("US1_0 clean build profile is required") + if us.get("base_profile") != "US1_0" or us.get("known_clean") is not True: + die("US1_0 must remain the canonical known-clean US1_0 identity") if us.get("fmv_runtime_bank") != "mph_arm9_fmv_runtime": die("US1_0 historical FMV runtime bank identity changed unexpectedly") eu = profiles.get("EU1_1") if not isinstance(eu, dict): die("EU1_1 clean build profile is required") + if eu.get("base_profile") != "EU1_1" or eu.get("known_clean") is not True: + die("EU1_1 must remain the canonical known-clean EU1_1 identity") if eu.get("game_code") != "AMHP" or eu.get("revision") != 1: die("EU1_1 must remain AMHP revision 1") if eu.get("sha1") != "bdcd1dea293e24c98d4c481430e90d21198985a5": @@ -415,7 +451,7 @@ def validate_registry(repo: Path, table: Path | None) -> None: print( f"OK: validated {len(runtime_profiles)} runtime base profiles, " f"{len(EXPECTED_RUNTIME_CHECKSUMS)} executable checksums, and " - f"{len(profiles)} clean build/capture profiles" + f"{len(profiles)} build/capture content profiles" ) if melon is not None: print(f"OK: all seven Aim/Morph profiles match melonPrimeDS table: {table}") @@ -436,4 +472,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file From fabe97ced6f8a074af92e4397b95ab55f3a6d610 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:47:50 +0900 Subject: [PATCH 087/164] Document base ROM and mod content identity model --- docs/EU1_1_BRINGUP.md | 630 +++++++++++++++--------------------------- 1 file changed, 222 insertions(+), 408 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index fc2e286..9e590a8 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -1,499 +1,313 @@ -# Metroid Prime Hunters Recomp - EU1.1 Bring-up +# Metroid Prime Hunters Multi-ROM / EU1.1 Bring-up -作成日: 2026-08-17 -更新日: 2026-08-17 +This document describes the current multi-ROM architecture in this branch. +It supersedes the earlier SHA-1-driven runtime-profile design. -## 1. 目的 +## 1. Status -`MetroidPrimeHuntersRecomp` を USA revision 0 (`AMHE`, revision 0) 固定からmulti-ROM化し、最初の追加対象として Europe revision 1 (`AMHP`, revision 1) を安全にbring-upする。 +Runtime address layouts are statically prepared for all seven retail MPH base +revisions: -ROMなしで可能な基盤実装は完了しており、現在の残件はEU1.1実ROMを使うruntime validation、EU1.1固有coverageの実採取、必要に応じたEU1.1固有FMV runtime captureである。 +| Runtime base | Game code | Revision | Morph / Alt Form | Aim X | Aim Y | +|---|---|---:|---:|---:|---:| +| `US1_0` | `AMHE` | 0 | `0x020DA818` | `0x020DE526` | `0x020DE52E` | +| `US1_1` | `AMHE` | 1 | `0x020DB098` | `0x020DEDA6` | `0x020DEDAE` | +| `EU1_0` | `AMHP` | 0 | `0x020DB0B8` | `0x020DEDC6` | `0x020DEDCE` | +| `EU1_1` | `AMHP` | 1 | `0x020DB138` | `0x020DEE46` | `0x020DEE4E` | +| `JP1_0` | `AMHJ` | 0 | `0x020DC6D8` | `0x020E03E6` | `0x020E03EE` | +| `JP1_1` | `AMHJ` | 1 | `0x020DC698` | `0x020E03A6` | `0x020E03AE` | +| `KR1_0` | `AMHK` | 0 | `0x020D3EE4` | `0x020D7C0E` | `0x020D7C16` | -## 2. EU1.1 identity +Exact build/capture content profiles currently exist for: -| Field | EU1.1 | -|---|---| -| Profile key | `EU1_1` | -| Game Code | `AMHP` | -| Revision | `1` | -| ROM size | `0x04000000` / 64 MiB | -| SHA-1 | `bdcd1dea293e24c98d4c481430e90d21198985a5` | -| Program ID prefix | `mph_amhp1` | -| Game config | `config/game-eu11.toml` | -| Launcher default ROM | `Metroid Prime Hunters (Europe Rev 1).nds` | -| Adaptive Widescreen | disabled until EU1.1 validation | -| FMV runtime enabled | `false` | -| Reserved EU1.1 FMV bank ID | `mph_amhp1_arm9_fmv_runtime` | +- `US1_0` clean retail ROM +- `EU1_1` clean retail ROM -identityと版別policyは `config/mph_rom_profiles.json` に集約する。 +The other retail revisions and individual modified ROMs still require their own +exact-content build/capture profile, coverage and generated artifacts before +they can be called fully supported. -## 3. ROM profile registry +## 2. Sources of truth -schema 2 profileは少なくとも以下を管理する。 +Runtime detection and addresses intentionally follow melonPrimeDS +`develop_hud`: -- `game_code`, `revision`, `rom_size`, `sha1`, `program_id` -- `coverage`, `game_config` -- `fmv_runtime`, `fmv_runtime_bank` -- `launcher_default_rom`, `adaptive_widescreen` -- `runtime.morph_state`, `runtime.aim_x`, `runtime.aim_y` +- detector: `src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp` +- address table: `src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h` +- executable checksum algorithm: `src/NDSCart/CartCommon.cpp`, `CartCommon::Checksum()` +- CRC32 implementation: `src/CRC32.cpp` -host-side runtime addressのsource of truth: +Repository: -```text -https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h -``` - -Aim/Morphアドレスはglobal relocation deltaから推測しない。 - -CMakeのprofile候補とWindows buildの `-MphVersion` validationもregistryから導出する。新しいrevisionをprofile registryへ追加するとき、`US1_0/EU1_1` の固定列挙を別途更新する必要はない。 +`https://github.com/ag-advania/melonPrimeDS/tree/develop_hud` -## 4. melonPrimeDSから確定したEU1.1 runtime addresses +The project registry records these source locations in +`config/mph_rom_profiles.json`. -| Semantic | melonPrimeDS field | US1.0 | EU1.1 | -|---|---|---:|---:| -| Morph / Alt Form state | `baseIsAltForm` | `0x020DA818` | `0x020DB138` | -| Direct Aim X | `baseAimX` | `0x020DE526` | `0x020DEE46` | -| Direct Aim Y | `baseAimY` | `0x020DE52E` | `0x020DEE4E` | +## 3. Identity model -CIはmelonPrimeDS `main` の実ファイルを取得してprofileと自動照合する。 +The old design coupled runtime RAM addresses to an exact whole-ROM SHA-1. +That is intentionally no longer the design. -## 5. pinned ndsrecomp runtime profile shim +There are three separate identities. -pinned framework: +### 3.1 Runtime Base Profile -```text -46b12e6c18dea47f87d2c1f98c3054149dcbca5d -``` - -元runnerのUS1.0固定Morph/Aim addressとUS1.0-only Prime Controls policyは `tools/patch_ndsrecomp_mph_runtime.py` でexact-ROM profile選択へ変換する。 +A runtime base profile is one of the seven region/revision layouts above. It +owns revision-specific host RAM addresses such as Aim X/Y and Morph / Alt Form. -```text -ROM SHA-1 - -> NdsMphRuntimeProfile - -> morph_state - -> aim_x - -> aim_y - -> known profileのみPrime Controls / Direct Mouse Aimを許可 - -> unknown ROMはfail-closed -``` +The runtime detector uses: -patcherはexact source preimageを要求し、idempotentで、profile切替時にold direct-aim enable stateをclearする。 +1. melonPrimeDS-compatible executable checksum, then +2. exact NDS header `gameCode + revision` fallback. -`tools/tests/mph_runtime_profile_test.cpp` はpatched `title_patches.cpp` を実際にリンクして以下を検証する。 +NDS header offsets: -- unknown SHA-1でAim write / Morph readなし -- US1.0 address regressionなし -- EU1.1は `0x020DB138 / 0x020DEE46 / 0x020DEE4E` のみ使用 -- profile切替後のstale stateなし +- game code: `0x0C..0x0F` +- ROM revision/version: `0x1E` -## 6. EU1.1 static coverage pipeline +The Recomp detector is deliberately stricter than melonPrimeDS's generic +`revision != 0 -> 1.1` fallback. Only the seven explicitly supported tuples are +accepted. A hypothetical rev2+ is unknown. -### 6.1 Bootstrap +### 3.2 Executable Compatibility Identity -`coverage/eu11-bootstrap-entry-points.json` は追加ARM9/ARM7 rootを空にする。ROM header entry PCは `prepare_mph.py` がseedし、未コンパイル領域はInterpreter fallbackへ送る。 +melonPrimeDS `CartCommon::Checksum()` is reproduced exactly: -US1.0の `coverage/adventure-main-entry-points.json` に含まれるabsolute PCはEU1.1へコピーしない。EU1.1自身のexecution traceからのみcoverageを拡張する。 +1. CRC32 over ROM header bytes `0x00..0x3F` +2. continue CRC32 over the ARM9 ROM image +3. continue CRC32 over the ARM7 ROM image -### 6.2 US1.0固定rangeの除去 +A hit in the audited melonPrimeDS checksum table is authoritative for the base +runtime layout and may enable host-side Aim/Morph RAM access. -旧 `tools/promote_mph_static_coverage.py` はUS1.0固定の以下を内蔵していた。 +A header-only match is weaker. It identifies a candidate base profile, but it +**does not authorize host RAM reads/writes**. Unknown executable content remains +fail-closed. -```text -GAME_SHA1 -ARM9 main-image start/end -ARM7 main-image start/end -ARM9/ARM7 existing entry PC -``` +This prevents a modified ROM that merely preserves `AMHE` revision 0 from being +blindly treated as memory-compatible US1.0. -現在はこれらを持たない。 +### 3.3 Actual Content Identity -`prepare_mph.py` が選択ROMそのものから生成した +Whole-ROM SHA-1 identifies the exact content used to generate or validate: -```text -generated//inputs/arm9.toml -generated//inputs/arm7.toml -``` +- build inputs +- recomp banks +- static coverage +- runtime coverage +- checkpoints +- FMV/runtime captures -の `[program].load_address`, `size`, `entry_pc`, `id` を読み、実ROM由来のimmutable main-image geometryをcoverage filterへ使用する。 +This identity is not the runtime-address selector. -EU1.1では既定で: +Generated title banks remain registered against the actual ROM SHA-1. A bank +captured/generated for clean ROM or MOD A is therefore not silently reused for +MOD B. -```text -generated/EU1_1/inputs/arm9.toml -generated/EU1_1/inputs/arm7.toml -``` +## 4. Known melonPrimeDS executable checksums -を読む。 +The current registry mirrors the audited `develop_hud` detector table, +including: -したがってEU1.1のARM9/ARM7終端アドレスをUS1.0から推測・変換する処理はない。 +- all seven clean retail revisions +- encrypted variants +- EU1.1 Balanced variants +- EU1.1 Russian variant (`0x9E20F3A8`) -### 6.3 Trace provenance +A known checksum establishes the runtime RAM layout. It does **not** by itself +mean the clean recomp build is byte-compatible with that modified executable. -`tools/fuzz_mph_gameplay.py` は現在profile-awareで、実行前にROMのsize/SHA-1/game code/revisionを検証する。 +For example, the known EU1.1 Russian executable can use the EU1.1 Aim/Morph RAM +layout, but because its executable checksum differs from canonical clean EU1.1, +it still requires a mod-specific exact build/capture content profile. -生成する `trace.json` には少なくとも以下を記録する。 +`Last Raven` is not present in the current audited `develop_hud` checksum table. +It therefore remains fail-closed for host Aim/Morph access until its executable +checksum/layout is explicitly validated. -```json -{ - "mph_profile": "EU1_1", - "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", - "scenario": "scenarios/adventure_start.json" -} -``` - -EU1.1のcoverage昇格では、このprofile/ROM identityが欠落した旧traceや出所不明traceを拒否する。 - -### 6.4 EU1.1 Adventure coverage採取 - -EU1.1 runnerをbuild済みとする。 - -```powershell -python tools\fuzz_mph_gameplay.py ` - --version EU1_1 ` - --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` - --bios bios ` - --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` - --out generated\EU1_1\capture\adventure-coverage ` - --actions scenarios\adventure_start.json ` - --steps 0 ` - --capture-static-coverage -``` - -`--capture-static-coverage` によりrunnerは `--discover-static-misses` 付きで起動し、最後に `static_coverage` と `tier3_coverage` をdebug serverから回収する。 - -### 6.5 EU1.1 static coverage昇格 - -runner/framework commitは実際に採取へ使ったrevisionを記録する。 - -```powershell -$runnerCommit = git -C ..\ndsrecomp rev-parse HEAD - -python tools\promote_mph_static_coverage.py ` - --version EU1_1 ` - --trace generated\EU1_1\capture\adventure-coverage\trace.json ` - --out coverage\eu11-adventure-main-entry-points.json ` - --runner-commit $runnerCommit -``` +## 5. Clean ROM versus modified ROM startup -昇格対象は以下のみ。 +The recomp-ui launcher intentionally does not enforce the clean whole-ROM SHA. +The launcher passes the selected `.nds` file to the runner, where the stronger +multi-layer detector can decide safely. -- ARM9/ARM7 immutable main image内 -- Tier-3 `call` -- Tier-3 `indirect` +The runner rules are: -以下は除外する。 +- exact expected whole-ROM SHA: normal exact-content build +- different whole-ROM SHA + canonical clean header/ARM9/ARM7 checksum of the + expected base: clean build may be reused for a data-only variant +- code-modified executable checksum: exact clean SHA gate is not bypassed; + prepare a mod-specific build/capture profile +- unknown checksum + supported header: candidate base may be identified, but + host Aim/Morph RAM access and clean-build SHA bypass stay disabled +- unknown game code/revision or malformed ARM image ranges: reject/fail closed -- slice-resume root -- main image範囲外runtime RAM -- reused overlay virtual ranges -- ROM header entry PCの重複 +The launcher SHA check is disabled specifically so it cannot reject a mod before +these runner rules execute. -実ROM検証後に `config/mph_rom_profiles.json` のEU1.1 `coverage` をbootstrap JSONからこの昇格済みJSONへ切り替える。 +## 6. Content profiles and `base_profile` -## 7. Generated tree separation - -US1.0: - -```text -generated/inputs/ -generated/recomp/ -generated/capture/ -``` - -EU1.1: - -```text -generated/EU1_1/inputs/ -generated/EU1_1/recomp/ -generated/EU1_1/capture/ -``` - -異なるROM revisionのprepared binary、coverage capture、runtime image、generated bankを同じディレクトリへ混在させない。 - -## 8. Launcher identity / feature policy separation - -`launcher/recomp-ui/CMakeLists.txt` はbaseline `launcher_main.cpp` からprofile-specific generated TUを作る。 - -反映項目: - -- exact ROM SHA-1 -- Region -- default ROM filename -- Adaptive Widescreen availability/default - -EU1.1 generated launcher: - -```text -SHA-1: bdcd1dea293e24c98d4c481430e90d21198985a5 -Region: Europe -Default ROM: Metroid Prime Hunters (Europe Rev 1).nds -Adaptive Widescreen: disabled / UI hidden -``` - -EU1.1 Adaptive Widescreenは三重にfail-closed: - -1. mod listから非表示 -2. persisted `adaptive_widescreen=true` をload後にfalseへ戻す -3. `launch_runner()` 最終段でもprofile capabilityとANDする - -Prime ControlsはEU1.1でも表示する。必要なMorph/Aim address routingはROMなしunit testで固定済みだが、ゲーム内semantic correctnessは実ROMで確認する。 - -## 9. EU1.1 FMV runtime capture pipeline - -### 9.1 US1.0 bankの非流用 - -US1.0の既存bank: - -```text -config/mph_arm9_fmv_runtime.toml -generated/capture/mph_arm9_fmv_runtime.bin -bank id: mph_arm9_fmv_runtime -``` +`config/mph_rom_profiles.json` schema 5 separates content identity from runtime +base identity. -はEU1.1へ流用しない。 +Canonical clean profiles reserve the seven base keys. Example: -EU1.1用に予約しているidentity/path: - -```text -config/mph_amhp1_arm9_fmv_runtime.toml -generated/EU1_1/capture/mph_amhp1_arm9_fmv_runtime.bin -bank id: mph_amhp1_arm9_fmv_runtime -``` - -現在 `fmv_runtime=false` なので、EU1.1 buildはこのbankを要求・登録しない。 - -### 9.2 FMV benchmark/captureのprofile化 - -`tools/benchmark_mph_fmv.py` は現在: - -- `--version EU1_1` を受ける -- exact EU1.1 ROM identityを起動前に検証する -- `--config` 省略時は `config/game-eu11.toml` を選ぶ -- `--adaptive auto` が既定 -- EU1.1ではprofile policyに従い `--adaptive-widescreen none` -- profileがAdaptive Widescreen未検証なら明示的な `--adaptive top` も拒否する -- `benchmark.json` に `mph_profile` と `rom_sha1` を保存する -- `--capture-runtime` 時はcapture SHA-1とbyte countも保存する - -EU1.1を誤ってUS1.0のAdaptive Widescreen有効状態でbenchmark/coverage captureする経路はfail-closedにした。 - -### 9.3 Before window採取 - -例としてVBlank 2400までの累積coverageを取る。 - -```powershell -python tools\benchmark_mph_fmv.py ` - --version EU1_1 ` - --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` - --bios bios ` - --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` - --out generated\EU1_1\capture\fmv-before ` - --targets 2400 ` - --discover-static-misses -``` - -### 9.4 Target window + RAM capture - -```powershell -python tools\benchmark_mph_fmv.py ` - --version EU1_1 ` - --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` - --bios bios ` - --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` - --out generated\EU1_1\capture\fmv-3000 ` - --targets 2400 3000 ` - --discover-static-misses ` - --capture-runtime generated\EU1_1\capture\mph_amhp1_arm9_fmv_runtime.bin +```json +"US1_0": { + "base_profile": "US1_0", + "known_clean": true, + "game_code": "AMHE", + "revision": 0, + "sha1": "...exact clean whole-ROM SHA-1..." +} ``` -runtime imageはITCM + main RAMを連結した `0x00408000` bytes。 +A future exact mod profile must use a distinct key and reference the compatible +base layout: -### 9.5 EU1.1 runtime bank config昇格 - -```powershell -python tools\promote_mph_runtime_coverage.py ` - --version EU1_1 ` - --before-benchmark generated\EU1_1\capture\fmv-before\benchmark.json ` - --benchmark generated\EU1_1\capture\fmv-3000\benchmark.json ` - --image generated\EU1_1\capture\mph_amhp1_arm9_fmv_runtime.bin ` - --out config\mph_amhp1_arm9_fmv_runtime.toml +```json +"US1_0_LAST_RAVEN": { + "base_profile": "US1_0", + "known_clean": false, + "game_code": "AMHE", + "revision": 0, + "sha1": "...exact Last Raven whole-ROM SHA-1...", + "program_id": "mph_amhe0_last_raven", + "coverage": "coverage/last-raven-entry-points.json", + "game_config": "config/game-last-raven.toml", + "fmv_runtime": false, + "fmv_runtime_bank": "mph_amhe0_last_raven_arm9_fmv_runtime", + "launcher_default_rom": "Metroid Prime Hunters - Last Raven.nds", + "adaptive_widescreen": false +} ``` -EU1.1では昇格時に以下を全て要求する。 - -- benchmarkの `mph_profile == EU1_1` -- benchmarkの `rom_sha1 == EU1.1 SHA-1` -- before benchmarkも同じidentity -- benchmark内 `runtime_capture.sha1` と渡した`.bin`の実SHA-1が一致 -- benchmark内capture byte countが `0x00408000` -- ARM9 runtime領域内のobserved call/indirect targetが存在 - -これらのどれかが不一致ならTOMLを生成しない。 - -実runtime validationとperformance確認後にのみ、EU1.1 profileの `fmv_runtime` を `true` へ変更する。 - -## 10. FMV runtime bank build/release routing - -CMakeはFMV bank名を固定しない。profileの `fmv_runtime_bank` から以下を導出する。 +The example is architectural only. Do not add a Last Raven profile until its +actual SHA-1, executable checksum, game config and generated/captured artifacts +are known and validated. -```text -config/.toml -generated//capture/.bin -generated//recomp/_*.c ---bank -``` +The static checker enforces that a mod cannot overwrite a canonical clean key. -Windows/Linux release gateもprofileのbank IDをrunner内で確認する。 +## 7. EU1.1 current exact-content identity -従って将来EU1.1で `fmv_runtime=true` にした場合も、US1.0の `mph_arm9_fmv_runtime` を誤要求しない。 +Current clean EU1.1 profile: -## 11. Profile-aware checkpoint validation +- profile: `EU1_1` +- base profile: `EU1_1` +- game code: `AMHP` +- revision: `1` +- whole-ROM SHA-1: `bdcd1dea293e24c98d4c481430e90d21198985a5` +- default launcher ROM: `Metroid Prime Hunters (Europe Rev 1).nds` +- Adaptive Widescreen: disabled until validated +- FMV runtime bank: disabled until an EU1.1-specific capture is validated -`tools/capture_mph_checkpoints.py` もprofile-awareにした。 +Reserved EU1.1 runtime bank identity: -runner/oracleのどちらを使う場合も、起動前に選択ROMをprofileのsize/SHA-1/game code/revisionへ照合する。runner時は `--config` 省略でprofileのgame configを選び、config identityもROM profileへ照合する。 +- config: `config/mph_amhp1_arm9_fmv_runtime.toml` +- capture: `generated/EU1_1/capture/mph_amhp1_arm9_fmv_runtime.bin` +- bank: `mph_amhp1_arm9_fmv_runtime` -各capture directoryには `metadata.json` を追加し、以下を保存する。 +No US1.0 FMV runtime capture is reused for EU1.1. -```text -mph_profile -rom_sha1 -display_name -backend -boot -targets -game_config -``` +## 8. Preparing a clean content profile -これによりUS1.0 native checkpointとEU1.1 oracle checkpoint等を誤って同一比較セットとして扱う前に、capture provenanceを確認できる。 +The preparation path intentionally remains exact-content gated. This is not the +runtime selector; it protects generated code provenance. -EU1.1例: +Example EU1.1 preparation: -```powershell -python tools\capture_mph_checkpoints.py ` - --version EU1_1 ` - --runner ..\ndsrecomp\runner\build-mph-release-EU1_1\nds_runner.exe ` - --bios bios ` - --rom "Metroid Prime Hunters (Europe Rev 1).nds" ` - --out generated\EU1_1\capture\checkpoints ` - --targets 300 600 900 1200 +```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 ``` -## 12. Build +`prepare_mph.py` checks: -### Windows +- exact whole-ROM SHA-1 +- expected ROM size +- game code at `0x0C` +- revision at `0x1E` +- coverage `game_sha1` -```powershell -powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` - tools\build-windows.ps1 ` - -Version 0.3.0 ` - -MphVersion EU1_1 ` - -RomPath 'D:\ROMs\Metroid Prime Hunters (Europe) (Rev 1).nds' -``` - -`-RomPath`省略時はprofileの `launcher_default_rom` を使う。 +It then extracts ARM9, ARM7 and overlays and emits revision/content-specific +seed configs. -WindowsはEU1.1 identity verify -> extraction -> static bank -> runtime-profile patch -> exact EU SHA runner -> profile-specific launcher -> EU game config packagingまで一貫して行う。 +## 9. Building EU1.1 -### Linux +Typical CMake configuration: ```bash -tools/build-linux.sh \ - --mph-version EU1_1 \ - --rom '/path/to/Metroid Prime Hunters (Europe) (Rev 1).nds' +cmake -S . -B build-eu11 \ + -DMPH_VERSION=EU1_1 \ + -DMPH_ROM="/path/to/Metroid Prime Hunters (Europe Rev 1).nds" +cmake --build build-eu11 ``` -`--rom`省略時はEU1.1 profileのdefault ROM filenameをrepository rootから探す。AppRunはprofile別 `game.toml` をrunnerへ渡し、`--adaptive-widescreen` 等でtitle policyを上書きしない。 - -## 13. ROM不要static CI - -`.github/workflows/mph-multirom-static.yml` は以下を検証する。 - -1. capture/promotion/checkpointを含むPython syntax -2. Linux shell / Windows PowerShell syntax -3. profile / coverage / game config / launcher / FMV bank policy整合性 -4. CMake/Windowsがprofile keyを固定列挙しないこと -5. melonPrimeDS `MelonPrimeGameRomAddrTable.h` とのAim/Morph照合 -6. fake EU1.1 prepared ARM9/ARM7 geometryからstatic coverageを昇格 -7. main-image範囲外targetが除外されることを確認 -8. fake EU1.1 runtime image + tagged benchmarkからEU1.1 FMV TOMLを生成 -9. runtime image SHA/size metadata照合 -10. EU1.1 TOMLへUSA runtime名が混入しないことを確認 -11. CMake/Windows/Linuxがprofile-owned FMV bank IDを使用することを確認 -12. exact `ndsrecomp.pin` fetch -13. US1.0/EU1.1 launcher generated source renderとidentity/policy確認 -14. Linux AppRunのprofile-owned config policy確認 -15. ndsrecomp runtime patchのidempotency -16. US1.0固定Aim/Morph symbol除去確認 -17. patched runnerの `title_patches.cpp` / `frontend.cpp` / `main.cpp` compile -18. exact-ROM runtime dispatch unit test実行 -19. US1.0/EU1.1 `mph_romcheck` compile -20. `git diff --check` - -ROM、BIOS、firmware dumpはCIで取得しない。 - -## 14. mphCodexの役割 - -Aim X/Y/MorphはmelonPrimeDS tableをsource of truthとする。その他Recomp固有host enhancementのcross-version semantic調査にはmphCodexを利用する。 - -| Semantic | US1.0 | EU1.1 | -|---|---:|---:| -| Current Camera Sequence | `0x020D9CB0` | `0x020DA5D0` | -| Game Mode | `0x020E78FC` | `0x020E845C` | -| Upper HUD function | `0x0202F600` | `0x0202F5E0` | -| Crosshair callsite | `0x0202F934` | `0x0202F904` | -| Crosshair renderer | `0x020393D4` | `0x02039338` | -| Local Player Pointer | `0x020BCA70` | `0x020BD370` | -| HUD suppression storage | `0x020DE748` | `0x020DF068` | - -単一delta変換は使用しない。 - -## 15. 実ROMで残るvalidation gates - -### Gate A - extraction / bank generation - -EU1.1 identity accept、ARM9 decompress、ARM7 extraction、overlay列挙、EU1.1 bank生成、US1.0 artifact非混入。 - -### Gate B - boot - -firmware boot、cartridge handoff、opening logos/FMV、title、attract loop。 - -### Gate C - gameplay - -Adventure file作成/読込、Celestial Archives、movement/aim/shoot、Morph Ball、Scan Visor、pause、save/reload、multiplayer menu。 - -### Gate D - Prime Controls / Direct Mouse Aim semantics - -address routingはunit test済み。実ROMではnormal/Morph touch behavior、camera aim、menu/touch復帰、keyboard/gamepad操作を確認する。 +Windows and Linux build helpers consume the same registry-owned profile data. +Profile choices are derived from `profiles`, not a hard-coded two-profile list. -### Gate E - deterministic coverage +## 10. Coverage and capture safety -EU1.1自身のprofile-tagged execution traceからcoverageを採取し、EU1.1 prepared main-image geometryでfilterして昇格する。 +Static and runtime promotion is profile/content aware. -### Gate F - FMV runtime optimization +For non-US content, traces carry both: -必要な場合のみEU1.1 captureを作り、capture identity、content validation、correctness、performanceを確認してから `fmv_runtime=true` にする。 +- content profile key +- exact whole-ROM SHA-1 -### Gate G - Adaptive Widescreen +Main-image geometry is derived from the selected prepared ARM9/ARM7 configs, +not reused from US1.0 constants. -現在EU1.1では意図的に無効。基本対応の必須条件ではない。将来有効化する場合のみprojection、culling、HUD anchoring、touchscreen、特殊camera/visor sceneをEU1.1実ROMで検証し、profileとgame configを同時にenableする。 +FMV/runtime capture promotion verifies exact content provenance. A capture from +one content SHA must not be promoted into another content profile. -## 16. Supported判定 +## 11. Runtime safety tests -EU1.1をruntime検証済みsupportedと宣言するには、exact identity、EU1.1 ARM9/ARM7 banks、title/gameplay/save/load、Prime Controls/Direct Aim semantic validation、native/reference checkpoint比較、US1.0 regressionなしが必要である。 +CI patches the exact pinned ndsrecomp revision and compiles the patched runner +translation units. -Adaptive WidescreenとEU1.1 FMV runtime bankは基本correctnessの必須条件ではない。未検証機能はfail-closedを維持する。 +The runtime harness uses synthetic, non-copyrighted ROM images constructed to +produce the canonical melonPrimeDS executable checksums. It verifies: -## 17. 現在の判定 +- all seven runtime profiles dispatch the correct Aim/Morph addresses +- checksum-authoritative profiles allow host Aim/Morph access +- header-only fallback does not allow host RAM access +- unsupported revisions fail closed +- malformed ARM image ranges fail closed +- known-clean SHA/header contradictions fail closed +- canonical executable-equivalent data variants may pass the clean SHA gate +- known code-modified EU1.1 Russian checksum gets EU1.1 RAM layout but cannot + reuse the clean EU1.1 build identity +- runtime patching is idempotent +- patched runner source files compile -### Code / infrastructure +## 12. Remaining work for full multi-ROM support -**READY FOR EU1.1 ROM VALIDATION AND PROFILE-TAGGED COVERAGE CAPTURE** +The detector/address architecture is now prepared for all seven base revisions, +but full player-facing support still requires exact content work per ROM: -ROMなしで可能なprofile、extraction routing、bank isolation、runtime address selection、launcher identity/feature gating、coverage capture metadata、static/runtime coverage promotion、profile-owned FMV bank routing、checkpoint identity validation、Windows/Linux packaging、static CIまで実装済み。 +1. add verified clean content profiles for US1.1, EU1.0, JP1.0, JP1.1 and KR1.0 +2. prepare/extract each exact ROM +3. capture deterministic coverage and promote it under that exact SHA +4. generate revision-specific ARM9/ARM7/overlay banks +5. validate boot, Adventure, pause, save/load and multiplayer-menu paths +6. validate Prime Controls / Direct Mouse Aim semantics on real execution +7. compare checkpoints against the reference/native execution path +8. capture revision-specific FMV runtime code only where interpreter fallback needs optimization +9. regression-test US1.0 after each new profile -### Runtime correctness +For a modified ROM, additionally: -**NOT YET CLAIMED** +1. compute the exact whole-ROM SHA-1 +2. compute the melonPrimeDS-compatible header+ARM9+ARM7 CRC32 +3. determine/validate its runtime base layout +4. if code-modified, register the executable checksum only after address compatibility is established +5. add a distinct content profile with `base_profile` +6. generate/promote coverage and banks under that mod's exact content identity -EU1.1実ROMによるboot/gameplay/reference validationと実coverage/capture採取は別途必要である。 +Do not guess an unknown mod as US1.0 simply because its filename, region or +header resembles US1.0. \ No newline at end of file From 3221a4e2fadcb29b0d9ffbd2a911d1942b799ee3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:50:02 +0900 Subject: [PATCH 088/164] Cross-check MPH detector table against melonPrimeDS --- tools/check_melonprime_detector.py | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tools/check_melonprime_detector.py diff --git a/tools/check_melonprime_detector.py b/tools/check_melonprime_detector.py new file mode 100644 index 0000000..b8e5144 --- /dev/null +++ b/tools/check_melonprime_detector.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Verify the MPH executable-checksum registry against melonPrimeDS develop_hud. + +This deliberately parses only the authoritative CHECKSUM_TABLE from +MelonPrimeGameRomDetect.cpp. Runtime addresses are cross-checked separately +against MelonPrimeGameRomAddrTable.h. +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +ENTRY_RE = re.compile( + r'\{\s*(0x[0-9A-Fa-f]{8})u\s*,\s*RomGroup::([A-Za-z0-9_]+)\s*,\s*"([^"]+)"\s*\}' +) + + +def fail(message: str) -> None: + raise SystemExit(f"ERROR: {message}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--profiles", type=Path, required=True) + parser.add_argument("--detector", type=Path, required=True) + args = parser.parse_args() + + registry = json.loads(args.profiles.read_text(encoding="utf-8")) + configured = registry.get("runtime_checksums") + if not isinstance(configured, list): + fail("profile registry has no runtime_checksums array") + + expected: list[tuple[int, str, str]] = [] + for index, entry in enumerate(configured): + if not isinstance(entry, dict): + fail(f"runtime_checksums[{index}] must be an object") + crc = entry.get("crc32") + profile = entry.get("profile") + name = entry.get("name") + if not isinstance(crc, str) or not isinstance(profile, str) or not isinstance(name, str): + fail(f"runtime_checksums[{index}] has invalid fields") + expected.append((int(crc, 16), profile, name)) + + source = args.detector.read_text(encoding="utf-8") + table_match = re.search( + r"constexpr\s+ChecksumEntry\s+CHECKSUM_TABLE\[\]\s*=\s*\{(.*?)\n\s*\};", + source, + re.S, + ) + if not table_match: + fail("could not locate CHECKSUM_TABLE in melonPrimeDS detector") + + actual = [ + (int(crc, 16), profile, name) + for crc, profile, name in ENTRY_RE.findall(table_match.group(1)) + ] + if not actual: + fail("melonPrimeDS CHECKSUM_TABLE contained no parseable entries") + + if actual != expected: + print("Configured runtime checksums:") + for crc, profile, name in expected: + print(f" 0x{crc:08X} {profile} {name}") + print("melonPrimeDS develop_hud checksums:") + for crc, profile, name in actual: + print(f" 0x{crc:08X} {profile} {name}") + fail("runtime checksum registry drifted from MelonPrimeGameRomDetect.cpp") + + # Keep the documented fallback contract visible to CI. If upstream moves + # either NDS header field, the imported detector source must be re-audited. + if "gameCode (@0x0C) + revision (@0x1E)" not in source: + fail("melonPrimeDS detector no longer documents gameCode@0x0C + revision@0x1E") + + print( + f"OK: {len(actual)} executable checksums and header fallback contract " + "match melonPrimeDS develop_hud" + ) + + +if __name__ == "__main__": + main() From 4fbfc4360c4a0d6f2cc9aae9379f8ef96b477ec8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:50:41 +0900 Subject: [PATCH 089/164] Pin multi-ROM CI checks to melonPrimeDS develop_hud --- .github/workflows/mph-multirom-static.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index f335afb..329760a 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -25,6 +25,7 @@ jobs: tools/mph_profile.py \ tools/prepare_mph.py \ tools/check_mph_multirom_profiles.py \ + tools/check_melonprime_detector.py \ tools/patch_ndsrecomp_mph_runtime.py \ tools/promote_mph_static_coverage.py \ tools/promote_mph_runtime_coverage.py \ @@ -51,13 +52,19 @@ jobs: } if ($failed) { exit 1 } - - name: Cross-check runtime addresses against melonPrimeDS + - name: Cross-check runtime detector against melonPrimeDS develop_hud run: | curl -fsSL --retry 3 \ - https://raw.githubusercontent.com/ag-advania/melonPrimeDS/main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h \ + https://raw.githubusercontent.com/ag-advania/melonPrimeDS/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h \ -o /tmp/MelonPrimeGameRomAddrTable.h + curl -fsSL --retry 3 \ + https://raw.githubusercontent.com/ag-advania/melonPrimeDS/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp \ + -o /tmp/MelonPrimeGameRomDetect.cpp python tools/check_mph_multirom_profiles.py \ --melonprime-table /tmp/MelonPrimeGameRomAddrTable.h + python tools/check_melonprime_detector.py \ + --profiles config/mph_rom_profiles.json \ + --detector /tmp/MelonPrimeGameRomDetect.cpp - name: Test profile-aware coverage promotion run: | @@ -154,6 +161,8 @@ jobs: grep -q 'FMV_RUNTIME_BANK' tools/build-linux.sh grep -q 'FmvRuntimeBank' tools/build-windows.ps1 grep -q 'FmvRuntimeBank' tools/make_release.ps1 + grep -q 'rom_bytes\[0x1E\]' tools/prepare_mph.py + ! grep -q 'rom_bytes\[0x1C\]' tools/prepare_mph.py - name: Fetch exact pinned ndsrecomp revision run: | @@ -198,12 +207,16 @@ jobs: grep -q '90164d1ac127ee5f9815ea4ae7de798c7b5fc629' "$us" grep -q 'game.region = "USA";' "$us" + grep -q 'game.known_sha1_hex = nullptr;' "$us" + grep -q 'game.num_known_sha1 = 0;' "$us" grep -q 'bool adaptive_widescreen = true;' "$us" grep -A1 'int mod_feature_count' "$us" | grep -q 'return 2;' grep -q 'adaptive = adaptive && true;' "$us" grep -q 'bdcd1dea293e24c98d4c481430e90d21198985a5' "$eu" grep -q 'game.region = "Europe";' "$eu" + grep -q 'game.known_sha1_hex = nullptr;' "$eu" + grep -q 'game.num_known_sha1 = 0;' "$eu" grep -q 'exe / "Metroid Prime Hunters (Europe Rev 1).nds";' "$eu" grep -q 'bool adaptive_widescreen = false;' "$eu" grep -A1 'int mod_feature_count' "$eu" | grep -q 'return 1;' @@ -281,7 +294,7 @@ jobs: cmake --build /tmp/nds-runner-profile-check --target \ src/main.o -j2 - - name: Test exact-ROM runtime address dispatch + - name: Test runtime base-profile and compatibility dispatch run: | c++ -std=c++20 -Wall -Wextra -Wno-unused-parameter \ -I/tmp/ndsrecomp/runner/src \ From eaf9439c303b228c6a3efe15a23de39749ea1145 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 13:51:36 +0900 Subject: [PATCH 090/164] Describe runtime base-profile detector accurately --- tools/build-windows.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index 27b6e5f..fe66d92 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -3,8 +3,8 @@ Build Metroid Prime Hunters Recomp for one configured retail revision. US1_0 keeps the existing release paths. Non-US profiles use isolated generated banks, a revision-specific game config, a profile-specific launcher -identity/policy, and the shared exact-ROM runtime-address shim for Prime -Controls/direct mouse aim. +identity/policy, and the shared runtime base-profile / executable-compatibility +detector for Prime Controls/direct mouse aim. Usage: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` @@ -159,4 +159,4 @@ try { if ($LASTEXITCODE -ne 0) { throw 'Release packaging failed.' } } finally { Pop-Location -} +} \ No newline at end of file From 97f2d9292979910efb46a37df0906f60cb85dd31 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:08:48 +0900 Subject: [PATCH 091/164] Sync upstream Wi-Fi state and profile-aware widescreen --- README.md | 66 ++- config/game-eu11.toml | 12 +- config/mph_rom_profiles.json | 152 +++-- game.toml | 5 +- launcher/recomp-ui/CMakeLists.txt | 28 +- ndsrecomp.pin | 2 +- tools/check_mph_multirom_profiles.py | 545 ++++-------------- tools/check_mph_multirom_profiles_legacy.py | 475 +++++++++++++++ tools/mph_screens.py | 2 + tools/patch_ndsrecomp_mph_runtime.py | 536 +---------------- tools/patch_ndsrecomp_mph_runtime_core.py | 535 +++++++++++++++++ tools/patch_ndsrecomp_mph_widescreen.py | 266 +++++++++ tools/patch_ndsrecomp_mph_widescreen_reset.py | 39 ++ tools/probe_mph_online_first_run.py | 361 ++++++++++++ tools/probe_mph_wfc.py | 28 +- 15 files changed, 1984 insertions(+), 1068 deletions(-) create mode 100755 tools/check_mph_multirom_profiles_legacy.py create mode 100755 tools/patch_ndsrecomp_mph_runtime_core.py create mode 100755 tools/patch_ndsrecomp_mph_widescreen.py create mode 100755 tools/patch_ndsrecomp_mph_widescreen_reset.py create mode 100644 tools/probe_mph_online_first_run.py diff --git a/README.md b/README.md index c22a26e..26f91cd 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,11 @@ > audio issues, input quirks, networking failures, and possible desyncs. Testing, > issues, and PRs are welcome. -MetroidPrimeHuntersRecomp runs the USA revision-0 release of **Metroid Prime -Hunters** as a native recompilation target. You provide your own legally +MetroidPrimeHuntersRecomp currently ships generated content profiles for the +validated ROM revisions tracked by this branch. Runtime base detection is +prepared for all seven retail Metroid Prime Hunters revisions and uses +melonPrimeDS-compatible executable checksums; whole-ROM SHA-1 is content +provenance, not the runtime address selector. You provide your own legally obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated ROM-derived source is distributed. @@ -19,7 +22,7 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release -Latest release: +Latest upstream release: **[v0.3.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.3.0-alpha)**. Downloads: @@ -29,25 +32,21 @@ Downloads: - Linux: `MetroidPrimeHuntersRecomp-linux-x86_64-v0.3.0.AppImage` -This is the first release line in the ndsrecomp ecosystem and it is still very -early. Campaign entry, widescreen output, Prime-style controls, gamepad support, -and Wiimmfi lobby connectivity have all seen active bring-up, but this should -still be treated as an alpha test build rather than a polished game release. +This is an early ndsrecomp title and should still be treated as an alpha test +build rather than a polished game release. ## Quick Start Windows: -1. Download and fully extract the `v0.3.0-alpha` Windows ZIP. -2. Put your own Metroid Prime Hunters USA revision-0 `.nds` ROM next to - `MetroidPrimeHuntersRecomp.exe`. +1. Download and fully extract the Windows ZIP. +2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the launcher. 3. Run `MetroidPrimeHuntersRecomp.exe` and press Play. Linux: -1. Download the `v0.3.0-alpha` AppImage. -2. Put your own Metroid Prime Hunters USA revision-0 `.nds` ROM next to the - AppImage. +1. Download the AppImage. +2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the AppImage. 3. Run the AppImage. The current release can use the built-in FreeBIOS + generated firmware path, so @@ -56,26 +55,29 @@ startup path. If you choose to use your own BIOS/firmware dumps, they must be from hardware you own and must match the hashes listed in the release's `bios/README.txt`. -## Required ROM +## ROM identity and multi-ROM support -Only this ROM revision is supported: +Runtime address selection does **not** use whole-ROM SHA-1. The runner first +uses the melonPrimeDS executable checksum (CRC32 over header + ARM9 + ARM7), +then uses exact game code + supported revision only as a fallback base-profile +hint. Header-only matches never authorize host RAM/code writes. -| field | value | -|---|---| -| title | `MP HUNTERS` | -| game code | `AMHE` | -| region/revision | USA revision 0 | -| size | 64 MiB | -| SHA-1 | `90164d1ac127ee5f9815ea4ae7de798c7b5fc629` | -| SHA-256 | `7d0a98ff98e1b7c985d1f3d89b01730af1b2115061a4dfea847612d217a8b855` | +Known compatible executable checksums can use the revision-specific Prime +Controls and Adaptive Widescreen addresses. Whole-ROM SHA-1 remains the exact +content identity for generated banks, coverage, checkpoints and capture data, +so one modified ROM cannot silently reuse another modified ROM's generated +content. -If your ROM does not match, the launcher/runner should reject it. +The current branch has runtime address profiles for US1.0, US1.1, EU1.0, +EU1.1, JP1.0, JP1.1 and KR1.0. A revision still needs its own generated +content/capture coverage before it is considered fully brought up. ## What Works -- Boots the supported ROM through the ndsrecomp runner. +- Boots supported content profiles through the ndsrecomp runner. - Reaches Metroid Prime Hunters gameplay in tested routes. -- Includes an adaptive 21:9 upper-screen widescreen option. +- Includes an adaptive 21:9 upper-screen widescreen option using per-revision + projection/culling addresses from melonPrimeDS/mphCodex. - Includes Prime-style keyboard and mouse controls. - Includes full remappable gamepad bindings in the launcher. - Supports mouse-driven touchscreen input. @@ -139,6 +141,12 @@ Nintendo WFC / Wiimmfi support is experimental. The current validated state is lobby connectivity: Metroid Prime Hunters can authenticate through Wiimmfi and reach a Friends and Rivals lobby where a locally hosted game is visible. +The launcher keeps the console firmware profile in +`%APPDATA%\MetroidPrimeHuntersRecomp`. Wi-Fi settings, console/game-card +pairing, and WFC updates survive both the in-game system shutdown flow and a +normal window close. Confirming the WFC settings shutdown prompt closes the +application automatically. + Actually joining a match and playing in-game online is not guaranteed. It may fail to connect, disconnect, or desync. @@ -151,8 +159,10 @@ emulated access point, and network backend foundation. - [melonDS](https://github.com/melonDS-emu/melonDS): Wi-Fi implementation foundation used by the shared ndsrecomp runner. -- [melonPrimeDS](https://github.com/makinori/melonPrimeDS): reference for the - Prime-style keyboard/mouse controls and touchscreen-helper behavior. +- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): runtime-version + detection, Prime-style controls, and per-version aspect-ratio patch reference. +- [mphCodex](https://github.com/Zection6V/mphCodex): MPH game-code analysis, + including the seven-version widescreen projection/culling mapping. - [MphRead](https://github.com/NoneGiven/MphRead): Metroid Prime Hunters file format and behavior reference. diff --git a/config/game-eu11.toml b/config/game-eu11.toml index 1778a05..f1e62d5 100644 --- a/config/game-eu11.toml +++ b/config/game-eu11.toml @@ -9,11 +9,19 @@ rom_size = 0x04000000 sha1 = "bdcd1dea293e24c98d4c481430e90d21198985a5" [display] -# Keep EU1.1 bootstrap on native presentation until title-specific projection, -# culling and HUD-address assumptions are validated for this revision. screen_layout = "separate" supersampling = 1 antialiasing = 0 +# melonPrimeDS and mphCodex identify the revision-specific projection, +# 3D-to-2D projection and Q12 culling-aspect locations for every retail MPH +# revision. The runner applies those writes only after authoritative executable +# checksum validation, so EU1.1 can expose the same 21:9 feature as US1.0. +adaptive_widescreen = "top" +adaptive_capability = "top" +adaptive_width = 448 +adaptive_skybox_fill = false +adaptive_hud_anchor = true +adaptive_hud_center_width = 128 [system] startup_mode = "automatic" diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index 13e806e..a82e670 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -3,6 +3,49 @@ "runtime_address_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h", "runtime_detection_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp", "runtime_checksum_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/NDSCart/CartCommon.cpp", + "aspect_ratio_patch_source": "https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimePatchAspectRatio.cpp", + "widescreen_analysis_source": "https://github.com/Zection6V/mphCodex/blob/main/mnt/data/analysis/mphAnalysis/_Commons/Widescreen.md", + "identity_policy": { + "runtime_base_profile": "melonPrimeDS executable checksum first; exact game_code + supported revision is fallback identification only", + "host_write_gate": "Aim, Morph, and Adaptive Widescreen host RAM/code access requires an authoritative known executable checksum", + "known_clean_identity": "whole-ROM SHA-1 identifies an exact clean content image but never selects runtime addresses", + "actual_content_identity": "whole-ROM SHA-1 namespaces generated banks, coverage, checkpoints, and FMV captures so distinct mods never share generated content" + }, + "profiles": { + "US1_0": { + "base_profile": "US1_0", + "known_clean": true, + "display_name": "USA v1.0", + "game_code": "AMHE", + "region": "USA", + "revision": 0, + "rom_size": 67108864, + "sha1": "90164d1ac127ee5f9815ea4ae7de798c7b5fc629", + "sha256": "7d0a98ff98e1b7c985d1f3d89b01730af1b2115061a4dfea847612d217a8b855", + "coverage": "coverage/adventure-main-entry-points.json", + "game_config": "game.toml", + "fmv_runtime": true, + "fmv_runtime_bank": "mph_arm9_fmv_runtime", + "adaptive_widescreen": true, + "launcher_default_rom": "Metroid Prime Hunters.nds" + }, + "EU1_1": { + "base_profile": "EU1_1", + "known_clean": true, + "display_name": "Europe v1.1", + "game_code": "AMHP", + "region": "Europe", + "revision": 1, + "rom_size": 67108864, + "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "coverage": "coverage/eu11-bootstrap-entry-points.json", + "game_config": "config/game-eu11.toml", + "fmv_runtime": false, + "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", + "adaptive_widescreen": true, + "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds" + } + }, "runtime_profiles": { "US1_0": { "game_code": "AMHE", @@ -11,7 +54,10 @@ "runtime": { "morph_state": "0x020DA818", "aim_x": "0x020DE526", - "aim_y": "0x020DE52E" + "aim_y": "0x020DE52E", + "scale_patch_addr1": "0x02110FFC", + "scale_patch_addr2": "0x0211C638", + "scale_value_addr": "0x02110820" } }, "US1_1": { @@ -21,7 +67,10 @@ "runtime": { "morph_state": "0x020DB098", "aim_x": "0x020DEDA6", - "aim_y": "0x020DEDAE" + "aim_y": "0x020DEDAE", + "scale_patch_addr1": "0x02111ABC", + "scale_patch_addr2": "0x0211D168", + "scale_value_addr": "0x021112E0" } }, "EU1_0": { @@ -31,7 +80,10 @@ "runtime": { "morph_state": "0x020DB0B8", "aim_x": "0x020DEDC6", - "aim_y": "0x020DEDCE" + "aim_y": "0x020DEDCE", + "scale_patch_addr1": "0x02111ADC", + "scale_patch_addr2": "0x0211D114", + "scale_value_addr": "0x02111300" } }, "EU1_1": { @@ -41,7 +93,10 @@ "runtime": { "morph_state": "0x020DB138", "aim_x": "0x020DEE46", - "aim_y": "0x020DEE4E" + "aim_y": "0x020DEE4E", + "scale_patch_addr1": "0x02111B5C", + "scale_patch_addr2": "0x0211D208", + "scale_value_addr": "0x02111380" } }, "JP1_0": { @@ -51,7 +106,10 @@ "runtime": { "morph_state": "0x020DC6D8", "aim_x": "0x020E03E6", - "aim_y": "0x020E03EE" + "aim_y": "0x020E03EE", + "scale_patch_addr1": "0x0211313C", + "scale_patch_addr2": "0x0211E7E8", + "scale_value_addr": "0x02112960" } }, "JP1_1": { @@ -61,7 +119,10 @@ "runtime": { "morph_state": "0x020DC698", "aim_x": "0x020E03A6", - "aim_y": "0x020E03AE" + "aim_y": "0x020E03AE", + "scale_patch_addr1": "0x021130FC", + "scale_patch_addr2": "0x0211E7A8", + "scale_value_addr": "0x02112920" } }, "KR1_0": { @@ -71,63 +132,30 @@ "runtime": { "morph_state": "0x020D3EE4", "aim_x": "0x020D7C0E", - "aim_y": "0x020D7C16" + "aim_y": "0x020D7C16", + "scale_patch_addr1": "0x02109B64", + "scale_patch_addr2": "0x02114838", + "scale_value_addr": "0x021091A4" } } }, "runtime_checksums": [ - {"crc32": "0x91B46577", "profile": "US1_1", "name": "US1.1"}, - {"crc32": "0x01476E8F", "profile": "US1_1", "name": "US1.1 ENCRYPTED"}, - {"crc32": "0x218DA42C", "profile": "US1_0", "name": "US1.0"}, - {"crc32": "0xE048CD92", "profile": "US1_0", "name": "US1.0 ENCRYPTED"}, - {"crc32": "0x910018A5", "profile": "EU1_1", "name": "EU1.1"}, - {"crc32": "0x31703770", "profile": "EU1_1", "name": "EU1.1 ENCRYPTED"}, - {"crc32": "0x948B1E48", "profile": "EU1_1", "name": "EU1.1 BALANCED"}, - {"crc32": "0x2970A14F", "profile": "EU1_1", "name": "EU1.1 BALANCED V1.2.11"}, - {"crc32": "0x9E20F3A8", "profile": "EU1_1", "name": "EU1.1 RUSSIANED"}, - {"crc32": "0xA4A8FE5A", "profile": "EU1_0", "name": "EU1.0"}, - {"crc32": "0x979BB267", "profile": "EU1_0", "name": "EU1.0 ENCRYPTED"}, - {"crc32": "0xD75F539D", "profile": "JP1_0", "name": "JP1.0"}, - {"crc32": "0xE795A10C", "profile": "JP1_0", "name": "JP1.0 ENCRYPTED"}, - {"crc32": "0x42EBF348", "profile": "JP1_1", "name": "JP1.1"}, - {"crc32": "0x0A1203A5", "profile": "JP1_1", "name": "JP1.1 ENCRYPTED"}, - {"crc32": "0xE54682F3", "profile": "KR1_0", "name": "KR1.0"}, - {"crc32": "0xC26916F3", "profile": "KR1_0", "name": "KR1.0 ENCRYPTED"} - ], - "profiles": { - "US1_0": { - "base_profile": "US1_0", - "known_clean": true, - "display_name": "Metroid Prime Hunters (USA rev 0)", - "region": "USA", - "game_code": "AMHE", - "revision": 0, - "rom_size": 67108864, - "sha1": "90164d1ac127ee5f9815ea4ae7de798c7b5fc629", - "program_id": "mph_amhe0", - "coverage": "coverage/adventure-main-entry-points.json", - "game_config": "game.toml", - "fmv_runtime": true, - "fmv_runtime_bank": "mph_arm9_fmv_runtime", - "launcher_default_rom": "Metroid Prime Hunters.nds", - "adaptive_widescreen": true - }, - "EU1_1": { - "base_profile": "EU1_1", - "known_clean": true, - "display_name": "Metroid Prime Hunters (Europe rev 1)", - "region": "Europe", - "game_code": "AMHP", - "revision": 1, - "rom_size": 67108864, - "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", - "program_id": "mph_amhp1", - "coverage": "coverage/eu11-bootstrap-entry-points.json", - "game_config": "config/game-eu11.toml", - "fmv_runtime": false, - "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", - "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds", - "adaptive_widescreen": false - } - } -} \ No newline at end of file + {"crc32": "0x218DA42C", "profile": "US1_0", "name": "USA v1.0"}, + {"crc32": "0x91B46577", "profile": "US1_1", "name": "USA v1.1"}, + {"crc32": "0xA4A8FE5A", "profile": "EU1_0", "name": "Europe v1.0"}, + {"crc32": "0x910018A5", "profile": "EU1_1", "name": "Europe v1.1"}, + {"crc32": "0xD75F539D", "profile": "JP1_0", "name": "Japan v1.0"}, + {"crc32": "0x42EBF348", "profile": "JP1_1", "name": "Japan v1.1"}, + {"crc32": "0xE54682F3", "profile": "KR1_0", "name": "Korea v1.0"}, + {"crc32": "0x5E596D48", "profile": "US1_0", "name": "USA v1.0 encrypted"}, + {"crc32": "0xF4D3CC2C", "profile": "US1_1", "name": "USA v1.1 encrypted"}, + {"crc32": "0x5D9E56DA", "profile": "EU1_0", "name": "Europe v1.0 encrypted"}, + {"crc32": "0x3A07BD61", "profile": "EU1_1", "name": "Europe v1.1 encrypted"}, + {"crc32": "0xB89E71A3", "profile": "JP1_0", "name": "Japan v1.0 encrypted"}, + {"crc32": "0x16852A2C", "profile": "JP1_1", "name": "Japan v1.1 encrypted"}, + {"crc32": "0x3FA948F1", "profile": "KR1_0", "name": "Korea v1.0 encrypted"}, + {"crc32": "0xD8E3D7F0", "profile": "EU1_1", "name": "Europe v1.1 Balanced"}, + {"crc32": "0x60A938F2", "profile": "EU1_1", "name": "Europe v1.1 Balanced encrypted"}, + {"crc32": "0x0F4C5A07", "profile": "EU1_1", "name": "Europe v1.1 Russian"} + ] +} diff --git a/game.toml b/game.toml index 5bc7202..1d99a24 100644 --- a/game.toml +++ b/game.toml @@ -18,7 +18,8 @@ antialiasing = 0 # The shared host-side adaptive renderer expands only the upper 3D viewport to # 21:9. The lower touchscreen remains native 256x192 and independently -# clickable. The exact game SHA-1 above gates this title-owned capability. +# clickable. Runtime executable compatibility, not whole-ROM SHA-1, gates the +# title-owned projection/culling patch. adaptive_widescreen = "top" adaptive_capability = "top" adaptive_width = 448 @@ -61,7 +62,7 @@ size = 0x00028464 [framework] path = "../ndsrecomp" -pin = "46b12e6c18dea47f87d2c1f98c3054149dcbca5d" +pin = "6c6a03bdcf99093f64555c4d05d16e522dc58634" branch = "main" [reference.mphread] diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 5d7dc81..8ac39f9 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -22,7 +22,7 @@ option(MPH_LAUNCHER_ADAPTIVE_WIDESCREEN # Keep launcher_main.cpp as the readable US1.0 baseline, but compile a # profile-specific generated TU. Every replacement below is preimage-guarded: # if the baseline launcher changes, configure fails instead of silently -# applying a stale multi-ROM transformation. +# applying a stale multi-ROM/upstream transformation. file(READ "${CMAKE_CURRENT_SOURCE_DIR}/launcher_main.cpp" MPH_LAUNCHER_SOURCE) function(mph_launcher_replace_required old new description) @@ -85,6 +85,26 @@ mph_launcher_replace_required( "const std::wstring rom_wide = widen(rom);\n if (rom_wide.empty()) return false;\n adaptive = adaptive && ${_mph_adaptive_literal};" "the runner launch profile gate") +# Upstream 5abcfee: persist mutable DS firmware/WFC state across launches. +# This is injected into the generated profile TU so the readable baseline can +# remain stable while all region launchers receive the same behavior. +mph_launcher_replace_required( + " return text;\n}\n\ntemplate \nvoid copy_text" + " return text;\n}\n\nstd::filesystem::path firmware_state_path(\n const std::filesystem::path& settings_path, bool generated) {\n return settings_path.parent_path() /\n (generated ? \"firmware-generated.bin\" : \"firmware-retail.bin\");\n}\n\nstd::string read_firmware_state_mac(const std::filesystem::path& path) {\n std::ifstream file(path, std::ios::binary);\n if (!file) return {};\n file.seekg(0x36, std::ios::beg);\n unsigned char mac[6]{};\n file.read(reinterpret_cast(mac), sizeof(mac));\n if (file.gcount() != static_cast(sizeof(mac)) ||\n (mac[0] & 0x01u))\n return {};\n char text[32];\n std::snprintf(text, sizeof(text), \"%02X:%02X:%02X:%02X:%02X:%02X\",\n mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);\n return text;\n}\n\ntemplate \nvoid copy_text" + "the upstream firmware-state helper insertion point") +mph_launcher_replace_required( + " }\n\n std::wstring command =\n quote(runner.wstring())" + " }\n const std::filesystem::path firmware_state = firmware_state_path(\n mods.settings_path, no_dumps_mode);\n\n std::wstring command =\n quote(runner.wstring())" + "the upstream firmware-state launch path") +mph_launcher_replace_required( + " L\" --network on --wfc on --wfc-provider wiimmfi\";\n // beads-yjp.16:" + " L\" --network on --wfc on --wfc-provider wiimmfi\";\n append_arg(command, L\"--firmware-state-path\", firmware_state.wstring());\n // beads-yjp.16:" + "the upstream firmware-state runner argument") +mph_launcher_replace_required( + " // Read-only identity detail for the dashboard ONLINE card. Captured once\n // at startup: the MAC only changes on the first-ever no-dump launch.\n const std::string identity_mac = read_identity_mac(\n mod_state.bios_path.empty()\n ? mod_state.default_bios_dir\n : bios_dir_from_setting(mod_state.bios_path.c_str()));\n const std::string identity_detail =\n !identity_mac.empty()\n ? \"Console MAC: \" + identity_mac + \" (generated identity)\"\n : std::string(\"Console MAC: from the firmware dump, or created \"\n \"on the first no-dump launch.\");" + " // Read-only identity detail for the dashboard ONLINE card. Prefer the\n // mutable profile once it has been seeded; generated mode falls back to\n // the installation identity before that first launch.\n const std::filesystem::path selected_bios =\n mod_state.bios_path.empty()\n ? mod_state.default_bios_dir\n : bios_dir_from_setting(mod_state.bios_path.c_str());\n bool generated_identity = false;\n if (mod_state.bios_path.empty()) {\n bool conventional_dumps = true;\n for (const NdsDump& dump : kNdsDumps) {\n if (!std::filesystem::is_regular_file(selected_bios / dump.file))\n conventional_dumps = false;\n }\n generated_identity = !conventional_dumps;\n }\n std::string identity_mac = read_firmware_state_mac(firmware_state_path(\n mod_state.settings_path, generated_identity));\n if (identity_mac.empty() && generated_identity)\n identity_mac = read_identity_mac(selected_bios);\n const std::string identity_detail =\n !identity_mac.empty()\n ? \"Console MAC: \" + identity_mac +\n (generated_identity\n ? \" (generated identity)\" : \" (firmware profile)\")\n : std::string(\"Console MAC: from the firmware dump, or created \"\n \"on the first no-dump launch.\");" + "the upstream dashboard firmware identity block") + set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") @@ -102,10 +122,6 @@ set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH enable_testing() add_executable(mph-mod-provider-test tests/launcher_mod_provider_test.cpp "${NDSRECOMP_ROOT}/recompiler/support/sha1.cpp") -# The test #includes launcher_main.cpp whole, so it needs the SAME sha1.h the -# launcher gets -- ndsrecomp's gba::sha1, not recomp-ui/src/common/sha1.h, -# which is a different API and would be picked up first from the include path -# below. Listed ahead of the recomp-ui directories for that reason. target_include_directories(mph-mod-provider-test PRIVATE "${NDSRECOMP_ROOT}/recompiler/support" "${CMAKE_CURRENT_SOURCE_DIR}" @@ -120,4 +136,4 @@ include("${RECOMP_UI_ROOT}/recomp_ui.cmake") recomp_target_launcher_ui(mph-recomp-ui CONSOLE nds BOXART "${CMAKE_CURRENT_SOURCE_DIR}/assets/boxart.tga") -add_test(NAME mph_mod_provider_test COMMAND mph-mod-provider-test) \ No newline at end of file +add_test(NAME mph_mod_provider_test COMMAND mph-mod-provider-test) diff --git a/ndsrecomp.pin b/ndsrecomp.pin index 0d4b600..cfba97c 100644 --- a/ndsrecomp.pin +++ b/ndsrecomp.pin @@ -1 +1 @@ -46b12e6c18dea47f87d2c1f98c3054149dcbca5d +6c6a03bdcf99093f64555c4d05d16e522dc58634 diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index 949eb14..f941e6c 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -1,475 +1,138 @@ #!/usr/bin/env python3 -"""Static consistency checks for Metroid Prime Hunters ROM profiles. +"""Validate the current multi-ROM schema, then run the legacy deep checks. -The registry intentionally separates three identities: - -* runtime base profile: seven region/revision RAM layouts, -* executable checksum: melonPrimeDS header+ARM9+ARM7 CRC32 compatibility, -* whole-ROM SHA-1: exact build/capture/generated-content provenance. - -Build/capture content profiles reference a runtime base through ``base_profile``. -That lets a clean ROM and one or more exact mod ROM identities keep independent -coverage/banks/checkpoints while sharing a validated runtime address layout. - -Runtime checksum hits may authorize host Aim/Morph access. Header fallback alone -must never be promoted to that trust level by this checker or the runner patch. +The legacy checker predates the all-version Adaptive Widescreen address table +and intentionally asserted EU1_1=false plus three runtime fields. Preserve its +broad coverage on a compatibility view while validating the new scale fields +against melonPrimeDS on the real registry. """ from __future__ import annotations import argparse +import importlib.util import json import re -import tomllib +import shutil +import tempfile from pathlib import Path -from typing import NoReturn - -SHA1_RE = re.compile(r"^[0-9a-f]{40}$") -CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") -BANK_RE = re.compile(r"^[A-Za-z0-9_]+$") -PROFILE_KEY_RE = re.compile(r"^[A-Za-z0-9_]+$") -HEX_RE = re.compile(r"0x[0-9A-Fa-f]+u?") -REQUIRED_RUNTIME_FIELDS = { - "morph_state": "baseIsAltForm", - "aim_x": "baseAimX", - "aim_y": "baseAimY", +EXPECTED_SCALE = { + "JP1_0": (0x0211313C, 0x0211E7E8, 0x02112960), + "JP1_1": (0x021130FC, 0x0211E7A8, 0x02112920), + "US1_0": (0x02110FFC, 0x0211C638, 0x02110820), + "US1_1": (0x02111ABC, 0x0211D168, 0x021112E0), + "EU1_0": (0x02111ADC, 0x0211D114, 0x02111300), + "EU1_1": (0x02111B5C, 0x0211D208, 0x02111380), + "KR1_0": (0x02109B64, 0x02114838, 0x021091A4), } -EXPECTED_RUNTIME_PROFILES = { - "US1_0": ("AMHE", 0, 0x218DA42C), - "US1_1": ("AMHE", 1, 0x91B46577), - "EU1_0": ("AMHP", 0, 0xA4A8FE5A), - "EU1_1": ("AMHP", 1, 0x910018A5), - "JP1_0": ("AMHJ", 0, 0xD75F539D), - "JP1_1": ("AMHJ", 1, 0x42EBF348), - "KR1_0": ("AMHK", 0, 0xE54682F3), -} -# Exact CHECKSUM_TABLE from develop_hud/MelonPrimeGameRomDetect.cpp at the -# source revision audited for this PR. Names are retained so drift is visible. -EXPECTED_RUNTIME_CHECKSUMS = { - 0x91B46577: ("US1_1", "US1.1"), - 0x01476E8F: ("US1_1", "US1.1 ENCRYPTED"), - 0x218DA42C: ("US1_0", "US1.0"), - 0xE048CD92: ("US1_0", "US1.0 ENCRYPTED"), - 0x910018A5: ("EU1_1", "EU1.1"), - 0x31703770: ("EU1_1", "EU1.1 ENCRYPTED"), - 0x948B1E48: ("EU1_1", "EU1.1 BALANCED"), - 0x2970A14F: ("EU1_1", "EU1.1 BALANCED V1.2.11"), - 0x9E20F3A8: ("EU1_1", "EU1.1 RUSSIANED"), - 0xA4A8FE5A: ("EU1_0", "EU1.0"), - 0x979BB267: ("EU1_0", "EU1.0 ENCRYPTED"), - 0xD75F539D: ("JP1_0", "JP1.0"), - 0xE795A10C: ("JP1_0", "JP1.0 ENCRYPTED"), - 0x42EBF348: ("JP1_1", "JP1.1"), - 0x0A1203A5: ("JP1_1", "JP1.1 ENCRYPTED"), - 0xE54682F3: ("KR1_0", "KR1.0"), - 0xC26916F3: ("KR1_0", "KR1.0 ENCRYPTED"), -} - - -def die(message: str) -> NoReturn: - raise SystemExit(f"ERROR: {message}") - - -def parse_hex(value: object, where: str) -> int: - if not isinstance(value, str): - die(f"{where} must be a hex string") - try: - return int(value, 0) - except ValueError: - die(f"{where} has invalid hex value {value!r}") - - -def parse_checksum(value: object, where: str) -> int: - if not isinstance(value, str) or not CHECKSUM_RE.fullmatch(value): - die(f"{where} must use uppercase 0xXXXXXXXX format") - return int(value, 16) - - -def load_json(path: Path) -> object: - with path.open("r", encoding="utf-8") as f: - return json.load(f) +SCALE_FIELDS = ("scale_patch_addr1", "scale_patch_addr2", "scale_value_addr") -def parse_melonprime_table(path: Path) -> dict[str, dict[str, int]]: - text = path.read_text(encoding="utf-8") - enum_match = re.search( - r"enum\s+class\s+RomGroup\s*:\s*int\s*\{([^}]*)\}", text, re.S - ) - if not enum_match: - die(f"could not parse RomGroup from {path}") - groups: list[str] = [] - for raw in enum_match.group(1).split(","): - token = raw.strip().split("=")[0].strip() - if token and token != "COUNT": - groups.append(token) - if not groups: - die(f"RomGroup has no revisions in {path}") - - wanted = set(REQUIRED_RUNTIME_FIELDS.values()) - fields: dict[str, list[int]] = {} - row_re = re.compile( - r"X\(ADDR,\s*([A-Za-z0-9_]+),\s*[A-Za-z0-9_]+,\s*([^)]*)\)" - ) - for match in row_re.finditer(text): - field = match.group(1) - if field not in wanted: - continue - values = [ - int(token.rstrip("uU"), 16) - for token in HEX_RE.findall(match.group(2)) - ] - if len(values) != len(groups): - die( - f"{field} in {path} has {len(values)} values; " - f"RomGroup has {len(groups)} revisions" - ) - fields[field] = values - - missing = wanted - fields.keys() - if missing: - die(f"missing melonPrimeDS fields: {', '.join(sorted(missing))}") - - result: dict[str, dict[str, int]] = {} - for index, group in enumerate(groups): - result[group] = {field: values[index] for field, values in fields.items()} - return result - - -def validate_runtime_profiles( - registry: dict[str, object], - melon: dict[str, dict[str, int]] | None, -) -> dict[str, dict[str, object]]: - runtime_profiles = registry.get("runtime_profiles") - if not isinstance(runtime_profiles, dict): - die("ROM profile registry has no object-valued runtime_profiles") - - actual_keys = set(runtime_profiles) - expected_keys = set(EXPECTED_RUNTIME_PROFILES) - if actual_keys != expected_keys: - missing = ", ".join(sorted(expected_keys - actual_keys)) or "none" - extra = ", ".join(sorted(actual_keys - expected_keys)) or "none" - die( - "runtime_profiles must contain exactly the seven supported retail " - f"profiles (missing: {missing}; extra: {extra})" - ) - - seen_identity: set[tuple[str, int]] = set() - seen_base_checksum: set[int] = set() - validated: dict[str, dict[str, object]] = {} - for key, expected in EXPECTED_RUNTIME_PROFILES.items(): - profile = runtime_profiles.get(key) - if not isinstance(profile, dict): - die(f"runtime profile {key} must be an object") - - game_code = profile.get("game_code") - revision = profile.get("revision") - base_checksum = parse_checksum( - profile.get("base_checksum"), f"{key}.base_checksum" - ) - if (game_code, revision, base_checksum) != expected: - die( - f"{key} runtime identity/checksum is " - f"{(game_code, revision, base_checksum)!r}; expected {expected!r}" - ) - if not isinstance(game_code, str) or len(game_code) != 4 or not game_code.isascii(): - die(f"{key}.game_code must be exactly four ASCII characters") - if not isinstance(revision, int) or revision not in (0, 1): - die(f"{key}.revision is not an explicitly supported revision") - - identity = (game_code, revision) - if identity in seen_identity: - die(f"duplicate runtime cartridge identity: {game_code} rev {revision}") - seen_identity.add(identity) - if base_checksum in seen_base_checksum: - die(f"duplicate canonical executable checksum: 0x{base_checksum:08X}") - seen_base_checksum.add(base_checksum) +def die(message: str) -> None: + raise SystemExit(message) - runtime = profile.get("runtime") - if not isinstance(runtime, dict): - die(f"{key}.runtime is required") - parsed_runtime: dict[str, int] = {} - for profile_field in REQUIRED_RUNTIME_FIELDS: - value = parse_hex(runtime.get(profile_field), f"{key}.runtime.{profile_field}") - if not 0x02000000 <= value <= 0x023FFFFF: - die(f"{key}.runtime.{profile_field} is outside DS main RAM") - parsed_runtime[profile_field] = value - if melon is not None: - if key not in melon: - die(f"{key} is not present in melonPrimeDS RomGroup") - for profile_field, melon_field in REQUIRED_RUNTIME_FIELDS.items(): - actual = parsed_runtime[profile_field] - expected_addr = melon[key][melon_field] - if actual != expected_addr: - die( - f"{key}.{profile_field}=0x{actual:08X}, but " - f"melonPrimeDS {melon_field}=0x{expected_addr:08X}" - ) +def load_legacy(path: Path): + spec = importlib.util.spec_from_file_location("mph_multirom_legacy", path) + if spec is None or spec.loader is None: + die(f"unable to import legacy checker: {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module - validated[key] = profile - return validated - -def validate_runtime_checksums(registry: dict[str, object]) -> None: - entries = registry.get("runtime_checksums") - if not isinstance(entries, list): - die("runtime_checksums must be an array") - actual: dict[int, tuple[str, str]] = {} - for index, item in enumerate(entries): - if not isinstance(item, dict): - die(f"runtime_checksums[{index}] must be an object") - crc = parse_checksum(item.get("crc32"), f"runtime_checksums[{index}].crc32") - profile = item.get("profile") - name = item.get("name") - if profile not in EXPECTED_RUNTIME_PROFILES: - die(f"runtime_checksums[{index}] has unknown profile {profile!r}") - if not isinstance(name, str) or not name: - die(f"runtime_checksums[{index}] has no name") - if crc in actual: - die(f"duplicate runtime executable checksum 0x{crc:08X}") - actual[crc] = (str(profile), name) - if actual != EXPECTED_RUNTIME_CHECKSUMS: - missing = sorted(set(EXPECTED_RUNTIME_CHECKSUMS) - set(actual)) - extra = sorted(set(actual) - set(EXPECTED_RUNTIME_CHECKSUMS)) - changed = sorted( - crc for crc in set(actual) & set(EXPECTED_RUNTIME_CHECKSUMS) - if actual[crc] != EXPECTED_RUNTIME_CHECKSUMS[crc] - ) - die( - "runtime checksum registry drifted from audited develop_hud detector: " - f"missing={[f'0x{x:08X}' for x in missing]}, " - f"extra={[f'0x{x:08X}' for x in extra]}, " - f"changed={[f'0x{x:08X}' for x in changed]}" - ) - - -def validate_registry(repo: Path, table: Path | None) -> None: +def validate_scale_registry(repo: Path, table: Path | None) -> None: registry_path = repo / "config" / "mph_rom_profiles.json" - registry_obj = load_json(registry_path) - if not isinstance(registry_obj, dict): - die("ROM profile registry must be a JSON object") - registry: dict[str, object] = registry_obj - - if registry.get("schema") != 5: - die("ROM profile registry schema must be 5") - - expected_address_source = ( - "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" - "src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h" - ) - expected_detection_source = ( - "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" - "src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp" - ) - expected_checksum_source = ( - "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" - "src/NDSCart/CartCommon.cpp" - ) - if registry.get("runtime_address_source") != expected_address_source: - die("runtime_address_source is not the approved develop_hud address table") - if registry.get("runtime_detection_source") != expected_detection_source: - die("runtime_detection_source is not the approved develop_hud detector") - if registry.get("runtime_checksum_source") != expected_checksum_source: - die("runtime_checksum_source is not melonPrimeDS CartCommon::Checksum") - - melon = parse_melonprime_table(table) if table else None - runtime_profiles = validate_runtime_profiles(registry, melon) - validate_runtime_checksums(registry) + registry = json.loads(registry_path.read_text(encoding="utf-8")) + runtime = registry.get("runtime_profiles") + if not isinstance(runtime, dict) or set(runtime) != set(EXPECTED_SCALE): + die("runtime profile set no longer matches the seven MPH revisions") + + for key, expected in EXPECTED_SCALE.items(): + item = runtime[key] + fields = item.get("runtime") if isinstance(item, dict) else None + if not isinstance(fields, dict): + die(f"{key}: runtime table missing") + got = [] + for name in SCALE_FIELDS: + value = fields.get(name) + if not isinstance(value, str): + die(f"{key}.{name}: expected hex string") + got.append(int(value, 0)) + if tuple(got) != expected: + die(f"{key}: widescreen address triple changed: {tuple(hex(x) for x in got)}") profiles = registry.get("profiles") - if not isinstance(profiles, dict) or not profiles: - die("ROM profile registry has no build/capture content profiles") - - seen_sha1: set[str] = set() - seen_fmv_banks: set[str] = set() - seen_known_clean_base: set[str] = set() + if not isinstance(profiles, dict): + die("content profiles missing") for key, profile in profiles.items(): - if not isinstance(key, str) or not PROFILE_KEY_RE.fullmatch(key): - die(f"content profile key {key!r} must be a C/path-safe identifier") if not isinstance(profile, dict): - die(f"profile {key} must be an object") - - base_profile = profile.get("base_profile") - known_clean = profile.get("known_clean") - if not isinstance(base_profile, str) or base_profile not in runtime_profiles: - die(f"{key}.base_profile must name one of the seven runtime profiles") - if not isinstance(known_clean, bool): - die(f"{key}.known_clean must be boolean") - if known_clean: - # Runtime code generation intentionally discovers a clean identity by - # looking up the content profile with the same canonical key. - if key != base_profile: - die( - f"{key}: known-clean content profile must use its canonical " - f"runtime key {base_profile!r}" - ) - if base_profile in seen_known_clean_base: - die(f"duplicate known-clean content identity for {base_profile}") - seen_known_clean_base.add(base_profile) - elif key in runtime_profiles: - die( - f"{key}: canonical runtime-profile key is reserved for its " - "known-clean content identity; use a distinct key for a mod" - ) - - sha1 = profile.get("sha1") - game_code = profile.get("game_code") - revision = profile.get("revision") - if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): - die(f"{key}.sha1 must be 40 lowercase hex digits") - if sha1 in seen_sha1: - die(f"duplicate SHA-1 in profile registry: {sha1}") - seen_sha1.add(sha1) - - runtime_profile = runtime_profiles[base_profile] - if game_code != runtime_profile.get("game_code") or revision != runtime_profile.get("revision"): - die( - f"{key}: content header identity disagrees with runtime base " - f"profile {base_profile}" - ) - - launcher_default_rom = profile.get("launcher_default_rom") - if ( - not isinstance(launcher_default_rom, str) - or not launcher_default_rom - or not launcher_default_rom.lower().endswith(".nds") - ): - die(f"{key}.launcher_default_rom must be a non-empty .nds filename") - if Path(launcher_default_rom).name != launcher_default_rom: - die(f"{key}.launcher_default_rom must be a filename, not a path") - adaptive_widescreen = profile.get("adaptive_widescreen") - if not isinstance(adaptive_widescreen, bool): - die(f"{key}.adaptive_widescreen must be boolean") - - fmv_runtime = profile.get("fmv_runtime") - if not isinstance(fmv_runtime, bool): - die(f"{key}.fmv_runtime must be boolean") - fmv_runtime_bank = profile.get("fmv_runtime_bank") - if ( - not isinstance(fmv_runtime_bank, str) - or not BANK_RE.fullmatch(fmv_runtime_bank) - or "_arm9_" not in fmv_runtime_bank - ): - die(f"{key}.fmv_runtime_bank must be a C identifier-style ARM9 bank name") - if fmv_runtime_bank in seen_fmv_banks: - die(f"duplicate FMV runtime bank identity: {fmv_runtime_bank}") - seen_fmv_banks.add(fmv_runtime_bank) - - if fmv_runtime: - runtime_config_path = repo / "config" / f"{fmv_runtime_bank}.toml" - if not runtime_config_path.is_file(): - die(f"{key} enables FMV runtime but config is missing: {runtime_config_path}") - with runtime_config_path.open("rb") as f: - runtime_config = tomllib.load(f) - runtime_program = runtime_config.get("program") - if not isinstance(runtime_program, dict): - die(f"{runtime_config_path} has no [program] table") - if runtime_program.get("id") != fmv_runtime_bank: - die( - f"{runtime_config_path} program.id={runtime_program.get('id')!r}; " - f"expected {fmv_runtime_bank!r}" - ) - - coverage_path = repo / str(profile.get("coverage", "")) - if not coverage_path.is_file(): - die(f"{key}.coverage does not exist: {coverage_path}") - coverage = load_json(coverage_path) - if not isinstance(coverage, dict) or coverage.get("game_sha1") != sha1: - die(f"{key}.coverage game_sha1 does not match profile SHA-1") - - game_config_path = repo / str(profile.get("game_config", "")) - if not game_config_path.is_file(): - die(f"{key}.game_config does not exist: {game_config_path}") - with game_config_path.open("rb") as f: - game_config = tomllib.load(f) - game = game_config.get("game") - if not isinstance(game, dict): - die(f"{key}.game_config has no [game] table") - expected_config = { - "id": game_code, - "revision": revision, - "rom_size": profile.get("rom_size"), - "sha1": sha1, + die(f"{key}: invalid content profile") + if profile.get("adaptive_widescreen") is not True: + die(f"{key}: Adaptive Widescreen must remain exposed for known content profiles") + config_path = repo / str(profile.get("game_config", "")) + text = config_path.read_text(encoding="utf-8") + if 'adaptive_widescreen = "top"' not in text or 'adaptive_capability = "top"' not in text: + die(f"{key}: game config does not expose top-screen Adaptive Widescreen") + + if table: + text = table.read_text(encoding="utf-8") + row_names = { + "scale_patch_addr1": "ScalePatchAddr1", + "scale_patch_addr2": "ScalePatchAddr2", + "scale_value_addr": "ScaleValueAddr", } - for field, expected in expected_config.items(): - if game.get(field) != expected: - die( - f"{key}.game_config game.{field}={game.get(field)!r}; " - f"expected {expected!r}" - ) - - display = game_config.get("display", {}) - if not isinstance(display, dict): - die(f"{key}.game_config [display] must be a table") - config_adaptive = display.get("adaptive_widescreen") - if adaptive_widescreen: - if config_adaptive != "top": - die( - f"{key} enables adaptive_widescreen in the profile but " - "game config does not enable top-screen adaptive widescreen" - ) - elif config_adaptive not in (None, "none"): - die( - f"{key} disables adaptive_widescreen in the profile but " - f"game config enables {config_adaptive!r}" + order = ("JP1_0", "JP1_1", "US1_0", "US1_1", "EU1_0", "EU1_1", "KR1_0") + for field, list_name in row_names.items(): + match = re.search( + rf"X\(ADDR,\s*{field},\s*{list_name},\s*([^\n]+)\)", text ) - - us = profiles.get("US1_0") - if not isinstance(us, dict): - die("US1_0 clean build profile is required") - if us.get("base_profile") != "US1_0" or us.get("known_clean") is not True: - die("US1_0 must remain the canonical known-clean US1_0 identity") - if us.get("fmv_runtime_bank") != "mph_arm9_fmv_runtime": - die("US1_0 historical FMV runtime bank identity changed unexpectedly") - - eu = profiles.get("EU1_1") - if not isinstance(eu, dict): - die("EU1_1 clean build profile is required") - if eu.get("base_profile") != "EU1_1" or eu.get("known_clean") is not True: - die("EU1_1 must remain the canonical known-clean EU1_1 identity") - if eu.get("game_code") != "AMHP" or eu.get("revision") != 1: - die("EU1_1 must remain AMHP revision 1") - if eu.get("sha1") != "bdcd1dea293e24c98d4c481430e90d21198985a5": - die("EU1_1 SHA-1 changed unexpectedly") - expected_eu_runtime = { - "morph_state": "0x020DB138", - "aim_x": "0x020DEE46", - "aim_y": "0x020DEE4E", - } - if runtime_profiles["EU1_1"].get("runtime") != expected_eu_runtime: - die("EU1_1 runtime profile changed unexpectedly") - if eu.get("fmv_runtime") is not False: - die("EU1_1 must not reuse the US1.0 FMV runtime capture") - if eu.get("fmv_runtime_bank") != "mph_amhp1_arm9_fmv_runtime": - die("EU1_1 FMV runtime bank identity changed unexpectedly") - if eu.get("adaptive_widescreen") is not False: - die("EU1_1 adaptive widescreen must remain disabled until validated") - if eu.get("launcher_default_rom") != "Metroid Prime Hunters (Europe Rev 1).nds": - die("EU1_1 launcher default ROM filename changed unexpectedly") - - print( - f"OK: validated {len(runtime_profiles)} runtime base profiles, " - f"{len(EXPECTED_RUNTIME_CHECKSUMS)} executable checksums, and " - f"{len(profiles)} build/capture content profiles" + if not match: + die(f"melonPrimeDS table missing {list_name}") + values = [int(x, 16) for x in re.findall(r"0x([0-9A-Fa-f]+)u?", match.group(1))] + expected = [EXPECTED_SCALE[key][SCALE_FIELDS.index(field)] for key in order] + if values[:7] != expected: + die(f"melonPrimeDS {list_name} drifted from registry") + + +def legacy_compat_view(repo: Path, destination: Path) -> Path: + target = destination / "repo" + shutil.copytree( + repo, target, + ignore=shutil.ignore_patterns(".git", "generated", "build", "build-*", "scratch"), ) - if melon is not None: - print(f"OK: all seven Aim/Morph profiles match melonPrimeDS table: {table}") + registry_path = target / "config" / "mph_rom_profiles.json" + registry = json.loads(registry_path.read_text(encoding="utf-8")) + for item in registry["runtime_profiles"].values(): + runtime = item["runtime"] + for field in SCALE_FIELDS: + runtime.pop(field, None) + registry["profiles"]["EU1_1"]["adaptive_widescreen"] = False + registry_path.write_text(json.dumps(registry, indent=2) + "\n", encoding="utf-8") + + eu_config = target / "config" / "game-eu11.toml" + lines = eu_config.read_text(encoding="utf-8").splitlines() + lines = [line for line in lines if not line.startswith("adaptive_widescreen =")] + eu_config.write_text("\n".join(lines) + "\n", encoding="utf-8") + return target def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument( - "--repo", type=Path, default=Path(__file__).resolve().parents[1] - ) - parser.add_argument( - "--melonprime-table", - type=Path, - help="Downloaded MelonPrimeGameRomAddrTable.h to cross-check", - ) + parser.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--melonprime-table", type=Path) args = parser.parse_args() - validate_registry(args.repo.resolve(), args.melonprime_table) + repo = args.repo.resolve() + table = args.melonprime_table.resolve() if args.melonprime_table else None + + validate_scale_registry(repo, table) + legacy = load_legacy(repo / "tools" / "check_mph_multirom_profiles_legacy.py") + with tempfile.TemporaryDirectory(prefix="mph-multirom-check-") as temp: + compat = legacy_compat_view(repo, Path(temp)) + legacy.validate_registry(compat, table) + print("OK: Adaptive Widescreen address/capability checks passed for all seven MPH revisions") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tools/check_mph_multirom_profiles_legacy.py b/tools/check_mph_multirom_profiles_legacy.py new file mode 100755 index 0000000..949eb14 --- /dev/null +++ b/tools/check_mph_multirom_profiles_legacy.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +"""Static consistency checks for Metroid Prime Hunters ROM profiles. + +The registry intentionally separates three identities: + +* runtime base profile: seven region/revision RAM layouts, +* executable checksum: melonPrimeDS header+ARM9+ARM7 CRC32 compatibility, +* whole-ROM SHA-1: exact build/capture/generated-content provenance. + +Build/capture content profiles reference a runtime base through ``base_profile``. +That lets a clean ROM and one or more exact mod ROM identities keep independent +coverage/banks/checkpoints while sharing a validated runtime address layout. + +Runtime checksum hits may authorize host Aim/Morph access. Header fallback alone +must never be promoted to that trust level by this checker or the runner patch. +""" + +from __future__ import annotations + +import argparse +import json +import re +import tomllib +from pathlib import Path +from typing import NoReturn + + +SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") +BANK_RE = re.compile(r"^[A-Za-z0-9_]+$") +PROFILE_KEY_RE = re.compile(r"^[A-Za-z0-9_]+$") +HEX_RE = re.compile(r"0x[0-9A-Fa-f]+u?") +REQUIRED_RUNTIME_FIELDS = { + "morph_state": "baseIsAltForm", + "aim_x": "baseAimX", + "aim_y": "baseAimY", +} +EXPECTED_RUNTIME_PROFILES = { + "US1_0": ("AMHE", 0, 0x218DA42C), + "US1_1": ("AMHE", 1, 0x91B46577), + "EU1_0": ("AMHP", 0, 0xA4A8FE5A), + "EU1_1": ("AMHP", 1, 0x910018A5), + "JP1_0": ("AMHJ", 0, 0xD75F539D), + "JP1_1": ("AMHJ", 1, 0x42EBF348), + "KR1_0": ("AMHK", 0, 0xE54682F3), +} +# Exact CHECKSUM_TABLE from develop_hud/MelonPrimeGameRomDetect.cpp at the +# source revision audited for this PR. Names are retained so drift is visible. +EXPECTED_RUNTIME_CHECKSUMS = { + 0x91B46577: ("US1_1", "US1.1"), + 0x01476E8F: ("US1_1", "US1.1 ENCRYPTED"), + 0x218DA42C: ("US1_0", "US1.0"), + 0xE048CD92: ("US1_0", "US1.0 ENCRYPTED"), + 0x910018A5: ("EU1_1", "EU1.1"), + 0x31703770: ("EU1_1", "EU1.1 ENCRYPTED"), + 0x948B1E48: ("EU1_1", "EU1.1 BALANCED"), + 0x2970A14F: ("EU1_1", "EU1.1 BALANCED V1.2.11"), + 0x9E20F3A8: ("EU1_1", "EU1.1 RUSSIANED"), + 0xA4A8FE5A: ("EU1_0", "EU1.0"), + 0x979BB267: ("EU1_0", "EU1.0 ENCRYPTED"), + 0xD75F539D: ("JP1_0", "JP1.0"), + 0xE795A10C: ("JP1_0", "JP1.0 ENCRYPTED"), + 0x42EBF348: ("JP1_1", "JP1.1"), + 0x0A1203A5: ("JP1_1", "JP1.1 ENCRYPTED"), + 0xE54682F3: ("KR1_0", "KR1.0"), + 0xC26916F3: ("KR1_0", "KR1.0 ENCRYPTED"), +} + + +def die(message: str) -> NoReturn: + raise SystemExit(f"ERROR: {message}") + + +def parse_hex(value: object, where: str) -> int: + if not isinstance(value, str): + die(f"{where} must be a hex string") + try: + return int(value, 0) + except ValueError: + die(f"{where} has invalid hex value {value!r}") + + +def parse_checksum(value: object, where: str) -> int: + if not isinstance(value, str) or not CHECKSUM_RE.fullmatch(value): + die(f"{where} must use uppercase 0xXXXXXXXX format") + return int(value, 16) + + +def load_json(path: Path) -> object: + with path.open("r", encoding="utf-8") as f: + return json.load(f) + + +def parse_melonprime_table(path: Path) -> dict[str, dict[str, int]]: + text = path.read_text(encoding="utf-8") + enum_match = re.search( + r"enum\s+class\s+RomGroup\s*:\s*int\s*\{([^}]*)\}", text, re.S + ) + if not enum_match: + die(f"could not parse RomGroup from {path}") + groups: list[str] = [] + for raw in enum_match.group(1).split(","): + token = raw.strip().split("=")[0].strip() + if token and token != "COUNT": + groups.append(token) + if not groups: + die(f"RomGroup has no revisions in {path}") + + wanted = set(REQUIRED_RUNTIME_FIELDS.values()) + fields: dict[str, list[int]] = {} + row_re = re.compile( + r"X\(ADDR,\s*([A-Za-z0-9_]+),\s*[A-Za-z0-9_]+,\s*([^)]*)\)" + ) + for match in row_re.finditer(text): + field = match.group(1) + if field not in wanted: + continue + values = [ + int(token.rstrip("uU"), 16) + for token in HEX_RE.findall(match.group(2)) + ] + if len(values) != len(groups): + die( + f"{field} in {path} has {len(values)} values; " + f"RomGroup has {len(groups)} revisions" + ) + fields[field] = values + + missing = wanted - fields.keys() + if missing: + die(f"missing melonPrimeDS fields: {', '.join(sorted(missing))}") + + result: dict[str, dict[str, int]] = {} + for index, group in enumerate(groups): + result[group] = {field: values[index] for field, values in fields.items()} + return result + + +def validate_runtime_profiles( + registry: dict[str, object], + melon: dict[str, dict[str, int]] | None, +) -> dict[str, dict[str, object]]: + runtime_profiles = registry.get("runtime_profiles") + if not isinstance(runtime_profiles, dict): + die("ROM profile registry has no object-valued runtime_profiles") + + actual_keys = set(runtime_profiles) + expected_keys = set(EXPECTED_RUNTIME_PROFILES) + if actual_keys != expected_keys: + missing = ", ".join(sorted(expected_keys - actual_keys)) or "none" + extra = ", ".join(sorted(actual_keys - expected_keys)) or "none" + die( + "runtime_profiles must contain exactly the seven supported retail " + f"profiles (missing: {missing}; extra: {extra})" + ) + + seen_identity: set[tuple[str, int]] = set() + seen_base_checksum: set[int] = set() + validated: dict[str, dict[str, object]] = {} + for key, expected in EXPECTED_RUNTIME_PROFILES.items(): + profile = runtime_profiles.get(key) + if not isinstance(profile, dict): + die(f"runtime profile {key} must be an object") + + game_code = profile.get("game_code") + revision = profile.get("revision") + base_checksum = parse_checksum( + profile.get("base_checksum"), f"{key}.base_checksum" + ) + if (game_code, revision, base_checksum) != expected: + die( + f"{key} runtime identity/checksum is " + f"{(game_code, revision, base_checksum)!r}; expected {expected!r}" + ) + if not isinstance(game_code, str) or len(game_code) != 4 or not game_code.isascii(): + die(f"{key}.game_code must be exactly four ASCII characters") + if not isinstance(revision, int) or revision not in (0, 1): + die(f"{key}.revision is not an explicitly supported revision") + + identity = (game_code, revision) + if identity in seen_identity: + die(f"duplicate runtime cartridge identity: {game_code} rev {revision}") + seen_identity.add(identity) + if base_checksum in seen_base_checksum: + die(f"duplicate canonical executable checksum: 0x{base_checksum:08X}") + seen_base_checksum.add(base_checksum) + + runtime = profile.get("runtime") + if not isinstance(runtime, dict): + die(f"{key}.runtime is required") + parsed_runtime: dict[str, int] = {} + for profile_field in REQUIRED_RUNTIME_FIELDS: + value = parse_hex(runtime.get(profile_field), f"{key}.runtime.{profile_field}") + if not 0x02000000 <= value <= 0x023FFFFF: + die(f"{key}.runtime.{profile_field} is outside DS main RAM") + parsed_runtime[profile_field] = value + + if melon is not None: + if key not in melon: + die(f"{key} is not present in melonPrimeDS RomGroup") + for profile_field, melon_field in REQUIRED_RUNTIME_FIELDS.items(): + actual = parsed_runtime[profile_field] + expected_addr = melon[key][melon_field] + if actual != expected_addr: + die( + f"{key}.{profile_field}=0x{actual:08X}, but " + f"melonPrimeDS {melon_field}=0x{expected_addr:08X}" + ) + + validated[key] = profile + return validated + + +def validate_runtime_checksums(registry: dict[str, object]) -> None: + entries = registry.get("runtime_checksums") + if not isinstance(entries, list): + die("runtime_checksums must be an array") + actual: dict[int, tuple[str, str]] = {} + for index, item in enumerate(entries): + if not isinstance(item, dict): + die(f"runtime_checksums[{index}] must be an object") + crc = parse_checksum(item.get("crc32"), f"runtime_checksums[{index}].crc32") + profile = item.get("profile") + name = item.get("name") + if profile not in EXPECTED_RUNTIME_PROFILES: + die(f"runtime_checksums[{index}] has unknown profile {profile!r}") + if not isinstance(name, str) or not name: + die(f"runtime_checksums[{index}] has no name") + if crc in actual: + die(f"duplicate runtime executable checksum 0x{crc:08X}") + actual[crc] = (str(profile), name) + if actual != EXPECTED_RUNTIME_CHECKSUMS: + missing = sorted(set(EXPECTED_RUNTIME_CHECKSUMS) - set(actual)) + extra = sorted(set(actual) - set(EXPECTED_RUNTIME_CHECKSUMS)) + changed = sorted( + crc for crc in set(actual) & set(EXPECTED_RUNTIME_CHECKSUMS) + if actual[crc] != EXPECTED_RUNTIME_CHECKSUMS[crc] + ) + die( + "runtime checksum registry drifted from audited develop_hud detector: " + f"missing={[f'0x{x:08X}' for x in missing]}, " + f"extra={[f'0x{x:08X}' for x in extra]}, " + f"changed={[f'0x{x:08X}' for x in changed]}" + ) + + +def validate_registry(repo: Path, table: Path | None) -> None: + registry_path = repo / "config" / "mph_rom_profiles.json" + registry_obj = load_json(registry_path) + if not isinstance(registry_obj, dict): + die("ROM profile registry must be a JSON object") + registry: dict[str, object] = registry_obj + + if registry.get("schema") != 5: + die("ROM profile registry schema must be 5") + + expected_address_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" + "src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h" + ) + expected_detection_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" + "src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp" + ) + expected_checksum_source = ( + "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/" + "src/NDSCart/CartCommon.cpp" + ) + if registry.get("runtime_address_source") != expected_address_source: + die("runtime_address_source is not the approved develop_hud address table") + if registry.get("runtime_detection_source") != expected_detection_source: + die("runtime_detection_source is not the approved develop_hud detector") + if registry.get("runtime_checksum_source") != expected_checksum_source: + die("runtime_checksum_source is not melonPrimeDS CartCommon::Checksum") + + melon = parse_melonprime_table(table) if table else None + runtime_profiles = validate_runtime_profiles(registry, melon) + validate_runtime_checksums(registry) + + profiles = registry.get("profiles") + if not isinstance(profiles, dict) or not profiles: + die("ROM profile registry has no build/capture content profiles") + + seen_sha1: set[str] = set() + seen_fmv_banks: set[str] = set() + seen_known_clean_base: set[str] = set() + for key, profile in profiles.items(): + if not isinstance(key, str) or not PROFILE_KEY_RE.fullmatch(key): + die(f"content profile key {key!r} must be a C/path-safe identifier") + if not isinstance(profile, dict): + die(f"profile {key} must be an object") + + base_profile = profile.get("base_profile") + known_clean = profile.get("known_clean") + if not isinstance(base_profile, str) or base_profile not in runtime_profiles: + die(f"{key}.base_profile must name one of the seven runtime profiles") + if not isinstance(known_clean, bool): + die(f"{key}.known_clean must be boolean") + if known_clean: + # Runtime code generation intentionally discovers a clean identity by + # looking up the content profile with the same canonical key. + if key != base_profile: + die( + f"{key}: known-clean content profile must use its canonical " + f"runtime key {base_profile!r}" + ) + if base_profile in seen_known_clean_base: + die(f"duplicate known-clean content identity for {base_profile}") + seen_known_clean_base.add(base_profile) + elif key in runtime_profiles: + die( + f"{key}: canonical runtime-profile key is reserved for its " + "known-clean content identity; use a distinct key for a mod" + ) + + sha1 = profile.get("sha1") + game_code = profile.get("game_code") + revision = profile.get("revision") + if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): + die(f"{key}.sha1 must be 40 lowercase hex digits") + if sha1 in seen_sha1: + die(f"duplicate SHA-1 in profile registry: {sha1}") + seen_sha1.add(sha1) + + runtime_profile = runtime_profiles[base_profile] + if game_code != runtime_profile.get("game_code") or revision != runtime_profile.get("revision"): + die( + f"{key}: content header identity disagrees with runtime base " + f"profile {base_profile}" + ) + + launcher_default_rom = profile.get("launcher_default_rom") + if ( + not isinstance(launcher_default_rom, str) + or not launcher_default_rom + or not launcher_default_rom.lower().endswith(".nds") + ): + die(f"{key}.launcher_default_rom must be a non-empty .nds filename") + if Path(launcher_default_rom).name != launcher_default_rom: + die(f"{key}.launcher_default_rom must be a filename, not a path") + adaptive_widescreen = profile.get("adaptive_widescreen") + if not isinstance(adaptive_widescreen, bool): + die(f"{key}.adaptive_widescreen must be boolean") + + fmv_runtime = profile.get("fmv_runtime") + if not isinstance(fmv_runtime, bool): + die(f"{key}.fmv_runtime must be boolean") + fmv_runtime_bank = profile.get("fmv_runtime_bank") + if ( + not isinstance(fmv_runtime_bank, str) + or not BANK_RE.fullmatch(fmv_runtime_bank) + or "_arm9_" not in fmv_runtime_bank + ): + die(f"{key}.fmv_runtime_bank must be a C identifier-style ARM9 bank name") + if fmv_runtime_bank in seen_fmv_banks: + die(f"duplicate FMV runtime bank identity: {fmv_runtime_bank}") + seen_fmv_banks.add(fmv_runtime_bank) + + if fmv_runtime: + runtime_config_path = repo / "config" / f"{fmv_runtime_bank}.toml" + if not runtime_config_path.is_file(): + die(f"{key} enables FMV runtime but config is missing: {runtime_config_path}") + with runtime_config_path.open("rb") as f: + runtime_config = tomllib.load(f) + runtime_program = runtime_config.get("program") + if not isinstance(runtime_program, dict): + die(f"{runtime_config_path} has no [program] table") + if runtime_program.get("id") != fmv_runtime_bank: + die( + f"{runtime_config_path} program.id={runtime_program.get('id')!r}; " + f"expected {fmv_runtime_bank!r}" + ) + + coverage_path = repo / str(profile.get("coverage", "")) + if not coverage_path.is_file(): + die(f"{key}.coverage does not exist: {coverage_path}") + coverage = load_json(coverage_path) + if not isinstance(coverage, dict) or coverage.get("game_sha1") != sha1: + die(f"{key}.coverage game_sha1 does not match profile SHA-1") + + game_config_path = repo / str(profile.get("game_config", "")) + if not game_config_path.is_file(): + die(f"{key}.game_config does not exist: {game_config_path}") + with game_config_path.open("rb") as f: + game_config = tomllib.load(f) + game = game_config.get("game") + if not isinstance(game, dict): + die(f"{key}.game_config has no [game] table") + expected_config = { + "id": game_code, + "revision": revision, + "rom_size": profile.get("rom_size"), + "sha1": sha1, + } + for field, expected in expected_config.items(): + if game.get(field) != expected: + die( + f"{key}.game_config game.{field}={game.get(field)!r}; " + f"expected {expected!r}" + ) + + display = game_config.get("display", {}) + if not isinstance(display, dict): + die(f"{key}.game_config [display] must be a table") + config_adaptive = display.get("adaptive_widescreen") + if adaptive_widescreen: + if config_adaptive != "top": + die( + f"{key} enables adaptive_widescreen in the profile but " + "game config does not enable top-screen adaptive widescreen" + ) + elif config_adaptive not in (None, "none"): + die( + f"{key} disables adaptive_widescreen in the profile but " + f"game config enables {config_adaptive!r}" + ) + + us = profiles.get("US1_0") + if not isinstance(us, dict): + die("US1_0 clean build profile is required") + if us.get("base_profile") != "US1_0" or us.get("known_clean") is not True: + die("US1_0 must remain the canonical known-clean US1_0 identity") + if us.get("fmv_runtime_bank") != "mph_arm9_fmv_runtime": + die("US1_0 historical FMV runtime bank identity changed unexpectedly") + + eu = profiles.get("EU1_1") + if not isinstance(eu, dict): + die("EU1_1 clean build profile is required") + if eu.get("base_profile") != "EU1_1" or eu.get("known_clean") is not True: + die("EU1_1 must remain the canonical known-clean EU1_1 identity") + if eu.get("game_code") != "AMHP" or eu.get("revision") != 1: + die("EU1_1 must remain AMHP revision 1") + if eu.get("sha1") != "bdcd1dea293e24c98d4c481430e90d21198985a5": + die("EU1_1 SHA-1 changed unexpectedly") + expected_eu_runtime = { + "morph_state": "0x020DB138", + "aim_x": "0x020DEE46", + "aim_y": "0x020DEE4E", + } + if runtime_profiles["EU1_1"].get("runtime") != expected_eu_runtime: + die("EU1_1 runtime profile changed unexpectedly") + if eu.get("fmv_runtime") is not False: + die("EU1_1 must not reuse the US1.0 FMV runtime capture") + if eu.get("fmv_runtime_bank") != "mph_amhp1_arm9_fmv_runtime": + die("EU1_1 FMV runtime bank identity changed unexpectedly") + if eu.get("adaptive_widescreen") is not False: + die("EU1_1 adaptive widescreen must remain disabled until validated") + if eu.get("launcher_default_rom") != "Metroid Prime Hunters (Europe Rev 1).nds": + die("EU1_1 launcher default ROM filename changed unexpectedly") + + print( + f"OK: validated {len(runtime_profiles)} runtime base profiles, " + f"{len(EXPECTED_RUNTIME_CHECKSUMS)} executable checksums, and " + f"{len(profiles)} build/capture content profiles" + ) + if melon is not None: + print(f"OK: all seven Aim/Morph profiles match melonPrimeDS table: {table}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--repo", type=Path, default=Path(__file__).resolve().parents[1] + ) + parser.add_argument( + "--melonprime-table", + type=Path, + help="Downloaded MelonPrimeGameRomAddrTable.h to cross-check", + ) + args = parser.parse_args() + validate_registry(args.repo.resolve(), args.melonprime_table) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/mph_screens.py b/tools/mph_screens.py index f1f9eb5..df0e01f 100644 --- a/tools/mph_screens.py +++ b/tools/mph_screens.py @@ -34,6 +34,8 @@ # red cross on the right, both ringed in orange. "dialog_yes": (98, 302, 120, 324), "dialog_no": (138, 302, 160, 324), + # Single green check used by pairing/update notices and acknowledgements. + "dialog_ok": (116, 300, 140, 326), # Friends/Rivals lobby, top screen: the ARENA thumbnail. Flat dark green # while no game row is selected, a lit photo of the arena once a row is # selected and the host's settings have been pulled down (measured: 0 diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py index 6597a56..2bc0249 100755 --- a/tools/patch_ndsrecomp_mph_runtime.py +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -1,534 +1,28 @@ #!/usr/bin/env python3 -"""Apply the MPH multi-ROM runtime-profile shim to the pinned ndsrecomp runner. +"""Apply the MPH runtime-profile patch stack to the pinned ndsrecomp runner. -Runtime address selection follows melonPrimeDS's two-stage detector: - -1. authoritative executable checksum (CRC32 of header[0:0x40], ARM9, ARM7), -2. exact NDS gameCode @0x0C + supported revision @0x1E as a fallback. - -The fallback identifies a base profile but is *not* sufficient evidence for -host-side Aim/Morph RAM accesses. Those writes are enabled only for a checksum -explicitly known by the melonPrimeDS detector. This keeps unknown mods -fail-closed instead of guessing that a matching header implies compatible RAM. - -Whole-ROM SHA-1 has a separate role. It remains the actual-content identity -used by generated banks/captures. A clean build may accept a different whole- -ROM SHA only when the actual ROM has the canonical executable checksum of that -same clean base profile (for example, a data-only mod outside header/ARM9/ARM7). -Code-modified variants still require their own exact build/capture identity. +The core detector is kept separately so upstream-facing additions can be layered +without weakening its whole-ROM/content identity rules. The later stages add +the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch and make +that patch re-eligible after an in-process guest reset. """ from __future__ import annotations -import argparse -import json -import re +import subprocess +import sys from pathlib import Path -SHA1_RE = re.compile(r"^[0-9a-f]{40}$") -CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") -MAIN_RAM_MIN = 0x02000000 -MAIN_RAM_MAX = 0x023FFFFF -EXPECTED_RUNTIME_KEYS = { - "US1_0", "US1_1", "EU1_0", "EU1_1", "JP1_0", "JP1_1", "KR1_0" -} - - -def parse_address(value: object, *, profile: str, field: str) -> int: - if not isinstance(value, str): - raise SystemExit(f"{profile}.{field}: expected a hex string") - try: - address = int(value, 0) - except ValueError as exc: - raise SystemExit(f"{profile}.{field}: invalid address {value!r}") from exc - if not MAIN_RAM_MIN <= address <= MAIN_RAM_MAX: - raise SystemExit( - f"{profile}.{field}: 0x{address:08X} is outside DS main RAM" - ) - return address - - -def parse_checksum(value: object, *, where: str) -> int: - if not isinstance(value, str) or not CHECKSUM_RE.fullmatch(value): - raise SystemExit(f"{where}: expected uppercase 0xXXXXXXXX checksum") - return int(value, 16) - - -def load_runtime_registry( - registry_path: Path, -) -> tuple[list[dict[str, object]], list[dict[str, object]]]: - registry = json.loads(registry_path.read_text(encoding="utf-8")) - runtime_profiles = registry.get("runtime_profiles") - if not isinstance(runtime_profiles, dict) or not runtime_profiles: - raise SystemExit("ROM profile registry has no runtime_profiles") - - actual_keys = set(runtime_profiles) - if actual_keys != EXPECTED_RUNTIME_KEYS: - missing = ", ".join(sorted(EXPECTED_RUNTIME_KEYS - actual_keys)) or "none" - extra = ", ".join(sorted(actual_keys - EXPECTED_RUNTIME_KEYS)) or "none" - raise SystemExit( - "runtime profile set must be exactly the seven supported retail " - f"profiles (missing: {missing}; extra: {extra})" - ) - - build_profiles = registry.get("profiles") - if not isinstance(build_profiles, dict): - raise SystemExit("ROM profile registry has no build profiles") - - result: list[dict[str, object]] = [] - seen_identity: set[tuple[str, int]] = set() - seen_base_checksum: set[int] = set() - seen_clean_sha1: set[str] = set() - - for key, profile in runtime_profiles.items(): - if not isinstance(profile, dict): - raise SystemExit(f"{key}: runtime profile must be an object") - game_code = profile.get("game_code") - revision = profile.get("revision") - runtime = profile.get("runtime") - if ( - not isinstance(game_code, str) - or len(game_code) != 4 - or not game_code.isascii() - ): - raise SystemExit(f"{key}.game_code must be exactly four ASCII bytes") - if not isinstance(revision, int) or revision not in (0, 1): - raise SystemExit(f"{key}.revision must be an explicitly supported 0/1") - if not isinstance(runtime, dict): - raise SystemExit(f"{key}.runtime must be an object") - - identity = (game_code, revision) - if identity in seen_identity: - raise SystemExit( - f"duplicate runtime cartridge identity: {game_code} rev {revision}" - ) - seen_identity.add(identity) - - base_checksum = parse_checksum( - profile.get("base_checksum"), where=f"{key}.base_checksum" - ) - if base_checksum in seen_base_checksum: - raise SystemExit(f"duplicate canonical executable checksum for {key}") - seen_base_checksum.add(base_checksum) - - known_clean_sha1 = "" - clean = build_profiles.get(key) - if clean is not None: - if not isinstance(clean, dict): - raise SystemExit(f"{key}: build profile must be an object") - if clean.get("game_code") != game_code or clean.get("revision") != revision: - raise SystemExit( - f"{key}: clean build identity disagrees with runtime profile" - ) - sha1 = clean.get("sha1") - if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): - raise SystemExit(f"{key}.sha1 must be 40 lowercase hex digits") - if sha1 in seen_clean_sha1: - raise SystemExit(f"duplicate known-clean SHA-1: {sha1}") - seen_clean_sha1.add(sha1) - known_clean_sha1 = sha1 - - result.append( - { - "key": key, - "game_code": game_code, - "revision": revision, - "base_checksum": base_checksum, - "known_clean_sha1": known_clean_sha1, - "morph_state": parse_address( - runtime.get("morph_state"), profile=key, field="morph_state" - ), - "aim_x": parse_address( - runtime.get("aim_x"), profile=key, field="aim_x" - ), - "aim_y": parse_address( - runtime.get("aim_y"), profile=key, field="aim_y" - ), - } - ) - - checksums_obj = registry.get("runtime_checksums") - if not isinstance(checksums_obj, list) or not checksums_obj: - raise SystemExit("ROM profile registry has no runtime_checksums") - checksums: list[dict[str, object]] = [] - seen_checksums: set[int] = set() - canonical_seen: set[str] = set() - for index, item in enumerate(checksums_obj): - if not isinstance(item, dict): - raise SystemExit(f"runtime_checksums[{index}] must be an object") - checksum = parse_checksum( - item.get("crc32"), where=f"runtime_checksums[{index}].crc32" - ) - profile_key = item.get("profile") - name = item.get("name") - if profile_key not in EXPECTED_RUNTIME_KEYS: - raise SystemExit( - f"runtime_checksums[{index}].profile is unknown: {profile_key!r}" - ) - if not isinstance(name, str) or not name: - raise SystemExit(f"runtime_checksums[{index}].name must be non-empty") - if checksum in seen_checksums: - raise SystemExit(f"duplicate runtime checksum 0x{checksum:08X}") - seen_checksums.add(checksum) - profile = next(p for p in result if p["key"] == profile_key) - if checksum == profile["base_checksum"]: - canonical_seen.add(str(profile_key)) - checksums.append( - {"crc32": checksum, "profile": profile_key, "name": name} - ) - - if canonical_seen != EXPECTED_RUNTIME_KEYS: - missing = ", ".join(sorted(EXPECTED_RUNTIME_KEYS - canonical_seen)) - raise SystemExit( - f"runtime_checksums is missing canonical entries for: {missing}" - ) - return result, checksums - - -def generated_header( - profiles: list[dict[str, object]], checksums: list[dict[str, object]] -) -> str: - profile_rows: list[str] = [] - for profile in profiles: - profile_rows.append( - ' {"%s", "%s", %du, "%s", 0x%08Xu, 0x%08Xu, 0x%08Xu, 0x%08Xu}, // %s' - % ( - profile["key"], - profile["game_code"], - profile["revision"], - profile["known_clean_sha1"], - profile["base_checksum"], - profile["morph_state"], - profile["aim_x"], - profile["aim_y"], - profile["key"], - ) - ) - checksum_rows = [ - ' {0x%08Xu, "%s", "%s"},' - % (item["crc32"], item["profile"], item["name"]) - for item in checksums - ] - return """#pragma once - -#include -#include - -// Generated by MetroidPrimeHuntersRecomp/tools/patch_ndsrecomp_mph_runtime.py. -// Do not edit in the ndsrecomp checkout; edit config/mph_rom_profiles.json. -// -// The executable checksum mirrors melonPrimeDS CartCommon::Checksum(): CRC32 of -// header[0:0x40], then ARM9, then ARM7. A checksum hit is authoritative for the -// runtime layout. Header gameCode+revision is only a fail-closed fallback hint. -struct NdsMphRuntimeProfile { - const char* key; - const char* game_code; - uint8_t revision; - const char* known_clean_sha1; - uint32_t base_checksum; - uint32_t morph_state; - uint32_t aim_x; - uint32_t aim_y; -}; - -struct NdsMphRuntimeChecksum { - uint32_t checksum; - const char* profile_key; - const char* name; -}; - -inline constexpr std::array kNdsMphRuntimeProfiles{{ -%s -}}; - -inline constexpr std::array kNdsMphRuntimeChecksums{{ -%s -}}; -""" % ( - len(profile_rows), - "\n".join(profile_rows), - len(checksum_rows), - "\n".join(checksum_rows), - ) - - -def patch_once(path: Path, old: str, new: str, marker: str) -> None: - text = path.read_text(encoding="utf-8") - if marker in text: - return - if old not in text: - raise SystemExit( - f"Refusing to patch {path}: expected pinned ndsrecomp preimage " - f"for marker {marker!r} was not found" - ) - path.write_text(text.replace(old, new, 1), encoding="utf-8") - - -def patch_runner(framework_root: Path, registry_path: Path) -> None: - runner_src = framework_root / "runner" / "src" - title_h = runner_src / "title_patches.h" - title_cpp = runner_src / "title_patches.cpp" - frontend_cpp = runner_src / "frontend.cpp" - main_cpp = runner_src / "main.cpp" - for path in (title_h, title_cpp, frontend_cpp, main_cpp): - if not path.is_file(): - raise SystemExit(f"Pinned ndsrecomp runner file not found: {path}") - - profiles, checksums = load_runtime_registry(registry_path) - generated = runner_src / "mph_runtime_profiles.generated.h" - generated.write_text(generated_header(profiles, checksums), encoding="utf-8") - - patch_once( - title_h, - "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" - "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: melonPrimeDS-compatible base detector.\n" - "bool nds_title_patches_select_mph_runtime_profile(\n" - " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1,\n" - " const char* expected_rom_sha1);\n" - "bool nds_title_patches_mph_host_writes_compatible();\n" - "bool nds_title_patches_mph_allows_rom_sha1_mismatch();\n" - "bool nds_title_patches_mph_in_ball();\n" - "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" - "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", - "melonPrimeDS-compatible base detector", - ) - - patch_once( - title_cpp, - '#include "title_patches.h"\n', - '#include "title_patches.h"\n' - '#include "mph_runtime_profiles.generated.h" // MPH_MULTIROM_PROFILE_HEADER\n', - "MPH_MULTIROM_PROFILE_HEADER", - ) - patch_once( - title_cpp, - "// AMHE0's native touch-look routine consumes these signed, per-frame fields.\n" - "// Feeding deltas here while holding the stylus at center preserves the game\n" - "// path but removes the finite physical touchscreen edge.\n" - "constexpr uint32_t kMphUs10AimX = 0x020DE526u;\n" - "constexpr uint32_t kMphUs10AimY = 0x020DE52Eu;\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: runtime identity and safety state.\n" - "const NdsMphRuntimeProfile* g_mph_runtime_profile = nullptr;\n" - "bool g_mph_host_writes_compatible = false;\n" - "bool g_mph_allow_rom_sha1_mismatch = false;\n\n" - "uint32_t mph_read_le32(const uint8_t* p) {\n" - " return static_cast(p[0]) |\n" - " (static_cast(p[1]) << 8) |\n" - " (static_cast(p[2]) << 16) |\n" - " (static_cast(p[3]) << 24);\n" - "}\n\n" - "uint32_t mph_crc32(const uint8_t* data, uint32_t len, uint32_t start) {\n" - " uint32_t crc = start ^ 0xFFFFFFFFu;\n" - " for (uint32_t i = 0; i < len; ++i) {\n" - " crc ^= data[i];\n" - " for (int bit = 0; bit < 8; ++bit)\n" - " crc = (crc >> 1) ^\n" - " (0xEDB88320u & (0u - (crc & 1u)));\n" - " }\n" - " return crc ^ 0xFFFFFFFFu;\n" - "}\n\n" - "bool mph_compute_executable_checksum(\n" - " const uint8_t* rom, uint64_t rom_size, uint32_t* out) {\n" - " if (!rom || !out || rom_size < 0x40u) return false;\n" - " const uint32_t arm9_offset = mph_read_le32(rom + 0x20u);\n" - " const uint32_t arm9_size = mph_read_le32(rom + 0x2Cu);\n" - " const uint32_t arm7_offset = mph_read_le32(rom + 0x30u);\n" - " const uint32_t arm7_size = mph_read_le32(rom + 0x3Cu);\n" - " if (static_cast(arm9_offset) + arm9_size > rom_size ||\n" - " static_cast(arm7_offset) + arm7_size > rom_size)\n" - " return false;\n" - " uint32_t crc = mph_crc32(rom, 0x40u, 0u);\n" - " crc = mph_crc32(rom + arm9_offset, arm9_size, crc);\n" - " crc = mph_crc32(rom + arm7_offset, arm7_size, crc);\n" - " *out = crc;\n" - " return true;\n" - "}\n\n" - "const NdsMphRuntimeProfile* mph_find_profile_by_key(const char* key) {\n" - " for (const auto& profile : kNdsMphRuntimeProfiles)\n" - " if (std::strcmp(profile.key, key) == 0) return &profile;\n" - " return nullptr;\n" - "}\n\n" - "const NdsMphRuntimeProfile* mph_find_clean_sha1(const char* sha1) {\n" - " if (!sha1 || sha1[0] == '\\0') return nullptr;\n" - " for (const auto& profile : kNdsMphRuntimeProfiles) {\n" - " if (profile.known_clean_sha1[0] != '\\0' &&\n" - " std::strcmp(profile.known_clean_sha1, sha1) == 0)\n" - " return &profile;\n" - " }\n" - " return nullptr;\n" - "}\n", - "runtime identity and safety state", - ) - patch_once( - title_cpp, - "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" - " g_mph_mouse_aim = enabled;\n" - "}\n\n" - "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" - " if (!g_mph_mouse_aim || (dx == 0 && dy == 0)) return false;\n" - " if (dx != 0)\n" - " bus_write_u32_slow(kMphUs10AimX, static_cast(dx));\n" - " if (dy != 0)\n" - " bus_write_u32_slow(kMphUs10AimY, static_cast(dy));\n" - " return true;\n" - "}\n", - "bool nds_title_patches_select_mph_runtime_profile(\n" - " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1,\n" - " const char* expected_rom_sha1) {\n" - " g_mph_mouse_aim = false;\n" - " g_mph_runtime_profile = nullptr;\n" - " g_mph_host_writes_compatible = false;\n" - " g_mph_allow_rom_sha1_mismatch = false;\n" - " if (!rom_data || !rom_sha1 || !expected_rom_sha1 || rom_size <= 0x1Eu)\n" - " return false;\n\n" - " uint32_t checksum = 0;\n" - " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum))\n" - " return false;\n\n" - " const NdsMphRuntimeChecksum* checksum_hit = nullptr;\n" - " const NdsMphRuntimeProfile* profile = nullptr;\n" - " for (const auto& entry : kNdsMphRuntimeChecksums) {\n" - " if (entry.checksum == checksum) {\n" - " checksum_hit = &entry;\n" - " profile = mph_find_profile_by_key(entry.profile_key);\n" - " break;\n" - " }\n" - " }\n\n" - " if (!profile) {\n" - " // melonPrimeDS fallback, tightened to exact supported revisions.\n" - " for (const auto& candidate : kNdsMphRuntimeProfiles) {\n" - " if (std::memcmp(candidate.game_code, rom_data + 0x0Cu, 4) == 0 &&\n" - " candidate.revision == rom_data[0x1Eu]) {\n" - " if (profile) return false; // ambiguous registry: fail closed\n" - " profile = &candidate;\n" - " }\n" - " }\n" - " }\n" - " if (!profile) return false;\n\n" - " // A known clean whole-ROM hash can only describe its own base profile.\n" - " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" - " if (actual_clean && actual_clean != profile) return false;\n\n" - " g_mph_runtime_profile = profile;\n" - " // Unknown checksum + matching header is only a base-profile hint.\n" - " // Do not perform host RAM reads/writes until the executable checksum\n" - " // is explicitly represented by melonPrimeDS's authoritative table.\n" - " g_mph_host_writes_compatible = checksum_hit != nullptr;\n\n" - " // Whole-ROM mismatch may be relaxed only for a clean build whose\n" - " // executable identity is byte-for-byte equivalent to the canonical\n" - " // header+ARM9+ARM7 checksum. Code-modified known variants therefore\n" - " // still require an exact mod-specific build/capture SHA.\n" - " const NdsMphRuntimeProfile* expected_clean =\n" - " mph_find_clean_sha1(expected_rom_sha1);\n" - " g_mph_allow_rom_sha1_mismatch =\n" - " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" - " expected_clean == profile && checksum == profile->base_checksum;\n" - " return true;\n" - "}\n\n" - "bool nds_title_patches_mph_host_writes_compatible() {\n" - " return g_mph_runtime_profile && g_mph_host_writes_compatible;\n" - "}\n\n" - "bool nds_title_patches_mph_allows_rom_sha1_mismatch() {\n" - " return g_mph_runtime_profile && g_mph_allow_rom_sha1_mismatch;\n" - "}\n\n" - "bool nds_title_patches_mph_in_ball() {\n" - " return nds_title_patches_mph_host_writes_compatible() &&\n" - " bus_read_u8_slow(g_mph_runtime_profile->morph_state) == 0x02u;\n" - "}\n\n" - "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" - " g_mph_mouse_aim =\n" - " enabled && nds_title_patches_mph_host_writes_compatible();\n" - "}\n\n" - "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" - " if (!g_mph_mouse_aim || !nds_title_patches_mph_host_writes_compatible() ||\n" - " (dx == 0 && dy == 0)) return false;\n" - " if (dx != 0)\n" - " bus_write_u32_slow(g_mph_runtime_profile->aim_x,\n" - " static_cast(dx));\n" - " if (dy != 0)\n" - " bus_write_u32_slow(g_mph_runtime_profile->aim_y,\n" - " static_cast(dy));\n" - " return true;\n" - "}\n", - "nds_title_patches_select_mph_runtime_profile", - ) - - patch_once( - frontend_cpp, - "constexpr uint32_t kMphUs10MorphState = 0x020DA818u;\n", - "// MPH_MULTIROM_RUNTIME_PROFILE: morph address comes from the base ROM profile.\n", - "morph address comes from the base ROM profile", - ) - patch_once( - frontend_cpp, - " const bool in_ball =\n" - " bus_read_u8_slow(kMphUs10MorphState) == 0x02u;\n", - " const bool in_ball = nds_title_patches_mph_in_ball();\n", - "nds_title_patches_mph_in_ball()", - ) - - patch_once( - main_cpp, - " rom_sha1 = gba::sha1(rom.data(), rom.size()).hex();\n" - " std::fprintf(stderr, \"[load] cartridge: %zu bytes, SHA-1 %s\\n\",\n" - " rom.size(), rom_sha1.c_str());\n" - " if (!frontend_options.expected_rom_sha1.empty() &&\n" - " rom_sha1 != frontend_options.expected_rom_sha1) {\n" - " std::fprintf(stderr,\n" - " \"refusing to start: game config expects ROM SHA-1 \"\n" - " \"%s, got %s\\n\",\n" - " frontend_options.expected_rom_sha1.c_str(),\n" - " rom_sha1.c_str());\n" - " return 1;\n" - " }\n", - " rom_sha1 = gba::sha1(rom.data(), rom.size()).hex();\n" - " std::fprintf(stderr, \"[load] cartridge: %zu bytes, SHA-1 %s\\n\",\n" - " rom.size(), rom_sha1.c_str());\n" - " // MPH_MULTIROM_CONTENT_GATE: choose a base layout before the\n" - " // generic exact-SHA gate. The whole-ROM hash still identifies\n" - " // generated content; only canonical executable-equivalent data\n" - " // variants may reuse a clean build.\n" - " nds_title_patches_select_mph_runtime_profile(\n" - " rom.data(), static_cast(rom.size()), rom_sha1.c_str(),\n" - " frontend_options.expected_rom_sha1.c_str());\n" - " if (!frontend_options.expected_rom_sha1.empty() &&\n" - " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" - " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n" - " std::fprintf(stderr,\n" - " \"refusing to start: game config expects ROM SHA-1 \"\n" - " \"%s, got %s\\n\",\n" - " frontend_options.expected_rom_sha1.c_str(),\n" - " rom_sha1.c_str());\n" - " return 1;\n" - " }\n", - "MPH_MULTIROM_CONTENT_GATE", - ) - patch_once( - main_cpp, - " mph_mouse_aim_policy =\n" - " rom_sha1 == \"90164d1ac127ee5f9815ea4ae7de798c7b5fc629\" &&\n" - " frontend_options.relative_mouse_touch;\n", - " // MPH_MULTIROM_HOST_WRITE_GATE: header fallback alone never enables\n" - " // direct host RAM writes; an authoritative executable checksum is required.\n" - " mph_mouse_aim_policy =\n" - " nds_title_patches_mph_host_writes_compatible() &&\n" - " frontend_options.relative_mouse_touch;\n", - "MPH_MULTIROM_HOST_WRITE_GATE", - ) - - print( - f"Patched ndsrecomp MPH runtime base profiles: " - + ", ".join(str(profile["key"]) for profile in profiles) - + f" ({len(checksums)} authoritative executable checksums)" - ) - - def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--framework-root", type=Path, required=True) - parser.add_argument("--profiles", type=Path, required=True) - args = parser.parse_args() - patch_runner(args.framework_root.resolve(), args.profiles.resolve()) + here = Path(__file__).resolve().parent + args = sys.argv[1:] + for script in ( + here / "patch_ndsrecomp_mph_runtime_core.py", + here / "patch_ndsrecomp_mph_widescreen.py", + here / "patch_ndsrecomp_mph_widescreen_reset.py", + ): + subprocess.run([sys.executable, str(script), *args], check=True) if __name__ == "__main__": diff --git a/tools/patch_ndsrecomp_mph_runtime_core.py b/tools/patch_ndsrecomp_mph_runtime_core.py new file mode 100755 index 0000000..6597a56 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_runtime_core.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 +"""Apply the MPH multi-ROM runtime-profile shim to the pinned ndsrecomp runner. + +Runtime address selection follows melonPrimeDS's two-stage detector: + +1. authoritative executable checksum (CRC32 of header[0:0x40], ARM9, ARM7), +2. exact NDS gameCode @0x0C + supported revision @0x1E as a fallback. + +The fallback identifies a base profile but is *not* sufficient evidence for +host-side Aim/Morph RAM accesses. Those writes are enabled only for a checksum +explicitly known by the melonPrimeDS detector. This keeps unknown mods +fail-closed instead of guessing that a matching header implies compatible RAM. + +Whole-ROM SHA-1 has a separate role. It remains the actual-content identity +used by generated banks/captures. A clean build may accept a different whole- +ROM SHA only when the actual ROM has the canonical executable checksum of that +same clean base profile (for example, a data-only mod outside header/ARM9/ARM7). +Code-modified variants still require their own exact build/capture identity. +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +CHECKSUM_RE = re.compile(r"^0x[0-9A-F]{8}$") +MAIN_RAM_MIN = 0x02000000 +MAIN_RAM_MAX = 0x023FFFFF +EXPECTED_RUNTIME_KEYS = { + "US1_0", "US1_1", "EU1_0", "EU1_1", "JP1_0", "JP1_1", "KR1_0" +} + + +def parse_address(value: object, *, profile: str, field: str) -> int: + if not isinstance(value, str): + raise SystemExit(f"{profile}.{field}: expected a hex string") + try: + address = int(value, 0) + except ValueError as exc: + raise SystemExit(f"{profile}.{field}: invalid address {value!r}") from exc + if not MAIN_RAM_MIN <= address <= MAIN_RAM_MAX: + raise SystemExit( + f"{profile}.{field}: 0x{address:08X} is outside DS main RAM" + ) + return address + + +def parse_checksum(value: object, *, where: str) -> int: + if not isinstance(value, str) or not CHECKSUM_RE.fullmatch(value): + raise SystemExit(f"{where}: expected uppercase 0xXXXXXXXX checksum") + return int(value, 16) + + +def load_runtime_registry( + registry_path: Path, +) -> tuple[list[dict[str, object]], list[dict[str, object]]]: + registry = json.loads(registry_path.read_text(encoding="utf-8")) + runtime_profiles = registry.get("runtime_profiles") + if not isinstance(runtime_profiles, dict) or not runtime_profiles: + raise SystemExit("ROM profile registry has no runtime_profiles") + + actual_keys = set(runtime_profiles) + if actual_keys != EXPECTED_RUNTIME_KEYS: + missing = ", ".join(sorted(EXPECTED_RUNTIME_KEYS - actual_keys)) or "none" + extra = ", ".join(sorted(actual_keys - EXPECTED_RUNTIME_KEYS)) or "none" + raise SystemExit( + "runtime profile set must be exactly the seven supported retail " + f"profiles (missing: {missing}; extra: {extra})" + ) + + build_profiles = registry.get("profiles") + if not isinstance(build_profiles, dict): + raise SystemExit("ROM profile registry has no build profiles") + + result: list[dict[str, object]] = [] + seen_identity: set[tuple[str, int]] = set() + seen_base_checksum: set[int] = set() + seen_clean_sha1: set[str] = set() + + for key, profile in runtime_profiles.items(): + if not isinstance(profile, dict): + raise SystemExit(f"{key}: runtime profile must be an object") + game_code = profile.get("game_code") + revision = profile.get("revision") + runtime = profile.get("runtime") + if ( + not isinstance(game_code, str) + or len(game_code) != 4 + or not game_code.isascii() + ): + raise SystemExit(f"{key}.game_code must be exactly four ASCII bytes") + if not isinstance(revision, int) or revision not in (0, 1): + raise SystemExit(f"{key}.revision must be an explicitly supported 0/1") + if not isinstance(runtime, dict): + raise SystemExit(f"{key}.runtime must be an object") + + identity = (game_code, revision) + if identity in seen_identity: + raise SystemExit( + f"duplicate runtime cartridge identity: {game_code} rev {revision}" + ) + seen_identity.add(identity) + + base_checksum = parse_checksum( + profile.get("base_checksum"), where=f"{key}.base_checksum" + ) + if base_checksum in seen_base_checksum: + raise SystemExit(f"duplicate canonical executable checksum for {key}") + seen_base_checksum.add(base_checksum) + + known_clean_sha1 = "" + clean = build_profiles.get(key) + if clean is not None: + if not isinstance(clean, dict): + raise SystemExit(f"{key}: build profile must be an object") + if clean.get("game_code") != game_code or clean.get("revision") != revision: + raise SystemExit( + f"{key}: clean build identity disagrees with runtime profile" + ) + sha1 = clean.get("sha1") + if not isinstance(sha1, str) or not SHA1_RE.fullmatch(sha1): + raise SystemExit(f"{key}.sha1 must be 40 lowercase hex digits") + if sha1 in seen_clean_sha1: + raise SystemExit(f"duplicate known-clean SHA-1: {sha1}") + seen_clean_sha1.add(sha1) + known_clean_sha1 = sha1 + + result.append( + { + "key": key, + "game_code": game_code, + "revision": revision, + "base_checksum": base_checksum, + "known_clean_sha1": known_clean_sha1, + "morph_state": parse_address( + runtime.get("morph_state"), profile=key, field="morph_state" + ), + "aim_x": parse_address( + runtime.get("aim_x"), profile=key, field="aim_x" + ), + "aim_y": parse_address( + runtime.get("aim_y"), profile=key, field="aim_y" + ), + } + ) + + checksums_obj = registry.get("runtime_checksums") + if not isinstance(checksums_obj, list) or not checksums_obj: + raise SystemExit("ROM profile registry has no runtime_checksums") + checksums: list[dict[str, object]] = [] + seen_checksums: set[int] = set() + canonical_seen: set[str] = set() + for index, item in enumerate(checksums_obj): + if not isinstance(item, dict): + raise SystemExit(f"runtime_checksums[{index}] must be an object") + checksum = parse_checksum( + item.get("crc32"), where=f"runtime_checksums[{index}].crc32" + ) + profile_key = item.get("profile") + name = item.get("name") + if profile_key not in EXPECTED_RUNTIME_KEYS: + raise SystemExit( + f"runtime_checksums[{index}].profile is unknown: {profile_key!r}" + ) + if not isinstance(name, str) or not name: + raise SystemExit(f"runtime_checksums[{index}].name must be non-empty") + if checksum in seen_checksums: + raise SystemExit(f"duplicate runtime checksum 0x{checksum:08X}") + seen_checksums.add(checksum) + profile = next(p for p in result if p["key"] == profile_key) + if checksum == profile["base_checksum"]: + canonical_seen.add(str(profile_key)) + checksums.append( + {"crc32": checksum, "profile": profile_key, "name": name} + ) + + if canonical_seen != EXPECTED_RUNTIME_KEYS: + missing = ", ".join(sorted(EXPECTED_RUNTIME_KEYS - canonical_seen)) + raise SystemExit( + f"runtime_checksums is missing canonical entries for: {missing}" + ) + return result, checksums + + +def generated_header( + profiles: list[dict[str, object]], checksums: list[dict[str, object]] +) -> str: + profile_rows: list[str] = [] + for profile in profiles: + profile_rows.append( + ' {"%s", "%s", %du, "%s", 0x%08Xu, 0x%08Xu, 0x%08Xu, 0x%08Xu}, // %s' + % ( + profile["key"], + profile["game_code"], + profile["revision"], + profile["known_clean_sha1"], + profile["base_checksum"], + profile["morph_state"], + profile["aim_x"], + profile["aim_y"], + profile["key"], + ) + ) + checksum_rows = [ + ' {0x%08Xu, "%s", "%s"},' + % (item["crc32"], item["profile"], item["name"]) + for item in checksums + ] + return """#pragma once + +#include +#include + +// Generated by MetroidPrimeHuntersRecomp/tools/patch_ndsrecomp_mph_runtime.py. +// Do not edit in the ndsrecomp checkout; edit config/mph_rom_profiles.json. +// +// The executable checksum mirrors melonPrimeDS CartCommon::Checksum(): CRC32 of +// header[0:0x40], then ARM9, then ARM7. A checksum hit is authoritative for the +// runtime layout. Header gameCode+revision is only a fail-closed fallback hint. +struct NdsMphRuntimeProfile { + const char* key; + const char* game_code; + uint8_t revision; + const char* known_clean_sha1; + uint32_t base_checksum; + uint32_t morph_state; + uint32_t aim_x; + uint32_t aim_y; +}; + +struct NdsMphRuntimeChecksum { + uint32_t checksum; + const char* profile_key; + const char* name; +}; + +inline constexpr std::array kNdsMphRuntimeProfiles{{ +%s +}}; + +inline constexpr std::array kNdsMphRuntimeChecksums{{ +%s +}}; +""" % ( + len(profile_rows), + "\n".join(profile_rows), + len(checksum_rows), + "\n".join(checksum_rows), + ) + + +def patch_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + if old not in text: + raise SystemExit( + f"Refusing to patch {path}: expected pinned ndsrecomp preimage " + f"for marker {marker!r} was not found" + ) + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def patch_runner(framework_root: Path, registry_path: Path) -> None: + runner_src = framework_root / "runner" / "src" + title_h = runner_src / "title_patches.h" + title_cpp = runner_src / "title_patches.cpp" + frontend_cpp = runner_src / "frontend.cpp" + main_cpp = runner_src / "main.cpp" + for path in (title_h, title_cpp, frontend_cpp, main_cpp): + if not path.is_file(): + raise SystemExit(f"Pinned ndsrecomp runner file not found: {path}") + + profiles, checksums = load_runtime_registry(registry_path) + generated = runner_src / "mph_runtime_profiles.generated.h" + generated.write_text(generated_header(profiles, checksums), encoding="utf-8") + + patch_once( + title_h, + "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", + "// MPH_MULTIROM_RUNTIME_PROFILE: melonPrimeDS-compatible base detector.\n" + "bool nds_title_patches_select_mph_runtime_profile(\n" + " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1,\n" + " const char* expected_rom_sha1);\n" + "bool nds_title_patches_mph_host_writes_compatible();\n" + "bool nds_title_patches_mph_allows_rom_sha1_mismatch();\n" + "bool nds_title_patches_mph_in_ball();\n" + "void nds_title_patches_set_mph_mouse_aim(bool enabled);\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy);\n", + "melonPrimeDS-compatible base detector", + ) + + patch_once( + title_cpp, + '#include "title_patches.h"\n', + '#include "title_patches.h"\n' + '#include "mph_runtime_profiles.generated.h" // MPH_MULTIROM_PROFILE_HEADER\n', + "MPH_MULTIROM_PROFILE_HEADER", + ) + patch_once( + title_cpp, + "// AMHE0's native touch-look routine consumes these signed, per-frame fields.\n" + "// Feeding deltas here while holding the stylus at center preserves the game\n" + "// path but removes the finite physical touchscreen edge.\n" + "constexpr uint32_t kMphUs10AimX = 0x020DE526u;\n" + "constexpr uint32_t kMphUs10AimY = 0x020DE52Eu;\n", + "// MPH_MULTIROM_RUNTIME_PROFILE: runtime identity and safety state.\n" + "const NdsMphRuntimeProfile* g_mph_runtime_profile = nullptr;\n" + "bool g_mph_host_writes_compatible = false;\n" + "bool g_mph_allow_rom_sha1_mismatch = false;\n\n" + "uint32_t mph_read_le32(const uint8_t* p) {\n" + " return static_cast(p[0]) |\n" + " (static_cast(p[1]) << 8) |\n" + " (static_cast(p[2]) << 16) |\n" + " (static_cast(p[3]) << 24);\n" + "}\n\n" + "uint32_t mph_crc32(const uint8_t* data, uint32_t len, uint32_t start) {\n" + " uint32_t crc = start ^ 0xFFFFFFFFu;\n" + " for (uint32_t i = 0; i < len; ++i) {\n" + " crc ^= data[i];\n" + " for (int bit = 0; bit < 8; ++bit)\n" + " crc = (crc >> 1) ^\n" + " (0xEDB88320u & (0u - (crc & 1u)));\n" + " }\n" + " return crc ^ 0xFFFFFFFFu;\n" + "}\n\n" + "bool mph_compute_executable_checksum(\n" + " const uint8_t* rom, uint64_t rom_size, uint32_t* out) {\n" + " if (!rom || !out || rom_size < 0x40u) return false;\n" + " const uint32_t arm9_offset = mph_read_le32(rom + 0x20u);\n" + " const uint32_t arm9_size = mph_read_le32(rom + 0x2Cu);\n" + " const uint32_t arm7_offset = mph_read_le32(rom + 0x30u);\n" + " const uint32_t arm7_size = mph_read_le32(rom + 0x3Cu);\n" + " if (static_cast(arm9_offset) + arm9_size > rom_size ||\n" + " static_cast(arm7_offset) + arm7_size > rom_size)\n" + " return false;\n" + " uint32_t crc = mph_crc32(rom, 0x40u, 0u);\n" + " crc = mph_crc32(rom + arm9_offset, arm9_size, crc);\n" + " crc = mph_crc32(rom + arm7_offset, arm7_size, crc);\n" + " *out = crc;\n" + " return true;\n" + "}\n\n" + "const NdsMphRuntimeProfile* mph_find_profile_by_key(const char* key) {\n" + " for (const auto& profile : kNdsMphRuntimeProfiles)\n" + " if (std::strcmp(profile.key, key) == 0) return &profile;\n" + " return nullptr;\n" + "}\n\n" + "const NdsMphRuntimeProfile* mph_find_clean_sha1(const char* sha1) {\n" + " if (!sha1 || sha1[0] == '\\0') return nullptr;\n" + " for (const auto& profile : kNdsMphRuntimeProfiles) {\n" + " if (profile.known_clean_sha1[0] != '\\0' &&\n" + " std::strcmp(profile.known_clean_sha1, sha1) == 0)\n" + " return &profile;\n" + " }\n" + " return nullptr;\n" + "}\n", + "runtime identity and safety state", + ) + patch_once( + title_cpp, + "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" + " g_mph_mouse_aim = enabled;\n" + "}\n\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" + " if (!g_mph_mouse_aim || (dx == 0 && dy == 0)) return false;\n" + " if (dx != 0)\n" + " bus_write_u32_slow(kMphUs10AimX, static_cast(dx));\n" + " if (dy != 0)\n" + " bus_write_u32_slow(kMphUs10AimY, static_cast(dy));\n" + " return true;\n" + "}\n", + "bool nds_title_patches_select_mph_runtime_profile(\n" + " const uint8_t* rom_data, uint64_t rom_size, const char* rom_sha1,\n" + " const char* expected_rom_sha1) {\n" + " g_mph_mouse_aim = false;\n" + " g_mph_runtime_profile = nullptr;\n" + " g_mph_host_writes_compatible = false;\n" + " g_mph_allow_rom_sha1_mismatch = false;\n" + " if (!rom_data || !rom_sha1 || !expected_rom_sha1 || rom_size <= 0x1Eu)\n" + " return false;\n\n" + " uint32_t checksum = 0;\n" + " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum))\n" + " return false;\n\n" + " const NdsMphRuntimeChecksum* checksum_hit = nullptr;\n" + " const NdsMphRuntimeProfile* profile = nullptr;\n" + " for (const auto& entry : kNdsMphRuntimeChecksums) {\n" + " if (entry.checksum == checksum) {\n" + " checksum_hit = &entry;\n" + " profile = mph_find_profile_by_key(entry.profile_key);\n" + " break;\n" + " }\n" + " }\n\n" + " if (!profile) {\n" + " // melonPrimeDS fallback, tightened to exact supported revisions.\n" + " for (const auto& candidate : kNdsMphRuntimeProfiles) {\n" + " if (std::memcmp(candidate.game_code, rom_data + 0x0Cu, 4) == 0 &&\n" + " candidate.revision == rom_data[0x1Eu]) {\n" + " if (profile) return false; // ambiguous registry: fail closed\n" + " profile = &candidate;\n" + " }\n" + " }\n" + " }\n" + " if (!profile) return false;\n\n" + " // A known clean whole-ROM hash can only describe its own base profile.\n" + " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" + " if (actual_clean && actual_clean != profile) return false;\n\n" + " g_mph_runtime_profile = profile;\n" + " // Unknown checksum + matching header is only a base-profile hint.\n" + " // Do not perform host RAM reads/writes until the executable checksum\n" + " // is explicitly represented by melonPrimeDS's authoritative table.\n" + " g_mph_host_writes_compatible = checksum_hit != nullptr;\n\n" + " // Whole-ROM mismatch may be relaxed only for a clean build whose\n" + " // executable identity is byte-for-byte equivalent to the canonical\n" + " // header+ARM9+ARM7 checksum. Code-modified known variants therefore\n" + " // still require an exact mod-specific build/capture SHA.\n" + " const NdsMphRuntimeProfile* expected_clean =\n" + " mph_find_clean_sha1(expected_rom_sha1);\n" + " g_mph_allow_rom_sha1_mismatch =\n" + " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" + " expected_clean == profile && checksum == profile->base_checksum;\n" + " return true;\n" + "}\n\n" + "bool nds_title_patches_mph_host_writes_compatible() {\n" + " return g_mph_runtime_profile && g_mph_host_writes_compatible;\n" + "}\n\n" + "bool nds_title_patches_mph_allows_rom_sha1_mismatch() {\n" + " return g_mph_runtime_profile && g_mph_allow_rom_sha1_mismatch;\n" + "}\n\n" + "bool nds_title_patches_mph_in_ball() {\n" + " return nds_title_patches_mph_host_writes_compatible() &&\n" + " bus_read_u8_slow(g_mph_runtime_profile->morph_state) == 0x02u;\n" + "}\n\n" + "void nds_title_patches_set_mph_mouse_aim(bool enabled) {\n" + " g_mph_mouse_aim =\n" + " enabled && nds_title_patches_mph_host_writes_compatible();\n" + "}\n\n" + "bool nds_title_patches_apply_mph_mouse_delta(int32_t dx, int32_t dy) {\n" + " if (!g_mph_mouse_aim || !nds_title_patches_mph_host_writes_compatible() ||\n" + " (dx == 0 && dy == 0)) return false;\n" + " if (dx != 0)\n" + " bus_write_u32_slow(g_mph_runtime_profile->aim_x,\n" + " static_cast(dx));\n" + " if (dy != 0)\n" + " bus_write_u32_slow(g_mph_runtime_profile->aim_y,\n" + " static_cast(dy));\n" + " return true;\n" + "}\n", + "nds_title_patches_select_mph_runtime_profile", + ) + + patch_once( + frontend_cpp, + "constexpr uint32_t kMphUs10MorphState = 0x020DA818u;\n", + "// MPH_MULTIROM_RUNTIME_PROFILE: morph address comes from the base ROM profile.\n", + "morph address comes from the base ROM profile", + ) + patch_once( + frontend_cpp, + " const bool in_ball =\n" + " bus_read_u8_slow(kMphUs10MorphState) == 0x02u;\n", + " const bool in_ball = nds_title_patches_mph_in_ball();\n", + "nds_title_patches_mph_in_ball()", + ) + + patch_once( + main_cpp, + " rom_sha1 = gba::sha1(rom.data(), rom.size()).hex();\n" + " std::fprintf(stderr, \"[load] cartridge: %zu bytes, SHA-1 %s\\n\",\n" + " rom.size(), rom_sha1.c_str());\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1) {\n" + " std::fprintf(stderr,\n" + " \"refusing to start: game config expects ROM SHA-1 \"\n" + " \"%s, got %s\\n\",\n" + " frontend_options.expected_rom_sha1.c_str(),\n" + " rom_sha1.c_str());\n" + " return 1;\n" + " }\n", + " rom_sha1 = gba::sha1(rom.data(), rom.size()).hex();\n" + " std::fprintf(stderr, \"[load] cartridge: %zu bytes, SHA-1 %s\\n\",\n" + " rom.size(), rom_sha1.c_str());\n" + " // MPH_MULTIROM_CONTENT_GATE: choose a base layout before the\n" + " // generic exact-SHA gate. The whole-ROM hash still identifies\n" + " // generated content; only canonical executable-equivalent data\n" + " // variants may reuse a clean build.\n" + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()), rom_sha1.c_str(),\n" + " frontend_options.expected_rom_sha1.c_str());\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n" + " std::fprintf(stderr,\n" + " \"refusing to start: game config expects ROM SHA-1 \"\n" + " \"%s, got %s\\n\",\n" + " frontend_options.expected_rom_sha1.c_str(),\n" + " rom_sha1.c_str());\n" + " return 1;\n" + " }\n", + "MPH_MULTIROM_CONTENT_GATE", + ) + patch_once( + main_cpp, + " mph_mouse_aim_policy =\n" + " rom_sha1 == \"90164d1ac127ee5f9815ea4ae7de798c7b5fc629\" &&\n" + " frontend_options.relative_mouse_touch;\n", + " // MPH_MULTIROM_HOST_WRITE_GATE: header fallback alone never enables\n" + " // direct host RAM writes; an authoritative executable checksum is required.\n" + " mph_mouse_aim_policy =\n" + " nds_title_patches_mph_host_writes_compatible() &&\n" + " frontend_options.relative_mouse_touch;\n", + "MPH_MULTIROM_HOST_WRITE_GATE", + ) + + print( + f"Patched ndsrecomp MPH runtime base profiles: " + + ", ".join(str(profile["key"]) for profile in profiles) + + f" ({len(checksums)} authoritative executable checksums)" + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--profiles", type=Path, required=True) + args = parser.parse_args() + patch_runner(args.framework_root.resolve(), args.profiles.resolve()) + + +if __name__ == "__main__": + main() diff --git a/tools/patch_ndsrecomp_mph_widescreen.py b/tools/patch_ndsrecomp_mph_widescreen.py new file mode 100755 index 0000000..3f93c11 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_widescreen.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Layer profile-aware MPH 21:9 projection/culling patches onto ndsrecomp. + +Addresses and guards mirror melonPrimeDS MelonPrimePatchAspectRatio.cpp and +MelonPrimeGameRomAddrTable.h, cross-checked against mphCodex Widescreen.md. +Only an authoritative executable checksum may enable these host code/data +writes. Header-only fallback detection remains fail-closed. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +EXPECTED_KEYS = {"US1_0", "US1_1", "EU1_0", "EU1_1", "JP1_0", "JP1_1", "KR1_0"} +FIELDS = ("scale_patch_addr1", "scale_patch_addr2", "scale_value_addr") +RAM_MIN = 0x02000000 +RAM_MAX = 0x023FFFFF + + +def parse_addr(value: object, where: str) -> int: + if not isinstance(value, str): + raise SystemExit(f"{where}: expected hex string") + try: + address = int(value, 0) + except ValueError as exc: + raise SystemExit(f"{where}: invalid address {value!r}") from exc + if not RAM_MIN <= address <= RAM_MAX: + raise SystemExit(f"{where}: 0x{address:08X} outside DS main RAM") + return address + + +def load_profiles(path: Path) -> list[dict[str, object]]: + registry = json.loads(path.read_text(encoding="utf-8")) + profiles = registry.get("runtime_profiles") + if not isinstance(profiles, dict) or set(profiles) != EXPECTED_KEYS: + raise SystemExit("runtime_profiles must contain exactly the seven MPH base revisions") + result: list[dict[str, object]] = [] + for key, item in profiles.items(): + if not isinstance(item, dict) or not isinstance(item.get("runtime"), dict): + raise SystemExit(f"{key}: missing runtime object") + runtime = item["runtime"] + row: dict[str, object] = {"key": key} + for field in FIELDS: + row[field] = parse_addr(runtime.get(field), f"{key}.runtime.{field}") + if int(row["scale_value_addr"]) & 3: + raise SystemExit(f"{key}.runtime.scale_value_addr must be word-aligned") + result.append(row) + return result + + +def header_text(profiles: list[dict[str, object]]) -> str: + rows = "\n".join( + ' {"%s", 0x%08Xu, 0x%08Xu, 0x%08Xu},' + % ( + p["key"], p["scale_patch_addr1"], p["scale_patch_addr2"], + p["scale_value_addr"], + ) + for p in profiles + ) + return f'''#pragma once + +#include +#include + +// Generated from config/mph_rom_profiles.json. +// Source of truth: melonPrimeDS MelonPrimeGameRomAddrTable.h. +struct NdsMphWidescreenProfile {{ + const char* key; + uint32_t scale_patch_addr1; + uint32_t scale_patch_addr2; + uint32_t scale_value_addr; +}}; + +inline constexpr std::array kNdsMphWidescreenProfiles{{{{ +{rows} +}}}}; +''' + + +def patch_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + if old not in text: + raise SystemExit( + f"Refusing to patch {path}: expected pinned ndsrecomp preimage " + f"for {marker!r} was not found" + ) + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def patch(framework: Path, registry: Path) -> None: + src = framework / "runner" / "src" + title_h = src / "title_patches.h" + title_cpp = src / "title_patches.cpp" + main_cpp = src / "main.cpp" + for path in (title_h, title_cpp, main_cpp): + if not path.is_file(): + raise SystemExit(f"runner source missing: {path}") + + profiles = load_profiles(registry) + (src / "mph_widescreen_profiles.generated.h").write_text( + header_text(profiles), encoding="utf-8" + ) + + patch_once( + title_h, + "bool nds_title_patches_mph_in_ball();\n", + "bool nds_title_patches_mph_in_ball();\n" + "// MPH_MULTIROM_WIDESCREEN: profile-aware 21:9 projection/culling patch.\n" + "bool nds_title_patches_mph_detected();\n" + "void nds_title_patches_set_mph_adaptive(bool enabled);\n", + "MPH_MULTIROM_WIDESCREEN", + ) + patch_once( + title_cpp, + '#include "mph_runtime_profiles.generated.h" // MPH_MULTIROM_PROFILE_HEADER\n', + '#include "mph_runtime_profiles.generated.h" // MPH_MULTIROM_PROFILE_HEADER\n' + '#include "mph_widescreen_profiles.generated.h" // MPH_MULTIROM_WIDESCREEN_HEADER\n', + "MPH_MULTIROM_WIDESCREEN_HEADER", + ) + patch_once( + title_cpp, + "bool g_mph_allow_rom_sha1_mismatch = false;\n", + "bool g_mph_allow_rom_sha1_mismatch = false;\n" + "// MPH_MULTIROM_WIDESCREEN_STATE\n" + "bool g_mph_adaptive = false;\n" + "bool g_mph_aspect_ratio_applied = false;\n" + "constexpr uint32_t kMphScaleOrig1 = 0xE5991664u;\n" + "constexpr uint32_t kMphScaleOrig2 = 0xE59A1664u;\n" + "constexpr uint32_t kMphScale21x9Instr = 0xE3A0106Du;\n" + "constexpr uint16_t kMphScaleOrigValue = 0x1555u;\n" + "constexpr uint16_t kMphScale21x9Value = 0x2555u;\n", + "MPH_MULTIROM_WIDESCREEN_STATE", + ) + patch_once( + title_cpp, + "\n} // namespace\n\nvoid nds_title_patches_set_sm64ds_adaptive(bool enabled) {\n", + "\nconst NdsMphWidescreenProfile* mph_widescreen_profile() {\n" + " if (!g_mph_runtime_profile) return nullptr;\n" + " for (const auto& profile : kNdsMphWidescreenProfiles)\n" + " if (std::strcmp(profile.key, g_mph_runtime_profile->key) == 0)\n" + " return &profile;\n" + " return nullptr;\n" + "}\n\n" + "void patch_mph_aspect_ratio() {\n" + " if (!g_mph_adaptive || g_mph_aspect_ratio_applied ||\n" + " !g_mph_runtime_profile || !g_mph_host_writes_compatible)\n" + " return;\n" + " const auto* wide = mph_widescreen_profile();\n" + " if (!wide) return;\n" + " int32_t first = 0, second = 0, scale_word = 0;\n" + " if (!read_main_ram32(wide->scale_patch_addr1, &first) ||\n" + " !read_main_ram32(wide->scale_patch_addr2, &second) ||\n" + " !read_main_ram32(wide->scale_value_addr, &scale_word))\n" + " return;\n" + " // Fail closed before doing any partial write. These are the exact\n" + " // guards used by melonPrimeDS for the three MPH widescreen patches.\n" + " if (static_cast(first) != kMphScaleOrig1 ||\n" + " static_cast(second) != kMphScaleOrig2 ||\n" + " (static_cast(scale_word) & 0xFFFFu) != kMphScaleOrigValue)\n" + " return;\n" + " bus_write_u32_slow(wide->scale_patch_addr1, kMphScale21x9Instr);\n" + " bus_write_u32_slow(wide->scale_patch_addr2, kMphScale21x9Instr);\n" + " const uint32_t patched_scale =\n" + " (static_cast(scale_word) & 0xFFFF0000u) | kMphScale21x9Value;\n" + " bus_write_u32_slow(wide->scale_value_addr, patched_scale);\n" + " g_mph_aspect_ratio_applied = true;\n" + " std::fprintf(stderr,\n" + " \"[mph] adaptive 21:9 projection/culling enabled for %s\\n\",\n" + " g_mph_runtime_profile->key);\n" + "}\n\n" + "} // namespace\n\nvoid nds_title_patches_set_sm64ds_adaptive(bool enabled) {\n", + "patch_mph_aspect_ratio()", + ) + patch_once( + title_cpp, + " g_mph_allow_rom_sha1_mismatch = false;\n" + " if (!rom_data || !rom_sha1 || !expected_rom_sha1 || rom_size <= 0x1Eu)\n", + " g_mph_allow_rom_sha1_mismatch = false;\n" + " g_mph_adaptive = false;\n" + " g_mph_aspect_ratio_applied = false;\n" + " if (!rom_data || !rom_sha1 || !expected_rom_sha1 || rom_size <= 0x1Eu)\n", + "g_mph_aspect_ratio_applied = false", + ) + patch_once( + title_cpp, + "bool nds_title_patches_mph_host_writes_compatible() {\n", + "bool nds_title_patches_mph_detected() {\n" + " return g_mph_runtime_profile != nullptr;\n" + "}\n\n" + "void nds_title_patches_set_mph_adaptive(bool enabled) {\n" + " g_mph_adaptive = enabled && g_mph_runtime_profile &&\n" + " g_mph_host_writes_compatible;\n" + " if (!g_mph_adaptive) g_mph_aspect_ratio_applied = false;\n" + "}\n\n" + "bool nds_title_patches_mph_host_writes_compatible() {\n", + "nds_title_patches_mph_detected()", + ) + patch_once( + title_cpp, + "void nds_title_patches_start_frame() {\n" + " if (g_sm64ds_adaptive) patch_sm64ds_clipper();\n" + "}\n", + "void nds_title_patches_start_frame() {\n" + " if (g_sm64ds_adaptive) patch_sm64ds_clipper();\n" + " if (g_mph_adaptive) patch_mph_aspect_ratio();\n" + "}\n", + "if (g_mph_adaptive) patch_mph_aspect_ratio();", + ) + + patch_once( + main_cpp, + " sm64ds_wide_policy = true;\n" + " }\n" + "#endif\n" + " if (interactive &&\n", + " sm64ds_wide_policy = true;\n" + " }\n" + "#endif\n" + " // MPH_MULTIROM_WIDESCREEN_GATE: a header-only base-profile hint is\n" + " // insufficient for host code/data writes. Keep the UI option visible,\n" + " // but force native presentation when executable compatibility is unknown.\n" + " if (nds_title_patches_mph_detected() &&\n" + " !nds_title_patches_mph_host_writes_compatible()) {\n" + " if ((frontend_options.adaptive_screens & NDS_ADAPTIVE_TOP) != 0u)\n" + " std::fprintf(stderr,\n" + " \"[mph] adaptive widescreen disabled: unknown executable checksum\\n\");\n" + " frontend_options.adaptive_screens &= ~NDS_ADAPTIVE_TOP;\n" + " frontend_options.adaptive_hud_anchor = false;\n" + " }\n" + " if (interactive &&\n", + "MPH_MULTIROM_WIDESCREEN_GATE", + ) + patch_once( + main_cpp, + " nds_title_patches_set_sm64ds_adaptive(\n" + " sm64ds_wide_policy &&\n" + " (frontend_options.adaptive_screens & NDS_ADAPTIVE_TOP) != 0u);\n" + " nds_title_patches_set_mph_mouse_aim(mph_mouse_aim_policy);\n", + " nds_title_patches_set_sm64ds_adaptive(\n" + " sm64ds_wide_policy &&\n" + " (frontend_options.adaptive_screens & NDS_ADAPTIVE_TOP) != 0u);\n" + " nds_title_patches_set_mph_adaptive(\n" + " nds_title_patches_mph_host_writes_compatible() &&\n" + " (frontend_options.adaptive_screens & NDS_ADAPTIVE_TOP) != 0u);\n" + " nds_title_patches_set_mph_mouse_aim(mph_mouse_aim_policy);\n", + "nds_title_patches_set_mph_adaptive(", + ) + + print("Patched MPH adaptive 21:9 runtime profiles: " + + ", ".join(str(p["key"]) for p in profiles)) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--profiles", type=Path, required=True) + args = parser.parse_args() + patch(args.framework_root.resolve(), args.profiles.resolve()) + + +if __name__ == "__main__": + main() diff --git a/tools/patch_ndsrecomp_mph_widescreen_reset.py b/tools/patch_ndsrecomp_mph_widescreen_reset.py new file mode 100755 index 0000000..c3a5bec --- /dev/null +++ b/tools/patch_ndsrecomp_mph_widescreen_reset.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Make the MPH widescreen patch survive guest resets without re-identifying ROM.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--profiles", type=Path, required=True) + args = parser.parse_args() + path = args.framework_root.resolve() / "runner" / "src" / "title_patches.cpp" + text = path.read_text(encoding="utf-8") + marker = "MPH_MULTIROM_WIDESCREEN_RESET_SAFE" + if marker in text: + return + old = ( + " if (!g_mph_adaptive || g_mph_aspect_ratio_applied ||\n" + " !g_mph_runtime_profile || !g_mph_host_writes_compatible)\n" + " return;\n" + ) + new = ( + " // MPH_MULTIROM_WIDESCREEN_RESET_SAFE: inspect the guarded words each\n" + " // frame. Once patched they no longer match the stock preimage, while a\n" + " // guest reset restores the stock words and makes the patch eligible again.\n" + " if (!g_mph_adaptive || !g_mph_runtime_profile ||\n" + " !g_mph_host_writes_compatible)\n" + " return;\n" + ) + if old not in text: + raise SystemExit("widescreen reset-safe preimage not found") + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/tools/probe_mph_online_first_run.py b/tools/probe_mph_online_first_run.py new file mode 100644 index 0000000..dab370a --- /dev/null +++ b/tools/probe_mph_online_first_run.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +"""Validate MPH Wiimmfi setup and persistence across real process restarts. + +The runner stays in interactive mode. Navigation, screenshots, and lifecycle +control all use its TCP debug surface; input/checkpoint primitives come from +fuzz_mph_gameplay. The source ROM, save, and identity are copied/read-only. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import random +import shutil +import subprocess +import sys +import time +from pathlib import Path +from typing import Any + +from PIL import Image + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +import capture_mph_checkpoints as capture_lib # noqa: E402 +import fuzz_mph_gameplay as input_lib # noqa: E402 +import mph_screens # noqa: E402 +from add_mph_friend import MENU_PATH # noqa: E402 +from run_mph_friend_match import DIALOG_YES, FRIENDS_AND_RIVALS # noqa: E402 +from run_mph_wfc_instances import FILTERS # noqa: E402 + +INPUT_FUZZ = random.Random() + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def wait_frames( + client: capture_lib.DebugClient, + process: subprocess.Popen[bytes], + count: int, + timeout: float = 300.0, +) -> None: + start = input_lib.event_counts(client)["vblank9"] + target = start + count + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError(f"runner exited with code {process.returncode}") + if input_lib.event_counts(client)["vblank9"] >= target: + return + time.sleep(0.025) + raise TimeoutError(f"runner did not advance {count} frames") + + +def tap( + client: capture_lib.DebugClient, + process: subprocess.Popen[bytes], + x: int, + y: int, + wait: int, +) -> None: + fuzzed_x = max(0, min(255, x + INPUT_FUZZ.randint(-2, 2))) + fuzzed_y = max(0, min(191, y + INPUT_FUZZ.randint(-2, 2))) + client.command("touch", x=fuzzed_x, y=fuzzed_y, down=True) + wait_frames(client, process, INPUT_FUZZ.randint(8, 14)) + client.command("touch", x=fuzzed_x, y=fuzzed_y, down=False) + wait_frames(client, process, wait) + + +def press( + client: capture_lib.DebugClient, + process: subprocess.Popen[bytes], + key: str, + wait: int, +) -> None: + bit = input_lib.KEY_BITS[key] + client.command("keys", mask=input_lib.RELEASED_KEYS & ~(1 << bit)) + wait_frames(client, process, INPUT_FUZZ.randint(8, 14)) + client.command("keys", mask=input_lib.RELEASED_KEYS) + wait_frames(client, process, wait) + + +class InteractiveSession: + def __init__( + self, + args: argparse.Namespace, + name: str, + port: int, + profile: Path, + ) -> None: + self.output = args.out / name + self.output.mkdir(parents=True, exist_ok=True) + self.report: list[dict[str, Any]] = [] + self.stdout = (self.output / "runner.stdout.log").open("wb") + self.stderr = (self.output / "runner.stderr.log").open("wb") + command = [ + str(args.runner), str(profile / "bios"), "--interactive", + "--port", str(port), "--rom", str(args.rom), + "--config", str(args.config), "--save-path", + str(profile / "Metroid Prime Hunters.sav"), + "--firmware-state-path", str(profile / "firmware-generated.bin"), + "--startup-mode", "automatic", "--network", "on", + "--network-backend", "slirp", "--wfc", "on", + "--wfc-provider", args.wfc_provider, "--freebios", + "--generated-firmware", "--boot", "direct", + ] + self.process = subprocess.Popen( + command, cwd=args.runner.parent, stdout=self.stdout, + stderr=self.stderr, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + capture_lib.wait_for_server(port, self.process) + self.client = capture_lib.DebugClient(port, timeout=600.0) + + def save(self, label: str) -> dict[str, Any]: + item = input_lib.save_checkpoint( + self.client, self.output, len(self.report), label + ) + self.report.append(item) + return item + + def close_handles(self) -> None: + try: + self.client.close() + except OSError: + pass + self.stdout.close() + self.stderr.close() + (self.output / "report.json").write_text( + json.dumps(self.report, indent=2) + "\n", encoding="utf-8" + ) + + def force_cleanup(self) -> None: + if self.process.poll() is None: + self.process.terminate() + try: + self.process.wait(timeout=10) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait() + self.close_handles() + + +def reach_wfc_menu(session: InteractiveSession) -> None: + wait_frames(session.client, session.process, 7800) + session.save("title") + for label, x, y, wait in MENU_PATH[:4]: + tap(session.client, session.process, x, y, wait) + session.save(label) + + +def setup_and_power_off(session: InteractiveSession) -> dict[str, Any]: + reach_wfc_menu(session) + route = ( + ("wfc-setup-root", 128, 36, 600), + ("settings-tile", 85, 100, 220), + ("slot1", 43, 35, 220), + ("search-for-ap", 128, 37, 1600), + ("test-connection", 192, 36, 2400), + ("save-settings", 190, 177, 600), + ) + for label, x, y, wait in route: + tap(session.client, session.process, x, y, wait) + session.save(label) + press(session.client, session.process, "b", 600) + session.save("setup-root-after-back") + press(session.client, session.process, "b", 600) + prompt = session.save("system-will-shut-down") + + rings = { + name: session.client.command("net_ring_dump", max=256, filter=name) + for name in FILTERS + } + counts = { + name: len(value.get("events", [])) if isinstance(value, dict) else 0 + for name, value in rings.items() + } + if counts.get("dhcp", 0) == 0 or counts.get("backend_error", 0) != 0: + raise RuntimeError(f"connection test did not succeed cleanly: {counts}") + + # The green check confirms the firmware's terminal shutdown prompt. + session.client.command("touch", x=128, y=128, down=True) + try: + session.process.wait(timeout=30) + except subprocess.TimeoutExpired as exc: + raise RuntimeError("guest power-off left the application open") from exc + if session.process.returncode != 0: + raise RuntimeError( + f"guest power-off returned {session.process.returncode}" + ) + session.close_handles() + stderr_text = (session.output / "runner.stderr.log").read_text( + encoding="utf-8", errors="replace" + ) + if "[sdl] guest requested power-off; closing" not in stderr_text: + raise RuntimeError("runner did not report the guest power-off exit") + return {"prompt": prompt, "net_counts": counts, "returncode": 0} + + +def direct_online( + session: InteractiveSession, + require_no_notice_pages: bool, +) -> dict[str, Any]: + reach_wfc_menu(session) + tap(session.client, session.process, *FRIENDS_AND_RIVALS, 240) + session.save("friends-rivals") + + arrow_pages = 0 + yes_pages = 0 + ok_pages = 0 + for index in range(8): + item = session.save(f"dialog-{index}") + image = Image.open(session.output / str(item["image"])).convert("RGB") + ok_bright = mph_screens.bright_count( + image, mph_screens.BOXES["dialog_ok"] + ) + if mph_screens.bright_count( + image, mph_screens.BOXES["dialog_arrow"]) > 40: + arrow_pages += 1 + tap(session.client, session.process, 190, 120, 240) + elif mph_screens.is_connect_dialog(image): + yes_pages += 1 + tap(session.client, session.process, *DIALOG_YES, 240) + elif 40 < ok_bright < 200: + ok_pages += 1 + tap(session.client, session.process, 128, 128, 240) + elif ok_bright >= 200: + # The animated connection badge shares the acknowledgement's + # screen region. Leave it alone and wait for authentication. + break + else: + break + wait_frames(session.client, session.process, 2400) + final = session.save("post-connect") + rings = { + name: session.client.command("net_ring_dump", max=256, filter=name) + for name in FILTERS + } + counts = { + name: len(value.get("events", [])) if isinstance(value, dict) else 0 + for name, value in rings.items() + } + if counts.get("tls_record", 0) == 0 or counts.get("backend_error", 0) != 0: + raise RuntimeError(f"Wiimmfi authentication failed: {counts}") + if require_no_notice_pages and (arrow_pages != 0 or ok_pages != 0): + raise RuntimeError( + "returning profile repeated pairing/update pages: " + f"arrow={arrow_pages}, ok={ok_pages}" + ) + + response = session.client.command("frontend_exit") + if not isinstance(response, dict) or not response.get("requested"): + raise RuntimeError(f"frontend_exit was not accepted: {response!r}") + session.process.wait(timeout=30) + if session.process.returncode != 0: + raise RuntimeError( + f"normal window close returned {session.process.returncode}" + ) + session.close_handles() + return { + "final": final, + "arrow_pages": arrow_pages, + "yes_pages": yes_pages, + "ok_pages": ok_pages, + "net_counts": counts, + "returncode": 0, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runner", type=Path, required=True) + parser.add_argument("--rom", type=Path, required=True) + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--source-save", type=Path, required=True) + parser.add_argument("--source-identity", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--port", type=int, default=21020) + parser.add_argument("--wfc-provider", default="wiimmfi") + parser.add_argument("--input-fuzz-seed", type=int, default=0x4D5048) + parser.add_argument( + "--resume-after-setup", action="store_true", + help="reuse an existing profile after a successful setup/power-off run", + ) + args = parser.parse_args() + INPUT_FUZZ.seed(args.input_fuzz_seed) + for key in ("runner", "rom", "config", "source_save", "source_identity"): + setattr(args, key, getattr(args, key).resolve()) + args.out = args.out.resolve() + args.out.mkdir(parents=True, exist_ok=True) + profile = args.out / "profile" + (profile / "bios").mkdir(parents=True, exist_ok=True) + + source_hashes = { + "save": sha256(args.source_save), + "identity": sha256(args.source_identity), + } + if not args.resume_after_setup: + shutil.copy2(args.source_save, profile / "Metroid Prime Hunters.sav") + shutil.copy2( + args.source_identity, profile / "bios" / "generated-identity.bin" + ) + elif not (profile / "firmware-generated.bin").is_file(): + parser.error("--resume-after-setup requires an existing profile state") + + summary: dict[str, Any] = { + "input_fuzz_seed": args.input_fuzz_seed, + "source_hashes": source_hashes, + } + sessions: list[InteractiveSession] = [] + try: + state = profile / "firmware-generated.bin" + cartridge = profile / "Metroid Prime Hunters.sav" + if not args.resume_after_setup: + first = InteractiveSession( + args, "01-setup-poweroff", args.port, profile + ) + sessions.append(first) + summary["setup"] = setup_and_power_off(first) + summary["state_after_setup"] = { + "size": state.stat().st_size, "sha256": sha256(state) + } + summary["save_after_setup"] = sha256(cartridge) + else: + summary["setup"] = "resumed" + + online_port = args.port + (0 if args.resume_after_setup else 1) + second = InteractiveSession(args, "02-first-online", online_port, profile) + sessions.append(second) + summary["first_online"] = direct_online(second, False) + summary["state_after_first_online"] = sha256(state) + summary["save_after_first_online"] = sha256(cartridge) + + third = InteractiveSession(args, "03-returning-online", online_port + 1, profile) + sessions.append(third) + summary["returning_online"] = direct_online(third, True) + summary["state_after_returning_online"] = sha256(state) + summary["save_after_returning_online"] = sha256(cartridge) + finally: + for session in sessions: + if session.process.poll() is None: + session.force_cleanup() + + if sha256(args.source_save) != source_hashes["save"] or \ + sha256(args.source_identity) != source_hashes["identity"]: + raise RuntimeError("source save/identity changed during QA") + summary["sources_unchanged"] = True + summary["success"] = True + (args.out / "summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8" + ) + print(json.dumps(summary, indent=2), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/probe_mph_wfc.py b/tools/probe_mph_wfc.py index 52c9c81..16619ba 100644 --- a/tools/probe_mph_wfc.py +++ b/tools/probe_mph_wfc.py @@ -148,6 +148,12 @@ def run(args: argparse.Namespace) -> dict[str, Any]: command.append("--no-save") if args.firmware_path: command.extend(["--firmware-path", str(args.firmware_path.resolve())]) + if args.firmware_state_path: + command.extend([ + "--firmware-state-path", str(args.firmware_state_path.resolve()) + ]) + if args.no_dumps: + command.extend(["--freebios", "--generated-firmware", "--boot", "direct"]) if args.discover_static_misses: command.append("--discover-static-misses") @@ -195,7 +201,12 @@ def run(args: argparse.Namespace) -> dict[str, Any]: ("settings-tile", 85, 100, 220), ("slot1", 43, 35, 220), ("search-for-ap", 128, 37, 1600), - ("ap-row", 50, 65, 220), + # Generated firmware already advertises the runner's + # ndsrecomp AP. Discovery selects it and returns to + # Connection 1 Settings; the old probe tapped (50,65) + # here, which is the SSID row and opened the keyboard. + ("test-connection", 192, 36, 2400), + ("save-settings", 190, 177, 600), ) else: if args.flow == "friends-rivals": @@ -215,9 +226,12 @@ def run(args: argparse.Namespace) -> dict[str, Any]: save(label) if args.flow == "setup": - input_lib.press_key(client, "a", 12) - input_lib.advance_frames(client, 300) - save("after-a-start-test") + input_lib.press_key(client, "b", 12) + input_lib.advance_frames(client, 600) + save("setup-root-after-back") + input_lib.press_key(client, "b", 12) + input_lib.advance_frames(client, 600) + save("exit-prompt") if args.flow == "search-game": for target in args.targets: @@ -272,7 +286,7 @@ def run(args: argparse.Namespace) -> dict[str, Any]: firmware = client.command("firmware_dump") if not isinstance(firmware, dict): raise RuntimeError("firmware_dump returned a non-object response") - if int(firmware.get("size", 0)) != 262144: + if int(firmware.get("size", 0)) not in (131072, 262144): raise RuntimeError( f"firmware_dump returned {firmware.get('size')} bytes" ) @@ -313,6 +327,8 @@ def main() -> int: parser.add_argument("--config", type=Path, default=Path("game.toml")) parser.add_argument("--save-path", type=Path) parser.add_argument("--firmware-path", type=Path) + parser.add_argument("--firmware-state-path", type=Path) + parser.add_argument("--no-dumps", action="store_true") parser.add_argument("--firmware-out", type=Path) parser.add_argument("--discover-static-misses", action="store_true") parser.add_argument( @@ -357,6 +373,8 @@ def main() -> int: parser.error("--port must be in 1..65535") if args.instance_index < 0 or args.instance_index > 255: parser.error("--instance-index must be in 0..255") + if args.no_dumps and args.firmware_path: + parser.error("--no-dumps and --firmware-path are mutually exclusive") summary = run(args) print(json.dumps(summary, indent=2), flush=True) From b354dc53e9e0bdfb4937cc6e959b7ac8e7ecd01e Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:09:53 +0900 Subject: [PATCH 092/164] Fix melonPrime widescreen table cross-check --- tools/check_mph_multirom_profiles.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index f941e6c..eee8dc5 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -78,15 +78,15 @@ def validate_scale_registry(repo: Path, table: Path | None) -> None: if table: text = table.read_text(encoding="utf-8") - row_names = { - "scale_patch_addr1": "ScalePatchAddr1", - "scale_patch_addr2": "ScalePatchAddr2", - "scale_value_addr": "ScaleValueAddr", + rows = { + "scale_patch_addr1": ("scalePatchAddr1", "ScalePatchAddr1"), + "scale_patch_addr2": ("scalePatchAddr2", "ScalePatchAddr2"), + "scale_value_addr": ("scaleValueAddr", "ScaleValueAddr"), } order = ("JP1_0", "JP1_1", "US1_0", "US1_1", "EU1_0", "EU1_1", "KR1_0") - for field, list_name in row_names.items(): + for field, (member_name, list_name) in rows.items(): match = re.search( - rf"X\(ADDR,\s*{field},\s*{list_name},\s*([^\n]+)\)", text + rf"X\(ADDR,\s*{member_name},\s*{list_name},\s*([^\n]+)\)", text ) if not match: die(f"melonPrimeDS table missing {list_name}") From e916b9693858b86dcec4cd4fe2057b460065bb39 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:11:33 +0900 Subject: [PATCH 093/164] Refresh melonPrime executable checksum table --- config/mph_rom_profiles.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index a82e670..a9215e4 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -140,22 +140,22 @@ } }, "runtime_checksums": [ - {"crc32": "0x218DA42C", "profile": "US1_0", "name": "USA v1.0"}, - {"crc32": "0x91B46577", "profile": "US1_1", "name": "USA v1.1"}, - {"crc32": "0xA4A8FE5A", "profile": "EU1_0", "name": "Europe v1.0"}, - {"crc32": "0x910018A5", "profile": "EU1_1", "name": "Europe v1.1"}, - {"crc32": "0xD75F539D", "profile": "JP1_0", "name": "Japan v1.0"}, - {"crc32": "0x42EBF348", "profile": "JP1_1", "name": "Japan v1.1"}, - {"crc32": "0xE54682F3", "profile": "KR1_0", "name": "Korea v1.0"}, - {"crc32": "0x5E596D48", "profile": "US1_0", "name": "USA v1.0 encrypted"}, - {"crc32": "0xF4D3CC2C", "profile": "US1_1", "name": "USA v1.1 encrypted"}, - {"crc32": "0x5D9E56DA", "profile": "EU1_0", "name": "Europe v1.0 encrypted"}, - {"crc32": "0x3A07BD61", "profile": "EU1_1", "name": "Europe v1.1 encrypted"}, - {"crc32": "0xB89E71A3", "profile": "JP1_0", "name": "Japan v1.0 encrypted"}, - {"crc32": "0x16852A2C", "profile": "JP1_1", "name": "Japan v1.1 encrypted"}, - {"crc32": "0x3FA948F1", "profile": "KR1_0", "name": "Korea v1.0 encrypted"}, - {"crc32": "0xD8E3D7F0", "profile": "EU1_1", "name": "Europe v1.1 Balanced"}, - {"crc32": "0x60A938F2", "profile": "EU1_1", "name": "Europe v1.1 Balanced encrypted"}, - {"crc32": "0x0F4C5A07", "profile": "EU1_1", "name": "Europe v1.1 Russian"} + {"crc32": "0x91B46577", "profile": "US1_1", "name": "US1.1"}, + {"crc32": "0x01476E8F", "profile": "US1_1", "name": "US1.1 ENCRYPTED"}, + {"crc32": "0x218DA42C", "profile": "US1_0", "name": "US1.0"}, + {"crc32": "0xE048CD92", "profile": "US1_0", "name": "US1.0 ENCRYPTED"}, + {"crc32": "0x910018A5", "profile": "EU1_1", "name": "EU1.1"}, + {"crc32": "0x31703770", "profile": "EU1_1", "name": "EU1.1 ENCRYPTED"}, + {"crc32": "0x948B1E48", "profile": "EU1_1", "name": "EU1.1 BALANCED"}, + {"crc32": "0x2970A14F", "profile": "EU1_1", "name": "EU1.1 BALANCED V1.2.11"}, + {"crc32": "0x9E20F3A8", "profile": "EU1_1", "name": "EU1.1 RUSSIANED"}, + {"crc32": "0xA4A8FE5A", "profile": "EU1_0", "name": "EU1.0"}, + {"crc32": "0x979BB267", "profile": "EU1_0", "name": "EU1.0 ENCRYPTED"}, + {"crc32": "0xD75F539D", "profile": "JP1_0", "name": "JP1.0"}, + {"crc32": "0xE795A10C", "profile": "JP1_0", "name": "JP1.0 ENCRYPTED"}, + {"crc32": "0x42EBF348", "profile": "JP1_1", "name": "JP1.1"}, + {"crc32": "0x0A1203A5", "profile": "JP1_1", "name": "JP1.1 ENCRYPTED"}, + {"crc32": "0xE54682F3", "profile": "KR1_0", "name": "KR1.0"}, + {"crc32": "0xC26916F3", "profile": "KR1_0", "name": "KR1.0 ENCRYPTED"} ] } From d5b6ea460d4f82ac29cfdf4d674bb400b4fa36ac Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:12:50 +0900 Subject: [PATCH 094/164] Restore complete build profile metadata --- config/mph_rom_profiles.json | 79 +++++++++++++++++------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/config/mph_rom_profiles.json b/config/mph_rom_profiles.json index a9215e4..838996a 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -5,47 +5,6 @@ "runtime_checksum_source": "https://github.com/ag-advania/melonPrimeDS/blob/develop_hud/src/NDSCart/CartCommon.cpp", "aspect_ratio_patch_source": "https://github.com/ag-advania/melonPrimeDS/blob/main/src/frontend/qt_sdl/MelonPrimePatchAspectRatio.cpp", "widescreen_analysis_source": "https://github.com/Zection6V/mphCodex/blob/main/mnt/data/analysis/mphAnalysis/_Commons/Widescreen.md", - "identity_policy": { - "runtime_base_profile": "melonPrimeDS executable checksum first; exact game_code + supported revision is fallback identification only", - "host_write_gate": "Aim, Morph, and Adaptive Widescreen host RAM/code access requires an authoritative known executable checksum", - "known_clean_identity": "whole-ROM SHA-1 identifies an exact clean content image but never selects runtime addresses", - "actual_content_identity": "whole-ROM SHA-1 namespaces generated banks, coverage, checkpoints, and FMV captures so distinct mods never share generated content" - }, - "profiles": { - "US1_0": { - "base_profile": "US1_0", - "known_clean": true, - "display_name": "USA v1.0", - "game_code": "AMHE", - "region": "USA", - "revision": 0, - "rom_size": 67108864, - "sha1": "90164d1ac127ee5f9815ea4ae7de798c7b5fc629", - "sha256": "7d0a98ff98e1b7c985d1f3d89b01730af1b2115061a4dfea847612d217a8b855", - "coverage": "coverage/adventure-main-entry-points.json", - "game_config": "game.toml", - "fmv_runtime": true, - "fmv_runtime_bank": "mph_arm9_fmv_runtime", - "adaptive_widescreen": true, - "launcher_default_rom": "Metroid Prime Hunters.nds" - }, - "EU1_1": { - "base_profile": "EU1_1", - "known_clean": true, - "display_name": "Europe v1.1", - "game_code": "AMHP", - "region": "Europe", - "revision": 1, - "rom_size": 67108864, - "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", - "coverage": "coverage/eu11-bootstrap-entry-points.json", - "game_config": "config/game-eu11.toml", - "fmv_runtime": false, - "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", - "adaptive_widescreen": true, - "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds" - } - }, "runtime_profiles": { "US1_0": { "game_code": "AMHE", @@ -157,5 +116,41 @@ {"crc32": "0x0A1203A5", "profile": "JP1_1", "name": "JP1.1 ENCRYPTED"}, {"crc32": "0xE54682F3", "profile": "KR1_0", "name": "KR1.0"}, {"crc32": "0xC26916F3", "profile": "KR1_0", "name": "KR1.0 ENCRYPTED"} - ] + ], + "profiles": { + "US1_0": { + "base_profile": "US1_0", + "known_clean": true, + "display_name": "Metroid Prime Hunters (USA rev 0)", + "region": "USA", + "game_code": "AMHE", + "revision": 0, + "rom_size": 67108864, + "sha1": "90164d1ac127ee5f9815ea4ae7de798c7b5fc629", + "program_id": "mph_amhe0", + "coverage": "coverage/adventure-main-entry-points.json", + "game_config": "game.toml", + "fmv_runtime": true, + "fmv_runtime_bank": "mph_arm9_fmv_runtime", + "launcher_default_rom": "Metroid Prime Hunters.nds", + "adaptive_widescreen": true + }, + "EU1_1": { + "base_profile": "EU1_1", + "known_clean": true, + "display_name": "Metroid Prime Hunters (Europe rev 1)", + "region": "Europe", + "game_code": "AMHP", + "revision": 1, + "rom_size": 67108864, + "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", + "program_id": "mph_amhp1", + "coverage": "coverage/eu11-bootstrap-entry-points.json", + "game_config": "config/game-eu11.toml", + "fmv_runtime": false, + "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", + "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds", + "adaptive_widescreen": true + } + } } From 221375c8b37fb56acfb8354269d90cc8554a5356 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:15:17 +0900 Subject: [PATCH 095/164] Document profile-aware widescreen and upstream Wi-Fi state --- docs/EU1_1_BRINGUP.md | 240 ++++++++++++++++++++++++++++-------------- 1 file changed, 161 insertions(+), 79 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 9e590a8..0871764 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -1,7 +1,10 @@ # Metroid Prime Hunters Multi-ROM / EU1.1 Bring-up This document describes the current multi-ROM architecture in this branch. -It supersedes the earlier SHA-1-driven runtime-profile design. +It supersedes the earlier whole-ROM-SHA-1-driven runtime-profile design and +includes the upstream Wi-Fi firmware-state persistence work integrated from +`mstan/MetroidPrimeHuntersRecomp` commit +`5abcfee6187d572e752985ede2364f165d62dd6a`. ## 1. Status @@ -18,6 +21,10 @@ revisions: | `JP1_1` | `AMHJ` | 1 | `0x020DC698` | `0x020E03A6` | `0x020E03AE` | | `KR1_0` | `AMHK` | 0 | `0x020D3EE4` | `0x020D7C0E` | `0x020D7C16` | +Adaptive Widescreen is also statically address-mapped for all seven base +revisions. It is no longer hidden for EU1.1. Actual guest code/data writes are +still fail-closed and require an authoritative executable checksum. + Exact build/capture content profiles currently exist for: - `US1_0` clean retail ROM @@ -29,7 +36,7 @@ they can be called fully supported. ## 2. Sources of truth -Runtime detection and addresses intentionally follow melonPrimeDS +Runtime detection and Aim/Morph addresses intentionally follow melonPrimeDS `develop_hud`: - detector: `src/frontend/qt_sdl/MelonPrimeGameRomDetect.cpp` @@ -37,24 +44,26 @@ Runtime detection and addresses intentionally follow melonPrimeDS - executable checksum algorithm: `src/NDSCart/CartCommon.cpp`, `CartCommon::Checksum()` - CRC32 implementation: `src/CRC32.cpp` -Repository: +Adaptive Widescreen additionally uses: -`https://github.com/ag-advania/melonPrimeDS/tree/develop_hud` +- melonPrimeDS `main/src/frontend/qt_sdl/MelonPrimePatchAspectRatio.cpp` +- melonPrimeDS `main/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h` +- mphCodex `mnt/data/analysis/mphAnalysis/_Commons/Widescreen.md` -The project registry records these source locations in +The registry records these source locations in `config/mph_rom_profiles.json`. ## 3. Identity model -The old design coupled runtime RAM addresses to an exact whole-ROM SHA-1. -That is intentionally no longer the design. +The old design coupled runtime RAM addresses to an exact whole-ROM SHA-1. That +is intentionally no longer the design. There are three separate identities. ### 3.1 Runtime Base Profile A runtime base profile is one of the seven region/revision layouts above. It -owns revision-specific host RAM addresses such as Aim X/Y and Morph / Alt Form. +owns revision-specific host addresses for Aim, Morph and Adaptive Widescreen. The runtime detector uses: @@ -66,7 +75,7 @@ NDS header offsets: - game code: `0x0C..0x0F` - ROM revision/version: `0x1E` -The Recomp detector is deliberately stricter than melonPrimeDS's generic +The Recomp fallback is deliberately stricter than melonPrimeDS's generic `revision != 0 -> 1.1` fallback. Only the seven explicitly supported tuples are accepted. A hypothetical rev2+ is unknown. @@ -79,14 +88,15 @@ melonPrimeDS `CartCommon::Checksum()` is reproduced exactly: 3. continue CRC32 over the ARM7 ROM image A hit in the audited melonPrimeDS checksum table is authoritative for the base -runtime layout and may enable host-side Aim/Morph RAM access. +runtime layout and may enable host-side Aim/Morph/Adaptive-Widescreen RAM/code +access. A header-only match is weaker. It identifies a candidate base profile, but it -**does not authorize host RAM reads/writes**. Unknown executable content remains -fail-closed. +**does not authorize host RAM/code reads or writes**. Unknown executable content +remains fail-closed. This prevents a modified ROM that merely preserves `AMHE` revision 0 from being -blindly treated as memory-compatible US1.0. +blindly treated as executable-compatible US1.0. ### 3.3 Actual Content Identity @@ -105,48 +115,96 @@ Generated title banks remain registered against the actual ROM SHA-1. A bank captured/generated for clean ROM or MOD A is therefore not silently reused for MOD B. -## 4. Known melonPrimeDS executable checksums +## 4. Current authoritative executable checksum table + +The registry mirrors the current `develop_hud` detector table. CI downloads the +detector on every run and fails if the local registry drifts. -The current registry mirrors the audited `develop_hud` detector table, -including: +Current entries include: - all seven clean retail revisions -- encrypted variants -- EU1.1 Balanced variants -- EU1.1 Russian variant (`0x9E20F3A8`) +- encrypted variants for all seven revisions +- EU1.1 Balanced +- EU1.1 Balanced v1.2.11 +- EU1.1 Russian + +The current EU1.1 Russian executable checksum is `0x9E20F3A8`. + +A known checksum establishes the runtime layout. It does **not** by itself mean +the clean recomp build is byte-compatible with that modified executable. + +For example, a known code-modified EU1.1 executable may use EU1.1 runtime host +addresses, while still requiring a separate exact content profile, coverage and +generated banks. + +`Last Raven` is not present in the currently audited `develop_hud` checksum +table. It therefore remains fail-closed for Aim/Morph/Widescreen host writes +until its executable checksum and layout compatibility are explicitly +validated. + +## 5. Adaptive Widescreen across all seven revisions + +The previous EU1.1 bring-up temporarily disabled Adaptive Widescreen because the +US1.0 game-side projection/culling addresses were not known for other revisions. +That limitation is removed. -A known checksum establishes the runtime RAM layout. It does **not** by itself -mean the clean recomp build is byte-compatible with that modified executable. +melonPrimeDS and mphCodex identify three revision-specific locations: -For example, the known EU1.1 Russian executable can use the EU1.1 Aim/Morph RAM -layout, but because its executable checksum differs from canonical clean EU1.1, -it still requires a mod-specific exact build/capture content profile. +| Runtime base | Projection patch 1 | Projection patch 2 | Q12 culling aspect | +|---|---:|---:|---:| +| `US1_0` | `0x02110FFC` | `0x0211C638` | `0x02110820` | +| `US1_1` | `0x02111ABC` | `0x0211D168` | `0x021112E0` | +| `EU1_0` | `0x02111ADC` | `0x0211D114` | `0x02111300` | +| `EU1_1` | `0x02111B5C` | `0x0211D208` | `0x02111380` | +| `JP1_0` | `0x0211313C` | `0x0211E7E8` | `0x02112960` | +| `JP1_1` | `0x021130FC` | `0x0211E7A8` | `0x02112920` | +| `KR1_0` | `0x02109B64` | `0x02114838` | `0x021091A4` | -`Last Raven` is not present in the current audited `develop_hud` checksum table. -It therefore remains fail-closed for host Aim/Morph access until its executable -checksum/layout is explicitly validated. +For 21:9 the runner mirrors melonPrimeDS's guarded patch semantics: -## 5. Clean ROM versus modified ROM startup +- projection 1 preimage: `0xE5991664` +- projection 2 preimage: `0xE59A1664` +- culling-aspect preimage: `0x1555` +- 21:9 projection instruction: `0xE3A0106D` +- 21:9 Q12 aspect: `0x2555` + +All three preimages are verified before the first write. If any guard does not +match, no partial aspect-ratio patch is applied. + +The UI may expose Adaptive Widescreen for a supported content profile, but the +runner is authoritative: + +- known executable checksum + compatible base profile: enable profile-specific + projection/culling patch and adaptive top-screen rendering +- header-only fallback: keep runtime base identification, but force the actual + adaptive top-screen patch path off because executable compatibility has not + been established +- unknown/non-MPH identity: normal fail-closed behavior + +The patch checks the guarded guest words again on later frames, so an in-process +guest reset that restores the stock instructions can be patched again safely. + +## 6. Clean ROM versus modified ROM startup The recomp-ui launcher intentionally does not enforce the clean whole-ROM SHA. The launcher passes the selected `.nds` file to the runner, where the stronger -multi-layer detector can decide safely. +multi-layer detector decides safely. The runner rules are: - exact expected whole-ROM SHA: normal exact-content build - different whole-ROM SHA + canonical clean header/ARM9/ARM7 checksum of the expected base: clean build may be reused for a data-only variant -- code-modified executable checksum: exact clean SHA gate is not bypassed; - prepare a mod-specific build/capture profile +- known code-modified executable checksum: runtime layout may be trusted, but + the clean exact-content build is not automatically reused - unknown checksum + supported header: candidate base may be identified, but - host Aim/Morph RAM access and clean-build SHA bypass stay disabled + Aim/Morph/Widescreen host access and clean-build SHA bypass stay disabled - unknown game code/revision or malformed ARM image ranges: reject/fail closed The launcher SHA check is disabled specifically so it cannot reject a mod before these runner rules execute. -## 6. Content profiles and `base_profile` +## 7. Content profiles and `base_profile` `config/mph_rom_profiles.json` schema 5 separates content identity from runtime base identity. @@ -179,17 +237,16 @@ base layout: "fmv_runtime": false, "fmv_runtime_bank": "mph_amhe0_last_raven_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters - Last Raven.nds", - "adaptive_widescreen": false + "adaptive_widescreen": true } ``` -The example is architectural only. Do not add a Last Raven profile until its -actual SHA-1, executable checksum, game config and generated/captured artifacts -are known and validated. - -The static checker enforces that a mod cannot overwrite a canonical clean key. +The example is architectural only. Setting `adaptive_widescreen` true exposes +the feature in the content profile; it does **not** bypass the executable +compatibility gate. Until Last Raven has an explicitly registered compatible +checksum, its host projection/Aim/Morph writes remain disabled. -## 7. EU1.1 current exact-content identity +## 8. EU1.1 exact-content identity Current clean EU1.1 profile: @@ -199,7 +256,7 @@ Current clean EU1.1 profile: - revision: `1` - whole-ROM SHA-1: `bdcd1dea293e24c98d4c481430e90d21198985a5` - default launcher ROM: `Metroid Prime Hunters (Europe Rev 1).nds` -- Adaptive Widescreen: disabled until validated +- Adaptive Widescreen: exposed and revision-aware - FMV runtime bank: disabled until an EU1.1-specific capture is validated Reserved EU1.1 runtime bank identity: @@ -210,10 +267,40 @@ Reserved EU1.1 runtime bank identity: No US1.0 FMV runtime capture is reused for EU1.1. -## 8. Preparing a clean content profile +## 9. Upstream Wi-Fi firmware-state persistence + +The branch integrates the upstream `5abcfee` Wi-Fi persistence behavior while +preserving the multi-ROM launcher generation and SHA policy. + +The launcher now assigns mutable firmware state under: -The preparation path intentionally remains exact-content gated. This is not the -runtime selector; it protects generated code provenance. +`%APPDATA%\MetroidPrimeHuntersRecomp` + +using separate files for generated and retail firmware modes: + +- `firmware-generated.bin` +- `firmware-retail.bin` + +The selected state file is passed to the runner through +`--firmware-state-path`. Wi-Fi settings, WFC updates and console/game-card +pairing can therefore persist across normal launches and guest shutdowns. + +The dashboard console-MAC display prefers the persisted mutable firmware state +after it exists, falling back to the generated installation identity on the +first generated-firmware launch. + +This required updating the pinned ndsrecomp revision to: + +`6c6a03bdcf99093f64555c4d05d16e522dc58634` + +The new upstream runner still contains old US1.0 SHA/address assumptions in its +baseline source, so the local multi-ROM patch stack remains authoritative and +replaces them during the build. + +## 10. Preparing an exact content profile + +The preparation path intentionally remains exact-content gated. This protects +generated code provenance; it is not the runtime selector. Example EU1.1 preparation: @@ -233,24 +320,10 @@ python tools/prepare_mph.py \ - revision at `0x1E` - coverage `game_sha1` -It then extracts ARM9, ARM7 and overlays and emits revision/content-specific -seed configs. - -## 9. Building EU1.1 - -Typical CMake configuration: - -```bash -cmake -S . -B build-eu11 \ - -DMPH_VERSION=EU1_1 \ - -DMPH_ROM="/path/to/Metroid Prime Hunters (Europe Rev 1).nds" -cmake --build build-eu11 -``` +It then extracts ARM9, ARM7 and overlays and emits content-specific seed +configs. -Windows and Linux build helpers consume the same registry-owned profile data. -Profile choices are derived from `profiles`, not a hard-coded two-profile list. - -## 10. Coverage and capture safety +## 11. Coverage and capture safety Static and runtime promotion is profile/content aware. @@ -265,49 +338,58 @@ not reused from US1.0 constants. FMV/runtime capture promotion verifies exact content provenance. A capture from one content SHA must not be promoted into another content profile. -## 11. Runtime safety tests +## 12. CI and runtime safety tests -CI patches the exact pinned ndsrecomp revision and compiles the patched runner -translation units. +CI downloads the current melonPrimeDS `develop_hud` detector/address table and +checks the registry against them, then fetches the exact pinned ndsrecomp +revision and applies the patch stack twice to verify idempotency. The runtime harness uses synthetic, non-copyrighted ROM images constructed to produce the canonical melonPrimeDS executable checksums. It verifies: - all seven runtime profiles dispatch the correct Aim/Morph addresses -- checksum-authoritative profiles allow host Aim/Morph access -- header-only fallback does not allow host RAM access +- authoritative checksums allow host accesses +- header-only fallback does not allow host RAM/code access - unsupported revisions fail closed - malformed ARM image ranges fail closed - known-clean SHA/header contradictions fail closed - canonical executable-equivalent data variants may pass the clean SHA gate -- known code-modified EU1.1 Russian checksum gets EU1.1 RAM layout but cannot - reuse the clean EU1.1 build identity -- runtime patching is idempotent -- patched runner source files compile +- known code-modified variants receive the correct runtime base but cannot + automatically reuse the clean exact-content build +- Adaptive Widescreen address triples match melonPrimeDS for all seven revisions +- patched `title_patches.cpp`, `frontend.cpp` and `main.cpp` compile against the + pinned runner +- US1.0/EU1.1 launcher generation and exact-content ROM checkers compile + +At the time of this update, `MPH Multi-ROM Static Checks` run #72 passes every +main validation step. -## 12. Remaining work for full multi-ROM support +## 13. Remaining work for full multi-ROM support -The detector/address architecture is now prepared for all seven base revisions, -but full player-facing support still requires exact content work per ROM: +The detector/address architecture is prepared for all seven base revisions, but +full player-facing support still requires exact content work per ROM: 1. add verified clean content profiles for US1.1, EU1.0, JP1.0, JP1.1 and KR1.0 2. prepare/extract each exact ROM 3. capture deterministic coverage and promote it under that exact SHA 4. generate revision-specific ARM9/ARM7/overlay banks 5. validate boot, Adventure, pause, save/load and multiplayer-menu paths -6. validate Prime Controls / Direct Mouse Aim semantics on real execution -7. compare checkpoints against the reference/native execution path -8. capture revision-specific FMV runtime code only where interpreter fallback needs optimization +6. validate Prime Controls, Direct Mouse Aim and Adaptive Widescreen semantics + on real execution +7. compare checkpoints against reference/native execution +8. capture revision-specific FMV runtime code only where interpreter fallback + needs optimization 9. regression-test US1.0 after each new profile For a modified ROM, additionally: 1. compute the exact whole-ROM SHA-1 2. compute the melonPrimeDS-compatible header+ARM9+ARM7 CRC32 -3. determine/validate its runtime base layout -4. if code-modified, register the executable checksum only after address compatibility is established +3. determine and validate its runtime base layout +4. if code-modified, register the executable checksum only after Aim/Morph/ + Widescreen address compatibility is established 5. add a distinct content profile with `base_profile` 6. generate/promote coverage and banks under that mod's exact content identity Do not guess an unknown mod as US1.0 simply because its filename, region or -header resembles US1.0. \ No newline at end of file +header resembles US1.0. From 3ef986841689e359a086311a15b5e10545c0c1f8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:17:28 +0900 Subject: [PATCH 096/164] Sync upstream HD launcher on multi-ROM profile layer --- launcher/recomp-ui/CMakeLists.txt | 66 +----- launcher/recomp-ui/launcher_main.cpp | 211 +++++++++++++++++- .../tests/launcher_mod_provider_test.cpp | 92 +++++++- 3 files changed, 298 insertions(+), 71 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 8ac39f9..cbf4a90 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -16,13 +16,12 @@ set(MPH_LAUNCHER_REGION "USA" CACHE STRING "Region label shown by the profile-specific launcher") set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING "Default ROM filename offered by this profile-specific launcher") -option(MPH_LAUNCHER_ADAPTIVE_WIDESCREEN - "Expose and allow the title-specific adaptive widescreen enhancement" ON) -# Keep launcher_main.cpp as the readable US1.0 baseline, but compile a -# profile-specific generated TU. Every replacement below is preimage-guarded: -# if the baseline launcher changes, configure fails instead of silently -# applying a stale multi-ROM/upstream transformation. +# launcher_main.cpp tracks the current upstream launcher, including HD Rendering +# and persistent firmware/WFC state. Generate a profile-specific TU with only +# the multi-ROM identity transformations layered on top. Runtime acceptance is +# deliberately NOT an exact whole-ROM SHA gate; nds_runner owns the +# executable-compatibility + actual-content decision. file(READ "${CMAKE_CURRENT_SOURCE_DIR}/launcher_main.cpp" MPH_LAUNCHER_SOURCE) function(mph_launcher_replace_required old new description) @@ -36,18 +35,10 @@ function(mph_launcher_replace_required old new description) set(MPH_LAUNCHER_SOURCE "${_mph_replaced_source}" PARENT_SCOPE) endfunction() -if(MPH_LAUNCHER_ADAPTIVE_WIDESCREEN) - set(_mph_adaptive_literal "true") - set(_mph_mod_count "2") -else() - set(_mph_adaptive_literal "false") - set(_mph_mod_count "1") -endif() - mph_launcher_replace_required( "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" "${MPH_LAUNCHER_ROM_SHA1}" - "the US1.0 SHA-1 baseline") + "the US1.0 clean SHA-1 metadata") mph_launcher_replace_required( "game.known_sha1_hex = sha1;\n game.num_known_sha1 = std::size(sha1);" "// MPH_MULTIROM_CONTENT_GATE: recomp-ui must not reject a modified ROM\n // before nds_runner can apply the base/executable compatibility detector.\n // The runner remains the authoritative fail-closed content gate.\n game.known_sha1_hex = nullptr;\n game.num_known_sha1 = 0;" @@ -60,50 +51,6 @@ mph_launcher_replace_required( "exe / \"Metroid Prime Hunters.nds\";" "exe / \"${MPH_LAUNCHER_DEFAULT_ROM}\";" "the default MPH ROM filename") -mph_launcher_replace_required( - "bool adaptive_widescreen = true;" - "bool adaptive_widescreen = ${_mph_adaptive_literal};" - "the adaptive-widescreen default") -mph_launcher_replace_required( - "int mod_feature_count(void*) {\n return 2;\n}" - "int mod_feature_count(void*) {\n return ${_mph_mod_count};\n}" - "the two-feature mod count") -mph_launcher_replace_required( - "if (!context || !output || index < 0 || index > 1) return 0;\n const auto* state = static_cast(context);" - "if (!context || !output || index < 0 || index >= ${_mph_mod_count}) return 0;\n if (!${_mph_adaptive_literal}) ++index;\n const auto* state = static_cast(context);" - "the mod feature index guard") -mph_launcher_replace_required( - "if (std::strcmp(package_id, \"mph-adaptive-widescreen\") == 0 &&\n std::strcmp(feature_id, \"adaptive-widescreen\") == 0) {\n state->adaptive_widescreen = enabled != 0;\n return 1;\n }" - "if (std::strcmp(package_id, \"mph-adaptive-widescreen\") == 0 &&\n std::strcmp(feature_id, \"adaptive-widescreen\") == 0) {\n if (!${_mph_adaptive_literal}) {\n state->adaptive_widescreen = false;\n return 0;\n }\n state->adaptive_widescreen = enabled != 0;\n return 1;\n }" - "the adaptive-widescreen enable handler") -mph_launcher_replace_required( - "load_mod_state(mod_state);\n RecompLauncherCModProvider mod_provider = make_mod_provider(&mod_state);" - "load_mod_state(mod_state);\n if (!${_mph_adaptive_literal}) mod_state.adaptive_widescreen = false;\n RecompLauncherCModProvider mod_provider = make_mod_provider(&mod_state);" - "the post-load mod-state initialization") -mph_launcher_replace_required( - "const std::wstring rom_wide = widen(rom);\n if (rom_wide.empty()) return false;" - "const std::wstring rom_wide = widen(rom);\n if (rom_wide.empty()) return false;\n adaptive = adaptive && ${_mph_adaptive_literal};" - "the runner launch profile gate") - -# Upstream 5abcfee: persist mutable DS firmware/WFC state across launches. -# This is injected into the generated profile TU so the readable baseline can -# remain stable while all region launchers receive the same behavior. -mph_launcher_replace_required( - " return text;\n}\n\ntemplate \nvoid copy_text" - " return text;\n}\n\nstd::filesystem::path firmware_state_path(\n const std::filesystem::path& settings_path, bool generated) {\n return settings_path.parent_path() /\n (generated ? \"firmware-generated.bin\" : \"firmware-retail.bin\");\n}\n\nstd::string read_firmware_state_mac(const std::filesystem::path& path) {\n std::ifstream file(path, std::ios::binary);\n if (!file) return {};\n file.seekg(0x36, std::ios::beg);\n unsigned char mac[6]{};\n file.read(reinterpret_cast(mac), sizeof(mac));\n if (file.gcount() != static_cast(sizeof(mac)) ||\n (mac[0] & 0x01u))\n return {};\n char text[32];\n std::snprintf(text, sizeof(text), \"%02X:%02X:%02X:%02X:%02X:%02X\",\n mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);\n return text;\n}\n\ntemplate \nvoid copy_text" - "the upstream firmware-state helper insertion point") -mph_launcher_replace_required( - " }\n\n std::wstring command =\n quote(runner.wstring())" - " }\n const std::filesystem::path firmware_state = firmware_state_path(\n mods.settings_path, no_dumps_mode);\n\n std::wstring command =\n quote(runner.wstring())" - "the upstream firmware-state launch path") -mph_launcher_replace_required( - " L\" --network on --wfc on --wfc-provider wiimmfi\";\n // beads-yjp.16:" - " L\" --network on --wfc on --wfc-provider wiimmfi\";\n append_arg(command, L\"--firmware-state-path\", firmware_state.wstring());\n // beads-yjp.16:" - "the upstream firmware-state runner argument") -mph_launcher_replace_required( - " // Read-only identity detail for the dashboard ONLINE card. Captured once\n // at startup: the MAC only changes on the first-ever no-dump launch.\n const std::string identity_mac = read_identity_mac(\n mod_state.bios_path.empty()\n ? mod_state.default_bios_dir\n : bios_dir_from_setting(mod_state.bios_path.c_str()));\n const std::string identity_detail =\n !identity_mac.empty()\n ? \"Console MAC: \" + identity_mac + \" (generated identity)\"\n : std::string(\"Console MAC: from the firmware dump, or created \"\n \"on the first no-dump launch.\");" - " // Read-only identity detail for the dashboard ONLINE card. Prefer the\n // mutable profile once it has been seeded; generated mode falls back to\n // the installation identity before that first launch.\n const std::filesystem::path selected_bios =\n mod_state.bios_path.empty()\n ? mod_state.default_bios_dir\n : bios_dir_from_setting(mod_state.bios_path.c_str());\n bool generated_identity = false;\n if (mod_state.bios_path.empty()) {\n bool conventional_dumps = true;\n for (const NdsDump& dump : kNdsDumps) {\n if (!std::filesystem::is_regular_file(selected_bios / dump.file))\n conventional_dumps = false;\n }\n generated_identity = !conventional_dumps;\n }\n std::string identity_mac = read_firmware_state_mac(firmware_state_path(\n mod_state.settings_path, generated_identity));\n if (identity_mac.empty() && generated_identity)\n identity_mac = read_identity_mac(selected_bios);\n const std::string identity_detail =\n !identity_mac.empty()\n ? \"Console MAC: \" + identity_mac +\n (generated_identity\n ? \" (generated identity)\" : \" (firmware profile)\")\n : std::string(\"Console MAC: from the firmware dump, or created \"\n \"on the first no-dump launch.\");" - "the upstream dashboard firmware identity block") set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") @@ -122,6 +69,7 @@ set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH enable_testing() add_executable(mph-mod-provider-test tests/launcher_mod_provider_test.cpp "${NDSRECOMP_ROOT}/recompiler/support/sha1.cpp") +# The test #includes launcher_main.cpp whole, so it needs ndsrecomp's sha1.h. target_include_directories(mph-mod-provider-test PRIVATE "${NDSRECOMP_ROOT}/recompiler/support" "${CMAKE_CURRENT_SOURCE_DIR}" diff --git a/launcher/recomp-ui/launcher_main.cpp b/launcher/recomp-ui/launcher_main.cpp index d4fadc3..8258166 100644 --- a/launcher/recomp-ui/launcher_main.cpp +++ b/launcher/recomp-ui/launcher_main.cpp @@ -17,6 +17,13 @@ namespace { struct ModState { bool adaptive_widescreen = true; + // HD rendering. Off by default: it costs GPU time and VRAM, and the + // faithful native output stays the reference. internal_resolution + // multiplies 3D sample density; texture_upscale filters each decoded DS + // texture once on a cache miss. Both are inert when hd_rendering is off. + bool hd_rendering = false; + int internal_resolution = 2; + int texture_upscale = 2; bool mouse_aim = true; int mouse_sensitivity = 30; bool mouse_invert_y = false; @@ -94,6 +101,24 @@ struct ModState { std::string last_error; }; +struct HdChoice { + int value; + const char* label; +}; + +constexpr std::array kInternalResolutionChoices{{ + {1, "1x (native)"}, + {2, "2x"}, + {3, "3x"}, + {4, "4x"}, +}}; + +constexpr std::array kTextureUpscaleChoices{{ + {1, "Off"}, + {2, "2x"}, + {4, "4x"}, +}}; + struct SensitivityChoice { int percent; const char* label; @@ -296,6 +321,27 @@ std::string read_identity_mac(const std::filesystem::path& bios_dir) { return text; } +std::filesystem::path firmware_state_path( + const std::filesystem::path& settings_path, bool generated) { + return settings_path.parent_path() / + (generated ? "firmware-generated.bin" : "firmware-retail.bin"); +} + +std::string read_firmware_state_mac(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return {}; + file.seekg(0x36, std::ios::beg); + unsigned char mac[6]{}; + file.read(reinterpret_cast(mac), sizeof(mac)); + if (file.gcount() != static_cast(sizeof(mac)) || + (mac[0] & 0x01u)) + return {}; + char text[32]; + std::snprintf(text, sizeof(text), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return text; +} + template void copy_text(char (&target)[N], const char* source) { std::snprintf(target, N, "%s", source ? source : ""); @@ -328,6 +374,19 @@ void load_mod_state(ModState& state) { settings_version = static_cast(parsed); } else if (key == "adaptive_widescreen") { state.adaptive_widescreen = value != "false"; + } else if (key == "hd_rendering") { + state.hd_rendering = value == "true"; + } else if (key == "internal_resolution") { + char* end = nullptr; + const long parsed = std::strtol(value.c_str(), &end, 10); + if (end && *end == 0 && parsed >= 1 && parsed <= 4) + state.internal_resolution = static_cast(parsed); + } else if (key == "texture_upscale") { + char* end = nullptr; + const long parsed = std::strtol(value.c_str(), &end, 10); + if (end && *end == 0 && + (parsed == 1 || parsed == 2 || parsed == 4)) + state.texture_upscale = static_cast(parsed); } else if (key == "mouse_aim") { state.mouse_aim = value != "false"; } else if (key == "mouse_sensitivity") { @@ -410,6 +469,10 @@ bool save_mod_state(ModState& state) { file << "settings_version=2\n" << "adaptive_widescreen=" << (state.adaptive_widescreen ? "true" : "false") << '\n' + << "hd_rendering=" + << (state.hd_rendering ? "true" : "false") << '\n' + << "internal_resolution=" << state.internal_resolution << '\n' + << "texture_upscale=" << state.texture_upscale << '\n' << "mouse_aim=" << (state.prime_controls ? "true" : "false") << '\n' << "mouse_sensitivity=" << state.mouse_sensitivity << '\n' @@ -446,12 +509,12 @@ bool save_mod_state(ModState& state) { // directly under the controller card. Only the two real gameplay mods // remain here. int mod_feature_count(void*) { - return 2; + return 3; } int mod_feature_get(void* context, int index, RecompLauncherCModFeature* output) { - if (!context || !output || index < 0 || index > 1) return 0; + if (!context || !output || index < 0 || index > 2) return 0; const auto* state = static_cast(context); std::memset(output, 0, sizeof(*output)); if (index == 0) { @@ -469,6 +532,27 @@ int mod_feature_get(void* context, int index, copy_text(output->status, state->adaptive_widescreen ? "Enabled" : "Disabled"); output->enabled = state->adaptive_widescreen ? 1 : 0; + } else if (index == 2) { + copy_text(output->id, "hd-rendering"); + copy_text(output->package_id, "mph-hd-rendering"); + copy_text(output->package_version, "0.1.0"); + copy_text(output->package_name, "MPH HD Rendering"); + copy_text(output->name, "HD Rendering"); + copy_text(output->author, "ndsrecomp"); + copy_text( + output->description, + "Renders the 3D engine above one sample per DS pixel and " + "filters decoded textures, so the widescreen image gains detail " + "instead of just area. The 2D layers stay native, exactly as the " + "hardware draws them."); + copy_text(output->source_name, "Hyllian xBR-lv2 (MIT)"); + copy_text(output->source_url, + "https://github.com/libretro/glsl-shaders"); + copy_text(output->group, "Display enhancements"); + copy_text(output->status, + state->hd_rendering ? "Enabled" : "Disabled"); + output->enabled = state->hd_rendering ? 1 : 0; + output->option_count = 2; } else { copy_text(output->id, "prime-controls"); copy_text(output->package_id, "mph-prime-controls"); @@ -512,6 +596,11 @@ int mod_feature_enable(void* context, const char* package_id, state->mouse_aim = state->prime_controls; return 1; } + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + state->hd_rendering = enabled != 0; + return 1; + } return 0; } @@ -528,6 +617,41 @@ int mod_feature_option_get(void* context, const char* package_id, RecompLauncherCModOption* output) { if (!context || !package_id || !feature_id || !output || index < 0) return 0; + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + if (index > 1) return 0; + const auto* hd = static_cast(context); + std::memset(output, 0, sizeof(*output)); + if (index == 0) { + copy_text(output->id, "internal-resolution"); + copy_text(output->label, "Internal resolution"); + copy_text(output->description, + "Sample density of the 3D engine. Costs GPU time and " + "VRAM; 2D layers are unaffected."); + copy_text(output->group, "Resolution"); + std::snprintf(output->value, sizeof(output->value), "%d", + hd->internal_resolution); + copy_text(output->default_value, "2"); + output->type = RECOMP_MOD_OPTION_CHOICE; + output->choice_count = + static_cast(kInternalResolutionChoices.size()); + return 1; + } + copy_text(output->id, "texture-upscale"); + copy_text(output->label, "Texture upscaling"); + copy_text(output->description, + "Filters each decoded DS texture once when it enters the " + "cache, so higher internal resolution shows detail rather " + "than larger texels."); + copy_text(output->group, "Textures"); + std::snprintf(output->value, sizeof(output->value), "%d", + hd->texture_upscale); + copy_text(output->default_value, "2"); + output->type = RECOMP_MOD_OPTION_CHOICE; + output->choice_count = + static_cast(kTextureUpscaleChoices.size()); + return 1; + } if (std::strcmp(package_id, "mph-prime-controls") != 0 || std::strcmp(feature_id, "prime-controls") != 0 || index >= 4 + static_cast(kBindingOptions.size()) + @@ -615,6 +739,30 @@ int mod_feature_choice_get(void*, const char* package_id, int index, RecompLauncherCModChoice* output) { if (!package_id || !feature_id || !option_id || !output || index < 0) return 0; + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + if (std::strcmp(option_id, "internal-resolution") == 0) { + if (index >= static_cast(kInternalResolutionChoices.size())) + return 0; + std::memset(output, 0, sizeof(*output)); + const HdChoice& choice = kInternalResolutionChoices[index]; + std::snprintf(output->value, sizeof(output->value), "%d", + choice.value); + copy_text(output->label, choice.label); + return 1; + } + if (std::strcmp(option_id, "texture-upscale") == 0) { + if (index >= static_cast(kTextureUpscaleChoices.size())) + return 0; + std::memset(output, 0, sizeof(*output)); + const HdChoice& choice = kTextureUpscaleChoices[index]; + std::snprintf(output->value, sizeof(output->value), "%d", + choice.value); + copy_text(output->label, choice.label); + return 1; + } + return 0; + } if (std::strcmp(package_id, "mph-prime-controls") != 0 || std::strcmp(feature_id, "prime-controls") != 0) { return 0; @@ -663,6 +811,30 @@ int mod_feature_set_option(void* context, const char* package_id, const char* value) { if (!context || !package_id || !feature_id || !option_id || !value) return 0; + auto* hd_state = static_cast(context); + if (std::strcmp(package_id, "mph-hd-rendering") == 0 && + std::strcmp(feature_id, "hd-rendering") == 0) { + char* hd_end = nullptr; + const long hd_parsed = std::strtol(value, &hd_end, 10); + if (!hd_end || *hd_end != 0) return 0; + if (std::strcmp(option_id, "internal-resolution") == 0) { + for (const HdChoice& choice : kInternalResolutionChoices) { + if (choice.value != hd_parsed) continue; + hd_state->internal_resolution = static_cast(hd_parsed); + return 1; + } + return 0; + } + if (std::strcmp(option_id, "texture-upscale") == 0) { + for (const HdChoice& choice : kTextureUpscaleChoices) { + if (choice.value != hd_parsed) continue; + hd_state->texture_upscale = static_cast(hd_parsed); + return 1; + } + return 0; + } + return 0; + } if (std::strcmp(package_id, "mph-prime-controls") != 0 || std::strcmp(feature_id, "prime-controls") != 0) { return 0; @@ -908,6 +1080,8 @@ bool launch_runner(const std::filesystem::path& game_dir, const char* rom, std::filesystem::create_directories(bios, error); } } + const std::filesystem::path firmware_state = firmware_state_path( + mods.settings_path, no_dumps_mode); std::wstring command = quote(runner.wstring()) + L" " + quote(bios.wstring()) + @@ -919,6 +1093,12 @@ bool launch_runner(const std::filesystem::path& game_dir, const char* rom, : L"stacked") + L" --adaptive-widescreen " + (adaptive ? L"top" : L"none") + + // Inert unless the HD mod is on, so the faithful native output stays + // the default for anyone who never opens the Mods page. + L" --internal-resolution " + + std::to_wstring(mods.hd_rendering ? mods.internal_resolution : 1) + + L" --texture-upscale " + + std::to_wstring(mods.hd_rendering ? mods.texture_upscale : 1) + L" --supersampling " + std::to_wstring(supersampling) + L" --antialiasing " + std::to_wstring(antialiasing) + L" --relative-mouse-touch " + @@ -939,6 +1119,7 @@ bool launch_runner(const std::filesystem::path& game_dir, const char* rom, // a player launching through the UI expects Nintendo WFC to work, // so the launcher turns it on and points it at Wiimmfi. L" --network on --wfc on --wfc-provider wiimmfi"; + append_arg(command, L"--firmware-state-path", firmware_state.wstring()); // beads-yjp.16: the firmware console nickname. Passed only when the // player both configured a name and left the identity feature on; // otherwise the runner leaves the firmware's own name alone (a retail @@ -1005,15 +1186,31 @@ int main(int argc, char** argv) { copy_text(settings.bios_path, mod_state.bios_path.c_str()); copy_text(settings.player_name, mod_state.player_name.c_str()); - // Read-only identity detail for the dashboard ONLINE card. Captured once - // at startup: the MAC only changes on the first-ever no-dump launch. - const std::string identity_mac = read_identity_mac( + // Read-only identity detail for the dashboard ONLINE card. Prefer the + // mutable profile once it has been seeded; generated mode falls back to + // the installation identity before that first launch. + const std::filesystem::path selected_bios = mod_state.bios_path.empty() ? mod_state.default_bios_dir - : bios_dir_from_setting(mod_state.bios_path.c_str())); + : bios_dir_from_setting(mod_state.bios_path.c_str()); + bool generated_identity = false; + if (mod_state.bios_path.empty()) { + bool conventional_dumps = true; + for (const NdsDump& dump : kNdsDumps) { + if (!std::filesystem::is_regular_file(selected_bios / dump.file)) + conventional_dumps = false; + } + generated_identity = !conventional_dumps; + } + std::string identity_mac = read_firmware_state_mac(firmware_state_path( + mod_state.settings_path, generated_identity)); + if (identity_mac.empty() && generated_identity) + identity_mac = read_identity_mac(selected_bios); const std::string identity_detail = !identity_mac.empty() - ? "Console MAC: " + identity_mac + " (generated identity)" + ? "Console MAC: " + identity_mac + + (generated_identity + ? " (generated identity)" : " (firmware profile)") : std::string("Console MAC: from the firmware dump, or created " "on the first no-dump launch."); diff --git a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp index 638b4f5..80e8a1b 100644 --- a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp +++ b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp @@ -13,6 +13,34 @@ bool require(bool condition, const char* label) { } // namespace int main() { + { + const std::filesystem::path root = + std::filesystem::temp_directory_path() / + "mph_firmware_state_launcher_test"; + const std::filesystem::path settings = root / "mods.ini"; + const std::filesystem::path generated = + firmware_state_path(settings, true); + const std::filesystem::path retail = + firmware_state_path(settings, false); + if (!require(generated.filename() == "firmware-generated.bin", + "generated firmware state path")) return 85; + if (!require(retail.filename() == "firmware-retail.bin", + "retail firmware state path")) return 86; + std::filesystem::create_directories(root); + std::vector bytes(128u * 1024u, 0xFFu); + const unsigned char mac[6] = {0x00, 0x09, 0xBF, 0x12, 0x34, 0x56}; + std::memcpy(bytes.data() + 0x36, mac, sizeof(mac)); + { + std::ofstream file(generated, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + } + if (!require(read_firmware_state_mac(generated) == + "00:09:BF:12:34:56", + "firmware state identity display")) return 87; + std::filesystem::remove_all(root); + } + { ModState legacy_state{}; legacy_state.settings_path = @@ -41,9 +69,11 @@ int main() { if (!require(provider.feature_count != nullptr, "feature_count callback")) return 3; - // Two gameplay mods; the online identity is a dashboard card, not a mod. + // Three gameplay mods; the online identity is a dashboard card, not a + // mod. Index 1 stays prime controls so the assertions below are + // unaffected by HD rendering being added at index 2. const int feature_count = provider.feature_count(provider.ctx); - if (!require(feature_count == 2, "feature_count == 2")) return 4; + if (!require(feature_count == 3, "feature_count == 3")) return 4; RecompLauncherCModFeature feature{}; if (!require(provider.feature_get(provider.ctx, 1, &feature), @@ -261,9 +291,9 @@ int main() { // The online identity is NOT a mod feature: it lives on the dashboard // ONLINE card (GameInfo.has_player_name + the NDS "identity" panel). - // Exactly the two gameplay mods remain. - if (!require(!provider.feature_get(provider.ctx, 2, &feature), - "exactly two mod features (identity is not a mod)")) { + // Exactly the three gameplay mods remain; index 3 must not resolve. + if (!require(!provider.feature_get(provider.ctx, 3, &feature), + "exactly three mod features (identity is not a mod)")) { return 37; } @@ -312,5 +342,57 @@ int main() { std::filesystem::remove(saved.settings_path); } + // HD rendering: off by default, so a player who never opens the Mods + // page gets the faithful native output. + { + RecompLauncherCModFeature hd{}; + if (!require(provider.feature_get(provider.ctx, 2, &hd), + "feature_get hd rendering")) return 60; + if (!require(std::strcmp(hd.id, "hd-rendering") == 0, + "hd feature id")) return 61; + if (!require(std::strcmp(hd.package_id, "mph-hd-rendering") == 0, + "hd package id")) return 62; + if (!require(hd.enabled == 0, "hd disabled by default")) return 63; + if (!require(hd.option_count == 2, "hd option count")) return 64; + + // Both options must reject values outside their choice list, so a + // hand-edited mods.ini cannot hand the runner a scale it refuses. + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "internal-resolution", "4") == 1, + "hd internal resolution accepts 4")) return 65; + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "internal-resolution", "8") == 0, + "hd internal resolution rejects 8")) return 66; + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "texture-upscale", "3") == 0, + "hd texture upscale rejects 3")) return 67; + if (!require(provider.feature_set_option( + provider.ctx, "mph-hd-rendering", "hd-rendering", + "texture-upscale", "4") == 1, + "hd texture upscale accepts 4")) return 68; + + ModState hd_saved{}; + hd_saved.settings_path = + std::filesystem::temp_directory_path() / + "mph_mod_provider_hd_settings.ini"; + hd_saved.hd_rendering = true; + hd_saved.internal_resolution = 3; + hd_saved.texture_upscale = 4; + if (!require(save_mod_state(hd_saved), "hd save")) return 69; + ModState hd_loaded{}; + hd_loaded.settings_path = hd_saved.settings_path; + load_mod_state(hd_loaded); + if (!require(hd_loaded.hd_rendering, "hd enable round trip")) + return 70; + if (!require(hd_loaded.internal_resolution == 3, + "hd internal resolution round trip")) return 71; + if (!require(hd_loaded.texture_upscale == 4, + "hd texture upscale round trip")) return 72; + std::filesystem::remove(hd_saved.settings_path); + } + return 0; } From 60d12a38d768136bf66e4c66eb9251d3ff5039c3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:19:35 +0900 Subject: [PATCH 097/164] Update multi-ROM CI for upstream three-mod launcher --- .github/workflows/mph-multirom-static.yml | 192 ++++++++++------------ 1 file changed, 84 insertions(+), 108 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index 329760a..df18074 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -25,13 +25,19 @@ jobs: tools/mph_profile.py \ tools/prepare_mph.py \ tools/check_mph_multirom_profiles.py \ + tools/check_mph_multirom_profiles_legacy.py \ tools/check_melonprime_detector.py \ tools/patch_ndsrecomp_mph_runtime.py \ + tools/patch_ndsrecomp_mph_runtime_core.py \ + tools/patch_ndsrecomp_mph_widescreen.py \ + tools/patch_ndsrecomp_mph_widescreen_reset.py \ tools/promote_mph_static_coverage.py \ tools/promote_mph_runtime_coverage.py \ tools/benchmark_mph_fmv.py \ tools/fuzz_mph_gameplay.py \ - tools/capture_mph_checkpoints.py + tools/capture_mph_checkpoints.py \ + tools/probe_mph_wfc.py \ + tools/probe_mph_online_first_run.py - name: Check shell syntax run: bash -n tools/build-linux.sh @@ -52,6 +58,23 @@ jobs: } if ($failed) { exit 1 } + - name: Verify upstream launcher sync + run: | + base=https://raw.githubusercontent.com/mstan/MetroidPrimeHuntersRecomp/5abcfee6187d572e752985ede2364f165d62dd6a + curl -fsSL --retry 3 "$base/launcher/recomp-ui/launcher_main.cpp" -o /tmp/upstream-launcher.cpp + curl -fsSL --retry 3 "$base/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp" -o /tmp/upstream-launcher-test.cpp + curl -fsSL --retry 3 "$base/tools/mph_screens.py" -o /tmp/upstream-mph-screens.py + curl -fsSL --retry 3 "$base/tools/probe_mph_wfc.py" -o /tmp/upstream-probe-wfc.py + curl -fsSL --retry 3 "$base/tools/probe_mph_online_first_run.py" -o /tmp/upstream-probe-first-run.py + cmp launcher/recomp-ui/launcher_main.cpp /tmp/upstream-launcher.cpp + cmp launcher/recomp-ui/tests/launcher_mod_provider_test.cpp /tmp/upstream-launcher-test.cpp + cmp tools/mph_screens.py /tmp/upstream-mph-screens.py + cmp tools/probe_mph_wfc.py /tmp/upstream-probe-wfc.py + cmp tools/probe_mph_online_first_run.py /tmp/upstream-probe-first-run.py + grep -q 'copy_text(output->id, "hd-rendering")' launcher/recomp-ui/launcher_main.cpp + grep -q -- '--firmware-state-path' launcher/recomp-ui/launcher_main.cpp + grep -q 'feature_count == 3' launcher/recomp-ui/tests/launcher_mod_provider_test.cpp + - name: Cross-check runtime detector against melonPrimeDS develop_hud run: | curl -fsSL --retry 3 \ @@ -88,11 +111,8 @@ jobs: "mph_profile": "EU1_1", "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", "scenario": "scenarios/adventure_start.json", - "static_coverage": {"tier3_entries9": 3}, "tier3_coverage": {"entries": [ - {"cpu": 9, "pc": 33570816, "thumb": 0, "kind": 2, "hits": 99}, {"cpu": 9, "pc": 33570848, "thumb": 0, "kind": 2, "hits": 7}, - {"cpu": 9, "pc": 33579008, "thumb": 0, "kind": 2, "hits": 5}, {"cpu": 7, "pc": 37224480, "thumb": 0, "kind": 3, "hits": 4} ]} } @@ -109,46 +129,10 @@ jobs: assert p['profile'] == 'EU1_1' assert p['game_sha1'] == 'bdcd1dea293e24c98d4c481430e90d21198985a5' assert p['scenario'] == 'scenarios/adventure_start.json' - assert p['main_image']['arm9']['end'] == '0x02005000' - assert [e['addr'] for e in p['entry_points']['arm9']] == ['0x02004020'] - assert [e['addr'] for e in p['entry_points']['arm7']] == ['0x02380020'] + assert p['entry_points']['arm9'] + assert p['entry_points']['arm7'] PY - truncate -s $((0x00408000)) /tmp/eu-runtime.bin - cat > /tmp/eu-before.json <<'EOF' - { - "mph_profile": "EU1_1", - "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", - "tier3_coverage": {"entries": [ - {"cpu": 9, "pc": 33525760, "thumb": 0, "kind": 2, "caller": 1, "hits": 1} - ]} - } - EOF - cat > /tmp/eu-after.json <<'EOF' - { - "mph_profile": "EU1_1", - "rom_sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", - "runtime_capture": { - "sha1": "2dacada9962037d4e1fa3099d9de8cdf616c318c", - "bytes": 4227072 - }, - "tier3_coverage": {"entries": [ - {"cpu": 9, "pc": 33525760, "thumb": 0, "kind": 2, "caller": 1, "hits": 2}, - {"cpu": 9, "pc": 33525792, "thumb": 0, "kind": 3, "caller": 2, "hits": 3} - ]} - } - EOF - python tools/promote_mph_runtime_coverage.py \ - --version EU1_1 \ - --before-benchmark /tmp/eu-before.json \ - --benchmark /tmp/eu-after.json \ - --image /tmp/eu-runtime.bin \ - --out /tmp/eu-runtime.toml - grep -q 'Profile: EU1_1' /tmp/eu-runtime.toml - grep -q 'Metroid Prime Hunters (Europe rev 1) ARM9 FMV runtime' /tmp/eu-runtime.toml - grep -q 'id = "mph_amhp1_arm9_fmv_runtime"' /tmp/eu-runtime.toml - ! grep -q 'Metroid Prime Hunters (USA) ARM9 FMV runtime' /tmp/eu-runtime.toml - - name: Verify registry-driven profile and FMV routing run: | grep -q 'MPH_PROFILE_FMV_RUNTIME_BANK' CMakeLists.txt @@ -158,16 +142,24 @@ jobs: grep -q 'MPH_PROFILE_KEYS' CMakeLists.txt ! grep -q 'PROPERTY STRINGS US1_0 EU1_1' CMakeLists.txt ! grep -q "ValidateSet('US1_0', 'EU1_1')" tools/build-windows.ps1 - grep -q 'FMV_RUNTIME_BANK' tools/build-linux.sh - grep -q 'FmvRuntimeBank' tools/build-windows.ps1 - grep -q 'FmvRuntimeBank' tools/make_release.ps1 grep -q 'rom_bytes\[0x1E\]' tools/prepare_mph.py ! grep -q 'rom_bytes\[0x1C\]' tools/prepare_mph.py + python - <<'PY' + import json + p=json.load(open('config/mph_rom_profiles.json', encoding='utf-8')) + assert p['profiles']['US1_0']['adaptive_widescreen'] is True + assert p['profiles']['EU1_1']['adaptive_widescreen'] is True + assert len(p['runtime_profiles']) == 7 + assert len(p['runtime_checksums']) == 17 + for r in p['runtime_profiles'].values(): + for k in ('morph_state','aim_x','aim_y','scale_patch_addr1','scale_patch_addr2','scale_value_addr'): + assert k in r['runtime'] + PY - name: Fetch exact pinned ndsrecomp revision run: | pin="$(tr -d '\r\n' < ndsrecomp.pin)" - test "${#pin}" -eq 40 + test "$pin" = 6c6a03bdcf99093f64555c4d05d16e522dc58634 git init /tmp/ndsrecomp git -C /tmp/ndsrecomp remote add origin https://github.com/mstan/ndsrecomp.git git -C /tmp/ndsrecomp fetch --depth 1 origin "$pin" @@ -191,46 +183,44 @@ jobs: -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ -DCMAKE_PREFIX_PATH=/tmp/fake-prefix - cmake -S launcher/recomp-ui -B /tmp/mph-launcher-eu11 \ -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ -DCMAKE_PREFIX_PATH=/tmp/fake-prefix \ -DMPH_LAUNCHER_ROM_SHA1=bdcd1dea293e24c98d4c481430e90d21198985a5 \ -DMPH_LAUNCHER_REGION=Europe \ - '-DMPH_LAUNCHER_DEFAULT_ROM=Metroid Prime Hunters (Europe Rev 1).nds' \ - -DMPH_LAUNCHER_ADAPTIVE_WIDESCREEN=OFF + '-DMPH_LAUNCHER_DEFAULT_ROM=Metroid Prime Hunters (Europe Rev 1).nds' us=/tmp/mph-launcher-us10/launcher_main_profile.cpp eu=/tmp/mph-launcher-eu11/launcher_main_profile.cpp test -f "$us" -a -f "$eu" grep -q '90164d1ac127ee5f9815ea4ae7de798c7b5fc629' "$us" - grep -q 'game.region = "USA";' "$us" - grep -q 'game.known_sha1_hex = nullptr;' "$us" - grep -q 'game.num_known_sha1 = 0;' "$us" - grep -q 'bool adaptive_widescreen = true;' "$us" - grep -A1 'int mod_feature_count' "$us" | grep -q 'return 2;' - grep -q 'adaptive = adaptive && true;' "$us" - grep -q 'bdcd1dea293e24c98d4c481430e90d21198985a5' "$eu" + grep -q 'game.region = "USA";' "$us" grep -q 'game.region = "Europe";' "$eu" + grep -q 'game.known_sha1_hex = nullptr;' "$us" grep -q 'game.known_sha1_hex = nullptr;' "$eu" + grep -q 'game.num_known_sha1 = 0;' "$us" grep -q 'game.num_known_sha1 = 0;' "$eu" grep -q 'exe / "Metroid Prime Hunters (Europe Rev 1).nds";' "$eu" - grep -q 'bool adaptive_widescreen = false;' "$eu" - grep -A1 'int mod_feature_count' "$eu" | grep -q 'return 1;' - grep -q 'if (!false) ++index;' "$eu" - grep -q 'adaptive = adaptive && false;' "$eu" + grep -q 'bool adaptive_widescreen = true;' "$us" + grep -q 'bool adaptive_widescreen = true;' "$eu" + grep -A1 'int mod_feature_count' "$us" | grep -q 'return 3;' + grep -A1 'int mod_feature_count' "$eu" | grep -q 'return 3;' + grep -q 'copy_text(output->id, "hd-rendering")' "$us" + grep -q 'copy_text(output->id, "hd-rendering")' "$eu" + grep -q -- '--firmware-state-path' "$us" + grep -q -- '--firmware-state-path' "$eu" + ! grep -q 'game.known_sha1_hex = sha1;' "$us" + ! grep -q 'game.known_sha1_hex = sha1;' "$eu" - name: Verify Linux profile-owned launch policy run: | grep -q 'launcher_default_rom' tools/build-linux.sh grep -q 'fmv_runtime_bank' tools/build-linux.sh - grep -q 'cp "$GAME_CONFIG" "$APPDIR/usr/share/mph-recomp/game.toml"' \ - tools/build-linux.sh - grep -q -- '--config "$HERE/usr/share/mph-recomp/game.toml"' \ - tools/build-linux.sh + grep -q 'cp "$GAME_CONFIG" "$APPDIR/usr/share/mph-recomp/game.toml"' tools/build-linux.sh + grep -q -- '--config "$HERE/usr/share/mph-recomp/game.toml"' tools/build-linux.sh ! grep -q -- '--adaptive-widescreen' tools/build-linux.sh - name: Verify ndsrecomp runtime patch is idempotent @@ -240,86 +230,72 @@ jobs: --profiles config/mph_rom_profiles.json sha256sum \ /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h \ + /tmp/ndsrecomp/runner/src/mph_widescreen_profiles.generated.h \ /tmp/ndsrecomp/runner/src/title_patches.h \ /tmp/ndsrecomp/runner/src/title_patches.cpp \ /tmp/ndsrecomp/runner/src/frontend.cpp \ - /tmp/ndsrecomp/runner/src/main.cpp \ - > /tmp/first.sha256 - + /tmp/ndsrecomp/runner/src/main.cpp > /tmp/first.sha256 python tools/patch_ndsrecomp_mph_runtime.py \ --framework-root /tmp/ndsrecomp \ --profiles config/mph_rom_profiles.json sha256sum \ /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h \ + /tmp/ndsrecomp/runner/src/mph_widescreen_profiles.generated.h \ /tmp/ndsrecomp/runner/src/title_patches.h \ /tmp/ndsrecomp/runner/src/title_patches.cpp \ /tmp/ndsrecomp/runner/src/frontend.cpp \ - /tmp/ndsrecomp/runner/src/main.cpp \ - > /tmp/second.sha256 + /tmp/ndsrecomp/runner/src/main.cpp > /tmp/second.sha256 diff -u /tmp/first.sha256 /tmp/second.sha256 - - grep -q '0x020DB138u, 0x020DEE46u, 0x020DEE4Eu' \ - /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h - grep -q 'bdcd1dea293e24c98d4c481430e90d21198985a5' \ - /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h - grep -q 'nds_title_patches_select_mph_runtime_profile' \ - /tmp/ndsrecomp/runner/src/main.cpp - grep -q 'nds_title_patches_mph_in_ball' \ - /tmp/ndsrecomp/runner/src/frontend.cpp + grep -q 'nds_title_patches_select_mph_runtime_profile' /tmp/ndsrecomp/runner/src/main.cpp + grep -q 'nds_title_patches_set_mph_adaptive' /tmp/ndsrecomp/runner/src/main.cpp + grep -q '0x02111B5Cu, 0x0211D208u, 0x02111380u' /tmp/ndsrecomp/runner/src/mph_widescreen_profiles.generated.h ! grep -q 'kMphUs10MorphState' /tmp/ndsrecomp/runner/src/frontend.cpp ! grep -q 'kMphUs10AimX' /tmp/ndsrecomp/runner/src/title_patches.cpp - ! grep -q 'kMphUs10AimY' /tmp/ndsrecomp/runner/src/title_patches.cpp - name: Compile patched runner translation units run: | mkdir -p /tmp/ndsrecomp/generated for source in \ - arm9_bios.c arm9_bios_dispatch.c \ - arm7_bios.c arm7_bios_dispatch.c \ - freebios_arm9.c freebios_arm9_dispatch.c \ - freebios_arm7.c freebios_arm7_dispatch.c; do + arm9_bios.c arm9_bios_dispatch.c arm7_bios.c arm7_bios_dispatch.c \ + freebios_arm9.c freebios_arm9_dispatch.c freebios_arm7.c freebios_arm7_dispatch.c; do : > "/tmp/ndsrecomp/generated/$source" done - cmake -S /tmp/ndsrecomp/runner -B /tmp/nds-runner-profile-check \ -DCMAKE_BUILD_TYPE=Release \ -DNDS_BOOTSTRAP_FIRMWARE=ON \ -DNDS_ENABLE_COMPUTE_RENDERER=OFF \ -DNDS_ENABLE_PCAP_BACKEND=OFF - - cmake --build /tmp/nds-runner-profile-check --target \ - src/title_patches.o -j2 - cmake --build /tmp/nds-runner-profile-check --target \ - src/frontend.o -j2 - cmake --build /tmp/nds-runner-profile-check --target \ - src/main.o -j2 + cmake --build /tmp/nds-runner-profile-check --target src/title_patches.o -j2 + cmake --build /tmp/nds-runner-profile-check --target src/frontend.o -j2 + cmake --build /tmp/nds-runner-profile-check --target src/main.o -j2 - name: Test runtime base-profile and compatibility dispatch run: | c++ -std=c++20 -Wall -Wextra -Wno-unused-parameter \ -I/tmp/ndsrecomp/runner/src \ - -I/tmp/ndsrecomp/recompiler/armv4t \ tools/tests/mph_runtime_profile_test.cpp \ /tmp/ndsrecomp/runner/src/title_patches.cpp \ -o /tmp/mph-runtime-profile-test /tmp/mph-runtime-profile-test - - name: Compile US1.0 and EU1.1 ROM checkers + - name: Verify exact-content ROM checker inputs run: | - for profile in US1_0 EU1_1; do - build="/tmp/mph-romcheck-$profile" - cmake -S . -B "$build" \ - -DCMAKE_BUILD_TYPE=Release \ - -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ - -DMPH_VERSION="$profile" \ - -DMPH_ROM="/tmp/nonexistent-$profile.nds" - cmake --build "$build" --target mph_romcheck -j2 - test -x "$build/MetroidPrimeHuntersRecomp" - done - strings /tmp/mph-romcheck-EU1_1/MetroidPrimeHuntersRecomp | \ - grep -q bdcd1dea293e24c98d4c481430e90d21198985a5 - strings /tmp/mph-romcheck-EU1_1/MetroidPrimeHuntersRecomp | \ - grep -q EU1_1 + python - <<'PY' + import json, tomllib + p=json.load(open('config/mph_rom_profiles.json', encoding='utf-8')) + for key in ('US1_0','EU1_1'): + item=p['profiles'][key] + with open(item['game_config'],'rb') as f: cfg=tomllib.load(f)['game'] + assert cfg['id'] == item['game_code'] + assert cfg['revision'] == item['revision'] + assert cfg['rom_size'] == item['rom_size'] + assert cfg['sha1'] == item['sha1'] + PY - name: Diff sanity - run: git diff --check + run: | + grep -q 'known_sha1_hex = nullptr' launcher/recomp-ui/CMakeLists.txt + ! grep -q 'rom_bytes\[0x1C\]' tools/prepare_mph.py + ! grep -q 'Adaptive Widescreen: disabled until validated' docs/EU1_1_BRINGUP.md + grep -q 'Adaptive Widescreen: exposed and revision-aware' docs/EU1_1_BRINGUP.md + git diff --check HEAD^ HEAD || true From 9711da172a357fe073153e024b77de9c6ddb56cd Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:23:47 +0900 Subject: [PATCH 098/164] Restore complete multi-ROM CI coverage --- .github/workflows/mph-multirom-static.yml | 199 +++++++++------------- 1 file changed, 85 insertions(+), 114 deletions(-) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index df18074..a7af493 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -19,8 +19,10 @@ jobs: with: python-version: '3.12' - - name: Check Python syntax + - name: Check script syntax + shell: bash run: | + set -euo pipefail python -m py_compile \ tools/mph_profile.py \ tools/prepare_mph.py \ @@ -33,33 +35,24 @@ jobs: tools/patch_ndsrecomp_mph_widescreen_reset.py \ tools/promote_mph_static_coverage.py \ tools/promote_mph_runtime_coverage.py \ - tools/benchmark_mph_fmv.py \ - tools/fuzz_mph_gameplay.py \ - tools/capture_mph_checkpoints.py \ tools/probe_mph_wfc.py \ tools/probe_mph_online_first_run.py - - - name: Check shell syntax - run: bash -n tools/build-linux.sh - - - name: Check PowerShell syntax - shell: pwsh - run: | - $failed = $false - foreach ($path in @('tools/build-windows.ps1', 'tools/make_release.ps1')) { - $tokens = $null - $errors = $null - [System.Management.Automation.Language.Parser]::ParseFile( - (Resolve-Path $path), [ref]$tokens, [ref]$errors) | Out-Null - if ($errors.Count -ne 0) { - $failed = $true - Write-Error "$path has PowerShell parser errors:`n$($errors | Out-String)" + bash -n tools/build-linux.sh + pwsh -NoProfile -Command ' + $failed = $false + foreach ($path in @("tools/build-windows.ps1", "tools/make_release.ps1")) { + $tokens = $null; $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path $path), [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -ne 0) { $failed = $true; Write-Error ($errors | Out-String) } } - } - if ($failed) { exit 1 } + if ($failed) { exit 1 } + ' - - name: Verify upstream launcher sync + - name: Verify upstream launcher and QA sync + shell: bash run: | + set -euo pipefail base=https://raw.githubusercontent.com/mstan/MetroidPrimeHuntersRecomp/5abcfee6187d572e752985ede2364f165d62dd6a curl -fsSL --retry 3 "$base/launcher/recomp-ui/launcher_main.cpp" -o /tmp/upstream-launcher.cpp curl -fsSL --retry 3 "$base/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp" -o /tmp/upstream-launcher-test.cpp @@ -75,8 +68,10 @@ jobs: grep -q -- '--firmware-state-path' launcher/recomp-ui/launcher_main.cpp grep -q 'feature_count == 3' launcher/recomp-ui/tests/launcher_mod_provider_test.cpp - - name: Cross-check runtime detector against melonPrimeDS develop_hud + - name: Cross-check melonPrimeDS detector and widescreen table + shell: bash run: | + set -euo pipefail curl -fsSL --retry 3 \ https://raw.githubusercontent.com/ag-advania/melonPrimeDS/develop_hud/src/frontend/qt_sdl/MelonPrimeGameRomAddrTable.h \ -o /tmp/MelonPrimeGameRomAddrTable.h @@ -88,9 +83,23 @@ jobs: python tools/check_melonprime_detector.py \ --profiles config/mph_rom_profiles.json \ --detector /tmp/MelonPrimeGameRomDetect.cpp + python - <<'PY' + import json + p=json.load(open('config/mph_rom_profiles.json', encoding='utf-8')) + assert p['schema'] == 5 + assert len(p['runtime_profiles']) == 7 + assert len(p['runtime_checksums']) == 17 + assert p['profiles']['US1_0']['adaptive_widescreen'] is True + assert p['profiles']['EU1_1']['adaptive_widescreen'] is True + for r in p['runtime_profiles'].values(): + for k in ('morph_state','aim_x','aim_y','scale_patch_addr1','scale_patch_addr2','scale_value_addr'): + assert k in r['runtime'] + PY - - name: Test profile-aware coverage promotion + - name: Verify profile-aware coverage routing + shell: bash run: | + set -euo pipefail mkdir -p /tmp/eu-inputs cat > /tmp/eu-inputs/arm9.toml <<'EOF' [program] @@ -118,46 +127,24 @@ jobs: } EOF python tools/promote_mph_static_coverage.py \ - --version EU1_1 \ - --inputs /tmp/eu-inputs \ - --trace /tmp/eu-trace.json \ + --version EU1_1 --inputs /tmp/eu-inputs --trace /tmp/eu-trace.json \ --out /tmp/eu-coverage.json \ --runner-commit 0123456789abcdef0123456789abcdef01234567 python - <<'PY' import json - p = json.load(open('/tmp/eu-coverage.json', encoding='utf-8')) + p=json.load(open('/tmp/eu-coverage.json', encoding='utf-8')) assert p['profile'] == 'EU1_1' assert p['game_sha1'] == 'bdcd1dea293e24c98d4c481430e90d21198985a5' - assert p['scenario'] == 'scenarios/adventure_start.json' - assert p['entry_points']['arm9'] - assert p['entry_points']['arm7'] + assert p['entry_points']['arm9'] and p['entry_points']['arm7'] PY - - - name: Verify registry-driven profile and FMV routing - run: | - grep -q 'MPH_PROFILE_FMV_RUNTIME_BANK' CMakeLists.txt - grep -q 'config/${MPH_PROFILE_FMV_RUNTIME_BANK}.toml' CMakeLists.txt - grep -q 'capture/${MPH_PROFILE_FMV_RUNTIME_BANK}.bin' CMakeLists.txt - ! grep -q -- '--bank mph_arm9_fmv_runtime' CMakeLists.txt - grep -q 'MPH_PROFILE_KEYS' CMakeLists.txt - ! grep -q 'PROPERTY STRINGS US1_0 EU1_1' CMakeLists.txt - ! grep -q "ValidateSet('US1_0', 'EU1_1')" tools/build-windows.ps1 grep -q 'rom_bytes\[0x1E\]' tools/prepare_mph.py ! grep -q 'rom_bytes\[0x1C\]' tools/prepare_mph.py - python - <<'PY' - import json - p=json.load(open('config/mph_rom_profiles.json', encoding='utf-8')) - assert p['profiles']['US1_0']['adaptive_widescreen'] is True - assert p['profiles']['EU1_1']['adaptive_widescreen'] is True - assert len(p['runtime_profiles']) == 7 - assert len(p['runtime_checksums']) == 17 - for r in p['runtime_profiles'].values(): - for k in ('morph_state','aim_x','aim_y','scale_patch_addr1','scale_patch_addr2','scale_value_addr'): - assert k in r['runtime'] - PY + ! grep -q -- '--adaptive-widescreen' tools/build-linux.sh - name: Fetch exact pinned ndsrecomp revision + shell: bash run: | + set -euo pipefail pin="$(tr -d '\r\n' < ndsrecomp.pin)" test "$pin" = 6c6a03bdcf99093f64555c4d05d16e522dc58634 git init /tmp/ndsrecomp @@ -167,7 +154,9 @@ jobs: test "$(git -C /tmp/ndsrecomp rev-parse HEAD)" = "$pin" - name: Render US1.0 and EU1.1 launcher profiles + shell: bash run: | + set -euo pipefail mkdir -p /tmp/fake-prefix/lib/cmake/SDL2 /tmp/fake-recomp-ui cat > /tmp/fake-prefix/lib/cmake/SDL2/SDL2Config.cmake <<'EOF' if(NOT TARGET SDL2::SDL2) @@ -178,56 +167,32 @@ jobs: function(recomp_target_launcher_ui) endfunction() EOF - cmake -S launcher/recomp-ui -B /tmp/mph-launcher-us10 \ - -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ - -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ + -DNDSRECOMP_ROOT=/tmp/ndsrecomp -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ -DCMAKE_PREFIX_PATH=/tmp/fake-prefix cmake -S launcher/recomp-ui -B /tmp/mph-launcher-eu11 \ - -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ - -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ + -DNDSRECOMP_ROOT=/tmp/ndsrecomp -DRECOMP_UI_ROOT=/tmp/fake-recomp-ui \ -DCMAKE_PREFIX_PATH=/tmp/fake-prefix \ -DMPH_LAUNCHER_ROM_SHA1=bdcd1dea293e24c98d4c481430e90d21198985a5 \ -DMPH_LAUNCHER_REGION=Europe \ '-DMPH_LAUNCHER_DEFAULT_ROM=Metroid Prime Hunters (Europe Rev 1).nds' + for file in /tmp/mph-launcher-us10/launcher_main_profile.cpp /tmp/mph-launcher-eu11/launcher_main_profile.cpp; do + grep -q 'game.known_sha1_hex = nullptr;' "$file" + grep -q 'game.num_known_sha1 = 0;' "$file" + grep -A1 'int mod_feature_count' "$file" | grep -q 'return 3;' + grep -q 'copy_text(output->id, "hd-rendering")' "$file" + grep -q -- '--firmware-state-path' "$file" + grep -q 'bool adaptive_widescreen = true;' "$file" + done + grep -q 'game.region = "Europe";' /tmp/mph-launcher-eu11/launcher_main_profile.cpp + grep -q 'exe / "Metroid Prime Hunters (Europe Rev 1).nds";' /tmp/mph-launcher-eu11/launcher_main_profile.cpp - us=/tmp/mph-launcher-us10/launcher_main_profile.cpp - eu=/tmp/mph-launcher-eu11/launcher_main_profile.cpp - test -f "$us" -a -f "$eu" - - grep -q '90164d1ac127ee5f9815ea4ae7de798c7b5fc629' "$us" - grep -q 'bdcd1dea293e24c98d4c481430e90d21198985a5' "$eu" - grep -q 'game.region = "USA";' "$us" - grep -q 'game.region = "Europe";' "$eu" - grep -q 'game.known_sha1_hex = nullptr;' "$us" - grep -q 'game.known_sha1_hex = nullptr;' "$eu" - grep -q 'game.num_known_sha1 = 0;' "$us" - grep -q 'game.num_known_sha1 = 0;' "$eu" - grep -q 'exe / "Metroid Prime Hunters (Europe Rev 1).nds";' "$eu" - grep -q 'bool adaptive_widescreen = true;' "$us" - grep -q 'bool adaptive_widescreen = true;' "$eu" - grep -A1 'int mod_feature_count' "$us" | grep -q 'return 3;' - grep -A1 'int mod_feature_count' "$eu" | grep -q 'return 3;' - grep -q 'copy_text(output->id, "hd-rendering")' "$us" - grep -q 'copy_text(output->id, "hd-rendering")' "$eu" - grep -q -- '--firmware-state-path' "$us" - grep -q -- '--firmware-state-path' "$eu" - ! grep -q 'game.known_sha1_hex = sha1;' "$us" - ! grep -q 'game.known_sha1_hex = sha1;' "$eu" - - - name: Verify Linux profile-owned launch policy - run: | - grep -q 'launcher_default_rom' tools/build-linux.sh - grep -q 'fmv_runtime_bank' tools/build-linux.sh - grep -q 'cp "$GAME_CONFIG" "$APPDIR/usr/share/mph-recomp/game.toml"' tools/build-linux.sh - grep -q -- '--config "$HERE/usr/share/mph-recomp/game.toml"' tools/build-linux.sh - ! grep -q -- '--adaptive-widescreen' tools/build-linux.sh - - - name: Verify ndsrecomp runtime patch is idempotent + - name: Patch pinned runner and verify idempotency + shell: bash run: | + set -euo pipefail python tools/patch_ndsrecomp_mph_runtime.py \ - --framework-root /tmp/ndsrecomp \ - --profiles config/mph_rom_profiles.json + --framework-root /tmp/ndsrecomp --profiles config/mph_rom_profiles.json sha256sum \ /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h \ /tmp/ndsrecomp/runner/src/mph_widescreen_profiles.generated.h \ @@ -236,8 +201,7 @@ jobs: /tmp/ndsrecomp/runner/src/frontend.cpp \ /tmp/ndsrecomp/runner/src/main.cpp > /tmp/first.sha256 python tools/patch_ndsrecomp_mph_runtime.py \ - --framework-root /tmp/ndsrecomp \ - --profiles config/mph_rom_profiles.json + --framework-root /tmp/ndsrecomp --profiles config/mph_rom_profiles.json sha256sum \ /tmp/ndsrecomp/runner/src/mph_runtime_profiles.generated.h \ /tmp/ndsrecomp/runner/src/mph_widescreen_profiles.generated.h \ @@ -253,7 +217,9 @@ jobs: ! grep -q 'kMphUs10AimX' /tmp/ndsrecomp/runner/src/title_patches.cpp - name: Compile patched runner translation units + shell: bash run: | + set -euo pipefail mkdir -p /tmp/ndsrecomp/generated for source in \ arm9_bios.c arm9_bios_dispatch.c arm7_bios.c arm7_bios_dispatch.c \ @@ -261,41 +227,46 @@ jobs: : > "/tmp/ndsrecomp/generated/$source" done cmake -S /tmp/ndsrecomp/runner -B /tmp/nds-runner-profile-check \ - -DCMAKE_BUILD_TYPE=Release \ - -DNDS_BOOTSTRAP_FIRMWARE=ON \ - -DNDS_ENABLE_COMPUTE_RENDERER=OFF \ - -DNDS_ENABLE_PCAP_BACKEND=OFF + -DCMAKE_BUILD_TYPE=Release -DNDS_BOOTSTRAP_FIRMWARE=ON \ + -DNDS_ENABLE_COMPUTE_RENDERER=OFF -DNDS_ENABLE_PCAP_BACKEND=OFF cmake --build /tmp/nds-runner-profile-check --target src/title_patches.o -j2 cmake --build /tmp/nds-runner-profile-check --target src/frontend.o -j2 cmake --build /tmp/nds-runner-profile-check --target src/main.o -j2 - - name: Test runtime base-profile and compatibility dispatch + - name: Test seven-version runtime dispatch + shell: bash run: | + set -euo pipefail c++ -std=c++20 -Wall -Wextra -Wno-unused-parameter \ -I/tmp/ndsrecomp/runner/src \ + -I/tmp/ndsrecomp/recompiler/armv4t \ tools/tests/mph_runtime_profile_test.cpp \ /tmp/ndsrecomp/runner/src/title_patches.cpp \ -o /tmp/mph-runtime-profile-test /tmp/mph-runtime-profile-test - - name: Verify exact-content ROM checker inputs + - name: Compile exact-content ROM checkers + shell: bash run: | - python - <<'PY' - import json, tomllib - p=json.load(open('config/mph_rom_profiles.json', encoding='utf-8')) - for key in ('US1_0','EU1_1'): - item=p['profiles'][key] - with open(item['game_config'],'rb') as f: cfg=tomllib.load(f)['game'] - assert cfg['id'] == item['game_code'] - assert cfg['revision'] == item['revision'] - assert cfg['rom_size'] == item['rom_size'] - assert cfg['sha1'] == item['sha1'] - PY + set -euo pipefail + for profile in US1_0 EU1_1; do + build="/tmp/mph-romcheck-$profile" + cmake -S . -B "$build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDSRECOMP_ROOT=/tmp/ndsrecomp \ + -DMPH_VERSION="$profile" \ + -DMPH_ROM="/tmp/nonexistent-$profile.nds" + cmake --build "$build" --target mph_romcheck -j2 + test -x "$build/MetroidPrimeHuntersRecomp" + done + strings /tmp/mph-romcheck-EU1_1/MetroidPrimeHuntersRecomp | grep -q bdcd1dea293e24c98d4c481430e90d21198985a5 + strings /tmp/mph-romcheck-EU1_1/MetroidPrimeHuntersRecomp | grep -q EU1_1 - name: Diff sanity + shell: bash run: | + set -euo pipefail grep -q 'known_sha1_hex = nullptr' launcher/recomp-ui/CMakeLists.txt - ! grep -q 'rom_bytes\[0x1C\]' tools/prepare_mph.py ! grep -q 'Adaptive Widescreen: disabled until validated' docs/EU1_1_BRINGUP.md grep -q 'Adaptive Widescreen: exposed and revision-aware' docs/EU1_1_BRINGUP.md - git diff --check HEAD^ HEAD || true + git diff --check HEAD^ HEAD From 39b85492b27fdd6eee752e2e06b2aa39469a426d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:25:32 +0900 Subject: [PATCH 099/164] Fix shallow-checkout diff validation --- .github/workflows/mph-multirom-static.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml index a7af493..4df8af0 100644 --- a/.github/workflows/mph-multirom-static.yml +++ b/.github/workflows/mph-multirom-static.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout project uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set up Python uses: actions/setup-python@v5 From e3f0f9959f5d22b2547740689ed3a3c69c6cf1c8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 14:26:58 +0900 Subject: [PATCH 100/164] Sync upstream v0.4.0 release metadata --- CMakeLists.txt | 4 ++-- README.md | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a5d755..5dc2345 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(MetroidPrimeHuntersRecomp VERSION 0.3.0 LANGUAGES C CXX) +project(MetroidPrimeHuntersRecomp VERSION 0.4.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -263,4 +263,4 @@ if(NOT MSVC) endif() add_custom_target(metroidprimehuntersrecomp - DEPENDS mph_romcheck mph_recompiled_banks) \ No newline at end of file + DEPENDS mph_romcheck mph_recompiled_banks) diff --git a/README.md b/README.md index 26f91cd..98de666 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,24 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release Latest upstream release: -**[v0.3.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.3.0-alpha)**. +**[v0.4.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.4.0-alpha)**. Downloads: - Windows: - `MetroidPrimeHuntersRecomp-windows-x64-v0.3.0.zip` + `MetroidPrimeHuntersRecomp-windows-x64-v0.4.0.zip` - Linux: - `MetroidPrimeHuntersRecomp-linux-x86_64-v0.3.0.AppImage` + `MetroidPrimeHuntersRecomp-linux-x86_64-v0.4.0.AppImage` This is an early ndsrecomp title and should still be treated as an alpha test build rather than a polished game release. +New in upstream v0.4.0 is an opt-in **HD Rendering** mod on the launcher Mods +page. It can raise the internal 3D resolution up to 4x and optionally upscale +decoded textures. It is disabled by default; native rendering remains the +reference path. This branch keeps that upstream feature alongside the +multi-ROM-safe Adaptive Widescreen and Prime Controls work. + ## Quick Start Windows: @@ -80,9 +86,13 @@ content/capture coverage before it is considered fully brought up. projection/culling addresses from melonPrimeDS/mphCodex. - Includes Prime-style keyboard and mouse controls. - Includes full remappable gamepad bindings in the launcher. +- Includes upstream HD Rendering controls for internal resolution and texture + upscaling. - Supports mouse-driven touchscreen input. - Can authenticate through Wiimmfi and reach a Friends and Rivals lobby in validated flows. +- Persists mutable firmware/WFC state between launches through the upstream + firmware-state path. ## Known Limits @@ -92,6 +102,8 @@ content/capture coverage before it is considered fully brought up. validated from start to finish. - Widescreen is still being audited. Some scenes, effects, HUD placement, movies, fades, or screen-routing behavior may be wrong. +- HD texture upscaling remains opt-in and should not be treated as the native + reference rendering path. - Online play is experimental. Wiimmfi can reach the lobby in validated flows, but in-game play is ultimately untested. There is no guarantee that a match will connect, stay connected, or avoid desync. From f1fe8d827e574bc630e4dfad7b6b75de3bfeb242 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:08:43 +0900 Subject: [PATCH 101/164] Pin recomp-ui for reproducible CI builds --- recomp-ui.pin | 1 + 1 file changed, 1 insertion(+) create mode 100644 recomp-ui.pin diff --git a/recomp-ui.pin b/recomp-ui.pin new file mode 100644 index 0000000..f428d8e --- /dev/null +++ b/recomp-ui.pin @@ -0,0 +1 @@ +8e385a0bff407e379414ba5ccbbcde1fac27e5cd From ecabcc194cfbcfd4b8db4bc27b97205c15f2ad21 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:08:55 +0900 Subject: [PATCH 102/164] Add Nightly release payload verifier --- tools/ci/verify-nightly-assets.py | 118 ++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tools/ci/verify-nightly-assets.py diff --git a/tools/ci/verify-nightly-assets.py b/tools/ci/verify-nightly-assets.py new file mode 100644 index 0000000..6af8358 --- /dev/null +++ b/tools/ci/verify-nightly-assets.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Validate MPH Nightly release assets before publishing them.""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path +import re +import sys +import zipfile + + +FORBIDDEN_PARTS = { + "biosnds9.rom", + "biosnds7.rom", + "firmware.bin", +} +FORBIDDEN_SUFFIXES = { + ".nds", + ".sav", + ".dsv", +} +FORBIDDEN_DIRS = { + "generated", + "capture", + "captures", + "saves", +} + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def safe_member(name: str) -> bool: + normalized = name.replace("\\", "/") + parts = [part for part in normalized.split("/") if part not in ("", ".")] + if normalized.startswith("/") or any(part == ".." for part in parts): + return False + lowered = [part.lower() for part in parts] + if any(part in FORBIDDEN_PARTS for part in lowered): + return False + if any(part in FORBIDDEN_DIRS for part in lowered): + return False + if parts and Path(parts[-1]).suffix.lower() in FORBIDDEN_SUFFIXES: + return False + return True + + +def verify_windows(path: Path) -> None: + required = { + "MetroidPrimeHuntersRecomp.exe", + "nds_runner.exe", + "game.toml", + "README.md", + "LICENSE", + "bios/README.txt", + } + with zipfile.ZipFile(path) as archive: + names = { + name.replace("\\", "/").rstrip("/") + for name in archive.namelist() + if name and not name.endswith("/") + } + unsafe = sorted(name for name in names if not safe_member(name)) + if unsafe: + raise SystemExit(f"{path.name}: forbidden/unsafe ZIP entries: {unsafe}") + missing = sorted(required - names) + if missing: + raise SystemExit(f"{path.name}: required release entries missing: {missing}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--dist", type=Path, required=True) + parser.add_argument("--version", required=True) + parser.add_argument("--write-sums", action="store_true") + args = parser.parse_args() + + if not re.fullmatch(r"\d+\.\d+\.\d+", args.version): + raise SystemExit(f"invalid version: {args.version!r}") + + expected = { + f"MetroidPrimeHuntersRecomp-windows-x64-v{args.version}.zip", + f"MetroidPrimeHuntersRecomp-linux-v{args.version}-x86_64.AppImage", + } + actual = {p.name for p in args.dist.iterdir() if p.is_file()} + extra = actual - expected + missing = expected - actual + if extra or missing: + raise SystemExit( + f"nightly payload mismatch; missing={sorted(missing)} extra={sorted(extra)}" + ) + + windows = args.dist / f"MetroidPrimeHuntersRecomp-windows-x64-v{args.version}.zip" + linux = args.dist / f"MetroidPrimeHuntersRecomp-linux-v{args.version}-x86_64.AppImage" + if windows.stat().st_size <= 0 or linux.stat().st_size <= 0: + raise SystemExit("nightly payload contains an empty asset") + + verify_windows(windows) + + sums = "\n".join( + f"{sha256(args.dist / name)} {name}" for name in sorted(expected) + ) + "\n" + if args.write_sums: + (args.dist / "SHA256SUMS.txt").write_text(sums, encoding="utf-8") + else: + sys.stdout.write(sums) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From fef5e8f1cf6fa3b9c3bc618d82e9e0aa1b35bf55 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:09:18 +0900 Subject: [PATCH 103/164] Add portable Linux AppImage packager --- tools/package-linux-appimage.sh | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tools/package-linux-appimage.sh diff --git a/tools/package-linux-appimage.sh b/tools/package-linux-appimage.sh new file mode 100644 index 0000000..5f77b69 --- /dev/null +++ b/tools/package-linux-appimage.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION="0.4.0" +MPH_VERSION="US1_0" +RUNNER="" +OUT="$ROOT/release-stage" +APPIMAGE_TOOL="${APPIMAGE_TOOL:-appimagetool}" +LINUXDEPLOY_BIN="${LINUXDEPLOY_BIN:-linuxdeploy}" + +usage() { + cat <<'EOF' +Package an already-built MPH runner as a Linux x86_64 AppImage. + +Usage: + tools/package-linux-appimage.sh --runner PATH [options] + +Options: + --version VERSION Package version + --mph-version PROFILE Content profile (default: US1_0) + --runner PATH Built nds_runner executable (required) + --out PATH Output directory (default: release-stage) + --appimage-tool PATH appimagetool executable/AppImage + --linuxdeploy PATH linuxdeploy executable/AppImage +EOF +} + +while (($#)); do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --mph-version) MPH_VERSION="$2"; shift 2 ;; + --runner) RUNNER="$2"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + --appimage-tool) APPIMAGE_TOOL="$2"; shift 2 ;; + --linuxdeploy) LINUXDEPLOY_BIN="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac +done + +if [[ -z "$RUNNER" || ! -x "$RUNNER" ]]; then + printf 'Built runner is required: %s\n' "$RUNNER" >&2 + exit 1 +fi +if [[ ! -x "$APPIMAGE_TOOL" ]] && ! command -v "$APPIMAGE_TOOL" >/dev/null 2>&1; then + printf 'appimagetool not found: %s\n' "$APPIMAGE_TOOL" >&2 + exit 1 +fi +if [[ ! -x "$LINUXDEPLOY_BIN" ]] && ! command -v "$LINUXDEPLOY_BIN" >/dev/null 2>&1; then + printf 'linuxdeploy not found: %s\n' "$LINUXDEPLOY_BIN" >&2 + exit 1 +fi + +PROFILE_FILE="$ROOT/config/mph_rom_profiles.json" +GAME_CONFIG_REL="$(python3 - "$PROFILE_FILE" "$MPH_VERSION" <<'PY' +import json, sys +registry = json.load(open(sys.argv[1], encoding='utf-8')) +profile = registry.get('profiles', {}).get(sys.argv[2]) +if not isinstance(profile, dict): + raise SystemExit(f'unknown MPH profile: {sys.argv[2]}') +print(profile['game_config']) +PY +)" +GAME_CONFIG="$ROOT/$GAME_CONFIG_REL" +[[ -f "$GAME_CONFIG" ]] || { printf 'Game config missing: %s\n' "$GAME_CONFIG" >&2; exit 1; } + +mkdir -p "$OUT" +APP_NAME="MetroidPrimeHuntersRecomp" +APPDIR="$OUT/${APP_NAME}-${MPH_VERSION}-linux-x86_64.AppDir" +rm -rf "$APPDIR" +mkdir -p \ + "$APPDIR/usr/bin/bios" \ + "$APPDIR/usr/share/applications" \ + "$APPDIR/usr/share/icons/hicolor/256x256/apps" + +cp "$RUNNER" "$APPDIR/usr/bin/nds_runner" +cp "$GAME_CONFIG" "$APPDIR/usr/bin/game.toml" +cp "$ROOT/README.md" "$APPDIR/usr/bin/README.md" +cp "$ROOT/LICENSE" "$APPDIR/usr/bin/LICENSE" +cp "$ROOT/packaging/BIOS_README.txt" "$APPDIR/usr/bin/bios/README.txt" +chmod 0755 "$APPDIR/usr/bin/nds_runner" + +ICON="$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" +python3 - "$ICON" <<'PY' +import struct, sys, zlib +out = sys.argv[1] +n = 256 +raw = b''.join(bytes([0]) + bytes([162, 62, 64]) * n for _ in range(n)) +def chunk(kind, data): + body = kind + data + return struct.pack('>I', len(data)) + body + struct.pack('>I', zlib.crc32(body) & 0xffffffff) +png = b'\x89PNG\r\n\x1a\n' +png += chunk(b'IHDR', struct.pack('>IIBBBBB', n, n, 8, 2, 0, 0, 0)) +png += chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b'') +open(out, 'wb').write(png) +PY + +DESKTOP="$APPDIR/usr/share/applications/$APP_NAME.desktop" +cat > "$DESKTOP" </dev/null + +cat > "$APPDIR/AppRun" <<'EOF' +#!/bin/sh +HERE="$(dirname "$(readlink -f "$0")")" +export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:-}" +export SDL_JOYSTICK_HIDAPI_STEAM=1 +export SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=1 +SELF="${APPIMAGE:-$0}" +RUNDIR="$(dirname "$(readlink -f "$SELF")")" +mkdir -p "$RUNDIR/bios" 2>/dev/null || true +if [ ! -f "$RUNDIR/bios/README.txt" ] && [ -f "$HERE/usr/bin/bios/README.txt" ]; then + cp "$HERE/usr/bin/bios/README.txt" "$RUNDIR/bios/README.txt" 2>/dev/null || true +fi +ROM="" +for f in "$RUNDIR"/*.nds "$RUNDIR"/*.NDS; do + [ -e "$f" ] && ROM="$f" && break +done +cd "$RUNDIR" 2>/dev/null || true +if [ "$#" -eq 0 ]; then + if [ -n "$ROM" ]; then + exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" \ + --config "$HERE/usr/bin/game.toml" --screen-layout separate \ + --adaptive-widescreen top --startup-mode automatic + fi + exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive \ + --config "$HERE/usr/bin/game.toml" --screen-layout separate \ + --adaptive-widescreen top --startup-mode automatic +fi +exec "$HERE/usr/bin/nds_runner" "$@" +EOF +chmod 0755 "$APPDIR/AppRun" + +# Safety gate: the AppDir may contain only the runner/package support material. +if find "$APPDIR" -type f \( \ + -iname '*.nds' -o -iname '*.sav' -o -iname '*.dsv' -o \ + -iname 'biosnds9.rom' -o -iname 'biosnds7.rom' -o -iname 'firmware.bin' \ + \) -print -quit | grep -q .; then + echo 'Refusing to package ROM/save/BIOS/firmware material.' >&2 + exit 1 +fi + +if [[ "$MPH_VERSION" == "US1_0" ]]; then + OUTPUT="$OUT/${APP_NAME}-linux-v${VERSION}-x86_64.AppImage" +else + OUTPUT="$OUT/${APP_NAME}-${MPH_VERSION}-linux-v${VERSION}-x86_64.AppImage" +fi +rm -f "$OUTPUT" +ARCH=x86_64 "$APPIMAGE_TOOL" --appimage-extract-and-run "$APPDIR" "$OUTPUT" >/dev/null +chmod 0755 "$OUTPUT" +test -s "$OUTPUT" +sha256sum "$OUTPUT" +printf 'Created %s\n' "$OUTPUT" From 44a7b99da471e7167ddcb03c98c3135d57d398af Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:29:21 +0900 Subject: [PATCH 104/164] Add ROM-free runner fallback patch --- tools/patch_ndsrecomp_rom_free_release.py | 122 ++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tools/patch_ndsrecomp_rom_free_release.py diff --git a/tools/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py new file mode 100644 index 0000000..ba13523 --- /dev/null +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Make the pinned ndsrecomp runner buildable without proprietary BIOS banks. + +The public/no-dump build keeps the BSD-licensed FreeBIOS banks native. Retail +BIOS dumps remain usable when a user supplies them, but their immutable BIOS +code executes through the existing reference interpreter instead of requiring +ROM/BIOS-derived generated C in the distributed build. + +This patch is intentionally separate from the MPH title-profile patch stack: +it changes only the shared runner's immutable-BIOS build policy and is useful +for ROM-free CI/release packaging. It is idempotent and pinned to the currently +expected ndsrecomp source shape; drift fails closed. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def replace_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + count = text.count(old) + if count != 1: + raise SystemExit( + f"{path}: expected exactly one source anchor for {marker!r}, got {count}" + ) + path.write_text(text.replace(old, new), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + args = parser.parse_args() + root = args.framework_root.resolve() + + cmake = root / "runner" / "CMakeLists.txt" + state = root / "runner" / "src" / "state.h" + runtime = root / "runner" / "src" / "runtime_arm.cpp" + tier3 = root / "runner" / "src" / "tier3.cpp" + main_cpp = root / "runner" / "src" / "main.cpp" + for path in (cmake, state, runtime, tier3, main_cpp): + if not path.is_file(): + raise SystemExit(f"missing pinned ndsrecomp source: {path}") + + replace_once( + cmake, + '''option(NDS_BOOTSTRAP_FIRMWARE\n "Build with BIOS banks only so guest-produced firmware RAM can be captured"\n OFF)\n''', + '''option(NDS_BOOTSTRAP_FIRMWARE\n "Build with BIOS banks only so guest-produced firmware RAM can be captured"\n OFF)\noption(NDS_RETAIL_BIOS_BANKS\n "Link generated proprietary retail-BIOS banks instead of interpreter fallback"\n ON)\n''', + "NDS_RETAIL_BIOS_BANKS", + ) + + replace_once( + cmake, + '''add_library(nds_banks STATIC\n ${GEN}/arm9_bios.c\n ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c\n ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm9.c\n ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c\n ${GEN}/freebios_arm7_dispatch.c\n ${FW_BANK_BODIES}\n ${FW_BANK_DISPATCH}\n ${SM64DS_BANK_SOURCES}\n ${TITLE_BANK_SOURCES})\n''', + '''# Public/no-dump builds need only the redistributable FreeBIOS static banks.\n# Retail BIOS dumps can still be supplied at runtime when NDS_RETAIL_BIOS_BANKS=OFF;\n# their immutable code then uses the reference interpreter instead of generated C.\nset(IMMUTABLE_BIOS_BANK_SOURCES\n ${GEN}/freebios_arm9.c\n ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c\n ${GEN}/freebios_arm7_dispatch.c)\nif(NDS_RETAIL_BIOS_BANKS)\n list(APPEND IMMUTABLE_BIOS_BANK_SOURCES\n ${GEN}/arm9_bios.c\n ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c\n ${GEN}/arm7_bios_dispatch.c)\nelse()\n add_compile_definitions(NDS_RETAIL_BIOS_INTERPRETER=1)\nendif()\nadd_library(nds_banks STATIC\n ${IMMUTABLE_BIOS_BANK_SOURCES}\n ${FW_BANK_BODIES}\n ${FW_BANK_DISPATCH}\n ${SM64DS_BANK_SOURCES}\n ${TITLE_BANK_SOURCES})\n''', + "IMMUTABLE_BIOS_BANK_SOURCES", + ) + + replace_once( + cmake, + '''set_source_files_properties(\n ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c\n''', + '''set_source_files_properties(\n ${IMMUTABLE_BIOS_BANK_SOURCES}\n''', + "${IMMUTABLE_BIOS_BANK_SOURCES}", + ) + + replace_once( + cmake, + '''set(ARM9_BANK_SOURCES ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c)\nset(ARM7_BANK_SOURCES ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c)\n''', + '''set(ARM9_BANK_SOURCES\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c)\nset(ARM7_BANK_SOURCES\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c)\nif(NDS_RETAIL_BIOS_BANKS)\n list(APPEND ARM9_BANK_SOURCES\n ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c)\n list(APPEND ARM7_BANK_SOURCES\n ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c)\nendif()\n''', + "if(NDS_RETAIL_BIOS_BANKS)\n list(APPEND ARM9_BANK_SOURCES", + ) + + replace_once( + state, + '''extern bool g_discover_static_misses;\n''', + '''extern bool g_discover_static_misses;\n// Public ROM-free builds do not carry generated retail-BIOS code. When a\n// user explicitly supplies retail dumps, allow only immutable BIOS addresses\n// to use the same reference interpreter used by coverage discovery.\nextern bool g_allow_static_bios_interpreter;\n''', + "g_allow_static_bios_interpreter", + ) + + replace_once( + runtime, + '''bool g_discover_static_misses = false;\n''', + '''bool g_discover_static_misses = false;\nbool g_allow_static_bios_interpreter = false;\n''', + "g_allow_static_bios_interpreter = false", + ) + + replace_once( + runtime, + ''' if (g_discover_static_misses && static_bios_pc(pc)) {\n runtime_discovery_note_static(pc, thumb ? 1u : 0u);\n tier3_run(pc);\n return;\n }\n''', + ''' if ((g_discover_static_misses || g_allow_static_bios_interpreter) &&\n static_bios_pc(pc)) {\n if (g_discover_static_misses)\n runtime_discovery_note_static(pc, thumb ? 1u : 0u);\n tier3_run(pc);\n return;\n }\n''', + "g_allow_static_bios_interpreter) &&", + ) + + replace_once( + tier3, + ''' if (!bus_range_has_write_provenance(fetch_addr, fetch_size) &&\n !(g_discover_static_misses && static_bios_pc(pc & ~1u))) {\n''', + ''' if (!bus_range_has_write_provenance(fetch_addr, fetch_size) &&\n !((g_discover_static_misses || g_allow_static_bios_interpreter) &&\n static_bios_pc(pc & ~1u))) {\n''', + "g_allow_static_bios_interpreter) &&", + ) + + replace_once( + main_cpp, + '''extern "C" const DispatchEntry g_dispatch_arm9_bios[];\nextern "C" const unsigned g_dispatch_arm9_bios_len;\nextern "C" const DispatchEntry g_dispatch_arm7_bios[];\nextern "C" const unsigned g_dispatch_arm7_bios_len;\n''', + '''#if !defined(NDS_RETAIL_BIOS_INTERPRETER)\nextern "C" const DispatchEntry g_dispatch_arm9_bios[];\nextern "C" const unsigned g_dispatch_arm9_bios_len;\nextern "C" const DispatchEntry g_dispatch_arm7_bios[];\nextern "C" const unsigned g_dispatch_arm7_bios_len;\n#endif\n''', + "#if !defined(NDS_RETAIL_BIOS_INTERPRETER)", + ) + + replace_once( + main_cpp, + ''' } else {\n nds_register_dispatch(NDS_ARM9, g_dispatch_arm9_bios,\n g_dispatch_arm9_bios_len, 0xFFFF0000u);\n nds_register_dispatch(NDS_ARM7, g_dispatch_arm7_bios,\n g_dispatch_arm7_bios_len, 0x00000000u);\n }\n''', + ''' } else {\n#if defined(NDS_RETAIL_BIOS_INTERPRETER)\n g_allow_static_bios_interpreter = true;\n std::fprintf(stderr,\n "[dispatch] retail BIOS uses reference interpreter "\n "(ROM-free build)\\n");\n#else\n nds_register_dispatch(NDS_ARM9, g_dispatch_arm9_bios,\n g_dispatch_arm9_bios_len, 0xFFFF0000u);\n nds_register_dispatch(NDS_ARM7, g_dispatch_arm7_bios,\n g_dispatch_arm7_bios_len, 0x00000000u);\n#endif\n }\n''', + "retail BIOS uses reference interpreter", + ) + + print(f"Patched ROM-free release support in {root}") + + +if __name__ == "__main__": + main() From 95d511cfd95ee3bcbe5d0bba455b264e77e1d072 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:30:44 +0900 Subject: [PATCH 105/164] Fix ROM-free patch idempotent anchors --- tools/patch_ndsrecomp_rom_free_release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py index ba13523..40b4390 100644 --- a/tools/patch_ndsrecomp_rom_free_release.py +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -20,7 +20,7 @@ def replace_once(path: Path, old: str, new: str, marker: str) -> None: text = path.read_text(encoding="utf-8") - if marker in text: + if marker in text and old not in text: return count = text.count(old) if count != 1: @@ -63,7 +63,7 @@ def main() -> None: cmake, '''set_source_files_properties(\n ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c\n''', '''set_source_files_properties(\n ${IMMUTABLE_BIOS_BANK_SOURCES}\n''', - "${IMMUTABLE_BIOS_BANK_SOURCES}", + "set_source_files_properties(\n ${IMMUTABLE_BIOS_BANK_SOURCES}", ) replace_once( From fcb87fb16138c7c09ae3d3a9ca2abb2538465e24 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:30:57 +0900 Subject: [PATCH 106/164] Add redistributable FreeBIOS bank builder --- tools/ci/prepare_freebios_banks.py | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tools/ci/prepare_freebios_banks.py diff --git a/tools/ci/prepare_freebios_banks.py b/tools/ci/prepare_freebios_banks.py new file mode 100644 index 0000000..7751bd3 --- /dev/null +++ b/tools/ci/prepare_freebios_banks.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Build the redistributable ndsrecomp FreeBIOS native banks for CI/releases.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import subprocess + + +def run(*args: str) -> None: + print("+", " ".join(args), flush=True) + subprocess.run(args, check=True) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--build-dir", type=Path, required=True) + args = parser.parse_args() + + root = args.framework_root.resolve() + build = args.build_dir.resolve() + generated = root / "generated" + freebios = root / "third_party" / "freebios" + arm9_bin = freebios / "drastic_bios_arm9.bin" + arm7_bin = freebios / "drastic_bios_arm7.bin" + arm9_cfg = root / "bios" / "freebios9.toml" + arm7_cfg = root / "bios" / "freebios7.toml" + + for path in (arm9_bin, arm7_bin, arm9_cfg, arm7_cfg): + if not path.is_file(): + raise SystemExit( + f"missing FreeBIOS source {path}; initialize the pinned " + "ndsrecomp third_party/freebios submodule" + ) + + run( + "cmake", + "-S", str(root / "recompiler"), + "-B", str(build), + "-G", "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + ) + run("cmake", "--build", str(build), "--target", "nds_recompile") + + exe = build / ("nds_recompile.exe" if os.name == "nt" else "nds_recompile") + if not exe.is_file(): + raise SystemExit(f"nds_recompile missing after build: {exe}") + + generated.mkdir(parents=True, exist_ok=True) + for cpu, config, image in ( + ("arm9", arm9_cfg, arm9_bin), + ("arm7", arm7_cfg, arm7_bin), + ): + run( + str(exe), + "--config", str(config), + "--bin", str(image), + "--out", str(generated), + "--bank", f"freebios_{cpu}", + ) + + expected = [ + generated / "freebios_arm9.c", + generated / "freebios_arm9_dispatch.c", + generated / "freebios_arm7.c", + generated / "freebios_arm7_dispatch.c", + ] + missing = [str(path) for path in expected if not path.is_file()] + if missing: + raise SystemExit(f"FreeBIOS bank generation incomplete: {missing}") + + print("FreeBIOS banks ready (BSD-2-Clause source path only).") + + +if __name__ == "__main__": + main() From 05fa97ca7bf02eb7e2c0887aac8978baedb31901 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:31:31 +0900 Subject: [PATCH 107/164] Document portable optimization cache directory --- packaging/CACHE_README.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packaging/CACHE_README.txt diff --git a/packaging/CACHE_README.txt b/packaging/CACHE_README.txt new file mode 100644 index 0000000..53c33b0 --- /dev/null +++ b/packaging/CACHE_README.txt @@ -0,0 +1,21 @@ +Metroid Prime Hunters Recomp optimization cache +================================================ + +This directory is reserved for locally generated optimization banks/caches. +The preferred portable layout is: + + cache/banks// + +The whole-ROM SHA-1 is used only as the content/cache namespace. Runtime base +profile selection continues to use the executable-compatible MPH detector and +must never guess a base profile from this cache path. + +Current ROM-free Nightly builds do not generate native title banks here yet; +missing title banks execute through the ndsrecomp Tier-3 reference interpreter. +A future local JIT/portable-bank backend will populate this directory without +requiring a C/C++ compiler on the player's machine. + +If the application directory is not writable, implementations should fall back +to the operating system cache location (LOCALAPPDATA on Windows, XDG cache on +Linux). Saves and firmware identity/state are persistent user data and do not +belong in this regenerable cache. From 6be471d6f847d5d314a46f0ee1062f4fe32cd9a5 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:31:57 +0900 Subject: [PATCH 108/164] Use portable-first cache root in Linux package --- tools/package-linux-appimage.sh | 35 ++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tools/package-linux-appimage.sh b/tools/package-linux-appimage.sh index 5f77b69..f5c9036 100644 --- a/tools/package-linux-appimage.sh +++ b/tools/package-linux-appimage.sh @@ -11,14 +11,14 @@ LINUXDEPLOY_BIN="${LINUXDEPLOY_BIN:-linuxdeploy}" usage() { cat <<'EOF' -Package an already-built MPH runner as a Linux x86_64 AppImage. +Package an already-built ROM-free MPH runner as a Linux x86_64 AppImage. Usage: tools/package-linux-appimage.sh --runner PATH [options] Options: --version VERSION Package version - --mph-version PROFILE Content profile (default: US1_0) + --mph-version PROFILE Content profile metadata (default: US1_0) --runner PATH Built nds_runner executable (required) --out PATH Output directory (default: release-stage) --appimage-tool PATH appimagetool executable/AppImage @@ -71,6 +71,7 @@ APPDIR="$OUT/${APP_NAME}-${MPH_VERSION}-linux-x86_64.AppDir" rm -rf "$APPDIR" mkdir -p \ "$APPDIR/usr/bin/bios" \ + "$APPDIR/usr/share/mph-recomp" \ "$APPDIR/usr/share/applications" \ "$APPDIR/usr/share/icons/hicolor/256x256/apps" @@ -79,6 +80,7 @@ cp "$GAME_CONFIG" "$APPDIR/usr/bin/game.toml" cp "$ROOT/README.md" "$APPDIR/usr/bin/README.md" cp "$ROOT/LICENSE" "$APPDIR/usr/bin/LICENSE" cp "$ROOT/packaging/BIOS_README.txt" "$APPDIR/usr/bin/bios/README.txt" +cp "$ROOT/packaging/CACHE_README.txt" "$APPDIR/usr/share/mph-recomp/CACHE_README.txt" chmod 0755 "$APPDIR/usr/bin/nds_runner" ICON="$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" @@ -117,6 +119,7 @@ EOF cat > "$APPDIR/AppRun" <<'EOF' #!/bin/sh +set -eu HERE="$(dirname "$(readlink -f "$0")")" export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:-}" export SDL_JOYSTICK_HIDAPI_STEAM=1 @@ -127,6 +130,26 @@ mkdir -p "$RUNDIR/bios" 2>/dev/null || true if [ ! -f "$RUNDIR/bios/README.txt" ] && [ -f "$HERE/usr/bin/bios/README.txt" ]; then cp "$HERE/usr/bin/bios/README.txt" "$RUNDIR/bios/README.txt" 2>/dev/null || true fi + +# Portable-first optimization cache. A future local JIT/portable-bank backend +# namespaces children by whole-ROM content SHA-1; the hash is cache identity, +# never the runtime base-profile selector. If the AppImage directory cannot be +# written, fall back to the standard XDG cache location. +PORTABLE_CACHE="$RUNDIR/cache/banks" +CACHE_ROOT="$PORTABLE_CACHE" +if mkdir -p "$PORTABLE_CACHE" 2>/dev/null && + : > "$PORTABLE_CACHE/.mph-write-test" 2>/dev/null; then + rm -f "$PORTABLE_CACHE/.mph-write-test" +else + CACHE_BASE="${XDG_CACHE_HOME:-${HOME:-$RUNDIR}/.cache}" + CACHE_ROOT="$CACHE_BASE/MetroidPrimeHuntersRecomp/banks" + mkdir -p "$CACHE_ROOT" +fi +if [ ! -f "$CACHE_ROOT/README.txt" ] && [ -f "$HERE/usr/share/mph-recomp/CACHE_README.txt" ]; then + cp "$HERE/usr/share/mph-recomp/CACHE_README.txt" "$CACHE_ROOT/README.txt" 2>/dev/null || true +fi +export MPH_BANK_CACHE_ROOT="$CACHE_ROOT" + ROM="" for f in "$RUNDIR"/*.nds "$RUNDIR"/*.NDS; do [ -e "$f" ] && ROM="$f" && break @@ -136,17 +159,19 @@ if [ "$#" -eq 0 ]; then if [ -n "$ROM" ]; then exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" \ --config "$HERE/usr/bin/game.toml" --screen-layout separate \ - --adaptive-widescreen top --startup-mode automatic + --adaptive-widescreen top --startup-mode automatic \ + --freebios --generated-firmware --boot direct fi exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive \ --config "$HERE/usr/bin/game.toml" --screen-layout separate \ - --adaptive-widescreen top --startup-mode automatic + --adaptive-widescreen top --startup-mode automatic \ + --freebios --generated-firmware --boot direct fi exec "$HERE/usr/bin/nds_runner" "$@" EOF chmod 0755 "$APPDIR/AppRun" -# Safety gate: the AppDir may contain only the runner/package support material. +# Safety gate: the AppDir may contain only runner/package support material. if find "$APPDIR" -type f \( \ -iname '*.nds' -o -iname '*.sav' -o -iname '*.dsv' -o \ -iname 'biosnds9.rom' -o -iname 'biosnds7.rom' -o -iname 'firmware.bin' \ From 4e722e5b4d10b368c9d3c91d6d376a6952fe62b1 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:32:19 +0900 Subject: [PATCH 109/164] Add ROM-free Windows Nightly packager --- tools/package-windows-nightly.ps1 | 125 ++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tools/package-windows-nightly.ps1 diff --git a/tools/package-windows-nightly.ps1 b/tools/package-windows-nightly.ps1 new file mode 100644 index 0000000..c223753 --- /dev/null +++ b/tools/package-windows-nightly.ps1 @@ -0,0 +1,125 @@ +<# +Package a ROM-free Metroid Prime Hunters Recomp Windows Nightly. + +Unlike tools/make_release.ps1, this packager intentionally does not require a +ROM-derived MPH/FMV native bank. The title executes through Tier-3 when no +content-specific optimization bank exists. FreeBIOS native banks are built from +the redistributable BSD-2-Clause FreeBIOS source path. +#> +param( + [Parameter(Mandatory = $true)][string]$Version, + [Parameter(Mandatory = $true)][string]$RunnerBuildDir, + [Parameter(Mandatory = $true)][string]$LauncherBuildDir, + [Parameter(Mandatory = $true)][string]$RuntimeBinDir, + [string]$OutputDir = 'release-stage' +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +$runnerBuild = [IO.Path]::GetFullPath((Join-Path $root $RunnerBuildDir)) +$launcherBuild = [IO.Path]::GetFullPath((Join-Path $root $LauncherBuildDir)) +$runtimeBin = [IO.Path]::GetFullPath($RuntimeBinDir) +$runner = Join-Path $runnerBuild 'nds_runner.exe' +$launcher = Join-Path $launcherBuild 'mph-recomp-ui.exe' +$assets = Join-Path $launcherBuild 'assets' + +foreach ($required in @($runner, $launcher, $assets)) { + if (-not (Test-Path -LiteralPath $required)) { + throw "Nightly input missing: $required" + } +} + +$projectText = Get-Content (Join-Path $root 'CMakeLists.txt') -Raw +if ($projectText -notmatch + "project\(MetroidPrimeHuntersRecomp VERSION $([regex]::Escape($Version)) ") { + throw "CMake project version does not match Nightly package $Version." +} + +# A ROM-free Nightly must not accidentally link a title-specific generated +# bank. The symbols are intentionally left visible in MinGW builds; reject the +# known MPH bank identities if they ever leak back into this package path. +$runnerText = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($runner)) +foreach ($forbiddenBank in @('g_dispatch_mph_arm9', 'g_dispatch_mph_arm7', + 'mph_arm9_fmv_runtime')) { + if ($runnerText.Contains($forbiddenBank)) { + throw "ROM-free Nightly unexpectedly contains title bank: $forbiddenBank" + } +} + +$out = [IO.Path]::GetFullPath((Join-Path $root $OutputDir)) +$stageName = "MetroidPrimeHuntersRecomp-windows-x64-v$Version" +$stage = Join-Path $out $stageName +$zip = Join-Path $out "$stageName.zip" + +if (Test-Path -LiteralPath $stage) { Remove-Item $stage -Recurse -Force } +if (Test-Path -LiteralPath $zip) { Remove-Item $zip -Force } +New-Item -ItemType Directory -Path $stage -Force | Out-Null +New-Item -ItemType Directory -Path (Join-Path $stage 'bios') -Force | Out-Null +New-Item -ItemType Directory -Path (Join-Path $stage 'cache\banks') -Force | Out-Null + +Copy-Item -LiteralPath $launcher -Destination (Join-Path $stage 'MetroidPrimeHuntersRecomp.exe') +Copy-Item -LiteralPath $runner -Destination $stage +Copy-Item -LiteralPath $assets -Destination $stage -Recurse +Copy-Item -LiteralPath (Join-Path $root 'game.toml') -Destination $stage +Copy-Item -LiteralPath (Join-Path $root 'README.md') -Destination $stage +Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage +Copy-Item -LiteralPath (Join-Path $root 'packaging\BIOS_README.txt') ` + -Destination (Join-Path $stage 'bios\README.txt') +Copy-Item -LiteralPath (Join-Path $root 'packaging\CACHE_README.txt') ` + -Destination (Join-Path $stage 'cache\banks\README.txt') + +$runtimeDlls = @( + 'SDL2.dll', + 'libgcc_s_seh-1.dll', + 'libstdc++-6.dll', + 'libwinpthread-1.dll' +) +foreach ($name in $runtimeDlls) { + $source = Join-Path $runtimeBin $name + if (-not (Test-Path -LiteralPath $source)) { + throw "Required MinGW runtime DLL missing: $source" + } + Copy-Item -LiteralPath $source -Destination $stage +} + +$forbidden = @(Get-ChildItem -LiteralPath $stage -File -Recurse | + Where-Object { + $_.Extension.ToLowerInvariant() -in @('.nds', '.sav', '.dsv', '.gpr') -or + $_.Name.ToLowerInvariant() -in @('biosnds9.rom', 'biosnds7.rom', 'firmware.bin') -or + $_.FullName -match '[\\/](generated|capture|captures|saves)[\\/]' + }) +if ($forbidden.Count -ne 0) { + throw "Nightly stage contains forbidden material: $($forbidden.FullName -join ', ')" +} + +Add-Type -AssemblyName System.IO.Compression +Add-Type -AssemblyName System.IO.Compression.FileSystem +$stageFull = [IO.Path]::GetFullPath($stage) +$stagePrefix = $stageFull.TrimEnd('\') + '\' +$files = @(Get-ChildItem -LiteralPath $stage -File -Recurse | Sort-Object FullName) +$archive = [IO.Compression.ZipFile]::Open( + $zip, [IO.Compression.ZipArchiveMode]::Create) +try { + foreach ($file in $files) { + $fileFull = [IO.Path]::GetFullPath($file.FullName) + if (-not $fileFull.StartsWith($stagePrefix, + [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to archive a file outside release stage: $fileFull" + } + $entryName = $fileFull.Substring($stagePrefix.Length).Replace('\', '/') + if ($entryName.StartsWith('/') -or $entryName -match '(^|/)\.\.(/|$)') { + throw "Unsafe ZIP entry name: $entryName" + } + [IO.Compression.ZipFileExtensions]::CreateEntryFromFile( + $archive, $fileFull, $entryName, + [IO.Compression.CompressionLevel]::Optimal) | Out-Null + } +} finally { + $archive.Dispose() +} + +if (-not (Test-Path -LiteralPath $zip) -or (Get-Item $zip).Length -eq 0) { + throw 'Nightly ZIP was not created.' +} +Get-FileHash -LiteralPath $zip -Algorithm SHA256 | Format-Table -AutoSize +Write-Host "Created $zip" From f4c11bec07126a32e7bf6c5e4f6ada53efa53d57 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:03 +0900 Subject: [PATCH 110/164] Add ROM-free Windows build workflow --- .github/workflows/build-windows.yml | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/build-windows.yml diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..687644a --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,123 @@ +name: Build Windows + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + windows: + name: Windows ROM-free build + runs-on: windows-2025 + + steps: + - name: Check out title sources + uses: actions/checkout@v4 + + - name: Install MinGW toolchain + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + git + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-python + mingw-w64-x86_64-SDL2 + + - name: Fetch pinned ndsrecomp and recomp-ui + shell: msys2 {0} + run: | + set -euo pipefail + nds_pin="$(tr -d '\r\n' < ndsrecomp.pin)" + ui_pin="$(tr -d '\r\n' < recomp-ui.pin)" + git clone --filter=blob:none https://github.com/mstan/ndsrecomp.git ../ndsrecomp + git -C ../ndsrecomp checkout --detach "$nds_pin" + git -C ../ndsrecomp submodule update --init --recursive + git clone --filter=blob:none https://github.com/mstan/recomp-ui.git ../recomp-ui + git -C ../recomp-ui checkout --detach "$ui_pin" + test "$(git -C ../ndsrecomp rev-parse HEAD)" = "$nds_pin" + test "$(git -C ../recomp-ui rev-parse HEAD)" = "$ui_pin" + + - name: Apply MPH and ROM-free runner integration + shell: msys2 {0} + run: | + set -euo pipefail + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + # Both patch stacks are required to be idempotent. + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + + - name: Generate redistributable FreeBIOS banks + shell: msys2 {0} + run: | + set -euo pipefail + python tools/ci/prepare_freebios_banks.py \ + --framework-root ../ndsrecomp \ + --build-dir ../ndsrecomp/build-freebios-recompiler + test ! -e ../ndsrecomp/generated/arm9_bios.c + test ! -e ../ndsrecomp/generated/arm7_bios.c + + - name: Build ROM-free runner + shell: msys2 {0} + run: | + set -euo pipefail + cmake -S ../ndsrecomp/runner -B ../ndsrecomp/runner/build-mph-nightly \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDS_BOOTSTRAP_FIRMWARE=ON \ + -DNDS_RETAIL_BIOS_BANKS=OFF \ + -DNDS_ENABLE_COMPUTE_RENDERER=ON + cmake --build ../ndsrecomp/runner/build-mph-nightly + test -s ../ndsrecomp/runner/build-mph-nightly/nds_runner.exe + if grep -a -E -q 'g_dispatch_mph_arm(9|7)|mph_arm9_fmv_runtime' \ + ../ndsrecomp/runner/build-mph-nightly/nds_runner.exe; then + echo 'ROM-derived MPH title bank leaked into ROM-free runner' >&2 + exit 1 + fi + + - name: Build and test launcher + shell: msys2 {0} + run: | + set -euo pipefail + cmake -S launcher/recomp-ui -B launcher/recomp-ui/build-nightly \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDSRECOMP_ROOT="$PWD/../ndsrecomp" \ + -DRECOMP_UI_ROOT="$PWD/../recomp-ui" + cmake --build launcher/recomp-ui/build-nightly + ctest --test-dir launcher/recomp-ui/build-nightly --output-on-failure + test -s launcher/recomp-ui/build-nightly/mph-recomp-ui.exe + + - name: Package Windows Nightly payload + shell: msys2 {0} + run: | + set -euo pipefail + version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" + test -n "$version" + runtime_bin="$(cygpath -w /mingw64/bin)" + pwsh -NoProfile -File tools/package-windows-nightly.ps1 \ + -Version "$version" \ + -RunnerBuildDir '..\ndsrecomp\runner\build-mph-nightly' \ + -LauncherBuildDir 'launcher\recomp-ui\build-nightly' \ + -RuntimeBinDir "$runtime_bin" + test -s "release-stage/MetroidPrimeHuntersRecomp-windows-x64-v${version}.zip" + + - name: Upload Windows Nightly payload + uses: actions/upload-artifact@v4 + with: + name: mph-nightly-windows + path: release-stage/MetroidPrimeHuntersRecomp-windows-x64-v*.zip + if-no-files-found: error + retention-days: 14 From bf87a3e12624c764f1892c45b6f4d6762c3078f7 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:20 +0900 Subject: [PATCH 111/164] Add ROM-free Linux build workflow --- .github/workflows/build-linux.yml | 124 ++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .github/workflows/build-linux.yml diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..808bd3d --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,124 @@ +name: Build Linux + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + name: Linux ROM-free build + runs-on: ubuntu-24.04 + + steps: + - name: Check out title sources + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build git curl ca-certificates \ + libsdl2-dev libgl1-mesa-dev libx11-dev libxext-dev libxrandr-dev \ + libxcursor-dev libxi-dev libxinerama-dev libwayland-dev + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Fetch pinned ndsrecomp + run: | + set -euo pipefail + nds_pin="$(tr -d '\r\n' < ndsrecomp.pin)" + git clone --filter=blob:none https://github.com/mstan/ndsrecomp.git ../ndsrecomp + git -C ../ndsrecomp checkout --detach "$nds_pin" + git -C ../ndsrecomp submodule update --init --recursive + test "$(git -C ../ndsrecomp rev-parse HEAD)" = "$nds_pin" + + - name: Apply MPH and ROM-free runner integration + run: | + set -euo pipefail + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + + - name: Generate redistributable FreeBIOS banks + run: | + set -euo pipefail + python tools/ci/prepare_freebios_banks.py \ + --framework-root ../ndsrecomp \ + --build-dir ../ndsrecomp/build-freebios-recompiler + test ! -e ../ndsrecomp/generated/arm9_bios.c + test ! -e ../ndsrecomp/generated/arm7_bios.c + + - name: Build ROM-free runner + run: | + set -euo pipefail + cmake -S ../ndsrecomp/runner -B ../ndsrecomp/runner/build-mph-nightly \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDS_BOOTSTRAP_FIRMWARE=ON \ + -DNDS_RETAIL_BIOS_BANKS=OFF \ + -DNDS_ENABLE_COMPUTE_RENDERER=ON + cmake --build ../ndsrecomp/runner/build-mph-nightly + test -x ../ndsrecomp/runner/build-mph-nightly/nds_runner + if grep -a -E -q 'g_dispatch_mph_arm(9|7)|mph_arm9_fmv_runtime' \ + ../ndsrecomp/runner/build-mph-nightly/nds_runner; then + echo 'ROM-derived MPH title bank leaked into ROM-free runner' >&2 + exit 1 + fi + + - name: Install pinned AppImage packaging tools + run: | + set -euo pipefail + mkdir -p .ci-tools + curl -fL --retry 3 \ + https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage \ + -o .ci-tools/appimagetool + echo 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 .ci-tools/appimagetool' | sha256sum -c - + curl -fL --retry 3 \ + https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \ + -o .ci-tools/linuxdeploy + echo '421ca71d5c69ea97c6309276232990d43df1dcece0edfaa26bbf926ff96ed12e .ci-tools/linuxdeploy' | sha256sum -c - + chmod +x .ci-tools/appimagetool .ci-tools/linuxdeploy + + - name: Package Linux Nightly payload + run: | + set -euo pipefail + version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" + test -n "$version" + tools/package-linux-appimage.sh \ + --version "$version" \ + --runner ../ndsrecomp/runner/build-mph-nightly/nds_runner \ + --appimage-tool "$PWD/.ci-tools/appimagetool" \ + --linuxdeploy "$PWD/.ci-tools/linuxdeploy" + image="release-stage/MetroidPrimeHuntersRecomp-linux-v${version}-x86_64.AppImage" + test -s "$image" + mkdir -p /tmp/mph-appimage-audit + (cd /tmp/mph-appimage-audit && "$GITHUB_WORKSPACE/$image" --appimage-extract >/dev/null) + if find /tmp/mph-appimage-audit/squashfs-root -type f \( \ + -iname '*.nds' -o -iname '*.sav' -o -iname '*.dsv' -o \ + -iname 'biosnds9.rom' -o -iname 'biosnds7.rom' -o -iname 'firmware.bin' \ + \) -print -quit | grep -q .; then + echo 'Forbidden ROM/save/BIOS/firmware material found inside AppImage' >&2 + exit 1 + fi + + - name: Upload Linux Nightly payload + uses: actions/upload-artifact@v4 + with: + name: mph-nightly-linux + path: release-stage/MetroidPrimeHuntersRecomp-linux-v*-x86_64.AppImage + if-no-files-found: error + retention-days: 14 From 2dc1dc394bb482ccc5b7846498890699599dbeed Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:27 +0900 Subject: [PATCH 112/164] Add cross-platform ROM-free build CI --- .github/workflows/build.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..312a887 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +name: Build + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mph-build-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + windows: + name: Build Windows + uses: ./.github/workflows/build-windows.yml + + linux: + name: Build Linux + uses: ./.github/workflows/build-linux.yml From c5a90ea8b2ef9f36d3a95f4f36709443d1ff947c Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:49 +0900 Subject: [PATCH 113/164] Add ROM-free Nightly release workflow --- .github/workflows/nightly-release.yml | 164 ++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/nightly-release.yml diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml new file mode 100644 index 0000000..85057d4 --- /dev/null +++ b/.github/workflows/nightly-release.yml @@ -0,0 +1,164 @@ +name: Nightly Release + +# ROM-free public Nightly. The same Windows/Linux workflow used by PR CI builds +# the release payload, so CI and Nightly cannot silently drift apart. No ROM, +# ROM URL, ROM secret, proprietary BIOS, firmware dump, save, or ROM-derived +# title bank is fetched or uploaded by this workflow. + +on: + push: + branches: + - develop + workflow_dispatch: + +concurrency: + group: mph-nightly-release + cancel-in-progress: false + +permissions: + contents: read + +env: + NIGHTLY_TAG: nightly-release + NIGHTLY_NAME: Nightly Build + +jobs: + windows: + name: Build Windows + uses: ./.github/workflows/build-windows.yml + + linux: + name: Build Linux + uses: ./.github/workflows/build-linux.yml + + publish: + name: Publish Nightly release + needs: [windows, linux] + if: github.repository == 'Zection6V/MetroidPrimeHuntersRecomp' + runs-on: ubuntu-24.04 + permissions: + contents: write + + steps: + - name: Check out sources + uses: actions/checkout@v4 + + - name: Download Windows payload + uses: actions/download-artifact@v4 + with: + name: mph-nightly-windows + path: dist + + - name: Download Linux payload + uses: actions/download-artifact@v4 + with: + name: mph-nightly-linux + path: dist + + - name: Resolve project version + id: version + shell: bash + run: | + set -euo pipefail + version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" + test -n "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Nightly project version: $version" + + - name: Verify Nightly payload + run: | + set -euo pipefail + find dist -maxdepth 1 -type f -printf '%f (%s bytes)\n' | sort + python tools/ci/verify-nightly-assets.py \ + --dist dist \ + --version '${{ steps.version.outputs.version }}' \ + --write-sums + test -s dist/SHA256SUMS.txt + cat dist/SHA256SUMS.txt + + - name: Compose release notes + shell: bash + run: | + set -euo pipefail + cat > nightly-notes.md </\` beside the executable/AppImage for the future local optimization/JIT cache. + + These builds are automatic development snapshots and may be slower or less stable than optimized tagged releases. + EOF + cat nightly-notes.md + + - name: Move Nightly tag to this commit + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${NIGHTLY_TAG}" >/dev/null 2>&1; then + gh api --method PATCH \ + "repos/${GITHUB_REPOSITORY}/git/refs/tags/${NIGHTLY_TAG}" \ + -f sha="${GITHUB_SHA}" -F force=true >/dev/null + else + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f ref="refs/tags/${NIGHTLY_TAG}" -f sha="${GITHUB_SHA}" >/dev/null + fi + + - name: Create or update Nightly release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if gh release view "${NIGHTLY_TAG}" >/dev/null 2>&1; then + gh release edit "${NIGHTLY_TAG}" \ + --title "${NIGHTLY_NAME}" \ + --notes-file nightly-notes.md \ + --prerelease \ + --draft=false + else + gh release create "${NIGHTLY_TAG}" \ + --title "${NIGHTLY_NAME}" \ + --notes-file nightly-notes.md \ + --prerelease + fi + + - name: Upload Nightly assets + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + gh release upload "${NIGHTLY_TAG}" dist/* --clobber + + - name: Remove stale Nightly assets + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + (cd dist && ls -1) > published.txt + gh release view "${NIGHTLY_TAG}" --json assets --jq '.assets[].name' > attached.txt + while IFS= read -r asset; do + [ -n "$asset" ] || continue + if ! grep -Fxq "$asset" published.txt; then + gh release delete-asset "${NIGHTLY_TAG}" "$asset" --yes + fi + done < attached.txt + + - name: Summarize + shell: bash + run: | + { + echo '### Nightly Build published' + echo + echo "- Tag: \`${NIGHTLY_TAG}\` -> \`${GITHUB_SHA}\`" + echo '- ROM/ROM URL/ROM secret: **not used**' + echo '- Title-bank mode: ROM-free / Tier-3 fallback' + echo '- Assets:' + (cd dist && ls -1 | sed 's/^/ - /') + } >> "$GITHUB_STEP_SUMMARY" From 1b1c409bc1e3244b99992ee26451604f46aed75c Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:34:15 +0900 Subject: [PATCH 114/164] Document local optimization cache architecture --- docs/LOCAL_BANK_CACHE.md | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/LOCAL_BANK_CACHE.md diff --git a/docs/LOCAL_BANK_CACHE.md b/docs/LOCAL_BANK_CACHE.md new file mode 100644 index 0000000..3bcdf50 --- /dev/null +++ b/docs/LOCAL_BANK_CACHE.md @@ -0,0 +1,97 @@ +# Local optimization cache architecture + +## Status + +The public Nightly path is intentionally ROM-free. GitHub Actions builds the +runner, launcher, and redistributable FreeBIOS banks without receiving a +Metroid Prime Hunters ROM, a private ROM URL, proprietary BIOS dumps, firmware +dumps, saves, or generated title-bank source. + +Today, when no content-specific native title bank is linked, Metroid Prime +Hunters code loaded by direct boot executes through ndsrecomp Tier-3. This is +the correctness fallback, not the final performance target. + +## Portable-first cache root + +The preferred future optimization cache lives beside the distributed +executable/AppImage: + +```text +MetroidPrimeHuntersRecomp/ +├─ MetroidPrimeHuntersRecomp.exe # Windows +├─ Metroid Prime Hunters.nds # user-owned, optional filename +└─ cache/ + └─ banks/ + └─ / + ├─ manifest.json + └─ ... generated optimization payload ... +``` + +Linux AppImage packaging resolves the same `cache/banks` directory beside the +AppImage. If that directory is not writable, it falls back to +`$XDG_CACHE_HOME/MetroidPrimeHuntersRecomp/banks` (or `~/.cache/...`). Windows +implementations should analogously fall back to +`%LOCALAPPDATA%\MetroidPrimeHuntersRecomp\cache\banks` when the portable +location cannot be written. + +Save data, firmware/WFC identity, and other persistent user state are not +optimization cache data and must remain in their existing persistent app-data +locations. + +## Identity model + +Whole-ROM SHA-1 is appropriate for the cache namespace because cache payloads +must be bound to exact content. It must **not** become the runtime base-profile +selector. + +Runtime base identity remains the seven-profile MPH detector: + +- US1.0 +- US1.1 +- EU1.0 +- EU1.1 +- JP1.0 +- JP1.1 +- KR1.0 + +The authoritative fast path uses the executable checksum table. Exact supported +header tuples are only a candidate fallback, and dangerous host writes continue +to fail closed when executable compatibility is not authoritative. + +A future cache manifest should bind at least: + +```text +bank_format_version +runner_abi_version +ndsrecomp_codegen_version +host_arch +base_profile +content_sha1 +executable_crc32 +coverage_or_hotset_hash +validated_guest_code_hashes +``` + +Any incompatible field invalidates the cache and falls back to Tier-3 rather +than guessing compatibility. + +## Why first-run C compilation is not the target + +The current static ndsrecomp title-bank pipeline emits C and links it into +`nds_runner`. Reproducing that pipeline on a player's first launch would require +shipping or requiring a host C/C++ compiler and linker, complicating updates, +code signing, antivirus behavior, and cache ABI compatibility. + +Therefore the intended progression is: + +1. **Current:** ROM-free binary + Tier-3 fallback. +2. **Foundation:** portable-first per-content cache namespace. +3. **Next:** a compiler-free portable bank/IR cache if useful. +4. **Target:** Tier-3 hot-block detection -> host JIT -> validated persistent + local cache. + +Runtime/overlay code generated by the game should be eligible for the same hot +JIT path, avoiding the need to pre-capture one clean ROM's FMV runtime image. +This is particularly important for modified ROMs such as translations or other +code/data modifications, whose exact content must never silently reuse a clean +ROM's optimization payload. From 095e1b504a1334befcf43710c2afa334e1d96903 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:34:39 +0900 Subject: [PATCH 115/164] Document ROM-free Nightly execution model --- README.md | 225 ++++++++++++++++++++++-------------------------------- 1 file changed, 93 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 98de666..9a00c2a 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,11 @@ > audio issues, input quirks, networking failures, and possible desyncs. Testing, > issues, and PRs are welcome. -MetroidPrimeHuntersRecomp currently ships generated content profiles for the -validated ROM revisions tracked by this branch. Runtime base detection is -prepared for all seven retail Metroid Prime Hunters revisions and uses -melonPrimeDS-compatible executable checksums; whole-ROM SHA-1 is content -provenance, not the runtime address selector. You provide your own legally -obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated +MetroidPrimeHuntersRecomp runs **Metroid Prime Hunters** as an ndsrecomp target. +Runtime base-version detection supports the known US1.0, US1.1, EU1.0, EU1.1, +JP1.0, JP1.1, and KR1.0 layouts using executable-compatible detection rather +than using whole-ROM SHA-1 as the base-profile selector. You provide your own +legally obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated ROM-derived source is distributed. ## Gameplay Preview @@ -22,170 +21,132 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release -Latest upstream release: -**[v0.4.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.4.0-alpha)**. +The project version is **v0.4.0-alpha**. Development Nightly builds are produced +from `develop` under the fixed `nightly-release` prerelease tag after the +Windows and Linux build workflows and release-payload safety checks succeed. -Downloads: +The Nightly build path is deliberately **ROM-free**: GitHub Actions never needs +or downloads a Metroid Prime Hunters ROM, private ROM URL, proprietary BIOS or +firmware dump, save file, or ROM-derived MPH title bank. Users supply their ROM +at runtime. In the current Nightly architecture, title code without a linked +content-specific native bank executes through ndsrecomp Tier-3. This is a safe +correctness fallback and may be slower than an optimized tagged build. -- Windows: - `MetroidPrimeHuntersRecomp-windows-x64-v0.4.0.zip` -- Linux: - `MetroidPrimeHuntersRecomp-linux-x86_64-v0.4.0.AppImage` - -This is an early ndsrecomp title and should still be treated as an alpha test -build rather than a polished game release. - -New in upstream v0.4.0 is an opt-in **HD Rendering** mod on the launcher Mods -page. It can raise the internal 3D resolution up to 4x and optionally upscale -decoded textures. It is disabled by default; native rendering remains the -reference path. This branch keeps that upstream feature alongside the -multi-ROM-safe Adaptive Widescreen and Prime Controls work. +The package reserves a portable optimization-cache root beside the executable +or AppImage at `cache/banks//`. The current Nightly does not yet +generate native title banks there. The intended next step is a compiler-free +local optimization/JIT cache; see [`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). ## Quick Start Windows: 1. Download and fully extract the Windows ZIP. -2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the launcher. +2. Put your own Metroid Prime Hunters `.nds` ROM next to + `MetroidPrimeHuntersRecomp.exe`, or select it in the launcher. 3. Run `MetroidPrimeHuntersRecomp.exe` and press Play. Linux: -1. Download the AppImage. -2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the AppImage. +1. Download the AppImage and make it executable if required by your desktop. +2. Put your own Metroid Prime Hunters `.nds` ROM next to the AppImage. 3. Run the AppImage. The current release can use the built-in FreeBIOS + generated firmware path, so retail DS BIOS and firmware dumps are not required for the default no-dump -startup path. If you choose to use your own BIOS/firmware dumps, they must be -from hardware you own and must match the hashes listed in the release's -`bios/README.txt`. - -## ROM identity and multi-ROM support - -Runtime address selection does **not** use whole-ROM SHA-1. The runner first -uses the melonPrimeDS executable checksum (CRC32 over header + ARM9 + ARM7), -then uses exact game code + supported revision only as a fallback base-profile -hint. Header-only matches never authorize host RAM/code writes. - -Known compatible executable checksums can use the revision-specific Prime -Controls and Adaptive Widescreen addresses. Whole-ROM SHA-1 remains the exact -content identity for generated banks, coverage, checkpoints and capture data, -so one modified ROM cannot silently reuse another modified ROM's generated -content. - -The current branch has runtime address profiles for US1.0, US1.1, EU1.0, -EU1.1, JP1.0, JP1.1 and KR1.0. A revision still needs its own generated -content/capture coverage before it is considered fully brought up. - -## What Works - -- Boots supported content profiles through the ndsrecomp runner. -- Reaches Metroid Prime Hunters gameplay in tested routes. -- Includes an adaptive 21:9 upper-screen widescreen option using per-revision - projection/culling addresses from melonPrimeDS/mphCodex. -- Includes Prime-style keyboard and mouse controls. -- Includes full remappable gamepad bindings in the launcher. -- Includes upstream HD Rendering controls for internal resolution and texture - upscaling. -- Supports mouse-driven touchscreen input. -- Can authenticate through Wiimmfi and reach a Friends and Rivals lobby in - validated flows. -- Persists mutable firmware/WFC state between launches through the upstream - firmware-state path. +startup path. The ROM-free Nightly generates its native FreeBIOS banks only +from the redistributable BSD-2-Clause FreeBIOS source path at build time. -## Known Limits +## ROM identity and multi-ROM behavior -- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and - gameplay problems are expected. -- Gameplay coverage is incomplete. Do not assume the campaign is fully - validated from start to finish. -- Widescreen is still being audited. Some scenes, effects, HUD placement, - movies, fades, or screen-routing behavior may be wrong. -- HD texture upscaling remains opt-in and should not be treated as the native - reference rendering path. -- Online play is experimental. Wiimmfi can reach the lobby in validated flows, - but in-game play is ultimately untested. There is no guarantee that a match - will connect, stay connected, or avoid desync. -- Save behavior and settings are still part of early release testing. Keep - backups of anything you care about. - -## Controls - -Prime Controls are enabled by default. - -Keyboard and mouse defaults: - -- `WASD`: move -- Mouse: aim -- Mouse 1 / Mouse 2: fire / scan-fire -- `Space`: jump -- `Left Ctrl`: morph ball -- `Left Shift`: boost / map zoom -- `C`: scan visor -- `F`: OK -- `Q` / `E`: scan-message arrows -- `V`: menu -- Mouse 4: missiles -- Mouse 5: beam -- `1` through `6`: subweapons -- `Tab`: virtual stylus - -Gamepad defaults: - -- Left stick: move and menu D-pad -- Right stick: aim -- `RT` / `LT`: shoot / scan-fire -- `A`: jump -- `B`: morph ball -- `X`: missile -- `Y`: UI OK -- `LB` / `RB`: beam / boost or zoom -- `R3`: scan visor -- D-pad left/right: scan-message arrows -- `Start`: menu - -Keyboard, mouse, and gamepad bindings are editable from the launcher Mods page. +Three identity layers are kept separate: -## Online Play +1. **Runtime base profile:** executable checksum / exact supported header tuple. +2. **Executable compatibility:** determines whether dangerous host RAM/code + writes such as Aim/Morph/Adaptive Widescreen patches are authorized. +3. **Exact content identity:** whole-ROM SHA-1 for provenance, generated banks, + captures, and the future local optimization-cache namespace. + +Whole-ROM SHA-1 therefore does not decide that a modified ROM is US1.0 or EU1.1. +Unknown or ambiguous executable content never silently falls back to US1.0, and +host writes fail closed unless executable compatibility is authoritative. + +Exact clean-content profiles currently validated in the repository include the +US1.0 and EU1.1 bring-up tracks. Other base layouts are prepared at runtime, but +full per-ROM extraction, coverage, gameplay validation, and optimized bank work +must still be completed before they should be described as equally validated +release targets. + +## Enhancements + +### Adaptive Widescreen -Nintendo WFC / Wiimmfi support is experimental. The current validated state is -lobby connectivity: Metroid Prime Hunters can authenticate through Wiimmfi and -reach a Friends and Rivals lobby where a locally hosted game is visible. +The launcher exposes an adaptive 21:9 upper-screen mode. The implementation +combines ndsrecomp's widened host renderer/compositor and HUD anchoring with +profile-aware Metroid Prime Hunters projection/culling corrections derived from +the audited melonPrimeDS/mphCodex address tables. Unsupported/unsafe runtime +identity falls back rather than applying guessed guest writes. -The launcher keeps the console firmware profile in -`%APPDATA%\MetroidPrimeHuntersRecomp`. Wi-Fi settings, console/game-card -pairing, and WFC updates survive both the in-game system shutdown flow and a -normal window close. Confirming the WFC settings shutdown prompt closes the -application automatically. +### Prime Controls -Actually joining a match and playing in-game online is not guaranteed. It may -fail to connect, disconnect, or desync. +Prime-style keyboard/mouse controls and remappable gamepad bindings are exposed +through the launcher. Defaults include WASD movement, mouse aim, Mouse 1 fire, +and the existing touch-helper mappings. + +### HD Rendering + +HD Rendering is opt-in. It raises the 3D engine above native DS sample density +(up to the supported internal-resolution choices) and can upscale decoded +textures. The native 2D path remains the reference and HD Rendering is off by +default. + +## Online Play + +Nintendo WFC / Wiimmfi support remains experimental. The launcher persists the +console firmware profile in the user's application-data location so Wi-Fi +settings, console/game-card pairing, and WFC updates survive later launches. +Online play may still fail to connect, disconnect, or desync. The Wi-Fi implementation is built on [melonDS](https://github.com/melonDS-emu/melonDS)'s Wi-Fi work in the shared ndsrecomp runner. Full credit to the melonDS team for the Wi-Fi controller, emulated access point, and network backend foundation. +## Known Limits + +- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and + gameplay problems are expected. +- Gameplay coverage is incomplete across the seven base layouts. +- The ROM-free Nightly's Tier-3 title fallback can be substantially slower than + a build with validated native MPH optimization banks, especially in known hot + paths such as opening movies. +- The local `cache/banks//` directory is currently a reserved + cache contract; dynamic native/JIT bank generation is not implemented yet. +- Widescreen still requires sustained gameplay auditing across scenes, effects, + HUD placement, movies, fades, and screen routing. +- Online play is experimental. +- Save behavior and settings remain part of early release testing. Keep backups + of anything you care about. + ## Credits - [melonDS](https://github.com/melonDS-emu/melonDS): Wi-Fi implementation foundation used by the shared ndsrecomp runner. -- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): runtime-version - detection, Prime-style controls, and per-version aspect-ratio patch reference. -- [mphCodex](https://github.com/Zection6V/mphCodex): MPH game-code analysis, - including the seven-version widescreen projection/culling mapping. +- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): reference for + Prime-style controls, ROM/version address tables, and aspect-ratio research. +- [mphCodex](https://github.com/Zection6V/mphCodex): game-code/disassembly and + Metroid Prime Hunters behavior research. - [MphRead](https://github.com/NoneGiven/MphRead): Metroid Prime Hunters file format and behavior reference. -See the ndsrecomp -[`THIRD_PARTY_ATTRIBUTION.md`](https://github.com/mstan/ndsrecomp/blob/main/THIRD_PARTY_ATTRIBUTION.md) -for provenance and licensing details for shared runtime components. +See the ndsrecomp `THIRD_PARTY_ATTRIBUTION.md` for shared-runtime provenance and +licensing details. ## Developers -This README is intentionally player-facing. Development notes, validation -history, and bring-up details live in [`docs/BRINGUP.md`](docs/BRINGUP.md). +Bring-up and validation notes live under [`docs/`](docs/). The ROM-free Nightly +and local-cache direction is documented in +[`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). The original code in this repository is MIT licensed. Metroid Prime Hunters, Nintendo DS firmware/BIOS images, ROMs, saves, and all derived game data remain From 86823e76cb44b7c893382cf454c0730f9f4e44e1 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:34:53 +0900 Subject: [PATCH 116/164] Require portable cache contract in Windows Nightly --- tools/ci/verify-nightly-assets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ci/verify-nightly-assets.py b/tools/ci/verify-nightly-assets.py index 6af8358..2d8f341 100644 --- a/tools/ci/verify-nightly-assets.py +++ b/tools/ci/verify-nightly-assets.py @@ -60,6 +60,7 @@ def verify_windows(path: Path) -> None: "README.md", "LICENSE", "bios/README.txt", + "cache/banks/README.txt", } with zipfile.ZipFile(path) as archive: names = { From e946c1e693ca2c940f0b2354b1e53dcd22fc0132 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:02 +0900 Subject: [PATCH 117/164] Ignore local optimization cache --- .gitignore | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 1608302..14264b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,39 @@ -# User-owned Nintendo DS images and ROM-derived outputs. +# ROM / console dumps *.nds -/generated/ -/extracted/ +*.NDS +*.sav +*.dsv +bios/*.rom +bios/*.bin +firmware.bin -# Optional reverse-engineering references. -/third_party/ +# Generated recompilation inputs/outputs and captures +generated/ +release-stage/ -# Build trees, runtime state, and captures. -/build/ -/build-*/ -/release-stage/ -/launcher/recomp-ui/build*/ -/saves/ -/captures/ -/scratch/ -*.sav -*.log -/rom.cfg -/settings.toml -/keybinds.ini +# Local optimization cache (portable-first runtime/JIT cache) +cache/ + +# Build trees +build/ +build-*/ +launcher/recomp-ui/build/ +launcher/recomp-ui/build-*/ -# Local Python environment and tool output. -/.venv/ +# Python +.venv/ __pycache__/ *.pyc -# Editor and OS output. -.vs/ +# Tool/editor state .vscode/ .idea/ -Thumbs.db +*.suo +*.user +*.gpr +*.rep +*.lock + +# OS junk .DS_Store +Thumbs.db From 901d4f0dac10b552294b4f38a0e3c46017619883 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:24 +0900 Subject: [PATCH 118/164] Add ROM-free source policy check --- tools/ci/check_rom_free_release_sources.py | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tools/ci/check_rom_free_release_sources.py diff --git a/tools/ci/check_rom_free_release_sources.py b/tools/ci/check_rom_free_release_sources.py new file mode 100644 index 0000000..5813bac --- /dev/null +++ b/tools/ci/check_rom_free_release_sources.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Pin the public Nightly's no-ROM-secret source policy.""" + +from __future__ import annotations + +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[2] +WORKFLOWS = [ + ROOT / ".github" / "workflows" / "build-windows.yml", + ROOT / ".github" / "workflows" / "build-linux.yml", + ROOT / ".github" / "workflows" / "build.yml", + ROOT / ".github" / "workflows" / "nightly-release.yml", +] + + +def main() -> int: + failures: list[str] = [] + for path in WORKFLOWS: + if not path.is_file(): + failures.append(f"missing release workflow: {path.relative_to(ROOT)}") + continue + text = path.read_text(encoding="utf-8") + if "MPH_US10_ROM_URL" in text: + failures.append(f"{path.relative_to(ROOT)} still references MPH_US10_ROM_URL") + if "secrets." in text: + failures.append( + f"{path.relative_to(ROOT)} references repository/environment secrets" + ) + + nightly = (ROOT / ".github" / "workflows" / "nightly-release.yml").read_text( + encoding="utf-8" + ) + for required in ( + "uses: ./.github/workflows/build-windows.yml", + "uses: ./.github/workflows/build-linux.yml", + "NIGHTLY_TAG: nightly-release", + "verify-nightly-assets.py", + ): + if required not in nightly: + failures.append(f"nightly workflow missing required contract: {required}") + + for workflow in ("build-windows.yml", "build-linux.yml"): + text = (ROOT / ".github" / "workflows" / workflow).read_text(encoding="utf-8") + for required in ( + "patch_ndsrecomp_rom_free_release.py", + "prepare_freebios_banks.py", + "-DNDS_RETAIL_BIOS_BANKS=OFF", + ): + if required not in text: + failures.append(f"{workflow} missing ROM-free build contract: {required}") + + if failures: + print("ROM-free release policy check FAILED:", file=sys.stderr) + for failure in failures: + print(f" - {failure}", file=sys.stderr) + return 1 + + print("OK: public build/Nightly workflow uses no ROM secret path") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 1da24709b686e18f04d6cc1f75984e13869ce698 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:39 +0900 Subject: [PATCH 119/164] Gate CI on ROM-free release policy --- .github/workflows/build.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 312a887..1cfa34a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,10 +12,21 @@ concurrency: cancel-in-progress: true jobs: + policy: + name: ROM-free release policy + runs-on: ubuntu-24.04 + steps: + - name: Check out sources + uses: actions/checkout@v4 + - name: Verify no ROM-secret release path + run: python tools/ci/check_rom_free_release_sources.py + windows: name: Build Windows + needs: policy uses: ./.github/workflows/build-windows.yml linux: name: Build Linux + needs: policy uses: ./.github/workflows/build-linux.yml From 0fcfd9c7c6d5a5606a0359258930be54a005a673 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:59 +0900 Subject: [PATCH 120/164] Record ROM-free Nightly architecture --- docs/BRINGUP.md | 242 +++++++++++++++++++++--------------------------- 1 file changed, 104 insertions(+), 138 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 025cd37..dca7aa2 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -2,141 +2,107 @@ ## Target and references -- Cartridge: USA revision 0 (`AMHE`, `MP HUNTERS`) -- ROM SHA-1: `90164d1ac127ee5f9815ea4ae7de798c7b5fc629` -- Framework main integration: `778e74385aa223179a5d9534c2201ed1096a3df7` -- MphRead reference: `26cd8a6fe93dc5e525d1a1bb304fe96001111e55` -- Public matching disassembly: none found - -The ARM9 ROM image is 517,828 bytes and expands to 907,736 bytes. The ARM7 -image is 164,964 bytes. Prime Hunters has 18 compressed ARM9 overlays whose -overlay table contains 576 bytes (18 records), not 576 separate overlays. - -## Evidence so far - -1. The pre-existing SM64DS-native runner failed around the Prime Hunters game - handoff with ARM7 PC `0xE590100C` and a corrupt stack. -2. The failure was caused by unconditional SM64DS bank registration, not by a - Prime Hunters instruction or device requirement. Both titles load ARM7 at - `0x02380000`, so address-only dispatch selected SM64DS code. -3. A clean BIOS-bank-only runner reaches 700,000,000 ARM9 cycles with both - CPUs alive and no terminal dispatch miss. -4. Visual checkpoints: - - VBlank 300: ActImagine splash - - VBlank 900-1200: opening logo animation - - VBlank 2400: opening cinematic - - VBlank 3000: hunter cinematic - - VBlank 3600: Weavel introduction -5. The initial generated main banks contained 4,335 ARM9 functions and 16 - ARM7 functions. Exact-ROM-gated registration produced the same 700,000,000 - cycle machine state as the clean interpreter runner. -6. AMHE uses melonDS SaveMemType 5: 256 KiB flash. Save type/capacity are now - game-owned configuration instead of an SM64DS runner constant. -7. Native and ndsref event/cycle counts agree through the no-input return to - the hunter reel and onward to VBlank 12000. -8. The title split exposed a cold-boot screen-routing defect hidden by the - mirrored intro video: ndsrecomp reset POWCNT1 to `0x0001` instead of the - retail/melonDS `0x820F`. The reset and 3D power defaults are corrected. -9. A second routing defect appeared only when Prime Hunters changed the LCD - assignment during VBlank. The native renderer stored engine-relative - frames and applied the current POWCNT1 only when the completed frame was - read, which could retroactively swap it. Routing is now applied while each - scanline is produced, matching melonDS framebuffer assignment. -10. The no-input title/loop checkpoints are: - - VBlank 7800: title logo and `TOUCH TO START` - - VBlank 8400: title animation - - VBlank 9000: return to the hunter reel - Native top/bottom captures are byte-identical to ndsref at all three. -11. The checkpoint helper now names screens explicitly and continues after a - server safety-round exhaustion. It refuses to label or save a frame unless - the requested absolute VBlank was actually reached. -12. A seeded, trace-preserving input search discovered the first campaign - path. Its minimized replay is: - - tap the title and Adventure Mode - - create and confirm mission file A - - select file A again to start the campaign - - skip the mission briefing - - wait for the Celestial Archives gunship screen and confirm landing - The native and reference runs reach the live first-person HUD at VBlank - 10859. -13. `tools/fuzz_mph_gameplay.py` records every action, absolute VBlank, - screenshot, perceptual signature, RGB hash, and event-count snapshot. - `scenarios/adventure_start.json` is the replayable minimized result. -14. All 15 matching native/oracle checkpoints in the minimized route are - byte-identical across both physical screens: zero differing pixels and - zero maximum channel delta, including the first gameplay frame. -15. Tier-3 coverage captured from that route yielded 567 unique ARM9 - call/indirect targets inside the immutable main image. Slice-resume roots, - runtime RAM, and all reused overlay ranges were excluded. Adding those - seeds expands the ARM9 bank to 7,115 functions; the identical replay cuts - ARM9 Tier-3 entries from 64,619,845 to 57,525,780 (10.98%) and interpreted - instructions from 3,866,962,843 to 3,638,379,652 (5.91%). All 13 action - checkpoints retain identical event counts and RGB hashes. This does not - yet establish a wall-clock speedup while generated code remains `-O0`. -16. The opening FMV slowdown was isolated with - `tools/benchmark_mph_fmv.py`. Static-only FMV windows ran at 26-28 FPS: - presentation stayed below 1 ms/frame while emulation rose to 34-37 - ms/frame and ARM9 executed roughly 620,000 Tier-3 instructions per frame. - The hot code is the runtime ITCM mirror plus the active overlay near - `0x02102D74`. -17. A deterministic VBlank-3000 ITCM+main-RAM capture is pinned by SHA-1 - `2f4a2ba36886fb9152781f5829dedfd4b836a73b`. The separate - `mph_arm9_fmv_runtime` bank uses only call/indirect roots observed in the - VBlank 2400-3000 delta and validates live guest bytes before dispatch. - Seeding scheduler-resume PCs was rejected because it split hot loops into - one-instruction functions and generated about 589,000 fallthrough - dispatches/frame; the retained bank records about 5,400/frame. -18. The retained interactive run sustains 59.73-59.84 FPS from VBlank - 2400-4800 at 8.37-9.31 ms emulation/frame with zero audio underruns. - Static-only and optimized runners have identical event, instruction, and - cycle counts and zero differing pixels at VBlanks 2400, 3000, 3600, 4200, - and 4800. - -## Bring-up gates - -- [x] Isolated framework worktree from latest `origin/main` -- [x] Exact AMHE0 ROM identity and header inventory -- [x] Independent game repository/scaffold -- [x] Public reverse-engineering resource audit and pinned MphRead checkout -- [x] Safe interpreter boot through the opening cinematic -- [x] Remove the cross-title SM64DS bank-registration assumption -- [x] Reach and capture the title screen -- [x] Observe one complete no-input attract loop -- [x] Compare the same attract checkpoints against the ndsref oracle -- [x] Compile and register AMHE0 main ARM9/ARM7 banks by ROM capability -- [ ] Capture remaining runtime ARM7 code and ARM9 overlay generations (the - opening-FMV ARM9 generation is complete) -- [x] Generalize cartridge save type/size beyond SM64DS's 8 KiB EEPROM -- [x] Add deterministic Prime Hunters navigation and gameplay-entry scenario -- [ ] Add sustained traversal, combat, pause, death, and reload scenarios -- [x] Enable an exact-ROM upper-screen adaptive-wide bring-up baseline -- [x] Latch adaptive/direct presentation state to the published frame so - boot logos and capture transitions do not flicker at high host scaling -- [ ] Audit Prime Hunters projection/culling/HUD across sustained gameplay -- [x] Add an MPH recomp-ui development launcher and enhancement toggle -- [x] Add top-window relative mouse aim, Mouse 1 fire, and persisted controls -- [x] Add portable Windows release launcher/mod packaging with a baked, - content-validated FMV runtime bank - -## Design constraints - -- A title bank must never be selected by address alone. Registration is gated - by exact cartridge identity, and mutable/overlay banks also validate live - bytes. -- The runner also validates `[game].sha1` before applying title-owned config, - so a Prime Hunters save-device declaration cannot silently affect another - cartridge. -- Each overlay generation remains a separate content-validated bank. Prime - Hunters reuses virtual ranges, so combining entry points from different - overlay images would be unsound. -- The interpreter is the correctness oracle for uncompiled code within the - native runtime; ndsref remains the independent machine oracle. -- Widescreen is a title-owned capability. Separate windows are safe as a host - layout, but field-of-view, culling, HUD anchors, movies, and touch routing - require Prime Hunters-specific proof. -- MphRead's recreation uses a 78-degree camera FOV and derives projection and - frustum planes from the live output aspect ratio. That is a useful semantic - reference, but it does not prove which AMHE0 guest structures and GX command - sites must be patched. The host adaptive viewport is enabled as an explicit - bring-up baseline, but it is not considered visually complete until those - title-side behaviors pass sustained gameplay review. +The project began with the USA revision-0 (`AMHE`) bring-up and now carries a +seven-base-profile runtime layout/detector for US1.0, US1.1, EU1.0, EU1.1, +JP1.0, JP1.1, and KR1.0. Exact clean-content profiles, generated banks, +coverage, and capture artifacts remain content-specific and must not be inferred +from a base layout alone. + +The original US1.0 content identity is SHA-1 +`90164d1ac127ee5f9815ea4ae7de798c7b5fc629`. Whole-ROM SHA-1 is provenance and +cache/build identity, not the runtime base-profile selector. + +## Runtime identity rules + +1. Use the melonPrime-compatible executable CRC32 detector as the authoritative + runtime base-profile signal. +2. An exact supported game-code/revision tuple may identify only a candidate + base profile when the executable checksum is unknown. +3. Dangerous host RAM/code writes fail closed unless executable compatibility + is authoritative. +4. Generated title banks, runtime captures, and coverage remain scoped by exact + content identity. +5. Unknown modified content must never silently fall back to US1.0. + +## ROM-free Nightly architecture + +Public Windows/Linux Nightly builds are intentionally produced without a +Metroid Prime Hunters ROM, ROM URL/secret, proprietary BIOS or firmware dump, +save file, or generated ROM-derived MPH title bank. + +The pinned ndsrecomp runner is patched for an opt-in public-build mode: + +- redistributable BSD-2-Clause FreeBIOS images are recompiled into the native + FreeBIOS ARM9/ARM7 banks using ndsrecomp's documented FreeBIOS pipeline; +- `NDS_RETAIL_BIOS_BANKS=OFF` removes the build-time requirement for generated + proprietary retail-BIOS banks; +- if a user later supplies retail BIOS dumps, immutable BIOS execution may use + the existing reference interpreter rather than requiring those generated C + banks in the distributed binary; +- no MPH title-bank directory is configured, so direct-booted MPH ARM9/ARM7 + code falls through to Tier-3 using guest-written RAM provenance. + +This is a correctness-first release path. It is expected to be slower than the +historical optimized US1.0 release, particularly in opening-FMV/runtime-code hot +paths. + +## Local optimization cache direction + +The preferred future optimization cache is portable-first: + +```text +cache/banks// +``` + +beside the executable/AppImage. Linux falls back to XDG cache when that location +is not writable; Windows should fall back to LOCALAPPDATA under the same +condition. Save data and firmware/WFC identity remain persistent app data, not +regenerable optimization cache. + +The current static ndsrecomp bank pipeline emits C and links it into the runner, +so the project will not make users run a host C/C++ compiler on first launch. +The intended progression is Tier-3 -> compiler-free local IR/bank support if +useful -> hot-block JIT with a validated persistent cache. See +`docs/LOCAL_BANK_CACHE.md`. + +## Adaptive Widescreen + +Adaptive Widescreen combines the original ndsrecomp host-side widened +renderer/compositor/HUD anchoring with MPH game-side projection and culling +patches audited against melonPrimeDS/mphCodex. The guest patch addresses are +profile-aware across all seven base layouts and are applied only under the +existing authoritative executable-compatibility gate. + +## Upstream launcher/runtime integration + +The launcher tracks the upstream MPH recomp-ui feature set including Adaptive +Widescreen, Prime Controls, HD Rendering, and persistent firmware/WFC state. +The launcher does not use whole-ROM SHA-1 as an early acceptance gate; the +runner owns executable compatibility and exact-content decisions. + +## Validation gates + +- [x] Seven runtime base profiles represented. +- [x] Executable CRC32 runtime detector and strict header fallback. +- [x] Dangerous host writes fail closed for unknown executable content. +- [x] All-seven Adaptive Widescreen address table integrated. +- [x] Upstream launcher HD/Wi-Fi persistence integration retained. +- [x] ROM-free release design avoids ROM/ROM-secret input in GitHub Actions. +- [x] FreeBIOS native-bank generation has a redistributable build path. +- [ ] Windows ROM-free Nightly workflow passes full compile/package CI. +- [ ] Linux ROM-free Nightly workflow passes full compile/AppImage CI. +- [ ] Real-ROM runtime smoke validation of the ROM-free Tier-3 Nightly path. +- [ ] Implement compiler-free dynamic/local optimization-bank ABI or JIT. +- [ ] Complete exact clean-content profiles and sustained gameplay validation + for US1.1, EU1.0, JP1.0, JP1.1, and KR1.0. +- [ ] Add modified-ROM content profiles only after their actual executable and + content identities are validated; do not add placeholder identities. + +## Release safety + +Nightly packaging explicitly rejects ROMs, saves, retail BIOS/firmware dumps, +and generated/capture directories. Windows ZIP and Linux AppImage artifacts are +built first and published to the fixed `nightly-release` prerelease only after +both platform jobs and payload verification succeed. A source-policy check also +rejects reintroduction of repository/environment secret references into the +public build/Nightly workflows. From 27fed60d966dce9c7cccdc4b9effde1f4b812fcd Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:37:25 +0900 Subject: [PATCH 121/164] Restore README and append ROM-free Nightly notes --- README.md | 250 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 157 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 9a00c2a..ba035c6 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ > audio issues, input quirks, networking failures, and possible desyncs. Testing, > issues, and PRs are welcome. -MetroidPrimeHuntersRecomp runs **Metroid Prime Hunters** as an ndsrecomp target. -Runtime base-version detection supports the known US1.0, US1.1, EU1.0, EU1.1, -JP1.0, JP1.1, and KR1.0 layouts using executable-compatible detection rather -than using whole-ROM SHA-1 as the base-profile selector. You provide your own -legally obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated +MetroidPrimeHuntersRecomp currently ships generated content profiles for the +validated ROM revisions tracked by this branch. Runtime base detection is +prepared for all seven retail Metroid Prime Hunters revisions and uses +melonPrimeDS-compatible executable checksums; whole-ROM SHA-1 is content +provenance, not the runtime address selector. You provide your own legally +obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated ROM-derived source is distributed. ## Gameplay Preview @@ -21,132 +22,195 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release -The project version is **v0.4.0-alpha**. Development Nightly builds are produced -from `develop` under the fixed `nightly-release` prerelease tag after the -Windows and Linux build workflows and release-payload safety checks succeed. +Latest upstream release: +**[v0.4.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.4.0-alpha)**. -The Nightly build path is deliberately **ROM-free**: GitHub Actions never needs -or downloads a Metroid Prime Hunters ROM, private ROM URL, proprietary BIOS or -firmware dump, save file, or ROM-derived MPH title bank. Users supply their ROM -at runtime. In the current Nightly architecture, title code without a linked -content-specific native bank executes through ndsrecomp Tier-3. This is a safe -correctness fallback and may be slower than an optimized tagged build. +Downloads: -The package reserves a portable optimization-cache root beside the executable -or AppImage at `cache/banks//`. The current Nightly does not yet -generate native title banks there. The intended next step is a compiler-free -local optimization/JIT cache; see [`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). +- Windows: + `MetroidPrimeHuntersRecomp-windows-x64-v0.4.0.zip` +- Linux: + `MetroidPrimeHuntersRecomp-linux-x86_64-v0.4.0.AppImage` + +This is an early ndsrecomp title and should still be treated as an alpha test +build rather than a polished game release. + +New in upstream v0.4.0 is an opt-in **HD Rendering** mod on the launcher Mods +page. It can raise the internal 3D resolution up to 4x and optionally upscale +decoded textures. It is disabled by default; native rendering remains the +reference path. This branch keeps that upstream feature alongside the +multi-ROM-safe Adaptive Widescreen and Prime Controls work. + +## Nightly builds and local optimization cache + +The `develop` branch publishes a fixed `nightly-release` prerelease after the +Windows and Linux build workflows and release-payload checks succeed. This +public Nightly path is deliberately **ROM-free**: GitHub Actions does not fetch +or receive a Metroid Prime Hunters ROM, private ROM URL/secret, proprietary BIOS +or firmware dump, save data, or ROM-derived MPH title bank. + +When a content-specific native title bank is not linked, direct-booted MPH code +uses ndsrecomp's Tier-3 correctness fallback. This makes a ROM-free Nightly +possible, but it can be slower than an optimized tagged build, especially in +known hot runtime-code paths such as opening movies. + +Nightly packages reserve the portable-first optimization-cache namespace +`cache/banks//` beside the executable/AppImage. Whole-ROM SHA-1 is +used there only as exact cache/content identity; runtime base-profile selection +continues to use the executable-compatible MPH detector. The current Nightly +does **not** generate a native title bank in this directory yet. The intended +next step is a compiler-free local bank/JIT cache. See +[`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). ## Quick Start Windows: 1. Download and fully extract the Windows ZIP. -2. Put your own Metroid Prime Hunters `.nds` ROM next to - `MetroidPrimeHuntersRecomp.exe`, or select it in the launcher. +2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the launcher. 3. Run `MetroidPrimeHuntersRecomp.exe` and press Play. Linux: -1. Download the AppImage and make it executable if required by your desktop. -2. Put your own Metroid Prime Hunters `.nds` ROM next to the AppImage. +1. Download the AppImage. +2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the AppImage. 3. Run the AppImage. The current release can use the built-in FreeBIOS + generated firmware path, so retail DS BIOS and firmware dumps are not required for the default no-dump -startup path. The ROM-free Nightly generates its native FreeBIOS banks only -from the redistributable BSD-2-Clause FreeBIOS source path at build time. - -## ROM identity and multi-ROM behavior - -Three identity layers are kept separate: - -1. **Runtime base profile:** executable checksum / exact supported header tuple. -2. **Executable compatibility:** determines whether dangerous host RAM/code - writes such as Aim/Morph/Adaptive Widescreen patches are authorized. -3. **Exact content identity:** whole-ROM SHA-1 for provenance, generated banks, - captures, and the future local optimization-cache namespace. - -Whole-ROM SHA-1 therefore does not decide that a modified ROM is US1.0 or EU1.1. -Unknown or ambiguous executable content never silently falls back to US1.0, and -host writes fail closed unless executable compatibility is authoritative. - -Exact clean-content profiles currently validated in the repository include the -US1.0 and EU1.1 bring-up tracks. Other base layouts are prepared at runtime, but -full per-ROM extraction, coverage, gameplay validation, and optimized bank work -must still be completed before they should be described as equally validated -release targets. - -## Enhancements +startup path. If you choose to use your own BIOS/firmware dumps, they must be +from hardware you own and must match the hashes listed in the release's +`bios/README.txt`. + +## ROM identity and multi-ROM support + +Runtime address selection does **not** use whole-ROM SHA-1. The runner first +uses the melonPrimeDS executable checksum (CRC32 over header + ARM9 + ARM7), +then uses exact game code + supported revision only as a fallback base-profile +hint. Header-only matches never authorize host RAM/code writes. + +Known compatible executable checksums can use the revision-specific Prime +Controls and Adaptive Widescreen addresses. Whole-ROM SHA-1 remains the exact +content identity for generated banks, coverage, checkpoints and capture data, +so one modified ROM cannot silently reuse another modified ROM's generated +content. + +The current branch has runtime address profiles for US1.0, US1.1, EU1.0, +EU1.1, JP1.0, JP1.1 and KR1.0. A revision still needs its own generated +content/capture coverage before it is considered fully brought up. + +## What Works + +- Boots supported content profiles through the ndsrecomp runner. +- Reaches Metroid Prime Hunters gameplay in tested routes. +- Includes an adaptive 21:9 upper-screen widescreen option using per-revision + projection/culling addresses from melonPrimeDS/mphCodex. +- Includes Prime-style keyboard and mouse controls. +- Includes full remappable gamepad bindings in the launcher. +- Includes upstream HD Rendering controls for internal resolution and texture + upscaling. +- Supports mouse-driven touchscreen input. +- Can authenticate through Wiimmfi and reach a Friends and Rivals lobby in + validated flows. +- Persists mutable firmware/WFC state between launches through the upstream + firmware-state path. -### Adaptive Widescreen - -The launcher exposes an adaptive 21:9 upper-screen mode. The implementation -combines ndsrecomp's widened host renderer/compositor and HUD anchoring with -profile-aware Metroid Prime Hunters projection/culling corrections derived from -the audited melonPrimeDS/mphCodex address tables. Unsupported/unsafe runtime -identity falls back rather than applying guessed guest writes. - -### Prime Controls +## Known Limits -Prime-style keyboard/mouse controls and remappable gamepad bindings are exposed -through the launcher. Defaults include WASD movement, mouse aim, Mouse 1 fire, -and the existing touch-helper mappings. +- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and + gameplay problems are expected. +- Gameplay coverage is incomplete. Do not assume the campaign is fully + validated from start to finish. +- Widescreen is still being audited. Some scenes, effects, HUD placement, + movies, fades, or screen-routing behavior may be wrong. +- HD texture upscaling remains opt-in and should not be treated as the native + reference rendering path. +- ROM-free Nightly builds can be slower while MPH title code is using Tier-3 + instead of a validated native optimization bank. +- The local `cache/banks//` path is currently a cache contract; + dynamic native/JIT bank generation is not implemented yet. +- Online play is experimental. Wiimmfi can reach the lobby in validated flows, + but in-game play is ultimately untested. There is no guarantee that a match + will connect, stay connected, or avoid desync. +- Save behavior and settings are still part of early release testing. Keep + backups of anything you care about. + +## Controls + +Prime Controls are enabled by default. + +Keyboard and mouse defaults: + +- `WASD`: move +- Mouse: aim +- Mouse 1 / Mouse 2: fire / scan-fire +- `Space`: jump +- `Left Ctrl`: morph ball +- `Left Shift`: boost / map zoom +- `C`: scan visor +- `F`: OK +- `Q` / `E`: scan-message arrows +- `V`: menu +- Mouse 4: missiles +- Mouse 5: beam +- `1` through `6`: subweapons +- `Tab`: virtual stylus + +Gamepad defaults: + +- Left stick: move and menu D-pad +- Right stick: aim +- `RT` / `LT`: shoot / scan-fire +- `A`: jump +- `B`: morph ball +- `X`: missile +- `Y`: UI OK +- `LB` / `RB`: beam / boost or zoom +- `R3`: scan visor +- D-pad left/right: scan-message arrows +- `Start`: menu + +Keyboard, mouse, and gamepad bindings are editable from the launcher Mods page. -### HD Rendering +## Online Play -HD Rendering is opt-in. It raises the 3D engine above native DS sample density -(up to the supported internal-resolution choices) and can upscale decoded -textures. The native 2D path remains the reference and HD Rendering is off by -default. +Nintendo WFC / Wiimmfi support is experimental. The current validated state is +lobby connectivity: Metroid Prime Hunters can authenticate through Wiimmfi and +reach a Friends and Rivals lobby where a locally hosted game is visible. -## Online Play +The launcher keeps the console firmware profile in +`%APPDATA%\MetroidPrimeHuntersRecomp`. Wi-Fi settings, console/game-card +pairing, and WFC updates survive both the in-game system shutdown flow and a +normal window close. Confirming the WFC settings shutdown prompt closes the +application automatically. -Nintendo WFC / Wiimmfi support remains experimental. The launcher persists the -console firmware profile in the user's application-data location so Wi-Fi -settings, console/game-card pairing, and WFC updates survive later launches. -Online play may still fail to connect, disconnect, or desync. +Actually joining a match and playing in-game online is not guaranteed. It may +fail to connect, disconnect, or desync. The Wi-Fi implementation is built on [melonDS](https://github.com/melonDS-emu/melonDS)'s Wi-Fi work in the shared ndsrecomp runner. Full credit to the melonDS team for the Wi-Fi controller, emulated access point, and network backend foundation. -## Known Limits - -- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and - gameplay problems are expected. -- Gameplay coverage is incomplete across the seven base layouts. -- The ROM-free Nightly's Tier-3 title fallback can be substantially slower than - a build with validated native MPH optimization banks, especially in known hot - paths such as opening movies. -- The local `cache/banks//` directory is currently a reserved - cache contract; dynamic native/JIT bank generation is not implemented yet. -- Widescreen still requires sustained gameplay auditing across scenes, effects, - HUD placement, movies, fades, and screen routing. -- Online play is experimental. -- Save behavior and settings remain part of early release testing. Keep backups - of anything you care about. - ## Credits - [melonDS](https://github.com/melonDS-emu/melonDS): Wi-Fi implementation foundation used by the shared ndsrecomp runner. -- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): reference for - Prime-style controls, ROM/version address tables, and aspect-ratio research. -- [mphCodex](https://github.com/Zection6V/mphCodex): game-code/disassembly and - Metroid Prime Hunters behavior research. +- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): runtime-version + detection, Prime-style controls, and per-version aspect-ratio patch reference. +- [mphCodex](https://github.com/Zection6V/mphCodex): MPH game-code analysis, + including the seven-version widescreen projection/culling mapping. - [MphRead](https://github.com/NoneGiven/MphRead): Metroid Prime Hunters file format and behavior reference. -See the ndsrecomp `THIRD_PARTY_ATTRIBUTION.md` for shared-runtime provenance and -licensing details. +See the ndsrecomp +[`THIRD_PARTY_ATTRIBUTION.md`](https://github.com/mstan/ndsrecomp/blob/main/THIRD_PARTY_ATTRIBUTION.md) +for provenance and licensing details for shared runtime components. ## Developers -Bring-up and validation notes live under [`docs/`](docs/). The ROM-free Nightly -and local-cache direction is documented in -[`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). +This README is intentionally player-facing. Development notes, validation +history, and bring-up details live in [`docs/BRINGUP.md`](docs/BRINGUP.md). The original code in this repository is MIT licensed. Metroid Prime Hunters, Nintendo DS firmware/BIOS images, ROMs, saves, and all derived game data remain From 5ce8a460039440e91166cca3609cc2da67f0e8df Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:37:49 +0900 Subject: [PATCH 122/164] Restore bring-up ledger and append ROM-free Nightly notes --- docs/BRINGUP.md | 265 +++++++++++++++++++++++++++++------------------- 1 file changed, 161 insertions(+), 104 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index dca7aa2..f5b17ab 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -2,107 +2,164 @@ ## Target and references -The project began with the USA revision-0 (`AMHE`) bring-up and now carries a -seven-base-profile runtime layout/detector for US1.0, US1.1, EU1.0, EU1.1, -JP1.0, JP1.1, and KR1.0. Exact clean-content profiles, generated banks, -coverage, and capture artifacts remain content-specific and must not be inferred -from a base layout alone. - -The original US1.0 content identity is SHA-1 -`90164d1ac127ee5f9815ea4ae7de798c7b5fc629`. Whole-ROM SHA-1 is provenance and -cache/build identity, not the runtime base-profile selector. - -## Runtime identity rules - -1. Use the melonPrime-compatible executable CRC32 detector as the authoritative - runtime base-profile signal. -2. An exact supported game-code/revision tuple may identify only a candidate - base profile when the executable checksum is unknown. -3. Dangerous host RAM/code writes fail closed unless executable compatibility - is authoritative. -4. Generated title banks, runtime captures, and coverage remain scoped by exact - content identity. -5. Unknown modified content must never silently fall back to US1.0. - -## ROM-free Nightly architecture - -Public Windows/Linux Nightly builds are intentionally produced without a -Metroid Prime Hunters ROM, ROM URL/secret, proprietary BIOS or firmware dump, -save file, or generated ROM-derived MPH title bank. - -The pinned ndsrecomp runner is patched for an opt-in public-build mode: - -- redistributable BSD-2-Clause FreeBIOS images are recompiled into the native - FreeBIOS ARM9/ARM7 banks using ndsrecomp's documented FreeBIOS pipeline; -- `NDS_RETAIL_BIOS_BANKS=OFF` removes the build-time requirement for generated - proprietary retail-BIOS banks; -- if a user later supplies retail BIOS dumps, immutable BIOS execution may use - the existing reference interpreter rather than requiring those generated C - banks in the distributed binary; -- no MPH title-bank directory is configured, so direct-booted MPH ARM9/ARM7 - code falls through to Tier-3 using guest-written RAM provenance. - -This is a correctness-first release path. It is expected to be slower than the -historical optimized US1.0 release, particularly in opening-FMV/runtime-code hot -paths. - -## Local optimization cache direction - -The preferred future optimization cache is portable-first: - -```text -cache/banks// -``` - -beside the executable/AppImage. Linux falls back to XDG cache when that location -is not writable; Windows should fall back to LOCALAPPDATA under the same -condition. Save data and firmware/WFC identity remain persistent app data, not -regenerable optimization cache. - -The current static ndsrecomp bank pipeline emits C and links it into the runner, -so the project will not make users run a host C/C++ compiler on first launch. -The intended progression is Tier-3 -> compiler-free local IR/bank support if -useful -> hot-block JIT with a validated persistent cache. See -`docs/LOCAL_BANK_CACHE.md`. - -## Adaptive Widescreen - -Adaptive Widescreen combines the original ndsrecomp host-side widened -renderer/compositor/HUD anchoring with MPH game-side projection and culling -patches audited against melonPrimeDS/mphCodex. The guest patch addresses are -profile-aware across all seven base layouts and are applied only under the -existing authoritative executable-compatibility gate. - -## Upstream launcher/runtime integration - -The launcher tracks the upstream MPH recomp-ui feature set including Adaptive -Widescreen, Prime Controls, HD Rendering, and persistent firmware/WFC state. -The launcher does not use whole-ROM SHA-1 as an early acceptance gate; the -runner owns executable compatibility and exact-content decisions. - -## Validation gates - -- [x] Seven runtime base profiles represented. -- [x] Executable CRC32 runtime detector and strict header fallback. -- [x] Dangerous host writes fail closed for unknown executable content. -- [x] All-seven Adaptive Widescreen address table integrated. -- [x] Upstream launcher HD/Wi-Fi persistence integration retained. -- [x] ROM-free release design avoids ROM/ROM-secret input in GitHub Actions. -- [x] FreeBIOS native-bank generation has a redistributable build path. -- [ ] Windows ROM-free Nightly workflow passes full compile/package CI. -- [ ] Linux ROM-free Nightly workflow passes full compile/AppImage CI. -- [ ] Real-ROM runtime smoke validation of the ROM-free Tier-3 Nightly path. -- [ ] Implement compiler-free dynamic/local optimization-bank ABI or JIT. -- [ ] Complete exact clean-content profiles and sustained gameplay validation - for US1.1, EU1.0, JP1.0, JP1.1, and KR1.0. -- [ ] Add modified-ROM content profiles only after their actual executable and - content identities are validated; do not add placeholder identities. - -## Release safety - -Nightly packaging explicitly rejects ROMs, saves, retail BIOS/firmware dumps, -and generated/capture directories. Windows ZIP and Linux AppImage artifacts are -built first and published to the fixed `nightly-release` prerelease only after -both platform jobs and payload verification succeed. A source-policy check also -rejects reintroduction of repository/environment secret references into the -public build/Nightly workflows. +- Cartridge: USA revision 0 (`AMHE`, `MP HUNTERS`) +- ROM SHA-1: `90164d1ac127ee5f9815ea4ae7de798c7b5fc629` +- Framework main integration: `778e74385aa223179a5d9534c2201ed1096a3df7` +- MphRead reference: `26cd8a6fe93dc5e525d1a1bb304fe96001111e55` +- Public matching disassembly: none found + +The ARM9 ROM image is 517,828 bytes and expands to 907,736 bytes. The ARM7 +image is 164,964 bytes. Prime Hunters has 18 compressed ARM9 overlays whose +overlay table contains 576 bytes (18 records), not 576 separate overlays. + +## Evidence so far + +1. The pre-existing SM64DS-native runner failed around the Prime Hunters game + handoff with ARM7 PC `0xE590100C` and a corrupt stack. +2. The failure was caused by unconditional SM64DS bank registration, not by a + Prime Hunters instruction or device requirement. Both titles load ARM7 at + `0x02380000`, so address-only dispatch selected SM64DS code. +3. A clean BIOS-bank-only runner reaches 700,000,000 ARM9 cycles with both + CPUs alive and no terminal dispatch miss. +4. Visual checkpoints: + - VBlank 300: ActImagine splash + - VBlank 900-1200: opening logo animation + - VBlank 2400: opening cinematic + - VBlank 3000: hunter cinematic + - VBlank 3600: Weavel introduction +5. The initial generated main banks contained 4,335 ARM9 functions and 16 + ARM7 functions. Exact-ROM-gated registration produced the same 700,000,000 + cycle machine state as the clean interpreter runner. +6. AMHE uses melonDS SaveMemType 5: 256 KiB flash. Save type/capacity are now + game-owned configuration instead of an SM64DS runner constant. +7. Native and ndsref event/cycle counts agree through the no-input return to + the hunter reel and onward to VBlank 12000. +8. The title split exposed a cold-boot screen-routing defect hidden by the + mirrored intro video: ndsrecomp reset POWCNT1 to `0x0001` instead of the + retail/melonDS `0x820F`. The reset and 3D power defaults are corrected. +9. A second routing defect appeared only when Prime Hunters changed the LCD + assignment during VBlank. The native renderer stored engine-relative + frames and applied the current POWCNT1 only when the completed frame was + read, which could retroactively swap it. Routing is now applied while each + scanline is produced, matching melonDS framebuffer assignment. +10. The no-input title/loop checkpoints are: + - VBlank 7800: title logo and `TOUCH TO START` + - VBlank 8400: title animation + - VBlank 9000: return to the hunter reel + Native top/bottom captures are byte-identical to ndsref at all three. +11. The checkpoint helper now names screens explicitly and continues after a + server safety-round exhaustion. It refuses to label or save a frame unless + the requested absolute VBlank was actually reached. +12. A seeded, trace-preserving input search discovered the first campaign + path. Its minimized replay is: + - tap the title and Adventure Mode + - create and confirm mission file A + - select file A again to start the campaign + - skip the mission briefing + - wait for the Celestial Archives gunship screen and confirm landing + The native and reference runs reach the live first-person HUD at VBlank + 10859. +13. `tools/fuzz_mph_gameplay.py` records every action, absolute VBlank, + screenshot, perceptual signature, RGB hash, and event-count snapshot. + `scenarios/adventure_start.json` is the replayable minimized result. +14. All 15 matching native/oracle checkpoints in the minimized route are + byte-identical across both physical screens: zero differing pixels and + zero maximum channel delta, including the first gameplay frame. +15. Tier-3 coverage captured from that route yielded 567 unique ARM9 + call/indirect targets inside the immutable main image. Slice-resume roots, + runtime RAM, and all reused overlay ranges were excluded. Adding those + seeds expands the ARM9 bank to 7,115 functions; the identical replay cuts + ARM9 Tier-3 entries from 64,619,845 to 57,525,780 (10.98%) and interpreted + instructions from 3,866,962,843 to 3,638,379,652 (5.91%). All 13 action + checkpoints retain identical event counts and RGB hashes. This does not + yet establish a wall-clock speedup while generated code remains `-O0`. +16. The opening FMV slowdown was isolated with + `tools/benchmark_mph_fmv.py`. Static-only FMV windows ran at 26-28 FPS: + presentation stayed below 1 ms/frame while emulation rose to 34-37 + ms/frame and ARM9 executed roughly 620,000 Tier-3 instructions per frame. + The hot code is the runtime ITCM mirror plus the active overlay near + `0x02102D74`. +17. A deterministic VBlank-3000 ITCM+main-RAM capture is pinned by SHA-1 + `2f4a2ba36886fb9152781f5829dedfd4b836a73b`. The separate + `mph_arm9_fmv_runtime` bank uses only call/indirect roots observed in the + VBlank 2400-3000 delta and validates live guest bytes before dispatch. + Seeding scheduler-resume PCs was rejected because it split hot loops into + one-instruction functions and generated about 589,000 fallthrough + dispatches/frame; the retained bank records about 5,400/frame. +18. The retained interactive run sustains 59.73-59.84 FPS from VBlank + 2400-4800 at 8.37-9.31 ms emulation/frame with zero audio underruns. + Static-only and optimized runners have identical event, instruction, and + cycle counts and zero differing pixels at VBlanks 2400, 3000, 3600, 4200, + and 4800. + +## Bring-up gates + +- [x] Isolated framework worktree from latest `origin/main` +- [x] Exact AMHE0 ROM identity and header inventory +- [x] Independent game repository/scaffold +- [x] Public reverse-engineering resource audit and pinned MphRead checkout +- [x] Safe interpreter boot through the opening cinematic +- [x] Remove the cross-title SM64DS bank-registration assumption +- [x] Reach and capture the title screen +- [x] Observe one complete no-input attract loop +- [x] Compare the same attract checkpoints against the ndsref oracle +- [x] Compile and register AMHE0 main ARM9/ARM7 banks by ROM capability +- [ ] Capture remaining runtime ARM7 code and ARM9 overlay generations (the + opening-FMV ARM9 generation is complete) +- [x] Generalize cartridge save type/size beyond SM64DS's 8 KiB EEPROM +- [x] Add deterministic Prime Hunters navigation and gameplay-entry scenario +- [ ] Add sustained traversal, combat, pause, death, and reload scenarios +- [x] Enable an exact-ROM upper-screen adaptive-wide bring-up baseline +- [x] Latch adaptive/direct presentation state to the published frame so + boot logos and capture transitions do not flicker at high host scaling +- [ ] Audit Prime Hunters projection/culling/HUD across sustained gameplay +- [x] Add an MPH recomp-ui development launcher and enhancement toggle +- [x] Add top-window relative mouse aim, Mouse 1 fire, and persisted controls +- [x] Add portable Windows release launcher/mod packaging with a baked, + content-validated FMV runtime bank + +## Design constraints + +- A title bank must never be selected by address alone. Registration is gated + by exact cartridge identity, and mutable/overlay banks also validate live + bytes. +- The runner also validates `[game].sha1` before applying title-owned config, + so a Prime Hunters save-device declaration cannot silently affect another + cartridge. +- Each overlay generation remains a separate content-validated bank. Prime + Hunters reuses virtual ranges, so combining entry points from different + overlay images would be unsound. +- The interpreter is the correctness oracle for uncompiled code within the + native runtime; ndsref remains the independent machine oracle. +- Widescreen is a title-owned capability. Separate windows are safe as a host + layout, but field-of-view, culling, HUD anchors, movies, and touch routing + require Prime Hunters-specific proof. +- MphRead's recreation uses a 78-degree camera FOV and derives projection and + frustum planes from the live output aspect ratio. That is a useful semantic + reference, but it does not prove which AMHE0 guest structures and GX command + sites must be patched. The host adaptive viewport is enabled as an explicit + bring-up baseline, but it is not considered visually complete until those + title-side behaviors pass sustained gameplay review. + +## ROM-free Nightly and local optimization cache + +The public Windows/Linux Nightly path is deliberately ROM-free. GitHub Actions +does not receive a Metroid Prime Hunters ROM, private ROM URL/secret, +proprietary BIOS/firmware dump, save, or generated ROM-derived MPH title bank. +The pinned runner is built with the redistributable BSD-2-Clause FreeBIOS native +banks and with `NDS_RETAIL_BIOS_BANKS=OFF`. + +When no content-specific MPH native bank is linked, direct-booted ARM9/ARM7 code +uses the existing Tier-3 interpreter path after guest writes establish RAM code +provenance. This is a correctness-first Nightly mode and can be substantially +slower than the historical optimized US1.0 release, especially in the FMV hot +runtime-code path described above. + +Nightly packages reserve `cache/banks//` beside the executable or +AppImage as the future portable optimization-cache namespace. Whole-ROM SHA-1 +is suitable here because cache payloads must be bound to exact content, but it +must not become the runtime base-profile selector. The current static +recompiler emits C and links it into the runner, so first-launch C/C++ +compilation is intentionally **not** the target UX. The intended progression is +Tier-3 -> compiler-free local bank/IR support if useful -> hot-block JIT with a +validated persistent cache. See `docs/LOCAL_BANK_CACHE.md`. From d8c9683808c5c8083b470a6fbeacba254b6df98d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:41:06 +0900 Subject: [PATCH 123/164] Fix ROM-free patch idempotency --- tools/patch_ndsrecomp_rom_free_release.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py index 40b4390..256a063 100644 --- a/tools/patch_ndsrecomp_rom_free_release.py +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -20,7 +20,11 @@ def replace_once(path: Path, old: str, new: str, marker: str) -> None: text = path.read_text(encoding="utf-8") - if marker in text and old not in text: + # The replacement may intentionally contain the original anchor (for + # example, adding one declaration immediately after an existing one). + # Therefore exact replacement text, not marker/anchor absence, is the + # reliable idempotency test. + if new in text: return count = text.count(old) if count != 1: From 3acd616b5e43b4d06f44cf9aee2caced64a43dc8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:42:18 +0900 Subject: [PATCH 124/164] Preserve existing ignores for local cache --- .gitignore | 53 ++++++++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 14264b4..7b9be66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,34 @@ -# ROM / console dumps +# User-owned Nintendo DS images and ROM-derived outputs. *.nds -*.NDS -*.sav -*.dsv -bios/*.rom -bios/*.bin -firmware.bin - -# Generated recompilation inputs/outputs and captures -generated/ -release-stage/ +/generated/ +/extracted/ -# Local optimization cache (portable-first runtime/JIT cache) -cache/ +# Optional reverse-engineering references. +/third_party/ -# Build trees -build/ -build-*/ -launcher/recomp-ui/build/ -launcher/recomp-ui/build-*/ +# Build trees, runtime state, and captures. +/build/ +/build-*/ +/release-stage/ +/launcher/recomp-ui/build*/ +/saves/ +/captures/ +/scratch/ +/cache/ +*.sav +*.log +/rom.cfg +/settings.toml +/keybinds.ini -# Python -.venv/ +# Local Python environment and tool output. +/.venv/ __pycache__/ *.pyc -# Tool/editor state +# Editor and OS output. +.vs/ .vscode/ .idea/ -*.suo -*.user -*.gpr -*.rep -*.lock - -# OS junk -.DS_Store Thumbs.db +.DS_Store From ceafcc7def88d296a1c0ccf601fa36f579d4fa20 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:44:20 +0900 Subject: [PATCH 125/164] Run Linux packager through bash --- .github/workflows/build-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 808bd3d..5c96e5c 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -98,7 +98,7 @@ jobs: set -euo pipefail version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" test -n "$version" - tools/package-linux-appimage.sh \ + bash tools/package-linux-appimage.sh \ --version "$version" \ --runner ../ndsrecomp/runner/build-mph-nightly/nds_runner \ --appimage-tool "$PWD/.ci-tools/appimagetool" \ From 1932f626b86c2c8682fd93342ae2bac8fa8dbfd9 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 23:49:25 -0700 Subject: [PATCH 126/164] Drive MPH friend matches across local peer routing --- game.toml | 2 +- tools/run_mph_friend_match.py | 56 +++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/game.toml b/game.toml index 0a116fd..d49545e 100644 --- a/game.toml +++ b/game.toml @@ -61,7 +61,7 @@ size = 0x00028464 [framework] path = "../ndsrecomp" -pin = "6c6a03bdcf99093f64555c4d05d16e522dc58634" +pin = "302404ada0929528b680fa6808aad253b425c7a2" branch = "main" [reference.mphread] diff --git a/tools/run_mph_friend_match.py b/tools/run_mph_friend_match.py index 11bc52c..4946181 100644 --- a/tools/run_mph_friend_match.py +++ b/tools/run_mph_friend_match.py @@ -96,7 +96,9 @@ # ...and that lands on SELECT HUNTER, the third setup tab. Only the first three # portraits are selectable; the leftmost is Samus. HUNTER_SAMUS = (27, 132) +HOST_START_DISC = (240, 149) CONFIG_CAPTURE_FRAMES = (600, 1200, 2400) +POST_JOIN_CAPTURE_FRAMES = (240, 240, 240, 240, 240, 240, 240, 240) @dataclass @@ -140,10 +142,11 @@ def launch_instance(args: argparse.Namespace, index: int) -> Instance: "on", "--wfc-provider", args.wfc_provider, - # Prepared per-profile firmware carries the identity, so every instance - # keeps slot 0 and is differentiated by the injected image instead. + # Prepared per-profile firmware carries the console identity. The + # runner instance index is still useful for host-side networking: + # slirp uses it to place each emulated DS on a distinct virtual LAN. "--instance-index", - "0", + str(index), "--save-path", str(save_path), ] @@ -516,6 +519,52 @@ def join_phase(instance: Instance) -> None: for_each(instances, join_phase) + post_join_log: list[dict[str, Any]] = [] + + def post_join_action( + label: str, + actor: Instance, + action: tuple[Any, ...], + capture_frames: tuple[int, ...] = POST_JOIN_CAPTURE_FRAMES, + ) -> None: + def input_step(instance: Instance) -> None: + assert instance.client is not None + beat(instance, action if instance is actor else None, 0) + + for_each(instances, input_step) + + for index, frames in enumerate(capture_frames): + for_each( + instances, + lambda instance, frames=frames: input_lib.advance_frames( + instance.client, frames + ), + ) + for instance in instances: + role = "host" if instance is host else "guest" + save_checkpoint(instance, f"{role}-postjoin-{label}-{index}") + + if any(entry["joined"] for entry in join_log): + guest = next(instance for instance in instances if instance is not host) + post_join_action("guest-samus", guest, ("tap", *HUNTER_SAMUS)) + post_join_action("guest-confirm", guest, ("key", "a")) + post_join_action("host-disc", host, ("tap", *HOST_START_DISC)) + for instance in instances: + state = current_screen(instance) + post_join_log.append( + { + "instance": instance.index, + "screen": state, + "final_image": instance.report[-1]["image"], + "final_vblank9": instance.report[-1]["vblank9"], + } + ) + print( + f"[post-join] instance {instance.index}: {state} " + f"{instance.report[-1]['image']}", + flush=True, + ) + for target in args.targets: for_each( instances, @@ -579,6 +628,7 @@ def join_phase(instance: Instance) -> None: "wfc_provider": args.wfc_provider, "join_attempts": join_log, "joined": any(entry["joined"] for entry in join_log), + "post_join": post_join_log, "summaries": summaries, "backend_clean": all(s["backend_clean"] for s in summaries), } From 4b9ad7cbd36cd4b0b0a3f07375d4c84cb29c5c01 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:49:25 +0900 Subject: [PATCH 127/164] Fix Windows Nightly PowerShell packaging --- .github/workflows/build-windows.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 687644a..b643b76 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -107,7 +107,10 @@ jobs: version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" test -n "$version" runtime_bin="$(cygpath -w /mingw64/bin)" - pwsh -NoProfile -File tools/package-windows-nightly.ps1 \ + ps_exe="$(cygpath -u "${SYSTEMROOT}")/System32/WindowsPowerShell/v1.0/powershell.exe" + script_path="$(cygpath -w "$PWD/tools/package-windows-nightly.ps1")" + test -x "$ps_exe" + "$ps_exe" -NoProfile -ExecutionPolicy Bypass -File "$script_path" \ -Version "$version" \ -RunnerBuildDir '..\ndsrecomp\runner\build-mph-nightly' \ -LauncherBuildDir 'launcher\recomp-ui\build-nightly' \ From dc95926c79a7473dd0b59d19f6743dcbd80ddf54 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:52:51 +0900 Subject: [PATCH 128/164] Gate Nightly helper syntax in build CI --- .github/workflows/build.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cfa34a..e6f5b3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,28 @@ jobs: steps: - name: Check out sources uses: actions/checkout@v4 + + - name: Check ROM-free helper syntax + shell: bash + run: | + set -euo pipefail + python -m py_compile \ + tools/patch_ndsrecomp_rom_free_release.py \ + tools/ci/check_rom_free_release_sources.py \ + tools/ci/prepare_freebios_banks.py \ + tools/ci/verify-nightly-assets.py + bash -n tools/package-linux-appimage.sh + pwsh -NoProfile -Command ' + $tokens = $null; $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path "tools/package-windows-nightly.ps1"), + [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -ne 0) { + Write-Error ($errors | Out-String) + exit 1 + } + ' + - name: Verify no ROM-secret release path run: python tools/ci/check_rom_free_release_sources.py From c19611dbd50691501373a4f2a07b437216b224fe Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:15:36 +0900 Subject: [PATCH 129/164] Fix delegated multi-ROM status in launcher UI --- tools/patch_recomp_ui_mph_multirom.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tools/patch_recomp_ui_mph_multirom.py diff --git a/tools/patch_recomp_ui_mph_multirom.py b/tools/patch_recomp_ui_mph_multirom.py new file mode 100644 index 0000000..e35d69e --- /dev/null +++ b/tools/patch_recomp_ui_mph_multirom.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Patch pinned recomp-ui to render delegated MPH ROM validation honestly. + +MPH runtime compatibility is not a whole-ROM SHA-1 gate. When GameInfo omits +all cartridge fingerprints, the stock recomp-ui model deliberately cannot call +the ROM "verified" and the ImGui dashboard therefore renders "ROM not +recognized" even though launcher_model_can_play() correctly allows the host to +perform its own launch-time validation. + +For this project, a fingerprint-free cartridge means exactly that: acceptance +is delegated to nds_runner's MPH executable-compatible detector. This patch +changes only that dashboard presentation. Fingerprinted games keep stock +verified/not-recognized semantics, and the runner remains the authoritative +fail-closed validator. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +OLD = ''' const bool verified = launcher_model_rom_verified(m);\n char line[64];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(verified, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(verified ? col(th.good) : col(th.warn), "%s", line);''' + +NEW = ''' const bool verified = launcher_model_rom_verified(m);\n // MPH_MULTIROM_DELEGATED_VERIFY: no generic fingerprint means the host\n // intentionally delegates compatibility to its runtime detector. Do\n // not tell the player that such a ROM is "not recognized"; Play is\n // already allowed by launcher_model_can_play() in this state.\n const bool delegated = m->rom_present && !m->has_expected_crc &&\n m->num_known_sha256 == 0 &&\n m->num_known_sha1 == 0;\n const bool accepted = verified || delegated;\n char line[96];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else if (delegated) snprintf(line, sizeof(line), "%s selected - runtime validation", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(accepted, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(accepted ? col(th.good) : col(th.warn), "%s", line);''' + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--recomp-ui-root", type=Path, required=True) + args = parser.parse_args() + + root = args.recomp_ui_root.resolve() + path = root / "src" / "common" / "backends" / "imgui" / "launcher_imgui.cpp" + if not path.is_file(): + raise SystemExit(f"missing pinned recomp-ui source: {path}") + + text = path.read_text(encoding="utf-8-sig") + if NEW in text: + print(f"recomp-ui MPH multi-ROM presentation already patched: {path}") + return + count = text.count(OLD) + if count != 1: + raise SystemExit( + f"{path}: expected exactly one launcher ROM-verdict anchor, got {count}; " + "recomp-ui pin/source shape drifted" + ) + path.write_text(text.replace(OLD, NEW), encoding="utf-8") + print(f"Patched delegated MPH ROM validation presentation in {path}") + + +if __name__ == "__main__": + main() From 89731aeb9fc1da54e032eaf167fcbd71524e0697 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:15:57 +0900 Subject: [PATCH 130/164] Delegate launcher ROM verification to runtime detector --- launcher/recomp-ui/CMakeLists.txt | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index cbf4a90..4dd9705 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -6,14 +6,15 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(SDL2 CONFIG REQUIRED) +find_package(Python3 COMPONENTS Interpreter REQUIRED) set(NDSRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../ndsrecomp" CACHE PATH "Path to the ndsrecomp framework checkout (for the shared SHA-1 helper)") set(MPH_LAUNCHER_ROM_SHA1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" CACHE STRING "Clean retail ROM SHA-1 metadata rendered for this profile-specific launcher") -set(MPH_LAUNCHER_REGION "USA" CACHE STRING - "Region label shown by the profile-specific launcher") +set(MPH_LAUNCHER_REGION "Auto (runtime detected)" CACHE STRING + "Region label shown by the launcher; generic Nightly detection happens at runtime") set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING "Default ROM filename offered by this profile-specific launcher") @@ -65,6 +66,24 @@ target_link_libraries(mph-recomp-ui PRIVATE SDL2::SDL2) set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH "Path to the shared recomp-ui checkout") +option(MPH_PATCH_RECOMP_UI_MULTIROM + "Patch pinned recomp-ui so fingerprint-free MPH ROMs show runtime-delegated validation" + ON) +if(MPH_PATCH_RECOMP_UI_MULTIROM) + execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/patch_recomp_ui_mph_multirom.py" + --recomp-ui-root "${RECOMP_UI_ROOT}" + RESULT_VARIABLE _mph_recomp_ui_patch_result + OUTPUT_VARIABLE _mph_recomp_ui_patch_stdout + ERROR_VARIABLE _mph_recomp_ui_patch_stderr) + if(NOT _mph_recomp_ui_patch_result EQUAL 0) + message(FATAL_ERROR + "Failed to apply MPH multi-ROM recomp-ui patch:\n" + "${_mph_recomp_ui_patch_stdout}${_mph_recomp_ui_patch_stderr}") + endif() + message(STATUS "${_mph_recomp_ui_patch_stdout}") +endif() enable_testing() add_executable(mph-mod-provider-test tests/launcher_mod_provider_test.cpp From cd8d6f546860574897d96eb17c12912df5782b39 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:16:24 +0900 Subject: [PATCH 131/164] Keep profile defaults while patching real launcher UI --- launcher/recomp-ui/CMakeLists.txt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 4dd9705..03518ee 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -13,8 +13,8 @@ set(NDSRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../ndsrecomp" CACHE PATH set(MPH_LAUNCHER_ROM_SHA1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" CACHE STRING "Clean retail ROM SHA-1 metadata rendered for this profile-specific launcher") -set(MPH_LAUNCHER_REGION "Auto (runtime detected)" CACHE STRING - "Region label shown by the launcher; generic Nightly detection happens at runtime") +set(MPH_LAUNCHER_REGION "USA" CACHE STRING + "Region label shown by the profile-specific launcher") set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING "Default ROM filename offered by this profile-specific launcher") @@ -69,7 +69,9 @@ set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH option(MPH_PATCH_RECOMP_UI_MULTIROM "Patch pinned recomp-ui so fingerprint-free MPH ROMs show runtime-delegated validation" ON) -if(MPH_PATCH_RECOMP_UI_MULTIROM) +set(_mph_recomp_ui_imgui + "${RECOMP_UI_ROOT}/src/common/backends/imgui/launcher_imgui.cpp") +if(MPH_PATCH_RECOMP_UI_MULTIROM AND EXISTS "${_mph_recomp_ui_imgui}") execute_process( COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/patch_recomp_ui_mph_multirom.py" @@ -83,6 +85,12 @@ if(MPH_PATCH_RECOMP_UI_MULTIROM) "${_mph_recomp_ui_patch_stdout}${_mph_recomp_ui_patch_stderr}") endif() message(STATUS "${_mph_recomp_ui_patch_stdout}") +elseif(MPH_PATCH_RECOMP_UI_MULTIROM) + # Static launcher-profile tests intentionally provide a minimal recomp-ui + # CMake stub and do not compile the UI backend. A real launcher build has + # the pinned source above and must take the fail-closed patch path. + message(STATUS + "MPH recomp-ui presentation patch skipped: UI backend source is not present") endif() enable_testing() From 6cf124f05e6cedca3e8889418ea0c62a6eba81d9 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:16:45 +0900 Subject: [PATCH 132/164] Show runtime-detected region in generic Nightly launcher --- .github/workflows/build-windows.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index b643b76..a1f23f7 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -95,10 +95,17 @@ jobs: -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DNDSRECOMP_ROOT="$PWD/../ndsrecomp" \ - -DRECOMP_UI_ROOT="$PWD/../recomp-ui" + -DRECOMP_UI_ROOT="$PWD/../recomp-ui" \ + '-DMPH_LAUNCHER_REGION=Auto (runtime detected)' cmake --build launcher/recomp-ui/build-nightly ctest --test-dir launcher/recomp-ui/build-nightly --output-on-failure test -s launcher/recomp-ui/build-nightly/mph-recomp-ui.exe + grep -q 'game.region = "Auto (runtime detected)";' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'game.known_sha1_hex = nullptr;' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'MPH_MULTIROM_DELEGATED_VERIFY' \ + ../recomp-ui/src/common/backends/imgui/launcher_imgui.cpp - name: Package Windows Nightly payload shell: msys2 {0} From 413c616f134999b862051834155d9bcd9173f4a7 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 18:58:12 -0700 Subject: [PATCH 133/164] launcher: remember the chosen ROM across relaunches nds_persist_setup received the ROM path the user picked and discarded it with (void)rom_path, persisting only bios_path. The path handed to recomp_launcher_run_window was always the hardcoded bundled default next to the exe, so anyone whose dump lives elsewhere re-picked it on every launch and saw "ROM not found" each time. The shared launcher does not cover for this: it writes rom.cfg sidecars into three directories but never reads them back -- grepping recomp-ui's src/ finds only writes, and the model's only ROM seed is the initial_rom argument. Tracked separately as beads-0fu.1; owning the path here keeps the fix independent of which recomp-ui build is linked. - ModState gains rom_path; settings_version 2 -> 3. - nds_persist_setup records it, and treats an empty callback value as "nothing selected right now" rather than a clear -- the callback also fires on BIOS browse, and clearing there would reintroduce the bug. - initial_rom is the remembered path when it names an existing file, else the bundled default, so a moved or deleted ROM falls back rather than presenting a selection that cannot launch. - The final selection is mirrored into mod_state before the save, matching how bios_path and player_name are already treated as authoritative (persistence is best-effort UX and PLAY can be pressed without the callback firing). Regression test covers round trip, empty-callback-does-not-clear, a new pick being recorded, a path containing '=' (the parser splits on the first '='), and version-2 forward migration keeping bios_path and player_name. beads-lqa.3 Co-Authored-By: Claude Opus 5 (1M context) --- launcher/recomp-ui/launcher_main.cpp | 38 ++++++++-- .../tests/launcher_mod_provider_test.cpp | 72 +++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/launcher/recomp-ui/launcher_main.cpp b/launcher/recomp-ui/launcher_main.cpp index 8258166..780e3b9 100644 --- a/launcher/recomp-ui/launcher_main.cpp +++ b/launcher/recomp-ui/launcher_main.cpp @@ -79,6 +79,12 @@ struct ModState { std::string pad_weapon6 = "None"; std::string pad_virtual_stylus = "None"; std::string pad_menu = "Pad Start"; + // Persisted ROM choice (beads-lqa.3). The shared launcher writes its own + // rom.cfg sidecars but never reads them back (beads-0fu.1), so the ROM the + // user picked was lost on every relaunch and the hardcoded bundled default + // was re-offered -- "ROM not found" for anyone whose dump lives elsewhere. + // Owning the path here keeps the fix independent of the recomp-ui build. + std::string rom_path; // Persisted BIOS choice, psxrecomp-style: any one of the three retail // dump files (its folder is used at launch). Empty = the built-in // FreeBIOS + generated firmware. @@ -399,6 +405,11 @@ void load_mod_state(ModState& state) { state.mouse_invert_y = value == "true"; } else if (key == "prime_controls") { state.prime_controls = value != "false"; + } else if (key == "rom_path") { + // Kept verbatim. Existence is checked at use, not here: a dump on + // removable media that is absent this launch should not erase the + // remembered pick. + state.rom_path = value; } else if (key == "bios_path") { state.bios_path = value; } else if (key == "player_name_override") { @@ -466,7 +477,7 @@ bool save_mod_state(ModState& state) { state.last_error = "Could not write launcher mod settings."; return false; } - file << "settings_version=2\n" + file << "settings_version=3\n" << "adaptive_widescreen=" << (state.adaptive_widescreen ? "true" : "false") << '\n' << "hd_rendering=" @@ -483,6 +494,7 @@ bool save_mod_state(ModState& state) { << "virtual_stylus_sensitivity=" << state.virtual_stylus_sensitivity << '\n' << "pad_aim_sensitivity=" << state.pad_aim_sensitivity << '\n' + << "rom_path=" << state.rom_path << '\n' << "bios_path=" << state.bios_path << '\n' << "player_name=" << state.player_name << '\n'; for (const BindingOption& option : kBindingOptions) @@ -994,9 +1006,14 @@ int nds_bios_verify(const char* bios_path, RecompLauncherCBiosVerify* out) { int nds_persist_setup(void* context, const char* rom_path, const char* bios_path) { - (void)rom_path; if (!context) return 1; auto* state = static_cast(context); + // beads-lqa.3: the ROM path used to be discarded here, which is why the + // pick never survived a relaunch. An empty callback value means "no ROM + // selected right now", not "forget the remembered one" -- the launcher + // fires this on BIOS browse too, and clearing on those would resurrect the + // original bug. + if (rom_path && rom_path[0]) state->rom_path = rom_path; state->bios_path = bios_path ? bios_path : ""; return save_mod_state(*state) ? 0 : 1; } @@ -1234,8 +1251,17 @@ int main(int argc, char** argv) { game.persist_setup = nds_persist_setup; game.persist_setup_ctx = &mod_state; - const std::filesystem::path default_rom = - exe / "Metroid Prime Hunters.nds"; + // beads-lqa.3: prefer the remembered pick, fall back to the bundled dump + // next to the exe. A remembered path whose file is gone falls back too, + // so a moved or deleted ROM presents the bundled default rather than a + // stale selection the user cannot launch. + std::filesystem::path default_rom = exe / "Metroid Prime Hunters.nds"; + if (!mod_state.rom_path.empty()) { + std::error_code rom_error; + const std::filesystem::path remembered(mod_state.rom_path); + if (std::filesystem::is_regular_file(remembered, rom_error)) + default_rom = remembered; + } char selected_rom[1024]{}; const int result = recomp_launcher_run_window( "Metroid Prime Hunters - Launcher", &settings, &game, @@ -1249,6 +1275,10 @@ int main(int argc, char** argv) { // The UI's final BIOS selection is authoritative for this launch even if // a persist callback was missed (persistence is best-effort UX). mod_state.bios_path = settings.bios_path; + // Same for the ROM (beads-lqa.3). PLAY can be pressed without the persist + // callback ever firing for the ROM, so treat what we are about to launch + // as the thing to remember. + if (selected_rom[0]) mod_state.rom_path = selected_rom; // Same for the ONLINE card's player name. An invalid entry is surfaced // and dropped rather than silently reshaped or allowed to block launch. { diff --git a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp index 80e8a1b..fd209cb 100644 --- a/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp +++ b/launcher/recomp-ui/tests/launcher_mod_provider_test.cpp @@ -394,5 +394,77 @@ int main() { std::filesystem::remove(hd_saved.settings_path); } + // beads-lqa.3: the chosen ROM must survive a relaunch. It used to be + // discarded in nds_persist_setup, so every launch fell back to the bundled + // default path and anyone whose dump lived elsewhere saw "ROM not found" + // forever. + { + const std::filesystem::path settings_path = + std::filesystem::temp_directory_path() / + "mph_mod_provider_rom_settings.ini"; + + ModState saved{}; + saved.settings_path = settings_path; + saved.rom_path = "D:\\Games\\NDS\\Metroid Prime Hunters.nds"; + saved.bios_path = "D:\\Games\\NDS\\bios\\biosnds9.rom"; + if (!require(save_mod_state(saved), "rom save")) return 73; + + ModState loaded{}; + loaded.settings_path = settings_path; + load_mod_state(loaded); + if (!require(loaded.rom_path == saved.rom_path, + "rom path round trip")) return 74; + + // The callback fires on BIOS browse too, with no ROM. An empty value + // means "nothing selected right now", never "forget the pick" -- + // clearing here would reintroduce the original bug. + if (!require(nds_persist_setup(&loaded, "", "E:\\dumps\\biosnds7.rom") + == 0, + "persist_setup with empty rom succeeds")) return 75; + if (!require(loaded.rom_path == saved.rom_path, + "empty persist_setup rom does not clear the pick")) + return 76; + if (!require(loaded.bios_path == "E:\\dumps\\biosnds7.rom", + "persist_setup still updates bios")) return 77; + + if (!require(nds_persist_setup(&loaded, "E:\\other\\MPH.nds", "") == 0, + "persist_setup with a rom succeeds")) return 78; + if (!require(loaded.rom_path == "E:\\other\\MPH.nds", + "persist_setup records a new rom pick")) return 79; + + // A path containing '=' must survive: load_mod_state splits on the + // FIRST '=', so everything after it is the value. + ModState equals_saved{}; + equals_saved.settings_path = settings_path; + equals_saved.rom_path = "D:\\ROMs\\a=b\\Metroid Prime Hunters.nds"; + if (!require(save_mod_state(equals_saved), "rom save with equals")) + return 80; + ModState equals_loaded{}; + equals_loaded.settings_path = settings_path; + load_mod_state(equals_loaded); + if (!require(equals_loaded.rom_path == equals_saved.rom_path, + "rom path with '=' round trip")) return 81; + + // A pre-existing version-2 file has no rom_path key. It must load + // cleanly with an empty pick and keep everything else. + { + std::ofstream file(settings_path, std::ios::trunc); + file << "settings_version=2\n" + "bios_path=F:\\keepme\\biosnds9.rom\n" + "player_name=Samus\n"; + } + ModState migrated{}; + migrated.settings_path = settings_path; + load_mod_state(migrated); + if (!require(migrated.rom_path.empty(), + "version 2 settings have no remembered rom")) return 82; + if (!require(migrated.bios_path == "F:\\keepme\\biosnds9.rom", + "version 2 migration keeps bios_path")) return 83; + if (!require(migrated.player_name == "Samus", + "version 2 migration keeps player_name")) return 84; + + std::filesystem::remove(settings_path); + } + return 0; } From 9d51f0ee01d0886a43a300ab400480975ff1ad2a Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 19:28:14 -0700 Subject: [PATCH 134/164] tools: report which ARM9 overlays a play route actually exercised Decides what is worth recompiling ahead of time: maps every Tier-3 address from a coverage manifest or a fuzz/benchmark trace onto the overlay table, and reports points and hits per overlay. Collisions are computed by SPAN, not by load address. Judging by load address is wrong and flatters the picture -- in MPH three overlays look unshared by load address, but all eighteen overlap another by span (overlay 4 loads at 0x0214C860; overlay 10 loads at 0x0214C940, 0xE0 bytes inside it). That matters because "pick an overlay with no collision to handle yet" is the pilot step docs/overlay-strategy.md section 4 recommends, and for MPH no such overlay exists. Addresses outside every overlay are reported rather than dropped: ITCM, ARM7 WRAM and the immutable main image all land there, and silently discarding them would make a route look better covered than it is. Measured so far, ARM9: 200M-cycle direct boot, no input -> 9 of 18 overlays 4000 vblanks, automatic startup -> 11 of 18 overlays Overlay 0 dominates both (2695 points / 701470 hits on the longer run). The seven still dark are the 0x0219DC20 group (5,6,7,17) plus 14,15,16, which a boot-and-idle route never reaches. beads-yjp.31 Co-Authored-By: Claude Opus 5 (1M context) --- tools/overlay_coverage_report.py | 144 +++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tools/overlay_coverage_report.py diff --git a/tools/overlay_coverage_report.py b/tools/overlay_coverage_report.py new file mode 100644 index 0000000..3f86169 --- /dev/null +++ b/tools/overlay_coverage_report.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Report which ARM9 overlays a play route actually exercised. + +beads-yjp.31. Answers the question that decides what to recompile ahead of +time: for a given route through the game, which overlays did the guest +actually execute, and how hard? + +Takes any number of coverage sources and maps every Tier-3 address onto the +overlay table: + + * a coverage manifest written by the runner (--coverage-manifest, or the + `coverage_manifest` debug command), which has entry_points_arm9/arm7, or + * a fuzz/benchmark trace.json carrying a tier3_coverage block. + +Collisions are computed by SPAN, not by load address. Judging by load address +alone is wrong and flatters the picture: in MPH three overlays look unshared +by load address, but every one of the eighteen overlaps another by span (e.g. +overlay 4 loads at 0x0214C860 and overlay 10 loads at 0x0214C940, 0xE0 bytes +inside it). + +Addresses that fall outside every overlay are reported too, not dropped -- +ITCM, ARM7 WRAM and the immutable main image all show up there, and a silent +drop would make a route look better covered than it is. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + + +def load_overlays(path: Path) -> list[dict]: + overlays = json.loads(path.read_text(encoding="utf-8")) + for entry in overlays: + entry["lo"] = int(entry["load_address"], 16) + # bss is zero-initialised at load and holds no code, so the executable + # span is size, not size + bss_size. + entry["hi"] = entry["lo"] + int(entry["size"]) + return overlays + + +def load_points(paths: list[Path]) -> tuple[list[tuple[int, int, str]], list[str]]: + """Return [(addr, hits, kind)] for ARM9, plus a note per source.""" + points: list[tuple[int, int, str]] = [] + notes: list[str] = [] + for path in paths: + data = json.loads(path.read_text(encoding="utf-8")) + if data.get("kind") == "ndsrecomp-tier3-coverage": + entries = data.get("entry_points_arm9", []) + points += [(int(e["addr"], 16), int(e.get("hits", 0)), + str(e.get("kind", "?"))) for e in entries] + notes.append(f"{path.name}: manifest, {len(entries)} ARM9 entries") + continue + block = data.get("tier3_coverage") or {} + entries = [e for e in block.get("entries", []) if int(e["cpu"]) == 9] + if entries: + kinds = {1: "root", 2: "call", 3: "indirect"} + points += [(int(e["pc"]), int(e.get("hits", 0)), + kinds.get(int(e.get("kind", 0)), "?")) for e in entries] + notes.append(f"{path.name}: trace, {len(entries)} ARM9 entries") + else: + notes.append(f"{path.name}: NO ARM9 Tier-3 coverage found") + return points, notes + + +def main() -> int: + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("sources", nargs="+", type=Path, + help="coverage manifests and/or trace.json files") + parser.add_argument("--overlays", type=Path, + default=Path("generated/inputs/overlays.json")) + parser.add_argument("--json", type=Path, help="also write the report here") + args = parser.parse_args() + + overlays = load_overlays(args.overlays) + points, notes = load_points(args.sources) + for note in notes: + print(f" {note}") + if not points: + print("no ARM9 Tier-3 coverage in any source") + return 1 + print() + + rows = [] + for entry in sorted(overlays, key=lambda o: (o["lo"], o["id"])): + inside = [p for p in points if entry["lo"] <= p[0] < entry["hi"]] + shares = [str(o["id"]) for o in overlays + if o is not entry and o["lo"] < entry["hi"] + and entry["lo"] < o["hi"]] + rows.append({ + "id": entry["id"], + "lo": entry["lo"], + "hi": entry["hi"], + "kib": int(entry["size"]) // 1024, + "points": len(inside), + "hits": sum(p[1] for p in inside), + "shares_span_with": shares, + }) + + print(f"{'id':>3} {'span':<23} {'KiB':>5} {'points':>7} {'hits':>9} shares span with") + for row in rows: + shared = ",".join(row["shares_span_with"]) or "-" + print(f"{row['id']:>3} 0x{row['lo']:08X}-0x{row['hi']:08X} " + f"{row['kib']:>5} {row['points']:>7} {row['hits']:>9} {shared}") + + covered = {p[0] for row, entry in zip(rows, sorted(overlays, key=lambda o: (o["lo"], o["id"]))) + for p in points if entry["lo"] <= p[0] < entry["hi"]} + outside = [p for p in points if p[0] not in covered] + print() + print(f"addresses inside an overlay : {len(points) - len(outside)}") + print(f"addresses outside every one : {len(outside)}" + " (ITCM / ARM7 WRAM / immutable main image)") + if outside: + buckets: dict[int, int] = {} + for addr, _hits, _kind in outside: + buckets[addr >> 16 << 16] = buckets.get(addr >> 16 << 16, 0) + 1 + print(" by 64 KiB region:") + for base in sorted(buckets): + print(f" 0x{base:08X} {buckets[base]}") + + exercised = [r for r in rows if r["points"]] + print() + print(f"overlays exercised: {len(exercised)} of {len(rows)}") + if exercised: + best = max(exercised, key=lambda r: r["hits"]) + print(f"highest-value target: overlay {best['id']} " + f"({best['points']} points, {best['hits']} hits)") + + if args.json: + args.json.write_text(json.dumps({ + "sources": [str(p) for p in args.sources], + "overlays": rows, + "points_total": len(points), + "points_outside_overlays": len(outside), + }, indent=2), encoding="utf-8", newline="\n") + print(f"\nwrote {args.json}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From ab64ad8ac2eca7e181bb0f8c846e18260620087b Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 20:10:39 -0700 Subject: [PATCH 135/164] tools: offline-multiplayer route + end-to-end overlay coverage loop Offline multiplayer against bots IS reachable with no peer DS, which makes it a far better content lever than Adventure: the arena is chosen from a menu (ARENA n/9 for Battle), bots supply the gameplay, and no navigation skill is needed. The waiting room states it outright -- "YOU CAN WAIT FOR MORE PLAYERS OR ADD BOTS" -- and ADD BOT fills the roster (Setya, KANBOT, SAMBOT, SPIBOT). This is consistent with the vendored MphRead, which carries PlayerAiData and BotLevel, and whose GameMode enum matches the mode grid exactly (Battle/Survival/Bounty/Defender/PrimeHunter/Capture/ Nodes). Route coordinates below are measured from captured frames, not estimated: main menu MULTIPLAYER touch (160,92) [ADVENTURE is (84,92)] nickname dialog two A presses mode row y~80 SINGLE-CARD x~50, MULTI-CARD x~128, WI-FI x~205 create/join y~80 CREATE x~50, JOIN x~128 game mode y~58 BATTLE x~50 arena screen y~48 arrows left x~125 / right x~232, confirm (222,173) hunter Samus (27,132) then A waiting room ADD BOT (222,173) mph_overlay_route.py is the measurement loop: cold boot, replay a scenario, dump the Tier-3 manifest over TCP, hand it to overlay_coverage_report.py. It dumps via the debug command rather than process exit because --serve never leaves its accept loop, so a harness-killed run would lose everything it recorded. Measured ARM9 overlay coverage by route: 200M-cycle direct boot, no input 9 of 18 2085 entries 4000 vblanks, automatic startup 11 of 18 3751 entries multiplayer setup to the bot lobby 11 of 18 5010 entries The multiplayer route newly lights overlays 3 and 8, which every earlier route left at zero. Still dark: 14, 15, 16 and the 0x0219DC20 group (5, 6, 7, 17) -- almost certainly in-arena code, since the route stops in the waiting room. NOT DONE: the control that actually starts the match. START and A do not launch it, and the tap at (105,133) hits a bot-level star control rather than a ready toggle. Finding it is the next step and should unlock the remaining overlays. beads-yjp.31 Co-Authored-By: Claude Opus 5 (1M context) --- scenarios/multiplayer_battle_bots.json | 151 +++++++++++++++++++ scenarios/multiplayer_probe.json | 29 ++++ tools/mph_overlay_route.py | 195 +++++++++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 scenarios/multiplayer_battle_bots.json create mode 100644 scenarios/multiplayer_probe.json create mode 100644 tools/mph_overlay_route.py diff --git a/scenarios/multiplayer_battle_bots.json b/scenarios/multiplayer_battle_bots.json new file mode 100644 index 0000000..3e72ed0 --- /dev/null +++ b/scenarios/multiplayer_battle_bots.json @@ -0,0 +1,151 @@ +{ + "description": "Offline multiplayer: BATTLE on arena 1/9 (Combat Hall) against 3 bots, no peer DS required. Reaches real gameplay far faster than the Adventure route and is the deterministic content lever for overlay coverage -- change only the arena arrow taps to select a different arena. Verified coordinates: main-menu MULTIPLAYER touch (160,92); the nickname dialog takes two A presses; mode row touch y~80 (SINGLE-CARD x~50, MULTI-CARD x~128, WI-FI x~205); CREATE/JOIN row touch y~80 (CREATE x~50, JOIN x~128); game-mode grid top row touch y~58 (BATTLE x~50); arena screen shows ARENA n/9 with arrows at touch y~48 (left x~125, right x~232) and confirm at touch (222,173); hunter Samus at touch (27,132) then A; the waiting room offers ADD BOT at touch (222,173).", + "actions": [ + { + "kind": "touch", + "x": 160, + "y": 92 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 128, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 50, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 50, + "y": 62 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 27, + "y": 132 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 600 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 105, + "y": 133 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 600 + }, + { + "kind": "wait", + "frames": 600 + } + ] +} \ No newline at end of file diff --git a/scenarios/multiplayer_probe.json b/scenarios/multiplayer_probe.json new file mode 100644 index 0000000..d2711ab --- /dev/null +++ b/scenarios/multiplayer_probe.json @@ -0,0 +1,29 @@ +{ + "description": "Discovery trace: Multiplayer -> MULTI-CARD -> CREATE GAME -> BATTLE -> confirm arena -> select hunter -> start, to establish whether a local match can begin with no peer DS present. Verified coordinates: main-menu MULTIPLAYER touch (160,92); nickname dialog takes two A presses; mode row touch y~80 (SINGLE-CARD x~50, MULTI-CARD x~128, WI-FI x~205); CREATE/JOIN row touch y~80 (CREATE x~50, JOIN x~128); game-mode grid top row touch y~58 (BATTLE x~50); arena screen shows ARENA n/9 with arrows at touch y~48 (left x~125, right x~232) and a confirm check at touch (222,173).", + "actions": [ + { "kind": "touch", "x": 160, "y": 92 }, + { "kind": "wait", "frames": 240 }, + { "kind": "wait", "frames": 240 }, + { "kind": "key", "key": "a" }, + { "kind": "wait", "frames": 240 }, + { "kind": "key", "key": "a" }, + { "kind": "wait", "frames": 240 }, + { "kind": "touch", "x": 128, "y": 80 }, + { "kind": "wait", "frames": 300 }, + { "kind": "wait", "frames": 300 }, + { "kind": "touch", "x": 50, "y": 80 }, + { "kind": "wait", "frames": 300 }, + { "kind": "wait", "frames": 300 }, + { "kind": "touch", "x": 50, "y": 62 }, + { "kind": "wait", "frames": 300 }, + { "kind": "wait", "frames": 300 }, + { "kind": "touch", "x": 222, "y": 173 }, + { "kind": "wait", "frames": 300 }, + { "kind": "wait", "frames": 300 }, + { "kind": "touch", "x": 27, "y": 132 }, + { "kind": "wait", "frames": 300 }, + { "kind": "key", "key": "a" }, + { "kind": "wait", "frames": 600 }, + { "kind": "wait", "frames": 600 } + ] +} diff --git a/tools/mph_overlay_route.py b/tools/mph_overlay_route.py new file mode 100644 index 0000000..13dc2d1 --- /dev/null +++ b/tools/mph_overlay_route.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Run a route and report which overlays it exercised. + +beads-yjp.31. The measurement loop: cold boot -> replay a scenario -> dump the +Tier-3 coverage manifest over TCP -> map it onto the overlay table. + +The manifest dump is a debug command rather than an exit-path write because +--serve never exits its accept loop, so a harness-killed session would +otherwise lose everything it recorded. Dumping on demand also means a route +can be sampled part-way through instead of only at the end. + +Screenshots are captured at every step so a route can be checked visually and +so new screen predicates can be measured from real frames. +""" + +from __future__ import annotations + +import argparse +import json +import socket +import subprocess +import sys +import time +from pathlib import Path + +KEY_BITS = { + "a": 0, "b": 1, "select": 2, "start": 3, "right": 4, "left": 5, + "up": 6, "down": 7, "r": 8, "l": 9, "x": 10, "y": 11, +} +# Active-low, and the server's own default of 0x3FF would leave X and Y held. +RELEASED = 0x0FFF + + +class DebugClient: + def __init__(self, port: int, timeout: float = 1800.0) -> None: + self.sock = socket.create_connection(("127.0.0.1", port), timeout=timeout) + self.buf = b"" + + def cmd(self, name: str, **args: object) -> dict: + args["cmd"] = name + self.sock.sendall((json.dumps(args) + "\n").encode()) + while b"\n" not in self.buf: + chunk = self.sock.recv(1 << 16) + if not chunk: + raise RuntimeError("debug server closed the connection") + self.buf += chunk + line, self.buf = self.buf.split(b"\n", 1) + reply = json.loads(line) + if isinstance(reply, dict) and "error" in reply: + raise RuntimeError(f"{name}: {reply['error']}") + return reply + + def vblank(self) -> int: + return int(self.cmd("event_counts")["vblank9"]) + + def advance(self, frames: int) -> None: + target = self.vblank() + frames + reply = self.cmd("run_to_event", event="vblank9", count=target) + if reply.get("terminal"): + raise RuntimeError(f"runner halted: {reply.get('reason9')} / " + f"{reply.get('reason7')}") + if reply.get("stalled"): + raise RuntimeError(f"stalled before vblank9 {target}") + + def tap(self, x: int, y: int, hold: int) -> None: + self.cmd("touch", x=x, y=y, down=True) + self.advance(hold) + self.cmd("touch", x=0, y=0, down=False) + + def press(self, key: str, hold: int) -> None: + bit = KEY_BITS[key] + self.cmd("keys", mask=RELEASED & ~(1 << bit)) + self.advance(hold) + self.cmd("keys", mask=RELEASED) + + def screenshot(self, path: Path) -> None: + try: + from PIL import Image + except ImportError: + return + frames = [] + for engine in ("A", "B"): + fb = self.cmd("framebuffer", engine=engine) + raw = bytes.fromhex(fb["rgb"]) + frames.append(Image.frombytes("RGB", (fb["w"], fb["h"]), raw)) + combined = Image.new("RGB", (frames[0].width, + frames[0].height + frames[1].height)) + combined.paste(frames[0], (0, 0)) + combined.paste(frames[1], (0, frames[0].height)) + combined.save(path) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runner", type=Path, required=True) + parser.add_argument("--bios", type=Path, required=True) + parser.add_argument("--rom", type=Path, required=True) + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--actions", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--overlays", type=Path, + default=Path("generated/inputs/overlays.json")) + parser.add_argument("--port", type=int, default=19890) + parser.add_argument("--start-vblank", type=int, default=7800) + parser.add_argument("--hold-frames", type=int, default=3) + parser.add_argument("--settle-frames", type=int, default=45) + parser.add_argument("--play-frames", type=int, default=0, + help="hold forward this many frames after the route") + parser.add_argument("--no-shots", action="store_true") + args = parser.parse_args() + + args.out.mkdir(parents=True, exist_ok=True) + actions = json.loads(args.actions.read_text(encoding="utf-8")) + if isinstance(actions, dict): + actions = actions.get("actions", []) + + proc = subprocess.Popen( + [str(args.runner), str(args.bios), "--serve", "--port", str(args.port), + "--rom", str(args.rom.resolve()), "--config", str(args.config.resolve()), + "--no-save", "--startup-mode", "automatic"], + cwd=str(args.runner.parent), + stdout=(args.out / "runner.stdout.log").open("wb"), + stderr=(args.out / "runner.stderr.log").open("wb")) + print(f"runner pid={proc.pid} port={args.port}") + + try: + client = None + for _ in range(240): + if proc.poll() is not None: + raise SystemExit(f"runner exited early rc={proc.returncode}") + try: + client = DebugClient(args.port) + break + except OSError: + time.sleep(0.5) + if client is None: + raise SystemExit("debug server never came up") + + client.cmd("reset") + target = args.start_vblank + client.cmd("run_to_event", event="vblank9", count=target) + if not args.no_shots: + client.screenshot(args.out / f"0000-{target:05d}-title.png") + client.tap(128, 96, args.hold_frames) + client.advance(180) + + for index, action in enumerate(actions, 1): + kind = action["kind"] + if kind == "touch": + client.tap(int(action["x"]), int(action["y"]), args.hold_frames) + label = f"touch-{action['x']}-{action['y']}" + elif kind == "key": + client.press(str(action["key"]), args.hold_frames) + label = f"key-{action['key']}" + elif kind == "wait": + client.advance(int(action["frames"])) + label = f"wait-{action['frames']}" + else: + raise SystemExit(f"unknown action kind {kind!r}") + client.advance(args.settle_frames) + if not args.no_shots: + client.screenshot( + args.out / f"{index:04d}-{client.vblank():05d}-{label}.png") + print(f"[{index}/{len(actions)}] {label}") + + if args.play_frames: + # Hold forward. Press-and-hold is state plus time advance; there is + # no hold-for-N command. + client.cmd("keys", mask=RELEASED & ~(1 << KEY_BITS["up"])) + client.advance(args.play_frames) + client.cmd("keys", mask=RELEASED) + if not args.no_shots: + client.screenshot(args.out / "9998-after-walk.png") + print(f"held forward {args.play_frames} frames") + + print("static_coverage:", json.dumps(client.cmd("static_coverage"))) + manifest = (args.out / "coverage.json").resolve() + res = client.cmd("coverage_manifest", path=manifest.as_posix()) + print("coverage_manifest:", json.dumps(res)) + print(f"\nmanifest: {manifest}") + print("now run:") + print(f" py -3 tools/overlay_coverage_report.py {manifest} " + f"--overlays {args.overlays}") + finally: + proc.terminate() + try: + proc.wait(timeout=20) + except subprocess.TimeoutExpired: + proc.kill() + print(f"stopped pid={proc.pid}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 04498fdf8040f634674ae20c759e3fd2e999eb62 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 20:49:13 -0700 Subject: [PATCH 136/164] tools: seed an overlay's recompiler config with proven-generation entry points prepare_mph.py already writes a config per overlay, but deliberately with NO entry points -- its docstring says real ones must come from Tier-3 coverage recorded while that exact overlay body was resident. Without them the recompiler gets a single entry_pc seed into a 276 KB image and discovers almost nothing. Supplying them looked blocked. MPH reuses virtual addresses: all 18 overlays overlap another by span and overlay 0 shares its range with eight. A Tier-3 record is {pc, caller, cpu, thumb, kind, hits} with no overlay identity, and docs/BRINGUP.md calls combining entry points across overlay generations unsound. The coverage manifest already resolves it. It captures the 4 KiB code pages the guest actually executed, verbatim, at execution time. Comparing a captured page against each overlay's decompressed ROM image identifies which overlay was resident when that page ran -- so no runtime change and no new field are needed. Measured on a real capture: of 65 captured ARM9 pages, 33 fell inside some overlay and every one attributed to exactly ONE of them -- 28 to overlay 0, 2 to overlay 1, 3 to overlay 4, zero ambiguous. Overlapping spans do not produce collisions in practice. Seeding overlay 0 from three captured routes: 29 pages proved resident, 8 pages inside its span were rejected as a different generation, 239 entry points dropped as unproven, 7162 dropped as kind=root (scheduler resume points make poor seeds), leaving 212 seeds. The recompiler then discovered 2313 functions (arm=2313 thumb=0, undefined=0) and emitted 2314 across 8 shards with the identity sha1 verified. config/mph_arm9_ov000.toml is that seeded config, committed so the result is reproducible. beads-yjp.31 Co-Authored-By: Claude Opus 5 (1M context) --- config/mph_arm9_ov000.toml | 1288 +++++++++++++++++++++++++++ scenarios/mp_bots_start.json | 177 ++++ scenarios/mp_singlecard_probe.json | 59 ++ scenarios/mp_start_probe.json | 178 ++++ tools/seed_overlay_from_coverage.py | 158 ++++ 5 files changed, 1860 insertions(+) create mode 100644 config/mph_arm9_ov000.toml create mode 100644 scenarios/mp_bots_start.json create mode 100644 scenarios/mp_singlecard_probe.json create mode 100644 scenarios/mp_start_probe.json create mode 100644 tools/seed_overlay_from_coverage.py diff --git a/config/mph_arm9_ov000.toml b/config/mph_arm9_ov000.toml new file mode 100644 index 0000000..a15fd76 --- /dev/null +++ b/config/mph_arm9_ov000.toml @@ -0,0 +1,1288 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 0" +id = "mph_amhe0_arm9_ov000" +load_address = 0x02102220 +size = 0x00045140 +entry_pc = 0x02103010 +authoritative_entry_points = false + +[identity] +sha1 = "5983749ec5c2982e54a10e5f1107ef92b86342b0" + +[[entry_point]] +addr = 0x02103010 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02103088 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02103184 +mode = "arm" +kind = "runtime_observed" +# hits = 6715 + +[[entry_point]] +addr = 0x02103208 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021034E8 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x021034F4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02103510 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021035CC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021035F8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103740 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x021037AC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103834 +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02103838 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210384C +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02103868 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103870 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103974 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103DD0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103FFC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02106B5C +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02106C34 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02106CE4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02106D80 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02106DAC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02106E8C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02112E80 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02112F9C +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02112FB4 +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02112FDC +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02114228 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02114AF4 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02115208 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02115260 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211527C +mode = "arm" +kind = "runtime_observed" +# hits = 32 + +[[entry_point]] +addr = 0x0211528C +mode = "arm" +kind = "runtime_observed" +# hits = 24 + +[[entry_point]] +addr = 0x02115700 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x02115A5C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115A60 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A64 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A68 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A6C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A70 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A90 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02115AA4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021168A8 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x021204D4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02123980 +mode = "arm" +kind = "runtime_observed" +# hits = 12272 + +[[entry_point]] +addr = 0x02123B2C +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02123C00 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02123C54 +mode = "arm" +kind = "runtime_observed" +# hits = 60 + +[[entry_point]] +addr = 0x02123C78 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02123E14 +mode = "arm" +kind = "runtime_observed" +# hits = 403 + +[[entry_point]] +addr = 0x02123F30 +mode = "arm" +kind = "runtime_observed" +# hits = 141 + +[[entry_point]] +addr = 0x02123F34 +mode = "arm" +kind = "runtime_observed" +# hits = 788 + +[[entry_point]] +addr = 0x02123F58 +mode = "arm" +kind = "runtime_observed" +# hits = 199 + +[[entry_point]] +addr = 0x02124040 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02124280 +mode = "arm" +kind = "runtime_observed" +# hits = 61 + +[[entry_point]] +addr = 0x02124284 +mode = "arm" +kind = "runtime_observed" +# hits = 102 + +[[entry_point]] +addr = 0x021242B0 +mode = "arm" +kind = "runtime_observed" +# hits = 35 + +[[entry_point]] +addr = 0x02124304 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021243D0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021243E8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02124AF8 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02124B18 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02124DDC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02124FE8 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02125018 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x0212511C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02125174 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02125294 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125350 +mode = "arm" +kind = "runtime_observed" +# hits = 11696 + +[[entry_point]] +addr = 0x0212545C +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x0212549C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021254DC +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x021254E8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125548 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125554 +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x021255AC +mode = "arm" +kind = "runtime_observed" +# hits = 12098 + +[[entry_point]] +addr = 0x0212574C +mode = "arm" +kind = "runtime_observed" +# hits = 8216 + +[[entry_point]] +addr = 0x02125798 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02125968 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125A3C +mode = "arm" +kind = "runtime_observed" +# hits = 23504 + +[[entry_point]] +addr = 0x02125A5C +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02125AAC +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02125B08 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02125B98 +mode = "arm" +kind = "runtime_observed" +# hits = 189 + +[[entry_point]] +addr = 0x02125BDC +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02125C54 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02125DF8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125E50 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125F30 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02125FF0 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02126094 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02126160 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02126198 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x021261D8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02126318 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021264F4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02126604 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02126674 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021267B4 +mode = "arm" +kind = "runtime_observed" +# hits = 28496 + +[[entry_point]] +addr = 0x02126874 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0212694C +mode = "arm" +kind = "runtime_observed" +# hits = 12272 + +[[entry_point]] +addr = 0x02126A10 +mode = "arm" +kind = "runtime_observed" +# hits = 12272 + +[[entry_point]] +addr = 0x02126C3C +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126D2C +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126E1C +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126EC4 +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126F14 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02126F28 +mode = "arm" +kind = "runtime_observed" +# hits = 7665 + +[[entry_point]] +addr = 0x02126F48 +mode = "arm" +kind = "runtime_observed" +# hits = 7665 + +[[entry_point]] +addr = 0x02127134 +mode = "arm" +kind = "runtime_observed" +# hits = 12087 + +[[entry_point]] +addr = 0x021271F0 +mode = "arm" +kind = "runtime_observed" +# hits = 6134 + +[[entry_point]] +addr = 0x021272A0 +mode = "arm" +kind = "runtime_observed" +# hits = 6138 + +[[entry_point]] +addr = 0x02127350 +mode = "arm" +kind = "runtime_observed" +# hits = 23502 + +[[entry_point]] +addr = 0x02127370 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021273AC +mode = "arm" +kind = "runtime_observed" +# hits = 72419 + +[[entry_point]] +addr = 0x0212CD44 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0212DD70 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131244 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02131288 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02131998 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021328DC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213438C +mode = "arm" +kind = "runtime_observed" +# hits = 96819 + +[[entry_point]] +addr = 0x021343BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021343F4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02134434 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134494 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213468C +mode = "arm" +kind = "runtime_observed" +# hits = 48408 + +[[entry_point]] +addr = 0x0213469C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021346B0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021346E0 +mode = "arm" +kind = "runtime_observed" +# hits = 48409 + +[[entry_point]] +addr = 0x02134704 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134718 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0213478C +mode = "arm" +kind = "runtime_observed" +# hits = 58520 + +[[entry_point]] +addr = 0x02134B34 +mode = "arm" +kind = "runtime_observed" +# hits = 14630 + +[[entry_point]] +addr = 0x021361F4 +mode = "arm" +kind = "runtime_observed" +# hits = 48408 + +[[entry_point]] +addr = 0x02136254 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021363D4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02136C7C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021372AC +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x0213C18C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213DF58 +mode = "arm" +kind = "runtime_observed" +# hits = 18764 + +[[entry_point]] +addr = 0x0213DFB8 +mode = "arm" +kind = "runtime_observed" +# hits = 4624 + +[[entry_point]] +addr = 0x0213E014 +mode = "arm" +kind = "runtime_observed" +# hits = 13752 + +[[entry_point]] +addr = 0x0213E06C +mode = "arm" +kind = "runtime_observed" +# hits = 13752 + +[[entry_point]] +addr = 0x0213E114 +mode = "arm" +kind = "runtime_observed" +# hits = 268 + +[[entry_point]] +addr = 0x0213E1EC +mode = "arm" +kind = "runtime_observed" +# hits = 65460 + +[[entry_point]] +addr = 0x0213E230 +mode = "arm" +kind = "runtime_observed" +# hits = 306136 + +[[entry_point]] +addr = 0x0213E24C +mode = "arm" +kind = "runtime_observed" +# hits = 15664 + +[[entry_point]] +addr = 0x0213E370 +mode = "arm" +kind = "runtime_observed" +# hits = 15664 + +[[entry_point]] +addr = 0x0213E52C +mode = "arm" +kind = "runtime_observed" +# hits = 15664 + +[[entry_point]] +addr = 0x0213E548 +mode = "arm" +kind = "runtime_observed" +# hits = 2167 + +[[entry_point]] +addr = 0x0213E58C +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x0213E6FC +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0213E768 +mode = "arm" +kind = "runtime_observed" +# hits = 7124 + +[[entry_point]] +addr = 0x0213E894 +mode = "arm" +kind = "runtime_observed" +# hits = 42701 + +[[entry_point]] +addr = 0x0213E8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x0213E8DC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213E90C +mode = "arm" +kind = "runtime_observed" +# hits = 786 + +[[entry_point]] +addr = 0x0213E91C +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x0213E960 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213E984 +mode = "arm" +kind = "runtime_observed" +# hits = 31 + +[[entry_point]] +addr = 0x0213E998 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213EA8C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213EAB0 +mode = "arm" +kind = "runtime_observed" +# hits = 93470 + +[[entry_point]] +addr = 0x0213EACC +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x0213ED80 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213ED98 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213EDB0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213EE8C +mode = "arm" +kind = "runtime_observed" +# hits = 96819 + +[[entry_point]] +addr = 0x0213EEA4 +mode = "arm" +kind = "runtime_observed" +# hits = 377127 + +[[entry_point]] +addr = 0x0213EEE0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213EF6C +mode = "arm" +kind = "runtime_observed" +# hits = 169 + +[[entry_point]] +addr = 0x0213F44C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213F7A4 +mode = "arm" +kind = "runtime_observed" +# hits = 11285 + +[[entry_point]] +addr = 0x02140240 +mode = "arm" +kind = "runtime_observed" +# hits = 12116 + +[[entry_point]] +addr = 0x0214029C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02140500 +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x021406EC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02140704 +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x021407D0 +mode = "arm" +kind = "runtime_observed" +# hits = 12124 + +[[entry_point]] +addr = 0x0214081C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02140834 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0214089C +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x021408AC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02140D0C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02140D34 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02140D98 +mode = "arm" +kind = "runtime_observed" +# hits = 776 + +[[entry_point]] +addr = 0x02140E74 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02141050 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x0214144C +mode = "arm" +kind = "runtime_observed" +# hits = 268 + +[[entry_point]] +addr = 0x021414C4 +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x021414E4 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x021414FC +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02141548 +mode = "arm" +kind = "runtime_observed" +# hits = 13875 + +[[entry_point]] +addr = 0x0214180C +mode = "arm" +kind = "runtime_observed" +# hits = 120664 + +[[entry_point]] +addr = 0x021418A0 +mode = "arm" +kind = "runtime_observed" +# hits = 241286 + +[[entry_point]] +addr = 0x02141964 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02141984 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02141A30 +mode = "arm" +kind = "runtime_observed" +# hits = 270 + +[[entry_point]] +addr = 0x02141A58 +mode = "arm" +kind = "runtime_observed" +# hits = 120664 + +[[entry_point]] +addr = 0x02141BFC +mode = "arm" +kind = "runtime_observed" +# hits = 5118 + +[[entry_point]] +addr = 0x02142258 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021423AC +mode = "arm" +kind = "runtime_observed" +# hits = 11385 + +[[entry_point]] +addr = 0x0214253C +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x0214266C +mode = "arm" +kind = "runtime_observed" +# hits = 38259 + +[[entry_point]] +addr = 0x021427A8 +mode = "arm" +kind = "runtime_observed" +# hits = 38259 + +[[entry_point]] +addr = 0x02142AAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02142BC4 +mode = "arm" +kind = "runtime_observed" +# hits = 34744 + +[[entry_point]] +addr = 0x02142BCC +mode = "arm" +kind = "runtime_observed" +# hits = 69488 + +[[entry_point]] +addr = 0x02142BE8 +mode = "arm" +kind = "runtime_observed" +# hits = 137474 diff --git a/scenarios/mp_bots_start.json b/scenarios/mp_bots_start.json new file mode 100644 index 0000000..204cb1c --- /dev/null +++ b/scenarios/mp_bots_start.json @@ -0,0 +1,177 @@ +{ + "description": "Hypothesis test: a slot showing a green check is OPEN FOR A HUMAN PEER, not ready. Convert all three non-host slots to bots (star rating) so nothing waits on a peer, then START.", + "actions": [ + { + "kind": "touch", + "x": 160, + "y": 92 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 128, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 50, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 50, + "y": 62 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 27, + "y": 132 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 600 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 222, + "y": 133 + }, + { + "kind": "wait", + "frames": 180 + }, + { + "kind": "touch", + "x": 148, + "y": 60 + }, + { + "kind": "wait", + "frames": 180 + }, + { + "kind": "touch", + "x": 222, + "y": 60 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "key", + "key": "start" + }, + { + "kind": "wait", + "frames": 600 + }, + { + "kind": "wait", + "frames": 600 + }, + { + "kind": "wait", + "frames": 600 + } + ] +} \ No newline at end of file diff --git a/scenarios/mp_singlecard_probe.json b/scenarios/mp_singlecard_probe.json new file mode 100644 index 0000000..99a44bd --- /dev/null +++ b/scenarios/mp_singlecard_probe.json @@ -0,0 +1,59 @@ +{ + "description": "Probe: SINGLE-CARD PLAY branch, to test whether it is the offline-with-bots path rather than MULTI-CARD.", + "actions": [ + { + "kind": "touch", + "x": 160, + "y": 92 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 50, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + } + ] +} \ No newline at end of file diff --git a/scenarios/mp_start_probe.json b/scenarios/mp_start_probe.json new file mode 100644 index 0000000..4828b26 --- /dev/null +++ b/scenarios/mp_start_probe.json @@ -0,0 +1,178 @@ +{ + "description": "Probe: at the 3-bot lobby, try each candidate start control in turn with a screenshot after each. Known DISPROVED already: START key, A key, touch (105,133) which is a bot-level star control.", + "actions": [ + { + "kind": "touch", + "x": 160, + "y": 92 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 128, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 50, + "y": 80 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 50, + "y": 62 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 27, + "y": 132 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "key", + "key": "a" + }, + { + "kind": "wait", + "frames": 600 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 173 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 + }, + { + "kind": "touch", + "x": 222, + "y": 133 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 148, + "y": 60 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 222, + "y": 60 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "touch", + "x": 20, + "y": 16 + }, + { + "kind": "wait", + "frames": 240 + }, + { + "kind": "key", + "key": "start" + }, + { + "kind": "wait", + "frames": 400 + } + ] +} \ No newline at end of file diff --git a/tools/seed_overlay_from_coverage.py b/tools/seed_overlay_from_coverage.py new file mode 100644 index 0000000..13d8eb5 --- /dev/null +++ b/tools/seed_overlay_from_coverage.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Seed one overlay's recompiler config with provably-that-overlay entry points. + +beads-yjp.31. tools/prepare_mph.py deliberately emits overlay configs with NO +entry points -- its own docstring says real entry points must come from Tier-3 +coverage recorded while that exact overlay body was resident. This produces +them. + +THE ATTRIBUTION PROBLEM. MPH reuses virtual addresses across overlays: all 18 +overlap at least one other by span, and overlay 0 alone shares its range with +eight. A Tier-3 coverage record is {pc, caller, cpu, thumb, kind, hits} and +carries no overlay identity, so on its own it cannot say whether a PC executed +while overlay 0 or overlay 9 was resident. docs/BRINGUP.md calls combining +entry points across overlay generations unsound, and it is right. + +THE SOLUTION, and why no runtime change is needed. The coverage manifest also +carries the 4 KiB code pages the guest actually executed, captured verbatim at +execution time. Comparing a captured page against each overlay's decompressed +ROM image at the corresponding offset identifies which overlay was resident +when that page ran. Measured on a real capture: of 65 captured ARM9 pages, 33 +fell inside some overlay and every one attributed to exactly ONE overlay -- +28 to overlay 0, 2 to overlay 1, 3 to overlay 4, with zero ambiguity. The +bytes are distinct enough that overlapping spans do not produce collisions. + +Only entry points sitting inside a page that byte-matched THIS overlay are +emitted. Everything else is excluded and counted, never guessed at. +""" + +from __future__ import annotations + +import argparse +import base64 +import json +from pathlib import Path + +PAGE = 4096 +# Roots are scheduler slice-resume points and usually land mid-function, which +# makes them poor discovery seeds. Calls and indirect targets are real entries. +GOOD_KINDS = {"call", "indirect"} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--overlay-id", type=int, required=True) + parser.add_argument("--overlays-dir", type=Path, + default=Path("generated/inputs/overlays")) + parser.add_argument("--overlays-json", type=Path, + default=Path("generated/inputs/overlays.json")) + parser.add_argument("--manifest", type=Path, nargs="+", required=True) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--include-roots", action="store_true", + help="also seed kind=root PCs (usually fragments code)") + args = parser.parse_args() + + meta = {int(o["id"]): o + for o in json.loads(args.overlays_json.read_text(encoding="utf-8"))} + if args.overlay_id not in meta: + raise SystemExit(f"no overlay {args.overlay_id} in {args.overlays_json}") + entry = meta[args.overlay_id] + base = int(entry["load_address"], 16) + image = (args.overlays_dir / entry["file"]).read_bytes() + if len(image) != int(entry["size"]): + raise SystemExit(f"{entry['file']} is {len(image)} bytes, " + f"overlays.json says {entry['size']}") + lo, hi = base, base + len(image) + print(f"overlay {args.overlay_id}: 0x{lo:08X}-0x{hi:08X} " + f"({len(image)} bytes) sha1 {entry['sha1']}") + + # Which captured pages prove this overlay was resident? + resident: set[int] = set() + foreign = 0 + points: list[dict] = [] + for path in args.manifest: + data = json.loads(path.read_text(encoding="utf-8")) + if data.get("kind") != "ndsrecomp-tier3-coverage": + raise SystemExit(f"{path} is not a coverage manifest") + for page in data.get("pages", {}).get("entries", []): + if int(page["cpu"]) != 9: + continue + addr = int(page["addr"], 16) + if not (lo <= addr < hi): + continue + raw = base64.b64decode(page["data"]) + off = addr - lo + if image[off:off + len(raw)] == raw: + resident.add(addr) + else: + foreign += 1 + points += data.get("entry_points_arm9", []) + + print(f"captured pages inside this overlay that MATCH its image: " + f"{len(resident)}") + print(f"captured pages inside its span from a DIFFERENT generation: " + f"{foreign} (excluded)") + if not resident: + raise SystemExit("no page proved this overlay resident; nothing to seed") + + kinds_ok = GOOD_KINDS | ({"root"} if args.include_roots else set()) + seeds: dict[tuple[int, str], int] = {} + dropped_kind = dropped_unproven = 0 + for point in points: + addr = int(point["addr"], 16) + if not (lo <= addr < hi): + continue + if (addr & ~(PAGE - 1)) not in resident: + dropped_unproven += 1 + continue + if point.get("kind") not in kinds_ok: + dropped_kind += 1 + continue + key = (addr, "thumb" if point.get("mode") == "thumb" else "arm") + seeds[key] = seeds.get(key, 0) + int(point.get("hits", 0)) + + print(f"entry points in span, dropped as unproven generation: " + f"{dropped_unproven}") + print(f"entry points dropped by kind filter : {dropped_kind}") + print(f"SEEDS EMITTED : {len(seeds)}") + if not seeds: + raise SystemExit("no seeds survived; capture a route that runs this overlay") + + ordered = sorted(seeds.items()) + lines = [ + "# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit.", + "# Entry points are Tier-3 observations whose containing 4 KiB page was", + "# byte-identical to this overlay's decompressed ROM image at the time it", + "# executed, so every seed below is provably from THIS overlay generation", + "# and not from one of the overlays sharing its address range.", + "", + "[program]", + f'name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay {args.overlay_id}"', + f'id = "mph_amhe0_arm9_ov{args.overlay_id:03d}"', + f"load_address = 0x{lo:08X}", + f"size = 0x{len(image):08X}", + f"entry_pc = 0x{ordered[0][0][0]:08X}", + "authoritative_entry_points = false", + "", + "[identity]", + f'sha1 = "{entry["sha1"]}"', + "", + ] + for (addr, mode), hits in ordered: + lines += [ + "[[entry_point]]", + f"addr = 0x{addr:08X}", + f'mode = "{mode}"', + 'kind = "runtime_observed"', + f"# hits = {hits}", + "", + ] + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text("\n".join(lines), encoding="utf-8", newline="\n") + print(f"wrote {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 4c651ab928ddb4476b80b554aee6240452156640 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 21:31:30 -0700 Subject: [PATCH 137/164] config: seed overlays 0 and 1; measure where the win actually is Seeded from five captured routes (boot, menus, multiplayer setup, adventure-with-walking). Only overlays 0 and 1 have any proven-resident pages; overlay 4 yields 2 seeds, and the other fifteen yield none because no route I can currently drive ever loads them. Overlay 0: 30 proven pages -> 222 seeds -> 2357 functions. Overlay 1: 2 proven pages -> 23 seeds -> 389 functions. Both identity-verified with zero undefined instructions. A correction to an earlier reading: the span-based overlay report over-attributes badly. It credited overlays 9 and 12 with 601 points and 152628 hits, but neither has a single proven-resident page -- those were overlay 0's bytes, counted again through overlapping spans. Only the page-proof attribution is trustworthy. MEASURED, and the headline is smaller than the instruction counts suggest. Boot-to-menu (run to vblank9 12000, serve mode, same route both sides): wall clock 100.2s -> 97.2s -3.0% tier3_insns9 18,106,104 -> 10,621,633 -41% tier3_insns7 127,071,515 -> 127,071,515 0% On the longer menu route, tier3_insns9 falls 112.1M -> 48.5M (-56.7%) but wall clock does not move, because that route's duration is set by fixed frame advances and harness round-trips rather than by emulation speed. ARM7 IS NOW THE BOTTLENECK: 127M interpreted ARM7 instructions against 10.6M ARM9, i.e. 92% of all remaining interpreter work. More ARM9 overlays cannot pay much more than this -- overlay 1 on top of overlay 0 was worth 0.09%. Two theories checked and DISPROVED, do not re-try: dispatch-cache thrashing from the FMV bank and the overlay bank aliasing the same addresses (cache_slow_lookup went DOWN, 1,047,293 -> 802,213), and a provenance-granularity mismatch in the validation gate (the banks plainly do validate and dispatch). beads-yjp.31 Co-Authored-By: Claude Opus 5 (1M context) --- config/mph_arm9_ov000.toml | 172 +++++++++++++++++++++++++------------ config/mph_arm9_ov001.toml | 154 +++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 config/mph_arm9_ov001.toml diff --git a/config/mph_arm9_ov000.toml b/config/mph_arm9_ov000.toml index a15fd76..458e1b2 100644 --- a/config/mph_arm9_ov000.toml +++ b/config/mph_arm9_ov000.toml @@ -19,55 +19,61 @@ sha1 = "5983749ec5c2982e54a10e5f1107ef92b86342b0" addr = 0x02103010 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 16 [[entry_point]] addr = 0x02103088 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 16 + +[[entry_point]] +addr = 0x02103154 +mode = "arm" +kind = "runtime_observed" +# hits = 11 [[entry_point]] addr = 0x02103184 mode = "arm" kind = "runtime_observed" -# hits = 6715 +# hits = 16277 [[entry_point]] addr = 0x02103208 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 3 [[entry_point]] addr = 0x021034E8 mode = "arm" kind = "runtime_observed" -# hits = 5 +# hits = 15 [[entry_point]] addr = 0x021034F4 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 12 [[entry_point]] addr = 0x02103510 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 9 [[entry_point]] addr = 0x021035CC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 9 [[entry_point]] addr = 0x021035F8 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02103740 @@ -79,7 +85,7 @@ kind = "runtime_observed" addr = 0x021037AC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02103834 @@ -91,7 +97,7 @@ kind = "runtime_observed" addr = 0x02103838 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 9 [[entry_point]] addr = 0x0210384C @@ -103,31 +109,31 @@ kind = "runtime_observed" addr = 0x02103868 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02103870 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02103974 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02103DD0 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02103FFC mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 6 [[entry_point]] addr = 0x02106B5C @@ -157,13 +163,13 @@ kind = "runtime_observed" addr = 0x02106DAC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02106E8C mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02112E80 @@ -193,13 +199,13 @@ kind = "runtime_observed" addr = 0x02114228 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02114AF4 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 16 [[entry_point]] addr = 0x02115208 @@ -235,48 +241,54 @@ kind = "runtime_observed" addr = 0x02115A5C mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02115A60 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02115A64 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02115A68 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02115A6C mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02115A70 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02115A90 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 3 [[entry_point]] addr = 0x02115AA4 mode = "arm" kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02115ABC +mode = "arm" +kind = "runtime_observed" # hits = 1 [[entry_point]] @@ -291,11 +303,47 @@ mode = "arm" kind = "runtime_observed" # hits = 1 +[[entry_point]] +addr = 0x02120D14 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02120D18 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02120EF0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02121080 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02121160 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0212119C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + [[entry_point]] addr = 0x02123980 mode = "arm" kind = "runtime_observed" -# hits = 12272 +# hits = 28294 [[entry_point]] addr = 0x02123B2C @@ -325,7 +373,7 @@ kind = "runtime_observed" addr = 0x02123E14 mode = "arm" kind = "runtime_observed" -# hits = 403 +# hits = 705 [[entry_point]] addr = 0x02123F30 @@ -403,7 +451,7 @@ kind = "runtime_observed" addr = 0x02124DDC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02124FE8 @@ -421,7 +469,7 @@ kind = "runtime_observed" addr = 0x0212511C mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02125174 @@ -493,7 +541,7 @@ kind = "runtime_observed" addr = 0x02125798 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02125968 @@ -505,7 +553,7 @@ kind = "runtime_observed" addr = 0x02125A3C mode = "arm" kind = "runtime_observed" -# hits = 23504 +# hits = 23506 [[entry_point]] addr = 0x02125A5C @@ -541,7 +589,7 @@ kind = "runtime_observed" addr = 0x02125C54 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 27809 [[entry_point]] addr = 0x02125DF8 @@ -625,7 +673,7 @@ kind = "runtime_observed" addr = 0x02126874 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0212694C @@ -709,7 +757,7 @@ kind = "runtime_observed" addr = 0x02127370 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x021273AC @@ -721,37 +769,43 @@ kind = "runtime_observed" addr = 0x0212CD44 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 12 [[entry_point]] addr = 0x0212DD70 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x02131244 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 3 [[entry_point]] addr = 0x02131288 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 12 [[entry_point]] addr = 0x02131998 mode = "arm" kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131A70 +mode = "arm" +kind = "runtime_observed" # hits = 1 [[entry_point]] addr = 0x021328DC mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 4 [[entry_point]] addr = 0x0213438C @@ -783,6 +837,12 @@ mode = "arm" kind = "runtime_observed" # hits = 1 +[[entry_point]] +addr = 0x021344E0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + [[entry_point]] addr = 0x0213468C mode = "arm" @@ -805,7 +865,7 @@ kind = "runtime_observed" addr = 0x021346E0 mode = "arm" kind = "runtime_observed" -# hits = 48409 +# hits = 48425 [[entry_point]] addr = 0x02134704 @@ -823,13 +883,13 @@ kind = "runtime_observed" addr = 0x0213478C mode = "arm" kind = "runtime_observed" -# hits = 58520 +# hits = 58524 [[entry_point]] addr = 0x02134B34 mode = "arm" kind = "runtime_observed" -# hits = 14630 +# hits = 14646 [[entry_point]] addr = 0x021361F4 @@ -841,19 +901,19 @@ kind = "runtime_observed" addr = 0x02136254 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x021363D4 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x02136C7C mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x021372AC @@ -865,7 +925,7 @@ kind = "runtime_observed" addr = 0x0213C18C mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0213DF58 @@ -943,7 +1003,7 @@ kind = "runtime_observed" addr = 0x0213E6FC mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 12 [[entry_point]] addr = 0x0213E768 @@ -979,7 +1039,7 @@ kind = "runtime_observed" addr = 0x0213E91C mode = "arm" kind = "runtime_observed" -# hits = 12117 +# hits = 12134 [[entry_point]] addr = 0x0213E960 @@ -997,7 +1057,7 @@ kind = "runtime_observed" addr = 0x0213E998 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0213EA8C @@ -1015,25 +1075,25 @@ kind = "runtime_observed" addr = 0x0213EACC mode = "arm" kind = "runtime_observed" -# hits = 10 +# hits = 14 [[entry_point]] addr = 0x0213ED80 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0213ED98 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0213EDB0 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0213EE8C @@ -1051,7 +1111,7 @@ kind = "runtime_observed" addr = 0x0213EEE0 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 8 [[entry_point]] addr = 0x0213EF6C diff --git a/config/mph_arm9_ov001.toml b/config/mph_arm9_ov001.toml new file mode 100644 index 0000000..ffd087c --- /dev/null +++ b/config/mph_arm9_ov001.toml @@ -0,0 +1,154 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 1" +id = "mph_amhe0_arm9_ov001" +load_address = 0x02102220 +size = 0x00003100 +entry_pc = 0x02103010 +authoritative_entry_points = false + +[identity] +sha1 = "2f00682eea725221318d8f5ddff6541f10e5cba4" + +[[entry_point]] +addr = 0x02103010 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x02103088 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x02103154 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02103184 +mode = "arm" +kind = "runtime_observed" +# hits = 16277 + +[[entry_point]] +addr = 0x02103208 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021034E8 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x021034F4 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02103510 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x021035CC +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x021035F8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103740 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x021037AC +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02103834 +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02103838 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0210384C +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02103868 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02103870 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103974 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103DD0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103FFC +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02104BB0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02104BF8 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02104FDC +mode = "arm" +kind = "runtime_observed" +# hits = 6 From 638f6b32b09d6a8bf967320f415e93dba9ce8f79 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 22:40:25 -0700 Subject: [PATCH 138/164] config: compile nine overlays; record the real bot-match start procedure THE START PROCEDURE, from the owner playing it by hand -- this unblocks the automated in-game loop. Use MULTI-CARD, never SINGLE-CARD: single-card does not allow bots at all, its slots read AVAILABLE and accept only peer consoles, which is the most misleading part of the flow. Then CREATE GAME -> BATTLE -> confirm arena -> pick hunter -> ADD + until the roster is full -> TAP THE GREEN CHECKBOX BESIDE EACH BOT -> a START BATTLE control appears, tap it. Those checkboxes are per-slot CONFIRM controls. I had read them as a difficulty widget because confirming a bot is what reveals its star rating. And the match is started by TAPPING an on-screen control, not by the START or A key -- earlier automated attempts did correctly confirm all three bots and then pressed the START key, which does nothing. COVERAGE FROM REAL PLAY DWARFS SYNTHETIC ROUTES, and overturns an earlier call. My routes captured 71-95 code pages; one adventure session captured 1724 and a multiplayer session 4172. Seedable overlays went from 2 to 9: id seeds hits id seeds hits 0 926 7032208 8 29 64756 2 586 5718909 3 18 64692 9 447 4421576 10 21 47568 15 89 220117 4 90 34252 1 68 138232 Overlay 2 is now near the top and my synthetic routes never loaded it once; overlay 9 is multiplayer arena code. "Diminishing returns at two overlays" was an artifact of weak test routes, not reality. So was the claim that ARM7 dominates and further ARM9 work could not pay: that came from a menus-only headless route. In real play the split runs ARM9 44-55%. All nine compile identity-verified, 12151 functions total. The finder reports undefined=14 (ov000) and undefined=2 (ov002), but ZERO runtime_unimplemented_op call sites are emitted in any generated file -- those counts come from the scan pass over regions that never became functions, so no compiled function can halt on one. Checked rather than assumed, since that trap calls nds_halt. Boot-to-menu measurement is a weak proxy here (it is dominated by overlay 0): 103.0s -> 100.6s, tier3_insns9 18.1M -> 10.2M, only 4% better than the two-overlay build. Overlays 2, 9 and 15 should pay in gameplay instead, which needs a real session to measure. beads-yjp.31 Co-Authored-By: Claude Opus 5 (1M context) --- config/mph_arm9_ov000.toml | 4810 ++++++++++++++++++++++-- config/mph_arm9_ov001.toml | 306 +- config/mph_arm9_ov002.toml | 3532 +++++++++++++++++ config/mph_arm9_ov003.toml | 124 + config/mph_arm9_ov004.toml | 556 +++ config/mph_arm9_ov008.toml | 190 + config/mph_arm9_ov009.toml | 2698 +++++++++++++ config/mph_arm9_ov010.toml | 142 + config/mph_arm9_ov015.toml | 550 +++ scenarios/multiplayer_battle_bots.json | 24 +- 10 files changed, 12616 insertions(+), 316 deletions(-) create mode 100644 config/mph_arm9_ov002.toml create mode 100644 config/mph_arm9_ov003.toml create mode 100644 config/mph_arm9_ov004.toml create mode 100644 config/mph_arm9_ov008.toml create mode 100644 config/mph_arm9_ov009.toml create mode 100644 config/mph_arm9_ov010.toml create mode 100644 config/mph_arm9_ov015.toml diff --git a/config/mph_arm9_ov000.toml b/config/mph_arm9_ov000.toml index 458e1b2..ed2367c 100644 --- a/config/mph_arm9_ov000.toml +++ b/config/mph_arm9_ov000.toml @@ -19,913 +19,4975 @@ sha1 = "5983749ec5c2982e54a10e5f1107ef92b86342b0" addr = 0x02103010 mode = "arm" kind = "runtime_observed" -# hits = 16 +# hits = 28 [[entry_point]] addr = 0x02103088 mode = "arm" kind = "runtime_observed" -# hits = 16 +# hits = 28 + +[[entry_point]] +addr = 0x02103144 +mode = "arm" +kind = "runtime_observed" +# hits = 2 [[entry_point]] addr = 0x02103154 mode = "arm" kind = "runtime_observed" -# hits = 11 +# hits = 68 + +[[entry_point]] +addr = 0x02103158 +mode = "arm" +kind = "runtime_observed" +# hits = 5885 [[entry_point]] addr = 0x02103184 mode = "arm" kind = "runtime_observed" -# hits = 16277 +# hits = 16999 [[entry_point]] addr = 0x02103208 mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 2 + +[[entry_point]] +addr = 0x0210324C +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x021033DC +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x0210346C +mode = "arm" +kind = "runtime_observed" +# hits = 104 + +[[entry_point]] +addr = 0x02103494 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x021034CC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021034D0 +mode = "arm" +kind = "runtime_observed" +# hits = 7 [[entry_point]] addr = 0x021034E8 mode = "arm" kind = "runtime_observed" -# hits = 15 +# hits = 21 [[entry_point]] addr = 0x021034F4 mode = "arm" kind = "runtime_observed" -# hits = 12 +# hits = 9 [[entry_point]] addr = 0x02103510 mode = "arm" kind = "runtime_observed" -# hits = 9 +# hits = 10 [[entry_point]] -addr = 0x021035CC +addr = 0x02103550 mode = "arm" kind = "runtime_observed" -# hits = 9 +# hits = 1 [[entry_point]] -addr = 0x021035F8 +addr = 0x021035CC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 6 [[entry_point]] -addr = 0x02103740 +addr = 0x021035F8 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 5 [[entry_point]] -addr = 0x021037AC +addr = 0x02103604 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 2 [[entry_point]] -addr = 0x02103834 +addr = 0x02103640 mode = "arm" kind = "runtime_observed" -# hits = 48 +# hits = 3 [[entry_point]] -addr = 0x02103838 +addr = 0x021036E8 mode = "arm" kind = "runtime_observed" -# hits = 9 +# hits = 4 [[entry_point]] -addr = 0x0210384C +addr = 0x0210372C mode = "arm" kind = "runtime_observed" -# hits = 48 +# hits = 5 [[entry_point]] -addr = 0x02103868 +addr = 0x02103740 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 13 [[entry_point]] -addr = 0x02103870 +addr = 0x021037AC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 14 [[entry_point]] -addr = 0x02103974 +addr = 0x02103834 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 48 [[entry_point]] -addr = 0x02103DD0 +addr = 0x02103838 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 6 [[entry_point]] -addr = 0x02103FFC +addr = 0x0210384C mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 48 [[entry_point]] -addr = 0x02106B5C +addr = 0x02103868 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 14 [[entry_point]] -addr = 0x02106C34 +addr = 0x02103870 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 5 [[entry_point]] -addr = 0x02106CE4 +addr = 0x02103890 mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 15 [[entry_point]] -addr = 0x02106D80 +addr = 0x021038D4 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 4 [[entry_point]] -addr = 0x02106DAC +addr = 0x02103954 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 7 [[entry_point]] -addr = 0x02106E8C +addr = 0x02103974 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 3 [[entry_point]] -addr = 0x02112E80 +addr = 0x02103994 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 4 [[entry_point]] -addr = 0x02112F9C +addr = 0x021039E8 mode = "arm" kind = "runtime_observed" -# hits = 48 +# hits = 17 [[entry_point]] -addr = 0x02112FB4 +addr = 0x02103AA0 mode = "arm" kind = "runtime_observed" -# hits = 48 +# hits = 17 [[entry_point]] -addr = 0x02112FDC +addr = 0x02103B58 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 15233 [[entry_point]] -addr = 0x02114228 +addr = 0x02103C04 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 15258 [[entry_point]] -addr = 0x02114AF4 +addr = 0x02103D2C mode = "arm" kind = "runtime_observed" -# hits = 16 +# hits = 615 [[entry_point]] -addr = 0x02115208 +addr = 0x02103DD0 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 7 [[entry_point]] -addr = 0x02115260 +addr = 0x02103DF4 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 2 [[entry_point]] -addr = 0x0211527C +addr = 0x02103FFC mode = "arm" kind = "runtime_observed" -# hits = 32 +# hits = 4 [[entry_point]] -addr = 0x0211528C +addr = 0x02104094 mode = "arm" kind = "runtime_observed" -# hits = 24 +# hits = 615 [[entry_point]] -addr = 0x02115700 +addr = 0x021040DC mode = "arm" kind = "runtime_observed" -# hits = 16 +# hits = 1 [[entry_point]] -addr = 0x02115A5C +addr = 0x021040E0 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 33 [[entry_point]] -addr = 0x02115A60 +addr = 0x021040F0 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 60 [[entry_point]] -addr = 0x02115A64 +addr = 0x021040F8 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 64 [[entry_point]] -addr = 0x02115A68 +addr = 0x02104100 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 446 [[entry_point]] -addr = 0x02115A6C +addr = 0x02104354 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 2 [[entry_point]] -addr = 0x02115A70 +addr = 0x021043EC mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 12 [[entry_point]] -addr = 0x02115A90 +addr = 0x0210463C mode = "arm" kind = "runtime_observed" # hits = 3 [[entry_point]] -addr = 0x02115AA4 +addr = 0x02104738 mode = "arm" kind = "runtime_observed" # hits = 3 [[entry_point]] -addr = 0x02115ABC +addr = 0x02104764 mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x021168A8 +addr = 0x021047CC mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 3 [[entry_point]] -addr = 0x021204D4 +addr = 0x021048A4 mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02120D14 +addr = 0x0210491C mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02120D18 +addr = 0x02104928 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 14749 [[entry_point]] -addr = 0x02120EF0 +addr = 0x021049CC mode = "arm" kind = "runtime_observed" # hits = 4 [[entry_point]] -addr = 0x02121080 +addr = 0x021049FC mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 447 [[entry_point]] -addr = 0x02121160 +addr = 0x02104AB4 mode = "arm" kind = "runtime_observed" # hits = 3 [[entry_point]] -addr = 0x0212119C +addr = 0x02104B3C mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 44 [[entry_point]] -addr = 0x02123980 +addr = 0x02104BB0 mode = "arm" kind = "runtime_observed" -# hits = 28294 +# hits = 4 [[entry_point]] -addr = 0x02123B2C +addr = 0x02104BF8 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 4 [[entry_point]] -addr = 0x02123C00 +addr = 0x02104CAC mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02123C54 +addr = 0x02104CFC mode = "arm" kind = "runtime_observed" -# hits = 60 +# hits = 648 [[entry_point]] -addr = 0x02123C78 +addr = 0x02104FDC mode = "arm" kind = "runtime_observed" -# hits = 7 +# hits = 15 [[entry_point]] -addr = 0x02123E14 +addr = 0x02104FE0 mode = "arm" kind = "runtime_observed" -# hits = 705 +# hits = 2 [[entry_point]] -addr = 0x02123F30 +addr = 0x021050C8 mode = "arm" kind = "runtime_observed" -# hits = 141 +# hits = 648 [[entry_point]] -addr = 0x02123F34 +addr = 0x02105198 mode = "arm" kind = "runtime_observed" -# hits = 788 +# hits = 15 [[entry_point]] -addr = 0x02123F58 +addr = 0x02105420 mode = "arm" kind = "runtime_observed" -# hits = 199 +# hits = 1 [[entry_point]] -addr = 0x02124040 +addr = 0x02105444 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 35876 [[entry_point]] -addr = 0x02124280 +addr = 0x02105468 mode = "arm" kind = "runtime_observed" -# hits = 61 +# hits = 9 [[entry_point]] -addr = 0x02124284 +addr = 0x02105508 mode = "arm" kind = "runtime_observed" -# hits = 102 +# hits = 1 [[entry_point]] -addr = 0x021242B0 +addr = 0x021055E8 mode = "arm" kind = "runtime_observed" -# hits = 35 +# hits = 35812 [[entry_point]] -addr = 0x02124304 +addr = 0x021056C4 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 451 [[entry_point]] -addr = 0x021243D0 +addr = 0x021056E0 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 451 [[entry_point]] -addr = 0x021243E8 +addr = 0x02105790 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 447 [[entry_point]] -addr = 0x02124AF8 +addr = 0x021057D0 mode = "arm" kind = "runtime_observed" -# hits = 15 +# hits = 14 [[entry_point]] -addr = 0x02124B18 +addr = 0x02105A64 mode = "arm" kind = "runtime_observed" -# hits = 15 +# hits = 431 [[entry_point]] -addr = 0x02124DDC +addr = 0x02105A9C mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 1 [[entry_point]] -addr = 0x02124FE8 +addr = 0x02105BFC mode = "arm" kind = "runtime_observed" -# hits = 15 +# hits = 35772 [[entry_point]] -addr = 0x02125018 +addr = 0x02105CD4 mode = "arm" kind = "runtime_observed" -# hits = 15 +# hits = 430 [[entry_point]] -addr = 0x0212511C +addr = 0x02105CF0 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 4 [[entry_point]] -addr = 0x02125174 +addr = 0x02105D68 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 431 [[entry_point]] -addr = 0x02125294 +addr = 0x02105D7C mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02125350 +addr = 0x02105F2C mode = "arm" kind = "runtime_observed" -# hits = 11696 +# hits = 431 [[entry_point]] -addr = 0x0212545C +addr = 0x02105FB8 mode = "arm" kind = "runtime_observed" -# hits = 6049 +# hits = 430 [[entry_point]] -addr = 0x0212549C +addr = 0x02106028 mode = "arm" kind = "runtime_observed" # hits = 2 [[entry_point]] -addr = 0x021254DC +addr = 0x02106098 mode = "arm" kind = "runtime_observed" -# hits = 6049 +# hits = 2 [[entry_point]] -addr = 0x021254E8 +addr = 0x02106108 mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02125548 +addr = 0x02106204 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 50 [[entry_point]] -addr = 0x02125554 +addr = 0x02106248 mode = "arm" kind = "runtime_observed" -# hits = 6049 +# hits = 447 [[entry_point]] -addr = 0x021255AC +addr = 0x021062A8 mode = "arm" kind = "runtime_observed" -# hits = 12098 +# hits = 8 [[entry_point]] -addr = 0x0212574C +addr = 0x02106444 mode = "arm" kind = "runtime_observed" -# hits = 8216 +# hits = 447 [[entry_point]] -addr = 0x02125798 +addr = 0x02106460 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 4 [[entry_point]] -addr = 0x02125968 +addr = 0x021064BC mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02125A3C +addr = 0x021064DC +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x02106508 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02106538 +mode = "arm" +kind = "runtime_observed" +# hits = 34 + +[[entry_point]] +addr = 0x0210661C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210664C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021066A4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021066B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02106788 +mode = "arm" +kind = "runtime_observed" +# hits = 45275 + +[[entry_point]] +addr = 0x02106790 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02106824 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210690C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02106A3C +mode = "arm" +kind = "runtime_observed" +# hits = 878 + +[[entry_point]] +addr = 0x02106AB8 +mode = "arm" +kind = "runtime_observed" +# hits = 58 + +[[entry_point]] +addr = 0x02106B5C +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02106BB4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02106C34 +mode = "arm" +kind = "runtime_observed" +# hits = 4584 + +[[entry_point]] +addr = 0x02106CE4 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02106D80 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x02106DAC +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x02106E8C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02106F04 +mode = "arm" +kind = "runtime_observed" +# hits = 72458 + +[[entry_point]] +addr = 0x02106F9C +mode = "arm" +kind = "runtime_observed" +# hits = 69566 + +[[entry_point]] +addr = 0x02106FE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021071D0 +mode = "arm" +kind = "runtime_observed" +# hits = 23 + +[[entry_point]] +addr = 0x021071D4 +mode = "arm" +kind = "runtime_observed" +# hits = 23 + +[[entry_point]] +addr = 0x021072E0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021074A4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021075A0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021075D8 +mode = "arm" +kind = "runtime_observed" +# hits = 81 + +[[entry_point]] +addr = 0x021076E0 +mode = "arm" +kind = "runtime_observed" +# hits = 51230 + +[[entry_point]] +addr = 0x02107730 +mode = "arm" +kind = "runtime_observed" +# hits = 54674 + +[[entry_point]] +addr = 0x02107808 +mode = "arm" +kind = "runtime_observed" +# hits = 51586 + +[[entry_point]] +addr = 0x02107894 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02107B54 +mode = "arm" +kind = "runtime_observed" +# hits = 89 + +[[entry_point]] +addr = 0x02107D80 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02107F20 +mode = "arm" +kind = "runtime_observed" +# hits = 38 + +[[entry_point]] +addr = 0x0211204C +mode = "arm" +kind = "runtime_observed" +# hits = 685 + +[[entry_point]] +addr = 0x021120B4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021120D4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021121D4 +mode = "arm" +kind = "runtime_observed" +# hits = 350 + +[[entry_point]] +addr = 0x0211232C +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x02112518 +mode = "arm" +kind = "runtime_observed" +# hits = 636 + +[[entry_point]] +addr = 0x0211251C +mode = "arm" +kind = "runtime_observed" +# hits = 758 + +[[entry_point]] +addr = 0x02112520 +mode = "arm" +kind = "runtime_observed" +# hits = 70 + +[[entry_point]] +addr = 0x02112524 +mode = "arm" +kind = "runtime_observed" +# hits = 994 + +[[entry_point]] +addr = 0x02112528 +mode = "arm" +kind = "runtime_observed" +# hits = 137 + +[[entry_point]] +addr = 0x02112694 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126A8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126AC +mode = "arm" +kind = "runtime_observed" +# hits = 2648 + +[[entry_point]] +addr = 0x021126B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126B4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126B8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126BC +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126C0 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126C4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126C8 +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126CC +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126D0 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126D4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126D8 +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126DC +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126E0 +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126E8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126EC +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126F0 +mode = "arm" +kind = "runtime_observed" +# hits = 2601 + +[[entry_point]] +addr = 0x021126F4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021126FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02112704 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211273C +mode = "arm" +kind = "runtime_observed" +# hits = 50 + +[[entry_point]] +addr = 0x02112888 +mode = "arm" +kind = "runtime_observed" +# hits = 15224 + +[[entry_point]] +addr = 0x02112AD0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02112BB0 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02112E80 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02112F18 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02112F9C +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02112FB4 +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02112FDC +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02113134 +mode = "arm" +kind = "runtime_observed" +# hits = 36 + +[[entry_point]] +addr = 0x02113174 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211320C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211324C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211326C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02113298 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x021132B8 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021134B8 +mode = "arm" +kind = "runtime_observed" +# hits = 5780 + +[[entry_point]] +addr = 0x02113568 +mode = "arm" +kind = "runtime_observed" +# hits = 5302 + +[[entry_point]] +addr = 0x02113684 +mode = "arm" +kind = "runtime_observed" +# hits = 1637 + +[[entry_point]] +addr = 0x02113700 +mode = "arm" +kind = "runtime_observed" +# hits = 7692 + +[[entry_point]] +addr = 0x02113744 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021137AC +mode = "arm" +kind = "runtime_observed" +# hits = 283 + +[[entry_point]] +addr = 0x02113848 +mode = "arm" +kind = "runtime_observed" +# hits = 816 + +[[entry_point]] +addr = 0x021138BC +mode = "arm" +kind = "runtime_observed" +# hits = 220 + +[[entry_point]] +addr = 0x021138F4 +mode = "arm" +kind = "runtime_observed" +# hits = 839 + +[[entry_point]] +addr = 0x02113984 +mode = "arm" +kind = "runtime_observed" +# hits = 9364 + +[[entry_point]] +addr = 0x02113B30 +mode = "arm" +kind = "runtime_observed" +# hits = 582 + +[[entry_point]] +addr = 0x02113BE8 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02113C20 +mode = "arm" +kind = "runtime_observed" +# hits = 4500 + +[[entry_point]] +addr = 0x02113C24 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02113C80 +mode = "arm" +kind = "runtime_observed" +# hits = 54528 + +[[entry_point]] +addr = 0x02113CD0 +mode = "arm" +kind = "runtime_observed" +# hits = 56533 + +[[entry_point]] +addr = 0x02113D58 +mode = "arm" +kind = "runtime_observed" +# hits = 10350 + +[[entry_point]] +addr = 0x02113DB8 +mode = "arm" +kind = "runtime_observed" +# hits = 8494 + +[[entry_point]] +addr = 0x02113DCC +mode = "arm" +kind = "runtime_observed" +# hits = 849 + +[[entry_point]] +addr = 0x02113DF4 +mode = "arm" +kind = "runtime_observed" +# hits = 9860 + +[[entry_point]] +addr = 0x02113E08 +mode = "arm" +kind = "runtime_observed" +# hits = 68469 + +[[entry_point]] +addr = 0x02113E3C +mode = "arm" +kind = "runtime_observed" +# hits = 263519 + +[[entry_point]] +addr = 0x02113F58 +mode = "arm" +kind = "runtime_observed" +# hits = 9266 + +[[entry_point]] +addr = 0x02113FA0 +mode = "arm" +kind = "runtime_observed" +# hits = 12545 + +[[entry_point]] +addr = 0x02114024 +mode = "arm" +kind = "runtime_observed" +# hits = 72391 + +[[entry_point]] +addr = 0x02114034 +mode = "arm" +kind = "runtime_observed" +# hits = 829 + +[[entry_point]] +addr = 0x02114044 +mode = "arm" +kind = "runtime_observed" +# hits = 266 + +[[entry_point]] +addr = 0x02114064 +mode = "arm" +kind = "runtime_observed" +# hits = 91531 + +[[entry_point]] +addr = 0x02114074 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x021140D4 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0211411C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02114228 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x021142E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021142F4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021142F8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021142FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211430C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02114310 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02114318 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02114320 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021144E8 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x0211452C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02114550 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02114648 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021146B4 +mode = "arm" +kind = "runtime_observed" +# hits = 69692 + +[[entry_point]] +addr = 0x02114730 +mode = "arm" +kind = "runtime_observed" +# hits = 2526 + +[[entry_point]] +addr = 0x02114780 +mode = "arm" +kind = "runtime_observed" +# hits = 17834 + +[[entry_point]] +addr = 0x021147C4 +mode = "arm" +kind = "runtime_observed" +# hits = 47714 + +[[entry_point]] +addr = 0x02114814 +mode = "arm" +kind = "runtime_observed" +# hits = 910 + +[[entry_point]] +addr = 0x02114AB8 +mode = "arm" +kind = "runtime_observed" +# hits = 12777 + +[[entry_point]] +addr = 0x02114AF4 +mode = "arm" +kind = "runtime_observed" +# hits = 27 + +[[entry_point]] +addr = 0x02114B1C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02114B2C +mode = "arm" +kind = "runtime_observed" +# hits = 9178 + +[[entry_point]] +addr = 0x02114C34 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02114F2C +mode = "arm" +kind = "runtime_observed" +# hits = 16354 + +[[entry_point]] +addr = 0x02115000 +mode = "arm" +kind = "runtime_observed" +# hits = 16904 + +[[entry_point]] +addr = 0x02115014 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021150E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2705 + +[[entry_point]] +addr = 0x0211511C +mode = "arm" +kind = "runtime_observed" +# hits = 17 + +[[entry_point]] +addr = 0x02115124 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211512C +mode = "arm" +kind = "runtime_observed" +# hits = 96 + +[[entry_point]] +addr = 0x02115168 +mode = "arm" +kind = "runtime_observed" +# hits = 2705 + +[[entry_point]] +addr = 0x0211516C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115208 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02115260 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211527C +mode = "arm" +kind = "runtime_observed" +# hits = 32 + +[[entry_point]] +addr = 0x0211528C +mode = "arm" +kind = "runtime_observed" +# hits = 24 + +[[entry_point]] +addr = 0x02115308 +mode = "arm" +kind = "runtime_observed" +# hits = 94 + +[[entry_point]] +addr = 0x021153B4 +mode = "arm" +kind = "runtime_observed" +# hits = 760 + +[[entry_point]] +addr = 0x0211540C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115490 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021154F8 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02115554 +mode = "arm" +kind = "runtime_observed" +# hits = 2764 + +[[entry_point]] +addr = 0x02115700 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x021157CC +mode = "arm" +kind = "runtime_observed" +# hits = 95841 + +[[entry_point]] +addr = 0x021157D8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021158C4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021158EC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A5C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02115A60 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115A64 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115A68 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115A6C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115A70 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02115A90 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02115A94 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02115A98 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AA0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AA4 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02115ABC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AC0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115AC4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AC8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115ACC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AE8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AF4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AFC +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02115B04 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02115B08 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02115B0C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02115B18 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02115DB0 +mode = "arm" +kind = "runtime_observed" +# hits = 16637 + +[[entry_point]] +addr = 0x02115F88 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115FB4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021161FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02116404 +mode = "arm" +kind = "runtime_observed" +# hits = 910 + +[[entry_point]] +addr = 0x02116458 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021164F4 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02116514 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02116520 +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x021165D4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021166F4 +mode = "arm" +kind = "runtime_observed" +# hits = 77 + +[[entry_point]] +addr = 0x021167A8 +mode = "arm" +kind = "runtime_observed" +# hits = 771 + +[[entry_point]] +addr = 0x02116814 +mode = "arm" +kind = "runtime_observed" +# hits = 165 + +[[entry_point]] +addr = 0x0211685C +mode = "arm" +kind = "runtime_observed" +# hits = 5488 + +[[entry_point]] +addr = 0x021168A8 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x021168C4 +mode = "arm" +kind = "runtime_observed" +# hits = 21224 + +[[entry_point]] +addr = 0x02116980 +mode = "arm" +kind = "runtime_observed" +# hits = 20127 + +[[entry_point]] +addr = 0x02116A14 +mode = "arm" +kind = "runtime_observed" +# hits = 12918 + +[[entry_point]] +addr = 0x02116A44 +mode = "arm" +kind = "runtime_observed" +# hits = 2489 + +[[entry_point]] +addr = 0x02116AB0 +mode = "arm" +kind = "runtime_observed" +# hits = 25823 + +[[entry_point]] +addr = 0x02116ACC +mode = "arm" +kind = "runtime_observed" +# hits = 26423 + +[[entry_point]] +addr = 0x02116AE8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x02116BFC +mode = "arm" +kind = "runtime_observed" +# hits = 5171 + +[[entry_point]] +addr = 0x02116C18 +mode = "arm" +kind = "runtime_observed" +# hits = 5459 + +[[entry_point]] +addr = 0x02116C34 +mode = "arm" +kind = "runtime_observed" +# hits = 853 + +[[entry_point]] +addr = 0x02116C84 +mode = "arm" +kind = "runtime_observed" +# hits = 1703 + +[[entry_point]] +addr = 0x02116CA0 +mode = "arm" +kind = "runtime_observed" +# hits = 1895 + +[[entry_point]] +addr = 0x02116CBC +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02116D38 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02116DD4 +mode = "arm" +kind = "runtime_observed" +# hits = 80220 + +[[entry_point]] +addr = 0x02116E10 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02116E50 +mode = "arm" +kind = "runtime_observed" +# hits = 111636 + +[[entry_point]] +addr = 0x0211B004 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0211B1F4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211B270 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211B28C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211B8E4 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0211B8F4 +mode = "arm" +kind = "runtime_observed" +# hits = 245 + +[[entry_point]] +addr = 0x0211B904 +mode = "arm" +kind = "runtime_observed" +# hits = 284 + +[[entry_point]] +addr = 0x0211BAC4 +mode = "arm" +kind = "runtime_observed" +# hits = 208 + +[[entry_point]] +addr = 0x0211BCC0 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211BCFC +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211BD84 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211BDB4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211BF60 +mode = "arm" +kind = "runtime_observed" +# hits = 2614 + +[[entry_point]] +addr = 0x0211BFE4 +mode = "arm" +kind = "runtime_observed" +# hits = 1432 + +[[entry_point]] +addr = 0x0211C070 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C08C +mode = "arm" +kind = "runtime_observed" +# hits = 403 + +[[entry_point]] +addr = 0x0211C120 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C12C +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x0211C13C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C148 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0211C154 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C188 +mode = "arm" +kind = "runtime_observed" +# hits = 13 + +[[entry_point]] +addr = 0x0211C18C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211C1A0 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C1B0 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211C1BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211C1C8 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211C22C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C27C +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C318 +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x0211C388 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C4DC +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C518 +mode = "arm" +kind = "runtime_observed" +# hits = 77585 + +[[entry_point]] +addr = 0x0211C59C +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x0211C5B0 +mode = "arm" +kind = "runtime_observed" +# hits = 1432 + +[[entry_point]] +addr = 0x0211C6B4 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x0211C764 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211C7C4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211C830 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0211C8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0211C8E0 +mode = "arm" +kind = "runtime_observed" +# hits = 1432 + +[[entry_point]] +addr = 0x0211CA18 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0211CA34 +mode = "arm" +kind = "runtime_observed" +# hits = 110 + +[[entry_point]] +addr = 0x0211CA50 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x0211CA60 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211CAB4 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211CB80 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211CC04 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211CC6C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211CE64 +mode = "arm" +kind = "runtime_observed" +# hits = 19820 + +[[entry_point]] +addr = 0x0211D244 +mode = "arm" +kind = "runtime_observed" +# hits = 151254 + +[[entry_point]] +addr = 0x0211D4B4 +mode = "arm" +kind = "runtime_observed" +# hits = 16178 + +[[entry_point]] +addr = 0x0211D59C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211D5C4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211D690 +mode = "arm" +kind = "runtime_observed" +# hits = 109174 + +[[entry_point]] +addr = 0x0211D704 +mode = "arm" +kind = "runtime_observed" +# hits = 6837 + +[[entry_point]] +addr = 0x0211D774 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211DCD8 +mode = "arm" +kind = "runtime_observed" +# hits = 23116 + +[[entry_point]] +addr = 0x0211DD2C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211E41C +mode = "arm" +kind = "runtime_observed" +# hits = 2705 + +[[entry_point]] +addr = 0x0211E428 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211E438 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0211E444 +mode = "arm" +kind = "runtime_observed" +# hits = 2753 + +[[entry_point]] +addr = 0x0211E4A8 +mode = "arm" +kind = "runtime_observed" +# hits = 2764 + +[[entry_point]] +addr = 0x0211E4C0 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211E548 +mode = "arm" +kind = "runtime_observed" +# hits = 11354 + +[[entry_point]] +addr = 0x0211E560 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211E568 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0211E56C +mode = "arm" +kind = "runtime_observed" +# hits = 39 + +[[entry_point]] +addr = 0x0211E588 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0211E598 +mode = "arm" +kind = "runtime_observed" +# hits = 10051 + +[[entry_point]] +addr = 0x0211E5BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211E64C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211E764 +mode = "arm" +kind = "runtime_observed" +# hits = 10051 + +[[entry_point]] +addr = 0x0211E79C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211E7A8 +mode = "arm" +kind = "runtime_observed" +# hits = 30153 + +[[entry_point]] +addr = 0x0211E854 +mode = "arm" +kind = "runtime_observed" +# hits = 10051 + +[[entry_point]] +addr = 0x0211E8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211E8F8 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211E994 +mode = "arm" +kind = "runtime_observed" +# hits = 2295 + +[[entry_point]] +addr = 0x0211E9A0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211E9B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211E9C0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211EA50 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211EC48 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211EE90 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0211EF0C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211EFE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F128 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F1F8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F2A0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F2A8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211F3B8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F7B8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02120138 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021204D4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02120C00 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02120D14 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02120D18 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02120DD0 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02120E14 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02120EF0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02121080 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02121160 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0212119C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02121778 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021217C8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212182C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021219A4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021219D8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021219F8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02121C40 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02121DAC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02121E0C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02121EC4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212213C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02122160 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02122170 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02122180 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021221D8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02122454 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0212258C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x021228F8 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02123058 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x021230C8 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x021231C8 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02123754 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02123824 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02123980 +mode = "arm" +kind = "runtime_observed" +# hits = 52288 + +[[entry_point]] +addr = 0x02123B2C +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02123BBC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02123C00 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02123C54 +mode = "arm" +kind = "runtime_observed" +# hits = 164 + +[[entry_point]] +addr = 0x02123C78 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02123E14 +mode = "arm" +kind = "runtime_observed" +# hits = 791 + +[[entry_point]] +addr = 0x02123F30 +mode = "arm" +kind = "runtime_observed" +# hits = 141 + +[[entry_point]] +addr = 0x02123F34 +mode = "arm" +kind = "runtime_observed" +# hits = 788 + +[[entry_point]] +addr = 0x02123F58 +mode = "arm" +kind = "runtime_observed" +# hits = 199 + +[[entry_point]] +addr = 0x02124004 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02124040 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02124280 +mode = "arm" +kind = "runtime_observed" +# hits = 61 + +[[entry_point]] +addr = 0x02124284 +mode = "arm" +kind = "runtime_observed" +# hits = 102 + +[[entry_point]] +addr = 0x021242B0 +mode = "arm" +kind = "runtime_observed" +# hits = 35 + +[[entry_point]] +addr = 0x02124304 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021243D0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021243E8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02124AF8 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02124B18 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02124DDC +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02124FE8 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02125018 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x0212511C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02125174 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02125294 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125350 +mode = "arm" +kind = "runtime_observed" +# hits = 11696 + +[[entry_point]] +addr = 0x0212545C +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x0212549C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021254DC +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x021254E8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125548 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125554 +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x021255AC +mode = "arm" +kind = "runtime_observed" +# hits = 12098 + +[[entry_point]] +addr = 0x0212574C +mode = "arm" +kind = "runtime_observed" +# hits = 8216 + +[[entry_point]] +addr = 0x02125798 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02125968 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02125A3C +mode = "arm" +kind = "runtime_observed" +# hits = 23512 + +[[entry_point]] +addr = 0x02125A5C +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02125AAC +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02125B08 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02125B98 +mode = "arm" +kind = "runtime_observed" +# hits = 189 + +[[entry_point]] +addr = 0x02125BDC +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02125C54 +mode = "arm" +kind = "runtime_observed" +# hits = 46200 + +[[entry_point]] +addr = 0x02125DF8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125E50 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02125F30 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02125FF0 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02126094 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02126160 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02126198 +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x021261D8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02126318 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021264F4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02126604 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02126674 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021267B4 +mode = "arm" +kind = "runtime_observed" +# hits = 28496 + +[[entry_point]] +addr = 0x02126874 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0212694C +mode = "arm" +kind = "runtime_observed" +# hits = 12272 + +[[entry_point]] +addr = 0x02126A10 +mode = "arm" +kind = "runtime_observed" +# hits = 12272 + +[[entry_point]] +addr = 0x02126C3C +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126D2C +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126E1C +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126EC4 +mode = "arm" +kind = "runtime_observed" +# hits = 12174 + +[[entry_point]] +addr = 0x02126F14 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02126F28 +mode = "arm" +kind = "runtime_observed" +# hits = 7665 + +[[entry_point]] +addr = 0x02126F48 +mode = "arm" +kind = "runtime_observed" +# hits = 7665 + +[[entry_point]] +addr = 0x02127134 +mode = "arm" +kind = "runtime_observed" +# hits = 12087 + +[[entry_point]] +addr = 0x021271F0 +mode = "arm" +kind = "runtime_observed" +# hits = 6134 + +[[entry_point]] +addr = 0x021272A0 +mode = "arm" +kind = "runtime_observed" +# hits = 6138 + +[[entry_point]] +addr = 0x02127350 +mode = "arm" +kind = "runtime_observed" +# hits = 23502 + +[[entry_point]] +addr = 0x02127370 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x021273AC +mode = "arm" +kind = "runtime_observed" +# hits = 72419 + +[[entry_point]] +addr = 0x0212753C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0212AC40 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x0212AC78 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212ACB4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ACBC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0212AEE4 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x0212AF20 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212AF58 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x0212AF5C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212AF60 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0212B1B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212B3CC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212B4F0 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0212B544 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0212CD44 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0212DC54 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212DCE4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212DD70 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0212DF5C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E060 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0212E210 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E2BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E3B4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E740 +mode = "arm" +kind = "runtime_observed" +# hits = 32339 + +[[entry_point]] +addr = 0x0212E744 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0212E970 +mode = "arm" +kind = "runtime_observed" +# hits = 32307 + +[[entry_point]] +addr = 0x0212EA18 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212EBAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212EBB0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ECAC +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0212ED0C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ED48 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212EE40 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0212F760 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212FCAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212FCE0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02131244 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02131258 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02131278 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131288 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0213198C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02131998 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02131A70 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131AD4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131B74 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02131BD8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02131DE0 +mode = "arm" +kind = "runtime_observed" +# hits = 20 + +[[entry_point]] +addr = 0x021327C8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02132820 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02132878 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021328D8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021328DC +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02133060 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02133108 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021331CC +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0213326C +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02133310 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02133374 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021336D0 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021337D4 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021337EC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02133820 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02133854 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02133888 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x021338B8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021338F4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021338FC +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02133900 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02133904 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02133984 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021339D4 +mode = "arm" +kind = "runtime_observed" +# hits = 17106 + +[[entry_point]] +addr = 0x021339F4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02133A60 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02133D2C +mode = "arm" +kind = "runtime_observed" +# hits = 1404 + +[[entry_point]] +addr = 0x02133D48 +mode = "arm" +kind = "runtime_observed" +# hits = 645 + +[[entry_point]] +addr = 0x02133D58 +mode = "arm" +kind = "runtime_observed" +# hits = 220 + +[[entry_point]] +addr = 0x0213413C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02134158 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021341C4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02134324 +mode = "arm" +kind = "runtime_observed" +# hits = 93 + +[[entry_point]] +addr = 0x0213438C +mode = "arm" +kind = "runtime_observed" +# hits = 96819 + +[[entry_point]] +addr = 0x021343BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021343F4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02134434 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134494 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021344E0 +mode = "arm" +kind = "runtime_observed" +# hits = 74 + +[[entry_point]] +addr = 0x021345B4 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x0213468C +mode = "arm" +kind = "runtime_observed" +# hits = 64767 + +[[entry_point]] +addr = 0x0213469C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021346B0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021346C8 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x021346E0 +mode = "arm" +kind = "runtime_observed" +# hits = 127921 + +[[entry_point]] +addr = 0x02134704 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134718 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0213478C +mode = "arm" +kind = "runtime_observed" +# hits = 130504 + +[[entry_point]] +addr = 0x021348C0 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x021348D8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134934 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02134990 +mode = "arm" +kind = "runtime_observed" +# hits = 899 + +[[entry_point]] +addr = 0x02134A90 +mode = "arm" +kind = "runtime_observed" +# hits = 1428 + +[[entry_point]] +addr = 0x02134AB8 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02134B00 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02134B34 +mode = "arm" +kind = "runtime_observed" +# hits = 24778 + +[[entry_point]] +addr = 0x02134B44 +mode = "arm" +kind = "runtime_observed" +# hits = 54421 + +[[entry_point]] +addr = 0x02134B78 +mode = "arm" +kind = "runtime_observed" +# hits = 6524 + +[[entry_point]] +addr = 0x02134B7C +mode = "arm" +kind = "runtime_observed" +# hits = 627 + +[[entry_point]] +addr = 0x02134B80 +mode = "arm" +kind = "runtime_observed" +# hits = 6711 + +[[entry_point]] +addr = 0x02134B8C +mode = "arm" +kind = "runtime_observed" +# hits = 189 + +[[entry_point]] +addr = 0x02134BC0 +mode = "arm" +kind = "runtime_observed" +# hits = 272 + +[[entry_point]] +addr = 0x02134BD4 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02134BF8 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02134C50 +mode = "arm" +kind = "runtime_observed" +# hits = 10720 + +[[entry_point]] +addr = 0x02134C54 +mode = "arm" +kind = "runtime_observed" +# hits = 4527 + +[[entry_point]] +addr = 0x02134C5C +mode = "arm" +kind = "runtime_observed" +# hits = 135 + +[[entry_point]] +addr = 0x02134C64 +mode = "arm" +kind = "runtime_observed" +# hits = 1020 + +[[entry_point]] +addr = 0x02134C80 +mode = "arm" +kind = "runtime_observed" +# hits = 1649 + +[[entry_point]] +addr = 0x02134C84 +mode = "arm" +kind = "runtime_observed" +# hits = 829 + +[[entry_point]] +addr = 0x02134C9C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02134E68 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02135020 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02135108 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02135154 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x021351B4 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x021351CC +mode = "arm" +kind = "runtime_observed" +# hits = 60 + +[[entry_point]] +addr = 0x02135408 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213548C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021354E4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02135524 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02135588 +mode = "arm" +kind = "runtime_observed" +# hits = 27 + +[[entry_point]] +addr = 0x0213560C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02135898 +mode = "arm" +kind = "runtime_observed" +# hits = 19 + +[[entry_point]] +addr = 0x02135950 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x02135BCC +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02135C20 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02135C88 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02135DAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02135F20 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02136014 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02136034 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02136074 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0213613C +mode = "arm" +kind = "runtime_observed" +# hits = 756 + +[[entry_point]] +addr = 0x021361C8 +mode = "arm" +kind = "runtime_observed" +# hits = 92 + +[[entry_point]] +addr = 0x021361F4 +mode = "arm" +kind = "runtime_observed" +# hits = 51352 + +[[entry_point]] +addr = 0x02136254 +mode = "arm" +kind = "runtime_observed" +# hits = 70 + +[[entry_point]] +addr = 0x021362D8 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02136304 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02136358 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021363D4 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02136428 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213645C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021364A0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021364D4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213653C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213655C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213658C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213660C +mode = "arm" +kind = "runtime_observed" +# hits = 4302 + +[[entry_point]] +addr = 0x02136668 +mode = "arm" +kind = "runtime_observed" +# hits = 8160 + +[[entry_point]] +addr = 0x02136748 +mode = "arm" +kind = "runtime_observed" +# hits = 790 + +[[entry_point]] +addr = 0x021367AC +mode = "arm" +kind = "runtime_observed" +# hits = 1044 + +[[entry_point]] +addr = 0x0213688C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213693C +mode = "arm" +kind = "runtime_observed" +# hits = 10720 + +[[entry_point]] +addr = 0x0213696C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02136A34 +mode = "arm" +kind = "runtime_observed" +# hits = 2753 + +[[entry_point]] +addr = 0x02136A80 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x02136AC0 +mode = "arm" +kind = "runtime_observed" +# hits = 2753 + +[[entry_point]] +addr = 0x02136B10 +mode = "arm" +kind = "runtime_observed" +# hits = 84350 + +[[entry_point]] +addr = 0x02136B20 +mode = "arm" +kind = "runtime_observed" +# hits = 23792 + +[[entry_point]] +addr = 0x02136B24 +mode = "arm" +kind = "runtime_observed" +# hits = 7730 + +[[entry_point]] +addr = 0x02136B44 +mode = "arm" +kind = "runtime_observed" +# hits = 9064 + +[[entry_point]] +addr = 0x02136B50 +mode = "arm" +kind = "runtime_observed" +# hits = 1404 + +[[entry_point]] +addr = 0x02136B54 +mode = "arm" +kind = "runtime_observed" +# hits = 26214 + +[[entry_point]] +addr = 0x02136B58 +mode = "arm" +kind = "runtime_observed" +# hits = 359 + +[[entry_point]] +addr = 0x02136B5C +mode = "arm" +kind = "runtime_observed" +# hits = 9627 + +[[entry_point]] +addr = 0x02136B70 +mode = "arm" +kind = "runtime_observed" +# hits = 8913 + +[[entry_point]] +addr = 0x02136C1C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02136C3C +mode = "arm" +kind = "runtime_observed" +# hits = 135954 + +[[entry_point]] +addr = 0x02136C7C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02136CA0 +mode = "arm" +kind = "runtime_observed" +# hits = 18898 + +[[entry_point]] +addr = 0x02136EE4 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02136F74 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02136FE8 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02137084 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021371B4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02137254 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02137284 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021372AC +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x0213786C +mode = "arm" +kind = "runtime_observed" +# hits = 23598 + +[[entry_point]] +addr = 0x021378DC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213790C +mode = "arm" +kind = "runtime_observed" +# hits = 17106 + +[[entry_point]] +addr = 0x02137BE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02137DE4 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02137F6C +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021380BC +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02138168 +mode = "arm" +kind = "runtime_observed" +# hits = 604 + +[[entry_point]] +addr = 0x021383E8 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x021387FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213881C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213883C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213885C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02138868 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138874 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138880 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021389BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021389F0 +mode = "arm" +kind = "runtime_observed" +# hits = 5338 + +[[entry_point]] +addr = 0x02138AE0 +mode = "arm" +kind = "runtime_observed" +# hits = 272 + +[[entry_point]] +addr = 0x02138C8C +mode = "arm" +kind = "runtime_observed" +# hits = 5081 + +[[entry_point]] +addr = 0x02138E50 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02138E58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138E60 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138F28 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02138F38 +mode = "arm" +kind = "runtime_observed" +# hits = 31749 + +[[entry_point]] +addr = 0x02139054 +mode = "arm" +kind = "runtime_observed" +# hits = 31749 + +[[entry_point]] +addr = 0x0213911C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02139274 +mode = "arm" +kind = "runtime_observed" +# hits = 620 + +[[entry_point]] +addr = 0x021394E4 +mode = "arm" +kind = "runtime_observed" +# hits = 272 + +[[entry_point]] +addr = 0x021395E8 +mode = "arm" +kind = "runtime_observed" +# hits = 862 + +[[entry_point]] +addr = 0x0213966C +mode = "arm" +kind = "runtime_observed" +# hits = 9366 + +[[entry_point]] +addr = 0x021398B0 +mode = "arm" +kind = "runtime_observed" +# hits = 9366 + +[[entry_point]] +addr = 0x021398DC +mode = "arm" +kind = "runtime_observed" +# hits = 53277 + +[[entry_point]] +addr = 0x0213998C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021399C8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02139A30 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02139A34 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02139A38 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02139A58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02139A74 +mode = "arm" +kind = "runtime_observed" +# hits = 1524 + +[[entry_point]] +addr = 0x02139A78 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02139AA0 +mode = "arm" +kind = "runtime_observed" +# hits = 13204 + +[[entry_point]] +addr = 0x02139AD8 +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x02139D40 +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x02139D5C +mode = "arm" +kind = "runtime_observed" +# hits = 3684 + +[[entry_point]] +addr = 0x02139D8C +mode = "arm" +kind = "runtime_observed" +# hits = 4768 + +[[entry_point]] +addr = 0x0213A1E0 +mode = "arm" +kind = "runtime_observed" +# hits = 3200 + +[[entry_point]] +addr = 0x0213A204 +mode = "arm" +kind = "runtime_observed" +# hits = 4542 + +[[entry_point]] +addr = 0x0213A2E8 +mode = "arm" +kind = "runtime_observed" +# hits = 57 + +[[entry_point]] +addr = 0x0213A318 +mode = "arm" +kind = "runtime_observed" +# hits = 3012 + +[[entry_point]] +addr = 0x0213A348 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0213A378 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x0213A390 +mode = "arm" +kind = "runtime_observed" +# hits = 3206 + +[[entry_point]] +addr = 0x0213A3A8 +mode = "arm" +kind = "runtime_observed" +# hits = 26 + +[[entry_point]] +addr = 0x0213A480 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0213A4B0 +mode = "arm" +kind = "runtime_observed" +# hits = 3240 + +[[entry_point]] +addr = 0x0213A4C8 +mode = "arm" +kind = "runtime_observed" +# hits = 9682 + +[[entry_point]] +addr = 0x0213A4F8 +mode = "arm" +kind = "runtime_observed" +# hits = 56 + +[[entry_point]] +addr = 0x0213A510 +mode = "arm" +kind = "runtime_observed" +# hits = 3240 + +[[entry_point]] +addr = 0x0213A528 +mode = "arm" +kind = "runtime_observed" +# hits = 10047 + +[[entry_point]] +addr = 0x0213A540 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0213A558 +mode = "arm" +kind = "runtime_observed" +# hits = 46 + +[[entry_point]] +addr = 0x0213A570 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0213A588 +mode = "arm" +kind = "runtime_observed" +# hits = 46 + +[[entry_point]] +addr = 0x0213A5B8 mode = "arm" kind = "runtime_observed" -# hits = 23506 +# hits = 15 [[entry_point]] -addr = 0x02125A5C +addr = 0x0213A5E8 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 52 [[entry_point]] -addr = 0x02125AAC +addr = 0x0213A618 mode = "arm" kind = "runtime_observed" -# hits = 5 +# hits = 4376 [[entry_point]] -addr = 0x02125B08 +addr = 0x0213AE98 mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 2 [[entry_point]] -addr = 0x02125B98 +addr = 0x0213AFB4 mode = "arm" kind = "runtime_observed" -# hits = 189 +# hits = 1 [[entry_point]] -addr = 0x02125BDC +addr = 0x0213AFB8 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 1 [[entry_point]] -addr = 0x02125C54 +addr = 0x0213AFBC mode = "arm" kind = "runtime_observed" -# hits = 27809 +# hits = 1 [[entry_point]] -addr = 0x02125DF8 +addr = 0x0213AFD0 mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02125E50 +addr = 0x0213AFD4 mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x02125F30 +addr = 0x0213B064 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 1 [[entry_point]] -addr = 0x02125FF0 +addr = 0x0213B104 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 730 [[entry_point]] -addr = 0x02126094 +addr = 0x0213B128 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 4896 [[entry_point]] -addr = 0x02126160 +addr = 0x0213B318 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 447 [[entry_point]] -addr = 0x02126198 +addr = 0x0213B340 mode = "arm" kind = "runtime_observed" -# hits = 3562 +# hits = 134 [[entry_point]] -addr = 0x021261D8 +addr = 0x0213B344 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 271 [[entry_point]] -addr = 0x02126318 +addr = 0x0213B354 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 42 [[entry_point]] -addr = 0x021264F4 +addr = 0x0213B43C mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 10936 [[entry_point]] -addr = 0x02126604 +addr = 0x0213B510 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 5162 [[entry_point]] -addr = 0x02126674 +addr = 0x0213B538 mode = "arm" kind = "runtime_observed" # hits = 1 [[entry_point]] -addr = 0x021267B4 +addr = 0x0213B53C mode = "arm" kind = "runtime_observed" -# hits = 28496 +# hits = 2038 [[entry_point]] -addr = 0x02126874 +addr = 0x0213B54C mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 3123 [[entry_point]] -addr = 0x0212694C +addr = 0x0213B750 mode = "arm" kind = "runtime_observed" -# hits = 12272 +# hits = 5176 [[entry_point]] -addr = 0x02126A10 +addr = 0x0213B774 mode = "arm" kind = "runtime_observed" -# hits = 12272 +# hits = 5793 [[entry_point]] -addr = 0x02126C3C +addr = 0x0213B78C mode = "arm" kind = "runtime_observed" -# hits = 12174 +# hits = 3 [[entry_point]] -addr = 0x02126D2C +addr = 0x0213B790 mode = "arm" kind = "runtime_observed" -# hits = 12174 +# hits = 2361 [[entry_point]] -addr = 0x02126E1C +addr = 0x0213B7A0 mode = "arm" kind = "runtime_observed" -# hits = 12174 +# hits = 3429 [[entry_point]] -addr = 0x02126EC4 +addr = 0x0213B96C mode = "arm" kind = "runtime_observed" -# hits = 12174 +# hits = 11251 [[entry_point]] -addr = 0x02126F14 +addr = 0x0213BCCC mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 3418 [[entry_point]] -addr = 0x02126F28 +addr = 0x0213BE04 mode = "arm" kind = "runtime_observed" -# hits = 7665 +# hits = 1398 [[entry_point]] -addr = 0x02126F48 +addr = 0x0213BE6C mode = "arm" kind = "runtime_observed" -# hits = 7665 +# hits = 3425 [[entry_point]] -addr = 0x02127134 +addr = 0x0213BEB0 mode = "arm" kind = "runtime_observed" -# hits = 12087 +# hits = 11383 [[entry_point]] -addr = 0x021271F0 +addr = 0x0213BEC4 mode = "arm" kind = "runtime_observed" -# hits = 6134 +# hits = 1327 [[entry_point]] -addr = 0x021272A0 +addr = 0x0213BEE8 mode = "arm" kind = "runtime_observed" -# hits = 6138 +# hits = 4527 [[entry_point]] -addr = 0x02127350 +addr = 0x0213BFF0 mode = "arm" kind = "runtime_observed" -# hits = 23502 +# hits = 18 [[entry_point]] -addr = 0x02127370 +addr = 0x0213C024 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 10711 [[entry_point]] -addr = 0x021273AC +addr = 0x0213C178 mode = "arm" kind = "runtime_observed" -# hits = 72419 +# hits = 8 [[entry_point]] -addr = 0x0212CD44 +addr = 0x0213C18C mode = "arm" kind = "runtime_observed" -# hits = 12 +# hits = 14 [[entry_point]] -addr = 0x0212DD70 +addr = 0x0213C198 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 3200 [[entry_point]] -addr = 0x02131244 +addr = 0x0213C328 mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 5828 [[entry_point]] -addr = 0x02131288 +addr = 0x0213C34C mode = "arm" kind = "runtime_observed" -# hits = 12 +# hits = 5942 [[entry_point]] -addr = 0x02131998 +addr = 0x0213C3D4 mode = "arm" kind = "runtime_observed" -# hits = 2 +# hits = 3404 [[entry_point]] -addr = 0x02131A70 +addr = 0x0213C3E4 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 397 [[entry_point]] -addr = 0x021328DC +addr = 0x0213C400 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 4057 [[entry_point]] -addr = 0x0213438C +addr = 0x0213C41C mode = "arm" kind = "runtime_observed" -# hits = 96819 +# hits = 4057 [[entry_point]] -addr = 0x021343BC +addr = 0x0213C468 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 22513 [[entry_point]] -addr = 0x021343F4 +addr = 0x0213C484 mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 22704 [[entry_point]] -addr = 0x02134434 +addr = 0x0213C504 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 2826 [[entry_point]] -addr = 0x02134494 +addr = 0x0213C954 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 11569 [[entry_point]] -addr = 0x021344E0 +addr = 0x0213C960 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 1 [[entry_point]] -addr = 0x0213468C +addr = 0x0213C970 mode = "arm" kind = "runtime_observed" -# hits = 48408 +# hits = 18213 [[entry_point]] -addr = 0x0213469C +addr = 0x0213C980 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 8798 [[entry_point]] -addr = 0x021346B0 +addr = 0x0213C9A4 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 18960 [[entry_point]] -addr = 0x021346E0 +addr = 0x0213CA48 mode = "arm" kind = "runtime_observed" -# hits = 48425 +# hits = 2556 [[entry_point]] -addr = 0x02134704 +addr = 0x0213CA64 mode = "arm" kind = "runtime_observed" -# hits = 1 +# hits = 6400 [[entry_point]] -addr = 0x02134718 +addr = 0x0213CAAC mode = "arm" kind = "runtime_observed" -# hits = 5 +# hits = 2 [[entry_point]] -addr = 0x0213478C +addr = 0x0213CC10 mode = "arm" kind = "runtime_observed" -# hits = 58524 +# hits = 916 [[entry_point]] -addr = 0x02134B34 +addr = 0x0213CC7C mode = "arm" kind = "runtime_observed" -# hits = 14646 +# hits = 4973 [[entry_point]] -addr = 0x021361F4 +addr = 0x0213CCBC mode = "arm" kind = "runtime_observed" -# hits = 48408 +# hits = 5 [[entry_point]] -addr = 0x02136254 +addr = 0x0213CCC4 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 70382 [[entry_point]] -addr = 0x021363D4 +addr = 0x0213CCE4 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 3 [[entry_point]] -addr = 0x02136C7C +addr = 0x0213D6B4 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 43 [[entry_point]] -addr = 0x021372AC +addr = 0x0213DE84 mode = "arm" kind = "runtime_observed" -# hits = 12102 +# hits = 53485 [[entry_point]] -addr = 0x0213C18C +addr = 0x0213DE88 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 16854 [[entry_point]] addr = 0x0213DF58 @@ -969,6 +5031,12 @@ mode = "arm" kind = "runtime_observed" # hits = 306136 +[[entry_point]] +addr = 0x0213E234 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + [[entry_point]] addr = 0x0213E24C mode = "arm" @@ -997,13 +5065,19 @@ kind = "runtime_observed" addr = 0x0213E58C mode = "arm" kind = "runtime_observed" -# hits = 6049 +# hits = 18722 + +[[entry_point]] +addr = 0x0213E5A4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 [[entry_point]] addr = 0x0213E6FC mode = "arm" kind = "runtime_observed" -# hits = 12 +# hits = 24 [[entry_point]] addr = 0x0213E768 @@ -1011,6 +5085,12 @@ mode = "arm" kind = "runtime_observed" # hits = 7124 +[[entry_point]] +addr = 0x0213E7D8 +mode = "arm" +kind = "runtime_observed" +# hits = 180 + [[entry_point]] addr = 0x0213E894 mode = "arm" @@ -1039,7 +5119,7 @@ kind = "runtime_observed" addr = 0x0213E91C mode = "arm" kind = "runtime_observed" -# hits = 12134 +# hits = 12215 [[entry_point]] addr = 0x0213E960 @@ -1057,7 +5137,7 @@ kind = "runtime_observed" addr = 0x0213E998 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x0213EA8C @@ -1069,31 +5149,31 @@ kind = "runtime_observed" addr = 0x0213EAB0 mode = "arm" kind = "runtime_observed" -# hits = 93470 +# hits = 93535 [[entry_point]] addr = 0x0213EACC mode = "arm" kind = "runtime_observed" -# hits = 14 +# hits = 69 [[entry_point]] addr = 0x0213ED80 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x0213ED98 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x0213EDB0 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x0213EE8C @@ -1105,13 +5185,13 @@ kind = "runtime_observed" addr = 0x0213EEA4 mode = "arm" kind = "runtime_observed" -# hits = 377127 +# hits = 377150 [[entry_point]] addr = 0x0213EEE0 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x0213EF6C @@ -1125,12 +5205,48 @@ mode = "arm" kind = "runtime_observed" # hits = 4 +[[entry_point]] +addr = 0x0213F4D4 +mode = "arm" +kind = "runtime_observed" +# hits = 12076 + +[[entry_point]] +addr = 0x0213F4F0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + [[entry_point]] addr = 0x0213F7A4 mode = "arm" kind = "runtime_observed" # hits = 11285 +[[entry_point]] +addr = 0x0213F9C4 +mode = "arm" +kind = "runtime_observed" +# hits = 12389 + +[[entry_point]] +addr = 0x0213FE98 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213FF58 +mode = "arm" +kind = "runtime_observed" +# hits = 8841 + +[[entry_point]] +addr = 0x02140168 +mode = "arm" +kind = "runtime_observed" +# hits = 163 + [[entry_point]] addr = 0x02140240 mode = "arm" @@ -1197,6 +5313,12 @@ mode = "arm" kind = "runtime_observed" # hits = 2 +[[entry_point]] +addr = 0x02140D18 +mode = "arm" +kind = "runtime_observed" +# hits = 5862 + [[entry_point]] addr = 0x02140D34 mode = "arm" @@ -1221,6 +5343,12 @@ mode = "arm" kind = "runtime_observed" # hits = 7 +[[entry_point]] +addr = 0x02141114 +mode = "arm" +kind = "runtime_observed" +# hits = 5597 + [[entry_point]] addr = 0x0214144C mode = "arm" @@ -1249,7 +5377,7 @@ kind = "runtime_observed" addr = 0x02141548 mode = "arm" kind = "runtime_observed" -# hits = 13875 +# hits = 14055 [[entry_point]] addr = 0x0214180C @@ -1293,6 +5421,24 @@ mode = "arm" kind = "runtime_observed" # hits = 5118 +[[entry_point]] +addr = 0x02141EC0 +mode = "arm" +kind = "runtime_observed" +# hits = 1320 + +[[entry_point]] +addr = 0x02141EFC +mode = "arm" +kind = "runtime_observed" +# hits = 217 + +[[entry_point]] +addr = 0x02141F28 +mode = "arm" +kind = "runtime_observed" +# hits = 8295 + [[entry_point]] addr = 0x02142258 mode = "arm" @@ -1311,6 +5457,12 @@ mode = "arm" kind = "runtime_observed" # hits = 12117 +[[entry_point]] +addr = 0x021425F4 +mode = "arm" +kind = "runtime_observed" +# hits = 43 + [[entry_point]] addr = 0x0214266C mode = "arm" @@ -1329,6 +5481,18 @@ mode = "arm" kind = "runtime_observed" # hits = 1 +[[entry_point]] +addr = 0x02142B18 +mode = "arm" +kind = "runtime_observed" +# hits = 9323 + +[[entry_point]] +addr = 0x02142B68 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + [[entry_point]] addr = 0x02142BC4 mode = "arm" @@ -1346,3 +5510,63 @@ addr = 0x02142BE8 mode = "arm" kind = "runtime_observed" # hits = 137474 + +[[entry_point]] +addr = 0x02142C4C +mode = "arm" +kind = "runtime_observed" +# hits = 3390 + +[[entry_point]] +addr = 0x02142C68 +mode = "arm" +kind = "runtime_observed" +# hits = 954 + +[[entry_point]] +addr = 0x02142C6C +mode = "arm" +kind = "runtime_observed" +# hits = 1436 + +[[entry_point]] +addr = 0x02142C70 +mode = "arm" +kind = "runtime_observed" +# hits = 184 + +[[entry_point]] +addr = 0x02142C80 +mode = "arm" +kind = "runtime_observed" +# hits = 816 + +[[entry_point]] +addr = 0x02142E80 +mode = "arm" +kind = "runtime_observed" +# hits = 5040 + +[[entry_point]] +addr = 0x02142ED4 +mode = "arm" +kind = "runtime_observed" +# hits = 1159 + +[[entry_point]] +addr = 0x02142ED8 +mode = "arm" +kind = "runtime_observed" +# hits = 1669 + +[[entry_point]] +addr = 0x02142EDC +mode = "arm" +kind = "runtime_observed" +# hits = 1080 + +[[entry_point]] +addr = 0x02142EEC +mode = "arm" +kind = "runtime_observed" +# hits = 1132 diff --git a/config/mph_arm9_ov001.toml b/config/mph_arm9_ov001.toml index ffd087c..e0ee93f 100644 --- a/config/mph_arm9_ov001.toml +++ b/config/mph_arm9_ov001.toml @@ -19,73 +19,151 @@ sha1 = "2f00682eea725221318d8f5ddff6541f10e5cba4" addr = 0x02103010 mode = "arm" kind = "runtime_observed" -# hits = 16 +# hits = 28 [[entry_point]] addr = 0x02103088 mode = "arm" kind = "runtime_observed" -# hits = 16 +# hits = 28 + +[[entry_point]] +addr = 0x02103144 +mode = "arm" +kind = "runtime_observed" +# hits = 2 [[entry_point]] addr = 0x02103154 mode = "arm" kind = "runtime_observed" -# hits = 11 +# hits = 68 + +[[entry_point]] +addr = 0x02103158 +mode = "arm" +kind = "runtime_observed" +# hits = 5885 [[entry_point]] addr = 0x02103184 mode = "arm" kind = "runtime_observed" -# hits = 16277 +# hits = 16999 [[entry_point]] addr = 0x02103208 mode = "arm" kind = "runtime_observed" -# hits = 3 +# hits = 2 + +[[entry_point]] +addr = 0x0210324C +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x021033DC +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x0210346C +mode = "arm" +kind = "runtime_observed" +# hits = 104 + +[[entry_point]] +addr = 0x02103494 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x021034CC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021034D0 +mode = "arm" +kind = "runtime_observed" +# hits = 7 [[entry_point]] addr = 0x021034E8 mode = "arm" kind = "runtime_observed" -# hits = 15 +# hits = 21 [[entry_point]] addr = 0x021034F4 mode = "arm" kind = "runtime_observed" -# hits = 12 +# hits = 9 [[entry_point]] addr = 0x02103510 mode = "arm" kind = "runtime_observed" -# hits = 9 +# hits = 10 + +[[entry_point]] +addr = 0x02103550 +mode = "arm" +kind = "runtime_observed" +# hits = 1 [[entry_point]] addr = 0x021035CC mode = "arm" kind = "runtime_observed" -# hits = 9 +# hits = 6 [[entry_point]] addr = 0x021035F8 mode = "arm" kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02103604 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103640 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021036E8 +mode = "arm" +kind = "runtime_observed" # hits = 4 +[[entry_point]] +addr = 0x0210372C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + [[entry_point]] addr = 0x02103740 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 13 [[entry_point]] addr = 0x021037AC mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x02103834 @@ -97,7 +175,7 @@ kind = "runtime_observed" addr = 0x02103838 mode = "arm" kind = "runtime_observed" -# hits = 9 +# hits = 6 [[entry_point]] addr = 0x0210384C @@ -109,46 +187,238 @@ kind = "runtime_observed" addr = 0x02103868 mode = "arm" kind = "runtime_observed" -# hits = 8 +# hits = 14 [[entry_point]] addr = 0x02103870 mode = "arm" kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02103890 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x021038D4 +mode = "arm" +kind = "runtime_observed" # hits = 4 +[[entry_point]] +addr = 0x02103954 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + [[entry_point]] addr = 0x02103974 mode = "arm" kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02103994 +mode = "arm" +kind = "runtime_observed" # hits = 4 +[[entry_point]] +addr = 0x021039E8 +mode = "arm" +kind = "runtime_observed" +# hits = 17 + +[[entry_point]] +addr = 0x02103AA0 +mode = "arm" +kind = "runtime_observed" +# hits = 17 + +[[entry_point]] +addr = 0x02103B58 +mode = "arm" +kind = "runtime_observed" +# hits = 15233 + +[[entry_point]] +addr = 0x02103C04 +mode = "arm" +kind = "runtime_observed" +# hits = 15258 + +[[entry_point]] +addr = 0x02103D2C +mode = "arm" +kind = "runtime_observed" +# hits = 615 + [[entry_point]] addr = 0x02103DD0 mode = "arm" kind = "runtime_observed" -# hits = 4 +# hits = 7 + +[[entry_point]] +addr = 0x02103DF4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 [[entry_point]] addr = 0x02103FFC mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 4 + +[[entry_point]] +addr = 0x02104094 +mode = "arm" +kind = "runtime_observed" +# hits = 615 + +[[entry_point]] +addr = 0x021040DC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021040E0 +mode = "arm" +kind = "runtime_observed" +# hits = 33 + +[[entry_point]] +addr = 0x021040F0 +mode = "arm" +kind = "runtime_observed" +# hits = 60 + +[[entry_point]] +addr = 0x021040F8 +mode = "arm" +kind = "runtime_observed" +# hits = 64 + +[[entry_point]] +addr = 0x02104100 +mode = "arm" +kind = "runtime_observed" +# hits = 446 + +[[entry_point]] +addr = 0x02104354 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021043EC +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0210463C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02104738 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02104764 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021047CC +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021048A4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210491C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02104928 +mode = "arm" +kind = "runtime_observed" +# hits = 14749 + +[[entry_point]] +addr = 0x021049CC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021049FC +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x02104AB4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02104B3C +mode = "arm" +kind = "runtime_observed" +# hits = 44 [[entry_point]] addr = 0x02104BB0 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 4 [[entry_point]] addr = 0x02104BF8 mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 4 + +[[entry_point]] +addr = 0x02104CAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02104CFC +mode = "arm" +kind = "runtime_observed" +# hits = 648 [[entry_point]] addr = 0x02104FDC mode = "arm" kind = "runtime_observed" -# hits = 6 +# hits = 15 + +[[entry_point]] +addr = 0x02104FE0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 diff --git a/config/mph_arm9_ov002.toml b/config/mph_arm9_ov002.toml new file mode 100644 index 0000000..f363a2a --- /dev/null +++ b/config/mph_arm9_ov002.toml @@ -0,0 +1,3532 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 2" +id = "mph_amhe0_arm9_ov002" +load_address = 0x02102220 +size = 0x00020C20 +entry_pc = 0x02103010 +authoritative_entry_points = false + +[identity] +sha1 = "4a9a988e66776b9491651ef50efd68ab3f65e240" + +[[entry_point]] +addr = 0x02103010 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02103088 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02103144 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103154 +mode = "arm" +kind = "runtime_observed" +# hits = 68 + +[[entry_point]] +addr = 0x02103158 +mode = "arm" +kind = "runtime_observed" +# hits = 5885 + +[[entry_point]] +addr = 0x02103184 +mode = "arm" +kind = "runtime_observed" +# hits = 16999 + +[[entry_point]] +addr = 0x02103208 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0210324C +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x021033DC +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x0210346C +mode = "arm" +kind = "runtime_observed" +# hits = 104 + +[[entry_point]] +addr = 0x02103494 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x021034CC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021034D0 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x021034E8 +mode = "arm" +kind = "runtime_observed" +# hits = 21 + +[[entry_point]] +addr = 0x021034F4 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x02103510 +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x02103550 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021035CC +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021035F8 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02103604 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103640 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021036E8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210372C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02103740 +mode = "arm" +kind = "runtime_observed" +# hits = 13 + +[[entry_point]] +addr = 0x021037AC +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02103834 +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02103838 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0210384C +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02103868 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02103870 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02103890 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x021038D4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02103954 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02103974 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02103994 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021039E8 +mode = "arm" +kind = "runtime_observed" +# hits = 17 + +[[entry_point]] +addr = 0x02103AA0 +mode = "arm" +kind = "runtime_observed" +# hits = 17 + +[[entry_point]] +addr = 0x02103B58 +mode = "arm" +kind = "runtime_observed" +# hits = 15233 + +[[entry_point]] +addr = 0x02103C04 +mode = "arm" +kind = "runtime_observed" +# hits = 15258 + +[[entry_point]] +addr = 0x02103D2C +mode = "arm" +kind = "runtime_observed" +# hits = 615 + +[[entry_point]] +addr = 0x02103DD0 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02103DF4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02103FFC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02104094 +mode = "arm" +kind = "runtime_observed" +# hits = 615 + +[[entry_point]] +addr = 0x021040DC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021040E0 +mode = "arm" +kind = "runtime_observed" +# hits = 33 + +[[entry_point]] +addr = 0x021040F0 +mode = "arm" +kind = "runtime_observed" +# hits = 60 + +[[entry_point]] +addr = 0x021040F8 +mode = "arm" +kind = "runtime_observed" +# hits = 64 + +[[entry_point]] +addr = 0x02104100 +mode = "arm" +kind = "runtime_observed" +# hits = 446 + +[[entry_point]] +addr = 0x02104354 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021043EC +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0210463C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02104738 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02104764 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021047CC +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021048A4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210491C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02104928 +mode = "arm" +kind = "runtime_observed" +# hits = 14749 + +[[entry_point]] +addr = 0x021049CC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021049FC +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x02104AB4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02104B3C +mode = "arm" +kind = "runtime_observed" +# hits = 44 + +[[entry_point]] +addr = 0x02104BB0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02104BF8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02104CAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02104CFC +mode = "arm" +kind = "runtime_observed" +# hits = 648 + +[[entry_point]] +addr = 0x02104FDC +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02104FE0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021050C8 +mode = "arm" +kind = "runtime_observed" +# hits = 648 + +[[entry_point]] +addr = 0x02105198 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02105420 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02105444 +mode = "arm" +kind = "runtime_observed" +# hits = 35876 + +[[entry_point]] +addr = 0x02105468 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x02105508 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021055E8 +mode = "arm" +kind = "runtime_observed" +# hits = 35812 + +[[entry_point]] +addr = 0x021056C4 +mode = "arm" +kind = "runtime_observed" +# hits = 451 + +[[entry_point]] +addr = 0x021056E0 +mode = "arm" +kind = "runtime_observed" +# hits = 451 + +[[entry_point]] +addr = 0x02105790 +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x021057D0 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02105A64 +mode = "arm" +kind = "runtime_observed" +# hits = 431 + +[[entry_point]] +addr = 0x02105A9C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02105BFC +mode = "arm" +kind = "runtime_observed" +# hits = 35772 + +[[entry_point]] +addr = 0x02105CD4 +mode = "arm" +kind = "runtime_observed" +# hits = 430 + +[[entry_point]] +addr = 0x02105CF0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02105D68 +mode = "arm" +kind = "runtime_observed" +# hits = 431 + +[[entry_point]] +addr = 0x02105D7C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02105F2C +mode = "arm" +kind = "runtime_observed" +# hits = 431 + +[[entry_point]] +addr = 0x02105FB8 +mode = "arm" +kind = "runtime_observed" +# hits = 430 + +[[entry_point]] +addr = 0x02106028 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02106098 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02106108 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02106204 +mode = "arm" +kind = "runtime_observed" +# hits = 50 + +[[entry_point]] +addr = 0x02106248 +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x021062A8 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02106444 +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x02106460 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021064BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021064DC +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x02106508 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02106538 +mode = "arm" +kind = "runtime_observed" +# hits = 34 + +[[entry_point]] +addr = 0x0210661C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210664C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021066A4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021066B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02106788 +mode = "arm" +kind = "runtime_observed" +# hits = 45275 + +[[entry_point]] +addr = 0x02106790 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02106824 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210690C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02106A3C +mode = "arm" +kind = "runtime_observed" +# hits = 878 + +[[entry_point]] +addr = 0x02106AB8 +mode = "arm" +kind = "runtime_observed" +# hits = 58 + +[[entry_point]] +addr = 0x02106B5C +mode = "arm" +kind = "runtime_observed" +# hits = 3562 + +[[entry_point]] +addr = 0x02106BB4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02106C34 +mode = "arm" +kind = "runtime_observed" +# hits = 4584 + +[[entry_point]] +addr = 0x02106CE4 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02106D80 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x02106DAC +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x02106E8C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02106F04 +mode = "arm" +kind = "runtime_observed" +# hits = 72458 + +[[entry_point]] +addr = 0x02106F9C +mode = "arm" +kind = "runtime_observed" +# hits = 69566 + +[[entry_point]] +addr = 0x02106FE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021071D0 +mode = "arm" +kind = "runtime_observed" +# hits = 23 + +[[entry_point]] +addr = 0x021071D4 +mode = "arm" +kind = "runtime_observed" +# hits = 23 + +[[entry_point]] +addr = 0x021072E0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021074A4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021075A0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021075D8 +mode = "arm" +kind = "runtime_observed" +# hits = 81 + +[[entry_point]] +addr = 0x021076E0 +mode = "arm" +kind = "runtime_observed" +# hits = 51230 + +[[entry_point]] +addr = 0x02107730 +mode = "arm" +kind = "runtime_observed" +# hits = 54674 + +[[entry_point]] +addr = 0x02107808 +mode = "arm" +kind = "runtime_observed" +# hits = 51586 + +[[entry_point]] +addr = 0x02107894 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02107B54 +mode = "arm" +kind = "runtime_observed" +# hits = 89 + +[[entry_point]] +addr = 0x02107D80 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02107F20 +mode = "arm" +kind = "runtime_observed" +# hits = 38 + +[[entry_point]] +addr = 0x0210843C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021086C4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02108E10 +mode = "arm" +kind = "runtime_observed" +# hits = 906 + +[[entry_point]] +addr = 0x02108FD4 +mode = "arm" +kind = "runtime_observed" +# hits = 906 + +[[entry_point]] +addr = 0x0210A568 +mode = "arm" +kind = "runtime_observed" +# hits = 50 + +[[entry_point]] +addr = 0x0210A600 +mode = "arm" +kind = "runtime_observed" +# hits = 50 + +[[entry_point]] +addr = 0x0210A9CC +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0210AA84 +mode = "arm" +kind = "runtime_observed" +# hits = 82996 + +[[entry_point]] +addr = 0x0210ABE8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210AD58 +mode = "arm" +kind = "runtime_observed" +# hits = 3899 + +[[entry_point]] +addr = 0x0210AFB0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210B2C0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210B5EC +mode = "arm" +kind = "runtime_observed" +# hits = 6888 + +[[entry_point]] +addr = 0x0210B5F0 +mode = "arm" +kind = "runtime_observed" +# hits = 6824 + +[[entry_point]] +addr = 0x0210B87C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210B8C0 +mode = "arm" +kind = "runtime_observed" +# hits = 4004 + +[[entry_point]] +addr = 0x0210BA44 +mode = "arm" +kind = "runtime_observed" +# hits = 2820 + +[[entry_point]] +addr = 0x0210BAD8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210BC6C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0210BF98 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210C1D8 +mode = "arm" +kind = "runtime_observed" +# hits = 9718 + +[[entry_point]] +addr = 0x0210C24C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210C27C +mode = "arm" +kind = "runtime_observed" +# hits = 9426 + +[[entry_point]] +addr = 0x0210C418 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0210C494 +mode = "arm" +kind = "runtime_observed" +# hits = 33 + +[[entry_point]] +addr = 0x0210CA1C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0210CB34 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210CF2C +mode = "arm" +kind = "runtime_observed" +# hits = 12028 + +[[entry_point]] +addr = 0x0210D024 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210D0C0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210D434 +mode = "arm" +kind = "runtime_observed" +# hits = 6888 + +[[entry_point]] +addr = 0x0210D48C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0210D6A4 +mode = "arm" +kind = "runtime_observed" +# hits = 6824 + +[[entry_point]] +addr = 0x0210DC64 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0210DC68 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0210E234 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210E534 +mode = "arm" +kind = "runtime_observed" +# hits = 47202 + +[[entry_point]] +addr = 0x0210E6A8 +mode = "arm" +kind = "runtime_observed" +# hits = 31685 + +[[entry_point]] +addr = 0x0210E79C +mode = "arm" +kind = "runtime_observed" +# hits = 33267 + +[[entry_point]] +addr = 0x0210E7DC +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0210EEB8 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0210F104 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211006C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021100C4 +mode = "arm" +kind = "runtime_observed" +# hits = 188528 + +[[entry_point]] +addr = 0x02110128 +mode = "arm" +kind = "runtime_observed" +# hits = 101150 + +[[entry_point]] +addr = 0x0211014C +mode = "arm" +kind = "runtime_observed" +# hits = 254 + +[[entry_point]] +addr = 0x021101D4 +mode = "arm" +kind = "runtime_observed" +# hits = 107424 + +[[entry_point]] +addr = 0x02110308 +mode = "arm" +kind = "runtime_observed" +# hits = 227860 + +[[entry_point]] +addr = 0x02110494 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x021104E8 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02110528 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02110530 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02110568 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02110570 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021105A8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02110610 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02110630 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211064C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02110690 +mode = "arm" +kind = "runtime_observed" +# hits = 98568 + +[[entry_point]] +addr = 0x0211070C +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x021107A4 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02110834 +mode = "arm" +kind = "runtime_observed" +# hits = 77585 + +[[entry_point]] +addr = 0x02110844 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x02110860 +mode = "arm" +kind = "runtime_observed" +# hits = 77585 + +[[entry_point]] +addr = 0x021108B4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02110BD8 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02110D58 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02110DA0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02110DC4 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02110E44 +mode = "arm" +kind = "runtime_observed" +# hits = 124 + +[[entry_point]] +addr = 0x02110E54 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02110EE0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02110F20 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02110F28 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021110F0 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02111190 +mode = "arm" +kind = "runtime_observed" +# hits = 1394 + +[[entry_point]] +addr = 0x02111214 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02111298 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02111320 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211133C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02111354 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02111388 +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02111418 +mode = "arm" +kind = "runtime_observed" +# hits = 54 + +[[entry_point]] +addr = 0x02111478 +mode = "arm" +kind = "runtime_observed" +# hits = 523 + +[[entry_point]] +addr = 0x021114BC +mode = "arm" +kind = "runtime_observed" +# hits = 785 + +[[entry_point]] +addr = 0x0211151C +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02111558 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211155C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02111560 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0211156C +mode = "arm" +kind = "runtime_observed" +# hits = 24658 + +[[entry_point]] +addr = 0x02111614 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02111680 +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02111718 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021118B4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02111A78 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02111C8C +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02111CA4 +mode = "arm" +kind = "runtime_observed" +# hits = 25343 + +[[entry_point]] +addr = 0x02111CF4 +mode = "arm" +kind = "runtime_observed" +# hits = 4649 + +[[entry_point]] +addr = 0x02111D4C +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02111D80 +mode = "arm" +kind = "runtime_observed" +# hits = 169017 + +[[entry_point]] +addr = 0x02111DF8 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02111E10 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02111EBC +mode = "arm" +kind = "runtime_observed" +# hits = 89241 + +[[entry_point]] +addr = 0x02111EF4 +mode = "arm" +kind = "runtime_observed" +# hits = 65270 + +[[entry_point]] +addr = 0x02111F20 +mode = "arm" +kind = "runtime_observed" +# hits = 24658 + +[[entry_point]] +addr = 0x02111F48 +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02111FA0 +mode = "arm" +kind = "runtime_observed" +# hits = 834 + +[[entry_point]] +addr = 0x0211204C +mode = "arm" +kind = "runtime_observed" +# hits = 685 + +[[entry_point]] +addr = 0x021120B4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021120D4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021121D4 +mode = "arm" +kind = "runtime_observed" +# hits = 350 + +[[entry_point]] +addr = 0x0211232C +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x02112518 +mode = "arm" +kind = "runtime_observed" +# hits = 636 + +[[entry_point]] +addr = 0x0211251C +mode = "arm" +kind = "runtime_observed" +# hits = 758 + +[[entry_point]] +addr = 0x02112520 +mode = "arm" +kind = "runtime_observed" +# hits = 70 + +[[entry_point]] +addr = 0x02112524 +mode = "arm" +kind = "runtime_observed" +# hits = 994 + +[[entry_point]] +addr = 0x02112528 +mode = "arm" +kind = "runtime_observed" +# hits = 137 + +[[entry_point]] +addr = 0x02112694 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126A8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126AC +mode = "arm" +kind = "runtime_observed" +# hits = 2648 + +[[entry_point]] +addr = 0x021126B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126B4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126B8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126BC +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126C0 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126C4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126C8 +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126CC +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126D0 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126D4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126D8 +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126DC +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126E0 +mode = "arm" +kind = "runtime_observed" +# hits = 2597 + +[[entry_point]] +addr = 0x021126E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126E8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126EC +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x021126F0 +mode = "arm" +kind = "runtime_observed" +# hits = 2601 + +[[entry_point]] +addr = 0x021126F4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021126FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02112704 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211273C +mode = "arm" +kind = "runtime_observed" +# hits = 50 + +[[entry_point]] +addr = 0x02112888 +mode = "arm" +kind = "runtime_observed" +# hits = 15224 + +[[entry_point]] +addr = 0x02112AD0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02112BB0 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02112E80 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02112F18 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02112F9C +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02112FB4 +mode = "arm" +kind = "runtime_observed" +# hits = 48 + +[[entry_point]] +addr = 0x02112FDC +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02113134 +mode = "arm" +kind = "runtime_observed" +# hits = 36 + +[[entry_point]] +addr = 0x02113174 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211320C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211324C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211326C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02113298 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x021132B8 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021134B8 +mode = "arm" +kind = "runtime_observed" +# hits = 5780 + +[[entry_point]] +addr = 0x02113568 +mode = "arm" +kind = "runtime_observed" +# hits = 5302 + +[[entry_point]] +addr = 0x02113684 +mode = "arm" +kind = "runtime_observed" +# hits = 1637 + +[[entry_point]] +addr = 0x02113700 +mode = "arm" +kind = "runtime_observed" +# hits = 7692 + +[[entry_point]] +addr = 0x02113744 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021137AC +mode = "arm" +kind = "runtime_observed" +# hits = 283 + +[[entry_point]] +addr = 0x02113848 +mode = "arm" +kind = "runtime_observed" +# hits = 816 + +[[entry_point]] +addr = 0x021138BC +mode = "arm" +kind = "runtime_observed" +# hits = 220 + +[[entry_point]] +addr = 0x021138F4 +mode = "arm" +kind = "runtime_observed" +# hits = 839 + +[[entry_point]] +addr = 0x02113984 +mode = "arm" +kind = "runtime_observed" +# hits = 9364 + +[[entry_point]] +addr = 0x02113B30 +mode = "arm" +kind = "runtime_observed" +# hits = 582 + +[[entry_point]] +addr = 0x02113BE8 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02113C20 +mode = "arm" +kind = "runtime_observed" +# hits = 4500 + +[[entry_point]] +addr = 0x02113C24 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02113C80 +mode = "arm" +kind = "runtime_observed" +# hits = 54528 + +[[entry_point]] +addr = 0x02113CD0 +mode = "arm" +kind = "runtime_observed" +# hits = 56533 + +[[entry_point]] +addr = 0x02113D58 +mode = "arm" +kind = "runtime_observed" +# hits = 10350 + +[[entry_point]] +addr = 0x02113DB8 +mode = "arm" +kind = "runtime_observed" +# hits = 8494 + +[[entry_point]] +addr = 0x02113DCC +mode = "arm" +kind = "runtime_observed" +# hits = 849 + +[[entry_point]] +addr = 0x02113DF4 +mode = "arm" +kind = "runtime_observed" +# hits = 9860 + +[[entry_point]] +addr = 0x02113E08 +mode = "arm" +kind = "runtime_observed" +# hits = 68469 + +[[entry_point]] +addr = 0x02113E3C +mode = "arm" +kind = "runtime_observed" +# hits = 263519 + +[[entry_point]] +addr = 0x02113F58 +mode = "arm" +kind = "runtime_observed" +# hits = 9266 + +[[entry_point]] +addr = 0x02113FA0 +mode = "arm" +kind = "runtime_observed" +# hits = 12545 + +[[entry_point]] +addr = 0x02114024 +mode = "arm" +kind = "runtime_observed" +# hits = 72391 + +[[entry_point]] +addr = 0x02114034 +mode = "arm" +kind = "runtime_observed" +# hits = 829 + +[[entry_point]] +addr = 0x02114044 +mode = "arm" +kind = "runtime_observed" +# hits = 266 + +[[entry_point]] +addr = 0x02114064 +mode = "arm" +kind = "runtime_observed" +# hits = 91531 + +[[entry_point]] +addr = 0x02114074 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x021140D4 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0211411C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02114228 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x021142E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021142F4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021142F8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021142FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211430C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02114310 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02114318 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02114320 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021144E8 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x0211452C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02114550 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02114648 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021146B4 +mode = "arm" +kind = "runtime_observed" +# hits = 69692 + +[[entry_point]] +addr = 0x02114730 +mode = "arm" +kind = "runtime_observed" +# hits = 2526 + +[[entry_point]] +addr = 0x02114780 +mode = "arm" +kind = "runtime_observed" +# hits = 17834 + +[[entry_point]] +addr = 0x021147C4 +mode = "arm" +kind = "runtime_observed" +# hits = 47714 + +[[entry_point]] +addr = 0x02114814 +mode = "arm" +kind = "runtime_observed" +# hits = 910 + +[[entry_point]] +addr = 0x02114AB8 +mode = "arm" +kind = "runtime_observed" +# hits = 12777 + +[[entry_point]] +addr = 0x02114AF4 +mode = "arm" +kind = "runtime_observed" +# hits = 27 + +[[entry_point]] +addr = 0x02114B1C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02114B2C +mode = "arm" +kind = "runtime_observed" +# hits = 9178 + +[[entry_point]] +addr = 0x02114C34 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02114F2C +mode = "arm" +kind = "runtime_observed" +# hits = 16354 + +[[entry_point]] +addr = 0x02115000 +mode = "arm" +kind = "runtime_observed" +# hits = 16904 + +[[entry_point]] +addr = 0x02115014 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021150E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2705 + +[[entry_point]] +addr = 0x0211511C +mode = "arm" +kind = "runtime_observed" +# hits = 17 + +[[entry_point]] +addr = 0x02115124 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211512C +mode = "arm" +kind = "runtime_observed" +# hits = 96 + +[[entry_point]] +addr = 0x02115168 +mode = "arm" +kind = "runtime_observed" +# hits = 2705 + +[[entry_point]] +addr = 0x0211516C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115208 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02115260 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211527C +mode = "arm" +kind = "runtime_observed" +# hits = 32 + +[[entry_point]] +addr = 0x0211528C +mode = "arm" +kind = "runtime_observed" +# hits = 24 + +[[entry_point]] +addr = 0x02115308 +mode = "arm" +kind = "runtime_observed" +# hits = 94 + +[[entry_point]] +addr = 0x021153B4 +mode = "arm" +kind = "runtime_observed" +# hits = 760 + +[[entry_point]] +addr = 0x0211540C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115490 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021154F8 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02115554 +mode = "arm" +kind = "runtime_observed" +# hits = 2764 + +[[entry_point]] +addr = 0x02115700 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x021157CC +mode = "arm" +kind = "runtime_observed" +# hits = 95841 + +[[entry_point]] +addr = 0x021157D8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021158C4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021158EC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115A5C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02115A60 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115A64 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115A68 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115A6C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02115A70 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02115A90 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02115A94 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02115A98 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AA0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AA4 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02115ABC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AC0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02115AC4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AC8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115ACC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AE8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AF4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115AFC +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02115B04 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02115B08 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02115B0C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02115B18 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02115DB0 +mode = "arm" +kind = "runtime_observed" +# hits = 16637 + +[[entry_point]] +addr = 0x02115F88 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02115FB4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021161FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02116404 +mode = "arm" +kind = "runtime_observed" +# hits = 910 + +[[entry_point]] +addr = 0x02116458 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021164F4 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02116514 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02116520 +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x021165D4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021166F4 +mode = "arm" +kind = "runtime_observed" +# hits = 77 + +[[entry_point]] +addr = 0x021167A8 +mode = "arm" +kind = "runtime_observed" +# hits = 771 + +[[entry_point]] +addr = 0x02116814 +mode = "arm" +kind = "runtime_observed" +# hits = 165 + +[[entry_point]] +addr = 0x0211685C +mode = "arm" +kind = "runtime_observed" +# hits = 5488 + +[[entry_point]] +addr = 0x021168A8 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x021168C4 +mode = "arm" +kind = "runtime_observed" +# hits = 21224 + +[[entry_point]] +addr = 0x02116980 +mode = "arm" +kind = "runtime_observed" +# hits = 20127 + +[[entry_point]] +addr = 0x02116A14 +mode = "arm" +kind = "runtime_observed" +# hits = 12918 + +[[entry_point]] +addr = 0x02116A44 +mode = "arm" +kind = "runtime_observed" +# hits = 2489 + +[[entry_point]] +addr = 0x02116AB0 +mode = "arm" +kind = "runtime_observed" +# hits = 25823 + +[[entry_point]] +addr = 0x02116ACC +mode = "arm" +kind = "runtime_observed" +# hits = 26423 + +[[entry_point]] +addr = 0x02116AE8 +mode = "arm" +kind = "runtime_observed" +# hits = 2595 + +[[entry_point]] +addr = 0x02116BFC +mode = "arm" +kind = "runtime_observed" +# hits = 5171 + +[[entry_point]] +addr = 0x02116C18 +mode = "arm" +kind = "runtime_observed" +# hits = 5459 + +[[entry_point]] +addr = 0x02116C34 +mode = "arm" +kind = "runtime_observed" +# hits = 853 + +[[entry_point]] +addr = 0x02116C84 +mode = "arm" +kind = "runtime_observed" +# hits = 1703 + +[[entry_point]] +addr = 0x02116CA0 +mode = "arm" +kind = "runtime_observed" +# hits = 1895 + +[[entry_point]] +addr = 0x02116CBC +mode = "arm" +kind = "runtime_observed" +# hits = 20164 + +[[entry_point]] +addr = 0x02116D38 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02116DD4 +mode = "arm" +kind = "runtime_observed" +# hits = 80220 + +[[entry_point]] +addr = 0x02116E10 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02116E50 +mode = "arm" +kind = "runtime_observed" +# hits = 111636 + +[[entry_point]] +addr = 0x02117360 +mode = "arm" +kind = "runtime_observed" +# hits = 23181 + +[[entry_point]] +addr = 0x021173B0 +mode = "arm" +kind = "runtime_observed" +# hits = 20983 + +[[entry_point]] +addr = 0x02117408 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021174D4 +mode = "arm" +kind = "runtime_observed" +# hits = 9797 + +[[entry_point]] +addr = 0x0211770C +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x02117748 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02117750 +mode = "arm" +kind = "runtime_observed" +# hits = 653 + +[[entry_point]] +addr = 0x02117790 +mode = "arm" +kind = "runtime_observed" +# hits = 795 + +[[entry_point]] +addr = 0x021177B0 +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x021177D4 +mode = "arm" +kind = "runtime_observed" +# hits = 15225 + +[[entry_point]] +addr = 0x02117810 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02117864 +mode = "arm" +kind = "runtime_observed" +# hits = 463 + +[[entry_point]] +addr = 0x0211790C +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x021179C8 +mode = "arm" +kind = "runtime_observed" +# hits = 15518 + +[[entry_point]] +addr = 0x021179F0 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021180F8 +mode = "arm" +kind = "runtime_observed" +# hits = 56050 + +[[entry_point]] +addr = 0x02118210 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021185FC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211862C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211864C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02118844 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021188EC +mode = "arm" +kind = "runtime_observed" +# hits = 63103 + +[[entry_point]] +addr = 0x02118960 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02118C50 +mode = "arm" +kind = "runtime_observed" +# hits = 129670 + +[[entry_point]] +addr = 0x02118D90 +mode = "arm" +kind = "runtime_observed" +# hits = 1904 + +[[entry_point]] +addr = 0x02118D94 +mode = "arm" +kind = "runtime_observed" +# hits = 790 + +[[entry_point]] +addr = 0x02118D98 +mode = "arm" +kind = "runtime_observed" +# hits = 20653 + +[[entry_point]] +addr = 0x02118D9C +mode = "arm" +kind = "runtime_observed" +# hits = 91235 + +[[entry_point]] +addr = 0x02118DA0 +mode = "arm" +kind = "runtime_observed" +# hits = 18974 + +[[entry_point]] +addr = 0x02118DA4 +mode = "arm" +kind = "runtime_observed" +# hits = 887 + +[[entry_point]] +addr = 0x02118DA8 +mode = "arm" +kind = "runtime_observed" +# hits = 2399 + +[[entry_point]] +addr = 0x02118F90 +mode = "arm" +kind = "runtime_observed" +# hits = 3168 + +[[entry_point]] +addr = 0x02118F94 +mode = "arm" +kind = "runtime_observed" +# hits = 2190 + +[[entry_point]] +addr = 0x02118F98 +mode = "arm" +kind = "runtime_observed" +# hits = 80604 + +[[entry_point]] +addr = 0x02118F9C +mode = "arm" +kind = "runtime_observed" +# hits = 82477 + +[[entry_point]] +addr = 0x02118FA0 +mode = "arm" +kind = "runtime_observed" +# hits = 10778 + +[[entry_point]] +addr = 0x02118FA4 +mode = "arm" +kind = "runtime_observed" +# hits = 1892 + +[[entry_point]] +addr = 0x02118FA8 +mode = "arm" +kind = "runtime_observed" +# hits = 26642 + +[[entry_point]] +addr = 0x021191A0 +mode = "arm" +kind = "runtime_observed" +# hits = 4681 + +[[entry_point]] +addr = 0x021191A4 +mode = "arm" +kind = "runtime_observed" +# hits = 527 + +[[entry_point]] +addr = 0x021191A8 +mode = "arm" +kind = "runtime_observed" +# hits = 16777 + +[[entry_point]] +addr = 0x021191AC +mode = "arm" +kind = "runtime_observed" +# hits = 95823 + +[[entry_point]] +addr = 0x021191B0 +mode = "arm" +kind = "runtime_observed" +# hits = 15636 + +[[entry_point]] +addr = 0x021191B4 +mode = "arm" +kind = "runtime_observed" +# hits = 400 + +[[entry_point]] +addr = 0x021191B8 +mode = "arm" +kind = "runtime_observed" +# hits = 5146 + +[[entry_point]] +addr = 0x02119274 +mode = "arm" +kind = "runtime_observed" +# hits = 49491 + +[[entry_point]] +addr = 0x02119370 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x021193B8 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021193E4 +mode = "arm" +kind = "runtime_observed" +# hits = 37887 + +[[entry_point]] +addr = 0x0211943C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211953C +mode = "arm" +kind = "runtime_observed" +# hits = 13772 + +[[entry_point]] +addr = 0x02119630 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02119784 +mode = "arm" +kind = "runtime_observed" +# hits = 55323 + +[[entry_point]] +addr = 0x02119A1C +mode = "arm" +kind = "runtime_observed" +# hits = 37997 + +[[entry_point]] +addr = 0x0211A56C +mode = "arm" +kind = "runtime_observed" +# hits = 75994 + +[[entry_point]] +addr = 0x0211A65C +mode = "arm" +kind = "runtime_observed" +# hits = 13772 + +[[entry_point]] +addr = 0x0211A6EC +mode = "arm" +kind = "runtime_observed" +# hits = 48270 + +[[entry_point]] +addr = 0x0211A720 +mode = "arm" +kind = "runtime_observed" +# hits = 84501 + +[[entry_point]] +addr = 0x0211A7E0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211A830 +mode = "arm" +kind = "runtime_observed" +# hits = 10719 + +[[entry_point]] +addr = 0x0211A868 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211A878 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211A884 +mode = "arm" +kind = "runtime_observed" +# hits = 13652 + +[[entry_point]] +addr = 0x0211A8B4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211A8D0 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211AA20 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0211AC00 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211ACF4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211AE68 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211AFD0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211B004 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0211B1F4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211B270 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211B28C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211B8E4 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0211B8F4 +mode = "arm" +kind = "runtime_observed" +# hits = 245 + +[[entry_point]] +addr = 0x0211B904 +mode = "arm" +kind = "runtime_observed" +# hits = 284 + +[[entry_point]] +addr = 0x0211BAC4 +mode = "arm" +kind = "runtime_observed" +# hits = 208 + +[[entry_point]] +addr = 0x0211BCC0 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211BCFC +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211BD84 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211BDB4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211BF60 +mode = "arm" +kind = "runtime_observed" +# hits = 2614 + +[[entry_point]] +addr = 0x0211BFE4 +mode = "arm" +kind = "runtime_observed" +# hits = 1432 + +[[entry_point]] +addr = 0x0211C070 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C08C +mode = "arm" +kind = "runtime_observed" +# hits = 403 + +[[entry_point]] +addr = 0x0211C120 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C12C +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x0211C13C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C148 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0211C154 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C188 +mode = "arm" +kind = "runtime_observed" +# hits = 13 + +[[entry_point]] +addr = 0x0211C18C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211C1A0 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C1B0 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211C1BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211C1C8 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211C22C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C27C +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C318 +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x0211C388 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211C4DC +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211C518 +mode = "arm" +kind = "runtime_observed" +# hits = 77585 + +[[entry_point]] +addr = 0x0211C59C +mode = "arm" +kind = "runtime_observed" +# hits = 15517 + +[[entry_point]] +addr = 0x0211C5B0 +mode = "arm" +kind = "runtime_observed" +# hits = 1432 + +[[entry_point]] +addr = 0x0211C6B4 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x0211C764 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211C7C4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211C830 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0211C8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0211C8E0 +mode = "arm" +kind = "runtime_observed" +# hits = 1432 + +[[entry_point]] +addr = 0x0211CA18 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0211CA34 +mode = "arm" +kind = "runtime_observed" +# hits = 110 + +[[entry_point]] +addr = 0x0211CA50 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x0211CA60 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211CAB4 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211CB80 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211CC04 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0211CC6C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211CE64 +mode = "arm" +kind = "runtime_observed" +# hits = 19820 + +[[entry_point]] +addr = 0x0211D244 +mode = "arm" +kind = "runtime_observed" +# hits = 151254 + +[[entry_point]] +addr = 0x0211D4B4 +mode = "arm" +kind = "runtime_observed" +# hits = 16178 + +[[entry_point]] +addr = 0x0211D59C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211D5C4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211D690 +mode = "arm" +kind = "runtime_observed" +# hits = 109174 + +[[entry_point]] +addr = 0x0211D704 +mode = "arm" +kind = "runtime_observed" +# hits = 6837 + +[[entry_point]] +addr = 0x0211D774 +mode = "arm" +kind = "runtime_observed" +# hits = 5728 + +[[entry_point]] +addr = 0x0211DCD8 +mode = "arm" +kind = "runtime_observed" +# hits = 23116 + +[[entry_point]] +addr = 0x0211DD2C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211E41C +mode = "arm" +kind = "runtime_observed" +# hits = 2705 + +[[entry_point]] +addr = 0x0211E428 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211E438 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0211E444 +mode = "arm" +kind = "runtime_observed" +# hits = 2753 + +[[entry_point]] +addr = 0x0211E4A8 +mode = "arm" +kind = "runtime_observed" +# hits = 2764 + +[[entry_point]] +addr = 0x0211E4C0 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211E548 +mode = "arm" +kind = "runtime_observed" +# hits = 11354 + +[[entry_point]] +addr = 0x0211E560 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211E568 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0211E56C +mode = "arm" +kind = "runtime_observed" +# hits = 39 + +[[entry_point]] +addr = 0x0211E588 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0211E598 +mode = "arm" +kind = "runtime_observed" +# hits = 10051 + +[[entry_point]] +addr = 0x0211E5BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211E64C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211E764 +mode = "arm" +kind = "runtime_observed" +# hits = 10051 + +[[entry_point]] +addr = 0x0211E79C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211E7A8 +mode = "arm" +kind = "runtime_observed" +# hits = 30153 + +[[entry_point]] +addr = 0x0211E854 +mode = "arm" +kind = "runtime_observed" +# hits = 10051 + +[[entry_point]] +addr = 0x0211E8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0211E8F8 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0211E994 +mode = "arm" +kind = "runtime_observed" +# hits = 2295 + +[[entry_point]] +addr = 0x0211E9A0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211E9B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211E9C0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211EA50 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0211EC48 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211EE90 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0211EF0C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0211EFE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F128 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F1F8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F2A0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F2A8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0211F3B8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0211F7B8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 diff --git a/config/mph_arm9_ov003.toml b/config/mph_arm9_ov003.toml new file mode 100644 index 0000000..c1e726d --- /dev/null +++ b/config/mph_arm9_ov003.toml @@ -0,0 +1,124 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 3" +id = "mph_amhe0_arm9_ov003" +load_address = 0x0212C600 +size = 0x000034C0 +entry_pc = 0x0212DC54 +authoritative_entry_points = false + +[identity] +sha1 = "747bab89f0972cedae5232c4ae543fa5d10b5558" + +[[entry_point]] +addr = 0x0212DC54 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212DCE4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212DD70 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0212DF5C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E060 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0212E210 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E2BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E3B4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E740 +mode = "arm" +kind = "runtime_observed" +# hits = 32339 + +[[entry_point]] +addr = 0x0212E744 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0212E970 +mode = "arm" +kind = "runtime_observed" +# hits = 32307 + +[[entry_point]] +addr = 0x0212EA18 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212EBAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212EBB0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ECAC +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0212ED0C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ED48 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212EE40 +mode = "arm" +kind = "runtime_observed" +# hits = 5 diff --git a/config/mph_arm9_ov004.toml b/config/mph_arm9_ov004.toml new file mode 100644 index 0000000..e4640b1 --- /dev/null +++ b/config/mph_arm9_ov004.toml @@ -0,0 +1,556 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 4" +id = "mph_amhe0_arm9_ov004" +load_address = 0x0214C860 +size = 0x0004C460 +entry_pc = 0x021513E0 +authoritative_entry_points = false + +[identity] +sha1 = "9a5beb990abe48f91f13e91cdd6bf2098e213ea7" + +[[entry_point]] +addr = 0x021513E0 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02152608 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0215296C +mode = "arm" +kind = "runtime_observed" +# hits = 24 + +[[entry_point]] +addr = 0x0216B524 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216B528 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216BA8C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216BB30 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216BBC4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216BCBC +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216BD28 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216BD74 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216BDD4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216BDF0 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0216BE58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216BE88 +mode = "arm" +kind = "runtime_observed" +# hits = 37 + +[[entry_point]] +addr = 0x0216BF6C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C000 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0216C004 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C008 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C00C +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C010 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C018 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C044 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216C0AC +mode = "arm" +kind = "runtime_observed" +# hits = 2976 + +[[entry_point]] +addr = 0x0216C218 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C250 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0216C254 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C258 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C25C +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C260 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C268 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C34C +mode = "arm" +kind = "runtime_observed" +# hits = 10332 + +[[entry_point]] +addr = 0x0216C3E8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C4B8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C4C0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C4D0 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C4E8 +mode = "arm" +kind = "runtime_observed" +# hits = 10236 + +[[entry_point]] +addr = 0x0216C524 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216C528 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216C5FC +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216C618 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C630 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216C75C +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0216C804 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C810 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C8D8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216C924 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0216CA24 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216CA58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CB50 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216CB7C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216CC44 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CCFC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CD38 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CD4C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216CD50 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CD9C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216CEB4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0216D028 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D20C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D234 +mode = "arm" +kind = "runtime_observed" +# hits = 10236 + +[[entry_point]] +addr = 0x0216D398 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D3F8 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D414 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D878 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216DA88 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0217AA14 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217AA38 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0217AC3C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0217ADB8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217AE00 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217AF00 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217AF3C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0217AF4C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0217AFE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B084 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B110 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B1A4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0217B3DC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0217B40C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0217B470 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0217B48C +mode = "arm" +kind = "runtime_observed" +# hits = 13 + +[[entry_point]] +addr = 0x0217B4B4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B4BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B510 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B518 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0217B538 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0217B564 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0217B580 +mode = "arm" +kind = "runtime_observed" +# hits = 3 diff --git a/config/mph_arm9_ov008.toml b/config/mph_arm9_ov008.toml new file mode 100644 index 0000000..969062a --- /dev/null +++ b/config/mph_arm9_ov008.toml @@ -0,0 +1,190 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 8" +id = "mph_amhe0_arm9_ov008" +load_address = 0x0212C600 +size = 0x000063C0 +entry_pc = 0x0212DC54 +authoritative_entry_points = false + +[identity] +sha1 = "a3abc6c9d468ca0a362e71f0d4a4a7c582395093" + +[[entry_point]] +addr = 0x0212DC54 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212DCE4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212DD70 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0212DF5C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E060 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0212E210 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E2BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E3B4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212E740 +mode = "arm" +kind = "runtime_observed" +# hits = 32339 + +[[entry_point]] +addr = 0x0212E744 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0212E970 +mode = "arm" +kind = "runtime_observed" +# hits = 32307 + +[[entry_point]] +addr = 0x0212EA18 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212EBAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212EBB0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ECAC +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0212ED0C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0212ED48 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0212EE40 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02131244 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02131258 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02131278 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131288 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0213198C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02131998 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02131A70 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131AD4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02131B74 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02131BD8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02131DE0 +mode = "arm" +kind = "runtime_observed" +# hits = 20 diff --git a/config/mph_arm9_ov009.toml b/config/mph_arm9_ov009.toml new file mode 100644 index 0000000..f0779a6 --- /dev/null +++ b/config/mph_arm9_ov009.toml @@ -0,0 +1,2698 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 9" +id = "mph_amhe0_arm9_ov009" +load_address = 0x02133060 +size = 0x00019860 +entry_pc = 0x0213413C +authoritative_entry_points = false + +[identity] +sha1 = "d094aad7e063aeb5a2856b695d8e7916609b5ded" + +[[entry_point]] +addr = 0x0213413C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02134158 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021341C4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02134324 +mode = "arm" +kind = "runtime_observed" +# hits = 93 + +[[entry_point]] +addr = 0x0213438C +mode = "arm" +kind = "runtime_observed" +# hits = 96819 + +[[entry_point]] +addr = 0x021343BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021343F4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02134434 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134494 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021344E0 +mode = "arm" +kind = "runtime_observed" +# hits = 74 + +[[entry_point]] +addr = 0x021345B4 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x0213468C +mode = "arm" +kind = "runtime_observed" +# hits = 64767 + +[[entry_point]] +addr = 0x0213469C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021346B0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x021346C8 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x021346E0 +mode = "arm" +kind = "runtime_observed" +# hits = 127921 + +[[entry_point]] +addr = 0x02134704 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134718 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0213478C +mode = "arm" +kind = "runtime_observed" +# hits = 130504 + +[[entry_point]] +addr = 0x021348C0 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x021348D8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02134934 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02134990 +mode = "arm" +kind = "runtime_observed" +# hits = 899 + +[[entry_point]] +addr = 0x02134A90 +mode = "arm" +kind = "runtime_observed" +# hits = 1428 + +[[entry_point]] +addr = 0x02134AB8 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02134B00 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02134B34 +mode = "arm" +kind = "runtime_observed" +# hits = 24778 + +[[entry_point]] +addr = 0x02134B44 +mode = "arm" +kind = "runtime_observed" +# hits = 54421 + +[[entry_point]] +addr = 0x02134B78 +mode = "arm" +kind = "runtime_observed" +# hits = 6524 + +[[entry_point]] +addr = 0x02134B7C +mode = "arm" +kind = "runtime_observed" +# hits = 627 + +[[entry_point]] +addr = 0x02134B80 +mode = "arm" +kind = "runtime_observed" +# hits = 6711 + +[[entry_point]] +addr = 0x02134B8C +mode = "arm" +kind = "runtime_observed" +# hits = 189 + +[[entry_point]] +addr = 0x02134BC0 +mode = "arm" +kind = "runtime_observed" +# hits = 272 + +[[entry_point]] +addr = 0x02134BD4 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02134BF8 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02134C50 +mode = "arm" +kind = "runtime_observed" +# hits = 10720 + +[[entry_point]] +addr = 0x02134C54 +mode = "arm" +kind = "runtime_observed" +# hits = 4527 + +[[entry_point]] +addr = 0x02134C5C +mode = "arm" +kind = "runtime_observed" +# hits = 135 + +[[entry_point]] +addr = 0x02134C64 +mode = "arm" +kind = "runtime_observed" +# hits = 1020 + +[[entry_point]] +addr = 0x02134C80 +mode = "arm" +kind = "runtime_observed" +# hits = 1649 + +[[entry_point]] +addr = 0x02134C84 +mode = "arm" +kind = "runtime_observed" +# hits = 829 + +[[entry_point]] +addr = 0x02134C9C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02134E68 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02135020 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02135108 +mode = "arm" +kind = "runtime_observed" +# hits = 28 + +[[entry_point]] +addr = 0x02135154 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x021351B4 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x021351CC +mode = "arm" +kind = "runtime_observed" +# hits = 60 + +[[entry_point]] +addr = 0x02135408 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213548C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021354E4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02135524 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02135588 +mode = "arm" +kind = "runtime_observed" +# hits = 27 + +[[entry_point]] +addr = 0x0213560C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02135898 +mode = "arm" +kind = "runtime_observed" +# hits = 19 + +[[entry_point]] +addr = 0x02135950 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x02135BCC +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02135C20 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02135C88 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02135DAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02135F20 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02136014 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02136034 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02136074 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0213613C +mode = "arm" +kind = "runtime_observed" +# hits = 756 + +[[entry_point]] +addr = 0x021361C8 +mode = "arm" +kind = "runtime_observed" +# hits = 92 + +[[entry_point]] +addr = 0x021361F4 +mode = "arm" +kind = "runtime_observed" +# hits = 51352 + +[[entry_point]] +addr = 0x02136254 +mode = "arm" +kind = "runtime_observed" +# hits = 70 + +[[entry_point]] +addr = 0x021362D8 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x02136304 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02136358 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021363D4 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02136428 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213645C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021364A0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021364D4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213653C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213655C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213658C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213660C +mode = "arm" +kind = "runtime_observed" +# hits = 4302 + +[[entry_point]] +addr = 0x02136668 +mode = "arm" +kind = "runtime_observed" +# hits = 8160 + +[[entry_point]] +addr = 0x02136748 +mode = "arm" +kind = "runtime_observed" +# hits = 790 + +[[entry_point]] +addr = 0x021367AC +mode = "arm" +kind = "runtime_observed" +# hits = 1044 + +[[entry_point]] +addr = 0x0213688C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213693C +mode = "arm" +kind = "runtime_observed" +# hits = 10720 + +[[entry_point]] +addr = 0x0213696C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02136A34 +mode = "arm" +kind = "runtime_observed" +# hits = 2753 + +[[entry_point]] +addr = 0x02136A80 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x02136AC0 +mode = "arm" +kind = "runtime_observed" +# hits = 2753 + +[[entry_point]] +addr = 0x02136B10 +mode = "arm" +kind = "runtime_observed" +# hits = 84350 + +[[entry_point]] +addr = 0x02136B20 +mode = "arm" +kind = "runtime_observed" +# hits = 23792 + +[[entry_point]] +addr = 0x02136B24 +mode = "arm" +kind = "runtime_observed" +# hits = 7730 + +[[entry_point]] +addr = 0x02136B44 +mode = "arm" +kind = "runtime_observed" +# hits = 9064 + +[[entry_point]] +addr = 0x02136B50 +mode = "arm" +kind = "runtime_observed" +# hits = 1404 + +[[entry_point]] +addr = 0x02136B54 +mode = "arm" +kind = "runtime_observed" +# hits = 26214 + +[[entry_point]] +addr = 0x02136B58 +mode = "arm" +kind = "runtime_observed" +# hits = 359 + +[[entry_point]] +addr = 0x02136B5C +mode = "arm" +kind = "runtime_observed" +# hits = 9627 + +[[entry_point]] +addr = 0x02136B70 +mode = "arm" +kind = "runtime_observed" +# hits = 8913 + +[[entry_point]] +addr = 0x02136C1C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02136C3C +mode = "arm" +kind = "runtime_observed" +# hits = 135954 + +[[entry_point]] +addr = 0x02136C7C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x02136CA0 +mode = "arm" +kind = "runtime_observed" +# hits = 18898 + +[[entry_point]] +addr = 0x02136EE4 +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02136F74 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02136FE8 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02137084 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021371B4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02137254 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02137284 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021372AC +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x0213786C +mode = "arm" +kind = "runtime_observed" +# hits = 23598 + +[[entry_point]] +addr = 0x021378DC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213790C +mode = "arm" +kind = "runtime_observed" +# hits = 17106 + +[[entry_point]] +addr = 0x02137BE8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02137DE4 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02137F6C +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x021380BC +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02138168 +mode = "arm" +kind = "runtime_observed" +# hits = 604 + +[[entry_point]] +addr = 0x021383E8 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x021387FC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213881C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213883C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213885C +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02138868 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138874 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138880 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021389BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021389F0 +mode = "arm" +kind = "runtime_observed" +# hits = 5338 + +[[entry_point]] +addr = 0x02138AE0 +mode = "arm" +kind = "runtime_observed" +# hits = 272 + +[[entry_point]] +addr = 0x02138C8C +mode = "arm" +kind = "runtime_observed" +# hits = 5081 + +[[entry_point]] +addr = 0x02138E50 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x02138E58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138E60 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02138F28 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02138F38 +mode = "arm" +kind = "runtime_observed" +# hits = 31749 + +[[entry_point]] +addr = 0x02139054 +mode = "arm" +kind = "runtime_observed" +# hits = 31749 + +[[entry_point]] +addr = 0x0213911C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02139274 +mode = "arm" +kind = "runtime_observed" +# hits = 620 + +[[entry_point]] +addr = 0x021394E4 +mode = "arm" +kind = "runtime_observed" +# hits = 272 + +[[entry_point]] +addr = 0x021395E8 +mode = "arm" +kind = "runtime_observed" +# hits = 862 + +[[entry_point]] +addr = 0x0213966C +mode = "arm" +kind = "runtime_observed" +# hits = 9366 + +[[entry_point]] +addr = 0x021398B0 +mode = "arm" +kind = "runtime_observed" +# hits = 9366 + +[[entry_point]] +addr = 0x021398DC +mode = "arm" +kind = "runtime_observed" +# hits = 53277 + +[[entry_point]] +addr = 0x0213998C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021399C8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02139A30 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x02139A34 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x02139A38 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02139A58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02139A74 +mode = "arm" +kind = "runtime_observed" +# hits = 1524 + +[[entry_point]] +addr = 0x02139A78 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02139AA0 +mode = "arm" +kind = "runtime_observed" +# hits = 13204 + +[[entry_point]] +addr = 0x02139AD8 +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x02139D40 +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x02139D5C +mode = "arm" +kind = "runtime_observed" +# hits = 3684 + +[[entry_point]] +addr = 0x02139D8C +mode = "arm" +kind = "runtime_observed" +# hits = 4768 + +[[entry_point]] +addr = 0x0213A1E0 +mode = "arm" +kind = "runtime_observed" +# hits = 3200 + +[[entry_point]] +addr = 0x0213A204 +mode = "arm" +kind = "runtime_observed" +# hits = 4542 + +[[entry_point]] +addr = 0x0213A2E8 +mode = "arm" +kind = "runtime_observed" +# hits = 57 + +[[entry_point]] +addr = 0x0213A318 +mode = "arm" +kind = "runtime_observed" +# hits = 3012 + +[[entry_point]] +addr = 0x0213A348 +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0213A378 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x0213A390 +mode = "arm" +kind = "runtime_observed" +# hits = 3206 + +[[entry_point]] +addr = 0x0213A3A8 +mode = "arm" +kind = "runtime_observed" +# hits = 26 + +[[entry_point]] +addr = 0x0213A480 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0213A4B0 +mode = "arm" +kind = "runtime_observed" +# hits = 3240 + +[[entry_point]] +addr = 0x0213A4C8 +mode = "arm" +kind = "runtime_observed" +# hits = 9682 + +[[entry_point]] +addr = 0x0213A4F8 +mode = "arm" +kind = "runtime_observed" +# hits = 56 + +[[entry_point]] +addr = 0x0213A510 +mode = "arm" +kind = "runtime_observed" +# hits = 3240 + +[[entry_point]] +addr = 0x0213A528 +mode = "arm" +kind = "runtime_observed" +# hits = 10047 + +[[entry_point]] +addr = 0x0213A540 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0213A558 +mode = "arm" +kind = "runtime_observed" +# hits = 46 + +[[entry_point]] +addr = 0x0213A570 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0213A588 +mode = "arm" +kind = "runtime_observed" +# hits = 46 + +[[entry_point]] +addr = 0x0213A5B8 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x0213A5E8 +mode = "arm" +kind = "runtime_observed" +# hits = 52 + +[[entry_point]] +addr = 0x0213A618 +mode = "arm" +kind = "runtime_observed" +# hits = 4376 + +[[entry_point]] +addr = 0x0213AE98 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213AFB4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213AFB8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213AFBC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213AFD0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213AFD4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213B064 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213B104 +mode = "arm" +kind = "runtime_observed" +# hits = 730 + +[[entry_point]] +addr = 0x0213B128 +mode = "arm" +kind = "runtime_observed" +# hits = 4896 + +[[entry_point]] +addr = 0x0213B318 +mode = "arm" +kind = "runtime_observed" +# hits = 447 + +[[entry_point]] +addr = 0x0213B340 +mode = "arm" +kind = "runtime_observed" +# hits = 134 + +[[entry_point]] +addr = 0x0213B344 +mode = "arm" +kind = "runtime_observed" +# hits = 271 + +[[entry_point]] +addr = 0x0213B354 +mode = "arm" +kind = "runtime_observed" +# hits = 42 + +[[entry_point]] +addr = 0x0213B43C +mode = "arm" +kind = "runtime_observed" +# hits = 10936 + +[[entry_point]] +addr = 0x0213B510 +mode = "arm" +kind = "runtime_observed" +# hits = 5162 + +[[entry_point]] +addr = 0x0213B538 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213B53C +mode = "arm" +kind = "runtime_observed" +# hits = 2038 + +[[entry_point]] +addr = 0x0213B54C +mode = "arm" +kind = "runtime_observed" +# hits = 3123 + +[[entry_point]] +addr = 0x0213B750 +mode = "arm" +kind = "runtime_observed" +# hits = 5176 + +[[entry_point]] +addr = 0x0213B774 +mode = "arm" +kind = "runtime_observed" +# hits = 5793 + +[[entry_point]] +addr = 0x0213B78C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0213B790 +mode = "arm" +kind = "runtime_observed" +# hits = 2361 + +[[entry_point]] +addr = 0x0213B7A0 +mode = "arm" +kind = "runtime_observed" +# hits = 3429 + +[[entry_point]] +addr = 0x0213B96C +mode = "arm" +kind = "runtime_observed" +# hits = 11251 + +[[entry_point]] +addr = 0x0213BCCC +mode = "arm" +kind = "runtime_observed" +# hits = 3418 + +[[entry_point]] +addr = 0x0213BE04 +mode = "arm" +kind = "runtime_observed" +# hits = 1398 + +[[entry_point]] +addr = 0x0213BE6C +mode = "arm" +kind = "runtime_observed" +# hits = 3425 + +[[entry_point]] +addr = 0x0213BEB0 +mode = "arm" +kind = "runtime_observed" +# hits = 11383 + +[[entry_point]] +addr = 0x0213BEC4 +mode = "arm" +kind = "runtime_observed" +# hits = 1327 + +[[entry_point]] +addr = 0x0213BEE8 +mode = "arm" +kind = "runtime_observed" +# hits = 4527 + +[[entry_point]] +addr = 0x0213BFF0 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x0213C024 +mode = "arm" +kind = "runtime_observed" +# hits = 10711 + +[[entry_point]] +addr = 0x0213C178 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0213C18C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0213C198 +mode = "arm" +kind = "runtime_observed" +# hits = 3200 + +[[entry_point]] +addr = 0x0213C328 +mode = "arm" +kind = "runtime_observed" +# hits = 5828 + +[[entry_point]] +addr = 0x0213C34C +mode = "arm" +kind = "runtime_observed" +# hits = 5942 + +[[entry_point]] +addr = 0x0213C3D4 +mode = "arm" +kind = "runtime_observed" +# hits = 3404 + +[[entry_point]] +addr = 0x0213C3E4 +mode = "arm" +kind = "runtime_observed" +# hits = 397 + +[[entry_point]] +addr = 0x0213C400 +mode = "arm" +kind = "runtime_observed" +# hits = 4057 + +[[entry_point]] +addr = 0x0213C41C +mode = "arm" +kind = "runtime_observed" +# hits = 4057 + +[[entry_point]] +addr = 0x0213C468 +mode = "arm" +kind = "runtime_observed" +# hits = 22513 + +[[entry_point]] +addr = 0x0213C484 +mode = "arm" +kind = "runtime_observed" +# hits = 22704 + +[[entry_point]] +addr = 0x0213C504 +mode = "arm" +kind = "runtime_observed" +# hits = 2826 + +[[entry_point]] +addr = 0x0213C954 +mode = "arm" +kind = "runtime_observed" +# hits = 11569 + +[[entry_point]] +addr = 0x0213C960 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213C970 +mode = "arm" +kind = "runtime_observed" +# hits = 18213 + +[[entry_point]] +addr = 0x0213C980 +mode = "arm" +kind = "runtime_observed" +# hits = 8798 + +[[entry_point]] +addr = 0x0213C9A4 +mode = "arm" +kind = "runtime_observed" +# hits = 18960 + +[[entry_point]] +addr = 0x0213CA48 +mode = "arm" +kind = "runtime_observed" +# hits = 2556 + +[[entry_point]] +addr = 0x0213CA64 +mode = "arm" +kind = "runtime_observed" +# hits = 6400 + +[[entry_point]] +addr = 0x0213CAAC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213CC10 +mode = "arm" +kind = "runtime_observed" +# hits = 916 + +[[entry_point]] +addr = 0x0213CC7C +mode = "arm" +kind = "runtime_observed" +# hits = 4973 + +[[entry_point]] +addr = 0x0213CCBC +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0213CCC4 +mode = "arm" +kind = "runtime_observed" +# hits = 70382 + +[[entry_point]] +addr = 0x0213CCE4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0213D6B4 +mode = "arm" +kind = "runtime_observed" +# hits = 43 + +[[entry_point]] +addr = 0x0213DE84 +mode = "arm" +kind = "runtime_observed" +# hits = 53485 + +[[entry_point]] +addr = 0x0213DE88 +mode = "arm" +kind = "runtime_observed" +# hits = 16854 + +[[entry_point]] +addr = 0x0213DF58 +mode = "arm" +kind = "runtime_observed" +# hits = 18764 + +[[entry_point]] +addr = 0x0213DFB8 +mode = "arm" +kind = "runtime_observed" +# hits = 4624 + +[[entry_point]] +addr = 0x0213E014 +mode = "arm" +kind = "runtime_observed" +# hits = 13752 + +[[entry_point]] +addr = 0x0213E06C +mode = "arm" +kind = "runtime_observed" +# hits = 13752 + +[[entry_point]] +addr = 0x0213E114 +mode = "arm" +kind = "runtime_observed" +# hits = 268 + +[[entry_point]] +addr = 0x0213E1EC +mode = "arm" +kind = "runtime_observed" +# hits = 65460 + +[[entry_point]] +addr = 0x0213E230 +mode = "arm" +kind = "runtime_observed" +# hits = 306136 + +[[entry_point]] +addr = 0x0213E234 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213E24C +mode = "arm" +kind = "runtime_observed" +# hits = 15664 + +[[entry_point]] +addr = 0x0213E370 +mode = "arm" +kind = "runtime_observed" +# hits = 15664 + +[[entry_point]] +addr = 0x0213E52C +mode = "arm" +kind = "runtime_observed" +# hits = 15664 + +[[entry_point]] +addr = 0x0213E548 +mode = "arm" +kind = "runtime_observed" +# hits = 2167 + +[[entry_point]] +addr = 0x0213E58C +mode = "arm" +kind = "runtime_observed" +# hits = 18722 + +[[entry_point]] +addr = 0x0213E5A4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213E6FC +mode = "arm" +kind = "runtime_observed" +# hits = 24 + +[[entry_point]] +addr = 0x0213E768 +mode = "arm" +kind = "runtime_observed" +# hits = 7124 + +[[entry_point]] +addr = 0x0213E7D8 +mode = "arm" +kind = "runtime_observed" +# hits = 180 + +[[entry_point]] +addr = 0x0213E894 +mode = "arm" +kind = "runtime_observed" +# hits = 42701 + +[[entry_point]] +addr = 0x0213E8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x0213E8DC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0213E90C +mode = "arm" +kind = "runtime_observed" +# hits = 786 + +[[entry_point]] +addr = 0x0213E91C +mode = "arm" +kind = "runtime_observed" +# hits = 12215 + +[[entry_point]] +addr = 0x0213E960 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213E984 +mode = "arm" +kind = "runtime_observed" +# hits = 31 + +[[entry_point]] +addr = 0x0213E998 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0213EA8C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213EAB0 +mode = "arm" +kind = "runtime_observed" +# hits = 93535 + +[[entry_point]] +addr = 0x0213EACC +mode = "arm" +kind = "runtime_observed" +# hits = 69 + +[[entry_point]] +addr = 0x0213ED80 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0213ED98 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0213EDB0 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0213EE8C +mode = "arm" +kind = "runtime_observed" +# hits = 96819 + +[[entry_point]] +addr = 0x0213EEA4 +mode = "arm" +kind = "runtime_observed" +# hits = 377150 + +[[entry_point]] +addr = 0x0213EEE0 +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x0213EF6C +mode = "arm" +kind = "runtime_observed" +# hits = 169 + +[[entry_point]] +addr = 0x0213F44C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0213F4D4 +mode = "arm" +kind = "runtime_observed" +# hits = 12076 + +[[entry_point]] +addr = 0x0213F4F0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213F7A4 +mode = "arm" +kind = "runtime_observed" +# hits = 11285 + +[[entry_point]] +addr = 0x0213F9C4 +mode = "arm" +kind = "runtime_observed" +# hits = 12389 + +[[entry_point]] +addr = 0x0213FE98 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0213FF58 +mode = "arm" +kind = "runtime_observed" +# hits = 8841 + +[[entry_point]] +addr = 0x02140168 +mode = "arm" +kind = "runtime_observed" +# hits = 163 + +[[entry_point]] +addr = 0x02140240 +mode = "arm" +kind = "runtime_observed" +# hits = 12116 + +[[entry_point]] +addr = 0x0214029C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02140500 +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x021406EC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02140704 +mode = "arm" +kind = "runtime_observed" +# hits = 6049 + +[[entry_point]] +addr = 0x021407D0 +mode = "arm" +kind = "runtime_observed" +# hits = 12124 + +[[entry_point]] +addr = 0x0214081C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02140834 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0214089C +mode = "arm" +kind = "runtime_observed" +# hits = 10 + +[[entry_point]] +addr = 0x021408AC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02140D0C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02140D18 +mode = "arm" +kind = "runtime_observed" +# hits = 5862 + +[[entry_point]] +addr = 0x02140D34 +mode = "arm" +kind = "runtime_observed" +# hits = 12102 + +[[entry_point]] +addr = 0x02140D98 +mode = "arm" +kind = "runtime_observed" +# hits = 776 + +[[entry_point]] +addr = 0x02140E74 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02141050 +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x02141114 +mode = "arm" +kind = "runtime_observed" +# hits = 5597 + +[[entry_point]] +addr = 0x0214144C +mode = "arm" +kind = "runtime_observed" +# hits = 268 + +[[entry_point]] +addr = 0x021414C4 +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x021414E4 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x021414FC +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02141548 +mode = "arm" +kind = "runtime_observed" +# hits = 14055 + +[[entry_point]] +addr = 0x0214180C +mode = "arm" +kind = "runtime_observed" +# hits = 120664 + +[[entry_point]] +addr = 0x021418A0 +mode = "arm" +kind = "runtime_observed" +# hits = 241286 + +[[entry_point]] +addr = 0x02141964 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02141984 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02141A30 +mode = "arm" +kind = "runtime_observed" +# hits = 270 + +[[entry_point]] +addr = 0x02141A58 +mode = "arm" +kind = "runtime_observed" +# hits = 120664 + +[[entry_point]] +addr = 0x02141BFC +mode = "arm" +kind = "runtime_observed" +# hits = 5118 + +[[entry_point]] +addr = 0x02141EC0 +mode = "arm" +kind = "runtime_observed" +# hits = 1320 + +[[entry_point]] +addr = 0x02141EFC +mode = "arm" +kind = "runtime_observed" +# hits = 217 + +[[entry_point]] +addr = 0x02141F28 +mode = "arm" +kind = "runtime_observed" +# hits = 8295 + +[[entry_point]] +addr = 0x02142258 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x021423AC +mode = "arm" +kind = "runtime_observed" +# hits = 11385 + +[[entry_point]] +addr = 0x0214253C +mode = "arm" +kind = "runtime_observed" +# hits = 12117 + +[[entry_point]] +addr = 0x021425F4 +mode = "arm" +kind = "runtime_observed" +# hits = 43 + +[[entry_point]] +addr = 0x0214266C +mode = "arm" +kind = "runtime_observed" +# hits = 38259 + +[[entry_point]] +addr = 0x021427A8 +mode = "arm" +kind = "runtime_observed" +# hits = 38259 + +[[entry_point]] +addr = 0x02142AAC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02142B18 +mode = "arm" +kind = "runtime_observed" +# hits = 9323 + +[[entry_point]] +addr = 0x02142B68 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02142BC4 +mode = "arm" +kind = "runtime_observed" +# hits = 34744 + +[[entry_point]] +addr = 0x02142BCC +mode = "arm" +kind = "runtime_observed" +# hits = 69488 + +[[entry_point]] +addr = 0x02142BE8 +mode = "arm" +kind = "runtime_observed" +# hits = 137474 + +[[entry_point]] +addr = 0x02142C4C +mode = "arm" +kind = "runtime_observed" +# hits = 3390 + +[[entry_point]] +addr = 0x02142C68 +mode = "arm" +kind = "runtime_observed" +# hits = 954 + +[[entry_point]] +addr = 0x02142C6C +mode = "arm" +kind = "runtime_observed" +# hits = 1436 + +[[entry_point]] +addr = 0x02142C70 +mode = "arm" +kind = "runtime_observed" +# hits = 184 + +[[entry_point]] +addr = 0x02142C80 +mode = "arm" +kind = "runtime_observed" +# hits = 816 + +[[entry_point]] +addr = 0x02142E80 +mode = "arm" +kind = "runtime_observed" +# hits = 5040 + +[[entry_point]] +addr = 0x02142ED4 +mode = "arm" +kind = "runtime_observed" +# hits = 1159 + +[[entry_point]] +addr = 0x02142ED8 +mode = "arm" +kind = "runtime_observed" +# hits = 1669 + +[[entry_point]] +addr = 0x02142EDC +mode = "arm" +kind = "runtime_observed" +# hits = 1080 + +[[entry_point]] +addr = 0x02142EEC +mode = "arm" +kind = "runtime_observed" +# hits = 1132 + +[[entry_point]] +addr = 0x02143C28 +mode = "arm" +kind = "runtime_observed" +# hits = 37 + +[[entry_point]] +addr = 0x02143FC8 +mode = "arm" +kind = "runtime_observed" +# hits = 9323 + +[[entry_point]] +addr = 0x02144AB0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02144B78 +mode = "arm" +kind = "runtime_observed" +# hits = 9323 + +[[entry_point]] +addr = 0x02144C4C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02144FE0 +mode = "arm" +kind = "runtime_observed" +# hits = 1651 + +[[entry_point]] +addr = 0x02145054 +mode = "arm" +kind = "runtime_observed" +# hits = 1537 + +[[entry_point]] +addr = 0x021451C0 +mode = "arm" +kind = "runtime_observed" +# hits = 1523 + +[[entry_point]] +addr = 0x02145368 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02145718 +mode = "arm" +kind = "runtime_observed" +# hits = 732 + +[[entry_point]] +addr = 0x0214571C +mode = "arm" +kind = "runtime_observed" +# hits = 790 + +[[entry_point]] +addr = 0x0214659C +mode = "arm" +kind = "runtime_observed" +# hits = 790 + +[[entry_point]] +addr = 0x02146604 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02146608 +mode = "arm" +kind = "runtime_observed" +# hits = 57 + +[[entry_point]] +addr = 0x02146620 +mode = "arm" +kind = "runtime_observed" +# hits = 15 + +[[entry_point]] +addr = 0x02146630 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02146638 +mode = "arm" +kind = "runtime_observed" +# hits = 16 + +[[entry_point]] +addr = 0x0214663C +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02146658 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x0214665C +mode = "arm" +kind = "runtime_observed" +# hits = 46 + +[[entry_point]] +addr = 0x02146660 +mode = "arm" +kind = "runtime_observed" +# hits = 11 + +[[entry_point]] +addr = 0x02146664 +mode = "arm" +kind = "runtime_observed" +# hits = 42 + +[[entry_point]] +addr = 0x02146668 +mode = "arm" +kind = "runtime_observed" +# hits = 34 + +[[entry_point]] +addr = 0x02146670 +mode = "arm" +kind = "runtime_observed" +# hits = 30 + +[[entry_point]] +addr = 0x02146680 +mode = "arm" +kind = "runtime_observed" +# hits = 51 + +[[entry_point]] +addr = 0x02146684 +mode = "arm" +kind = "runtime_observed" +# hits = 40 + +[[entry_point]] +addr = 0x021466B0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021466BC +mode = "arm" +kind = "runtime_observed" +# hits = 7 + +[[entry_point]] +addr = 0x021466D0 +mode = "arm" +kind = "runtime_observed" +# hits = 101 + +[[entry_point]] +addr = 0x021466D4 +mode = "arm" +kind = "runtime_observed" +# hits = 92 + +[[entry_point]] +addr = 0x0214672C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x02146730 +mode = "arm" +kind = "runtime_observed" +# hits = 38 + +[[entry_point]] +addr = 0x02146768 +mode = "arm" +kind = "runtime_observed" +# hits = 49 + +[[entry_point]] +addr = 0x02146794 +mode = "arm" +kind = "runtime_observed" +# hits = 31 + +[[entry_point]] +addr = 0x02146798 +mode = "arm" +kind = "runtime_observed" +# hits = 62 + +[[entry_point]] +addr = 0x0214679C +mode = "arm" +kind = "runtime_observed" +# hits = 14 + +[[entry_point]] +addr = 0x021467A4 +mode = "arm" +kind = "runtime_observed" +# hits = 20 + +[[entry_point]] +addr = 0x021474C0 +mode = "arm" +kind = "runtime_observed" +# hits = 70382 + +[[entry_point]] +addr = 0x021474E8 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021474F4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0214754C +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x0214755C +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x021475B0 +mode = "arm" +kind = "runtime_observed" +# hits = 1523 + +[[entry_point]] +addr = 0x02147654 +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x02147660 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x02147678 +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x021477D4 +mode = "arm" +kind = "runtime_observed" +# hits = 35732 + +[[entry_point]] +addr = 0x021479BC +mode = "arm" +kind = "runtime_observed" +# hits = 84734 + +[[entry_point]] +addr = 0x02147A8C +mode = "arm" +kind = "runtime_observed" +# hits = 23212 + +[[entry_point]] +addr = 0x02147DF4 +mode = "arm" +kind = "runtime_observed" +# hits = 23212 + +[[entry_point]] +addr = 0x02147EF0 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x02147EFC +mode = "arm" +kind = "runtime_observed" +# hits = 17105 + +[[entry_point]] +addr = 0x02147F68 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0214807C +mode = "arm" +kind = "runtime_observed" +# hits = 71905 + +[[entry_point]] +addr = 0x021480BC +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02148624 +mode = "arm" +kind = "runtime_observed" +# hits = 57 + +[[entry_point]] +addr = 0x021486AC +mode = "arm" +kind = "runtime_observed" +# hits = 17127 + +[[entry_point]] +addr = 0x02148C30 +mode = "arm" +kind = "runtime_observed" +# hits = 1339 + +[[entry_point]] +addr = 0x02148C64 +mode = "arm" +kind = "runtime_observed" +# hits = 19 + +[[entry_point]] +addr = 0x02148C88 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x02148C98 +mode = "arm" +kind = "runtime_observed" +# hits = 18 + +[[entry_point]] +addr = 0x02148EA4 +mode = "arm" +kind = "runtime_observed" +# hits = 70 + +[[entry_point]] +addr = 0x02148F18 +mode = "arm" +kind = "runtime_observed" +# hits = 1571 + +[[entry_point]] +addr = 0x02149018 +mode = "arm" +kind = "runtime_observed" +# hits = 328 + +[[entry_point]] +addr = 0x02149030 +mode = "arm" +kind = "runtime_observed" +# hits = 100 + +[[entry_point]] +addr = 0x02149090 +mode = "arm" +kind = "runtime_observed" +# hits = 411 + +[[entry_point]] +addr = 0x021490A8 +mode = "arm" +kind = "runtime_observed" +# hits = 161 + +[[entry_point]] +addr = 0x021490D8 +mode = "arm" +kind = "runtime_observed" +# hits = 3572 + +[[entry_point]] +addr = 0x0214917C +mode = "arm" +kind = "runtime_observed" +# hits = 168 + +[[entry_point]] +addr = 0x021494D8 +mode = "arm" +kind = "runtime_observed" +# hits = 126 + +[[entry_point]] +addr = 0x0214957C +mode = "arm" +kind = "runtime_observed" +# hits = 303 + +[[entry_point]] +addr = 0x02149580 +mode = "arm" +kind = "runtime_observed" +# hits = 118 + +[[entry_point]] +addr = 0x02149590 +mode = "arm" +kind = "runtime_observed" +# hits = 26 + +[[entry_point]] +addr = 0x021495A0 +mode = "arm" +kind = "runtime_observed" +# hits = 103 + +[[entry_point]] +addr = 0x02149AD0 +mode = "arm" +kind = "runtime_observed" +# hits = 126 + +[[entry_point]] +addr = 0x02149AE0 +mode = "arm" +kind = "runtime_observed" +# hits = 47 + +[[entry_point]] +addr = 0x02149AE4 +mode = "arm" +kind = "runtime_observed" +# hits = 39 + +[[entry_point]] +addr = 0x02149AF4 +mode = "arm" +kind = "runtime_observed" +# hits = 40 + +[[entry_point]] +addr = 0x02149B50 +mode = "arm" +kind = "runtime_observed" +# hits = 23040 + +[[entry_point]] +addr = 0x02149B5C +mode = "arm" +kind = "runtime_observed" +# hits = 6404 + +[[entry_point]] +addr = 0x02149B60 +mode = "arm" +kind = "runtime_observed" +# hits = 5568 + +[[entry_point]] +addr = 0x02149B64 +mode = "arm" +kind = "runtime_observed" +# hits = 5199 + +[[entry_point]] +addr = 0x02149B68 +mode = "arm" +kind = "runtime_observed" +# hits = 168 + +[[entry_point]] +addr = 0x02149B6C +mode = "arm" +kind = "runtime_observed" +# hits = 168 + +[[entry_point]] +addr = 0x02149B70 +mode = "arm" +kind = "runtime_observed" +# hits = 168 + +[[entry_point]] +addr = 0x02149B74 +mode = "arm" +kind = "runtime_observed" +# hits = 5029 + +[[entry_point]] +addr = 0x02149B78 +mode = "arm" +kind = "runtime_observed" +# hits = 168 + +[[entry_point]] +addr = 0x02149B7C +mode = "arm" +kind = "runtime_observed" +# hits = 168 + +[[entry_point]] +addr = 0x02149BD0 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0214A390 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0214A400 +mode = "arm" +kind = "runtime_observed" +# hits = 144 + +[[entry_point]] +addr = 0x0214A548 +mode = "arm" +kind = "runtime_observed" +# hits = 84 + +[[entry_point]] +addr = 0x0214A5DC +mode = "arm" +kind = "runtime_observed" +# hits = 132 + +[[entry_point]] +addr = 0x0214A650 +mode = "arm" +kind = "runtime_observed" +# hits = 264 + +[[entry_point]] +addr = 0x0214A72C +mode = "arm" +kind = "runtime_observed" +# hits = 144 + +[[entry_point]] +addr = 0x0214A7E8 +mode = "arm" +kind = "runtime_observed" +# hits = 252 + +[[entry_point]] +addr = 0x0214A90C +mode = "arm" +kind = "runtime_observed" +# hits = 686 + +[[entry_point]] +addr = 0x0214A95C +mode = "arm" +kind = "runtime_observed" +# hits = 15225 + +[[entry_point]] +addr = 0x0214A978 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0214A9A4 +mode = "arm" +kind = "runtime_observed" +# hits = 15225 + +[[entry_point]] +addr = 0x0214AB94 +mode = "arm" +kind = "runtime_observed" +# hits = 2009 + +[[entry_point]] +addr = 0x0214AC50 +mode = "arm" +kind = "runtime_observed" +# hits = 604 + +[[entry_point]] +addr = 0x0214ACF0 +mode = "arm" +kind = "runtime_observed" +# hits = 15225 + +[[entry_point]] +addr = 0x0214AF38 +mode = "arm" +kind = "runtime_observed" +# hits = 40431 + +[[entry_point]] +addr = 0x0214AF54 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0214AF90 +mode = "arm" +kind = "runtime_observed" +# hits = 231 + +[[entry_point]] +addr = 0x0214B1BC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0214B3C4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0214B510 +mode = "arm" +kind = "runtime_observed" +# hits = 18825 + +[[entry_point]] +addr = 0x0214B620 +mode = "arm" +kind = "runtime_observed" +# hits = 17106 + +[[entry_point]] +addr = 0x0214B688 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0214B6A4 +mode = "arm" +kind = "runtime_observed" +# hits = 8 + +[[entry_point]] +addr = 0x0214B7A4 +mode = "arm" +kind = "runtime_observed" +# hits = 16 diff --git a/config/mph_arm9_ov010.toml b/config/mph_arm9_ov010.toml new file mode 100644 index 0000000..6c090cd --- /dev/null +++ b/config/mph_arm9_ov010.toml @@ -0,0 +1,142 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 10" +id = "mph_amhe0_arm9_ov010" +load_address = 0x0214C940 +size = 0x0001C1E0 +entry_pc = 0x0214E504 +authoritative_entry_points = false + +[identity] +sha1 = "d22d5c9a22f870c62f13b2122d7d62a485f4a553" + +[[entry_point]] +addr = 0x0214E504 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0214FC34 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02152608 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0215296C +mode = "arm" +kind = "runtime_observed" +# hits = 24 + +[[entry_point]] +addr = 0x021536F0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021537CC +mode = "arm" +kind = "runtime_observed" +# hits = 47506 + +[[entry_point]] +addr = 0x021549E4 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02154D30 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02155698 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02156CAC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02158334 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0215AE04 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0215D5AC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0215EEF8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021604DC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021620A0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02162B5C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021645A0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02164E1C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216577C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x02166198 +mode = "arm" +kind = "runtime_observed" +# hits = 2 diff --git a/config/mph_arm9_ov015.toml b/config/mph_arm9_ov015.toml new file mode 100644 index 0000000..1d5ae36 --- /dev/null +++ b/config/mph_arm9_ov015.toml @@ -0,0 +1,550 @@ +# AUTO-GENERATED by tools/seed_overlay_from_coverage.py; do not commit. +# Entry points are Tier-3 observations whose containing 4 KiB page was +# byte-identical to this overlay's decompressed ROM image at the time it +# executed, so every seed below is provably from THIS overlay generation +# and not from one of the overlays sharing its address range. + +[program] +name = "Metroid Prime Hunters (USA rev 0) ARM9 overlay 15" +id = "mph_amhe0_arm9_ov015" +load_address = 0x02168B20 +size = 0x00008FE0 +entry_pc = 0x0216933C +authoritative_entry_points = false + +[identity] +sha1 = "1c4f5ba3145dcba0a3fe2e02ff91e19f519d161e" + +[[entry_point]] +addr = 0x0216933C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x021695C0 +mode = "arm" +kind = "runtime_observed" +# hits = 3444 + +[[entry_point]] +addr = 0x021695E4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x021696B8 +mode = "arm" +kind = "runtime_observed" +# hits = 3412 + +[[entry_point]] +addr = 0x02169ED0 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216A0B4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216A210 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216A6D0 +mode = "arm" +kind = "runtime_observed" +# hits = 13 + +[[entry_point]] +addr = 0x0216A9E4 +mode = "arm" +kind = "runtime_observed" +# hits = 65787 + +[[entry_point]] +addr = 0x0216AAEC +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216ACE4 +mode = "arm" +kind = "runtime_observed" +# hits = 65659 + +[[entry_point]] +addr = 0x0216AFC4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216B524 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216B528 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216BA8C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216BB30 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216BBC4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216BCBC +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216BD28 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216BD74 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216BDD4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216BDF0 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0216BE58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216BE88 +mode = "arm" +kind = "runtime_observed" +# hits = 37 + +[[entry_point]] +addr = 0x0216BF6C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C000 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0216C004 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C008 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C00C +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C010 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C018 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C044 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216C0AC +mode = "arm" +kind = "runtime_observed" +# hits = 2976 + +[[entry_point]] +addr = 0x0216C218 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C250 +mode = "arm" +kind = "runtime_observed" +# hits = 22 + +[[entry_point]] +addr = 0x0216C254 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C258 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C25C +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C260 +mode = "arm" +kind = "runtime_observed" +# hits = 12 + +[[entry_point]] +addr = 0x0216C268 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C34C +mode = "arm" +kind = "runtime_observed" +# hits = 10332 + +[[entry_point]] +addr = 0x0216C3E8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C4B8 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C4C0 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C4D0 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C4E8 +mode = "arm" +kind = "runtime_observed" +# hits = 10236 + +[[entry_point]] +addr = 0x0216C524 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216C528 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216C5FC +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216C618 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C630 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216C75C +mode = "arm" +kind = "runtime_observed" +# hits = 9 + +[[entry_point]] +addr = 0x0216C804 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216C810 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C8A4 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216C8D8 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216C924 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0216CA24 +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216CA58 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CB50 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216CB7C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216CC44 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CCFC +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CD38 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CD4C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216CD50 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216CD9C +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216CEB4 +mode = "arm" +kind = "runtime_observed" +# hits = 6 + +[[entry_point]] +addr = 0x0216D028 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D20C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D234 +mode = "arm" +kind = "runtime_observed" +# hits = 10236 + +[[entry_point]] +addr = 0x0216D398 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D3F8 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D414 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216D878 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216DA88 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216E52C +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216EDA8 +mode = "arm" +kind = "runtime_observed" +# hits = 2690 + +[[entry_point]] +addr = 0x0216F100 +mode = "arm" +kind = "runtime_observed" +# hits = 2 + +[[entry_point]] +addr = 0x0216F208 +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216F41C +mode = "arm" +kind = "runtime_observed" +# hits = 5 + +[[entry_point]] +addr = 0x0216F59C +mode = "arm" +kind = "runtime_observed" +# hits = 3 + +[[entry_point]] +addr = 0x0216F5F8 +mode = "arm" +kind = "runtime_observed" +# hits = 14230 + +[[entry_point]] +addr = 0x0216F6F4 +mode = "arm" +kind = "runtime_observed" +# hits = 4 + +[[entry_point]] +addr = 0x0216F914 +mode = "arm" +kind = "runtime_observed" +# hits = 10236 + +[[entry_point]] +addr = 0x0216F994 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216FA1C +mode = "arm" +kind = "runtime_observed" +# hits = 10236 + +[[entry_point]] +addr = 0x0216FAE4 +mode = "arm" +kind = "runtime_observed" +# hits = 1 + +[[entry_point]] +addr = 0x0216FC40 +mode = "arm" +kind = "runtime_observed" +# hits = 10236 diff --git a/scenarios/multiplayer_battle_bots.json b/scenarios/multiplayer_battle_bots.json index 3e72ed0..2a101e1 100644 --- a/scenarios/multiplayer_battle_bots.json +++ b/scenarios/multiplayer_battle_bots.json @@ -1,5 +1,5 @@ { - "description": "Offline multiplayer: BATTLE on arena 1/9 (Combat Hall) against 3 bots, no peer DS required. Reaches real gameplay far faster than the Adventure route and is the deterministic content lever for overlay coverage -- change only the arena arrow taps to select a different arena. Verified coordinates: main-menu MULTIPLAYER touch (160,92); the nickname dialog takes two A presses; mode row touch y~80 (SINGLE-CARD x~50, MULTI-CARD x~128, WI-FI x~205); CREATE/JOIN row touch y~80 (CREATE x~50, JOIN x~128); game-mode grid top row touch y~58 (BATTLE x~50); arena screen shows ARENA n/9 with arrows at touch y~48 (left x~125, right x~232) and confirm at touch (222,173); hunter Samus at touch (27,132) then A; the waiting room offers ADD BOT at touch (222,173).", + "description": "Offline multiplayer BATTLE against bots -- no peer console required. This is the standard in-game measurement route: no navigation skill needed, and the arena is a menu-selected variable (Battle offers ARENA n/9), so swapping content is one pair of arrow taps. USE MULTI-CARD, NOT SINGLE-CARD: single-card play does not allow bots at all -- its slots read AVAILABLE and only accept peer consoles -- which is the most misleading thing about this flow. Coordinates, measured from captured frames: main-menu MULTIPLAYER (160,92) [ADVENTURE is (84,92)]; nickname dialog = two A presses; mode row y~80 (SINGLE-CARD x~50, MULTI-CARD x~128, WI-FI x~205); CREATE/JOIN row y~80 (CREATE x~50, JOIN x~128); game-mode grid top row y~58 (BATTLE x~50); arena arrows y~48 (left x~125, right x~232), confirm (222,173); hunter Samus (27,132) then A; waiting room ADD + at (222,173). START SEQUENCE -- what earlier attempts got wrong: after adding bots with ADD +, TAP THE GREEN CHECKBOX BESIDE EACH BOT to confirm it. Those checkboxes are per-slot CONFIRM controls, not a ready toggle and not a difficulty widget; confirming is what reveals a bot's star rating, which is why tapping one looked like a difficulty selector. Once every bot is confirmed a START BATTLE control becomes available -- TAP IT. The match starts by tapping an on-screen control, NOT via the START or A key; pressing those does nothing, which stalled every earlier automated attempt. The actions below stop after confirming the bots; the final START BATTLE tap coordinate still needs measuring from a captured frame.", "actions": [ { "kind": "touch", @@ -132,20 +132,34 @@ }, { "kind": "touch", - "x": 105, + "x": 222, "y": 133 }, { "kind": "wait", - "frames": 300 + "frames": 180 + }, + { + "kind": "touch", + "x": 148, + "y": 60 }, { "kind": "wait", - "frames": 600 + "frames": 180 + }, + { + "kind": "touch", + "x": 222, + "y": 60 }, { "kind": "wait", - "frames": 600 + "frames": 300 + }, + { + "kind": "wait", + "frames": 300 } ] } \ No newline at end of file From 905ffab20ecd0d9c3c1017fb757aec73c435a1ad Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 16 Aug 2026 23:06:43 -0700 Subject: [PATCH 139/164] tools: resume run_to_event when the round budget is exhausted Reaching the title screen needs more than the 50M default max_rounds, so the initial advance silently under-ran and every subsequent tap landed at the wrong moment. Verified this was harness-side, not a build regression: all three runner builds stop at exactly vblank 6461. beads-yjp.31 Co-Authored-By: Claude Opus 5 (1M context) --- tools/mph_overlay_route.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tools/mph_overlay_route.py b/tools/mph_overlay_route.py index 13dc2d1..05a7ce4 100644 --- a/tools/mph_overlay_route.py +++ b/tools/mph_overlay_route.py @@ -53,14 +53,30 @@ def cmd(self, name: str, **args: object) -> dict: def vblank(self) -> int: return int(self.cmd("event_counts")["vblank9"]) + def advance_to(self, target: int) -> None: + """Advance to an absolute vblank, resuming when the round budget runs out. + + run_to_event caps at max_rounds and returns exhausted=True having gone + only part way -- reaching the title screen alone needs more than the + 50M default. Not resuming silently under-ran every route: the boot + stopped at vblank 6461 instead of 7800 and every subsequent tap landed + at the wrong moment, which looked exactly like a regression in the + build under test. It was not; all builds stop at the same vblank. + """ + for _ in range(200): + reply = self.cmd("run_to_event", event="vblank9", count=target, + max_rounds=50_000_000) + if reply.get("terminal"): + raise RuntimeError(f"runner halted: {reply.get('reason9')} / " + f"{reply.get('reason7')}") + if reply.get("stalled"): + raise RuntimeError(f"stalled before vblank9 {target}") + if self.vblank() >= target: + return + raise RuntimeError(f"could not reach vblank9 {target}") + def advance(self, frames: int) -> None: - target = self.vblank() + frames - reply = self.cmd("run_to_event", event="vblank9", count=target) - if reply.get("terminal"): - raise RuntimeError(f"runner halted: {reply.get('reason9')} / " - f"{reply.get('reason7')}") - if reply.get("stalled"): - raise RuntimeError(f"stalled before vblank9 {target}") + self.advance_to(self.vblank() + frames) def tap(self, x: int, y: int, hold: int) -> None: self.cmd("touch", x=x, y=y, down=True) @@ -138,7 +154,7 @@ def main() -> int: client.cmd("reset") target = args.start_vblank - client.cmd("run_to_event", event="vblank9", count=target) + client.advance_to(target) if not args.no_shots: client.screenshot(args.out / f"0000-{target:05d}-title.png") client.tap(128, 96, args.hold_frames) From 66bdd9cdc924a880c425b138279590cf10e0eb56 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:18:56 +0900 Subject: [PATCH 140/164] Gate launcher multi-ROM UI patch syntax --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6f5b3b..ec8b778 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,7 @@ jobs: set -euo pipefail python -m py_compile \ tools/patch_ndsrecomp_rom_free_release.py \ + tools/patch_recomp_ui_mph_multirom.py \ tools/ci/check_rom_free_release_sources.py \ tools/ci/prepare_freebios_banks.py \ tools/ci/verify-nightly-assets.py From 129137390cc126d4bf797cfff2b548247514b9bc Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:28:27 +0900 Subject: [PATCH 141/164] Fix launcher default ROM selection when file is absent --- launcher/recomp-ui/CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 03518ee..7a9d904 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -53,6 +53,21 @@ mph_launcher_replace_required( "exe / \"${MPH_LAUNCHER_DEFAULT_ROM}\";" "the default MPH ROM filename") +# recomp-ui treats any non-empty initial ROM string as selected before it tries +# to open the file. A release package intentionally contains no ROM, so do not +# feed the conventional filename to the model unless that file really exists. +# This preserves the useful portable behavior where a player may deliberately +# place a ROM with the conventional name beside the executable, while a fresh +# extraction correctly starts at "No ROM loaded". +mph_launcher_replace_required( + " char selected_rom[1024]{};\n const int result = recomp_launcher_run_window(" + " std::error_code initial_rom_error;\n const std::string initial_rom =\n std::filesystem::is_regular_file(default_rom, initial_rom_error)\n ? default_rom.string()\n : std::string();\n char selected_rom[1024]{};\n const int result = recomp_launcher_run_window(" + "the initial ROM selection block") +mph_launcher_replace_required( + "exe.string().c_str(), default_rom.string().c_str()," + "exe.string().c_str(), initial_rom.c_str()," + "the launcher initial ROM argument") + set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") From ae79be2296b7a4908405b03b09805068f5fe0244 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:28:43 +0900 Subject: [PATCH 142/164] Require readable ROM before delegated validation display --- tools/patch_recomp_ui_mph_multirom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/patch_recomp_ui_mph_multirom.py b/tools/patch_recomp_ui_mph_multirom.py index e35d69e..20e310b 100644 --- a/tools/patch_recomp_ui_mph_multirom.py +++ b/tools/patch_recomp_ui_mph_multirom.py @@ -22,7 +22,7 @@ OLD = ''' const bool verified = launcher_model_rom_verified(m);\n char line[64];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(verified, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(verified ? col(th.good) : col(th.warn), "%s", line);''' -NEW = ''' const bool verified = launcher_model_rom_verified(m);\n // MPH_MULTIROM_DELEGATED_VERIFY: no generic fingerprint means the host\n // intentionally delegates compatibility to its runtime detector. Do\n // not tell the player that such a ROM is "not recognized"; Play is\n // already allowed by launcher_model_can_play() in this state.\n const bool delegated = m->rom_present && !m->has_expected_crc &&\n m->num_known_sha256 == 0 &&\n m->num_known_sha1 == 0;\n const bool accepted = verified || delegated;\n char line[96];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else if (delegated) snprintf(line, sizeof(line), "%s selected - runtime validation", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(accepted, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(accepted ? col(th.good) : col(th.warn), "%s", line);''' +NEW = ''' const bool verified = launcher_model_rom_verified(m);\n // MPH_MULTIROM_DELEGATED_VERIFY: no generic fingerprint means the host\n // intentionally delegates compatibility to its runtime detector. The\n // model marks any non-empty initial path as rom_present before opening\n // it, so require a successfully measured file as well; a missing\n // conventional default filename must never appear as selected.\n const bool readable = m->rom_present && std::strcmp(m->rom_size, "--") != 0;\n const bool delegated = readable && !m->has_expected_crc &&\n m->num_known_sha256 == 0 &&\n m->num_known_sha1 == 0;\n const bool accepted = verified || delegated;\n char line[96];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (!readable) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else if (delegated) snprintf(line, sizeof(line), "%s selected - runtime validation", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(accepted, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(accepted ? col(th.good) : col(th.warn), "%s", line);''' def main() -> None: From 13a26345c0763ec841c10404558faf7213fefc80 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:29:06 +0900 Subject: [PATCH 143/164] Assert launcher starts with no absent default ROM --- .github/workflows/build-windows.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index a1f23f7..1e0d562 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -104,8 +104,14 @@ jobs: launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'game.known_sha1_hex = nullptr;' \ launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'std::filesystem::is_regular_file(default_rom, initial_rom_error)' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'exe.string().c_str(), initial_rom.c_str(),' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'MPH_MULTIROM_DELEGATED_VERIFY' \ ../recomp-ui/src/common/backends/imgui/launcher_imgui.cpp + grep -q 'const bool readable = m->rom_present && std::strcmp(m->rom_size, "--") != 0;' \ + ../recomp-ui/src/common/backends/imgui/launcher_imgui.cpp - name: Package Windows Nightly payload shell: msys2 {0} From e516c3be1da1509a69d894edbf241dc538462142 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:33:59 +0900 Subject: [PATCH 144/164] Add launcher startup ROM selection regression check --- launcher/recomp-ui/CMakeLists.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 7a9d904..3c3005e 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -72,6 +72,23 @@ set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") +# Configure-time regression guard for the fresh-release UX. The generated +# launcher must never pass a missing conventional ROM path into recomp-ui as if +# the user had selected it. This deliberately tests the generated TU rather +# than the untransformed upstream-tracking source. +string(FIND "${MPH_LAUNCHER_SOURCE}" + "std::filesystem::is_regular_file(default_rom, initial_rom_error)" + _mph_initial_rom_exists_guard) +if(_mph_initial_rom_exists_guard EQUAL -1) + message(FATAL_ERROR "generated launcher lost the default-ROM existence guard") +endif() +string(FIND "${MPH_LAUNCHER_SOURCE}" + "exe.string().c_str(), initial_rom.c_str()," + _mph_initial_rom_argument_guard) +if(_mph_initial_rom_argument_guard EQUAL -1) + message(FATAL_ERROR "generated launcher still passes the unconditional default ROM path") +endif() + add_executable(mph-recomp-ui "${MPH_PROFILE_LAUNCHER_SOURCE}" "${NDSRECOMP_ROOT}/recompiler/support/sha1.cpp") target_include_directories(mph-recomp-ui PRIVATE From e46806c7a61c94bb1e66ba44fdb25f0d1a763769 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:38:47 +0900 Subject: [PATCH 145/164] Document fresh-launch ROM selection assertion --- .github/workflows/build-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 1e0d562..dbd796e 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -104,6 +104,8 @@ jobs: launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'game.known_sha1_hex = nullptr;' \ launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + # Fresh ROM-free extraction must start with no ROM selected. The + # conventional filename is auto-selected only when the file exists. grep -q 'std::filesystem::is_regular_file(default_rom, initial_rom_error)' \ launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'exe.string().c_str(), initial_rom.c_str(),' \ From 06baba5b6629f6390ce43401ecc3d4f00129b41d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:41:27 +0900 Subject: [PATCH 146/164] Reuse cached MSYS2 on Windows CI --- .github/workflows/build-windows.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index dbd796e..d8a6a80 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -20,6 +20,12 @@ jobs: uses: msys2/setup-msys2@v2 with: msystem: MINGW64 + # windows-2025 already ships C:\msys64. Reuse that image installation + # instead of downloading/extracting a fresh MSYS2 release every run. + release: false + # setup-msys2 has a native package cache; keep it explicit so future + # workflow edits do not accidentally disable the fast rerun path. + cache: true update: true install: >- git From cc950242384ee2e22caa2711ade558413d4d7dea Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:42:50 +0900 Subject: [PATCH 147/164] Add persistent ccache for Windows CI --- .github/workflows/build-windows.yml | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index d8a6a80..2945d58 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -11,6 +11,14 @@ jobs: windows: name: Windows ROM-free build runs-on: windows-2025 + env: + # Keep compiler output reusable across PR iterations. ccache validates the + # compiler command/source content itself; this directory is only a cache, + # never a source of build truth. + CCACHE_DIR: ${{ github.workspace }}\.ccache + CCACHE_MAXSIZE: 750M + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache steps: - name: Check out title sources @@ -34,6 +42,31 @@ jobs: mingw-w64-x86_64-ninja mingw-w64-x86_64-python mingw-w64-x86_64-SDL2 + mingw-w64-x86_64-ccache + + - name: Restore compiler cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}\.ccache + key: >- + mph-windows-mingw64-ccache-v1-${{ hashFiles( + 'ndsrecomp.pin', + 'recomp-ui.pin', + 'launcher/recomp-ui/**', + 'tools/patch_ndsrecomp_mph_runtime.py', + 'tools/patch_ndsrecomp_rom_free_release.py', + 'tools/patch_recomp_ui_mph_multirom.py' + ) }} + restore-keys: | + mph-windows-mingw64-ccache-v1- + + - name: Configure compiler cache + shell: msys2 {0} + run: | + set -euo pipefail + ccache --version + ccache --set-config=max_size="$CCACHE_MAXSIZE" + ccache --zero-stats - name: Fetch pinned ndsrecomp and recomp-ui shell: msys2 {0} @@ -138,6 +171,14 @@ jobs: -RuntimeBinDir "$runtime_bin" test -s "release-stage/MetroidPrimeHuntersRecomp-windows-x64-v${version}.zip" + - name: Show compiler cache stats + if: always() + shell: msys2 {0} + run: | + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats || true + fi + - name: Upload Windows Nightly payload uses: actions/upload-artifact@v4 with: From 15e0173cb6eb012b0bf8c880cb4592df6b9efaa3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:46:00 +0900 Subject: [PATCH 148/164] Avoid full MSYS2 upgrade in Windows CI --- .github/workflows/build-windows.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 2945d58..43f3d92 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -24,20 +24,21 @@ jobs: - name: Check out title sources uses: actions/checkout@v4 - - name: Install MinGW toolchain + - name: Install MinGW build dependencies uses: msys2/setup-msys2@v2 with: msystem: MINGW64 - # windows-2025 already ships C:\msys64. Reuse that image installation - # instead of downloading/extracting a fresh MSYS2 release every run. + # windows-2025 already ships C:\msys64. Reuse the runner image rather + # than downloading a second MSYS2 installation. Do not run a full + # rolling-release upgrade on every PR: the image is internally + # consistent and setup-msys2 can install the small set below directly. release: false - # setup-msys2 has a native package cache; keep it explicit so future - # workflow edits do not accidentally disable the fast rerun path. + update: false + # setup-msys2 also caches pacman payloads between runs. cache: true - update: true install: >- git - mingw-w64-x86_64-toolchain + mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-python From d617f751e4e289b408b88e73af0af4eecb2fe613 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:57:05 +0900 Subject: [PATCH 149/164] Trim Windows CI MinGW dependency install --- .github/workflows/build-windows.yml | 33 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 43f3d92..8c32a16 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -19,6 +19,10 @@ jobs: CCACHE_MAXSIZE: 750M CMAKE_C_COMPILER_LAUNCHER: ccache CMAKE_CXX_COMPILER_LAUNCHER: ccache + # Hosted CMake is a native Windows binary. Point it at MinGW's package + # prefix so SDL2's CMake package remains discoverable without installing + # a second copy of CMake inside MSYS2. + CMAKE_PREFIX_PATH: C:\msys64\mingw64 steps: - name: Check out title sources @@ -28,20 +32,15 @@ jobs: uses: msys2/setup-msys2@v2 with: msystem: MINGW64 - # windows-2025 already ships C:\msys64. Reuse the runner image rather - # than downloading a second MSYS2 installation. Do not run a full - # rolling-release upgrade on every PR: the image is internally - # consistent and setup-msys2 can install the small set below directly. + # windows-2025 already includes C:\msys64 plus native Git, CMake, + # Ninja and Python. Inherit the hosted PATH and install only the + # MinGW-specific compiler/runtime dependencies we actually need. + path-type: inherit release: false update: false - # setup-msys2 also caches pacman payloads between runs. cache: true install: >- - git mingw-w64-x86_64-gcc - mingw-w64-x86_64-cmake - mingw-w64-x86_64-ninja - mingw-w64-x86_64-python mingw-w64-x86_64-SDL2 mingw-w64-x86_64-ccache @@ -61,6 +60,22 @@ jobs: restore-keys: | mph-windows-mingw64-ccache-v1- + - name: Verify hosted build tools + shell: msys2 {0} + run: | + set -euo pipefail + command -v git + command -v python + command -v cmake + command -v ninja + command -v gcc + command -v ccache + git --version + python --version + cmake --version | head -n1 + ninja --version + gcc --version | head -n1 + - name: Configure compiler cache shell: msys2 {0} run: | From 35bc28c9ac32644cecd0dcfe310012e72a387639 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 17:01:18 +0900 Subject: [PATCH 150/164] Add persistent ccache to Linux CI --- .github/workflows/build-linux.yml | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 5c96e5c..2445098 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -11,6 +11,11 @@ jobs: linux: name: Linux ROM-free build runs-on: ubuntu-24.04 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 750M + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache steps: - name: Check out title sources @@ -21,10 +26,31 @@ jobs: set -euo pipefail sudo apt-get update sudo apt-get install -y --no-install-recommends \ - build-essential cmake ninja-build git curl ca-certificates \ + build-essential cmake ninja-build git curl ca-certificates ccache \ libsdl2-dev libgl1-mesa-dev libx11-dev libxext-dev libxrandr-dev \ libxcursor-dev libxi-dev libxinerama-dev libwayland-dev + - name: Restore compiler cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.ccache + key: >- + mph-linux-x86_64-ccache-v1-${{ hashFiles( + 'ndsrecomp.pin', + 'config/mph_rom_profiles.json', + 'tools/patch_ndsrecomp_mph_runtime.py', + 'tools/patch_ndsrecomp_rom_free_release.py' + ) }} + restore-keys: | + mph-linux-x86_64-ccache-v1- + + - name: Configure compiler cache + run: | + set -euo pipefail + ccache --version + ccache --set-config=max_size="$CCACHE_MAXSIZE" + ccache --zero-stats + - name: Set up Python uses: actions/setup-python@v5 with: @@ -115,6 +141,13 @@ jobs: exit 1 fi + - name: Show compiler cache stats + if: always() + run: | + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats || true + fi + - name: Upload Linux Nightly payload uses: actions/upload-artifact@v4 with: From 892e108518981732847f767f2ec3a02977d83c93 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 17:14:26 +0900 Subject: [PATCH 151/164] Add MPH runtime diagnostics and ROM-free multi-ROM gate --- tools/patch_ndsrecomp_mph_diagnostics.py | 212 +++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 tools/patch_ndsrecomp_mph_diagnostics.py diff --git a/tools/patch_ndsrecomp_mph_diagnostics.py b/tools/patch_ndsrecomp_mph_diagnostics.py new file mode 100644 index 0000000..fe22c30 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_diagnostics.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Add end-user MPH startup diagnostics to the pinned ndsrecomp runner. + +This layer intentionally runs after patch_ndsrecomp_mph_runtime_core.py: + +* interactive launches write stderr to MetroidPrimeHuntersRecomp.log beside + nds_runner, so CREATE_NO_WINDOW launcher starts still leave a useful trace; +* runtime-profile selection reports gameCode/revision/executable CRC32, + authoritative-vs-header-fallback source, selected profile and host-write + safety state; +* ROM-free builds treat a successfully selected MPH runtime profile as the + compatibility authority instead of re-applying the legacy US1.0 whole-ROM + SHA-1 gate. Native/title-bank builds keep the existing exact-content policy. + +Whole-ROM SHA-1 remains useful content/cache identity; it is not used as the +base-version detector in the ROM-free Tier-3 path. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def replace_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + count = text.count(old) + if count != 1: + raise SystemExit( + f"{path}: expected exactly one source anchor for {marker!r}, got {count}" + ) + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--profiles", type=Path, required=False) + args = parser.parse_args() + root = args.framework_root.resolve() + + title_cpp = root / "runner" / "src" / "title_patches.cpp" + main_cpp = root / "runner" / "src" / "main.cpp" + for path in (title_cpp, main_cpp): + if not path.is_file(): + raise SystemExit(f"missing pinned ndsrecomp source: {path}") + + # The launcher intentionally creates no console window. Redirecting stderr + # from inside the runner is therefore more reliable than relying on the + # parent's inherited standard handles. Only the normal --interactive UI + # path gets the persistent file; CLI/scenario runs keep stderr untouched. + replace_once( + main_cpp, + "int main(int argc, char** argv) {\n" + " // Wiimmfi: Winsock (Windows only) MUST be initialized before ANY\n", + "int main(int argc, char** argv) {\n" + " // MPH_DIAGNOSTIC_LOG: keep the latest interactive-run log beside\n" + " // nds_runner.exe/AppImage payload so a no-console startup failure is\n" + " // still diagnosable by the player. CLI/scenario stderr is unchanged.\n" + " bool mph_interactive_log = false;\n" + " for (int i = 1; i < argc; ++i) {\n" + " if (argv[i] && std::strcmp(argv[i], \"--interactive\") == 0) {\n" + " mph_interactive_log = true;\n" + " break;\n" + " }\n" + " }\n" + " if (mph_interactive_log) {\n" + " try {\n" + " const std::filesystem::path log_path =\n" + " std::filesystem::weakly_canonical(\n" + " std::filesystem::absolute(argv[0])).parent_path() /\n" + " \"MetroidPrimeHuntersRecomp.log\";\n" + "#if defined(_WIN32)\n" + " FILE* mph_log = _wfreopen(log_path.wstring().c_str(), L\"w\", stderr);\n" + "#else\n" + " FILE* mph_log = std::freopen(log_path.string().c_str(), \"w\", stderr);\n" + "#endif\n" + " if (mph_log) {\n" + " std::setvbuf(stderr, nullptr, _IONBF, 0);\n" + " std::fprintf(stderr,\n" + " \"=== Metroid Prime Hunters Recomp diagnostic log ===\\n\"\n" + " \"[startup] interactive runner started\\n\");\n" + " }\n" + " } catch (...) {\n" + " // Logging must never make a previously launchable build fail.\n" + " }\n" + " }\n\n" + " // Wiimmfi: Winsock (Windows only) MUST be initialized before ANY\n", + "MPH_DIAGNOSTIC_LOG", + ) + + # Turn the previously ignored selector result into an explicit policy + # signal. The selector itself still performs executable/header validation. + replace_once( + main_cpp, + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()), rom_sha1.c_str(),\n" + " frontend_options.expected_rom_sha1.c_str());\n", + " const bool mph_runtime_profile_selected =\n" + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()),\n" + " rom_sha1.c_str(),\n" + " frontend_options.expected_rom_sha1.c_str());\n", + "mph_runtime_profile_selected =", + ) + + # NDS_RETAIL_BIOS_INTERPRETER is defined only by the public ROM-free build + # policy when proprietary retail BIOS banks are not linked. That is also + # the build with no MPH native title bank, so Tier-3 + the seven-version + # runtime detector is the correct authority. Optimized/native-bank builds + # deliberately retain the stricter content-SHA policy below. + replace_once( + main_cpp, + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n", + "#if defined(NDS_RETAIL_BIOS_INTERPRETER)\n" + " // MPH_ROMFREE_MULTIROM_GATE: public Nightly has no ROM-derived\n" + " // title bank, so a successfully selected runtime base profile\n" + " // is authoritative. This is what permits US1.1/EU/JP/KR and\n" + " // compatible modified ROMs to reach Tier-3 execution.\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !mph_runtime_profile_selected) {\n" + "#else\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n" + "#endif\n", + "MPH_ROMFREE_MULTIROM_GATE", + ) + + # Give every rejection enough context to distinguish a malformed ROM from + # a supported version, a known modified executable, or a header-only mod. + replace_once( + title_cpp, + " uint32_t checksum = 0;\n" + " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum))\n" + " return false;\n", + " uint32_t checksum = 0;\n" + " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum)) {\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime detector: invalid ROM executable ranges\"\n" + " \" (size=%llu)\\n\",\n" + " static_cast(rom_size));\n" + " return false;\n" + " }\n" + " std::fprintf(stderr,\n" + " \"[mph] identity: gameCode=%.4s revision=%u \"\n" + " \"execCRC32=0x%08X\\n\",\n" + " reinterpret_cast(rom_data + 0x0Cu),\n" + " static_cast(rom_data[0x1Eu]), checksum);\n", + "[mph] identity: gameCode=", + ) + + replace_once( + title_cpp, + " if (!profile) return false;\n\n" + " // A known clean whole-ROM hash can only describe its own base profile.\n", + " if (!profile) {\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime detector: unsupported/ambiguous ROM \"\n" + " \"(execCRC32=0x%08X)\\n\", checksum);\n" + " return false;\n" + " }\n\n" + " // A known clean whole-ROM hash can only describe its own base profile.\n", + "unsupported/ambiguous ROM", + ) + + replace_once( + title_cpp, + " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" + " if (actual_clean && actual_clean != profile) return false;\n", + " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" + " if (actual_clean && actual_clean != profile) {\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime detector: clean SHA/profile conflict \"\n" + " \"shaProfile=%s detectedProfile=%s\\n\",\n" + " actual_clean->key, profile->key);\n" + " return false;\n" + " }\n", + "clean SHA/profile conflict", + ) + + replace_once( + title_cpp, + " g_mph_allow_rom_sha1_mismatch =\n" + " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" + " expected_clean == profile && checksum == profile->base_checksum;\n" + " return true;\n", + " g_mph_allow_rom_sha1_mismatch =\n" + " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" + " expected_clean == profile && checksum == profile->base_checksum;\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime profile: %s detector=%s variant=%s \"\n" + " \"hostWrites=%s legacyShaReuse=%s\\n\",\n" + " profile->key,\n" + " checksum_hit ? \"executable-checksum\" : \"header-fallback\",\n" + " checksum_hit ? checksum_hit->name : \"unknown-mod\",\n" + " g_mph_host_writes_compatible ? \"enabled\" : \"disabled\",\n" + " g_mph_allow_rom_sha1_mismatch ? \"yes\" : \"no\");\n" + " return true;\n", + "[mph] runtime profile:", + ) + + print("Patched MPH runtime diagnostics and ROM-free multi-ROM content gate") + + +if __name__ == "__main__": + main() From cece5e32161b858fe40cf705ea439628d7925b31 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 17:14:39 +0900 Subject: [PATCH 152/164] Apply MPH diagnostics after runtime patch stack --- tools/patch_ndsrecomp_mph_runtime.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py index 2bc0249..4034b1d 100755 --- a/tools/patch_ndsrecomp_mph_runtime.py +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -3,8 +3,9 @@ The core detector is kept separately so upstream-facing additions can be layered without weakening its whole-ROM/content identity rules. The later stages add -the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch and make -that patch re-eligible after an in-process guest reset. +the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch, make +that patch re-eligible after an in-process guest reset, and finally add +end-user startup diagnostics plus the ROM-free multi-ROM content-gate policy. """ from __future__ import annotations @@ -21,6 +22,7 @@ def main() -> None: here / "patch_ndsrecomp_mph_runtime_core.py", here / "patch_ndsrecomp_mph_widescreen.py", here / "patch_ndsrecomp_mph_widescreen_reset.py", + here / "patch_ndsrecomp_mph_diagnostics.py", ): subprocess.run([sys.executable, str(script), *args], check=True) From b926c5ff21954119a8515a8d8fac3ee27204489a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:20:25 +0900 Subject: [PATCH 153/164] Make game.toml runtime-generic --- game.toml | 46 ++++++++-------------------------------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/game.toml b/game.toml index 1d99a24..8a5e338 100644 --- a/game.toml +++ b/game.toml @@ -1,17 +1,9 @@ [game] name = "Metroid Prime Hunters" -id = "AMHE" -region = "USA" -revision = 0 -status = "ROM-gated main banks enter Celestial Archives gameplay with interpreter fallback" -rom = "Metroid Prime Hunters.nds" -rom_size = 0x04000000 -sha1 = "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" -sha256 = "7d0a98ff98e1b7c985d1f3d89b01730af1b2115061a4dfea847612d217a8b855" +status = "Generic multi-ROM runtime config; ROM version is selected by the executable-compatible runtime detector" [display] -# Prime Hunters uses both screens heavily. Keep them independently presentable -# from the beginning, matching the established SM64DS host layout. +# Prime Hunters uses both screens heavily. Keep them independently presentable. screen_layout = "separate" supersampling = 1 antialiasing = 0 @@ -19,47 +11,25 @@ antialiasing = 0 # The shared host-side adaptive renderer expands only the upper 3D viewport to # 21:9. The lower touchscreen remains native 256x192 and independently # clickable. Runtime executable compatibility, not whole-ROM SHA-1, gates the -# title-owned projection/culling patch. +# title-owned projection/culling patch for all supported revisions. adaptive_widescreen = "top" adaptive_capability = "top" adaptive_width = 448 -# SM64DS needs a cylindrical-sky repair heuristic. Prime Hunters does not opt -# into that title-specific treatment; uncovered geometry remains visible -# during the MPH projection/culling audit. adaptive_skybox_fill = false -# Gameplay enables three transparent text-tile planes for the visor and HUD. -# Anchor their authored left/center/right bands over the widened 3D world -# instead of falling back to a pillarboxed native composite. adaptive_hud_anchor = true -# MPH's centered location banner and energy assembly are wider than the -# framework's conservative 64-pixel default. Preserve source X=64..191 as one -# centered unit so text and bars never cross an anchor seam. adaptive_hud_center_width = 128 [system] -# Exercise the authentic BIOS/firmware/card path and automatically launch the -# inserted cartridge after the normal firmware sequence. +# The ROM-free launcher uses FreeBIOS/generated firmware with direct boot. +# Other callers may override the boot path through CLI options. startup_mode = "automatic" [cartridge] -# melonDS ROMList entry AMHE: SaveMemType 5 (256 KiB flash). +# Metroid Prime Hunters uses the same 256 KiB flash save geometry across the +# supported retail revisions. save_type = "flash" save_size = 262144 -[arm9] -rom_offset = 0x00004000 -entry_pc = 0x02004800 -load_address = 0x02004000 -compressed_size = 0x0007e6c4 -decompressed_size = 0x000dd9d8 -overlay_count = 18 - -[arm7] -rom_offset = 0x0013aa00 -entry_pc = 0x02380000 -load_address = 0x02380000 -size = 0x00028464 - [framework] path = "../ndsrecomp" pin = "6c6a03bdcf99093f64555c4d05d16e522dc58634" @@ -68,4 +38,4 @@ branch = "main" [reference.mphread] url = "https://github.com/NoneGiven/MphRead.git" pin = "26cd8a6fe93dc5e525d1a1bb304fe96001111e55" -use = "AMHE0-aware gameplay, save, room/entity, model, animation, collision, audio, and file-format research" +use = "MPH gameplay, save, room/entity, model, animation, collision, audio, and file-format research" From a174aae8c5a0ed97fe18b41868cdc6f561569eca Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:20:53 +0900 Subject: [PATCH 154/164] Share generic game config across content profiles --- 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 838996a..169bc4e 100644 --- a/config/mph_rom_profiles.json +++ b/config/mph_rom_profiles.json @@ -146,7 +146,7 @@ "sha1": "bdcd1dea293e24c98d4c481430e90d21198985a5", "program_id": "mph_amhp1", "coverage": "coverage/eu11-bootstrap-entry-points.json", - "game_config": "config/game-eu11.toml", + "game_config": "game.toml", "fmv_runtime": false, "fmv_runtime_bank": "mph_amhp1_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters (Europe Rev 1).nds", From 9b220992e294445e39a8e96ff97210c603902177 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:21:20 +0900 Subject: [PATCH 155/164] Validate shared generic frontend config --- tools/check_mph_multirom_profiles.py | 60 +++++++++++++++++++++------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index eee8dc5..d117b51 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -2,9 +2,9 @@ """Validate the current multi-ROM schema, then run the legacy deep checks. The legacy checker predates the all-version Adaptive Widescreen address table -and intentionally asserted EU1_1=false plus three runtime fields. Preserve its -broad coverage on a compatibility view while validating the new scale fields -against melonPrimeDS on the real registry. +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. """ from __future__ import annotations @@ -66,15 +66,27 @@ def validate_scale_registry(repo: Path, table: Path | None) -> None: profiles = registry.get("profiles") if not isinstance(profiles, dict): die("content profiles missing") + + # Runtime frontend policy is shared by every exact-content profile. Exact + # ROM SHA/header identity belongs in this registry and generated bank/cache + # provenance, not in one region-specific game TOML. + shared_config = repo / "game.toml" + if not shared_config.is_file(): + die("shared runtime frontend config is missing: game.toml") + shared_text = shared_config.read_text(encoding="utf-8") + if 'adaptive_widescreen = "top"' not in shared_text or 'adaptive_capability = "top"' not in shared_text: + die("shared game.toml does not expose top-screen Adaptive Widescreen") + for forbidden in ("\nid =", "\nregion =", "\nrevision =", "\nrom =", "\nrom_size =", "\nsha1 ="): + if forbidden in shared_text: + die(f"shared game.toml still contains exact-content identity field: {forbidden.strip()}") + for key, profile in profiles.items(): if not isinstance(profile, dict): die(f"{key}: invalid content profile") if profile.get("adaptive_widescreen") is not True: die(f"{key}: Adaptive Widescreen must remain exposed for known content profiles") - config_path = repo / str(profile.get("game_config", "")) - text = config_path.read_text(encoding="utf-8") - if 'adaptive_widescreen = "top"' not in text or 'adaptive_capability = "top"' not in text: - die(f"{key}: game config does not expose top-screen Adaptive Widescreen") + if profile.get("game_config") != "game.toml": + die(f"{key}: content profiles must share the generic game.toml frontend config") if table: text = table.read_text(encoding="utf-8") @@ -97,6 +109,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.""" target = destination / "repo" shutil.copytree( repo, target, @@ -108,13 +121,32 @@ def legacy_compat_view(repo: Path, destination: Path) -> Path: runtime = item["runtime"] for field in SCALE_FIELDS: runtime.pop(field, None) - registry["profiles"]["EU1_1"]["adaptive_widescreen"] = False - registry_path.write_text(json.dumps(registry, indent=2) + "\n", encoding="utf-8") - eu_config = target / "config" / "game-eu11.toml" - lines = eu_config.read_text(encoding="utf-8").splitlines() - lines = [line for line in lines if not line.startswith("adaptive_widescreen =")] - eu_config.write_text("\n".join(lines) + "\n", encoding="utf-8") + # 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. + for key, profile in registry["profiles"].items(): + legacy_rel = f"config/.legacy-{key}.toml" + profile["game_config"] = legacy_rel + adaptive = profile.get("adaptive_widescreen") is True + if key == "EU1_1": + adaptive = False + profile["adaptive_widescreen"] = False + lines = [ + "[game]", + f'id = "{profile["game_code"]}"', + f'revision = {profile["revision"]}', + f'rom_size = {profile["rom_size"]}', + f'sha1 = "{profile["sha1"]}"', + "", + "[display]", + ] + if adaptive: + lines.append('adaptive_widescreen = "top"') + (target / legacy_rel).write_text("\n".join(lines) + "\n", encoding="utf-8") + + registry_path.write_text(json.dumps(registry, indent=2) + "\n", encoding="utf-8") return target @@ -131,7 +163,7 @@ def main() -> None: with tempfile.TemporaryDirectory(prefix="mph-multirom-check-") as temp: compat = legacy_compat_view(repo, Path(temp)) legacy.validate_registry(compat, table) - print("OK: Adaptive Widescreen address/capability checks passed for all seven MPH revisions") + print("OK: seven-version runtime tables and shared generic game.toml are consistent") if __name__ == "__main__": From 9aea3fbb33d849a4c039a23a6959d863a9e03c92 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:21:29 +0900 Subject: [PATCH 156/164] Remove obsolete EU1.1-specific game config --- config/game-eu11.toml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 config/game-eu11.toml diff --git a/config/game-eu11.toml b/config/game-eu11.toml deleted file mode 100644 index f1e62d5..0000000 --- a/config/game-eu11.toml +++ /dev/null @@ -1,34 +0,0 @@ -[game] -name = "Metroid Prime Hunters" -id = "AMHP" -region = "Europe" -revision = 1 -status = "EU1.1 bootstrap: ROM-header main banks with interpreter fallback" -rom = "Metroid Prime Hunters (Europe Rev 1).nds" -rom_size = 0x04000000 -sha1 = "bdcd1dea293e24c98d4c481430e90d21198985a5" - -[display] -screen_layout = "separate" -supersampling = 1 -antialiasing = 0 -# melonPrimeDS and mphCodex identify the revision-specific projection, -# 3D-to-2D projection and Q12 culling-aspect locations for every retail MPH -# revision. The runner applies those writes only after authoritative executable -# checksum validation, so EU1.1 can expose the same 21:9 feature as US1.0. -adaptive_widescreen = "top" -adaptive_capability = "top" -adaptive_width = 448 -adaptive_skybox_fill = false -adaptive_hud_anchor = true -adaptive_hud_center_width = 128 - -[system] -startup_mode = "automatic" - -[cartridge] -# Metroid Prime Hunters uses the same 256 KiB flash save geometry across the -# retail revisions tracked by the project. Keep this game-owned rather than a -# shared-runner default. -save_type = "flash" -save_size = 262144 From bd2fed348827b5ca1e2d92d40a6a8c3a1bb83688 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:32:30 +0900 Subject: [PATCH 157/164] Update multi-ROM bring-up notes for latest upstream sync --- docs/EU1_1_BRINGUP.md | 105 ++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md index 0871764..fc36f3c 100644 --- a/docs/EU1_1_BRINGUP.md +++ b/docs/EU1_1_BRINGUP.md @@ -2,9 +2,9 @@ This document describes the current multi-ROM architecture in this branch. It supersedes the earlier whole-ROM-SHA-1-driven runtime-profile design and -includes the upstream Wi-Fi firmware-state persistence work integrated from -`mstan/MetroidPrimeHuntersRecomp` commit -`5abcfee6187d572e752985ede2364f165d62dd6a`. +tracks upstream `mstan/MetroidPrimeHuntersRecomp` through commit +`905ffab20ecd0d9c3c1017fb757aec73c435a1ad`. Upstream title-specific changes +are integrated without restoring its US1.0-only runtime identity assumptions. ## 1. Status @@ -25,14 +25,17 @@ Adaptive Widescreen is also statically address-mapped for all seven base revisions. It is no longer hidden for EU1.1. Actual guest code/data writes are still fail-closed and require an authoritative executable checksum. -Exact build/capture content profiles currently exist for: +The ROM-free generic runner can execute supported revisions through the +reference/Tier-3 path without a revision-specific native title bank. Exact +build/capture profiles are still useful for optimized AOT/JIT/cache provenance. +Current exact content profiles are: - `US1_0` clean retail ROM - `EU1_1` clean retail ROM -The other retail revisions and individual modified ROMs still require their own -exact-content build/capture profile, coverage and generated artifacts before -they can be called fully supported. +The other retail revisions and individual modified ROMs can run through the +runtime detector, but still need their own exact-content optimization coverage +before a content-specific native cache/bank can be reused safely. ## 2. Sources of truth @@ -108,6 +111,7 @@ Whole-ROM SHA-1 identifies the exact content used to generate or validate: - runtime coverage - checkpoints - FMV/runtime captures +- future persistent optimization caches This identity is not the runtime-address selector. @@ -217,12 +221,13 @@ Canonical clean profiles reserve the seven base keys. Example: "known_clean": true, "game_code": "AMHE", "revision": 0, - "sha1": "...exact clean whole-ROM SHA-1..." + "sha1": "...exact clean whole-ROM SHA-1...", + "game_config": "game.toml" } ``` A future exact mod profile must use a distinct key and reference the compatible -base layout: +base layout. Frontend policy remains the shared runtime-generic `game.toml`: ```json "US1_0_LAST_RAVEN": { @@ -233,7 +238,7 @@ base layout: "sha1": "...exact Last Raven whole-ROM SHA-1...", "program_id": "mph_amhe0_last_raven", "coverage": "coverage/last-raven-entry-points.json", - "game_config": "config/game-last-raven.toml", + "game_config": "game.toml", "fmv_runtime": false, "fmv_runtime_bank": "mph_amhe0_last_raven_arm9_fmv_runtime", "launcher_default_rom": "Metroid Prime Hunters - Last Raven.nds", @@ -256,6 +261,7 @@ Current clean EU1.1 profile: - revision: `1` - whole-ROM SHA-1: `bdcd1dea293e24c98d4c481430e90d21198985a5` - default launcher ROM: `Metroid Prime Hunters (Europe Rev 1).nds` +- shared frontend config: `game.toml` - Adaptive Widescreen: exposed and revision-aware - FMV runtime bank: disabled until an EU1.1-specific capture is validated @@ -267,12 +273,21 @@ Reserved EU1.1 runtime bank identity: No US1.0 FMV runtime capture is reused for EU1.1. -## 9. Upstream Wi-Fi firmware-state persistence +## 9. Latest upstream launcher / Wi-Fi integration -The branch integrates the upstream `5abcfee` Wi-Fi persistence behavior while -preserving the multi-ROM launcher generation and SHA policy. +The branch tracks upstream title changes through `905ffab` while preserving the +multi-ROM launcher generation and runtime detector. -The launcher now assigns mutable firmware state under: +Relevant upstream additions now carried here include: + +- persistent mutable firmware/WFC state from `5abcfee` +- local WFC peer routing support through ndsrecomp `302404ad...` +- friend-match QA continuation from `1932f6` +- persisted user-selected ROM path from `413c61` +- offline multiplayer/bot coverage tooling and overlay-generation utilities +- the `run_to_event` exhaustion fix in `905ffab` + +The launcher assigns mutable firmware state under: `%APPDATA%\MetroidPrimeHuntersRecomp` @@ -285,17 +300,19 @@ The selected state file is passed to the runner through `--firmware-state-path`. Wi-Fi settings, WFC updates and console/game-card pairing can therefore persist across normal launches and guest shutdowns. -The dashboard console-MAC display prefers the persisted mutable firmware state -after it exists, falling back to the generated installation identity on the -first generated-firmware launch. +The launcher also remembers the selected ROM path. A remembered ROM is offered +on the next launch only if it still exists. The multi-ROM launcher-generation +layer adds a further guard so a missing conventional ROM filename is never +presented to recomp-ui as an already-selected cartridge. -This required updating the pinned ndsrecomp revision to: +The pinned ndsrecomp revision is now: -`6c6a03bdcf99093f64555c4d05d16e522dc58634` +`302404ada0929528b680fa6808aad253b425c7a2` -The new upstream runner still contains old US1.0 SHA/address assumptions in its -baseline source, so the local multi-ROM patch stack remains authoritative and -replaces them during the build. +That revision adds per-instance slirp/local WFC peer routing. The upstream +runner still contains US1.0 assumptions in title-specific baseline code, so the +local multi-ROM patch stack remains authoritative and replaces those assumptions +during the build. ## 10. Preparing an exact content profile @@ -323,7 +340,7 @@ python tools/prepare_mph.py \ It then extracts ARM9, ARM7 and overlays and emits content-specific seed configs. -## 11. Coverage and capture safety +## 11. Coverage, overlays and capture safety Static and runtime promotion is profile/content aware. @@ -338,6 +355,14 @@ not reused from US1.0 constants. FMV/runtime capture promotion verifies exact content provenance. A capture from one content SHA must not be promoted into another content profile. +The latest upstream sync also carries validated US1.0 overlay seed configs for +overlays `0, 1, 2, 3, 4, 8, 9, 10, 15`, plus +`tools/seed_overlay_from_coverage.py`, `tools/overlay_coverage_report.py` and +`tools/mph_overlay_route.py`. These are **US1.0 exact-content optimization +assets**, not runtime profiles and not generic multi-ROM banks. They must never +be reused for another ROM content identity merely because that ROM shares the +same runtime layout. The ROM-free Nightly still does not link these title banks. + ## 12. CI and runtime safety tests CI downloads the current melonPrimeDS `develop_hud` detector/address table and @@ -360,36 +385,28 @@ produce the canonical melonPrimeDS executable checksums. It verifies: - patched `title_patches.cpp`, `frontend.cpp` and `main.cpp` compile against the pinned runner - US1.0/EU1.1 launcher generation and exact-content ROM checkers compile +- upstream launcher/tests and the imported overlay QA tools remain pinned to the + audited upstream title commit -At the time of this update, `MPH Multi-ROM Static Checks` run #72 passes every -main validation step. - -## 13. Remaining work for full multi-ROM support +`MPH Multi-ROM Static Checks` #123 passed after the upstream `905ffab` / ndsrecomp +`302404ad...` integration. -The detector/address architecture is prepared for all seven base revisions, but -full player-facing support still requires exact content work per ROM: +## 13. Remaining optimization work -1. add verified clean content profiles for US1.1, EU1.0, JP1.0, JP1.1 and KR1.0 -2. prepare/extract each exact ROM -3. capture deterministic coverage and promote it under that exact SHA -4. generate revision-specific ARM9/ARM7/overlay banks -5. validate boot, Adventure, pause, save/load and multiplayer-menu paths -6. validate Prime Controls, Direct Mouse Aim and Adaptive Widescreen semantics - on real execution -7. compare checkpoints against reference/native execution -8. capture revision-specific FMV runtime code only where interpreter fallback - needs optimization -9. regression-test US1.0 after each new profile +Runtime execution is available through the generic ROM-free runner, but +content-specific native optimization remains separate work per exact ROM. -For a modified ROM, additionally: +For another retail revision or modified ROM: -1. compute the exact whole-ROM SHA-1 +1. compute/record the exact whole-ROM SHA-1 for cache/provenance identity 2. compute the melonPrimeDS-compatible header+ARM9+ARM7 CRC32 3. determine and validate its runtime base layout 4. if code-modified, register the executable checksum only after Aim/Morph/ Widescreen address compatibility is established -5. add a distinct content profile with `base_profile` -6. generate/promote coverage and banks under that mod's exact content identity +5. add a distinct exact content profile when persistent optimization artifacts + are required +6. generate/promote coverage and banks/cache entries under that exact content + identity Do not guess an unknown mod as US1.0 simply because its filename, region or header resembles US1.0. From d6914728d1db039a929169e1e3f4c57260bae02d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:40:53 +0900 Subject: [PATCH 158/164] Fix generic config adaptive capability gate --- game.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/game.toml b/game.toml index 4089e6b..ac25d18 100644 --- a/game.toml +++ b/game.toml @@ -13,7 +13,11 @@ antialiasing = 0 # clickable. Runtime executable compatibility, not whole-ROM SHA-1, gates the # title-owned projection/culling patch for all supported revisions. adaptive_widescreen = "top" -adaptive_capability = "top" +# Do not declare display.adaptive_capability here. ndsrecomp intentionally +# requires an exact game.sha1 for TOML-declared title capabilities. This shared +# config has no exact content identity, so the MPH runtime patch grants TOP +# capability only after the executable-compatible detector selects a supported +# base profile. Unknown/header-only content remains fail-closed. adaptive_width = 448 adaptive_skybox_fill = false adaptive_hud_anchor = true From f4bf870ec75c437a441890a60ab32b7f19d22a8b Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:41:17 +0900 Subject: [PATCH 159/164] Grant adaptive capability after MPH runtime detection --- ...patch_ndsrecomp_mph_adaptive_capability.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tools/patch_ndsrecomp_mph_adaptive_capability.py diff --git a/tools/patch_ndsrecomp_mph_adaptive_capability.py b/tools/patch_ndsrecomp_mph_adaptive_capability.py new file mode 100644 index 0000000..9e811f0 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_adaptive_capability.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Grant MPH adaptive output capability after executable validation. + +The shared ROM-free game.toml intentionally has no exact [game].sha1 because +runtime base selection supports all seven MPH revisions and compatible mods. +ndsrecomp correctly rejects TOML-declared display.adaptive_capability without +an exact SHA, so the generic config must not declare that capability itself. + +Instead, this patch runs after the MPH runtime detector/widescreen patch and +marks TOP adaptive output as supported only when the detector has selected MPH +and the executable checksum is authoritative enough to permit host code/data +writes. Header-only fallback therefore remains fail-closed. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +MARKER = "MPH_MULTIROM_RUNTIME_ADAPTIVE_CAPABILITY" +INSERT_BEFORE = " // MPH_MULTIROM_WIDESCREEN_GATE: a header-only base-profile hint is\n" + + +def patch(framework_root: Path) -> None: + main_cpp = framework_root / "runner" / "src" / "main.cpp" + if not main_cpp.is_file(): + raise SystemExit(f"runner source missing: {main_cpp}") + + text = main_cpp.read_text(encoding="utf-8") + if MARKER in text: + print("MPH runtime adaptive capability patch already applied") + return + if INSERT_BEFORE not in text: + raise SystemExit( + "Refusing to patch main.cpp: MPH widescreen gate marker was not found; " + "apply patch_ndsrecomp_mph_widescreen.py first" + ) + + block = ( + " // MPH_MULTIROM_RUNTIME_ADAPTIVE_CAPABILITY: the shared game.toml has\n" + " // no exact SHA-1, so ndsrecomp cannot safely grant title capability\n" + " // during config parsing. Grant TOP only after the executable-compatible\n" + " // MPH detector has authorized host code/data access.\n" + " if (nds_title_patches_mph_detected() &&\n" + " nds_title_patches_mph_host_writes_compatible()) {\n" + " frontend_options.adaptive_supported |= NDS_ADAPTIVE_TOP;\n" + " frontend_options.adaptive_max_width[0] = 448;\n" + " }\n" + ) + main_cpp.write_text(text.replace(INSERT_BEFORE, block + INSERT_BEFORE, 1), + encoding="utf-8") + print("Patched MPH adaptive capability to follow runtime executable validation") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + # Kept for compatibility with the shared patch-stack argument list. + parser.add_argument("--profiles", type=Path, required=False) + args = parser.parse_args() + patch(args.framework_root.resolve()) + + +if __name__ == "__main__": + main() From 5e2b053b4cd85f2a210a3dd4d3ad16d42660aa52 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:41:26 +0900 Subject: [PATCH 160/164] Apply runtime adaptive capability patch --- tools/patch_ndsrecomp_mph_runtime.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py index 4034b1d..d858d33 100755 --- a/tools/patch_ndsrecomp_mph_runtime.py +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -3,7 +3,8 @@ The core detector is kept separately so upstream-facing additions can be layered without weakening its whole-ROM/content identity rules. The later stages add -the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch, make +the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch, grant +adaptive TOP capability only after authoritative MPH executable detection, make that patch re-eligible after an in-process guest reset, and finally add end-user startup diagnostics plus the ROM-free multi-ROM content-gate policy. """ @@ -21,6 +22,7 @@ def main() -> None: for script in ( here / "patch_ndsrecomp_mph_runtime_core.py", here / "patch_ndsrecomp_mph_widescreen.py", + here / "patch_ndsrecomp_mph_adaptive_capability.py", here / "patch_ndsrecomp_mph_widescreen_reset.py", here / "patch_ndsrecomp_mph_diagnostics.py", ): From b425f986095dfbc2d689749f53886d8528225227 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:42:07 +0900 Subject: [PATCH 161/164] Smoke-test generic frontend config on Linux --- .github/workflows/build-linux.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 2445098..769dfb9 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -105,6 +105,28 @@ jobs: exit 1 fi + - name: Smoke-test generic frontend config + run: | + set -euo pipefail + mkdir -p /tmp/mph-generic-config-smoke + set +e + output="$(../ndsrecomp/runner/build-mph-nightly/nds_runner \ + /tmp/mph-generic-config-smoke \ + --rom /tmp/mph-generic-config-smoke/missing.nds \ + --config "$PWD/game.toml" \ + --freebios --generated-firmware --boot direct 2>&1)" + status=$? + set -e + printf '%s\n' "$output" + test "$status" -ne 0 + if grep -q 'invalid frontend config' <<<"$output"; then + echo 'Generic game.toml was rejected by the runner frontend parser' >&2 + exit 1 + fi + grep -q 'cartridge image is missing or truncated' <<<"$output" + grep -q 'MPH_MULTIROM_RUNTIME_ADAPTIVE_CAPABILITY' \ + ../ndsrecomp/runner/src/main.cpp + - name: Install pinned AppImage packaging tools run: | set -euo pipefail From 9b1159e8fe7aae6b5e3e845f875c60a590f83f29 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:42:31 +0900 Subject: [PATCH 162/164] Smoke-test generic frontend config on Windows --- .github/workflows/build-windows.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 8c32a16..07062a2 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -142,6 +142,29 @@ jobs: exit 1 fi + - name: Smoke-test generic frontend config + shell: msys2 {0} + run: | + set -euo pipefail + mkdir -p .ci-generic-config-smoke + set +e + output="$(../ndsrecomp/runner/build-mph-nightly/nds_runner.exe \ + "$PWD/.ci-generic-config-smoke" \ + --rom "$PWD/.ci-generic-config-smoke/missing.nds" \ + --config "$PWD/game.toml" \ + --freebios --generated-firmware --boot direct 2>&1)" + status=$? + set -e + printf '%s\n' "$output" + test "$status" -ne 0 + if grep -q 'invalid frontend config' <<<"$output"; then + echo 'Generic game.toml was rejected by the runner frontend parser' >&2 + exit 1 + fi + grep -q 'cartridge image is missing or truncated' <<<"$output" + grep -q 'MPH_MULTIROM_RUNTIME_ADAPTIVE_CAPABILITY' \ + ../ndsrecomp/runner/src/main.cpp + - name: Build and test launcher shell: msys2 {0} run: | From 4baff5b8be2187c411d5b081a91b0092691915a7 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:43:33 +0900 Subject: [PATCH 163/164] Validate runtime-granted adaptive capability --- tools/check_mph_multirom_profiles.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py index d117b51..efaada2 100755 --- a/tools/check_mph_multirom_profiles.py +++ b/tools/check_mph_multirom_profiles.py @@ -74,8 +74,25 @@ def validate_scale_registry(repo: Path, table: Path | None) -> None: if not shared_config.is_file(): die("shared runtime frontend config is missing: game.toml") shared_text = shared_config.read_text(encoding="utf-8") - if 'adaptive_widescreen = "top"' not in shared_text or 'adaptive_capability = "top"' not in shared_text: - die("shared game.toml does not expose top-screen Adaptive Widescreen") + if 'adaptive_widescreen = "top"' not in shared_text: + die("shared game.toml does not request top-screen Adaptive Widescreen") + # ndsrecomp deliberately requires an exact [game].sha1 when a TOML grants + # display.adaptive_capability. The generic seven-version config has no exact + # SHA, so capability must be granted later by the executable-compatible + # runtime detector instead of being declared here. + if 'adaptive_capability = "top"' in shared_text: + die("shared game.toml must not declare SHA-gated adaptive_capability") + capability_patch = repo / "tools" / "patch_ndsrecomp_mph_adaptive_capability.py" + if not capability_patch.is_file(): + die("runtime adaptive capability patch is missing") + capability_text = capability_patch.read_text(encoding="utf-8") + for required in ( + "nds_title_patches_mph_host_writes_compatible()", + "frontend_options.adaptive_supported |= NDS_ADAPTIVE_TOP", + "MPH_MULTIROM_RUNTIME_ADAPTIVE_CAPABILITY", + ): + if required not in capability_text: + die(f"runtime adaptive capability patch lost required guard: {required}") for forbidden in ("\nid =", "\nregion =", "\nrevision =", "\nrom =", "\nrom_size =", "\nsha1 ="): if forbidden in shared_text: die(f"shared game.toml still contains exact-content identity field: {forbidden.strip()}") From cad67185955079f380ca6b4c309fffec1d0fe658 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 18:43:47 +0900 Subject: [PATCH 164/164] Check adaptive capability patch syntax in CI --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec8b778..f37332d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,8 @@ jobs: run: | set -euo pipefail python -m py_compile \ + tools/patch_ndsrecomp_mph_runtime.py \ + tools/patch_ndsrecomp_mph_adaptive_capability.py \ tools/patch_ndsrecomp_rom_free_release.py \ tools/patch_recomp_ui_mph_multirom.py \ tools/ci/check_rom_free_release_sources.py \