From f1fe8d827e574bc630e4dfad7b6b75de3bfeb242 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:08:43 +0900 Subject: [PATCH 01/44] Pin recomp-ui for reproducible CI builds --- recomp-ui.pin | 1 + 1 file changed, 1 insertion(+) create mode 100644 recomp-ui.pin diff --git a/recomp-ui.pin b/recomp-ui.pin new file mode 100644 index 0000000..f428d8e --- /dev/null +++ b/recomp-ui.pin @@ -0,0 +1 @@ +8e385a0bff407e379414ba5ccbbcde1fac27e5cd From ecabcc194cfbcfd4b8db4bc27b97205c15f2ad21 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:08:55 +0900 Subject: [PATCH 02/44] Add Nightly release payload verifier --- tools/ci/verify-nightly-assets.py | 118 ++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tools/ci/verify-nightly-assets.py diff --git a/tools/ci/verify-nightly-assets.py b/tools/ci/verify-nightly-assets.py new file mode 100644 index 0000000..6af8358 --- /dev/null +++ b/tools/ci/verify-nightly-assets.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Validate MPH Nightly release assets before publishing them.""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path +import re +import sys +import zipfile + + +FORBIDDEN_PARTS = { + "biosnds9.rom", + "biosnds7.rom", + "firmware.bin", +} +FORBIDDEN_SUFFIXES = { + ".nds", + ".sav", + ".dsv", +} +FORBIDDEN_DIRS = { + "generated", + "capture", + "captures", + "saves", +} + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def safe_member(name: str) -> bool: + normalized = name.replace("\\", "/") + parts = [part for part in normalized.split("/") if part not in ("", ".")] + if normalized.startswith("/") or any(part == ".." for part in parts): + return False + lowered = [part.lower() for part in parts] + if any(part in FORBIDDEN_PARTS for part in lowered): + return False + if any(part in FORBIDDEN_DIRS for part in lowered): + return False + if parts and Path(parts[-1]).suffix.lower() in FORBIDDEN_SUFFIXES: + return False + return True + + +def verify_windows(path: Path) -> None: + required = { + "MetroidPrimeHuntersRecomp.exe", + "nds_runner.exe", + "game.toml", + "README.md", + "LICENSE", + "bios/README.txt", + } + with zipfile.ZipFile(path) as archive: + names = { + name.replace("\\", "/").rstrip("/") + for name in archive.namelist() + if name and not name.endswith("/") + } + unsafe = sorted(name for name in names if not safe_member(name)) + if unsafe: + raise SystemExit(f"{path.name}: forbidden/unsafe ZIP entries: {unsafe}") + missing = sorted(required - names) + if missing: + raise SystemExit(f"{path.name}: required release entries missing: {missing}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--dist", type=Path, required=True) + parser.add_argument("--version", required=True) + parser.add_argument("--write-sums", action="store_true") + args = parser.parse_args() + + if not re.fullmatch(r"\d+\.\d+\.\d+", args.version): + raise SystemExit(f"invalid version: {args.version!r}") + + expected = { + f"MetroidPrimeHuntersRecomp-windows-x64-v{args.version}.zip", + f"MetroidPrimeHuntersRecomp-linux-v{args.version}-x86_64.AppImage", + } + actual = {p.name for p in args.dist.iterdir() if p.is_file()} + extra = actual - expected + missing = expected - actual + if extra or missing: + raise SystemExit( + f"nightly payload mismatch; missing={sorted(missing)} extra={sorted(extra)}" + ) + + windows = args.dist / f"MetroidPrimeHuntersRecomp-windows-x64-v{args.version}.zip" + linux = args.dist / f"MetroidPrimeHuntersRecomp-linux-v{args.version}-x86_64.AppImage" + if windows.stat().st_size <= 0 or linux.stat().st_size <= 0: + raise SystemExit("nightly payload contains an empty asset") + + verify_windows(windows) + + sums = "\n".join( + f"{sha256(args.dist / name)} {name}" for name in sorted(expected) + ) + "\n" + if args.write_sums: + (args.dist / "SHA256SUMS.txt").write_text(sums, encoding="utf-8") + else: + sys.stdout.write(sums) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From fef5e8f1cf6fa3b9c3bc618d82e9e0aa1b35bf55 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:09:18 +0900 Subject: [PATCH 03/44] Add portable Linux AppImage packager --- tools/package-linux-appimage.sh | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tools/package-linux-appimage.sh diff --git a/tools/package-linux-appimage.sh b/tools/package-linux-appimage.sh new file mode 100644 index 0000000..5f77b69 --- /dev/null +++ b/tools/package-linux-appimage.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION="0.4.0" +MPH_VERSION="US1_0" +RUNNER="" +OUT="$ROOT/release-stage" +APPIMAGE_TOOL="${APPIMAGE_TOOL:-appimagetool}" +LINUXDEPLOY_BIN="${LINUXDEPLOY_BIN:-linuxdeploy}" + +usage() { + cat <<'EOF' +Package an already-built MPH runner as a Linux x86_64 AppImage. + +Usage: + tools/package-linux-appimage.sh --runner PATH [options] + +Options: + --version VERSION Package version + --mph-version PROFILE Content profile (default: US1_0) + --runner PATH Built nds_runner executable (required) + --out PATH Output directory (default: release-stage) + --appimage-tool PATH appimagetool executable/AppImage + --linuxdeploy PATH linuxdeploy executable/AppImage +EOF +} + +while (($#)); do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --mph-version) MPH_VERSION="$2"; shift 2 ;; + --runner) RUNNER="$2"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + --appimage-tool) APPIMAGE_TOOL="$2"; shift 2 ;; + --linuxdeploy) LINUXDEPLOY_BIN="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac +done + +if [[ -z "$RUNNER" || ! -x "$RUNNER" ]]; then + printf 'Built runner is required: %s\n' "$RUNNER" >&2 + exit 1 +fi +if [[ ! -x "$APPIMAGE_TOOL" ]] && ! command -v "$APPIMAGE_TOOL" >/dev/null 2>&1; then + printf 'appimagetool not found: %s\n' "$APPIMAGE_TOOL" >&2 + exit 1 +fi +if [[ ! -x "$LINUXDEPLOY_BIN" ]] && ! command -v "$LINUXDEPLOY_BIN" >/dev/null 2>&1; then + printf 'linuxdeploy not found: %s\n' "$LINUXDEPLOY_BIN" >&2 + exit 1 +fi + +PROFILE_FILE="$ROOT/config/mph_rom_profiles.json" +GAME_CONFIG_REL="$(python3 - "$PROFILE_FILE" "$MPH_VERSION" <<'PY' +import json, sys +registry = json.load(open(sys.argv[1], encoding='utf-8')) +profile = registry.get('profiles', {}).get(sys.argv[2]) +if not isinstance(profile, dict): + raise SystemExit(f'unknown MPH profile: {sys.argv[2]}') +print(profile['game_config']) +PY +)" +GAME_CONFIG="$ROOT/$GAME_CONFIG_REL" +[[ -f "$GAME_CONFIG" ]] || { printf 'Game config missing: %s\n' "$GAME_CONFIG" >&2; exit 1; } + +mkdir -p "$OUT" +APP_NAME="MetroidPrimeHuntersRecomp" +APPDIR="$OUT/${APP_NAME}-${MPH_VERSION}-linux-x86_64.AppDir" +rm -rf "$APPDIR" +mkdir -p \ + "$APPDIR/usr/bin/bios" \ + "$APPDIR/usr/share/applications" \ + "$APPDIR/usr/share/icons/hicolor/256x256/apps" + +cp "$RUNNER" "$APPDIR/usr/bin/nds_runner" +cp "$GAME_CONFIG" "$APPDIR/usr/bin/game.toml" +cp "$ROOT/README.md" "$APPDIR/usr/bin/README.md" +cp "$ROOT/LICENSE" "$APPDIR/usr/bin/LICENSE" +cp "$ROOT/packaging/BIOS_README.txt" "$APPDIR/usr/bin/bios/README.txt" +chmod 0755 "$APPDIR/usr/bin/nds_runner" + +ICON="$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" +python3 - "$ICON" <<'PY' +import struct, sys, zlib +out = sys.argv[1] +n = 256 +raw = b''.join(bytes([0]) + bytes([162, 62, 64]) * n for _ in range(n)) +def chunk(kind, data): + body = kind + data + return struct.pack('>I', len(data)) + body + struct.pack('>I', zlib.crc32(body) & 0xffffffff) +png = b'\x89PNG\r\n\x1a\n' +png += chunk(b'IHDR', struct.pack('>IIBBBBB', n, n, 8, 2, 0, 0, 0)) +png += chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b'') +open(out, 'wb').write(png) +PY + +DESKTOP="$APPDIR/usr/share/applications/$APP_NAME.desktop" +cat > "$DESKTOP" </dev/null + +cat > "$APPDIR/AppRun" <<'EOF' +#!/bin/sh +HERE="$(dirname "$(readlink -f "$0")")" +export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:-}" +export SDL_JOYSTICK_HIDAPI_STEAM=1 +export SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=1 +SELF="${APPIMAGE:-$0}" +RUNDIR="$(dirname "$(readlink -f "$SELF")")" +mkdir -p "$RUNDIR/bios" 2>/dev/null || true +if [ ! -f "$RUNDIR/bios/README.txt" ] && [ -f "$HERE/usr/bin/bios/README.txt" ]; then + cp "$HERE/usr/bin/bios/README.txt" "$RUNDIR/bios/README.txt" 2>/dev/null || true +fi +ROM="" +for f in "$RUNDIR"/*.nds "$RUNDIR"/*.NDS; do + [ -e "$f" ] && ROM="$f" && break +done +cd "$RUNDIR" 2>/dev/null || true +if [ "$#" -eq 0 ]; then + if [ -n "$ROM" ]; then + exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" \ + --config "$HERE/usr/bin/game.toml" --screen-layout separate \ + --adaptive-widescreen top --startup-mode automatic + fi + exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive \ + --config "$HERE/usr/bin/game.toml" --screen-layout separate \ + --adaptive-widescreen top --startup-mode automatic +fi +exec "$HERE/usr/bin/nds_runner" "$@" +EOF +chmod 0755 "$APPDIR/AppRun" + +# Safety gate: the AppDir may contain only the runner/package support material. +if find "$APPDIR" -type f \( \ + -iname '*.nds' -o -iname '*.sav' -o -iname '*.dsv' -o \ + -iname 'biosnds9.rom' -o -iname 'biosnds7.rom' -o -iname 'firmware.bin' \ + \) -print -quit | grep -q .; then + echo 'Refusing to package ROM/save/BIOS/firmware material.' >&2 + exit 1 +fi + +if [[ "$MPH_VERSION" == "US1_0" ]]; then + OUTPUT="$OUT/${APP_NAME}-linux-v${VERSION}-x86_64.AppImage" +else + OUTPUT="$OUT/${APP_NAME}-${MPH_VERSION}-linux-v${VERSION}-x86_64.AppImage" +fi +rm -f "$OUTPUT" +ARCH=x86_64 "$APPIMAGE_TOOL" --appimage-extract-and-run "$APPDIR" "$OUTPUT" >/dev/null +chmod 0755 "$OUTPUT" +test -s "$OUTPUT" +sha256sum "$OUTPUT" +printf 'Created %s\n' "$OUTPUT" From 44a7b99da471e7167ddcb03c98c3135d57d398af Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:29:21 +0900 Subject: [PATCH 04/44] Add ROM-free runner fallback patch --- tools/patch_ndsrecomp_rom_free_release.py | 122 ++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tools/patch_ndsrecomp_rom_free_release.py diff --git a/tools/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py new file mode 100644 index 0000000..ba13523 --- /dev/null +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Make the pinned ndsrecomp runner buildable without proprietary BIOS banks. + +The public/no-dump build keeps the BSD-licensed FreeBIOS banks native. Retail +BIOS dumps remain usable when a user supplies them, but their immutable BIOS +code executes through the existing reference interpreter instead of requiring +ROM/BIOS-derived generated C in the distributed build. + +This patch is intentionally separate from the MPH title-profile patch stack: +it changes only the shared runner's immutable-BIOS build policy and is useful +for ROM-free CI/release packaging. It is idempotent and pinned to the currently +expected ndsrecomp source shape; drift fails closed. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def replace_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + count = text.count(old) + if count != 1: + raise SystemExit( + f"{path}: expected exactly one source anchor for {marker!r}, got {count}" + ) + path.write_text(text.replace(old, new), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + args = parser.parse_args() + root = args.framework_root.resolve() + + cmake = root / "runner" / "CMakeLists.txt" + state = root / "runner" / "src" / "state.h" + runtime = root / "runner" / "src" / "runtime_arm.cpp" + tier3 = root / "runner" / "src" / "tier3.cpp" + main_cpp = root / "runner" / "src" / "main.cpp" + for path in (cmake, state, runtime, tier3, main_cpp): + if not path.is_file(): + raise SystemExit(f"missing pinned ndsrecomp source: {path}") + + replace_once( + cmake, + '''option(NDS_BOOTSTRAP_FIRMWARE\n "Build with BIOS banks only so guest-produced firmware RAM can be captured"\n OFF)\n''', + '''option(NDS_BOOTSTRAP_FIRMWARE\n "Build with BIOS banks only so guest-produced firmware RAM can be captured"\n OFF)\noption(NDS_RETAIL_BIOS_BANKS\n "Link generated proprietary retail-BIOS banks instead of interpreter fallback"\n ON)\n''', + "NDS_RETAIL_BIOS_BANKS", + ) + + replace_once( + cmake, + '''add_library(nds_banks STATIC\n ${GEN}/arm9_bios.c\n ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c\n ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm9.c\n ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c\n ${GEN}/freebios_arm7_dispatch.c\n ${FW_BANK_BODIES}\n ${FW_BANK_DISPATCH}\n ${SM64DS_BANK_SOURCES}\n ${TITLE_BANK_SOURCES})\n''', + '''# Public/no-dump builds need only the redistributable FreeBIOS static banks.\n# Retail BIOS dumps can still be supplied at runtime when NDS_RETAIL_BIOS_BANKS=OFF;\n# their immutable code then uses the reference interpreter instead of generated C.\nset(IMMUTABLE_BIOS_BANK_SOURCES\n ${GEN}/freebios_arm9.c\n ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c\n ${GEN}/freebios_arm7_dispatch.c)\nif(NDS_RETAIL_BIOS_BANKS)\n list(APPEND IMMUTABLE_BIOS_BANK_SOURCES\n ${GEN}/arm9_bios.c\n ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c\n ${GEN}/arm7_bios_dispatch.c)\nelse()\n add_compile_definitions(NDS_RETAIL_BIOS_INTERPRETER=1)\nendif()\nadd_library(nds_banks STATIC\n ${IMMUTABLE_BIOS_BANK_SOURCES}\n ${FW_BANK_BODIES}\n ${FW_BANK_DISPATCH}\n ${SM64DS_BANK_SOURCES}\n ${TITLE_BANK_SOURCES})\n''', + "IMMUTABLE_BIOS_BANK_SOURCES", + ) + + replace_once( + cmake, + '''set_source_files_properties(\n ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c\n''', + '''set_source_files_properties(\n ${IMMUTABLE_BIOS_BANK_SOURCES}\n''', + "${IMMUTABLE_BIOS_BANK_SOURCES}", + ) + + replace_once( + cmake, + '''set(ARM9_BANK_SOURCES ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c)\nset(ARM7_BANK_SOURCES ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c)\n''', + '''set(ARM9_BANK_SOURCES\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c)\nset(ARM7_BANK_SOURCES\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c)\nif(NDS_RETAIL_BIOS_BANKS)\n list(APPEND ARM9_BANK_SOURCES\n ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c)\n list(APPEND ARM7_BANK_SOURCES\n ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c)\nendif()\n''', + "if(NDS_RETAIL_BIOS_BANKS)\n list(APPEND ARM9_BANK_SOURCES", + ) + + replace_once( + state, + '''extern bool g_discover_static_misses;\n''', + '''extern bool g_discover_static_misses;\n// Public ROM-free builds do not carry generated retail-BIOS code. When a\n// user explicitly supplies retail dumps, allow only immutable BIOS addresses\n// to use the same reference interpreter used by coverage discovery.\nextern bool g_allow_static_bios_interpreter;\n''', + "g_allow_static_bios_interpreter", + ) + + replace_once( + runtime, + '''bool g_discover_static_misses = false;\n''', + '''bool g_discover_static_misses = false;\nbool g_allow_static_bios_interpreter = false;\n''', + "g_allow_static_bios_interpreter = false", + ) + + replace_once( + runtime, + ''' if (g_discover_static_misses && static_bios_pc(pc)) {\n runtime_discovery_note_static(pc, thumb ? 1u : 0u);\n tier3_run(pc);\n return;\n }\n''', + ''' if ((g_discover_static_misses || g_allow_static_bios_interpreter) &&\n static_bios_pc(pc)) {\n if (g_discover_static_misses)\n runtime_discovery_note_static(pc, thumb ? 1u : 0u);\n tier3_run(pc);\n return;\n }\n''', + "g_allow_static_bios_interpreter) &&", + ) + + replace_once( + tier3, + ''' if (!bus_range_has_write_provenance(fetch_addr, fetch_size) &&\n !(g_discover_static_misses && static_bios_pc(pc & ~1u))) {\n''', + ''' if (!bus_range_has_write_provenance(fetch_addr, fetch_size) &&\n !((g_discover_static_misses || g_allow_static_bios_interpreter) &&\n static_bios_pc(pc & ~1u))) {\n''', + "g_allow_static_bios_interpreter) &&", + ) + + replace_once( + main_cpp, + '''extern "C" const DispatchEntry g_dispatch_arm9_bios[];\nextern "C" const unsigned g_dispatch_arm9_bios_len;\nextern "C" const DispatchEntry g_dispatch_arm7_bios[];\nextern "C" const unsigned g_dispatch_arm7_bios_len;\n''', + '''#if !defined(NDS_RETAIL_BIOS_INTERPRETER)\nextern "C" const DispatchEntry g_dispatch_arm9_bios[];\nextern "C" const unsigned g_dispatch_arm9_bios_len;\nextern "C" const DispatchEntry g_dispatch_arm7_bios[];\nextern "C" const unsigned g_dispatch_arm7_bios_len;\n#endif\n''', + "#if !defined(NDS_RETAIL_BIOS_INTERPRETER)", + ) + + replace_once( + main_cpp, + ''' } else {\n nds_register_dispatch(NDS_ARM9, g_dispatch_arm9_bios,\n g_dispatch_arm9_bios_len, 0xFFFF0000u);\n nds_register_dispatch(NDS_ARM7, g_dispatch_arm7_bios,\n g_dispatch_arm7_bios_len, 0x00000000u);\n }\n''', + ''' } else {\n#if defined(NDS_RETAIL_BIOS_INTERPRETER)\n g_allow_static_bios_interpreter = true;\n std::fprintf(stderr,\n "[dispatch] retail BIOS uses reference interpreter "\n "(ROM-free build)\\n");\n#else\n nds_register_dispatch(NDS_ARM9, g_dispatch_arm9_bios,\n g_dispatch_arm9_bios_len, 0xFFFF0000u);\n nds_register_dispatch(NDS_ARM7, g_dispatch_arm7_bios,\n g_dispatch_arm7_bios_len, 0x00000000u);\n#endif\n }\n''', + "retail BIOS uses reference interpreter", + ) + + print(f"Patched ROM-free release support in {root}") + + +if __name__ == "__main__": + main() From 95d511cfd95ee3bcbe5d0bba455b264e77e1d072 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:30:44 +0900 Subject: [PATCH 05/44] Fix ROM-free patch idempotent anchors --- tools/patch_ndsrecomp_rom_free_release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py index ba13523..40b4390 100644 --- a/tools/patch_ndsrecomp_rom_free_release.py +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -20,7 +20,7 @@ def replace_once(path: Path, old: str, new: str, marker: str) -> None: text = path.read_text(encoding="utf-8") - if marker in text: + if marker in text and old not in text: return count = text.count(old) if count != 1: @@ -63,7 +63,7 @@ def main() -> None: cmake, '''set_source_files_properties(\n ${GEN}/arm9_bios.c ${GEN}/arm9_bios_dispatch.c\n ${GEN}/arm7_bios.c ${GEN}/arm7_bios_dispatch.c\n ${GEN}/freebios_arm9.c ${GEN}/freebios_arm9_dispatch.c\n ${GEN}/freebios_arm7.c ${GEN}/freebios_arm7_dispatch.c\n''', '''set_source_files_properties(\n ${IMMUTABLE_BIOS_BANK_SOURCES}\n''', - "${IMMUTABLE_BIOS_BANK_SOURCES}", + "set_source_files_properties(\n ${IMMUTABLE_BIOS_BANK_SOURCES}", ) replace_once( From fcb87fb16138c7c09ae3d3a9ca2abb2538465e24 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:30:57 +0900 Subject: [PATCH 06/44] Add redistributable FreeBIOS bank builder --- tools/ci/prepare_freebios_banks.py | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tools/ci/prepare_freebios_banks.py diff --git a/tools/ci/prepare_freebios_banks.py b/tools/ci/prepare_freebios_banks.py new file mode 100644 index 0000000..7751bd3 --- /dev/null +++ b/tools/ci/prepare_freebios_banks.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Build the redistributable ndsrecomp FreeBIOS native banks for CI/releases.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import subprocess + + +def run(*args: str) -> None: + print("+", " ".join(args), flush=True) + subprocess.run(args, check=True) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--build-dir", type=Path, required=True) + args = parser.parse_args() + + root = args.framework_root.resolve() + build = args.build_dir.resolve() + generated = root / "generated" + freebios = root / "third_party" / "freebios" + arm9_bin = freebios / "drastic_bios_arm9.bin" + arm7_bin = freebios / "drastic_bios_arm7.bin" + arm9_cfg = root / "bios" / "freebios9.toml" + arm7_cfg = root / "bios" / "freebios7.toml" + + for path in (arm9_bin, arm7_bin, arm9_cfg, arm7_cfg): + if not path.is_file(): + raise SystemExit( + f"missing FreeBIOS source {path}; initialize the pinned " + "ndsrecomp third_party/freebios submodule" + ) + + run( + "cmake", + "-S", str(root / "recompiler"), + "-B", str(build), + "-G", "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + ) + run("cmake", "--build", str(build), "--target", "nds_recompile") + + exe = build / ("nds_recompile.exe" if os.name == "nt" else "nds_recompile") + if not exe.is_file(): + raise SystemExit(f"nds_recompile missing after build: {exe}") + + generated.mkdir(parents=True, exist_ok=True) + for cpu, config, image in ( + ("arm9", arm9_cfg, arm9_bin), + ("arm7", arm7_cfg, arm7_bin), + ): + run( + str(exe), + "--config", str(config), + "--bin", str(image), + "--out", str(generated), + "--bank", f"freebios_{cpu}", + ) + + expected = [ + generated / "freebios_arm9.c", + generated / "freebios_arm9_dispatch.c", + generated / "freebios_arm7.c", + generated / "freebios_arm7_dispatch.c", + ] + missing = [str(path) for path in expected if not path.is_file()] + if missing: + raise SystemExit(f"FreeBIOS bank generation incomplete: {missing}") + + print("FreeBIOS banks ready (BSD-2-Clause source path only).") + + +if __name__ == "__main__": + main() From 05fa97ca7bf02eb7e2c0887aac8978baedb31901 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:31:31 +0900 Subject: [PATCH 07/44] Document portable optimization cache directory --- packaging/CACHE_README.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packaging/CACHE_README.txt diff --git a/packaging/CACHE_README.txt b/packaging/CACHE_README.txt new file mode 100644 index 0000000..53c33b0 --- /dev/null +++ b/packaging/CACHE_README.txt @@ -0,0 +1,21 @@ +Metroid Prime Hunters Recomp optimization cache +================================================ + +This directory is reserved for locally generated optimization banks/caches. +The preferred portable layout is: + + cache/banks// + +The whole-ROM SHA-1 is used only as the content/cache namespace. Runtime base +profile selection continues to use the executable-compatible MPH detector and +must never guess a base profile from this cache path. + +Current ROM-free Nightly builds do not generate native title banks here yet; +missing title banks execute through the ndsrecomp Tier-3 reference interpreter. +A future local JIT/portable-bank backend will populate this directory without +requiring a C/C++ compiler on the player's machine. + +If the application directory is not writable, implementations should fall back +to the operating system cache location (LOCALAPPDATA on Windows, XDG cache on +Linux). Saves and firmware identity/state are persistent user data and do not +belong in this regenerable cache. From 6be471d6f847d5d314a46f0ee1062f4fe32cd9a5 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:31:57 +0900 Subject: [PATCH 08/44] Use portable-first cache root in Linux package --- tools/package-linux-appimage.sh | 35 ++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tools/package-linux-appimage.sh b/tools/package-linux-appimage.sh index 5f77b69..f5c9036 100644 --- a/tools/package-linux-appimage.sh +++ b/tools/package-linux-appimage.sh @@ -11,14 +11,14 @@ LINUXDEPLOY_BIN="${LINUXDEPLOY_BIN:-linuxdeploy}" usage() { cat <<'EOF' -Package an already-built MPH runner as a Linux x86_64 AppImage. +Package an already-built ROM-free MPH runner as a Linux x86_64 AppImage. Usage: tools/package-linux-appimage.sh --runner PATH [options] Options: --version VERSION Package version - --mph-version PROFILE Content profile (default: US1_0) + --mph-version PROFILE Content profile metadata (default: US1_0) --runner PATH Built nds_runner executable (required) --out PATH Output directory (default: release-stage) --appimage-tool PATH appimagetool executable/AppImage @@ -71,6 +71,7 @@ APPDIR="$OUT/${APP_NAME}-${MPH_VERSION}-linux-x86_64.AppDir" rm -rf "$APPDIR" mkdir -p \ "$APPDIR/usr/bin/bios" \ + "$APPDIR/usr/share/mph-recomp" \ "$APPDIR/usr/share/applications" \ "$APPDIR/usr/share/icons/hicolor/256x256/apps" @@ -79,6 +80,7 @@ cp "$GAME_CONFIG" "$APPDIR/usr/bin/game.toml" cp "$ROOT/README.md" "$APPDIR/usr/bin/README.md" cp "$ROOT/LICENSE" "$APPDIR/usr/bin/LICENSE" cp "$ROOT/packaging/BIOS_README.txt" "$APPDIR/usr/bin/bios/README.txt" +cp "$ROOT/packaging/CACHE_README.txt" "$APPDIR/usr/share/mph-recomp/CACHE_README.txt" chmod 0755 "$APPDIR/usr/bin/nds_runner" ICON="$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png" @@ -117,6 +119,7 @@ EOF cat > "$APPDIR/AppRun" <<'EOF' #!/bin/sh +set -eu HERE="$(dirname "$(readlink -f "$0")")" export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:-}" export SDL_JOYSTICK_HIDAPI_STEAM=1 @@ -127,6 +130,26 @@ mkdir -p "$RUNDIR/bios" 2>/dev/null || true if [ ! -f "$RUNDIR/bios/README.txt" ] && [ -f "$HERE/usr/bin/bios/README.txt" ]; then cp "$HERE/usr/bin/bios/README.txt" "$RUNDIR/bios/README.txt" 2>/dev/null || true fi + +# Portable-first optimization cache. A future local JIT/portable-bank backend +# namespaces children by whole-ROM content SHA-1; the hash is cache identity, +# never the runtime base-profile selector. If the AppImage directory cannot be +# written, fall back to the standard XDG cache location. +PORTABLE_CACHE="$RUNDIR/cache/banks" +CACHE_ROOT="$PORTABLE_CACHE" +if mkdir -p "$PORTABLE_CACHE" 2>/dev/null && + : > "$PORTABLE_CACHE/.mph-write-test" 2>/dev/null; then + rm -f "$PORTABLE_CACHE/.mph-write-test" +else + CACHE_BASE="${XDG_CACHE_HOME:-${HOME:-$RUNDIR}/.cache}" + CACHE_ROOT="$CACHE_BASE/MetroidPrimeHuntersRecomp/banks" + mkdir -p "$CACHE_ROOT" +fi +if [ ! -f "$CACHE_ROOT/README.txt" ] && [ -f "$HERE/usr/share/mph-recomp/CACHE_README.txt" ]; then + cp "$HERE/usr/share/mph-recomp/CACHE_README.txt" "$CACHE_ROOT/README.txt" 2>/dev/null || true +fi +export MPH_BANK_CACHE_ROOT="$CACHE_ROOT" + ROM="" for f in "$RUNDIR"/*.nds "$RUNDIR"/*.NDS; do [ -e "$f" ] && ROM="$f" && break @@ -136,17 +159,19 @@ if [ "$#" -eq 0 ]; then if [ -n "$ROM" ]; then exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive --rom "$ROM" \ --config "$HERE/usr/bin/game.toml" --screen-layout separate \ - --adaptive-widescreen top --startup-mode automatic + --adaptive-widescreen top --startup-mode automatic \ + --freebios --generated-firmware --boot direct fi exec "$HERE/usr/bin/nds_runner" "$RUNDIR/bios" --interactive \ --config "$HERE/usr/bin/game.toml" --screen-layout separate \ - --adaptive-widescreen top --startup-mode automatic + --adaptive-widescreen top --startup-mode automatic \ + --freebios --generated-firmware --boot direct fi exec "$HERE/usr/bin/nds_runner" "$@" EOF chmod 0755 "$APPDIR/AppRun" -# Safety gate: the AppDir may contain only the runner/package support material. +# Safety gate: the AppDir may contain only runner/package support material. if find "$APPDIR" -type f \( \ -iname '*.nds' -o -iname '*.sav' -o -iname '*.dsv' -o \ -iname 'biosnds9.rom' -o -iname 'biosnds7.rom' -o -iname 'firmware.bin' \ From 4e722e5b4d10b368c9d3c91d6d376a6952fe62b1 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:32:19 +0900 Subject: [PATCH 09/44] Add ROM-free Windows Nightly packager --- tools/package-windows-nightly.ps1 | 125 ++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tools/package-windows-nightly.ps1 diff --git a/tools/package-windows-nightly.ps1 b/tools/package-windows-nightly.ps1 new file mode 100644 index 0000000..c223753 --- /dev/null +++ b/tools/package-windows-nightly.ps1 @@ -0,0 +1,125 @@ +<# +Package a ROM-free Metroid Prime Hunters Recomp Windows Nightly. + +Unlike tools/make_release.ps1, this packager intentionally does not require a +ROM-derived MPH/FMV native bank. The title executes through Tier-3 when no +content-specific optimization bank exists. FreeBIOS native banks are built from +the redistributable BSD-2-Clause FreeBIOS source path. +#> +param( + [Parameter(Mandatory = $true)][string]$Version, + [Parameter(Mandatory = $true)][string]$RunnerBuildDir, + [Parameter(Mandatory = $true)][string]$LauncherBuildDir, + [Parameter(Mandatory = $true)][string]$RuntimeBinDir, + [string]$OutputDir = 'release-stage' +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +$runnerBuild = [IO.Path]::GetFullPath((Join-Path $root $RunnerBuildDir)) +$launcherBuild = [IO.Path]::GetFullPath((Join-Path $root $LauncherBuildDir)) +$runtimeBin = [IO.Path]::GetFullPath($RuntimeBinDir) +$runner = Join-Path $runnerBuild 'nds_runner.exe' +$launcher = Join-Path $launcherBuild 'mph-recomp-ui.exe' +$assets = Join-Path $launcherBuild 'assets' + +foreach ($required in @($runner, $launcher, $assets)) { + if (-not (Test-Path -LiteralPath $required)) { + throw "Nightly input missing: $required" + } +} + +$projectText = Get-Content (Join-Path $root 'CMakeLists.txt') -Raw +if ($projectText -notmatch + "project\(MetroidPrimeHuntersRecomp VERSION $([regex]::Escape($Version)) ") { + throw "CMake project version does not match Nightly package $Version." +} + +# A ROM-free Nightly must not accidentally link a title-specific generated +# bank. The symbols are intentionally left visible in MinGW builds; reject the +# known MPH bank identities if they ever leak back into this package path. +$runnerText = [Text.Encoding]::ASCII.GetString([IO.File]::ReadAllBytes($runner)) +foreach ($forbiddenBank in @('g_dispatch_mph_arm9', 'g_dispatch_mph_arm7', + 'mph_arm9_fmv_runtime')) { + if ($runnerText.Contains($forbiddenBank)) { + throw "ROM-free Nightly unexpectedly contains title bank: $forbiddenBank" + } +} + +$out = [IO.Path]::GetFullPath((Join-Path $root $OutputDir)) +$stageName = "MetroidPrimeHuntersRecomp-windows-x64-v$Version" +$stage = Join-Path $out $stageName +$zip = Join-Path $out "$stageName.zip" + +if (Test-Path -LiteralPath $stage) { Remove-Item $stage -Recurse -Force } +if (Test-Path -LiteralPath $zip) { Remove-Item $zip -Force } +New-Item -ItemType Directory -Path $stage -Force | Out-Null +New-Item -ItemType Directory -Path (Join-Path $stage 'bios') -Force | Out-Null +New-Item -ItemType Directory -Path (Join-Path $stage 'cache\banks') -Force | Out-Null + +Copy-Item -LiteralPath $launcher -Destination (Join-Path $stage 'MetroidPrimeHuntersRecomp.exe') +Copy-Item -LiteralPath $runner -Destination $stage +Copy-Item -LiteralPath $assets -Destination $stage -Recurse +Copy-Item -LiteralPath (Join-Path $root 'game.toml') -Destination $stage +Copy-Item -LiteralPath (Join-Path $root 'README.md') -Destination $stage +Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage +Copy-Item -LiteralPath (Join-Path $root 'packaging\BIOS_README.txt') ` + -Destination (Join-Path $stage 'bios\README.txt') +Copy-Item -LiteralPath (Join-Path $root 'packaging\CACHE_README.txt') ` + -Destination (Join-Path $stage 'cache\banks\README.txt') + +$runtimeDlls = @( + 'SDL2.dll', + 'libgcc_s_seh-1.dll', + 'libstdc++-6.dll', + 'libwinpthread-1.dll' +) +foreach ($name in $runtimeDlls) { + $source = Join-Path $runtimeBin $name + if (-not (Test-Path -LiteralPath $source)) { + throw "Required MinGW runtime DLL missing: $source" + } + Copy-Item -LiteralPath $source -Destination $stage +} + +$forbidden = @(Get-ChildItem -LiteralPath $stage -File -Recurse | + Where-Object { + $_.Extension.ToLowerInvariant() -in @('.nds', '.sav', '.dsv', '.gpr') -or + $_.Name.ToLowerInvariant() -in @('biosnds9.rom', 'biosnds7.rom', 'firmware.bin') -or + $_.FullName -match '[\\/](generated|capture|captures|saves)[\\/]' + }) +if ($forbidden.Count -ne 0) { + throw "Nightly stage contains forbidden material: $($forbidden.FullName -join ', ')" +} + +Add-Type -AssemblyName System.IO.Compression +Add-Type -AssemblyName System.IO.Compression.FileSystem +$stageFull = [IO.Path]::GetFullPath($stage) +$stagePrefix = $stageFull.TrimEnd('\') + '\' +$files = @(Get-ChildItem -LiteralPath $stage -File -Recurse | Sort-Object FullName) +$archive = [IO.Compression.ZipFile]::Open( + $zip, [IO.Compression.ZipArchiveMode]::Create) +try { + foreach ($file in $files) { + $fileFull = [IO.Path]::GetFullPath($file.FullName) + if (-not $fileFull.StartsWith($stagePrefix, + [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to archive a file outside release stage: $fileFull" + } + $entryName = $fileFull.Substring($stagePrefix.Length).Replace('\', '/') + if ($entryName.StartsWith('/') -or $entryName -match '(^|/)\.\.(/|$)') { + throw "Unsafe ZIP entry name: $entryName" + } + [IO.Compression.ZipFileExtensions]::CreateEntryFromFile( + $archive, $fileFull, $entryName, + [IO.Compression.CompressionLevel]::Optimal) | Out-Null + } +} finally { + $archive.Dispose() +} + +if (-not (Test-Path -LiteralPath $zip) -or (Get-Item $zip).Length -eq 0) { + throw 'Nightly ZIP was not created.' +} +Get-FileHash -LiteralPath $zip -Algorithm SHA256 | Format-Table -AutoSize +Write-Host "Created $zip" From f4c11bec07126a32e7bf6c5e4f6ada53efa53d57 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:03 +0900 Subject: [PATCH 10/44] Add ROM-free Windows build workflow --- .github/workflows/build-windows.yml | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/build-windows.yml diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..687644a --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,123 @@ +name: Build Windows + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + windows: + name: Windows ROM-free build + runs-on: windows-2025 + + steps: + - name: Check out title sources + uses: actions/checkout@v4 + + - name: Install MinGW toolchain + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + git + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-python + mingw-w64-x86_64-SDL2 + + - name: Fetch pinned ndsrecomp and recomp-ui + shell: msys2 {0} + run: | + set -euo pipefail + nds_pin="$(tr -d '\r\n' < ndsrecomp.pin)" + ui_pin="$(tr -d '\r\n' < recomp-ui.pin)" + git clone --filter=blob:none https://github.com/mstan/ndsrecomp.git ../ndsrecomp + git -C ../ndsrecomp checkout --detach "$nds_pin" + git -C ../ndsrecomp submodule update --init --recursive + git clone --filter=blob:none https://github.com/mstan/recomp-ui.git ../recomp-ui + git -C ../recomp-ui checkout --detach "$ui_pin" + test "$(git -C ../ndsrecomp rev-parse HEAD)" = "$nds_pin" + test "$(git -C ../recomp-ui rev-parse HEAD)" = "$ui_pin" + + - name: Apply MPH and ROM-free runner integration + shell: msys2 {0} + run: | + set -euo pipefail + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + # Both patch stacks are required to be idempotent. + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + + - name: Generate redistributable FreeBIOS banks + shell: msys2 {0} + run: | + set -euo pipefail + python tools/ci/prepare_freebios_banks.py \ + --framework-root ../ndsrecomp \ + --build-dir ../ndsrecomp/build-freebios-recompiler + test ! -e ../ndsrecomp/generated/arm9_bios.c + test ! -e ../ndsrecomp/generated/arm7_bios.c + + - name: Build ROM-free runner + shell: msys2 {0} + run: | + set -euo pipefail + cmake -S ../ndsrecomp/runner -B ../ndsrecomp/runner/build-mph-nightly \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDS_BOOTSTRAP_FIRMWARE=ON \ + -DNDS_RETAIL_BIOS_BANKS=OFF \ + -DNDS_ENABLE_COMPUTE_RENDERER=ON + cmake --build ../ndsrecomp/runner/build-mph-nightly + test -s ../ndsrecomp/runner/build-mph-nightly/nds_runner.exe + if grep -a -E -q 'g_dispatch_mph_arm(9|7)|mph_arm9_fmv_runtime' \ + ../ndsrecomp/runner/build-mph-nightly/nds_runner.exe; then + echo 'ROM-derived MPH title bank leaked into ROM-free runner' >&2 + exit 1 + fi + + - name: Build and test launcher + shell: msys2 {0} + run: | + set -euo pipefail + cmake -S launcher/recomp-ui -B launcher/recomp-ui/build-nightly \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDSRECOMP_ROOT="$PWD/../ndsrecomp" \ + -DRECOMP_UI_ROOT="$PWD/../recomp-ui" + cmake --build launcher/recomp-ui/build-nightly + ctest --test-dir launcher/recomp-ui/build-nightly --output-on-failure + test -s launcher/recomp-ui/build-nightly/mph-recomp-ui.exe + + - name: Package Windows Nightly payload + shell: msys2 {0} + run: | + set -euo pipefail + version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" + test -n "$version" + runtime_bin="$(cygpath -w /mingw64/bin)" + pwsh -NoProfile -File tools/package-windows-nightly.ps1 \ + -Version "$version" \ + -RunnerBuildDir '..\ndsrecomp\runner\build-mph-nightly' \ + -LauncherBuildDir 'launcher\recomp-ui\build-nightly' \ + -RuntimeBinDir "$runtime_bin" + test -s "release-stage/MetroidPrimeHuntersRecomp-windows-x64-v${version}.zip" + + - name: Upload Windows Nightly payload + uses: actions/upload-artifact@v4 + with: + name: mph-nightly-windows + path: release-stage/MetroidPrimeHuntersRecomp-windows-x64-v*.zip + if-no-files-found: error + retention-days: 14 From bf87a3e12624c764f1892c45b6f4d6762c3078f7 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:20 +0900 Subject: [PATCH 11/44] Add ROM-free Linux build workflow --- .github/workflows/build-linux.yml | 124 ++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .github/workflows/build-linux.yml diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..808bd3d --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,124 @@ +name: Build Linux + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + name: Linux ROM-free build + runs-on: ubuntu-24.04 + + steps: + - name: Check out title sources + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build git curl ca-certificates \ + libsdl2-dev libgl1-mesa-dev libx11-dev libxext-dev libxrandr-dev \ + libxcursor-dev libxi-dev libxinerama-dev libwayland-dev + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Fetch pinned ndsrecomp + run: | + set -euo pipefail + nds_pin="$(tr -d '\r\n' < ndsrecomp.pin)" + git clone --filter=blob:none https://github.com/mstan/ndsrecomp.git ../ndsrecomp + git -C ../ndsrecomp checkout --detach "$nds_pin" + git -C ../ndsrecomp submodule update --init --recursive + test "$(git -C ../ndsrecomp rev-parse HEAD)" = "$nds_pin" + + - name: Apply MPH and ROM-free runner integration + run: | + set -euo pipefail + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + python tools/patch_ndsrecomp_mph_runtime.py \ + --framework-root ../ndsrecomp \ + --profiles config/mph_rom_profiles.json + python tools/patch_ndsrecomp_rom_free_release.py \ + --framework-root ../ndsrecomp + + - name: Generate redistributable FreeBIOS banks + run: | + set -euo pipefail + python tools/ci/prepare_freebios_banks.py \ + --framework-root ../ndsrecomp \ + --build-dir ../ndsrecomp/build-freebios-recompiler + test ! -e ../ndsrecomp/generated/arm9_bios.c + test ! -e ../ndsrecomp/generated/arm7_bios.c + + - name: Build ROM-free runner + run: | + set -euo pipefail + cmake -S ../ndsrecomp/runner -B ../ndsrecomp/runner/build-mph-nightly \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNDS_BOOTSTRAP_FIRMWARE=ON \ + -DNDS_RETAIL_BIOS_BANKS=OFF \ + -DNDS_ENABLE_COMPUTE_RENDERER=ON + cmake --build ../ndsrecomp/runner/build-mph-nightly + test -x ../ndsrecomp/runner/build-mph-nightly/nds_runner + if grep -a -E -q 'g_dispatch_mph_arm(9|7)|mph_arm9_fmv_runtime' \ + ../ndsrecomp/runner/build-mph-nightly/nds_runner; then + echo 'ROM-derived MPH title bank leaked into ROM-free runner' >&2 + exit 1 + fi + + - name: Install pinned AppImage packaging tools + run: | + set -euo pipefail + mkdir -p .ci-tools + curl -fL --retry 3 \ + https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage \ + -o .ci-tools/appimagetool + echo 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 .ci-tools/appimagetool' | sha256sum -c - + curl -fL --retry 3 \ + https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \ + -o .ci-tools/linuxdeploy + echo '421ca71d5c69ea97c6309276232990d43df1dcece0edfaa26bbf926ff96ed12e .ci-tools/linuxdeploy' | sha256sum -c - + chmod +x .ci-tools/appimagetool .ci-tools/linuxdeploy + + - name: Package Linux Nightly payload + run: | + set -euo pipefail + version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" + test -n "$version" + tools/package-linux-appimage.sh \ + --version "$version" \ + --runner ../ndsrecomp/runner/build-mph-nightly/nds_runner \ + --appimage-tool "$PWD/.ci-tools/appimagetool" \ + --linuxdeploy "$PWD/.ci-tools/linuxdeploy" + image="release-stage/MetroidPrimeHuntersRecomp-linux-v${version}-x86_64.AppImage" + test -s "$image" + mkdir -p /tmp/mph-appimage-audit + (cd /tmp/mph-appimage-audit && "$GITHUB_WORKSPACE/$image" --appimage-extract >/dev/null) + if find /tmp/mph-appimage-audit/squashfs-root -type f \( \ + -iname '*.nds' -o -iname '*.sav' -o -iname '*.dsv' -o \ + -iname 'biosnds9.rom' -o -iname 'biosnds7.rom' -o -iname 'firmware.bin' \ + \) -print -quit | grep -q .; then + echo 'Forbidden ROM/save/BIOS/firmware material found inside AppImage' >&2 + exit 1 + fi + + - name: Upload Linux Nightly payload + uses: actions/upload-artifact@v4 + with: + name: mph-nightly-linux + path: release-stage/MetroidPrimeHuntersRecomp-linux-v*-x86_64.AppImage + if-no-files-found: error + retention-days: 14 From 2dc1dc394bb482ccc5b7846498890699599dbeed Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:27 +0900 Subject: [PATCH 12/44] Add cross-platform ROM-free build CI --- .github/workflows/build.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..312a887 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +name: Build + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mph-build-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + windows: + name: Build Windows + uses: ./.github/workflows/build-windows.yml + + linux: + name: Build Linux + uses: ./.github/workflows/build-linux.yml From c5a90ea8b2ef9f36d3a95f4f36709443d1ff947c Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:33:49 +0900 Subject: [PATCH 13/44] Add ROM-free Nightly release workflow --- .github/workflows/nightly-release.yml | 164 ++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/nightly-release.yml diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml new file mode 100644 index 0000000..85057d4 --- /dev/null +++ b/.github/workflows/nightly-release.yml @@ -0,0 +1,164 @@ +name: Nightly Release + +# ROM-free public Nightly. The same Windows/Linux workflow used by PR CI builds +# the release payload, so CI and Nightly cannot silently drift apart. No ROM, +# ROM URL, ROM secret, proprietary BIOS, firmware dump, save, or ROM-derived +# title bank is fetched or uploaded by this workflow. + +on: + push: + branches: + - develop + workflow_dispatch: + +concurrency: + group: mph-nightly-release + cancel-in-progress: false + +permissions: + contents: read + +env: + NIGHTLY_TAG: nightly-release + NIGHTLY_NAME: Nightly Build + +jobs: + windows: + name: Build Windows + uses: ./.github/workflows/build-windows.yml + + linux: + name: Build Linux + uses: ./.github/workflows/build-linux.yml + + publish: + name: Publish Nightly release + needs: [windows, linux] + if: github.repository == 'Zection6V/MetroidPrimeHuntersRecomp' + runs-on: ubuntu-24.04 + permissions: + contents: write + + steps: + - name: Check out sources + uses: actions/checkout@v4 + + - name: Download Windows payload + uses: actions/download-artifact@v4 + with: + name: mph-nightly-windows + path: dist + + - name: Download Linux payload + uses: actions/download-artifact@v4 + with: + name: mph-nightly-linux + path: dist + + - name: Resolve project version + id: version + shell: bash + run: | + set -euo pipefail + version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" + test -n "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Nightly project version: $version" + + - name: Verify Nightly payload + run: | + set -euo pipefail + find dist -maxdepth 1 -type f -printf '%f (%s bytes)\n' | sort + python tools/ci/verify-nightly-assets.py \ + --dist dist \ + --version '${{ steps.version.outputs.version }}' \ + --write-sums + test -s dist/SHA256SUMS.txt + cat dist/SHA256SUMS.txt + + - name: Compose release notes + shell: bash + run: | + set -euo pipefail + cat > nightly-notes.md </\` beside the executable/AppImage for the future local optimization/JIT cache. + + These builds are automatic development snapshots and may be slower or less stable than optimized tagged releases. + EOF + cat nightly-notes.md + + - name: Move Nightly tag to this commit + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${NIGHTLY_TAG}" >/dev/null 2>&1; then + gh api --method PATCH \ + "repos/${GITHUB_REPOSITORY}/git/refs/tags/${NIGHTLY_TAG}" \ + -f sha="${GITHUB_SHA}" -F force=true >/dev/null + else + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f ref="refs/tags/${NIGHTLY_TAG}" -f sha="${GITHUB_SHA}" >/dev/null + fi + + - name: Create or update Nightly release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if gh release view "${NIGHTLY_TAG}" >/dev/null 2>&1; then + gh release edit "${NIGHTLY_TAG}" \ + --title "${NIGHTLY_NAME}" \ + --notes-file nightly-notes.md \ + --prerelease \ + --draft=false + else + gh release create "${NIGHTLY_TAG}" \ + --title "${NIGHTLY_NAME}" \ + --notes-file nightly-notes.md \ + --prerelease + fi + + - name: Upload Nightly assets + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + gh release upload "${NIGHTLY_TAG}" dist/* --clobber + + - name: Remove stale Nightly assets + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + (cd dist && ls -1) > published.txt + gh release view "${NIGHTLY_TAG}" --json assets --jq '.assets[].name' > attached.txt + while IFS= read -r asset; do + [ -n "$asset" ] || continue + if ! grep -Fxq "$asset" published.txt; then + gh release delete-asset "${NIGHTLY_TAG}" "$asset" --yes + fi + done < attached.txt + + - name: Summarize + shell: bash + run: | + { + echo '### Nightly Build published' + echo + echo "- Tag: \`${NIGHTLY_TAG}\` -> \`${GITHUB_SHA}\`" + echo '- ROM/ROM URL/ROM secret: **not used**' + echo '- Title-bank mode: ROM-free / Tier-3 fallback' + echo '- Assets:' + (cd dist && ls -1 | sed 's/^/ - /') + } >> "$GITHUB_STEP_SUMMARY" From 1b1c409bc1e3244b99992ee26451604f46aed75c Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:34:15 +0900 Subject: [PATCH 14/44] Document local optimization cache architecture --- docs/LOCAL_BANK_CACHE.md | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/LOCAL_BANK_CACHE.md diff --git a/docs/LOCAL_BANK_CACHE.md b/docs/LOCAL_BANK_CACHE.md new file mode 100644 index 0000000..3bcdf50 --- /dev/null +++ b/docs/LOCAL_BANK_CACHE.md @@ -0,0 +1,97 @@ +# Local optimization cache architecture + +## Status + +The public Nightly path is intentionally ROM-free. GitHub Actions builds the +runner, launcher, and redistributable FreeBIOS banks without receiving a +Metroid Prime Hunters ROM, a private ROM URL, proprietary BIOS dumps, firmware +dumps, saves, or generated title-bank source. + +Today, when no content-specific native title bank is linked, Metroid Prime +Hunters code loaded by direct boot executes through ndsrecomp Tier-3. This is +the correctness fallback, not the final performance target. + +## Portable-first cache root + +The preferred future optimization cache lives beside the distributed +executable/AppImage: + +```text +MetroidPrimeHuntersRecomp/ +├─ MetroidPrimeHuntersRecomp.exe # Windows +├─ Metroid Prime Hunters.nds # user-owned, optional filename +└─ cache/ + └─ banks/ + └─ / + ├─ manifest.json + └─ ... generated optimization payload ... +``` + +Linux AppImage packaging resolves the same `cache/banks` directory beside the +AppImage. If that directory is not writable, it falls back to +`$XDG_CACHE_HOME/MetroidPrimeHuntersRecomp/banks` (or `~/.cache/...`). Windows +implementations should analogously fall back to +`%LOCALAPPDATA%\MetroidPrimeHuntersRecomp\cache\banks` when the portable +location cannot be written. + +Save data, firmware/WFC identity, and other persistent user state are not +optimization cache data and must remain in their existing persistent app-data +locations. + +## Identity model + +Whole-ROM SHA-1 is appropriate for the cache namespace because cache payloads +must be bound to exact content. It must **not** become the runtime base-profile +selector. + +Runtime base identity remains the seven-profile MPH detector: + +- US1.0 +- US1.1 +- EU1.0 +- EU1.1 +- JP1.0 +- JP1.1 +- KR1.0 + +The authoritative fast path uses the executable checksum table. Exact supported +header tuples are only a candidate fallback, and dangerous host writes continue +to fail closed when executable compatibility is not authoritative. + +A future cache manifest should bind at least: + +```text +bank_format_version +runner_abi_version +ndsrecomp_codegen_version +host_arch +base_profile +content_sha1 +executable_crc32 +coverage_or_hotset_hash +validated_guest_code_hashes +``` + +Any incompatible field invalidates the cache and falls back to Tier-3 rather +than guessing compatibility. + +## Why first-run C compilation is not the target + +The current static ndsrecomp title-bank pipeline emits C and links it into +`nds_runner`. Reproducing that pipeline on a player's first launch would require +shipping or requiring a host C/C++ compiler and linker, complicating updates, +code signing, antivirus behavior, and cache ABI compatibility. + +Therefore the intended progression is: + +1. **Current:** ROM-free binary + Tier-3 fallback. +2. **Foundation:** portable-first per-content cache namespace. +3. **Next:** a compiler-free portable bank/IR cache if useful. +4. **Target:** Tier-3 hot-block detection -> host JIT -> validated persistent + local cache. + +Runtime/overlay code generated by the game should be eligible for the same hot +JIT path, avoiding the need to pre-capture one clean ROM's FMV runtime image. +This is particularly important for modified ROMs such as translations or other +code/data modifications, whose exact content must never silently reuse a clean +ROM's optimization payload. From 095e1b504a1334befcf43710c2afa334e1d96903 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:34:39 +0900 Subject: [PATCH 15/44] Document ROM-free Nightly execution model --- README.md | 225 ++++++++++++++++++++++-------------------------------- 1 file changed, 93 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 98de666..9a00c2a 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,11 @@ > audio issues, input quirks, networking failures, and possible desyncs. Testing, > issues, and PRs are welcome. -MetroidPrimeHuntersRecomp currently ships generated content profiles for the -validated ROM revisions tracked by this branch. Runtime base detection is -prepared for all seven retail Metroid Prime Hunters revisions and uses -melonPrimeDS-compatible executable checksums; whole-ROM SHA-1 is content -provenance, not the runtime address selector. You provide your own legally -obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated +MetroidPrimeHuntersRecomp runs **Metroid Prime Hunters** as an ndsrecomp target. +Runtime base-version detection supports the known US1.0, US1.1, EU1.0, EU1.1, +JP1.0, JP1.1, and KR1.0 layouts using executable-compatible detection rather +than using whole-ROM SHA-1 as the base-profile selector. You provide your own +legally obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated ROM-derived source is distributed. ## Gameplay Preview @@ -22,170 +21,132 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release -Latest upstream release: -**[v0.4.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.4.0-alpha)**. +The project version is **v0.4.0-alpha**. Development Nightly builds are produced +from `develop` under the fixed `nightly-release` prerelease tag after the +Windows and Linux build workflows and release-payload safety checks succeed. -Downloads: +The Nightly build path is deliberately **ROM-free**: GitHub Actions never needs +or downloads a Metroid Prime Hunters ROM, private ROM URL, proprietary BIOS or +firmware dump, save file, or ROM-derived MPH title bank. Users supply their ROM +at runtime. In the current Nightly architecture, title code without a linked +content-specific native bank executes through ndsrecomp Tier-3. This is a safe +correctness fallback and may be slower than an optimized tagged build. -- Windows: - `MetroidPrimeHuntersRecomp-windows-x64-v0.4.0.zip` -- Linux: - `MetroidPrimeHuntersRecomp-linux-x86_64-v0.4.0.AppImage` - -This is an early ndsrecomp title and should still be treated as an alpha test -build rather than a polished game release. - -New in upstream v0.4.0 is an opt-in **HD Rendering** mod on the launcher Mods -page. It can raise the internal 3D resolution up to 4x and optionally upscale -decoded textures. It is disabled by default; native rendering remains the -reference path. This branch keeps that upstream feature alongside the -multi-ROM-safe Adaptive Widescreen and Prime Controls work. +The package reserves a portable optimization-cache root beside the executable +or AppImage at `cache/banks//`. The current Nightly does not yet +generate native title banks there. The intended next step is a compiler-free +local optimization/JIT cache; see [`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). ## Quick Start Windows: 1. Download and fully extract the Windows ZIP. -2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the launcher. +2. Put your own Metroid Prime Hunters `.nds` ROM next to + `MetroidPrimeHuntersRecomp.exe`, or select it in the launcher. 3. Run `MetroidPrimeHuntersRecomp.exe` and press Play. Linux: -1. Download the AppImage. -2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the AppImage. +1. Download the AppImage and make it executable if required by your desktop. +2. Put your own Metroid Prime Hunters `.nds` ROM next to the AppImage. 3. Run the AppImage. The current release can use the built-in FreeBIOS + generated firmware path, so retail DS BIOS and firmware dumps are not required for the default no-dump -startup path. If you choose to use your own BIOS/firmware dumps, they must be -from hardware you own and must match the hashes listed in the release's -`bios/README.txt`. - -## ROM identity and multi-ROM support - -Runtime address selection does **not** use whole-ROM SHA-1. The runner first -uses the melonPrimeDS executable checksum (CRC32 over header + ARM9 + ARM7), -then uses exact game code + supported revision only as a fallback base-profile -hint. Header-only matches never authorize host RAM/code writes. - -Known compatible executable checksums can use the revision-specific Prime -Controls and Adaptive Widescreen addresses. Whole-ROM SHA-1 remains the exact -content identity for generated banks, coverage, checkpoints and capture data, -so one modified ROM cannot silently reuse another modified ROM's generated -content. - -The current branch has runtime address profiles for US1.0, US1.1, EU1.0, -EU1.1, JP1.0, JP1.1 and KR1.0. A revision still needs its own generated -content/capture coverage before it is considered fully brought up. - -## What Works - -- Boots supported content profiles through the ndsrecomp runner. -- Reaches Metroid Prime Hunters gameplay in tested routes. -- Includes an adaptive 21:9 upper-screen widescreen option using per-revision - projection/culling addresses from melonPrimeDS/mphCodex. -- Includes Prime-style keyboard and mouse controls. -- Includes full remappable gamepad bindings in the launcher. -- Includes upstream HD Rendering controls for internal resolution and texture - upscaling. -- Supports mouse-driven touchscreen input. -- Can authenticate through Wiimmfi and reach a Friends and Rivals lobby in - validated flows. -- Persists mutable firmware/WFC state between launches through the upstream - firmware-state path. +startup path. The ROM-free Nightly generates its native FreeBIOS banks only +from the redistributable BSD-2-Clause FreeBIOS source path at build time. -## Known Limits +## ROM identity and multi-ROM behavior -- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and - gameplay problems are expected. -- Gameplay coverage is incomplete. Do not assume the campaign is fully - validated from start to finish. -- Widescreen is still being audited. Some scenes, effects, HUD placement, - movies, fades, or screen-routing behavior may be wrong. -- HD texture upscaling remains opt-in and should not be treated as the native - reference rendering path. -- Online play is experimental. Wiimmfi can reach the lobby in validated flows, - but in-game play is ultimately untested. There is no guarantee that a match - will connect, stay connected, or avoid desync. -- Save behavior and settings are still part of early release testing. Keep - backups of anything you care about. - -## Controls - -Prime Controls are enabled by default. - -Keyboard and mouse defaults: - -- `WASD`: move -- Mouse: aim -- Mouse 1 / Mouse 2: fire / scan-fire -- `Space`: jump -- `Left Ctrl`: morph ball -- `Left Shift`: boost / map zoom -- `C`: scan visor -- `F`: OK -- `Q` / `E`: scan-message arrows -- `V`: menu -- Mouse 4: missiles -- Mouse 5: beam -- `1` through `6`: subweapons -- `Tab`: virtual stylus - -Gamepad defaults: - -- Left stick: move and menu D-pad -- Right stick: aim -- `RT` / `LT`: shoot / scan-fire -- `A`: jump -- `B`: morph ball -- `X`: missile -- `Y`: UI OK -- `LB` / `RB`: beam / boost or zoom -- `R3`: scan visor -- D-pad left/right: scan-message arrows -- `Start`: menu - -Keyboard, mouse, and gamepad bindings are editable from the launcher Mods page. +Three identity layers are kept separate: -## Online Play +1. **Runtime base profile:** executable checksum / exact supported header tuple. +2. **Executable compatibility:** determines whether dangerous host RAM/code + writes such as Aim/Morph/Adaptive Widescreen patches are authorized. +3. **Exact content identity:** whole-ROM SHA-1 for provenance, generated banks, + captures, and the future local optimization-cache namespace. + +Whole-ROM SHA-1 therefore does not decide that a modified ROM is US1.0 or EU1.1. +Unknown or ambiguous executable content never silently falls back to US1.0, and +host writes fail closed unless executable compatibility is authoritative. + +Exact clean-content profiles currently validated in the repository include the +US1.0 and EU1.1 bring-up tracks. Other base layouts are prepared at runtime, but +full per-ROM extraction, coverage, gameplay validation, and optimized bank work +must still be completed before they should be described as equally validated +release targets. + +## Enhancements + +### Adaptive Widescreen -Nintendo WFC / Wiimmfi support is experimental. The current validated state is -lobby connectivity: Metroid Prime Hunters can authenticate through Wiimmfi and -reach a Friends and Rivals lobby where a locally hosted game is visible. +The launcher exposes an adaptive 21:9 upper-screen mode. The implementation +combines ndsrecomp's widened host renderer/compositor and HUD anchoring with +profile-aware Metroid Prime Hunters projection/culling corrections derived from +the audited melonPrimeDS/mphCodex address tables. Unsupported/unsafe runtime +identity falls back rather than applying guessed guest writes. -The launcher keeps the console firmware profile in -`%APPDATA%\MetroidPrimeHuntersRecomp`. Wi-Fi settings, console/game-card -pairing, and WFC updates survive both the in-game system shutdown flow and a -normal window close. Confirming the WFC settings shutdown prompt closes the -application automatically. +### Prime Controls -Actually joining a match and playing in-game online is not guaranteed. It may -fail to connect, disconnect, or desync. +Prime-style keyboard/mouse controls and remappable gamepad bindings are exposed +through the launcher. Defaults include WASD movement, mouse aim, Mouse 1 fire, +and the existing touch-helper mappings. + +### HD Rendering + +HD Rendering is opt-in. It raises the 3D engine above native DS sample density +(up to the supported internal-resolution choices) and can upscale decoded +textures. The native 2D path remains the reference and HD Rendering is off by +default. + +## Online Play + +Nintendo WFC / Wiimmfi support remains experimental. The launcher persists the +console firmware profile in the user's application-data location so Wi-Fi +settings, console/game-card pairing, and WFC updates survive later launches. +Online play may still fail to connect, disconnect, or desync. The Wi-Fi implementation is built on [melonDS](https://github.com/melonDS-emu/melonDS)'s Wi-Fi work in the shared ndsrecomp runner. Full credit to the melonDS team for the Wi-Fi controller, emulated access point, and network backend foundation. +## Known Limits + +- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and + gameplay problems are expected. +- Gameplay coverage is incomplete across the seven base layouts. +- The ROM-free Nightly's Tier-3 title fallback can be substantially slower than + a build with validated native MPH optimization banks, especially in known hot + paths such as opening movies. +- The local `cache/banks//` directory is currently a reserved + cache contract; dynamic native/JIT bank generation is not implemented yet. +- Widescreen still requires sustained gameplay auditing across scenes, effects, + HUD placement, movies, fades, and screen routing. +- Online play is experimental. +- Save behavior and settings remain part of early release testing. Keep backups + of anything you care about. + ## Credits - [melonDS](https://github.com/melonDS-emu/melonDS): Wi-Fi implementation foundation used by the shared ndsrecomp runner. -- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): runtime-version - detection, Prime-style controls, and per-version aspect-ratio patch reference. -- [mphCodex](https://github.com/Zection6V/mphCodex): MPH game-code analysis, - including the seven-version widescreen projection/culling mapping. +- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): reference for + Prime-style controls, ROM/version address tables, and aspect-ratio research. +- [mphCodex](https://github.com/Zection6V/mphCodex): game-code/disassembly and + Metroid Prime Hunters behavior research. - [MphRead](https://github.com/NoneGiven/MphRead): Metroid Prime Hunters file format and behavior reference. -See the ndsrecomp -[`THIRD_PARTY_ATTRIBUTION.md`](https://github.com/mstan/ndsrecomp/blob/main/THIRD_PARTY_ATTRIBUTION.md) -for provenance and licensing details for shared runtime components. +See the ndsrecomp `THIRD_PARTY_ATTRIBUTION.md` for shared-runtime provenance and +licensing details. ## Developers -This README is intentionally player-facing. Development notes, validation -history, and bring-up details live in [`docs/BRINGUP.md`](docs/BRINGUP.md). +Bring-up and validation notes live under [`docs/`](docs/). The ROM-free Nightly +and local-cache direction is documented in +[`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). The original code in this repository is MIT licensed. Metroid Prime Hunters, Nintendo DS firmware/BIOS images, ROMs, saves, and all derived game data remain From 86823e76cb44b7c893382cf454c0730f9f4e44e1 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:34:53 +0900 Subject: [PATCH 16/44] Require portable cache contract in Windows Nightly --- tools/ci/verify-nightly-assets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ci/verify-nightly-assets.py b/tools/ci/verify-nightly-assets.py index 6af8358..2d8f341 100644 --- a/tools/ci/verify-nightly-assets.py +++ b/tools/ci/verify-nightly-assets.py @@ -60,6 +60,7 @@ def verify_windows(path: Path) -> None: "README.md", "LICENSE", "bios/README.txt", + "cache/banks/README.txt", } with zipfile.ZipFile(path) as archive: names = { From e946c1e693ca2c940f0b2354b1e53dcd22fc0132 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:02 +0900 Subject: [PATCH 17/44] Ignore local optimization cache --- .gitignore | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 1608302..14264b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,39 @@ -# User-owned Nintendo DS images and ROM-derived outputs. +# ROM / console dumps *.nds -/generated/ -/extracted/ +*.NDS +*.sav +*.dsv +bios/*.rom +bios/*.bin +firmware.bin -# Optional reverse-engineering references. -/third_party/ +# Generated recompilation inputs/outputs and captures +generated/ +release-stage/ -# Build trees, runtime state, and captures. -/build/ -/build-*/ -/release-stage/ -/launcher/recomp-ui/build*/ -/saves/ -/captures/ -/scratch/ -*.sav -*.log -/rom.cfg -/settings.toml -/keybinds.ini +# Local optimization cache (portable-first runtime/JIT cache) +cache/ + +# Build trees +build/ +build-*/ +launcher/recomp-ui/build/ +launcher/recomp-ui/build-*/ -# Local Python environment and tool output. -/.venv/ +# Python +.venv/ __pycache__/ *.pyc -# Editor and OS output. -.vs/ +# Tool/editor state .vscode/ .idea/ -Thumbs.db +*.suo +*.user +*.gpr +*.rep +*.lock + +# OS junk .DS_Store +Thumbs.db From 901d4f0dac10b552294b4f38a0e3c46017619883 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:24 +0900 Subject: [PATCH 18/44] Add ROM-free source policy check --- tools/ci/check_rom_free_release_sources.py | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tools/ci/check_rom_free_release_sources.py diff --git a/tools/ci/check_rom_free_release_sources.py b/tools/ci/check_rom_free_release_sources.py new file mode 100644 index 0000000..5813bac --- /dev/null +++ b/tools/ci/check_rom_free_release_sources.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Pin the public Nightly's no-ROM-secret source policy.""" + +from __future__ import annotations + +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[2] +WORKFLOWS = [ + ROOT / ".github" / "workflows" / "build-windows.yml", + ROOT / ".github" / "workflows" / "build-linux.yml", + ROOT / ".github" / "workflows" / "build.yml", + ROOT / ".github" / "workflows" / "nightly-release.yml", +] + + +def main() -> int: + failures: list[str] = [] + for path in WORKFLOWS: + if not path.is_file(): + failures.append(f"missing release workflow: {path.relative_to(ROOT)}") + continue + text = path.read_text(encoding="utf-8") + if "MPH_US10_ROM_URL" in text: + failures.append(f"{path.relative_to(ROOT)} still references MPH_US10_ROM_URL") + if "secrets." in text: + failures.append( + f"{path.relative_to(ROOT)} references repository/environment secrets" + ) + + nightly = (ROOT / ".github" / "workflows" / "nightly-release.yml").read_text( + encoding="utf-8" + ) + for required in ( + "uses: ./.github/workflows/build-windows.yml", + "uses: ./.github/workflows/build-linux.yml", + "NIGHTLY_TAG: nightly-release", + "verify-nightly-assets.py", + ): + if required not in nightly: + failures.append(f"nightly workflow missing required contract: {required}") + + for workflow in ("build-windows.yml", "build-linux.yml"): + text = (ROOT / ".github" / "workflows" / workflow).read_text(encoding="utf-8") + for required in ( + "patch_ndsrecomp_rom_free_release.py", + "prepare_freebios_banks.py", + "-DNDS_RETAIL_BIOS_BANKS=OFF", + ): + if required not in text: + failures.append(f"{workflow} missing ROM-free build contract: {required}") + + if failures: + print("ROM-free release policy check FAILED:", file=sys.stderr) + for failure in failures: + print(f" - {failure}", file=sys.stderr) + return 1 + + print("OK: public build/Nightly workflow uses no ROM secret path") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 1da24709b686e18f04d6cc1f75984e13869ce698 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:39 +0900 Subject: [PATCH 19/44] Gate CI on ROM-free release policy --- .github/workflows/build.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 312a887..1cfa34a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,10 +12,21 @@ concurrency: cancel-in-progress: true jobs: + policy: + name: ROM-free release policy + runs-on: ubuntu-24.04 + steps: + - name: Check out sources + uses: actions/checkout@v4 + - name: Verify no ROM-secret release path + run: python tools/ci/check_rom_free_release_sources.py + windows: name: Build Windows + needs: policy uses: ./.github/workflows/build-windows.yml linux: name: Build Linux + needs: policy uses: ./.github/workflows/build-linux.yml From 0fcfd9c7c6d5a5606a0359258930be54a005a673 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:35:59 +0900 Subject: [PATCH 20/44] Record ROM-free Nightly architecture --- docs/BRINGUP.md | 242 +++++++++++++++++++++--------------------------- 1 file changed, 104 insertions(+), 138 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 025cd37..dca7aa2 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -2,141 +2,107 @@ ## Target and references -- Cartridge: USA revision 0 (`AMHE`, `MP HUNTERS`) -- ROM SHA-1: `90164d1ac127ee5f9815ea4ae7de798c7b5fc629` -- Framework main integration: `778e74385aa223179a5d9534c2201ed1096a3df7` -- MphRead reference: `26cd8a6fe93dc5e525d1a1bb304fe96001111e55` -- Public matching disassembly: none found - -The ARM9 ROM image is 517,828 bytes and expands to 907,736 bytes. The ARM7 -image is 164,964 bytes. Prime Hunters has 18 compressed ARM9 overlays whose -overlay table contains 576 bytes (18 records), not 576 separate overlays. - -## Evidence so far - -1. The pre-existing SM64DS-native runner failed around the Prime Hunters game - handoff with ARM7 PC `0xE590100C` and a corrupt stack. -2. The failure was caused by unconditional SM64DS bank registration, not by a - Prime Hunters instruction or device requirement. Both titles load ARM7 at - `0x02380000`, so address-only dispatch selected SM64DS code. -3. A clean BIOS-bank-only runner reaches 700,000,000 ARM9 cycles with both - CPUs alive and no terminal dispatch miss. -4. Visual checkpoints: - - VBlank 300: ActImagine splash - - VBlank 900-1200: opening logo animation - - VBlank 2400: opening cinematic - - VBlank 3000: hunter cinematic - - VBlank 3600: Weavel introduction -5. The initial generated main banks contained 4,335 ARM9 functions and 16 - ARM7 functions. Exact-ROM-gated registration produced the same 700,000,000 - cycle machine state as the clean interpreter runner. -6. AMHE uses melonDS SaveMemType 5: 256 KiB flash. Save type/capacity are now - game-owned configuration instead of an SM64DS runner constant. -7. Native and ndsref event/cycle counts agree through the no-input return to - the hunter reel and onward to VBlank 12000. -8. The title split exposed a cold-boot screen-routing defect hidden by the - mirrored intro video: ndsrecomp reset POWCNT1 to `0x0001` instead of the - retail/melonDS `0x820F`. The reset and 3D power defaults are corrected. -9. A second routing defect appeared only when Prime Hunters changed the LCD - assignment during VBlank. The native renderer stored engine-relative - frames and applied the current POWCNT1 only when the completed frame was - read, which could retroactively swap it. Routing is now applied while each - scanline is produced, matching melonDS framebuffer assignment. -10. The no-input title/loop checkpoints are: - - VBlank 7800: title logo and `TOUCH TO START` - - VBlank 8400: title animation - - VBlank 9000: return to the hunter reel - Native top/bottom captures are byte-identical to ndsref at all three. -11. The checkpoint helper now names screens explicitly and continues after a - server safety-round exhaustion. It refuses to label or save a frame unless - the requested absolute VBlank was actually reached. -12. A seeded, trace-preserving input search discovered the first campaign - path. Its minimized replay is: - - tap the title and Adventure Mode - - create and confirm mission file A - - select file A again to start the campaign - - skip the mission briefing - - wait for the Celestial Archives gunship screen and confirm landing - The native and reference runs reach the live first-person HUD at VBlank - 10859. -13. `tools/fuzz_mph_gameplay.py` records every action, absolute VBlank, - screenshot, perceptual signature, RGB hash, and event-count snapshot. - `scenarios/adventure_start.json` is the replayable minimized result. -14. All 15 matching native/oracle checkpoints in the minimized route are - byte-identical across both physical screens: zero differing pixels and - zero maximum channel delta, including the first gameplay frame. -15. Tier-3 coverage captured from that route yielded 567 unique ARM9 - call/indirect targets inside the immutable main image. Slice-resume roots, - runtime RAM, and all reused overlay ranges were excluded. Adding those - seeds expands the ARM9 bank to 7,115 functions; the identical replay cuts - ARM9 Tier-3 entries from 64,619,845 to 57,525,780 (10.98%) and interpreted - instructions from 3,866,962,843 to 3,638,379,652 (5.91%). All 13 action - checkpoints retain identical event counts and RGB hashes. This does not - yet establish a wall-clock speedup while generated code remains `-O0`. -16. The opening FMV slowdown was isolated with - `tools/benchmark_mph_fmv.py`. Static-only FMV windows ran at 26-28 FPS: - presentation stayed below 1 ms/frame while emulation rose to 34-37 - ms/frame and ARM9 executed roughly 620,000 Tier-3 instructions per frame. - The hot code is the runtime ITCM mirror plus the active overlay near - `0x02102D74`. -17. A deterministic VBlank-3000 ITCM+main-RAM capture is pinned by SHA-1 - `2f4a2ba36886fb9152781f5829dedfd4b836a73b`. The separate - `mph_arm9_fmv_runtime` bank uses only call/indirect roots observed in the - VBlank 2400-3000 delta and validates live guest bytes before dispatch. - Seeding scheduler-resume PCs was rejected because it split hot loops into - one-instruction functions and generated about 589,000 fallthrough - dispatches/frame; the retained bank records about 5,400/frame. -18. The retained interactive run sustains 59.73-59.84 FPS from VBlank - 2400-4800 at 8.37-9.31 ms emulation/frame with zero audio underruns. - Static-only and optimized runners have identical event, instruction, and - cycle counts and zero differing pixels at VBlanks 2400, 3000, 3600, 4200, - and 4800. - -## Bring-up gates - -- [x] Isolated framework worktree from latest `origin/main` -- [x] Exact AMHE0 ROM identity and header inventory -- [x] Independent game repository/scaffold -- [x] Public reverse-engineering resource audit and pinned MphRead checkout -- [x] Safe interpreter boot through the opening cinematic -- [x] Remove the cross-title SM64DS bank-registration assumption -- [x] Reach and capture the title screen -- [x] Observe one complete no-input attract loop -- [x] Compare the same attract checkpoints against the ndsref oracle -- [x] Compile and register AMHE0 main ARM9/ARM7 banks by ROM capability -- [ ] Capture remaining runtime ARM7 code and ARM9 overlay generations (the - opening-FMV ARM9 generation is complete) -- [x] Generalize cartridge save type/size beyond SM64DS's 8 KiB EEPROM -- [x] Add deterministic Prime Hunters navigation and gameplay-entry scenario -- [ ] Add sustained traversal, combat, pause, death, and reload scenarios -- [x] Enable an exact-ROM upper-screen adaptive-wide bring-up baseline -- [x] Latch adaptive/direct presentation state to the published frame so - boot logos and capture transitions do not flicker at high host scaling -- [ ] Audit Prime Hunters projection/culling/HUD across sustained gameplay -- [x] Add an MPH recomp-ui development launcher and enhancement toggle -- [x] Add top-window relative mouse aim, Mouse 1 fire, and persisted controls -- [x] Add portable Windows release launcher/mod packaging with a baked, - content-validated FMV runtime bank - -## Design constraints - -- A title bank must never be selected by address alone. Registration is gated - by exact cartridge identity, and mutable/overlay banks also validate live - bytes. -- The runner also validates `[game].sha1` before applying title-owned config, - so a Prime Hunters save-device declaration cannot silently affect another - cartridge. -- Each overlay generation remains a separate content-validated bank. Prime - Hunters reuses virtual ranges, so combining entry points from different - overlay images would be unsound. -- The interpreter is the correctness oracle for uncompiled code within the - native runtime; ndsref remains the independent machine oracle. -- Widescreen is a title-owned capability. Separate windows are safe as a host - layout, but field-of-view, culling, HUD anchors, movies, and touch routing - require Prime Hunters-specific proof. -- MphRead's recreation uses a 78-degree camera FOV and derives projection and - frustum planes from the live output aspect ratio. That is a useful semantic - reference, but it does not prove which AMHE0 guest structures and GX command - sites must be patched. The host adaptive viewport is enabled as an explicit - bring-up baseline, but it is not considered visually complete until those - title-side behaviors pass sustained gameplay review. +The project began with the USA revision-0 (`AMHE`) bring-up and now carries a +seven-base-profile runtime layout/detector for US1.0, US1.1, EU1.0, EU1.1, +JP1.0, JP1.1, and KR1.0. Exact clean-content profiles, generated banks, +coverage, and capture artifacts remain content-specific and must not be inferred +from a base layout alone. + +The original US1.0 content identity is SHA-1 +`90164d1ac127ee5f9815ea4ae7de798c7b5fc629`. Whole-ROM SHA-1 is provenance and +cache/build identity, not the runtime base-profile selector. + +## Runtime identity rules + +1. Use the melonPrime-compatible executable CRC32 detector as the authoritative + runtime base-profile signal. +2. An exact supported game-code/revision tuple may identify only a candidate + base profile when the executable checksum is unknown. +3. Dangerous host RAM/code writes fail closed unless executable compatibility + is authoritative. +4. Generated title banks, runtime captures, and coverage remain scoped by exact + content identity. +5. Unknown modified content must never silently fall back to US1.0. + +## ROM-free Nightly architecture + +Public Windows/Linux Nightly builds are intentionally produced without a +Metroid Prime Hunters ROM, ROM URL/secret, proprietary BIOS or firmware dump, +save file, or generated ROM-derived MPH title bank. + +The pinned ndsrecomp runner is patched for an opt-in public-build mode: + +- redistributable BSD-2-Clause FreeBIOS images are recompiled into the native + FreeBIOS ARM9/ARM7 banks using ndsrecomp's documented FreeBIOS pipeline; +- `NDS_RETAIL_BIOS_BANKS=OFF` removes the build-time requirement for generated + proprietary retail-BIOS banks; +- if a user later supplies retail BIOS dumps, immutable BIOS execution may use + the existing reference interpreter rather than requiring those generated C + banks in the distributed binary; +- no MPH title-bank directory is configured, so direct-booted MPH ARM9/ARM7 + code falls through to Tier-3 using guest-written RAM provenance. + +This is a correctness-first release path. It is expected to be slower than the +historical optimized US1.0 release, particularly in opening-FMV/runtime-code hot +paths. + +## Local optimization cache direction + +The preferred future optimization cache is portable-first: + +```text +cache/banks// +``` + +beside the executable/AppImage. Linux falls back to XDG cache when that location +is not writable; Windows should fall back to LOCALAPPDATA under the same +condition. Save data and firmware/WFC identity remain persistent app data, not +regenerable optimization cache. + +The current static ndsrecomp bank pipeline emits C and links it into the runner, +so the project will not make users run a host C/C++ compiler on first launch. +The intended progression is Tier-3 -> compiler-free local IR/bank support if +useful -> hot-block JIT with a validated persistent cache. See +`docs/LOCAL_BANK_CACHE.md`. + +## Adaptive Widescreen + +Adaptive Widescreen combines the original ndsrecomp host-side widened +renderer/compositor/HUD anchoring with MPH game-side projection and culling +patches audited against melonPrimeDS/mphCodex. The guest patch addresses are +profile-aware across all seven base layouts and are applied only under the +existing authoritative executable-compatibility gate. + +## Upstream launcher/runtime integration + +The launcher tracks the upstream MPH recomp-ui feature set including Adaptive +Widescreen, Prime Controls, HD Rendering, and persistent firmware/WFC state. +The launcher does not use whole-ROM SHA-1 as an early acceptance gate; the +runner owns executable compatibility and exact-content decisions. + +## Validation gates + +- [x] Seven runtime base profiles represented. +- [x] Executable CRC32 runtime detector and strict header fallback. +- [x] Dangerous host writes fail closed for unknown executable content. +- [x] All-seven Adaptive Widescreen address table integrated. +- [x] Upstream launcher HD/Wi-Fi persistence integration retained. +- [x] ROM-free release design avoids ROM/ROM-secret input in GitHub Actions. +- [x] FreeBIOS native-bank generation has a redistributable build path. +- [ ] Windows ROM-free Nightly workflow passes full compile/package CI. +- [ ] Linux ROM-free Nightly workflow passes full compile/AppImage CI. +- [ ] Real-ROM runtime smoke validation of the ROM-free Tier-3 Nightly path. +- [ ] Implement compiler-free dynamic/local optimization-bank ABI or JIT. +- [ ] Complete exact clean-content profiles and sustained gameplay validation + for US1.1, EU1.0, JP1.0, JP1.1, and KR1.0. +- [ ] Add modified-ROM content profiles only after their actual executable and + content identities are validated; do not add placeholder identities. + +## Release safety + +Nightly packaging explicitly rejects ROMs, saves, retail BIOS/firmware dumps, +and generated/capture directories. Windows ZIP and Linux AppImage artifacts are +built first and published to the fixed `nightly-release` prerelease only after +both platform jobs and payload verification succeed. A source-policy check also +rejects reintroduction of repository/environment secret references into the +public build/Nightly workflows. From 27fed60d966dce9c7cccdc4b9effde1f4b812fcd Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:37:25 +0900 Subject: [PATCH 21/44] Restore README and append ROM-free Nightly notes --- README.md | 250 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 157 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 9a00c2a..ba035c6 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ > audio issues, input quirks, networking failures, and possible desyncs. Testing, > issues, and PRs are welcome. -MetroidPrimeHuntersRecomp runs **Metroid Prime Hunters** as an ndsrecomp target. -Runtime base-version detection supports the known US1.0, US1.1, EU1.0, EU1.1, -JP1.0, JP1.1, and KR1.0 layouts using executable-compatible detection rather -than using whole-ROM SHA-1 as the base-profile selector. You provide your own -legally obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated +MetroidPrimeHuntersRecomp currently ships generated content profiles for the +validated ROM revisions tracked by this branch. Runtime base detection is +prepared for all seven retail Metroid Prime Hunters revisions and uses +melonPrimeDS-compatible executable checksums; whole-ROM SHA-1 is content +provenance, not the runtime address selector. You provide your own legally +obtained ROM. No Nintendo ROM, BIOS, firmware, save data, or generated ROM-derived source is distributed. ## Gameplay Preview @@ -21,132 +22,195 @@ Click the image to watch the gameplay preview on YouTube. ## Current Release -The project version is **v0.4.0-alpha**. Development Nightly builds are produced -from `develop` under the fixed `nightly-release` prerelease tag after the -Windows and Linux build workflows and release-payload safety checks succeed. +Latest upstream release: +**[v0.4.0-alpha](https://github.com/mstan/MetroidPrimeHuntersRecomp/releases/tag/v0.4.0-alpha)**. -The Nightly build path is deliberately **ROM-free**: GitHub Actions never needs -or downloads a Metroid Prime Hunters ROM, private ROM URL, proprietary BIOS or -firmware dump, save file, or ROM-derived MPH title bank. Users supply their ROM -at runtime. In the current Nightly architecture, title code without a linked -content-specific native bank executes through ndsrecomp Tier-3. This is a safe -correctness fallback and may be slower than an optimized tagged build. +Downloads: -The package reserves a portable optimization-cache root beside the executable -or AppImage at `cache/banks//`. The current Nightly does not yet -generate native title banks there. The intended next step is a compiler-free -local optimization/JIT cache; see [`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). +- Windows: + `MetroidPrimeHuntersRecomp-windows-x64-v0.4.0.zip` +- Linux: + `MetroidPrimeHuntersRecomp-linux-x86_64-v0.4.0.AppImage` + +This is an early ndsrecomp title and should still be treated as an alpha test +build rather than a polished game release. + +New in upstream v0.4.0 is an opt-in **HD Rendering** mod on the launcher Mods +page. It can raise the internal 3D resolution up to 4x and optionally upscale +decoded textures. It is disabled by default; native rendering remains the +reference path. This branch keeps that upstream feature alongside the +multi-ROM-safe Adaptive Widescreen and Prime Controls work. + +## Nightly builds and local optimization cache + +The `develop` branch publishes a fixed `nightly-release` prerelease after the +Windows and Linux build workflows and release-payload checks succeed. This +public Nightly path is deliberately **ROM-free**: GitHub Actions does not fetch +or receive a Metroid Prime Hunters ROM, private ROM URL/secret, proprietary BIOS +or firmware dump, save data, or ROM-derived MPH title bank. + +When a content-specific native title bank is not linked, direct-booted MPH code +uses ndsrecomp's Tier-3 correctness fallback. This makes a ROM-free Nightly +possible, but it can be slower than an optimized tagged build, especially in +known hot runtime-code paths such as opening movies. + +Nightly packages reserve the portable-first optimization-cache namespace +`cache/banks//` beside the executable/AppImage. Whole-ROM SHA-1 is +used there only as exact cache/content identity; runtime base-profile selection +continues to use the executable-compatible MPH detector. The current Nightly +does **not** generate a native title bank in this directory yet. The intended +next step is a compiler-free local bank/JIT cache. See +[`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). ## Quick Start Windows: 1. Download and fully extract the Windows ZIP. -2. Put your own Metroid Prime Hunters `.nds` ROM next to - `MetroidPrimeHuntersRecomp.exe`, or select it in the launcher. +2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the launcher. 3. Run `MetroidPrimeHuntersRecomp.exe` and press Play. Linux: -1. Download the AppImage and make it executable if required by your desktop. -2. Put your own Metroid Prime Hunters `.nds` ROM next to the AppImage. +1. Download the AppImage. +2. Put your own supported Metroid Prime Hunters `.nds` ROM next to the AppImage. 3. Run the AppImage. The current release can use the built-in FreeBIOS + generated firmware path, so retail DS BIOS and firmware dumps are not required for the default no-dump -startup path. The ROM-free Nightly generates its native FreeBIOS banks only -from the redistributable BSD-2-Clause FreeBIOS source path at build time. - -## ROM identity and multi-ROM behavior - -Three identity layers are kept separate: - -1. **Runtime base profile:** executable checksum / exact supported header tuple. -2. **Executable compatibility:** determines whether dangerous host RAM/code - writes such as Aim/Morph/Adaptive Widescreen patches are authorized. -3. **Exact content identity:** whole-ROM SHA-1 for provenance, generated banks, - captures, and the future local optimization-cache namespace. - -Whole-ROM SHA-1 therefore does not decide that a modified ROM is US1.0 or EU1.1. -Unknown or ambiguous executable content never silently falls back to US1.0, and -host writes fail closed unless executable compatibility is authoritative. - -Exact clean-content profiles currently validated in the repository include the -US1.0 and EU1.1 bring-up tracks. Other base layouts are prepared at runtime, but -full per-ROM extraction, coverage, gameplay validation, and optimized bank work -must still be completed before they should be described as equally validated -release targets. - -## Enhancements +startup path. If you choose to use your own BIOS/firmware dumps, they must be +from hardware you own and must match the hashes listed in the release's +`bios/README.txt`. + +## ROM identity and multi-ROM support + +Runtime address selection does **not** use whole-ROM SHA-1. The runner first +uses the melonPrimeDS executable checksum (CRC32 over header + ARM9 + ARM7), +then uses exact game code + supported revision only as a fallback base-profile +hint. Header-only matches never authorize host RAM/code writes. + +Known compatible executable checksums can use the revision-specific Prime +Controls and Adaptive Widescreen addresses. Whole-ROM SHA-1 remains the exact +content identity for generated banks, coverage, checkpoints and capture data, +so one modified ROM cannot silently reuse another modified ROM's generated +content. + +The current branch has runtime address profiles for US1.0, US1.1, EU1.0, +EU1.1, JP1.0, JP1.1 and KR1.0. A revision still needs its own generated +content/capture coverage before it is considered fully brought up. + +## What Works + +- Boots supported content profiles through the ndsrecomp runner. +- Reaches Metroid Prime Hunters gameplay in tested routes. +- Includes an adaptive 21:9 upper-screen widescreen option using per-revision + projection/culling addresses from melonPrimeDS/mphCodex. +- Includes Prime-style keyboard and mouse controls. +- Includes full remappable gamepad bindings in the launcher. +- Includes upstream HD Rendering controls for internal resolution and texture + upscaling. +- Supports mouse-driven touchscreen input. +- Can authenticate through Wiimmfi and reach a Friends and Rivals lobby in + validated flows. +- Persists mutable firmware/WFC state between launches through the upstream + firmware-state path. -### Adaptive Widescreen - -The launcher exposes an adaptive 21:9 upper-screen mode. The implementation -combines ndsrecomp's widened host renderer/compositor and HUD anchoring with -profile-aware Metroid Prime Hunters projection/culling corrections derived from -the audited melonPrimeDS/mphCodex address tables. Unsupported/unsafe runtime -identity falls back rather than applying guessed guest writes. - -### Prime Controls +## Known Limits -Prime-style keyboard/mouse controls and remappable gamepad bindings are exposed -through the launcher. Defaults include WASD movement, mouse aim, Mouse 1 fire, -and the existing touch-helper mappings. +- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and + gameplay problems are expected. +- Gameplay coverage is incomplete. Do not assume the campaign is fully + validated from start to finish. +- Widescreen is still being audited. Some scenes, effects, HUD placement, + movies, fades, or screen-routing behavior may be wrong. +- HD texture upscaling remains opt-in and should not be treated as the native + reference rendering path. +- ROM-free Nightly builds can be slower while MPH title code is using Tier-3 + instead of a validated native optimization bank. +- The local `cache/banks//` path is currently a cache contract; + dynamic native/JIT bank generation is not implemented yet. +- Online play is experimental. Wiimmfi can reach the lobby in validated flows, + but in-game play is ultimately untested. There is no guarantee that a match + will connect, stay connected, or avoid desync. +- Save behavior and settings are still part of early release testing. Keep + backups of anything you care about. + +## Controls + +Prime Controls are enabled by default. + +Keyboard and mouse defaults: + +- `WASD`: move +- Mouse: aim +- Mouse 1 / Mouse 2: fire / scan-fire +- `Space`: jump +- `Left Ctrl`: morph ball +- `Left Shift`: boost / map zoom +- `C`: scan visor +- `F`: OK +- `Q` / `E`: scan-message arrows +- `V`: menu +- Mouse 4: missiles +- Mouse 5: beam +- `1` through `6`: subweapons +- `Tab`: virtual stylus + +Gamepad defaults: + +- Left stick: move and menu D-pad +- Right stick: aim +- `RT` / `LT`: shoot / scan-fire +- `A`: jump +- `B`: morph ball +- `X`: missile +- `Y`: UI OK +- `LB` / `RB`: beam / boost or zoom +- `R3`: scan visor +- D-pad left/right: scan-message arrows +- `Start`: menu + +Keyboard, mouse, and gamepad bindings are editable from the launcher Mods page. -### HD Rendering +## Online Play -HD Rendering is opt-in. It raises the 3D engine above native DS sample density -(up to the supported internal-resolution choices) and can upscale decoded -textures. The native 2D path remains the reference and HD Rendering is off by -default. +Nintendo WFC / Wiimmfi support is experimental. The current validated state is +lobby connectivity: Metroid Prime Hunters can authenticate through Wiimmfi and +reach a Friends and Rivals lobby where a locally hosted game is visible. -## Online Play +The launcher keeps the console firmware profile in +`%APPDATA%\MetroidPrimeHuntersRecomp`. Wi-Fi settings, console/game-card +pairing, and WFC updates survive both the in-game system shutdown flow and a +normal window close. Confirming the WFC settings shutdown prompt closes the +application automatically. -Nintendo WFC / Wiimmfi support remains experimental. The launcher persists the -console firmware profile in the user's application-data location so Wi-Fi -settings, console/game-card pairing, and WFC updates survive later launches. -Online play may still fail to connect, disconnect, or desync. +Actually joining a match and playing in-game online is not guaranteed. It may +fail to connect, disconnect, or desync. The Wi-Fi implementation is built on [melonDS](https://github.com/melonDS-emu/melonDS)'s Wi-Fi work in the shared ndsrecomp runner. Full credit to the melonDS team for the Wi-Fi controller, emulated access point, and network backend foundation. -## Known Limits - -- This is an alpha. Bugs, crashes, hangs, graphical issues, audio issues, and - gameplay problems are expected. -- Gameplay coverage is incomplete across the seven base layouts. -- The ROM-free Nightly's Tier-3 title fallback can be substantially slower than - a build with validated native MPH optimization banks, especially in known hot - paths such as opening movies. -- The local `cache/banks//` directory is currently a reserved - cache contract; dynamic native/JIT bank generation is not implemented yet. -- Widescreen still requires sustained gameplay auditing across scenes, effects, - HUD placement, movies, fades, and screen routing. -- Online play is experimental. -- Save behavior and settings remain part of early release testing. Keep backups - of anything you care about. - ## Credits - [melonDS](https://github.com/melonDS-emu/melonDS): Wi-Fi implementation foundation used by the shared ndsrecomp runner. -- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): reference for - Prime-style controls, ROM/version address tables, and aspect-ratio research. -- [mphCodex](https://github.com/Zection6V/mphCodex): game-code/disassembly and - Metroid Prime Hunters behavior research. +- [melonPrimeDS](https://github.com/ag-advania/melonPrimeDS): runtime-version + detection, Prime-style controls, and per-version aspect-ratio patch reference. +- [mphCodex](https://github.com/Zection6V/mphCodex): MPH game-code analysis, + including the seven-version widescreen projection/culling mapping. - [MphRead](https://github.com/NoneGiven/MphRead): Metroid Prime Hunters file format and behavior reference. -See the ndsrecomp `THIRD_PARTY_ATTRIBUTION.md` for shared-runtime provenance and -licensing details. +See the ndsrecomp +[`THIRD_PARTY_ATTRIBUTION.md`](https://github.com/mstan/ndsrecomp/blob/main/THIRD_PARTY_ATTRIBUTION.md) +for provenance and licensing details for shared runtime components. ## Developers -Bring-up and validation notes live under [`docs/`](docs/). The ROM-free Nightly -and local-cache direction is documented in -[`docs/LOCAL_BANK_CACHE.md`](docs/LOCAL_BANK_CACHE.md). +This README is intentionally player-facing. Development notes, validation +history, and bring-up details live in [`docs/BRINGUP.md`](docs/BRINGUP.md). The original code in this repository is MIT licensed. Metroid Prime Hunters, Nintendo DS firmware/BIOS images, ROMs, saves, and all derived game data remain From 5ce8a460039440e91166cca3609cc2da67f0e8df Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:37:49 +0900 Subject: [PATCH 22/44] Restore bring-up ledger and append ROM-free Nightly notes --- docs/BRINGUP.md | 265 +++++++++++++++++++++++++++++------------------- 1 file changed, 161 insertions(+), 104 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index dca7aa2..f5b17ab 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -2,107 +2,164 @@ ## Target and references -The project began with the USA revision-0 (`AMHE`) bring-up and now carries a -seven-base-profile runtime layout/detector for US1.0, US1.1, EU1.0, EU1.1, -JP1.0, JP1.1, and KR1.0. Exact clean-content profiles, generated banks, -coverage, and capture artifacts remain content-specific and must not be inferred -from a base layout alone. - -The original US1.0 content identity is SHA-1 -`90164d1ac127ee5f9815ea4ae7de798c7b5fc629`. Whole-ROM SHA-1 is provenance and -cache/build identity, not the runtime base-profile selector. - -## Runtime identity rules - -1. Use the melonPrime-compatible executable CRC32 detector as the authoritative - runtime base-profile signal. -2. An exact supported game-code/revision tuple may identify only a candidate - base profile when the executable checksum is unknown. -3. Dangerous host RAM/code writes fail closed unless executable compatibility - is authoritative. -4. Generated title banks, runtime captures, and coverage remain scoped by exact - content identity. -5. Unknown modified content must never silently fall back to US1.0. - -## ROM-free Nightly architecture - -Public Windows/Linux Nightly builds are intentionally produced without a -Metroid Prime Hunters ROM, ROM URL/secret, proprietary BIOS or firmware dump, -save file, or generated ROM-derived MPH title bank. - -The pinned ndsrecomp runner is patched for an opt-in public-build mode: - -- redistributable BSD-2-Clause FreeBIOS images are recompiled into the native - FreeBIOS ARM9/ARM7 banks using ndsrecomp's documented FreeBIOS pipeline; -- `NDS_RETAIL_BIOS_BANKS=OFF` removes the build-time requirement for generated - proprietary retail-BIOS banks; -- if a user later supplies retail BIOS dumps, immutable BIOS execution may use - the existing reference interpreter rather than requiring those generated C - banks in the distributed binary; -- no MPH title-bank directory is configured, so direct-booted MPH ARM9/ARM7 - code falls through to Tier-3 using guest-written RAM provenance. - -This is a correctness-first release path. It is expected to be slower than the -historical optimized US1.0 release, particularly in opening-FMV/runtime-code hot -paths. - -## Local optimization cache direction - -The preferred future optimization cache is portable-first: - -```text -cache/banks// -``` - -beside the executable/AppImage. Linux falls back to XDG cache when that location -is not writable; Windows should fall back to LOCALAPPDATA under the same -condition. Save data and firmware/WFC identity remain persistent app data, not -regenerable optimization cache. - -The current static ndsrecomp bank pipeline emits C and links it into the runner, -so the project will not make users run a host C/C++ compiler on first launch. -The intended progression is Tier-3 -> compiler-free local IR/bank support if -useful -> hot-block JIT with a validated persistent cache. See -`docs/LOCAL_BANK_CACHE.md`. - -## Adaptive Widescreen - -Adaptive Widescreen combines the original ndsrecomp host-side widened -renderer/compositor/HUD anchoring with MPH game-side projection and culling -patches audited against melonPrimeDS/mphCodex. The guest patch addresses are -profile-aware across all seven base layouts and are applied only under the -existing authoritative executable-compatibility gate. - -## Upstream launcher/runtime integration - -The launcher tracks the upstream MPH recomp-ui feature set including Adaptive -Widescreen, Prime Controls, HD Rendering, and persistent firmware/WFC state. -The launcher does not use whole-ROM SHA-1 as an early acceptance gate; the -runner owns executable compatibility and exact-content decisions. - -## Validation gates - -- [x] Seven runtime base profiles represented. -- [x] Executable CRC32 runtime detector and strict header fallback. -- [x] Dangerous host writes fail closed for unknown executable content. -- [x] All-seven Adaptive Widescreen address table integrated. -- [x] Upstream launcher HD/Wi-Fi persistence integration retained. -- [x] ROM-free release design avoids ROM/ROM-secret input in GitHub Actions. -- [x] FreeBIOS native-bank generation has a redistributable build path. -- [ ] Windows ROM-free Nightly workflow passes full compile/package CI. -- [ ] Linux ROM-free Nightly workflow passes full compile/AppImage CI. -- [ ] Real-ROM runtime smoke validation of the ROM-free Tier-3 Nightly path. -- [ ] Implement compiler-free dynamic/local optimization-bank ABI or JIT. -- [ ] Complete exact clean-content profiles and sustained gameplay validation - for US1.1, EU1.0, JP1.0, JP1.1, and KR1.0. -- [ ] Add modified-ROM content profiles only after their actual executable and - content identities are validated; do not add placeholder identities. - -## Release safety - -Nightly packaging explicitly rejects ROMs, saves, retail BIOS/firmware dumps, -and generated/capture directories. Windows ZIP and Linux AppImage artifacts are -built first and published to the fixed `nightly-release` prerelease only after -both platform jobs and payload verification succeed. A source-policy check also -rejects reintroduction of repository/environment secret references into the -public build/Nightly workflows. +- Cartridge: USA revision 0 (`AMHE`, `MP HUNTERS`) +- ROM SHA-1: `90164d1ac127ee5f9815ea4ae7de798c7b5fc629` +- Framework main integration: `778e74385aa223179a5d9534c2201ed1096a3df7` +- MphRead reference: `26cd8a6fe93dc5e525d1a1bb304fe96001111e55` +- Public matching disassembly: none found + +The ARM9 ROM image is 517,828 bytes and expands to 907,736 bytes. The ARM7 +image is 164,964 bytes. Prime Hunters has 18 compressed ARM9 overlays whose +overlay table contains 576 bytes (18 records), not 576 separate overlays. + +## Evidence so far + +1. The pre-existing SM64DS-native runner failed around the Prime Hunters game + handoff with ARM7 PC `0xE590100C` and a corrupt stack. +2. The failure was caused by unconditional SM64DS bank registration, not by a + Prime Hunters instruction or device requirement. Both titles load ARM7 at + `0x02380000`, so address-only dispatch selected SM64DS code. +3. A clean BIOS-bank-only runner reaches 700,000,000 ARM9 cycles with both + CPUs alive and no terminal dispatch miss. +4. Visual checkpoints: + - VBlank 300: ActImagine splash + - VBlank 900-1200: opening logo animation + - VBlank 2400: opening cinematic + - VBlank 3000: hunter cinematic + - VBlank 3600: Weavel introduction +5. The initial generated main banks contained 4,335 ARM9 functions and 16 + ARM7 functions. Exact-ROM-gated registration produced the same 700,000,000 + cycle machine state as the clean interpreter runner. +6. AMHE uses melonDS SaveMemType 5: 256 KiB flash. Save type/capacity are now + game-owned configuration instead of an SM64DS runner constant. +7. Native and ndsref event/cycle counts agree through the no-input return to + the hunter reel and onward to VBlank 12000. +8. The title split exposed a cold-boot screen-routing defect hidden by the + mirrored intro video: ndsrecomp reset POWCNT1 to `0x0001` instead of the + retail/melonDS `0x820F`. The reset and 3D power defaults are corrected. +9. A second routing defect appeared only when Prime Hunters changed the LCD + assignment during VBlank. The native renderer stored engine-relative + frames and applied the current POWCNT1 only when the completed frame was + read, which could retroactively swap it. Routing is now applied while each + scanline is produced, matching melonDS framebuffer assignment. +10. The no-input title/loop checkpoints are: + - VBlank 7800: title logo and `TOUCH TO START` + - VBlank 8400: title animation + - VBlank 9000: return to the hunter reel + Native top/bottom captures are byte-identical to ndsref at all three. +11. The checkpoint helper now names screens explicitly and continues after a + server safety-round exhaustion. It refuses to label or save a frame unless + the requested absolute VBlank was actually reached. +12. A seeded, trace-preserving input search discovered the first campaign + path. Its minimized replay is: + - tap the title and Adventure Mode + - create and confirm mission file A + - select file A again to start the campaign + - skip the mission briefing + - wait for the Celestial Archives gunship screen and confirm landing + The native and reference runs reach the live first-person HUD at VBlank + 10859. +13. `tools/fuzz_mph_gameplay.py` records every action, absolute VBlank, + screenshot, perceptual signature, RGB hash, and event-count snapshot. + `scenarios/adventure_start.json` is the replayable minimized result. +14. All 15 matching native/oracle checkpoints in the minimized route are + byte-identical across both physical screens: zero differing pixels and + zero maximum channel delta, including the first gameplay frame. +15. Tier-3 coverage captured from that route yielded 567 unique ARM9 + call/indirect targets inside the immutable main image. Slice-resume roots, + runtime RAM, and all reused overlay ranges were excluded. Adding those + seeds expands the ARM9 bank to 7,115 functions; the identical replay cuts + ARM9 Tier-3 entries from 64,619,845 to 57,525,780 (10.98%) and interpreted + instructions from 3,866,962,843 to 3,638,379,652 (5.91%). All 13 action + checkpoints retain identical event counts and RGB hashes. This does not + yet establish a wall-clock speedup while generated code remains `-O0`. +16. The opening FMV slowdown was isolated with + `tools/benchmark_mph_fmv.py`. Static-only FMV windows ran at 26-28 FPS: + presentation stayed below 1 ms/frame while emulation rose to 34-37 + ms/frame and ARM9 executed roughly 620,000 Tier-3 instructions per frame. + The hot code is the runtime ITCM mirror plus the active overlay near + `0x02102D74`. +17. A deterministic VBlank-3000 ITCM+main-RAM capture is pinned by SHA-1 + `2f4a2ba36886fb9152781f5829dedfd4b836a73b`. The separate + `mph_arm9_fmv_runtime` bank uses only call/indirect roots observed in the + VBlank 2400-3000 delta and validates live guest bytes before dispatch. + Seeding scheduler-resume PCs was rejected because it split hot loops into + one-instruction functions and generated about 589,000 fallthrough + dispatches/frame; the retained bank records about 5,400/frame. +18. The retained interactive run sustains 59.73-59.84 FPS from VBlank + 2400-4800 at 8.37-9.31 ms emulation/frame with zero audio underruns. + Static-only and optimized runners have identical event, instruction, and + cycle counts and zero differing pixels at VBlanks 2400, 3000, 3600, 4200, + and 4800. + +## Bring-up gates + +- [x] Isolated framework worktree from latest `origin/main` +- [x] Exact AMHE0 ROM identity and header inventory +- [x] Independent game repository/scaffold +- [x] Public reverse-engineering resource audit and pinned MphRead checkout +- [x] Safe interpreter boot through the opening cinematic +- [x] Remove the cross-title SM64DS bank-registration assumption +- [x] Reach and capture the title screen +- [x] Observe one complete no-input attract loop +- [x] Compare the same attract checkpoints against the ndsref oracle +- [x] Compile and register AMHE0 main ARM9/ARM7 banks by ROM capability +- [ ] Capture remaining runtime ARM7 code and ARM9 overlay generations (the + opening-FMV ARM9 generation is complete) +- [x] Generalize cartridge save type/size beyond SM64DS's 8 KiB EEPROM +- [x] Add deterministic Prime Hunters navigation and gameplay-entry scenario +- [ ] Add sustained traversal, combat, pause, death, and reload scenarios +- [x] Enable an exact-ROM upper-screen adaptive-wide bring-up baseline +- [x] Latch adaptive/direct presentation state to the published frame so + boot logos and capture transitions do not flicker at high host scaling +- [ ] Audit Prime Hunters projection/culling/HUD across sustained gameplay +- [x] Add an MPH recomp-ui development launcher and enhancement toggle +- [x] Add top-window relative mouse aim, Mouse 1 fire, and persisted controls +- [x] Add portable Windows release launcher/mod packaging with a baked, + content-validated FMV runtime bank + +## Design constraints + +- A title bank must never be selected by address alone. Registration is gated + by exact cartridge identity, and mutable/overlay banks also validate live + bytes. +- The runner also validates `[game].sha1` before applying title-owned config, + so a Prime Hunters save-device declaration cannot silently affect another + cartridge. +- Each overlay generation remains a separate content-validated bank. Prime + Hunters reuses virtual ranges, so combining entry points from different + overlay images would be unsound. +- The interpreter is the correctness oracle for uncompiled code within the + native runtime; ndsref remains the independent machine oracle. +- Widescreen is a title-owned capability. Separate windows are safe as a host + layout, but field-of-view, culling, HUD anchors, movies, and touch routing + require Prime Hunters-specific proof. +- MphRead's recreation uses a 78-degree camera FOV and derives projection and + frustum planes from the live output aspect ratio. That is a useful semantic + reference, but it does not prove which AMHE0 guest structures and GX command + sites must be patched. The host adaptive viewport is enabled as an explicit + bring-up baseline, but it is not considered visually complete until those + title-side behaviors pass sustained gameplay review. + +## ROM-free Nightly and local optimization cache + +The public Windows/Linux Nightly path is deliberately ROM-free. GitHub Actions +does not receive a Metroid Prime Hunters ROM, private ROM URL/secret, +proprietary BIOS/firmware dump, save, or generated ROM-derived MPH title bank. +The pinned runner is built with the redistributable BSD-2-Clause FreeBIOS native +banks and with `NDS_RETAIL_BIOS_BANKS=OFF`. + +When no content-specific MPH native bank is linked, direct-booted ARM9/ARM7 code +uses the existing Tier-3 interpreter path after guest writes establish RAM code +provenance. This is a correctness-first Nightly mode and can be substantially +slower than the historical optimized US1.0 release, especially in the FMV hot +runtime-code path described above. + +Nightly packages reserve `cache/banks//` beside the executable or +AppImage as the future portable optimization-cache namespace. Whole-ROM SHA-1 +is suitable here because cache payloads must be bound to exact content, but it +must not become the runtime base-profile selector. The current static +recompiler emits C and links it into the runner, so first-launch C/C++ +compilation is intentionally **not** the target UX. The intended progression is +Tier-3 -> compiler-free local bank/IR support if useful -> hot-block JIT with a +validated persistent cache. See `docs/LOCAL_BANK_CACHE.md`. From d8c9683808c5c8083b470a6fbeacba254b6df98d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:41:06 +0900 Subject: [PATCH 23/44] Fix ROM-free patch idempotency --- tools/patch_ndsrecomp_rom_free_release.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/patch_ndsrecomp_rom_free_release.py b/tools/patch_ndsrecomp_rom_free_release.py index 40b4390..256a063 100644 --- a/tools/patch_ndsrecomp_rom_free_release.py +++ b/tools/patch_ndsrecomp_rom_free_release.py @@ -20,7 +20,11 @@ def replace_once(path: Path, old: str, new: str, marker: str) -> None: text = path.read_text(encoding="utf-8") - if marker in text and old not in text: + # The replacement may intentionally contain the original anchor (for + # example, adding one declaration immediately after an existing one). + # Therefore exact replacement text, not marker/anchor absence, is the + # reliable idempotency test. + if new in text: return count = text.count(old) if count != 1: From 3acd616b5e43b4d06f44cf9aee2caced64a43dc8 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:42:18 +0900 Subject: [PATCH 24/44] Preserve existing ignores for local cache --- .gitignore | 53 ++++++++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 14264b4..7b9be66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,34 @@ -# ROM / console dumps +# User-owned Nintendo DS images and ROM-derived outputs. *.nds -*.NDS -*.sav -*.dsv -bios/*.rom -bios/*.bin -firmware.bin - -# Generated recompilation inputs/outputs and captures -generated/ -release-stage/ +/generated/ +/extracted/ -# Local optimization cache (portable-first runtime/JIT cache) -cache/ +# Optional reverse-engineering references. +/third_party/ -# Build trees -build/ -build-*/ -launcher/recomp-ui/build/ -launcher/recomp-ui/build-*/ +# Build trees, runtime state, and captures. +/build/ +/build-*/ +/release-stage/ +/launcher/recomp-ui/build*/ +/saves/ +/captures/ +/scratch/ +/cache/ +*.sav +*.log +/rom.cfg +/settings.toml +/keybinds.ini -# Python -.venv/ +# Local Python environment and tool output. +/.venv/ __pycache__/ *.pyc -# Tool/editor state +# Editor and OS output. +.vs/ .vscode/ .idea/ -*.suo -*.user -*.gpr -*.rep -*.lock - -# OS junk -.DS_Store Thumbs.db +.DS_Store From ceafcc7def88d296a1c0ccf601fa36f579d4fa20 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:44:20 +0900 Subject: [PATCH 25/44] Run Linux packager through bash --- .github/workflows/build-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 808bd3d..5c96e5c 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -98,7 +98,7 @@ jobs: set -euo pipefail version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" test -n "$version" - tools/package-linux-appimage.sh \ + bash tools/package-linux-appimage.sh \ --version "$version" \ --runner ../ndsrecomp/runner/build-mph-nightly/nds_runner \ --appimage-tool "$PWD/.ci-tools/appimagetool" \ From 4b9ad7cbd36cd4b0b0a3f07375d4c84cb29c5c01 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:49:25 +0900 Subject: [PATCH 26/44] Fix Windows Nightly PowerShell packaging --- .github/workflows/build-windows.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 687644a..b643b76 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -107,7 +107,10 @@ jobs: version="$(sed -n 's/^project(MetroidPrimeHuntersRecomp VERSION \([0-9.]*\).*/\1/p' CMakeLists.txt)" test -n "$version" runtime_bin="$(cygpath -w /mingw64/bin)" - pwsh -NoProfile -File tools/package-windows-nightly.ps1 \ + ps_exe="$(cygpath -u "${SYSTEMROOT}")/System32/WindowsPowerShell/v1.0/powershell.exe" + script_path="$(cygpath -w "$PWD/tools/package-windows-nightly.ps1")" + test -x "$ps_exe" + "$ps_exe" -NoProfile -ExecutionPolicy Bypass -File "$script_path" \ -Version "$version" \ -RunnerBuildDir '..\ndsrecomp\runner\build-mph-nightly' \ -LauncherBuildDir 'launcher\recomp-ui\build-nightly' \ From dc95926c79a7473dd0b59d19f6743dcbd80ddf54 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 15:52:51 +0900 Subject: [PATCH 27/44] Gate Nightly helper syntax in build CI --- .github/workflows/build.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cfa34a..e6f5b3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,28 @@ jobs: steps: - name: Check out sources uses: actions/checkout@v4 + + - name: Check ROM-free helper syntax + shell: bash + run: | + set -euo pipefail + python -m py_compile \ + tools/patch_ndsrecomp_rom_free_release.py \ + tools/ci/check_rom_free_release_sources.py \ + tools/ci/prepare_freebios_banks.py \ + tools/ci/verify-nightly-assets.py + bash -n tools/package-linux-appimage.sh + pwsh -NoProfile -Command ' + $tokens = $null; $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path "tools/package-windows-nightly.ps1"), + [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -ne 0) { + Write-Error ($errors | Out-String) + exit 1 + } + ' + - name: Verify no ROM-secret release path run: python tools/ci/check_rom_free_release_sources.py From c19611dbd50691501373a4f2a07b437216b224fe Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:15:36 +0900 Subject: [PATCH 28/44] Fix delegated multi-ROM status in launcher UI --- tools/patch_recomp_ui_mph_multirom.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tools/patch_recomp_ui_mph_multirom.py diff --git a/tools/patch_recomp_ui_mph_multirom.py b/tools/patch_recomp_ui_mph_multirom.py new file mode 100644 index 0000000..e35d69e --- /dev/null +++ b/tools/patch_recomp_ui_mph_multirom.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Patch pinned recomp-ui to render delegated MPH ROM validation honestly. + +MPH runtime compatibility is not a whole-ROM SHA-1 gate. When GameInfo omits +all cartridge fingerprints, the stock recomp-ui model deliberately cannot call +the ROM "verified" and the ImGui dashboard therefore renders "ROM not +recognized" even though launcher_model_can_play() correctly allows the host to +perform its own launch-time validation. + +For this project, a fingerprint-free cartridge means exactly that: acceptance +is delegated to nds_runner's MPH executable-compatible detector. This patch +changes only that dashboard presentation. Fingerprinted games keep stock +verified/not-recognized semantics, and the runner remains the authoritative +fail-closed validator. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +OLD = ''' const bool verified = launcher_model_rom_verified(m);\n char line[64];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(verified, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(verified ? col(th.good) : col(th.warn), "%s", line);''' + +NEW = ''' const bool verified = launcher_model_rom_verified(m);\n // MPH_MULTIROM_DELEGATED_VERIFY: no generic fingerprint means the host\n // intentionally delegates compatibility to its runtime detector. Do\n // not tell the player that such a ROM is "not recognized"; Play is\n // already allowed by launcher_model_can_play() in this state.\n const bool delegated = m->rom_present && !m->has_expected_crc &&\n m->num_known_sha256 == 0 &&\n m->num_known_sha1 == 0;\n const bool accepted = verified || delegated;\n char line[96];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else if (delegated) snprintf(line, sizeof(line), "%s selected - runtime validation", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(accepted, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(accepted ? col(th.good) : col(th.warn), "%s", line);''' + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--recomp-ui-root", type=Path, required=True) + args = parser.parse_args() + + root = args.recomp_ui_root.resolve() + path = root / "src" / "common" / "backends" / "imgui" / "launcher_imgui.cpp" + if not path.is_file(): + raise SystemExit(f"missing pinned recomp-ui source: {path}") + + text = path.read_text(encoding="utf-8-sig") + if NEW in text: + print(f"recomp-ui MPH multi-ROM presentation already patched: {path}") + return + count = text.count(OLD) + if count != 1: + raise SystemExit( + f"{path}: expected exactly one launcher ROM-verdict anchor, got {count}; " + "recomp-ui pin/source shape drifted" + ) + path.write_text(text.replace(OLD, NEW), encoding="utf-8") + print(f"Patched delegated MPH ROM validation presentation in {path}") + + +if __name__ == "__main__": + main() From 89731aeb9fc1da54e032eaf167fcbd71524e0697 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:15:57 +0900 Subject: [PATCH 29/44] Delegate launcher ROM verification to runtime detector --- launcher/recomp-ui/CMakeLists.txt | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index cbf4a90..4dd9705 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -6,14 +6,15 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(SDL2 CONFIG REQUIRED) +find_package(Python3 COMPONENTS Interpreter REQUIRED) set(NDSRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../ndsrecomp" CACHE PATH "Path to the ndsrecomp framework checkout (for the shared SHA-1 helper)") set(MPH_LAUNCHER_ROM_SHA1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" CACHE STRING "Clean retail ROM SHA-1 metadata rendered for this profile-specific launcher") -set(MPH_LAUNCHER_REGION "USA" CACHE STRING - "Region label shown by the profile-specific launcher") +set(MPH_LAUNCHER_REGION "Auto (runtime detected)" CACHE STRING + "Region label shown by the launcher; generic Nightly detection happens at runtime") set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING "Default ROM filename offered by this profile-specific launcher") @@ -65,6 +66,24 @@ target_link_libraries(mph-recomp-ui PRIVATE SDL2::SDL2) set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH "Path to the shared recomp-ui checkout") +option(MPH_PATCH_RECOMP_UI_MULTIROM + "Patch pinned recomp-ui so fingerprint-free MPH ROMs show runtime-delegated validation" + ON) +if(MPH_PATCH_RECOMP_UI_MULTIROM) + execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/patch_recomp_ui_mph_multirom.py" + --recomp-ui-root "${RECOMP_UI_ROOT}" + RESULT_VARIABLE _mph_recomp_ui_patch_result + OUTPUT_VARIABLE _mph_recomp_ui_patch_stdout + ERROR_VARIABLE _mph_recomp_ui_patch_stderr) + if(NOT _mph_recomp_ui_patch_result EQUAL 0) + message(FATAL_ERROR + "Failed to apply MPH multi-ROM recomp-ui patch:\n" + "${_mph_recomp_ui_patch_stdout}${_mph_recomp_ui_patch_stderr}") + endif() + message(STATUS "${_mph_recomp_ui_patch_stdout}") +endif() enable_testing() add_executable(mph-mod-provider-test tests/launcher_mod_provider_test.cpp From cd8d6f546860574897d96eb17c12912df5782b39 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:16:24 +0900 Subject: [PATCH 30/44] Keep profile defaults while patching real launcher UI --- launcher/recomp-ui/CMakeLists.txt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 4dd9705..03518ee 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -13,8 +13,8 @@ set(NDSRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../ndsrecomp" CACHE PATH set(MPH_LAUNCHER_ROM_SHA1 "90164d1ac127ee5f9815ea4ae7de798c7b5fc629" CACHE STRING "Clean retail ROM SHA-1 metadata rendered for this profile-specific launcher") -set(MPH_LAUNCHER_REGION "Auto (runtime detected)" CACHE STRING - "Region label shown by the launcher; generic Nightly detection happens at runtime") +set(MPH_LAUNCHER_REGION "USA" CACHE STRING + "Region label shown by the profile-specific launcher") set(MPH_LAUNCHER_DEFAULT_ROM "Metroid Prime Hunters.nds" CACHE STRING "Default ROM filename offered by this profile-specific launcher") @@ -69,7 +69,9 @@ set(RECOMP_UI_ROOT "F:/Projects/recomp-ui" CACHE PATH option(MPH_PATCH_RECOMP_UI_MULTIROM "Patch pinned recomp-ui so fingerprint-free MPH ROMs show runtime-delegated validation" ON) -if(MPH_PATCH_RECOMP_UI_MULTIROM) +set(_mph_recomp_ui_imgui + "${RECOMP_UI_ROOT}/src/common/backends/imgui/launcher_imgui.cpp") +if(MPH_PATCH_RECOMP_UI_MULTIROM AND EXISTS "${_mph_recomp_ui_imgui}") execute_process( COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/patch_recomp_ui_mph_multirom.py" @@ -83,6 +85,12 @@ if(MPH_PATCH_RECOMP_UI_MULTIROM) "${_mph_recomp_ui_patch_stdout}${_mph_recomp_ui_patch_stderr}") endif() message(STATUS "${_mph_recomp_ui_patch_stdout}") +elseif(MPH_PATCH_RECOMP_UI_MULTIROM) + # Static launcher-profile tests intentionally provide a minimal recomp-ui + # CMake stub and do not compile the UI backend. A real launcher build has + # the pinned source above and must take the fail-closed patch path. + message(STATUS + "MPH recomp-ui presentation patch skipped: UI backend source is not present") endif() enable_testing() From 6cf124f05e6cedca3e8889418ea0c62a6eba81d9 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:16:45 +0900 Subject: [PATCH 31/44] Show runtime-detected region in generic Nightly launcher --- .github/workflows/build-windows.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index b643b76..a1f23f7 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -95,10 +95,17 @@ jobs: -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DNDSRECOMP_ROOT="$PWD/../ndsrecomp" \ - -DRECOMP_UI_ROOT="$PWD/../recomp-ui" + -DRECOMP_UI_ROOT="$PWD/../recomp-ui" \ + '-DMPH_LAUNCHER_REGION=Auto (runtime detected)' cmake --build launcher/recomp-ui/build-nightly ctest --test-dir launcher/recomp-ui/build-nightly --output-on-failure test -s launcher/recomp-ui/build-nightly/mph-recomp-ui.exe + grep -q 'game.region = "Auto (runtime detected)";' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'game.known_sha1_hex = nullptr;' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'MPH_MULTIROM_DELEGATED_VERIFY' \ + ../recomp-ui/src/common/backends/imgui/launcher_imgui.cpp - name: Package Windows Nightly payload shell: msys2 {0} From 66bdd9cdc924a880c425b138279590cf10e0eb56 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:18:56 +0900 Subject: [PATCH 32/44] Gate launcher multi-ROM UI patch syntax --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6f5b3b..ec8b778 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,7 @@ jobs: set -euo pipefail python -m py_compile \ tools/patch_ndsrecomp_rom_free_release.py \ + tools/patch_recomp_ui_mph_multirom.py \ tools/ci/check_rom_free_release_sources.py \ tools/ci/prepare_freebios_banks.py \ tools/ci/verify-nightly-assets.py From 129137390cc126d4bf797cfff2b548247514b9bc Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:28:27 +0900 Subject: [PATCH 33/44] Fix launcher default ROM selection when file is absent --- launcher/recomp-ui/CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 03518ee..7a9d904 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -53,6 +53,21 @@ mph_launcher_replace_required( "exe / \"${MPH_LAUNCHER_DEFAULT_ROM}\";" "the default MPH ROM filename") +# recomp-ui treats any non-empty initial ROM string as selected before it tries +# to open the file. A release package intentionally contains no ROM, so do not +# feed the conventional filename to the model unless that file really exists. +# This preserves the useful portable behavior where a player may deliberately +# place a ROM with the conventional name beside the executable, while a fresh +# extraction correctly starts at "No ROM loaded". +mph_launcher_replace_required( + " char selected_rom[1024]{};\n const int result = recomp_launcher_run_window(" + " std::error_code initial_rom_error;\n const std::string initial_rom =\n std::filesystem::is_regular_file(default_rom, initial_rom_error)\n ? default_rom.string()\n : std::string();\n char selected_rom[1024]{};\n const int result = recomp_launcher_run_window(" + "the initial ROM selection block") +mph_launcher_replace_required( + "exe.string().c_str(), default_rom.string().c_str()," + "exe.string().c_str(), initial_rom.c_str()," + "the launcher initial ROM argument") + set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") From ae79be2296b7a4908405b03b09805068f5fe0244 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:28:43 +0900 Subject: [PATCH 34/44] Require readable ROM before delegated validation display --- tools/patch_recomp_ui_mph_multirom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/patch_recomp_ui_mph_multirom.py b/tools/patch_recomp_ui_mph_multirom.py index e35d69e..20e310b 100644 --- a/tools/patch_recomp_ui_mph_multirom.py +++ b/tools/patch_recomp_ui_mph_multirom.py @@ -22,7 +22,7 @@ OLD = ''' const bool verified = launcher_model_rom_verified(m);\n char line[64];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(verified, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(verified ? col(th.good) : col(th.warn), "%s", line);''' -NEW = ''' const bool verified = launcher_model_rom_verified(m);\n // MPH_MULTIROM_DELEGATED_VERIFY: no generic fingerprint means the host\n // intentionally delegates compatibility to its runtime detector. Do\n // not tell the player that such a ROM is "not recognized"; Play is\n // already allowed by launcher_model_can_play() in this state.\n const bool delegated = m->rom_present && !m->has_expected_crc &&\n m->num_known_sha256 == 0 &&\n m->num_known_sha1 == 0;\n const bool accepted = verified || delegated;\n char line[96];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else if (delegated) snprintf(line, sizeof(line), "%s selected - runtime validation", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(accepted, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(accepted ? col(th.good) : col(th.warn), "%s", line);''' +NEW = ''' const bool verified = launcher_model_rom_verified(m);\n // MPH_MULTIROM_DELEGATED_VERIFY: no generic fingerprint means the host\n // intentionally delegates compatibility to its runtime detector. The\n // model marks any non-empty initial path as rom_present before opening\n // it, so require a successfully measured file as well; a missing\n // conventional default filename must never appear as selected.\n const bool readable = m->rom_present && std::strcmp(m->rom_size, "--") != 0;\n const bool delegated = readable && !m->has_expected_crc &&\n m->num_known_sha256 == 0 &&\n m->num_known_sha1 == 0;\n const bool accepted = verified || delegated;\n char line[96];\n if (!m->rom_present) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (!readable) snprintf(line, sizeof(line), "No %s loaded", noun);\n else if (verified) snprintf(line, sizeof(line), "%s verified", noun);\n else if (delegated) snprintf(line, sizeof(line), "%s selected - runtime validation", noun);\n else snprintf(line, sizeof(line), "%s not recognized", noun);\n float w = ImGui::GetTextLineHeight() + px(6) + ImGui::CalcTextSize(line).x;\n ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availw - w) * 0.5f);\n state_mark(accepted, th);\n ImGui::SameLine(0, px(6));\n ImGui::TextColored(accepted ? col(th.good) : col(th.warn), "%s", line);''' def main() -> None: From 13a26345c0763ec841c10404558faf7213fefc80 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:29:06 +0900 Subject: [PATCH 35/44] Assert launcher starts with no absent default ROM --- .github/workflows/build-windows.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index a1f23f7..1e0d562 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -104,8 +104,14 @@ jobs: launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'game.known_sha1_hex = nullptr;' \ launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'std::filesystem::is_regular_file(default_rom, initial_rom_error)' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + grep -q 'exe.string().c_str(), initial_rom.c_str(),' \ + launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'MPH_MULTIROM_DELEGATED_VERIFY' \ ../recomp-ui/src/common/backends/imgui/launcher_imgui.cpp + grep -q 'const bool readable = m->rom_present && std::strcmp(m->rom_size, "--") != 0;' \ + ../recomp-ui/src/common/backends/imgui/launcher_imgui.cpp - name: Package Windows Nightly payload shell: msys2 {0} From e516c3be1da1509a69d894edbf241dc538462142 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:33:59 +0900 Subject: [PATCH 36/44] Add launcher startup ROM selection regression check --- launcher/recomp-ui/CMakeLists.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/launcher/recomp-ui/CMakeLists.txt b/launcher/recomp-ui/CMakeLists.txt index 7a9d904..3c3005e 100644 --- a/launcher/recomp-ui/CMakeLists.txt +++ b/launcher/recomp-ui/CMakeLists.txt @@ -72,6 +72,23 @@ set(MPH_PROFILE_LAUNCHER_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/launcher_main_profile.cpp") file(WRITE "${MPH_PROFILE_LAUNCHER_SOURCE}" "${MPH_LAUNCHER_SOURCE}") +# Configure-time regression guard for the fresh-release UX. The generated +# launcher must never pass a missing conventional ROM path into recomp-ui as if +# the user had selected it. This deliberately tests the generated TU rather +# than the untransformed upstream-tracking source. +string(FIND "${MPH_LAUNCHER_SOURCE}" + "std::filesystem::is_regular_file(default_rom, initial_rom_error)" + _mph_initial_rom_exists_guard) +if(_mph_initial_rom_exists_guard EQUAL -1) + message(FATAL_ERROR "generated launcher lost the default-ROM existence guard") +endif() +string(FIND "${MPH_LAUNCHER_SOURCE}" + "exe.string().c_str(), initial_rom.c_str()," + _mph_initial_rom_argument_guard) +if(_mph_initial_rom_argument_guard EQUAL -1) + message(FATAL_ERROR "generated launcher still passes the unconditional default ROM path") +endif() + add_executable(mph-recomp-ui "${MPH_PROFILE_LAUNCHER_SOURCE}" "${NDSRECOMP_ROOT}/recompiler/support/sha1.cpp") target_include_directories(mph-recomp-ui PRIVATE From e46806c7a61c94bb1e66ba44fdb25f0d1a763769 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:38:47 +0900 Subject: [PATCH 37/44] Document fresh-launch ROM selection assertion --- .github/workflows/build-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 1e0d562..dbd796e 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -104,6 +104,8 @@ jobs: launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'game.known_sha1_hex = nullptr;' \ launcher/recomp-ui/build-nightly/launcher_main_profile.cpp + # Fresh ROM-free extraction must start with no ROM selected. The + # conventional filename is auto-selected only when the file exists. grep -q 'std::filesystem::is_regular_file(default_rom, initial_rom_error)' \ launcher/recomp-ui/build-nightly/launcher_main_profile.cpp grep -q 'exe.string().c_str(), initial_rom.c_str(),' \ From 06baba5b6629f6390ce43401ecc3d4f00129b41d Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:41:27 +0900 Subject: [PATCH 38/44] Reuse cached MSYS2 on Windows CI --- .github/workflows/build-windows.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index dbd796e..d8a6a80 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -20,6 +20,12 @@ jobs: uses: msys2/setup-msys2@v2 with: msystem: MINGW64 + # windows-2025 already ships C:\msys64. Reuse that image installation + # instead of downloading/extracting a fresh MSYS2 release every run. + release: false + # setup-msys2 has a native package cache; keep it explicit so future + # workflow edits do not accidentally disable the fast rerun path. + cache: true update: true install: >- git From cc950242384ee2e22caa2711ade558413d4d7dea Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:42:50 +0900 Subject: [PATCH 39/44] Add persistent ccache for Windows CI --- .github/workflows/build-windows.yml | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index d8a6a80..2945d58 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -11,6 +11,14 @@ jobs: windows: name: Windows ROM-free build runs-on: windows-2025 + env: + # Keep compiler output reusable across PR iterations. ccache validates the + # compiler command/source content itself; this directory is only a cache, + # never a source of build truth. + CCACHE_DIR: ${{ github.workspace }}\.ccache + CCACHE_MAXSIZE: 750M + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache steps: - name: Check out title sources @@ -34,6 +42,31 @@ jobs: mingw-w64-x86_64-ninja mingw-w64-x86_64-python mingw-w64-x86_64-SDL2 + mingw-w64-x86_64-ccache + + - name: Restore compiler cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}\.ccache + key: >- + mph-windows-mingw64-ccache-v1-${{ hashFiles( + 'ndsrecomp.pin', + 'recomp-ui.pin', + 'launcher/recomp-ui/**', + 'tools/patch_ndsrecomp_mph_runtime.py', + 'tools/patch_ndsrecomp_rom_free_release.py', + 'tools/patch_recomp_ui_mph_multirom.py' + ) }} + restore-keys: | + mph-windows-mingw64-ccache-v1- + + - name: Configure compiler cache + shell: msys2 {0} + run: | + set -euo pipefail + ccache --version + ccache --set-config=max_size="$CCACHE_MAXSIZE" + ccache --zero-stats - name: Fetch pinned ndsrecomp and recomp-ui shell: msys2 {0} @@ -138,6 +171,14 @@ jobs: -RuntimeBinDir "$runtime_bin" test -s "release-stage/MetroidPrimeHuntersRecomp-windows-x64-v${version}.zip" + - name: Show compiler cache stats + if: always() + shell: msys2 {0} + run: | + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats || true + fi + - name: Upload Windows Nightly payload uses: actions/upload-artifact@v4 with: From 15e0173cb6eb012b0bf8c880cb4592df6b9efaa3 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:46:00 +0900 Subject: [PATCH 40/44] Avoid full MSYS2 upgrade in Windows CI --- .github/workflows/build-windows.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 2945d58..43f3d92 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -24,20 +24,21 @@ jobs: - name: Check out title sources uses: actions/checkout@v4 - - name: Install MinGW toolchain + - name: Install MinGW build dependencies uses: msys2/setup-msys2@v2 with: msystem: MINGW64 - # windows-2025 already ships C:\msys64. Reuse that image installation - # instead of downloading/extracting a fresh MSYS2 release every run. + # windows-2025 already ships C:\msys64. Reuse the runner image rather + # than downloading a second MSYS2 installation. Do not run a full + # rolling-release upgrade on every PR: the image is internally + # consistent and setup-msys2 can install the small set below directly. release: false - # setup-msys2 has a native package cache; keep it explicit so future - # workflow edits do not accidentally disable the fast rerun path. + update: false + # setup-msys2 also caches pacman payloads between runs. cache: true - update: true install: >- git - mingw-w64-x86_64-toolchain + mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-python From d617f751e4e289b408b88e73af0af4eecb2fe613 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 16:57:05 +0900 Subject: [PATCH 41/44] Trim Windows CI MinGW dependency install --- .github/workflows/build-windows.yml | 33 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 43f3d92..8c32a16 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -19,6 +19,10 @@ jobs: CCACHE_MAXSIZE: 750M CMAKE_C_COMPILER_LAUNCHER: ccache CMAKE_CXX_COMPILER_LAUNCHER: ccache + # Hosted CMake is a native Windows binary. Point it at MinGW's package + # prefix so SDL2's CMake package remains discoverable without installing + # a second copy of CMake inside MSYS2. + CMAKE_PREFIX_PATH: C:\msys64\mingw64 steps: - name: Check out title sources @@ -28,20 +32,15 @@ jobs: uses: msys2/setup-msys2@v2 with: msystem: MINGW64 - # windows-2025 already ships C:\msys64. Reuse the runner image rather - # than downloading a second MSYS2 installation. Do not run a full - # rolling-release upgrade on every PR: the image is internally - # consistent and setup-msys2 can install the small set below directly. + # windows-2025 already includes C:\msys64 plus native Git, CMake, + # Ninja and Python. Inherit the hosted PATH and install only the + # MinGW-specific compiler/runtime dependencies we actually need. + path-type: inherit release: false update: false - # setup-msys2 also caches pacman payloads between runs. cache: true install: >- - git mingw-w64-x86_64-gcc - mingw-w64-x86_64-cmake - mingw-w64-x86_64-ninja - mingw-w64-x86_64-python mingw-w64-x86_64-SDL2 mingw-w64-x86_64-ccache @@ -61,6 +60,22 @@ jobs: restore-keys: | mph-windows-mingw64-ccache-v1- + - name: Verify hosted build tools + shell: msys2 {0} + run: | + set -euo pipefail + command -v git + command -v python + command -v cmake + command -v ninja + command -v gcc + command -v ccache + git --version + python --version + cmake --version | head -n1 + ninja --version + gcc --version | head -n1 + - name: Configure compiler cache shell: msys2 {0} run: | From 35bc28c9ac32644cecd0dcfe310012e72a387639 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 17:01:18 +0900 Subject: [PATCH 42/44] Add persistent ccache to Linux CI --- .github/workflows/build-linux.yml | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 5c96e5c..2445098 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -11,6 +11,11 @@ jobs: linux: name: Linux ROM-free build runs-on: ubuntu-24.04 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 750M + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache steps: - name: Check out title sources @@ -21,10 +26,31 @@ jobs: set -euo pipefail sudo apt-get update sudo apt-get install -y --no-install-recommends \ - build-essential cmake ninja-build git curl ca-certificates \ + build-essential cmake ninja-build git curl ca-certificates ccache \ libsdl2-dev libgl1-mesa-dev libx11-dev libxext-dev libxrandr-dev \ libxcursor-dev libxi-dev libxinerama-dev libwayland-dev + - name: Restore compiler cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.ccache + key: >- + mph-linux-x86_64-ccache-v1-${{ hashFiles( + 'ndsrecomp.pin', + 'config/mph_rom_profiles.json', + 'tools/patch_ndsrecomp_mph_runtime.py', + 'tools/patch_ndsrecomp_rom_free_release.py' + ) }} + restore-keys: | + mph-linux-x86_64-ccache-v1- + + - name: Configure compiler cache + run: | + set -euo pipefail + ccache --version + ccache --set-config=max_size="$CCACHE_MAXSIZE" + ccache --zero-stats + - name: Set up Python uses: actions/setup-python@v5 with: @@ -115,6 +141,13 @@ jobs: exit 1 fi + - name: Show compiler cache stats + if: always() + run: | + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats || true + fi + - name: Upload Linux Nightly payload uses: actions/upload-artifact@v4 with: From 892e108518981732847f767f2ec3a02977d83c93 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 17:14:26 +0900 Subject: [PATCH 43/44] Add MPH runtime diagnostics and ROM-free multi-ROM gate --- tools/patch_ndsrecomp_mph_diagnostics.py | 212 +++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 tools/patch_ndsrecomp_mph_diagnostics.py diff --git a/tools/patch_ndsrecomp_mph_diagnostics.py b/tools/patch_ndsrecomp_mph_diagnostics.py new file mode 100644 index 0000000..fe22c30 --- /dev/null +++ b/tools/patch_ndsrecomp_mph_diagnostics.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Add end-user MPH startup diagnostics to the pinned ndsrecomp runner. + +This layer intentionally runs after patch_ndsrecomp_mph_runtime_core.py: + +* interactive launches write stderr to MetroidPrimeHuntersRecomp.log beside + nds_runner, so CREATE_NO_WINDOW launcher starts still leave a useful trace; +* runtime-profile selection reports gameCode/revision/executable CRC32, + authoritative-vs-header-fallback source, selected profile and host-write + safety state; +* ROM-free builds treat a successfully selected MPH runtime profile as the + compatibility authority instead of re-applying the legacy US1.0 whole-ROM + SHA-1 gate. Native/title-bank builds keep the existing exact-content policy. + +Whole-ROM SHA-1 remains useful content/cache identity; it is not used as the +base-version detector in the ROM-free Tier-3 path. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def replace_once(path: Path, old: str, new: str, marker: str) -> None: + text = path.read_text(encoding="utf-8") + if marker in text: + return + count = text.count(old) + if count != 1: + raise SystemExit( + f"{path}: expected exactly one source anchor for {marker!r}, got {count}" + ) + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--framework-root", type=Path, required=True) + parser.add_argument("--profiles", type=Path, required=False) + args = parser.parse_args() + root = args.framework_root.resolve() + + title_cpp = root / "runner" / "src" / "title_patches.cpp" + main_cpp = root / "runner" / "src" / "main.cpp" + for path in (title_cpp, main_cpp): + if not path.is_file(): + raise SystemExit(f"missing pinned ndsrecomp source: {path}") + + # The launcher intentionally creates no console window. Redirecting stderr + # from inside the runner is therefore more reliable than relying on the + # parent's inherited standard handles. Only the normal --interactive UI + # path gets the persistent file; CLI/scenario runs keep stderr untouched. + replace_once( + main_cpp, + "int main(int argc, char** argv) {\n" + " // Wiimmfi: Winsock (Windows only) MUST be initialized before ANY\n", + "int main(int argc, char** argv) {\n" + " // MPH_DIAGNOSTIC_LOG: keep the latest interactive-run log beside\n" + " // nds_runner.exe/AppImage payload so a no-console startup failure is\n" + " // still diagnosable by the player. CLI/scenario stderr is unchanged.\n" + " bool mph_interactive_log = false;\n" + " for (int i = 1; i < argc; ++i) {\n" + " if (argv[i] && std::strcmp(argv[i], \"--interactive\") == 0) {\n" + " mph_interactive_log = true;\n" + " break;\n" + " }\n" + " }\n" + " if (mph_interactive_log) {\n" + " try {\n" + " const std::filesystem::path log_path =\n" + " std::filesystem::weakly_canonical(\n" + " std::filesystem::absolute(argv[0])).parent_path() /\n" + " \"MetroidPrimeHuntersRecomp.log\";\n" + "#if defined(_WIN32)\n" + " FILE* mph_log = _wfreopen(log_path.wstring().c_str(), L\"w\", stderr);\n" + "#else\n" + " FILE* mph_log = std::freopen(log_path.string().c_str(), \"w\", stderr);\n" + "#endif\n" + " if (mph_log) {\n" + " std::setvbuf(stderr, nullptr, _IONBF, 0);\n" + " std::fprintf(stderr,\n" + " \"=== Metroid Prime Hunters Recomp diagnostic log ===\\n\"\n" + " \"[startup] interactive runner started\\n\");\n" + " }\n" + " } catch (...) {\n" + " // Logging must never make a previously launchable build fail.\n" + " }\n" + " }\n\n" + " // Wiimmfi: Winsock (Windows only) MUST be initialized before ANY\n", + "MPH_DIAGNOSTIC_LOG", + ) + + # Turn the previously ignored selector result into an explicit policy + # signal. The selector itself still performs executable/header validation. + replace_once( + main_cpp, + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()), rom_sha1.c_str(),\n" + " frontend_options.expected_rom_sha1.c_str());\n", + " const bool mph_runtime_profile_selected =\n" + " nds_title_patches_select_mph_runtime_profile(\n" + " rom.data(), static_cast(rom.size()),\n" + " rom_sha1.c_str(),\n" + " frontend_options.expected_rom_sha1.c_str());\n", + "mph_runtime_profile_selected =", + ) + + # NDS_RETAIL_BIOS_INTERPRETER is defined only by the public ROM-free build + # policy when proprietary retail BIOS banks are not linked. That is also + # the build with no MPH native title bank, so Tier-3 + the seven-version + # runtime detector is the correct authority. Optimized/native-bank builds + # deliberately retain the stricter content-SHA policy below. + replace_once( + main_cpp, + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n", + "#if defined(NDS_RETAIL_BIOS_INTERPRETER)\n" + " // MPH_ROMFREE_MULTIROM_GATE: public Nightly has no ROM-derived\n" + " // title bank, so a successfully selected runtime base profile\n" + " // is authoritative. This is what permits US1.1/EU/JP/KR and\n" + " // compatible modified ROMs to reach Tier-3 execution.\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !mph_runtime_profile_selected) {\n" + "#else\n" + " if (!frontend_options.expected_rom_sha1.empty() &&\n" + " rom_sha1 != frontend_options.expected_rom_sha1 &&\n" + " !nds_title_patches_mph_allows_rom_sha1_mismatch()) {\n" + "#endif\n", + "MPH_ROMFREE_MULTIROM_GATE", + ) + + # Give every rejection enough context to distinguish a malformed ROM from + # a supported version, a known modified executable, or a header-only mod. + replace_once( + title_cpp, + " uint32_t checksum = 0;\n" + " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum))\n" + " return false;\n", + " uint32_t checksum = 0;\n" + " if (!mph_compute_executable_checksum(rom_data, rom_size, &checksum)) {\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime detector: invalid ROM executable ranges\"\n" + " \" (size=%llu)\\n\",\n" + " static_cast(rom_size));\n" + " return false;\n" + " }\n" + " std::fprintf(stderr,\n" + " \"[mph] identity: gameCode=%.4s revision=%u \"\n" + " \"execCRC32=0x%08X\\n\",\n" + " reinterpret_cast(rom_data + 0x0Cu),\n" + " static_cast(rom_data[0x1Eu]), checksum);\n", + "[mph] identity: gameCode=", + ) + + replace_once( + title_cpp, + " if (!profile) return false;\n\n" + " // A known clean whole-ROM hash can only describe its own base profile.\n", + " if (!profile) {\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime detector: unsupported/ambiguous ROM \"\n" + " \"(execCRC32=0x%08X)\\n\", checksum);\n" + " return false;\n" + " }\n\n" + " // A known clean whole-ROM hash can only describe its own base profile.\n", + "unsupported/ambiguous ROM", + ) + + replace_once( + title_cpp, + " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" + " if (actual_clean && actual_clean != profile) return false;\n", + " const NdsMphRuntimeProfile* actual_clean = mph_find_clean_sha1(rom_sha1);\n" + " if (actual_clean && actual_clean != profile) {\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime detector: clean SHA/profile conflict \"\n" + " \"shaProfile=%s detectedProfile=%s\\n\",\n" + " actual_clean->key, profile->key);\n" + " return false;\n" + " }\n", + "clean SHA/profile conflict", + ) + + replace_once( + title_cpp, + " g_mph_allow_rom_sha1_mismatch =\n" + " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" + " expected_clean == profile && checksum == profile->base_checksum;\n" + " return true;\n", + " g_mph_allow_rom_sha1_mismatch =\n" + " std::strcmp(rom_sha1, expected_rom_sha1) != 0 &&\n" + " expected_clean == profile && checksum == profile->base_checksum;\n" + " std::fprintf(stderr,\n" + " \"[mph] runtime profile: %s detector=%s variant=%s \"\n" + " \"hostWrites=%s legacyShaReuse=%s\\n\",\n" + " profile->key,\n" + " checksum_hit ? \"executable-checksum\" : \"header-fallback\",\n" + " checksum_hit ? checksum_hit->name : \"unknown-mod\",\n" + " g_mph_host_writes_compatible ? \"enabled\" : \"disabled\",\n" + " g_mph_allow_rom_sha1_mismatch ? \"yes\" : \"no\");\n" + " return true;\n", + "[mph] runtime profile:", + ) + + print("Patched MPH runtime diagnostics and ROM-free multi-ROM content gate") + + +if __name__ == "__main__": + main() From cece5e32161b858fe40cf705ea439628d7925b31 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 17 Aug 2026 17:14:39 +0900 Subject: [PATCH 44/44] Apply MPH diagnostics after runtime patch stack --- tools/patch_ndsrecomp_mph_runtime.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/patch_ndsrecomp_mph_runtime.py b/tools/patch_ndsrecomp_mph_runtime.py index 2bc0249..4034b1d 100755 --- a/tools/patch_ndsrecomp_mph_runtime.py +++ b/tools/patch_ndsrecomp_mph_runtime.py @@ -3,8 +3,9 @@ The core detector is kept separately so upstream-facing additions can be layered without weakening its whole-ROM/content identity rules. The later stages add -the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch and make -that patch re-eligible after an in-process guest reset. +the melonPrimeDS/mphCodex profile-aware 21:9 projection/culling patch, make +that patch re-eligible after an in-process guest reset, and finally add +end-user startup diagnostics plus the ROM-free multi-ROM content-gate policy. """ from __future__ import annotations @@ -21,6 +22,7 @@ def main() -> None: here / "patch_ndsrecomp_mph_runtime_core.py", here / "patch_ndsrecomp_mph_widescreen.py", here / "patch_ndsrecomp_mph_widescreen_reset.py", + here / "patch_ndsrecomp_mph_diagnostics.py", ): subprocess.run([sys.executable, str(script), *args], check=True)