Skip to content

Sync upstream main and unify the multi-ROM runtime config - #3

Merged
Zection6V merged 26 commits into
developfrom
agent/cleanup-generic-game-config
Aug 17, 2026
Merged

Sync upstream main and unify the multi-ROM runtime config#3
Zection6V merged 26 commits into
developfrom
agent/cleanup-generic-game-config

Conversation

@Zection6V

@Zection6V Zection6V commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Syncs mstan/MetroidPrimeHuntersRecomp main through 905ffab20ecd0d9c3c1017fb757aec73c435a1ad while preserving this fork's generic seven-version runtime detector, ROM-free Nightly path, and shared frontend config.

This PR also removes the obsolete EU1.1-only game.toml fork and fixes the generic-config Adaptive Widescreen startup regression found during real Windows testing.

Generic multi-ROM frontend config

  • delete config/game-eu11.toml
  • make top-level game.toml runtime-generic by removing US1.0-only SHA/header/ARM-image metadata
  • point the existing US1.0 and EU1.1 exact-content profiles at the shared game.toml
  • keep exact whole-ROM SHA/coverage identities in mph_rom_profiles.json for bank/cache provenance only
  • keep config/mph_arm9_fmv_runtime.toml; it remains the validated US1.0 FMV AOT optimization definition

Runtime version selection remains the seven-profile executable-compatible detector. Whole-ROM SHA-1 remains exact content/bank/cache identity, not runtime base selection.

Adaptive capability regression fix

ndsrecomp intentionally rejects TOML-declared display.adaptive_capability unless [game].sha1 is exact. The generic multi-ROM config deliberately has no exact SHA, so leaving adaptive_capability = "top" caused the packaged runner to exit immediately with:

display.adaptive_capability requires an exact game.sha1

The fix does not restore a whole-ROM SHA runtime gate:

  • remove adaptive_capability from shared game.toml
  • keep adaptive_widescreen = "top" as the requested presentation mode
  • grant NDS_ADAPTIVE_TOP internally only after the MPH runtime detector has selected a profile and the executable checksum is authoritative enough to permit host writes
  • header-only fallback still disables Adaptive Widescreen/fails closed
  • add Windows and Linux startup smoke tests that execute the real ROM-free runner with the packaged generic config and fail CI on any invalid frontend config rejection

Upstream main sync

The branch contains a real two-parent merge commit with upstream 905ffab as the second parent, so future upstream ancestry remains explicit rather than being represented as unrelated file copies.

Imported upstream changes include:

  • launcher persistence for the ROM selected by the user (413c616)
  • local friend-match QA continuation (1932f6)
  • offline multiplayer/bot scenarios
  • overlay coverage/report/seeding tools
  • validated US1.0 overlay seed configs for overlays 0, 1, 2, 3, 4, 8, 9, 10 and 15
  • run_to_event exhaustion/resume fix (905ffab)

The imported overlay TOMLs are treated as US1.0 exact-content optimization assets, not generic seven-version runtime banks. The ROM-free Nightly still does not link ROM-derived MPH title banks.

ndsrecomp update

Effective framework pin:

302404ada0929528b680fa6808aad253b425c7a2

This adds per-instance slirp/local WFC peer routing. The local multi-ROM runtime patch stack remains idempotent on this revision.

Launcher compatibility adaptations

The upstream launcher source/test are synced to 905ffab, while the local generated-launcher layer keeps:

  • no whole-ROM SHA compatibility gate in recomp-ui
  • runtime validation authoritative in nds_runner
  • generic runtime-detected region presentation
  • missing conventional ROM path does not appear as an already-selected ROM
  • remembered-ROM behavior only when the remembered file still exists

Validation

Current head: cad67185955079f380ca6b4c309fffec1d0fe658

  • MPH Multi-ROM Static Checks #131: SUCCESS
    • shared generic config contract
    • runtime adaptive capability guard
    • melonPrimeDS detector/widescreen cross-check
    • ndsrecomp 302404ad... fetch
    • runner patch idempotency
    • patched runner translation-unit compile
    • seven-version runtime dispatch
  • Build #44: SUCCESS
    • ROM-free release policy: SUCCESS
    • Linux ROM-free runner + generic-config startup smoke test: SUCCESS
    • Linux AppImage/package: SUCCESS
    • Windows ROM-free runner + generic-config startup smoke test: SUCCESS
    • launcher build/tests: SUCCESS
    • Windows ZIP packaging/upload: SUCCESS

The startup smoke test explicitly requires the runner to parse game.toml successfully and progress to the expected missing-ROM error; any future invalid frontend config regression fails CI.

