Sync upstream main and unify the multi-ROM runtime config - #3
Merged
Conversation
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
marked this pull request as ready for review
August 17, 2026 09:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Syncs
mstan/MetroidPrimeHuntersRecompmain through905ffab20ecd0d9c3c1017fb757aec73c435a1adwhile 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.tomlfork and fixes the generic-config Adaptive Widescreen startup regression found during real Windows testing.Generic multi-ROM frontend config
config/game-eu11.tomlgame.tomlruntime-generic by removing US1.0-only SHA/header/ARM-image metadatagame.tomlmph_rom_profiles.jsonfor bank/cache provenance onlyconfig/mph_arm9_fmv_runtime.toml; it remains the validated US1.0 FMV AOT optimization definitionRuntime 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_capabilityunless[game].sha1is exact. The generic multi-ROM config deliberately has no exact SHA, so leavingadaptive_capability = "top"caused the packaged runner to exit immediately with:display.adaptive_capability requires an exact game.sha1The fix does not restore a whole-ROM SHA runtime gate:
adaptive_capabilityfrom sharedgame.tomladaptive_widescreen = "top"as the requested presentation modeNDS_ADAPTIVE_TOPinternally only after the MPH runtime detector has selected a profile and the executable checksum is authoritative enough to permit host writesinvalid frontend configrejectionUpstream main sync
The branch contains a real two-parent merge commit with upstream
905ffabas the second parent, so future upstream ancestry remains explicit rather than being represented as unrelated file copies.Imported upstream changes include:
413c616)1932f6)run_to_eventexhaustion/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:
302404ada0929528b680fa6808aad253b425c7a2This 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:nds_runnerValidation
Current head:
cad67185955079f380ca6b4c309fffec1d0fe658302404ad...fetchThe startup smoke test explicitly requires the runner to parse
game.tomlsuccessfully and progress to the expected missing-ROM error; any futureinvalid frontend configregression fails CI.