Files
seedds 3a993fa3fe Subsystem references and a symptom index: the failure catalogue of docs/agents/03-bringup.md and the per-title material of docs/agents/04-graphics.md (section 1's container list, section 6 and its "Seen in <game>" subsections) move into docs/reference/, one heading per mechanism: rewriter.md, memory.md, threads.md, services.md, movies.md, audio.md, frame-rate.md, nvn-objects.md, rendering.md (with the Vulkan and OpenGL layers), shaders.md and game-state.md, indexed by docs/reference/README.md. Each entry gives the symptom, the cause, the fix (file and function), the games it was found on (linked to their notes in dm/games/<id>/README.md) and any check, caveat or later change; one mechanism met on several games is one entry. docs/agents/symptoms.md lists every entry's symptom (an assertion, a [RED] line, an abort or crash site, a visible result) with a link to it, 350 lines, generated from the Symptom bullets. 03 keeps the loop and reading the log, and its Failure catalogue section now points to the references, the index and where a new finding goes; 04 keeps sections 1-5 and 7 (the container list is shaders.md's Containers by engine), and section 6 says where each kind of finding is. Facts in the old sections about one game only were checked against that game's note, where they already were. Statements the code no longer matches are marked as history beside the current value (movie output slots: MD_NOUT 64, MD_NOUT_AAC 32, MD_NOUT_VIDEO 16; APP_POOL is the profile key app_pool; Engage's layout is 5 / 4.375 GB; inline uniform updates are deferred by default; scaled copies are drawn), and Tears of the Kingdom joins the node-area entry's build list (issue #70). The game notes, docs/agents/README.md, 02 and 05 link to the exact entries, and so do the comments in nvn_mtl.m (nvnTextureCompare) and hle_audio.c (the RemoteIO buffer) and the provisional-profile note in profile_guess.py. 03 goes from 29,600 to 470 words, 04 from 34,400 to 3,500. Checked: every backticked span, module address, issue / PR number and 16-digit hash of AGENTS.md, development.md, dm/games/README.md and docs/agents/01-05 at 6a06877 (4,600 items) still appears in the repository's Markdown (the same five file patterns as before appear as the full names they stood for), every relative link and heading anchor in the repository's Markdown resolves, and the tool tests pass (the four that need capstone or lz4 in the T7 venv)
2026-10-25 12:31:58 +08:00

9.8 KiB
Raw Permalink Blame History

Pikmin 4 (0100B7C00933A000)

Update 1.1.1 (v262144) over the base game, SDK 10.4.8 (its own sdk build). Nintendo EPD / Eighting, Unreal Engine 4.26.2 (Carrot4; SDK MW+EpicGames+UnrealEngine-4_26_2 in main), NVN, PhysX 3.4, Wwise (its own AudioOut sink, Opus through the host's software decoder), VP9 movies through Unreal's Switch media player and the SDK's movie module. Modules main, subsdk0 (the movie module: the title and the opening are VP9 movies) and sdk. Plays on the iPhone (IPA 10.8 GB); 30 fps, as on the Switch.

Status: on the Mac, the boot, the title (a VP9 movie behind the logo), the main menu, a new game (the auto-save notice, players, creature activity level), the opening (the shadow-play story, the cutscenes with Olimar and Moss) and the Olimar prologue in play, Continue from a save; 30 fps as on the Switch, Wwise sound, about 2.6–2.8 GB resident and 1.7 GB of Metal allocations, no red line. The title is up about 25 s after launch. The prewarm list covers that route (806 lines).

What it needs

Setting Where Why
Engine region default cut 62 GB → 4064 MB: main+0x353f56c and main+0x353f578 (orr #0xf80000000 → mov #0xfe000000) patches.py, main 060f827b… this build reserves the region only when -SwitchVirtualAddressSpaceSize= is absent, so the profile has no game_args; see Memory
FMallocBinned3 constructor cut: main+0x353f788 bl 0x35ba610 → nop patches.py its pools are cut out of the engine region; the host runs Unreal's heap (hle_uemalloc.c layout UE4_27)
Observation gate OBS_UE_MALLOC at main+0x353f78c (orig ldp x29, x30, [sp, #0x10]) patches.py puts the host allocator's vtable into the new object in x19
Node area cut 1 GB → 62 MB (sdk+0x340ea0 / +0x340f3c) and 32 MB alignment for regions of 1 GB and more (sdk+0x34109c) dm/tools/sdk_patches.py, sdk build 30a1e87b… the SDK 10.4.1 build's AddressRegionManager at the same offsets, with the same cuts; the region starts 62 MB in and a 4 GB region needs no 1 GB alignment slack
Engage's 5 GB block: mem_reserve 0x140000000, mem_alias 0x118000000, mem_stack 0x10000000, app_pool 0xCD500000 game.tsv 256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region, as FINAL FANTASY RESONANCE's
sys_resource 0x5000000 game.tsv the NPDM declares 80 MB of system resource
pad_layout full game.tsv a 3D game with a free camera on the right stick; it plays with the controller, so it stays docked (1920x1080)

Frame rate

  • Current: 30 fps, as on the Switch. The game steps a fixed frame time (bUseFixedFrameRate=True in DefaultEngine.ini, Unreal's default FixedFrameRate of 30) and asks for present interval 2 (rhi.SyncInterval=2 in SwitchDeviceProfiles.ini, r.VSync=1), so a faster present rate would speed the game up, not smooth it. No fps_target.

Memory

  • nninitStartup needs virtual address memory, sets a 6 MB heap for the C library's malloc and reserves one address region for the engine, 62 GB.
  • Resolved: nn::diag::Abort from main+0x352fbec (Unreal's fatal error) with -SwitchVirtualAddressSpaceSize= given. This nninitStartup (main+0x353f1xx) reads the argument into main+0x93c2130 (MB up to 4095) but reserves the region only when the argument is absent: ldr x8,[x22,#0x130]; cbnz x8 jumps past the default 62 GB and its nn::os::AllocateAddressRegion(main+0x93c2128, 62 GB) (main+0x353f564). With the argument the base stayed 0, and the first commit, AllocateMemoryPages(0, 2 MB) while the RHI starts (main+0x353ff8c, result 0xe03), failed. The default is cut to 4064 MB instead (two orr #0xf80000000 → mov #0xfe000000) and the profile has no game_args.
  • Current: the allocator is the host's. It is made as in UE 4.27 (DRAGON QUEST VII Reimagined, 0100A9D01C446000), with the same 21-slot FMalloc vtable: GCreateMalloc (main+0x353f770) mallocs 0x1c38 bytes on the first allocation and constructs them (main+0x353f788 → main+0x35ba610, the one store of the vtable main+0x8da4758 outside its two destructors; found from the FMallocBinned3 error strings through the relocated slots). The constructor call is cut, and the gate on the ldp x29, x30 after it puts the host allocator's vtable into the object in x19 (hle_uemalloc.c). The Unreal heap lives on a malloc zone outside the guest block.
  • The SDK 10.4.8 build's AddressRegionManager is the 10.4.1 build's (Advance Wars 1+2, The Great Ace Attorney Chronicles) at the same offsets and takes the same three cuts.
  • With Engage's 5 GB block, from the boot into the Olimar prologue: up to +1.5 GB of the alias region with 1.0 GB mapped, an Unreal heap of 1 GB, 1.7 GB of Metal allocations (about 2.8 GB resident).

Files and shaders

  • IoStore containers Carrot4/Content/Paks/Carrot4-Switch.utoc / .ucas (7.5 GB, TOC version 2; .ucas is the profile's romfs_marker) hold the cooked packages; everything else is in Carrot4-Switch.pak (3.5 GB, version 11 with the magic 0xcc from the end as DRAGON QUEST VII Reimagined's, 10,046 files, Zlib, index not encrypted): the configs, 1,294 Wwise .wem files, the WebM movies (stored) and the two shader code archives (stored).
  • The update (v262144, 0.9 GB) is a delta over the base RomFS.
  • Shaders: 15,727 distinct (16,190 in the two archives), every one an LZ4 block (uepak.py lz4_shaders), as DRAGON QUEST VII Reimagined's; all build. Every vertex shader is made to link with any pixel shader (shader_split.py separable_pairs).

Services

  • Resolved: the boot sat at a black loading screen for good, CPU nearly idle, every thread waiting, about 1 boot in 6 (3 in 4 with four Mac hosts at once). The title movie was never opened and the save files never probed. The game's boot state machine (main+0x1f2f0e0, state at this+0xd4) stayed in state 7, which polls the status word (+0x14) of the game's BCAT news runnable ("OlimarMemo") for 0, 4, 5, 6 or 7; it read 1. The runnable's constructor (main+0x203f3b0) clears the status, starts FBCATThread and only then stores 1; the thread's Run (main+0x203f510) stores 2, calls nn::nifm::SubmitNetworkRequest, sleeps while IsNetworkRequestOnHold, and stores 6 when IsNetworkAvailable is false. On a Switch the request stays on hold while the nifm service evaluates it, so the constructor's 1 always comes first; the host answered "not on hold" at once, and a thread that ran before the constructor's store finished at 6, which the 1 then overwrote. The host now keeps a submitted request on hold for 50 ms, then settles it as unavailable, as Horizon's asynchronous request does (hle_net.c): 12 of 12 boots with four hosts at once reached the title.
  • How it was found: the hung boots' logs diverged from good ones at the save probe (DM_FS=1), the thread dumps showed nothing but idle waits, and DM_PROBE on the state machine (found by probing the save probe's call stack and dumping the state each tick), then peek lines in a hung run's DM_CTL file, followed the pointers to the status word. A probe on the hot path slowed the boot enough to hide the race (8 of 8 good).

Rewriter

  • Resolved: AssertionError: 0x… in site_window on an adrp inside an exclusive window (main+0x43da14c: ldaxr x9,[x9]; cmp x9,x25; b.ne out; adrp x10 + add x10 (the same address again, for the store); mov x18,x26; stlxr w9,x8,[x10], then loads and cbnz w9 back). A window that uses x18 is copied into the patch area, where the adrp would build another page; it is now re-encoded for the copy's address with the same target page (window_adrp).
  • Resolved: observation gate neither at a function entry, after a bl nor before a call on the epilogue ldp x29, x30, [sp, #0x10] right after the cut allocator constructor call (main+0x353f78c; the rule checks the image after the cuts, so the bl before it is a nop by then). scratch_dead now accepts a ret ahead as it does a bl.
  • The spin-wait search takes 80 → 83 loops in main since the store rule.

Input

  • The 3D pad layout (the camera is on the right stick); it plays with the controller, docked.

Mac routes

  • The work folder's run/ctlrun.sh <run> starts a run with DM_CTL, run/see.sh <shot> <wait> <presses…> presses and takes a screenshot, run/mash.sh presses A in rounds.
  • The title is up about 25 s into a boot: A (main menu), A (Story), A (the save file), A (Load). From a fresh save, A (Story) shows the auto-save notice; then the number of players and the creature activity level start the opening.

Found on this game

General host changes first needed here; each applies to every game:

Open

  • The comment on this sdk build in dm/tools/sdk_patches.py says the engine region comes from -SwitchVirtualAddressSpaceSize "the profile's game_args", but the profile has no game_args: the region's size comes from the cut in patches.py.