mstan and others added 17 commits August 16, 2026 01:27
Internal resolution and texture upscaling were reachable only through
game.toml, a CLI flag, or an environment variable. This puts them on the
Mods page next to Adaptive Widescreen and Prime Controls, where a player
will actually find them.

One feature, "HD Rendering", with two choice options:
- Internal resolution: 1x (native), 2x, 3x, 4x. Sample density of the 3D
  engine only; the 2D layers stay native, exactly as the hardware draws
  them.
- Texture upscaling: Off, 2x, 4x. Filters each decoded DS texture once as
  it enters the cache, so higher internal resolution shows detail rather
  than larger texels.

Disabled by default, and the launcher passes
--internal-resolution 1 --texture-upscale 1 while it is off, so anyone who
never opens the Mods page gets the faithful native output. Defaults are 2x
and 2x once enabled rather than the 4x maximum, since the cost scales
roughly with the square of both.

Attribution for the upscaler (Hyllian xBR-lv2, MIT) is carried on the
feature's source fields so it is visible in the UI.

Settings round-trip through mods.ini as hd_rendering, internal_resolution,
and texture_upscale, all validated on load so a hand-edited file cannot
hand the runner a scale it will refuse to start on.

Test: feature count 2 -> 3, plus coverage that HD is off by default, that
both options reject values outside their choice lists, and that all three
settings survive a save/load round trip. Index 1 is still Prime Controls,
so the existing assertions are unaffected.

Requires the ndsrecomp runner changes on branch hd-internal-resolution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exposes HD Rendering (internal resolution + texture upscaling) as an
opt-in mod on the launcher's Mods page. Disabled by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the opt-in HD Rendering mod: internal resolution up to 4x plus
texture upscaling, off by default and configured from the Mods page.

Gate status at cut time, recorded rather than glossed:
- G1 firmware suite 6/8. download_play_shutdown and pictochat_room_a fail
  on ARM7 IRQ/instruction divergence against the oracle. Both are
  PRE-EXISTING: building the merge-base 80bfd07 in a detached worktree
  reproduces byte-identical failure numbers, so they are not caused by
  this release's changes. Tracked as beads-yjp.24.
- MPH VB300/VB1800 checkpoints byte-identical to the pre-change baseline
  with HD off, including every instruction and cycle counter, so the
  default path is unchanged.
- Launcher mod-provider test passes in both the mingw and release builds.
- Texture upscaling has NOT had visual validation across DS texture
  formats, tiled surfaces, or cutout alpha edges. It is off by default and
  behind an explicit opt-in for that reason. Tracked as beads-yjp.22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nds_persist_setup received the ROM path the user picked and discarded it
with (void)rom_path, persisting only bios_path. The path handed to
recomp_launcher_run_window was always the hardcoded bundled default next
to the exe, so anyone whose dump lives elsewhere re-picked it on every
launch and saw "ROM not found" each time.

The shared launcher does not cover for this: it writes rom.cfg sidecars
into three directories but never reads them back -- grepping recomp-ui's
src/ finds only writes, and the model's only ROM seed is the initial_rom
argument. Tracked separately as beads-0fu.1; owning the path here keeps
the fix independent of which recomp-ui build is linked.

- ModState gains rom_path; settings_version 2 -> 3.
- nds_persist_setup records it, and treats an empty callback value as
  "nothing selected right now" rather than a clear -- the callback also
  fires on BIOS browse, and clearing there would reintroduce the bug.
- initial_rom is the remembered path when it names an existing file, else
  the bundled default, so a moved or deleted ROM falls back rather than
  presenting a selection that cannot launch.
- The final selection is mirrored into mod_state before the save, matching
  how bios_path and player_name are already treated as authoritative
  (persistence is best-effort UX and PLAY can be pressed without the
  callback firing).

Regression test covers round trip, empty-callback-does-not-clear, a new
pick being recorded, a path containing '=' (the parser splits on the
first '='), and version-2 forward migration keeping bios_path and
player_name.

beads-lqa.3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decides what is worth recompiling ahead of time: maps every Tier-3
address from a coverage manifest or a fuzz/benchmark trace onto the
overlay table, and reports points and hits per overlay.

Collisions are computed by SPAN, not by load address. Judging by load
address is wrong and flatters the picture -- in MPH three overlays look
unshared by load address, but all eighteen overlap another by span
(overlay 4 loads at 0x0214C860; overlay 10 loads at 0x0214C940, 0xE0
bytes inside it). That matters because "pick an overlay with no
collision to handle yet" is the pilot step docs/overlay-strategy.md
section 4 recommends, and for MPH no such overlay exists.

