diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..769dfb9 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,179 @@ +name: Build Linux + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +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 + 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 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: + 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: 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 + 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" + bash 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: 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: + name: mph-nightly-linux + path: release-stage/MetroidPrimeHuntersRecomp-linux-v*-x86_64.AppImage + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..07062a2 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,227 @@ +name: Build Windows + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +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 + # 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 + uses: actions/checkout@v4 + + - name: Install MinGW build dependencies + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + # 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 + cache: true + install: >- + mingw-w64-x86_64-gcc + 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: 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: | + 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} + 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: 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: | + 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" \ + '-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 + # 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(),' \ + 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} + 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)" + 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' \ + -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: + name: mph-nightly-windows + path: release-stage/MetroidPrimeHuntersRecomp-windows-x64-v*.zip + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f37332d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,57 @@ +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: + policy: + name: ROM-free release policy + runs-on: ubuntu-24.04 + 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_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 \ + 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 + + windows: + name: Build Windows + needs: policy + uses: ./.github/workflows/build-windows.yml + + linux: + name: Build Linux + needs: policy + uses: ./.github/workflows/build-linux.yml diff --git a/.github/workflows/mph-multirom-static.yml b/.github/workflows/mph-multirom-static.yml new file mode 100644 index 0000000..a90348e --- /dev/null +++ b/.github/workflows/mph-multirom-static.yml @@ -0,0 +1,295 @@ +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 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check script syntax + shell: bash + run: | + set -euo pipefail + python -m py_compile \ + 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/probe_mph_wfc.py \ + tools/probe_mph_online_first_run.py \ + tools/run_mph_friend_match.py \ + tools/mph_overlay_route.py \ + tools/overlay_coverage_report.py \ + tools/seed_overlay_from_coverage.py + 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 } + ' + + - name: Verify upstream launcher and QA sync + shell: bash + run: | + set -euo pipefail + base=https://raw.githubusercontent.com/mstan/MetroidPrimeHuntersRecomp/905ffab20ecd0d9c3c1017fb757aec73c435a1ad + 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 + curl -fsSL --retry 3 "$base/tools/run_mph_friend_match.py" -o /tmp/upstream-friend-match.py + curl -fsSL --retry 3 "$base/tools/mph_overlay_route.py" -o /tmp/upstream-overlay-route.py + curl -fsSL --retry 3 "$base/tools/overlay_coverage_report.py" -o /tmp/upstream-overlay-report.py + curl -fsSL --retry 3 "$base/tools/seed_overlay_from_coverage.py" -o /tmp/upstream-overlay-seed.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 + cmp tools/run_mph_friend_match.py /tmp/upstream-friend-match.py + cmp tools/mph_overlay_route.py /tmp/upstream-overlay-route.py + cmp tools/overlay_coverage_report.py /tmp/upstream-overlay-report.py + cmp tools/seed_overlay_from_coverage.py /tmp/upstream-overlay-seed.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 'std::string rom_path;' launcher/recomp-ui/launcher_main.cpp + grep -q 'feature_count == 3' launcher/recomp-ui/tests/launcher_mod_provider_test.cpp + grep -q 'rom path round trip' launcher/recomp-ui/tests/launcher_mod_provider_test.cpp + for ov in 000 001 002 003 004 008 009 010 015; do + test -f "config/mph_arm9_ov${ov}.toml" + done + + - 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 + 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 + 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: Verify profile-aware coverage routing + shell: bash + run: | + set -euo pipefail + 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", + "scenario": "scenarios/adventure_start.json", + "tier3_coverage": {"entries": [ + {"cpu": 9, "pc": 33570848, "thumb": 0, "kind": 2, "hits": 7}, + {"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['entry_points']['arm9'] and p['entry_points']['arm7'] + PY + grep -q 'rom_bytes\[0x1E\]' tools/prepare_mph.py + ! grep -q 'rom_bytes\[0x1C\]' tools/prepare_mph.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" = 302404ada0929528b680fa6808aad253b425c7a2 + grep -q "pin = \"$pin\"" game.toml + 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: 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) + 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' + 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" + grep -q 'std::filesystem::is_regular_file(default_rom, initial_rom_error)' "$file" + grep -q 'exe.string().c_str(), initial_rom.c_str(),' "$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 + + - 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 + 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 + 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 + diff -u /tmp/first.sha256 /tmp/second.sha256 + 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 'slirp_virtual_network_instance' /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 + + - 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 \ + 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 + + - 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: Compile exact-content ROM checkers + shell: bash + run: | + 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 '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 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" diff --git a/.gitignore b/.gitignore index 1608302..7b9be66 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /saves/ /captures/ /scratch/ +/cache/ *.sav *.log /rom.cfg diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f7ea40..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) +cmake_minimum_required(VERSION 3.20) +project(MetroidPrimeHuntersRecomp VERSION 0.4.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -18,13 +18,76 @@ add_subdirectory( "${NDSRECOMP_ROOT}/recompiler" "${CMAKE_BINARY_DIR}/ndsrecomp-recompiler") +set(MPH_VERSION "US1_0" CACHE STRING + "Metroid Prime Hunters retail revision profile") +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_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 + "${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) +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 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 +111,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 +156,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 +174,56 @@ 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_PROFILE_FMV_RUNTIME_BANK}.toml") + set(MPH_FMV_RUNTIME_IMAGE + "${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) + set(shard_name "0${shard}") + else() + set(shard_name "${shard}") + endif() + list(APPEND MPH_FMV_RUNTIME_SOURCES + "${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_${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_PROFILE_FMV_RUNTIME_BANK}_dispatch.c") + set(MPH_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_PROFILE_FMV_RUNTIME_BANK}.h" + COMMAND $ + --config "${MPH_FMV_RUNTIME_CONFIG}" + --bin "${MPH_FMV_RUNTIME_IMAGE}" + --out "${MPH_RECOMP_DIR}" + --bank "${MPH_PROFILE_FMV_RUNTIME_BANK}" + --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 for ${MPH_VERSION}: expected " + "${MPH_FMV_RUNTIME_CONFIG} and ${MPH_FMV_RUNTIME_IMAGE}") + 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 +252,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() diff --git a/README.md b/README.md index c22a26e..ba035c6 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,35 +22,58 @@ 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)**. +Latest upstream release: +**[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` - -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. + `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 `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,31 +82,38 @@ 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. +- 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 @@ -90,6 +123,12 @@ If your ROM does not match, the launcher/runner should reject it. 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. @@ -139,6 +178,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 +196,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/mph_arm9_ov000.toml b/config/mph_arm9_ov000.toml new file mode 100644 index 0000000..ed2367c --- /dev/null +++ b/config/mph_arm9_ov000.toml @@ -0,0 +1,5572 @@ +# 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 = 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 = 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 = 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 diff --git a/config/mph_arm9_ov001.toml b/config/mph_arm9_ov001.toml new file mode 100644 index 0000000..e0ee93f --- /dev/null +++ b/config/mph_arm9_ov001.toml @@ -0,0 +1,424 @@ +# 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 = 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 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/config/mph_rom_profiles.json b/config/mph_rom_profiles.json new file mode 100644 index 0000000..169bc4e --- /dev/null +++ b/config/mph_rom_profiles.json @@ -0,0 +1,156 @@ +{ + "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", + "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", + "runtime_profiles": { + "US1_0": { + "game_code": "AMHE", + "revision": 0, + "base_checksum": "0x218DA42C", + "runtime": { + "morph_state": "0x020DA818", + "aim_x": "0x020DE526", + "aim_y": "0x020DE52E", + "scale_patch_addr1": "0x02110FFC", + "scale_patch_addr2": "0x0211C638", + "scale_value_addr": "0x02110820" + } + }, + "US1_1": { + "game_code": "AMHE", + "revision": 1, + "base_checksum": "0x91B46577", + "runtime": { + "morph_state": "0x020DB098", + "aim_x": "0x020DEDA6", + "aim_y": "0x020DEDAE", + "scale_patch_addr1": "0x02111ABC", + "scale_patch_addr2": "0x0211D168", + "scale_value_addr": "0x021112E0" + } + }, + "EU1_0": { + "game_code": "AMHP", + "revision": 0, + "base_checksum": "0xA4A8FE5A", + "runtime": { + "morph_state": "0x020DB0B8", + "aim_x": "0x020DEDC6", + "aim_y": "0x020DEDCE", + "scale_patch_addr1": "0x02111ADC", + "scale_patch_addr2": "0x0211D114", + "scale_value_addr": "0x02111300" + } + }, + "EU1_1": { + "game_code": "AMHP", + "revision": 1, + "base_checksum": "0x910018A5", + "runtime": { + "morph_state": "0x020DB138", + "aim_x": "0x020DEE46", + "aim_y": "0x020DEE4E", + "scale_patch_addr1": "0x02111B5C", + "scale_patch_addr2": "0x0211D208", + "scale_value_addr": "0x02111380" + } + }, + "JP1_0": { + "game_code": "AMHJ", + "revision": 0, + "base_checksum": "0xD75F539D", + "runtime": { + "morph_state": "0x020DC6D8", + "aim_x": "0x020E03E6", + "aim_y": "0x020E03EE", + "scale_patch_addr1": "0x0211313C", + "scale_patch_addr2": "0x0211E7E8", + "scale_value_addr": "0x02112960" + } + }, + "JP1_1": { + "game_code": "AMHJ", + "revision": 1, + "base_checksum": "0x42EBF348", + "runtime": { + "morph_state": "0x020DC698", + "aim_x": "0x020E03A6", + "aim_y": "0x020E03AE", + "scale_patch_addr1": "0x021130FC", + "scale_patch_addr2": "0x0211E7A8", + "scale_value_addr": "0x02112920" + } + }, + "KR1_0": { + "game_code": "AMHK", + "revision": 0, + "base_checksum": "0xE54682F3", + "runtime": { + "morph_state": "0x020D3EE4", + "aim_x": "0x020D7C0E", + "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": "game.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 + } + } +} 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": [] + } +} diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 025cd37..f5b17ab 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -140,3 +140,26 @@ overlay table contains 576 bytes (18 records), not 576 separate overlays. 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`. diff --git a/docs/EU1_1_BRINGUP.md b/docs/EU1_1_BRINGUP.md new file mode 100644 index 0000000..fc36f3c --- /dev/null +++ b/docs/EU1_1_BRINGUP.md @@ -0,0 +1,412 @@ +# Metroid Prime Hunters Multi-ROM / EU1.1 Bring-up + +This document describes the current multi-ROM architecture in this branch. +It supersedes the earlier whole-ROM-SHA-1-driven runtime-profile design and +tracks upstream `mstan/MetroidPrimeHuntersRecomp` through commit +`905ffab20ecd0d9c3c1017fb757aec73c435a1ad`. Upstream title-specific changes +are integrated without restoring its US1.0-only runtime identity assumptions. + +## 1. Status + +Runtime address layouts are statically prepared for all seven retail MPH base +revisions: + +| 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` | + +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. + +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 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 + +Runtime detection and Aim/Morph addresses intentionally follow melonPrimeDS +`develop_hud`: + +- 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` + +Adaptive Widescreen additionally uses: + +- 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 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. + +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 addresses for Aim, Morph and Adaptive Widescreen. + +The runtime detector uses: + +1. melonPrimeDS-compatible executable checksum, then +2. exact NDS header `gameCode + revision` fallback. + +NDS header offsets: + +- game code: `0x0C..0x0F` +- ROM revision/version: `0x1E` + +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. + +### 3.2 Executable Compatibility Identity + +melonPrimeDS `CartCommon::Checksum()` is reproduced exactly: + +1. CRC32 over ROM header bytes `0x00..0x3F` +2. continue CRC32 over the ARM9 ROM image +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/Adaptive-Widescreen RAM/code +access. + +A header-only match is weaker. It identifies a candidate base profile, but it +**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 executable-compatible US1.0. + +### 3.3 Actual Content Identity + +Whole-ROM SHA-1 identifies the exact content used to generate or validate: + +- build inputs +- recomp banks +- static coverage +- runtime coverage +- checkpoints +- FMV/runtime captures +- future persistent optimization caches + +This identity is not the runtime-address selector. + +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. 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. + +Current entries include: + +- all seven clean retail revisions +- 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. + +melonPrimeDS and mphCodex identify three revision-specific locations: + +| 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` | + +For 21:9 the runner mirrors melonPrimeDS's guarded patch semantics: + +- 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 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 +- 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 + 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. + +## 7. Content profiles and `base_profile` + +`config/mph_rom_profiles.json` schema 5 separates content identity from runtime +base identity. + +Canonical clean profiles reserve the seven base keys. Example: + +```json +"US1_0": { + "base_profile": "US1_0", + "known_clean": true, + "game_code": "AMHE", + "revision": 0, + "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. Frontend policy remains the shared runtime-generic `game.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": "game.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": true +} +``` + +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. + +## 8. EU1.1 exact-content identity + +Current clean EU1.1 profile: + +- 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` +- shared frontend config: `game.toml` +- Adaptive Widescreen: exposed and revision-aware +- FMV runtime bank: disabled until an EU1.1-specific capture is validated + +Reserved EU1.1 runtime bank identity: + +- 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` + +No US1.0 FMV runtime capture is reused for EU1.1. + +## 9. Latest upstream launcher / Wi-Fi integration + +The branch tracks upstream title changes through `905ffab` while preserving the +multi-ROM launcher generation and runtime detector. + +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` + +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 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. + +The pinned ndsrecomp revision is now: + +`302404ada0929528b680fa6808aad253b425c7a2` + +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 + +The preparation path intentionally remains exact-content gated. This protects +generated code provenance; it is not the runtime selector. + +Example EU1.1 preparation: + +```bash +python tools/prepare_mph.py \ + --version EU1_1 \ + --rom "/path/to/Metroid Prime Hunters (Europe Rev 1).nds" \ + --coverage coverage/eu11-bootstrap-entry-points.json \ + --out generated/EU1_1/inputs +``` + +`prepare_mph.py` checks: + +- exact whole-ROM SHA-1 +- expected ROM size +- game code at `0x0C` +- revision at `0x1E` +- coverage `game_sha1` + +It then extracts ARM9, ARM7 and overlays and emits content-specific seed +configs. + +## 11. Coverage, overlays and capture safety + +Static and runtime promotion is profile/content aware. + +For non-US content, traces carry both: + +- content profile key +- exact whole-ROM SHA-1 + +Main-image geometry is derived from the selected prepared ARM9/ARM7 configs, +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 +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 +- 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 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 +- upstream launcher/tests and the imported overlay QA tools remain pinned to the + audited upstream title commit + +`MPH Multi-ROM Static Checks` #123 passed after the upstream `905ffab` / ndsrecomp +`302404ad...` integration. + +## 13. Remaining optimization work + +Runtime execution is available through the generic ROM-free runner, but +content-specific native optimization remains separate work per exact ROM. + +For another retail revision or modified ROM: + +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 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. 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. diff --git a/game.toml b/game.toml index 5bc7202..ac25d18 100644 --- a/game.toml +++ b/game.toml @@ -1,70 +1,45 @@ [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 # 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 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 -# 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 = "46b12e6c18dea47f87d2c1f98c3054149dcbca5d" +pin = "302404ada0929528b680fa6808aad253b425c7a2" 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" diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index f4fe4a7..3c3005e 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -6,26 +6,129 @@ 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_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING + "Default ROM filename offered by this profile-specific launcher") -add_executable(mph-recomp-ui launcher_main.cpp +# 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) + 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() + +mph_launcher_replace_required( + "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" + "${MPH_LAUNCHER_ROM_SHA1}" + "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;" + "the recomp-ui whole-ROM SHA-1 gate") +mph_launcher_replace_required( + "game.region = \"USA\";" + "game.region = \"${MPH_LAUNCHER_REGION}\";" + "the USA region baseline") +mph_launcher_replace_required( + "exe / \"Metroid Prime Hunters.nds\";" + "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}") + +# 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 - "${NDSRECOMP_ROOT}/recompiler/support") + "${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 "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) +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" + --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}") +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() 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. +# 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..780e3b9 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; @@ -72,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. @@ -94,6 +107,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 +327,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 +380,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") { @@ -340,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") { @@ -407,9 +477,13 @@ 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=" + << (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' @@ -420,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) @@ -446,12 +521,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 +544,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 +608,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 +629,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 +751,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 +823,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; @@ -822,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; } @@ -908,6 +1097,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 +1110,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 +1136,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 +1203,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."); @@ -1037,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, @@ -1052,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 638b4f5..fd209cb 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,129 @@ 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); + } + + // 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; } diff --git a/ndsrecomp.pin b/ndsrecomp.pin index 0d4b600..a771dff 100644 --- a/ndsrecomp.pin +++ b/ndsrecomp.pin @@ -1 +1 @@ -46b12e6c18dea47f87d2c1f98c3054149dcbca5d +302404ada0929528b680fa6808aad253b425c7a2 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. 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 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/scenarios/multiplayer_battle_bots.json b/scenarios/multiplayer_battle_bots.json new file mode 100644 index 0000000..2a101e1 --- /dev/null +++ b/scenarios/multiplayer_battle_bots.json @@ -0,0 +1,165 @@ +{ + "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", + "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": "wait", + "frames": 300 + } + ] +} \ 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/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; } diff --git a/tools/benchmark_mph_fmv.py b/tools/benchmark_mph_fmv.py index 5c36090..be5a1de 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,26 @@ 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) + + 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 profile_adaptive else "none" + targets = sorted(set(args.targets)) if not targets or targets[0] <= 0: parser.error("targets must contain positive VBlank counts") @@ -132,16 +195,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 +223,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 +236,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 +310,7 @@ def main() -> int: encoding="utf-8", newline="\n", ) + if args.discover_static_misses: report["tier3_coverage"] = client.command( "tier3_coverage", max=262_144 diff --git a/tools/build-linux.sh b/tools/build-linux.sh index c2bd0bb..f6c8d74 100644 --- a/tools/build-linux.sh +++ b/tools/build-linux.sh @@ -1,169 +1,239 @@ #!/usr/bin/env bash -# Build a Metroid Prime Hunters Recomp Linux x86_64 AppImage. -# -# 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. set -euo pipefail -APP_NAME="MetroidPrimeHuntersRecomp" -TITLE_TARGET="metroidprimehuntersrecomp" -ROM_SHA1="90164d1ac127ee5f9815ea4ae7de798c7b5fc629" -RUNNER_NAME="nds_runner" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FRAMEWORK_ROOT="$(cd "$ROOT/../ndsrecomp" && pwd)" VERSION="0.1.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" +MPH_VERSION="US1_0" +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;; - --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,14p' "$0" + usage exit 0 ;; - *) echo "unknown arg: $1" >&2; exit 2;; + *) + printf 'Unknown option: %s\n' "$1" >&2 + usage >&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_FILE="$ROOT/config/mph_rom_profiles.json" +readarray -t PROFILE_VALUES < <( + python3 - "$PROFILE_FILE" "$MPH_VERSION" <<'PY' +import json +import pathlib +import sys + +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", "fmv_runtime_bank", +): + 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"]) +print(profile["fmv_runtime_bank"]) +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]}" +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 + ROM_PATH="$ROOT/$DEFAULT_ROM_NAME" +fi -cd "$REPO" -test -f "$FRAMEWORK_ROOT/recompiler/CMakeLists.txt" || { - echo "ERROR: sibling ndsrecomp checkout is missing." >&2 +if [[ ! -f "$ROM_PATH" ]]; then + printf 'ROM not found: %s\n' "$ROM_PATH" >&2 exit 1 -} -test -f "$REPO/Metroid Prime Hunters.nds" || { - echo "ERROR: verified Metroid Prime Hunters ROM is missing from the repo root." >&2 +fi +if [[ ! -f "$GAME_CONFIG" ]]; then + printf 'Game config not found: %s\n' "$GAME_CONFIG" >&2 exit 1 -} +fi + +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 -echo "[1/4] configure title banks" -cmake -S "$REPO" -B "$GAME_BUILD" -G "Unix Makefiles" \ +if [[ "$MPH_VERSION" == "US1_0" ]]; then + TITLE_BANK_DIR="$ROOT/generated/recomp" +else + TITLE_BANK_DIR="$ROOT/generated/$MPH_VERSION/recomp" +fi + +printf 'Building MPH profile %s (%s rev %s)\n' "$MPH_VERSION" "$GAME_CODE" "$REVISION" + +cmake -S "$ROOT" -B "$BUILD_DIR" \ -DCMAKE_BUILD_TYPE=Release \ - -DNDSRECOMP_ROOT="$FRAMEWORK_ROOT" -echo "[2/4] build title banks" -cmake --build "$GAME_BUILD" --target "$TITLE_TARGET" -j"$JOBS" + -DNDSRECOMP_ROOT="$FRAMEWORK_ROOT" \ + -DMPH_VERSION="$MPH_VERSION" \ + -DMPH_ROM="$ROM_PATH" +cmake --build "$BUILD_DIR" --target metroidprimehuntersrecomp -j "$JOBS" + +python3 "$ROOT/tools/patch_ndsrecomp_mph_runtime.py" \ + --framework-root "$FRAMEWORK_ROOT" \ + --profiles "$PROFILE_FILE" -echo "[3/4] 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" + +RUNNER="$RUNNER_BUILD_DIR/nds_runner" +if [[ ! -x "$RUNNER" ]]; then + printf 'Runner missing after build: %s\n' "$RUNNER" >&2 + exit 1 +fi -if [ "$DO_PACKAGE" = "0" ]; then - echo "done: $RUNNER_BUILD/$RUNNER_NAME" +if [[ "$FMV_RUNTIME" == "1" ]]; then + 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 + +if ((PACKAGE == 0)); then + printf 'Runner ready: %s\n' "$RUNNER" + printf 'Profile config: %s\n' "$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 -} +rm -rf "$APPDIR" +mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/mph-recomp" "$APPDIR/bios" +cp "$RUNNER" "$APPDIR/usr/bin/nds_runner" +cp "$GAME_CONFIG" "$APPDIR/usr/share/mph-recomp/game.toml" +cp "$ROOT/packaging/BIOS_README.txt" "$APPDIR/bios/README.txt" +cp "$ROOT/README.md" "$APPDIR/README.md" +cp "$ROOT/LICENSE" "$APPDIR/LICENSE" -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 "$REPO/game.toml" "$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' +#!/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 +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" -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 +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 -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 + +if [[ "$MPH_VERSION" == "US1_0" ]]; then + OUTPUT="$ROOT/release-stage/MetroidPrimeHuntersRecomp-linux-v${VERSION}-x86_64.AppImage" +else + OUTPUT="$ROOT/release-stage/MetroidPrimeHuntersRecomp-${MPH_VERSION}-linux-v${VERSION}-x86_64.AppImage" fi -exec "$HERE/usr/bin/nds_runner" "$@" -EOF -chmod +x "$APPDIR/AppRun" "$APPDIR/usr/bin/$RUNNER_NAME" - -echo "[4/4] 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 - -APP="$OUT/$APP_NAME-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" -bash "$REPO/tools/test_appimage_layout.sh" "$APPDIR" -sha256sum "$APP" +ARCH=x86_64 "$APPIMAGE_TOOL" "$APPDIR" "$OUTPUT" +printf 'Created %s\n' "$OUTPUT" \ No newline at end of file diff --git a/tools/build-windows.ps1 b/tools/build-windows.ps1 index 3f75d53..fe66d92 100644 --- a/tools/build-windows.ps1 +++ b/tools/build-windows.ps1 @@ -1,22 +1,26 @@ <# -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 paths. Non-US profiles use isolated generated +banks, a revision-specific game config, a profile-specific launcher +identity/policy, and the shared runtime base-profile / executable-compatibility +detector for Prime Controls/direct mouse aim. 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 ` + -RomPath 'D:\ROMs\Metroid Prime Hunters (Europe) (Rev 1).nds' #> param( [string]$Version = '0.1.0', + [string]$MphVersion = 'US1_0', + [string]$RomPath = '', [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]$LauncherBuildDir = 'launcher\recomp-ui\build-release', + [string]$GameBuildDir = '', + [string]$RunnerBuildDir = '', + [string]$LauncherBuildDir = '', [string]$RuntimeBinDir = 'C:\msys64\mingw64\bin', [string]$RecompUiRoot = 'F:\Projects\recomp-ui' ) @@ -28,46 +32,131 @@ 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) { + $choices = @($registry.profiles.PSObject.Properties.Name) -join ', ' + throw "Unknown MPH profile '$MphVersion'. Configured profiles: $choices" +} +$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' } +$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))) + +if ([string]::IsNullOrWhiteSpace($RomPath)) { + $RomPath = Join-Path $root $launcherDefaultRom +} +$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') { + $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" + } +} +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)) $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")) +} + +$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))" + Write-Host "Launcher adaptive widescreen: $launcherAdaptive" + Write-Host "FMV runtime bank identity: $fmvRuntimeBank" + & $cmakePath -G $Generator -S $root -B $gameBuild ` -DCMAKE_BUILD_TYPE=Release ` - -DNDSRECOMP_ROOT="$frameworkRoot" + -DNDSRECOMP_ROOT="$frameworkRoot" ` + -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 ` "-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.' } & $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" ` + "-DMPH_LAUNCHER_DEFAULT_ROM=$launcherDefaultRom" ` + "-DMPH_LAUNCHER_ADAPTIVE_WIDESCREEN=$launcherAdaptive" 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, + '-FmvRuntimeBank', $fmvRuntimeBank + ) + if (-not [bool]$profile.fmv_runtime) { + $releaseArgs += '-AllowNoFmvRuntime' + } + & powershell.exe @releaseArgs if ($LASTEXITCODE -ne 0) { throw 'Release packaging failed.' } } finally { Pop-Location -} +} \ No newline at end of file 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", 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() diff --git a/tools/check_mph_multirom_profiles.py b/tools/check_mph_multirom_profiles.py new file mode 100755 index 0000000..efaada2 --- /dev/null +++ b/tools/check_mph_multirom_profiles.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Validate the current multi-ROM schema, then run the legacy deep checks. + +The legacy checker predates the all-version Adaptive Widescreen address table +and the shared runtime-generic frontend config. Preserve its broad coverage on +a temporary compatibility view while validating the current scale fields and +generic config contract against the real registry. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import re +import shutil +import tempfile +from pathlib import Path + +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), +} +SCALE_FIELDS = ("scale_patch_addr1", "scale_patch_addr2", "scale_value_addr") + + +def die(message: str) -> None: + raise SystemExit(message) + + +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 + + +def validate_scale_registry(repo: Path, table: Path | None) -> None: + registry_path = repo / "config" / "mph_rom_profiles.json" + 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): + 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: + 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()}") + + 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") + 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") + 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, (member_name, list_name) in rows.items(): + match = re.search( + rf"X\(ADDR,\s*{member_name},\s*{list_name},\s*([^\n]+)\)", text + ) + 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: + """Synthesize the old per-profile config shape only for the legacy checker.""" + target = destination / "repo" + shutil.copytree( + repo, target, + ignore=shutil.ignore_patterns(".git", "generated", "build", "build-*", "scratch"), + ) + 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) + + # 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 + + +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) + args = parser.parse_args() + 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: seven-version runtime tables and shared generic game.toml are consistent") + + +if __name__ == "__main__": + 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/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()) 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() diff --git a/tools/ci/verify-nightly-assets.py b/tools/ci/verify-nightly-assets.py new file mode 100644 index 0000000..2d8f341 --- /dev/null +++ b/tools/ci/verify-nightly-assets.py @@ -0,0 +1,119 @@ +#!/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", + "cache/banks/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()) diff --git a/tools/fuzz_mph_gameplay.py b/tools/fuzz_mph_gameplay.py index 74f329a..762ec3f 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,10 @@ def main() -> int: process = launch(args, output) trace: dict[str, object] = { + "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, diff --git a/tools/make_release.ps1 b/tools/make_release.ps1 index 84de180..ef31866 100644 --- a/tools/make_release.ps1 +++ b/tools/make_release.ps1 @@ -1,22 +1,24 @@ <# 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 +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, [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', + [string]$FmvRuntimeBank = 'mph_arm9_fmv_runtime', + [switch]$AllowNoFmvRuntime ) $ErrorActionPreference = 'Stop' @@ -26,18 +28,26 @@ $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) { + if ([string]::IsNullOrWhiteSpace($FmvRuntimeBank)) { + throw 'FMV runtime bank identity is empty.' + } + $runnerText = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($runner)) + if (-not $runnerText.Contains($FmvRuntimeBank)) { + throw "Runner does not contain required FMV runtime bank '$FmvRuntimeBank'." + } } $projectText = Get-Content (Join-Path $root 'CMakeLists.txt') -Raw @@ -47,7 +57,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 +88,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') ` @@ -153,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 diff --git a/tools/mph_overlay_route.py b/tools/mph_overlay_route.py new file mode 100644 index 0000000..05a7ce4 --- /dev/null +++ b/tools/mph_overlay_route.py @@ -0,0 +1,211 @@ +#!/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_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: + 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) + 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.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) + 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()) diff --git a/tools/mph_profile.py b/tools/mph_profile.py new file mode 100755 index 0000000..6305a09 --- /dev/null +++ b/tools/mph_profile.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Shared Metroid Prime Hunters ROM-profile helpers for build/capture tools.""" + +from __future__ import annotations + +import hashlib +import json +import tomllib +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, + "coverage": str, + "game_config": str, + "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) + 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") + + 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 + + +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_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], + 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) <= 0x1E: + 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[0x1E] != revision: + raise SystemExit( + f"ROM revision mismatch for {version}: got {header[0x1E]}, " + f"expected {revision}" + ) + return actual_sha1 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/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()) diff --git a/tools/package-linux-appimage.sh b/tools/package-linux-appimage.sh new file mode 100644 index 0000000..f5c9036 --- /dev/null +++ b/tools/package-linux-appimage.sh @@ -0,0 +1,193 @@ +#!/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 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 metadata (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/mph-recomp" \ + "$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" +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" +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 +set -eu +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 + +# 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 +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 \ + --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 \ + --freebios --generated-firmware --boot direct +fi +exec "$HERE/usr/bin/nds_runner" "$@" +EOF +chmod 0755 "$APPDIR/AppRun" + +# 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' \ + \) -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" 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" 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() 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() diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py new file mode 100755 index 0000000..d858d33 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Apply the MPH runtime-profile patch stack to the pinned ndsrecomp runner. + +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, 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. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def main() -> None: + 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_adaptive_capability.py", + here / "patch_ndsrecomp_mph_widescreen_reset.py", + here / "patch_ndsrecomp_mph_diagnostics.py", + ): + subprocess.run([sys.executable, str(script), *args], check=True) + + +if __name__ == "__main__": + 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/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py new file mode 100644 index 0000000..256a063 --- /dev/null +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -0,0 +1,126 @@ +#!/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") + # 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: + 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''', + "set_source_files_properties(\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() diff --git a/tools/patch_recomp_ui_mph_multirom.py b/tools/patch_recomp_ui_mph_multirom.py new file mode 100644 index 0000000..20e310b --- /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. 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: + 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() diff --git a/tools/prepare_mph.py b/tools/prepare_mph.py index af509d7..e16f1da 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[0x1E] != 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[0x1E]}, " + 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}" @@ -261,4 +340,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file 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) diff --git a/tools/promote_mph_runtime_coverage.py b/tools/promote_mph_runtime_coverage.py index 1acfd25..74433de 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,148 @@ 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, + 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}" + ) + 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( + "--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", + "--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"]) + require_identity = args.version != DEFAULT_VERSION + 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", + 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_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}" + ) + try: + capture_bytes = int(capture_bytes_raw) + except (TypeError, ValueError) as exc: + raise SystemExit( + 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}" + ) + 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", + require_identity=require_identity, + ) 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 +160,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 +192,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)) diff --git a/tools/promote_mph_static_coverage.py b/tools/promote_mph_static_coverage.py index d2b1173..864f965 100644 --- a/tools/promote_mph_static_coverage.py +++ b/tools/promote_mph_static_coverage.py @@ -1,37 +1,130 @@ #!/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") + 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") + 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}" + ) + 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 +133,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 +158,23 @@ 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) + scenario = args.scenario if args.scenario is not None else trace.get("scenario") payload = { "schema": 1, - "game_sha1": GAME_SHA1, - "scenario": args.scenario, + "profile": args.version, + "game_sha1": expected_sha1, + "scenario": 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 +183,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 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), } 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()) diff --git a/tools/tests/mph_runtime_profile_test.cpp b/tools/tests/mph_runtime_profile_test.cpp new file mode 100644 index 0000000..21b99f6 --- /dev/null +++ b/tools/tests/mph_runtime_profile_test.cpp @@ -0,0 +1,226 @@ +#include +#include +#include +#include +#include + +#include "state.h" +#include "title_patches.h" + +namespace { + +struct Write32 { + uint32_t addr; + uint32_t value; +}; + +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, {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 = + "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; +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); +} + +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_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()), + actual_sha1, expected_sha1); +} + +} // namespace + +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() { + for (const RuntimeCase& c : kCases) { + 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"); + + g_writes.clear(); + expect(!nds_title_patches_apply_mph_mouse_delta(7, -5), + "profile switch must clear direct-aim enable state"); + nds_title_patches_set_mph_mouse_aim(true); + expect(nds_title_patches_apply_mph_mouse_delta(7, -5), + "authoritative checksum must accept direct mouse aim"); + expect(g_writes.size() == 2, + "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), + "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"); + } + + // 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, ""), + "truncated NDS header must fail closed"); + expect(!nds_title_patches_select_mph_runtime_profile( + us10.data(), static_cast(us10.size()), + nullptr, ""), + "missing actual-content identity must fail closed"); + + if (g_failures != 0) { + std::fprintf(stderr, "%d runtime-profile assertion(s) failed\n", + g_failures); + return 1; + } + std::puts( + "OK: seven MPH base profiles use melonPrimeDS executable CRC; " + "header fallback and whole-ROM provenance gates fail closed"); + return 0; +}