Addresses outside every overlay are reported rather than dropped: ITCM,
ARM7 WRAM and the immutable main image all land there, and silently
discarding them would make a route look better covered than it is.

Measured so far, ARM9:
  200M-cycle direct boot, no input   ->  9 of 18 overlays
  4000 vblanks, automatic startup    -> 11 of 18 overlays
Overlay 0 dominates both (2695 points / 701470 hits on the longer run).
The seven still dark are the 0x0219DC20 group (5,6,7,17) plus 14,15,16,
which a boot-and-idle route never reaches.

beads-yjp.31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Offline multiplayer against bots IS reachable with no peer DS, which makes
it a far better content lever than Adventure: the arena is chosen from a
menu (ARENA n/9 for Battle), bots supply the gameplay, and no navigation
skill is needed. The waiting room states it outright -- "YOU CAN WAIT FOR
MORE PLAYERS OR ADD BOTS" -- and ADD BOT fills the roster (Setya, KANBOT,
SAMBOT, SPIBOT). This is consistent with the vendored MphRead, which
carries PlayerAiData and BotLevel, and whose GameMode enum matches the
mode grid exactly (Battle/Survival/Bounty/Defender/PrimeHunter/Capture/
Nodes).

Route coordinates below are measured from captured frames, not estimated:
  main menu MULTIPLAYER   touch (160,92)   [ADVENTURE is (84,92)]
  nickname dialog         two A presses
  mode row     y~80       SINGLE-CARD x~50, MULTI-CARD x~128, WI-FI x~205
  create/join  y~80       CREATE x~50, JOIN x~128
  game mode    y~58       BATTLE x~50
  arena screen y~48       arrows left x~125 / right x~232, confirm (222,173)
  hunter                  Samus (27,132) then A
  waiting room            ADD BOT (222,173)

mph_overlay_route.py is the measurement loop: cold boot, replay a
scenario, dump the Tier-3 manifest over TCP, hand it to
overlay_coverage_report.py. It dumps via the debug command rather than
process exit because --serve never leaves its accept loop, so a
harness-killed run would lose everything it recorded.

Measured ARM9 overlay coverage by route:
  200M-cycle direct boot, no input      9 of 18   2085 entries
  4000 vblanks, automatic startup      11 of 18   3751 entries
  multiplayer setup to the bot lobby   11 of 18   5010 entries
The multiplayer route newly lights overlays 3 and 8, which every earlier
route left at zero. Still dark: 14, 15, 16 and the 0x0219DC20 group
(5, 6, 7, 17) -- almost certainly in-arena code, since the route stops in
the waiting room.

NOT DONE: the control that actually starts the match. START and A do not
launch it, and the tap at (105,133) hits a bot-level star control rather
than a ready toggle. Finding it is the next step and should unlock the
remaining overlays.

beads-yjp.31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ry points

prepare_mph.py already writes a config per overlay, but deliberately with
NO entry points -- its docstring says real ones must come from Tier-3
coverage recorded while that exact overlay body was resident. Without
them the recompiler gets a single entry_pc seed into a 276 KB image and
discovers almost nothing.

Supplying them looked blocked. MPH reuses virtual addresses: all 18
overlays overlap another by span and overlay 0 shares its range with
eight. A Tier-3 record is {pc, caller, cpu, thumb, kind, hits} with no
overlay identity, and docs/BRINGUP.md calls combining entry points across
overlay generations unsound.

The coverage manifest already resolves it. It captures the 4 KiB code
pages the guest actually executed, verbatim, at execution time. Comparing
a captured page against each overlay's decompressed ROM image identifies
which overlay was resident when that page ran -- so no runtime change and
no new field are needed.

Measured on a real capture: of 65 captured ARM9 pages, 33 fell inside
some overlay and every one attributed to exactly ONE of them -- 28 to
overlay 0, 2 to overlay 1, 3 to overlay 4, zero ambiguous. Overlapping
spans do not produce collisions in practice.

Seeding overlay 0 from three captured routes: 29 pages proved resident,
8 pages inside its span were rejected as a different generation, 239
entry points dropped as unproven, 7162 dropped as kind=root (scheduler
resume points make poor seeds), leaving 212 seeds. The recompiler then
discovered 2313 functions (arm=2313 thumb=0, undefined=0) and emitted
2314 across 8 shards with the identity sha1 verified.

config/mph_arm9_ov000.toml is that seeded config, committed so the result
is reproducible.

beads-yjp.31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seeded from five captured routes (boot, menus, multiplayer setup,
adventure-with-walking). Only overlays 0 and 1 have any proven-resident
pages; overlay 4 yields 2 seeds, and the other fifteen yield none because
no route I can currently drive ever loads them.

Overlay 0: 30 proven pages -> 222 seeds -> 2357 functions.
Overlay 1:  2 proven pages ->  23 seeds ->  389 functions.
Both identity-verified with zero undefined instructions.

A correction to an earlier reading: the span-based overlay report
over-attributes badly. It credited overlays 9 and 12 with 601 points and
152628 hits, but neither has a single proven-resident page -- those were
overlay 0's bytes, counted again through overlapping spans. Only the
page-proof attribution is trustworthy.

MEASURED, and the headline is smaller than the instruction counts suggest.
Boot-to-menu (run to vblank9 12000, serve mode, same route both sides):
    wall clock        100.2s -> 97.2s   -3.0%
    tier3_insns9   18,106,104 -> 10,621,633   -41%
    tier3_insns7  127,071,515 -> 127,071,515    0%
On the longer menu route, tier3_insns9 falls 112.1M -> 48.5M (-56.7%) but
wall clock does not move, because that route's duration is set by fixed
frame advances and harness round-trips rather than by emulation speed.

ARM7 IS NOW THE BOTTLENECK: 127M interpreted ARM7 instructions against
10.6M ARM9, i.e. 92% of all remaining interpreter work. More ARM9
overlays cannot pay much more than this -- overlay 1 on top of overlay 0
was worth 0.09%.

Two theories checked and DISPROVED, do not re-try: dispatch-cache
thrashing from the FMV bank and the overlay bank aliasing the same
addresses (cache_slow_lookup went DOWN, 1,047,293 -> 802,213), and a
provenance-granularity mismatch in the validation gate (the banks plainly
do validate and dispatch).

beads-yjp.31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
THE START PROCEDURE, from the owner playing it by hand -- this unblocks the
automated in-game loop. Use MULTI-CARD, never SINGLE-CARD: single-card does
not allow bots at all, its slots read AVAILABLE and accept only peer
consoles, which is the most misleading part of the flow. Then CREATE GAME ->
BATTLE -> confirm arena -> pick hunter -> ADD + until the roster is full ->
TAP THE GREEN CHECKBOX BESIDE EACH BOT -> a START BATTLE control appears,
tap it.

Those checkboxes are per-slot CONFIRM controls. I had read them as a
difficulty widget because confirming a bot is what reveals its star rating.
And the match is started by TAPPING an on-screen control, not by the START or
A key -- earlier automated attempts did correctly confirm all three bots and
then pressed the START key, which does nothing.

COVERAGE FROM REAL PLAY DWARFS SYNTHETIC ROUTES, and overturns an earlier
call. My routes captured 71-95 code pages; one adventure session captured
1724 and a multiplayer session 4172. Seedable overlays went from 2 to 9:

    id   seeds     hits      id   seeds     hits
     0     926  7032208       8      29    64756
     2     586  5718909       3      18    64692
     9     447  4421576      10      21    47568
    15      89   220117       4      90    34252
     1      68   138232

Overlay 2 is now near the top and my synthetic routes never loaded it once;
overlay 9 is multiplayer arena code. "Diminishing returns at two overlays"
was an artifact of weak test routes, not reality. So was the claim that ARM7
dominates and further ARM9 work could not pay: that came from a menus-only
headless route. In real play the split runs ARM9 44-55%.

All nine compile identity-verified, 12151 functions total. The finder reports
undefined=14 (ov000) and undefined=2 (ov002), but ZERO runtime_unimplemented_op
call sites are emitted in any generated file -- those counts come from the
scan pass over regions that never became functions, so no compiled function
can halt on one. Checked rather than assumed, since that trap calls nds_halt.

Boot-to-menu measurement is a weak proxy here (it is dominated by overlay 0):
103.0s -> 100.6s, tier3_insns9 18.1M -> 10.2M, only 4% better than the
two-overlay build. Overlays 2, 9 and 15 should pay in gameplay instead, which
needs a real session to measure.

beads-yjp.31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reaching the title screen needs more than the 50M default max_rounds, so
the initial advance silently under-ran and every subsequent tap landed at
the wrong moment. Verified this was harness-side, not a build regression:
all three runner builds stop at exactly vblank 6461.

beads-yjp.31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Zection6V
Zection6V marked this pull request as ready for review August 17, 2026 09:24
@Zection6V Zection6V changed the title Unify MPH runtime frontend config across ROM versions Sync upstream main and unify the multi-ROM runtime config Aug 17, 2026
@Zection6V
Zection6V merged commit 10aaf9a into develop Aug 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants