diff --git a/AGENTS.md b/AGENTS.md index 1617178..e884bf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Notes for agents -- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Sixty games are playable on the iPhone: Super Mario Bros. Wonder (update 1.2.1, profile `010015100B514000`), Fire Emblem Engage (update 2.0.0, `0100A6301214E000`), Metroid Dread (update 2.1.0, `010093801237C000`), 13 Sentinels: Aegis Rim (update 1.10, `01003FC01670C000`), Super Mario 3D World + Bowser's Fury (update 1.2.2, `010028600EBDA000`), The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, `01007EF00011E000`), The Legend of Zelda: Link's Awakening (update 1.1.0, SDK 8, `01006BB00C6F0000`) Super Mario Odyssey (update 1.4.1, SDK 7, `0100000000010000`), Hades (update 1.0.38233, SDK 9, `0100535012974000`), Hyrule Warriors: Age of Calamity (update 1.3.0, SDK 9.6, `01002B00111A2000`), Fire Emblem: Three Houses (update 1.2.0, SDK 6.5, `010055D009F78000`), Super Mario Galaxy (update 1.3.1, SDK 18, `010099C022B96000`) The Legend of Zelda: Tears of the Kingdom (update 1.4.3, SDK 20, `0100F2C0115B6000`) Monster Hunter Rise (update 16.0.2, SDK 11.4.3, `0100B04011742000`), Ace Attorney Investigations Collection (update 1.0.0.2, SDK 17.5.3, `010005501E68C000`), Hades II (base game, SDK 19.3.9, `0100A00019DE0000`), Unicorn Overlord (update 1.05, SDK 16.2.4, `010069401ADB8000`), Rhythm Heaven Groove (base game 1.0.0, SDK 21.4.0, `0100D9F01D474000`), Octopath Traveler 0 (update 1.0.5, SDK 19.3.13, `01005270232F2000`), Pokémon Legends: Z-A (update 2.0.2, SDK 19.3.11, `0100F43008C44000`), Cuphead (update 1.3.7, SDK 5.5.0, `0100A5C00D162000`), Super Mario Galaxy 2 (update 1.4.0, SDK 18.3.5, `0100FD8022DAA000`), FINAL FANTASY TACTICS - The Ivalice Chronicles (update 1.4.0, SDK 19.3.9, `010038B015560000`), Absolum (update 1.1.1, SDK 18.3.2, `0100A83022A22000`), Captain Toad: Treasure Tracker (update 1.4.2, SDK 8.2.0, `01009BF0072D4000`) and Brain Memory Training Academy (base game 1.0.0, SDK 16.2.4, `0100F5D01DA3A000`), The Great Ace Attorney Chronicles (base game 1.0.0, SDK 10.4.1, `010036E00FB20000`), STEINS;GATE RE:BOOT (update 1.0.3, SDK 20.5.17, `01008BC024FDE000`), Big Brain Academy: Brain vs. Brain (update 1.2.0, SDK 11.4.3, `0100620012D6E000`), Patrick's Parabox (update 1.0.1, SDK 13.3.7, `0100E44016C22000`), Celeste (update 1.4.1.1, SDK 15.3.2, `01002B30028F6000`), DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (update 1.0.4, SDK 7.3.2, `01006C300E9F0000`), Monster Train 2 (update 1.1.1, SDK 18.3.5, `010051701FB46000`), Raging Loop (base game 1.0.0, SDK 7.3.2, `010020400E1C2000`), DARK SOULS: REMASTERED (update 1.0.3, SDK 4.5.0, `01004AB00A260000`), Paper Mario: The Thousand-Year Door (update 1.0.1, SDK 15.3.2, `0100ECD018EBE000`), Hollow Knight: Silksong (update 1.0.30000, SDK 20.5.8, `010013C00E930000`), Catherine: Full Body (update 1.0.1a, SDK 8.2.1, `0100BF00112C0000`), Digimon Story Cyber Sleuth: Complete Edition (update 1.0.3, SDK 6.4.0, `010014E00DB56000`), Animal Crossing: New Horizons (update 3.0.3, SDK 20.5.4, `01006F8002326000`), DRAGON QUEST VII Reimagined (update 1.2.0, SDK 19.3.1, `0100A9D01C446000`), FINAL FANTASY RESONANCE (update 1.0.1, SDK 20.5.17, `0100606026F12000`), Luigi's Mansion 3 (update 1.4.0, SDK 7.3.2, `0100DCA0064A6000`), AI: THE SOMNIUM FILES (update 1.0.1, SDK 6.4.2, `010089B00D09C000`), ANIMAL WELL (update 1.0.10, SDK 20.5.7, `010020D01AD24000`), AI: THE SOMNIUM FILES - nirvanA Initiative (update 1.0.2, SDK 13.3.5, `0100713016CCC000`), Mario Kart 8 Deluxe (update 4.0.0, SDK 4.4.0, `0100152000022000`), Balatro (update 1.1.3, SDK 18.3.2, `0100CD801CE5E000`), ROMANCE OF THE THREE KINGDOMS 8 REMAKE (update 1.1.3, SDK 17.5.3, `0100C2A01A730000`), Taiko Risshiden V DX (update 1.1.1, SDK 12.3.3, `0100346017304000`), Super Smash Bros. Ultimate (update 13.0.5, SDK 8.2.1, `01006A800016E000`), Bayonetta 2 (update 1.2, SDK 3.6.0, `01007960049A0000`), Bayonetta 3 (update 1.2.0, SDK 13.3.2, `01004A4010FEA000`), Advance Wars 1+2: Re-Boot Camp (base game 1.0.0, SDK 10.4.1, `0100300012F2A000`) Pokémon Sword (update 1.3.2, SDK 7.3.2, `0100ABF008968000`), Pikmin 4 (update 1.1.1, SDK 10.4.8, `0100B7C00933A000`), Stardew Valley (update 1.6.15.13, SDK 20.5.27, `0100E65002BB8000`), BALL x PIT (update 1.293, SDK 20.5.19, `010086A022444000`), Mario vs. Donkey Kong (update 1.0.1, SDK 15.3.0, `0100B99019412000`) and Bayonetta (update 1.1, SDK 3.6.0, `010076F0049A2000`). Hades renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`; `main` imports Vulkan, not NVN). Age of Calamity is Koei Tecmo's KTGL engine: files in RDB archives, shaders as GLSLC outputs (`shader_split.py` `idrk_shaders`). Three Houses is KTGL too; 9 of its shaders exist only as compiles of the game's own GLSLC at run time, captured once on the Mac (its `main` holds GLSL sources and imports `glslcCompile`). Breath of the Wild and Tears of the Kingdom run at 60 fps through a frame step the host decides in whole vsyncs (`FRAME_STEP` in their profiles' `patches.py`, `nvn_fstep.h`, profile keys `fps_target` and `frame_step`); Tears of the Kingdom has a 5 GB guest layout, and its `sdk` build (Breath of the Wild's) has the SDK's address-region node area cut (`sdk_patches.py`): without it the resource heaps ran out of alias region at the first descent from the Great Sky Island and the game played its memory-shortage Blood Moon, which put Link back at the start (its log shows `MSF`). Monster Hunter Rise is Capcom's RE Engine: files in KPKA PAK archives (`shader_split.py` `kpka_jobs`, shaders in SDF files), the base game an XCI, its GPU heap a 64 GB sparse virtual pool (`hle_nvn.c` `vpool_*`, a 4 KB page table the replay reads textures through); it runs at 60 fps (30 on the Switch). Super Mario Galaxy is the Wii game on NERD's Hagi layer (PowerPC code recompiled into `main`), renders with Vulkan, and generates its shaders at run time, which the NSP script captures on the Mac (no SPIR-V file in its RomFS); its pointer is the touch screen in handheld mode (profile key `op_mode`). Ace Attorney Investigations Collection is Unity 2022 IL2CPP: its shaders keep their parameters as blob entries of their own (`shader_split.py` `unity_code_blobs`), and its C# file API goes through the SDK's C library, whose path checks the host answers (`hle_fs.c` `IsPathMounted` and the length limits). Hades II, unlike Hades, renders with NVN; its shaders bind textures at fixed slots up to 34 (slot 34's handle lies in the driver's image table, `dmsh_build.py`), and the two long lines through Melinoë while the staff's Omega attack charges are the game's own aim lines, not a rendering bug. Unicorn Overlord is Vanillaware's engine like 13 Sentinels (shaders compiled into `main`); both need the profile key `alloc_big_align`, which aligns large allocations to 64 KB so the ring heaps the engine cuts from a memory pool fit. Rhythm Heaven Groove is Nintendo's own 2D engine on SDK 21, the newest SDK so far (its DSP command list has no magic, `hle_audren.c`); it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), which skips its TV latency calibration and keeps its 1920x1080 draw size. Octopath Traveler 0 is Unreal Engine 5: everything in one pak with an AES-encrypted index whose key is found in `main` (`dm/gfx/tools/uepak.py`), its shaders Oodle-compressed in the pak's shader code archives and decoded once on the Mac by the game's own Oodle (any game with Unreal paks, `DM_OODLE_JOB`), its allocator replaced by the host's because Unreal's reserves 37 GB of address space (a cut in its `patches.py`, `hle_uemalloc.c`), its address region capped by the game's own argument (profile key `game_args`), and the particle shaders that pick a texture slot at run time bound by slot (profile key `tex_dyn_slots`); it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Pokémon Legends: Z-A is Game Freak's Trinity engine: everything in the packs of `arc/data.trpfs` (FlatBuffers, every file Oodle-compressed), which the shader split decodes with the game's own decoder in the Mac host's tool mode (`shader_split.py` `trinity_shaders`, `hle_oodle.c` `DM_OODLE_SERVE`); its 12 GB StandardAllocator is cut to 4 GB to fit a 5 GB guest layout, and the host refuses maps past the pool as Horizon does (profile key `mem_map_limit`); its characters are skinned with transform feedback (`dmsh_build.py` `vs_tf`, `nvn_mtl.m` `tf_draw`), and its occlusion queries, which decide how characters are animated, need the draw's base instance in the driver segment (c0 + 0x644, `encode_args`). Cuphead is Unity 2017.4 IL2CPP on SDK 5.5.0: Unity 2017's serialized and Shader formats are read by `unityfs.py` / `shader_split.py`, its rewritten modules (80 MB) need the profile key `mem_used` above the default 64 MB, or its `SetHeapSize` fails, and its NVN pool allocator breaks on a zero-size block, so the host never reports a ZCull storage size of 0 (`hle_nvn.c` `zcull_size`). Super Mario Galaxy 2 is Galaxy's Hagi layer with Galaxy's `sdk` build; its movie player reserves 384 MB of virtual address memory, which fits only because a rewriter cut on that `sdk` build shrinks the SDK's address-region node area to 62 MB (`sdk_patches.py`; the profile's alias region is 512 MB). FINAL FANTASY TACTICS - The Ivalice Chronicles is Square Enix's engine on NVN: its shaders are GLSLC outputs in loose technique files, which `shader_split.py` reads in any RomFS file no other reader takes; its UI textures are written by the CPU into uncached pools (profile keys `tex_flush_upload`, `tex_dirty_upload`); and two job-pool workers share one auto-clear event, so a build cut makes a worker woken in another's place pass the wake on (its `patches.py`). Absolum is FNA (XNA 4 in C#) on .NET NativeAOT with SDL3 and FNA3D's NVN driver: its `main` starts its text with 1.7 MB of data the rewriter leaves alone (`nso_code.py`), its shaders are GLSLC outputs in `shaders.nx.bin`, its picture is flipped by a viewport swizzle the host applies, and everything lives in virtual address memory: malloc's 40 GB StandardAllocator is cut to 2.5 GB and the SDK's node area and large-region alignment are cut (its `patches.py` and `sdk_patches.py`), and the .NET GC's region range is 1.5 GB (profile key `game_env`), to fit a 5 GB guest layout. Captain Toad: Treasure Tracker is Nintendo EPD Tokyo's engine (the `al` library over sead / agl, as 3D World and Odyssey) and needed no host change: it plays handheld because its pointer is the touch screen there (the Joy-Con gyro docked; profile key `op_mode`), and agl's post-effect program that samples its own attachment reads a snapshot (profile key `feedback_snap`). Brain Memory Training Academy is Unity 2022.3 IL2CPP with no bundles: its serialized files carry no type trees and nothing lends it a `Shader` tree, so `unityfs.py` has Unity 2022.3's `Shader` layout written out (`KNOWN_TREES`), and like Ace Attorney Investigations Collection it needs the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). The Great Ace Attorney Chronicles is Capcom's MT Framework: files in ARC archives of zlib entries, its 460 shaders in one shader package inside one of them (`shader_split.py` `mtspk_shaders`); it copies its textures into a CPU-cached pool after creating them and never flushes (its `main` has no cache maintenance at all), so it needs `tex_flush_upload` and `tex_dirty_upload` like FINAL FANTASY TACTICS. STEINS;GATE RE:BOOT is M2's ADV engine (E-mote characters, PSB archives, H.264 movies through the SDK's movie library): it needed `nn::audio::SetAudioOutVolume` (`hle_audio.c`), and because it decodes every picture into one reused region of a pool that lives for good, the storage-sharing texture table now drops an entry whose storage another texture takes over (`nvn_mtl.m` `alias_take_over`); it draws 1280x720 in both modes by its own choice. Big Brain Academy: Brain vs. Brain is NintendoWare Bezel Engine 1.56: nearly all of RomFS is in Bezel Engine archives of zstd entries, its shaders in their `.bfsha` entries (`shader_split.py` `bea_members`); its `nninitStartup` sets a heap or a 60 GB virtual address region by `IsVirtualAddressMemoryEnabled`, so the host reports the NPDM's system resource (profile key `sys_resource`, 0 here); its activity previews loop the movie decoder with Flush and Start from four threads (`hle_movie.c`); and it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), because only handheld does it read the touch screen. Patrick's Parabox is Unity 2020.3 IL2CPP with no bundles: `unityfs.py` has Unity 2020.3's `Shader` layout written out (`KNOWN_TREES`), it needs the SDK's address-region node area cut on its `sdk` build like Brain Memory Training Academy (`sdk_patches.py`), and Unity's player hands the shared fonts straight to FreeType, so a game that imports no `ScalableFontEngineHelper::Decode` gets the TTF itself (`hle_sdk16.c` `font_skip`). Celeste is MonoGame for the Switch, its C# compiled to C++ by Sickhead's BRUTE: its shaders are MonoGame effects (MGFX) in its XNB files and in `main` (`shader_split.py` `mgfx_effect`), and it needed no host change. DRAGON QUEST XI S is Unreal Engine 4.18: legacy paks with LZ5 blocks, decoded on the Mac by the game's own LZ5 (`uepak.py`, `hle_oodle.c`), its shaders inline in the cooked packages (`shader_split.py` `ue_legacy_jobs`), and the slice-writing geometry shader of its colour-grading LUT folded into the vertex shader (`dmsh_build.py` `gs_slice`, `nvn_mtl.m` `prog_fold_gs`); Square Enix's sound driver never waits on the renderer event, so audio catch-up frames wait for queue space (`hle_audren.c`); its movie player plays only once every track holds a decoded output, so audio movie decoders keep 32 (`hle_movie.c`); and its Unreal region is capped at 4064 MB (profile key `game_args`, MB up to 4095) with the node area and large-region alignment cut on its SDK 7.3.2 build (`sdk_patches.py`) for a 5 GB guest layout; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `nvn.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Monster Train 2 is Unity 2022.3 IL2CPP on Super Mario Galaxy's SDK 18.3.5 `sdk` build: Unity creates a thread only when `nn::os::GetFreeThreadCount` (`svcGetInfo` type 24, `hle_svc.c`) is at least 1; its MP4 intro needs a movie audio decoder with more output slots than a video one, because Unity's player holds about half a second of audio before it plays (`hle_movie.c` `MD_NOUT`); and its post-processing compute shaders (the colour grading LUT among them) are Unity `ComputeShader` objects, which `shader_split.py` reads (`unity_compute`) with Unity 2022.3's layout written out in `unityfs.py`. It reads no touch, so it stays docked with the 3D pad layout, and it runs at 60 fps (30 on the Switch) with the profile key `present_interval display`, as its animations follow the time that passed. Raging Loop is Unity 2018.3 IL2CPP with no bundles on DRAGON QUEST XI S's `sdk` and `subsdk0` builds: `unityfs.py` has Unity 2018.3's `Shader` layout written out (`KNOWN_TREES`, an older form than the 2020 and 2022 layouts, with the parameters in each subprogram), and it needed no host change; it reads no touch, so it stays docked with the 2D pad layout. DARK SOULS: REMASTERED is FromSoftware's engine on SDK 4.5.0, the oldest SDK so far: its shaders are GLSLC outputs in BND3 archives inside the dvdbnd archives (`shader_split.py` `bdt_shaders`), and SDK 4's GLSLC outputs are read through their section headers (`glslc_section`); it keeps 12 sampler pools with different samplers under the same IDs, so handles a shader reads from a uniform buffer take the base of the pool bound at replay (`nvn_mtl.m` `ubo_tex_id` / `ubo_samp_id`); and it runs at 60 fps (30 on the Switch) with two rewriter cuts, its frame limiter's 30 and its frame update's fixed 1/30 s step (its `patches.py`). Paper Mario: The Thousand-Year Door is Intelligent Systems' engine on NintendoWare, on Celeste's SDK 15.3.2 `sdk` build: nearly every RomFS file is zstd (`.zst`, which `shader_split.py` decompresses by magic); its AngelScript call bridges leave sp misaligned, which a general rewriter rule rounds up (`site_sp_align`); it runs at 60 fps (30 on the Switch) by the host's frame step (`FRAME_STEP`, with the floor of its vsync step cut from 2 to 1); and its three job workers poll the job list with no SVC, which held it at 24 fps on an iPhone until the general spin wait rule (`patch_nso.py` "Spin waits", `hle_thread.c` `dm_spin_wait`). Hollow Knight: Silksong is Unity 6 (6000.0.50f1) IL2CPP, the newest Unity so far: its bundles carry their own type trees, so `unityfs.py` needed no layout; Unity 6 polls `nn::hid::GetDebugPadStates` every frame (`hle_hid.c`), and it takes all its memory itself through `nn::os::AllocateAddressRegion` / `AllocateMemoryPages` with no heap, so it runs in the 5 GB guest layout that `profile_guess.py` now gives every game with an NPDM system resource, with the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). Catherine: Full Body is Atlus's GFD engine with CRI middleware (CPK archives, ADX2 sound, Sofdec2 movies through `movie::Decoder`, whose player reads `nv12-uv-stride`, `hle_movie.c`): its 3,128 shaders are GLSLC outputs in `data/shaders_binary`, `main` and two compiles of its own GLSLC at boot, which the host lets run by reporting at least that GLSLC's GPU code version (`hle_glslc.c`); that GLSLC writes every vertex shader as a VertexA part (shader type 0, the position) and a VertexB part, which `dmsh_build.py` merges as Ryujinx does (`vertex_ab`; the screen was black without it), and because the game reuses a copy's staging memory a frame after recording it, the host commits a segment at the first pass boundary after a buffer → texture copy (`nvn_mtl.m` `g_cut_copy`). Digimon Story Cyber Sleuth: Complete Edition is Media.Vision's MVGL engine and renders with NVIDIA's desktop OpenGL 4.5 through EGL, not NVN, which the host implements on Metal (`dm/gfx/gl/hle_gl.m`: the 13 EGL imports and the 79 GL functions its code calls of the 2,956 its glad loader fetches, counted with `dm/tools/loader_census.py`; `profile_guess.py` gives `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`); its shaders are GLSL ES 3.20 source in its MVGL archives (`dm/gfx/tools/mvgl.py`: MDB1 with doboz entries) and in `main`, which `gl_shaders.py` translates offline with glslang and SPIRV-Cross (packing needs `brew install glslang`) and the host finds by the hash of its text. Animal Crossing: New Horizons is Nintendo EPD's engine (sead / agl / gsys) on SDK 20.5.4: its `main` links its own HIPC client for `olsc:u` (cloud backup settings), which the host answers with sessions of its own (`hle_sf.c`), its island lives in device save data (`MountDeviceSaveData`, `hle_fs.c`), it skips clears with conditional rendering (profile key `render_cond`), its 63 MB `main` needed halfword jump tables padded only to their largest entry (`patch_nso.py` `half_count`), and its `SaveProc` builds a new island in grid walks that yield once per cell, so a yield whose loop moved its callee-saved registers since the last one now returns at once instead of backing off, for every game (`hle_thread.c` `thr_yield`; the island showed after 77 s of black screen, now about 10 s). DRAGON QUEST VII Reimagined is Unreal Engine 4.27: IoStore containers beside one pak, its shaders LZ4 blocks in the pak's shader code archives, which `uepak.py` decodes itself (`lz4_shaders`), and its allocator replaced as Octopath Traveler 0's (a cut and an observation gate in its `patches.py`, `hle_uemalloc.c` layout `UE4_27`) in a 2 GB engine region (profile key `game_args`); its audio renderer runs at 32 kHz, resampled by the host on the CPU path (`hle_audren.c`); and Unreal's volumetric lightmap merge sizes its indirection texture with a query on the image it writes, so a texture slot that shares its c2 word with an image slot reads the bound image (`nvn_mtl.m` `tex_slot_h`) and the translator types such a query from the image (`dmsh_xlate` `ImageTypesOf`): without them every character in the opening cutscene glowed white; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`), the same two limits a 60 fps mod for the base game lifts with code patches (`UEngine::SetMaxFPS` returning at once, the name `r.VSync` broken). FINAL FANTASY RESONANCE is Unreal Engine 5.6.1, the newest Unreal so far: its shader libraries are IoStore chunks in its container, not in its pak, two layers of Oodle that `uepak.py` reads through the game's own decoder in the Mac host with no boot (`Toc`, `io_archive`, `OodleHost`); its 94 MB `main` is 95% without unwind entries, so the rewriter analyses long functions in pieces (`patch_nso.py` `pieces`; one 82 MB function took 100 GB of memory) and treats code before the first unwind entry as a function; its allocator is replaced as Octopath Traveler 0's (UE 5.6's 32-slot FMalloc, `hle_uemalloc.c` layout `UE5_6`), and its engine region is 4064 MB (profile key `game_args`), because the engine's own page allocator never gives address space back (2 GB aborted in the first battle); 2,077 particle shaders need `tex_dyn_slots`; it runs at 60 fps (30 on the Switch) by the host's `rhi.SyncInterval` write (`hle_fpslock.c`, profile key `fps_target`). Luigi's Mansion 3 is Next Level Games' own engine on NVN: its RomFS files are in the engine's `.dict` / `.data` archives, and update 1.4.0 ships `.patch` files beside the base ones, which `shader_split.py` applies as the game does (`nlg_shaders`); the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from `GetCommandMemoryUsed`, so single texture binds are recorded in the hardware's layout (`OP_BIND_TEX1`, every game; without it the title showed a stretched icon and a new game a white screen); its G-buffer is two integer targets, which its pixel shaders write through a second, integer-output entry point (`dmsh_build.py` `fs_uint`, every game); and it runs at 60 fps (30 on the Switch) because the host writes its frame limiter's 33.333 ms, a constant in read-only data, as 1000 / `fps_target` less half a millisecond (`hle_fpslock.c`, with `present_interval display`). Its tessellated draws are skipped (Metal 4 has no tessellation stage) and its geometry shaders do not build; neither was drawn in play so far. AI: THE SOMNIUM FILES is Unity 2017.4 IL2CPP (Cuphead's Unity layout) with FMOD: its 168 movies are H.264 MP4s that Unity's player copies into NV12 texture pairs, and its Spike Chunsoft logo movie held the game before the title until a released output slot also went on with the H.264 decoder's drain at the end of a stream (`hle_movie.c`: Unity holds all 16 video outputs by then); it binds its vertex and fragment programs apart, and the precompile prediction reused the ring slot of the combined program the replay had saved, so a draw ran with another pair's shaders until each restore of the replay state took the combination again from the per-stage bindings (`nvn_mtl.m` `prog_resolve`, every game); it plays docked with the 3D pad layout (the investigation's view is on the right stick). ANIMAL WELL embeds all its assets and 46 BNSH shaders in `main`; its base and update Program NCAs have no data section, which `nx_extract` represents as an empty RomFS directory. It needed no host change; its profile uses the 2D pad layout. AI: THE SOMNIUM FILES - nirvanA Initiative is Unity 2020.3 IL2CPP (Patrick's Parabox's Unity) with Addressables bundles and FMOD, and needed no host change: Unity 2020.3 keys a compute shader kernel's variants by keyword set (`shader_split.py` `unity_compute`, `variantMap`); its `nninitStartup` hands pool − used to StandardAllocator as virtual address memory and Unity reserves its own regions beside it, so it has the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`) and a 1 GB pool, which the NSP script's heap check now sets for such a game (`profile_guess.py` `heap_signals`: the alias region nearly full within seconds and no heap); it plays docked with the 3D pad layout (the investigation's view is on the right stick). Mario Kart 8 Deluxe is Nintendo's sead / agl / gsys engine on SDK 4.4.0, the oldest SDK so far: it lists the six default Miis at boot and builds Mii heads from the console's MiiModel system data, so the host answers the Mii database itself (`hle_mii.c`: the six default Miis, none of the console's own) and mounts a stand-in of that archive with every part empty (`dm_mii_standin`, `hle_fs.c` `MountSystemData`; the firmware is not the host's to ship, so Mii heads draw nothing); its DLC licence is decrypted in a 0x3130-byte heap that also holds the mount cache, now sized as Horizon's RomFS working memory (`romfs_work_size`); it mounts device and account save data, which are now two host saves for a game whose `main` imports both (at boot it deletes `CommonSave:/userdata.dat`); SDK 4's DeviceSink is command 0xf of 0x130 bytes, and the audio renderer's update lock is read off each SDK's own `AudioRenderSystem::Update` (`hle_audren.c` `update_lock`: it was SDK 16's for every SDK before 20); it evaluates its conditional rendering (profile key `render_cond`) and runs at 60 fps. Balatro is LÖVE 11.4 (love-nx: Lua source in the RomFS, LuaJIT's interpreter, SDL) on NVIDIA's desktop OpenGL through EGL, like Digimon: `main` imports `vkGetInstanceProcAddr` only for SDL's Vulkan loader, which `profile_guess.py` leaves out of its Vulkan count; LÖVE takes its core-profile paths when the GL layer says core (`GL_CONTEXT_PROFILE_MASK`): persistently mapped vertex streams fenced per frame, attribute bindings and current values, uniform queries, texture storage, 3D and array textures, swizzles and mipmaps, about 150 GL functions in all (`hle_gl.m`, counted with `loader_census.py`, which now follows LÖVE's glad through registers; `DM_GL_STUBS=1` names a missing one at its first call); its 35 shaders are GLSL that LÖVE puts together at run time, captured by the NSP script on the Mac (profile `gl_capture.tsv`, `capture_shaders`), with `bool` uniforms keeping their type (`gl_shaders.py`); it opens the audio renderer without an event, which started no render thread before (`hle_audren.c` `ar_open`: it was silent); and it plays by touch in handheld mode (profile keys `op_mode handheld`, `pad_layout touch`: the iPhone gamepad's Tap layout sends every touch to the game). Any Lua error makes it quit, and love-nx then crashes in its video worker's teardown (`main+0x339c00`): look for the Lua error first. ROMANCE OF THE THREE KINGDOMS 8 REMAKE is Koei Tecmo's KTGL on SDK 17.5.3, with a newer RDB than Age of Calamity's: `.fdata` files (a `PDRK` header, then `IDRK` entries) whose entries are LZ4 or zlib in 16 KB chunks of a new form (`shader_split.py` `idrk_data`: flag 0x400000, LZ4 with 0x100000 too), 59,363 shaders in all; its update is a multi-program patch that carries a whole second program (`…0001`, started with `nn::oe::ExecuteProgram(1)`) beside the game's, so `nx_extract` takes program index 0's NCAs (it took the second program and failed); SDK 17.5.3's `OpenAudioRenderer` with an event calls the form without one through the PLT, which the host's hook now passes straight to the SDK (`hle_audren.c` `t_in_open`); it plays at 30 fps with a controller (it reads a USB mouse, not the touch screen). Taiko Risshiden V DX is KTGL on SDK 12.3.3, mostly 2D, in Japanese and Chinese only (the NSP script packs Japanese): its 212 shaders are GLSLC outputs in `main` and one G2S container (`eff/KTGLEffect.g1s`), read as before; it needed `nn::oe::SetCpuOverclockEnabled` (`hle_oe.c`: its first boot crashed in apm); it plays by touch in handheld mode (profile keys `op_mode handheld`, `pad_layout touch`), taking its taps, drags and flicks only from `nn::hid::GetGestureStates`, which the host now recognizes from the touch points for every game (`hle_touch.c` `gesture_sample`, yuzu's rules plus an 8 px slop; unit test `t_gesture`; the `DM_CTL` line `drag` tests pans on the Mac); and its frame end slept in 2 ms rounds until 1/60 s had passed, which ended at 18.2 ms (55 fps), so a build cut skips that wait and the presents hold 60 (its `patches.py`). Super Smash Bros. Ultimate is Bandai Namco / Sora's engine on Catherine: Full Body's SDK 8.2.1 `sdk` build: everything is in one `data.arc` of Hash40-named zstd files (`dm/gfx/tools/smash_arc.py`; the split reads its current files, 35,012 shaders); its fighters' code is 98 NROs in that archive, loaded and unloaded per battle with `nn::ro::LoadModule`, which the NSP script collects by the SHA-256s its NRRs list and rewrites (`dm/tools/nro_collect.py`), the host loads by the image's hash and binds `main`'s `lua2cpp` imports to (`hle_ro.c`, `loader.c` "NRO modules"), and the iOS app carries packed into three sections of the executable (`gen_embed.py`, `loader.c` `nro_pieces`); its engine reserves 62 GB of address space, cut to 4 GB in its `patches.py` with the SDK's node area and 1 GB alignment cut on that `sdk` build (`sdk_patches.py`) for the 5 GB guest layout; and it fetches `nvnSetExternalDebuggerEnabledNVX`, a no-op. Bayonetta 3 is PlatinumGames' engine: everything in PKZ archives of DAT archives whose entries are Oodle Kraken, which the shader split decodes with the game's own streaming decoder (`main` has no `OodleLZ_Decompress`, so `hle_oodle.c` drives `OodleLZDecoder_*`), its shaders the engine's own shader objects (`shader_split.py` `pg_shader`); the engine refuses shader code newer than the driver reports, so the NVN layer's newest GPU code version follows the SDK (`hle_nvn.c`, 1.16 from SDK 13). Advance Wars 1+2: Re-Boot Camp is Unity 2020.2 IL2CPP on The Great Ace Attorney Chronicles' SDK 10.4.1 `sdk` build, which takes the node-area and 1 GB alignment cuts (`sdk_patches.py`) for its virtual address memory, with a 2 GB pool (the heap check's 1 GB ran its malloc dry at the first loading screen, and Unity's GPU pool allocation then spun for good); its Bink 2 movies draw with 32 shaders whose control sections and code areas lie loose in `main`, paired by data records or by the code that builds their addresses (`shader_split.py` `raw_shaders`; every movie drew white without them). Pokémon Sword is Game Freak's engine before Trinity, on Super Mario Odyssey's SDK 7.3.2 `sdk` and movie builds: its shaders are BNSH entries of GFPAK archives (LZ4, `shader_split.py` `gfpak_members`) and Silicon Studio's YEBIS post effects, a `.tfxo` file in a zlib stream in `main` (`tfxo_streams`); its G-buffer writes float and integer targets at once, which takes a pixel shader entry point that returns only the integer outputs as integers (`dmsh_build.py` `fs_uint`, `main0_m`; `nvn_mtl.m` `pso_mixed_uint`), its cube views of fewer than 6 layers count layers in cubes (`tex_by_id`), and it writes beside its packaged textures in host pages they share, which no longer re-uploads them when the texture's own bytes are unchanged (`nvn_mtl.m` `pkg_edges_clean`; frames went from 33 to 75 ms without it); it asks for a Poké Ball Plus (Palma, `hle_hid.c`) and the Let's Go play statistics (`hle_offline.c`), both answered as absent. Pikmin 4 is Unreal Engine 4.26.2 with IoStore (its shaders LZ4 in the pak's shader code archives, as DRAGON QUEST VII Reimagined's) and Wwise: its allocator is replaced as DRAGON QUEST VII Reimagined's (`hle_uemalloc.c` layout `UE4_27`), but this build reserves its engine region only when `-SwitchVirtualAddressSpaceSize=` is absent, so a cut in its `patches.py` lowers the 62 GB default to 4064 MB instead of `game_args`; the rewriter now re-encodes an `adrp` inside an exclusive window (`window_adrp`) and accepts a gate before a `ret` (`scratch_dead`); and a network request now stays on hold for 50 ms, as Horizon's asynchronous one (`hle_net.c`): answered at once, its BCAT thread could finish before its creator stored "started", and about one boot in six waited at the black loading screen for good. Stardew Valley is MonoGame on Sickhead's BRUTE like Celeste (C# compiled to C++, a Boehm collector) with XACT sound, on SDK 20.5.27: 3,535 of its 3,549 XNB files are compressed (LZ4, 8 LZX), which `shader_split.py` decompresses (`xnb_decompress`) to reach its one content effect beside the 64 shaders in `main`; it draws nearly everything with SpriteBatch (a prewarm list of 2 shaders and 4 pipelines), reads only the pad, and its character creation names the farmer and the farm through the software keyboard, whose Mac text box releases the buttons that opened it (`front_mac.m`, found on this game); its profile uses the 3D pad layout (the D-pad in menus, the right stick for the cursor), and it needed no other host change. BALL x PIT is Unity 6 (6000.0.62f1) IL2CPP with FMOD and Rewired, as Hollow Knight: Silksong: it needed no host change, only the SDK's address-region node area cut on its SDK 20.5.19 `sdk` build (`sdk_patches.py`); its loose serialized files have no type trees and borrow the `Shader` tree its bundles carry, and it draws 1280x720 into 1920x1080 window textures with a window crop (a Mac shot shows the crop only with `DM_NVN_SHOT_OUT`); it plays docked with the 3D pad layout (the right stick aims). Mario vs. Donkey Kong is Nintendo's sead / agl / gsys engine on SDK 15.3.0, its base game a cartridge dump (.xci), its cutscenes MP4 files (H.264 and AAC) through the SDK's movie library and an AudioOut of its own: its movie player keeps the outputs it holds in a ring of 32 per track with no bounds check, so AAC movie decoders offer 32 outputs (Vorbis keeps 64), and it sizes its sound buffers by `GetOutputBufferSize` and appends each whole, so an AAC decoder reports one frame (`hle_movie.c`: with 64 outputs the opening movie overran the heap, with the 128 KB answer it froze); it skips clears under render conditions (profile key `render_cond`), sets the advanced blend mode SRC_OVER, which the replay now blends with the Porter-Duff factors it amounts to (`nvn_mtl.m` `adv_blend`), and plays docked with the 2D pad layout. Bayonetta 2 is PlatinumGames' engine in its Wii U form on SDK 3.6.0, the oldest SDK so far, which needed no host change of its own: its files are DAT archives in CRI CPK archives (CRILAYLA-compressed entries, `dm/gfx/tools/cpk.py`, which also reads the masked @UTF tables of Unicorn Overlord's CPKs), its 1,856 shaders BNSH files of one stage each in the shader DATs, which the split decodes from every CPK entry (`shader_split.py` `cpk_jobs`); a cleared attachment larger than the render pass is now cleared in a pass of its own, as NVN clears the whole target (`nvn_mtl.m` `rp_begin_ex_`; the opening narration drew in a corner of a black screen), and `Downsample` of a single-sample texture is a filtered scaling copy that may convert the format (its bloom chain, `copy_t2t`); it plays docked with the 3D pad layout (the camera is on the right stick). Bayonetta is the same engine on Bayonetta 2's `sdk` and `subsdk0` builds and needed no host change: nine CPK archives, its 458 shaders in `shader.dat` of `data00.cpk`, CRI ADX2 sound, the 3D pad layout; its prologue tutorial waits on a Witch Time page until a dodge lands right before the strike (the work folder's `run/witch.py`). [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and per-game notes. +- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Sixty games are playable on the iPhone: Super Mario Bros. Wonder (update 1.2.1, profile `010015100B514000`), Fire Emblem Engage (update 2.0.0, `0100A6301214E000`), Metroid Dread (update 2.1.0, `010093801237C000`), 13 Sentinels: Aegis Rim (update 1.10, `01003FC01670C000`), Super Mario 3D World + Bowser's Fury (update 1.2.2, `010028600EBDA000`), The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, `01007EF00011E000`), The Legend of Zelda: Link's Awakening (update 1.1.0, SDK 8, `01006BB00C6F0000`) Super Mario Odyssey (update 1.4.1, SDK 7, `0100000000010000`), Hades (update 1.0.38233, SDK 9, `0100535012974000`), Hyrule Warriors: Age of Calamity (update 1.3.0, SDK 9.6, `01002B00111A2000`), Fire Emblem: Three Houses (update 1.2.0, SDK 6.5, `010055D009F78000`), Super Mario Galaxy (update 1.3.1, SDK 18, `010099C022B96000`) The Legend of Zelda: Tears of the Kingdom (update 1.4.3, SDK 20, `0100F2C0115B6000`) Monster Hunter Rise (update 16.0.2, SDK 11.4.3, `0100B04011742000`), Ace Attorney Investigations Collection (update 1.0.0.2, SDK 17.5.3, `010005501E68C000`), Hades II (base game, SDK 19.3.9, `0100A00019DE0000`), Unicorn Overlord (update 1.05, SDK 16.2.4, `010069401ADB8000`), Rhythm Heaven Groove (base game 1.0.0, SDK 21.4.0, `0100D9F01D474000`), Octopath Traveler 0 (update 1.0.5, SDK 19.3.13, `01005270232F2000`), Pokémon Legends: Z-A (update 2.0.2, SDK 19.3.11, `0100F43008C44000`), Cuphead (update 1.3.7, SDK 5.5.0, `0100A5C00D162000`), Super Mario Galaxy 2 (update 1.4.0, SDK 18.3.5, `0100FD8022DAA000`), FINAL FANTASY TACTICS - The Ivalice Chronicles (update 1.4.0, SDK 19.3.9, `010038B015560000`), Absolum (update 1.1.1, SDK 18.3.2, `0100A83022A22000`), Captain Toad: Treasure Tracker (update 1.4.2, SDK 8.2.0, `01009BF0072D4000`) and Brain Memory Training Academy (base game 1.0.0, SDK 16.2.4, `0100F5D01DA3A000`), The Great Ace Attorney Chronicles (base game 1.0.0, SDK 10.4.1, `010036E00FB20000`), STEINS;GATE RE:BOOT (update 1.0.3, SDK 20.5.17, `01008BC024FDE000`), Big Brain Academy: Brain vs. Brain (update 1.2.0, SDK 11.4.3, `0100620012D6E000`), Patrick's Parabox (update 1.0.1, SDK 13.3.7, `0100E44016C22000`), Celeste (update 1.4.1.1, SDK 15.3.2, `01002B30028F6000`), DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (update 1.0.4, SDK 7.3.2, `01006C300E9F0000`), Monster Train 2 (update 1.1.1, SDK 18.3.5, `010051701FB46000`), Raging Loop (base game 1.0.0, SDK 7.3.2, `010020400E1C2000`), DARK SOULS: REMASTERED (update 1.0.3, SDK 4.5.0, `01004AB00A260000`), Paper Mario: The Thousand-Year Door (update 1.0.1, SDK 15.3.2, `0100ECD018EBE000`), Hollow Knight: Silksong (update 1.0.30000, SDK 20.5.8, `010013C00E930000`), Catherine: Full Body (update 1.0.1a, SDK 8.2.1, `0100BF00112C0000`), Digimon Story Cyber Sleuth: Complete Edition (update 1.0.3, SDK 6.4.0, `010014E00DB56000`), Animal Crossing: New Horizons (update 3.0.3, SDK 20.5.4, `01006F8002326000`), DRAGON QUEST VII Reimagined (update 1.2.0, SDK 19.3.1, `0100A9D01C446000`), FINAL FANTASY RESONANCE (update 1.0.1, SDK 20.5.17, `0100606026F12000`), Luigi's Mansion 3 (update 1.4.0, SDK 7.3.2, `0100DCA0064A6000`), AI: THE SOMNIUM FILES (update 1.0.1, SDK 6.4.2, `010089B00D09C000`), ANIMAL WELL (update 1.0.10, SDK 20.5.7, `010020D01AD24000`), AI: THE SOMNIUM FILES - nirvanA Initiative (update 1.0.2, SDK 13.3.5, `0100713016CCC000`), Mario Kart 8 Deluxe (update 4.0.0, SDK 4.4.0, `0100152000022000`), Balatro (update 1.1.3, SDK 18.3.2, `0100CD801CE5E000`), ROMANCE OF THE THREE KINGDOMS 8 REMAKE (update 1.1.3, SDK 17.5.3, `0100C2A01A730000`), Taiko Risshiden V DX (update 1.1.1, SDK 12.3.3, `0100346017304000`), Super Smash Bros. Ultimate (update 13.0.5, SDK 8.2.1, `01006A800016E000`), Bayonetta 2 (update 1.2, SDK 3.6.0, `01007960049A0000`), Bayonetta 3 (update 1.2.0, SDK 13.3.2, `01004A4010FEA000`), Advance Wars 1+2: Re-Boot Camp (base game 1.0.0, SDK 10.4.1, `0100300012F2A000`) Pokémon Sword (update 1.3.2, SDK 7.3.2, `0100ABF008968000`), Pikmin 4 (update 1.1.1, SDK 10.4.8, `0100B7C00933A000`), Stardew Valley (update 1.6.15.13, SDK 20.5.27, `0100E65002BB8000`), BALL x PIT (update 1.293, SDK 20.5.19, `010086A022444000`), Mario vs. Donkey Kong (update 1.0.1, SDK 15.3.0, `0100B99019412000`) and Bayonetta (update 1.1, SDK 3.6.0, `010076F0049A2000`). Hades renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`; `main` imports Vulkan, not NVN). Age of Calamity is Koei Tecmo's KTGL engine: files in RDB archives, shaders as GLSLC outputs (`shader_split.py` `idrk_shaders`). Three Houses is KTGL too; 9 of its shaders exist only as compiles of the game's own GLSLC at run time, captured once on the Mac (its `main` holds GLSL sources and imports `glslcCompile`). Breath of the Wild and Tears of the Kingdom run at 60 fps through a frame step the host decides in whole vsyncs (`FRAME_STEP` in their profiles' `patches.py`, `nvn_fstep.h`, profile keys `fps_target` and `frame_step`); Tears of the Kingdom has a 5 GB guest layout, and its `sdk` build (Breath of the Wild's) has the SDK's address-region node area cut (`sdk_patches.py`): without it the resource heaps ran out of alias region at the first descent from the Great Sky Island and the game played its memory-shortage Blood Moon, which put Link back at the start (its log shows `MSF`). Monster Hunter Rise is Capcom's RE Engine: files in KPKA PAK archives (`shader_split.py` `kpka_jobs`, shaders in SDF files), the base game an XCI, its GPU heap a 64 GB sparse virtual pool (`hle_nvn.c` `vpool_*`, a 4 KB page table the replay reads textures through); it runs at 60 fps (30 on the Switch). Super Mario Galaxy is the Wii game on NERD's Hagi layer (PowerPC code recompiled into `main`), renders with Vulkan, and generates its shaders at run time, which the NSP script captures on the Mac (no SPIR-V file in its RomFS); its pointer is the touch screen in handheld mode (profile key `op_mode`). Ace Attorney Investigations Collection is Unity 2022 IL2CPP: its shaders keep their parameters as blob entries of their own (`shader_split.py` `unity_code_blobs`), and its C# file API goes through the SDK's C library, whose path checks the host answers (`hle_fs.c` `IsPathMounted` and the length limits). Hades II, unlike Hades, renders with NVN; its shaders bind textures at fixed slots up to 34 (slot 34's handle lies in the driver's image table, `dmsh_build.py`), and the two long lines through Melinoë while the staff's Omega attack charges are the game's own aim lines, not a rendering bug. Unicorn Overlord is Vanillaware's engine like 13 Sentinels (shaders compiled into `main`); both need the profile key `alloc_big_align`, which aligns large allocations to 64 KB so the ring heaps the engine cuts from a memory pool fit. Rhythm Heaven Groove is Nintendo's own 2D engine on SDK 21, the newest SDK so far (its DSP command list has no magic, `hle_audren.c`); it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), which skips its TV latency calibration and keeps its 1920x1080 draw size. Octopath Traveler 0 is Unreal Engine 5: everything in one pak with an AES-encrypted index whose key is found in `main` (`dm/gfx/tools/uepak.py`), its shaders Oodle-compressed in the pak's shader code archives and decoded once on the Mac by the game's own Oodle (any game with Unreal paks, `DM_OODLE_JOB`), its allocator replaced by the host's because Unreal's reserves 37 GB of address space (a cut in its `patches.py`, `hle_uemalloc.c`), its address region capped by the game's own argument (profile key `game_args`), and the particle shaders that pick a texture slot at run time bound by slot (profile key `tex_dyn_slots`); it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Pokémon Legends: Z-A is Game Freak's Trinity engine: everything in the packs of `arc/data.trpfs` (FlatBuffers, every file Oodle-compressed), which the shader split decodes with the game's own decoder in the Mac host's tool mode (`shader_split.py` `trinity_shaders`, `hle_oodle.c` `DM_OODLE_SERVE`); its 12 GB StandardAllocator is cut to 4 GB to fit a 5 GB guest layout, and the host refuses maps past the pool as Horizon does (profile key `mem_map_limit`); its characters are skinned with transform feedback (`dmsh_build.py` `vs_tf`, `nvn_mtl.m` `tf_draw`), and its occlusion queries, which decide how characters are animated, need the draw's base instance in the driver segment (c0 + 0x644, `encode_args`). Cuphead is Unity 2017.4 IL2CPP on SDK 5.5.0: Unity 2017's serialized and Shader formats are read by `unityfs.py` / `shader_split.py`, its rewritten modules (80 MB) need the profile key `mem_used` above the default 64 MB, or its `SetHeapSize` fails, and its NVN pool allocator breaks on a zero-size block, so the host never reports a ZCull storage size of 0 (`hle_nvn.c` `zcull_size`). Super Mario Galaxy 2 is Galaxy's Hagi layer with Galaxy's `sdk` build; its movie player reserves 384 MB of virtual address memory, which fits only because a rewriter cut on that `sdk` build shrinks the SDK's address-region node area to 62 MB (`sdk_patches.py`; the profile's alias region is 512 MB). FINAL FANTASY TACTICS - The Ivalice Chronicles is Square Enix's engine on NVN: its shaders are GLSLC outputs in loose technique files, which `shader_split.py` reads in any RomFS file no other reader takes; its UI textures are written by the CPU into uncached pools (profile keys `tex_flush_upload`, `tex_dirty_upload`); and two job-pool workers share one auto-clear event, so a build cut makes a worker woken in another's place pass the wake on (its `patches.py`). Absolum is FNA (XNA 4 in C#) on .NET NativeAOT with SDL3 and FNA3D's NVN driver: its `main` starts its text with 1.7 MB of data the rewriter leaves alone (`nso_code.py`), its shaders are GLSLC outputs in `shaders.nx.bin`, its picture is flipped by a viewport swizzle the host applies, and everything lives in virtual address memory: malloc's 40 GB StandardAllocator is cut to 2.5 GB and the SDK's node area and large-region alignment are cut (its `patches.py` and `sdk_patches.py`), and the .NET GC's region range is 1.5 GB (profile key `game_env`), to fit a 5 GB guest layout. Captain Toad: Treasure Tracker is Nintendo EPD Tokyo's engine (the `al` library over sead / agl, as 3D World and Odyssey) and needed no host change: it plays handheld because its pointer is the touch screen there (the Joy-Con gyro docked; profile key `op_mode`), and agl's post-effect program that samples its own attachment reads a snapshot (profile key `feedback_snap`). Brain Memory Training Academy is Unity 2022.3 IL2CPP with no bundles: its serialized files carry no type trees and nothing lends it a `Shader` tree, so `unityfs.py` has Unity 2022.3's `Shader` layout written out (`KNOWN_TREES`), and like Ace Attorney Investigations Collection it needs the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). The Great Ace Attorney Chronicles is Capcom's MT Framework: files in ARC archives of zlib entries, its 460 shaders in one shader package inside one of them (`shader_split.py` `mtspk_shaders`); it copies its textures into a CPU-cached pool after creating them and never flushes (its `main` has no cache maintenance at all), so it needs `tex_flush_upload` and `tex_dirty_upload` like FINAL FANTASY TACTICS. STEINS;GATE RE:BOOT is M2's ADV engine (E-mote characters, PSB archives, H.264 movies through the SDK's movie library): it needed `nn::audio::SetAudioOutVolume` (`hle_audio.c`), and because it decodes every picture into one reused region of a pool that lives for good, the storage-sharing texture table now drops an entry whose storage another texture takes over (`nvn_mtl.m` `alias_take_over`); it draws 1280x720 in both modes by its own choice. Big Brain Academy: Brain vs. Brain is NintendoWare Bezel Engine 1.56: nearly all of RomFS is in Bezel Engine archives of zstd entries, its shaders in their `.bfsha` entries (`shader_split.py` `bea_members`); its `nninitStartup` sets a heap or a 60 GB virtual address region by `IsVirtualAddressMemoryEnabled`, so the host reports the NPDM's system resource (profile key `sys_resource`, 0 here); its activity previews loop the movie decoder with Flush and Start from four threads (`hle_movie.c`); and it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), because only handheld does it read the touch screen. Patrick's Parabox is Unity 2020.3 IL2CPP with no bundles: `unityfs.py` has Unity 2020.3's `Shader` layout written out (`KNOWN_TREES`), it needs the SDK's address-region node area cut on its `sdk` build like Brain Memory Training Academy (`sdk_patches.py`), and Unity's player hands the shared fonts straight to FreeType, so a game that imports no `ScalableFontEngineHelper::Decode` gets the TTF itself (`hle_sdk16.c` `font_skip`). Celeste is MonoGame for the Switch, its C# compiled to C++ by Sickhead's BRUTE: its shaders are MonoGame effects (MGFX) in its XNB files and in `main` (`shader_split.py` `mgfx_effect`), and it needed no host change. DRAGON QUEST XI S is Unreal Engine 4.18: legacy paks with LZ5 blocks, decoded on the Mac by the game's own LZ5 (`uepak.py`, `hle_oodle.c`), its shaders inline in the cooked packages (`shader_split.py` `ue_legacy_jobs`), and the slice-writing geometry shader of its colour-grading LUT folded into the vertex shader (`dmsh_build.py` `gs_slice`, `nvn_mtl.m` `prog_fold_gs`); Square Enix's sound driver never waits on the renderer event, so audio catch-up frames wait for queue space (`hle_audren.c`); its movie player plays only once every track holds a decoded output, so audio movie decoders keep 32 (`hle_movie.c`); and its Unreal region is capped at 4064 MB (profile key `game_args`, MB up to 4095) with the node area and large-region alignment cut on its SDK 7.3.2 build (`sdk_patches.py`) for a 5 GB guest layout; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `nvn.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Monster Train 2 is Unity 2022.3 IL2CPP on Super Mario Galaxy's SDK 18.3.5 `sdk` build: Unity creates a thread only when `nn::os::GetFreeThreadCount` (`svcGetInfo` type 24, `hle_svc.c`) is at least 1; its MP4 intro needs a movie audio decoder with more output slots than a video one, because Unity's player holds about half a second of audio before it plays (`hle_movie.c` `MD_NOUT`); and its post-processing compute shaders (the colour grading LUT among them) are Unity `ComputeShader` objects, which `shader_split.py` reads (`unity_compute`) with Unity 2022.3's layout written out in `unityfs.py`. It reads no touch, so it stays docked with the 3D pad layout, and it runs at 60 fps (30 on the Switch) with the profile key `present_interval display`, as its animations follow the time that passed. Raging Loop is Unity 2018.3 IL2CPP with no bundles on DRAGON QUEST XI S's `sdk` and `subsdk0` builds: `unityfs.py` has Unity 2018.3's `Shader` layout written out (`KNOWN_TREES`, an older form than the 2020 and 2022 layouts, with the parameters in each subprogram), and it needed no host change; it reads no touch, so it stays docked with the 2D pad layout. DARK SOULS: REMASTERED is FromSoftware's engine on SDK 4.5.0, the oldest SDK so far: its shaders are GLSLC outputs in BND3 archives inside the dvdbnd archives (`shader_split.py` `bdt_shaders`), and SDK 4's GLSLC outputs are read through their section headers (`glslc_section`); it keeps 12 sampler pools with different samplers under the same IDs, so handles a shader reads from a uniform buffer take the base of the pool bound at replay (`nvn_mtl.m` `ubo_tex_id` / `ubo_samp_id`); and it runs at 60 fps (30 on the Switch) with two rewriter cuts, its frame limiter's 30 and its frame update's fixed 1/30 s step (its `patches.py`). Paper Mario: The Thousand-Year Door is Intelligent Systems' engine on NintendoWare, on Celeste's SDK 15.3.2 `sdk` build: nearly every RomFS file is zstd (`.zst`, which `shader_split.py` decompresses by magic); its AngelScript call bridges leave sp misaligned, which a general rewriter rule rounds up (`site_sp_align`); it runs at 60 fps (30 on the Switch) by the host's frame step (`FRAME_STEP`, with the floor of its vsync step cut from 2 to 1); and its three job workers poll the job list with no SVC, which held it at 24 fps on an iPhone until the general spin wait rule (`patch_nso.py` "Spin waits", `hle_thread.c` `dm_spin_wait`). Hollow Knight: Silksong is Unity 6 (6000.0.50f1) IL2CPP, the newest Unity so far: its bundles carry their own type trees, so `unityfs.py` needed no layout; Unity 6 polls `nn::hid::GetDebugPadStates` every frame (`hle_hid.c`), and it takes all its memory itself through `nn::os::AllocateAddressRegion` / `AllocateMemoryPages` with no heap, so it runs in the 5 GB guest layout that `profile_guess.py` now gives every game with an NPDM system resource, with the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). Catherine: Full Body is Atlus's GFD engine with CRI middleware (CPK archives, ADX2 sound, Sofdec2 movies through `movie::Decoder`, whose player reads `nv12-uv-stride`, `hle_movie.c`): its 3,128 shaders are GLSLC outputs in `data/shaders_binary`, `main` and two compiles of its own GLSLC at boot, which the host lets run by reporting at least that GLSLC's GPU code version (`hle_glslc.c`); that GLSLC writes every vertex shader as a VertexA part (shader type 0, the position) and a VertexB part, which `dmsh_build.py` merges as Ryujinx does (`vertex_ab`; the screen was black without it), and because the game reuses a copy's staging memory a frame after recording it, the host commits a segment at the first pass boundary after a buffer → texture copy (`nvn_mtl.m` `g_cut_copy`). Digimon Story Cyber Sleuth: Complete Edition is Media.Vision's MVGL engine and renders with NVIDIA's desktop OpenGL 4.5 through EGL, not NVN, which the host implements on Metal (`dm/gfx/gl/hle_gl.m`: the 13 EGL imports and the 79 GL functions its code calls of the 2,956 its glad loader fetches, counted with `dm/tools/loader_census.py`; `profile_guess.py` gives `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`); its shaders are GLSL ES 3.20 source in its MVGL archives (`dm/gfx/tools/mvgl.py`: MDB1 with doboz entries) and in `main`, which `gl_shaders.py` translates offline with glslang and SPIRV-Cross (packing needs `brew install glslang`) and the host finds by the hash of its text. Animal Crossing: New Horizons is Nintendo EPD's engine (sead / agl / gsys) on SDK 20.5.4: its `main` links its own HIPC client for `olsc:u` (cloud backup settings), which the host answers with sessions of its own (`hle_sf.c`), its island lives in device save data (`MountDeviceSaveData`, `hle_fs.c`), it skips clears with conditional rendering (profile key `render_cond`), its 63 MB `main` needed halfword jump tables padded only to their largest entry (`patch_nso.py` `half_count`), and its `SaveProc` builds a new island in grid walks that yield once per cell, so a yield whose loop moved its callee-saved registers since the last one now returns at once instead of backing off, for every game (`hle_thread.c` `thr_yield`; the island showed after 77 s of black screen, now about 10 s). DRAGON QUEST VII Reimagined is Unreal Engine 4.27: IoStore containers beside one pak, its shaders LZ4 blocks in the pak's shader code archives, which `uepak.py` decodes itself (`lz4_shaders`), and its allocator replaced as Octopath Traveler 0's (a cut and an observation gate in its `patches.py`, `hle_uemalloc.c` layout `UE4_27`) in a 2 GB engine region (profile key `game_args`); its audio renderer runs at 32 kHz, resampled by the host on the CPU path (`hle_audren.c`); and Unreal's volumetric lightmap merge sizes its indirection texture with a query on the image it writes, so a texture slot that shares its c2 word with an image slot reads the bound image (`nvn_mtl.m` `tex_slot_h`) and the translator types such a query from the image (`dmsh_xlate` `ImageTypesOf`): without them every character in the opening cutscene glowed white; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`), the same two limits a 60 fps mod for the base game lifts with code patches (`UEngine::SetMaxFPS` returning at once, the name `r.VSync` broken). FINAL FANTASY RESONANCE is Unreal Engine 5.6.1, the newest Unreal so far: its shader libraries are IoStore chunks in its container, not in its pak, two layers of Oodle that `uepak.py` reads through the game's own decoder in the Mac host with no boot (`Toc`, `io_archive`, `OodleHost`); its 94 MB `main` is 95% without unwind entries, so the rewriter analyses long functions in pieces (`patch_nso.py` `pieces`; one 82 MB function took 100 GB of memory) and treats code before the first unwind entry as a function; its allocator is replaced as Octopath Traveler 0's (UE 5.6's 32-slot FMalloc, `hle_uemalloc.c` layout `UE5_6`), and its engine region is 4064 MB (profile key `game_args`), because the engine's own page allocator never gives address space back (2 GB aborted in the first battle); 2,077 particle shaders need `tex_dyn_slots`; it runs at 60 fps (30 on the Switch) by the host's `rhi.SyncInterval` write (`hle_fpslock.c`, profile key `fps_target`). Luigi's Mansion 3 is Next Level Games' own engine on NVN: its RomFS files are in the engine's `.dict` / `.data` archives, and update 1.4.0 ships `.patch` files beside the base ones, which `shader_split.py` applies as the game does (`nlg_shaders`); the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from `GetCommandMemoryUsed`, so single texture binds are recorded in the hardware's layout (`OP_BIND_TEX1`, every game; without it the title showed a stretched icon and a new game a white screen); its G-buffer is two integer targets, which its pixel shaders write through a second, integer-output entry point (`dmsh_build.py` `fs_uint`, every game); and it runs at 60 fps (30 on the Switch) because the host writes its frame limiter's 33.333 ms, a constant in read-only data, as 1000 / `fps_target` less half a millisecond (`hle_fpslock.c`, with `present_interval display`). Its tessellated draws are skipped (Metal 4 has no tessellation stage) and its geometry shaders do not build; neither was drawn in play so far. AI: THE SOMNIUM FILES is Unity 2017.4 IL2CPP (Cuphead's Unity layout) with FMOD: its 168 movies are H.264 MP4s that Unity's player copies into NV12 texture pairs, and its Spike Chunsoft logo movie held the game before the title until a released output slot also went on with the H.264 decoder's drain at the end of a stream (`hle_movie.c`: Unity holds all 16 video outputs by then); it binds its vertex and fragment programs apart, and the precompile prediction reused the ring slot of the combined program the replay had saved, so a draw ran with another pair's shaders until each restore of the replay state took the combination again from the per-stage bindings (`nvn_mtl.m` `prog_resolve`, every game); it plays docked with the 3D pad layout (the investigation's view is on the right stick). ANIMAL WELL embeds all its assets and 46 BNSH shaders in `main`; its base and update Program NCAs have no data section, which `nx_extract` represents as an empty RomFS directory. It needed no host change; its profile uses the 2D pad layout. AI: THE SOMNIUM FILES - nirvanA Initiative is Unity 2020.3 IL2CPP (Patrick's Parabox's Unity) with Addressables bundles and FMOD, and needed no host change: Unity 2020.3 keys a compute shader kernel's variants by keyword set (`shader_split.py` `unity_compute`, `variantMap`); its `nninitStartup` hands pool − used to StandardAllocator as virtual address memory and Unity reserves its own regions beside it, so it has the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`) and a 1 GB pool, which the NSP script's heap check now sets for such a game (`profile_guess.py` `heap_signals`: the alias region nearly full within seconds and no heap); it plays docked with the 3D pad layout (the investigation's view is on the right stick). Mario Kart 8 Deluxe is Nintendo's sead / agl / gsys engine on SDK 4.4.0, the oldest SDK so far: it lists the six default Miis at boot and builds Mii heads from the console's MiiModel system data, so the host answers the Mii database itself (`hle_mii.c`: the six default Miis, none of the console's own) and mounts a stand-in of that archive with every part empty (`dm_mii_standin`, `hle_fs.c` `MountSystemData`; the firmware is not the host's to ship, so Mii heads draw nothing); its DLC licence is decrypted in a 0x3130-byte heap that also holds the mount cache, now sized as Horizon's RomFS working memory (`romfs_work_size`); it mounts device and account save data, which are now two host saves for a game whose `main` imports both (at boot it deletes `CommonSave:/userdata.dat`); SDK 4's DeviceSink is command 0xf of 0x130 bytes, and the audio renderer's update lock is read off each SDK's own `AudioRenderSystem::Update` (`hle_audren.c` `update_lock`: it was SDK 16's for every SDK before 20); it evaluates its conditional rendering (profile key `render_cond`) and runs at 60 fps. Balatro is LÖVE 11.4 (love-nx: Lua source in the RomFS, LuaJIT's interpreter, SDL) on NVIDIA's desktop OpenGL through EGL, like Digimon: `main` imports `vkGetInstanceProcAddr` only for SDL's Vulkan loader, which `profile_guess.py` leaves out of its Vulkan count; LÖVE takes its core-profile paths when the GL layer says core (`GL_CONTEXT_PROFILE_MASK`): persistently mapped vertex streams fenced per frame, attribute bindings and current values, uniform queries, texture storage, 3D and array textures, swizzles and mipmaps, about 150 GL functions in all (`hle_gl.m`, counted with `loader_census.py`, which now follows LÖVE's glad through registers; `DM_GL_STUBS=1` names a missing one at its first call); its 35 shaders are GLSL that LÖVE puts together at run time, captured by the NSP script on the Mac (profile `gl_capture.tsv`, `capture_shaders`), with `bool` uniforms keeping their type (`gl_shaders.py`); it opens the audio renderer without an event, which started no render thread before (`hle_audren.c` `ar_open`: it was silent); and it plays by touch in handheld mode (profile keys `op_mode handheld`, `pad_layout touch`: the iPhone gamepad's Tap layout sends every touch to the game). Any Lua error makes it quit, and love-nx then crashes in its video worker's teardown (`main+0x339c00`): look for the Lua error first. ROMANCE OF THE THREE KINGDOMS 8 REMAKE is Koei Tecmo's KTGL on SDK 17.5.3, with a newer RDB than Age of Calamity's: `.fdata` files (a `PDRK` header, then `IDRK` entries) whose entries are LZ4 or zlib in 16 KB chunks of a new form (`shader_split.py` `idrk_data`: flag 0x400000, LZ4 with 0x100000 too), 59,363 shaders in all; its update is a multi-program patch that carries a whole second program (`…0001`, started with `nn::oe::ExecuteProgram(1)`) beside the game's, so `nx_extract` takes program index 0's NCAs (it took the second program and failed); SDK 17.5.3's `OpenAudioRenderer` with an event calls the form without one through the PLT, which the host's hook now passes straight to the SDK (`hle_audren.c` `t_in_open`); it plays at 30 fps with a controller (it reads a USB mouse, not the touch screen). Taiko Risshiden V DX is KTGL on SDK 12.3.3, mostly 2D, in Japanese and Chinese only (the NSP script packs Japanese): its 212 shaders are GLSLC outputs in `main` and one G2S container (`eff/KTGLEffect.g1s`), read as before; it needed `nn::oe::SetCpuOverclockEnabled` (`hle_oe.c`: its first boot crashed in apm); it plays by touch in handheld mode (profile keys `op_mode handheld`, `pad_layout touch`), taking its taps, drags and flicks only from `nn::hid::GetGestureStates`, which the host now recognizes from the touch points for every game (`hle_touch.c` `gesture_sample`, yuzu's rules plus an 8 px slop; unit test `t_gesture`; the `DM_CTL` line `drag` tests pans on the Mac); and its frame end slept in 2 ms rounds until 1/60 s had passed, which ended at 18.2 ms (55 fps), so a build cut skips that wait and the presents hold 60 (its `patches.py`). Super Smash Bros. Ultimate is Bandai Namco / Sora's engine on Catherine: Full Body's SDK 8.2.1 `sdk` build: everything is in one `data.arc` of Hash40-named zstd files (`dm/gfx/tools/smash_arc.py`; the split reads its current files, 35,012 shaders); its fighters' code is 98 NROs in that archive, loaded and unloaded per battle with `nn::ro::LoadModule`, which the NSP script collects by the SHA-256s its NRRs list and rewrites (`dm/tools/nro_collect.py`), the host loads by the image's hash and binds `main`'s `lua2cpp` imports to (`hle_ro.c`, `loader.c` "NRO modules"), and the iOS app carries packed into three sections of the executable (`gen_embed.py`, `loader.c` `nro_pieces`); its engine reserves 62 GB of address space, cut to 4 GB in its `patches.py` with the SDK's node area and 1 GB alignment cut on that `sdk` build (`sdk_patches.py`) for the 5 GB guest layout; and it fetches `nvnSetExternalDebuggerEnabledNVX`, a no-op. Bayonetta 3 is PlatinumGames' engine: everything in PKZ archives of DAT archives whose entries are Oodle Kraken, which the shader split decodes with the game's own streaming decoder (`main` has no `OodleLZ_Decompress`, so `hle_oodle.c` drives `OodleLZDecoder_*`), its shaders the engine's own shader objects (`shader_split.py` `pg_shader`); the engine refuses shader code newer than the driver reports, so the NVN layer's newest GPU code version follows the SDK (`hle_nvn.c`, 1.16 from SDK 13). Advance Wars 1+2: Re-Boot Camp is Unity 2020.2 IL2CPP on The Great Ace Attorney Chronicles' SDK 10.4.1 `sdk` build, which takes the node-area and 1 GB alignment cuts (`sdk_patches.py`) for its virtual address memory, with a 2 GB pool (the heap check's 1 GB ran its malloc dry at the first loading screen, and Unity's GPU pool allocation then spun for good); its Bink 2 movies draw with 32 shaders whose control sections and code areas lie loose in `main`, paired by data records or by the code that builds their addresses (`shader_split.py` `raw_shaders`; every movie drew white without them). Pokémon Sword is Game Freak's engine before Trinity, on Super Mario Odyssey's SDK 7.3.2 `sdk` and movie builds: its shaders are BNSH entries of GFPAK archives (LZ4, `shader_split.py` `gfpak_members`) and Silicon Studio's YEBIS post effects, a `.tfxo` file in a zlib stream in `main` (`tfxo_streams`); its G-buffer writes float and integer targets at once, which takes a pixel shader entry point that returns only the integer outputs as integers (`dmsh_build.py` `fs_uint`, `main0_m`; `nvn_mtl.m` `pso_mixed_uint`), its cube views of fewer than 6 layers count layers in cubes (`tex_by_id`), and it writes beside its packaged textures in host pages they share, which no longer re-uploads them when the texture's own bytes are unchanged (`nvn_mtl.m` `pkg_edges_clean`; frames went from 33 to 75 ms without it); it asks for a Poké Ball Plus (Palma, `hle_hid.c`) and the Let's Go play statistics (`hle_offline.c`), both answered as absent. Pikmin 4 is Unreal Engine 4.26.2 with IoStore (its shaders LZ4 in the pak's shader code archives, as DRAGON QUEST VII Reimagined's) and Wwise: its allocator is replaced as DRAGON QUEST VII Reimagined's (`hle_uemalloc.c` layout `UE4_27`), but this build reserves its engine region only when `-SwitchVirtualAddressSpaceSize=` is absent, so a cut in its `patches.py` lowers the 62 GB default to 4064 MB instead of `game_args`; the rewriter now re-encodes an `adrp` inside an exclusive window (`window_adrp`) and accepts a gate before a `ret` (`scratch_dead`); and a network request now stays on hold for 50 ms, as Horizon's asynchronous one (`hle_net.c`): answered at once, its BCAT thread could finish before its creator stored "started", and about one boot in six waited at the black loading screen for good. Stardew Valley is MonoGame on Sickhead's BRUTE like Celeste (C# compiled to C++, a Boehm collector) with XACT sound, on SDK 20.5.27: 3,535 of its 3,549 XNB files are compressed (LZ4, 8 LZX), which `shader_split.py` decompresses (`xnb_decompress`) to reach its one content effect beside the 64 shaders in `main`; it draws nearly everything with SpriteBatch (a prewarm list of 2 shaders and 4 pipelines), reads only the pad, and its character creation names the farmer and the farm through the software keyboard, whose Mac text box releases the buttons that opened it (`front_mac.m`, found on this game); its profile uses the 3D pad layout (the D-pad in menus, the right stick for the cursor), and it needed no other host change. BALL x PIT is Unity 6 (6000.0.62f1) IL2CPP with FMOD and Rewired, as Hollow Knight: Silksong: it needed no host change, only the SDK's address-region node area cut on its SDK 20.5.19 `sdk` build (`sdk_patches.py`); its loose serialized files have no type trees and borrow the `Shader` tree its bundles carry, and it draws 1280x720 into 1920x1080 window textures with a window crop (a Mac shot shows the crop only with `DM_NVN_SHOT_OUT`); it plays docked with the 3D pad layout (the right stick aims). Mario vs. Donkey Kong is Nintendo's sead / agl / gsys engine on SDK 15.3.0, its base game a cartridge dump (.xci), its cutscenes MP4 files (H.264 and AAC) through the SDK's movie library and an AudioOut of its own: its movie player keeps the outputs it holds in a ring of 32 per track with no bounds check, so AAC movie decoders offer 32 outputs (Vorbis keeps 64), and it sizes its sound buffers by `GetOutputBufferSize` and appends each whole, so an AAC decoder reports one frame (`hle_movie.c`: with 64 outputs the opening movie overran the heap, with the 128 KB answer it froze); it skips clears under render conditions (profile key `render_cond`), sets the advanced blend mode SRC_OVER, which the replay now blends with the Porter-Duff factors it amounts to (`nvn_mtl.m` `adv_blend`), and plays docked with the 2D pad layout. Bayonetta 2 is PlatinumGames' engine in its Wii U form on SDK 3.6.0, the oldest SDK so far, which needed no host change of its own: its files are DAT archives in CRI CPK archives (CRILAYLA-compressed entries, `dm/gfx/tools/cpk.py`, which also reads the masked @UTF tables of Unicorn Overlord's CPKs), its 1,856 shaders BNSH files of one stage each in the shader DATs, which the split decodes from every CPK entry (`shader_split.py` `cpk_jobs`); a cleared attachment larger than the render pass is now cleared in a pass of its own, as NVN clears the whole target (`nvn_mtl.m` `rp_begin_ex_`; the opening narration drew in a corner of a black screen), and `Downsample` of a single-sample texture is a filtered scaling copy that may convert the format (its bloom chain, `copy_t2t`); it plays docked with the 3D pad layout (the camera is on the right stick). Bayonetta is the same engine on Bayonetta 2's `sdk` and `subsdk0` builds and needed no host change: nine CPK archives, its 458 shaders in `shader.dat` of `data00.cpk`, CRI ADX2 sound, the 3D pad layout; its prologue tutorial waits on a Witch Time page until a dodge lands right before the strike (the work folder's `run/witch.py`). [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand and prewarm lists; each game's notes are in `dm/games//README.md` (the [games table](dm/games/README.md#games)). - Each game's constants and shader tables form a game profile in `dm/games//`, where `` is the base game's title ID: `game.tsv` plus the tables (and `patches.py` when the rewriter has patches for its builds). Mario's profile is `010015100B514000`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. There is no default game: set it every time (the tools stop and list the profiles otherwise). Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)). - Prefer general optimizations and fixes to game-specific rules. Performance work (frame pacing, power, caches, load times) counts here as much as correctness. First look for a change that holds for every game: Horizon / NVN / Vulkan semantics, or what the binary, its SDK version, its imports or its files show. Use a game-specific rule (a profile key, a rewriter cut, a build-ID gate, a per-game branch in the host) only when it is absolutely necessary, that is, when no general rule can do it. - Per-build knowledge is keyed by the NSO build ID: rewriter cuts, observation gates and frame-step stubs in the profile's `patches.py` (a game's own modules) or `dm/tools/sdk_patches.py` (Nintendo's `sdk` modules, which several games ship), which `dm/tools/patch_nso.py` (the general rewriter) loads, the profile's `build` rows (the builds the game was brought up on, and their display versions; the NSP script packs any other version from a derived profile, without those builds' fixes), and host behaviour that uses fixed module addresses checks `module_t.build_id` (from `fixups.tsv`; `dm_main_build()`). When a new game build appears, carry addresses over with `dm/tools/nso_fnmap.py` and check them by disassembly; never let fixed addresses run on a build they were not found in. diff --git a/README.md b/README.md index 0312926..fe99611 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ A connected game controller hides the touch controls and keeps every button. ## For developers -How it works, the script's options, running games on the Mac, adding a game, and per-game notes: [docs/development.md](docs/development.md). In-depth bring-up and debugging notes: [docs/agents](docs/agents/README.md). +How it works, the script's options, running games on the Mac and adding a game: [docs/development.md](docs/development.md). Per-game notes: [dm/games](dm/games/README.md#games). In-depth bring-up and debugging notes: [docs/agents](docs/agents/README.md). ## License diff --git a/dm/games/0100000000010000/README.md b/dm/games/0100000000010000/README.md new file mode 100644 index 0000000..9ecf846 --- /dev/null +++ b/dm/games/0100000000010000/README.md @@ -0,0 +1,70 @@ +# Super Mario Odyssey (`0100000000010000`) + +Update 1.4.1 (v393216) over the base game, SDK 7.3.2. Nintendo EPD's sead / agl engine with NintendoWare (G3d / Ui2d / Vfx), on NVN. Modules `main`, `subsdk0` (the movie module) and `sdk`; DRAGON QUEST XI S 1.0.4 runs the same `sdk` build. Plays on the iPhone. The README lists its IPA at about 6 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `alpha_from_red` with the 26 layout pixel shaders (`main`'s BNSH at 0x132c000) | [`game.tsv`](game.tsv) | the HUD's coin counters came out as solid white cards; inert on the Mac since the `nvnTextureCompare` fix, kept until an iPhone run; see [Rendering](#rendering) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | from the NSP script's provisional profile | +| Node area cut and 32 MB alignment of large regions | `dm/tools/sdk_patches.py`, `sdk` build `fcf93a41…` | registered for DRAGON QUEST XI S on the same build; Odyssey has no virtual address memory and never allocates an address region | + +No `patches.py`. `op_mode` stays docked: Odyssey only takes its motion moves in its default docked mode. + +## Input + +- **Resolved: buttons and menus worked, the sticks did nothing in play** (its first hint, "Look Around", waited forever). The game reads all 16 entries of the controller history (`GetNpadStates`) and ignores the returned count; the host filled only the newest. It polls every controller slot each frame, which moves the shared sampling number on by about 16, so the stick samples it combined were mostly empty, while buttons and the stick's direction bits (which menus use) still came through. The host now fills every requested entry with the current state under consecutive sampling numbers. +- **Resolved: motion moves never happened; shaking did nothing** (cap throws by shaking). The host handed out six-axis handles but reported no sensor; it now reports one per connected controller. The game reads the six-axis history the same way (16 entries per sensor, the count ignored, `main+0x93fd84`). Its motion analyzer (`main+0x5d5590`) turns the newest acceleration, seen against the controller's pose, into a swing value per hand and sets the swing flags above 0.2. A still controller gives 0, and the host's shake (±3 G side to side) gives up to 1.5, so a `Shake` sets the left, right and any-swing flags for its length. To find the swing detector, probe the accelerometer value it reads (`DM_PROBE=main:626248@x8+10` in 1.4.1). On the iPhone, shake the phone (a sharp flick); on the Mac, Space. + +## Rendering + +- **HUD (layout captures).** The coin counters (coin, regional coin) and other layout panes take their alpha from single-channel render targets, as in Breath of the Wild; read literally they were solid white cards, which `alpha_from_red` fixed. Odyssey's layouts rebuild their captures through the same agl code (`main+0x982320` replaces the registered descriptor only if `nvnTextureCompare` reports a difference), so the comparison fix covers them too. **Current:** on the Mac smoke route to frame 20,100 the list converted no texture (the host before the fix converted its first, a 640×80 R8 target, at frame 4129), and the HUD at frames 17,300 and 17,600 is pixel-identical. The list stays until the change has been seen on an iPhone. +- **Resolved: the Cap Kingdom fog never parted around Mario (reported on an iPhone; fix checked on the Mac).** The fog's shading reads velocity fields that a small fluid simulation updates every frame: two 256×256 and two 128×128 RG16F targets, with a two-pass blur and a blended composite. Its only input is a point-sprite splat per mover, drawn with `alPointSpriteShader`: one vertex per mover, its size in the vertex data (Mario's main splat about 18 px, others 3–5 px) and a soft disc in the fragment shader (`clamp(1 - 4r², 0, 1)` × velocity). The vertex shader writes `gl_PointSize = min(size, c0[0x610])`, and the host had nothing at that offset of the driver segment, so every splat had size 0 and the fields stayed exactly zero (raw dumps with `DM_NVN_TEXDUMP` + `DM_NVN_SNAPRAW`). The host now writes the largest point size, 2047, at `c0 + 0x610` for the graphics stages. Writing the `SetPointSize` value instead (always 1 here) left one-pixel splats whose disc weight came out 0 (the fields held `-0.0`). After the fix the fields hold velocities up to ±44 and the fog clears around Mario. +- **Resolved: the Cap Kingdom ground showed 32×32 blocks of a stale image on every fourth frame (Mac).** Presents 4k were brighter and blocky, 4k+1 and 4k+2 clean, 4k+3 partly blocked, repeating to the decimal across runs; only with the ground's material (`b6384a85afd3ef94`) drawn. In some frames the game first draws a depth pre-pass (`alRenderDepthOnly`) for objects its occlusion queries report visible (the queries rotate through three slots, hence the cycle), then the G-buffer with other vertex shaders and depth test less-or-equal, without clearing the colour targets. Metal compiled the two vertex shaders' identical position math to slightly different depths, the G-buffer fragments failed the test and kept the previous content. Every Metal compile now uses `-fpreserve-invariance`. Ruled out on the way: the TBDR optimizations, same-pass feedback, the present path; evaluating the game's conditional rendering on the CPU from earlier frames' results made it worse (the condition belongs to the same frame on the GPU), so the host still ignores the condition here and draws (no `render_cond`). +- Its three geometry shaders (a thunder effect) do not translate. + +## Movies + +- MP4 / H.264: the videos in its tutorial hint boxes and the kingdom travel videos, 41 of 44 High or Main profile with B-frames. The host decodes them with VideoToolbox; the SDK's demuxer passes the parameter sets as the `avcc` record (the MP4 `avcC` box), and pictures leave in presentation order through a small queue. + +## Services + +- Start-up calls no game before it made, each answered by the host: the aborting `account::OpenPreselectedUser` and `GetLastOpenedUser` (a null read), news delivery (`bcat`: the delivery storage does not mount, which the game treats as "nothing delivered"), HOME-button blocking, the parental 3D-mode setting, `vi::SetLayerScalingMode`, `time::ToPosixTimeFromUtc`, the debug pad, and the SDK 7 names of two audio-device calls (`audio::AcquireAudioDeviceSwitchNotification` aborted). Each aborted with no message in the SDK's IPC; frames resolved with `sdis.py --sym`. +- Command memory: it hands out many 512-byte command memory grants before recording; with no recording to reclaim them by, 256 KB host segments each reached the cap entering the Wooded Kingdom, so an empty-pool grant of at most 4 KB outside a recording gets a 4 KB segment (`hostseg_size`). The `GetCommandMemoryUsed` count from the last `AddCommandMemory` (found on Three Houses) left it unchanged. +- Shared fonts: SDK 7, so the address is the plain TTF. Focus handling mode 3 (no focus notifications; the system suspends the game out of focus). +- Its SDK 7 driver's GPU code range (1.5–1.14, `nvnDeviceGetInteger` 0x2E–0x31) was measured here; the host now reports newer versions for newer SDKs. + +## Audio + +- It is silent for its first minute with either audio renderer update lock: the old SDK 16 offset and the one `update_lock` now reads off the SDK's own `Update` (`MutexType` at +0x1d8 on SDK 6.4–8.2) ([03, Audio](../../../docs/agents/03-bringup.md#audio)). + +## Files and shaders + +- 27,984 shaders: agl `sharcb` and `bfsha` in `ShaderData`, particles in `EffectData`; groups per RomFS folder in `game.tsv`. +- The spin-wait rewrite leaves its modules byte for byte as before the lock and store rules. + +## Mac routes + +- A fresh save wakes Mario in the Cap Kingdom with `DM_KEYS="f1000:A,f8200:RLeft~6,f9200:LUp~4"`: the "Look Around" hint waits for the right stick held about 6 s one way (left then right cancels out). Play starts at about frame 9,900, and the game saves there. +- From that save, `f1000:A,f1300:A` resumes into play at about frame 1,500, and `f1600:LUp~7` walks straight across the fog to the hills by the bridge, where Cappy flees from Mario. +- `DM_KEYS` takes `RLeft` / `RUp` / `RRight` / `RDown` for the right stick and `Shake` for the controller. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Largest point size at `c0 + 0x610`**: [04, section 3](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census) and [04, Seen in Odyssey](../../../docs/agents/04-graphics.md#seen-in-super-mario-odyssey). +- **Invariant positions** (`-fpreserve-invariance` in `dmsh_build.py` and `ios_metal.py`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **Full controller and six-axis histories**, **six-axis sensors and the shake** (`DM_BTN_SHAKE`), **SDK 7 start-up services**: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **H.264 movies through VideoToolbox** (the `avcc` record, a presentation-order queue): described under [Movies](#movies); `hle_movie.c`. +- **Small host segments for small grants** (`hostseg_size`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- `alpha_from_red` is inert on the Mac but stays until an iPhone run shows the HUD without it. +- The three thunder geometry shaders do not translate. +- The silent first minute of sound. +- development.md calls SDK 7.3.2 "the oldest so far"; Mario Kart 8 Deluxe (SDK 4.4.0) is older now. +- `game.tsv` says values marked "Mario's" are not measured yet, but none is marked. diff --git a/dm/games/010005501E68C000/README.md b/dm/games/010005501E68C000/README.md new file mode 100644 index 0000000..c0de899 --- /dev/null +++ b/dm/games/010005501E68C000/README.md @@ -0,0 +1,59 @@ +# Ace Attorney Investigations Collection (`010005501E68C000`) + +Update 1.0.0.2 (v131072) over the base game, which is an XCI; its DLC NSP is a licence only. SDK 17.5.3. Unity 2022.3.17f1 IL2CPP on NVN. Modules `main`, `subsdk0` (the movie module, `NintendoSDK_movie`) and `sdk`. Plays on the iPhone; 60 fps, as on the Switch (present interval 1). On the Mac it was brought up through the first-boot settings, the title, Investigations 1's menus and Episode 1's opening and first investigation up to the first Logic prompt (25 minutes with no red line). Much of the bring-up was worked out first by armyja in a fork (GitHub issues #17, #18, #19 and pull requests #15, #16). + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `311deab1…` (the AddressRegionManager constructor at `sdk+0x139458`; the movs at `+0x139468` and `+0x1394fc`) | without it `StandardAllocator::Initialize(nullptr, pool − used)` (`main+0x874b6c`) aborts at its first `CentralHeap::Init`; see [Memory](#memory) | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000` | [`game.tsv`](game.tsv) | Engage's block: 256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region (the game never calls `SetHeapSize`) | +| `app_pool 0x40000000` (1 GB) | `game.tsv` | with Engage's 2.5 GB pool the live address regions reached the end of the alias region after 140 s; see [Memory](#memory) | +| `shader_groups _unitybuiltinshaders_=engine /Resources/=engine /StreamingAssets/=assets /Data/=data` | `game.tsv` | engine built-ins (the `unitybuiltinshaders` bundle, `Resources/`) go to engine, the player's serialized files to data, the other Addressables bundles to assets | + +## Memory + +- The NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true: `nninitStartup` (`main+0x874b00`) hands application pool − used memory to StandardAllocator as virtual address memory, one 1 GB-aligned address region in the alias region, and never calls `SetMemoryHeapSize`. Unity's own allocator (`main+0x40f1d4` → `AllocateAddressRegion`) reserves its pools beside it, each aligned by its size. +- **Resolved: `StandardAllocator::Initialize` aborted in `nninitStartup`.** SDK 17's AddressRegionManager takes about 1 GB of the alias region as its node area first (the same node area as Engage's SDK): `CentralHeap::Init(nullptr, pool − used)` got no 1 GB-aligned region for 3.14 GB after it. The constructor (`sdk+0x139458`: limit = alias start + 0x3fe00000 at +0x28, a unit heap of 0x30-byte nodes, the area then registered as occupied) takes Engage's cut to 62 MB. +- **Resolved: then the alias region filled in minutes.** With Engage's 2.5 GB pool the game ran, but probes on `VammManager::AllocateAddressRegion`'s success path (`sdk+0x1395a8`: x21 size, x22 address) and `FreeAddressRegion` (`sdk+0x139df0`), with commits from the `MapPhysicalMemory` log, over the first five minutes (the title, Episode 1's opening, the first investigation) showed StandardAllocator reserving 2.44 GB and committing only 0.19 GB, Unity's pools holding 1.06 GB, and the live regions ending 4.37 GB into the 4.375 GB alias region after 140 s. Over ten minutes of the same route, a 1.5 GB pool (StandardAllocator 1.44 GB at +0.75 GB) let them reach +3.91 GB, a 1 GB pool +2.85 GB. +- **Current:** the pool is reported as 1 GB. StandardAllocator gets 0.94 GB (five times its use) right after the node area, and running out of address space is a crash, while a pool a little smaller than what the game maps in all (1.02 GB at the peak) changes nothing: the host does not enforce it and only `nninitStartup` reads it. This game is why `mem_map_limit` is off by default ([dm/games/README.md, Keys](../README.md#keys)). +- On the Mac about 1.55 GB resident and 1.25 GB of Metal allocations, flat from the first investigation on (25 minutes; address space at +2.81 GB). + +## Rewriter + +- **Resolved: a `jump table ` assertion on a `br x18` with no table in front of it** (`main+0x1d271bc` and `+0x204ebec`). Not a jump table but an indirect tail call: an IL2CPP thunk loads the method pointer with `ldr x18, [x0, #0x18]`, moves its stack arguments with every other scratch register (`ldr x16, [sp]`, `ldp x17, x15, [sp, #8]`, …) and ends with `br x18`. The rewriter now turns it into `x16 = guest x18; br x16` (`tail_call` / `site_br_call`). + +## Services + +The C# file API and the real-time clock reach the SDK's C library (nnmusl), not `nn::fs` / `nn::time` directly. + +- **Resolved: Addressables never started, then a null asset crash (`main+0x31100ec`).** The `DM_FS=1` output had no `StreamingAssets/aa/settings.json` at all. Unity 2022's C# file API goes `File.Exists` → `SystemNative_Stat` → `stat` = `open` + `fstat` + `close` (`sdk+0x948b0`). nnmusl's path check (`sdk+0x904e0`, then the converter `sdk+0x8ef50`) asks `nn::fs::IsPathMounted`, `GetPathLengthMax`, `GetUtf16PathLengthMax`, `GetEntryNameLengthMax` and `GetUtf16EntryNameLengthMax` before it calls `nn::fs::GetEntryType` / `OpenFile`, and the SDK's own versions read the empty mount table (`IsPathMounted` false; the next one aborted with Result 0x35f202). The host answers them from its mount table, with Horizon's 0x300-byte limits and no UTF-16 limit (`hle_fs.c`). The path .NET builds is `/rom:/…`; nnmusl's converter turns it back into `rom:/…`. armyja's fork (GitHub issue #18) patched the managed stat wrappers by address instead; the hooks need no per-build table. +- **Resolved: `std::terminate` (`Break` from `sdk+0x5bfa48`) at the first real-time read**, right after the first-boot dialog (`main+0x3f3f68` → `std::chrono::system_clock::now()`). `clock_gettime(CLOCK_REALTIME)` calls through a source pointer (`sdk+0xb05010`) that `nntimeStartup` sets once `time::Initialize` succeeds: 1 = `nn::time::GetStandardUserSystemClockCurrentTimespec`, 2 = the adjustable clock's (`sdk+0x8e414`). Both asked the time service; they are host take-overs now (`hle_time.c`), so the C library's real-time clock is the host's clock. +- Not needed in the end: the process heap of pull request #16 (`nninitStartup` calls `SetMemoryHeapSize` / `AllocateMemoryBlock` only without virtual address memory, and nothing else in `main` calls them) and the tolerant common string table of pull request #15 (1.0.0.2's `main` has the table intact at `main+0x46169ac`, 109 entries ending at `Hash128`). +- It calls `GetSharedFontAddress` without importing `ScalableFontEngineHelper::Decode`, so the host gives it the TTF itself (`font_skip`, see [Patrick's Parabox](../0100E44016C22000/README.md)). + +## Files and shaders + +- 4,951 stage shaders: 2,475 vertex / pixel pairs (4,294 in the player's serialized files, 517 built-ins, 140 in the Addressables bundles) and one geometry shader, which Metal cannot express (its build fails). Unity's player also has 11 GLSLC outputs of its own in `main` beside the game's shaders. +- **Unity 2022 `Shader` objects:** the program code keeps the 0x60-byte stage header, but from Unity 2022 the blob also holds each program's parameter tables as entries of their own (the parsed form's `m_ParameterBlobIndices`; their bytes start `PGlobals`), and the player's programs are listed in `m_PlayerSubPrograms[tier][].m_BlobIndex` (`m_SubPrograms` is empty); `unity_code_blobs` keeps only those. Subprogram version 202012090 (from Unity 2021.2) has no local keyword list, so `unityfs.subprogram` reads no second keyword array (with one it read the code length as a keyword count). +- The player's serialized files have no type trees and the Addressables bundles hold no `Shader`, except the `unitybuiltinshaders` bundle, whose tree has the same hash and is borrowed in the split's second pass. armyja's issue #17 proposed UnityPy and a reader of its own; neither was needed. +- Its `Shader` type hash (`ea424a5f…`) is the one Brain Memory Training Academy's stripped files carry; the Unity 2022.3 layout now written out in `unityfs.py` (`KNOWN_TREES`) was checked against this game's own tree: the same 2,324 nodes with the same alignment flags, and its 40 bundle `Shader` objects decode identically with either. The written-out tree is given only in the second pass: taken in the first, this game's player files came before its `unitybuiltinshaders` bundle and the built-ins' source column and package indices moved; with the second pass its tables come out byte-identical. +- Binding layout as Engage's (separate textures and samplers): all 4,950 translatable shaders decode every binding (`shaders with a binding whose slot was not decoded 0`), and 190 read `c2` rows as data. + +## Mac routes + +- A fresh save through the first-boot notices (the privacy prompt's second button is "Next"), the title and Investigations 1's Episode 1: `DM_KEYS="f600:A~0.3,f1200:A~0.3,f1800:A~0.3,f2400:A~0.3,f3000:Down~0.3,f3200:A~0.3,f3800:A~0.3,f4400:A~0.3,f5000:A~0.3,f5600:A~0.3,f6200:A~0.3,f6800:A~0.3*270/120"`. The first investigation in Edgeworth's office comes at about frame 18,000; at about frame 30,000 the first Logic prompt waits for L. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Indirect tail calls through `x18`** (`tail_call` / `site_br_call`), tried only where the rewriter used to stop: [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **The SDK 17 C library's path checks** (`IsPathMounted` and the length-limit queries, `hle_fs.c`) and **the C library's real-time clock** (`hle_time.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Node area cut for a Unity 2022 game with virtual address memory** (`StandardAllocator::Initialize(nullptr, pool − used)`): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **Unity 2022 `Shader` objects** (`unity_code_blobs`, no local keyword list in `unityfs.subprogram`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). + +## Open + +- The one geometry shader does not build (Metal cannot express it). +- `game.tsv` has no `sys_resource` row, so the host reports its default, 32 MB (`game.py`, `hle_svc.c`), where the NPDM declares 16 MB. diff --git a/dm/games/010013C00E930000/README.md b/dm/games/010013C00E930000/README.md new file mode 100644 index 0000000..acca672 --- /dev/null +++ b/dm/games/010013C00E930000/README.md @@ -0,0 +1,58 @@ +# Hollow Knight: Silksong (`010013C00E930000`) + +Update 1.0.30000 (v655360) over the base game, SDK 20.5.8. Unity 6000.0.50f1 IL2CPP with FMOD (one AudioOut) on NVN. Modules `main`, `subsdk0` (the SDK's movie library, whose `movie::Extractor` demuxes the 27 MP4 cinematics, H.264 and AAC; `Intro_Cinematic.mp4` after a new file) and `sdk`. Plays on the iPhone. On the Mac: the logos, the title, a new file, the intro poem, the opening cinematic with its sound, and Hornet under control in the first two rooms of Moss Grotto (bind, jump, attack), with music and sound, at 60 fps as on the Switch (30 while a cinematic plays, as the game asks); memory flat in play. The base game alone (1.0.27776: another `main`, the same `sdk` and movie module) packs from a derived profile with its own 2,244 shaders and plays the same opening on the Mac with no red line. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `e01950c5…` (the constructor at `sdk+0x145bec`; the movs at `+0x145bfc` and `+0x145c68`) | Unity 6's address regions start after the node area: +3.00 GB of the alias region in the first room without it, +2.26 GB with it; see [Memory](#memory) | +| `sys_resource 0x1000000` | [`game.tsv`](game.tsv) | the NPDM's 16 MB of system resource: virtual address memory | +| 5 GB guest layout with the hardware pool: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the block of the other Unity games with virtual address memory (256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region); the pool is the hardware's (3.2 GB): Unity only reads it through `QueryMemoryInfo`, and nothing maps against it | +| `nn::hid::GetDebugPadStates` | host, `hle_hid.c` (every game) | an `nn::diag` abort in the first frames; see [Services](#services) | +| `shader_groups /Data/=data` | `game.tsv` | everything is under `Data/`: the player's serialized files and the `StreamingAssets` bundles | +| No `pad_layout` (the 2D default) | `game.tsv` | A binds, B jumps, Y attacks; see [Input](#input) and [Open](#open) | + +## Frame rate + +- 60 fps, as on the Switch. While a cinematic plays the game draws 1280x720 with a window crop and asks for present interval 2 (30 fps). + +## Memory + +- 16 MB of system resource (virtual address memory) and no heap: Unity 6's `nninitStartup` (`main+0x118cb70`) only calls `QueryMemoryInfo` and `SetMemoryAllocatorForThreadLocal`, so it sets up no StandardAllocator, and nothing aborts at startup as with 2022.3. Unity's own allocator reserves address regions with `nn::os::AllocateAddressRegion` and commits pages with `AllocateMemoryPages` in the alias region. +- **Resolved: the provisional profile's block was far too small.** `main` imports no `SetMemoryHeapSize`, and the provisional profile took that for a game living in its bss (a 1 GB block, 256 MB of alias region) while Unity maps 1.5 GB in the first room. `profile_guess.py` now gives every game with an NPDM system resource the 5 GB block (Absolum and Pokémon Legends: Z-A, heapless too, came out right with it). +- **Current:** the node area still sits at the start of the alias region, so it takes the cut: without it the high-water mark was +1.00 GB at `nninitStartup` and +3.00 GB in the first room of play; with it +2.26 GB, 1.51 GB mapped. +- Measured on the Mac: resident about 2.1–2.15 GB and 1.25 GB of GPU memory, flat for 100 s of play. +- BALL x PIT ([010086A022444000](../010086A022444000/README.md), Unity 6000.0.62f1, SDK 20.5.19) takes its memory the same way and needed only the same cut on its `sdk` build. + +## Services + +- **Resolved: an `nn::diag` abort with Result 0x2aca in the first frames of `nnMain`** (`bt: sdk+0x514580 main+0x1296c64 …`). `main+0x1296c60` calls `hid::GetDebugPadStates(states, 16)` every frame, and the SDK's own reads a debug pad resource nothing initialized (module 202, hid). The host had only the single-state `GetDebugPadState`; the history form now answers every requested entry as not connected, newest first with consecutive sampling numbers (`hle_hid.c`). + +## Input + +- A binds, B jumps, Y attacks (the game's own prompts name A and B). It stays docked at 1920x1080 with the 2D pad layout. Whether it reads touch in handheld mode has not been tried. + +## Files and shaders + +- Nearly everything is in 2,068 Addressables bundles (`StreamingAssets/aa/Switch`). +- 2,450 stage shaders: 1,189 vertex, 1,193 pixel, 67 compute and one geometry shader (which does not build), 11 of them the player's own GLSLC outputs in `main`. +- The bundles carry their type trees (they report the engine version `0.0.0`, Unity's stripped version string), so `unityfs.py` read Unity 6's `Shader` objects with no layout written out. + +## Mac routes + +- From a fresh save, `DM_KEYS="f1600:A,f1900:A,f2200:A,f2500:A,f2800:A,f9400:A~1.5,f9700:LLeft~60,f9800:B~0.5*60/50,f9830:Y*60/50"` starts a new file in slot 1, plays the poem and the opening cinematic (control at about frame 9,200), binds, and walks left into the second room (its "B Jump" prompt), jumping and attacking as it goes. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **`hid::GetDebugPadStates`**, the history form (`hle_hid.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Unity 6 memory with no StandardAllocator and no heap**, and the NSP script's 5 GB block for every game with an NPDM system resource (`profile_guess.py`): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space), [dm/games/README.md, Adding a game](../README.md#adding-a-game). +- **Unity 6 `Shader` objects** read from the bundles' own type trees: [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). + +## Open + +- The pad layout disagrees: the profile has no `pad_layout` (the default `simple`, 2D) and the notes say 2D, but README.md's touch-controls paragraph does not list this game among those that start in 2D. +- Whether it reads touch in handheld mode has not been tried. +- The one geometry shader does not build. diff --git a/dm/games/010014E00DB56000/README.md b/dm/games/010014E00DB56000/README.md new file mode 100644 index 0000000..cdec9d1 --- /dev/null +++ b/dm/games/010014E00DB56000/README.md @@ -0,0 +1,69 @@ +# Digimon Story Cyber Sleuth: Complete Edition (`010014E00DB56000`) + +Update 1.0.3 (v196608), SDK 6.4.0. Media.Vision's MVGL engine (the PC version's), rendering with **OpenGL**, not NVN: NVIDIA's desktop GL 4.5 core profile through EGL, which the host implements on Metal (`dm/gfx/gl/hle_gl.m`). CRI middleware plays the sound (one 6-channel AudioOut) and the USM movies. Modules `main`, `subsdk1` (the SDK's movie library) and `sdk`; `subsdk0` is NVIDIA's OpenGL driver and is never loaded. Plays on the iPhone. + +On the Mac: the logos, the title, the privacy policy, the DigiLab story choice, the EDEN chat opening (movies and real-time scenes), character creation and the login, into EDEN (the fly-in, then its entrance plaza in 3D with Taiga under control), with sound, at 60 fps in the menus and 30 from the story on (the game's own rates); no red line. About 0.95 GB resident (0.3 GB of Metal allocations in EDEN), flat over half an hour. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `modules main subsdk1 sdk` | [`game.tsv`](game.tsv) | load order as on Horizon (`subsdk1` = NintendoSDK_movie). `subsdk0` is NVIDIA's OpenGL driver, which programs the GPU through NvRm: never loaded, the GL layer answers `main`'s EGL imports | +| `sys_resource 0x0` | `game.tsv` | no system resource, so no virtual address memory: `nninitStartup` gives its malloc a heap of the pool − used memory (3.13 GB of the 3.25 GB heap region), which it maps only as it touches it | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the 4 GB layout of the games without virtual address memory | +| `mem_used 0x4a00000` (74 MB) | `game.tsv` | the rewritten modules are larger than the default 64 MB of used memory | +| No NVN shader tables | `game.tsv` `shader_note` | no NVN shaders: `gl_shaders.py` writes the GL library and its table, not the profile | +| The GL path | `main`'s imports | `profile_guess.py` gives `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`; of the 22 games extracted ([04](../../../docs/agents/04-graphics.md#seen-in-digimon-story-cyber-sleuth-complete-edition-opengl) says), only Digimon does | +| glslang | packing | for a GL game the NSP script runs `gl_shaders.py` in place of the NVN shader split and chain build (Mac and iOS libraries in `_build/shader/gl` and `shader/ios/gl`); it needs `brew install glslang` | + +No `pad_layout`: the virtual gamepad starts in the 2D layout (the default). + +## Rendering (OpenGL) + +- **Telling the API apart:** `main` imports `nvnBootstrapLoader` and 13 EGL functions. Its graphics setup (`main+0x5ed8f0`) makes an NVN device first, which only a helper beside the movie player uses (`main+0x528000`–`+0x52c400`: a memory pool and four textures next to the movie's GL textures; 30 NVN functions, all in that helper). Then `eglGetDisplay`, `eglBindAPI(EGL_OPENGL_API)`, a 4.5 core context, a window surface on `vi::CreateLayer(w, h)` and a glad loader (`main+0x52c460`). Absolum imports the same EGL functions (SDL3) but renders with NVN and names no GL function. +- **Which GL functions:** glad asks `eglGetProcAddress` for 2,956 names and stores each in a global its code reaches through the GOT. A census of the GOT slots the code loads outside the loader (`dm/tools/loader_census.py`) gives the 79 functions the game calls: buffers, vertex arrays, textures (2D and cube, S3TC), framebuffers and renderbuffers, programs with plain `glUniform*`, `glDrawElements` / `glDrawArrays`, and three compatibility calls (`glTexEnvi`, `glColorTable`, `glPixelTransferi`). No uniform buffers, instancing or buffer mapping. Every other name gets a null pointer. +- **Current: the GL layer.** One context; GL calls are encoded into Metal as they come, one command buffer per frame; GL's bottom-up orientation through the NVN layer's lower-left origin (`mtl_set_origin_lower_left`: flipped viewports, reversed winding, a flipped present); textures made on first use from the levels given (the loader defines them level by level); buffers orphaned on `glBufferData` (a new `MTLBuffer` when the GPU may still read the old one); the window surface (the size of the game's `vi::CreateLayer`, 1920x1080) presented through the NVN layer's present path, paced by the swap interval. +- Without `glBindAttribLocation` the game's attribute index is the Metal one: `glGetAttribLocation` returns the shader's own attribute numbers. +- **Resolved:** Metal refuses a swizzle on a texture that can be a render target (`Texture swizzling is not compatable with MTLTextureUsageRenderTarget`, in the first story scene), so only luminance / alpha formats get a swizzle; Metal reads a channel a format lacks as 0 (alpha 1), as GL does. +- **Present:** three window textures through the NVN layer's present path (`mtl_ext_tex`). The game sets the swap interval to 60 ÷ its frame rate before each swap: 1 at the title and in the DigiLab, 2 from the story's opening on. + +## Shaders + +- **Current:** GLSL ES 3.20 source compiled at run time with `glShaderSource` / `glCompileShader`: 1,682 `.shad` files in the MVGL archives (615 distinct) and 37 strings in `main` (post effects, FXAA, shadows, the movie's YUV). +- `dm/gfx/tools/gl_shaders.py
[--ios ]` translates them offline with glslang and SPIRV-Cross into `/gl/` (676 shaders). A string in `main` says nothing of its stage, so it is translated as each stage that compiles (one fragment shader defines `vPosition` as `gl_Position`). The host finds each shader by the hash of its text and its stage. +- What NVIDIA's compiler accepts and glslang does not, fixed up on the preprocessed text: the GLSL ES 1.0 functions `textureCube` / `texture2D` in ES 3.20 code (89 fragment shaders; with `-V` glslang reads `textureCube` as Vulkan's texture type, so a macro cannot rename it), `out vec4 gl_Position` redeclared through a macro (the built-in sprite shader), and `textureOffset` with an offset that is not a constant (FXAA), which becomes a plain sample at P + offset ÷ level 0 size. +- `DM_GL_CAPTURE=` writes every source the game compiles, which `gl_shaders.py` reads like a RomFS; every one captured on the route so far is in the archives or `main`. + +## Files + +- Everything but the movies is in nine MVGL archives (`DSDB*.nx64.mvgl`: `MDB1`, a name tree, doboz-compressed entries; `dm/gfx/tools/mvgl.py` lists and reads them); four of them are CRI AFS2 sound banks under the same extension. The profile's `romfs_marker` is `DSDB.nx64.mvgl`. + +## Movies + +- The USM movies are loose CRI files. Their H.264 is decoded through the SDK's movie library (`subsdk1`, the host's decoder) into NV12 buffers (3,110,400 bytes a 1920x1080 frame); the game uploads the planes into a `GL_RED` and a `GL_RG` texture (two `glTexSubImage2D` a frame) and converts YUV in its own shader (in `main`, `SamplerY` / `SamplerUV`). + +## Services + +SDK 6.4.0 boots, one each: + +- **Resolved: `UNIMPL eglGetDisplay`, then `Break` right after `nv::InitializeGraphics` and `vi::OpenDefaultDisplay`** (`main+0x5ed9d0`): the game renders with GL through EGL. The host's GL layer answers the EGL imports and `eglGetProcAddress`. +- **Resolved: SIGBUS in `__vfprintf` under `aoc_load`, from `h_aoc_Count`** (a store to `0x…67ff0`, right below a page of the guest thread's stack): the first add-on content query lists `DM_AOC_DIR` and logs, a few KB of stack, and the game asks from a thread with little left. `aoc::CountAddOnContent` and `ListAddOnContent` now run on the big host stack (`HLE_FORCE_BIG`, `hle_misc.c`). +- `vi::CreateLayer(w, h)` keeps the layer's size for the window surface. + +## Mac routes + +- A fresh save reaches EDEN with `DM_KBD=Taiga DM_KEYS="f2000:A*26/200,f7300:Right,f7400:A,f7600:A,f7800:Right,f7850:A,f8100:A*39/250,f9000:Plus*15/600,f18000:Down*3/1200,f18100:A*3/1200,f21700:Left*300/250,f21800:A*300/250"` (the title, the privacy policy, the DigiLab: Cyber Sleuth, no options, A through the opening chat; the login form comes at frame 18,500 to 19,700). +- The name is a field of that form: A opens the keyboard, and the game's name screen stays up about 500 frames after the text arrives, where another A opens it again, so Down and A come 1,200 frames apart. The login prompt defaults to No, hence Left before each A from there; EDEN at about frame 22,500. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The OpenGL layer** (`dm/gfx/gl/hle_gl.m`: EGL, GL on Metal, `mtl_set_origin_lower_left`, `mtl_ext_tex` presents) and **offline GLSL translation** (`dm/gfx/tools/gl_shaders.py`: glslang `-V -R`, SPIRV-Cross, varyings matched by name, `--fixup-clipspace`; `DM_GL_CAPTURE`): [04, Seen in Digimon Story Cyber Sleuth](../../../docs/agents/04-graphics.md#seen-in-digimon-story-cyber-sleuth-complete-edition-opengl). +- **MVGL archives** (`dm/gfx/tools/mvgl.py`). +- **Loader census** (`dm/tools/loader_census.py`) and the `graphics gl` guess in `profile_guess.py`: [02, NVN surface](../../../docs/agents/02-survey.md#nvn-surface). +- **Add-on content queries on the big host stack** (`HLE_FORCE_BIG`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- The player-facing [README](../../../README.md) lists the games that start in the 2D layout and says the others start in 3D; Digimon is not in that list, but its profile has no `pad_layout` (the default, 2D). diff --git a/dm/games/010015100B514000/README.md b/dm/games/010015100B514000/README.md new file mode 100644 index 0000000..b41afe2 --- /dev/null +++ b/dm/games/010015100B514000/README.md @@ -0,0 +1,108 @@ +# Super Mario Bros. Wonder (`010015100B514000`) + +Base game v0 (1.0.0, SDK 16.2.0) and update 1.2.1 (v327680, a new `main` build on SDK 16.2.12); the profile carries both builds and packs 1.2.1 by default. Nintendo EPD's engine (sead, agl), on NVN 55 (it requires 55.x with minor ≥ 15). Modules `main` and `sdk`. The first game of this repository, after Diablo II: Resurrected in d2r-mobile: much of the host was brought up on it. Plays on the iPhone. The README lists its IPA at about 4 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Resource heap +16 KB | [`patches.py`](patches.py) `CUTS`, v0 `main` `cd6e42ae…` at `main+0x1e62fc`, 1.2.1 `main` `ff773e90…` at `main+0x1cf9b0` | a resource heap came up about 0x70 bytes short; see [Memory](#memory) | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0xB8000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | 256 MB in front + 2.875 GB alias region + 256 MB stack region + 1.625 GB heap region: the game maps 1.75 GB of virtual address memory on demand into the alias region and asks for a 1.47 GB heap | +| `shader_versions v0 v327680` | `game.tsv` | the shader tables are the union of both versions, so packing either needs both NSPs | + +No `dm/tools/sdk_patches.py` cut for its `sdk` builds (`3053b229…` v0, `e4bd58ca…` 1.2.1). No `render_cond`: a minute's boot counted 65,536 or more clears under render conditions, and the game needs none. No `pad_layout`: the virtual gamepad starts in the 2D layout. + +## Versions + +- v0 (1.0.0) and 1.2.1 (v327680). With the update layered over v0, unchanged files become APFS clones, so the merged RomFS adds only about 114 MB to the 3.5 GB base. +- 1.2.1 is a new `main` build (SDK 16.2.12). Its resource-heap cut was carried over from v0 with `dm/tools/nso_fnmap.py` (containing function `0x1e603c` → `0x1cf6f0`, identical after address masking; `ExpHeap::create` `0x4aac38` → `0x4c4a18`). +- 1.2.1 imports 28 SDK functions v0 did not, which the host answers: `nn::nfp` (amiibo, initialized at boot) finds none, `pl::IsRunningOnOunce` (is this a Switch 2) is false, `ec::ShowShopApplicationInformationForUpgrade` is a no-op, `account::PopOpenUsers` returns none and `aoc::CountAddOnContent` none. Mario only reads the count from `PopOpenUsers` and falls back to `TryOpenPreselectedUser` on 0, and counts add-on content only on a Switch 2 (`hle_sdk16.c`, `hle_account.c`, `hle_misc.c`). +- 1.2.1 compresses the NACP's title area (a u16 size, then raw deflate inflating to 32 entries of 0x300 bytes), which LibHac 0.19 does not decode; `nx_extract` and the NSP script do. Its patch NCA (key generation 17) makes hactool stop with `Invalid BKTR layout!`, which is why `nx_extract` uses LibHac. + +## Memory + +- The game reserves "virtual address memory" (1.75 GB in `System/HeapSize/*.HeapDef.byml`) and maps it into the alias region with `MapPhysicalMemory` on demand. With the alias region left after the node area too small, large resources were silently skipped and a null deref followed where they were used, also visible as `SetHeapSize` asking for much less than the pool. The 5 GB layout above fixed it. +- Its two modules are about 74 MB, and its heap request matches hardware with the host's 64 MB of "used" memory. +- **Current: every resource heap gets +16 KB.** Each resource gets its own `sead::ExpHeap` sized resource-size-table value + 0x1008 (`main+0x1e603c` → `ExpHeap::create` `main+0x4aac38`, v0). The table values are Nintendo's measured peaks with almost no margin (Yoshi's animation file `PlayerYoshiAnm` uses 99% of its budget). `Model/NpcCommonerA.bfres` in the opening course (Course900) came up about 0x70 bytes short: its last GPU chunk (MatBlock, 0x9000 bytes, 4 KB aligned, `main+0x4290c0`) fit, but the chunk's 0x598-byte descriptor did not; a buffer was then built on a null memory pool and the game crashed on a null read at `main+0x106f5c8`. The cut makes `mov x0,x23` → `add x0,x23,#0x4000` (the `ExpHeap::create` size argument). +- The single 128 MB NVN virtual memory pool (flag 0x200) is mapped onto one physical pool in one request, so the host aliases it (`hle_nvn.c`). Before, a failed `MapVirtual` gave a buffer with a null pool right after a scene change. +- How the heap was traced: probes on the gsys GPU allocator (`main+0x4290c0`) and the heap's `tryAlloc` (vtable slot +0x30); the `!xN` heap walk uses Mario's sead layout by default (`DM_PROBE_HEAP` `0,28,30,b0,c8`; [05, Probes](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe)). + +## Frame rate + +- **Current:** 60 fps with a dynamic present interval. The game times every frame (`main+0xcfd24`); after a frame that ran past one vertical sync it sets present interval 2 for the next frame and sets its own time step to the interval (a float at `main+0x36ceff0`), so that frame catches up 2/60 s (Mario 1.2.1 on the iPhone). A user's 2-minute iPhone session switched 123 times, mostly for one frame. +- **Resolved:** in `display` mode those frames were shown for 16.7 ms and the game visibly jumped ahead ("sometimes the speed increases when jumping"). In `game` mode (the default since 2026-10-15) the display link keeps a frame whose period is over 25 ms on screen for its whole period (`g_link_hold_until` in `nvn_mtl.m`), and the 30 fps target waits until interval 2 has held for a full second (`nvn_game_fps` in `hle_nvn.c`), so one-frame switches no longer flip the display-link mode. `DM_TEST_PRESENT_LAG=90,30` (30 ms before every 90th present) makes Mario do this on the Mac; run it with `DM_WINDOW=1` to get the display link. +- `core_preempt` stays off: with it Mario's loader threads waited for busy decompression threads and its first frame moved from 0.6 to 2.9 s. +- The precise `SleepThread` (`sleep_precise`, found on Metroid Dread) took Mario's frame-time p95 from 19.7 to 18.4 ms. +- The spin-wait rewrite finds 175 loops in `main`; the store rule added one more in each build. + +## Rendering + +Game-specific findings; the general host changes they led to are listed under [Found on this game](#found-on-this-game). + +- **Resolved: tall blue / textured panels flickering over the fountain.** Mario writes the same uniform slot twice in a row each frame (4 bytes of −1, then 16 bytes of wave coefficients). Without an intra-encoder barrier the writes occasionally landed in reverse order, the fountain's GPU wave simulation (a 512×1 RGBA16F texture, render → copy back → splash pass every frame) ran with −1 as its self coefficient, diverged to ±65504, and its vertex shader pushed the water surface to the top of the screen. How it was found ([05, Intermittent rendering glitches](../../../docs/agents/05-debugging.md#intermittent-rendering-glitches)): screenshots of frames 4430–4590 scored by a metric tuned to the glitch (long vertical straight edges in a sky region: ≈1.0 against ≈0.3), a control run, `DM_NVN_SKIP_FS` bisection, `DM_NVN_CBDUMP` showing byte-identical inputs, then `DM_NVN_T2T_TRACE` and a numpy re-implementation of the shader step: a coefficient of −1 instead of 1.81 explained every bad texel. `DM_NVN_BLIT_ORDER=0` reproduces it. +- **Resolved: the whole picture darker and more saturated (Mac and iPhone).** Mario's last pass writes the `RGBA8` swapchain texture through an `RGBA8_SRGB` view (`TextureViewSetFormat` 0x38 on a 0x25 texture); the host attached the base texture and stored the linear values unencoded (against a Switch screenshot, 0.55 → 0.26, 0.75 → 0.52). It is the only such view, 1 of 80 passes in a course frame. +- **Resolved: menus and dialogue boxes with no text.** Glyphs are rasterized on the CPU into a linear 2048×8192 R8 texture (flags IMAGE | LINEAR) in a CPU-visible pool; the font atlas is sampled with the swizzle (1, 1, 1, R), without which glyphs were white boxes. +- **Resolved: a black sky.** Four 64×1 RGBA16F sky colour ramps are CPU-written linear textures. +- **Resolved: a black band and flicker (iPhone; Mac runs of the scene looked clean).** Mario writes the byte 1 at `NVNcommandBuffer` +0x10 (`main+0x4da5c`, `main+0xb8dec`), which was the low byte of the host's `end`. Each write shrank the 512-byte material setup blocks, their recordings overflowed into host segments, `GetCommandMemoryUsed` returned 0, the game marked the setups empty and skipped calling them, and draws ran with the previous draw's vertex buffers (out-of-range vertices, NaN). `cmd_t` now reserves the byte (`nvn_flag10`). +- `NVNprogram` is smaller in Mario than the 0xC0 bytes in `nvn.h`: a 168-byte host record corrupted the next object (a crash in SHA-256 on the code address 0x16300), so `prog_t` stays 112 bytes. +- Mario calls `nvnProgramSetShaders` for some programs with a code address not yet mapped, so such a stage is hashed at its first draw. +- Its transient render targets are new texture objects every frame over the same storage, never finalized; without sharing host textures by `(pool, offset, description)` they leaked about 13 MB a frame. +- 8-bit index buffers in its full-screen passes and final copy; scissored zero clears of sub-rectangles of small targets (pyramid levels packed into 480×272, 256×256 and 64×36 targets); a depth clear scissored outside a pass, which the host still clears whole (`depth-stencil clear with a scissor or partial stencil mask, cleared whole`). +- Its 4×4 fallback textures (black, white, transparent, red) are filled with `nvnTextureWriteTexels`; since they upload from guest memory, materials that sample the default white texture got slightly brighter. +- Same-pass feedback draws `a7e5c6fd501cf21f` and `22a61cb1ec732aef` on its RG11B10F scene have shown no magenta blocks on the iPhone so far, so the profile lists no `feedback_snap`; snapshotting every shader raised submit time from 1.4 to 4.0 ms. +- Uniform updates inside a pass: attachment loads and stores unchanged by the deferred updates (its updates already covered what the draws read). +- It ships agl's BC4 block encoder `0544282c162305db` (the shader Tears of the Kingdom draws into a BC4 3D texture). +- Mario keeps NVN's GL depth mode (near = −w). + +## Shaders + +- 12,811 shaders in v0 and 16,268 across both versions; 16,266 compile. In v0 the main `bfsha` held only 4,888 of the 12,811. +- Containers: `Shader/*.bfsha` (BNSH), `Lib/agl/*.sarc` + `Shader/Secred.sarc` (agl BAHS `sharcb`), `Effect/*.esetb` (BNSH), `Layout/*.blarc` (BNSH) and two BNSH inside `main`. Groups in `game.tsv` (`SecredUber`, `agl`, `effect`, `layout`). +- Texture handles at `0x20 + 8 × slot` (the docs call it "Mario's binding layout"). 38% of texture reads go through handles stored in the game's uniform buffers (`fp_t_cb7_2A`: word 0x2A of c[7] = uniform slot 4), the binding kinds `tex_ubo` / `samp_ubo`. +- Many shaders share a control section and differ in code, so keys hash both. +- All 71 compute shaders have a header (magic `0x12345679`) and zero padding, instructions at +0x100. Fed from +0x30 at first, 12 compute shaders (particles, post effects) translated into an endless loop that hung the GPU at the first gameplay scene. +- 2 agl post-effect compute shaders still produce SPIR-V that SPIRV-Cross cannot parse (7 before the compute start-offset fix). +- Integer vertex inputs: 529 of 4,854 vertex shaders are re-translated with `uint` inputs (skinned models read bone indices as integer bits; without it every character was stretched into streaks). +- Pairs: 331 of 9,043 vertex / pixel pairs failed to link before `vs_pad` (the opening course used none of them; later levels did, about 11,500 failed builds in one 11-minute session). The offline link check: 8,712 linked / 331 failed before, 9,043 / 0 after, 14,603 / 0 for the v0 + 1.2.1 tables. +- Vertex format `RGB10A2SN` (0x69) → `MTLVertexFormatInt1010102Normalized`. + +## Audio + +- The `sdk` carries the audio renderer (SDK 16): the host switches it to CPU rendering with manual execution and drives it from a guest thread, no IPC ([03, Audio](../../../docs/agents/03-bringup.md#audio)). +- **Resolved:** Mario updates the renderer twice in a row at start-up; with a real audio device the first frame came after both, and the light limiter later crashed on a null state pointer (`ApplyLightLimiterEffect`, `sdk+0x228494`). The `AudioRenderSystem::Update` hook renders a catch-up frame first when nothing was rendered since the last update. + +## Services and survey + +- `main` imported 1,107 symbols, 692 of which D2R never imported (`nn::audio` 197, `nn::hid` 48); 172 of the host's 186 hook names then existed in its `sdk`. Its SVC set was a subset of D2R's. +- NVN entry points: the loader (`main+0x1070910`) calls `GetProcAddress` per name and stores through the GOT (`nvn_slots_got.py`). It fetched 534 functions and called 236; 79 were new to the host (CommandBuffer 31, Texture 15, Buffer 10, Sampler 10). +- Its device setup checks `nvnDeviceGetInteger(API_MAJOR/MINOR)` and returns silently on a failed check. +- Files: `System/Resource/ResourceSizeTable.*.rsizetable` (RESTBL), `Font/*.bfarc`, `Mals/.sarc`, `Voice/Resource/`; `*.zs` is plain zstd with no dictionary; BYML version 7. +- Fonts: SDK 16 and `main` imports `ScalableFontEngineHelper::Decode`, so the shared font is served as BFTTF (marker + size + TTF). Controller-button icons in text come from the Nintendo-extension font (`GameMsg/Key.msbt`: U+E0E0–E0F0, U+E0AB–E0AE, U+E090–E097, U+E100/E101), served from `make_ext_font.py`'s `nintendo_ext.ttf`. +- **Resolved:** a crash indexing a per-core table with index −2 in Mario's font engine (`main+0x107dbc0`): threads created with ideal core −2 now resolve to the process default core. +- Focus handling mode 2 (focus notifications are sent). Mario never throws C++ exceptions. +- Ghidra: `main` (28 MB) took about 35 minutes to analyse, 138k functions. + +## Mac routes + +- `DM_KEYS="12:A,14:A,20:A,26:A,32:A"` reaches the opening course. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **NVN on Metal basics** in [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario): 8-bit index widening, texture swizzles (`nvn_fmt_to_mtl`, `img_fit`), format-changing render-target views (`attach`, `present_raw_src`), storage-shared transient targets, inline uniform updates as in-order copies snapshotted at encode (`mtl_sub_update`), CPU-written linear textures on the pool buffer (`tex_linear`), integer vertex inputs (`int_inputs`, `vfmt_for_ct`), intra-encoder barriers (`blit_buf`, `DM_NVN_BLIT_ORDER`), zero-filled new textures (`tex_zero_fill`), scissored zero clears (`clear_rect_zero`), vertex output padding from shader pairs (`vs_pad`, `shader_pairs.tsv`), virtual pools aliased onto one physical pool. +- **Binding layout and handles in uniform buffers** (`NVN_RB_TEXU` / `NVN_RB_SAMPU`): [04, section 3](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census); the compute code start: [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **The SDK's audio renderer in-process** and the catch-up frame between two updates: [03, Audio](../../../docs/agents/03-bringup.md#audio). +- **Resource heaps cut by build ID** (`CUTS` in `patches.py`), **memory partition** (`mem_init`), **virtual NVN pools**: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space) and [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Update imports diffed before the first run**, **fixed-address code gated by `module_t.build_id`**, **BFTTF shared font** (`hle_sdk16.c`), **the extension font**, **`GetCommandMemoryUsed` counting host segments**, **`NVNcommandBuffer` +0x10** (`nvn_flag10`), **`prog_t` size**: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk) and [03, NVN object sizes](../../../docs/agents/03-bringup.md#nvn-object-sizes-memory-corruption). +- **Exclusive windows** (fall-through exit first, Mario had 82): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). **Ideal core −2**: [03, Threads](../../../docs/agents/03-bringup.md#threads). +- **Present-interval holds** (`g_link_hold_until`, `nvn_game_fps`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **Glitch detector before bisection**: [05, Intermittent rendering glitches](../../../docs/agents/05-debugging.md#intermittent-rendering-glitches); **offline pipeline link check**: [05](../../../docs/agents/05-debugging.md#offline-pipeline-link-check). + +## Open + +- On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone), per development.md's known gaps. +- Why the resource heap is short is not known (`patches.py`: every allocation size before that block is set by game code and data); Breath of the Wild's identical failure came from host record sizes, and whether Mario's chunk is sized the same way is not checked yet. +- dm/games/README.md says the heap check turns "Wonder's 1.44 GB" request into a 0xc0000000 alias region; 03 says it asks for 1.47 GB, and the profile's alias region is 0xB8000000. +- Several addresses in 02 and 03 (`main+0x1070910`, `main+0x107dbc0`, `main+0x4da5c`, `main+0xb8dec`, `main+0xcfd24`, `main+0x36ceff0`) do not name their build; the present-interval ones are from 1.2.1. +- 2 agl post-effect compute shaders do not translate. diff --git a/dm/games/0100152000022000/README.md b/dm/games/0100152000022000/README.md new file mode 100644 index 0000000..66a5e73 --- /dev/null +++ b/dm/games/0100152000022000/README.md @@ -0,0 +1,68 @@ +# Mario Kart 8 Deluxe (`0100152000022000`) + +Update 4.0.0 (v1441792) over the base game, SDK 4.4.0, the oldest SDK so far. Nintendo's sead / agl / gsys engine with NintendoWare (G3d, Ui2d, Vfx, Atk) and Pia networking, on NVN. Modules `main`, `subsdk0` (the SDK's movie library: the menus' H.264 previews) and `sdk`. RomFS is mostly Yaz0-compressed SARC (`.szs`), BFRES models and BNTX textures, which the game reads itself. Plays on the iPhone (A20 Pro): 60 fps docked with about 6 ms of GPU time a frame, a 2.4 GB footprint with 3.7 GB of headroom, no audio underruns. Mii heads draw nothing (no firmware); see [Miis](#miis). + +Brought up on the Mac from 4.0.0: boot, the title with its demo race, the Mii pick, the menus (their movie previews play), a 50cc Grand Prix race at Mario Kart Stadium to its results, and a second boot that loads the save. 60 fps (p50 16.7 ms), sound; about 2.4 GB resident and 1.26 GB of Metal allocations in the race, flat over minutes. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `render_cond on` | [`game.tsv`](game.tsv) | the race records `SetRenderEnableConditional` as a course loads: evaluated at replay, as on the GPU (drawn anyway, the same race route at the same frames looked the same; evaluated is NVN's behaviour) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no virtual address memory: the game sets a 3.03 GB heap (pool − 64 MB) in the 3.25 GB heap region | +| `sys_resource 0x0` | `game.tsv` | the NPDM declares 0 MB of system resource: `IsVirtualAddressMemoryEnabled` is false, as on Horizon | +| `modules main subsdk0 sdk` | `game.tsv` | every module `main` imports from | +| `shader_groups` (`/MapObj/`, `/Course/`, `/UI/`, `/Kart/`, `/Driver/`, …) | `game.tsv` | the shader split's groups; see [Files and shaders](#files-and-shaders) | + +## Miis + +The game builds Mii heads from the console's MiiModel system data, starting at boot for the title's demo race, and lists the six default Miis for its "Select a Mii". Firmware content is not the host's to ship, so Mii heads draw nothing while bodies and favourite colours show. + +- **Current: the Mii database** (`hle_mii.c`). The first boot logged `[RED] sf: the game connects to service mii:e … mii:u itself`, then aborted: `nn::mii::Database` keeps an `IDatabaseService` of the mii service at +0 (`Database::Initialize`, `sdk+0x1c5664`), and `AppendMii` / `ShowMiiEdit` start the MiiEdit applet. The host takes over the public methods: a console with no Mii of its own (an empty database: no index, nothing to update) that still has the six default Miis every console has. The game asks for those with `Get(…, 6, SourceFlag 2)` and never reads the count (`main+0x81e954`): its boot-time list of Mii icons (`main+0x419dc8`) builds a head model for each of the six, and zeroed entries failed the model's `CharInfoAccessor::IsValid` (`CharModel::Initialize` returned 0xd67e, `main+0x8f15b0`) and crashed at `main+0x8f152c`. A CharInfo the host builds is the default Mii's parameters with Switch common colours, a create ID with version 4 and variant bits (fixed per default Mii, so a saved choice finds the same Mii; fresh for `BuildRandom`, which the title's demo race calls for 12 racers) and the nickname "no name"; every field is within `CharInfoRaw::Verify`'s ranges (`sdk+0x1b68c8`). The Mii editor is cancelled (0xE7E, which `ShowMiiEdit` itself treats as success). +- **Current: the MiiModel stand-in** (`dm_mii_standin` in `hle_mii.c`, mounted by `hle_fs.c` `MountSystemData`; any other system data answers "target not found"). The game aborted in `fs::detail::GetFileSystemProxyServiceObject` under `fs::QueryMountSystemDataCacheSize` (`main+0x8f23bc`): it mounts system data 0100000000000802, MiiModel, as `MiiResource:` and reads `NXTexture{Low,Mid}{Linear,SRGB}.dat` and `Shape{Mid,High}.dat` whole; the SDK's Mii rendering is linked into `main` (`main+0x8f1400`–`+0x8fc000`). The host writes a stand-in of the same layout with every part empty. The layout as the code reads it: + - texture file: +0xc the largest decompressed size of each of 11 part kinds, +0x38 their alignments, from +0x64 one 0x1c-byte entry per part (offset, size, stored size, 4 zlib parameters, then width, height, format, mips); + - shape file: +0xc / +0x3c the same for 12 kinds, from +0x6c one 0x58-byte entry per part (offset, size, stored size, parameters, five vertex attribute offsets and sizes, index offset and byte size, bounding box), then hair (132 × 0x48, +0x12704) and faceline (12 × 0x24, +0x14c24) transforms. + - A texture of size 0 is skipped (`main+0x8faaf4`). Shapes cannot all be absent, because the faceline is drawn without a check (a null buffer read at `main+0xa690d0` under `main+0x8286c4`), so every shape entry points at one shared mesh of one vertex and the triangle (0, 0, 0), 32 bytes of zeros in a stored zlib block. The largest sizes must not be 0: they size the model's memory pool (`main+0x8f2e6c`), whose alignment is the first one. + +## Files and saves + +- **Resolved: SIGSEGV in `crypto::detail::CbcModeAes128Helper::DecryptBlocks` with a null destination** (`main+0x8737e8`, right after `aoc:` is mounted). The add-on content (0100152000023001: `contents.dat` and `version.txt`; the courses are in the update) is decrypted in `AocHeap`, a 0x3130-byte heap that also holds the mount cache. The host answered `QueryMountAddOnContentCacheSize` with 4 KB, and the 0x1400-byte decryption buffer then did not fit. On Horizon the cache is the RomFS's file table as `RomFsFileSystem::GetRequiredWorkingMemorySize` sizes it: both hash tables (4 bytes a bucket, the builder's bucket count) and all directory and file entries (0x18 / 0x20 bytes plus the name, padded to 4), 0x88 bytes for these two files. The host computes that from the folder it mounts (`romfs_work_size` in `hle_fs.c`); `rom` keeps its minimum answer, which is too small rather than too large. +- **Resolved: every boot asked to pick a Mii again** (`DM_FS=1`: `UserSave:/userdata.dat` not found although the host save had it). The game mounts `CommonSave` with `MountDeviceSaveData` and `UserSave` with `MountSaveData(uid)` (`main+0x8b0c84`), two saves on Horizon; the host made device save data the account save (for Animal Crossing: New Horizons, which mounts only device save data), so both names shared one directory. At boot the save manager deletes `CommonSave:/userdata.dat` (`main+0x8b1910`, through its file device's `DeleteFile`, `main+0x633afc`; a probe there with `@*x1+8` shows the name) and commits, and in the shared directory that was the account save's own file. A game whose `main` imports `MountSaveData` too now gets its device save as `_device`, a transaction of its own (`hle_fs.c`); of the games on hand only this one imports both. +- `romfs_marker` is `UI/cmn/movie/StaffCredits.mp4`. + +## Audio + +- **Resolved: no sound; `[RED] audren: no DeviceSink (ID 0x10) in the command list, 0 commands of 0x148 bytes`.** SDK 4's command list has SDK 6's header, but its DeviceSink (`CommandBuffer::GenerateDeviceSinkCommand`, `sdk+0x19e690`) is ID 0xf and 0x130 bytes: device name +0x14, upsample buffer +0x118, input count +0x120, input indices as bytes from +0x124. `capture` takes the one 0x130-byte command with 1 to 6 inputs when there is no 0x148-byte one (`g_sink4` in `hle_audren.c`). +- **Resolved: the update lock.** Its `AudioRenderSystem::Update` (`sdk+0x19b77c`) locks an `nn::os::MutexType` at +0x1d0, not SDK 16's `SdkMutexType` at +0x1f0. The host took SDK 16's for every SDK before 20, which wrote a lock word into another field and excluded nothing; this was the first game it was plainly wrong for. `update_lock` in `hle_audren.c` now reads it off each SDK's own `Update`. + +## Rendering + +- **Resolved: a 2D array in a cube slot.** As a race loads, the game binds a one-layer 2D array (format 0x44) at slot 10 of two pixel shaders that declare a cube there, and it was bound as is (`texture type mismatch … declares slot 10 as Metal type 5, texture … is type 3`). The zero cube is now bound for a 2D array of fewer than six layers in a cube slot (`tex_fit`). +- The race records conditional rendering as a course loads; `render_cond on` evaluates it at replay. + +## Files and shaders + +- 29,650 shaders (12,529 vertex, 17,092 pixel, 28 compute, 1 geometry): BNSH in the courses' and objects' `.bfsha` / `.bfres` archives inside the `.szs` SARCs (courses, objects, karts, drivers), the effects' `.ptcl`, the layouts, agl's two BAHS sharcb and 37 in `main`; 18,747 variation pairs. +- The NSP script's Mac-only build (`--platform mac`) saves about 30,000 iOS shader compiles and the 12 GB IPA for this game ([development.md, A Mac-only build for development](../../../docs/development.md#a-mac-only-build-for-development)). The player README gives the IPA as about 11.9 GB. + +## Rewriter + +- The spin-wait search with the lock, test and store rules finds `main` 25 → 26 loops (a state setter like Breath of the Wild's, `main+0x715df8`) and one loop wider in `subsdk0` (the movie library's thread loop, `0x24118`) ([development.md, Power](../../../docs/development.md#power)). + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL`. +- `run/race.sh` boots from a copy of a save past the Mii pick into a 50cc Grand Prix (Mario, Mushroom Cup) with frame-timed keys and holds accelerate: `DM_KEYS=f960:L+R~0.3,f1260:A,f1500:A,f1740:A,f2000:A,f2240:A,f2400:A,f2560:A,f2800:A,f3040:A,f4300:A~70`. +- From a fresh save, L+R at the title opens "Select a Mii" (Right, A takes the first default Mii). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The Mii database and the MiiModel stand-in** (`hle_mii.c`, `dm_mii_standin`, `MountSystemData`), **the add-on content cache sized as Horizon's RomFS working memory** (`romfs_work_size`), **device save data as a save of its own** for a game that mounts account save data too (`_device`) and **SDK 4's DeviceSink** (`g_sink4`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **The audio renderer's update lock read off each SDK's `Update`** (`update_lock`): [03, Audio](../../../docs/agents/03-bringup.md#audio). +- **The zero cube for a 2D array of fewer than six layers in a cube slot** (`tex_fit`): [04, Seen in Pokémon Legends: Z-A](../../../docs/agents/04-graphics.md#seen-in-pokémon-legends-z-a). + +## Open + +- Mii heads draw nothing: the host ships no MiiModel firmware data (the player README notes "no Mii heads"). diff --git a/dm/games/010020400E1C2000/README.md b/dm/games/010020400E1C2000/README.md new file mode 100644 index 0000000..c2fd1d3 --- /dev/null +++ b/dm/games/010020400E1C2000/README.md @@ -0,0 +1,44 @@ +# Raging Loop (`010020400E1C2000`) + +Base game 1.0.0 (v0), SDK 7.3.2. Unity 2018.3.10f1 IL2CPP with FMOD on NVN. Modules `main`, `subsdk0` (the movie module, which plays `StreamingAssets/Movies/*.mp4`, H.264 Main and AAC) and `sdk`; `sdk` and `subsdk0` are DRAGON QUEST XI S's builds ([01006C300E9F0000](../01006C300E9F0000/README.md)), so that build's cuts apply as they are. No host change was needed. Playable on an iPhone (see [Open](#open)). On the Mac: the publisher logo, the opening movie with its sound, the title, options, a new game through the tutorial and the prologue (the date card, the opening pictures, choices, the in-game menu, scenario chart and log), at 60 fps as on the Switch (the game asks for present interval 1), with saves; no red line at run time. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| DRAGON QUEST XI S's `sdk` cuts: node area 1 GB → 62 MB, regions of 1 GB and more aligned to 32 MB | `dm/tools/sdk_patches.py`, `sdk` build `fcf93a41…` (the constructor at `sdk+0x32c450`, cuts at `+0x32c460` and `+0x32c500`; `AllocateAddressRegion`'s alignment at `+0x32c66c`) | they put StandardAllocator's region 64 MB into the alias region; see [Memory](#memory) | +| `sys_resource 0x1000000` | [`game.tsv`](game.tsv) | the NPDM's 16 MB of system resource: virtual address memory | +| 5 GB guest layout with the hardware pool: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | Engage's block (256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region); Unity 2018.3 maps only inside StandardAllocator's region, so the pool stays as on the Switch | +| `shader_groups /Data/=data` | `game.tsv` | | +| No `pad_layout` (the 2D default) | `game.tsv` | controller only; see [Input](#input) | + +## Memory + +- The NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true: `nninitStartup` hands application pool − used memory to StandardAllocator as virtual address memory, one address region in the alias region, and the game never calls `SetMemoryHeapSize` (no heap). +- Unlike Unity 2020 and 2022 (Ace Attorney Investigations Collection, Monster Train 2: a 1 GB pool), Unity 2018.3 reserves no address regions of its own: every page the game maps lies in StandardAllocator's region, which ends at +3.21 GB of the 4.38 GB alias region. So the hardware pool (3.2 GB) leaves no address space short, and only mapped pages cost memory: 377 MB mapped at the peak over the title, the tutorial and the opening (Mac, `DM_SVC` map log). The NSP script's heap check keeps the hardware pool for this game for the same reason ([dm/games/README.md, Adding a game](../README.md#adding-a-game)). +- On the Mac about 0.6 GB resident at the title and 0.77 GB after ten minutes of story (it grows with each new picture and slows down), 0.4 GB of Metal allocations. + +## Input + +- Controller only: the game reads no touch, docked or handheld (a handheld run with taps on the title's READ logged no touch read). It stays docked at 1920x1080 with the 2D pad layout. Menus answer the left stick as well as the D-pad, and no menu item is selected until a direction is pressed. A advances and selects, B goes back, X opens the menu, Y the scenario chart, L the log, R auto mode. + +## Files and shaders + +- The story's pictures are loose PNG / JPG files in `StreamingAssets` that the game loads itself. +- 452 shaders: the 48 `Shader` objects of the player's serialized files (`Data/*.assets`, the Unity built-ins in `Data/Resources`; 450 subprograms) and the player's 4 GLSLC outputs in `main`. +- **Resolved: the split stopped with "no bundle has a Shader type tree of the same hash to borrow"** (hash `8db76007…`). There are no bundles, and no game of that Unity version with a bundle was at hand, so `SHADER_2018_3` in `unityfs.py`'s `KNOWN_TREES` is written from AssetRipper's TypeTreeDumps (`StructsDump/release/2018.3.10f1.dump`), checked node for node against the dump (1,325 nodes, the alignment flags included). It is a layout of its own, older than the common structs: no conservative raster state, `BufferBinding` without `m_ArraySize`, `ConstantBuffer` without `m_IsPartialCB`, the parameters in each subprogram and no `m_CommonParameters`, one segment per platform (the pre-2019.3 form Cuphead's trees gave `shader_programs`). Each of the 48 objects reads to its exact end: 450 subprograms of version 201802150 (older than the local keyword list, which starts at 201806140). +- The two built-in `ComputeShader` objects in `unity default resources` stay unread (a `[RED]` line in the split): their 2018 layout lists kernels with no unique variants. The game dispatches no compute. +- To take one file out of an NSP without extracting the RomFS, filter `nx_extract`'s `Copy` by path. + +## Mac routes + +- From a fresh save, `DM_KEYS="f700:A,f900:LLeft,f1000:LLeft,f1100:LLeft,f1200:A,f1500:A*14/120,f3300:LDown*5/60,f3640:A,f3900:A*35/120"` skips the opening movie, starts READ, reads through the tutorial, picks "I'm good (to main story)" and reads into the prologue (the motorbike picture at about frame 6,600). + +## Found on this game + +- **Unity 2018.3's `Shader` layout written out** (`SHADER_2018_3`, from AssetRipper's TypeTreeDumps): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **The heap check keeps the hardware pool** for a Unity 2018.3 game that maps only inside StandardAllocator's region: [dm/games/README.md, Adding a game](../README.md#adding-a-game). + +## Open + +- Status disagrees: the `game.tsv` header says "Plays on the Mac", docs/development.md says playable on an iPhone. diff --git a/dm/games/010020D01AD24000/README.md b/dm/games/010020D01AD24000/README.md new file mode 100644 index 0000000..ddf213d --- /dev/null +++ b/dm/games/010020D01AD24000/README.md @@ -0,0 +1,39 @@ +# ANIMAL WELL (`010020D01AD24000`) + +Update 1.0.10 (v655360), SDK 20.5.7, on NVN (46 BNSH shaders). Modules `main` and `sdk`. All its assets, the shaders among them, are embedded in `main`: the program has no RomFS. The standard host and rewriter run this build with no game-specific fix: no shader overrides, rewriter cuts or host changes. Only this update is registered in the profile. The unsigned IPA is confirmed playable on the iPhone. + +On the Mac: the opening rooms at 60 fps with movement, jumping and audio; item pickup and pause menus work. One repeat run recovered from an AudioUnit startup timeout through the existing audio restart path. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `romfs_marker` empty | [`game.tsv`](game.tsv) | the Program NCAs have no data section: `nx_extract` creates an empty RomFS directory, which the empty marker checks; see [Files](#files) | +| 3.5 GB layout: `mem_reserve 0xe0000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the provisional layout, which holds: a 256 MB alias region, a 256 MB stack region and a 2816 MB heap region. `main` sets a heap of at most the pool − 64 MB − 536 MB of bss (counted as used memory) | +| `sys_resource 0x0` | `game.tsv` | the NPDM declares 0 MB of system resource: `IsVirtualAddressMemoryEnabled` is false, as on Horizon | +| `pad_layout simple` | `game.tsv` | the 2D virtual pad; B jumps | +| `shader_groups /=romfs` | `game.tsv` | the shaders are in `main` | + +## Files + +- Both the base and the update Program NCAs have no data section. The assets live in `main`, whose read-only segment is about 32 MB. +- **Current:** `nx_extract` creates an empty RomFS directory (and a header-only survey manifest) when neither program has a data section. Base and update extraction were checked in both full and manifest modes. + +## Shaders + +- **Current:** 46 BNSH shaders embedded in `main` (1 vertex, 45 pixel), all translated for Mac and iOS by the existing splitter and translator. The prewarm list covers the opening rooms. + +## Memory + +- `main` has about 537 MB of bss, counted by the existing big-module memory rule. +- During a four-minute Mac run, resident memory stayed near 855 MB and Metal allocations at 159 MB. + +## Found on this game + +- **An empty RomFS for a program without a data section** (`nx_extract`; an empty `romfs_marker` checks the directory itself): [dm/games/README.md](../README.md). + +## Open + +- Save and reload are unverified: the test did not reach a save checkpoint. +- The bss is 536 MB in `game.tsv`'s comment and about 537 MB in the bring-up notes ([Memory](#memory)). +- The player-facing [README](../../../README.md) lists the games that start in the 2D layout and says the others start in 3D; ANIMAL WELL is not in that list, but its profile has `pad_layout simple` (2D). diff --git a/dm/games/010028600EBDA000/README.md b/dm/games/010028600EBDA000/README.md new file mode 100644 index 0000000..5fa64da --- /dev/null +++ b/dm/games/010028600EBDA000/README.md @@ -0,0 +1,67 @@ +# Super Mario 3D World + Bowser's Fury (`010028600EBDA000`) + +Update 1.2.2 (v262144) over the base game, SDK 10.4.0. Nintendo EPD's engine (sead / agl, NintendoWare G3d / Ui2d / Vfx / Atk), a Wii U port, on NVN API 53. Modules `main` and `sdk`. Plays on the iPhone. The README lists its IPA at about 3 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `feedback_snap 66a71a3081b91b9a 30e3ccb0bd304ead` | [`game.tsv`](game.tsv) | the final composite samples the attachment it renders into: solid magenta 16×16 blocks on the iPhone; see [Rendering](#rendering) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the right stick turns the camera | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource in the NPDM: the game uses `SetHeapSize` (`main`'s bss is only about 7 MB). 13 Sentinels' partition, "until measured" | + +No `patches.py`, no `dm/tools/sdk_patches.py` cut for its `sdk` build (`204eaef8…`). `shared_font` and `device_api` are decided by its SDK 10, so no profile key sets them. + +## Rendering + +- **Resolved: daylight scenes washed out to white.** The game creates its deferred pre-lighting buffer (the G-buffer's fourth target, R11G11B10F) as a one-layer 2D array texture, and the lighting shader samples it as 2D. On Maxwell a non-array sample of an array texture reads layer 0; on Metal the type mismatch returned one fixed value for every read, so the whole picture got one pale ambient colour (bloom blew it out further). The host compares each binding's declared type (the reflection's `dtt`) with the texture and binds a compatible view (`tex_fit`). How it was found: a blit-based pass snapshot of that target showed a correct image while a lighting shader edited to output only that input showed a uniform colour, both resolving to the same Metal texture. Earlier suspects that turned out correct: fog, bindings, the 1D lookup tables, clear colours, the depth convention, the final sRGB view, blend states, GPU luminance readback. There is no tone-mapping pass: the final composite writes `scene + bloom` into an RGB10A2 target. +- **1D textures**: the material shaders sample 64×1 R32F lookup tables created as NVN 1D textures (filled with `WriteTexels`); NVN 1D and 1D-array textures are now one-row 2D / 2D-array Metal textures, since translated shaders never declare `texture1d`. +- **Targets the fragment shader does not write**: volume draws after the G-buffer (`50d813761ca3f1f9` and others) write only outputs 0 and 2, with blending and all channels enabled on targets 1 and 3. Pipelines now get a zero write mask and no blending for colour outputs the shader does not write (reflection `colors`, `color_known`). +- **Current (iPhone): solid magenta blocks in World 1-1.** The blocks are exactly (255, 0, 255), hard-edged, aligned to the 16-pixel grid (16×16, sometimes two stacked). `DM_NVN_MAGSCAN` on the A20 Pro found them first in the 1920×1080 RGB10A2 composite (`ser=30`), in the pass whose shader `66a71a3081b91b9a` renders into that texture while sampling it. They kept appearing with the sparkle particle `5e4de7561c670fee` dropped, so that earlier suspect is not the cause. Writing and sampling one attachment in a pass is undefined in Metal: the M4 Pro happens to return the old content, the A20 Pro now and then returns whole 16×16 blocks as (1, 0, 1), probably blocks of the compressed render target caught mid-write. The host ends the pass before such a draw, copies the attachment's level and layer into a snapshot texture and binds it for that draw (`fb_prepare`), for the shaders `feedback_snap` lists: `66a71a3081b91b9a`, plus `30e3ccb0bd304ead`, which samples a G-buffer target of its own pass on the title screen. In 1-1 that is one 8 MB copy per frame. A sampled view that spans more levels than the attachment's one is left as before with a red line: `3e8567da37eb28a1` renders level 1 of a texture while its view covers levels 0–4 (mip-chain passes). +- **Scaled copy when leaving the title** (1920×1080 → 1280×720, RGB10A2, flags `ENGINE_2D`, probably the captured frame for the transition): Metal blits cannot scale. 04's 3D World section lists it as skipped with a red line; its Link's Awakening section says `copy_t2t_draw` now covers this copy. +- Depth mode: it keeps NVN's GL mode (near = −w). +- Uniform updates inside a pass no longer end it: 5% less attachment traffic on the Mac. +- 11 compute and 1 vertex shader read the driver segment `c0`; it rendered the same after compute `c0` got the dispatch size. +- agl's post-effect program behind `66a71a3081b91b9a` appears in Captain Toad: Treasure Tracker as `54f58a7cab608766` ([01009BF0072D4000](../01009BF0072D4000/README.md)). + +## Files and shaders + +- RomFS is almost entirely Yaz0-compressed SARC (`*.szs`), which `shader_split.py` decompresses itself (`yaz0`, about 20–45 MB/s per core; the whole 4.7 GB decompressed RomFS scans in about a minute). One SARC, `SystemData/LayoutShader.szs`, is big-endian (BOM `FE FF`), which the SARC reader does not handle; it holds no shader container the game uses. +- 11,080 shaders: bfsha (BNSH) and agl sharcb (BAHS version 9) in `ShaderData/*.szs`, particle effects (BNSH, including 25 compute shaders) in `EffectData`, six layouts and `SystemData/AglResource.szs` (BAHS), and two BNSH in `main`. All bind with Mario's texture handle layout (`0x20 + 8 × slot`); the one failure is a geometry shader (fur fins, `RenderFinFur`), which Metal cannot express. +- SDK 10's shorter control sections: 2,888 of 0x850 bytes (its ShaderData bfsha and six layouts) and 0x848 in BAHS version 9, each followed directly by other data, so keys hash `min(0x880, control +0x20)` bytes (`ctl_len`, `keys_hash`). +- Its particle compute shaders start their instructions at code +0, with no `0x12345679` header. + +## Services and SDK 10 + +- **Resolved: a null-pointer crash early in `nnMain`** (`main+0x759e08`: the device singleton `main+0x1a28ea0` still null): the game checks `nvnDeviceGetInteger(API_MAJOR / API_MINOR)` for major 53 and minor ≥ 313 (`main+0x711c88`) and quietly skips creating its device otherwise. The reported version now follows the SDK (`hle_nvn.c` `nvn_api`: 53.313 for SDK 10 and older). NVN 53 also brings `nvnQueueAcquireTexture`, `nvnSamplerCompare` and the debug group calls (no-ops). +- **Resolved: a crash after the shared fonts load, looking up the "?" glyph** (`main+0x6c73c0`): SDK 10's `ScalableFontEngine::LoadFont` failed with error 403 and `SetFont` with 301. The game records shared fonts as type 0 (`main+0x975658`), calls no `ScalableFontEngineHelper::Decode` and hands the `GetSharedFontAddress` pointer straight to `LoadFont`, so the pointer must be the plain TTF (`font_skip`). Found by probing the engine's error code after each call. +- SDK 10 calls that reach a service: `settings::LanguageCode::Make` and the reversed `operator==(LanguageCode, Language)` (the first comparison in `nnMain` aborted in `sm`), `account::ListAllUsers`, `oe::GetPerformanceConfiguration`, `oe::SetAlbumImageOrientation`, the `nifm` IP queries with SDK 10's `socket::InAddr` / `in_addr` mangling, `prepo::AddSessionId` / `AddInternetConnectionStatus` (entering the save select), and `vi::GetIndirectImageMemoryRequirement`. +- **Resolved: a null read in `applet::CreateLibraryApplet` under `swkbd::InlineKeyboard::Initialize`** (entering the save select after the character select): `hle_swkbd.c` takes over every inline keyboard entry that touches the applet; `GetImageMemoryRequirement` asks vi, hence the vi hook. +- **Resolved: SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then a hang**: the save thread has an 8 KB stack (CreateThread stack top `0x…9fe0`, fault `0x21a0` below it), and one file open took 6–7 KB of host stack. Every `hle_fs.c` hook now runs on the big host stack (`HLE_FORCE_BIG`). +- Bowser's Fury also uses `nvnTextureGetSwizzle` and `nvnTextureGetStorageClass`. +- Focus handling mode 3 (no focus notifications; the system suspends the game out of focus). + +## Input and Mac routes + +- After the title, a character select with four "L + R to join" slots: the game calls `hid::StartLrAssignmentMode` and waits for L and R pressed together on that screen (`L+R~0.5` while it is up; presses on the title before it are lost), then a character choice with A. +- On the game select, holding the stick right for a second (`7:LRight~1`) switches to Bowser's Fury. +- Map and level movement read the left stick only (`LLeft` / `LUp` / `LRight` / `LDown`). +- Into World 1-1 on a fresh save: `DM_KEYS=6:A,10:A,14:L+R~0.5,17:A*3/2,24:Plus*6/3,45:A*6/3,66:LUp~1,70:A,74:LRight~1,78:A,82:LLeft~1,86:A` (World 1-1 by frame ~6000). +- On the Mac, 3D World reaches World 1-1 and Bowser's Fury its open world (`game.tsv`). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Compatible texture views for a mismatched declared type** (`tex_fit`), **1D textures as one-row 2D**, **no writes to unwritten colour outputs** (`color_known`), **same-pass feedback snapshots** (`fb_prepare`, `feedback_snap`, `DM_NVN_MAGSCAN`): [04, Seen in 3D World](../../../docs/agents/04-graphics.md#seen-in-super-mario-3d-world--bowsers-fury). +- **SDK 10 control section lengths** (`ctl_len`) and **compute code without a header**: [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **The NVN API version by SDK** (`nvn_api`, `device_api`), **plain-TTF shared fonts by SDK** (`font_skip`, `shared_font`), **the inline keyboard** (`hle_swkbd.c`), **file system hooks on the big stack** (`HLE_FORCE_BIG`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk) and [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **Yaz0 SARC in `shader_split.py`**: [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **`L+R` joins in `DM_KEYS`**: [03, Game state](../../../docs/agents/03-bringup.md#game-state). + +## Open + +- The magenta-block fix: 04 says it waits for a device run; the README lists the game as tested and playable on the iPhone. +- The scaled copy when leaving the title: 04's 3D World section says it is skipped, its Link's Awakening section says `copy_t2t_draw` covers it. +- The fur fin geometry shader (`RenderFinFur`) does not build. +- `game.tsv` still says "In bring-up" and its memory values "until measured". diff --git a/dm/games/01002B00111A2000/README.md b/dm/games/01002B00111A2000/README.md new file mode 100644 index 0000000..3700db5 --- /dev/null +++ b/dm/games/01002B00111A2000/README.md @@ -0,0 +1,98 @@ +# Hyrule Warriors: Age of Calamity (`01002B00111A2000`) + +Update 1.3.0 (v327680) over the base game, with four DLC packages (one is only a licence). SDK 9.6.0. Koei Tecmo's KTGL engine on NVN. Modules `main`, `subsdk0` (Nintendo's movie library, NintendoSDK_movie, as in Metroid Dread) and `sdk`. Plays on the iPhone; 60 fps (30 on the Switch) through the host's frame step. Started from the NSP script's provisional profile. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Frame step, `fps_target 60` | [`patches.py`](patches.py) `FRAME_STEP`, [`game.tsv`](game.tsv) | three stubs run the game's fixed-step updates at the cap; see [Frame rate](#frame-rate) | +| `fps_hold off` | `game.tsv` | the updates step by the cap whatever the hold, so holding late frames only paces the screen: each frame is shown as soon as it is ready (the 60fps IPA ran battles at 45–57) | +| Sound heap cut: engine heap +4 MB, sound data heap −4 MB | `patches.py` `CUTS`, `main` build `c3cf52bf…` | the KTSL sound setup gave up quietly and the main thread waited forever; see [Audio](#audio) | +| `mem_used 0x4800000` (72 MB) | `game.tsv` | the three modules take 66.6 MB, more than the default 64 MB, so `SetHeapSize` failed; see [Memory](#memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0), so the game uses `SetHeapSize`. Link's Awakening's partition, not measured for this game | +| `tex_flush_upload on` | `game.tsv` | movie frames are CPU-written and published with `nvnMemoryPoolFlushMappedRange`; without the upload the intro movie stayed black under its "+ Skip" prompt | +| `vconv_range on` | `game.tsv` | KTGL binds whole mesh pools; the CPU vertex conversion runs only over the vertices each draw reads; see [Battles](#battles) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the camera is on the right stick | + +## Frame rate + +- The game steps a fixed amount per update, from three 30 fps values: + - a KIDS task on `KIDSTaskScheduler` threads (`main+0x166d980`, once per update) waits until 1 / fps has passed since the last update (fps at time +0x30: 30 from a frame-rate object, `main+0x166daa4`; `csinc` at `main+0x166db80`) and then steps the game clock (system +0x3348, the step at +0x3350) by that object's step (x23 +0x34: 1/30 s times the game's timescale, a second time at +0x44, fps 30 at +0x48), not by the time that passed (the measured path, `main+0x166dca8`, runs only without the object); + - the battle steps 1 / the game's frame rate per update: a block at `main+0x269e150` {fps, interval, fps, float fps} = {30, 2, 30, 30.0}, read by the scene update's caller (`main+0x8c3610`: step = 1.0 / fps) and its jobs (32 reads; the game's own writers at `main+0x8a35e0` / `main+0xfc1618`, a run-time interval request at +0x1d58, never ran in a probe); + - the event scripts step by the KTGL device's frame rate (device +0x32d0), 59.94 / the present interval in its config (2). nnMain builds the device config on its stack and inits the device once (`main+0x8d94c4` → `main+0x17c34f0`): {1920, 1080, 3, 9, 5, interval 2, 9, …}. The device keeps the interval (`main+0x17c4ba8` → device +0x2aa4, passed to every present at `main+0x17ced78`, which sets it on the window when it changes) and its frame rate, 59.94 / interval (`main+0x17c37c8` → device +0x32d0). +- **Resolved:** the first 60 fps cuts (interval 1, a 1/60 s wait) were checked only on cutscenes, which follow the device: `DM_PROBE` showed the game clock at 2.00 × real time, and battles ran twice as fast on an iPhone. Fixing the clock alone was not enough: in a 3 s stick hold Link still covered twice the ground of the 30 fps original, and the enemy waves came early, until a `sample` profile of a battle led to the frame-rate block. +- **Current:** the game's updates are not held by the presents (the task kept 60 updates a second while the host held frames 3 vsyncs), so three frame-step stubs (`FRAME_STEP`) run the updates at the cap and the host's frame step paces only the presents: + - at `main+0x166dab4` (where both fps paths join, before the update's tasks and the wait) the task's fps and the frame-rate block become the cap (60, or 30 at a 30 cap), the update's vsyncs (60 / cap) are marked (state +0x28) and the device frame rate becomes 59.94 / those vsyncs; + - at `main+0x166dc60` the object's steps are scaled by its fps × vsyncs per update / 60, and the vsyncs are counted; + - at `main+0x8d94b0` nnMain's device config gets interval 1 above a 30 fps cap (the engine's own 60 fps mode), else the game's 2. +- Every step stays per update, as on the Switch, so an update rate below the cap slows them all together. The host shows each frame as soon as it is ready (`fps_hold off`); the updates keep their speed whatever is shown. +- `DM_FPS_TARGET=30` runs updates and presents at 30; `DM_FPS_TARGET=0` is the Switch original (interval 2, the stubs' original paths; the device frame rate follows the game's fps, 29.97 at 30, and the steps and the block stay the game's). +- Checked on the Mac: the forced 30 and the original put Link on the same spot after the same stick hold, and the opening scenes at frame 2N at 60 match the original's frame N (same subtitle, same poses). +- x16, x17, s16–s18 and the flags are dead at the stub sites; the function uses no x16 / x17. + +## Memory + +- **Resolved: `nnMain` returned right after `nv::InitializeGraphics`, with no error.** The game's `nninitStartup` (`main+0x8ad318`) sizes its heap as total − used memory (`QueryMemoryInfo`), calls `SetMemoryHeapSize` and hands the block to its own malloc. The host reported 64 MB used beside the heap, but `SetHeapSize` checks a request against the real size of the loaded modules (66.6 MB, 68 MB rounded), so `SetHeapSize(pool − 64 MB)` failed with ResultLimitReached, `AllocateMemoryBlock` returned 0x1003, malloc was never set up, the 866 MB graphics heap allocation came back null (`main+0x8aeb4c`) and `nnMain` returned. Found by probing `DM_SVC=1`'s `svc 0x1` request and the `AllocateMemoryBlock` result. `mem_used` 72 MB (the image plus stacks) gives a heap of 0xc8c00000 (3212 MB), in which the game's fifteen sub-heaps (3098 MB) fit. + +## Audio + +- **Resolved: the main thread spun at start-up in a resource wait** (a `YieldThread` loop, no crash, right after `OpenAudioRenderer`; nnMain's resource wait at `main+0x8d9bc8`). Koei Tecmo's sound engine (KTSL) failed its setup quietly. The init (`main+0x8d9610`) hands it three heaps cut from one block: 15 MB engine memory, 2 MB and 142 MB sound data (sizes built by `mov x8,#0xf00000; movk x8,#0x20,lsl #48` and `mov w9,#0x8e00000`; constructor `main+0x12647a0`, heaps created at `main+0x1a2ba58`). In the 15 MB heap the setup allocates the renderer config and work buffer (0x760000: `AudioRenderSystem::GetWorkBufferSize`, 0x40a9c8 of it command buffer) and one block per player (up to 0x1b5860; three of the four mix 5.1), about 16.9 MB in all. The fourth player block (`main+0x1a32f70`) came back null, the setup released its memory pools (`RequestAttachMemoryPool` then `Detach` with no update between) and returned −51, the first sound archive (ASRS) never loaded, and the resource the main thread waited for depended on it. +- How it was found: probe the pump the wait calls (`main+0x8e1c0`: first missing stage bit → the type's stage handler), the handler's dependency list (an incomplete entry of type `0xbbd39f2d`, ASRS), then the memory pool states (`MemoryPoolInfo` +0x10: 1 new, 3 detached, 4 request attach, 5 attached, 6 unused) and the engine allocator's arguments and results. +- **Current:** a per-build cut moves 4 MB from the sound data heap to the engine heap (`mov x8,#0xf00000` → `#0x1300000` at `main+0x8d9610`, `mov w9,#0x8e00000` → `#0x8a00000` at `main+0x8d9618`), so the block the three come from keeps its size. Why hardware fits in 15 MB is not known. +- The multi-stream hardware Opus decoder is the SDK's software one (`HardwareOpusMultiStreamDecoder`, see [Services](#services)). + +## Rendering + +- **Resolved: draws after a dispatch were dropped, so the intro movie stayed black.** The game interleaves dispatches and draws; `BindProgram` with the compute bit replaced the one bound program, and the intro movie's NV12 → RGB draw was dropped (black under its "+ Skip" prompt). Compute programs now have a binding of their own in the replay. +- Images are registered on their own (`nvnTexturePoolRegisterImage`). +- Its GPU-driven effects are indirect quads, converted to indexed indirect arguments by a compute pass. +- **Movie frames:** the decoder's NV12 output (VP9) is copied by the game into a 1280×720 R8 and a 640×360 RG8 texture in a CPU-visible pool and published with `nvnMemoryPoolFlushMappedRange`, so the profile turns on `tex_flush_upload`. Buffer textures (a 1,048,576-texel R32F one) stay out of that path: they are views onto the pool buffer, and an upload would have sent them past Metal's 2D size limit. +- **Light and decal lists:** 16,438 shaders (98,970 bindings) read KTGL's light and decal lists (slots 16–21) from buffer textures with the instruction Maxwell also uses for 1D textures. The translation read them as 2D and the host bound a dummy, so every list read 0. They are now read as rows of 16,384 texels from a linear 2D texture over the buffer (row wrap). No shader both size-queries and fetches such a slot. A 15-minute Mac battle bound 15.7 million of them through 320 row textures, none failed; the first battle is darker and more saturated than with the zeros (mean luma 163 against 178 at the start of the battle), GPU time unchanged. +- **Open red line:** a 2D texture bound where three vertex shaders sample 3D (slots 8 and 9, `texture type mismatch … declares slot 8 as Metal type 7`) is still bound as is; it was hidden behind the earlier red line. + +## Battles + +- **Vertex conversion over the draw's vertices (`vconv_range`).** The soldiers' Half2 and Short2 attributes feed integer shader inputs, which the replay converted on the CPU (`vconv_bind` in `nvn_mtl.m`); without that conversion, reading the raw bytes as integers gave denormals and the soldiers' UVs collapsed onto one texel. KTGL binds whole mesh pools of up to ~590,000 vertices, so converting the bound ranges came to 4.4 million vertices per submission in the first battle: 24 of the 32 ms the replay took per frame on the Mac, 26 fps at a 30 or a 60 fps target. `vconv_range` converts only what each draw reads (219,000 vertices per submission, about twelve submissions a frame), and the half and 8 / 16-bit integer formats have conversion loops of their own. Keeping the conversions across frames did not pay: the soldiers' attributes change every frame. `DM_NVN_VCONV_CHECK=1` found no stale index range in 983,000 reuses. +- The visibility buffer ring was 4 deep, so the submit thread waited for the previous frame's GPU work; it now has as many buffers as command allocators (8). +- Then: the battle ran at 58.6 fps on the Mac (M4 Pro; frame p50 16.9 ms, replay CPU 10 ms, GPU 11 ms). On an iPhone the GPU decides (14–16 ms per frame at p50 in the first battle, 40–60 ms once the phone reaches thermal state 2): the frame step holds 30 there. +- **Shadow vertex inputs** have since replaced that CPU conversion, a quarter of the replay thread: 2,259 side libraries, 77 MB each for Mac and iOS; with more than 7 integer inputs the last ones get only the float shadow (169 of 8,201 inputs). Mac battle at 60 fps against the CPU conversion: 0 vertices converted instead of 545,000 per submission, submit + present 10.0 → 5.7 ms a frame, CPU 3.97 → 2.44 W, GPU unchanged, the same picture. `vconv_range` still serves the draws that keep the CPU conversion (transform feedback, `DM_NVN_VCONV_CPU=1`). +- **Resolved: battles after the first cutscene ran at 17 fps on the iPhone.** An iPhone log (2026-10-21, `fps_hold off`, thermal state 0) went from about 57 to 17 fps after the first battle's first cutscene: the same 1,000–1,400 draws a frame, but NVN calls from 82,000 to 287,000 a frame, GPU 15 → 51 ms and the replay 12 → 24 ms. The battle then runs more small compute dispatches (on the Mac about 280 → 900–1,500 a frame, each with a game barrier and about 17 texture binds), and two host costs grew with them: + - every dispatch had a compute encoder of its own with full queue barriers on both sides (about 10 µs of GPU each on the Mac, GPU 9 → 18 ms a frame). Dispatches now share the copy encoder (about 1.3 µs each; GPU 7.3 → 8.1 ms across the cutscene); + - KTGL registers a texture into the next ID of a ring before every bind (`RegisterTexture`, `GetTextureHandle` and `BindTexture` counts are equal: 8,000 a frame in battle, up to 28,000 after the cutscene). Only 1 in 6 registrations repeated what the ID held, so the per-ID view was rebuilt for half of all binds (`newTextureViewWithPixelFormat` took 45% of the replay thread). Views are now cached by texture serial. + - Mac runs side by side, after the cutscene: GPU 18.0 → 8.1 ms and frame 35 → 22 ms with the dispatch change; the view cache took the replay before the cutscene from 14.3 to 10.8 ms (2,036 views made in 15 minutes, 211 million reused). +- The game builds 8,705 programs during its first frame (about 10 s on the Mac). + +## Services + +- **Resolved: null read in `oe::InitializeCopyrightFrameBuffer`** (right after the window setup): `InitializeCopyrightFrameBuffer`, `SetCopyrightImage` and `SetCopyrightVisibility` are host no-ops (`hle_oe.c`). +- **`HardwareOpusMultiStreamDecoder`:** the game builds it with the imported constructor and then calls it through its vtable; the constructor hook builds the SDK's software `OpusMultiStreamDecoder` in place (`hle_codec.c`). This game needed only the class and the four-argument work buffer size. +- **Other games' saves:** it asks whether Breath of the Wild's (and `0100A2C01320E000`'s) save exists for bonus items; the host says no. + +## Files and shaders + +- Everything sits in RDB archives: `asset/*.rdb` is the index (`_DRK0000` header, one `IDRK0000` record per file); `asset/*.rdb.bin*` and `asset/data/*.file` hold `IDRK` entries, zlib in 16 KB chunks (flag 0x100000). The DLC folders add `.rdb.bin3_0` files of the same layout ([01](../../../docs/agents/01-extract.md#compressed-romfs-content)). `romfs_marker` is `asset/MaterialEditor.rdb.bin`. +- Shaders: 40,726 GLSLC outputs (37,043 pixel, 3,440 vertex, 241 compute, 2 geometry): KTGL's G2S containers (`_S2G3000`, type KTID `0x7bcd279f`; 23,108 entries in `asset/system.rdb.bin*`, 2.3 GB inflated) and one `_DOK0000` container (type `0x20a6a0bb`, the engine's system resources: 151 bare GLSLC outputs, 21 of them drawn in the first battle), plus 117 in `main` (`shader_split.py` `idrk_shaders`). Many have 0x850-byte controls and the compute ones no code header. Mario's texture handle layout (`0x20 + 8 × slot`); pairs come from the single-stage cover (`separable_pairs`). +- `glslc_section` took 3 more outputs (GLSLC version 0x1d.0, whose control +0x20 holds odd values) that used to be dropped ([04, Seen in Dark Souls Remastered](../../../docs/agents/04-graphics.md#seen-in-dark-souls-remastered)). + +## Mac routes + +- With a save whose first-boot settings were made, `DM_KEYS="f300:A,f420:A,f540:A,f660:A,f780:A,f900:A,f1300:Plus,f1350:Plus"` confirms the settings, skips the intro movie and plays the opening into the first battle, The Battle of Hyrule Field (about frame 4000). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Rewriter:** byte jump tables indexed by x30 (`main+0x138216c`; capstone names x30 `lr` and x29 `fp`, mapped back): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **`mem_used`** for games whose modules exceed 64 MB: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **Copyright frame buffer no-ops** and the **hardware Opus multi-stream decoder** as the SDK's software one (`hle_oe.c`, `hle_codec.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Compute's own binding** (`S.cprog`, `g_pp_cprog`), **`nvnTexturePoolRegisterImage`**, **indirect draws and dispatches** (`ixc_run`, `DM_NVN_IXCLOG`), **dispatches in the copy encoder** (`dispatch_enc`, `DM_NVN_DISPATCH_OWN=1`) and **texture views cached by texture** (`vc_get`): [04, Seen in Hyrule Warriors: Age of Calamity](../../../docs/agents/04-graphics.md#seen-in-hyrule-warriors-age-of-calamity). +- **CPU vertex conversion over the draw's vertices** (`vconv_range`, `vconv_bind`): [dm/games/README.md](../README.md). +- **Shadow vertex inputs** (`dmsh_vin.py`, `vin_ct` / `vin_on`) and **row wrap** for buffer textures read by 1D fetches (`fetch1d`, `tbuf_as_2d`, `nvn_tbuf_rows`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **`fps_hold off`** for games whose updates do not wait for the presents: [dm/games/README.md](../README.md). + +## Open + +- The 3D texture mismatch on slots 8 and 9 above. +- Sources disagree on small counts: `game.tsv` says 118 shaders in `main`, the shader notes 117; `game.tsv` puts the dispatches after the first cutscene at 1,000 to 2,800 a frame and the iPhone battle's GPU at 10 → 41 ms, the development notes at 900–1,500 a frame (Mac) and 15 → 51 ms (iPhone log); `patches.py` speaks of "two 30 fps values" before describing the third. +- The memory layout is Link's Awakening's, not measured for this game (`game.tsv`). diff --git a/dm/games/01002B30028F6000/README.md b/dm/games/01002B30028F6000/README.md new file mode 100644 index 0000000..5aadf9a --- /dev/null +++ b/dm/games/01002B30028F6000/README.md @@ -0,0 +1,43 @@ +# Celeste (`01002B30028F6000`) + +Update 1.4.1.1 (v589824; the title shows v.1.4.1.0), SDK 15.3.2. MonoGame for the Switch: MonoGame's and the game's C# compiled to C++ by Sickhead's BRUTE into `main` (78 MB with its bss), FMOD Studio sound (its banks in `Content/FMOD/Desktop`) through AudioOut, on NVN. Modules `main` and `sdk` only; `sdk` is the same build as Paper Mario: The Thousand-Year Door 1.0.1's ([0100ECD018EBE000](../0100ECD018EBE000/README.md)). Brought up with no host change; plays on the iPhone. + +On the Mac: the logos, the title, a new file and the prologue (walking, jumping, the camera following Madeline), at 60 fps with music and sound, no red line; about 1.6–1.7 GB resident, flat over five minutes. + +## What it needs + +The provisional profile's guesses held: no key needed changing. + +| Setting | Where | Why | +|---|---|---| +| `sys_resource 0x0` | [`game.tsv`](game.tsv) | the NPDM declares no system resource, so `IsVirtualAddressMemoryEnabled` is false, as on Horizon: no virtual address memory. The game sets a 3.11 GB heap (pool − used memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| `mem_used 0x6400000` (100 MB) | `game.tsv` | the rewritten modules are larger than the default 64 MB of used memory (BRUTE's `main` alone is 78 MB with its bss), so the heap request plus the modules would exceed the pool | +| Node area cut and 1 GB → 32 MB region alignment | `dm/tools/sdk_patches.py`, `sdk` build `8618895246b4…` | Absolum's cuts, registered on this build for Paper Mario; Celeste has no virtual address memory and never makes an address region | +| `shader_groups /Content/=content` | `game.tsv` | the MonoGame effects; see [Shaders](#shaders) | + +No `pad_layout`: the virtual gamepad starts in the 2D layout (the default). + +## Shaders + +- **Current:** 26 (20 pixel, 6 vertex) in MonoGame effects (MGFX version 8): the 13 XNB files in `Content/Effects` and SpriteBatch's built-in effect in `main`, read by `shader_split.py` (`mgfx_effect`). No other file and no GLSLC output holds shaders. Every binding decodes, with Mario's binding layout. +- The XNB files are platform `S`, uncompressed, read by `EffectReader`; each holds one MGFX effect. In `main`, four bytes `MGFX` count only when version 8 and a control section follow. +- 10 of the 13 effects have no vertex shader: SpriteBatch binds its own and the effect's pass only replaces the pixel shader. Vertex and pixel shaders therefore combine across effects, and they are paired as single-stage programs. + +## Input + +- Controller only. The game maps its buttons by position, so B and X jump. + +## Mac routes + +- A fresh save reaches the prologue with `DM_KEYS="f1850:A,f2200:A,f2600:A,f3000:A"` (the title, Climb, a new file, Begin; control of Madeline from about frame 5,000); then `f5100:LRight~1.0,f5400:LRight+X~0.3` walks to the first ledge and jumps onto it. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **MonoGame effects** (MGFX) in XNB files and in `main` (`mgfx_effect` in `shader_split.py`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). + +## Open + +- Only the merged 1.4.1.1 copy is extracted; the base game v0 is not. diff --git a/dm/games/0100300012F2A000/README.md b/dm/games/0100300012F2A000/README.md new file mode 100644 index 0000000..ba58e80 --- /dev/null +++ b/dm/games/0100300012F2A000/README.md @@ -0,0 +1,54 @@ +# Advance Wars 1+2: Re-Boot Camp (`0100300012F2A000`) + +Base game 1.0.0 (v0), the only version, SDK 10.4.1 (The Great Ace Attorney Chronicles' `sdk` and movie module builds, [010036E00FB20000](../010036E00FB20000/README.md)). Unity 2020.2.1f3 IL2CPP on NVN; FMOD sound through `AudioOut`; Bink 2 movies (`.bk2`) decoded by Bink's Unity plugin in `main`; Pia / NEX for online play. Modules `main`, `subsdk0` (`NintendoSDK_movie`, which `main` imports from; the movies seen so far, the opening among them, are Bink's) and `sdk`. Plays on the iPhone (IPA 7.1 GB). + +On the Mac: the boot, the Bink opening movie, the title, Nell's introduction, a new casual campaign and the first training battle (Field Training, Troop Orders); sound, about 2.5 GB resident and 1.6–1.9 GB of Metal allocations, level over 15 minutes with no red line. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB, and 32 MB alignment for regions of 1 GB and more | `dm/tools/sdk_patches.py`, `sdk` build `d9add76f…` | StandardAllocator's region (more than 1 GB) aborted in `nninitStartup`; see [Memory](#memory) | +| `sys_resource 0x1000000` | [`game.tsv`](game.tsv) | the NPDM declares 16 MB of system resource (virtual address memory) | +| 5 GB block with a 2 GB pool: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0x80000000` | `game.tsv` | Engage's 5 GB block (4.375 GB alias region). The heap check's 1 GB pool ran the game's malloc dry at the first loading screen | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the map cursor moves with the D-pad | + +## Frame rate + +- 60 fps in the menus, 30 fps in battle by the game's own limit (present interval 1, the frame's work 33 ms with the CPU and GPU mostly idle). + +## Memory + +- The NPDM declares virtual address memory: `nninitStartup` (`main+0x365310`) hands pool − used to StandardAllocator as one address region and `main` sets no heap; Unity reserves its own regions beside it. +- **Resolved: `StandardAllocator::Initialize` aborted in `nninitStartup`** (`sdk+0x2edf90`, its `CentralHeap::Init` at `sdk+0x2ee060`; the heap check reported "the host stopped before nnMain"). The `sdk` build is The Great Ace Attorney Chronicles', which has no virtual address memory. Its AddressRegionManager constructor (`sdk+0x340e90`) loads the node area with `orr w8 / w9, wzr, #0x3fe00000` (`e8230b32` at `+0x340ea0`, `e9230b32` at `+0x340f3c`), the SDK 7 / 8 form, so the search for the `mov` form finds nothing there. `AllocateAddressRegion` (`sdk+0x341030`) aligns a region by its size (64 KB under 2 MB, 2 MB under 4 MB, 4 MB under 32 MB, 32 MB under 1 GB, else 1 GB, at `+0x34109c`), as DRAGON QUEST XI S's SDK 7.3.2 build, and the region is more than 1 GB. The cuts: both `orr` → `mov w8 / w9,#0x3e00000` (62 MB) and `orr w10,wzr,#0x40000000` → `mov w10,#0x2000000` at `+0x34109c` (32 MB, as from 32 MB up). Pikmin 4's SDK 10.4.8 build ([0100B7C00933A000](../0100B7C00933A000/README.md)) has the same cuts at the same offsets. +- **Resolved: then the frames stopped at the first loading screen, a graphics worker at 100% CPU** (17 s in, `UnityGfxDeviceWorker` running, the main thread waiting on a condition variable, no red line). The heap check had set a 1 GB pool (the hardware pool had used the alias region up to +4.25 GB within seconds), as for the other Unity games with virtual address memory, and that left the game's malloc too little: Unity's GPU memory pool block allocation (`main+0x96f430`) calls `aligned_alloc` through `main+0x866810`, halves the request after each failure, and once the request rounds to 0 calls on for ever with a size of 0. `sample ` on the Mac host showed the spin (the guest frames are `???` in the host's image list; subtract the module base from the log's `base` lines); it worked here where lldb does not. +- **Current:** a 2 GB pool. Through 15 minutes of the title, the opening movie, the menus and the first training battle: 2.26 GB of the alias region mapped, its high-water mark +3.27 GB; the process holds about 2.5 GB on the Mac, 1.6–1.9 GB of it Metal allocations. The 1 GB of the heap check is a starting point, not a measurement ([dm/games/README.md, Adding a game](../README.md#adding-a-game)). + +## Movies + +- The opening and the other movies seen so far are Bink 2 (`.bk2`), decoded by Bink's Unity plugin in `main`. Their 32 shaders are loose in `main` ([Files and shaders](#files-and-shaders)); without them every movie drew white. + +## Files and shaders + +- Everything is in Addressables bundles under `Data/StreamingAssets/aa/Switch`, read as Engage's (`unityfs.py`). `romfs_marker` is `Data/StreamingAssets/music.bank`. +- 20,424 shaders, all of which build: 20,392 from the bundles and Unity's 11 GLSLC outputs in `main`, and Bink's 32 (2 vertex, 22 pixel, 8 compute). +- **Resolved: every movie drew white.** Bink's movie player builds its programs from 32 control sections and code areas in `main`'s read-only segment that are in no GLSLC output and no BNSH file. 26 are 0x20-byte records in two tables in `.data` (`{code area pointer, control section pointer, u32 code length, u32 control length}`, both pointers relative relocations). The code takes the other 6 by address (`adrp` + `add` to the control section and, a few instructions away, to the code area it copies with `memcpy`); the code area lies right after the control section's own length (+0x20, 16-byte aligned) or right before it, and two control sections share one code area. Both neighbours can pass the header and length checks, so the reader (`shader_split.py` `raw_shaders`) takes only a code area the code builds the address of, the nearest one to the control section's. The 8 compute shaders' code areas have no header (SDK 10). Without them the shaders were not in the map (two `[RED] shader … is not in the shader map` lines and `program has no vertex shader` as the opening movie starts). Of 25 `main`s on hand no other has such control sections. + +## Input + +- The 3D pad layout: the map cursor moves with the D-pad (the tutorial battle was played with it on the Mac). + +## Packing + +- A regional release of the same build (`0100300012FFF000`, issue #114) packs from a profile derived from this one, with the folder's title ID and `main.npdm`'s program ID (`same_build`, `game.py` `by_build`; [development.md, The packing script in detail](../../../docs/development.md#the-packing-script-in-detail)). + +## Mac routes + +- The work folder's `run/step.sh ` drives a run started with `DM_CTL`. About 22 s into a boot the opening movie plays: A skips it, `L+R~0.5` opens the main menu (the first time Nell's introduction, A about 25 times, then the file select); A (Campaign), A, A (Advance Wars, the file), A, A, A, A (the mission, Field Training, Troop Orders) start the first battle. + +## Found on this game + +- **Shader control sections loose in `main`** (`shader_split.py` `raw_shaders`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **`sdk` node-area and 1 GB alignment cuts in the `orr` form** of the SDK 7 / 8 constructor: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **The heap check's 1 GB pool is too small here**: [dm/games/README.md, Adding a game](../README.md#adding-a-game). +- The spin-wait search's store rule takes one wider loop in its `main`: [development.md, Power](../../../docs/development.md#power). diff --git a/dm/games/0100346017304000/README.md b/dm/games/0100346017304000/README.md new file mode 100644 index 0000000..7b2722b --- /dev/null +++ b/dm/games/0100346017304000/README.md @@ -0,0 +1,50 @@ +# Taiko Risshiden V DX (`0100346017304000`) + +Update 1.1.1 (v131072) over the base game, SDK 12.3.3 (no other game's `sdk` build). Koei Tecmo's KTGL engine on NVN, mostly 2D. Modules `main` (15 MB), `subsdk0` (the movie library, NintendoSDK_movie: the WebM opening and ending movies) and `sdk`. In Japanese, Simplified and Traditional Chinese only (no English), so the NSP script packs Japanese (`DM_LANG` in `Documents/env.txt` switches). Plays on the iPhone (IPA 3.3 GB) by touch in handheld mode; 60 fps. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Frame limiter wait skipped (`main+0x443fd4`, `b.hs end` → `b end`) | [`patches.py`](patches.py) `CUTS`, `main` build `f5ab74b9…` | its 2 ms sleep rounds ended frames at 18.2 ms (55 fps); see [Frame rate](#frame-rate) | +| `op_mode handheld` | [`game.tsv`](game.tsv) | handheld, the Switch build plays by touch; docked it reads only the pads | +| `pad_layout touch` | `game.tsv` | the Tap layout sends every touch to the game (the pads' corners would cover the cards, the menu icons at the top right and the on-screen B button) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0): no virtual address memory; `nninitStartup` sets a heap of the pool − used memory (3.14 GB), which the game maps only as it touches it. The default 64 MB of used memory fits its small modules | + +## Frame rate + +- **Resolved: 55 fps with every frame 18.2 ms, the main thread asleep most of the time** (the menus and the town; the opening movie, paced by its own player, ran at 60). KTGL's frame end (`main+0x443e8c`, after the present) waits until N / 60 s have passed since the frame's start tick (taken at `main+0x443d44` after the previous present; N = 1, the int at GOT 0xa1c088), polling `nn::os::GetSystemTick` and sleeping 2 ms per round (`main+0x4c9f24`, `SleepThread`). With about 2 ms of work a frame the rounds end at 18.2 ms, never at 16.7: the Mac ran 55 fps (frame time p50 18.17 / p95 18.69 ms), every 11th frame on screen for two vsyncs. +- How it was found: sampling the host (`sample `: 90% of the main thread in `dm_guest_sleep_ns` under a guest return address of `main+0x443d74`) and a probe on the target's multiply (x20 = 1 frame, x0 = 19.2 MHz). +- **Current:** the game presents at interval 1, so the swap chain holds it to the display anyway; the cut makes the check jump straight to the end, and the host's present pacing (the acquire waits) sets the 60. In the town: 60.0 fps, frame time p50 16.66 / p95 16.85 ms, display interval p99 17.0 ms; the main thread waits in the acquire instead of nine sleeps a frame (wakeups 2,050 → 320 a second, CPU energy 1.7 → 0.7 mJ a frame). + +## Input + +- **Current:** handheld (`op_mode handheld`, `pad_layout touch`), the game plays by touch: tap the cards and buttons, drag or flick the map. On the iPhone those are all the touches in the Tap layout, on the Mac the mouse. The layout button brings back the gamepad (the map scrolls with the right stick in the 3D layout; L / R and ZL / ZR switch tabs and lists). It draws 1920x1080 in both modes. +- **Resolved: taps in handheld mode did nothing, though the game reads the touch screen** (the title's PRESS ANY BUTTON, the menus' cards). Its touch handler (`main+0x42f1ac`) uses `GetTouchScreenState` only to track the finger and takes its taps, pans and swipes from `hid::GetGestureStates` (it acts on types 5–9), which the host answered with no entries. The host now recognizes gestures from the same touch points (`hle_touch.c` `gesture_sample`, unit test `t_gesture`). Every menu, the cards, the map's drag and flick worked by touch with it. `DM_TOUCH_LOG=1` logs each gesture entry, and a `DM_CTL` line `drag [seconds]` moves a finger for testing pans and swipes on the Mac. + +## Services + +- **Resolved: null read at `sdk+0x362b8` (`apm::SetCpuOverclockEnabled`) in the first calls of `nnMain`** (`main+0x2e1e4c`). `oe::SetCpuOverclockEnabled(bool)` goes through the apm session the host never opened. The host keeps the flag and the query reports it (`hle_oe.c`); it does not scale clocks. + +## Files and shaders + +- Loose files, 3.3 GB: KTGL G1T textures (some in MRLK packs), the game's own TR5 / TE5 / TS5 data, KTSS Opus streams (`.kno`, through `nn::codec`) and KTSL wave banks (`.wbd` / `.whd`). `romfs_marker` is `TDATA/BUSTUP.G1T`. +- Shaders: 212 GLSLC outputs: 45 in `main` and 167 in the G2S container `eff/KTGLEffect.g1s` (`_S2G3000`, as Age of Calamity's), read by the existing splitter; nothing is compiled at run time. The game is almost all 2D sprites (two draws a frame in the menus). + +## Status on the Mac + +- The notice, the logos and the opening movie, the title, a new game (1560, Kinoshita Tokichiro) through the opening story, the town, the council with a battle proposal, the territory map, a rest at home, a save and a load, and the field battle tutorial, docked with buttons and in handheld mode by touch. +- 60 fps (frame time p50 16.66 ms, p95 16.85 ms in the town), sound; about 0.7 GB resident and 1.6 GB of Metal allocations, flat over 15 minutes. No red line on that route. The prewarm list covers it (18 lines). +- **Not a host bug:** the town's top-right panel runs off the screen on a council day (its "council today" text is longer than the usual "n days left"): the panel starts where the manual's screenshot has it and widens for the longer text; on other days it fits. + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL`. From an empty save: A skips the notice and the movie, A at the title and on New Game, then A five times (the scenario, its story, Tokichiro, his abilities, the settings) and about 150 presses of A through the story to the town (on the council day: A, Right ×3, A enters the castle). +- `run/save_ref` holds a save in the town on 1560 May 1. +- Handheld (`DM_OPMODE=0`), the same by touch: five taps at the centre (`T0.5x0.5`) reach the load screen of Continue, then `T0.5x0.29` and `T0.5x0.47` load slot 1. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **`oe::SetCpuOverclockEnabled`** (`hle_oe.c`) and **gestures for `hid::GetGestureStates`** recognized from the touch points (`gesture_sample`: yuzu's rules plus an 8 px slop, `t_gesture`, `DM_TOUCH_LOG`, `DM_CTL` `drag`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). diff --git a/dm/games/010036E00FB20000/README.md b/dm/games/010036E00FB20000/README.md new file mode 100644 index 0000000..f8bc140 --- /dev/null +++ b/dm/games/010036E00FB20000/README.md @@ -0,0 +1,53 @@ +# The Great Ace Attorney Chronicles (`010036E00FB20000`) + +Base game 1.0.0 (v0), with its DLC `010036E00FB21001`. SDK 10.4.1. Capcom's MT Framework on NVN. Modules `main`, `subsdk0` (the SDK's movie module, NintendoSDK_movie, whose extractor runs natively while the host decodes the H.264 movies with VideoToolbox) and `sdk`; `subsdk0` and `sdk` are the same builds as Advance Wars 1+2: Re-Boot Camp's ([0100300012F2A000](../0100300012F2A000/README.md)). Plays on the iPhone; 30 fps, as the game paces itself. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `tex_flush_upload on`, `tex_dirty_upload on` | [`game.tsv`](game.tsv) | the CPU copies every texture into a CPU-cached pool after creating it and never flushes; with `tex_flush_upload` alone the screen stayed black. See [Rendering](#rendering) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0): `nninitStartup` sets a heap of the pool − 64 MB (3.14 GB); `main`'s bss is 9.2 MB | +| Node area cut 1 GB → 62 MB, 1 GB alignment → 32 MB | `dm/tools/sdk_patches.py`, `sdk` build `d9add76f…` | applies to this build by its ID; made for Advance Wars 1+2: Re-Boot Camp, which shares it. This game has no virtual address memory and never makes an address region | +| `pad_layout` not set (the default `simple`, 2D) | `game.tsv` | the gamepad starts in the 2D layout (the player-facing README lists it among the games that do) | + +## Rendering + +- Upper-left window origin, depth mode near = 0, a 1920x1080 RGBA8 swapchain; the game asks for present interval 1 and paces itself at 30 fps. Mario's binding layout (combined textures at `0x20 + 8 × slot`, slots 0–2). +- **Resolved: black screen at boot, with the draws of a dialog box in the pass dump** (sampling 256x256 and 1024x256 textures of format 0x4d). No texture had been uploaded from guest memory (`packaged textures: first uploads 0`), and `DM_NVN_TEXINFO=1024x256` showed storage that already held data at first use (226,555 of 262,144 bytes non-zero): no packaged layout, block-linear, in a 1000 MB `CPU_CACHED | GPU_CACHED | COMPRESSIBLE` pool (flags 0xa4). A log of the pool flushes, texture creations and file reads (temporary lines under `DM_NVN_LOG`) showed how they are filled: every `.tex` is read into staging buffers (512 KB `ReadFile`s, none into the pool), and the CPU copies the texels into the texture's storage after `nvnTextureInitialize`, with no `nvnMemoryPoolFlushMappedRange` over them and no `WriteTexels`; only the four linear 1024x64 text textures are flushed, every frame. `main` has no cache maintenance instruction at all, so there is no flush for the host to take as the signal. With `tex_dirty_upload` a registered texture whose pages the CPU writes after creation is uploaded at its first use (as for FINAL FANTASY TACTICS - The Ivalice Chronicles, [010038B015560000](../010038B015560000/README.md)). + +## Services + +- **Resolved: the first boot crashed in `oe::DisableAlbumImageTakenNotification`** (a null read at `sdk+0x338070`, the first call in `nnMain`, `main+0x6b188c`: the SDK asks am through the applet proxy the host never made). +- The import list against the hooks (`nso_syms.py`, then `main`'s undefined `nn::` symbols no `HLE_FORCE` names) gave the rest before they were met: + - `oe::IsInStoreDemoModeEnabled` (no); + - `vi::ListDisplays` (one display, "Default"; the game lists 10 into a stack buffer and does not read them, `main+0x808f08`); + - `fs::RenameDirectory` (as `RenameFile`); + - `account::StoreSaveDataThumbnailImage` / `DeleteSaveDataThumbnailImage` (nothing kept); + - the `ec::ShowShop…` overloads without an `ApplicationId` (the calling application's own pages); + - `friends::StartFacedFriendRequest` / `StartSendingFriendRequest` (fail); + - `ngc::ProfanityFilter::MaskProfanityWordsInText(int *, char *, uint32_t)`, `SetMaskMode` and `Finalize`, which this game calls by name rather than through the vtable. +- The touch-screen states (`GetTouchScreenState<16>`) were already hooked by a macro the import table does not see. +- Two NVN calls were red stubs: `nvnDeviceSetIntermediateShaderCache` and `nvnCommandBufferDiscardColor` (a discarded target is undefined, so keeping its content is correct). After that it ran its frame loop at 30 fps; the black screen it showed was its textures (above). +- **Add-on content:** index 1 (`DLC.dlcp`); the game reports it found and added to Special Contents. + +## Files and shaders + +- Everything is under `nativeNXx64/`: 447 ARC archives (`*.arc`, 17,910 files; version 7, one zlib stream per file) of models (`.mod` / `.mrl`), textures (`.tex`), scripts and messages (`.gmd`), beside loose `.tex` textures, Opus / ADPCM sound and 24 H.264 `.mp4` movies (`shader_split.py` `mtarc_members`; [01](../../../docs/agents/01-extract.md#compressed-romfs-content)). `romfs_marker` is `nativeNXx64/sa/NX_64/root.arc`. +- Shaders: 460 (230 vertex, 230 pixel) in the engine's one shader package, `sc\NX_64\root` (`SPK\0`, type hash `0x02358e1a`) in `nativeNXx64/sa/NX_64/root.arc` (`shader_split.py` `mtspk_shaders`). All build and every binding decodes. The package's own vertex / pixel combinations are not read, so the vertex shaders are paired with the single-stage cover. `CustomShaderPackage.mfx` (`MFX\0`, the effect descriptions) holds no shader code, and neither `main` nor any other ARC entry does. + +## Status on the Mac + +- The first-boot notices (the DLC found, auto-save), the title and its Options, a new game of The Great Ace Attorney: Adventures, the opening movie, the first episode's opening scenes and its trial, at 30 fps with sound and saves; about 1.1 GB resident, flat over 14 minutes (1.07 GB in the first episode's holding cell). + +## Mac routes + +- `DM_KEYS="f700:A*1000/24"` (A every 24 frames) goes through the notices, the title and a new game into the opening movie; the first episode's dialogue starts at about frame 6,000 and its trial at about 20,000. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **CPU-written textures with no flush** (`tex_dirty_upload`): [04, Seen in The Great Ace Attorney Chronicles](../../../docs/agents/04-graphics.md#seen-in-the-great-ace-attorney-chronicles), key in [dm/games/README.md](../README.md). +- **The MT Framework ARC and SPK readers** (`mtarc_members`, `mtspk_shaders`): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **SDK 10 calls found from the import list**, `nvnDeviceSetIntermediateShaderCache` and `nvnCommandBufferDiscardColor`: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). diff --git a/dm/games/010038B015560000/README.md b/dm/games/010038B015560000/README.md new file mode 100644 index 0000000..009e2f6 --- /dev/null +++ b/dm/games/010038B015560000/README.md @@ -0,0 +1,55 @@ +# FINAL FANTASY TACTICS - The Ivalice Chronicles (`010038B015560000`) + +Update 1.4.0 (v393216) over the base game, SDK 19.3.9 (its `sdk` is byte-identical to Hades II's, [0100A00019DE0000](../0100A00019DE0000/README.md)). Square Enix's engine on NVN: `main` imports only `nvnBootstrapLoader`, and the bundled OpenGL driver (`subsdk0`, NintendoSDK_OpenGL) is not loaded, as `main` takes nothing from it the host does not supply. Modules `main` and `sdk`. Files are loose in `resources/` and `resources_nx/` (textures `.tex`, models, CRI ADX audio, CRI USM movies decoded by the game's own CRI Mana on the CPU); CRI Mana movies and ADX audio run in the game's own code. Playable on an iPhone. + +Status: brought up on the Mac: initial settings, the opening movie, the prologue and the Orbonne Monastery battle up to the player's first turn, at 60 fps with no red line (about 1.4 GB resident). + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Job pool wake cut: `main+0x12554` `ldr x0,[x0]` → `str x0,[x0,#8]`, `main+0x12558` `b SignalEvent` → `b 0x1254c` | [`patches.py`](patches.py), `main` `3cfd4578…` | a worker woken in another's place passes the wake on; the boot hung in 4 of 6 runs without it; see [Threads](#threads) | +| `tex_flush_upload on`, `tex_dirty_upload on` | [`game.tsv`](game.tsv) | the UI textures are written by the CPU into uncached pools after creation, with no flush; without both keys every texture read 0 and the screen stayed black; see [Rendering](#rendering) | +| `mem_used 0x4c00000` | `game.tsv` | the rewritten modules (76 MB with the bss) are larger than the default 64 MB of used memory the game takes off the pool for its heap | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the NPDM declares no system resource: the game uses `SetHeapSize`, 3.13 GB (pool − used memory) in the 3.25 GB heap region | +| `pad_layout full` | `game.tsv` | the D-pad moves the cursor and the right stick turns the camera (the game's controls screen) | + +## Threads + +- **Resolved: the main thread waited forever on a job's done event at boot while every worker slept, two of them on one event** (4 of 6 Mac runs, `main+0x1426c` waiting on `Job::m_doneEvent`). The engine's job pools wake one worker through the pool's single auto-clear event. A job goes into worker k's own queue (`main+0x154c0`), and if k's state reads asleep the caller signals the event (`main+0x12554`, from the per-worker dispatch loops `main+0x14000` / `main+0x14350` / `main+0x17670`, the single-job submits `main+0x14640` / `main+0x14790` / `main+0x14dc0` and the shutdown `main+0x13df0`). The kernel wakes the waiter that slept first, which is k only while the workers sleep in index order. A worker woken for another's job runs only its own queue (worker loop `main+0x15ce0`) and sleeps again, so with two workers in one pool (threads 0x1006 / 0x1007, later pinned to core 2) the signal is lost: at boot when worker 1 went to sleep before worker 0, and later as a job left in worker 0's queue for 12 s. +- The thread dump's `wait #` (the order condition waiters arrived in, which `SignalProcessWideKey` wakes them in at equal priority) showed the order in every hung run. +- **Current:** the game's own wait wrapper (`main+0x12500`, `{event, pending}`) signals the event once more when it takes a nonzero pending word after waking, and nothing in `main` set it. The build cut makes the signal set it first, so a worker woken in another's place passes the wake on (in a one-worker pool the extra signal costs one empty loop): 6 of 6 runs boot. +- A host change that made `StartThread` return only once the new thread runs (as a free Switch core takes it within microseconds) brought the hangs to 1 in 6 and was dropped. + +## Rendering + +Mario's binding layout (combined textures at `0x20 + 8 × slot`, images at `0x120`); every binding decodes. + +- **Resolved: black screen; the UI drew in the right places under `HILITE`.** The UI reads its textures from `.tex` files the engine loads straight into a `CPU_UNCACHED | GPU_CACHED | COMPRESSIBLE` pool (flags 0xa2) after creating the textures: 231 textures by the title screen, no packaged layout, no `WriteTexels`, no copies. Writes to an uncached pool need no flush, so neither the flush path nor `tex_init_fill` (which only takes `CPU_NO_ACCESS` pools) saw them. With `tex_flush_upload` and `tex_dirty_upload` the registered textures' pages are write-protected, and a write after `nvnTextureInitialize` uploads them. +- **Resolved: translucent UI (the frosted text bands, button guides, dialog headers) yellow and white.** The last passes have no render target: fragment shaders composite each UI layer into the window texture with image stores, blending in software. The composite (`uirender.tec`, `4cf1975ed1e36590`) loads the window texture's texel as `r32ui`, unpacks the four bytes and stores the blend through a float view of the same image. The host bound the RGBA8Unorm texture to the `texture2d` argument as it was, whose reads are undefined (Metal's validation layer does not check textures in argument tables). An image slot the shader declares as uint / int now gets a view of the same texel size in that class (`img_fit` in `nvn_mtl.m`); a texture gets the format-view usage only when a shader needs such a view, upgraded before the next draw pass opens. +- **Resolved: dialog boxes with holes the size of their text, and white specks where text faded.** Each UI element is drawn into its own layers and then composited into the window image by one draw per layer, all in one attachment-less pass, each a read-modify-write of the image, with `nvnCommandBufferBarrier(ORDER_FRAGMENTS | INVALIDATE_TEXTURE)` (bits 0x12) between them. The host took a barrier only as an order for the next pass, and Metal orders nothing between draws of one render pass for shader writes: the text element's composite read the window before the parchment had landed and wrote the old pixels back. A barrier with `ORDER_FRAGMENTS` or `ORDER_FRAGMENTS_TILED` now ends the open pass when a draw in it wrote an image or a storage buffer (`swrite_note` in `nvn_mtl.m`); reflection marks an image binding written when its type has write access (`nvn_refl.m`). +- **Resolved: `texture clear case not implemented … mask 0x1`.** The game clears an R8 render target with channel mask 0x1; a mask that covers every channel the format has is a whole clear (`mtl_pf_chmask`). +- **New NVN entry points:** `nvnCommandBufferBindTextures` (the plural of `BindTexture`), `nvnSamplerPoolRegisterSamplerBuilder` (a sampler registered from its builder), `nvnTextureWriteTexelsStrided` (`WriteTexels` with row and image strides) and device item 0x67, which the game reads before every program it loads and uses as the alignment of its copies of the shader (answered 256, not measured). + +## Files and shaders + +- 306 shaders: 257 GLSLC outputs in the engine's technique files (`resources_nx/release/master/shader/*.tec`, magic `TEC\0`, each output whole after a table of parameter names) and 49 in `main`. `shader_split.py` reads GLSLC outputs in any file no other reader takes, and pairs each as a single-stage program. Groups `resources` and `resourcesnx` (`shader_groups`). +- **Unbuilt:** one geometry shader (`bahamuteffect.tec`), as Metal has no geometry stage. +- On the outputs the old GLSLC rule keeps, this game's among them, the section-header reading added for SDK 4 (`glslc_section`) gives the same key ([04, Seen in Dark Souls Remastered](../../../docs/agents/04-graphics.md#seen-in-dark-souls-remastered)). +- `romfs_marker`: `resources/release/master/system/ffto/fftpack.en.bin`. + +## Mac routes + +- A fresh save reaches the first battle turn at about frame 21,000 with `DM_KEYS="22:A*60/2,150:Plus,152:X~2.5,165:Plus,167:X~2.5,175:A*70/2"` (A through the settings and the birthday, then + and a held X skip each part of the opening). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **GLSLC outputs in any RomFS file no other reader takes** (`shader_split.py`): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **CPU writes into uncached pools uploaded** (`tex_dirty_upload` with `tex_flush_upload`), **integer views of image slots** (`img_fit`), **fragment barriers that end a pass after shader writes** (`swrite_note`, `nvn_refl.m`), **whole clears by channel mask** (`mtl_pf_chmask`) and the **new NVN entry points**: [04, Seen in FINAL FANTASY TACTICS - The Ivalice Chronicles](../../../docs/agents/04-graphics.md#seen-in-final-fantasy-tactics---the-ivalice-chronicles). +- **A job wake lost between two workers on one auto-clear event** (thread dump `wait #`): [03, Threads](../../../docs/agents/03-bringup.md#threads). + +## Open + +- No source gives an iPhone measurement (frame rate, footprint); development.md says only "playable on an iPhone". diff --git a/dm/games/01003FC01670C000/README.md b/dm/games/01003FC01670C000/README.md new file mode 100644 index 0000000..fba2098 --- /dev/null +++ b/dm/games/01003FC01670C000/README.md @@ -0,0 +1,49 @@ +# 13 Sentinels: Aegis Rim (`01003FC01670C000`) + +Update 1.10 (v65536) over the base game, SDK 11.4.3. Vanillaware's own engine on NVN. Modules `main`, `subsdk0` (NintendoSDK_movie 11.4.3) and `sdk`; `sdk` and `subsdk0` are byte-identical to Metroid Dread's ([010093801237C000](../010093801237C000/README.md)). Playable on the iPhone; on the Mac a new game gets through the prologue into the first battle. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `alloc_big_align on` | [`game.tsv`](game.tsv) | the engine cuts ring heaps out of a memory pool at 64 KB GPU alignment, and the pool's storage comes from malloc: without 64 KB-aligned large allocations the third ring heap keeps base 0 and the prologue's first battle writes through a null pointer; see [Memory](#memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region. No system resource: the game sets a heap | + +No `patches.py`, no `sdk_patches.py` cut for its `sdk` build (`2d369d99…`), no `pad_layout` (the iPhone pad starts in the 2D layout). + +## Memory + +- The NPDM declares no system resource (META +0x14 = 0): the game uses `SetHeapSize`. `main`'s bss is only about 21 MB. Measured with `DM_SVC=1`: the heap request is pool − used = `0xc9400000` (3.14 GB); the alias region gets one page during SDK initialization; thread stacks go into the stack region through `MapMemory`. On the Mac about 2.2 GB is resident in the first battle. +- **Resolved: a null write as soon as the main thread starts recording a command buffer.** A constructor at `main+0x2fa9a0` ran on `this = 0` when the prologue entered its first battle. The object came from one of the engine's ring heaps (`main+0x2d83ec`), and that heap had base 0. At startup the engine takes one GPU memory pool, sized as exactly the sum of three ring heaps, and cuts the heaps out at 64 KB alignment relative to the pool's GPU address (`main+0x30c118`). On the Switch a pool's GPU address is big-page aligned; the host's GPU address is the CPU address, and the storage came from the game's `malloc` (`nn::mem::StandardAllocator::Allocate(size)`) rounded only to 4 KB. Up to 60 KB was lost at the front, the third heap (16 MB) did not fit, and the sub-allocator returned 0. Probing the heap object at the allocation (`DM_PROBE=main:2bec60@x0+8`) showed the zero base directly. **Current:** `alloc_big_align` routes allocations of 64 KB or more to `Allocate(size, 0x10000)` (`hle_os.c`). + +## Services + +- **Resolved:** SDK 11 calls that reach a service: `hid::GetMouseState` (the first boot aborted in it: the SDK's mouse resource was never initialized), `fs::CleanDirectoryRecursively`, `fs::EnableGlobalFileDataCache`, `oe::SetUserInactivityDetectionTimeExtendedUnsafe`, `time::ToPosixTime` (host `mktime`) and `audio::GetAudioOutName` ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)). + +## Files and shaders + +- Everything is in six CRI CPK archives (`ROBO_*.CPK`, 4.3 GB; `ROBO_US.CPK` is the profile's `romfs_marker`) that CRI File System opens through `nn::fs`; they hold textures, scripts, CRI ADX2 audio (acb / awb) and CRI USM movies. The host needs nothing but plain file access for them. +- To look inside one: the header and TOC are `@UTF` tables (big-endian columns), masked with CRI's byte stream (`m = 0x655f`, then `m *= 0x4115` per byte); file offsets in the TOC are relative to the smaller of `TocOffset` and `ContentOffset`, and files whose `ExtractSize` differs from `FileSize` are CRILAYLA-compressed. The listing showed no shader container, which is how the shaders were found in `main` instead ([02, Data containers](../../../docs/agents/02-survey.md#data-containers-that-matter-to-the-host)). +- Shaders: no shader file in RomFS. 838 GLSLC compiler outputs (magic `0x19866891`) sit in `main`'s read-only segment, each referenced from a `{pointer, size, 0}` table in `.data` (`shader_split.py` `glslc_shaders`). Each output holds one control section with the code area right after it; a debug section with the NV assembly and the GLSL source follows. The control sections are all at least 0x880 bytes. + +## Rendering + +- **Single-stage programs:** the game builds one NVN program per GLSLC output (838 `ProgramSetShaders` at startup) and combines vertex and pixel programs per draw with `BindProgram` stage bits, so the real pairs exist only at draw time. Every pixel shader input is a plain `float4` at locations 1–9 (the SPH input map, words 6–13, matches the translated MSL inputs for all 375 pixel shaders), so the cover that pairs every vertex shader here is one pixel shader. +- Depth mode: the game sets near = 0 (`nvnDeviceSetDepthMode`), as Link's Awakening and Engage do. +- **Resolved: an assertion a minute and a half into the prologue,** `buffer index (31) must be < 31`: the game binds vertex streams 14 and 15, and the host bound NVN stream *n* to Metal vertex buffer 16 + *n*. +- Every texture is created with `TextureBuilderSetPackagedTextureData`. **Current:** the host copies that data at `TextureInitialize` and uploads it like a packaged texture; the linear ones (the save thumbnails) are excluded and, in a CPU-visible pool, read straight from the pool buffer. +- Attachment loads and stores per frame on the Mac went down about 15% once an inline uniform update inside a render pass stopped ending the pass ([development.md, Power](../../../docs/development.md#power)). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **64 KB-aligned large allocations** (`alloc_big_align`, `hle_os.c`; shared with Unicorn Overlord, [010069401ADB8000](../010069401ADB8000/README.md)): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space) and [the profile keys](../README.md#keys). +- **GLSLC outputs in `main`** (`shader_split.py` `glslc_shaders`, the control / code / length check): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **Single-stage programs paired with a cover** (`vs_pad`), **a shader cache key that covers the pairs** (`shader_key`, `pair_keyed`), **triangle fans and indexed quads** (a shared fan index buffer; CPU expansion at replay in `nvn_mtl.m`), **vertex streams 14 and 15 at slots 14 and 15** (`VB_SLOT`) and **packaged texture data copied at `TextureInitialize`**: [04, Seen in 13 Sentinels](../../../docs/agents/04-graphics.md#seen-in-13-sentinels). +- **Finding service calls before the first boot** (`main`'s `nn::` imports filtered against `HLE_FORCE` names): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- `game.tsv` still describes the game as "In bring-up" (the Mac reaching the first battle), while AGENTS.md lists it as playable on the iPhone. +- `game.tsv`: only the merged 1.10 copy is extracted; the base game's own build IDs are added when needed. diff --git a/dm/games/01004A4010FEA000/README.md b/dm/games/01004A4010FEA000/README.md new file mode 100644 index 0000000..e902d13 --- /dev/null +++ b/dm/games/01004A4010FEA000/README.md @@ -0,0 +1,52 @@ +# Bayonetta 3 (`01004A4010FEA000`) + +Update 1.2.0 (v131072) over the base game, SDK 13.3.2 (no other game's `sdk` build). PlatinumGames' own engine on NVN; Wwise sound through `AudioOut`. Modules `main`, `subsdk0` (the movie library, NintendoSDK_movie: WebM movies, VP9) and `sdk`. Languages: English, Japanese, French, German, Spanish, Italian, Russian, Korean, Simplified and Traditional Chinese. Plays on the iPhone (IPA 14.2 GB); 60 fps in play, 30 where the game asks for it (the title and loading screens). + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | [`game.tsv`](game.tsv) | the NPDM declares no system resource (META +0x14 = 0): no virtual address memory; `nninitStartup` sets a heap of the pool − used memory (3.09 GB), which the game maps only as it touches it | +| `mem_used 0x7400000` (116 MB) | `game.tsv` | the rewritten modules are larger than the default 64 MB of used memory (the game's `SetHeapSize` asks for pool − used) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the camera is on the right stick, and the D-pad picks items | + +## Services and SDK + +- **Resolved: null read at `main+0xb8462c` right after the first two textures, called from `main+0x670eb0`.** Not the fault itself: `nnMain`'s start-up (`main+0x6705c0`) returns false when any of its 30 steps fails, the game then skips its main loop and goes straight to shutdown, and the shutdown walks a pool (`main+0x4efce70`) whose initialization (`main+0xb840d0`) came after the failed step. `DM_PROBE` on each step's entry (lldb was unavailable: developer mode off) showed the last one entered, `main+0x69c698`, the built-in shaders of `shader2.dat` (`hwfillcolor`, `hwshaderpf`, …), and probes inside it the step that failed: creating a shader from its object (`main+0xd0d998`). The engine checks each shader object's GPU code version (byte 6 major, byte 7 minor of the object) against the driver's range (`nvnDeviceGetInteger` 0x2E–0x31: newest major, oldest major, newest minor, oldest minor) and refuses newer code; the host reported 1.14, the table measured on Super Mario Odyssey's SDK 7 driver, and `hwfillcolor.vso` is 1.15 (the game's objects run from 1.9 to 1.16). The newest version the host reports now follows the SDK: 1.16 from SDK 13 (`hle_nvn.c`). +- Two NVN getters were red stubs: `nvnWindowGetCrop` (read back every few frames; before any `SetCrop` it is the whole first window texture) and `nvnSamplerBuilderGetLodClamp`. +- **Not a hang:** the first boot sat on the loading clock for about a minute, the CPU idle, with every file read done after 12 s: that run compiled its Metal libraries and pipelines on first use (cold cache). With a warm cache the title is up after about 15 s; what looked like a 40-second wait in the read log (`DM_FSLOG`) was the title screen itself, which reads nothing. Screenshots every few seconds (`ctl.sh snap` from a script) told the two apart. + +## Rendering + +- Shaders are single-stage programs: the engine makes a program of each object and binds them per stage at draw time, so each is paired with a cover pixel shader. It finds its built-in ones by name (`hwfillcolor.vso`, …). +- **Two domain shaders do not build** (`DS_Vanish05_*.dso`: `SPIRV-Cross threw an exception: Subgroup builtins are not available in this type of function`); the one-lane rewrite (`spv_single_lane`) covers vertex shaders only, and tessellated draws are skipped anyway. +- From the title to the prologue's first play the picture is right as far as compared (title scene, menus, the VP9 opening movie, the first real-time scene, play with the HUD). Two red lines, once each: a pixel shader declares a cube texture where the game binds a 2D one (the zero cube is bound), and a draw whose stage 0 reads cb1 with none bound. +- The spin-wait search takes 37 loops in `main` (35 before its store rule) ([development.md, Power](../../../docs/development.md#power)). + +## Files and shaders + +- 72 PKZ archives in the RomFS root (14 GB; `root_000.pkz`, `core_000.pkz`, `pl_000.pkz`, `st1_000.pkz`, `sound_000.pkz` … `sound_017.pkz`, …) of DAT archives, compressed per entry with Oodle Kraken or stored (`OodleKraken` 11,760 entries, `None` 5,159; `main` knows `ZStandard` too, `main+0xcbdf80` maps the three names to 0xc9, 0x65 and 0), beside 478 loose WebM movies in `movie/` ([01](../../../docs/agents/01-extract.md#compressed-romfs-content)). `romfs_marker` is `root_000.pkz`. +- `main` reads the archives only through Oodle's streaming decoder (`main+0xcbc698` creates one, `main+0xcbc7f8` feeds it 256 KB at a time into a 10 MB window), so `OodleLZ_Decompress` was never linked in and its name string is not there. The Mac host's Oodle tool mode now finds `OodleLZDecoder_MemorySizeNeeded`, `Create`, `Destroy` and `DecodeSome` instead (by the strings of Oodle's `oodlelzcompressors.cpp` and their order) and decodes each entry with them in `DM_OODLE_SERVE` (`hle_oodle.c`). `profile_guess.py` turns it on for any game with Kraken PKZ archives (`oodle_packs`). The shader split decodes every entry of every PKZ (8 workers, one Mac host each) in about 20 s; only `root_000.pkz` holds shaders. +- Shaders: 8,024 PlatinumGames shader objects in `root_000.pkz` (`shader.dat`, `shader2.dat`, `shadereff*.dat`, `autoshadereff.dat`): `VSO\0` / `PSO\0` / `CSO\0` / `HSO\0` / `DSO\0` (the stage), u32 compiler version, u32 control length, u32 control offset (0x18), u32 code length, u32 code offset. 6,424 distinct keys (4,288 pixel, 2,047 vertex, 78 compute, 6 domain, 5 hull) (`shader_split.py` `pkz_entries`, `pgdat_members`, `pg_shader`). All but two domain shaders build. + +## Status on the Mac + +- The boot, the title, a new game (difficulty, Naive Angel Mode, brightness, the auto-save notice), the VP9 opening movie, the prologue's first real-time scene and a minute of play in it, with sound. +- 60 fps in play (frame time p50 16.67 ms, p95 17.5 ms; GPU about 5 ms a frame on an M6), 30 where the game asks for it (the title and loading screens); about 2 GB resident and 3.1 GB of Metal allocations, flat after the first scene. The title is up about 15 s after launch with a warm shader cache. The prewarm list covers that route (678 lines). + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL` (`CTL=` picks another run's control file), `run/step.sh ` presses and takes a screenshot, `run/snapseq.sh` boots and takes one every few seconds, and `run/route.sh ` boots on a fresh save and drives the route above: A at the title, A through the logo, A (Standard), A, B (closes the Naive Angel Mode help), A (Off), A (brightness), A (the auto-save notice), then Plus, Down, A skips the opening movie. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The NVN layer's newest GPU code version follows the SDK** (1.15 from SDK 8, 1.16 from SDK 13, `hle_nvn.c`), `nvnWindowGetCrop` and `nvnSamplerBuilderGetLodClamp`: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Oodle's streaming decoder in the Mac host's tool mode** (`hle_oodle.c`, `OodleLZDecoder_*`, `oodle_packs`) and the **PKZ / DAT readers**: [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **PlatinumGames shader objects** (`pg_shader`): [04, Seen in Bayonetta 3](../../../docs/agents/04-graphics.md#seen-in-bayonetta-3). + +## Open + +- Dotted patches on some surfaces (a grid every 4 pixels: the platforms breaking up in the prologue, a floor near the camera) look like the engine's dithered dissolve; not compared with the Switch yet. +- The two domain shaders and the two red lines above. diff --git a/dm/games/01004AB00A260000/README.md b/dm/games/01004AB00A260000/README.md new file mode 100644 index 0000000..0e36849 --- /dev/null +++ b/dm/games/01004AB00A260000/README.md @@ -0,0 +1,79 @@ +# DARK SOULS: REMASTERED (`01004AB00A260000`) + +Update 1.0.3 (v196608; the base game v0 is not extracted), SDK 4.5.0, NVN API 53.313. FromSoftware's dantelion2 engine with Havok and FMOD (sound through one 6-channel AudioOut). Modules `main`, `subsdk0` (the SDK's movie library) and `sdk`. Plays on the iPhone at 60 fps and the game's normal speed (30 on the Switch) through two rewriter cuts. + +On the Mac: the logos and the opening movie, the title (offline), the first-run Brightness, Options and Button settings, character creation with the name through the inline keyboard, the opening cutscene and the Undead Asylum cell (walking, the camera, pillaging the corpse), at 60 fps with music and sound; no red line but the conditional rendering note. About 1.6 GB resident in the Asylum, flat over three minutes. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Frame limiter `mov w1,#30` → `#60` at `main+0x706fa4` | [`patches.py`](patches.py), `main` build `df3766a2…` | the render thread's CPU limiter sleeps out 1000 / fps ms; see [Frame rate](#frame-rate) | +| Frame update step `ldr s8,[x8,#0x6ec]` (1/30) → `ldr s8,[x8,#0x834]` (1/60) at `main+0x667904` | `patches.py`, `main` build `df3766a2…` | the frame update takes a fixed step: with the limiter cut alone everything ran twice as fast on the iPhone | +| `pad_layout full` | [`game.tsv`](game.tsv) | the 3D layout: the right stick turns the camera and the D-pad switches spells and items | +| `sys_resource 0x0` | `game.tsv` | the NPDM declares 0 MB of system resource: `IsVirtualAddressMemoryEnabled` is false, as on Horizon. The game sets a 3.10 GB heap (pool − used) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the 4 GB layout of the games without virtual address memory | +| `mem_used 0x6e00000` (110 MB) | `game.tsv` | the rewritten modules (110 MB with `main`'s 51 MB bss) are larger than the default 64 MB of used memory the game takes off the pool for its heap | +| `shader_groups .bdt#=dvdbnd` | `game.tsv` | the shaders in the dvdbnd archives; see [Files and shaders](#files-and-shaders) | + +No `render_cond`: `SetRenderEnableConditional` is drawn unconditionally, as in Big Brain Academy; nothing seen looked wrong. + +## Frame rate + +- **Current: 60 fps** (30 on the Switch). The game presents at interval 1, yet every frame took exactly 33.3 ms of "frame work". The cap is the render thread's own limiter (`HGCommandDispatcher`, `main+0x8a4b28`): `main+0x707090(this, fps)` sleeps out 1000 / fps ms less the time since its last call (`nn::os::GetSystemTick`), when more than 2 ms are left; the main thread waits for the render thread. The 30 is a constant at the only call (`main+0x706fa4`), which the first cut makes 60. Found from the render thread's backtrace in the thread dump, not from the NVN calls. The limiter's frame-skip mode only starts above 36.4 ms a frame, so 60 does not reach it. +- **Resolved: twice the game's speed on the iPhone.** The game does not step by the time that passed: the main thread's frame update (`main+0x6a337c`, on the manager at GOT `0x2287be0`) takes a fixed step, the float 1/30 at `main+0x167a6ec`, loaded at `main+0x667904`. The second cut loads the 1/60 that rodata holds at `main+0x167a834` instead; the community's 60 fps cheat for this build changes the same two words. A 0.8 s walk that looked the same at 30 and 60 fps on the Mac had been taken for a time-based game. Some 20 other loads of that 1/30 turn parameters counted in 30 fps frames into seconds or step single systems; they stay. Probing every site that loads such a constant (`DM_PROBE`, 16 per run) tells per-frame steps from one-off conversions, but which of them sets the game's speed only shows by timing the game. +- **Resolved: stalls of 30–60 ms about once a second, at 30 fps and at 60** (in the Asylum cell: 99th percentile 63–127 ms). The thread that acquires the window (here the render thread) runs under a time-constraint policy whose computation the host re-declares every 120 frames; it was 1.25 × the 90th percentile of that thread's CPU time per frame. This render thread needs 1.9–2.0 ms a frame with one or two frames in 120 near 4.4 ms, against 2.5 ms declared, and each frame past the declaration stalled the thread. Probes on both threads' waits showed the main thread waiting 20–34 ms for the render thread once a second; adding probes slowed the render thread and the stalls vanished, and `DM_RT=0` or a fixed `DM_RT_COMP=5` / `10` removed them too (99th percentile 18–21 ms) at the same CPU energy. The declaration now also covers 1.1 × the window's peak (`rt_comp_tick` in `hle_nvn.c`): 5.6 ms here, 60.0 fps, 99th percentile 21 ms. + +## Rendering + +FromSoftware's engine draws forward into a 1920x1080 RGBA16F target, with depth of field, light shafts and bloom at quarter size. + +- **Resolved: distant walls and floors smeared sideways.** The map materials (`FRPG_Phn_*Lit*`) read their combined texture / sampler handles from a uniform buffer (`fp_t_cb13_*`, resolved at replay by `ubo_handle`), and the game keeps 12 sampler pools of 2,304 entries, each with its own samplers under the same IDs (ID 261 is clamp-to-edge with LOD 0..0 in one pool, repeat with mips in others), one per command buffer that records the scene. The replay resolved uniform-buffer handles in the first pool, so the walls' tiling base textures (u ≈ −3.45) took a clamp-to-edge sampler and repeated their edge column across the wall. The replay now keeps the texture and sampler pools bound (`OP_SET_TEX_POOL` / `OP_SET_SAMP_POOL`) and adds their bases to handles read from uniform buffers (`ubo_tex_id` / `ubo_samp_id` in `nvn_mtl.m`). Found by highlighting the six most common fragment shaders of the scene pass (`DM_NVN_HILITE`), then the next six, and reading the wall draw's binding lines (`DM_NVN_PASSDUMP_ALL`). +- **Resolved: 6,734 red lines in one run.** The list of command buffers that bound a non-first pool had 64 entries and was never cleared, and this game binds a non-first pool in more than 64. The bases of bound handles now live in each command buffer (`cmd_t` `tex_base` / `samp_base`). + +## Files and shaders + +- RomFS is two dvdbnd archives, `dvdbnd0.bdt` and `dvdbnd1.bdt` (3.6 GB), which the game reads itself, each indexed by the `.bhd5` beside it: `BHD5`, 0xff (little-endian), u32 1, u32 file size, u32 bucket count, u32 0, u64 bucket table offset; each 16-byte bucket `{u32 count, u32, u64 offset}` points at 16-byte entries `{u32 path hash, u32 size, u64 offset}` (paths only hashed). +- 5,184 entries are DCX containers (`DCX\0`, big-endian header, `DFLT` = a zlib stream at +0x4c), most of them BND3 archives (28-byte entries `{u8 flags, i32 size, u64 offset, i32 ID, u32 name offset, i32 unpacked size}` in formats 0x5c / 0x7c). Only archives that contain a GLSLC magic are parsed (the others use format 0x78). +- **Current:** 3,099 shaders (391 vertex, 2,708 pixel), GLSLC outputs: 3,040 in six BND3 archives in `dvdbnd0.bdt`, one per entry (`.vpo` vertex, `.fpo` pixel, named `Shader/FRPG_Sfx/NVNShaderBinary/ps/…` or by base name in the two FLVER material archives, 2,602 + 486 entries), and 59 more in `main`. `shader_split.py` reads them with `bhd5_entries`, `undcx`, `bnd3_members` and `bdt_shaders`, in 64 jobs by entry range, about 10 s. Single-stage programs, as in 13 Sentinels; every binding decodes. +- **Resolved: SDK 4's GLSLC outputs** (version 0x20.4). Pixel shaders' control sections store at +0x20 a value 12 bytes short of their length, so the code area is not right after it, and 44 of `main`'s 72 outputs and most of the archives' pixel shaders failed the reader's check. `glslc_section` reads such outputs through their own section headers. It also gave Three Houses 8 and Age of Calamity 3 shaders that used to be dropped. + +## Movies + +- `subsdk0` is the SDK's movie library: its `movie::Extractor` demuxes the H.264 MP4 movies (`movww/`, `movjp/`) and the host decodes them into the game's RGBA texture (a native-texture movie, as in Monster Hunter Rise). +- **Resolved: a call through a null pointer in the host's `movie::Decoder::Configure` right after `nnMain` starts the logos.** SDK 4's `Decoder` constructor has no priority argument (`_ZN5movie7DecoderC1ENS_11DecoderTypeENS_19DecoderOutputFormatENS_11DecoderModeE`); the host took over only the four-argument one, so the SDK built its own decoder and the host's `Configure` read that object as its own. Both forms are taken over now (`hle_movie.c`). +- **Resolved: `FATAL movie: missing SDK symbol _ZNK5movie6Buffer10GetNvnSyncEv`** at the first frame of the movie. SDK 4's `movie::Buffer` carries the texture object but no sync object. The lookup is optional now; the game reads the texture once `GetOutputBuffer` returns, which the CPU write satisfies. + +## Services + +SDK 4.5.0 boots, one each: + +- **Resolved: abort in `sm::Initialize` under `nn::friends::GetBlockedUserList`** after the opening movie: SDK 4's overload without the `Uid` (`EPiPNS_7account23NetworkServiceAccountIdEii`). It reports an empty list with success: the game turns a failure into an error for the error applet (`main+0x8ff9f0`). +- **Resolved: crash in `lmem::AllocateFromExpHeap` under `ngc::ProfanityFilter::MaskProfanityWordsInText` right after the character name is entered.** The game calls the UTF-16 overload (`EPiPtj`) by name; the host had taken it over only in the filter's vtable, so the SDK's version ran on a filter whose word list the host never loaded. The `unsigned short` and `char16_t` overloads are taken over by name too (`hle_misc.c`). +- **Resolved: one thread at 100% CPU from boot, with one SVC in 99 s** (`NexusRevolution Socket`, `main+0x92c7c4`, the game's network thread). It calls `nn::socket::Poll(fds, 0, 33)`: no descriptors and a 33 ms timeout, which on hardware is a 33 ms sleep. The host failed every socket call at once, so the thread spun 11,000 times a second (88,000 hits of a probe in 8 s). `Poll` and `Select` with nothing to wait on now wait out their timeout and return 0, including SDK 4's `Select(int, fd_set*, …, timeval*)` (`hle_net.c`, `dm_guest_sleep_ns` in `hle_thread.c`). Guest CPU fell from 1,030 to 60 ms per second. + +## Input + +- The game maps its buttons by position: B confirms, A goes back (its prompts say so); menus that seem to ignore A are not a host fault. +- The first boot shows two notices before the title menu ("Unable to connect to the network", then "Game will start in offline mode"), and a new game first walks through Brightness, Options and Button settings. + +## Mac routes + +- From a fresh save, `DM_KBD=Ash DM_KEYS="20:A~0.2,25:Plus~0.2,40:A~0.2,50:A~0.2,60:A~0.2,70:B~0.2,80:Down~0.08,82:B~0.2,90:Up~0.08,92:B~0.2,100:Down~0.08,102:B~0.2,110:B~0.2,116:B~0.2,122:B~0.2*15/3,168:Left~0.1,170:B~0.2,185:Plus~0.2,190:B~0.2,200:Plus~0.2,210:B~0.2"` goes through the two network notices, New Game, the first-run settings, character creation (a Warrior with the default look) and the opening into the Asylum cell (control at about 245 s). The steps are in seconds, not frames: the loads take different frame counts from run to run. +- From a save made there, `DM_KEYS="20:A,25:Plus,40:A,50:A,60:A,70:B,76:B,82:B"` continues into the cell at about 90 s. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **SDK 4's GLSLC outputs read through their section headers** (`glslc_section`) and **uniform-buffer handles resolved against the bound pools** (`OP_SET_TEX_POOL` / `OP_SET_SAMP_POOL`, `ubo_tex_id` / `ubo_samp_id`, `cmd_t` `tex_base` / `samp_base`): [04, Seen in Dark Souls Remastered](../../../docs/agents/04-graphics.md#seen-in-dark-souls-remastered). +- **The dvdbnd / DCX / BND3 reader** (`bdt_shaders`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **SDK 4's movie decoder constructor and its buffer without a sync object** (`hle_movie.c`), **`friends::GetBlockedUserList` without a `Uid`**, **the UTF-16 profanity masking by name** (`hle_misc.c`), and **`Poll` / `Select` with nothing to wait on sleeping out their timeout** (`hle_net.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **The real-time declaration covering the window thread's peak frames** (`rt_comp_tick` in `hle_nvn.c`): [03, Frame rate](../../../docs/agents/03-bringup.md#frame-rate). + +## Open + +- Climbing ladders still misplays at 60 fps (with the community's cheat too). +- Once, loading a save on the iPhone stayed on the loading screen (no save write, no red line); the next launch loaded it, and it has not come back. +- SDK 4.5.0 is called "the oldest so far" in `game.tsv`, the bring-up notes and `AGENTS.md`, while Mario Kart 8 Deluxe ([0100152000022000](../0100152000022000/README.md)) is SDK 4.4.0, also called the oldest. +- [04](../../../docs/agents/04-graphics.md#seen-in-dark-souls-remastered) speaks of `main`'s 72 GLSLC outputs (44 of which failed the old check), `game.tsv` of 59 shaders in `main`; the sources do not say how the two counts relate. diff --git a/dm/games/010051701FB46000/README.md b/dm/games/010051701FB46000/README.md new file mode 100644 index 0000000..cb97f6e --- /dev/null +++ b/dm/games/010051701FB46000/README.md @@ -0,0 +1,78 @@ +# Monster Train 2 (`010051701FB46000`) + +Update 1.1.1 (v131072) over the base game, SDK 18.3.5. Unity 2022.3.54f1 IL2CPP with FMOD on NVN. Modules `main`, `subsdk0` (the movie module, whose `movie::Extractor` demuxes the intro) and `sdk`; the `sdk` is Super Mario Galaxy's build ([010099C022B96000](../010099C022B96000/README.md)). Plays on the iPhone; 60 fps (30 on the Switch). On the Mac: the title, the 90 s intro movie with its sound, the tutorial battle (deployment, three waves, enemies on two floors), the merchant and the route map, at 60 fps with music and saves (the intro plays once per save); no red line, about 2.17 GB resident and 1.21 GB of Metal allocations, flat over twelve minutes. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `present_interval display` | [`game.tsv`](game.tsv) | 60 fps: the game asks for interval 2 and its animations follow the time that passed; see [Frame rate](#frame-rate) | +| `pad_layout full` | `game.tsv` | the 3D layout, whose right stick scrolls the route map as the game's prompt shows | +| `sys_resource 0x1000000` | `game.tsv` | the NPDM's 16 MB of system resource: virtual address memory | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0x40000000` | `game.tsv` | Ace Attorney Investigations Collection's layout ([010005501E68C000](../010005501E68C000/README.md)): with the hardware pool (3.2 GB) StandardAllocator's region alone reached +3.9 GB of the 4.38 GB alias region in `nninitStartup`; see [Memory](#memory) | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `60751516…` (Super Mario Galaxy's and Galaxy 2's; the constructor at `sdk+0x13be3c`, the movs at `+0x13be4c` and `+0x13bee0`) | the build already had it | +| `svcGetInfo` type 24 (FreeThreadCount) | host, `hle_svc.c` / `thr_limit_used` (every game) | Unity creates a thread only when the count is at least 1; see [Threads](#threads) | +| Movie audio decoder output slots | host, `hle_movie.c` (every game) | the intro hung on a black screen; see [Movies](#movies) | +| `shader_groups /Data/=data` | `game.tsv` | everything is under `Data/`: the player's serialized files (the post-processing compute shaders in `sharedassets2.assets`) and the `StreamingAssets` bundles | + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). The game asks for present interval 2 (Unity's 30 fps setting) and nothing else holds it at 30: `present_interval display` presents a frame every display tick, and the iPhone's display link runs at 60 Hz. The GPU takes about 2 ms a frame in battle and on the route map (M4 Pro). +- Its animations follow the time that passed (Unity's `deltaTime`; the PC versions run at any refresh rate). From one save, screenshots every third of a second through the move to the merchant (its fade-in, the greeting) match at 30 and 60 fps: mean difference 3.9, against 11.0 (up to 89 in the fade) for what frame counting would show. +- `DM_PRESENT_INTERVAL=game` (`Documents/env.txt` on the iPhone) keeps 30 and saves power. +- **Resolved: `[RED] fs ReadFile into … failed at +0 (EFAULT): VM region … protection 1`, now and then at 60 fps** (two runs in three as the route map loads, never at 30 fps). The destination was texture pool memory, which the dirty-page tracker write-protects after each upload (`nvn_dirty.c`); `ReadFile` lifts the protection first, but the graphics layer can protect the pages again before `pread` writes them, and at 60 fps uploads come twice as often. On EFAULT the host now prepares the rest of the buffer again and reads on, up to eight times (`h_fs_ReadFile`; the first case is logged, `[fs] ReadFile … prepared again`). Four runs at 60 fps: one retry, no failed read. + +## Memory + +- The NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true: `nninitStartup` hands application pool − used memory to StandardAllocator as virtual address memory and never calls `SetMemoryHeapSize` (no heap), as in Ace Attorney Investigations Collection, Brain Memory Training Academy and Patrick's Parabox. The node-area cut was already on this `sdk` build. +- With the hardware pool (3.2 GB), StandardAllocator's region alone took the alias region to +3.9 GB of 4.38 GB in `nninitStartup`; hence their 1 GB pool and 5 GB block (256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region). +- Measured on the Mac: the alias region is mapped up to +3.25 GB within 90 s and stays there for 12 minutes (+3.5 GB in a run to the first shop), at most 1.75 GB of it in use. The host does not hold the game to the pool, as for Ace Attorney. + +## Threads + +- **Resolved: the main thread waited forever on an event right after `nnMain` started, with only three or four threads created.** `main+0x3e7288` starts a thread and waits for it to signal `main+0x539cf80`. Unity's thread start (`main+0x3e1a50`) creates a thread only when `nn::os::GetFreeThreadCount()` is at least 1, and that is `svcGetInfo` type 24 (FreeThreadCount, Horizon 11 and later), which the host answered 0 like every type it did not know: the thread was never asked for (`DM_THR=1` showed no `CreateThread` for it). The host now answers 96 less the game's threads that have not exited (`thr_limit_used`). Of the 20 games extracted on that machine, only this `main` imports the function. With it the game creates 40 threads and reaches its title. + +## Movies + +- The intro is `StreamingAssets/intro_fmv.mp4`, 90 s of H.264 High with 2 reorder frames and AAC interleaved packet by packet, played by Unity 2022.3's movie player: `movie::Extractor` in `subsdk0` feeds both decoders from one thread in file order. The game's A, B and + do not skip it. +- **Resolved: the movie stayed black after its first picture, the sound never started, and the game kept presenting frames.** The player takes every output a decoder offers and starts playing only once it holds about half a second of audio. The host then gave each decoder 8 output slots and takes input only while one is free (as MediaCodec does). `DM_MOVIE_TRACE=1` (each line names the decoder): Unity held all 8 audio outputs, the audio decoder kept the next 4 packets and handed out no input index, the video packets behind them in the file never came, and the video decoder had given out one picture (the host releases H.264 pictures once more than 4 are queued). Slot counts per decoder kind, measured: 8 audio / 8 video and 8 audio / 32 video hang; 64 audio / 8 video plays, starting with 27 audio and 8 video outputs held (peak 30 audio). Audio decoders then got 64 slots (they had 8 when the game was brought up, 32 since DRAGON QUEST XI S). +- **Current:** an AAC decoder has 32 output slots (`MD_NOUT_AAC`, set for Mario vs. Donkey Kong ([0100B99019412000](../0100B99019412000/README.md)), with this game's peak of 30 held as the reason it is enough), Vorbis 64 (`MD_NOUT`), video 16 (`MD_NOUT_VIDEO`). A PCM slot is 128 KB, allocated when first used. + +## Services + +- `nvnSyncSetDebugLabel` (a no-op). + +## Input + +- Controller only: the game reads no touch, docked or handheld (taps on the title and on route map nodes in handheld mode did nothing). It stays docked at 1920x1080 with the 3D pad layout, whose right stick scrolls the route map. A selects and plays cards, B goes back, Y ends the turn. + +## Files and shaders + +- 15,937 shaders: 15,841 vertex, pixel and geometry shaders in the player's serialized files and bundles, the player's 11 GLSLC outputs in `main` (one of them compute), and 85 kernels of the post-processing stack's `ComputeShader` objects in `Data/sharedassets2.assets`. The `Shader` objects use the Unity 2022.3 layout already in `unityfs.py`. The built-in VR blit's geometry shader does not build. +- **Resolved: the 3D battle scene was black behind a correct UI**, with `[RED] shader 854a8afc9d07f88d is not in the shader map` and `the dispatched program has no compute shader`. `DM_NVN_SHDUMP` saved the program; its code bytes were in `Data/sharedassets2.assets`, uncompressed, inside a `ComputeShader` (class 72): the post-processing stack's colour grading LUT baker (`Lut3DBaker#KGenLut3D_AcesTonemap`), so the LUT the scene is graded through was never baked and stayed black. The split read only `Shader` objects. A `ComputeShader` holds per platform variant a list of kernels, each with its unique variants; a variant's `code` is the program code of a `Shader` subprogram (the 0x60-byte stage header, only the compute block present) and its `threadGroupSize` matches control +0x748. `unity_compute` in `shader_split.py` reads them beside the `Shader` objects of every serialized file. No file carries a class 72 type tree, so `unityfs.py` carries Unity 2022.3.54f1's layout (`COMPUTE_2022_3` in `KNOWN_TREES`, written from AssetRipper's TypeTreeDumps for that version): all 12 objects decode, `resourcesResolved` true at each end. 85 compute shaders came in (86 with the player's own). + +## Saves + +- The intro plays once per save. A save past the intro starts at the title, and A there continues the run. + +## Mac routes + +- From a fresh save, `DM_KEYS="14:A"` starts the game; the intro movie plays to its end and the tutorial battle loads at about frame 3,400. +- `DM_KEYS="14:A,110:A*90/2,111:Y*20/9"` plays through the tutorial battle to the merchant by about frame 8,000. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **`svcGetInfo` type 24, FreeThreadCount** (`thr_limit_used`): [03, Threads](../../../docs/agents/03-bringup.md#threads). +- **Movie decoder output slots per decoder kind** (`MD_NOUT`, `MD_NOUT_AAC`, `MD_NOUT_VIDEO`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **`nvnSyncSetDebugLabel`** as a no-op (no other source describes it). +- **`ReadFile` re-prepared on EFAULT** against the dirty-page tracker (`h_fs_ReadFile`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Unity `ComputeShader` objects** (`unity_compute`, `COMPUTE_2022_3`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **`present_interval display`** for a game that steps by frame time: [dm/games/README.md, Keys](../README.md#keys). +- The spin-wait search takes the movie library's thread loop wider in this game's `subsdk0`: [development.md, Power](../../../docs/development.md#power). + +## Open + +- docs/development.md says audio decoders have 64 output slots and the 03 entry says video keeps 8; the host now has 32 for AAC (`MD_NOUT_AAC`) and 16 for video (`MD_NOUT_VIDEO`). No source says the intro was played again after those changes. +- The built-in VR blit's geometry shader does not build. diff --git a/dm/games/01005270232F2000/README.md b/dm/games/01005270232F2000/README.md new file mode 100644 index 0000000..6b263a1 --- /dev/null +++ b/dm/games/01005270232F2000/README.md @@ -0,0 +1,94 @@ +# Octopath Traveler 0 (`01005270232F2000`) + +Update 1.0.5 (v327680) over the base game, an XCI; SDK 19.3.13. Square Enix, Unreal Engine 5 (the paged global distance field shaders and the LowLevelTasks task system) on its mobile renderer (`SF_GLSL_SWITCH_MOBILE` shaders), NVN, CRIWARE sound and movies. Modules `main`, `subsdk0` (the movie library, which `main` imports from) and `sdk`. Plays on the iPhone at 60 fps (30 on the Switch) by the host's writes of Unreal's frame limits. + +Status: brought up on the Mac through character creation and the story opening into free play (26 minutes from a fresh save with no red line, 2.74 GB resident). Playable on an iPhone: 30 fps through character creation, name entry and the story opening, no audio underruns, three saves, a footprint of about 2.35 GB after 3.4 minutes; it plays at 60 fps on an iPhone too (`fps_target 60`). Battles are not tested yet. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `fps_target 60` | [`game.tsv`](game.tsv), `hle_fpslock.c` | the host writes 60 into `t.MaxFPS` and 1 into `rhi.SyncInterval`; see [Frame rate](#frame-rate) | +| FMallocBinned3 constructor cut: `main+0x12e40` `bl 0x9f480` → `nop` | [`patches.py`](patches.py), `main` `9fe60a21…` | the constructor reserves 37 GB of address space; the host's allocator runs Unreal's heap (`hle_uemalloc.c`); see [Memory](#memory) | +| `game_args -SwitchVirtualAddressSpaceSize=2048` | `game.tsv` | caps the engine region Unreal's `nninitStartup` reserves (62 GB by default) at 2 GB | +| Node area cut 1 GB → 62 MB (`sdk+0x1423fc` / `+0x142468`) | `dm/tools/sdk_patches.py`, `sdk` build `7bba185a…` | Unreal's region then starts 62 MB into the alias region, and the guest block stays small enough for an iPhone | +| 3.75 GB guest block: `mem_reserve 0xF0000000`, `mem_alias 0xC8000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | alias region = node area + up to 1 GB of alignment (regions of 1 GB and more are 1 GB aligned) + the 2 GB region; 256 MB stacks, 128 MB heap | +| `tex_dyn_slots on` | `game.tsv` | 61 particle vertex shaders pick one of several c2 table slots at run time (separate texture slots 0-10, samplers 0-4); see [Shaders](#shaders) | + +No `pad_layout` key: the iPhone gamepad starts in the 2D layout ([README](../../../README.md)). + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). The game's config caps Unreal at 30: `t.MaxFPS=30` in `[ConsoleVariables]` of `DefaultEngine.ini` (and `FrameRateLimit=30` in `SwitchGameUserSettings.ini`, which sets it again), and `rhi.syncinterval=2` in `SwitchDeviceProfiles.ini`, the present interval 2 the game asks for. The shipping build reads no console variables from the command line (no `-ExecCmds`, `-DPCVars` or `-ini:`), so the host writes 60 and 1 into the two console variables (`hle_fpslock.c`, profile key `fps_target 60`), whenever the game sets them back; the game then asks for present interval 1 itself. +- Unreal steps the game by the time that really passed: on the Mac, `DM_NVN_REGLUM` traces of the same input script from a fresh save at 30 and 60 fps (frame time p50 33.4 → 16.7 ms) put the 1.5 s fade to white 70 s in at the same real time with the same shape, and both runs reach the same screens at the same times. +- `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) keeps the game's own 30 and saves power. +- Unreal paces frames by the layer's frame number (`nn::vi::GetLatestFrameNumber`, `main+0x10140e8`); see [Services](#services). + +## Memory + +- **Resolved: `exit(1)` in `nninitStartup`** (`main+0x12e74`; the function is at `main+0x12d74`). Unreal's `nninitStartup` needs virtual address memory and reserves one address region for the engine with `AllocateAddressRegion`, 62 GB by default (`-SwitchVirtualAddressSpaceSize=` in MB, or in bytes above 4095; the other options it reads are `-SwitchMaxMemorySize=`, `-NVNCommandBufferGranularity=`, `-NVNControlBufferSize=`, `-SwitchMainThreadStackSize=`, `-NVNTempMemoryPageSize=`, `-ResX=` / `-ResY=` and `-SwitchDisableVirtualAllocator`, which also exits). It reads them from the game arguments, which the host used to set only after `nninitStartup`; they are set before it now, as on hardware, and the profile key `game_args` supplies them on iOS. +- **Current: the allocator is the host's.** Unreal's allocator, an FMallocBinned3, is constructed in place at `main+0x68022c8` (`main+0x12e40` → constructor `main+0x9f480`) and reserves 37 GB of the region (`main+0x9f748`: 74 pools of 512 MB, pool index = offset >> 29; the null read in the out-of-memory report at `main+0x1005824` is the symptom when the region is smaller). The pools are uneven: at character creation the largest holds 68 MB, the 74 together 732 MB (measured with `DM_ALIAS_WINDOWS=512`, [05, Memory growth](../../../docs/agents/05-debugging.md#memory-growth)), so no smaller window fits a phone's ~5 GB (an iPhone has about 5 GB of address space without the extended virtual addressing entitlement). The constructor call is cut (`patches.py`) and the host puts its own allocator's vtable in the object (`hle_uemalloc.c`: Malloc, Realloc, Free and the size queries on a host malloc zone, the trivial slots kept). Every allocation goes through it: FMemory reaches the object through `FPlatformMemory::BaseAllocator` (`main+0x132b4`), and the thread-local storage allocator `nninitStartup` registers (`main+0x12e80` / `main+0x12ec0`) through its vtable. The Unreal heap logs its peak as `[mem] Unreal heap … in use`. +- **Current: a 2 GB region in a 3.75 GB block.** With the host allocator the region holds only what the engine maps itself (709 MB at character creation, spread over 1 GB), so it is capped at 2 GB (`game_args`); SDK 19's AddressRegionManager node area is cut to 62 MB as Engage's (`sdk+0x1423fc` / `+0x142468`), so the guest block is 3.75 GB. The Unreal heap lives outside the guest block. +- 15 minutes into the story on the Mac: region mapped up to 1.26 GB of the alias region, Unreal heap 0.75 GB, 2.8 GB resident, 1.84 GB of GPU allocations. + +## Shaders + +- 10,324 unique shaders (1,457 vertex, 6,346 pixel, 2,521 compute) in the pak's two shader code archives, `ShaderArchive-Global-SF_GLSL_SWITCH_MOBILE-SP_GLSL_SWITCH_MOBILE.ushaderbytecode` (1,219 shaders) and `ShaderArchive-Octopath_Traveler0_Chunk0-…` (9,377), both stored in the pak. The profile groups them as `global` and `game` (`shader_groups`). +- Version 2 archives: shader map hashes, shader hashes, shader map entries, shader entries `{u64 offset, u32 size, u32 raw size, u8 frequency}` (17 bytes packed; frequency 0 vertex, 3 pixel, 5 compute), preload entries and shader indices, then the code. Every shader is Oodle-compressed (Mermaid, block header `8C 0A`). The repository has no Oodle decoder, so the game's own `OodleLZ_Decompress` (statically linked into `main`, `main+0x4f77e10`, found as the one function that takes the address of the library's own name string, its profiler scope) decodes them in the Mac host: `DM_OODLE_JOB` (`dm/host/hle_oodle.c`), the NSP script's `decode_ue_shaders`, as for any game with Unreal paks; about 10 s for all of them (see [Open](#open)). +- A decoded shader is an `LSLG` record: `LSLG`, a stage tag (`SV`, `SP`, `SC`), a header with the source name (`/Engine/Private/…usf:MainPS`), then `u32` code length, the code area, `u32` control length (0x880), the control section and a 24- or 29-byte tail, all at unaligned offsets (`uepak.lslg`). The host keys came out exactly: 37 of 37 shaders a first run reported missing were in the two archives. +- Vertex and pixel shaders combine per draw from Unreal's pipeline state, so every vertex shader is paired with the single-stage cover and made to link with any pixel shader (`shader_split.py` `separable_pairs`, `dmsh_build.py` `vs_pad`). +- Bindings: separate textures and samplers, as Engage's (`0x168` / `0x568`). +- **Current: 61 vertex shaders pick a texture slot at run time** (particles reading their data from buffer textures). The shader reads one of several c2 texture-table words, chosen by branches, and ORs it with a sampler-table word; the translator cannot trace that to one slot and falls back to Ryujinx's bindless arrays (`array, 32> vp_tb_cb0_40000000`, `array vp_s_cb0_40000000`), which used not to load. Every integer read of c2 in those shaders is a table slot (texture slots 0-10, samplers 0-4), so `tex_dyn_slots` is right here. Host only: the chain version does not change. +- **Current: 2,084 shaders do atomics on buffer images** (Unreal's GPU particles; three ran in the first 100 seconds), now bound to the image's memory. +- **Current: a buffer texture at pool offset `0x168`**, read by the particle shaders, is created there (it used to be left a dummy, below Metal's reported 16-byte alignment). +- **Unbuilt:** 16 compute shaders do atomics on a 3D `r32ui` image, which SPIRV-Cross's MSL 2.3 image-atomic emulation indexes with an `int3` (`array subscript is not an integer`). All are Unreal's paged global distance field (`GlobalDistanceFieldCompositeObjects.usf:CompositeObjectsIntoPagesCS`), which the mobile renderer does not dispatch (none in any run). + +## Files + +- Everything is in one pak, `Octopath_Traveler0/Content/Paks/pakchunk0-Switch.pak` (1.5 GB, 159,757 files; the profile's `romfs_marker`), next to loose CRIWARE `.awb` sound banks and one `.usm` movie. +- A version 11 pak: its footer is the last 0xdd bytes (encryption key GUID, encrypted-index flag, magic `0x5A6F12E1`, version, index offset and size, SHA-1, five compression method names), its magic 0xc8 bytes from the end. The index is AES-256 (ECB) encrypted: mount point, entry count, path hash seed, path hash index, full directory index (a second encrypted block) and the encoded entries (one bit-packed u32 per file). +- The key is in `main`: the one 32-byte window of its read-only or data segment that decrypts the index start to the mount point `../../../` (`main+0x57c7f70`, found among 116,287 candidates in under a second). Never print it. +- Most files are Oodle-compressed in 64 KB blocks; the two shader code archives are stored. `dm/gfx/tools/uepak.py` reads all of this (AES through macOS CommonCrypto) and lists a pak (`uepak.py list
`). Details: [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). + +## Rewriter + +- **Resolved: a `jump table ` assertion on a byte table whose `add` is far from the `br`** (`main+0x37f9bc8`: `adr x18,#L; ldrb w0,[x17,x15]; add x18,x18,x0,lsl #2`, then 17 register moves before `br x18`). The byte / halfword search looked 16 instructions back; `jump_bytes_far` looks 32 back. One of the 12 `br x18` sites in this `main`. +- `main` has a 71 MB stretch without unwind entries; it got through the rewrite on a quieter Mac before functions were analysed in pieces, and comes out the same with 1 MB pieces and `b` within 256 KB. +- The spin-wait search takes 75 → 78 loops in `main` since the store rule (a parser state that leaves on the next pass, `main+0x4ebfa54`, among them), and the movie library's thread loop is wider in `subsdk0`. + +## Services + +- **Resolved: an abort after `UNIMPL` on the weakly imported `thread_local` init function `_ZTHN13LowLevelTasks5FTask10ActiveTaskE`.** The loader bound every undefined import to a named stub, so the address test the compiler emits for a weak symbol saw a function. Such a symbol is now 0 (`loader.c`, `undefined weak … bound to 0`). +- **Resolved: SIGSEGV in `android::Surface::getQueuedFrameNumber` under `nn::vi::GetLatestFrameNumber`** (Unreal paces frames by the layer's frame number, `main+0x10140e8`): the SDK read the vi service's surface, which the host never made. `hle_vi.c` answers with the frames presented so far; `ListFrameHistory` returns no entries. +- **Resolved: SIGSEGV at address 0 in `nn::fs::GetSaveDataSizeMax`, about 13 minutes into a new game** (Unreal's save system asks before its first save): it now reports 256 MB of data and journal, as `GetSaveDataSize` does (`hle_fs.c`). Listing `main`'s undefined imports that no host hook names, before they crashed, gave `fs::GetCacheStorageSize` and `fs::GetFreeSpaceSize` (the same answers), `account::PushOpenUsers` (nothing to hand over), `web::ShowOfflineHtmlPage` (cut, as `ShowWebPage`) and `oe::ExecuteProgram` (logged red: the host runs one program). The rest were pure computations (the swkbd presets, the web argument constructors, `time::GetDayOfWeek`, the audio resampler) or socket calls behind a `Socket()` that already fails offline. +- **Saves:** four saves in a 26-minute run, and a later boot continues from them. +- **NVN calls new with Unreal:** `nvnTexturePoolGetSize` / `nvnSamplerPoolGetSize` and their memory pool / offset getters (an abort in device setup when they returned 0), and `nvnCommandBufferFenceSync`, which Unreal's RHI records in its command buffers (`OP_FENCE_SYNC`: the replay fences the sync with the submission that reaches it). + +## Input + +- **Keyboard:** the character's name and favourite dish come from the system keyboard (`DM_KBD` on the Mac, the input box on the iPhone). +- **Resolved: the sound stuttered and the input box lagged while the player typed a name** (on an iPhone: one 4.7 s frame, 5,035 wakeups a second instead of about 1,800, no audio underruns). The host froze the guest clock while `swkbd::ShowKeyboard` was up; Unreal calls it off its game thread, so the game kept running, and every thread that waits on the clock spun while its audio mixing starved. On the Switch the applet blocks only the caller while the clock runs; the host now does the same (`hle_swkbd.c`). The iOS input box froze the clock a second time around its own wait (`front_ios.m`), so the first fix changed nothing on the phone (4,977 ms and 2,577 ms frames, 5,181 wakeups a second). On the Mac, `DM_KBD= DM_TEST_SWKBD_HOLD_MS=8000` holds the keyboard up: frozen, 8.8 fps and 7,936 wakeups a second in that window; running, 30 fps and 1,788. +- The game reads a mouse and no touch screen ([Romance of the Three Kingdoms 8 Remake's note](../0100C2A01A730000/README.md)). + +## Mac routes + +- A fresh save reaches the story with `DM_KBD=Lyra DM_KEYS="20:Plus,24:A*27/4,150:Plus,154:A*62/3,340:Right*12/3,341.5:A*12/3,380:Plus*5/3,396:A*20/3,470:A,474:Up,476:A,480:A*140/3"` (initial settings, the prologue, character creation with three belongings, "Begin Lyra's journey?"). + +## Found on this game + +General host changes first needed here; each applies to every game (or every Unreal game): + +- **Unreal's allocator replaced by the host's** (`hle_uemalloc.c`, a rewriter cut of the constructor), explained in [Memory](#memory) above; the later Unreal builds and their vtable layouts: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **Unreal's frame limits written in game memory** (`hle_fpslock.c`, `fps_target`), explained in [Frame rate](#frame-rate) above. +- **Game arguments set before `nninitStartup`** (`main.c`, profile key `game_args`): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **Unreal pak reading and Oodle-compressed shaders decoded by the game's own Oodle** (`uepak.py`, `DM_OODLE_JOB`, `decode_ue_shaders`): [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content), [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **Texture slots picked at run time** (`tex_dyn_slots`, `NVN_RB_TEXD` / `NVN_RB_SAMPD`), **image atomics bound to the image's storage** (`NVN_RB_IMGA`, `atom_scratch`), **buffer textures below 16 bytes**: [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **Rewriter:** byte tables far from their `br` (`jump_bytes_far`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). Not first found here but met here: long functions analysed in pieces (`pieces`, same section) and spin waits that store (`patch_nso.py` "Spin waits", [development.md, Power](../../../docs/development.md#power)). +- **Undefined weak imports bound to 0** (`loader.c`), **`vi::GetLatestFrameNumber`** (`hle_vi.c`), **`fs::GetSaveDataSizeMax`** (`hle_fs.c`), **NVN pool getters and `nvnCommandBufferFenceSync`** (`OP_FENCE_SYNC`), **the guest clock left running behind the software keyboard** (`hle_swkbd.c`, `front_ios.m`, `guest_clock_freeze_for`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **`DM_ALIAS_WINDOWS`**, the alias-region window census: [05, Memory growth](../../../docs/agents/05-debugging.md#memory-growth). + +## Open + +- Battles are not tested yet. +- Shader decode counts and times differ between sources: `docs/development.md` says 10,324 shaders decoded in about 10 s; `dm/games/README.md` says 10,596 shaders in 2 s (10,596 is the two archives' total, 1,219 + 9,377; 10,324 the unique count). +- Touches as a mouse would suit Romance of the Three Kingdoms 8 Remake, but this game, Tears of the Kingdom and Unicorn Overlord also read a mouse and no touch screen, so such a rule needs testing here first. diff --git a/dm/games/0100535012974000/README.md b/dm/games/0100535012974000/README.md new file mode 100644 index 0000000..90fac01 --- /dev/null +++ b/dm/games/0100535012974000/README.md @@ -0,0 +1,52 @@ +# Hades (`0100535012974000`) + +Update 1.0.38233 (v589824), SDK 9.3.0. Supergiant Games' engine on The Forge renderer, on **Vulkan**, not NVN: `main` imports 100 `vk*` functions from `subsdk0` (the Vulkan loader), which sits on NVIDIA's driver in `subsdk1` (NVN, OpenGL, Vulkan). The host implements those functions on Metal itself (`dm/gfx/vk/hle_vk.m`), so only `main` and `sdk` are loaded. Plays on the iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `modules main sdk` | [`game.tsv`](game.tsv) | `subsdk0` / `subsdk1` (Vulkan loader, NVIDIA driver) are replaced by the host's Vulkan layer and not loaded | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0), so the game uses `SetHeapSize`. Link's Awakening's partition, not measured for this game | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the right stick aims the cast | +| Vulkan shaders through `vk_shaders.py` | NSP script | the game has no NVN shaders (`shader_note`); see [Files and shaders](#files-and-shaders) | + +## Vulkan layer + +The layer was written for this game (Super Mario Galaxy extended it later: [010099C022B96000](../010099C022B96000/README.md)). + +- **Current:** one Tegra X1-like device (Vulkan 1.1). Device memory is a shared `MTLBuffer` made on first use; images are their own textures. +- **Current:** command buffers are recorded and replayed into one Metal command buffer per submit on the layer's own queue. Swapchain images are shown through the NVN layer's present path once their frame's GPU work is done. +- **Current:** names the game asks for through `vkGet*ProcAddr` are logged once each. +- How the host tells the API apart: a `main` that imports Vulkan functions and not `nvnBootstrapLoader` renders with Vulkan. `profile_guess.py` leaves `vkGetInstanceProcAddr` (SDL's Vulkan loader in other games) out of its Vulkan count; Hades and both Galaxies import 99 or 100 Vulkan functions, `vkCreateInstance` among them. + +## Files and shaders + +- 74 SPIR-V files in `Content/EffectsForgeSwitch` (`.vert` / `.frag`; the `.shader` files list each program's pair). `romfs_marker` is `Content/EffectsForgeSwitch/Default.vert`. +- `dm/gfx/tools/vk_shaders.py [--ios ]` translates them with SPIRV-Cross into `/vk/`: one library, and a table of the Metal slot each (set, binding) moved to. The game hands the same words to `vkCreateShaderModule`, so the host finds each shader by the hash of its SPIR-V. +- **Packing:** for a Vulkan game the NSP script runs `vk_shaders.py` (Mac and iOS libraries in `_build/shader/vk` and `shader/ios/vk`) in place of the NVN shader split and chain build, and `build_ipa.sh` ships `$MSL_IOS/vk` instead of the NVN chain libraries (`GFXAPI`). The NVN shader tables stay empty. + +## Services + +- **Resolved: the notification event.** The game polls `oe::GetNotificationMessageEvent` every frame. The SDK's own event was never created (the host's `oe::Initialize` is a no-op), so the host now hands out an event of its own, signalled while a notification is queued. +- **Current: offline.** `main` imports `EOS_*` functions (Epic Online Services, from `/nro/EOSSDK-Switch-Shipping.nro`, loaded through `nn::ro`); they stay offline. `main` imports only EOS Auth, Connect and PlayerDataStorage (Epic-account cross-save, which the host cannot serve). Since the host loads NROs, it refuses online SDKs: an NRR that registers only such modules fails `RegisterModuleInfo`, and Hades never asks for the module (checked on 1.0.38233: `refused an NRR that registers only online SDKs (1 hashes)`, no `LoadModule`, its frames as before). + +## Rewriter + +- Under the spin-wait search with the lock, test and store rules, Hades' modules rewrite byte for byte as before ([development.md, Power](../../../docs/development.md#power)). + +## Mac routes + +- `DM_KEYS="f800:A,f1000:A,f1200:A,f1500:A,f1900:A"` goes through the title and the intro into Tartarus. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The Vulkan layer on Metal** (`dm/gfx/vk/hle_vk.m`) and the SPIR-V translation (`vk_shaders.py`), described above; [dm/games/README.md, What the game files decide at every build](../README.md#what-the-game-files-decide-at-every-build). +- **A host notification event** for `oe::GetNotificationMessageEvent` (`hle_oe.c`), described above. + +## Open + +- The memory partition is Link's Awakening's, not measured for this game (`game.tsv`). +- Online SDKs in NROs: always load them, refuse them (as now) or decide per profile is an open question for the maintainer ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk), the Absolum entry). diff --git a/dm/games/010055D009F78000/README.md b/dm/games/010055D009F78000/README.md new file mode 100644 index 0000000..7af4613 --- /dev/null +++ b/dm/games/010055D009F78000/README.md @@ -0,0 +1,84 @@ +# Fire Emblem: Three Houses (`010055D009F78000`) + +Update 1.2.0 (v327680) over the base game, SDK 6.5.0. Koei Tecmo's older KTGL engine on NVN (the game is co-developed with Intelligent Systems). Modules `main`, `subsdk0` (GLSLC, NVN's runtime shader compiler), `subsdk1` (the movie library, NintendoSDK_movie) and `sdk`. Plays on the iPhone; 60 fps (30 on the Switch) through rewriter cuts on 1.2.0 `main`. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Present interval table → 1 for every render mode (int and float tables) | [`patches.py`](patches.py) `CUTS`, `main` build `89048449…` | 60 fps at the same game speed: KTGL already updates the game 60 times a second; see [Frame rate](#frame-rate) | +| Render thread's forced interval 2 ignored (`main+0x50fb7c`) | `patches.py` | the opening movie and scenes such as character creation run at 60 too | +| Frame limiter sleeps instead of yielding (`main+0x5be4c0`–`0x5be4d0`) | `patches.py` | its yield loop spun on the host: 59.5 updates a second and 1.5 W of CPU on the Mac | +| Render thread waits a frame instead of 16 µs (`main+0x50f8e8`–`0x50f8f4`) | `patches.py` | its wait polled about 50,000 times a second at 60 fps | +| GLSLC capture (`/glslc_capture/=runtime` in `shader_groups`) | [`game.tsv`](game.tsv), NSP script | 9 built-in shaders exist only as compiles of the game's own GLSLC at boot; see [Files and shaders](#files-and-shaders) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0): the game uses `SetHeapSize`. Link's Awakening's partition, not measured for this game | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the camera is on the right stick in the monastery and in battle | + +## Frame rate + +- KTGL runs the game on the main thread at 60 updates a second whatever it shows: each update steps by 1/fps with fps = 60.0 (`main+0x1a38f08`, set at `main+0x50e114`; dt at `main+0x510010`), and the frame end (`main+0x50f680`) waits out 60/fps ticks of 1/59.94 s in a yield loop (`main+0x5be430`). +- The render thread (`main+0x50f8c0`) draws the latest update and presents it with the interval of the render mode (frame +0xbb84, set at `main+0x50f3d0`): 0 = 1080p docked, 2 = 720p handheld, 3 / 5 their two-view variants (interval 2); 1 and 4 are 720p / 1080p modes at interval 1 that nothing selects. Measured on the Mac before the cuts: 59.2 updates and 29.8 presents a second. +- **Current:** the per-mode interval table (int at `main+0xd13de0`, `{2,1,2,2,1,2}`, read by `main+0x4b0420` for the present and by the 6-tick fade counter in `main+0x4b7530`; float at `main+0xd13e40`, the per-frame scroll step in `main+0x4c2b30`) becomes 1 for every mode, so every update is shown, the game runs at the same speed and the render thread's own steps stay at the same speed. Resolutions stay per mode. +- **Current:** the render thread also forced interval 2 while either of two flags was set: `main+0x21c9a03`, a full-screen image drawn before the frame (`main+0x4b4908`: the opening movie), and frame +0xc55f / +0xc560 (from the object at app +0x40: the character creation screen after the movie, and so on). In the engine's own 60 fps modes this traded 60 fps for resolution in those scenes (`main+0x50f7d0` drops 720p mode 1 to 1080p mode 0). A cut at `main+0x50fb7c` (`orr w8,w9,w8` → `mov w8,wzr`) ignores both, so those scenes run at 60 too; the movie still lasts 210.6 s (6,318 frames at 30 fps), as in the original. +- **Current:** the frame limiter (`main+0x5be430`: yield, read the tick, until the target has passed; x8 = time passed and x23 = target, in 100 ns) yielded and polled the clock, which on the host spun its first 2 ms on a performance core and then overslept by up to 250 µs: frames took 16.8 ms (59.5 fps, a repeated display frame every 2 s) and CPU energy went from 0.16 W at 30 fps to 1.5 W on the Mac (228,000 yields a frame on an iPhone). The yield becomes `nn::os::SleepThread` of 96% of the time left (PLT `main+0xafc300`), and the loop goes back to the limiter's own tick read at `main+0x5be44c`; the frames take 16.68 ms. +- **Current:** the render thread waits for the next update with KTGL's event wait (`main+0xaa90e0`, timeout in µs: × 1000 → `nn::os::TimedWaitEvent`) and passed 1/fps × 1000 = 16, one frame in ms, so it polled every 16 µs (42,000–53,000 wakeups a second on the Mac at 60 fps, against 1,200 at 30 fps, where it waited in the present instead). The timeout becomes 1000/fps × 1000 = 16,666 µs. The other four callers wait without a timeout. +- Prologue battle on the Mac with the cuts: 59.9 fps, 55% of a core and 1.8 W of CPU energy (the original at 30 fps: 44% and 1.7 W). +- Before the limiter cut, the host's yield back-off alone took its CPU time on the Mac from 78% to 19% of a core, same frame times ([development.md, Power](../../../docs/development.md#power)). + +## Memory + +- About 3.5 GB resident and 2.8 GB of Metal allocations on the Mac; the game itself makes a 1.5 GB GPU-only memory pool. + +## Rendering + +Game-specific findings. The general host changes they led to are listed under [Found on this game](#found-on-this-game). + +- **Resolved: frames stopped after about 15 s; a game thread spun in its command-memory callback.** KTGL gives its main command buffer one 16 MB chunk of a ring and, in its callback (`main+0x9ca960`), takes chunk start + `GetCommandMemoryUsed` as the position the GPU has reached; the ring allocator (`main+0x9cc120`) waited for fences that had all signalled. NVN counts used bytes from the last `AddCommandMemory`; the host counted from `BeginRecording`, so the position stayed at one frame's size and the ring never freed. +- **Resolved: `FATAL submit re-entered` in the first frames.** The game initializes two NVN queues and submits on both from two threads; submissions are serialized now. +- **Resolved: menus showed plain bars or only the cursor; the screen was black.** The scene binds a 12,544-entry texture pool and a 3,856-entry sampler pool, the 2D UI's command buffer binds 4,096-entry ones, and both fill IDs from 256. With one ID table the scene's default 8×8 white texture overwrote the UI's textures: the title menu's labels drew as plain bars, other menus were empty except for the cursor. The pools are now kept apart. The scene registers each draw's textures into fresh IDs of a ring (8,729 and up a few seconds in), which used to read as empty slots and left the screen black. +- **Resolved: black silhouettes, then flat tan characters.** The character shaders read Half2 UVs through integer-declared inputs: Metal refused the pipeline (`Vertex attribute m_36(5) of type uint4 cannot be read using MTLAttributeFormatHalf2`), so the characters' shading draws were dropped, and reading the raw bytes as integers gave denormals (UVs collapsed onto one texel). They are now converted to float32 bits. +- **Current:** depth clamp is replayed (the game turns `SetDepthClamp` on; it used to be ignored). +- **Resolved: the ground under close-up battle cameras was flat steel blue.** The terrain's vertex shader (`56778feb1cfdee7e`) reads its grid position from an `RGBA16I` attribute: x and z the grid cell, y the height, which spans the whole int16 range; so does the side pass that draws the river surface into targets of its own (`db725c85ca060403`). The input was declared `uint4`, and the host swapped `Short4` for `UShort4`, which widens with zeros: every negative height read as 32,768–65,535, and those vertices went far up out of view. Where the ground was missing nothing wrote the depth (reversed-Z, cleared to 0), and the lighting takes depth 0 for sky and paints the fog colour. The G-buffer colour targets are never cleared, so they kept an old image there: cobblestones of a mesh under the terrain, which from the map camera showed through the gaps like a road. + - How it was traced, since two steps misled: a `HILITE` colour over the gap was stale too (the never-cleared albedo), and the end-of-frame depth showed 0 there. Recomputing every draw's clip positions from `CBDUMP` data showed that no draw of the shader reached the gap. Then the side pass's layer, which showed only scattered tiles of the river, led to the vertex format. Ruled out on the way: depth clamp (Metal's clip and clamp modes gave the same depth), the pass without a depth attachment (142595 is a screen-space decal pass: it samples the depth and writes albedo and normals), polygon offset (none), dropped draws. +- **Light and decal lists:** 321 shaders read them through the instruction Maxwell also uses for 1D textures, and they used to read 0. The decal pass (`417a523053457352`, slots 16 and 18) and the lighting (`094a2d11253f126c`, `f54830a75e07e14a`, slots 18–20) read them from texture buffers declared `texture2d` (`texture type mismatch: shader declares 2, texture is 9`). Since row wrap they read the real lists: slot 18 holds 1,052,640 elements, 65 rows. On the Mac prologue route the torches and burning houses now light the battle map and the close-up ground (they lit nothing before), and the blood decals take the scene's light instead of showing as dark blue patches. Its Half2 / RGBA16I vertex attributes go through shadow vertex inputs instead of the CPU conversion. +- **Resolved: the first loading screen took 78 s on the iPhone (5 s on the Mac).** KTGL's loader uploads each resource with a copy on the queue followed by `nvnQueueFinish`, hundreds of times per loading screen (effect banks, face models), and that call waited for the display to take every queued frame: one to two display frames per call. The windowless Mac runs present at once, so they never showed it; `DM_WINDOW=1` does (one step took 20 s instead of 1.7 s). `nvnQueueFinish` now waits only for the GPU. Windowed Mac run after the fix: first load 6.1 s, frame pacing unchanged. Found on the device with `Documents/env.txt` holding `DM_WATCH=50` (thread table at 50 s: only the yield-polling main thread used CPU) and `DM_FSLOG=` (the reads came in bursts, with stalls of 4 / 24 / 13 s right after the effect banks). + +## Audio + +- **Resolved: no sound; red `audren: command 1 in the command list is malformed (magic 0 size 0)`.** SDK 6's DSP command header is +0x04 size (s16), +0x06 command ID (s16), +0x10 enabled, where SDK 8–20 have +0x04 enabled, +0x05 ID, +0x06 size (`CommandListProcessor::Process`, `sdk+0xaaa00`); its DeviceSink is ID 0x10. + +## Services + +- **Resolved: the first loading screen never ended; `DM_FS=1` showed `save:/system` "not found" and no `fs mount save`.** In SDK 6 `account::OpenPreselectedUser` returns a Result, and the game skips its save mount when it is not 0 (`main+0x53b7e0`). The host's hook returned void and left the handle pointer in w0; it returns 0 now. +- DLC: the Mac needs `DM_AOC_DIR=/010055D009F78000_build/aoc`, or a "DLC deleted" dialog blocks loading a save. + +## Files and shaders + +- RomFS: the LINKDATA archive (`DATA0.bin` is an index of 32-byte entries `{u64 offset, u64 length, u64 stored length, u64 compressed}` into `DATA1.bin`, 6.9 GB), loose overrides in `patch1`–`patch4` (listed by `patchN/INFO0.bin`, 0x120-byte entries ending in a `rom:/patchN/…` path), WebM movies in `nx/movie` and KTSL sound banks (`*.ktsl2stbin`). Compressed entries and `*.gz` files are Koei Tecmo's chunked zlib (`kt_gz`; [01](../../../docs/agents/01-extract.md#compressed-romfs-content)). +- Shaders: 1,364. `shader_split.py` (`kt_shaders`) reads the SPKG packages (G2S containers, as in Age of Calamity) in `DATA1.bin` (132 entries) and `patch*/nx/shader`, a `_SPK4100` post-effect package without G2S (`DATA1.bin` entry 549), map and model archives carrying packages of their own, and the BNSH and GLSLC outputs in `main`. `DATA1.bin` is split into 64 scan jobs by entry range (`linkdata_jobs`). +- Its GLSLC (output version 0x1d.2) writes 0x848 / 0x850-byte control sections and compute code with no header, which the reader used to drop: 24 shaders before, 1,355 after. A few outputs of version 0x1d.0 keep something else at control +0x20 (odd values such as 0x136b), so a control length must be a multiple of 4; `glslc_section` later took 8 more of them ([04, Seen in Dark Souls Remastered](../../../docs/agents/04-graphics.md#seen-in-dark-souls-remastered)). +- **Shaders compiled at run time:** KTGL compiles 9 built-in GLSL shaders at boot with the game's own GLSLC (`glslcCompile` from `main+0x9c89f8`; the sources are strings in `main`): 2D primitives, text, the loading quill. No game file holds their output, so they are captured on the Mac: the game runs with `DM_GLSLC_CAPTURE=/glslc_capture` (`hle_glslc.c`), and `shader_split.py` reads that folder. The outputs are identical on every run. The NSP script does this by itself in its `GLSLC capture` stage, so the NSPs and prod.keys are enough: it builds the Mac host into `_build/tools/` and boots the game until the 9 files are in `_build/extract/v327680/glslc_capture`. Without the folder the split tables would differ from the profile, and the script would stop at its table check. By hand, run the game for a few seconds with that variable (step 5 of the build by hand). + +## Mac routes + +- A fresh save needs `DM_KBD=` for the name entry, and its confirm dialogs default to No. +- From a new game into the prologue battle (frame-based at 60 fps, about 7 minutes): `DM_KEYS="f720:A*185/90,f2400:Plus*35/438,f17400:Left~0.3*120/500,f17480:A*120/500,f17560:Left~0.3*120/500,f17640:A*120/500,f18020:Plus*100/562"`; the game offers a save right before the battle. The repeating Left, A, Left, A reaches Yes whether a confirm dialog is open or not (A, Left, A loops once it starts on an open dialog: A answers No, Left changes the selection, A reopens it). A presses during the opening movie pause it, so the scenes after it start a few seconds later from run to run. +- To a first fight, from a save right before the prologue battle, with `DM_AOC_DIR` set: `DM_KEYS` with A every 90 frames from `f720` to `f2610`, then `f2700:B,f2800:B,f2900:A,f3000:Up,f3060:Up,f3120:Up,f3180:Up,f3240:A,f3340:A,f3440:A,f3540:A,f3640:A,f3740:A` closes the movement tutorial, moves Byleth up to the thief and attacks: the combat close-up runs at frames 3,400–3,700. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Submissions serialized across queues** (`g_sub_mu`) and **`GetCommandMemoryUsed` counted from the last `AddCommandMemory`**: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **SDK 6:** `account::OpenPreselectedUser` returning a Result, SDK 6's DSP command header (`capture` in `hle_audren.c`) and **run-time GLSLC capture** (`DM_GLSLC_CAPTURE`, `hle_glslc.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk); the NSP script's capture stage: [development.md](../../../docs/development.md#the-packing-script-in-detail). +- **Overlapping texture / sampler pool IDs** kept apart (`bind_handles`, pool base in `cmd_t`), **integer vertex inputs fed a float-type format** (`vconv_bind`) and **other-signedness integer formats** (`vconv_int`, since replaced by shadow vertex inputs), **depth clamp** (`OP_DEPTH_CLAMP`), **`nvnQueueFinish` waits only for the GPU** (`mtl_finish`): [04, Seen in Fire Emblem: Three Houses](../../../docs/agents/04-graphics.md#seen-in-fire-emblem-three-houses). +- **Row wrap** and **shadow vertex inputs**: [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **GLSLC 0x848 / 0x850 controls and headerless compute code** (`glslc_shaders`) and the **SPKG / LINKDATA reader** (`kt_shaders`): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **Yield back-off** and the **yield-progress rule** (`thr_yield`; this game's limiter is a poll that keeps its registers): [development.md, Power](../../../docs/development.md#power). + +## Open + +- **To confirm:** 64-pixel rectangles in the albedo where decals land; skipping the decal pass removed them. They did not appear on the Mac prologue route before or after row wrap (no 64-pixel structure in the close-up frames), so the scene where they were seen still has to be checked. The player-facing README lists "small square patches on the ground where decals land" as a known issue. +- Deferred binds are not remapped to the pool base (red line). +- `game.tsv` still describes the game as "In bring-up" and the memory layout as Link's Awakening's, not measured; the development notes call SDK 6.5.0 "the oldest so far", though DARK SOULS: REMASTERED (SDK 4.5.0) and Mario Kart 8 Deluxe (SDK 4.4.0) are older. +- The capture boot's time: the development notes give "about 10 s in all" for the NSP script's stage and "about a second after launch" for the 9 files. diff --git a/dm/games/0100606026F12000/README.md b/dm/games/0100606026F12000/README.md new file mode 100644 index 0000000..910ce2e --- /dev/null +++ b/dm/games/0100606026F12000/README.md @@ -0,0 +1,71 @@ +# FINAL FANTASY RESONANCE (`0100606026F12000`) + +Update 1.0.1 (v65536) over the base game, SDK 20.5.17. Square Enix, Unreal Engine 5.6.1 (`SDK MW+EpicGames+UnrealEngine-5_6_1` in `main`; the desktop renderer, `SF_GLSL_SWITCH` shaders), NVN, CRIWARE sound. Modules `main`, `subsdk0` (the movie module, which `main` imports from) and `sdk`; `sdk` and `subsdk0` are the builds STEINS;GATE RE:BOOT runs ([01008BC024FDE000](../01008BC024FDE000/README.md)). Plays on the iPhone; 60 fps (30 on the Switch) by the host's write of `rhi.SyncInterval`. + +Status: brought up on the Mac: the first-boot brightness screen, the title, a new game (Normal difficulty), the opening cutscene, the first field (Battlefield) with its autosave, Continue from that save, and the first two battles, at 60 fps with no red line apart from one moment of the new game's load (see [Open](#open)). Playable on an iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `fps_target 60` | [`game.tsv`](game.tsv), `hle_fpslock.c` | the host writes 1 into `rhi.SyncInterval`; see [Frame rate](#frame-rate) | +| FMallocBinned3 constructor cut: `main+0x137f0` `bl 0xae770` → `nop` | [`patches.py`](patches.py), `main` `84e182f7…` | the constructor reserves 38 GB of address space for its pools; the host runs Unreal's heap (`hle_uemalloc.c` layout `UE5_6`); see [Memory](#memory) | +| `game_args -SwitchVirtualAddressSpaceSize=4064` | `game.tsv` | the engine's page allocator never gives address space back: a 2 GB region aborted in the first battle | +| Node area cut 1 GB → 62 MB (`sdk+0x145cbc` / `+0x145d28`) | `dm/tools/sdk_patches.py`, `sdk` build `0023a1b0…` | as on the other Unreal games' SDKs; this build aligns regions of 2 MB and more to 2 MB only, so it needs no alignment cut | +| 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 DRAGON QUEST XI S's | +| `sys_resource 0x1400000` | `game.tsv` | the NPDM declares 20 MB of system resource | +| `tex_dyn_slots on` | `game.tsv` | 2,077 particle vertex shaders pick their texture slot at run time; see [Shaders](#shaders) | +| Integer output row `c254eaa40e0c4f1e` (`fout` 0 `uint`) | [`shader_facts_manual.tsv`](shader_facts_manual.tsv) | `CopyStencilToLightingChannelsPS` (`DownsampleDepthPixelShader.usf`, global shader 1627) writes the stencil's lighting channel bits to an `R8Uint` target: without it the lighting channels stayed empty and the sprites showed as dark silhouettes (seen in the first scene after the opening cutscene) | +| `pad_layout full` | `game.tsv` | a 3D field whose camera the right stick tilts and turns. It plays docked (1920x1080) | + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). The engine's Switch device profile sets `rhi.syncinterval=2` (`BaseSwitchDeviceProfiles.ini`), so the game asks for present interval 2; the game sets no `t.MaxFPS`. The host writes `rhi.SyncInterval` 1 into its console variable (`hle_fpslock.c`) whenever the game sets it back (at level loads), so the game asks for present interval 1 itself. +- Unreal steps the game by the time that really passed: `DM_NVN_REGLUM` traces of the same keys from the same save at 30 and 60 fps (59.8 fps, frame time p99 18 ms on the Mac) match from the load through a 3 s walk (thumbnail difference ≤ 0.006). +- `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) keeps the game's 30 and saves power. + +## Memory + +- **Current: the allocator is the host's.** `nninitStartup` (`main+0x13370`) reserves one 62 GB region for the engine (unless `-SwitchVirtualAddressSpaceSize=` caps it, `main+0x137d0`), then constructs its FMallocBinned3 in place in the bss, as Octopath Traveler 0's ([01005270232F2000](../01005270232F2000/README.md)): the call at `main+0x137f0` → `main+0xae770` on `main+0x77c6210`, whose constructor would reserve 38 GB for its pools (`main+0xae860`). The call is cut; the host puts its allocator's vtable in the object before any guest code runs (`hle_uemalloc.c`). `FPlatformMemory::BaseAllocator` (`main+0x13c64`) returns the object and the thread-local storage allocator (`main+0x13830` / `main+0x13870`) calls its Malloc / Free slots; its pool base (`main+0x7c148a0`) and page size (`main+0x7c14800`) are read only by the allocator's own code (`main+0xae024` to `main+0xb4568`). +- **UE 5.6's FMalloc has 32 slots** (layout `UE5_6`: Malloc 6 … Free 10, QuantizeSize 13, GetAllocationSize 14). UE 5.5 and later add `MallocZeroed` / `TryMallocZeroed` after `Free`, two TLS cache calls (`MarkTLSCachesAsUsed` / `Unused`) and two cached-memory queries. The base vtable shows it: its three pure slots are 0, and its slots 11 and 12 are one function that calls slot 6, then `memset`. +- **Resolved: a 2 GB region (DRAGON QUEST VII Reimagined's) aborted in the first battle** (`nn::diag` abort from `main+0x11bc`, no message). The engine hands its region out with a page allocator of its own (`main+0x13ef0`): a bump pointer plus free lists per size class, so the address space it has used only grows, and past the region's end it calls `OnOutOfMemory` (`main+0x13c74`), with about 1 GB mapped. Probed where each new block comes back (`DM_PROBE=main:13fcc`: x0 the block, x21 its size, x8 the region's end) from the save in the first field through the first battle: a 1 GB block at boot from a static constructor, the high-water mark at +1.50 GB 15 s in, +1.77 GB at 56 s, +2.02 GB at 166 s, level at +2.03 GB over seven minutes (2.03 GB of address space, about 1 GB of it mapped). +- **Current: a 4064 MB region.** This SDK build aligns regions of 2 MB and more to 2 MB only (`VammManager::AllocateAddressRegion`, `sdk+0x145e00`), so the region starts right after the node area, in Engage's 5 GB block, as DRAGON QUEST XI S's ([01006C300E9F0000](../01006C300E9F0000/README.md)). +- On the Mac in the field and the first battles: about 2.3–2.6 GB resident and 1.2–1.6 GB of Metal allocations, flat over seven minutes (development.md); `game.tsv` gives, for the field and the first battle, about 1 GB of the alias region mapped, an Unreal heap of 1 GB, 2.2–2.4 GB resident and 1.2–1.5 GB of Metal allocations. + +## Shaders + +- 73,496 unique shaders (9,253 vertex, 61,305 pixel, 2,934 compute, 4 geometry) in the container's two IoStore shader libraries (3,542 + 70,702 shaders), grouped as `global` and `game`. They hold the same `LSLG` records as the other Unreal games. Each shader entry packs its frequency (bits 0–3), group (4–33) and offset in the group's decoded bytes (from bit 34), and a shader runs to the next one's offset; `uepak.py` writes the libraries out as version 2 archives, so the split reads them as any other. Every vertex shader is made to link with any pixel shader (`shader_split.py` `separable_pairs`). +- **Current: 2,077 particle vertex shaders** (Unreal's particle vertex factories) pick one of several c2 table slots at run time: texture table words 0x168-0x1c0 and sampler table words 0x568-0x590, and no other word of c2, so `tex_dyn_slots` is right here. Without the key the first one aborted the host in a double free (now fixed). +- **Unbuilt:** 11 shaders, none met so far: 3 geometry shaders and 8 compute shaders with 3D image atomics. + +## Files + +- Everything is in one IoStore container, `FFRS/Content/Paks/FFRS-Switch.utoc` / `.ucas` (6 GB, 63,910 chunks, Oodle in 64 KB blocks, not encrypted; `.ucas` is the profile's `romfs_marker`), beside `FFRS-Switch.pak` (2.9 GB, 2,363 files: configs, fonts, sound banks; version 11, index not encrypted, Oodle), a `global.utoc` holding only the script objects, and the RomFS's `Manifest_UFSFiles_Switch.txt`, which lists every file the engine sees, the shader archives among them. +- The pak has no shader code archive: the cooker converted both libraries into the container (EIoChunkType 8 `ShaderCodeLibrary`, one chunk each, the tables; type 9 `ShaderCode`, one chunk per shader group). A TOC (version 8) is a 144-byte header, the chunk IDs (12 bytes: hash, index, type), offsets and lengths (5 + 5 bytes big-endian), perfect hash seeds, the compression blocks (5-byte offset, 3-byte stored and raw sizes, method) and the method names; a library chunk's ID is CityHash64 of `-` lowercased in UTF-16 (`global-sf_glsl_switch-sp_glsl_switch`). +- The tables and each group are Oodle-compressed, two layers, so `uepak.py` (`Toc`, `io_archive`) reads them through the game's own decoder in the Mac host's tool mode (`OodleHost`, `DM_OODLE_SERVE`; served before the SDK starts, no boot) and writes each library as a version 2 archive with every shader decoded: 707 MB in about 40 s. +- The pak stores block size 0 for a file of one block (`Pak.entry`). + +## Rewriter + +`main` has 94 MB of text, 95% of it without unwind entries: 82 MB in one stretch (`main+0x6c4f88`–`+0x59220c0`) and the 6.65 MB before the first FDE (none before `main+0x6a75d4`). + +- **Resolved: a `jump table ` assertion on an ordinary byte table near the start of the text** (`main+0x219234`, `adr x18; ldrb w0,[x17,x16]; add x18,x18,x0,lsl #2; br x18`). `func_of` made every site before the first FDE a function of one instruction, so no form saw the `adr` in front of the `br`. The text before the first FDE is now one function, from the end of the data block before it (or the text's start), as a gap between two FDEs already was. +- **Resolved: the rewrite never finished, the Mac swapped, the rewriter's footprint reached 100 GB.** A stretch without FDEs is one function to `func_of`, and its detailed disassembly costs about 880 bytes an instruction once built (2.6 KB while capstone builds it): 21.6 million instructions. A function longer than 256 KB is now analysed in pieces (`pieces` in `patch_nso.py`), cut only right after a `ret` / `br` / `b` where no conditional branch and no `b` within 64 KB crosses. Counting `b` up to 1 MB left only 21,385 cut points in the 82 MB (tail calls between neighbours) and pieces of up to 6.5 MB; with 64 KB there are 185,228, and no piece is over 256 KB (the two stretches: 28 and 344 pieces). The rewrite then takes 13 minutes at about 2.7 GB resident on a loaded Mac. It came out the same as with `b` counted up to 1 MB (3 and 136 pieces), so the analysis does not depend on where the cuts fall. + +## Mac routes + +- The first boot shows the brightness screen. From a fresh save, `DM_KEYS="f600:A,f1500:A,f1700:A,f1850:Up,f1950:A,f2400:A*36/150,f7950:B,f8100:B,f8250:B,f8400:B"` takes New Game, Normal, "Yes", the opening and the tutorials into the first field (about frame 9,600 at 30 fps; B closes a tutorial on its last page, the stick turns its pages). +- From a save made there, `DM_KEYS="52:A,58:A,62:A"` continues into the field; `70:LUp~4` walks north to the first battle. The work folder's `run/save_battlefield` holds such a save. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **UE 5.6's 32-slot FMalloc** (`hle_uemalloc.c` layout `UE5_6`): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **IoStore shader libraries read through the game's own Oodle with no boot** (`uepak.py` `Toc`, `io_archive`, `OodleHost`, `DM_OODLE_SERVE`): [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content), [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **Rewriter:** text before the first FDE as one function (`func_of`) and long functions analysed in pieces (`pieces`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). + +## Open + +- **Known:** once in two runs, the frames that begin the new game's load bind 2D textures where a base pass shader declares 3D ones (red `texture type mismatch` lines, under a black screen). +- 11 shaders do not build (3 geometry, 8 compute with 3D image atomics); none met so far. diff --git a/dm/games/0100620012D6E000/README.md b/dm/games/0100620012D6E000/README.md new file mode 100644 index 0000000..29604cf --- /dev/null +++ b/dm/games/0100620012D6E000/README.md @@ -0,0 +1,78 @@ +# Big Brain Academy: Brain vs. Brain (`0100620012D6E000`) + +Update 1.2.0 (v131072), SDK 11.4.3. NintendoWare Bezel Engine 1.56 on NVN. Modules `main`, `subsdk0` (GLSLC: imported, never called, as `main` holds no GLSL source), `subsdk1` (the movie module) and `sdk`. Plays on the iPhone, handheld with Boost; 60 fps (present interval 1). The README lists its IPA at about 0.8 GB. + +Brought up on the Mac: the title, Solo, enrolling a new student (birthday, profession, appearance, online features, the creepy-crawly filter), the main menu (Practice, Ghost Clash, Test), the Practice categories and activities from each (Whack Match, Species Spotlight, Frame Filler, Fast Focus, Flash Memory, Cubegame, the photo quiz, Tick-Tock Turn, Shadow Shift) with their results, coins and unlocks, at 60 fps with sound, the H.264 activity previews and saves, by controller and by touch. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `sys_resource 0` | [`game.tsv`](game.tsv) | the NPDM declares no system resource, and `nninitStartup` chooses its memory setup by `IsVirtualAddressMemoryEnabled`; see [Memory](#memory) | +| `op_mode handheld` | `game.tsv` | handheld, the game reads the touch screen (menus, every activity, Party's two-player touch battles); docked it does not | +| `perf_mode boost` | `game.tsv` | handheld Normal draws 1280x720, handheld with Boost 1920x1080 (the window crop goes from 1280x720 to 1920x1080 once the game sees Boost) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | a 3328 MB heap region: `SetHeapSize` asks for 3.14 GB | + +No `patches.py`, and `dm/tools/sdk_patches.py` has no cut for its `sdk` build (`2d369d9999e3e10e665106f840ffff9b`). + +## Memory + +- **Resolved: `nn::diag` abort in `StandardAllocator::Initialize(void *, size_t, bool)` from `nninitStartup`** (`sdk+0x2f98e4` ← `main+0x5b69e0`). The game's `nninitStartup` (`main+0x5b6984`) asks `os::IsVirtualAddressMemoryEnabled()`: when true it gives StandardAllocator a 60 GB address region (`0xf00000000`), when false it sets a heap of pool − used (2 MB aligned) and hands that over. The SDK's answer is `GetInfo(TotalSystemResourceSize) != 0` (`VammManager::IsVirtualAddressMemoryEnabled`, `sdk+0x34e074`), and the host reported a 32 MB system resource to every game, while this game's NPDM declares none. The profile key `sys_resource` reports the NPDM's size (0 here, so the heap path, as on hardware); the survey writes it for every new game, and profiles without it keep the 32 MB answer. +- The game sets a 3.14 GB heap (pool − used) in the 4 GB layout. It creates 1.5 GB of NVN memory pools in its first 25 s (384 + 256 MB CPU-uncached, a 320 MB physical pool for sparse mappings, 192 + 64 MB of render targets), so about 1.3 GB is resident at the title and 1.5 GB in the first activity. +- Over 15 minutes of activities it rose to 1.86 GB in steps as each new activity loaded (pools 217 → 292, textures 444 → 1,173), flat in between. + +## Rendering + +- 2,125 shaders (955 vertex, 1,153 pixel, 12 geometry, 5 compute). The window origin is lower left, depth mode near = 0, the swapchain 1920x1080 RGBA8 sRGB (format 0x38; cropped to 1280x720 in handheld Normal), present interval 1 at 60 fps. Everything drew right at the first frame with shaders; no texture or pool key was needed. +- **Not done: Bezel's line renderer.** Its 12 geometry shaders (`_BezelSystemResources/line/shader/line.bfsha`) take a line strip with adjacency (primitive type 11) and emit up to 12 vertices per segment as triangle strips: thick lines with joins and caps (28 emit sites). Metal has no geometry stage, so they do not build (`unknown main0_out`) and their draws are skipped (`[RED] primitive type 11 not implemented`). From the main menu on, every frame draws one such line (width 10 in its uniform block) into the top-left 512x512 of the frame and copies that region into a texture, so the main menu's chart loses its outlines. +- **Not done: ten pixel shaders pick their texture at run time** (Ryujinx's fallback arrays, `fp_t2d_cb0_40000000`): six in `03GamePerception.bnbshpk`, two PBR, two in `main`. In `e10f24835bbaa5f6` the handle is c3 +0x130 or c12 +0x1d0 of the game's own uniform buffers, chosen by a flag in c12, packed as texture ID | sampler ID << 20. `tex_dyn_slots` covers only handles from the driver's c2 tables, so these do not load and their draws are skipped. Resolving them needs the translator to report such a choice of two uniform-buffer handles, a chain change for every profile. None of the activities checked uses them. +- **Conditional rendering:** the engine calls `SetRenderEnableConditional`; it is drawn unconditionally (the profile has no `render_cond`), and no screen played looked wrong. + +## Audio + +- Its `AudioRenderSystem::Update` lock is `SdkMutexType::Lock` at +0x1d8 (SDK 10.4–11.4); `update_lock` (`hle_audren.c`) was checked with sound on it ([03, Audio](../../../docs/agents/03-bringup.md#audio)). +- **Resolved:** when queued catch-up frames still signalled the renderer event, every frame of this game became a catch-up frame (about 38,000 catch-up renders instead of 37). A queued frame no longer signals the event ([03, Audio](../../../docs/agents/03-bringup.md#audio)). + +## Movies + +- Each activity's preview is an H.264 movie (`movie/Movie/*.mp4`) that the game loops with Flush and Start, from four threads at once. +- **Resolved: the process died when an activity started, 2 runs in 5, with no `CRASH` line** (exit 137, SIGKILL; `~/Library/Logs/DiagnosticReports/dmhost-*.ips` had `EXC_BREAKPOINT` in `_os_unfair_lock_corruption_abort` ← `pthread_mutex_destroy` ← `md_free`, the movie decoder's destructor). `DM_MOVIE_TRACE=1` (every decoder call with its thread): the event thread acquired an input index while the control thread's Flush waited for the packet being decoded; the flush then returned that index to the free list, it was handed out a second time, two packets shared one buffer (VideoToolbox -12909 on the overwritten one, in exactly the runs that died) and a fifth pending packet wrote past `pend[]` into the decoder's `npend` and `dmu`. Handing such an index out last was not enough: with three hosts running at once one run still sent an index twice. Fixed in `hle_movie.c` (see [Found on this game](#found-on-this-game)). +- **Resolved: the previews flashed black at the end of every loop.** The game reads the buffers of the end-of-stream output (no picture) and of an output it acquired before the flush, and the host painted its texture black for both. + +## Services + +SDK 11.4.3 calls it needed ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)): + +- **Abort in `sf::hipc::InitializeHipcServiceResolution` under `codec::GetHardwareOpusDecoderWorkBufferSize(int, int)`** (`main+0x31f8e8`, early in `nnMain`): SDK 11's form without the option argument; the host had only the three-argument one. +- From the import list before the next boot (`main`'s undefined `nn::` symbols no hook names): `fs::EnableIndividualFileDataCache` / `DisableIndividualFileDataCache`; `account::LoadCachedNintendoAccountInfo` (fails: no Nintendo Account; the game then skips the birthday and country getters); the IR camera (`irsensor`: every camera unconnected, processors cannot run; Bezel's input layer makes one per controller and asks its status, `main+0x497f84`); `ldn::GetNetworkConfig`; and SDK 11's socket overloads with plain `sockaddr` / `int` arguments (`Connect`, `Accept`, `GetSockName`, `Get` / `SetSockOpt`, `Fcntl`, `Recv`, `Send`). + +## Input + +- The profile plays it handheld with Boost. Handheld, the game reads the touch screen (every menu and activity, and Party's two-player touch battles); docked it does not. The buttons work as well; the virtual gamepad starts in the 2D layout, and touches outside the controls reach the game's screen. + +## Files and shaders + +- Nearly all of RomFS is in Bezel Engine archives: 603 `.bea` files (`Archive/~.nx[.].bea`, plus the engine's own `System.nx.bea` at the RomFS root), 7,665 files in all, 2.6 GB unpacked from 0.38 GB. `romfs_marker` is `Archive/Permanent~01Boot.nx.bea`. +- A `.bea` is a NintendoWare binary file with magic `SCNE` (version 0x10100, BOM `FF FE`): +0x20 u64 entry count, +0x28 u64 offset of an array of u64 entry offsets, +0x30 the name dictionary (`_DIC`), +0x40 the archive name. Each entry is a 0x30-byte `ASST` record: +0x10 u16 kind (2; 258 for the one sound project), u16 alignment shift, +0x14 u32 stored length, +0x18 u32 length, +0x20 u64 data offset, +0x28 u64 name offset (u16 length, then the path). Every entry is one zstd frame, even the two whose stored length equals their length. Inside: layouts (`.lyt`, SARCs with their own `__ArchiveShader.bnsh`), BNTX textures, `.bfsha` shader archives, models (`.fmdb`), MSBT text and fonts. The sound is loose `stream/*.bfstm` and the movies loose `movie/Movie/*.mp4` (H.264). [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- Shaders: 2,125 BNSH in the archives' `.bfsha` entries, most of them the engine's own in `System.nx.bea` (`_BezelSystemResources//shader/*.bfsha`: PBR shadow maps 846, post effects 770, …), the rest in the layouts' `__ArchiveShader.bnsh` and the per-archive shader packs (`.bnbshpk`), plus one BNSH in `main`. `shader_split.py` scans each archive entry as a file of its own (`bea_members`): 2,125 shaders in about 90 s. The profile groups `System.nx.bea` as `system` and the game's archives as `archive`. + +## Mac routes + +- From a fresh save, `DM_KEYS="f1900:A,f2400:A*9/150,f3800:Right*14/150,f3875:A*14/150,f6000:A*4/150,f6900:Down*5/60,f7250:A,f7500:A*17/150,f10050:A"` enrolls a student (birthday 01/01/2000, the default appearance, no online features) and starts Whack Match from Practice at about frame 10,050 (`/run/enroll.sh`). +- In handheld mode `Tx` taps work too: `f1900:T0.5x0.88` taps Start on the title. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **`sys_resource`**: the host reports the NPDM's system resource instead of 32 MB for every game: [dm/games/README.md, Keys](../README.md#keys), [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Movie decoder Flush from several threads** (a send claims an index a flush returned, `pend[]` cannot overflow, an index held across a flush is withheld for 250 ms; a flush keeps each slot's last picture, and in native-texture mode an output with no picture leaves the texture as it was; `hle_movie.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **SIGABRT caught by the crash handler**: before it was, this game died once with exit 134, nothing in the log and no report: [05, Reading a crash report](../../../docs/agents/05-debugging.md#reading-a-crash-report). +- **BNSH in Bezel Engine archives** (`bea_members`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- The spin-wait search's store rule takes one more loop in its `main`, a real flag wait (`main+0x1734e0`): [development.md, Power](../../../docs/development.md#power). + +## Open + +- The 12 line-renderer geometry shaders and the ten run-time texture pixel shaders (above). +- One 15-minute run of activities met one of the ten (`e10f24835bbaa5f6`, `03GamePerception.bnbshpk`) and the process aborted moments later, before the host caught SIGABRT, so its cause is not known; a second 15-minute run was clean. +- The development notes and 04 say the line renderer's geometry shaders do not build and that the chain does not use Ryujinx's geometry-to-compute transforms yet; the geometry research (`docs/geometry-metal-research.md`) now describes a compute path that does, and names this game's line renderer as an end-to-end target. Whether these 12 build with it is not recorded. diff --git a/dm/games/010069401ADB8000/README.md b/dm/games/010069401ADB8000/README.md new file mode 100644 index 0000000..722b50c --- /dev/null +++ b/dm/games/010069401ADB8000/README.md @@ -0,0 +1,58 @@ +# Unicorn Overlord (`010069401ADB8000`) + +Update 1.05 (v262144) over the base game, SDK 16.2.4. Vanillaware's engine, as in 13 Sentinels: Aegis Rim ([01003FC01670C000](../01003FC01670C000/README.md)), on NVN. Its ExeFS holds only `main` and `sdk` (no movie module); the `sdk` build is the one Brain Memory Training Academy ships ([0100F5D01DA3A000](../0100F5D01DA3A000/README.md)). Playable on the iPhone at 60 fps; on the Mac brought up through the logos, the title, a new game and the opening story scene with no red line. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `alloc_big_align on` | [`game.tsv`](game.tsv) | without it a thread's ring heap kept base 0 and the game wrote to `0x268` right after the title screen; see [Memory](#memory) | +| `mem_used 0x5c00000` | `game.tsv` | the rewritten modules are 92 MB, larger than the default 64 MB of used memory | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region, which holds the 3.12 GB heap | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `12f16103…` (`sdk+0x134778` / `+0x13480c`) | registered for Brain Memory Training Academy, which has virtual address memory. This game has none and never allocates an address region, so it never needed the cut; it runs the same with it | + +No `patches.py`, no `pad_layout` (the iPhone pad starts in the 2D layout). + +## Memory + +- The NPDM declares no system resource (META +0x14 = 0): the game uses `SetHeapSize` with pool − used, as 13 Sentinels does, a 3.12 GB heap. `main`'s bss is 30 MB; the rewritten modules are 92 MB (`mem_used`). +- Resident: about 2.9 GB in the opening scene on the Mac (development notes; `game.tsv` says about 3.0 GB in the opening story scene). On an iPhone (A20 Pro): a 2.88 GB footprint with at least 3.2 GB of headroom. +- **Resolved: a write to `0x268` right after the title screen** (`main+0x5d6708`, a constructor whose `this` is `0x200`). `operator new` (`main+0x5a55d4`) takes the thread's ring heap, a bump allocator whose cursor starts at its base. One ring heap kept base 0, as in 13 Sentinels: the engine cuts ring heaps out of a memory pool at 64 KB GPU alignment, and the host's pool storage from malloc is only 4 KB aligned. The first allocation returned 0, which `operator new`'s null check skipped, and the next returned `0x200`. With `alloc_big_align` (allocations of 64 KB or more aligned to 64 KB) the game plays from the title screen into its opening story scene. + +## Frame rate + +- 60 fps. The game sets present interval 1; on the iPhone the frame time is 16.7 ms at the median. + +## Audio and services + +Each found by one boot ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)): + +- **Resolved: abort in `sm::Initialize` under `nn::codec::GetHardwareOpusMultiStreamDecoderWorkBufferSize(int, int, int, int, int)`** (at `nnMain`, CRI's Opus streams). The host now has the C interface of the hardware multi-stream decoder, including the SDK 12+ overload with an option argument (Age of Calamity only needed the class and the four-argument size). Each call goes to the `sdk`'s software `OpusMultiStreamDecoder` C interface, with a host-held state per game object as for the single-stream decoder (`hle_codec.c`). `GetHardwareOpusMultiStreamDecoderProcessingTime` reports 0. +- **Resolved: abort in `sm::Initialize` under `nn::crypto::GenerateCryptographicallyRandomBytes`** (`std::random_device` → the `sdk`'s own `getentropy` → `InitializeCsrng`). The `sdk` defines `getentropy`, so the host's `getentropy` hook never sees the call, but the call into `nn::crypto` goes through the `sdk`'s PLT. An `HLE_FORCE` on it fills from the host's entropy stream (`hle_os.c`). + +## Files and shaders + +- All data is in 7 CRI CPK archives (`Unicorn.CPK`, the profile's `romfs_marker`, `UnicornPatch.CPK` and one per language). Audio is CRI ADX2 with Opus streams (`nn::codec`'s hardware multi-stream decoder, replaced by the SDK's software one). +- Both DLC NSPs (indices 1 and 2) are licences only: their RomFS holds just the SDK's placeholder file. +- Shaders: not in RomFS. 2,188 GLSLC compiler outputs (magic `0x19866891`) compiled into `main`'s read-only segment, 1,725 distinct (614 vertex, 1,111 pixel), read by `shader_split.py` `glslc_shaders`. Single-stage programs, combined per draw. Every binding decodes. + +## Input + +- The game reads a USB mouse and no touch screen, as OCTOPATH TRAVELER 0 and Tears of the Kingdom do; a rule that turns touches into a mouse would need testing on it ([ROMANCE OF THE THREE KINGDOMS 8 REMAKE](../0100C2A01A730000/README.md)). + +## Mac routes + +- `DM_KEYS=f2400:A*20/300` goes from the title screen (about frame 2,500) through a new game into the opening story scene (about frame 4,900). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- `alloc_big_align` was first needed on 13 Sentinels ([its note](../01003FC01670C000/README.md#memory), [the profile keys](../README.md#keys)). +- **The hardware multi-stream Opus decoder's C interface** forwarded to the SDK's software decoder (`hle_codec.c`) and **`GenerateCryptographicallyRandomBytes` under the `sdk`'s own `getentropy`** (`HLE_FORCE`, `hle_os.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- Resident memory in the opening scene: about 2.9 GB in the development notes, about 3.0 GB in `game.tsv`. +- `game.tsv` still describes the game as "In bring-up" (the Mac into the opening story scene), while AGENTS.md lists it as playable on the iPhone. +- `game.tsv`: only the merged 1.05 copy is extracted; the base game's own build IDs are added when needed. diff --git a/dm/games/01006A800016E000/README.md b/dm/games/01006A800016E000/README.md new file mode 100644 index 0000000..4f60a15 --- /dev/null +++ b/dm/games/01006A800016E000/README.md @@ -0,0 +1,64 @@ +# Super Smash Bros. Ultimate (`01006A800016E000`) + +Update 13.0.5 (`v2031616`) over the base game, with its 99 DLC licences; SDK 8.2.1. Bandai Namco / Sora's engine (namespaces `app`, `lib`, `phx`, `nu`) on NVN. Modules `main`, `subsdk0` (NintendoSDK_movie) and `sdk`, the same `sdk` and movie builds as Catherine: Full Body ([0100BF00112C0000](../0100BF00112C0000/README.md)); the fighters' code is 98 NRO modules loaded at run time. Plays on the iPhone (IPA 17.5 GB; offline). On the Mac: the opening movie, the title, the first launch's DLC unlocks and Challenges board, the main menu, the Smash rulesets, stage select, fighter select, 2-player battles (Frigate Orpheon, Big Blue, Great Bay, Find Mii) at 60 fps with about 2.7 ms of GPU per frame and 2 GB resident (1.2 GB of Metal allocations), quitting a battle to its results and a second battle whose fighters' NROs the game loaded again. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Engine region 62 GB → 4 GB (11 instructions) | [`patches.py`](patches.py), `main` `21450c64…` | the game aborts in its static initialisers when its 62 GB address region fails; see [Memory](#memory) | +| Node area 1 GB → 62 MB, regions of 1 GB and more aligned to 32 MB | `dm/tools/sdk_patches.py`, `sdk` build `5fd4f531…` (`sdk+0x336b50` / `+0x336bf0`, `+0x336d5c`) | with them the cut region starts 64 MB into the alias region instead of 1.95 GB | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | a 4.375 GB alias region, where the game maps its memory itself | +| `sys_resource 0x1000000` | `game.tsv` | the NPDM declares 16 MB of system resource (virtual address memory); `main` never sets a heap | +| NRO modules | `dm/tools/nro_collect.py` (`nro.tsv`), `loader.c` "NRO modules", `hle_ro.c`, `gen_embed.py` | the fighter, item and common scripts; see [Fighter code at run time](#fighter-code-at-run-time) | + +No `pad_layout` key (the iPhone pad starts in the 3D layout). + +## Memory + +- **Resolved: an abort in the static initialisers, before `nnMain`** (`main+0x37c9bd0`), when the engine's own 62 GB address region failed. The engine's memory (the `nu::` allocator behind `nu::VirtualAllocHook`, set up by a static constructor at `main+0x37c9a20`) reserves 62 GB with `nn::os::AllocateAddressRegion(0xf80000000)` (`main+0x37c9a5c`) and hands it out in 64 KB units: a number line of free ranges, first [0, 0xf7f20) units from the region's start (`main+0x37c9b98`, given to `main+0x37c9c00`), and the top 14 MB (region + 0xf7f200000) as the pool of its range nodes, committed 64 KB at a time up to the region's end (the bound checks at `main+0x37c9a9c`, `+0x37c9c50`, `+0x37ca3cc` and `+0x37caf68`). The size, the header offset, the page count and three region-end bounds all follow from 62 GB, so `patches.py` cuts all 11 instructions to a 4 GB region (the node pool at 4 GB − 14 MB, the first free range 0xff20 units); the application pool (3.2 GB) bounds what it can commit. +- Booted once with a 64 GB alias region (`mem_reserve 0x1030000000`, `mem_alias 0x1008000000`) and `DM_SVC=1`'s map log: the game commits from the region's start upward (1.73 GB contiguous at the title, 75 s in) and its page tables (the node pool) from the top; a battle commits up to 2.5 GB (`DM_ALIAS_WINDOWS`). +- The cut region still did not fit: SDK 8 has the same 1 GB node area and 1 GB alignment of regions of 1 GB and more as the newer SDKs (constructor `sdk+0x336b40`, `orr w9, wzr, #0x3fe00000` at `+0x336b50` / `+0x336bf0`; `VammManager::AllocateAddressRegion`'s alignment at `+0x336d5c`); with them it started 1.95 GB into the alias region (measured with a 64 GB one). They are cut in `sdk_patches.py` as on DRAGON QUEST XI S's SDK 7.3.2 build ([01006C300E9F0000](../01006C300E9F0000/README.md)), whose AddressRegionManager this is at other offsets; the region then starts 64 MB into the 4.375 GB alias region of the 5 GB block. + +## Fighter code at run time + +- **Resolved: the frame loop ran, but the game waited for code it loads itself** (`ro::RegisterModuleInfo` / `ro::GetBufferSize` red lines). `main` imports 1,045 `lua2cpp::create_agent_*` functions, global, that no ExeFS module defines, bound to stubs at start. The fighter, item and common scripts are 98 NRO modules in `data.arc` (`prebuilt:/nro/release/lua2cpp_.nro`, 193 MB), registered by `.nrr/cross2_*.nrr`. The game loads them with `nn::ro::LoadModule` as fighters appear (common and item at boot, the opening movie's fighters, each battle's) and unloads them after. +- **Current:** they are collected at pack time by the SHA-256s the NRRs list (`dm/tools/nro_collect.py`, among `data.arc`'s current files; the NRRs of older releases also register 666 superseded copies, which `data.arc` still holds and which are left out), rewritten like any module and loaded by the host, which binds `main`'s `lua2cpp` stubs to them. 87 need no rewrite at all, 11 have one `x18` site each. The game loads six NROs in its first 30 s and calls none of the stubs. +- On iOS they are packed into three sections of the executable (`gen_embed.py`: `__dm_nro_x0`, `__DM_NRO_W`, `__dm_nro_x1`, and a table of their pieces, `__dm_nro_tab`), 157 MB of code; a Mach-O file has at most 255 sections, and three per NRO would pass it (`loader.c` `nro_pieces`). Played on an iPhone. + +## Rewriter + +- **Resolved: an unhandled jump table, its far base passed through other registers** (`main+0x2b74458`: `ldrb w18,[x14]; sub w18,w18,#1; cmp w18,#6; b.hi; ldrsw x18,[x7,x18,lsl #2]; add x18,x18,x7; br x18`). `x7` is built by `adrp` + `add` 688 instructions before, then parked in `x30` around a loop (`mov x30,x7` … `mov x7,x30`), so the far chain met a `mov` and stopped; the function-wide search found a second `adrp x7` + `add` pair (another table further on) whose seven targets also lie in the function. It was that `main`'s only unhandled site; Smash's `sdk` and movie module come out byte-identical with the fix. + +## Services + +- **Resolved: a jump to pc=0 right after the NVN bootstrap loader** (`lr=main+0x37fe930`, `[RED] NVN address lookup of an unknown name: nvnSetExternalDebuggerEnabledNVX`). The game fetches an NVX extension of the SDK 8 driver by name and calls it without checking the pointer. The real driver has it, so it joins `dm/gfx/nvn/names.txt` as a no-op (thunks regenerated with `gen_protos.py`; no prototype in `nvn.h`, so `NVN_NOPROTO`). + +## Rendering + +- **Resolved: a pink window, every command buffer ignored, while headless runs looked right** (`DM_WINDOW=1`). The first presents failed on the display command buffer with `kIOGPUCommandBufferCallbackErrorPageFault`, and after a few of those macOS ignored every submission of the process ("Ignored (for causing prior/excessive GPU errors)"). The game presents its window texture in its first frames before drawing into it; the texture was made on that first resolution in the present path and added to the residency set (`res_add`), but only a submission commits the set, so the display command buffer sampled a texture outside the committed set. Headless runs never present, which is why they looked right. + +## Files and shaders + +- The RomFS is one `data.arc` (17.6 GB; the profile's `romfs_marker`) beside 14 NRRs. Its header (magic `0xABCDEF9876543210`) gives the offsets of the music, file and shared-file sections and of the file system table (one zstd frame of 50 MB); paths are Hash40s (CRC-32 of the path, its length in the top byte). A path leads to its current file through its info index, file info, info-to-data entry and file data, at file section + directory offset + 4 × its offset, zstd or stored (`dm/gfx/tools/smash_arc.py` documents the layout) ([01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content)). +- The archive also keeps every earlier version of updated files, which no path leads to any more: 183,882 data blobs, 169,939 of them current (the 98 fighter NROs, 764 with their old versions, which the NRRs of older releases still list). A file's first bytes name its format: SSBH `HBSS` resources (`RDHS` shader databases, `XFUN` effect libraries, `LDOM` / `HSEM` / `LTAM` / `MINA` / `LEKS` models, meshes, materials, animations, skeletons), `EFFN` effects, `SARC` UI layouts, `BNTX`, `NUS3` sound and the NROs (`NRO0` at +0x10). +- Shaders: 35,012 (22,197 vertex, 12,761 pixel, 53 compute, 1 geometry): GLSLC outputs in the 41 SSBH shader databases (`.nushdb`, 7,764), BNSH in the effects (`EFFN`, 26,419) and UI layouts (SARC, 752), and 77 in `main`. The split reads `data.arc`'s current files (`shader_split.py` `smash_arc_jobs`). + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL` (`run/step.sh ` presses, waits on the frame counter and takes a shot). +- From `run/save_ref` (past the first launch): A skips the movie, A at the title reaches the main menu, A (Smash), A (a ruleset), A (Random stage), then at the fighter select (after its "Joined!" banner) `LUp~0.85`, `LLeft~0.85`, `LDown~0.3`, `LRight~0.4` lands on Mario, A, Plus. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **NRO modules loaded at run time:** collected by the NRRs' SHA-256s (`nro_collect.py`, `nro.tsv`), rewritten (`patch_nso.py` reads NROs), mapped at start and activated by `nn::ro::LoadModule` / `UnloadModule` (`loader.c` "NRO modules", `hle_ro.c`; `DM_NRO=0` turns it off), embedded on iOS (`gen_embed.py`, `nro_pieces`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk), [development.md, The packing script in detail](../../../docs/development.md#the-packing-script-in-detail). +- **Register copies followed back to a jump table's base** (`copied_base`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **`nvnSetExternalDebuggerEnabledNVX`** as a no-op (`names.txt`, `NVN_NOPROTO`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **The residency set committed at present** (`present_now` → `res_commit`): [04, Seen in Super Smash Bros. Ultimate](../../../docs/agents/04-graphics.md#seen-in-super-smash-bros-ultimate). +- **The indexed archive reader** (`smash_arc.py`, `smash_arc_jobs`): [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). + +## Open + +- The one geometry shader among the 35,012: whether it builds is not recorded. diff --git a/dm/games/01006BB00C6F0000/README.md b/dm/games/01006BB00C6F0000/README.md new file mode 100644 index 0000000..55bb61e --- /dev/null +++ b/dm/games/01006BB00C6F0000/README.md @@ -0,0 +1,77 @@ +# The Legend of Zelda: Link's Awakening (`01006BB00C6F0000`) + +Update 1.1.0 (v131072) over the base game, SDK 8.2.0. Grezzo's engine on NintendoWare (G3d / Ui2d / Vfx / Atk / Font), on NVN (API 53). Modules `main`, `subsdk0` (GLSLC, NVN's runtime shader compiler), `subsdk1` (NintendoSDK_movie) and `sdk`; Captain Toad: Treasure Tracker 1.4.2 runs the same `sdk` build ([01009BF0072D4000](../01009BF0072D4000/README.md)). Plays on the iPhone; gameplay at 60 fps as on the Switch, event cutscenes at 30 by the game's own choice. The README lists its IPA at about 6 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Physics completion task `release` → `ret` | [`patches.py`](patches.py) `CUTS`, `main` `909e904a…` at `main+0x10cd070` | a late destructor wiped the rebuilt task and crashed the physics workers; see [Physics](#physics) | +| Float vertex inputs 1–5 for `147c829e79fb322f`, `03c43da3e3f46da8`, `079700f3577e311f`, `15c02683a10f8e9d`, `2344874045b1e758` | [`shader_facts_manual.tsv`](shader_facts_manual.tsv) | the integer-input guess declared them `uint4` while the game feeds 16-bit or 8-bit normalized formats; see [Rendering](#rendering) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | no system resource in the NPDM: the game uses `SetHeapSize`. 13 Sentinels' partition (256 MB in front + 256 MB alias + 256 MB stack + 3.25 GB heap), "until measured" | + +No `dm/tools/sdk_patches.py` cut for its `sdk` build (`3ae34e68…`). No `pad_layout`: the virtual gamepad starts in the 2D layout. + +## Rendering + +Game-specific findings; the general host changes they led to are listed under [Found on this game](#found-on-this-game). + +- **Current: lower-left window origin.** It never calls `nvnDeviceSetWindowOriginMode`, so it keeps NVN's default lower-left (OpenGL) origin, where NDC y = −1 lands on a render target's first memory row; the six games before it all switched to upper-left. Read as upper-left, every texture the game renders and samples came out flipped and cancelled out, but text it renders into textures for the title and the menus came out upside down, and it copies the frame with `MIRROR_Y`. The host flips the direction inside the viewport (negative viewport height), reverses the front-face winding, and flips the window image at present and in raw screenshots (`g_origin_ll` in `nvn_mtl.m`). +- **Current: depth mode near = 0.** Every vertex library has a GL entry point and a near = 0 one (`main0_z`), picked from the mode the game set. **Resolved: outdoors the whole screen was blurred (Mac and iPhone).** The depth-of-field composite (`52fcbb4ca2a78907`, `postprocess.bfsha`) blends the sharp frame with two blurred levels by a blur amount computed from the half-size depth copy: `pow(clamp((c5[2].y / (depth + c5[2].x) − c5[3].x) · c5[3].y), c5[3].z)` above a screen line, by screen position below it. With every vertex shader translated for GL, all depths sat in 0.5..1 and everything was out of focus. Found with a save from the iPhone (Mabe Village outside the house), `DM_NVN_PASSDUMP` / `DM_NVN_TEXDUMP` of that frame, and LASRecomp, which rewrites z only for the GL mode. +- **Resolved: outdoors was dim and nothing cast a sun shadow (Mac and iPhone).** The sun's shadow map (2048×2048, 32-bit float depth) was correct, but every lookup failed. The surface shaders compute the lookup from a world position the vertex shader passes in `locn24`, divided by its w; the vertex shader writes only x, y and z. The translation started that output at 0 instead of the hardware's (0, 0, 0, 1), so w was 0 and every compare value infinite. Found with `DM_NVN_TEXRAW` on the shadow map, the light matrices from `DM_NVN_CBDUMP`, and a hand-edited surface shader that wrote the compare value and the stored depth as its colour. Every vertex output now starts at (0, 0, 0, 1) (`vs_init`); 12,281 of the game's 18,410 vertex outputs never write w. Compared with forcing the compare to pass: the same brightness, plus shadows from the fences, the house and the trees. +- **Resolved: flickering hard-edged white tiles on the step of Marin and Tarin's doorway (Mac and iPhone).** The room shaders use tiled forward lighting: the fragment position picks a 64×64-pixel tile (`trunc(y / 64) × ceil(width / 64) + trunc(x / 64)`), whose light count and indices come from two storage buffers a compute shader (`light_culling_compute.bfsha`, one thread per workgroup, one workgroup per tile) fills every frame. The culling shader computes its tile index as workgroup y × `c0[0x200]` + workgroup x, reading the dispatch's workgroup count from the driver segment `c0`, which the host passed as zeros (`dmsh_build.py`'s `drv_reads` matches only fragment-shader names). Every row's workgroups wrote row 0's entries, the last writer won (a different one each frame), and the bottom tile row, where the step is, got other rows' lights. A dump of the tile lists showed only the first of 17 rows filled. A declared driver-segment binding now counts as read (`nvn_refl.m`), and compute `c0` carries the dispatch size at 0x200, the uniform buffer table at 0x210 + 16 r and the storage buffer table at 0x310 (`encode_args_`). The doorway now shows the game's soft light from outside. +- **Resolved: missing palm tree and flowers** in Marin and Tarin's house (the pipeline `147c829e79fb322f` + `0ebba5ff6a44aa52`, `shadowmap_pbr.bfsha` variant 641, failed to build). Inputs 1–5 only move bits (in a loop: x bit-cast to an integer, y copied as a float), so the translation declared them `uint4`, and the game feeds them 16-bit normalized formats (RG16 / RG16_SNORM), which Metal will not pair with an integer input. These are the foliage's shadow-map and depth pre-pass draws; the main pass shades with depth test EQUAL, so without the pre-pass the plants never appeared. Declared float, the bits match the hardware's. Four outdoor shaders with the same loop failed the same way on the iPhone in Mabe Village (Half2, RG8 / RG16 normalized) and are listed too; input 7, a bone index used as an integer, stays `uint`. The guess is not wrong in general (1,437 of the game's vertex shaders have integer-typed inputs, many of them bone indices), so overrides are added only for shaders seen failing with float formats (`failed to create render pipeline … Cannot convert attribute …`). The table is on the chain, so it changes only this profile's chain version. +- **Resolved: no ambient occlusion.** The occlusion pass (`aa05724ffa62cb82`, a 480×270 R8 target cleared to 1.0) only writes where stencil bit 1 of the quarter-size depth-stencil copy is clear; the copy left the stencil undefined (new depth-stencil textures are not zero-filled), so on the Mac the mask kept 1.0 everywhere. The copy now carries the stencil. +- **Copies a blit cannot do** (`copy_t2t_draw`): RG11B10F → RGBA16F every frame, half-size depth-stencil copies (depth and stencil written from a fragment shader, stencil through `[[stencil]]` export from an `X32_Stencil8` view), and a scaled `MIRROR_Y` copy of the frame to 1280×720 RGBA8. Each is one render pass per layer. +- Uniform updates inside a pass no longer end it: about 15% less attachment traffic on the Mac. + +## Audio and movies + +- **Resolved: no music and no sound effects, only the movies' sound** (the log repeated `audren: first-frame commands …` every frame and never printed `DeviceSink inputs`). SDK 8 numbers the DSP commands differently: its command list goes 4×6, 7×24, 11×2, 16×1, 21×1 where SDK 20's goes 7×6, 11×24, 15×2, 20×1, 25×1, and the output command (DeviceSink, the same 0x148-byte layout with device name `MainAudioOut`, six inputs) is 0x10, not SDK 16's 0x13. When the expected ID is missing from the first full command list, `capture` in `hle_audren.c` takes the one 0x148-byte command with 1 to 6 inputs as the sink and logs the ID it adopted. +- **Resolved: an abort with Result 0x499 in `audio::GetAudioRendererWorkBufferSize`.** SDK 8's `AudioRendererParameter` is one word longer than SDK 16's (its revision at +0x38); the host copied only 0x38 bytes before switching it to CPU rendering, so `CheckValidRevision` read garbage. It copies 0x40 now. +- **Movies:** VP9 with a 5.1 Vorbis sound track (decoder type 5, output format 1, PCM16). The host decodes the sound with stb_vorbis, which needed a local fix for type-2 residues with more than two channels ([README](../../third_party/stb_vorbis/README.md)). **Resolved:** before it the opening and title movies sounded distorted and gurgling, with pops. + +## Physics + +- **Resolved: the physics workers crashed when a conversation or cutscene began** (`pc=main+0x10ccf28`, `lr=main+0x13c236c`; on the iPhone and in 2 of 5 Mac runs, in every Mac run under CPU load). The physics step (`main+0x15730`, main thread) rebuilds each world's PhysX completion task in place (a `PxLightCpuTask` at world+0xc8) before every `simulate`. A worker runs it (`main+0x10ccf80`: `fetchResults`, world+0x1d8 done = 1, notify, unlock) and then calls its `release` (`main+0x10cd070`), which calls the task's own destructor (vtable back to the `PxLightCpuTask` base, `mTm = 0`, `main+0x10cced0`). At 30 fps the game steps physics twice per frame (step, wait, step, wait); the unlock hands the world's mutex to the woken main thread, and when the worker is descheduled there the main thread's next step rebuilds the task before the late destructor runs, which then wipes the new one. The next simulation's `NpSceneCompletion::release` reads `mTm = 0`. On the Switch the release follows the unlock within nanoseconds. +- **Current:** the release becomes a plain `ret`: the destructor only resets the vtable and `mTm` (no resources), the step rebuilds every field, and this release is used by this one vtable (`main+0x1c51308` slot 7) only. +- How it was found: 12 busy shell loops beside a scripted Mac run made the crash happen in every run (and the frame rate switch between 60 and 30 as on the phone), and observation gates (`OBS`) on the functions involved gave the order of events ([05, Racy crashes](../../../docs/agents/05-debugging.md#racy-crashes-load-trace-gates)). + +## Services and SDK 8 + +- **Resolved: an abort in `os::WaitSystemEvent` under `oe::PopNotificationMessage`**: the host's `oe::Initialize` now creates the notification event on its queue. +- **Resolved: SIGSEGV in `vi::SetLayerCrop`**: `SetLayerCrop`, `DestroyLayer`, `CloseDisplay` and `vi::Finalize` are host no-ops. +- **Resolved: the game never read the controller** (the title's "Press L+R" saw no press; no `GetNpadState` call): it waits for the event bound by `hid::BindNpadStyleSetUpdateEvent`, which the host now signals when bound for a connected controller and when a player's controller connects or goes away. +- `nvnWindowGetPresentInterval` and `nvnCommandBufferSetCopyImageStride` (the layer stride of texture ↔ buffer copies) implemented. +- Shared fonts: SDK 8, so the address is the plain TTF. Focus handling mode 2 (focus notifications are sent). +- Rewriter: offset jump tables in its GLSLC and movie `subsdk`s with the bounds check on another register (the index in `w0`, a copy, or a masked copy), which `jump_offsets` now follows. + +## Files and shaders + +- RomFS: bfres models and maps (`region_common/map`, `region_common/actor`), shaders in `region_common/shader/*.bfsha` (BNSH), particle effects in `region_common/effect/*.ptcl`, layouts in `*/ui/*.arc` (SARC). +- 9,698 shaders from the bfsha archives, effects and layouts. +- **GLSLC.** `subsdk0` is NVN's runtime GLSL compiler and `main` imports `glslcCompile*`; `game.tsv` says the `*_uber.bfsha` are compiled at run time through it, but up to the house, the beach and the menus every shader the game loaded was in the offline tables. + +## Mac routes + +- The title wants L+R: `DM_KEYS="f6000:L+R~0.4,f6300:A,f6500:A,f6700:Down,f6800:A"` reaches a new file's name entry (a fresh save directory via `SV=`). +- Racy crashes: run 12 busy loops beside the route (`for i in $(seq 12); do (while :; do :; done) & done`). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Lower-left window origin** (`g_origin_ll`), **copies drawn as passes** (`copy_t2t_draw`), **compute `c0` with the dispatch size** (`encode_args_`, `nvn_refl.m`), **stencil in depth-stencil copies**: [04, Seen in Link's Awakening](../../../docs/agents/04-graphics.md#seen-in-the-legend-of-zelda-links-awakening). +- **Per-depth-mode vertex entry points** (`vs_depth`, `main0_z`, `mtl_set_depth_zero`): [04, section 5](../../../docs/agents/04-graphics.md#5-depth-convention). +- **Vertex outputs start at (0, 0, 0, 1)** (`vs_init`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **`shader_facts_manual.tsv`** for facts the machine code cannot tell: [dm/games/README.md](../README.md). +- **The DeviceSink found by its size** (`capture` in `hle_audren.c`), the SDK 8 parameter size, `oe` notifications, the npad style event: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Jump tables checked on another register** (`jump_offsets`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **Racy crashes under CPU load and observation gates**: [05, Racy crashes](../../../docs/agents/05-debugging.md#racy-crashes-load-trace-gates). + +## Open + +- **`SR_Y_DIRECTION`.** 2,487 of its pixel shaders read the window's Y-direction system register, which is −1 under a lower-left origin. The Ryujinx recompiler answers a constant +1 (a TODO in `InstEmitMove.cs`; LASRecomp answers −1). In the room shader the value only scales a depth slope under `abs()`; other shaders may use its sign (`dFdy`-based effects). Answering it per profile means a translator change, i.e. every game's chain. +- **Two pixel shaders do not translate** (`9d8f6b8543355374`, `9338e3f1b597fe7c`: variants 0 and 1 of one program in `postprocess.bfsha` @0x252000; SPIRV-Cross throws on a null pointer). No run so far has loaded either. +- development.md calls SDK 8.2.0 "the oldest so far"; Mario Kart 8 Deluxe (SDK 4.4.0) is older now. +- `game.tsv` still says "In bring-up" and its memory values "until measured". diff --git a/dm/games/01006C300E9F0000/README.md b/dm/games/01006C300E9F0000/README.md new file mode 100644 index 0000000..7c039aa --- /dev/null +++ b/dm/games/01006C300E9F0000/README.md @@ -0,0 +1,85 @@ +# DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (`01006C300E9F0000`) + +Update 1.0.4 (v262144) over the base game, SDK 7.3.2. Square Enix, Unreal Engine 4.18 (legacy version 4 paks, LZ5-compressed) on NVN, with Square Enix's own sound driver (`SQEX::Sd`, "SEAD") on the audio renderer. Modules `main`, `subsdk0` (the movie module, driven by Unreal's Switch movie player: WebM, VP9 with two 6-channel Vorbis tracks) and `sdk`; the `sdk` is the same build as Super Mario Odyssey 1.4.1's ([0100000000010000](../0100000000010000/README.md)), and Raging Loop ([010020400E1C2000](../010020400E1C2000/README.md)) runs this game's `sdk` and `subsdk0` builds. Plays on the iPhone; 60 fps (30 on the Switch) by the host's writes of Unreal's frame limits. + +Status: brought up on the Mac through the opening movie, the title (name entry, system settings), the prologue movie (skipped with +) and into free play at Cobblestone Tor (the hero, Gemma, the villagers, the HUD and the minimap), at 30 fps with sound and saves; no red line, about 2.1 GB resident (1.1 GB of it Metal allocations), 1.5 GB mapped in the guest. Playable on an iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `fps_target 60` | [`game.tsv`](game.tsv), `hle_fpslock.c` | the host writes 60 into `t.MaxFPS` and 1 into `nvn.SyncInterval`; see [Frame rate](#frame-rate) | +| `game_args -SwitchVirtualAddressSpaceSize=4064` | `game.tsv` | caps Unreal's one engine region (16 GB by default) at 4064 MB; the value is MB up to 4095, bytes above | +| Node area cut 1 GB → 62 MB (`sdk+0x32c460` / `+0x32c500`) and 32 MB alignment for regions of 1 GB and more (`sdk+0x32c66c`) | `dm/tools/sdk_patches.py`, `sdk` build `fcf93a41…` | together they put the engine region 64 MB into the alias region; the 1 GB alignment would have put it 1 GB in; see [Memory](#memory) | +| 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 (the game sets no heap) | +| `pad_layout full` | `game.tsv` | a 3D game: D-pad, right stick (camera) and L3 / R3 on the iPhone's virtual gamepad | + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). The game's config caps Unreal at 30: `FrameRateLimit=30` in `DefaultGameUserSettings.ini`, which sets `t.MaxFPS`, and `nvn.SyncInterval=2` in `SwitchEngine.ini`, the present interval 2 it asks for. The profile's `fps_target` writes 60 into this build's `t.MaxFPS` and 1 into `nvn.SyncInterval` (`hle_fpslock.c`, both threads' copies), so the game asks for present interval 1 itself. +- Unreal steps the game by the time that really passed. On the Mac (M4 Pro) the field holds 60 fps (frame time p95 17 ms, GPU 6.6 ms p50 / 9.7 ms p95) at normal speed, with sound intact. +- The game's dynamic resolution budgets are for 30 fps (`r.DynamicResolution.*TimeBudget` 28–35 ms in `SwitchEngine.ini`), so it does not lower the resolution to reach 60: where a frame needs more than 16.7 ms, frames are dropped instead. +- `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) keeps the game's 30 and saves power. + +## Memory + +- Unreal Engine 4.18's `nninitStartup` takes one address region for the whole engine (16 GB by default). It reads `-SwitchVirtualAddressSpaceSize=` and the other options from `argv[1]` on (`main+0x177b5a4`; `main+0x177c440` parses each one: `atoi`, MB up to 4095, bytes above). The region needs no allocator cut: the SDK's `NumberLineAllocator` hands it out in 64 KB units (`main+0x177c4d0`), and the engine maps only what it uses. +- **Resolved: a crash in `NumberLineAllocator::Compaction` at boot.** A value of 4224 was taken as bytes: a 4 KB region, and the first 2 MB request crashed. The profile gives 4064 (MB). +- **Current:** the node area cut and 32 MB alignment for large regions on the SDK 7.3.2 build put a 4064 MB region 64 MB into Engage's 4.375 GB alias region. The AddressRegionManager constructor is at `sdk+0x32c450` (the limit at +0x28 and the occupied length both from `orr w9,wzr,#0x3fe00000`); `VammManager::AllocateAddressRegion`'s placement at `sdk+0x32c600` aligns a region by its size as SDK 18 does: 64 KB under 2 MB, 2 MB under 4 MB, 4 MB under 32 MB, 32 MB under 1 GB, else 1 GB (`+0x32c66c`). The same cuts serve Super Smash Bros. Ultimate's SDK 8.2.1 build and Advance Wars 1+2's SDK 10.4.1 build at other offsets. +- On the Mac the mapped total stays at 1.5 GB from the title to the first field (Cobblestone Tor), used up to +1.75 GB of the alias region; the process holds about 2.1 GB, 1.1 GB of it Metal allocations. + +## Rendering + +- **Resolved: the screen came out a flat blue.** Unreal Engine 4.18 builds its 32×32×32 colour-grading LUT with `WriteToSliceMainVS` + `FWriteToSliceGS`, one slice per instance; Metal has no geometry stage, so the tone mapper read an empty LUT. The slice-writing geometry shader is now folded into its vertex shader's layer entry points (`dmsh_build.py` `gs_slice`, `vs_layer_entry`; `nvn_mtl.m` `prog_fold_gs`). The other 22 geometry shaders (one-pass cube shadows that emit transformed triangles) do not build. +- **Resolved: an abort in `MTLTextureDescriptor` validation when the prologue movie ends** (`arrayLength (510) … greater than the max allowed size (341)`). Its cube-map array of 510 faces (85 cubes) needed NVN's layer-face count: the host took the depth as a cube count in the Metal texture, the upload's layer count and the guest layout (`nvn_tex.c` `shape`). A cube array is now created as a 2D array of all its faces, and `tex_fit` makes the cube-array view a shader asks for. +- The flushed-before-first-use texture fill (`tex_init_fill`) filled no texture in this game (title, free play, menus; 300 s). +- NVN calls new with Unreal Engine 4.18: `nvnTextureGetTextureAddress` (pool address + offset, as `nvnMemoryPoolGetBufferAddress`) and `nvnCommandBufferGetCommandMemorySize` (used + free, as in NVN; `GetCommandMemoryFree` is what is left in the current segment). + +## Files and shaders + +- Legacy version 4 paks: `JackGame/Content/Paks/pakchunk0-Switch.pak` (6.6 GB, 264,844 files) and `pakchunk1-Switch.pak` (7.2 GB, 147,824 files; the profile's `romfs_marker`). The update adds `*_0_P.pak` patch paks, which the engine mounts over the others (a path in a `_P` pak wins). +- The footer is the last 0x2d bytes (encrypted index flag, magic, version, index offset and size, SHA-1), and the index is not encrypted: the mount point, the file count, then each file's name and its FPakEntry (offset, stored and raw size, compression flags, SHA-1, the blocks' start and end, which are file offsets before version 5, the encrypted flag and the block size, 64 KB). +- The compression flags are no standard value: 3, which is LZ5 2.0 (LZ5 v2, later renamed Lizard; `main` exports `LZ5_*`, `LZ5_versionNumber` = 20001, and `FCompression::UncompressMemory` calls `LZ5_decompress_safe` for flags 3, `main+0x16f5c70`). Every block starts with its level, 0x2d = 45 (LIZv1 with Huffman). The repository has no LZ5 decoder: the game's own runs in the Mac host's tool mode (`hle_oodle.c`, `DM_OODLE_SERVE`), which `uepak.py` (`Pak.read` with a decode function) and the shader split use. It takes 512 KB of work memory from the game's `malloc` on every call, so the host serves it only after `nninitStartup`. Scanning both paks (13.8 GB) with 8 workers takes 28 s. +- There is no shader code archive. Each material's shader map is serialized in its `.uexp` (38 to 87 shaders in the ones looked at) and the engine's global shaders in `Engine/GlobalShaderCache-GLSL_SWITCH.bin` (945: 597 pixel, 104 vertex, 241 compute, 3 geometry). Each shader's code is a byte array (`i32` length, then the bytes) holding one zlib stream that inflates to an `LSLG` record. `shader_split.py` (`ue_legacy_jobs`, `ue_inline_shaders`) decodes every file of the newest copy of each path with the game's own LZ5 and takes each zlib stream that inflates whole from exactly the length in front of it and starts with `LSLG` (every such stream in the files looked at); records stored uncompressed are taken too, but none were seen. +- 38,892 unique shaders (4,654 vertex, 33,981 pixel, 234 compute, 23 geometry) from 1,740 files, grouped as `global`, `engine` and `game` (`shader_groups`). Vertex and pixel shaders are paired as single-stage programs, as for Octopath Traveler 0 ([01005270232F2000](../01005270232F2000/README.md)). + +## Sound and movies + +- **Resolved: the sound ran about 50 times faster than the output.** SEAD (`SQEX::Sd::Driver::Core::CoreAudioOut`, its loop at `main+0x442c5c0`) appends a wave buffer, mixes the next block on the same thread, calls `RequestUpdate` and takes the next released buffer; it waits on the renderer event only when no buffer is released. Every update finds no frame rendered since the last one and renders a catch-up frame, which releases the buffer just appended, so the loop never waits; when a full catch-up queue discarded the frame, the loop ran 11,000 updates a second on one core. An update with a full queue now waits (at most 50 ms) for the render thread to play one queued frame, so catch-up frames keep real time (`hle_audren.c`). +- The audio renderer's update lock on this SDK is `MutexType` at +0x1d8; checked with sound on this game (`update_lock` in `hle_audren.c`). +- **Resolved: a crash in `InternalCriticalSectionImplByHorizon::Enter` at the first deferred movie signal.** The movie player registers its event struct right after constructing the decoder (`main+0x39edab0`, through the decoder's vtable) and fills it later, so the host's copy held leftovers (`0xe00000010`). The SDK keeps only the pointer (`DecoderImpl::RegisterClientEvents` stores it at +0xd0) and reads the entry each time it signals, skipping a null one (`subsdk0+0x1feb0`); the host now does the same (`md_event` in `hle_movie.c`). +- **Resolved: the opening movie never started in about half the boots** (a black screen and no sound from boot on). Square Enix's `UMediaPlayer` has two natives of its own, `StartPreDecoding` and `IsPreDecodingCompleted`. The opening's Blueprint polls the second once a frame and only then plays its Matinee, whose event key calls `Play`. Its Switch player (vtable `main+0x77921b8`, `IsPreDecodingCompleted` at `main+0x42d4d20`) is complete when every selected track has at least one decoded output queued. The player starts its clock and its decoders on the first `TickInput` after `StartPreDecoding`, and the render thread's first fetch (`main+0x39f07d0`, the `+0x5c7` flag) takes every decoded picture and shows the newest. The host decoded all eight video outputs (to 236 ms) within 36 ms; each Vorbis decoder's 8 outputs were full by then, because nothing consumes audio before `Play`, so the extractor waited for an audio input and no picture followed the ones the render thread took. Audio decoders then kept 32 outputs (`MD_NOUT`, 64 since Monster Train 2; video keeps 8): 8 boots out of 8 start. Found by probing the player's `SetRate` (`DM_PROBE=main:`) to see whether the game ever asked to play, then following the Blueprint native that calls it (the `FNameNativePtrPair` tables give the names). +- **Resolved: 2.25 GB mapped within seconds after the prologue movie was skipped, then a crash in the game's allocator.** Once its extractor is done, the movie player sends an end-of-stream packet (length 0, time 0) for every input it gets, until the game stops the movie; each became an end-of-stream output that the player turned into an audio sample. An input marked end-of-stream is now the last until `Flush` / `Stop` (`in_eos` in `hle_movie.c`). +- **Resolved: distorted movie sound, with peaks far past full scale.** The 6-channel Vorbis tracks decode right only since stb_vorbis's fix for type-2 residues with more than two channels ([stb_vorbis README](../../third_party/stb_vorbis/README.md), GitHub PR #60). + +## Services + +- **Resolved: an abort from `ec::InitializeForShopServiceAccessors`**, called from Unreal's `FOnlineSubsystemSwitch::Init` at the start of `nnMain` (`SVC 0x15` / `0x1f` / `0x21` placeholders, then `Break` with `sm::Finalize` under `ShopServiceAccessServer::Proxy::Initialize`). The library init succeeds with nothing to set up, an accessor never initializes, and its async response is complete at once with no data (`hle_misc.c`); the accessor and response are stubbed whole because the SDK's destructor aborts unless `Finalize` succeeds. +- Listing the rest of `main`'s imports with no hook gave `oe::Begin` / `EndAutoSleepDisabledSection` and `IsAutoSleepDisabled` (counted, `hle_oe.c`), `pctl::CheckFreeCommunicationPermission` (never), SDK 7's `album::SaveScreenshot` without an orientation (fails), `friends::GetBlockedUserList`, `DeclareOpen` / `DeclareCloseOnlinePlaySession` (unavailable) and `fs::DisableGlobalFileDataCache`. The game then runs its opening movie at 30 fps. The `friends::GetBlockedUserList` overload with a `Uid` reports a failure, which this game takes as it is. + +## Rewriter + +- **Resolved: a `jump table ` assertion where the index is a copy made before the check** (`main+0x2dd8cd0`: `mov w0,w18`, then 41 instructions holding two other jump tables on `x0`, then `cmp w18,#4; b.hi; ldrsw x18,[x11,x0,lsl #2]; add x18,x18,x11; br x18`). `check_before_copy` accepts it. A copy of `patch_nso.py` whose assert prints and continues found this one alone among the 25,508 `x18` sites. +- `main` has no unwind entries for its first 70 MB (a 68 MB stretch), which the rewriter disassembled as one function (6 minutes); it got through on a quieter Mac with the compressor and swap. With functions analysed in pieces it takes 292 pieces, and the result is the same as with 1 MB pieces and `b` within 256 KB (75 pieces). + +## Mac routes + +- With the save of an earlier run (the name entered), `DM_KEYS="20:A*150/3,250:Plus*4/10,480:B,483:LUp~4"` skips the prologue movie at about 270 s and walks on Cobblestone Tor at about 480 s. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Unreal Engine 4.18 shaders inline in the cooked packages** (`ue_legacy_jobs`, `ue_inline_shaders`) and **the game's own LZ5 served in the Mac host's tool mode** (`hle_oodle.c`, `DM_OODLE_SERVE`, `uepak.py` `Pak.read`): [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content), [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **Slice-writing geometry shaders folded into the vertex shader** (`gs_slice`, `vs_layer_entry`, `prog_fold_gs`, `draw_gs_check`) and **cube map arrays counted in layer-faces** (`nvn_tex.c` `shape`, `tex_fit`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **Catch-up frames that wait for queue space** (`hle_audren.c`): [03, Audio](../../../docs/agents/03-bringup.md#audio). +- **Movie decoder events read at signal time** (`md_event`), **no input after end-of-stream** (`in_eos`), **32 audio decoder outputs** (`MD_NOUT`, now 64): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Shop service accessors** (`hle_misc.c`), **`nvnTextureGetTextureAddress` / `nvnCommandBufferGetCommandMemorySize`**: same section. +- **Vorbis type-2 residues with more than two channels**: [stb_vorbis README](../../third_party/stb_vorbis/README.md). +- **Rewriter:** an index copied before its bounds check (`check_before_copy`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **The node-area and 32 MB alignment cuts on SDK 7 / 8 builds** (the `orr w9, wzr, #0x3fe00000` form), later taken by Super Smash Bros. Ultimate's and Advance Wars 1+2's `sdk` builds: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). + +## Open + +- The bring-up status (development.md) says 30 fps on the Mac; its frame-rate entry says 60 fps on the Mac with `fps_target 60`. No source records a 60 fps run on the iPhone, while the player-facing README lists the game as 60 fps with 1.0.4. +- 22 geometry shaders (one-pass cube shadows) do not build. diff --git a/dm/games/01006F8002326000/README.md b/dm/games/01006F8002326000/README.md new file mode 100644 index 0000000..5b307eb --- /dev/null +++ b/dm/games/01006F8002326000/README.md @@ -0,0 +1,67 @@ +# Animal Crossing: New Horizons (`01006F8002326000`) + +Update 3.0.3 (v2228224) over the base game, with the Happy Home Paradise add-on mounted. SDK 20.5.4. Nintendo EPD's own engine (code name Park; sead / agl / gsys, NintendoWare G3d / Ui2d / Vfx2 / Atk / Font, Havok, Pia and NEX for local and online play) on NVN. Modules `main`, `subsdk0` (the SDK's movie library) and `sdk`. Plays on the iPhone; 30 fps, as on the Switch. + +On the Mac: a new game through Tom Nook's counter (name entry on the game's own keyboard, birthday, look, hemisphere, island choice), the in-flight movie with its sound, the island's creation and the arrival (Timmy's welcome, the player at the dock under control); no red line but the ones below. 1.85 GB resident and 1.15 GB of Metal allocations at the dock. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `render_cond on` | [`game.tsv`](game.tsv) | the game wraps its clears in render conditions that skip a target nothing drew into since its last clear, as Tears of the Kingdom does ([0100F2C0115B6000](../0100F2C0115B6000/README.md)). Without the key, four clear-only passes wiped the composite before the window copy at frame 900, and every frame came out black (the island probe and the loading icon were drawn into render targets) | +| `sys_resource 0x0` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0): `nninitStartup` sets a heap (3.09 GB) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| `mem_used 0x7a00000` (122 MB) | `game.tsv` | the rewritten modules are larger than the default 64 MB of used memory (`main` alone is 63 MB of text and 15 MB of bss); the NSP script's guess after the rewrite, which leaves a 3.09 GB heap | +| `pad_layout full` | `game.tsv` | the 3D layout: the D-pad holds the tool ring and the right stick turns the camera | +| `shader_groups` `/Shader/` `/System/` `/Effect/` `/Model/` `/Layout/` `/Swkbd/` | `game.tsv` | see [Files and shaders](#files-and-shaders) | + +## Rewriter + +- **Resolved: a `B()` assertion at a spin wait, a branch into the patch area beyond the ±128 MB a `b` reaches.** Its `main` was the first module to put branches out of range of the patch area: 63 MB of text and 51 halfword jump tables (`ldrh wE, [xT, xI, lsl #1]; …; adr x18, #L; add x18, x18, xE, lsl #2; br x18`). Each got a landing pad for every one of the 65,536 values an entry can hold (768 KB a table), whatever the switch's size: 40 MB, and the spin-wait stubs placed after them were out of reach. `half_count` now pads only up to a table's largest entry; 7 of the 51 cannot be read and keep all 65,536. The patch area went from 42 MB to 7.9 MB. +- The spin-wait search through stores finds 166 loops in `main` (162 before), and the movie library's thread loop is wider in `subsdk0` ([development.md, Power](../../../docs/development.md#power)). + +## Threads + +- **Resolved: a black screen with the plane icon for 77 s after the opening movie** (also about 40 s of the same at boot), the CPU nearly idle. `SaveProc` yielded about 2,600 times a second, the power line's top yielder with as many back-off sleeps. The thread was working, not waiting: its grid walks over the new island (`main+0x157d020`) call `nn::os::YieldThread` once per cell, and the back-off made each yield a 250 µs sleep. A yield whose caller's x19–x28 changed since the last one now counts as progress and returns at once (`thr_yield` in `hle_thread.c`). The island now shows about 10 s after the movie, as with `DM_YIELD_BACKOFF=0`. A temporary log of which of x19–x28 move at each yield, per thread, told the working loops from the polls. + +## Services + +- **Resolved: abort in `sm::Initialize` under `sf::hipc::InitializeHipcServiceResolution`, right after the first frames** (from `main+0x5b8198`). `main` links its own clients of `olsc:u` (cloud save data backup settings) and `npns` (push notifications), which talk HIPC through `sm` and `svcSendSyncRequest`. For each user the game asks whether backup is on and switches it off where it is (`main+0x2809c8c`); a failed connect aborts as well. `hle_sf.c` takes over the SDK's HIPC client entry points (`InitializeHipcServiceResolution`, `ConnectToHipcService`, `SendSyncRequest`, `CloseClientSessionHandle` and the control requests `QueryPointerBufferSize`, `ConvertSessionToDomain`, `DuplicateSession(Ex)`): `olsc:u` gets a host session, and every request on it succeeds with zeroed output, which reads as backup off for every user. Any other name is refused with a red line, as `sm` refuses a service nobody registered (`npns` was never connected). `DM_SF_TRACE=1` dumps each request. +- **Current: device save data.** 3.0.3's `main` mounts no account save data; the island is in device save data (`MountDeviceSaveData`, `GetDeviceSaveDataSize`, `ExtendDeviceSaveData`), which the host gives the account save's host directory and transaction (one player). A second mount name over a save already mounted shares that mount's transaction (`mount_ex`); it used to clone the committed directory again, over the first name's pending writes. Also `fs::Commit` with a `CommitOption` (its one flag clears the restore flag) and `GetSaveDataRestoreFlag` (never restored from a backup). A game whose `main` imports `MountSaveData` too gets a device save of its own (Mario Kart 8 Deluxe, [0100152000022000](../0100152000022000/README.md)); this game does not, so its islands stay where they are. +- **Resolved (PR #25): `fs::Commit(const char* const*, int)`, the mount-list overload.** The SDK's own code opened fsp-srv and aborted, leaving the save writes uncommitted. The host commits the list as one transaction when every name is the same host save; cache and temporary storage in the list are skipped, and a list of two separate saves is refused without committing either. +- Found in `main`'s imports before the first boot: the adjustable user clock (`time::AdjustableUserSystemClock::GetCurrentTime`, which the game's days follow) reads the time service's shared memory in the SDK (`MonotonicSystemClock::GetCurrentTime`, `sdk+0x4421cc`), which the host never maps, so it is the host's clock, as the user clock; `time::GetStandardSteadyClockCurrentTimePoint`, the free-function form of the steady clock; `oe::GetRegionCode` (from the language: Japan, the Americas, Europe or Hong Kong / Taiwan / Korea); the SDK 20 album calls `SaveAndShareScreenshot` with a share ID and `GetSharePostedLastUrl` (no album: they fail). + +## Saves + +- A save is not reached on the Mac: the game first saves when the player sleeps at the end of the first day (the `−` menu says "You can't save right now." before that). Until then it only probes for `main.dat`, `mainHeader.dat` and `Moved/personal.dat` in its device save data. + +## Movies + +- The in-flight movie `Movie/OpeningMovie.webm` is VP9 1080p with Vorbis, demuxed by `movie::Extractor` and decoded on the host (`hle_movie.c`). +- **Resolved: the movie showed as a black screen with the loading icon, one picture every few seconds, while its sound decoder kept up.** The player holds every output it is offered (16 of 16 video, 59 of 64 audio), so how far its extractor can get ahead of the picture on screen is the video slots plus the 4 queued packets. With 8 video slots that was 0.4 s, and the audio behind the next video packet, whose clock the player follows, ran dry: 86 pictures in 25 s. With 16 or 32 slots all 1,960 play at 30 fps. A video decoder now has 16 (`MD_NOUT_VIDEO`); a slot is allocated when first used, so the 8 more cost 24 MB of NV12 at 1080p only in a player that holds them all. + +## Files and shaders + +- RomFS: 97,997 zstd files (`.zs`, most of them models), BWAV sound, a few Yaz0 SARC archives (`.szs`). The profile's `romfs_marker` is `Movie/OpeningMovie.webm`. +- **Current:** 9,766 BNSH programs: `Shader/Park_UBER_All` (3,385) and the other bfsha / sharcb in `Shader/`, agl's `System/Agl/agl_resource` (3,206), particle effects in `Effect/` (2,522), models' own bfsha in `Model/`, layouts (bgsh) in `Layout/`, the game's keyboard in `Swkbd/` and 40 in `main`. +- The keyboard's ocean background has 8 geometry shaders, which do not translate; the name entry draws without them. +- Prewarm list: recorded from a fresh save through the counter, the movie and the arrival to the dock (965 lines). + +## Mac routes + +- From a fresh save, `DM_KEYS="40:A*51/2,145:B*12/0.4,151:Down,152:Right*4/0.4,154.5:A,155.5:Right*4/0.4,158:A,159:Down*2/0.4,160.5:Left*2/0.4,162:A,163.5:Plus,167:A*4/2,175:Plus,178:A*146/2,179:Plus*30/4"` names the player "tom" on the keyboard, takes the defaults, watches the movie and reaches the dock at about frame 14,100 (470 s). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Halfword jump tables padded to their largest entry** (`half_count` in `patch_nso.py`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **Yields that make progress return at once** (`thr_yield` in `hle_thread.c`): [03, Threads](../../../docs/agents/03-bringup.md#threads) and [development.md, Power](../../../docs/development.md#power) ("Yield loops"). +- **The game's own HIPC clients answered by host sessions** (`hle_sf.c`, `DM_SF_TRACE`), **device save data and shared transactions** (`mount_ex`), **`fs::Commit` with a mount list or a `CommitOption`**, and the adjustable user clock, region code and SDK 20 album and steady-clock calls: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **16 output slots for video movie decoders** (`MD_NOUT_VIDEO` in `hle_movie.c`): same section. + +## Open + +- `game.tsv` still describes the game as "In bring-up", while it plays on the iPhone. +- A save on the Mac (the end of the first day) has not been reached. +- The keyboard's 8 geometry shaders do not translate. diff --git a/dm/games/0100713016CCC000/README.md b/dm/games/0100713016CCC000/README.md new file mode 100644 index 0000000..0ebc11a --- /dev/null +++ b/dm/games/0100713016CCC000/README.md @@ -0,0 +1,48 @@ +# AI: THE SOMNIUM FILES - nirvanA Initiative (`0100713016CCC000`) + +Update 1.0.2 (v131072), SDK 13.3.5. Unity 2020.3.30f1 IL2CPP (Patrick's Parabox's Unity, [0100E44016C22000](../0100E44016C22000/README.md)) with FMOD and Addressables bundles. Modules `main`, `subsdk0` (the movie module) and `sdk`. Plays on the Mac and on the iPhone; 60 fps (present interval 1). It needed no host change. The README lists its IPA at about 11.6 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `3644a676…` | `nninitStartup` hands pool − used to StandardAllocator as virtual address memory and Unity reserves its own regions beside it; see [Memory](#memory) | +| `sys_resource 0x1000000` | [`game.tsv`](game.tsv) | the NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true | +| 5 GB block with a 1 GB pool: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0x40000000` | `game.tsv` | Ace Attorney Investigations Collection's layout ([010005501E68C000](../010005501E68C000/README.md)): 256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region. With the hardware pool the alias region was nearly full within 5 s | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the investigation moves its cursor with the left stick and the view with the right one | + +## Memory + +- The NPDM declares 16 MB of system resource. `nninitStartup` (`main+0x493300`) hands application pool − used memory to StandardAllocator as virtual address memory (`StandardAllocator::Initialize(nullptr, pool − used)`) and never calls `SetMemoryHeapSize`, as Patrick's Parabox (the same Unity 2020.3.30f1). +- **Node area cut** on the `sdk` build `3644a6760469451cabc99f00c540acc2`: the AddressRegionManager constructor is at `sdk+0x37c130` (limit = alias start + 0x3fe00000 at +0x28, a unit heap of 0x30-byte nodes); the cut changes `mov w8,#0x3fe00000` → `mov w8,#0x3e00000` at `sdk+0x37c140` (the node area limit) and `mov w9` the same at `sdk+0x37c1dc` (its length registered as an occupied region), 62 MB. +- **Pool:** with the hardware pool (3.2 GB), StandardAllocator's 1 GB-aligned region and Unity's own regions used the alias region up to +4.26 GB of 4.38 in the first 5 s (the NSP script's heap check). So the profile has a 1 GB pool; the heap check now sets that for such a game (`profile_guess.py` `heap_signals`: the alias region nearly full within seconds and no heap; [dm/games/README.md, Adding a game](../README.md#adding-a-game)). +- **Measured on the Mac** over 15 minutes from boot through a new game into the first investigation (probes on `VammManager::AllocateAddressRegion`'s success path `sdk+0x37c2a0` and `FreeAddressRegion` `sdk+0x37caf8`, commits from the `DM_SVC` map log): StandardAllocator takes 960 MB at +64 MB and commits 0.29 GB of it; Unity's allocator (`main+0x484024`) holds about 1.10 GB in 170 regions (16.25 MB and 2.25 MB blocks, created and freed as scenes load), 1.03 GB of it mapped; 2.1 GB of regions live in all, 1.26 GB mapped, 1.96 GB resident. +- The highest region end climbs (+3.77 GB after 7 minutes) while the live total stays flat: the SDK places each region at the bottom of the highest gap that fits (`sdk+0x37c2d0` walks the gaps from the top down), so new regions go above the highest live one until the top is used, and only then into the holes below. + +## Movies + +- `subsdk0` is the movie module; the game's movies play through Unity's video player (960x540 NV12 frames). + +## Rendering + +- Docked, the game draws 1280x720 by its own choice and crops the 1920x1080 window to it. It presents at interval 1 (60 fps). + +## Files and shaders + +- The shaders are Shader objects in the Addressables bundles of `StreamingAssets/aa` (they carry type trees) and in `Data/Resources`, and 147 ComputeShader kernels, which Unity 2020.3 keys by keyword set (`shader_split.py` `unity_compute`, `variantMap`). +- 6 geometry shaders (a wireframe effect, Unity's VR blit) do not build (`game.tsv`). +- `romfs_marker` is `Data/StreamingAssets/aa/settings.json`. + +## Input + +- Controller only, docked, with the 3D pad layout (the investigation's view is on the right stick). + +## Found on this game + +- **ComputeShader variants keyed by keyword set** in Unity 2020.3 (`shader_split.py` `unity_compute`, `variantMap`). +- **The heap check's 1 GB pool rule** for a game whose alias region is nearly full within seconds and that sets no heap (`profile_guess.py` `heap_signals`): [dm/games/README.md, Adding a game](../README.md#adding-a-game). + +## Open + +- The 6 geometry shaders are listed as not building in `game.tsv`; no source says whether the geometry compute path that now builds AI: THE SOMNIUM FILES' three ([010089B00D09C000](../010089B00D09C000/README.md)) builds these. +- The game has no entry in the development notes; its route on the Mac (a new game into the first investigation) is known only from `game.tsv`'s memory comment. diff --git a/dm/games/01007EF00011E000/README.md b/dm/games/01007EF00011E000/README.md new file mode 100644 index 0000000..f7dc542 --- /dev/null +++ b/dm/games/01007EF00011E000/README.md @@ -0,0 +1,125 @@ +# The Legend of Zelda: Breath of the Wild (`01007EF00011E000`) + +Update 1.9.0 (v1114112) over the base game, SDK 20.5.22. Nintendo EPD's engine (sead / agl / gsys / ksys, NintendoWare G3d / Ui2d / Vfx / Atk, Havok) on NVN. Modules `main`, `subsdk0` (NintendoSDK_movie) and `sdk`; `subsdk0` and `sdk` are the builds Tears of the Kingdom 1.4.3 runs ([0100F2C0115B6000](../0100F2C0115B6000/README.md)). Plays on the iPhone; 60 fps (30 on the Switch) through the host's frame step. The README lists its IPA at about 16 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Frame step, `fps_target 60` | [`patches.py`](patches.py) `FRAME_STEP` (`main` `cd57b23f…`), [`game.tsv`](game.tsv) | 60 fps at normal speed; see [Frame rate](#frame-rate) | +| Interval controller floor 2 → 1 (five cuts) and GPU-time path (two cuts) | `patches.py` `CUTS` | the game's own controller at `DM_FPS_TARGET=0` and while its VFR override holds; see [The game's own interval controller](#the-games-own-interval-controller) | +| Render scale stubs | `patches.py` `frame_step` (`main+0x1266880`, `main+0x126686c`) | `DM_RENDER_SCALE`; see [Render scale](#render-scale) | +| `gpu_timestamps segment` | `game.tsv` | the interval controller and the dynamic resolution read the GPU time between the frame's two timestamp reports | +| Integer output rows `8e2cc2671cae6020`, `93aa5e22a8202409` | [`shader_facts_manual.tsv`](shader_facts_manual.tsv) | agl's BC4 block encoder (3D and 2D source) writes `uint`; no PC version gives output types | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the right stick turns the camera | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource in the NPDM: the game uses `SetHeapSize`. 13 Sentinels' partition (256 MB in front + 256 MB alias + 256 MB stack + 3.25 GB heap) | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `574284b4…` | registered for Tears of the Kingdom's issue #70 on the same `sdk` build, so it applies here too | + +No `alpha_from_red` (no longer needed since the `nvnTextureCompare` fix), no `tex_flush_upload` (a minute's boot saw it flush 32 CPU-filled textures with data, and it needs none), no `feedback_snap`. + +## Frame rate + +### The frame step (current) + +- **60 fps at normal speed (30 on the Switch).** The game steps its time by the frame interval its framework picks (`ksys::VFR`: interval / 2 of a 30 fps tick). With the profile's `fps_target` (60) the host picks it instead, in whole vsyncs (`FRAME_STEP` in `patches.py`, `hle_nvn.c` `fstep_tick`, `nvn_fstep.h`, the same frame step as Tears of the Kingdom's): a stub at the framework's `setWaitVsyncInterval` hands the game the host's step once a frame, and the host keeps each frame on screen that many vsyncs. +- It holds 1 (60 fps). Three late frames in a row (the frame or its GPU work more than 1.7 ms over) step up to the cost, 2 (30 fps) as a rule: no 40 fps on 120 Hz screens. 30 frames that would fit the shorter hold try it again, and each failed try doubles the wait before the next (1 → 8 s). After a hitch, a cost at 30 like the one that held 60 tries it too (probes, below). +- Real time lost to a late frame or a hitch is paid back one vsync per frame (at most 15; the rest of a long load is dropped), so game time = real time. The log line `frame step: game time 30.00 s in 30.00 s (1.000 of real time)` checks it every 1,800 presents, with the frames held 1 / 2 / 3 vsyncs, late frames, paid-back vsyncs and hold changes. +- Scenes that fix the game's step themselves (the VFR's override: 2 at `main+0xc3b588` / `+0xc5ebf8`, 1 at `main+0xc609d8`) keep it, and the host holds frames that long (log line `the game fixes its own step`; not reached on the Mac yet). +- The stubs: `setWaitVsyncInterval` (`main+0x11269b8`, called once a frame at `main+0x11267c0`; the framework keeps the interval at fw+0x244, getter vtable +0x138, read by both callers of `VFR::updateInterval` and directly in 43 places, and passes it to `nvnWindowSetPresentInterval`) takes the host's step unless the VFR override (+0x29 flag, +0x2c interval) is set. `ksys::VFR::updateInterval` (`main+0x151d3d0`, x0 = the VFR at `[main+0x1e93890]`, called from `GameScene::calc` and the per-frame delegate `main+0x1280f80`) takes the framework's interval in the next frame (ratio = step / 2, base interval 2, and the int interval 16 pieces of logic read); its stub publishes the override at state +0x24 so the host holds frames that long. The step is not written into the VFR call itself, because the game presents on its own thread and the step can change between the two VFR calls of a frame. +- Mac (M4 Pro), from the title into the save's scene: 1.000 of real time at 60; with 12 ms added to every frame (`DM_TEST_PRESENT_LAG=1,12`) it drops to 30 and stays at 1.000. `DM_FPS_TARGET=30` holds 30 (saves power, also from `Documents/env.txt`); `0` runs the game's own interval controller. +- **Frame-step probes (iPhone).** The GPU clock follows the hold: on an iPhone the game went from 14.4 to about 25 ms of GPU time a frame at 30 fps with the same 4,900 draws, its CPU still on the performance cores, and never tried 60 again; a GPU time at the longer hold of at least 1.3 times its time at 60 that alone does not fit 60 (`nvn_fstep_gpu_slowed`) now allows probes. Stepped up by pipeline compiles after a cutscene, it took 12.8–15.2 ms of GPU time a frame at 30 fps (p50) where 60 had held for minutes, and stayed at 30 for 36 and 46 s until the player switched out and back (the same 4,900 draws took 5.8 ms right after a trip to the background and 13.5 ms half a minute later); a step up after 8 s or more at 60 now keeps that run's cost, and a cost at the longer hold of at most 1.1 times it allows a probe (`nvn_fstep_same`). Log lines: `frame step: hold 2 → 1 … (probe, …)`, `the probe held / failed`, `hold 1 held at 0.86`. Details: [development.md, Power](../../../docs/development.md#power). +- **Screens whose rate is not a multiple of 60 (from PR #27).** A cheap 50 ms frame shows 54.5 ms at 165 Hz; counted as late it kept the game at 20 fps for good, so the frame step lets a frame on time show one refresh longer than its hold (`shown_slack`, `nvn_fstep.h`). +- `DM_NVN_CUT` growing segments (64, 128, 256 … draws) were tried for fewer commits: the game fell from 60 to 30 fps in the same scene (GPU idle while the long last segments were encoded). + +### The game's own interval controller + +Used at `DM_FPS_TARGET=0` and for the interval the framework keeps while the VFR override holds; 04 says `DM_FRAME_STEP=legacy` still runs this method. + +- `sead::GameFrameworkNx`'s main loop (`main+0x112660c`, framework +0x267 = 1) runs a dynamic interval: from the last frame's time it computes how many vsyncs were needed (rounded up), clamps that to [2, 3] (the maximum from the interval controller +0xc, built at `main+0x1126c04`), steps up after three overrunning frames and down one level every six frames, and sets the result through `setWaitVsyncInterval` (vtable +0x130, `main+0x11269b8`). `ksys::VFR` is initialized with a constant base interval of 2 (`main+0x127e740` → `VFR::init` `main+0x151d2d0`) and every frame `VFR::updateInterval` (`main+0x151d3d0`, called at `main+0xcf9ad4` / `main+0x1281044`) takes the framework's interval, a frame ratio of interval / 2. The game's time step follows its frame interval, so the speed is right at a steady 60, 30 or 20. +- Five cuts lift its floor from 2 to 1: the candidate floor (`main+0x112670c` `fmov s0,s11` → `fmov s0,s10`, `main+0x1126710` `cmp w27,#2` → `#1`), the step-down target recorded when stepping up (`main+0x112665c` `mov w24,#3` → `#2`, `main+0x1126748` `cmp w8,#3` → `#2`) and the step-down stop (`main+0x1126794` `cmp w9,#3` → `#2`). Probes confirmed it: interval 1 with ratio 0.5, and back to 2 with ratio 1 on a long frame. The zeldaret/botw decompilation (`VFR.cpp`) gave the semantics; 1.9.0's addresses were found by the constants (`60 / interval`, the `fmov s11, #2.0` floor). +- **Resolved: slow motion when the GPU is the bottleneck (GitHub issue #9).** The controller measured a frame as framework +0x250, the main thread's CPU span from the start of `procFrame` to before its draw step (`main+0x1126cec` → `main+0x1126d20`): about 2.7 ms in the shrine (probe at `main+0x11266a4`, `@x19+0x250`; "about 3 ms" in development.md). Only while the object at `[main+0x1e3a638] + 0x1d2b8` exists did it take max(CPU span, GPU time), in 64 of 6,046 shrine frames. An iPhone 16 Pro Max in handheld mode at a 25 ms frame p50 never left interval 1 in 4,800 frames: at 40 fps, about two thirds of the normal speed. The GPU time (framework +0x220) is the difference between the frame's two timestamp reports, `TIMESTAMP_TOP` at the start of its command buffer (`main+0xe94584`) and `TIMESTAMP` at its end (`main+0xe94664`), read after the draw step waited for the frame's fence (`main+0xe949bc`); it read 0–6 ticks, because the host wrote all 5 reports of the frame's one submission with one time. Two cuts (`main+0x112691c`, `main+0x1126974`: `cbz` → `b`) make the controller always take the GPU path, and `gpu_timestamps segment` gives each report the GPU start or end of its `sub_cut` segment (`rep_ticks` in `nvn_mtl.m`). +- Measured on the Mac with two runs sharing the GPU under a heavy CPU load (load average about 75): GPU time p50 16–23 ms; the fixed run stayed at interval 2 (3 while the GPU time passed 1.9 vsyncs, 1 for under 2% of the frames) at 0.93–0.98 of real time (Σ VFR ratio / 30 per wall second, probe `main+0x151d3e0` `@x0+0x30`: +0x30 ratio, +0x34 interval). A run with the old timestamps (`DM_NVN_GPU_TS=submission`) spent 10–70% of its frames at interval 1 and ran at 0.78–0.95. +- The same GPU time drives the dynamic resolution (`main+0x1267070`): a running average as a percentage of a 60 Hz frame scales the resolution down above 175% (29 ms) and back up below 150% (25 ms). With 0 it always sat at its maximum, and it still does whenever the game holds 30. +- **Resolved: Link's move pace speeding up when pressing B (iPhone).** Sprinting flaps between interval 1 and 2 for runs of 6 or more frames, and the VFR step at interval 2 is a whole 30 fps tick. The acquire waited one display-link tick per frame whatever the interval, so the game kept producing frames every 16.7 ms while the screen held each interval-2 frame for 33 ms and dropped the one between (`old frames dropped by merging`): those runs went at double speed. `mtl_link_wait_tick` now waits as many ticks as the present interval. + +### Render scale + +- `DM_RENDER_SCALE=0.5`–`1.0` (also from `Documents/env.txt`) fixes the 3D views at that fraction of 1600×900 docked (1280×720 handheld) and turns the game's dynamic resolution off; 1.0, the default, keeps it. Factors above 1.0 are refused by the loader (the per-frame texture pool's size is unknown). +- The dynamic-resolution method (`main+0x12666e4`) keeps its scale in s8 (0.8–1.0 in 0.05 steps, `+0x1218`, `main+0x1266818`, or the forced 0.75 / 0.5 of `+0x284 & 0x1800`) and applies it from `main+0x1266880`: the views' scale (`+0x28a0` / `+0x28a4` = s8 × base width (`+0x1214`: 1600 docked, 1280 handheld) / output width), from which the size function (`main+0x106340c`) sizes the scene buffers each frame, the shader ratio `uking_dynamic_resolution_ratio` and the other readers (`main+0xc3c3f4`, an LOD check at `main+0x14126b4`). That block runs only when the dynamic path has just recomputed the scale (`+0x280` bit 4, set at `main+0x1266830`) or in a forced mode, else `main+0x126686c` skips it. The stub sets s8 at the block and, for a factor other than 1.0, takes the block every frame. A multiply in the size function (Tears of the Kingdom's way) would leave the ratio and those readers at the old scale. +- At 0.5 the 1600×900 passes become 800×450 and the 800×450, 400×225, 200×112 and 100×56 chains halve too, while the shadow maps and the 1920×1080 output keep their size and the HUD stays sharp. It holds in handheld mode (960×540 at 0.75). On the Mac (M4 Pro) the GPU time does not change (about 13 ms in the first shrine at 1.0 and at 0.5): a frame has about 650 passes, most of them tiny. Whether it helps an iPhone, whose GPU loads and stores each pass's attachments, is what the setting is for. + +### CPU + +- **Resolved: CPU hitches (Mac).** In steady play, frames with 30–210 ms of CPU work came every 1–10 s (Zora's Domain, M2 Max): the main thread and the two workers spinning in one job queue poll (`main+0xef62dc`). The first item of some of its jobs is for the thread of core 0; the threads of cores 1 and 2 wait until it is taken (the queue's flag at +0x98, its cursor at +0x94), taking and releasing the queue's lock (+0x1c) on every pass. Polled at full speed by the two workers, the lock went back and forth between their cores, and the main thread lost nearly every exclusive store: in 40 s of a Time Profiler trace it never held the lock while each worker held it about 750 times, and the three spun for 30–210 ms at a time. The spin-wait rule's lock case takes that loop: the workers leave the lock a moment between passes. Zora's Domain on an M2 Max at 120 Hz (42 s of steady play, three runs each, before → after): standing still, frames over 30 ms of CPU work 2.8 / 6.3 / 3.5 → 0 per 30 s, the frame's work p99 20–21 → 8.3–8.4 ms, late frames 2.8 / 5.6 / 2.8 → 2.1 / 0.7 / 2.1 per 30 s; turning the camera, CPU hitches 11.9 / 13.3 / 6.3 → 0, p99 21–25 → 9–15 ms, late frames 12.6 / 13.3 / 7.7 → 1.4 each. Found with `DM_HITCH_DUMP` (the main thread and two workers in one guest loop with no SVC; x0 held the item index) and an `xctrace` Time Profiler trace. +- The spin-wait search takes 310 loops in `main` (308 before the lock and store rules; the new two are the job queue and a state setter at `main+0xf82230` that stores its state and returns on the second pass unless another thread changed it) and widens one in `subsdk0` (`0x27804`, the movie library's thread loop, 6 → 23 instructions). Rewriting `main` takes 26 s of CPU instead of 19. +- `PlacementMgr` and `RadarMgr` yield once per item of their batches (the back-off stretched a 94,263-item radar batch to about 30 s). A yield whose caller's x19–x28 moved now returns at once: outdoors on the Mac, CPU energy went from 0.35–0.74 to 0.21–0.34 W at 60 fps, CPU from 76–83% to 96–106% of a core, and the frame interval's p99 from 19 to 20–21 ms. A `sched_yield` on each of their 200,000 yields a second cost a third of a core more than the back-off had. +- **Address → memory pool lookups**: the game keeps about 1,760 memory pools alive, and `mtl_addr` / `mtl_gpu_addr` scanned them all for every buffer binding of every draw (about 1,000 samples of a submit-thread profile). Last hit, then a binary search (`pool_find`): host submit time in the shrine 21 → 7 ms a frame (p50), the median frame 38 → 21 ms. +- Upload staging blocks are reused: the game used to allocate, make resident and free a 32 MB block for each re-upload of a small CPU-written texture, about 3 GB/s. + +## GPU work + +- **Submissions in segments.** The game does not submit frame N+1 until frame N's GPU work is done: in the shrine the main thread sat in `nvnSyncWait` 59% of the time and the render thread in `nvnQueueSubmitCommands` 40%. That submit is the host's translation (about 3,600 draws and 347 passes, 7 ms), so the GPU ran 13–14 ms and then sat idle 7.7 ms a frame. The replay now commits every 64 draws between two passes (`sub_cut`, `DM_NVN_CUT`): GPU idle 7.7 → 3.8 ms, most of the rest the game's own work between its fence wait and the next submit (about 2.5 ms). +- **Shrine of Resurrection on the Mac (M4 Pro)**, after the above: docked (1600×900) GPU about 13 ms, frame p50 16.7 ms (60 fps; p90 17.6 ms), was 20.8 ms before segmenting; handheld (`DM_OPMODE=0`, 1280×720) GPU p50 9.3 ms and a steady 60. Host translation is 8 ms a frame (22,000 instructions per draw), spread over Metal's encoder creation and draw calls, argument encoding, retain / release and pool lookups. `DM_NVN_PASSGPU` is off under Metal 4, so GPU time cannot be attributed to passes. The frame loaded 376 MB and stored 432 MB of render-target data (325 of 347 passes load and store everything). +- **Outdoors (2026-10-21, the user's Great Plateau save, Mac M4 Pro).** A frame had about 386 render passes and loaded and stored about 900 MB of attachments (on an iPhone at 60 fps about 55 GB/s, near the memory's bandwidth). 220 passes ended only because the game updates the first 64 bytes of a 64 KB vertex uniform block (`0x70c38c0000`) before each draw group while its shaders read up to 544 bytes of it (the terrain pixel shader `eb0afe555b86da79` indexes the whole block). Partial-cover uniform shadows (`ubp_get`, `DM_NVN_UB_PARTIAL`) and `cb_extent.tsv` keep those passes open: 386 → about 165 passes, GPU p50 about 9.3 → 6.5 ms, submit 5.7 → 4.9 ms; `DM_NVN_UBP_CHECK=1` found every shadow equal to the GPU's memory (968,000 checked). Lazy clears fold the four clear-only passes before the G-buffer into its first pass (about 30 MB less a frame at 1600x900). The frame still loads and stores about 800 MB: the NintendoWare VFX GPU particles run one compute dispatch per emitter between draws (ten 800x450 half-resolution passes and two G-buffer passes, about 190 MB a frame; the G-buffer ones read the depth being drawn), and decals sample the G-buffer right after the game sets it again. Tried and dropped: `DM_NVN_TBDR=1` (10–15% fewer stores, more CPU, red ledger lines, deferred uniform updates off), `DM_NVN_BARRIER=game` (no gain), same-target continuation outside TBDR mode. +- Same-pass feedback: over a thousand draws a frame sample the G-buffer of their own pass (`4d3d5a4408c2e8d9`, `753d0169ae3ce4f0` and others on the 1600×900 G-buffer); snapshotting all of them took the Mac frame time from 16.6 to 36.6 ms, and none has shown magenta blocks on the iPhone so far. The decals that sample targets of their own pass read the stored content on the Mac; `DM_NVN_FB_SNAP=all` would snapshot them at a large cost. + +## Rendering + +- **Resolved: the 3D scene black, only particles and the HUD drawn.** The engine binds almost every texture with `nvnCommandBufferBindTexturesDeferred` (`main+0xec2e64`, one handle, the pointer being the handle field inside the texture-sampler object): the GPU reads the handle when the command executes, and the game records its command blocks once and writes the handles afterwards. The host copied them at recording, mostly zeros, so the deferred-lighting composite read the dummy texture 0. The bind record keeps the source address and replay refreshes the handles first (`bh_fresh`). Found with a data-flow table from `DM_NVN_PASSDUMP_ALL`: the composite's `B` lines were all `tid=0 dummy` (without `_ALL` only the first draw of each pass is listed). +- **Resolved: the same texture attached at two colour slots.** The G-buffer passes attach the albedo and another buffer twice each (c1 = c2, c3 = c4) with write mask 0 on the duplicates. On Apple's TBDR the later copy (loaded, never written) overwrote the other's writes, per 32×32 tile: the albedo held only scattered squares. A repeated slot is left unattached (`rt_dup_slot`). +- **Cube textures sampled as cube arrays**: 1,512 shaders sample the environment probes as `texturecube_array` with a single cube bound; NVN cube textures are now 6-layer 2D arrays bound through views (`tex_fit`). +- **Resolved: a compressed texture as a render target (GitHub issue #4, iPhone 14 Pro).** agl's BC4 block encoder (`8e2cc2671cae6020`) writes a 32×32 RGTC1 (BC4) texture through an `RGBA16UI` view (one texel per 8-byte block). Metal can neither attach a compressed format nor view one uncompressed, and pipeline validation aborted, so the draws did nothing. The pass now draws into an uncompressed shadow of blocks × blocks (`bcrt_attach` / `bcrt_flush`, from PR #11); its output is declared `uint` in `shader_facts_manual.tsv`, as is its 2D-source twin `93aa5e22a8202409`, whose failure showed up in an iPhone run. The A16 has no BC formats; the old decompression path (about 3.9 GB instead of 2.8 GB for this game, over iOS's limit on a 6 GB iPhone) has been removed. +- **Resolved: the sky brownish grey, the distant haze brown (GitHub issue #52, Mac and iPhone).** Each frame a pass (`08fe25061d583637`, `uking_pass_shader.sharcb`) turns a 256×32×16 RGBA16F 3D table of in-scattered light into a 256×256 sky map, which the sky draw (`8110c74078548172`) and the materials' aerial perspective sample. The table and two 2D ones (transmittance 256×64, irradiance 64×64) come from `System/KSys/sky.skybin` in `Pack/Bootup_Graphics.pack` (1,212,416 bytes). The game makes a `CPU_CACHED | GPU_CACHED` pool per texture, creates the textures, copies the data in with `memcpy` and flushes (`main+0x1257d50`, once; the flush goes through `main+0xebf4c4`). The host made the 3D table a GPU texture of zeros. `DM_NVN_TEXRAW=212,214` and `DM_NVN_TEXINFO=256x32` (`storage 1048576 bytes non-zero 781567`, `swizzled=0`) showed it. Every game now fills a texture from storage the game flushed before its first use (`nvn_flushed`, `tex_init_fill`; `DM_NVN_FLUSH_FILL=0` turns it off). In the Great Plateau save it fills that one texture; the flushes (about 15 a frame) merge into about 530 ranges, frame times unchanged. +- **Resolved: dark boxes and cream cloud rectangles behind HUD prompts, a black stamina wheel and a black System menu background (layout captures, GitHub PR #6).** Panes tagged `CaptureOn` (about 140 layouts, e.g. `PaGuideKeySmoke_00`; user data `A8`, `CaptureScale` 0.5, `MultiFilter`) are drawn, blurred at half size and copied into a capture texture that replaces the placeholder `FBLayout_00`. The game creates it with its per-format default mapping (`main+0xecb0bc`: R8 → (R, 0, 0, 1), RG8 → (R, G, 0, 1), …), then rebuilds it on the same storage with the capture's mapping: `main+0xf597dc` picks (1, 1, 1, R) for an `A8` capture and alpha 1 when the pane says `CaptureOutputAlpha` 255 (read at `main+0xf58e98`); `main+0xecacf0` stores the mapping and rebuilds (`main+0xecabe8`), and `main+0xec33f0` replaces the registered descriptor only if `nvnTextureCompare` reports a difference. The host ignored the mapping, so the captures kept (R, 0, 0, 1): hard-edged dark boxes (the shadow pane `e6a88e12dbae9743`) and cream cloud (the glow `aa209f57b133e862`). The earlier `alpha_from_red` workaround (all 26 layout pixel shaders, `main`'s BNSH at 0x1d83000, `tex_alpha_red`) fixed the alpha but zeroed green and blue (a black stamina wheel) and left the System menu black. `nvnTextureCompare` now compares the mapping (an unset mapping counting as (R, G, B, A)); checked on the Mac by the PR's author: green stamina, the purple noise meter, the blurred System menu. +- The game keeps NVN's GL depth mode. It has an `RG8_UI2F` vertex attribute (converted on the CPU, `vconv_i2f`). 101 compute and 4 vertex shaders read the driver segment `c0`; they render the same since compute `c0` carries the dispatch size. +- It finalizes its transient render targets before every submit and builds them again the next frame, which the storage-takeover rule guards with a 16-submission window (`alias_take_over`). +- Windowed on the Mac with `present_now`'s residency commit: its title, 60 fps, no GPU error. + +## Memory + +- **Resolved: the world-load crash (about 1 run in 3 on the Mac) came from the host's command record sizes.** About 3 s after Start, a task worker's `operator new` (`main+0x155a79c`) got null and the game wrote through it (`main+0x155a7a4`). Each model file gets a heap of its resource size table entry + 0x1008; the file goes in 4 KB aligned, and the model's setup (`main+0x155a2ac`) takes the rest as a frame heap (vtable `main+0x1d4f950`, head +0xe0, tail +0xe8). Part of the setup is GPU memory for the model's prerecorded commands, sized by recording each mesh's draw and each shape's state for each pass into a scratch command buffer and summing `nvnCommandBufferGetCommandMemoryUsed` (`main+0xf9aeb4`, `main+0xf9bb54`), so the host's record sizes count against the table. Draw records had grown to carry the indirect and multi-draw fields (0x38 → 0x50 per recorded draw on 2026-10-18 and 10-20), and every recording began with an 8-byte BEGIN record. `Obj_TreeConiferous`, `Obj_TreeBroadleaf_B` and `FldObj_HyliaBridge_A-02` then needed one 4 KB page more than their entries allow, which fit only when the file's alignment pad (0 to 0xff8) was small (about 15% of loads each failed; most models have about 0xd00 to spare). Plain draws now leave those fields out and recordings have no BEGIN (0x30 per recorded draw, `nvn_obj.h` `draw_rec`): all 510 models of the world load fit whatever the pad. To check a whole scene, probe where the setup heap is cut (`main+0x155a520` with `!x21`) and the end of the setup (`main+0x155a5f4` with `!x2`) with `DM_PROBE_HEAP=28,38,40,e0,f8 DM_PROBE_HEAPFRAME=1d4f950 DM_PROBE_HEAPWORDS=1`. A model fits any placement when entry + 0x1008 covers the resource heap's header and object (0x310), the file, the frame heap's block header and its peak use, plus 0xff8 of pad. +- **Resolved: the iPhone app was killed for memory in the first shrine (GitHub issue #9, iPhone 16 Pro Max, handheld).** An 8 GB iPhone gives the app about 3.4 GB (resident plus `headroom to the system memory limit` stayed at 3,375 MB). Resident memory grew about 0.2 MB a frame from 2.6 GB at the shrine load until the kill, with the residency set and texture counts flat. The deferred-release drain ran on the guest submit thread with no autorelease pool (`defer_drain` now has its own `@autoreleasepool`): on the Mac a host without it grew 1 MB a frame in the shrine (GPU 3.0 → 4.4 GB in 2,400 frames); with it the shrine stays at 2.7–2.9 GB resident and 1.5–1.7 GB GPU (vmmap at 2.8 GB: about 1.6 GB guest memory, 0.75 GB private textures, 0.17 GB IOAccelerator). Found on the way: about 10 serial-table entries a frame were never deleted (finalized transient targets replay resolved again; 276,000 after 25,000 frames), now in a 512-entry table (`tex_put`, `g_ghost`, `finalized textures still referenced`); and the memory-pool tracking table had 4,096 slots against 4,006 pools on the phone, now growing in blocks of 1,024 (`nvn_dirty.c`, up to 262,144). +- About 2.8 GB in the first shrine in handheld mode, idle or walking (Mac), against the 3.4 GB an 8 GB iPhone gives the app; the README says it comes close to that limit without the Increased Memory Limit capability. + +## Audio + +- **Resolved: crashes in `CommandListProcessor::Process` (SIGSEGV under `render_main`, at 60 s and 134 s in the shrine).** SDK 20's `AudioRenderSystem` has another layout (getters inlined, `GenerateCommand` takes and returns a `Span`, device and mode at +0x48 / +0x4c, command buffer at +0xf8 / +0x100, `UpdateImpl` locks +0x2d0; DeviceSink is command 0x14). The public `ExecuteAudioRendererRendering` (`sdk+0x221e90`) reaches `AudioRendererImpl::ExecuteAudioRendererRendering` (`sdk+0x22ca88`) through the vtable, so the host's takeover of the `Impl` function was never called: the SDK's own `AudioRenderSystem::ExecuteAudioRendererRendering` ran with no update lock, every `RequestUpdate` rendered a catch-up frame, and two threads wrote the same command buffer. The host now also takes over the PLT target; catch-ups fell from one per update to 3 per run. + +## Services and loading + +- **Resolved: every pointer in `.data` / the GOT a module offset, crashes everywhere.** SDK 20 packs relative relocations as `DT_RELR`: `main` has 223,994 of them (`relr_apply` in `loader.c`). +- **Resolved: SIGBUS inside Metal (`AGCDeserializedReply`) at a guest-stack address.** Ending a render pass inside `nvnQueueSubmitCommands` made the driver compile its fast-clear blit on the render thread's small stack; every implemented NVN function except the recording calls now runs on the 512 KB host stack (`nvn_big_entry`). +- SDK 20 calls it needed: `applet::GetAppletResourceUserId` (SDK 20's `OpenAudioRenderer` aborts on ARUID 0; the host reports 0x80), `pl::RequestSharedFontLoad`, `pl::IsVrMode` / `BeginVrMode` / `EndVrMode`, `pctl::CheckStereoVisionPermission` (permitted), `oe::TryPopLaunchParameter` (none), `time::StandardSteadyClock::GetCurrentTimePoint`, the console six-axis sensor (`hid::*SevenSixAxisSensor*`: zero states) and more `nfp` calls (`AttachActivateEvent` / `AttachDeactivateEvent` crashed in the SDK at `sdk+0x3f2a78`; two-argument `Mount`, `Unmount`, `GetCommonInfo`, `GetRegisterInfo`, `GetState`, `Finalize`). `nvnPrivate0` (a flag in `sdk`'s bss) stays 0, and `nn::ro::ProtectRelro` runs natively on the host's no-op permission calls. +- `main` imports `ScalableFontEngineHelper::Decode` (SDK 20), so the shared font is served as BFTTF. Focus handling mode 2 (focus notifications are sent). +- Rewriter: 1 to 6 halfword jump tables in `main` changed when `half_count` padded only to their largest entry. + +## Files and shaders + +- RomFS is almost all Yaz0 (`.s*` extensions) and SARC (`Pack/*.pack`); `romfs_marker` `Pack/Bootup_Graphics.pack`. +- 19,592 shaders: `Shader/*.sbfsha` (BNSH), the gsys / ksys resource packs in `Pack/Bootup_Graphics.pack` and the terrain pack in `Pack/TitleBG.pack`, particle effects in `Effect/*.sesetlist` (11,373 shaders, 101 compute), layouts (`.sblarc`) and 37 in `main`. All bind with Mario's texture handle layout; the full build had no failure and no unresolved binding. +- `pack_ipa.py` packs its 16 GB IPA in 57 s (Info-ZIP took 2 min 50 s). + +## Mac routes + +- From the title menu into the autosave: `DM_KEYS=f2500:A,f2800:A,f3100:A,f3400:A` (control at frame ~3900). +- The frame step under load: `DM_TEST_PRESENT_LAG=1,12` (12 ms added to every frame: 30 fps at 1.000 of real time). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The host frame step in whole vsyncs** (`nvn_fstep.h`, `fstep_tick`, `FRAME_STEP` stubs, `fps_target`), explained in full under [Frame rate](#the-frame-step-current); its probes (`nvn_fstep_gpu_slowed`, `nvn_fstep_same`): [development.md, Power](../../../docs/development.md#power). +- **Real GPU timestamps per segment** (`gpu_timestamps`, `rep_ticks`), **submissions in segments** (`sub_cut`), **deferred texture binds** (`bh_fresh`), **duplicate colour slots** (`rt_dup_slot`), **cube textures as 2D arrays** (`tex_fit`), **compressed render targets through shadows** (`bcrt_attach` / `bcrt_flush`), **pool lookups** (`pool_find`), **memory fixes** (`defer_drain`'s pool, `g_ghost`, growing pool table), **`nvnTextureCompare` with the channel mapping**, **first-use fill of flushed storage** (`nvn_flushed`, `tex_init_fill`), **partial-cover uniform shadows** (`ubp_get`, `cb_extent.tsv`): [04, Seen in Breath of the Wild](../../../docs/agents/04-graphics.md#seen-in-the-legend-of-zelda-breath-of-the-wild). +- **Display-link waits per present interval** (`mtl_link_wait_tick`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **Spin waits that take a spin lock** (`patch_nso.py` "Spin waits", `dm_spin_wait`) and **yields that make progress** (`thr_yield`): [development.md, Power](../../../docs/development.md#power). +- **`DT_RELR`** (`relr_apply`) and **halfword jump tables** (`half_count`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). **NVN calls on the big host stack** (`nvn_big_entry`): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **SDK 20 service calls** and **record sizes against `GetCommandMemoryUsed`** (`draw_rec`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). **SDK 20's audio renderer layout** (`render_once`): [03, Audio](../../../docs/agents/03-bringup.md#audio). **Running a 30 fps game at 60**: [03, Frame rate](../../../docs/agents/03-bringup.md#frame-rate). +- **Hitch dumps and guest thread names** (`DM_HITCH_DUMP`), **heap probes** (`DM_PROBE_HEAP*`), **autorelease leaks**: [05, Scripted input and thread scheduling](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling), [05, Memory growth](../../../docs/agents/05-debugging.md#memory-growth). + +## Open + +- The GPU particles' compute dispatches and the G-buffer decals still split passes; about 800 MB of attachments a frame outdoors. +- `DM_RENDER_SCALE` does not change the Mac's GPU time; the sources give no iPhone measurement. +- The VFR override's `the game fixes its own step` path has not been reached on the Mac. +- `game.tsv` still says "In bring-up", and its memory comment "until measured". +- 05's `DM_PROBE_POLL` paragraph cites `PlacementMgr` in `main+0x5aaac0` as the one thread in guest code "for that flag" without naming the game; `PlacementMgr` is this game's. diff --git a/dm/games/010086A022444000/README.md b/dm/games/010086A022444000/README.md new file mode 100644 index 0000000..4e849f1 --- /dev/null +++ b/dm/games/010086A022444000/README.md @@ -0,0 +1,44 @@ +# BALL x PIT (`010086A022444000`) + +Update 1.293 (v393216), SDK 20.5.19 (its own `sdk` build). Unity 6000.0.62f1 IL2CPP with FMOD, Addressables and Rewired, on NVN, as Hollow Knight: Silksong ([010013C00E930000](../010013C00E930000/README.md)). Modules `main`, `subsdk0` (the SDK's movie library) and `sdk`. Plays on the iPhone (IPA 1.1 GB); 60 fps. It needed no host change. + +On the Mac: the boot, the title, a new game, the intro and the descent into the pit, the first dive and its tutorial (moving, aiming and shooting the balls, gems and XP, the enemy waves that overrun the player and end it), and the base-building tutorial (the sidebar, the build menu, placing a wheat field and a forest, the first harvest with ZR), with FMOD sound. 60 fps (frame time p50 16.66 ms, p95 about 16.8 ms; GPU 1–5 ms a frame), about 1.6 GB resident and 0.7 GB of Metal allocations, flat over 10 minutes; no red line on that route. The title is up within 25 s of launch. The prewarm list covers that route (297 lines). + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `8bfebccd…` | Unity 6 takes all its memory through `AllocateAddressRegion`, after the node area; see [Memory](#memory) | +| `sys_resource 0x1000000` | [`game.tsv`](game.tsv) | the NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true | +| 5 GB block: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | Engage's block of the other Unity games with virtual address memory (4.375 GB alias region) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: in a dive the right stick aims | + +## Memory + +- As Hollow Knight: Silksong: 16 MB of system resource, no heap (`main` never sets one), Unity 6 reserving and committing its memory itself through `nn::os::AllocateAddressRegion` and `AllocateMemoryPages` in the alias region, Engage's 5 GB block, and the node-area cut on this `sdk` build. +- **Node area cut** on `sdk` build `8bfebccd6f5c1392597b364b9ab22a8d`: its AddressRegionManager constructor is the SDK 20.5.22 build's (Tears of the Kingdom's, [0100F2C0115B6000](../0100F2C0115B6000/README.md)) at the same offsets: the constructor at `sdk+0x145cfc`, the limit at `sdk+0x145d0c`, the occupied length at `sdk+0x145d78` (`mov w8 / w9,#0x3fe00000` → `#0x3e00000`). +- The provisional profile was right as generated; the `sdk` build only needed the cut ([03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space)). +- Measured on the Mac over 10 minutes of the first dive and the base: the alias region's high-water mark +2.00 GB of 4.375 GB, 1.26 GB mapped, 1.6 GB resident, 0.7 GB of GPU memory, flat. + +## Rendering + +- Docked, the game draws 1280x720 into 1920x1080 window textures and crops the window to 1280x720 (`nvnWindowSetCrop`), which the present scales to the screen. A Mac shot shows the crop only with `DM_NVN_SHOT_OUT`. + +## Files and shaders + +- 38,099 shaders (18,913 vertex, 18,917 pixel, 116 compute, 76 hull and domain, one geometry), 11 of them the player's own GLSLC outputs in `main`; everything is under `Data/` (`shader_groups /Data/=data`). +- Addressables: 11 bundles in `StreamingAssets/aa/Switch`; FMOD banks in `StreamingAssets` (one stereo AudioOut). +- The loose serialized files (`resources.assets`, `sharedassets1.assets`, the built-in resources) have no type trees. They borrow the `Shader` tree that 10 of the bundles carry (the same type hash; `shader_split.py` defers a file that raises `NoTree` to a second pass), so `unityfs.py` needed no layout, as for Silksong ([04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container)). +- `romfs_marker` is `Data/sharedassets1.assets`. + +## Input + +- Docked with the 3D pad layout (`pad_layout full`); it reads no touch. In a dive the left stick moves, the right stick aims, ZR shoots and X toggles autofire. In the base the left stick moves the build cursor, A confirms, B goes back, X opens the sidebar, ZR sends a worker out to harvest and ZL + R pans. + +## Mac routes + +- The work folder's `run/ctlrun.sh ` starts a run with `DM_CTL`; `run/see.sh ` presses and takes a screenshot; `run/route.sh ` plays the route above from a fresh save (start the run with `SV=` set to an empty save folder). + +## Found on this game + +- The spin-wait search's store rule takes more loops here: `main` 87 → 94, and the movie library's thread loop in `subsdk0` is wider: [development.md, Power](../../../docs/development.md#power). diff --git a/dm/games/010089B00D09C000/README.md b/dm/games/010089B00D09C000/README.md new file mode 100644 index 0000000..20d64ff --- /dev/null +++ b/dm/games/010089B00D09C000/README.md @@ -0,0 +1,73 @@ +# AI: THE SOMNIUM FILES (`010089B00D09C000`) + +Update 1.0.1 (v65536) over the base game, SDK 6.4.2. Unity 2017.4.17f1 IL2CPP with FMOD, on NVN. Modules `main`, `subsdk0` (the movie module) and `sdk`. Plays on the iPhone; 60 fps (present interval 1). The README lists its IPA at about 8.3 GB. + +Brought up on the Mac: the Spike Chunsoft logo movie, the title (its background movies), a new game (language, auto save notice), the opening at the merry-go-round and the first investigation with its conversation tutorial, both sticks; loading the autosave from the title. About 2.27 GB resident, flat over 5 minutes; audio through FMOD's AudioOut. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `pad_layout full` | [`game.tsv`](game.tsv) | the 3D gamepad layout: the investigation moves its cursor with the left stick and the view with the right one | +| `mem_used 0x6800000` (104 MB) | `game.tsv` | the rewritten modules take more than the default 64 MB of used memory: with 64 MB the heap request plus the modules exceeds the pool and `SetHeapSize` fails (as Cuphead, [0100A5C00D162000](../0100A5C00D162000/README.md)) | +| `sys_resource 0x0` | `game.tsv` | the NPDM declares no system resource, so the game has no virtual address memory | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region; see [Memory](#memory) | + +No `patches.py`, and `dm/tools/sdk_patches.py` has no cut for its `sdk` build (`184751379007f9c50fc96ba4d26a5dca`). + +## Memory + +- No system resource: Unity sets a heap of the application pool − used memory (3.11 GB) in the 4 GB layout and allocates from it. `mem_used` is 104 MB for the rewritten modules. +- On the Mac: 2.27 GB resident over 5 minutes from boot to the first investigation, flat. + +## Movies + +- `subsdk0` is the movie module. The 168 movies (`StreamingAssets/Movie`, H.264 and AAC in MP4) play through Unity's video player: NV12 frames into CPU buffers, copied to an R8 + RG8 texture pair each frame (`nvnCommandBufferCopyBufferToTexture`). +- **Resolved: the game never left a movie that had played to its end; the last picture stayed on screen.** The 6 s Spike Chunsoft logo (`StreamingAssets/Movie/al_titel_cl_sc_1k_30.mp4`, H.264 High) before the title held the game there. `DM_MOVIE_TRACE=1` showed the end-of-stream packet arriving with H.264 pictures still in the reorder queue: its output went out as a black picture without the end-of-stream flag, and the queued pictures were to follow one per free output slot (the drain). Unity held all 16 video slots by then, and `ReleaseOutputBufferIndex` decoded again only when packets were queued, so the drain never went on and the game waited for the movie's end for good. Fixed in `hle_movie.c` (a released slot goes on with the drain; see [Found on this game](#found-on-this-game)). The audio decoder was never affected: AAC has no reorder queue. + +## Rendering + +- Docked, the game draws 1280x720 by its own choice and crops the 1920x1080 window to it. +- **Resolved: a draw with the vertex shader of another Shader object.** Once on entering the first investigation: `[RED] failed to create render pipeline (vertex 790573a52b9c5053_b fragment f39fb42850f50ecd_b): Fragment input(s) user(locn0),user(locn1) … not written by vertex shader`. The vertex shader was `Legacy Shaders/VertexLit`'s shadow caster, the pixel shader `PSYNC/BG_AlphaTest`'s, whose own pass pairs it with `00b462fe0dec6e06`. Unity binds the vertex and fragment programs apart, so every draw's pair is a composite of `bind_stages`, one of a ring of 64. In a 280-draw submission the precompile prediction (`pso_predict_rest`) made more than 64 composites and reused the slot the saved `S.prog` named, so the draws after the restore, up to the next bind, ran with that slot's new pair. Fixed by `prog_resolve` (logged as `combined program taken again after a state restore`). Two runs each from one save with one key script, prewarm off: the old host logged `texture type mismatch: shader declares a cube, texture is 2D` both times (a pixel shader run with another pair's bindings), the fixed host no red line, with the slot taken again twice. + +## Files and shaders + +- The shaders are Shader objects in the AssetBundles of `StreamingAssets` (read by `shader_split.py` through `unityfs.py`, Cuphead's Unity 2017.4 layout) and in `main`: 13,057 (6,572 vertex, 6,482 pixel, 3 geometry). All build for Mac and iOS. +- The ComputeShader objects of `Data/resources.assets` have no type tree (a red line in the split, as in Cuphead); no dispatch has missed one so far. +- `romfs_marker` is `Data/StreamingAssets/AssetBundles/Switch/audio`. + +### Geometry shaders + +The three geometry shaders were the test case of the general geometry-shader compute path ([geometry support and limits](../../../docs/geometry-metal-research.md#ai-the-somnium-files-evidence)): + +| GS key | Effect | Candidate VS | +|---|---|---| +| `1b378c5c014e3a32` | `Hidden/DepthOfField/BokehSplatting`: expands input vertex 0 to a four-vertex triangle strip | `ba2512f7f6973541` | +| `d57cdb168fdf6927` | `Hidden/Dof/DX11Dof`: the same expansion, input varying locations 0 and 1 exchanged | `771d597a6eb545aa` | +| `2341c1be4fcbf690` | `UCLA Game Lab/Wireframe/Single-Sided`: screen-space edge distances from all three positions, three vertices out | `ce9cbd7dca9fda74` | + +- The candidate vertex keys come from adjacent entries of the same Unity shader, not a captured runtime binding. The bokeh shaders really expand geometry and fail the layer-only fold; wireframe cannot drop its GS, as its fragment inputs depend on the other two vertices. +- **Current:** they build through the native-VS-capture / GS-compute path. `t_geometry`, built with `GAME=010089B00D09C000` and given the current `cache/b-` as its first argument, also checks the capture entry and the three translated GS kernels against numeric expectations (498 checks passed on an M4 Pro with Metal API validation). A 90-second Mac run reached the title at 60 fps with no `[RED]` line (its final `CRASH sig=14` is the harness's time limit). Artifacts: `010089B00D09C000-work/geometry-build/` and `geometry-probe/` beside the game's NSPs. +- None of them was drawn in the opening observed so far, so their in-game appearance and cost are not checked. + +## Input + +- Controller only, docked, with the 3D pad layout: the investigation moves its cursor with the left stick and the view with the right one. + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL`. From a fresh save: 15 s, A (New Game), Down, Down, A (English), Left, A (Yes), then A every 3 s into the investigation (about 3 minutes). +- With the autosave: `DM_KEYS="15:A,18:A,21:Left,22:A,30:A,40:A*45/3"` loads it and plays the opening again. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **H.264 drain on a released output slot** (`ReleaseOutputBufferIndex` goes on with the drain; the end-of-stream output takes the first queued picture, `jflush`, `hle_movie.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Per-stage composite taken again after a replay state restore** (`prog_resolve` in `nvn_mtl.m`, after `pso_predict_rest`, the fusion probe and the deferred-draw replay): [04, the entry under Seen in Metroid Dread](../../../docs/agents/04-graphics.md#seen-in-metroid-dread). +- **Geometry shaders as compute** (native VS capture, GS dispatch, output-reader vertex function; `t_geometry`): [geometry support and limits](../../../docs/geometry-metal-research.md). +- The spin-wait search's store rule takes one wider loop in its `main`: [development.md, Power](../../../docs/development.md#power). + +## Open + +- `game.tsv` still says the 3 geometry shaders "do not build"; the development notes and the geometry research say all 13,057 build, including them. diff --git a/dm/games/01008BC024FDE000/README.md b/dm/games/01008BC024FDE000/README.md new file mode 100644 index 0000000..0167cc0 --- /dev/null +++ b/dm/games/01008BC024FDE000/README.md @@ -0,0 +1,59 @@ +# STEINS;GATE RE:BOOT (`01008BC024FDE000`) + +Update 1.0.3 (v131072) over the base game, SDK 20.5.17 (the same SDK generation as Breath of the Wild and Tears of the Kingdom). M2's ADV engine (`advengine`, m2lib, the E-mote player for the animated characters) on NVN. Modules `main`, `subsdk0` (the SDK's movie library) and `sdk`; `sdk` and `subsdk0` are the builds FINAL FANTASY RESONANCE runs ([0100606026F12000](../0100606026F12000/README.md)). Playable on an iPhone; 60 fps. The README lists its IPA at about 6 GB. + +On the Mac: the logos, the title menu (its background is a movie), a new game through the prologue movies into the story, the config pages, save and quick load, at 60 fps with music, voices and movie sound; no red line in 15 minutes of story. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | 13 Sentinels' partition, as the other games without virtual address memory: 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `0023a1b0…` | made for FINAL FANTASY RESONANCE on the same build; this game has no virtual address memory | + +The pad layout is the default 2D one (no `pad_layout` row) and the operation mode the default docked one; see [Input](#input). + +## Memory + +- The NPDM declares no system resource (META +0x14 = 0): the game uses `SetHeapSize` (3.05 GB) in the 4 GB layout. The modules take about 35 MB, under the default used memory. +- About 1.0–1.1 GB resident in the story on the Mac. +- **Resolved: GPU memory grew with every picture shown.** The game decodes each picture it shows (ASTC 4x4 images up to 4096 wide, RGBA8 render targets, R8 masks) into one scratch region of a pool that lives for good, finalizes that texture when it is done with it and builds the next one in the same region with another size or format; during some transitions it builds a new render target every frame, one pixel wider each time (614x720, 615x720, …). The storage-sharing table (`g_alias`) kept every one of them until the pool was finalized: from a quick save, 297 entries and 1.29 GB of GPU memory at frame 6,600; 468 entries and 1.58 GB 24,000 frames into a new game. With `alias_take_over` (a new shared texture takes over the entries of its pool whose storage it overlaps under another description): 97 entries and 0.93 GB at frame 6,600 on the same route; 15 minutes of story from the quick save (53,700 frames) stayed at 1.08–1.15 GB of GPU memory and 1.02–1.13 GB resident, and a new game from boot stays at 0.97–1.03 GB from its first story scene to frame 27,300 (with the 16-submission guard). GPU memory now stays near 1 GB where it used to grow with every picture. + +## Rendering + +- 487 shaders (483 pixel, 4 vertex) in five BNSH in `main`; RomFS holds none. The build decodes every binding. Its frames are light: a background, the E-mote characters, the text window. +- **1280x720 in both modes, by the game's own choice.** The window is 1920x1080 and the game crops it (`nvnWindowSetCrop`) to the 1280x720 it draws; the present scales the crop to the screen. Its render size setting (engine object + 0x6000) holds 1920x1080 docked from its constructor (`main+0x383eb0`) until its start-up code sets 1280x720 (`main+0x17c50` → the setter `main+0x389e90`), whatever the host reports. Raw screenshots (`DM_NVN_SHOT`) show the whole swap texture, the picture in its lower-left corner (lower-left window origin); `DM_NVN_SHOT_OUT=960x540` gives the presented picture. +- **How the render size write was found:** the static search for stores with offset 0x6000, conditional probes on 25 candidate stores and a wrapper around the host's `memcpy` / `memset` (the guest's go straight to the host's libc, `hle_table.c`, where guest probes cannot see them) all found nothing; a watch that unprotected the page for every fault missed the write among the bursts of stores to the engine object's page; `DM_PROBE_WATCH` found it on the first run (`main+0x389e90`, a setter its start-up code calls with 0) ([05, Probes without a debugger](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe)). +- **A film look:** the backgrounds carry a green scratch line, coloured dust specks, chromatic fringes and displaced rows; by every sign the art's own film effect (not yet compared with a Switch capture). + +## Movies and audio + +- 87 H.264 / AAC MP4 movies in `movie/`, demuxed by the SDK's `movie::Extractor` in `subsdk0` and decoded by the host (`hle_movie.c`): decoders opened with output 4 (NV12 into the client's buffer, 1280 × 720 × 1.5 bytes) in mode 1. The title menu's background is one of those movies. +- **Resolved: a null read at `sdk+0x1f2314` as the first movie starts** (the title menu's background, right after the first press), in `nn::audio::SetAudioOutVolume`, called from `main+0x1c4fbc` on the AudioOut the game opens for the movie's AAC sound (`OpenDefaultAudioOut` without an event, `main+0x1c48c0`). The SDK's version calls the audout session's virtual method, and the host never made the session. The game sets the volume every frame while a movie plays. The host keeps it per output and scales the PCM16 it plays, saturating; `GetAudioOutVolume` reads it back (`hle_audio.c`). It was among `main`'s imports that no host hook names; the others there fill the game's own structures or run in `sdk` (the CPU audio renderer, the swkbd and hid argument builders). It was the one call the game needed ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)). +- Its sound goes through the hardware Opus decoder calls (`hle_codec.c`). + +## Files and shaders + +- Everything else is in the game's own PSB archives (`_info.psb.m` index + `_body.bin` data, mzs-compressed but for the plain PSB sound and voice bodies), which it reads itself. `romfs_marker` is `image_hd_body.bin`. +- Only the merged 1.0.3 copy is extracted; the base game's own build IDs are added when needed. + +## Input + +- Buttons only: it reads the touch screen in handheld mode but takes nothing from it, so the profile keeps the default docked mode and the 2D pad layout. +- Controls (its HELP screen): A enter, B cancel / hide text, X back log, Y auto mode, L force skip, R skip mode, ZL / ZR phone trigger, − tips, + system menu, R3 quick save (R3 needs the 3D pad layout). + +## Mac routes + +- Into the story from a quick save: A on the title, Right to CONTINUE, A, Right to QUICK LOAD, A, A on the save, Left to OK, A loads the quick save. The menu takes presses late and unevenly while its background movie starts (100 to 500 frames after the title's A), so check a `DM_KEYS` route's frames with screenshots. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **`nn::audio::SetAudioOutVolume` / `GetAudioOutVolume`** per output (`hle_audio.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Storage takeover in the storage-sharing texture table** (`alias_take_over` in `nvn_mtl.m`, with its guards for live texture objects and lookups in the last 16 submissions, and the `shared textures N (…)` memory line): [04, Seen in STEINS;GATE RE:BOOT](../../../docs/agents/04-graphics.md#seen-in-steinsgate-reboot). +- The halfword jump-table padding (`half_count`) changed 1 to 6 tables in some `main`s, this one among them: [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). + +## Open + +- 04 and `game.tsv` say the start-up code sets the render size to 1280x720 through the setter `main+0x389e90`; 05 describes the same setter as one "its start-up code calls with 0". Not reconciled. diff --git a/dm/games/010093801237C000/README.md b/dm/games/010093801237C000/README.md new file mode 100644 index 0000000..757a32f --- /dev/null +++ b/dm/games/010093801237C000/README.md @@ -0,0 +1,90 @@ +# Metroid Dread (`010093801237C000`) + +Update 2.1.0 (v327680) over the base game, SDK 11.4.3. MercurySteam's own engine on NVN; scripts are Lua 5.1 bytecode (`.lc`), audio is NintendoWare Atk. Modules `main`, `subsdk0` (NintendoSDK_movie 11.4.3) and `sdk`; `subsdk0` and `sdk` are the builds 13 Sentinels: Aegis Rim ships ([01003FC01670C000](../01003FC01670C000/README.md)), and the `sdk` is also Monster Hunter Rise's and Big Brain Academy's. Plays on the iPhone; gameplay at 60 fps, cutscenes at 30, as on the Switch. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `core_preempt on` | [`game.tsv`](game.tsv) | the movie player relies on Horizon's per-core preemption; without it about half the runs crashed at the intro / title movie changes. See [Movies](#movies) | +| Observation gates OBS 19 / 20 at `main+0x2247a4` / `main+0x2248e4` | [`patches.py`](patches.py), `main` build `646761f6…` | wait for the movie dispatcher's request ring to empty after each posted request (`movie_ring_wait`) | +| 2.25 GB block: `mem_reserve 0x90000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource; 256 MB in front + 256 MB alias region + 256 MB stack region + 1.5 GB heap region. See [Memory](#memory) | +| `modules main subsdk0 sdk` | `game.tsv` | `subsdk0` is the movie library, which the game's movies go through | +| `shader_groups /system/shd/=shd` | `game.tsv` | every shader is in `system/shd/*.bshdat`; see [Files and shaders](#files-and-shaders) | + +## Memory + +- The NPDM declares no system resource (META +0x14 = 0), so there is no virtual address memory: the game uses `SetHeapSize`. `main`'s bss is about 1.8 GB (`0x70cd2fa0`, the engine's static heap); it is part of the module image, not of the block. +- Measured (`DM_SVC=1`): the alias region gets one page (4 KB) mapped during SDK initialization; thread stacks go into the stack region through `MapMemory`; the heap request is pool − used = `0x58800000` (1.38 GB, with the 1.8 GB bss counted as used). The block needs only 2.25 GB; with `main`'s 1.9 GB image that is about 4.3 GB, below the 5 GB proven on a phone (D2R). The application pool is still the Switch's 3285 MB. +- **Resolved: `SetHeapSize` asked for almost the whole application pool and failed at boot** (`0xc9400000`, larger than the heap region). The SDK sizes the heap as total − used memory, and the host reported "used" as heap + 64 MB, which covers ordinary modules but not Dread's 1.8 GB bss, which the Switch counts as used. Used memory now adds the bss of modules in the front-patch layout (`big_bss_bytes` in `hle_svc.c`); the game then asks for 1.38 GB. +- **The submit-thread autorelease leak was first seen here:** GPU allocated climbing while the residency set, `shared textures` and the upload counts stay flat, because an objc call on the guest submit thread outside `g_sub_pool` leaks what it autoreleases ([05, Memory growth](../../../docs/agents/05-debugging.md#memory-growth); Breath of the Wild's issue #9 had the same cause in `defer_drain`). + +## Rewriter + +- **Resolved: a `B()` range assertion in `patch_nso.py` (`site_plain`) for `main` only.** With the 1.8 GB bss, a patch area appended after the image starts 1.9 GB from the code, and no `B` (±128 MB) reaches it. When an appended patch area would start beyond 112 MB, the rewriter now puts it 32 MB before the module base (negative `PATCH_OFF`); `patched.bin` then holds only text + ro + data (`FILE_IMAGE_LEN`) followed by the patch bytes, and the loader maps the bss and the key page as zero pages (`IMAGE_SIZE`). +- The spin-wait search with the lock, test and store rules finds one more loop in this game than the earlier search ([development.md, Power](../../../docs/development.md#power)). + +## Movies + +The game plays VP9 WebM movies through the SDK's own demuxer and a decoder in native-texture mode. + +- **Current:** `movie::Extractor` (libwebm in `subsdk0`) runs natively. **Resolved:** the host used to force `SetDataSource` to fail (Engage's Unity demuxes by itself), so the game crashed on a null right after the failed open. +- **Current:** the decoder is created with output format 3 and mode 2 (`DecoderMode_NativeTexture`). The client's `movie::Buffer` carries the `NVNtexture` object itself as its data, 8 bytes of capacity (an `RGBX8_SRGB` texture embedded in the game's texture pool entry, `main+0x226c98`), plus `SetNvnDev` / `SetNvnSync`. The host converts the VP9 picture to RGBA (BT.709 / BT.601 and range from the stream), writes it into the texture's block-linear storage like `WriteTexels` and marks the sync as signalled (`nvn_host_write_texture` / `nvn_host_signal_sync` in `hle_nvn.c`); the replay layer re-uploads the dirtied pages. +- **Resolved:** the game calls the complete-object destructor `movie::Decoder::~Decoder` (`D1`), which the host now takes over too; the SDK's own one destroyed the host object as a `DecoderImpl`. +- **Current:** the game's `RegisterClientEvents` struct is overwritten by its second movie. The host first copied the four event pointers; it now keeps the pointer and reads the entry each time it signals, as the SDK does (`md_event` in `hle_movie.c`), and when the client's entry is not a readable, 8-byte aligned pointer it falls back to the entry's value at registration (this game's case). +- The game ends a movie with `Flush`, `Flush`, `Stop`, destroy, and calls `Start` after a `Flush` when it loops. As with MediaCodec, `Flush` announces nothing, and no input or output index is handed out while the decoder is stopped. The "input available" after `Start` is signalled about 2 ms later from a guest thread, as the SDK's codec thread would; pending signals are dropped at `Stop`, `Flush` and destruction. +- **Resolved: crashes around movie changes in the game's own player threads, about one run in two** (`SignalEvent` → `MultiWaitImpl::NotifyAndWakeupThread` on a null MultiWait at address 0x20; `WaitAny` walking freed holders; a worker reading a freed object at `main+0x225338`). The movie player's main thread (priority 44, core 0) posts "link / unlink these holders" requests into the dispatcher thread's request ring (`main+0x222cc8`) and leaves messages for a worker thread (both priority 30, only core 0), then frees the object and builds the next one in the same memory without waiting. On hardware the two threads preempt the main thread as soon as it signals them, so their work is done first; on the host all threads run in parallel. Two fixes, both needed (with only the second, 3 of 6 runs still crashed; with both, 12 of 12 were clean): + - observation gates after each posted request (OBS 19 after the link request, loop at `main+0x224728`; OBS 20 after the unlink request, loop at `main+0x224868`; both on the `add x22, x22, #8` after the `bl`, `x19` = the request ring), which wait until the ring is empty (`movie_ring_wait` in `hle_movie.c`). Gates may now sit right after a `bl` as well as at function entries; + - the same-core preemption approximation, `core_preempt` (on only for this game: Mario's loader threads would wait for busy decompression threads, and its first frame moved from 0.6 s to 2.9 s). + +## Frame rate + +- **Current: cutscenes at 30 fps, gameplay at 60, as on the Switch.** The game keeps present interval 1 throughout. In a cutscene its main loop spins in 1 ms steps until the cutscene's audio clock advances, then renders a frame; that clock moves with a 30 Hz sound thread. +- **Resolved: cutscenes at 36 ms per frame instead of 33.3** (about 27 fps; gameplay was fine at 60). The sound thread (`main+0xf95b0`) reads a millisecond clock, updates, then sleeps "33 ms minus this round's work"; macOS timer slack (`nanosleep` and `mach_wait_until` overshoot by about 10% at any QoS: 33 ms → 36.4 ms, 5 ms → 6.1 ms, 1 ms → 1.26 ms) made its period 36.9 ms. The cutscene loop (`main+0x119a3d0`) renders a frame whenever the audio play position, which that thread refreshes, moves (played samples ÷ the sample rate of the cutscene's stream). `SleepThread` now sleeps three quarters of the remaining time per step until within 20 µs (`sleep_precise` in `hle_thread.c`): cutscenes at 33.3 ms, the sound thread at 33.2 ms. + +## Rendering + +- **Resolved: scenes and characters black, only foliage, light shafts and the HUD drawn.** NVN's `BindProgram(program, stageBits)` binds per stage, and a null program unbinds just those stages. The depth prepass calls `BindProgram(program, vertex | fragment)`, then `BindProgram(null, fragment)`, and draws depth with the vertex shader alone. The host replaced its single bound program on every bind, so a null bind meant "no program bound", and all of the prepass's ~140 draws per frame were dropped. The depth buffer stayed empty, and the G-buffer passes, which test depth `EQUAL` without writing, passed no pixel. The replay now tracks the program per stage (`bind_stages` in `nvn_mtl.m`) and builds a composite program when the stages come from different programs or the fragment stage is unbound (a vertex-only pipeline for the prepass). The combinations the game uses: (program, 0x3), (null, 0x2), (null, 0x7). Found through the pass dumps' new `Z` lines for dropped draws: 144 `Z … draw skipped: no program bound` lines in a frame whose G-buffer pass had draws but stayed black. +- **Resolved: `WriteTexels` one texel at a time.** The game's set-pixel helper (`main+0x160e80`) writes a 1×1 region per call (probably the map / minimap mask). The host used to decode the whole level, patch it and encode it again for every call; after a few minutes of play the main thread spent whole frames there (frames stopped at about 175 s). Non-3D regions are now written in place by block-linear address (`write_region_bl` in `hle_nvn.c`); `t_nvn` checks 15 regions in RGBA8, BC1 and a two-layer RGBA16F array, from 1×1 to the whole level, byte for byte against the old whole-level path. +- **Inline uniform updates:** the cutscene frames have about 180 passes with one draw each, because the game updates uniforms inline (`CommandBufferUpdateUniformBuffer`) between draws and every copy or update ended the host's render pass (about 5 ms of submit time per frame on the Mac). An inline uniform update inside a pass no longer ends it (the game ended about 246 passes a frame this way, `DM_NVN_UB_DEFER=0` restores the old behaviour); its attachment loads and stores per frame stayed unchanged, as its updates already covered what the draws read ([development.md, Power](../../../docs/development.md#power)). +- Texture handles use Mario's layout, `0x20 + 8 × slot`: the `TEX` / `TEXS` handle immediates in its shader files' own SASS text are words 0x8–0x22 in steps of 2. Its full build leaves no unresolved bindings and no unfillable driver-bank reads; the other binding rows are not measured separately. +- Depth mode: the game keeps GL's (near = −w). +- It logged `depth-stencil clear with a scissor or partial stencil mask, cleared whole` once while loading on its smoke route. + +## Services + +- **Resolved, SDK 11 names:** `fs::CommitSaveData(mount)` is the older name of `fs::Commit`; `fs::DeleteDirectory` (an empty directory) was missing too. +- **Resolved: abort in `nn::sm::Initialize` under `nn::oe::FinishStartupLogo`**, right after the first frames: it reaches the applet proxy through `am::GetApplicationFunctions`. The host has no startup logo: a no-op hook (`hle_oe.c`). +- **Resolved: abort in `nn::sm::Initialize` under `nn::hid::VibrationNode::Update`.** The SDK's vibration graph flushes queued values with `hid::detail::SendVibrationValues`, which opens the hid service; the game sets handles on its `VibrationTarget`s even when the host reports no vibration device. The flush calls it through the PLT, so an `HLE_FORCE` on that symbol catches it and returns success (`hle_hid.c`). + +## Audio + +- **Resolved: an Atk task aborted in a `switch` default (`UnexpectedDefaultImpl`, then `AbortImpl`) while a stream sound loaded** (`main+0x1225a8c`). The stream path was `rom:/system/snd/../../sounds/streams/…`, and the host answered "not found" for any `..` component. Horizon's path normalizer resolves `.` and `..` lexically and fails only above the mount root; `host_path_ex` now does the same (`norm_rest` in `hle_fs.c`). + +## Files and shaders + +- Every shader is in `system/shd/*.bshdat` (MSHD containers; nothing in `main` or the `.pkg` packs), which is also the profile's `romfs_marker` (`system/shd`). Each stage is a control section (its length at control +0x20, at least 0x880 bytes, sometimes followed by a small compiler-option table such as `fastmath true`), then the code area at the next code header. Every file holds vertex / pixel stages in alternating order (1, 2 or 4 programs) and is paired two by two. The control section carries NV assembly text after its first 0x880 bytes, and the SASS disassembly follows each code area, so the binding layout can be read straight from the files. +- 478 files, 1,342 stages, 1,010 unique keys (359 vertex, 651 pixel), 659 pairs. + +## Mac routes + +- One `DM_KEYS` switch runs through the title, a new game and the story recap ([05, Scripted input and thread scheduling](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling)). The key string itself is not in the docs. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Patch areas before the module base** for modules with a giant bss (negative `PATCH_OFF`, `FILE_IMAGE_LEN`, `IMAGE_SIZE`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **Used memory counts the bss of front-patch modules** (`big_bss_bytes`): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **`sleep_precise`** against macOS timer slack, **same-core preemption** (`core_preempt`, `svc_thread` / `dm_yield_to_core_peers`, `DM_PREEMPT`) and **observation gates right after a `bl`**: [03, Threads](../../../docs/agents/03-bringup.md#threads). +- **Native-texture movies** (`nvn_host_write_texture`, `nvn_host_signal_sync`), the `D1` destructor, `md_event`, the SDK 11 names, `FinishStartupLogo`, `SendVibrationValues` and lexical `..` (`norm_rest`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Per-stage program binding** (`bind_stages`, composite programs, `Z` lines in pass dumps), **in-place `WriteTexels`** (`write_region_bl`) and the **MSHD reader**: [04, Seen in Metroid Dread](../../../docs/agents/04-graphics.md#seen-in-metroid-dread) and [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **The submit-thread autorelease leak** (`g_sub_pool`): [05, Memory growth](../../../docs/agents/05-debugging.md#memory-growth). +- **Inline uniform updates inside a pass** (`DM_NVN_UB_DEFER`): [development.md, Power](../../../docs/development.md#power). + +## Open + +- [04, Seen in Metroid Dread](../../../docs/agents/04-graphics.md#seen-in-metroid-dread) still says every copy or update ends the host's render pass (about 180 one-draw passes in cutscenes); [development.md, Power](../../../docs/development.md#power) says an inline uniform update no longer does (about 246 passes a frame before). The cutscene pass count with the deferred update is not given. +- `game.tsv` opens with "Bring-up has just started: the memory values are Mario's until measured", while its memory section gives measured values. +- [dm/games/README.md, Adding a game](../README.md#adding-a-game) gives `0x90000000` as this game's heap region in the guesser's 4 GB layout; in `game.tsv` `0x90000000` is the whole block (`mem_reserve`) and the heap region is 1.5 GB. +- Only the merged 2.1.0 copy is extracted; the base game's own build IDs are not in the profile. diff --git a/dm/games/010099C022B96000/README.md b/dm/games/010099C022B96000/README.md new file mode 100644 index 0000000..fcf84c6 --- /dev/null +++ b/dm/games/010099C022B96000/README.md @@ -0,0 +1,85 @@ +# Super Mario Galaxy (`010099C022B96000`) + +Update 1.3.1 (v327680) over the base game, SDK 18.3.5. The Wii game (codename Comet) on NERD's Hagi layer, on **Vulkan**. The PowerPC code is statically recompiled into `main` (`hagi.config`: `Cpu` `Jit false`, `Statifier true`), so there is no JIT. Hagi stands in for the Wii hardware: GX on Vulkan, the DSP with its ROMs in `data/dsp`, the disc from the RomFS, HD texture packs in `data/texture_replace` and `data/ondemand_texture_replace`. Modules `main`, `subsdk0` (NintendoSDK_movie, for the WebM movies in `data/video_replace`) and `sdk`; `subsdk1` (GLSLC), `subsdk2` (`nn::pcm`) and `subsdk3` / `subsdk4` (NVIDIA's OpenGL and Vulkan drivers) are not loaded. Its `subsdk0` and `sdk` builds are also Super Mario Galaxy 2's ([0100FD8022DAA000](../0100FD8022DAA000/README.md)) and Monster Train 2's ([010051701FB46000](../010051701FB46000/README.md)). Plays on the iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `op_mode handheld` | [`game.tsv`](game.tsv) | the pointer (star bits, menus, the file select) is the touch screen in handheld mode; docked it is the Joy-Con gyro, which the host does not have. See [Input](#input) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout | +| `modules main subsdk0 sdk` | `game.tsv` | `subsdk1`–`subsdk4` are replaced by the host's Vulkan layer | +| 1 GB block: `mem_reserve 0x40000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the game never calls `SetHeapSize`; its memory is `main`'s bss. See [Memory](#memory) | +| `vk_capture.tsv` | profile | the keys of the SPIR-V modules Hagi generates at run time, which the NSP script captures; see [Shaders](#shaders) | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `6075151627016eef…` | needed by Galaxy 2's movie allocator on the same `sdk` build; the cut applies to every module of that build | + +## Memory + +- `main`'s bss is about 2.9 GB (`0xb964d0e8`, the emulated memory). The NPDM declares 8 MB of system resource. +- Measured (`DM_SVC=1`, Mac, to the Star Festival): no `SetHeapSize`, one alias page mapped during SDK initialization; the game's memory is `main`'s bss (charged 443 MB), not the block (7 MB charged). So the block is 1 GB = 256 MB in front + 256 MB alias region + 256 MB stack region + 256 MB heap region; with `main`'s 3 GB image that is about 4 GB of address space. +- Footprint on the Mac 1.9 GB, 1.1 GB of it Metal. +- Galaxy allocates the movie player's block from its main allocator first; Galaxy 2, on the same SDK, initializes that allocator with virtual address memory and aborted at boot without the node area cut. +- The provisional-profile guesser gives a game whose `main` never sets a heap this 1 GB block, and the Hagi preset (`hagi.config`) gets `op_mode handheld` and `pad_layout full` ([dm/games/README.md, Adding a game](../README.md#adding-a-game)). + +## Vulkan + +99 `vk*` imports through Hades' layer (`hle_vk.m`, [0100535012974000](../0100535012974000/README.md)), which Galaxy extended. Each addition was found from a red line or a screenshot on the route to the Star Festival: + +- **Push constants:** SPIRV-Cross binds the block at `buffer(0)`, where the first renumbered uniform buffer goes; the translator moves it to the next free buffer slot (`p:0.0=` in the table) and the replay sets the recorded bytes there (`setVertexBytes` / `setFragmentBytes`, the pipeline layout's range). +- **Descriptor arrays:** the vertex shaders read the Wii's transform memory as arrays of uniform buffers (4 position-matrix blocks, 2 normal, 2 dual-texture) and the fragment shaders 8 combined image samplers. SPIRV-Cross gives an array n consecutive slots, so the table says `slot*n` and the replay binds element k at slot + k. +- **Dual-source blending from location 1:** the GX shaders write the second blend source as an ordinary output at location 1 (no `Index` decoration), and pipelines blend with `SRC1` factors; NVIDIA's hardware feeds MRT 1 to source 1. Metal refuses the pipeline ("Fragment shader does not write to render target color(0), index(1)"). `vk_shaders.py` builds a variant `vk__d` with that output at `[[color(0), index(1)]]`, which the host takes for pipelines whose blend reads source 1. +- **GX vertex formats:** positions and colours arrive as integers read as unnormalized floats (`R16G16B16_SSCALED`, `R8G8B8A8_USCALED`, …) and in three-component formats; Metal's integer vertex formats convert to the shader's float type. A buffer layout no mapped attribute uses is left out: Metal's validation aborts on it. +- **Viewport and scissor persist across render passes** within a command buffer, as Vulkan says; the layer used to reset them at every pass. In handheld mode the game sets a 1280x720 viewport once and draws through several passes into its 1920x1080 swapchain, so its menus came out 1.5x too big (the file-creation dialog cut off at the right edge). +- **Copies inside a render pass:** the game copies from its EFB with `vkCmdCopyImage` between `vkCmdBeginRenderPass` and `vkCmdEndRenderPass`, which NVIDIA's driver runs. Render passes now start with deferred store actions; a copy stores every attachment and ends the encoder, the next draw resumes the pass with load actions, and the pass's own store actions are set when it really ends. Before, the rest of the pass was dropped (`draw outside a render pass`). +- **Swapchain:** the game asks for `preTransform` INHERIT and asserts (a stripped trap, `main+0x4d26e0`) when the surface does not list it. In handheld mode it keeps a 1920x1080 swapchain, draws into the top-left 1280x720 and calls `vi::SetLayerCrop` every frame; the host keeps that crop (`hle_vi.c`) and the Vulkan present shows only that rectangle. +- **Linear images:** staging images (`VK_IMAGE_TILING_LINEAR`, filled by the CPU and copied with `vkCmdCopyImage`) and one sampled 4x4 image alias their memory as on the Switch: the texture is made over the bound `MTLBuffer` with Metal's linear row alignment, which `vkGetImageSubresourceLayout` reports. +- **Queries:** two timestamp pools and a 3,072-entry occlusion pool. Timestamps are their submit's completion time; occlusion queries count into a per-submit visibility buffer, copied to the pool when the submit completes. +- Also added for Galaxy: pipeline caches, `vkCmdDraw`, image copies and read-backs. +- **Not translated:** two geometry shaders (GX points and lines expanded to quads); those draws come out as one-pixel points and lines. GX's sampler LOD bias has no Metal sampler equivalent (red line). + +## Shaders + +- Hagi generates its GX shaders as SPIR-V at run time. While it shows "Initializing system..." it creates 3,089 modules (the 56 built into `main` among them) and 4,535 pipelines from `data/database/pip/pipelineDB.pip`, the same on every boot (about 15 s on the Mac). +- The RomFS has no SPIR-V file, so the NSP script's `Vulkan shader capture` stage, with the profile's key list `vk_capture.tsv`, boots the game in the Mac host with `DM_VK_CAPTURE=/vk_capture` until every listed key is there (under a minute; it gives up after 120 s), then `vk_shaders.py` translates the RomFS and the capture (it reads capture folders like a RomFS). +- Its SPIR-V puts `ArrayStride` on the struct member (`OpMemberDecorate`) instead of the array type, which NVIDIA's driver accepts and SPIRV-Cross refuses ("Struct member does not have ArrayStride set"): `fix_strides` copies it onto the type before translating. +- `romfs_marker` is `hagi.config`. + +## CPU + +- **Resolved: a core at 100% in `SleepThread`.** Hagi polls with `SleepThread(100 ns)` (`main+0x517c`): 1.4 billion calls in 200 s, and the render thread nearly as busy. `sleep_precise` returned at once for anything under 20 µs (its nanosleep would overshoot), so such a loop never slept. Sleeps under 20 µs now take the yield path and its back-off (plain yields for the first 2 ms of a run, then short sleeps): the two threads went from 199 s and 171 s of CPU per 200 s to 5 s and 19 s per 120 s, at the same frame rate. + +## Input + +- Docked, Galaxy's pointer is the Joy-Con gyro, which the host does not have; handheld, it is the touch screen. `op_mode handheld` starts it in handheld mode (720p). +- On the iPhone, touches no gamepad control takes go to the game's touch screen: in the 3D layout, those outside the right third of the screen, which is the right stick; all of them while a controller is connected. On the Mac the mouse is a finger, and `DM_KEYS` taps with `Tx` (normalized picture coordinates). + +## Services + +- **Resolved: `sm::Initialize` aborted right after `ConnectToNamedPort` (SVC 0x1F) and `SendSyncRequest` (0x21).** An SDK call the host did not hook opened its service: `nn::hid::InitializeTouchScreen(int, int)`, the SDK 18 overload of the hooked `InitializeTouchScreen()`. +- Focus handling mode 3 (no focus notifications; the system suspends the game out of focus). The host queues message 15 only under modes 1 and 2 ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk), the Galaxy 2 entry). + +## Damaged NSP + +- The base NSP in hand was damaged: its program NCA hashed differently from its name, with 12 MB runs of zeros near its end (147 MB of damaged RomFS: the two WebM prologue movies, which the storybook prologue does not use), while its update and the small NCAs matched their names. It was packed with `--allow-damaged` ([01, NSP → NCA → ExeFS / RomFS](../../../docs/agents/01-extract.md#nsp--nca--exefs--romfs)). + +## Mac routes + +- From a fresh save, `DM_KEYS="f1500:A,f1950:T0.373x0.70,f2300:A,f2700:A,f3200:A,f3600:LRight,f3800:A,f4200:T0.247x0.426,f4500:A,f4800:LRight,f5000:A,f5400:T0.659x0.366,f5800:A,f6200:LRight,f6400:A,f7000:A,f8000:A,f9000:A,f10000:A,f11000:A"` creates file 1 (Mario icon, Regular Mode) and plays the storybook prologue into the Star Festival (about frame 12,000). +- Screenshots of the handheld picture need `DM_NVN_SHOT_OUT=1280x720` (the raw swapchain image is 1920x1080 with the picture in its top-left 1280x720). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Vulkan layer additions** (push constants, descriptor arrays, dual-source blending `vk__d`, GX vertex formats, viewport and scissor across passes, copies inside a pass, INHERIT transform and the vi layer crop, linear images, queries, `fix_strides`): [04, Seen in Super Mario Galaxy (Vulkan)](../../../docs/agents/04-graphics.md#seen-in-super-mario-galaxy-vulkan). +- **SPIR-V captured at run time** (`DM_VK_CAPTURE`, `vk_capture.tsv`, the NSP script's capture stage): [development.md, The packing script in detail](../../../docs/development.md#the-packing-script-in-detail) and [dm/games/README.md](../README.md#what-the-game-files-decide-at-every-build). +- **Sleeps under 20 µs take the yield back-off** (`sleep_precise`, `hle_thread.c`): [03, Threads](../../../docs/agents/03-bringup.md#threads). +- **SDK 18's `InitializeTouchScreen(int, int)`** hooked: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **`op_mode`** (the operation mode at start, `DM_OPMODE`): [dm/games/README.md, Keys](../README.md#keys). + +## Open + +- GX points and lines (the geometry shaders) and sampler LOD bias are not done. The player README lists it as a known issue: effects drawn as points or lines come out one pixel wide. +- The capture timing differs between sources: the game notes say the modules are created in about 15 s on the Mac (the capture "under a minute"); [development.md, The packing script in detail](../../../docs/development.md#the-packing-script-in-detail) says 3,089 keys about 7 s after launch. +- The `min` / `max` / `clamp` NaN fix (`precise::`, GitHub issue 72) was found on Galaxy 2's GX lighting; its notes have the detail ([0100FD8022DAA000](../0100FD8022DAA000/README.md)). Not stated whether this game showed it. +- `game.tsv` says values marked "Mario's" are not measured for this game; none of its rows is marked so. diff --git a/dm/games/01009BF0072D4000/README.md b/dm/games/01009BF0072D4000/README.md new file mode 100644 index 0000000..65b3da8 --- /dev/null +++ b/dm/games/01009BF0072D4000/README.md @@ -0,0 +1,49 @@ +# Captain Toad: Treasure Tracker (`01009BF0072D4000`) + +Update 1.4.2 (v393216) over the base game, SDK 8.2.0. Nintendo EPD Tokyo's engine (the `al` library over sead / agl, as Super Mario 3D World and Super Mario Odyssey; NintendoWare G3d / Ui2d / Vfx / Atk), on NVN. Modules `main` and `sdk`; the `sdk` is the build Link's Awakening 1.1.0 runs ([01006BB00C6F0000](../01006BB00C6F0000/README.md)). `main` exports its symbols (48,804 defined), so `sdis.py --find` names most functions. Brought up with no host change; plays on the Mac and on the iPhone. The README lists its IPA at about 2 GB. + +On the Mac: the opening course, the cutscene that follows it and the title menu; the first save, the Episode 1 book and Plucky Pass Beginnings; the Special Episode's book (add-on content 1). 60 fps with no red line. Three minutes in a course: about 950 MB resident and 690 MB of Metal allocations, flat. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `op_mode handheld` | [`game.tsv`](game.tsv) | docked, the game's pointer (touch blocks, stunning enemies) is the Joy-Con gyro, which the host does not have; handheld, it is the touch screen, which the iPhone front end feeds with the touches no gamepad control takes. Handheld also renders at 720p | +| `pad_layout full` | `game.tsv` | the virtual gamepad starts in the 3D layout: the right stick turns the camera | +| `feedback_snap 54f58a7cab608766` | `game.tsv` | agl's post-effect program samples its own attachment: 16x16 magenta blocks on the Mac too; see [Rendering](#rendering) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource in the NPDM: the game sets a 3.00 GB heap (pool − 64 MB). `main`'s bss is only 1.4 MB. 13 Sentinels' partition, as the other EPD games without virtual address memory | + +No `patches.py`, no `dm/tools/sdk_patches.py` cut for its `sdk` build (`3ae34e68…`). + +## Rendering + +- **Current: same-pass feedback on the Mac too.** agl's post-effect program 987 (`54f58a7cab608766`, `agl_technique_pfx.sharcb`, the same program as 3D World's `66a71a3081b91b9a` in another build) writes the 1920x1080 RGB10A2 image as an attachment while sampling it. In 3D World the 16x16 magenta blocks showed only on the iPhone; here a Mac (Apple M6) showed one in about half the frames of the opening course, moving with the falling leaves. `DM_NVN_FB_SNAP=all` cleared every frame and named this one shader, now in `feedback_snap`. +- Its opening course renders with agl's transient targets: the storage-sharing table keeps its 26 entries and takes none over under the takeover rule (found on STEINS;GATE RE:BOOT), with the same memory and frame times as before. + +## Files and shaders + +- RomFS is almost entirely Yaz0-compressed SARC (`*.szs`). +- 7,258 shaders: bfsha (BNSH) in `ShaderData`, agl sharcb (BAHS) and layout BNSH in `SystemData`, particle effects in `EffectData` and 40 BNSH in `main`, all with Mario's binding layout (every binding decodes). The one that does not build is the fin fur geometry shader (`RenderFinFur`), as in 3D World: Metal has no geometry stage. +- `main` imports `glslcCompile` and holds agl's GLSL sources, but no module defines it, so nothing is compiled at run time. `shader_split.py` now asks for captured GLSLC outputs only when a module of the profile defines `glslcCompile`, as `profile_guess.py` does. +- Shared fonts: it records them as plain TTFs and calls `Decode` only for its BFTTF formats (SDK 8: the address is the TTF). + +## Add-on content + +- Index 1 is the Special Episode; index 2 holds a 1-byte marker file. + +## Mac routes + +- A fresh save starts in the opening course with control of Toad. `DM_KEYS="f900:LUp+LRight~2.0,f1030:LUp+LLeft~2.2,f1170:LDown+LLeft~2.8,f1345:LDown+LRight~0.4,f1375:LUp+LRight~2.0,f1500:LDown+LRight~1.5,f1595:LUp+LRight~1.4,f1685:LUp+LLeft~0.9"` walks him to the star: through the arch, up the back ramp, around the rock on the grass top and over the roof. The course clears at about frame 2,100 and the title menu comes at about 5,200, with the first save; A dismisses the title's notices. +- From that save, `DM_KEYS="f600:A,f1000:LRight~0.3,f1300:A"` starts Plucky Pass Beginnings (play from about frame 1,900). +- How the route was found: screenshots alone took a dozen runs and still lost Toad behind the scenery, and turning the camera mid-route made the same input land differently each run. A probe reads the player's position every frame, `DM_PROBE=main:1fb2e0@*x0+48+8` (`PlayerActor::control`, the actor's pose at +0x48, the position 8 bytes into it, as `al::getTrans` reads it: x, y (height), z as three floats); the guest heap is laid out the same in every run, so the actor is at the same address. The stage map (`StageData/.szs`, a BYML) lists every actor with its `Translate`, including the goal and the rail the guide character walks. Stick directions are camera-relative: on the default camera two directions together (`LUp+LRight`) move along one world axis. Holding against walls, which stop the player at the same place in every run, made the route reproduce in 5 of 5 runs. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **GLSLC captures only when a module defines `glslcCompile`** (`shader_split.py`, as `profile_guess.py`): [04, Seen in Captain Toad](../../../docs/agents/04-graphics.md#seen-in-captain-toad-treasure-tracker). +- **Routes planned from a position probe and the stage map**: [05, Scripted input and thread scheduling](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling). + +## Open + +- The fin fur geometry shader does not build. diff --git a/dm/games/0100A00019DE0000/README.md b/dm/games/0100A00019DE0000/README.md new file mode 100644 index 0000000..4731252 --- /dev/null +++ b/dm/games/0100A00019DE0000/README.md @@ -0,0 +1,48 @@ +# Hades II (`0100A00019DE0000`) + +Base game v0 (the NACP's display version is 0.0.126424), SDK 19.3.9. Supergiant Games' engine on **NVN**, unlike Hades ([0100535012974000](../0100535012974000/README.md)), which renders with Vulkan. The ExeFS holds only `main` and `sdk`; this `sdk` is byte-identical to FINAL FANTASY TACTICS - The Ivalice Chronicles' ([010038B015560000](../010038B015560000/README.md)). The NSP script's provisional profile booted it on the Mac with no host change: the title, a new save, the opening and Erebus at 60 fps. Plays on the iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `mem_used 0x6c00000` | [`game.tsv`](game.tsv) | `main` has an 82 MB bss: the rewritten modules take more than the default 64 MB of used memory, so `SetHeapSize(pool − 64 MB)` would not fit; see [Memory](#memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource; the 3.25 GB heap region holds the 3.10 GB heap the game asks for | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the right stick aims (the weapons with `ManualAiming` in `Game/Weapons/PlayerWeapons.sjson`) | +| `shader_groups /Shaders/=shaders` | `game.tsv` | the shader split's group for `Shaders/NVN/bin` | + +## Memory + +- No system resource (META +0x14 = 0): the game uses `SetHeapSize` and asks for 3.10 GB (pool − used memory), which the 4 GB layout's 3.25 GB heap region holds. +- `main` has an 82 MB bss, so the profile's `mem_used` is 108 MB (`0x6c00000`). +- Resident memory levels off at about 3.4 GB in Erebus on the Mac. + +## Rendering + +- **Current: textures at fixed, sparse slots.** Its 88 shaders bind combined textures at slots 0, 1, 3, 6, 8, 11, 15, 21, 25, 26, 30, 31 and 34. Slot 34 (`SceneFade.frag`, c2 0x130; `fp_t_tcb_4C`: c2 word 0x4C = 0x20 + 8 × 34) lies past the driver's 32 combined slots, inside the image table at 0x120. With a 32-slot table, `dmsh_build.py` reported that binding as not decoded (`shaders with a binding whose slot was not decoded 1`, which prints a red line when the shader loads). The combined table now runs up to the separate textures at 0x168 (41 slots); past slot 31 it shares its words with the image table, and the replay writes a bound texture there when the image slot is unbound (`nvn_mtl.m`). The game has no image bindings, so a bound texture there wins. The census of `fp_t_*` names in the translated libraries gives the slots in use. +- **Not a bug: two long lines through Melinoë.** While the staff's Omega attack charges, two thin white lines run from her to the screen edges, one forward and one back. They are that weapon's aim lines (`WeaponStaffSwing5`: `AimLineCountOverride = 2`, `AimLineAngleOffsetOverride = 180`, animation `AuraAimLineNoMovementStaff`). The sprite is a long arrow with white edges, a dark fill drawn emissive and its tip off-screen, and the game hides it while she moves. Traced with `DM_NVN_CBDUMP` vertex dumps and `DM_PROBE` on the aim-line update (`main+0x39ade0`); a scripted run that taps Y shows them often. + +## Services + +- **Current: offline.** Its `EOS_*` imports (Epic Online Services, `nro/EOSSDK-Switch-Shipping.nro`, loaded through `nn::ro`) stay offline, as in Hades: `ro::RegisterModuleInfo` fails (red line), so the game never loads the NRO. +- **Resolved: SDK 19's `nn::ro::LoadModule`** takes one more bool (`_ZN2nn2ro10LoadModuleEPNS0_6ModuleEPKvPvmib`), so the hook for the older form did not catch it and the SDK's own code would have run. The host hooks that form and `ro::Finalize` too, so neither reaches the SDK's `ldr:ro` client. + +## Files and shaders + +- 88 BNSH files in `Shaders/NVN/bin` (`.vert` / `.frag`; the `.shader` files list each program's pair). +- `romfs_marker` is `Audio/Switch/Music.bank`. + +## Mac routes + +- `DM_KEYS="f700:A,f900:A,f1100:A,f1300:A,f1500:A,f1800:A,f2100:A,f2400:A,f2700:A"` goes through Play, the first save slot and the opening into Erebus (control around frame 3000). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Combined texture slots past 31** (the combined table up to 0x168, 41 slots, sharing words with the image table; a bound texture written there when the image slot is unbound): [04, Find the binding layout by census](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census). +- **The SDK 19 `nn::ro::LoadModule` form** and `ro::Finalize` hooked (`hle_ro.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- `game.tsv` says values marked "Mario's" are not measured for this game; none of its rows is marked so. diff --git a/dm/games/0100A5C00D162000/README.md b/dm/games/0100A5C00D162000/README.md new file mode 100644 index 0000000..794d49b --- /dev/null +++ b/dm/games/0100A5C00D162000/README.md @@ -0,0 +1,52 @@ +# Cuphead (`0100A5C00D162000`) + +Update 1.3.7 (v655360) over the base game; its DLC (The Delicious Last Course) is mounted as add-on content 1. SDK 5.5.0, NVN API 53.313. Unity 2017.4.9f1 IL2CPP on NVN. Modules `main`, `subsdk0` (the movie module, `NintendoSDK_movie`) and `sdk`. Plays on the iPhone. On the Mac it was brought up with no host change: the title, the save slots, the opening storybook, Elder Kettle's house, Inkwell Isle One and Forest Follies at 60 fps with no red line. Unity 2017.4's serialized and `Shader` formats (`unityfs.py`, `shader_split.py`) and the rewriter's jump-table form (`patch_nso.py`) were found by armyja on this game (GitHub pull requests #1, #2). + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `mem_used 0x5400000` (84 MB) | [`game.tsv`](game.tsv) | the rewritten modules take 80 MB (`main` 59.9, `sdk` 12.7, `subsdk0` 7.3); with the default 64 MB the heap request plus the modules exceeds the pool, `SetHeapSize` fails and the game aborts at its first allocation (a 0.03 GB heap) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource in the NPDM, so no virtual address memory: 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| ZCull storage size never 0 | host, `hle_nvn.c` `zcull_size` (every game) | Unity's NVN pool allocator breaks on a zero-size block; see [Rendering](#rendering) | +| `shader_groups /Data/=data` | `game.tsv` | | + +## Memory + +- No system resource, so no virtual address memory: Unity sets a heap of the application pool − used memory (3.12 GB) in the 4 GB block and allocates from it. +- On the Mac about 3.2 GB resident and 2.7 GB of Metal allocations, flat over the house and the map. + +## Rendering + +- **Resolved: sprites broke into dark slivers or went missing in about half the runs** (a log in Forest Follies splintered for a whole attempt), **and on the iPhone the picture flickered every fourth frame.** The host reported a ZCull storage size of 0. Unity's NVN pool allocator allocates each depth target's ZCull save area from its pool; asked for a zero-size block, it gave out the offset of a live block, lost that block from its map of allocations, and later freed it with a garbage size. Buffers allocated afterwards overlapped live ones, so copies read staging bytes another copy had overwritten (every fourth frame is the upload ring's period); whether a run broke depended on the allocation order. The size is now never 0 (`zcull_size`). +- **How it was traced:** the pass dump (`DM_NVN_PASSDUMP_ALL=1`, the `Y copy_b2b` / `Y update_ub` lines) listed in command order every copy and inline update of the frame. A copy whose source an earlier GPU write of the same frame covers reads that write, not the bytes the CPU staged: the broken runs had dozens of those a frame, the clean runs none. The game's allocations overlapped, so the allocator was tracked from observation gates (`OBS` in its `patches.py`) on its free-list push and after its map insert, checking at every event that each block allocated so far was still in the game's map. + +## Files and shaders + +- 3,118 shaders (1,558 pixel, 1,560 vertex) from the player's serialized files and the AssetBundles of `StreamingAssets`, plus 4 GLSLC outputs in `main`. Every binding decodes. +- Its Unity 2017.4 layout is the one AI: THE SOMNIUM FILES ([010089B00D09C000](../010089B00D09C000/README.md)) reads with. Before Unity 2019.3 a `Shader` keeps one segment per platform, the form Cuphead's trees gave `shader_programs`. +- Its 2 `ComputeShader` objects are only in files without type trees, of hashes no layout covers; the split prints a `[RED]` line and leaves their kernels out. + +## Saves + +- The game writes a slot when it reaches the map, not when the slot is picked. + +## Mac routes + +- A fresh save through the title (L+R), a new slot, the storybook and the house to the map (at about 160 s): `DM_KEYS="20:L+R,26:A,32:A,38:A,44:A,50:A,56:A*20/3,120:LRight~4,125:A,127:LRight~3,131:A*10/2,152:LLeft~3,155.5:A"`. +- From that save, across the bridge (its entry is narrow between the posts) into Forest Follies (play from about 68 s; Y shoots): `DM_KEYS="20:L+R,26:A,32:A,35:A,40:LRight~1.5,42:LDown~1.5,45:LRight~3,49:LUp~1,51:LRight~3,54.5:LLeft~0.5,55.2:LUp~0.25,55.6:LRight~2,58:LUp~0.2,58.4:LRight~0.25,58.8:LUp~0.2,59.2:LRight~0.25,59.6:LUp~0.2,60:LRight~0.25,60.4:LUp~0.2,61:A,63:A"`. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **A size the hardware never reports as 0 is never 0 on the host** (`zcull_size`): [03, NVN object sizes](../../../docs/agents/03-bringup.md#nvn-object-sizes-memory-corruption). +- **Overlapping copies in the pass dump** as the sign of a broken game allocator: [05, Intermittent rendering glitches](../../../docs/agents/05-debugging.md#intermittent-rendering-glitches). +- **`mem_used`** for rewritten modules over 64 MB, which the NSP script now sets after the rewrite: [dm/games/README.md, Keys](../README.md#keys) and [Adding a game](../README.md#adding-a-game). +- **Unity 2017.4 serialized and `Shader` formats** (`unityfs.py`, `shader_split.py`) and the rewriter's jump-table form (`patch_nso.py`), by armyja: [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container), [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). + +## Open + +- "SDK 5.5.0 (the oldest so far)" in `game.tsv` and docs/development.md is stale: DARK SOULS: REMASTERED (4.5.0) and Mario Kart 8 Deluxe (4.4.0) are older. +- The tracing text in [03](../../../docs/agents/03-bringup.md#nvn-object-sizes-memory-corruption) names observation gates "in its `patches.py`"; the profile has no `patches.py` now. +- `game.tsv` has no `sys_resource` row, so the host reports its default, 32 MB (`game.py`, `hle_svc.c`), although the NPDM declares none and the notes describe the game as having no virtual address memory. Not checked against the game's behaviour. diff --git a/dm/games/0100A6301214E000/README.md b/dm/games/0100A6301214E000/README.md new file mode 100644 index 0000000..e9ad822 --- /dev/null +++ b/dm/games/0100A6301214E000/README.md @@ -0,0 +1,87 @@ +# Fire Emblem Engage (`0100A6301214E000`) + +Update 2.0.0 (v327680) over the base game, SDK 12.3.7. Unity 2020.3.18f1 IL2CPP with Wwise, on NVN. Modules `main`, `subsdk0` (NintendoSDK_movie) and `sdk`. Plays on the iPhone; 60 fps with 2.0.0 (30 on the Switch) through the game's own 60 fps path. The README lists its IPA at about 15 GB. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `QualitySettings.set_vSyncCount` argument → 1 | [`patches.py`](patches.py) `CUTS`, `main` `8c08b971…` at `main+0x32f92d4` | the game's own 60 fps path; see [Frame rate](#frame-rate) | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `56ec2474…` (`sdk+0x346430`, `sdk+0x3464cc`) | the alias region ran out of address space minutes into play; see [Memory](#memory) | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0xA0000000` | [`game.tsv`](game.tsv) | 256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region, with a 2.5 GB pool reported. 5 GB is the total proven on a phone | +| `shader_groups` `engine` / `assets` | `game.tsv` | `data.unity3d` and `Resources/` are the engine's built-ins, `StreamingAssets/` the Addressables bundles | + +No `pad_layout`: the virtual gamepad starts in the 2D layout. `present_interval` stays `game`. + +## Frame rate + +- **Current: 60 fps from the game's own path.** The game's time class `App.GameTime` picks a vertical sync count per scene (Slow = 2, i.e. 30 fps: system, movies, demos, combat cutscenes, the hub; Normal is 1 only when `GameConfig.IsPerformance`) and hands it to Unity through `GameTime.SetVsyncCount(int)` (`QualitySettings.vSyncCount` → NVN present interval). About 96 pieces of per-frame logic (cameras, map cursor, unit actors, menu scrolling and cursors, hub camera, minigames) step by `GameTime.VsyncFixedCount` (= vSyncCount, how many 60 Hz ticks a frame advances): with a count of 2 and 60 frames on screen they all run twice as fast, with a count of 1 at 60 fps they are right. The real-time `VsyncDeltaCount` / `VsyncDeltaTime` (hub character movement, fishing, dragon ride, about 120 places) do not depend on the frame rate. +- The cut is in Unity's managed wrapper `QualitySettings.set_vSyncCount` (`main+0x32f92c0`): both paths use w19 as the argument, `mov w19,w0` → `mov w19,#1`, so every caller (`GameTime`, `QualitySettingsStack.PopImpl`, `XRSystem`) can only set 1; `get_vSyncCount` follows at 1. No code calls `SetQualityLevel`. The game then sets interval 1 itself and `game` mode gives 60 fps. +- Measured on the Mac (holding Down for 0.45 s in the save list): 60 fps with the cut and the original 30 fps both move 6 slots; without the cut, 60 fps forced by `DM_PRESENT_INTERVAL=display` reaches the end (9 or more): frame-counted logic speeds up. + +## Memory + +- The NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true: `nninitStartup` hands all free memory (application pool − 64 MB) to StandardAllocator as one address region in the alias region (1 GB aligned). Unity's low-level virtual allocator (vtable at `main+0x61ffeb0`; reserve `main+0x9aa0f4` → `nn::os::AllocateAddressRegion`) also takes regions itself, each aligned by size (2.25 MB takes 4 MB, 16.25 MB takes 20 MB): about 1.05 GB in the prologue battle. Engage never calls `SetHeapSize`; thread stacks are placed at random in the stack region. +- Measured (prologue to the battle, 12 minutes): StandardAllocator commits only 0.58 GB and Unity's own regions 0.97 GB, 1.55 GB in all. So the pool is reported as 2.5 GB (StandardAllocator about 2.44 GB, four times its use). Reserved address space peaks at 3.47 GB of 4.31 GB. +- The former 6 GB block left no margin on a phone: without the extended virtual addressing entitlement iOS allows one reservation of at most 6 GB, and Metal needs address space too (Engage allocates about 1 GB of GPU memory on the Mac). +- **Resolved: a crash a few minutes into the prologue battle (scripted run, about 357 s).** A write to `NULL + 0x1fffe0` at `main+0x9a4664`: Unity's TLSF heap allocator (`main+0x9a45d0`) added a pool and got no address region for it. The alias region was out of address space, not memory: at the crash Engage held 4.17 of the 4.375 GB left after the SDK's ~1 GB node area (StandardAllocator 3.14 GB, Unity about 1.05 GB, the rest fragments). On hardware the alias region is 64 GB. The node area cut (the two `mov w8/w9, #0x3fe00000` in the AddressRegionManager constructor, `sdk+0x346420`, → 62 MB, about 1.35 million 0x30-byte nodes; a few hundred regions are live in measurements) fixed it; a 12-minute scripted run now plays through the battle. +- **Resolved: the idle attract demo crashed after about 220 s.** Unity's particle geometry jobs wrote through a NULL index pointer on several job threads at once (`main+0xdb0ad4`, `0xdaea58`, `0xdadb5c`). The scheduler (`main+0xd91e0c`) takes its vertex and index chunks from a fallback that creates and maps a fresh buffer each time (`main+0x7b9c64`; Unity's Switch renderer always takes it, as on hardware), and from about 180 s 32-bit mesh indices also turned up inside Unity's texture objects (the replay's red lines on rejected texture objects showed their first bytes as 32-bit triangle indices; they stopped once the alias region had address space left). Same cause: past 2 MB the index buffers got no mapping. With the cut an 850 s idle run is clean; with the old SDK the same run dies at 159 s in Unity's heap allocator. To reproduce the old state: `boot.sh 245` with no input on an SDK rewritten without the cut. +- How it was measured: probe `VammManager::AllocateAddressRegion` at its success path (`ReleaseWriteLock` return: x21 = size, x22 = address) and `FreeAddressRegion` after its prologue (x1 = address) and sum live regions by caller; probe `VammManager::AllocateMemoryPages` / `FreeMemoryPages` (x1 address, x2 size) for commits. The `[mem]` log lines report the alias high-water mark and the mapped total ([03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space)). + +## Rendering + +- **Resolved: shaders reported missing at random, their dumped control sections garbage.** Unity frees a program's control section once `nvnProgramSetShaders` returns; the keys are now computed inside that call (`prog_key`). +- **Resolved: GPU memory grew with every scene change** (the idle title demo reached 5.4 GB of GPU memory and 3.6 GB resident in 850 s; it now stays at 1.2 to 1.4 GB). Unity frees memory pools on scene changes and builds new ones. The residency set still held each finalized pool's no-copy `MTLBuffer`, and the storage-sharing texture table (`g_alias`) never evicted, so old pools' render targets and pool-backed linear textures stayed alive. Finalizing a pool now evicts its entries (`mtl_alias_drop_pool`), and pool buffers are retired through deferred release. +- **Probably resolved: about one run in four stalled early** (around the opening movie or the title; no log output). Since the real thread pause for IL2CPP's GC, 12 runs (eight of 200 s through the movie and title, four of 330 to 850 s) have not stalled. A second candidate, fixed later: a race in the dirty-page tracker (`nvn_dirty.c`) could leave a page read-only while marked writable; Engage's movie decoder writes frames into texture storage from a host thread while the replay thread protects pages (`tests/t_dirty_race.c`). If a stall comes back, `sample 3` when the log stops growing. +- Unity binds only separate textures and samplers (`c2[0x168 + 8 × slot]`; texture word 0x5A, sampler 0x15A). +- Video: Unity's `Hidden/VideoDecode` samples luma as `.w` of an `R8` texture with the swizzle (0, 0, 0, R) (Alpha8) and chroma from an `RG8` texture at half size. The frames are block-linear textures written with `nvnTextureWriteTexels` every frame (Y `R8`, UV `RG8`), uploaded from guest memory and re-uploaded when dirtied. To check the movie path apart from the decoder, fill the frames with a gradient in `hle_movie.c`. +- One geometry shader (in `unity_builtin_extra`) fails to compile (`unknown main0_out`); nothing in the opening scenes uses it. +- The game sets depth mode near = 0. +- Uniform updates inside a render pass no longer end it: its battles went from about 1,850 to 360 MB of attachment loads and stores a frame (88 → 38 passes). + +## Movies + +- **Resolved: the game waited forever at its 94 s opening movie `S01F.webm`** (frames kept presenting). Unity demuxes the WebM itself and feeds VP9 packets to `movie::Decoder`, which the host implements (four events, input and output indices, one NV12 frame per packet, w × h × 1.5); libvpx decodes on one host thread per decoder. Unity reads `width`, `height`, `crop`, `nv12-y-stride`, `nv12-y-offset`, `nv12-uv-offset` and `slice-height`. When the movie ends the game goes on to its title screen. +- **Resolved:** `movie::Extractor` (libwebm in `subsdk0`) runs natively; the host used to force `SetDataSource` to fail, and the game crashed on a null right after the failed open. + +## Audio + +- Wwise drives AudioOut directly: one output opened at `main+0x177d924`, with no release event (the game polls). On the phone it opened a second one later; the Mac runs never did. Unity's FMOD also has an output (`main+0x4aec0c` / `0x4aec4c`). The log shows each as `audio #N`. +- Known issue (README): after switching away from the app and back, some sounds may stay silent until a restart. + +## Services + +- **Resolved: a long black screen at boot** (about 17 s on the Mac, 32 s on an iPhone). The game walks RomFS directories of 38,000 files, and every path component scanned its whole parent directory with `getxattr` on each entry. A per-directory name index (`dir_index_get` in `hle_fs.c`), which also speeds up every file Wwise opens, brought the first frame to 5.7 s; a lighter `ReadDirectory` to 1.8 s on the Mac (9.9 s → expected about 3 s on an iPhone), and the opening movie starts at 7 s instead of 49 s. +- **Resolved: a null read in `sdk` under `nn::applet::CreateLibraryApplet`** (title menu → DLC → `nn::ec::ShowShopAddOnContentList`). Every `Show…` overload the game imports is hooked; the shop calls act as if the player closed the shop (`hle_misc.c`). +- **Resolved: an abort in `nn::sm::Initialize` under `nn::pctl::IsFreeCommunicationAvailable`** entering the first battle map: answered "not available". +- Add-on content: its two DLC packages each hold one empty marker file (`e0`, `h0`); the content is already in update 2.0.0. `DLCManager.RefreshHasList` marks a content owned when its mount succeeded (probe `main+0x29f3c88`). +- It calls `GetSharedFontAddress` without importing `ScalableFontEngineHelper::Decode` (a Unity player), so it gets the plain TTF. +- `SignalToAddress` type 2 now subtracts 2 as Horizon does; it did not change Engage's demo crash (the address-space shortage above). +- The appearance choice opens the system keyboard (name entry): with no window the host answers cancel and the screen goes back; pass `DM_KBD=` in a scripted run. + +## Files and shaders + +- The survey found Unity 2020.3 IL2CPP on SDK 12.3.7 with NVN, an extra `subsdk0` (the movie player), 27,152 UnityFS bundles holding the shaders, and 101 `nn::hid` imports plus `irsensor`, `ro` and `swkbd`. +- Shaders: 209 `Shader` objects in 188 bundles (`StreamingAssets/aa`) plus the engine files (`data.unity3d`, `Resources/unity default resources`), read by `unityfs.py`: 9,269 stage shaders (4,634 vertex / pixel pairs and one geometry shader), no compute shaders; 9,268 compile. Unity's player has 11 GLSLC outputs of its own in `main`. +- Rewriter: 7 offset jump tables, 1 indirect call and 1 `sp` write through `x18` in `main`. +- Prewarm list: boot, the opening movie, the title, a save load and the prologue battle. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Unity's x18 shapes** (`site_br_tab`, `site_blr`, `site_sp`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **Node area cut by `sdk` build** (`dm/tools/sdk_patches.py`) and **sizing a virtual-address-memory game's reservation**: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **C++ exception unwinding through `svcQueryMemory`** (IL2CPP throws for every managed exception), **`movie::Decoder` on MediaCodec's protocol**, **applet `Show…` hooks**, **RomFS directory index** (`dir_index_get`), **add-on content from `DM_AOC_DIR`**, **program keys in `SetShaders`** (`prog_key`), **CPU-written block-linear textures** (`mtl_tex_cpu_written`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Texture objects copied into records** (`REC_SNAP` / `tex_snap`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Several audio outputs logged apart** (`audio #N`): [03, Audio](../../../docs/agents/03-bringup.md#audio). +- **Pool finalize evicting shared textures** (`mtl_alias_drop_pool`), deferred release of pool buffers, the memory line's `serial objects` / `freed but alive` / `shared textures`: [04, Seen in Engage](../../../docs/agents/04-graphics.md#seen-in-engage-unity). +- **Unity `Shader` objects** (`unityfs.py`): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container); **separate textures** (`NVN_SEPTEX_SLOT0`): [04, section 3](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census). +- **Prewarm lists**: [development.md](../../../docs/development.md#prewarm-lists). + +## Open + +- After a trip to the background some sounds may stay silent (README known issue). +- The early stalls are "probably fixed", not confirmed. +- 03's entry on `StandardAllocator::Initialize` still gives Engage's layout as "5.5 / 8 GB on the Mac"; the profile has 4.375 / 5 GB. +- `game.tsv`'s header still says the game "reaches the title screen on the Mac" and that the memory partition is "set for Mac bring-up". diff --git a/dm/games/0100A83022A22000/README.md b/dm/games/0100A83022A22000/README.md new file mode 100644 index 0000000..4091014 --- /dev/null +++ b/dm/games/0100A83022A22000/README.md @@ -0,0 +1,71 @@ +# Absolum (`0100A83022A22000`) + +Update 1.1.1 (v393216) over the base game (v0), SDK 18.3.2. FNA (XNA 4 reimplemented in C#) compiled with .NET NativeAOT (NativeAOT-NX; its own GC, regions enabled), SDL3, FNA3D's NVN driver (`FNA3D_Driver_NVN.cpp`), Wwise. Modules `main`, `subsdk2` (the movie module, for FNA3D's video player: H.264 logos) and `sdk`; NVIDIA's Vulkan and OpenGL drivers (`subsdk0`, `subsdk1`) are not loaded. Epic Online Services comes as an NRO the host does not load; the game plays offline without it. Playable on an iPhone. + +Status: brought up on the Mac: the logo movie, the licence agreement, the main menu, the campfire character select, the intro and the first level (the "Strike / Skill" tutorial and the cave after it), at 60 fps with real sound and saves; about 2.5 GB resident. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| malloc region cut 40 GB → 2.5 GB: `main+0x28bff4` `mov x0,#0xa00000000` → `mov x0,#0xa0000000` | [`patches.py`](patches.py), `main` `916f5b8d…` | malloc's StandardAllocator takes 40 GB of address space; 2.19 GB is used into the first level; see [Memory](#memory) | +| `game_env DOTNET_GCRegionRange=60000000` | [`game.tsv`](game.tsv), `hle_os.c` | the .NET GC's region range 1.5 GB instead of 5 × its hard limit (3.75 GB) | +| Node area cut 1 GB → 62 MB (`sdk+0x13be4c` / `+0x13bee0`) and 32 MB alignment for regions of 1 GB and more (`sdk+0x13c03c`) | `dm/tools/sdk_patches.py`, `sdk` build `f118ae1c…` | the 1 GB alignment lost up to 1 GB before each of the two large regions | +| 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 (never used) | + +No `pad_layout` key: the iPhone gamepad starts in the 2D layout ([README](../../../README.md)). + +## Memory + +- The NPDM declares 16 MB of system resource (virtual address memory), and `main` never calls `SetMemoryHeapSize`: everything lives in address regions of the alias region. `profile_guess.py` gives every game with an NPDM system resource the 5 GB block, and this game came out right with it ([03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space)). +- **Resolved: `nn::diag` abort at `sdk+0x12dad0` in `nninitStartup`, called from `main+0x756310`.** That is malloc's `StandardAllocator::Initialize(nullptr, 0xa00000000)`: 40 GB of address space (`main+0x28bff4`; `nninitStartup` is at `main+0x1b54d0`, the call goes through `main+0x7562d0`). +- How the needs were measured: one boot on the Mac with a 64 GB alias region (`mem_reserve 0x1030000000`, `mem_alias 0x1008000000`), probing `VammManager::AllocateAddressRegion`'s success path (`sdk+0x13bf8c`: x21 size, x22 address) with `DM_SVC=1`'s map log. The regions, from boot into the first level: + - malloc's: 2.19 GB used, 1.75 GB of it FNA3D's GPU heap, allocated whole at `FNA3D_CreateDevice` (`main+0xc03660` → `main+0x563370`); its blocks reach 332 MB from the region's start and 1.82 GB from its end, and no more after the title. The cut gives it 2.5 GB, 300 MB to spare. + - the .NET GC's region range: 5 × its hard limit (768 MB = 75% of the 1 GB its `GetPhysicalMemoryLimit`, `main+0x28bffc`, reports), 3.75 GB. The GC reads `DOTNET_GCRegionRange` through `getenv` first; the host answers `getenv` from the profile key `game_env` (`hle_os.c`; every other name goes to the SDK's own `getenv`, an empty environment), which sets 1.5 GB. The GC then uses its range from both ends, 756 MB from the start and 748 MB from the end (718 MB mapped into the first level), where it used only the first 1.14 GB of 3.75. + - a few small regions (bookkeeping, thread-local blocks), under 5 MB. +- **Resolved: the regions did not fit even then.** SDK 18's `AllocateAddressRegion` (`sdk+0x13bfd0`) aligns a region by its size: 64 KB under 2 MB, 2 MB under 4 MB, 4 MB under 32 MB, 32 MB under 1 GB, else 1 GB in absolute address (`+0x13c03c`), and places it first-fit after the regions already there. The two large regions, with a 4 KB one between them, lost up to 1 GB each to alignment, depending on where `mmap` put the guest block (measured with a 64 GB alias region: 0.6 GB before malloc's at that run's alias base, 1 GB before the GC's). A cut on that build aligns them to 32 MB as well (what the SDK gives regions from 32 MB up; nothing in the SDK or the game reads the region's alignment), beside the node-area cut. The `sdk`'s AddressRegionManager is at the offsets of Super Mario Galaxy's SDK 18.3.5 build. +- **Current:** the regions end 4.13 GB into the 4.375 GB alias region of Engage's 5 GB block. The mapped total peaks at 2.9 GB, under the hardware pool. + +## Rendering + +The effects (`.xnb`) are XNA's D3D9 effects (MOJOSHADER is linked in and parses them for the parameters). Every binding decodes with the usual layout. + +- **Resolved: the whole picture upside down** (the title logo and "Press Any Button" mirrored top to bottom). The game keeps NVN's lower-left window origin (handled as for Link's Awakening, [01006BB00C6F0000](../01006BB00C6F0000/README.md)) and, twice a frame, sets viewport 0's swizzle with `nvnCommandBufferSetViewportSwizzles` to (x, −y, z, w), which negates clip-space y: how a renderer with OpenGL heritage flips its picture. The host had it as a red-reporting stub. It is recorded now (`OP_VP_SWIZZLE`) and applied at the draw as a viewport flipped about its centre, the same mapping. Facing is decided after the swizzle on the GPU as in Metal, so the winding stays. Other swizzles (axis exchanges, negated x / z / w) give a red line and are drawn unswizzled. +- **Resolved: the logo movie as a strip of small grey copies at the top of the screen** (FNA3D's `nx_video_player.cpp`: type 1, output 3, mode 2). The texture held NV12 bytes: in RGBA8, 405 rows of 1920 pixels are exactly a 1920×1080 NV12 frame, rows 0–270 the luma at video black (16) and rows 270–405 the chroma (128). This game's MP4 demuxer hands the parameter sets to `Configure` (`csd-0` / `csd-1`), which made the H.264 decoder there without switching it to RGBA, and the first packet then found it made. Both paths now go through `md_avc_open` (`hle_movie.c`). + +## Files and shaders + +- The shaders the GPU runs are 163 GLSLC outputs in `rom:/shaders.nx.bin` (a u32 count, then per entry a u32 hash, a u32 size and one whole output), which `shader_split.py` reads as a loose file no other reader takes: 162 unique shaders, 83 pixel and 79 vertex, every vertex shader paired with the single-stage cover. Groups `fna` and `data` (`shader_groups`). +- `romfs_marker`: `data/textures`. + +## Code + +- **Resolved: `RuntimeError: 788 ldr x18, #0xd1eb0` in `patch_nso.py`.** The text segment starts with 1.7 MB of data (`main+0x0`–`+0x1b5000`, high-entropy, no FDE covers it, and no `adrp` or relocation points into it): 62% of its words do not decode, and 6,858 of the rest read as instructions that use `x18`, plus three "`svc`" words with numbers such as `#0x91e7`. The linear sweep rewrote them all. `dm/tools/nso_code.py` finds such data blocks (a stretch of at least 4 KB between the `.eh_frame` FDE ranges in which more than a quarter of the words do not decode); the sweep skips them (`DATA_BLOCKS`, logged as `data blocks left alone`), and so does `insn_census.py`, whose counts then match the rewriter's again (11,744 `x18` sites in this `main`). Code without unwind information is still swept: it decodes (zlib at `main+0x75261c`, libunwind's register restore at `main+0x28dbd4`). +- Padding halfword jump tables only to their largest entry (`half_count`) changed one to six tables in this `main`, as in a few other games' ([03, Rewriter](../../../docs/agents/03-bringup.md#rewriter)). + +## Services + +- **Resolved: SIGSEGV in `vi::CreateLayer(Layer**, Display*, int, int)`** (`sdk+0x3f9bc4`, from SDL3's Switch video driver at `main+0x239304`): the overload with the layer's size; the host had only the two-argument one. It returns the static layer (`hle_vi.c`). The survey's import table had listed every vi call as unhooked: `nso_syms.py host_hooks` read only `dm/host`, and the vi hooks are in `dm/gfx` (it reads `dm/gfx` and `dm/gfx/nvn` now). +- **AudioIn** (SDL3's audio driver lists the input devices when its audio starts): the host has no microphone. `ListAudioIns` reports none, both opens fail, and the calls that need an open AudioIn answer without touching the game's object (`hle_audio.c`). +- **Epic Online Services** in an NRO (`rom:/nro/EOSSDK-Switch-Shipping.nro`, loaded with `ro::LoadModule`; `main` imports the `EOS_*` functions). Before the host loaded NROs, the imports stayed `UNIMPL` stubs and `EOS_Platform_Create` handed back a garbage handle; the game checks it (`[EOS] Failed to create platform`) and plays offline. **Current:** the host never loads an online SDK: an NRO that exports `EOS_Platform_Create` is left out of the pack (`dm/tools/nro_collect.py` `ONLINE_SDKS`, a `refused` row in `nro.tsv`) and `ro::LoadModule` refuses it with one log line (`refused …: an online SDK (Epic Online Services), not loaded`). Offline, local saves. +- **The modules:** `main` imports EGL (SDL3's GL support, the same 13 EGL functions as Digimon Story Cyber Sleuth) and `vkGetInstanceProcAddr` (its Vulkan support), but FNA3D renders with NVN and names no GL function. NVIDIA's OpenGL and Vulkan drivers program the GPU through NvRm, which the host does not have, so they are never loaded (`profile_guess.py` `GPU_DRIVERS`); the EGL imports stay unbound stubs that nothing calls. + +## Mac routes + +- A fresh save reaches the first level at about frame 9,000 with `DM_KEYS="f1500:A,f1900:A,f2300:A,f2700:A,f3000:A,f3300:A,f3600:A,f4000:A,f4400:A,f4800:A,f5400:A*30/150"` (any button, accept the licence, New game, the character select, then A through the intro and the first dialogue); `LRight` walks, Y strikes and X uses the skill. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Data blocks at the start of the text left alone** (`nso_code.py`, `DATA_BLOCKS`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **`getenv` answered from the profile** (`game_env`, `hle_os.c`) and **the 32 MB alignment cut for large regions on SDK 18**, later taken by Paper Mario: The Thousand-Year Door's and Celeste's SDK 15.3.2 build: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **`vi::CreateLayer` with a size**, **AudioIn absent**, **online SDK NROs refused** (`ONLINE_SDKS`), **`nso_syms.py host_hooks` reading `dm/gfx`**, **NVIDIA's GPU drivers never loaded** (`GPU_DRIVERS`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **H.264 decoders made at `Configure` switched to RGBA** (`md_avc_open`): same section. +- **Viewport swizzles** (`OP_VP_SWIZZLE`): [04, Seen in Absolum](../../../docs/agents/04-graphics.md#seen-in-absolum). +- **GLSLC outputs in loose RomFS files**: [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). + +## Open + +- Shader counts differ: development.md says 162 GLSLC outputs in `shaders.nx.bin`; 04 and `dm/games/README.md` say 163 outputs (162 unique). +- Loading online SDKs: always load, refuse them (as now), or decide per profile is an open question for the maintainer ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)). diff --git a/dm/games/0100A9D01C446000/README.md b/dm/games/0100A9D01C446000/README.md new file mode 100644 index 0000000..cb9375c --- /dev/null +++ b/dm/games/0100A9D01C446000/README.md @@ -0,0 +1,87 @@ +# DRAGON QUEST VII Reimagined (`0100A9D01C446000`) + +Update 1.2.0 (v131072) over the base game, SDK 19.3.1. Square Enix / HEXA DRIVE, Unreal Engine 4.27 (PhysX 3.4, so UE4; the source paths say `UEngine/4.27-source`): IoStore containers beside one pak, the desktop renderer with forward shading, NVN, CRIWARE sound (a 6-channel AudioOut) and Sofdec movies (VP9 through the SDK's movie decoder, mode 1, NV12). Modules `main`, `subsdk0` (the movie module, which Sofdec feeds its VP9 video to) and `sdk`. Plays on the iPhone at 30 fps; 60 fps (30 on the Switch) on the Mac by the host's writes of Unreal's frame limits. + +Status: brought up on the Mac: the logos and opening movie with sound, the title, a new adventure log (name entry, settings), the opening cutscene (the well, the night scene with Maribel by the boat) into play at Pilchard Bay with the controls tutorial, and a saved log continued, at 30 fps with saves; the opening cutscene into the first free play (the hero's house) again at 60 fps (`fps_target 60`). No red line at run time apart from the geometry shaders below; about 2.6 GB resident (1.1 GB of Metal allocations) in the cutscene. Playable on an iPhone at 30 fps; 60 fps on an iPhone is not tested yet. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `fps_target 60` | [`game.tsv`](game.tsv), `hle_fpslock.c` | the host writes 60 into `t.MaxFPS` and 1 into `rhi.SyncInterval`; see [Frame rate](#frame-rate) | +| FMallocBinned3 constructor cut: `main+0x32758dc` `bl 0x291714` → `nop` | [`patches.py`](patches.py), `main` `f7ec03e4…` | its pools take 37 GB of the engine region; see [Memory](#memory) | +| Observation gate `OBS_UE_MALLOC` at `main+0x32758e4` (orig `mov x0, x19`) | `patches.py` | puts the host allocator's vtable into the new object in x19 (`hle_uemalloc.c` layout `UE4_27`) | +| `game_args -SwitchVirtualAddressSpaceSize=2048` | `game.tsv` | a 2 GB engine region instead of 62 GB | +| Node area cut 1 GB → 62 MB (`sdk+0x1423fc` / `+0x142468`) | `dm/tools/sdk_patches.py`, `sdk` build `145dde94…` | the same AddressRegionManager constructor as Octopath Traveler 0's SDK 19.3.13 build, at the same offsets | +| 3.75 GB guest block: `mem_reserve 0xF0000000`, `mem_alias 0xC8000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | alias region = node area + up to 1 GB of alignment (regions of 1 GB and more are 1 GB aligned) + the 2 GB region; 256 MB stacks, 384 MB heap region | +| `sys_resource 0x5000000` | `game.tsv` | the NPDM declares 80 MB of system resource | +| Integer output row `f555b53f62ec5196` (`fout` 0 `uint`) | [`shader_facts_manual.tsv`](shader_facts_manual.tsv) | `ShaderArchive-Global-SF_GLSL_SWITCH` shader 688 (pixel) writes integers it computes per pixel, bit-cast into its output, to an `R8Uint` target (seen at the camera settings' preview in the new-game menus); Unreal's `GLSL_SWITCH` shaders carry no output types, and Metal refuses a float output on an integer attachment | +| `pad_layout full` | `game.tsv` | a 3D game with a free camera on the right stick. It reads the controller only, so it stays docked (1920x1080) | + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). The game's config caps Unreal at 30 twice: `FrameRateLimit=30` in `DefaultGameUserSettings.ini` and `SwitchGameUserSettings.ini`, which `UEngine::SetMaxFPS` (`main+0x134d2c0`) puts in `t.MaxFPS`, and `rhi.syncinterval=2` in `SwitchDeviceProfiles.ini`, the present interval 2 it asks for while `r.VSync=1` (`BaseSwitchEngine.ini`) locks presents to vsync. As for Octopath Traveler 0 ([01005270232F2000](../01005270232F2000/README.md)), the host writes 60 and 1 into the two console variables (`hle_fpslock.c`, profile key `fps_target 60`) whenever the game sets them back, and the game then asks for present interval 1 itself. +- StevensND's 60 fps mod for the base game's `main` (build `ab0602…`; GameBanana) lifts the same two limits with patches in code: it returns at once from `UEngine::SetMaxFPS`, so `t.MaxFPS` stays 0 (no limit), and it overwrites the first two characters of the UTF-16 name `r.VSync`, so the config's `r.VSync=1` finds no variable and presents are no longer locked to `rhi.SyncInterval`. Its "Uncap" variant also patches the `sdk` for present interval 0. Neither patch touches how the game steps time. +- Unreal steps the game by the time that really passed. On the Mac, `DM_NVN_REGLUM` traces of `run/to_maribel.sh` from the same save at 30 and 60 fps (frame time p50 33.4 → 16.7 ms) match at time scale 1.00 with a constant offset (−1.75 ± 0.2 s over the last 90 s, correlation 0.998–1.000), the night scene comes out at the same resolution, and no new red line appears. +- `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) keeps the game's own 30 and saves power. + +## Memory + +- `nninitStartup` (`main+0xe89fa4`) needs virtual address memory, sets a 6 MB heap for the C library's malloc and reserves the same 62 GB region as Octopath Traveler 0's (`main+0xe8a2c0`) unless `-SwitchVirtualAddressSpaceSize=` caps it. Its FMallocBinned3 cuts 37 GB of pools out of that region (the mapped high-water mark reached +38 GB of a 64 GB alias region at boot), so a capped region would starve it. +- **Current: the allocator is the host's.** Here the allocator is not in the bss: `GCreateMalloc` (`main+0x32758b0`) mallocs 0x1c38 bytes on the first allocation, during the static constructors, constructs them (`main+0x32758dc` → `main+0x291714`) and stores `GMalloc` (`main+0x77767e0`). The constructor call is cut, and the observation gate right after it (`OBS_UE_MALLOC`, x19 = the new object) puts the host allocator's vtable in (`hle_uemalloc.c`). No code outside the allocator reads its pool base (`main+0x7744770`, read only there) or calls its methods directly: every FMemory call goes through the vtable. UE 4.27's FMalloc has one FExec slot, not four, so the slot numbers differ from UE5's (Malloc 3 … GetAllocationSize 9; `Free` is `main+0x10`, right after the 16-byte module header). +- How the allocator was found: from the commit calls (probe `nn::os::AllocateMemoryPages` past its `adrp`, `sdk+0x140f84`: every backtrace runs through the allocator's methods around `main+0x3270000`), the relative relocations that point at them (the vtable `main+0x5e4c0e8`; this build has no RTTI) and the one store of that vtable outside the destructors. +- **Current: a 2 GB region** (`game_args -SwitchVirtualAddressSpaceSize=2048`) with Octopath Traveler 0's node-area cut on this SDK build, in a 3.75 GB guest block. The Unreal heap lives on a malloc zone outside the guest block. +- On the Mac: up to +0.75 GB of the alias region with 0.5 GB mapped through the opening and the new-game menus, beside an Unreal heap of 1 GB and 0.9 GB of Metal allocations (about 2.3 GB resident; `game.tsv`); about 2.6 GB resident in play (the Unreal heap about 1.25 GB, 0.9 GB of Metal allocations). + +## Rendering + +The desktop renderer with forward shading: one RG11B10 colour pass draws every object, lit per pixel from the light grid, the volumetric lightmap and the screen-space shadow mask, with HEXA DRIVE's toon lighting on top. + +- **Resolved: the hero drawn as a dithered silhouette inside a dithered square.** Unreal's stencil shader resources are second textures on the depth buffer's storage built with `nvnTextureBuilderSetDepthStencilMode(STENCIL)`, which the host only logged, so the character occlusion mask (`edbfc7a088be545e`) read depth where it tests stencil bits. The builder's mode now goes to the texture (`tex_t.stencil`), whose sampling view is the `X32_Stencil8` one a stencil-mode texture view already got. +- **Resolved: characters and props glowing white in the opening cutscene only, about 10⁴ too bright, while walls and gameplay looked right.** The per-pixel volumetric lightmap read brick (0, 0, 0) of the brick atlas for almost every cell. Unreal merges each streamed level's lightmap into one atlas on the GPU: brick copies (`0a7ff4171bd1a82a`), then a patch of the shared 16³ `RGBA8_UINT` indirection texture (`7b958a7b21e72801`) whose bounds come from `GetDimensions` on the very `RWTexture3D` it writes. That size query is a TXQ on the image's handle, c2 `0x120` (image slot 0): the build numbered it combined texture slot 32, the slot that shares that word, and with no type in the facts table the translator read it as 2D. The host bound the empty texture slot 32, the 1×1 dummy's size passed only thread (0, 0, 0), so only that cell was written, every other cell kept size 0, and the shader fell back to the atlas origin: the first daytime level's bricks, baked about 2¹⁵ brighter (the same bricks in gameplay hold 0.5–1.4). Two general fixes: a texture binding of slot 32–39 reads the image bound in slot 0–7 when there is one, as c2 holds it (`nvn_mtl.m` `tex_slot_h`), and a query on an image handle the facts do not type is translated again with the type of that image from the first translation (`dmsh_xlate` `ImageTypesOf`). +- How the glow was found: the raw scene colour (`texraw`) put the characters at 1,000–2,000 and the walls at 0.05; a debug build of one character shader that wrote its terms in alternating 8-pixel columns isolated the volumetric lightmap ambient term; dumps of the atlas (`texlist` / `texraw`), of the uploaded level bricks (the 3D upload dump) and of the indirection texture, with the cell computed from the primitive's origin and View `VolumetricLightmapWorldToUVScale / Add` (View 220 / 221), gave the rest. +- Integer textures and images read and written through float declarations (the indirection `RGBA8_UINT` texture read by `texture3d` and bit-cast, `R16_UINT` / `R32_UINT` light grid buffers through `texture_buffer`, integer stores into a float-declared `texture3d` image): checked on an Apple GPU, all keep the integer bits, so they need no facts. +- **Unbuilt:** 62 of the 63 geometry shaders (one-pass cube shadow maps, six layers from one draw) do not build; their draws are skipped (point light shadows). + +## Files and shaders + +- Cooked packages in IoStore containers (`DOLL/Content/Paks/pakchunk0-Switch.utoc` / `.ucas`, 2.8 GB, and smaller chunks beside it; `.ucas` is the profile's `romfs_marker`); everything else in `pakchunk0-Switch.pak` (1.5 GB, 3,972 files: configs, the asset registry, fonts, CRIWARE `.awb` banks, the two shader code archives). The other `pakchunk*-Switch.pak` files are 339-byte empty paks. +- The pak is version 11 with the same 0xdd-byte footer as Octopath Traveler 0's, but the magic sits 0xcc from the end, not 0xc8 (five whole 32-byte compression method names, `Zlib` and `Oodle`); `uepak.py` takes either. The index is not encrypted. The update (v131072) is a delta over the base RomFS, so the merged extraction holds the whole game. +- Shaders: `ShaderArchive-Global-SF_GLSL_SWITCH.ushaderbytecode` (1,764 shaders) and `ShaderArchive-DOLL_Chunk0-SF_GLSL_SWITCH.ushaderbytecode` (63,185), stored in the pak: the same version 2 archives and `LSLG` records as Octopath Traveler 0's, but every shader is an LZ4 block (the first bytes are LZ4 tokens followed by `LSLG`; LZ4 is Unreal's default shader compression since 4.26), which `uepak.py` decodes itself (`lz4_shaders`: every compressed shader must decode to its raw size as an `LSLG` record, else the archive goes to the Oodle job as before). The NSP script runs no Oodle host for them. +- 62,901 unique shaders (10,114 vertex, 50,756 pixel, 1,968 compute, 63 geometry) of 64,949 in the archives. The game has no PC version, so the facts table holds only compute group sizes; every vertex shader is made to link with any pixel shader (`shader_split.py` `separable_pairs`). + +## Audio and movies + +- **Resolved: SIGSEGV at address 0x18 in `UpsamplerManager::Allocate` on the render thread.** The audio renderer runs at 32 kHz (rate 32000, 160 samples per frame, `GenerateSinkCommands` `sdk+0x225a80`). A DeviceSink below 48 kHz is upsampled by the SDK's upsampler, and `AudioRenderSystem::SetUpRenderResourceSet` allocates the `UpsamplerManager` only for the DSP (`sdk+0x21eb6c`: rate 0x7d00 and rendering device +0x48 == 0); with the host's CPU rendering the manager pointer (`AudioRenderSystem+0x150`, the render context's +0x20) is null. The host answers `Allocate` on a null manager with no upsampler, so the generator skips the upsample command (`sdk+0x225b40`), and resamples the DeviceSink's stereo downmix 3:2 itself (a 16-tap windowed sinc in three phases, `up_3_2` in `hle_audren.c`), 240 samples per 5 ms frame for the 48 kHz output. +- **Resolved: a movie in grey with vertical bands of colour, its luma sharp.** CRIWARE's Sofdec hands its VP9 video to `movie::Decoder` in mode 1, NV12 into its own buffers, and draws the planes with its shaders; the bands were the top row's chroma, every chroma row read from the first one. Sofdec reads `nv12-uv-stride` from the output format, which the host never set. The host now sets everything the SDK's `DecoderImpl::GetOutputBufferFormatFromDecoder` (SDK 19 `subsdk0+0x121f50`) does: also `nv12-uv-stride` (the Y stride), `nv12-width` and `nv12-height` (`md_format` in `hle_movie.c`). + +## Services + +- **Resolved: an abort in `sm::Initialize` under `audio::GetAudioDeviceOutputVolume`** (the game thread, `main+0xee8edc`): the volume `SetAudioDeviceOutputVolume` sets is now kept and read back (`hle_audren.c`). +- The import list against the hooks then gave the rest before they were met: `ec::FinalizeForShopServiceAccessors`, the shop response's `Cancel` and `GetErrorCode`, `ec::ShowShopSubscriptionDetails`, the Nintendo Account request's `GetAuthorizationCode` and `GetState` (both fail, as `GetIdToken`), and `kpr::GetKeyCodeMap`, which asks the settings service (`settings::detail::GetKeyCodeMap`) for the keyboard layout's map; the game zeroes a 0x1000-byte map and fetches it only once a keyboard reports keys (`main+0x1f0eb4`), and the host leaves it zero. The rest fill the game's own structures or sit behind a socket that fails offline. + +## Rewriter + +- **Resolved: `observation gate neither at a function entry nor after a bl`** for the gate on `mov x0, x19` at `main+0x32758e4`, after a cut `bl` and an `adrp`. A site is now also accepted when the straight-line code from it to the next `bl` reads none of x9–x17 before writing it (`scratch_dead`). The gate table takes the original instruction as a word value (`aa1303e0`), the cut table as bytes (`e00313aa`). +- `main` has two 23 MB stretches without unwind entries; they got through on a quieter Mac before functions were analysed in pieces, and the rewrite by the pieces rule is byte-identical to the previous `patch_nso.py`'s. It is the only `main` of 120 modules of 33 games with other `x18` sites before its first FDE (110 before `+0xb310`, none in a window or a `br x18`), and its rewrite stayed byte-identical when that text became one function. + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL` (`wait S` / `snap NAME`), and `run/to_maribel.sh ` goes from the title through a new adventure log into the opening cutscene's night scene by the boat (a save with an adventure log in the first slot): [05, Scripted input](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Unreal Engine 4.27's FMalloc layout and the allocator made on the first allocation** (`hle_uemalloc.c` layout `UE4_27`, cut + `OBS_UE_MALLOC` gate): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **LZ4 Unreal shaders decoded by `uepak.py` itself** (`lz4_shaders`): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container), [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **Stencil-mode texture builders** (`tex_t.stencil`), **texture slots 32–39 reading the bound image** (`tex_slot_h`) and **image size queries typed from the image** (`dmsh_xlate` `ImageTypesOf`): [04, Seen in DRAGON QUEST VII Reimagined](../../../docs/agents/04-graphics.md#seen-in-dragon-quest-vii-reimagined). +- **A 32 kHz audio renderer resampled 3:2 on the CPU** (`up_3_2`): [03, Audio](../../../docs/agents/03-bringup.md#audio). +- **`nv12-uv-stride`, `nv12-width`, `nv12-height` in the movie output format** (`md_format`), **`GetAudioDeviceOutputVolume`**, **`kpr::GetKeyCodeMap`** and the shop / Nintendo Account calls: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Rewriter:** gates before code that writes the scratch registers first (`scratch_dead`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **`DM_CTL` route scripts** (`run/ctl.sh`): [05, Scripted input and thread scheduling](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling). + +## Open + +- 60 fps on the iPhone: held for 3 minutes in an iPhone log of 2026-10-23 (GPU 13–15 ms mid-session, compile hitches up to 0.66 s). The same log had red lines for 7 point-light geometry shaders (missing shadows) and 1 texture type mismatch. +- 62 of the 63 geometry shaders do not build, so point light shadows are not drawn. diff --git a/dm/games/0100ABF008968000/README.md b/dm/games/0100ABF008968000/README.md new file mode 100644 index 0000000..766c594 --- /dev/null +++ b/dm/games/0100ABF008968000/README.md @@ -0,0 +1,58 @@ +# Pokémon Sword (`0100ABF008968000`) + +Update 1.3.2 (`v458752`) over the base game, SDK 7.3.2. Game Freak's engine before Trinity, on NVN, with Wwise sound (a 6-channel `AudioOut`), Pia / NEX networking and Silicon Studio's YEBIS post effects. Modules `main`, `subsdk0` (the movie module), `subsdk1` (GLSLC, which `main` imports but never calls) and `sdk`; `sdk` and `subsdk0` are the same builds as Super Mario Odyssey 1.4.1's ([0100000000010000](../0100000000010000/README.md)). Started from the provisional profile the NSP script wrote from its survey (`dm/games/profile_guess.py`). Plays on the iPhone (IPA 12.4 GB) at 30 fps, as on the Switch. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | [`game.tsv`](game.tsv) | no virtual address memory (the NPDM declares no system resource): `nninitStartup` sets a 3.13 GB heap | +| `mem_used 0x4c00000` | `game.tsv` | the rewritten modules (76 MB: `main` 25 MB of text, GLSLC 4.8 MB) are larger than the default 64 MB of used memory | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the camera is on the right stick | +| Node area cut and 32 MB alignment of large regions | `dm/tools/sdk_patches.py`, `sdk` build `fcf93a41…` | registered for DRAGON QUEST XI S on this build; this game has no virtual address memory | + +No `patches.py`. + +## Performance and memory + +- On the Mac: 30 fps as on the Switch (frame time p50 33.3 ms, p95 35–36 ms; GPU 4–9 ms a frame on an M6; CPU 42% of a core, 0.15 W), with sound. Resident memory 1.8 GB after the title, 2.4 GB after 17 minutes of new areas, 1.9 GB of Metal allocations. No red line on the route below. The prewarm list covers it (3,273 lines). + +## Services + +SDK 7 calls, one boot each; the import list against the hooks and every other game's imports named all of them beforehand (33 `nn::` imports no host hook names and no other game imports) ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)): + +- **Resolved: the Poké Ball Plus** (`hid::*Palma*`, 26 functions; the first boot crashed in `hid::detail::SetPalmaBoostMode`, a null read at `sdk+0x238eb0`, the hid service never opened). The host answers that none is connected: no npad has the Palma style, so the game never gets a connection handle, and every call on a handle fails (`hle_hid.c`). +- **Resolved: `pdm::QueryApplicationPlayStatistics`** (the second boot: a null read at `sdk+0x336ccc`). The game asks how often this user launched two other games (the Let's Go play statistics) and reads only the launch count, so each is never played (`hle_offline.c`). +- `err::ShowNetworkServiceError` is logged. +- `nvnDeviceGetWindowOriginMode` and `nvnDeviceGetDepthMode` were red stubs: the game sets neither mode and reads both. + +## Files and shaders + +- 7,699 GFPAK archives under `bin/archive` (7.9 GB of the 12.1 GB RomFS) hold the models, textures, effects and maps, beside loose message tables, battle data, Wwise banks (`bin/sound/NX64/wwise/bank/Stream.pck` is the profile's `romfs_marker`) and BNSH files. A GFPAK is `GFLXPACK`, u32 version 0x1000, u32 0, u32 file count, u32 folder count, u64 offset of the file table, u64 offset of the file hashes (FNV-1a 64 of each path; no names are stored), u64 offset of the folder table pointers; the file table holds 0x18 bytes per file {u16 level, u16 compression, u32 length, u32 stored length, u32 0, u64 data offset}, compression 0 stored, 1 zlib, 2 LZ4 (one block), 3 Oodle Kraken (pkNX). All 201,635 entries of Sword's are LZ4, so the split needs no Oodle. Inside: BNTX textures, BNSH shader archives, models (`.gfbmdl`) and animations ([01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content)). +- Shaders: 41,516. Nearly all are BNSH entries of the GFPAK archives (the models' and effects' shader archives; `shader_split.py` `gfpak_members`), beside loose BNSH files (`bin/battle/waza/model/**/*.bnsh_vsh` / `_fsh`, `bin/graphics`, `system_resource/shader`), two in `main`, and YEBIS's 3,155 (below). The provisional split, with no GFPAK reader, found 3,126. +- **Resolved: shaders in no file (YEBIS,** the post-effect library: bloom, glare, depth of field, vignette; `C:/workspace/YEBIS_draft/src/GPU/…` in `main`). After the GFPAK reader, 38 shaders the opening drew were still missing from the map (`DM_NVN_SHDUMP`): no RomFS file and no module held their code, stored or LZ4, and none was a patched copy of a split shader (the nearest ones of the same length differ in hundreds of bytes). The game does not compile them at run time either (`main` imports `glslcCompile` from `subsdk1` but never calls it; it holds no GLSL source). A temporary trace in `nvnProgramSetShaders` (the key's code and control addresses, a backtrace and the heap around them) showed control sections packed back to back with technique names such as `ps_BrightPassT1_THRESHOLD_LUMINANCE_S1` beside them. The names are in `main`, and so is the data: the only zlib stream in `main` that inflates (`main+0x1c98ed9`, 39.5 MB) is YEBIS's `.tfxo` file, a text header (the shader count, `hlsl`, `TexUtil_NVN_NX`, the technique list) and 3,155 shaders, each a control section (its own length 0x850 on SDK 7) directly followed by its code area (`tfxo_streams`, `tfxo_shaders`; 3,015 distinct, 3,112 pixel and 43 vertex, single-stage programs). They covered all 38. + +## Rendering + +- **Resolved: a G-buffer of float and integer targets** (`failed to create render pipeline … output of type float4 is not compatible with a MTLPixelFormatRGBA16Uint color attachment`, 10 pipelines as the opening's stadium loads). The scene shaders write colour 0 and 2 as floats and colour 1 from integer bits (`as_type`) into an RGBA16Uint target. Luigi's Mansion 3's integer entry point (`main0_u`, every output an integer) does not fit a pipeline with a float target as well. **Current:** such a shader gets a third entry point, `main0_m`, that returns only the outputs written from integer bits as unsigned integers (`uint_mask_m` in its reflection: colour 1 in all 336 of Sword's), taken for a pipeline whose integer attachments are exactly those (`pso_mixed_uint`). +- **Resolved: cube views of one layer** (an abort in Metal's validation as the player first walks out of the house: `newSliceRange.length(1) must be a multiple of (6)`). The environment maps are 6-layer textures (target 2D array, depth 6) sampled through views with target CUBEMAP and layers 0 + 1. As in the hardware's texture header, a cube view counts its layers in cubes; Metal counts faces. +- **Resolved: writes beside a texture** (73,232 re-uploads in 70 s from a save, 12 GB of upload staging in 8 minutes, frame times rising from 33 to 75 ms). The game keeps each texture of its models in a memory pool of its own, the pool's storage 4 KB after its own object, in the same 16 KB host page, and writes there every frame. The dirty-page tracker marks whole host pages, so every such texture counted as changed at each use; hashing showed every re-uploaded content unchanged. With the fix, the same 70 s: 0 re-uploads and 72,584 checks (`writes beside a texture only` in the `packaged textures` line). + +## Input + +- The 3D pad layout (the camera is on the right stick). + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL`, `run/step.sh [wait] ` presses and takes a screenshot (headless: a window that loses focus is throttled and its shots never come), and `run/route_out.sh ` boots a copy of `run/save_ref` (in the bedroom with the Bag) through the title and out of the front door. +- From a fresh save, A at the language (about frame 600), the photo and the name (`DM_KBD`) reach the opening. +- `run/save_hop` is a save in Hop's kitchen, before the walk to Wedgehurst: from it, down 2.5 s out of the house, left 3.5 s, up 0.5 s, left 5 s, right 0.6 s and up 8 s reach the hill above Wedgehurst, and A presses with the stick up through the station scene lead back to Hop's yard and the starters (the camera turns with each move: walk in short steps onto the court and press A facing the Pokémon). +- The route checked on the Mac: the first boot's language, photo and name choices, the opening (the Champion's broadcast, the stadium), Postwick (the house, the Bag, Hop's house), the walk to Wedgehurst and Leon's arrival at the station, the evening in Hop's yard, the starter choice (Scorbunny) and the first battle against Hop (Wooloo, then Grookey), the title screen and a save and load. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The Poké Ball Plus answered as absent** (`hle_hid.c`), **play statistics as never played** (`hle_offline.c`), `err::ShowNetworkServiceError`, `nvnDeviceGetWindowOriginMode` / `nvnDeviceGetDepthMode`: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **The GFPAK reader** (`gfpak_members`) and **`.tfxo` streams in `main`** (`tfxo_streams`, `tfxo_shaders`): [04, Seen in Pokémon Sword](../../../docs/agents/04-graphics.md#seen-in-pokémon-sword), [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **A mixed float / integer pixel shader entry point** (`fs_uint`, `main0_m`, `uint_mask_m`, `pso_mixed_uint`), **cube views counted in cubes** (`tex_by_id`) and **packaged textures whose shared edge pages are written but whose own bytes are not, protected again instead of re-uploaded** (`pkg_edges_take`, `pkg_edges_clean`; `t_nvn` 6c1): same section. diff --git a/dm/games/0100B04011742000/README.md b/dm/games/0100B04011742000/README.md new file mode 100644 index 0000000..f63f344 --- /dev/null +++ b/dm/games/0100B04011742000/README.md @@ -0,0 +1,99 @@ +# Monster Hunter Rise (`0100B04011742000`) + +Update 16.0.2 (v2228224, Sunbreak included) over the base game, SDK 11.4.3. Capcom's RE Engine on NVN. Modules `main` (314 MB, 125 MB of text), `subsdk0` (the movie library, NintendoSDK_movie 11.4.3) and `sdk`; `subsdk0` and `sdk` are the same builds as Metroid Dread's ([010093801237C000](../010093801237C000/README.md)) and 13 Sentinels' ([01003FC01670C000](../01003FC01670C000/README.md)). The base game is an XCI (cartridge dump), the update an NSP, and its 252 DLC NSPs are licences only; only the merged 16.0.2 copy is extracted. Plays on the iPhone; 60 fps (30 on the Switch) through the host's write of the engine's frame limit. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `fps_target 60` | [`game.tsv`](game.tsv), `hle_fpslock.c` (`main` build `c9a3dd77…`) | the host raises RE Engine's frame limit from 30 to 60; see [Frame rate](#frame-rate) | +| `present_interval display` | `game.tsv` | the game still asks for present interval 2; display mode ignores it. Both keys are needed for 60 | +| `tex_flush_upload on`, `tex_dirty_upload on` | `game.tsv` | RE Engine fills small textures from the CPU in place (the menu cursor's 8 × 8 BC1 texture: 32 bytes written into a CPU-visible pool, no packaged layout, no `WriteTexels`); without them the cursor highlight composited black on black and the menus looked frozen | +| `mem_used 0x14800000` (328 MB) | `game.tsv` | the loaded modules take 0x13fa4000 (`main` alone 314 MB of text + ro + data), so `SetHeapSize(pool − 64 MB − bss)` (0xc9000000) failed at boot (`[mem] SetHeapSize … + modules … > app pool`). With 328 MB the heap is 2.88 GB | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the NPDM declares no system resource, so there is no virtual address memory: 256 MB alias, 256 MB stacks, 3.25 GB heap region | +| `pad_layout full` | `game.tsv` | a 3D action game: the camera on the right stick, the item bar on the D-pad | + +## Frame rate + +- **Current:** the frame loop (`main+0x6b106c8`) sleeps until `[obj+0x430] / [obj+0x434]` sixtieths of a second have passed (`[+0x430]` = 60) and steps the game by the time that really passed (+0x41c), so a higher limit runs the game faster in frames at the same speed. The game sets the limit, a float at `[main+0x1295b3f0]+0x434` (the pointer is a global in `main`'s bss), to 30 after boot and asks for present interval 2. The host writes the profile's `fps_target` there (`hle_fpslock.c`, the address from masagrator's FPSLocker config for this build, FPSLocker-Warehouse `patches/0100B04011742000/C9A3DD7702075ECD.yaml`), and `present_interval display` ignores the interval. +- `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) gives back 30 and saves power. The Mac `DM_KEYS` routes below count frames at 30, so run them with it. +- The spin-wait search takes 166 loops in `main` (90 before its store rule; 72 of the new ones are one inlined countdown, `ldr w9,[x21,#0x24]; subs w9,w9,#1; b.ge; str w22,[x21,#0x24]; ldr; cbz`, whose reset makes the next pass leave). The game runs alike on the Mac with both rewrites from a fresh save (180 s headless from the boot each, the same host: 60 fps, frame p95 about 17.5 ms, the same red lines, no hang). Rewriting `main` takes 342 s of CPU (507 s before the x18 scan decoded in blocks) ([development.md, Power](../../../docs/development.md#power)). + +## Memory and the GPU heap + +- **The GPU heap is one 64 GB sparse virtual pool** built from separately allocated 4 MB physical pools, each mapped at a virtual offset of its own (45 at boot, over the first 180 MB). **Resolved:** `[RED] MapVirtual: non-contiguous mapping (true sparse) not implemented`. The pool now gets an address range of its own at `MemoryPoolInitialize` (reserved, inaccessible: 64 GB on the Mac, at most 1 GB on iOS), and each mapping request `vm_remap`s the physical pages over its part of the range (`vpool_*` in `hle_nvn.c`). +- **Resolved: `CRASH sig=10` in `memmove` from `pkg_upload` → `nvn_deswizzle`, at an address 32 GB into the pool.** The streamed textures (flag 0x20, packaged layout) live 32 GB into the virtual pool; the game writes their tiles on the CPU into 4 MB physical pools and then maps them, with no GPU copy. NVN maps at 4 KB and the host aliases at its 16 KB page, so a request whose offsets differ modulo 16 KB cannot be aliased: the ASTC 6x6 textures' mip tail starts 0x81000 into the texture. The replay now reads a sparse pool's textures through a 4 KB page table (`vpt_*`, `nvn_sparse_copy`) and uploads one again only when a mapping in its range changed or the CPU wrote a physical page behind it (`nvn_sparse_since`). Before that the untracked alias counted as written on every look: 32 textures, 18 MB, were uploaded again every frame, and those with unaliased tails came out magenta. Requests beyond the reserved range (iOS reserves 1 GB: the buffers stay under 0x20100000 through the opening) are kept in the table only: textures can read them, the GPU cannot address them. + +## Rendering + +Game-specific findings. The general host changes they led to are listed under [Found on this game](#found-on-this-game). + +- **Binding layout:** the game uses both texture tables at once: combined handles from `nvnCommandBufferBindTexture` at `0x20 + 8 × slot` and separate textures from `BindSeparateTexture(s)` at `0x168 + 8 × slot`, with separate samplers at 0x568, and binds slot 0 of both in the same frames (combined slot 0 beside separate slots 0–4). Separate binds are texture slots 128 + n. A census of 23,478 translated shaders' texture names: 33,813 combined (`fp_t_tcb_1E`), 29,342 separate textures with a constant sampler ID (handle type 3, `fp_t_cb65538_3041005C`: texture at c2 word 0x5C, sampler 0x104 ORed in), 8,265 separate texture + separate sampler (type 1, `fp_t_cb196610_1017815A`), 5,227 separate textures whose own handle carries the sampler (`fp_t_tcb_5A`), about 1,500 uniform-buffer handles with a constant sampler (`fp_t_cb65541_30410028`), and 121 fully dynamic handles (type 4, `fp_t2d_cb0_40000000`, not supported). +- **Subgroup operations in vertex shaders** (about 4% of them): RE Engine scalarizes uniform loads (ballot the active lanes, shuffle the first lane's value to all, take a fast path when all agree); `dmsh_build.py` rewrites them for a one-lane subgroup (`spv_single_lane`). +- **Resolved: the title screen showed only the UI.** The scene is GPU-driven: compute culling writes the draw records (stride 20) and a u32 count, then up to `max` draws (1–49 per call) are made from them with `nvnCommandBufferMultiDrawElementsIndirectCount` / `MultiDrawArraysIndirectCount`. Both were empty, and the G-buffer pass had no draws. The game draws the same records with two counts. +- **Resolved: Kamura Village frame rate on the iPhone.** The replay used to commit its work and wait for the GPU before the first such draw after each GPU write, then read the count on the CPU (`mdi_wait`): in Kamura Village that wait held the submitting thread 14 of its 20 ms a frame on an iPhone (36–54 fps). On the Mac the same scene's submit took 20 ms of wall time for 6.1 ms of work, and now 5.1 ms, all of it work (`mdx_run`). +- **Resolved: the hunter's innerwear (ASTC 6x6) came out magenta and the face black.** Textures in the sparse pool hold what the CPU wrote into physical pages before mapping them: the streamed ones and plain sampled ones with no packaged layout. The replay took the plain ones for GPU textures, so their Metal copies stayed 0. A sampled texture in a sparse pool is now uploaded like a packaged one, through the pool's page table. +- **Resolved: magenta trousers, sleeves and gloves of Hinoa and Minoto, worst in Kamura Village.** Sparse textures have a layout of their own (64 KB tiles); the game's files hold its streamed textures pre-swizzled that way, and it places each level at the offset the host reports. Laid out unpadded, their 1024×1024 ASTC 6×6 clothes (43 GOBs wide instead of 48, level 1 at 0x81000 instead of 0x90000) read 2,183 all-zero blocks in level 0 and 3,043 in level 1, which ASTC decodes as its error colour (`nvn_tex.c` `level_walk`). `DM_NVN_ZBLOCK` finds such textures. +- **Resolved: magenta on the title screen's clothes.** The translator drops TEX's LOD clamp and its residency predicate, so streamed textures were sampled at levels the game had not mapped yet, which read 0. A sparse texture is now bound as a view from its first fully mapped level (`sparse_view`). +- Bindless handles in uniform buffers written earlier in the same submission are read from the newest pending write (`pend_read`); the game queues more than the old limit of 256 writes per submission, after which every address counted as written. +- A whole-level clear of a texture made without render-target use (a 32x80 packaged one) upgrades it first (`tex_upgrade`). +- About 30 compute shaders read storage buffer addresses from the game's own uniform buffer (c3 + 0x38), marked `sbuf_ubo`. +- 218 pixel shaders keep a bindless handle in a loop variable (`c2[0x1A0] | 0x10500000`: separate texture slot 7, constant sampler 0x105); Ryujinx's bindless elimination gave up on the loop phi and the shader did not load (`no resolved slot number`) until the vendored translator followed the phis ([third_party/ryujinx/README.md](../../third_party/ryujinx/README.md)). +- Four size queries on image handles (`cp_t_tcb_48` / `_4A`) are of the kind `tex_slot_h` / `ImageTypesOf` handle ([04, Seen in DRAGON QUEST VII Reimagined](../../../docs/agents/04-graphics.md#seen-in-dragon-quest-vii-reimagined)). +- Shadow vertex inputs: 16,781 side libraries, 344 MB on iOS (21% of its shader libraries, 1.5% of its IPA); its Mac and iPhone runs so far load none. Its 8- and 16-bit integer formats feeding float inputs (red line 99, about 60,000 vertices per submission on its Mac route into the opening) still go through the CPU, since shadows cover only integer inputs. +- **Geometry shaders** (53) do not translate: SPIRV-Cross writes `unknown main0_out main0()` for the geometry stage, and Metal has none. + +## Movies + +- H.264 (type 1) into the client's NVN texture (output 3 = RGBA, mode 2), with AAC 5.1 sound (type 4, PCM16). VideoToolbox decodes to BGRA for that mode (`mavc_set_rgba`), and AAC goes through AudioToolbox's converter with an MPEG-4 ES descriptor built from the AudioSpecificConfig in `csd-0`. The game ends a stream with time −1 and the whole 4 MB input buffer as its length: that packet carries no data (decoding it gave VideoToolbox −12909 and AudioToolbox −50). It also deletes its decoders (`D0`), so the host frees the object with the game's `operator delete` after its own state. Its movies never bring parameter sets to `Configure`. + +## Services + +- **SDK 11 calls** (one boot each; the import list against the hooks and the other SDK 11 games found most of them before the first boot): `oe::IsCpuOverclockEnabled` (a null read in `sdk+0x35258`, apm's session, the first call in `nnMain`), `album::Initialize` (abort in `sm::Initialize` under `capsrv::InitializeForApplication`; saving to the album now fails, `hle_offline.c`), `oe::GetDefaultDisplayResolution` and its change event, `oe::SendServerMaintenanceOverlayNotification`, `time::LoadTimeZoneRule` and the `ToCalendarTime` overload with a rule (the host's zone is used), `time::StandardNetworkSystemClock::IsAccuracySufficient` (offline: no), `hid::GetKeyboardStates`, `fs::GetSaveDataSize` / `ExtendSaveData` and the one-argument `fs::MountCacheStorage`. `nvnDeviceGetTexelFetchHandle` returns the texture ID with sampler 0. +- **Resolved: one guest thread at about 1000 ms/s on the iPhone only, the phone hot, CPU energy 5–7 W.** `MessageSuperviseThread` (priority 32) waits on `oe::GetNotificationMessageEvent()` with `os::WaitAny`, which does not clear an event, then pops notifications until the queue is empty. The host only ever signalled its notification event, so after the first focus change (iOS changes focus at launch; the Mac does not) it stayed set and the loop never blocked again. The event now follows the queue (`hle_oe.c` `mq_ev_sync`). Mac repro: `DM_OE_SCRIPT=focus=2@5,focus=1@6`: before 995 ms/s, CPU 109%, 6.97 W; after, the thread is idle and CPU energy is 0.05 W. +- Focus handling mode 1 (focus notifications are sent). + +## Keyboard and names + +- The hunter, Palamute and Palico are named through the inline keyboard (`DM_KBD` on the Mac, the input box on the iPhone). The inline keyboard keeps the guest clock frozen while it is up: the game does not block on hardware at all, and the host's input box blocks it. +- **Resolved: texture creation returned null right after the inline keyboard's `Initialize`, then a null read at `main+0x7713044`.** The game makes three textures of `swkbd::InlineKeyboard::GetWindowSize`, and the host answered 0 × 0 (its creation function returns null for a size of 0, `main+0x77112b8`). It is 1280 × 720 as on hardware now. +- **Resolved: the hunter's name box stayed empty, then stayed open with the name in it.** The text now arrives through the enter callback (read off the game's callbacks at `main+0x6fb3910` / `main+0x6fb3afc`), and the game finishes the entry only when `Calc` returns 1 (ready / hidden) after a non-zero value (`main+0x6fb3e6c`). +- **Resolved: `CRASH sig=10` at a heap address, called from `main+0x6bca160`, right after the name was entered.** The game masks the name through `nn::ngc::ProfanityFilter`'s vtable (slot 8, `MaskProfanityWordsInText(int *, char16_t *, uint32_t)`), and the constructor hook left the object with what the heap held. It now gets a host vtable (`ngc_vtable` in `hle_misc.c`). Nothing is masked or reported. + +## Files and shaders + +- The base game is a cartridge dump: the card header's magic `HEAD` sits at 0x100, and its root HFS0 holds the `update`, `logo`, `normal` and `secure` partitions; the game's NCAs are in `secure` (the 7 GB program NCA, control, legal information, HTML manual and the meta NCA), with their key in the key area, so there is no ticket ([01](../../../docs/agents/01-extract.md#nsp--nca--exefs--romfs)). +- Everything sits in KPKA PAK archives: `re_chunk_000.pak` plus `re_chunk_000.pak.patch_001.pak` … `patch_016` (19.6 GB in all; KPKA v4, zstd / deflate entries, file names only as murmur hashes), later patch PAKs overriding earlier ones by path hash ([01](../../../docs/agents/01-extract.md#compressed-romfs-content)). `romfs_marker` is `re_chunk_000.pak.patch_001.pak`. +- Shaders: 63,111 in 2,685 SDF files (30,233 vertex, 21,475 pixel, 11,271 compute, 53 geometry, 33 hull, 46 domain) and 102,345 vertex / pixel pairs, in 5 s with 12 jobs (`shader_split.py` `kpka_jobs` / `sdf_shaders`); an SDF entry's stages are one program; control sections 0x880 bytes, compute code without a header ([04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container)). + +## Rewriter + +- `main` has 125 MB of text: with the patch area 32 MB before the base, sites near the end of the text were more than 128 MB away (`B()` range assertion, then `patch area of … bytes does not fit in the … before the base`). The patch area now goes right before the base (`BIG_TEXT`); the first pass tries a 2.6 MB gap, and since the patch area is larger (3.6 MB, 34,322 sites) the rewriter runs itself again with exactly that size (`DM_PATCH_FRONT`). +- **Resolved: `KeyError` at `site_br` for a `br x18` byte jump table** (`main+0x6b346d0`): the function held a trap word (`0xe7ffdefe`, `main+0x6b34540`) before the table, where capstone stopped. Function disassembly now goes on after undecodable words (`disasm_fn`). +- Halfword jump tables padded only to their largest entry (`half_count`, found on Animal Crossing: New Horizons) changed 1 to 6 tables in this `main`. + +## Mac routes + +Run with `DM_FPS_TARGET=30`: the frame numbers count at 30. + +- A fresh save (with the first-boot notices accepted) reaches character creation with `DM_KEYS="f4100:A~0.3,f6300:A~0.3,f7000:A~0.3,f7600:A~0.3"`. +- With `DM_KBD=Hunter`, `DM_KEYS="f4100:A~0.3,f6300:A~0.3,f7000:A~0.3,f7600:A~0.3,f8000:A~0.3,f8150:Down~0.25*6/40,f8450:A~0.3,f8650:A~0.3,f8900:A~0.3,f9100:A~0.3,f9500:A~0.3,f9800:Down~0.25*9/40,f10250:A~0.3,f10450:A~0.3,f10700:A~0.3,f10950:A~0.3,f11300:A~0.3,f11800:Down~0.25*10/40,f12250:A~0.3,f12450:A~0.3,f12700:A~0.3,f12950:A~0.3,f13300:A~0.3,f16000:A~0.3,f16300:A~0.3"` goes on through creation (hunter, Palamute, Palico, each named) and saves to slot 1. Holds under 0.25 s are not seen, and 0.5 s repeats in lists. +- From a save made that way, `f4100:A~0.3,f6300:A~0.3,f6800:A~0.3,f7100:A~0.3,f7500:A~0.3` continues into the opening. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Rewriter:** the patch area before the base for big text (`BIG_TEXT`, `DM_PATCH_FRONT`) and disassembly past undecodable words (`disasm_fn`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **`mem_used`** covering the modules: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). +- **Sparse virtual pools** (`vpool_*`, `vm_remap`) and their **4 KB page table** (`vpt_*`, `nvn_sparse_copy`, `nvn_sparse_since`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **SDK 11 calls**, the **inline keyboard** (window size, `ikb_str_arg`, `Calc` results), **`ProfanityFilter` vtable** (`ngc_vtable`), **native-texture H.264 / AAC movies** (`mavc_set_rgba`), the **notification event following the queue** (`mq_ev_sync`, `DM_OE_SCRIPT`) and focus handling modes: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Both texture tables at once** (separate binds at slots 128 + n, `samp_id`): [04, section 3](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census). +- **One-lane subgroups** (`spv_single_lane`), **indirect-count multi-draws** (`mdx_run`, `mdx_next`), **sampled textures in sparse pools uploaded through the page table**, **the sparse texture layout** (`level_walk`), **`sparse_view`**, **`pend_read`**, **`tex_upgrade`**, **`sbuf_ubo`**, **bindless handles through loop phis**: [04, Seen in Monster Hunter Rise](../../../docs/agents/04-graphics.md#seen-in-monster-hunter-rise). +- **Spin waits through stores** (`patch_nso.py` "Spin waits"; this game's `main` has the most new loops): [development.md, Power](../../../docs/development.md#power). + +## Open + +- Texture layout: NVN's automatic block height now truncates for every game (`pick_tile(ah * 3 / 16)`), which matters for this game's sparse textures; not re-checked here ([04, Seen in Tears of the Kingdom](../../../docs/agents/04-graphics.md#seen-in-the-legend-of-zelda-tears-of-the-kingdom)). +- The 53 geometry shaders and the 121 fully dynamic handles (type 4) are not supported. +- 8- and 16-bit integer formats feeding float inputs still go through the CPU conversion (red line 99). diff --git a/dm/games/0100B7C00933A000/README.md b/dm/games/0100B7C00933A000/README.md new file mode 100644 index 0000000..700b20d --- /dev/null +++ b/dm/games/0100B7C00933A000/README.md @@ -0,0 +1,67 @@ +# 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`](patches.py), `main` `060f827b…` | this build reserves the region only when `-SwitchVirtualAddressSpaceSize=` is absent, so the profile has no `game_args`; see [Memory](#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`](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](../0100A9D01C446000/README.md)), 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 ` starts a run with `DM_CTL`, `run/see.sh ` 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: + +- **A network request kept on hold for 50 ms** (`hle_net.c`, `nn::nifm`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Rewriter:** an `adrp` re-encoded inside a copied exclusive window (`window_adrp`) and gates before a `ret` (`scratch_dead`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). Spin waits that store (`patch_nso.py` "Spin waits"): [development.md, Power](../../../docs/development.md#power). +- **An Unreal engine region whose default is cut instead of capped by `game_args`**: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). + +## 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`. diff --git a/dm/games/0100B99019412000/README.md b/dm/games/0100B99019412000/README.md new file mode 100644 index 0000000..af30903 --- /dev/null +++ b/dm/games/0100B99019412000/README.md @@ -0,0 +1,59 @@ +# Mario vs. Donkey Kong (`0100B99019412000`) + +Update 1.0.1 (v65536) over the cartridge base game (an .xci), SDK 15.3.0. Nintendo's sead / agl / gsys engine (`ainb` AI scripts, `bgyml` parameters) on NVN. Modules `main`, `subsdk0` (the SDK's movie library, NintendoSDK_movie 15.3.0: `movie::Extractor` and `movie::Decoder`) and `sdk`. Plays on the iPhone (IPA 1.6 GB); 60 fps. + +On the Mac: the opening movie with its sound, the title (Press L+R), file select, the play style choice, the first cutscene, level 1-1 (both colour switches, the spring, a time-out and its restart), the pause menu, leaving the level, level select and the save it shows in file select. 60 fps (frame interval p99 about 17 ms), sound, about 1 GB resident in the level and flat while it runs (0.95 GB of Metal allocations), CPU about 30% of a core; no red line on that route. + +## What it needs + +The provisional profile's guesses held, with `render_cond` added and two host changes in the movie decoder. + +| Setting | Where | Why | +|---|---|---| +| `render_cond on` | [`game.tsv`](game.tsv) | the game skips clears under render conditions (`SetRenderEnableConditional`), as Tears of the Kingdom ([0100F2C0115B6000](../0100F2C0115B6000/README.md)) and Animal Crossing: New Horizons ([01006F8002326000](../01006F8002326000/README.md)) do; without evaluating them every level was black behind its HUD | +| `sys_resource 0x0` | `game.tsv` | the NPDM declares no system resource, so `IsVirtualAddressMemoryEnabled` is false, as on Horizon: no virtual address memory. The game sets a 3.14 GB heap (pool − used memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| `shader_groups` (one group per RomFS top folder, `/Model/` … `/GameParameter/`) | `game.tsv` | see [Files and shaders](#files-and-shaders) | + +No `pad_layout`: the virtual gamepad starts in the default 2D layout. + +## Movies + +The cutscenes are MP4 files (`Movie/*.mp4`, H.264 1080p and AAC stereo) played through the SDK's movie library, their sound through an AudioOut of the game's own. + +- **Resolved: a crash in the game's heap free (`sead::ExpHeap`) on a movie thread, a second into the opening movie (`Movie/Cutscene01.mp4`), at the same address every boot** (`pc=main+0x42f980`, a free-list unlink through a link of 0x3f, `addr=0x47`). The game's movie player keeps the outputs it holds in a ring of 32 entries {u32 output index, u32 flags, u64 time} per track (`main+0xe93d4c`: 0x200 bytes) and pushes with no bounds check (`main+0xe94140`). The host's audio decoder offered 64 outputs, so the 33rd outstanding one, index 63, went past the ring into the heap. The console's AAC decoder cannot offer more than 32, or the game would overflow there too: AAC decoders now have 32 outputs (`MD_NOUT_AAC`; Monster Train 2 peaked at 30 held), Vorbis keeps 64. +- How it was traced: `DM_PROBE_WATCH` on the link broke the movie's file reads (the page also held a 64 KB read buffer, and `read()` into a protected page fails with EFAULT), so the movie stopped and nothing crashed. `DM_PROBE_POLL=
+8=3f` (a snapshot only at the change to that value) showed the link being written right after the block was relinked; the values around it were the ring's entries. +- **Resolved: the movie then froze on its first second, its sound decoder full and the game taking no more audio.** The player opens its own AudioOut (`main+0xe92c2c`), sizes its buffers by the audio decoder's `GetOutputBufferSize`, decodes straight into them (`SetDataAndCapacity` with that size) and appends each whole, advancing its audio clock by size / 4 samples (`main+0xe92a74`). The host answered 128 KB, a bound for any Vorbis packet, so every 4 KB AAC frame played as 0.68 s and the clock crawled (a temporary log of `AppendAudioOutBuffer` showed 131,072-byte buffers). An AAC decoder now reports one frame: 1024 samples per channel, 2048 with SBR (object type 5 or 29 in the AudioSpecificConfig), which is what each output holds; Vorbis keeps the bound, its packets vary in length. + +## Rendering + +- **Resolved: level scenes black behind the HUD** (`[RED] SetRenderEnableConditional: the host draws anyway`): fixed by `render_cond`. +- **Resolved: advanced blend modes.** The level sets `nvnBlendStateSetAdvancedMode 8` (SRC_OVER) on one blend state. The host used to log it and blend with the state's own functions, which the hardware ignores while an advanced mode is set. The Porter-Duff modes now blend with the fixed-function factors they amount to (`blend_st` in `nvn_obj.h`, `adv_blend` in `nvn_mtl.m`). + +## Files and shaders + +- 3,162 RomFS files (1.26 GB), nearly all zstd (`.zs`), as Wonder's ([010015100B514000](../010015100B514000/README.md)). The profile's `romfs_marker` is `Model/World08_Textures.bfres.zs`. +- **Current:** 12,691 BNSH / BAHS programs (8,053 pixel, 4,587 vertex, 51 compute): the models' own shader archives in `Model/` (8,390), agl's in `Lib/` (3,520), effects, `Shader/`, layouts and 40 in `main`. All translate. +- Prewarm list: recorded from a fresh save over the route above (550 lines); with it nothing was built on demand on that route. + +## Input + +- The pad only (`main` imports no touch screen). The title asks for L+R. + +## Mac routes + +- The work folder's `run/boot.sh [secs] VAR=…` runs `build/konghost`; `run/ctl.sh` / `run/step.sh` drive a run started with `DM_CTL`. +- The opening movie plays at every boot and Plus skips it (about 18 s in); then L+R on the title, A for file 1 (level select) and A for level 1-1, which starts about 15 s later. +- In level 1-1, `LRight~0.3`, `LRight+A~0.25` hits the blue switch and `LRight~0.55`, `LRight+A~0.15` the red one. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **32 outputs for AAC movie decoders and one AAC frame from `GetOutputBufferSize`** (`MD_NOUT_AAC` in `hle_movie.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Advanced blend modes** (`blend_st`, `adv_blend`): [04, Seen in Mario vs. Donkey Kong](../../../docs/agents/04-graphics.md#seen-in-mario-vs-donkey-kong). +- **A value filter for `DM_PROBE_POLL`** (`=`, for a word that changes often before the bad store): [05, Probes without a debugger](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe). + +## Open + +- `game.tsv`'s header says the game plays on the Mac and does not mention the iPhone, where it plays. diff --git a/dm/games/0100BF00112C0000/README.md b/dm/games/0100BF00112C0000/README.md new file mode 100644 index 0000000..9856aa9 --- /dev/null +++ b/dm/games/0100BF00112C0000/README.md @@ -0,0 +1,74 @@ +# Catherine: Full Body (`0100BF00112C0000`) + +Update 1.0.1a (v131072) over the base game, SDK 8.2.1. Atlus's GFD engine (the game's code name xrd758) on NVN, with CRI middleware. Modules `main`, `subsdk0` (GLSLC, the run-time shader compiler; the same build as Link's Awakening's, [01006BB00C6F0000](../01006BB00C6F0000/README.md)), `subsdk1` (the SDK's movie library) and `sdk`; `sdk` and the movie module are the builds Super Smash Bros. Ultimate runs ([01006A800016E000](../01006A800016E000/README.md)). Plays on the iPhone; 30 fps, as on the Switch. The README lists its IPA at about 14 GB. + +Brought up on the Mac: the ATLUS logo, the title, the puzzle type choice, the Golden Playhouse intro, the anime opening (CRI movies with sound) and the first Nightmare stage with Vincent under control, no red line in a 6-minute run. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `mem_used 0x6000000` (96 MB) | [`game.tsv`](game.tsv) | the rewritten modules take 92 MB, more than the default 64 MB; see [Memory](#memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | the block of the other games without virtual address memory: 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| Node area cut and 32 MB alignment for regions of 1 GB and more | `dm/tools/sdk_patches.py`, `sdk` build `5fd4f531…` | made for Super Smash Bros. Ultimate on the same build; this game has no virtual address memory and never makes an address region | +| The 2 shaders it compiles at boot | captured on the Mac (`hle_glslc.c`) | compiled at run time by its own GLSLC from GLSL sources in `main`, captured on the Mac as Three Houses' are ([010055D009F78000](../010055D009F78000/README.md)) | + +## Frame rate + +- 30 fps, as on the Switch (present interval 2). GPU p50 4.7 ms, CPU about a fifth of a core on the Mac. + +## Memory + +- The NPDM declares no system resource (META +0x14 = 0): `nninitStartup` sets a 3.11 GB heap in the 4 GB layout (pool − used; `game.tsv` says the pool − 64 MB); `main`'s bss is 48 MB. +- **Resolved: the memory pool for the game's 96 MB GPU heap had no storage (`MemoryPoolInitialize` red line), then a null read at `main+0x2eb954`.** The log said `[mem] SetHeapSize … + modules 0x5c00000 > app pool`: the rewritten modules take 92 MB (`main` 61 MB with its bss, GLSLC 9.5 MB, the movie library 7.6 MB, `sdk` 13.8 MB), so `SetHeapSize(pool − 64 MB)` failed and the game ran on a 32 MB heap, as Age of Calamity did. `mem_used` is 96 MB, the NSP script's guess after the rewrite. +- On the Mac (M6) in the first stage: 0.79 GB resident, 1.9 GB of Metal allocations. + +## Shaders + +- 3,128 GLSLC outputs (1,244 vertex, 1,872 pixel, 12 compute), all of one stage each: 3,050 loose files in `data/shaders_binary` (`*.vs` / `*.ps`, read like FINAL FANTASY TACTICS' technique files), 76 in `main` and 2 the game compiles at boot with its own GLSLC (captured on the Mac). Single-stage programs paired with the cover, as in 13 Sentinels. Every binding decodes with Mario's layout. +- **Resolved: a null read at `main+0x3316dc` in `nnMain`, right after the window setup** (the program object's interface pointers at +0xd0 / +0xd8 both null). The game compiles two built-in programs at boot with its own GLSLC, and first checks that GLSLC's GPU code version against `nvnDeviceGetInteger` items 0x2E–0x31 (the driver's supported range, `main+0x330dcc`). The host reported Super Mario Odyssey's SDK 7 driver range, 1.5–1.14; this GLSLC writes 1.15, so nothing was compiled and the first uniform lookup read the empty program. The host now reports at least the version the game's own `glslcGetVersion` returns (`hle_glslc.c` `dm_glslc_gpu_code`, `hle_nvn.c`). +- **Resolved: shader type 0 in every vertex shader's SPH** (word 0 = `0x0061`; every other game's vertex shaders have `0x0461`). `shader_split.py` found no stage for them and stopped at the first captured output (`no GLSLC output`). Ryujinx's `ShaderHeader` reads a graphics header's type 0 as a vertex shader; `sph_stage` does too. A census of the other games' packages and of every `main` on hand found no other type-0 vertex shader. +- **Resolved: every post pass and the window drew one texel's colour (a black screen with sound).** Shader type 0 is Maxwell's first vertex slot (VERTEX_CULL_BEFORE_FETCH), and this GLSLC (GPU code version 1.15) writes every vertex shader as two programs in one code area: part A (VertexA) at code +0 (the SPH above) loads the position attribute and writes only the position; part B (VertexB), at control +0x700 with a 0x30-byte header of zeros and an SPH of type 1, reads the other attributes and writes the other outputs. Control +0x748 lists both as {SPH offset, length from the SPH, ?}. The hardware runs A, then B. Translated alone, A left every varying at (0, 0, 0, 1), so the full-screen passes sampled their sources at (0, 0) and the 2D draws their atlases' first texel. `dmsh_build.py` (`vertex_ab`) merges the two as Ryujinx's `TranslatorContext.Translate(VertexA)` does: A's instructions with each unconditional `EXIT` turned into a `BRA` to B's first instruction, then B's, under B's SPH with A's flags and input / output maps ORed in and the larger local memory sizes. In all 1,244: A writes no generic output, A ends in an `EXIT` predicated on CC FCSM_TR (which Ryujinx never takes; left as it is), an unconditional `EXIT` and a `BRA` to itself, and neither part has an absolute or table jump, so moving B keeps every branch. c1 is what follows part B (zero padding in all of them). A type-0 shader with no part B listed is translated as it is. + +## Rendering + +- **Resolved: the cutscene subtitle band was a hard-edged dark box with a magenta stroke through it.** The band is a 32x512 BC3 gradient the game copies from a staging buffer (`nvnCommandBufferCopyBufferToTexture`, recorded at the start of a frame, `main+0x305e64`), and the GPU copy read the next texture's blocks: the source was right when recorded and when encoded, but the game's resource thread wrote the next texture into the same staging block 64 ms later, before that first submission had even been committed, because the host spent 300 ms building pipelines inside it. On the Switch the GPU runs a submission's copies as soon as the submit returns. A segment is now committed at the first pass boundary after any buffer → texture copy (`g_cut_copy`, `sub_cut` in `nvn_mtl.m`); the `residency set` line counts these cuts (`N after buffer → texture copies`, a few per 300 frames in this game, only while it loads). The game reuses its staging memory about a frame after recording a copy. Found with `DM_NVN_HILITE` on the band's pixel shader, `DM_NVN_TEXRAW` of the texture (decoded: opaque black and magenta blocks, not a fade) and `DM_PROBE_WATCH` on the staging address beside a log of the copy's submission completing. + +## Movies and audio + +- CRI ADX2 sound (`data/sound*/*.acb` / `.awb`) through one 6-channel AudioOut. +- CRI Sofdec2 movies (`data/movie*/*.usm`) whose H.264 goes through the SDK's `movie::Decoder` (the host's VideoToolbox decoder; mode 1, output format 4). +- **Resolved: a read far outside guest memory at `main+0x344f74` on the first movie picture** (Sofdec2's NV12 → RGB conversion). The player reads `nv12-y-stride`, `nv12-y-offset`, `nv12-uv-stride` and `nv12-uv-offset` from the output format and checks none of the results (`main+0x34e694`); the host set no `nv12-uv-stride`, so the chroma stride was stack garbage. It is the luma stride now, as for any NV12 picture (`hle_movie.c` `md_format`). + +## Services + +- **Resolved: abort in `sm::Initialize` under `nn::settings::GetAvailableLanguageCodeCount()`** (`main+0x117590`: it compares `LanguageCode::Make(0 … count − 1)` with `GetDesiredLanguage`'s code). The host returns the count of its language table, beside Paper Mario's `GetAvailableLanguageCodes` (`hle_sdk16.c`). The other imports no hook names run in `sdk` (the `LanguageCode` comparison, `ClockSnapshot`'s constructor and getters, fibers, `NumberLineAllocator`). + +## Files + +- Everything is in five CRI CPK archives (`data/Base.cpk`, `Model.cpk`, `Puzzle.cpk`, `Keyfree.cpk`, `Patch.cpk`); `romfs_marker` is `data/Model.cpk`. +- Only the merged 1.0.1a copy is extracted; the base game's own build IDs are added when needed. + +## Input + +- + pauses a cutscene (B resumes, Y skips); the left stick moves Vincent, A grabs blocks. + +## Mac routes + +- From a fresh save, `DM_KEYS="5:A*25/2,60:Plus*14/4,61:Y*14/4,62:A*14/4"` skips the intro scenes into the first Nightmare stage (about frame 3,300); `120:LUp~0.4*40/1.5` then climbs. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **GLSLC GPU code version range follows the game's own GLSLC** (`hle_glslc.c` `dm_glslc_gpu_code`, `hle_nvn.c`; on Bayonetta 3 the reported newest version came to follow the SDK, 1.15 from SDK 8 after this game's GLSLC 8.2.1): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **VertexA / VertexB merge** (`shader_split.py` `sph_stage`, `dmsh_build.py` `vertex_ab`), explained in full under [Shaders](#shaders): [04, Seen in Catherine: Full Body](../../../docs/agents/04-graphics.md#seen-in-catherine-full-body). +- **Commit at the first pass boundary after a buffer → texture copy** (`g_cut_copy`, `sub_cut`; always on, even with `DM_NVN_CUT=0`): same section. +- **`nv12-uv-stride` in the movie decoder's output format** (`md_format` in `hle_movie.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **`nn::settings::GetAvailableLanguageCodeCount`** (`hle_sdk16.c`): same section. + +## Open + +- `game.tsv` still describes the game as "In bring-up"; the development notes say it plays on the iPhone. +- The heap: the development notes say 3.11 GB (pool − used), `game.tsv` "the pool − 64 MB (3.11 GB)". +- The profile has no `pad_layout` row, so the virtual gamepad starts in the default 2D layout; the README's list of games that start in 2D does not name it ("the other games in 3D"). diff --git a/dm/games/0100C2A01A730000/README.md b/dm/games/0100C2A01A730000/README.md new file mode 100644 index 0000000..a686058 --- /dev/null +++ b/dm/games/0100C2A01A730000/README.md @@ -0,0 +1,53 @@ +# ROMANCE OF THE THREE KINGDOMS 8 REMAKE (`0100C2A01A730000`) + +Update 1.1.3 (v851968) over the base game, with its four DLC. SDK 17.5.3 (Ace Attorney Investigations Collection's `sdk` and `subsdk0` builds). Koei Tecmo's KTGL engine on NVN. Modules `main`, `subsdk0` (the movie library, NintendoSDK_movie: the WebM logo and story movies) and `sdk`. Plays on the iPhone (IPA 16.8 GB); 30 fps, as on the Switch; controller only. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | [`game.tsv`](game.tsv) | the NPDM declares no system resource (META +0x14 = 0): no virtual address memory; `nninitStartup` sets a heap of the pool − used memory (3.13 GB) | +| `mem_used 0x5000000` (80 MB) | `game.tsv` | the rewritten modules take more than the default 64 MB, which `SetHeapSize` checks the request against | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the dialogue boxes' auto and log buttons are the right stick and R3 | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `311deab1…` | applies to this build by its ID; written for Ace Attorney Investigations Collection ([010005501E68C000](../010005501E68C000/README.md)), whose virtual address memory needs it. This game has none | + +## Display and input + +- The game draws 1280x720 into its 1920x1080 window textures and crops (`WindowCrop`), docked as well; a screenshot without `DM_NVN_SHOT_OUT` shows the whole texture, the picture in its top-left corner. +- Controller only, with the 3D pad layout. `main` initializes the touch screen and gestures but never reads them (`game.tsv`: `main` imports no `GetTouchScreenState`); it reads a USB mouse (`GetMouseStates`), which the host does not provide. Touches as a mouse would suit it, but OCTOPATH TRAVELER 0, Tears of the Kingdom and Unicorn Overlord also read a mouse and no touch screen, so such a rule needs testing on them first. + +## Audio + +- **Resolved: `[RED] audren: a second renderer` right after the first open, with the same work buffer and "original device 1 mode 1".** Not a second renderer: SDK 17.5.3's `OpenAudioRenderer` with an event opens it by calling the form without one through the PLT (`sdk+0x1e489c`), which the loader binds to the host's hook, so `ar_open` ran twice for one renderer. The inner call started the render thread before the outer one had created the event, and the outer call took the renderer for a second one. A thread-local flag (`t_in_open`) now hands the inner call straight to the SDK with the host's CPU / manual copy of the parameter, and only the outer call starts the thread. + +## Files and shaders + +- A newer RDB than Age of Calamity's: `0002/root.rdb` and `0002/system.rdb` are the indexes (`_DRK0000`, with a `.rdx` beside each) over 296 `0002/0x.fdata` files (12.2 GB) and `0002/data/0x.file` (and `jaJP/`, `zhCN/` copies); `game.tsv` gives 12.6 GB for the RDB data. An `.fdata` starts with `PDRK0000` and u64 0x10, then the same `IDRK0000` entries (the u32 after the lengths is 1 or 0, the padding 16 bytes). +- The compression is new: flag 0x400000 = chunks of u16 stored length, 8 bytes, then a zlib stream; with 0x100000 as well (flags 0x500000) the streams are LZ4 blocks. Each chunk inflates to at most 16 KB and a zero length ends. Of the 51,655 entries, 49,620 are LZ4, 2,026 zlib and 9 stored; all decode to the length in their header (`shader_split.py` `idrk_data`; [01](../../../docs/agents/01-extract.md#compressed-romfs-content)). +- Scenarios, messages, movies and their subtitles sit in loose folders beside the RDB. +- **The update is a multi-program patch:** beside the game's patch (`…0000`, 1.5 GB) it carries a whole second program (`0100C2A01A730001`, 34 MB, a full NCA, not a patch) with its own Control NCA, which `main` starts with `nn::oe::ExecuteProgram(1)` (`main+0xa31910`). **Resolved:** `nx_extract` took the first Program NCA it found, the second program, and failed with `NCA section 1 does not exist` layering it over the base game; it now takes program index 0's NCAs. The host runs program 0 only (`hle_oe.c` logs the call). +- Shaders: 59,363 GLSLC outputs (52,691 pixel, 5,359 vertex, 1,311 compute, 2 geometry): 59,214 in the G2S (`_S2G5000` / `6000` / `7000`) and `_DOK0000` containers of the RDB entries, and 149 in `main`. The vertex shaders are single-stage programs, paired with a cover pixel shader. All build for the Mac (1 h 19 min on a 12-core M6). +- Three shaders get textures of another type than they declare on some slots (`texture type mismatch … bound as is`, once each: a 2D array where a pixel shader declares 3D, cube and 3D textures where two vertex shaders declare 2D); nothing in the scenes below looks wrong. + +## Status on the Mac + +- From the logo movie and intro, the title and its attract demo, the main menu, a new game through the recommended setup (Ruler, Cao Cao, 195), its opening story on the map, the council (espionage, personnel, the help panel), a saved and loaded game, a month's missions, the city view, a visit and a practice duel in 3D. +- 30 fps, as on the Switch (frame time p50 33.3 ms, p95 34.5 ms; GPU 5–7 ms a frame on the Mac in the duel), sound; about 2 GB resident in the council and 2.5 GB in the duel, flat on a screen left idle. The prewarm list covers that route. + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL`. From a fresh save: A (skips the intro), A at the title, A (New Game), A on Recommended, A ×3, the quiz's first answers (A, A, A), A on Ruler, X at Confirm Settings, A ×3 and X, then X at the story's Skip dialog: the council of 195 April, about 2 minutes. +- `run/save_ref` holds a manual save there: A, A, Down, A (Load), A, X loads it. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Multi-program updates:** `nx_extract` takes program index 0's NCAs: [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **The newer RDB** (`.fdata`, `idrk_data`): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **`OpenAudioRenderer` calling its no-event form through the PLT** (`t_in_open` in `hle_audren.c`): [03, Audio](../../../docs/agents/03-bringup.md#audio). + +## Open + +- Touches as a mouse: not done; it needs testing on the other games that read a mouse and no touch screen (above). +- The three texture type mismatches above are bound as is. diff --git a/dm/games/0100CD801CE5E000/README.md b/dm/games/0100CD801CE5E000/README.md new file mode 100644 index 0000000..6f46294 --- /dev/null +++ b/dm/games/0100CD801CE5E000/README.md @@ -0,0 +1,67 @@ +# Balatro (`0100CD801CE5E000`) + +Update 1.1.3 (v851968), SDK 18.3.2. LÖVE 11.4 ("Mysterious Mysteries", love-nx) with LuaJIT 2.1.0-beta3 in its interpreter and SDL: the game is Lua source in the RomFS (`main.lua`, `game.lua`, `engine/`, `functions/`), its pictures PNG, its sound Ogg Vorbis through OpenAL Soft on the audio renderer. It renders with **OpenGL**, not NVN: NVIDIA's desktop GL 4.5 through EGL, which the host implements on Metal (`dm/gfx/gl/hle_gl.m`) on the core profile's paths. Modules `main` and `sdk`; `sdk` is the same build as Absolum 1.1.1's ([0100A83022A22000](../0100A83022A22000/README.md)). `subsdk0` (NVIDIA's OpenGL driver) and `subsdk1` (NVIDIA's Vulkan driver) are never loaded. Plays on the iPhone (IPA about 82 MB), by touch in handheld mode. + +On the Mac: the intro, the title, a new run by touch and the tutorial's first steps, at 60 fps with sound; about 0.65 GB resident and 0.2 GB of Metal allocations, flat over three minutes in a run. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `op_mode handheld` | [`game.tsv`](game.tsv) | the Switch build plays by touch there (SDL turns the touch screen into LÖVE touches and mouse presses: tap a card, drag it): on the iPhone the touches outside the gamepad controls, on the Mac the mouse. Docked it reads only the pads. Its window is then the 1280x720 handheld display | +| `pad_layout touch` | `game.tsv` | the virtual gamepad starts in its Tap layout (only the layout button; every touch goes to the game), since the 2D layout's corners cover Run Info, Options and the deck; the layout button brings back 2D and 3D | +| [`gl_capture.tsv`](gl_capture.tsv) | profile | the 35 shaders LÖVE puts together at run time, which no game file holds; the NSP script captures them on the Mac; see [Shaders](#shaders) | +| `modules main sdk` | `game.tsv` | `subsdk0` / `subsdk1` are NVIDIA's OpenGL and Vulkan drivers, which program the GPU through NvRm: never loaded, the GL layer answers `main`'s EGL imports | +| `sys_resource 0x0`; `mem_reserve 0xf0000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | no system resource, so no virtual address memory: `nninitStartup` gives its malloc a heap of the pool − used memory, which it maps only as it touches it; `main`'s 264 MB bss counts as used memory | +| Node area cut and 1 GB → 32 MB region alignment | `dm/tools/sdk_patches.py`, `sdk` build `f118ae1c…` | registered for Absolum 1.1.1, whose `sdk` build this is; the entry does not name Balatro | +| No NVN shader tables | `game.tsv` `shader_note` | no NVN shaders: `gl_shaders.py` writes the GL library and its table, not the profile | + +## Rendering (OpenGL) + +LÖVE's graphics module is its own OpenGL backend, so its choices follow what the context reports. + +- **Telling the API apart:** `main` imports the EGL functions and `vkGetInstanceProcAddr`, SDL's Vulkan loader, which nothing calls. **Resolved:** the NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer; `profile_guess.py` now leaves `vkGetInstanceProcAddr` out of its Vulkan count (Hades and both Galaxies import 99 or 100 Vulkan functions, `vkCreateInstance` among them). +- **Which GL functions:** LÖVE's glad loader asks for 2,133 GL names and stores each pointer straight into a global of its own (no GOT slot per name). The code reaches those globals through GOT entries that hold their addresses, which it often keeps in callee-saved registers across a loop, or picks between with `csel` (`glEnableVertexAttribArray` or `glDisableVertexAttribArray`). `loader_census.py` follows registers in a straight line for that (adrp, add, loads through relocated GOT entries, `mov`, `csel`): about 150 called. +- **Resolved: a jump to address 0 right after `glGetFloatv(0xb12)`** (`lr=main+0x2d58c4`): a GL function the layer did not have (here `glVertexAttrib4fv`, LÖVE's default colours). `DM_GL_STUBS=1` gives every name the layer lacks a stub of its own that reports its first call instead of a null pointer (a jump to address 0 names nothing); `loader_census.py` lists what the code calls before the first boot. +- **Core profile:** LÖVE reads `GL_CONTEXT_PROFILE_MASK`, which the layer now answers as core. On a core profile it streams vertices through persistently mapped buffers (`glBufferStorage` + `glMapBufferRange` of a 3-frame ring, a `glFenceSync` per frame and `glClientWaitSync` three frames later); on a compatibility one it would draw from client memory, which Metal cannot draw from. In the layer the mapping is the shared `MTLBuffer`'s memory, a fence is the serial of the command buffer being recorded, and a wait on a fence of that command buffer commits it first. The shaders then come as `#version 330 core`. +- **Attributes:** LÖVE binds VertexPosition, VertexTexCoord, VertexColor and ConstantColor to 0–3 with `glBindAttribLocation` before each link, keeps VertexColor and ConstantColor as disabled arrays and sets their values with `glVertexAttrib4f` (the draw colour); the layer maps each Metal input to the game's index at the link. +- **Uniforms:** LÖVE lists them with `glGetActiveUniform` (types as GL enums, arrays as `name[0]`) and reads their initial values back with `glGetUniformfv` / `iv`; every `glUniform*` form. +- **Textures:** made with `glTexStorage2D` (every level up front: the game's images are mipmapped, filled by `glGenerateMipmap` as a blit) or `glTexStorage3D`: LÖVE makes a 1x1 default texture of each type, 3D and 2D array among them, and the layer's units hold all four targets (96 of them, as `GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS` says). Font glyphs are RG8 with a `GL_TEXTURE_SWIZZLE_*` of R, R, R, G, applied when the texture is made (a swizzled texture cannot be a render target). +- **Draws and framebuffers:** batched quads share one 16-bit index buffer through `glDrawElementsBaseVertex`; instanced draws; canvases bind GL_FRAMEBUFFER (read and draw), check completeness, discard depth and stencil after use (`glInvalidateFramebuffer`) and clear with `glClearBufferfv`. Separate read and draw framebuffers, `glBlitFramebuffer` (colour, same size) and `glReadPixels` (RGBA8) are there for multisample resolves and `Canvas:newImageData`, which the game does not use. + +## Shaders + +- **Current:** LÖVE puts each shader's GLSL together at run time, in Lua, from its own header and built-in code and the game's 19 `resources/shaders/*.fs`: 35 sources (`#version 330 core`), all made by `Game:start_up` before the first frame. No game file holds them, so the NSP script captures them on the Mac (`DM_GL_CAPTURE` at `_build/extract//gl_capture` until every file in `gl_capture.tsv` is there) and `gl_shaders.py` translates them; the host finds each by the hash of its text. +- **Resolved:** a GLSL `bool` uniform is a `uint` in SPIR-V; `gl_shaders.py` now reports the declared type, since LÖVE's `Shader:send` takes only a boolean for a bool (the game stopped at its first card otherwise). +- A capture run has no shader library: every program then links with no uniforms, and the game's first `Shader:send` raises a Lua error, which quits (below), after every shader is captured. + +## Audio + +- **Resolved: silent.** The game opens the audio renderer without an event (`OpenAudioRenderer(handle, parameter, work, size)`, from LÖVE's OpenAL Soft) and paces its own updates; the renderer must still run every 5 ms. The host started its render thread only for the form with an event, so only the game's own updates rendered (catch-up frames that nothing played). Both forms now go through one open that starts the thread (`hle_audren.c` `ar_open`). The log showed `audren: OpenAudioRenderer (no event)` and no `render thread started`. + +## When the game stops + +- On the Switch, Balatro's `love.errhand` returns at once, so any Lua error quits. love-nx's video worker is then deleted twice (`love::video::theora::Video` → `~Worker`: its thread releases the worker that the closing video module also deletes), which crashes in `main+0x339c00` a second or two into the title. A run that reaches this has hit a Lua error first: look for it. +- How it was found: a `print` at the top of `love.errhand` in `main.lua`, in a copy of the RomFS (`DM_ROMFS=`), gave the errors: `Shader uniform 'distortion_fac' does not exist` (the capture run had no shader library, so every program linked with no uniforms), then `bad argument #2 to 'send' (number expected, got boolean)` (a `bool` uniform reported as `GL_UNSIGNED_INT`). Probing the destructors (`DM_PROBE`) and logging every object the Lua GC released (`DM_PROBE=main:0x18b920@x0+0`: the module proxies all at once) showed it was a shutdown, not a premature collection. + +## Rewriter + +- The spin-wait search through stores found 12 loops in `main` instead of 9; one is a parser state that leaves on the next pass (`main+0x1f4600`) ([development.md, Power](../../../docs/development.md#power)). + +## Mac routes + +- In handheld mode, `DM_KEYS="f1300:T0.35x0.86,f1700:T0.35x0.86,f2100:T0.5x0.68"` taps Play at the title, Play in the run setup and the tutorial's Next. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The GL layer's core-profile paths** (`GL_CONTEXT_PROFILE_MASK`, persistently mapped buffers and fences, attribute bindings and current values, uniform queries, texture storage, 3D and array textures, swizzles, mipmaps, base-vertex and instanced draws, blits) in `hle_gl.m`, and **`DM_GL_STUBS=1`**: [04, Seen in Balatro](../../../docs/agents/04-graphics.md#seen-in-balatro-löve-114-on-opengl). +- **`loader_census.py` following registers** (LÖVE's glad), and `vkGetInstanceProcAddr` left out of `profile_guess.py`'s Vulkan count: same section. +- **GL shaders captured at run time** (`DM_GL_CAPTURE`, `gl_capture.tsv`): [development.md, The packing script in detail](../../../docs/development.md#the-packing-script-in-detail). +- **A renderer opened without an event** starts the render thread (`ar_open`): [03, Audio](../../../docs/agents/03-bringup.md#audio). +- **The Tap pad layout** (`pad_layout touch`, `DmGamepad.m`): [dm/games/README.md](../README.md). + +## Open + +- The player-facing [README](../../../README.md) says the games outside its 2D list start in the 3D layout; Balatro starts in Tap (its gallery entry says "played by touch"). diff --git a/dm/games/0100D9F01D474000/README.md b/dm/games/0100D9F01D474000/README.md new file mode 100644 index 0000000..7eb4530 --- /dev/null +++ b/dm/games/0100D9F01D474000/README.md @@ -0,0 +1,51 @@ +# Rhythm Heaven Groove (`0100D9F01D474000`) + +Base game 1.0.0 (v0), SDK 21.4.0, the newest SDK so far. Nintendo's own 2D engine on NVN; audio is the SDK's AudioRenderer (`sound/sm/**/*.cwv`). Modules `main` and `sdk`. Plays on the iPhone, handheld with Boost. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `op_mode handheld` | [`game.tsv`](game.tsv) | docked means a TV to the game, which then runs a latency calibration (press A on every fourth beat) until one passes; a phone is its own screen. See [Input](#input) | +| `perf_mode boost` | `game.tsv` | Boost keeps the 1920x1080 draw size (handheld Normal draws 1280x720); the game takes it from the performance mode alone (`hle_oe.c`). Boost in handheld mode is a combination a Switch 1 never reports, so a profile choice | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | `main` sets a heap of at most the pool − 64 MB: a 3328 MB heap region. The game has no virtual address memory, so the NSP script's heap check does not run | +| `shader_groups` (`/mesg/`, `/graph/`, `/sound/`, `/wavmark/`, `/voicetext/`, `/shader/`) | `game.tsv` | the shader split's groups | + +## Rendering + +One BNSH, `shader/shader_NX.bnsh` (2 vertex and 33 pixel shaders, all 2D), with Mario's binding layout (every binding decodes). No packaged textures: each texture file (`graph/**/*.tex`, raw BC7 or linear R8 with its description in the `.spr` beside it) is read into memory and gets a memory pool the CPU cannot access. + +- **Resolved: black screen, every sprite and glyph in place under `DM_NVN_HILITE`.** The sprite shader (`40adb3021c79b155`) computes texture × colour multiply + colour add and discards a fragment whose alpha is 0. The uniform buffer was right (a `CBDUMP`: multiply 1, add 0), so every texture read 0. The game makes a `CPU_NO_ACCESS | GPU_CACHED` pool (flags 0x21) on the memory it read each texture file into: 245 pools by the first screens, each holding one texture at offset 0, 17 block-linear BC7 (flag `IMAGE`, no packaged layout) and 228 linear R8 (`IMAGE | LINEAR`). NVN keeps what a pool's storage held at `nvnMemoryPoolInitialize` as its content, but the host uploaded from guest memory only packaged, `WriteTexels` or flushed textures, and made a linear texture on its pool's buffer only in a pool the CPU can see. Now a new texture in a `CPU_NO_ACCESS` pool whose storage holds data starts with that content, uploaded once (`tex_init_fill` in `nvn_mtl.m`); it stays a GPU texture that can be rendered into, with no re-upload on page writes. A linear one is made on the pool's buffer in any pool (`tex_linear`). Left out: storage of zeros, `COMPRESSIBLE` textures (render targets here, in their own 0xa1 pools; their memory means nothing without the compression tags), multisample, depth / stencil and sparse ones. Found by a census of every texture's pool flags, flags, format and whether its storage held data at creation (a temporary line under `DM_NVN_LOG`). +- **Resolved: the latency calibration's beat numbers torn and its background in blue steps.** Two BC7 textures without a packaged layout were read with the wrong block height. A dump of the guest bytes deswizzled in Python with 1 to 16 GOBs and decoded with `texture2ddecoder` shows the right one by eye; a smoothness score does not, because a block height too tall reads padding and looks smooth. Truncating height in blocks × 1.5 / 8, as MacNativeGames found, matches every size checked by eye: 672×160 and 1232×160 (40 block rows: 4 GOBs, where rounding up gives 8), and 112×112 (4), 288×208 (8) and 880×432 (16), where both roundings agree and `floor(rows / 8)`, the other rule that fits 40 rows, does not. `nvn_tex.c` now uses it for every game (`pick_tile(ah * 3 / 16)`). + +## Audio + +- **Resolved: no sound; red `audren: command 0 in the command list is malformed (first word 0x181901 size 0)`.** SDK 21's DSP command header has no magic: +0x00 enabled (bit 0), +0x01 command ID, +0x02 size (s16), +0x04 estimated time, +0x08 node ID (`CommandListProcessor::Process`, `sdk+0x24fd50`), and every command body moved 8 bytes forward. DeviceSink is still ID 0x14 but 0x140 bytes: input count +0x128, input indices +0x12c, upsample buffer +0x120 (written at `sdk+0x23a7b0`). `AudioRenderSystem` keeps SDK 20's offsets (update lock +0x2d0). `capture` (`hle_audren.c`) tries this layout when neither magic walk works, bounded by the generated list's length; its first lists hold 4 commands or fewer, so the layout is now chosen on the first non-empty list. + +## Services + +- **Resolved: abort in `nn::sm::Initialize` under `nn::pl::sharedresource::GetPlatformSharedResourceManager`**, first thing in `nnMain`. The game asks for every shared font at once with `pl::GetSharedFont(int *, SharedFontData *, int, LanguageCode)` (`sdk+0x40f270`), not by type. `SharedFontData` is 0x18 bytes {u32 type, address, size}; the SDK adds the pl service's offsets, which point past each font's 8-byte BFTTF header, to the shared memory base, and the game hands each address straight to `ScalableFontEngine::LoadFont` (`main+0x501ff0`). The hook returns the six types in order with the stand-in TTFs past the header, whatever the profile's `shared_font` (`hle_sdk16.c`). +- **Resolved: SIGSEGV in `vi::GetDefaultDisplayPanelIdentifier`** (`main+0xa9a4`, before the window). The SDK's version calls through the vi service object the host never made. The game reads a u32 kind and, from kind 2, 0x14 more bytes; anything else makes it use zeros. The host answers an all-zero identifier (`hle_vi.c`). + +## Input + +- **Not a host bug: the TV latency calibration ("press A when you hear the fourth beat") ignores A.** Docked, the first boot runs it (scene script `main+0x4c0820`). It starts listening only `0x3c0` frames (16 s) after the prompt, through the active controller (`main+0x22b0` id `0x3ed`, virtual button 4 = A), and passes when about ten presses keep a steady offset within −5 to +13 frames of the cue (evaluation `main+0x2948c0`; offset = cue ticks at `+0x34` / about 17 ticks per frame, about 480 per beat). Scripted presses at fixed seconds drift off the beat; use frame-timed `DM_KEYS` at the bar period (117.12 frames at its 123 BPM) and read each press's offset with `DM_PROBE=main:2951f0@34`. A passed calibration is saved per TV panel identity (the host's all-zero one included) and not asked again. +- **Current:** the profile plays it handheld with Boost, which skips the TV setup and keeps the 1080p draw size. +- The iOS app starts it in the 2D gamepad layout. + +## Files + +- `romfs_marker` is `sound/sm/showtime/n_virus_very_hard.cwv`. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Initial content of `CPU_NO_ACCESS` pools** (`tex_init_fill`) and **linear textures in any pool** (`tex_linear`): [04, Seen in Rhythm Heaven Groove](../../../docs/agents/04-graphics.md#seen-in-rhythm-heaven-groove). +- **Truncated automatic block height** (`pick_tile(ah * 3 / 16)` in `nvn_tex.c`): same section, and [04, Seen in Tears of the Kingdom](../../../docs/agents/04-graphics.md#seen-in-the-legend-of-zelda-tears-of-the-kingdom). +- **SDK 21's DSP command list without magic** (`capture` in `hle_audren.c`), **`pl::GetSharedFont`** for every font at once (`hle_sdk16.c`) and **`vi::GetDefaultDisplayPanelIdentifier`** (`hle_vi.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **`perf_mode`** (Boost whatever the operation mode, `hle_oe.c`): [dm/games/README.md, Keys](../README.md#keys). + +## Open + +- `game.tsv` still describes the game as "In bring-up" (on the Mac it plays from boot through the setup and its TV latency calibration into the first rhythm game, Hoop Trundling, with sound), while AGENTS.md lists it as playable on the iPhone. diff --git a/dm/games/0100DCA0064A6000/README.md b/dm/games/0100DCA0064A6000/README.md new file mode 100644 index 0000000..d53c66d --- /dev/null +++ b/dm/games/0100DCA0064A6000/README.md @@ -0,0 +1,71 @@ +# Luigi's Mansion 3 (`0100DCA0064A6000`) + +Update 1.4.0 (v327680) over the base game, SDK 7.3.2 (NVN 53). Next Level Games' own engine on NVN, with Havok physics and cloth, Wwise sound (a 6-channel AudioOut) and NEX / Pia online. Modules `main`, `subsdk0` (`nnSdkEn`, a second copy of the SDK), `subsdk1` (NintendoSDK_movie) and `sdk`; `sdk` is the build of Super Mario Odyssey ([0100000000010000](../0100000000010000/README.md)) and DRAGON QUEST XI S ([01006C300E9F0000](../01006C300E9F0000/README.md)). Playable on the iPhone at 60 fps (30 on the Switch). On the Mac: the boot notices, the title (the hotel in daylight), a new game and the opening cutscene (the bus ride, the arrival at the hotel, the lobby), then the lobby in play at 60 fps, about 3.7 GB phys_footprint (3.0 GB of Metal allocations). + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `fps_target 60` | [`game.tsv`](game.tsv), `hle_fpslock.c` | the host writes the frame limiter's 33.333 ms as a frame time; see [Frame rate](#frame-rate) | +| `present_interval display` | `game.tsv` | ignores the present interval 2 the game also asks for | +| `mem_used 0x4600000` | `game.tsv` | the rewritten modules are larger than the default 64 MB of used memory | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | `game.tsv` | no virtual address memory (the NPDM declares no system resource): `main` sets a 2.94 GB heap | +| `pad_layout full` | `game.tsv` | Luigi moves with the left stick and aims with the right; the D-pad calls up the Virtual Boo: the iPhone pad starts in the 3D layout, which has both | +| Node area cut and 32 MB alignment of large regions | `dm/tools/sdk_patches.py`, `sdk` build `fcf93a41…` | registered for DRAGON QUEST XI S on this build; this game has no virtual address memory | + +No `patches.py`. + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). At the end of each frame the main thread calls its frame callbacks (`main+0xb7c308`) with the milliseconds since the frame began; the frame limiter among them (`main+0x7a3e70`) runs jobs or sleeps until 33.333 ms have passed, a float in read-only data (`main+0x13c94dc`, read only there), and the game steps by the time that really passed. The host writes the profile's `fps_target` there as a frame time (`hle_fpslock.c`): 16.167 ms at 60, half a millisecond under the vsync so the presents set the pace. At exactly 16.667 the work after the limiter made it 59.6 fps with a frame shown twice every 2.5 s. +- On the Mac the lobby holds 60.0 fps (display interval p99 16.67 ms); the same 2 s walk and 1.5 s turn from the same save end at the same spot at 30 and 60 fps, and the title's animation matches at time scale 1.00 (`DM_NVN_REGLUM`). `DM_FPS_TARGET=30` keeps the game's 33.333 ms. +- The limiter also has a phase lock, a nudge of at most 0.05 ms a frame from the layer's frame history, which stays off: the host's `nn::vi::ListFrameHistory` returns no entries. + +## Modules and memory + +- `subsdk0` (`nnSdkEn`) is `sdk`'s code byte for byte, with another module name and build ID; it comes first in the load order, so `main`'s imports bind to it. +- No virtual address memory: `main` sets a 2.94 GB heap. + +## Command buffers + +- **Resolved: the title showed one stretched 64×64 icon over the 3D scene, a new game a white screen.** The engine records a command buffer, then rewrites texture handles in it before submitting, as the hardware lays the command out: before `nvnCommandBufferBindTexture` it takes the command memory base + `GetCommandMemoryUsed` (`main+0x7c8964`, the only reader of that NVN entry point), and every frame it stores a 32-bit handle at that address + 0x10 (`main+0xb7d0d4`, on a job worker). In a host bind record that word was the count, and the replay read 535 million handles from whatever followed, so the post-process passes kept stale texture handles in their slots. Found with a check that reports a bind record holding more than 32 handles (`[RED] bind record … overwritten after recording`), then `DM_PROBE_WATCH` on that word. **Current:** a single texture bind is recorded as `OP_BIND_TEX1`, its handle 16 bytes in (`nvn_obj.h` `p_bind_tex1`, read through `bind_rec`). + +## Rendering + +A deferred renderer: the scene draws into a G-buffer of two integer targets (R32Uint, RGBA8Uint) and a D32S8 depth, light volumes and a full-screen resolve light it into RG11B10F targets, and a chain of 960×540 to 60×33 bloom passes ends in the window. Most textures are ASTC. + +- **Resolved: every scene draw skipped** (`failed to create render pipeline … output of type float4 is not compatible with a MTLPixelFormatR32Uint color attachment`, about 1,400 pixel shaders). With no PC version the translator types every fragment output float, and the G-buffer shaders write bit-packed integers through `as_type`. A hand-written row per shader (`shader_facts_manual.tsv`, Breath of the Wild's way) does not scale to that. **Current:** 1,396 pixel shaders get a second entry point, `main0_u`, that writes the G-buffer as integers (`fs_uint`); the host takes it for a pipeline whose written colour attachments are all unsigned integer formats (`pso_all_uint`). +- **Current: shadow cascades from an array of driver-table handles** (586 pixel shaders: `array, 32> fp_t2das_cb2_0`, `shaders with a binding whose slot was not decoded 588`), indexed by (0x80 + a cascade offset) ÷ 8 over the driver table c2 (`c2_array`). +- **Resolved: the boot aborted in Metal's pipeline validation** (`MTLPixelFormatDepth16Unorm is not color renderable`): a 1024×1024 D16 texture (a shadow) is drawn through an R16 view. Such a pass now draws into a colour shadow of the same pixel size, copied in before and back after the pass (`shrt_block`; D16 and D32F; not D24S8). +- **Resolved: a stencil-mode texture view aborted under the API validation layer:** Metal allows such a view only of a texture with the pixel-format-view usage; every depth-stencil texture has that usage now (PR #106). +- Host calls added at bring-up: `nvnBlendStateSetAdvancedMode` and `nvnCommandBufferSetDebugLabel`. +- **Not done: tessellation and geometry shaders.** The tessellation stages (15 hull, 18 domain shaders; `primitive type 14 not implemented`) do not build and their patch draws are skipped; 10 geometry shaders (one expands a box from two corners) do not build either. Neither is drawn much. Two tessellated programs (vertex `28a39709d0adfcfa` / hull `51d2d156bb12c260` / domain `0b95742e863bec50`, and `d4d89931b5a9a514` / `8aa6feeb7d33e9f6` / `f254b4dce2d6c012`) draw twice a frame for about 30 frames, in the title scene behind the file select as it slides out before a new game. From the title through a new game, the opening cutscene and the lobby, no draw used a geometry shader. A program whose geometry shader has no library used to draw silently without that stage, with the vertex shader's positions taken as clip coordinates; **Current:** `draw with geometry shader … which has no library` names it. +- SPIRV-Cross already writes the hull shaders as kernels and the domain shaders as post-tessellation vertex functions, but Metal 4, which the host draws with, has no tessellation stage (no `drawPatches`, no factor buffer): the draws would need a tessellator of the host's own in a compute kernel and the domain shaders rewritten as plain vertex functions. The hull kernels also write quad factors (`MTLQuadTessellationFactorsHalf`) for triangle patches. The vertex shaders that feed a hull or geometry shader can be found from the shader headers (SPH): the generic attribute map at +0x18 of the consumer's header (inputs) equals the one at +0x36 of the vertex shader's (outputs). 40 vertex shaders match a hull shader and 49 a geometry shader. +- A few pixel shaders declare a 3D texture in a slot the game fills with a 32×32 2D texture (`texture type mismatch`), on paths not seen taken. + +## Files and shaders + +- Every RomFS file but the Wwise banks (`wwiseaudio/*.pck`; `wwiseaudio/streamedfiles.pck` is the profile's `romfs_marker`) is an archive of the engine's own: `.dict` indexes zlib chunks in `.data` (`init`, `global`, `Hotel/`, … 98 of them). +- The `.dict` is 3,203 bytes in every archive: u32 magic `0xa9f32458`, u8 4, u8 1, u8 compressed, u8 0, u32 the largest stored chunk, u8 chunk count (120), u8 record count (52), u8 name count (3), u8 0; then the records (0x18 bytes: u32 hash, u16 the top-level entry count of its table, u16, and one chunk index for each of 16 sections), the chunks (0x10 bytes: u32 offset, length, stored length (one zlib stream; 0 in an archive stored as is), u16 alignment as log2, u8 file, u8 GPU memory) and the file names `.data`, `.debug`, `.nxpc`. Only `.data` ships: the 13 even records are the game's variants (languages), the odd ones point into `.nxpc`. Record v's resource table is chunk v: 12-byte entries {u32 kind (bit 31: a list of w1 entries from entry w2; bits 27–30 the section; bits 22–26 the alignment as log2), u32 length, u32 offset in the section}. The game reads an archive in `main+0x1ba00` (1.4.0). +- Update 1.4.0 leaves `.dict` and `.data` as they were and adds `.patch`: u64 a bitmask of the records it patches, u32 offset, stored and inflated length of its body (one zlib stream: 16 u32 section offsets, 16 u16 alignments, then each section's new data), then 0xd0 bytes per patched record {u32 offset, stored length and entry count of its new resource table (one zlib stream), u16 its top-level entry count, u16, u32 the new length of each of 16 sections, 16 u32 end and 16 u32 start of the old data that is overwritten before it is read}. After loading a section the game walks the new table and packs that section's entries from its start at their alignment, each from the old section (an offset below its length) or from the body (the rest, counted from the old length), and writes the new offset into the entry (`main+0x1c8d0`, `main+0x1cbd0`); it does this in place, keeping a copy of the old data in [start, end). `shader_split.py` `nlg_patch_section` does the same ([01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content)). +- Shaders: 5,317 (5,149 in the base game), all in `init` (`shader_split.py` `nlg_shaders`): control sections back to back in one section, their code areas in the same order in another, one NVN program per stage (paired as single-stage programs). 46 of the 103 the title draws exist only in the patched sections. + +## Mac routes + +- The work folder's `run/ctl.sh` drives a run started with `DM_CTL`; `DM_KEYS=14:A,18:A,26:A,31:A,41:Right,42:A,54:A` goes from the boot notices through a new game into the opening cutscene. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Single texture binds recorded in the hardware's layout** (`OP_BIND_TEX1`, `p_bind_tex1`, `bind_rec`): [03, NVN object sizes](../../../docs/agents/03-bringup.md#nvn-object-sizes-memory-corruption). +- **An integer-output entry point** (`fs_uint`, `main0_u`, `pso_all_uint`), **driver-table handle arrays** (`c2_array`, `tex_c2a` / `samp_c2a`, `NVN_RB_TEXA` / `NVN_RB_SAMPA`, `c2_handle`), **depth textures drawn through a colour view** (`shrt_block`) and the **`draw with geometry shader … which has no library`** line: [04, Seen in Luigi's Mansion 3](../../../docs/agents/04-graphics.md#seen-in-luigis-mansion-3). +- **Pixel-format-view usage on every depth-stencil texture** (`tex_desc`, PR #106): [04, Seen in Pokémon Legends: Z-A](../../../docs/agents/04-graphics.md#seen-in-pokémon-legends-z-a). +- **The engine's `.dict` / `.data` / `.patch` archives** (`nlg_shaders`, `nlg_patch_section`): [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **The frame limiter's milliseconds written in game memory** (`hle_fpslock.c`, `fps_target` with `present_interval display`): [the profile keys](../README.md#keys). + +## Open + +- Tessellation (15 hull, 18 domain shaders) and the 10 geometry shaders are not built ([Rendering](#rendering)). +- [docs/geometry-metal-research.md](../../../docs/geometry-metal-research.md) describes geometry shaders now built as compute kernels, checked on AI: THE SOMNIUM FILES only ("Other games and iOS device execution are unvalidated"); whether this game's 10 build with it is not recorded. +- The limiter's phase lock stays off while `nn::vi::ListFrameHistory` returns no entries. diff --git a/dm/games/0100E44016C22000/README.md b/dm/games/0100E44016C22000/README.md new file mode 100644 index 0000000..a6a51f7 --- /dev/null +++ b/dm/games/0100E44016C22000/README.md @@ -0,0 +1,51 @@ +# Patrick's Parabox (`0100E44016C22000`) + +Update 1.0.1 (v65536) over the base game, SDK 13.3.7. Unity 2020.3.30f1 IL2CPP with FMOD on NVN. Modules `main`, `subsdk0` (the movie module, which the game never uses) and `sdk`. Plays on the iPhone. On the Mac: the first-boot language menu (all ten languages drawn), the title, the hub, Intro 1 solved and Intro 2 with undo, reset and the menu, at 60 fps with music and saves (progress kept across a restart); no red line, about 0.42–0.48 GB resident, flat over four minutes. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `9c73fb73…` (the constructor at `sdk+0x37c3a0`; the movs at `+0x37c3b0` and `+0x37c44c`) | `StandardAllocator::Initialize` aborted at its `CentralHeap::Init`; see [Memory](#memory) | +| `sys_resource 0x1000000` | [`game.tsv`](game.tsv) | the NPDM's 16 MB of system resource: virtual address memory | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0x40000000` | `game.tsv` | the layout of Ace Attorney Investigations Collection ([010005501E68C000](../010005501E68C000/README.md)) and Brain Memory Training Academy ([0100F5D01DA3A000](../0100F5D01DA3A000/README.md)): a 1 GB pool (in place of the provisional profile's hardware pool) and the 5 GB block (256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region) | +| Shared fonts as plain TTFs | host, `hle_sdk16.c` `font_skip` (every game) | Japanese, Korean and Chinese text drew nothing; see [Fonts](#fonts) | +| `shader_groups /Resources/=engine /Data/=data` | `game.tsv` | engine built-ins (`Resources/`: `unity default resources`, `unity_builtin_extra`) go to engine, the player's serialized files to data | +| No `pad_layout` (the 2D default) | `game.tsv` | controller only; see [Input](#input) | + +## Memory + +- The NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true: `nninitStartup` (`main+0x1cd010`) hands application pool − used memory to StandardAllocator as virtual address memory and never calls `SetMemoryHeapSize`, as in Ace Attorney Investigations Collection and Brain Memory Training Academy. +- **Resolved: `StandardAllocator::Initialize` aborted** at `sdk+0x324dec`, called from `nninitStartup` at `main+0x1cd080`: the same `nninitStartup` and the same cut as theirs, on this build's constructor. With the 1 GB pool and the 5 GB block it boots to its title with no red line, and the alias region is mapped up to +1.5 GB with 0.27 GB in use. +- To find the constructor on a new `sdk` build, search its text for the two `mov w8 / w9, #0x3fe00000` words (`08fca752`, `09fca752`); `sdis.py --fn` names the function `AddressRegionManagerC2Emm` from its dynamic symbols. + +## Fonts + +- **Resolved: Japanese, Korean and Chinese text drew nothing** (the language menu showed three blank lines), Latin text was fine. The game's one font, Jost (a Unity dynamic font), falls back to the OS fonts for those glyphs, and Unity's Switch player loads them from the shared fonts: `main+0x2390b0` maps the names `Switch_Font_STD`, `Switch_Font_CN_Simplified`, `Switch_Font_CN_Extended`, `Switch_Font_CN_Traditional`, `Switch_Font_KR` and `Switch_Font_NIN_Extended` to types 0–5 and hands `GetSharedFontAddress` / `GetSharedFontSize` straight to FreeType's `FT_New_Memory_Face` (`main+0x23b2e0`). The host gave every SDK 11+ game the 8-byte BFTTF header in front, so every face failed to open. The host now gives a game that imports no `ScalableFontEngineHelper::Decode` the TTF itself (`font_skip` in `hle_sdk16.c`, `dm_main_imports` in `loader.c`; the `shared_font` key, which no profile sets, overrides it). +- To see which fonts a Unity game asks for, probe its name mapper's entry: `DM_PROBE=main:2390b0` on 1.0.1 (the function is the one `main` caller of `GetSharedFontLoadState`). + +## Input + +- Controller only (stick or D-pad to move, A redo, B undo, X reset, + menu): it stays docked at 1920x1080 with the 2D pad layout. + +## Files and shaders + +- 486 shaders, all Unity built-ins (`Data/Resources/unity default resources`, `unity_builtin_extra`) plus the player's 11 GLSLC outputs in `main`. The whole game draws with three shader pairs. The built-in VR blit's geometry shader does not build. +- **Resolved: the split stopped with "no bundle has a Shader type tree of the same hash to borrow"** (hash `a30e3dcf…`). There are no bundles, and the files carry no type trees. One 18 KB shader bundle of Fire Emblem Engage (2020.3.18f1) carries that hash; its tree is now in `unityfs.py`'s table (`SHADER_2020_3` in `KNOWN_TREES`: per subprogram global and local keyword indices, every subprogram in `m_SubPrograms`, no parameter blobs, keyword names or stage counts), equal to Engage's node for node (2,179 nodes, the alignment flags included). Each of the game's 48 `Shader` objects reads to its exact end. The structs it shares with the 2022.3 layout are written once (`_SHADER_COMMON`), and the 2022.3 layout comes out byte-identical. + +## Mac routes + +- A fresh save solves Intro 1: `DM_KEYS="f700:A,f1000:A,f1300:A,f1600:A,f2400:Right,f2450:Down,f2500:Down,f2550:Down,f2600:Down,f3000:Up*3/30,f3090:Right*3/30,f3180:Down*2/30,f3240:Up*3/30,f3330:Left*4/30"` (English, the title, into Intro 1 from the hub at about frame 2,700, solved and on to Intro 2 at about frame 3,500). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The shared-font address is the TTF** unless the game is on SDK 11 or later and imports `ScalableFontEngineHelper::Decode` (`font_skip`, `dm_main_imports`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk), [dm/games/README.md, Keys](../README.md#keys) (`shared_font`). +- **Unity 2020.3's `Shader` layout written out** (`SHADER_2020_3`, `_SHADER_COMMON`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **Finding the AddressRegionManager constructor on a new `sdk` build**: [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). + +## Open + +- The built-in VR blit's geometry shader does not build. +- The A button: docs/development.md says "A redo", the `game.tsv` header "A to confirm". diff --git a/dm/games/0100E65002BB8000/README.md b/dm/games/0100E65002BB8000/README.md new file mode 100644 index 0000000..a5678b2 --- /dev/null +++ b/dm/games/0100E65002BB8000/README.md @@ -0,0 +1,54 @@ +# Stardew Valley (`0100E65002BB8000`) + +Update 1.6.15.13 (v1310720; the base game v0 is not extracted), SDK 20.5.27. MonoGame for the Switch, as Celeste ([01002B30028F6000](../01002B30028F6000/README.md)): MonoGame's and the game's C# compiled to C++ by Sickhead's BRUTE into `main` (124 MB of code, 230 MB with its data and bss) with a Boehm collector; XACT sound (`Content/XACT`, 0.48 GB of wave banks) through the audio renderer; NVN. Modules `main` and `sdk` only. It links Pia and NPLN for online co-op and LDN for local co-op. Plays on the iPhone (IPA 0.6 GB); no host change in this profile's change. + +## Status on the Mac + +- The title, a new farm (character creation with the software keyboard, Skip Intro), the farmhouse, the farm and its tools, the bus stop, Pelican Town by day and night (lamp light), Pierre's shop and a purchase, the game menu, the mail, a night's sleep and its save, Load from the title, and the Co-op menu (Online Communication goes back to the title with no network). No red line on that route. +- 60 fps: frame time p50 16.67 ms, p95 16.8 ms; GPU about 0.6 ms a frame on an M6; CPU 8% of a core, 0.27 W. Sound. +- 0.46 GB resident at the title and 0.93 GB in play, flat over the last 10 of 23 minutes; 0.3 GB of Metal allocations. +- The title is up about 20 s after launch. + +## What it needs + +The provisional profile's guesses held. + +| Setting | Where | Why | +|---|---|---| +| `sys_resource 0x0` | [`game.tsv`](game.tsv) | the NPDM declares no system resource, so `IsVirtualAddressMemoryEnabled` is false, as on Horizon: no virtual address memory. The game sets a 2.96 GB heap (pool − used memory) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 256 MB alias region + 256 MB stack region + 3.25 GB heap region | +| `mem_used 0xda5c000` (218 MB) | `game.tsv` | the rewritten modules are larger than the default 64 MB of used memory (214 MB without `main`'s 29.6 MB of bss, + 4 MB: `profile_guess.py` `mem_used`), so the heap request plus the modules would exceed the pool | +| `pad_layout full` | `game.tsv` | the 3D layout: the D-pad for menus and the right stick for the cursor | +| `shader_groups /Content/=content` | `game.tsv` | the MonoGame effects; see [Files and shaders](#files-and-shaders) | + +## Files and shaders + +- 3,549 XNB files, 3,535 of them LZ4 and 8 LZX (MonoGame's compressed XNB; LZX is flag 0x80, LZ4 flag 0x40). +- **Current:** 65 shaders (23 pixel, 42 vertex): 64 in the MonoGame effects in `main` (SpriteBatch's and the game's) and one in `Content/Effects/ShadowRemove.xnb`, an LZ4 XNB that `shader_split.py` decompresses before reading it (`xnb_decompress`). +- Nearly everything draws with SpriteBatch: the prewarm list is its two shaders and four pipelines, and with it nothing was built on demand over the route above. + +## Input + +- The pad only (`main` imports no touch screen or mouse). The D-pad moves between menu items, the right stick moves the cursor, ZL / ZR change the toolbar slot (empty slots are skipped), X opens the menu, Y uses the tool, A checks. +- The game drops presses while a menu fades in or out (about a second after it opens or closes). +- **Resolved (PR #120): the Mac text box.** Character creation names the farmer and the farm through the software keyboard, and it re-opened the keyboard after every OK. The Mac text box (`front_mac.m`) now releases the buttons that opened it, and stays up while the text is shorter than the minimum length. + +## Mac routes + +- The work folder's `run_v/boot.sh [secs] VAR=…` runs `build/valleyhost` (`SV=` for a copy of `run_v/save_ref`, the morning of day 2 in the farmhouse); `run_v/ctl.sh` drives a run started with `DM_CTL`; `run_v/step.sh [wait] ` presses and takes a screenshot. +- The title is up about 20 s into a boot: A (Load) and, a second later, A (the save). +- Menu presses held 0.3 s or more repeat, so hold 0.2 s. Walking takes the left stick (`LLeft~1.3` is about five tiles). +- In character creation with `DM_KBD=`, A on each of the three text boxes fills it, and Skip Intro is the last item above OK. +- Out of the farmhouse (its door is below the gift box), the path east above the weeds, past the mailbox, leads to the bus stop, and east from there to Pelican Town. + +## Found on this game + +General host changes first needed here (PR #120, before this profile's change); each applies to every game: + +- **Compressed XNB** decompressed before the MGFX reader (`xnb_decompress` in `shader_split.py`): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **The Mac software keyboard's text box** releases the buttons that opened it and stays up below the minimum length (`front_mac.m`): described in [Input](#input) above. +- **Spin waits through stores** (`patch_nso.py` "Spin waits"): one more loop in this game's `main`: [development.md, Power](../../../docs/development.md#power). + +## Open + +- About one frame in 800 takes 24–47 ms (nine in two idle minutes in the farmhouse): game CPU time, with no pipeline build, upload or present in it, while the main thread otherwise sleeps 95% of each frame in `SleepThread` (sampled), whose host sleep is exact. Probably BRUTE's Boehm collector; not checked further. diff --git a/dm/games/0100ECD018EBE000/README.md b/dm/games/0100ECD018EBE000/README.md new file mode 100644 index 0000000..5459d07 --- /dev/null +++ b/dm/games/0100ECD018EBE000/README.md @@ -0,0 +1,85 @@ +# Paper Mario: The Thousand-Year Door (`0100ECD018EBE000`) + +Update 1.0.1 (v65536), SDK 15.3.2. Intelligent Systems' engine on NintendoWare (G3d, Vfx, Atk) on NVN. Modules `main`, `subsdk0` (the SDK's movie library) and `sdk`; `sdk` is the build Celeste ships ([01002B30028F6000](../01002B30028F6000/README.md)). Playable on the iPhone since the spin waits ([CPU](#cpu)); 60 fps (30 on the Switch) through the host's frame step. On the Mac: the title, a new file, the storybook prologue, Peach in Rogueport, Mario's house, the letter and the boat into Rogueport, with Mario under control on the docks, with music and sound, at 60 fps and the game's normal speed; no crash in eight minutes. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Frame step floor 2 → 1 | [`patches.py`](patches.py), `main` `0effe4af…` at `main+0x2e77a4` | with the floor at 2 a frame shown every vsync still stepped two vsyncs: double speed; see [Frame rate](#frame-rate) | +| Frame-step stub, `fps_target 60` | `patches.py` `FRAME_STEP` (site `main+0x2e77f8`), [`game.tsv`](game.tsv) | the base clock steps by the host's vsyncs, so game time = real time at any rate | +| `fps_hold off` | `game.tsv` | holding late frames left the render workers idle and the hold stuck at 20 fps | +| Allocator address space 62 GB → 4 GB | `patches.py` at `main+0x3136e8` | `StandardAllocator::Initialize(nullptr, 0xf80000000, false)` does not fit a phone; see [Memory](#memory) | +| Node area 1 GB → 62 MB, regions of 1 GB and more aligned to 32 MB | `dm/tools/sdk_patches.py`, `sdk` build `86188952…` (`sdk+0x12e0f0` / `+0x12e184`, `+0x12e2e0`) | Absolum's cuts on this build: the malloc region was placed 0.69 GB past the 1 GB node area | +| 5 GB block: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | Engage's block: 256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region (never used) | +| `sys_resource 0x800000` | `game.tsv` | the NPDM declares 8 MB of system resource | +| `tex_dyn_slots on` | `game.tsv` | 2,386 pixel shaders pick a texture at run time between bound slots; see [Rendering](#rendering) | +| `pad_layout full` | `game.tsv` | the D-pad opens the Gear and Journal menus (the HUD shows it in Rogueport): the iPhone pad starts in the 3D layout, which has one | + +## Frame rate + +- **Current:** 60 fps (30 on the Switch). The game asks for present interval 2, fixed at startup, and steps its clocks in 60 Hz vsyncs. Every frame `main+0x2e7750` measures the time since the last one (`oe::GetProgramTotalActiveTime`), rounds it to vsyncs and clamps it to [2, cap] (a step above cap + 10 counts as 2), and stores it as the base clock's step (time object +0x44, `main+0x2e77f8`). The base clock (time object +0x20, `main+0x2e753c`) advances by that step, the field and battle clocks below it by it times their scale, and game seconds by step / 60; timers, fades, animation and the scripts' frame counts follow the clocks. +- Presented every vsync without a change, it ran at double speed (each fade took half its time). With the floor cut to 1 alone, a frame time between one and two vsyncs rounded down to 1 ran the game slow whenever 60 did not hold (a run at 44 fps took 10–20% longer for each fade). The stub stores the host's step instead, the vsyncs the frame stays on screen (1 at 60 fps, 2 when 60 does not hold), and counts the vsyncs stepped; before the host has decided a step, and at target 0, the game's own. x16 and x17 are dead at the site. The log's `frame step: game time … of real time` reads 0.994–1.003. With `fps_target 60` the fades take as long as at 30 fps (the same press script, `DM_NVN_REGLUM` brightness curves). +- **`fps_hold off`:** the frame waits for its three render worker threads (`gfx::ParallelManager`), idle during a hold, so the cost of a frame grew with the hold (1.4 vsyncs at hold 1, 3.3 at hold 2 on a loaded Mac) and the hold stuck at 3 vsyncs, 20 fps; without holds the same load gave 32–58 fps at 0.994 of real time. `DM_FPS_HOLD=on` brings the holds back. +- `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) keeps the game's 30 fps. + +## CPU + +- **Resolved: Rogueport at 24 fps on the first iPhone build:** frame p50 42 ms against 8 ms of GPU time, with the three `gfx::ParallelManager` workers (pinned to cores 0–2, priority 45) at 94% of a core each and the main thread waiting for them. `main+0xa8e680` is their loop. While a batch is pending, an idle worker polls the job cursor (`main+0xa8e784`) through a serial job, which only worker 1 runs, and through the end of a parallel-for, registering with it and backing out. The first worker to finish a parallel-for polls until the others are done (`main+0xa8e9a8`). None of these polls makes an SVC; the general spin wait rule now handles them. +- Results on the Rogueport route on the Mac, side by side with the committed build of the time under the same load: frame p95 29–37 → 18–22 ms, CPU 93–129% → 38–57% of a core, the workers 576–885 → 65–94 ms/s, CPU energy 22–32 → 11–17 mJ per frame. Loops found: 17 in `main`, 5 in `sdk`, 8 in `subsdk0`. Rewriting `main` takes 11.7 s of CPU instead of 20.5 ([development.md, Power](../../../docs/development.md#power)). + +## Memory + +- 8 MB of system resource (virtual address memory) and no heap: `main` never calls `SetMemoryHeapSize`. +- **Resolved: `nn::diag` abort at `sdk+0x11ff34` (`StandardAllocator::Initialize`) in `nninitStartup`** (called from `main+0x31370c`). `nninitStartup` (`main+0x3136b8`) makes the game's malloc a StandardAllocator over 62 GB of address space (`Initialize(nullptr, 0xf80000000, false)`, `main+0x3136e8`), out of which it cuts 256 MB, 16 MB and 1 MB allocators. Booted once with a 64 GB alias region, the region came 0.69 GB past the 1 GB node area, at the next 1 GB boundary: this `sdk` build (Celeste's, which has no virtual address memory) aligns regions of 1 GB and more to 1 GB, as Absolum's ([0100A83022A22000](../0100A83022A22000/README.md)) did. Absolum's three cuts on it (the AddressRegionManager constructor `sdk+0x12e0e0`: `+0x12e0f0` / `+0x12e184`; `AllocateAddressRegion` at `sdk+0x12e1d0`, its alignment at `+0x12e2e0`) and the cut on `main` to 4 GB of address space, as Pokémon Legends: Z-A's ([0100F43008C44000](../0100F43008C44000/README.md)), put the region at alias +62 MB to +4.06 GB in Engage's 5 GB block. The pool (3.2 GB) bounds what it maps. +- The allocator maps 1.5 GB at boot and 2.0 GB by the arrival in Rogueport, under the hardware pool; about 2.4 GB resident on the Mac there. + +## Rewriter + +- **Resolved: `CRASH sig=10 addr=0` on an `stp` relative to `sp`, with `sp` not a multiple of 16** (`main+0x2bbba4`: `sub sp, sp, #0xc0` then `stp d15, d14, [sp, #0x60]` with sp ending in `…9dc`). Horizon leaves stack alignment checking off for user code; Apple's kernel faults on an sp-based access while sp is misaligned. The game's script engine (AngelScript) calls native functions through six hand-written bridges (`main+0x17c140` and five more beside it): each reserves the callee's stack arguments with `sub sp, sp, x19`, x19 being their sizes added up (4 for one int, `[x0 + 0x238]`), and gives them back with `add sp, sp, x19` after the call, so the callee ran with sp 4 bytes off. Of the 22 games' modules on hand then, only this `main` has the form (6 sites). + +## Services + +- **Resolved: abort in `sm::Initialize` under `nn::settings::GetAvailableLanguageCodes`** (early in `nnMain`, `main+0x327888`). The game asks for up to 32 codes and takes the index of `oe::GetDesiredLanguage`'s code in the list as its language. The host lists the system's codes in `nn::settings::Language` order, the table `LanguageCode::Make` uses (`hle_sdk16.c`). It was the one import of `main` that no host hook names and that reaches a service; `fs::MountRamDisk` and the swkbd and hid helpers it also imports run in `sdk` or were never called ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)). + +## Rendering + +11,858 shaders (3,154 vertex, 7,826 pixel, 874 compute, 4 domain), all BNSH; every one builds with Mario's binding layout ([04, Seen in Paper Mario](../../../docs/agents/04-graphics.md#seen-in-paper-mario-the-thousand-year-door)). + +- **Current: a texture picked at run time between bound slots** (2,386 pixel shaders: 5,476 cube samples, 487 2D). The handle is read from the driver table of combined texture handles (c2 `0x20 + 8 × slot`, slots 0, 7, 8, 13 and 28, constant offsets only) and chosen by branches, so the translator falls back to `array, 32> fp_tcube_cb0_40000000`. `tex_dyn_slots` puts `r | r << 20` in the combined table's slot r, so the handle names element r, which holds the texture bound in slot r. The array's other elements hold whatever the other slots do, mostly 2D textures where the array is of cubes; such an element gets the zero cube without a type mismatch report (`tex_fit_elem`). +- **Resolved: an array of handles in a uniform buffer** (the reflection shaders, `reflection.bfsha`: `array, 32> fp_t2d_cb14_0` and its samplers). A loop reads one handle per light from the game's uniform buffer c14 (uniform slot 11) at a computed offset; Ryujinx's texture array from a buffer indexes it by the handle's byte offset ÷ 8 (an NVN handle is 64-bit, the shader computes `(word >> 2) << 1 | (word & 3) >> 1`). The build read the name as one bindless texture (`tex_ubo`, offset 0), so the argument buffer was 32 entries longer than the reflection said and the host stopped at the shader's first load (`unexpected argument buffer layout: encodedLength=640 maxAid=48`). The first run with the fix showed elements 0–3, 6 and 8–12 holding textures, among them the shader's fixed handles at +8, +16 and +24 and the lights' from +64. No chain change: the build already wrote the array type. +- **Resolved: `nvnCommandBufferClearColori` / `ClearColorui`**: an integer target is cleared once Mario stands on Rogueport's docks, about frame 12,300 of a new file. +- **Resolved: a format-converting copy into a packaged texture** (a 128x128 RGBA8 render target into an RG8 texture, mirrored in Y, as the boat arrives in Rogueport, about frame 10,950): `copy_t2t_draw` declined a destination made without render-target use. +- **Resolved: an unbound uniform slot** (`stage 0 cb5 has no uniform buffer bound`, at the title): the argument buffer kept a null pointer there. +- Conditional rendering: the engine calls `SetRenderEnableConditional`; drawn unconditionally (the profile has no `render_cond`), no screen played looked wrong. +- The 4 domain shaders of `shaders/tessellation.bfsha` have no Metal stage; nothing seen so far draws with them. + +## Movies + +- `subsdk0` is the SDK's movie library: WebM movies (VP9 + Vorbis) through `movie::Extractor`, decoded by the host. + +## Files and shaders + +- Nearly every RomFS file is zstd, named `.zst` (8,726 of the RomFS's 9,959 files, the shader archives among them); `sound/sound.bfsar` is the profile's `romfs_marker`. The event scripts and data tables are ELF files the engine reads as data. +- Shaders: 8,993 in `shaders/*.bfsha.zst` and 2,865 in the particle effects (`effect/particle/*.ptcl.zst`). **Resolved:** `shader_split.py` decompressed only `.zs` files and found 0 shaders; it now goes by the zstd magic (`28 b5 2f fd`) whatever the name. Of the other 21 games' RomFS on hand then, only Super Mario Galaxy's `GDDR3.zst` had such a file. + +## Input + +- Buttons and sticks, docked (1920x1080). The 3D pad layout (`pad_layout full`). + +## Mac routes + +- A fresh save reaches Rogueport's docks with `DM_KEYS="35:A,45:A,55:A,65:A*195/2"` (the title, File 1, Start, then A every 2 s through the dialogue; Mario is under control at about 415 s, at 60 fps as at 30). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Spin waits:** the rewriter copies pure poll loops and sends their back-edges through a gate that polls 2 ms, then sleeps (`patch_nso.py` "Spin waits", `dm_spin_wait`, `DM_SPIN`, `DM_SPIN_POLL`, `DM_SPIN_LIST`): [development.md, Power](../../../docs/development.md#power), [05, Scripted input and thread scheduling](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling). +- **`sub sp, sp, xN` rounded up to 16** (`site_sp_align`; `insn_census.py` counts the sites): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **Zstd by magic** in `shader_split.py`, **`tex_dyn_slots` for combined handles** (`tex_fit_elem`), **handle arrays in a uniform buffer** (`NVN_RB_TEXU` / `SAMPU` with `n`, array length from the MSL type), **integer clears** converted at replay, **render-target use for a copy destination** (`tex_upgrade`) and **an unbound uniform slot reading the all-zero bank**: [04, Seen in Paper Mario](../../../docs/agents/04-graphics.md#seen-in-paper-mario-the-thousand-year-door). +- **`nn::settings::GetAvailableLanguageCodes`** (`hle_sdk16.c`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- The 4 domain shaders of `shaders/tessellation.bfsha` have no Metal stage and the host does not run them; nothing seen so far draws with them. diff --git a/dm/games/0100F2C0115B6000/README.md b/dm/games/0100F2C0115B6000/README.md new file mode 100644 index 0000000..079f030 --- /dev/null +++ b/dm/games/0100F2C0115B6000/README.md @@ -0,0 +1,79 @@ +# The Legend of Zelda: Tears of the Kingdom (`0100F2C0115B6000`) + +Update 1.4.3 (v720896) over the base game, SDK 20.5.22. Nintendo EPD's engine (sead / agl / gsys, NintendoWare G3d2 / Ui2d / Vfx2, Havok) on NVN. Modules `main`, `subsdk0` (the movie library) and `sdk`; `subsdk0` and `sdk` are the same builds as Breath of the Wild 1.9.0's ([01007EF00011E000](../01007EF00011E000/README.md)). Plays on the iPhone; 60 fps (30 on the Switch) through the host's frame step. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Frame step, `fps_target 60` | [`patches.py`](patches.py) `FRAME_STEP`, [`game.tsv`](game.tsv) | 60 fps at normal speed; see [Frame rate](#frame-rate) | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `574284b4…` | the resource heaps ran out of alias region (issue #70); see [Memory](#memory) | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x98000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | `game.tsv` | 256 MB in front + 2.375 GB alias + 256 MB stack + 2.125 GB heap region. 5 GB is the total proven on a phone (one reservation of at most 6 GB without the extended virtual addressing entitlement) | +| `render_cond on` | `game.tsv` | the 3D scene was black: the game skips clears with conditional rendering | +| `tex_flush_upload on` | `game.tsv` | streamed textures are written by the CPU into pool memory in GPU layout and published with `nvnMemoryPoolFlushMappedRange` | +| `tex_dirty_upload on` | `game.tsv` | textures filled from the CPU with no flush at all (the magenta cave, the unlit stone blocks) | +| Integer output rows | [`shader_facts_manual.tsv`](shader_facts_manual.tsv) | no PC version, so the translator has no output types: agl's BC4 encoder and the Ultrahand mask (`e3fb0bb58a691e10`, R8Uint, from PR #11; the Mac route does not reach it) | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout: the right stick turns the camera | + +## Frame rate + +- **Current:** the game picks a present interval each frame (`main+0x2db174`: 2 = 30 fps, 3 under load) and steps its game time by 60 / fps (int fps at timing +0x50, read at `main+0x2daa54` and `main+0x3e562c`). Stubs in `patches.py` (sites from masagrator's FPSLocker config for this build) replace that logic with the host's frame step in whole vsyncs, as in Breath of the Wild: int fps = 60 / step, and the float fps at timing +0x24 the same. Prerendered movies hold 30. `DM_FPS_TARGET=30` saves power, `0` gives the game's own step back (every stub then runs the original instructions). +- **Current:** before the host has decided a step (the first frames), the stub steps by the measured frame time, capped at the target. Game time that ran ahead after a short frame is paid back by the next frames (the idea from OJZen/totk-mobile); frames longer than 1/15 s are not caught up. +- Measured on the Mac (M4 Pro): the opening cave holds 60 at 1.000 of real time; with 12 ms added to every frame it drops to 30 after three late frames and stays at 1.000. +- **Render scale:** `DM_RENDER_SCALE=0.5`–`1.0` multiplies the size of the 3D views (1600x896 docked) and fixes it there; the game's own dynamic resolution runs only at 1.0. The HUD keeps its resolution. The stub multiplies in the view size function (`main+0xd5c24`), unlike Breath of the Wild's, which sets the scale at the dynamic-resolution block. +- **Resolved:** the yield back-off used to start after 200 µs, which caught the main thread waiting for its workers on the frame's critical path: frames ran past 33 ms and the game dropped itself to 20 fps (9 times in 300 s on the Mac; on the iPhone the threads also stayed on efficiency cores). From 2 ms on it never backs off ([development.md, Power](../../../docs/development.md#power)). +- The spin-wait rewrite finds 843 loops in `main`; 701 of them are one inlined atomic decrement (a compare-and-swap retried after another thread changed the count), which yields only on a second retry in a row. + +## Memory + +- The NPDM declares 24 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true and the game's memory lives in the alias region. `nninitStartup` also calls `SetHeapSize(0x86000000)` (2.1 GB of the pool as its heap; a 2 GB heap region aborts there with 0xd401). What it can map is bounded by the pool less the heap (1.1 GB). +- **Resolved (issue #70):** the resource heaps are address regions in the alias region (2.375 GB here, 64 GB on the Switch). With the SDK's ~1 GB node area at its start, the alias region was used up to +2.25 GB at the first descent from the Great Sky Island with 1.00 GB mapped. The game then took a memory shortage: its frame check (`main+0x1fd32ec`) logs `MSF`, then `R` and `Rf` (reason bits of one resource heap's object), and plays the memory-shortage Blood Moon, which puts Link back at the start. With the node area cut to 62 MB (as Fire Emblem Engage's, Hollow Knight: Silksong's and other builds have it) the high-water mark is +1.75 GB, and no shortage came in 3.7 minutes on the island (it came within a minute in 7 of 8 runs without it). + +## Rendering + +Game-specific findings. The general host changes they led to are listed under [Found on this game](#found-on-this-game). + +- **Resolved: the 3D scene was black.** The game wraps nearly every `ClearColor` / `ClearDepthStencil` in `SetRenderEnableConditional(RENDER_IF_NOT_EQUAL, rec)` … `SetRenderEnable(true)`, each clear followed by `ReportCounter(TIMESTAMP_TOP, rec)`. The CPU sets `rec` to 1 when a target needs a clear, and `rec + 16` is never written. A timestamp report puts 0 in the counter field, so after one clear the two counters are equal and later conditional clears of that target are skipped. The host ignored the condition, so every clear ran: the first tile-lighting round, the half-resolution scene depth the light volumes test against, and the light accumulation were all cleared before the composite read them. Fixed by `render_cond`. With it, the game's scissored colour clears (50×50 tiles of a 500×400 target, about five a frame) are all skipped. +- **Resolved: the cave was magenta.** The terrain samples two 121-layer ASTC 8×8 arrays (42 MB each), created on zeroed memory and filled from the CPU with no `nvnMemoryPoolFlushMappedRange` or `FlushTexels` (its cache maintenance runs natively). They stayed GPU-only with content 0, and Metal decodes an all-zero ASTC block as (1, 0, 1). Fixed by `tex_dirty_upload`. +- **Resolved: the stone blocks and steps of the opening cave got no torch light (Mac and iPhone).** Their fragment shader `54e8c08998050e79` takes its roughness from a 5120×5120 BC4 atlas (roughness = the square root of its red, packed into the upper six bits of blue in colour target 3, which the tile-light shaders read). The game fills 1024×1024 BC4 material textures on the CPU with no flush, reads them back with `nvnTextureReadTexels` on its own threads before any draw uses them, and copies the blocks into the atlas, which it fills after first binding it. `ReadTexels` returned the never-uploaded host texture (zeros). Found by running MacNativeGames' build on the same save (their atlas held data). Traced with `DM_NVN_TEXDUMP` (roughness bits in colour target 3), the shader's MSL, `DM_NVN_TEXRAW` on the atlas and `DM_NVN_TEXINFO`, then lldb reading the atlas in their process. +- **Resolved: draws into a 32×32×32 BC4 3D texture were lost.** agl's BC4 block encoder (`0544282c162305db`, the same shader Mario Wonder ships) encodes it one depth plane per pass through an `RGBA16UI` view. It draws through the compressed-target shadow (Breath of the Wild's), with its output declared `uint` in `shader_facts_manual.tsv`. A raw dump at frame 1500 of the new-game route: all 32 planes hold encoded blocks. +- **Resolved: a draw with no render target.** An agl fragment shader that only stores to a storage buffer draws one triangle into a 1×1 viewport with no target (it samples a 64×64 cube map; the game reads the buffer back). With the attachment-less pass, the cave's rock and stone blocks get ambient light they lacked, and Zelda's bloom halo in the descent cutscene is gone. +- **How the tile lighting was traced:** the G-buffer class byte selects per-class half-resolution passes through a depth buffer of class values (`gl_FragDepth = mix(0.5, 1, nibble-swapped class / 255)`, then full-screen or per-tile quads with depth `EQUAL`), so `DM_NVN_DEPTHOVR` (depth test forced to `ALWAYS`) with `HILITE` tells "rejected by depth" from "never rasterized". +- The game issues a depth clear scissored to 8×8 of a 64×64 target outside a pass, which the host still clears whole (`depth-stencil clear with a scissor or partial stencil mask, cleared whole`). +- About one draw per 8-bit index widening, up to a dozen widenings a frame on the title and load screens, no pass split (log line `8-bit index GPU expansion`). +- Its 14,766 vertex shaders have no transform feedback. +- Attachment loads and stores per frame on the Mac went from 1,100 to 700 MB once an inline uniform update inside a render pass stopped ending the pass ([development.md, Power](../../../docs/development.md#power)). + +## Command memory + +- **Resolved (issue #61): `CRASH sig=10` in `mtl_exec` after minutes of play**, the fault address equal to `x19` and shaped like `0x1000000003`. The game gives each model's prerecorded lists fixed slots of the size a recording of that list measured (`GetCommandMemoryUsed`), slot k at base + size × k, and passes `AddCommandMemory` max(size, 512) (`main+0xbb120`); its memory callback is empty. A JUMP the host wrote after a 496-byte list reached 8 bytes into the next slot. The host now never writes past what `GetCommandMemoryUsed` reported (`OP_RESERVE`, [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)). + +## Services + +- SDK 20 calls it needed (each aborted in `sm::Initialize` or read a null applet proxy, one per boot): `oe::GetPreviousProgramIndex`, the hardware Opus decoder's C interface, cache storage, `fs::IsSaveDataExisting` for another title (it asks for Breath of the Wild's save: none), `MountSaveDataReadOnly`, `time::ClockSnapshot::CreateWithStandardSystemClock` ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)). +- `main` imports `nn::ro` (it loads NRO modules at run time). +- Focus handling mode 2 (focus notifications are sent). + +## Files and shaders + +- Most RomFS files are zstd with dictionaries (`*.zs`); the dictionaries are `Pack/ZsDic.pack.zs`, which is also the profile's `romfs_marker`. +- Shaders: `Shader/*.bfsha.zs` (BNSH; `material.bfsha` holds most) and the layout shaders in `Shader/ApplicationPackage.Nin_NX_NVN.release.sarc.zs`. + +## Mac routes + +- The issue #70 save: `DM_KEYS="20:A*6/4,f2480:LUp~5"` (title, load, jump, steer into the pond). +- The in-cave autosave (`run/save`): keys `f2500:A,f2800:A,f3100:A,f3600:A`, shot at frame 4500. `DM_DETCLOCK` cannot be used: the game stops at frame 1 waiting for guest time. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **Conditional rendering** evaluated at replay (`OP_RENDER_COND`, profile key `render_cond`) and **CPU-written textures with no flush** (`tex_dirty_upload`, `ft_add`): [04, Seen in Tears of the Kingdom](../../../docs/agents/04-graphics.md#seen-in-the-legend-of-zelda-tears-of-the-kingdom). +- **Mid-pass, partial-mask and partial-scissor clears** drawn as a triangle (`clear_color_tri` / `clear_ds_tri`), **8-bit indices widened on the GPU** when written in the same submission (`wide_run`), **`ReadTexels` from guest memory on any thread** with `nvn_ft_poll` marking textures written after first use, **attachment-less passes** (`bare_dims`): same section. +- **Rewriter:** out-of-line atomic retry blocks (`retry_block` / `site_retry_window`, 8 sites) and jump tables relative to an `adr` (`jump_adr_offsets`, `main+0x393bb0`): [03, Rewriter](../../../docs/agents/03-bringup.md#rewriter). +- **Slotted command lists** (`OP_RESERVE`, `t_nvn` "slotted lists"): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). + +## Open + +- Texture layout: NVN's automatic block height now truncates for every game (`pick_tile(ah * 3 / 16)`). That changes layouts at 6–10, 17–21, 38–42 and 81–85 block rows, which matters for this game's flushed textures. Not re-checked here. +- `game.tsv` still describes the game as "In bring-up", and some memory values as "Breath of the Wild's", not measured for this game. diff --git a/dm/games/0100F43008C44000/README.md b/dm/games/0100F43008C44000/README.md new file mode 100644 index 0000000..5fa91d2 --- /dev/null +++ b/dm/games/0100F43008C44000/README.md @@ -0,0 +1,79 @@ +# Pokémon Legends: Z-A (`0100F43008C44000`) + +Update 2.0.2 (v393216) over the base game, SDK 19.3.11 (NVN 55). Game Freak's Trinity engine with Havok 2022.2 on NVN. Modules `main` and `sdk`. Started from the provisional profile the NSP script wrote from its survey (`dm/games/profile_guess.py`). Playable on the iPhone at 30 fps with a memory footprint of about 2 GB; on the Mac brought up through the language select, character creation, the opening and the first minutes of play in Lumiose City at a steady 30 fps. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Allocator address space 12 GB → 4 GB | [`patches.py`](patches.py), `main` `b1f12fd9…` at `main+0x1b3e3e4` | `StandardAllocator::Initialize(nullptr, 0x300000000)` does not fit a phone; see [Memory](#memory) | +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `df7a3960…` (`sdk+0x1423fc` / `+0x142468`) | the alias region must hold the node area and the allocator's region | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x110000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | the 4.25 GB alias region holds 62 MB + 4 GB (SDK 19 puts the region 4 MB aligned right after the node area); the 5 GB block Tears of the Kingdom runs in | +| `mem_map_limit on` | `game.tsv` | the SDK keeps the pages of `nninitStartup`'s probe mapped until a map fails; the pool is enforced as on Horizon, so it gives them back | +| Integer output rows `b767103838f729ca`, `c509e77bef5b4249` | [`shader_facts_manual.tsv`](shader_facts_manual.tsv) | no PC version, so the translator declares every output float; these two pixel shaders (variations 0 and 3 of one BNSH in the `imgui.trsha` pack, the `env_ikkaku` system shaders) write an integer ID (255, or one taken from the game's uniforms) to output 1, an `R8_UI` target; Metal refused them on the character creation screen | +| `pad_layout simple` | `game.tsv` | the 2D gamepad layout | + +`render_cond` stays off: it changes nothing in this game. + +## Memory + +- With virtual address memory (a 16 MB system resource), `nninitStartup` (`main+0x1b2e650`) initializes its StandardAllocator over 12 GB of address space, `StandardAllocator::Initialize(nullptr, 0x300000000)` (`main+0x1b3e3ec`), as the Switch's 64 GB alias region allows. +- **Resolved: `StandardAllocator::Initialize` aborted in `nninitStartup`** (`main+0x1b2e690`). The alias region had to hold the SDK's 1 GB node area and that region, 13 GB, more than a phone gives a process (one 6 GB reservation). The two cuts above make it fit the 5 GB block: 4 GB of address space for the allocator (the pool, 3.2 GB, bounds what it maps; its largest block is `nninitStartup`'s own probe of pool − used, 3.15 GB, allocated and freed at once) and Engage's node area cut. SDK 19's AddressRegionManager puts the region right after the node area (+0x3fe2c000 before the cut), not 1 GB aligned as Engage's SDK did, so a 4.25 GB alias region holds both. `QueryMemoryInfo` reporting more than 4 GB would take the game's big-memory path (heaps of allocatable − 4.27 GB instead of − 2.76 GB). +- **Resolved: the alias region's mapped total ran past the application pool in the first minute.** `nninitStartup` allocates and frees everything allocatable (pool − used, 3.15 GB, mapped at the top of the region) and the SDK keeps the freed pages mapped; later allocations map new pages at the bottom until a map fails, and only then does it unmap the cached block and retry. The host never refused a map, so the block stayed mapped beside the 0.4 GB the game used. **Current:** with `mem_map_limit` (`hle_svc.c`) a map past the pool fails with ResultLimitReached, and the used memory `GetInfo` reports counts the mapped pages, as on Horizon (without that, the startup probe itself, sized as total − used, was refused). Measured: one refused 4 MB map, the 3.15 GB unmap, the same map again. +- **Current:** a memory pool created while the game maps and unmaps its 4 GB region was sometimes refused once by `newBufferWithBytesNoCopy`, then accepted. `mtl_pool_add` retries up to 12 times with a short back-off and prints `[RED] memory pool … created at retry N` (one of the red lines on the Mac route); on a final failure it logs the VM regions around the pool. + +## Services + +The import list against the hooks (the survey's `nso_syms.py` table, then `main`'s undefined `nn::` symbols that no `HLE_FORCE` names) left 86 candidates; most only fill the game's own structures (error and web argument builders, `prepo` report buffers, `swkbd` configuration, the audio resampler, `nn::async`). Those that reach a service were hooked before they were met where the answer is clear: `time::ToPosixTime` with an explicit rule (the host's zone, as for `ToCalendarTime`), `album::SaveCurrentScreenshot` (no album: fails), `err::ShowApplicationError` (logged) and the `ShowShopAddOnContentList(UserHandle)` overload. Then one boot each ([03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk)): + +- **Resolved: abort in `sm::Initialize` under `crypto::GenerateCryptographicallyRandomBytes`** (the first call in `nnMain`, `main+0x1734b4`): the bytes come from the csrng service; the host fills them from its entropy source, the same stream as `getentropy` (`hle_os.c`). +- **Resolved: a null read in `audio::GetAudioOutBufferCount` on Wwise's sink wakeup thread** (`sdk+0x1e7ed0`, from `main+0x30faae8`): after every release event Wwise asks how many buffers are still queued and marks the sink starved at 0; the SDK's version read the audout session the host never made. It answers from the host queue: appended and not yet played to the end (`hle_audio.c`). +- **Resolved: NVN state read back through getters** (red `NVN unimplemented function called` lines for `nvnColorStateGetBlendEnable`, `nvnBlendStateGetBlendFunc`, `nvnPolygonStateGetCullFace`, `nvnDepthStencilStateGetDepthTestEnable`, `nvnBufferBuilderGetSize`, `nvnTextureBuilderGetWidth`, …). Trinity builds its pipeline states from state objects it reads back; the red-reporting stub returned whatever was in x0, the object's address, so a blend enable or depth test read as on and a texture builder's width as an address. The getters read the packed state objects (`hle_nvn.c`); the multisample setters the host lacked are recorded in fields `ms_st` already had. +- **Resolved: `nvnCommandBufferFenceSync` unimplemented, every `SyncWait` timed out at once.** The game fences once per frame from a command buffer and waits on it; a record (`OP_FENCE_SYNC`) now fences the sync at replay. +- `WaitForAddress` type 3 (Horizon 18's 64-bit "wait if equal") was added for SDK 19 (`hle_thread.c`); this game does not call it. + +## Files and shaders + +- Almost everything is in one archive, `arc/data.trpfs` (4.4 GB, 13,181 packs, 238,546 files, 17.7 GB unpacked; the profile's `romfs_marker`), indexed by `arc/data.trpfd`; both are FlatBuffers. The descriptor's root table holds the file hashes (u64, sorted), the pack names (`arc/.trpak`), one info table per file (its pack index) and one per pack (size, file count). The file system starts with `ONEPACK\0` and the u64 offset of its own root table: the pack name hashes and the pack offsets. The name hash is not a plain FNV-1a 64 of the descriptor's name, so a pack is named through its first file's hash. A pack is a table of file hashes and file tables `{1 compression i8, 2 level u8, 3 length u64, 4 data}`; compression 3 is Oodle (every file but 15,528 small stored ones), and every Oodle stream is Mermaid (header `8C 0A`, `CC 0A` for an uncompressed chunk) ([01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content)). +- Oodle is linked into `main` and there is no Mermaid decoder with a licence fit for this repository, so the shader split runs the game's own `OodleLZ_Decompress` in the Mac host's tool mode (`dm/host/hle_oodle.c`, `DM_OODLE_SERVE=1`). The host finds it in `main` by the one function that references its name string and calls it as the game does (`main+0x1a9b7bc` on 2.0.2: fuzz safe, no CRC check, thread phase 3). The split's full scan (every file decoded) takes 5 minutes with 12 workers on a 12-core Apple M6. +- Shaders: 28,603 BNSH (11,633 vertex, 16,845 pixel, 125 compute) and 26,051 variation pairs (`shader_split.py` `trpfs_jobs` / `trinity_shaders`): the material shaders in `system_resource/gfx2/shader/NX64/env_ikkaku` (`material/standard.bnsh`, `material/transparent.bnsh`, `system/light.bnsh`, and the `special/imgui.trsha` pack with the engine's system shaders), particle effects (`.ptcl` / `.trtml` in `ik_effectdata`, 19,842 of the shaders), UI layouts (`__ArchiveShader.bnsh` in the `.truiv` packs of `uidata`) and the 40 BNSH in `main`. The layout archives' control sections are 104 KB long, and their stage descriptors run 456 bytes past the control's own length (control +0x20), with bytes that differ between copies of one shader; `same_shader` compares copies only up to the control's own length. + +## Rendering + +Each item was found from a red line or a pass dump on the way through the language screen, character creation and the first minutes in Lumiose City. The general host changes are listed under [Found on this game](#found-on-this-game). + +- **Resolved: characters collapsed into a spike (transform feedback skinning).** The character preview drew one thin spike. Trinity skins on the GPU with transform feedback: a vertex-only program (28 such shaders, e.g. `681cc42e48c0bbc0` in the `special/imgui.trsha` pack; skin indices in attribute 12, weights in 13) runs under `SetRasterizerDiscard` between `BeginTransformFeedback` / `EndTransformFeedback` and writes positions (location 0 xyz, 12 bytes a vertex) and packed normal and tangent bits (locations 1 and 2, x only, 4 bytes) into three feedback buffers; the character draws then read those buffers as vertex streams 0–2 (`RGB32F`, `RGB10A2SN`, `RGB10A2SN`). The host ignored all of it, so the streams kept whatever was there. The pass dump gave it away: a pass of 19 draws with a vertex shader and no fragment shader (`fs=-`) before the character pass. Every buffer in this game holds one output from x on (strides 12/4/4, 12/12/12, 12/4, 12). +- **Resolved: a crash in the driver's compute encoder end, two minutes into a new game** (`SIGSEGV` in `-[AGXG18GFamilyComputeContext_mtlnext performEndEncoding]`, from `rp_begin_ex_` or in its `dealloc` on the encoding thread, about frame 3390 on the Mac, with the encoding thread on and off). Metal validation (`MTL_DEBUG_LAYER=1 MTL_DEBUG_LAYER_ERROR_MODE=nslog`) named it: "Command buffer already has an open command encoder". The transform feedback draw (`tf_draw`) opened its encoder while the copy encoder (`g_blit`) was open; it now ends it first (GitHub issue #101). +- **Resolved: a layered pass whose pipeline could not be built.** Two full-screen vertex shaders (`57246f9a04af8529`, `b29f08619f379a83`: one triangle per instance, layer = instance + offset, 20 pixel shaders read the layer) write `render_target_array_index`, and the translator writes a point size in every vertex shader: no input primitive class fits both. +- **Resolved: `fragmentFunction main0 cannot be used to build a pipeline state`** (a Metal validation assertion) on one imgui pixel shader that reads a texel buffer: SPIRV-Cross declares the optional function constant `spvLinearTextureAlignmentOverride` there. +- **Integer fragment outputs:** the two `shader_facts_manual.tsv` rows above. A scan for outputs built from integer operations was not trusted: the translated code keeps float bits in integer variables (register reuse), so it cannot tell an integer output from raw float bits stored into a float target. +- **Current: a 2D texture in a cube slot** (a placeholder for an environment map; one of the red lines on the Mac route) has no cube view: the zero cube is bound instead (`tex_fit`). +- **Resolved: a black world once the opening reached Lumiose.** The game builds its 32×32×32 colour grading LUT with `b29f08619f379a83`, one instance per depth plane, each writing its plane through `render_target_array_index`, and tone-maps the frame through that LUT. Without a layered render pass every plane went to plane 0 and the rest stayed black. +- **Resolved: striped lighting.** Every lit surface had black and white bands that followed depth. The deferred lighting pixel shader (`ead6e2a32540b19a`, 31 bindings) first reads the depth-stencil buffer through a view whose `nvnTextureViewSetDepthStencilMode` is `STENCIL`, tests stencil bit 6 and discards; the host ignored the mode and bound depth, so the test read bit 6 of the depth value's float bits. Inputs ruled out on the way: the G-buffer albedo, the normals (12-bit pairs packed into 3 bytes, noisy by design), the screen-space shadow mask, the scaled depth copies and conditional rendering. +- **Resolved: characters' legs folded back from the hips once they walked.** From the first step on, the player's legs stuck straight out behind her at hip height, and the rival's forward in a later cutscene; standing still looked right. The bone palette the game computes on the CPU was already bent. The cause was six occlusion queries a frame, one 36-vertex box each (`a793f436dcabd350`, base instance 61, 62, …), drawn against mip level 2 of the depth pyramid: every query came back with 0 samples, and the game took its characters for hidden and posed their legs that way. Forcing every result to visible fixed the legs, and depth testing with ALWAYS did not. The box shader picks its transform with (instance index − base instance) + `c0[0x644]`, where the NVN driver keeps the base instance; the host left `0x644` at 0, so every box was box 0. 793 of the game's vertex shaders read that word. How it was found: the raycasts were logged with scratch observation gates (all correct), then `DM_HLECOUNT` showed nothing walking-specific, then the counter reports. +- **Resolved: a min-reduction sampler averaged.** The game builds that depth pyramid with `SamplerBuilderSetReductionFilter 1` (`151607c7a92386cc` samples the level above at an explicit LOD), which the host used to average. +- **Resolved: image atomics past their buffer.** From about frame 5300 of the route (the scene after the story opening's first cutscene), the fragment shader `2bd5037003980716` does an atomic max into a 480x270 r32ui 2D array; sent to the 64 KB dummy buffer, the atomics ran about 1.4 MB past it. They wrote into zero-copy pools over the game's heap (pointers in the game's objects and the host's program objects got their upper 4 bytes replaced, and the game or the replay crashed) and into unmapped addresses (`kIOGPUCommandBufferCallbackErrorPageFault`, after which macOS ignores the process's command buffers and the picture freezes). An iPhone 17 freezing that way was reported in GitHub issue #101. Under `MTL_SHADER_VALIDATION=1` the scene played. Three runs of the whole route then reached frame 12,300 with no GPU error. + +## Mac routes + +- A fresh save reaches play in Lumiose City with `DM_KBD=Taylor DM_KEYS="f450:A,f1200:LRight~1,f1600:A,f1900:Down,f2000:Down,f2100:A,f2400:A*80/90"`. +- A save made there continues with `DM_KEYS="f300:A*3/150"` (play from about frame 600), and `LUp~12` then walks along the platform. +- A 5-minute walk: GPU about 6 ms a frame, no crash. + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **The pool enforced as on Horizon** (`mem_map_limit`, `hle_svc.c`): [the profile keys](../README.md#keys), [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Trinity packs decoded with the game's own Oodle** (`trpfs_jobs` / `trinity_shaders`, `hle_oodle.c`, `DM_OODLE_HOST` / `DM_OODLE_MODULES`, set by the NSP script for any game with `ONEPACK` files): [04, section 1](../../../docs/agents/04-graphics.md#1-find-every-shader-container), [01, Compressed RomFS content](../../../docs/agents/01-extract.md#compressed-romfs-content). +- **SDK 19 calls:** csrng bytes, `GetAudioOutBufferCount` from the host queue, NVN state getters, `WaitForAddress` type 3, `OP_FENCE_SYNC`: [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **Transform feedback** (`tf_layout`, `vs_tf` / `main0_tf`, `TF_SLOT`, `OP_TF_BIND` / `OP_TF_STATE` / `OP_RAST_DISCARD`, `tf_draw`, `pend_add` kind 4), the **feedback encoder ending `g_blit`**, **layered pipelines** (`vs_layer`, `layer_out`), **function constants specialized** (`shader_load`), the **zero cube for a 2D texture** (`tex_fit`), **layered passes** (`g_rp_layered`, `g_pass.layered`), **stencil-mode views** (`tex_by_id`, `X32_Stencil8`; every depth-stencil texture with pixel-format-view usage, `tex_desc`, PR #106, `t_stencil_view`), **the base instance in the driver segment** (`encode_args`, `c0[0x644]`; `nvn_refl.m` treats any declared driver-segment binding as read), **reduction samplers** (`samp_by_id`) and **pool creation retries** (`mtl_pool_add`): [04, Seen in Pokémon Legends: Z-A](../../../docs/agents/04-graphics.md#seen-in-pokémon-legends-z-a). +- **Image atomics into a scratch buffer as large as the image** (`atom_scratch`): [04, section 6](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). + +## Open + +- The driver-segment word `0x640`, added to the vertex ID by 58 shaders, stays 0 until what the driver puts there is known. +- Transform feedback: only plain array draws are captured; indexed, indirect or instanced ones give a red line. Feedback layouts other than one output per buffer from x on get a red line and no capture entry point. +- `c85de3713170e519` ORs bit fields into output 0 and has not been seen with an integer attachment (no row). +- The development notes list "the render condition the host does not evaluate" among the Mac route's red lines; `render_cond` changes nothing here and stays off. +- `game.tsv`'s header says "Plays on the Mac"; AGENTS.md and the development notes say it is playable on the iPhone. diff --git a/dm/games/0100F5D01DA3A000/README.md b/dm/games/0100F5D01DA3A000/README.md new file mode 100644 index 0000000..62e4649 --- /dev/null +++ b/dm/games/0100F5D01DA3A000/README.md @@ -0,0 +1,45 @@ +# Brain Memory Training Academy (`0100F5D01DA3A000`) + +Base game 1.0.0 (v0), SDK 16.2.4. Unity 2022.3.5f1 IL2CPP with FMOD on NVN. Modules `main`, `subsdk0` (the movie module) and `sdk`; the `sdk` is Unicorn Overlord 1.05's build ([010069401ADB8000](../010069401ADB8000/README.md)). Plays on the iPhone. On the Mac: the title, the controls screen, the level select and the levels with their Level Complete / Level Failed screens, at 60 fps with real sound and saves, no red line; about 0.75 GB resident, flat over 20 minutes. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `12f16103…` (the constructor `AddressRegionManagerC2` at `sdk+0x134768`; the movs at `+0x134778` and `+0x13480c`) | `StandardAllocator::Initialize` aborted in `nninitStartup`; see [Memory](#memory) | +| 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0x118000000`, `mem_stack 0x10000000`, `app_pool 0x40000000` | [`game.tsv`](game.tsv) | Ace Attorney Investigations Collection's layout ([010005501E68C000](../010005501E68C000/README.md)): a 1 GB pool and the 5 GB block (256 MB in front + 4.375 GB alias region + 256 MB stack region + 128 MB heap region) | +| `shader_groups /Resources/=engine /Data/=data` | `game.tsv` | engine built-ins (`Resources/`: `unity default resources`, `unity_builtin_extra`) go to engine, the player's serialized files to data | +| No `pad_layout` (the 2D default) | `game.tsv` | controller only; see [Input](#input) | + +## Memory + +- The NPDM declares 16 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true: `nninitStartup` (`main+0x73ad60`) hands application pool − used memory to StandardAllocator as virtual address memory and never calls `SetMemoryHeapSize`, as in Ace Attorney Investigations Collection. +- **Resolved: an `nn::diag` abort at `sdk+0x126490`** (`StandardAllocator::Initialize`, called from `nninitStartup` at `main+0x73adcc`); the NSP script's heap check reported "the host stopped before nnMain". The `sdk` build is Unicorn Overlord's, whose NPDM declares no system resource, so it never needed the cut; Unicorn Overlord runs the same with it. The constructor takes it at `+0x134778` / `+0x13480c`. +- Measured with probes on `VammManager::AllocateAddressRegion`'s success path (`sdk+0x1348b8`: x21 size, x22 address) and `FreeAddressRegion` (`sdk+0x135100`): StandardAllocator takes 960 MB at +64 MB (it ends at +1.0 GB), and Unity's own regions (`main+0x11bb4cc`) hold about 0.54 GB more. Over four minutes of level loads the live regions end at +2.0 to +2.1 GB of the 4.375 GB alias region. In 20 minutes the mapped high-water mark stopped at +2.51 GB (about 4 minutes in), at most 0.5 GB mapped, about 0.75 GB resident on the Mac. The bring-up run (five minutes of level loads) had the high-water mark at +2.25 GB. +- With this layout the game boots to its title in 5 s with no red line. + +## Input + +- Controller only (stick or D-pad, A, B, +): the game never reads the touch screen, so it stays docked at 1920x1080 and the virtual gamepad starts in the 2D layout. + +## Files and shaders + +- 496 stage shaders in the player's serialized files (`Data/*.assets`, `Data/Resources`) and `main`: 439 engine built-ins, 46 of the game's, 11 of the player's in `main`, and the built-in VR blit's geometry shader, which does not build. +- **Resolved: the split stopped with "no bundle has a Shader type tree of the same hash to borrow".** There are only `Data/*.assets`, `level*` and the two engine files in `Data/Resources`, none with type trees, and no bundle. The `Shader` type hash there (`ea424a5f…`) is the one in Ace Attorney Investigations Collection's bundles (2022.3.17f1): the same layout. `unityfs.py` now carries that layout written out (`KNOWN_TREES`: about 30 structs; `tree_from_layout` builds the tree a file with type trees would have), and the split's second pass gives it to the stripped files when no bundle lent a tree of that hash. +- It calls `GetSharedFontAddress` without importing `ScalableFontEngineHelper::Decode`, so the host gives it the TTF itself (`font_skip`, see [Patrick's Parabox](../0100E44016C22000/README.md)). + +## Mac routes + +- `DM_KEYS="f400:A,f700:A,f1000:A,f1200:Right,f1300:A"` starts a new game, enters level 1 and clears it (Level Complete at about frame 1,350). + +## Found on this game + +General changes first needed here; each applies to every game: + +- **Written-out `Shader` layouts for Unity players with no bundle to borrow a tree from** (`KNOWN_TREES`, `tree_from_layout`, given in the split's second pass): [04, Find every shader container](../../../docs/agents/04-graphics.md#1-find-every-shader-container). +- **The node area cut on an `sdk` build whose first game never needed it** (Unicorn Overlord's): [03, Memory and address space](../../../docs/agents/03-bringup.md#memory-and-address-space). + +## Open + +- The built-in VR blit's geometry shader does not build. +- `game.tsv` has no `sys_resource` row, so the host reports its default, 32 MB (`game.py`, `hle_svc.c`), where the NPDM declares 16 MB. diff --git a/dm/games/0100FD8022DAA000/README.md b/dm/games/0100FD8022DAA000/README.md new file mode 100644 index 0000000..71a09f6 --- /dev/null +++ b/dm/games/0100FD8022DAA000/README.md @@ -0,0 +1,70 @@ +# Super Mario Galaxy 2 (`0100FD8022DAA000`) + +Update 1.4.0 (v393216) over the base game, SDK 18.3.5. NERD's Hagi Wii layer, as in Super Mario Galaxy ([010099C022B96000](../010099C022B96000/README.md)): the Wii game's PowerPC code is statically recompiled into `main`, and Hagi stands in for the Wii hardware (GX on **Vulkan** with SPIR-V generated at run time, the DSP, disc reads from the RomFS). Modules `main`, `subsdk0` (NintendoSDK_movie) and `sdk`, the last two Galaxy's builds; `subsdk1`–`subsdk4` (GLSLC, `nn::pcm`, NVIDIA's OpenGL and Vulkan drivers) are not loaded. Everything Galaxy needed from the Vulkan layer and the host carries over; the profile is Galaxy's with its own memory layout and shader list. Brought up on the Mac (the title, file select by touch, the opening and the first storybook stage at 59 fps) and plays on the iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| Node area cut 1 GB → 62 MB | `dm/tools/sdk_patches.py`, `sdk` build `6075151627016eef…` (shared with Galaxy) | the movie player's 384 MB allocator aborted the game at boot; see [Memory](#memory) | +| 1.25 GB block: `mem_reserve 0x50000000`, `mem_alias 0x20000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | a 512 MB alias region for that allocator; the guessed 256 MB held nothing after the node area | +| `op_mode handheld` | `game.tsv` | the pointer (star bits, menus, the file select) is the touch screen in handheld mode; docked it is the Joy-Con gyro, which the host does not have | +| `pad_layout full` | `game.tsv` | the 3D gamepad layout | +| `modules main subsdk0 sdk` | `game.tsv` | `subsdk1`–`subsdk4` are replaced by the host's Vulkan layer | +| `vk_capture.tsv` | profile | the keys of the SPIR-V modules Hagi generates at run time, captured by the NSP script as for Galaxy | + +## Memory + +- `main`'s bss is about 2.6 GB (the emulated memory). NPDM system resource 8 MB. As in Galaxy: no `SetHeapSize`; the game's memory is `main`'s bss. +- **Resolved: `nn::diag` abort at boot** from `StandardAllocator::Initialize`, long after `nninitStartup`, right after the first Vulkan query pool (`sdk+0x12dad0` ← `main+0x62c52c`). The movie player initializes its own StandardAllocator with virtual address memory, `Initialize(nullptr, 0x18000000)` (384 MB, `main+0x62c280`); Galaxy, on the same SDK, allocates that block from its main allocator first. The guessed 256 MB alias region holds no region after the SDK's 1 GB address-region node area. Fixed by the node area cut for this `sdk` build (`sdk+0x13be4c` / `+0x13bee0`, shared with Galaxy) and a 512 MB alias region: the region is 32 MB aligned, so it starts 64 MB in. +- The block is 1.25 GB = 256 MB in front + 512 MB alias region + 256 MB stack region + 256 MB heap region; with `main`'s 2.7 GB image about 4 GB of address space, as Galaxy's. +- Footprint on the Mac (M4 Pro, the opening's first stage) 1.93 GB, flat from 2 to 5 minutes (1.0 GB of it Metal). + +## Shaders + +- 3,797 SPIR-V modules, all created about 7 s after launch (Galaxy: 3,089), captured by the NSP script as for Galaxy (`vk_capture.tsv`). +- The same two geometry shaders (GX points and lines) are skipped: those draws come out as one-pixel points and lines. +- `romfs_marker` is `hagi.config`. + +## Rendering + +- **Resolved (GitHub issue 72): the half of Mario facing the camera's left was white** on an iPhone. spirv-cross writes `FMin` / `FMax` / `FClamp` as `fast::min` / `max` / `clamp`, which leave a NaN operand's result to the GPU. Galaxy's GX lighting normalizes light directions the game leaves at (0, 0, 0) (the key and fill lights of its characters) and takes `max(0, dot(ldir, NaN))` as the spot term: 0 on the M4 Pro with either function, so the attenuation is 1. The fill light sits at the camera's left in view space, so its terminator runs down the middle of a character facing the camera. `vk_shaders.py` (and `dmsh_build.py`, `gl_shaders.py`) now write `precise::` functions, which return the other operand as Maxwell's `FMNMX` does. +- **Resolved (GitHub issue 68): a crash in `bind_res` on an iPhone when the ship took off after the first galaxy.** The replay binds every element of an array the shader takes, and a set may still name an image view the game has destroyed (the hardware reads a descriptor only when a draw samples it). The layer's descriptors were pointers to its own view, sampler and buffer structs; the crash read a freed view's texture from element 1 of the 8-texture array. A descriptor now holds the texture, sampler state and buffer it was written with (retained), as a hardware descriptor holds a copy of the handle. Through the opening and the first storybook stage the game allocates 22,785 descriptor sets and frees none, and destroys only 5 image views (and 1,244 images), so a stale element is rare: a Mac run that kept destroyed objects to catch their binds found none there. + +## Frame rate + +- **Resolved: on the iPhone only, a steady game's frame-rate counter wandered between 50 and 60.** The `display link up to tick` lines showed about as many empty ticks as frames dropped by merging: frames arrived two in one tick and none in the next. The Vulkan layer's acquire and the `vi::GetDisplayVsyncEvent` thread each kept a 60 Hz clock of their own, and Galaxy runs the Wii's VI retrace on that event, so its frames drifted against the screen. While the display link presents, both follow its ticks now (`hle_vk.m` acquire → `mtl_link_wait_tick`, as NVN's; `hle_sdk16.c` vsync thread → `mtl_link_wait_vsync`), with the old clocks as the fallback. A Mac window measures nothing here unless it is on screen: macOS fires an occluded window's display link about once a second (`display link recovered: stalled 1017 ms`). + +## CPU + +- Hagi's sleep poll (`main+0x11ec`) is one of the polls that keep x19–x28 between yields (the yield-progress rule, `thr_yield`). + +## Background + +Mac repro of both: `DM_OE_SCRIPT=bg=1@20,bg=0@25`. The game froze after switching apps and back, picture still, sound on, for two causes: + +- **Resolved: the game's own pause deadlocked** (the main loop waits in its pause handshake, `main+0x5d10d4` / `main+0x5cea68`). The game sets focus handling mode 3, which the SDK (`oe::SetFocusHandlingMode`, SDK 18 `sdk+0x3c5770`) turns into "no focus notifications, the system suspends the game out of focus"; only modes 1 and 2 notify. The host sent message 15 anyway, and Galaxy 2's Wii layer ran a pause the console never triggers. `hle_oe.c` now queues 15 only under modes 1 and 2 (or when no mode was set). +- **Resolved: one guest thread at 100% after the return, in a tight loop** (`main+0x9abd0`, `x10 = 0`). The frame pacing measured a frame period of 0 (`main+0x5926a8`: the clock difference between two frames). The host froze the guest clock in the background but its threads went on, so two frames read the same time. Guest reads of the system counter (the `cntpct` gate, `svc GetSystemTick`) now wait while the app is in the background (`hle_os.c` `guest_clock_read_wait`). + +## Damaged NSP + +- The base NSP in hand fails the hash check on `data/texture_replace/MarioGalaxy.hix` and `MarioGalaxy_Sub.har`; it was packed with `--allow-damaged`. + +## Mac routes + +- From a fresh save, `DM_KEYS="f1600:A,f2000:T0.268x0.579,f2400:T0.641x0.778,f2800:T0.128x0.426,f3200:T0.641x0.778,f3600:T0.659x0.366,f4000:T0.8125x0.889,f4500:A*6/300,f6000:LRight+A~1.2*200/100,f6050:Y*200/100"` creates file 1 (Mario icon, Regular Mode), plays the opening and walks the first storybook stage to the spin tutorial and on (about frame 11,000). `DM_KEYS` runs its steps one after another, so a hold blocks the steps after it: walking and jumping go in one step (`LRight+A`). + +## Found on this game + +General host changes first needed here; each applies to every game: + +- **`precise::` min / max / clamp** in every translator (`dmsh_build.py` `precise`, `vk_shaders.py`, `gl_shaders.py`; issue 72): [04, Other per-title surprises seen in Mario](../../../docs/agents/04-graphics.md#6-other-per-title-surprises-seen-in-mario). +- **Vulkan descriptors hold the Metal objects** (issue 68): [04, Seen in Super Mario Galaxy (Vulkan)](../../../docs/agents/04-graphics.md#seen-in-super-mario-galaxy-vulkan). +- **Vulkan acquire and the vsync event thread follow the display link** (`mtl_link_wait_tick`, `mtl_link_wait_vsync`): [03, Frame rate](../../../docs/agents/03-bringup.md#frame-rate). +- **Focus message 15 only under focus handling modes 1 and 2** (`hle_oe.c`) and **the guest clock waits in the background** (`guest_clock_read_wait`): [03, Services and SDK](../../../docs/agents/03-bringup.md#services-and-sdk). +- **A yield whose callee-saved registers moved counts as progress** (`thr_yield`; this game's poll keeps them): [03, Threads](../../../docs/agents/03-bringup.md#threads). + +## Open + +- GX points and lines (the geometry shaders) are not translated; the player README lists one-pixel point and line effects as a known issue. +- `game.tsv` says values marked "Galaxy's" are not measured for this game; none of its rows is marked so. diff --git a/dm/games/README.md b/dm/games/README.md index 8fb88d7..c3e553d 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -9,6 +9,7 @@ Each game the port knows has a folder `dm/games//`, named after the base gam - **`vk_capture.tsv`** (a Vulkan game that generates its shaders at run time, Super Mario Galaxy): the keys of the SPIR-V modules it generates. - **`gl_capture.tsv`** (an OpenGL game that puts its GLSL together at run time, Balatro): the files (`.vert` / `.frag`) of the shaders it compiles. - **`bf_want.tsv`**. +- **`README.md`**: the game's notes: what its profile needs and why, what was found on it, how to reach a scene on the Mac, what is still open. Listed in [Games](#games). - **`patches.py`** (optional): the rewriter's patches of the game's own builds (`main`), keyed by NSO build ID: `CUTS` (byte cuts, each original asserted), `OBS` (observation gates) and `FRAME_STEP` (frame-step stubs). `dm/tools/patch_nso.py` loads every profile's file and applies those of a module's build, wherever it comes from; a build claimed twice stops the rewrite. Cuts of Nintendo's `sdk` modules, which several games ship, are in `dm/tools/sdk_patches.py`. A game without patches has no file, and a derived profile never has one. `dm/tools/tests/test_patches.py` checks that a file names only its own profile's builds. The chain version is not in the profile: one version serves every game (`dm/gfx/nvn/dmsh_chain.h`, written by `dmsh_chain.py --header`). Two more tables live in the profile folder but are not committed (git ignores them). A full `dmsh_build.py` run makes both from its Mac outputs, so each checkout makes its own, and stamps its libraries with the digest of the profile's inputs (`b-/inputs.tsv`, `dmsh_chain.py --inputs`). The host takes a table only when its header names its library's chain version and that digest. @@ -30,6 +31,71 @@ None of them has a default game: without one they stop and list the profiles. To **Launcher settings.** `settings.tsv` lists the player-facing settings once (the environment variable each sets, its kind, default and the condition under which a game offers it; its header describes the columns). `build_ipa.sh` runs `settings.py `, which keeps the rows that hold for the game: a profile macro (`DM_GAME_FPS_TARGET!=0`), a key in `main`'s rewriter table (`RENDER_SCALE`, written by `patch_nso.py` for the builds with a render-scale stub), the game's languages. It writes them into the app as `dm/launcher.json`, from which the iOS launcher (`dm/ios/Sources/Launcher.swift`) draws its screen. A new player setting is one row; diagnostic switches stay environment-only. +## Games + +One row per profile; the notes are each profile's `README.md`. + +| Game | Title ID | Build | SDK | Engine | Graphics | +|---|---|---|---|---|---| +| [13 Sentinels: Aegis Rim](01003FC01670C000/README.md) | `01003FC01670C000` | 1.10 | 11.4.3 | Vanillaware's engine | NVN | +| [Absolum](0100A83022A22000/README.md) | `0100A83022A22000` | 1.1.1 | 18.3.2 | FNA / .NET NativeAOT (FNA3D's NVN driver) | NVN | +| [Ace Attorney Investigations Collection](010005501E68C000/README.md) | `010005501E68C000` | 1.0.0.2 | 17.5.3 | Unity 2022.3.17f1 IL2CPP | NVN | +| [Advance Wars 1+2: Re-Boot Camp](0100300012F2A000/README.md) | `0100300012F2A000` | 1.0.0 (base) | 10.4.1 | Unity 2020.2.1f3 IL2CPP | NVN | +| [AI: THE SOMNIUM FILES](010089B00D09C000/README.md) | `010089B00D09C000` | 1.0.1 | 6.4.2 | Unity 2017.4.17f1 IL2CPP | NVN | +| [AI: THE SOMNIUM FILES - nirvanA Initiative](0100713016CCC000/README.md) | `0100713016CCC000` | 1.0.2 | 13.3.5 | Unity 2020.3.30f1 IL2CPP | NVN | +| [ANIMAL WELL](010020D01AD24000/README.md) | `010020D01AD24000` | 1.0.10 | 20.5.7 | its own (not named) | NVN | +| [Animal Crossing: New Horizons](01006F8002326000/README.md) | `01006F8002326000` | 3.0.3 | 20.5.4 | Nintendo EPD (sead / agl / gsys) | NVN | +| [BALL x PIT](010086A022444000/README.md) | `010086A022444000` | 1.293 | 20.5.19 | Unity 6000.0.62f1 IL2CPP | NVN | +| [Balatro](0100CD801CE5E000/README.md) | `0100CD801CE5E000` | 1.1.3 | 18.3.2 | LÖVE 11.4 (love-nx, LuaJIT, SDL) | OpenGL | +| [Bayonetta 3](01004A4010FEA000/README.md) | `01004A4010FEA000` | 1.2.0 | 13.3.2 | PlatinumGames' engine | NVN | +| [Big Brain Academy: Brain vs. Brain](0100620012D6E000/README.md) | `0100620012D6E000` | 1.2.0 | 11.4.3 | NintendoWare Bezel Engine 1.56 | NVN | +| [Brain Memory Training Academy](0100F5D01DA3A000/README.md) | `0100F5D01DA3A000` | 1.0.0 (base) | 16.2.4 | Unity 2022.3.5f1 IL2CPP | NVN | +| [Captain Toad: Treasure Tracker](01009BF0072D4000/README.md) | `01009BF0072D4000` | 1.4.2 | 8.2.0 | Nintendo EPD Tokyo (`al` over sead / agl) | NVN | +| [Catherine: Full Body](0100BF00112C0000/README.md) | `0100BF00112C0000` | 1.0.1a | 8.2.1 | Atlus GFD (CRI middleware) | NVN | +| [Celeste](01002B30028F6000/README.md) | `01002B30028F6000` | 1.4.1.1 | 15.3.2 | MonoGame (C# → C++ by BRUTE) | NVN | +| [Cuphead](0100A5C00D162000/README.md) | `0100A5C00D162000` | 1.3.7 | 5.5.0 | Unity 2017.4.9f1 IL2CPP | NVN | +| [DARK SOULS: REMASTERED](01004AB00A260000/README.md) | `01004AB00A260000` | 1.0.3 | 4.5.0 | FromSoftware's engine | NVN | +| [Digimon Story Cyber Sleuth: Complete Edition](010014E00DB56000/README.md) | `010014E00DB56000` | 1.0.3 | 6.4.0 | Media.Vision MVGL | OpenGL | +| [DRAGON QUEST VII Reimagined](0100A9D01C446000/README.md) | `0100A9D01C446000` | 1.2.0 | 19.3.1 | Unreal Engine 4.27 | NVN | +| [DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition](01006C300E9F0000/README.md) | `01006C300E9F0000` | 1.0.4 | 7.3.2 | Unreal Engine 4.18 | NVN | +| [FINAL FANTASY RESONANCE](0100606026F12000/README.md) | `0100606026F12000` | 1.0.1 | 20.5.17 | Unreal Engine 5.6.1 | NVN | +| [FINAL FANTASY TACTICS - The Ivalice Chronicles](010038B015560000/README.md) | `010038B015560000` | 1.4.0 | 19.3.9 | Square Enix's engine | NVN | +| [Fire Emblem Engage](0100A6301214E000/README.md) | `0100A6301214E000` | 2.0.0 | 12.3.7 | Unity 2020.3.18f1 IL2CPP, Wwise | NVN | +| [Fire Emblem: Three Houses](010055D009F78000/README.md) | `010055D009F78000` | 1.2.0 | 6.5.0 | Koei Tecmo's KTGL | NVN | +| [Hades](0100535012974000/README.md) | `0100535012974000` | 1.0.38233 | 9.3.0 | Supergiant Games' engine (The Forge) | Vulkan | +| [Hades II](0100A00019DE0000/README.md) | `0100A00019DE0000` | base game | 19.3.9 | Supergiant Games' engine | NVN | +| [Hollow Knight: Silksong](010013C00E930000/README.md) | `010013C00E930000` | 1.0.30000 | 20.5.8 | Unity 6000.0.50f1 IL2CPP | NVN | +| [Hyrule Warriors: Age of Calamity](01002B00111A2000/README.md) | `01002B00111A2000` | 1.3.0 | 9.6.0 | Koei Tecmo's KTGL | NVN | +| [Luigi's Mansion 3](0100DCA0064A6000/README.md) | `0100DCA0064A6000` | 1.4.0 | 7.3.2 | Next Level Games' engine | NVN | +| [Mario Kart 8 Deluxe](0100152000022000/README.md) | `0100152000022000` | 4.0.0 | 4.4.0 | Nintendo (sead / agl / gsys, NintendoWare, Pia) | NVN | +| [Mario vs. Donkey Kong](0100B99019412000/README.md) | `0100B99019412000` | 1.0.1 (cartridge base) | 15.3.0 | Nintendo (sead / agl / gsys) | NVN | +| [Metroid Dread](010093801237C000/README.md) | `010093801237C000` | 2.1.0 | 11.4.3 | MercurySteam's engine | NVN | +| [Monster Hunter Rise](0100B04011742000/README.md) | `0100B04011742000` | 16.0.2 | 11.4.3 | Capcom's RE Engine | NVN | +| [Monster Train 2](010051701FB46000/README.md) | `010051701FB46000` | 1.1.1 | 18.3.5 | Unity 2022.3.54f1 IL2CPP | NVN | +| [Octopath Traveler 0](01005270232F2000/README.md) | `01005270232F2000` | 1.0.5 | 19.3.13 | Unreal Engine 5 | NVN | +| [Paper Mario: The Thousand-Year Door](0100ECD018EBE000/README.md) | `0100ECD018EBE000` | 1.0.1 | 15.3.2 | Intelligent Systems' engine on NintendoWare | NVN | +| [Patrick's Parabox](0100E44016C22000/README.md) | `0100E44016C22000` | 1.0.1 | 13.3.7 | Unity 2020.3.30f1 IL2CPP | NVN | +| [Pikmin 4](0100B7C00933A000/README.md) | `0100B7C00933A000` | 1.1.1 | 10.4.8 | Unreal Engine 4.26.2 | NVN | +| [Pokémon Legends: Z-A](0100F43008C44000/README.md) | `0100F43008C44000` | 2.0.2 | 19.3.11 | Game Freak's Trinity engine | NVN | +| [Pokémon Sword](0100ABF008968000/README.md) | `0100ABF008968000` | 1.3.2 | 7.3.2 | Game Freak's engine (before Trinity) | NVN | +| [Raging Loop](010020400E1C2000/README.md) | `010020400E1C2000` | 1.0.0 (base) | 7.3.2 | Unity 2018.3.10f1 IL2CPP | NVN | +| [Rhythm Heaven Groove](0100D9F01D474000/README.md) | `0100D9F01D474000` | 1.0.0 (base) | 21.4.0 | Nintendo's own 2D engine | NVN | +| [ROMANCE OF THE THREE KINGDOMS 8 REMAKE](0100C2A01A730000/README.md) | `0100C2A01A730000` | 1.1.3 | 17.5.3 | Koei Tecmo's KTGL | NVN | +| [STEINS;GATE RE:BOOT](01008BC024FDE000/README.md) | `01008BC024FDE000` | 1.0.3 | 20.5.17 | M2's ADV engine | NVN | +| [Stardew Valley](0100E65002BB8000/README.md) | `0100E65002BB8000` | 1.6.15.13 | 20.5.27 | MonoGame (C# → C++ by BRUTE) | NVN | +| [Super Mario 3D World + Bowser's Fury](010028600EBDA000/README.md) | `010028600EBDA000` | 1.2.2 | 10.4.0 | Nintendo EPD (sead / agl, NintendoWare) | NVN | +| [Super Mario Bros. Wonder](010015100B514000/README.md) | `010015100B514000` | 1.2.1 (and 1.0.0) | 16.2.12 (1.0.0: 16.2.0) | Nintendo EPD (sead / agl) | NVN | +| [Super Mario Galaxy](010099C022B96000/README.md) | `010099C022B96000` | 1.3.1 | 18.3.5 | NERD's Hagi Wii layer (recompiled PowerPC) | Vulkan | +| [Super Mario Galaxy 2](0100FD8022DAA000/README.md) | `0100FD8022DAA000` | 1.4.0 | 18.3.5 | NERD's Hagi Wii layer (recompiled PowerPC) | Vulkan | +| [Super Mario Odyssey](0100000000010000/README.md) | `0100000000010000` | 1.4.1 | 7.3.2 | Nintendo EPD (sead / agl, NintendoWare) | NVN | +| [Super Smash Bros. Ultimate](01006A800016E000/README.md) | `01006A800016E000` | 13.0.5 | 8.2.1 | Bandai Namco / Sora's engine | NVN | +| [Taiko Risshiden V DX](0100346017304000/README.md) | `0100346017304000` | 1.1.1 | 12.3.3 | Koei Tecmo's KTGL | NVN | +| [The Great Ace Attorney Chronicles](010036E00FB20000/README.md) | `010036E00FB20000` | 1.0.0 (base) | 10.4.1 | Capcom's MT Framework | NVN | +| [The Legend of Zelda: Breath of the Wild](01007EF00011E000/README.md) | `01007EF00011E000` | 1.9.0 | 20.5.22 | Nintendo EPD (sead / agl / gsys / ksys, Havok) | NVN | +| [The Legend of Zelda: Link's Awakening](01006BB00C6F0000/README.md) | `01006BB00C6F0000` | 1.1.0 | 8.2.0 | Grezzo's engine on NintendoWare | NVN | +| [The Legend of Zelda: Tears of the Kingdom](0100F2C0115B6000/README.md) | `0100F2C0115B6000` | 1.4.3 | 20.5.22 | Nintendo EPD (sead / agl / gsys) | NVN | +| [Unicorn Overlord](010069401ADB8000/README.md) | `010069401ADB8000` | 1.05 | 16.2.4 | Vanillaware's engine | NVN | + ## Keys | Key | Meaning | diff --git a/docs/agents/04-graphics.md b/docs/agents/04-graphics.md index 344eab3..37cf04f 100644 --- a/docs/agents/04-graphics.md +++ b/docs/agents/04-graphics.md @@ -161,7 +161,7 @@ So the translator leaves z alone (`QueryGraphicsState` depth mode false in `Prog - **Cube textures sampled as cube arrays.** 1,512 shaders sample the environment probes as `texturecube_array` while a single cube texture is bound. Metal can make a cube-array view only of a 2D-array base texture, so NVN cube textures are now created as 6-layer 2D arrays and bound through cube / cube-array views by `tex_fit` (which also maps a cube array read as a cube to its first cube). - **Fixed: a compressed texture as a render target.** agl's BC4 block encoder (`8e2cc2671cae6020`) writes a 32×32 RGTC1 (BC4) texture through an `RGBA16UI` view (one texel per 8-byte block: BC4 encoded on the GPU). Metal can neither attach a compressed format nor make an uncompressed view of one, and the pipeline-descriptor validation aborts the process, so the attachment used to be left out and its draws did nothing (`render target is a compressed-format texture …`). Such a pass now draws into an uncompressed shadow of blocks × blocks in the view's format: the level's (or layer's, or depth plane's) blocks are copied into it through a buffer before the pass and back after it (`bcrt_attach` / `bcrt_flush` in `nvn_mtl.m`; the method comes from PR #11). The shadows stay resident and are reused, and they are kept out of the load-skip and store-elision bookkeeping. The encoder's output must be declared `uint`: the game has no PC version, so the profile's `shader_facts_manual.tsv` lists it (without it the pipeline fails and the red line prints the row to add), and so does its twin for a 2D source (`93aa5e22a8202409`), whose failure showed up in an iPhone run. - **Big-stack NVN entry points**: the render thread's stack is small, and Metal's driver compiles internal programs (a fast-clear blit) on the calling thread while the host ends a render pass inside `nvnQueueSubmitCommands`. That overflowed the guest stack (SIGBUS in `AGCDeserializedReply`, address in the guest stack region). All implemented NVN functions except the `nvnCommandBuffer*` recording calls now run on the per-thread 512 KB host stack ([03](03-bringup.md), big-stack entry; `nvn_big_entry` in `nvn_entry.S`). -- **60 fps** is now the host's frame step in whole vsyncs ([development.md](../development.md), Breath of the Wild): a stub at the framework's `setWaitVsyncInterval` hands the game the host's step, and frames stay on screen exactly that long. The notes below are about the method before it, which `DM_FRAME_STEP=legacy` still runs: a rewriter cut on the game's frame-interval logic ([03](03-bringup.md)); the engine's variable frame rate (`ksys::VFR`) scales its time step by the present interval, so the speed is right as long as the interval matches the frames. **Fixed: slow motion when the GPU is the bottleneck (GitHub issue #9).** The interval controller (`main+0x112660c`) measured a frame as framework +0x250, the main thread's CPU span from the start of `procFrame` to before its draw step (`main+0x1126cec` → `main+0x1126d20`): about 2.7 ms in the shrine (probe at `main+0x11266a4`, `@x19+0x250`). Only while the object at `[main+0x1e3a638] + 0x1d2b8` exists did it take max(CPU span, GPU time), in 64 of 6,046 shrine frames. So the interval stepped up only on CPU spikes: an iPhone 16 Pro Max at a 25 ms frame p50 never left interval 1 in 4,800 frames, and the game ran at about two thirds of its speed. The GPU time (framework +0x220) is the difference between the frame's two timestamp reports: `TIMESTAMP_TOP` at the start of the frame's command buffer (`main+0xe94584`) and `TIMESTAMP` at its end (`main+0xe94664`), read after the draw step has waited for the frame's fence (`main+0xe949bc`). It read 0–6 ticks, because the host wrote all 5 reports of the frame's one submission with one time in its completion handler. Two more cuts (`main+0x112691c`, `main+0x1126974`: cbz → b) make the controller always take the GPU path. The profile key `gpu_timestamps segment` makes each report take the GPU start (`TIMESTAMP_TOP`) or end (other types) of the `sub_cut` segment it was encoded in (`rep_ticks` in `nvn_mtl.m`: MTL4CommitFeedback times converted to the guest clock, written in the last segment's handler as before). The GPU time now reads the frame's GPU span, gaps where the GPU waits for the host's translation included. Measured on the Mac with two BotW runs sharing the GPU under a heavy CPU load (load average about 75, a shader rebuild running): GPU time p50 16–23 ms; the fixed run stayed at interval 2 (3 while the GPU time passed 1.9 vsyncs, 1 for under 2% of the frames) at 0.93–0.98 of real time (Σ VFR ratio / 30 per wall second, probe `main+0x151d3e0` `@x0+0x30`: +0x30 ratio, +0x34 interval). At the same time a run with the old timestamps (`DM_NVN_GPU_TS=submission`) spent 10–70% of its frames at interval 1 and ran at 0.78–0.95. The same GPU time drives the game's dynamic resolution (`main+0x1267070`): a running average as a percentage of a 60 Hz frame scales the resolution down above 175% (29 ms) and back up below 150% (25 ms). With 0 it always sat at its maximum, and it still does whenever the game holds 30. +- **60 fps** is now the host's frame step in whole vsyncs ([Breath of the Wild's notes](../../dm/games/01007EF00011E000/README.md)): a stub at the framework's `setWaitVsyncInterval` hands the game the host's step, and frames stay on screen exactly that long. The notes below are about the method before it, which `DM_FRAME_STEP=legacy` still runs: a rewriter cut on the game's frame-interval logic ([03](03-bringup.md)); the engine's variable frame rate (`ksys::VFR`) scales its time step by the present interval, so the speed is right as long as the interval matches the frames. **Fixed: slow motion when the GPU is the bottleneck (GitHub issue #9).** The interval controller (`main+0x112660c`) measured a frame as framework +0x250, the main thread's CPU span from the start of `procFrame` to before its draw step (`main+0x1126cec` → `main+0x1126d20`): about 2.7 ms in the shrine (probe at `main+0x11266a4`, `@x19+0x250`). Only while the object at `[main+0x1e3a638] + 0x1d2b8` exists did it take max(CPU span, GPU time), in 64 of 6,046 shrine frames. So the interval stepped up only on CPU spikes: an iPhone 16 Pro Max at a 25 ms frame p50 never left interval 1 in 4,800 frames, and the game ran at about two thirds of its speed. The GPU time (framework +0x220) is the difference between the frame's two timestamp reports: `TIMESTAMP_TOP` at the start of the frame's command buffer (`main+0xe94584`) and `TIMESTAMP` at its end (`main+0xe94664`), read after the draw step has waited for the frame's fence (`main+0xe949bc`). It read 0–6 ticks, because the host wrote all 5 reports of the frame's one submission with one time in its completion handler. Two more cuts (`main+0x112691c`, `main+0x1126974`: cbz → b) make the controller always take the GPU path. The profile key `gpu_timestamps segment` makes each report take the GPU start (`TIMESTAMP_TOP`) or end (other types) of the `sub_cut` segment it was encoded in (`rep_ticks` in `nvn_mtl.m`: MTL4CommitFeedback times converted to the guest clock, written in the last segment's handler as before). The GPU time now reads the frame's GPU span, gaps where the GPU waits for the host's translation included. Measured on the Mac with two BotW runs sharing the GPU under a heavy CPU load (load average about 75, a shader rebuild running): GPU time p50 16–23 ms; the fixed run stayed at interval 2 (3 while the GPU time passed 1.9 vsyncs, 1 for under 2% of the frames) at 0.93–0.98 of real time (Σ VFR ratio / 30 per wall second, probe `main+0x151d3e0` `@x0+0x30`: +0x30 ratio, +0x34 interval). At the same time a run with the old timestamps (`DM_NVN_GPU_TS=submission`) spent 10–70% of its frames at interval 1 and ran at 0.78–0.95. The same GPU time drives the game's dynamic resolution (`main+0x1267070`): a running average as a percentage of a 60 Hz frame scales the resolution down above 175% (29 ms) and back up below 150% (25 ms). With 0 it always sat at its maximum, and it still does whenever the game holds 30. - **Render scale (`DM_RENDER_SCALE`)**: the dynamic-resolution method (`main+0x12666e4`) keeps its scale in s8 (0.8–1.0 in 0.05 steps, or the forced 0.75 / 0.5 of `+0x284 & 0x1800`) and applies it from `main+0x1266880`: the views' scale (`+0x28a0` / `+0x28a4`), from which the size function (`main+0x106340c`) sizes the scene buffers each frame, the shader ratio `uking_dynamic_resolution_ratio` and the other readers. That block runs only when the dynamic path has just recomputed the scale (`+0x280` bit 4, set at `main+0x1266830`) or in a forced mode, else `main+0x126686c` skips it. The stub sets s8 at the block and, for a factor other than 1.0, takes the block every frame, so the factor holds wherever the dynamic path does not run. A multiply in the size function (Tears of the Kingdom's way) would leave the ratio and those readers at the old scale. - **Fixed: the iPhone app was killed for memory in the first shrine (GitHub issue #9, iPhone 16 Pro Max, handheld).** An 8 GB iPhone gives this app about 3.4 GB (resident plus `headroom to the system memory limit` stayed at 3,375 MB). Resident memory grew about 0.2 MB a frame from 2.6 GB at the shrine load until the kill, while the residency set and the texture counts stayed flat. The cause was the deferred-release drain running on the guest submit thread with no autorelease pool, the Metroid Dread leak (`defer_drain` now has its own `@autoreleasepool`): on the Mac a host without that pool grew 1 MB a frame in the shrine (GPU 3.0 → 4.4 GB in 2,400 frames); with it the shrine stays at 2.7–2.9 GB resident and 1.5–1.7 GB GPU, idle or walking (vmmap at 2.8 GB: about 1.6 GB guest memory, 0.75 GB private textures, 0.17 GB IOAccelerator). Found on the way: (1) about 10 serial-table entries a frame were never deleted: the game records its transient render targets (the HUD captures, the 640×360 buffers) and finalizes them before it submits, and replay put the dead serials back into `g_by_serial` (276,000 after 25,000 frames). Dead serials now go to a 512-entry table (`tex_put`, `g_ghost`; the memory line's `finalized textures still referenced`). (2) The memory-pool tracking table had a fixed 4,096 slots and the phone reached 4,006 pools; a pool past the end would have its textures re-uploaded at every use. It now grows in blocks of 1,024 (`nvn_dirty.c`, up to 262,144). - **Address → memory pool lookups**: BotW keeps about 1,760 memory pools alive, and `mtl_addr` / `mtl_gpu_addr` scanned them all for every buffer binding of every draw: a CPU sample of the submit thread had about 1,000 of its samples under those scans (vertex buffers in `draw`, uniform buffers in `encode_args`). They now try the last hit, then a binary search over pools sorted by start address (`pool_find`, rebuilt when pools are added or removed). Host submit time in the shrine went from 21 ms to 7 ms per frame (p50), the median frame from 38 ms to 21 ms. diff --git a/docs/development.md b/docs/development.md index 9141d78..9e87540 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,6 +1,6 @@ # Development -The README covers packing and playing. This page is for working on nxnative itself: how it is built, the script's options, running games on the Mac, adding a game, and per-game notes. Agent-oriented details (bring-up failure catalogue, graphics specifics, debugging recipes) are in [docs/agents](agents/README.md). +The README covers packing and playing. This page is for working on nxnative itself: how it is built, the script's options, running games on the Mac, and adding a game (per-game notes are in [dm/games](../dm/games/README.md#games)). Agent-oriented details (bring-up failure catalogue, graphics specifics, debugging recipes) are in [docs/agents](agents/README.md). ## How it works @@ -278,543 +278,7 @@ The game's own work and its frame rate stay as they are; the host avoids work of ## Game notes -**Super Mario Bros. Wonder (`010015100B514000`)** -- **Versions:** v0 (1.0.0) and 1.2.1 (v327680). With the update layered over v0, unchanged files become APFS clones, so the merged RomFS adds only about 114 MB to the 3.5 GB base. -- **Shaders:** 12,811 in v0 and 16,268 across both versions; the tables hold both. -- **1.2.1:** it is a new `main` build (SDK 16.2.12), whose resource-heap cut (+16 KB slack) was carried over with `dm/tools/nso_fnmap.py`. It also makes new SDK calls, which the host answers: `nn::nfp` finds no amiibo, `pl::IsRunningOnOunce` is false, the shop call is a no-op, and `account::PopOpenUsers` returns none. -- **Mac input:** `DM_KEYS="12:A,14:A,20:A,26:A,32:A"` reaches the opening course. - -**Fire Emblem Engage (`0100A6301214E000`)** -- **Engine:** Unity 2020.3 IL2CPP, SDK 12.3.7. The shaders are read out of Unity bundles (`unityfs.py`). -- **Memory:** a 5 GB guest block, the total proven on a phone, with a 2.5 GB application pool. A per-build cut shrinks the SDK's address-region node area to 62 MB. -- **Audio:** Wwise drives AudioOut directly. -- **Movies:** VP9, decoded on the host. -- **60 fps:** the Switch version runs at 30; one patch to its code switches it to the 60 fps timing the game already has, so nothing runs too fast. - -**The Legend of Zelda: Breath of the Wild (`01007EF00011E000`)** -- **60 fps at normal speed (30 on the Switch).** The game steps its time by the frame interval its framework picks (`ksys::VFR`: interval / 2 of a 30 fps tick). With the profile's `fps_target` (60) the host picks it instead, in whole vsyncs (`FRAME_STEP` in the profile's `patches.py`, `hle_nvn.c` `fstep_tick`, `nvn_fstep.h`, the same frame step as Tears of the Kingdom's): a stub at the framework's `setWaitVsyncInterval` hands the game the host's step once a frame, and the host keeps each frame on screen that many vsyncs. - - It holds 1 (60 fps). Three late frames in a row (the frame or its GPU work more than 1.7 ms over) step up to the cost, 2 (30 fps) as a rule: no 40 fps on 120 Hz screens. 30 frames that would fit the shorter hold try it again, and each failed try doubles the wait before the next (1 → 8 s). After a hitch, a cost at 30 like the one that held 60 tries it too (probes, [Power](#power)). - - Real time lost to a late frame or a hitch is paid back one vsync per frame (at most 15; the rest of a long load is dropped), so game time = real time. The log line `frame step: game time 30.00 s in 30.00 s (1.000 of real time)` checks it every 1,800 presents, with the frames held 1 / 2 / 3 vsyncs, late frames, paid-back vsyncs and hold changes. - - Scenes that fix the game's step themselves (the VFR's override: 2 at `main+0xc3b588` / `+0xc5ebf8`, 1 at `main+0xc609d8`) keep it, and the host holds frames that long (log line `the game fixes its own step`; not reached on the Mac yet). - - Mac (M4 Pro), from the title into the save's scene: 1.000 of real time at 60; with 12 ms added to every frame (`DM_TEST_PRESENT_LAG=1,12`) it drops to 30 and stays at 1.000. `DM_FPS_TARGET=30` holds 30 (saves power). -- **CPU hitches (Mac):** in steady play, frames with 30–210 ms of CPU work came every 1–10 s (Zora's Domain, M2 Max), the main thread and the two workers spinning on a job queue's lock that the main thread could not get. Since the spin wait rule takes that loop they are gone ([Power](#power), Spin loops). -- **The game's own interval controller** (at `DM_FPS_TARGET=0`, and the interval the framework keeps while the VFR override holds): patches to it lift its floor from 2 to 1. The game's time step follows its frame interval, so the speed is right at a steady 60, 30 or 20. The game picks the interval from the frame's CPU time and its GPU time, which it reads from two GPU timestamps around the frame; the host gives each timestamp the GPU time of the work around it (profile key `gpu_timestamps`, which its dynamic resolution needs too). Before that it saw only the main thread's CPU time (about 3 ms a frame in the first shrine) and stayed at 60 in slow motion: at 40 fps, about two thirds of the normal speed (an iPhone 16 Pro Max in handheld mode, GitHub issue #9; [04](agents/04-graphics.md)). Under load it still ran at 0.93–0.98 of real time. -- **Outdoors on the Mac (M4 Pro, the Great Plateau):** about 165 render passes a frame instead of 386 and GPU p50 about 6.5 ms instead of 9.3 since a partly updated uniform block no longer ends the pass (the game updates 64 bytes of a 64 KB block before each draw group), and the G-buffer clears ride on its first pass ([04](agents/04-graphics.md)). The frame still loads and stores about 800 MB of attachments, most of it where compute dispatches of the GPU particles split passes. -- **Render scale:** `DM_RENDER_SCALE=0.5`–`1.0` (also from `Documents/env.txt`) fixes the 3D views at that fraction of 1600×900 docked (1280×720 handheld) and turns the game's own dynamic resolution off; 1.0, the default, keeps it. A stub sets the scale where the game applies its own (`main+0x1266880`), so every scene pass, the shader's resolution ratio and the LOD check follow it: at 0.5 the 1600×900 passes become 800×450 and the 800×450, 400×225, 200×112 and 100×56 chains halve too, while the shadow maps and the 1920×1080 output keep their size and the HUD stays sharp. It holds in handheld mode too (960×540 at 0.75). On the Mac (M4 Pro) the GPU time does not change (about 13 ms in the first shrine at 1.0 and at 0.5): a frame has about 650 passes, most of them tiny, whose fixed cost outweighs their pixels there. Whether it helps an iPhone, whose GPU loads and stores each pass's attachments, is what the setting is for. -- **The world-load crash (about 1 run in 3 on the Mac) came from the host's command record sizes.** About 3 s after Start, a task worker's `operator new` (`main+0x155a79c`) got null and the game wrote through it (`main+0x155a7a4`). Each model file gets a heap of its resource size table entry + 0x1008; the file goes in 4 KB aligned, and the model's setup (`main+0x155a2ac`) takes the rest as a frame heap. Part of that setup is GPU memory for the model's prerecorded commands, which the game sizes by recording each mesh's draw and each shape's state for each render pass into a scratch command buffer and adding up `nvnCommandBufferGetCommandMemoryUsed`, so the host's own record sizes count against the table. Draw records had grown to carry the indirect and multi-draw fields (0x38 → 0x50 per recorded draw on 2026-10-18 and 10-20), and every recording began with an 8-byte BEGIN record. `Obj_TreeConiferous`, `Obj_TreeBroadleaf_B` and `FldObj_HyliaBridge_A-02` then needed one 4 KB page more than their entries allow, which fit only when the file's alignment pad happened to be small (about 15% of loads each failed). Plain draws now leave those fields out and recordings have no BEGIN (0x30 per recorded draw, `nvn_obj.h` `draw_rec`): all 510 models of the world load fit whatever the pad. How it was measured: [03](agents/03-bringup.md). -- **Memory:** about 2.8 GB in the first shrine in handheld mode, idle or walking (Mac measurement), against the 3.4 GB an 8 GB iPhone gives the app. -- The game waits for each frame's GPU work before submitting the next, so the host hands its translated commands to the GPU in segments ([04](agents/04-graphics.md)). -- **Sky (GitHub issue #52):** outdoors the sky was brownish grey and the distant haze brown, on the Mac and the iPhone. The game copies its precomputed sky scattering tables (`System/KSys/sky.skybin`) into memory pools with the CPU and flushes them; the host made the 3D table a GPU texture of zeros. Every game now fills a texture from its storage at first use when the game flushed that storage before then ([04](agents/04-graphics.md)); the sky is blue again. -- **HUD prompt shadows and glows, stamina wheel, System menu background:** the game rebuilds each layout capture (a blurred mask of a prompt bar, the menu's background) on the same storage with its own channel mapping and keeps the new texture only if `nvnTextureCompare` calls the two different; the host's comparison ignored the mapping, so captures read alpha 1 (hard-edged dark boxes, cream cloud rectangles, a black menu background). It compares the mapping now ([04](agents/04-graphics.md)). - -**The Legend of Zelda: Link's Awakening (`01006BB00C6F0000`)** -- **Engine:** Grezzo's engine on NintendoWare, SDK 8.2.0 (the oldest so far), NVN 53. Modules main + subsdk0 (GLSLC, NVN's runtime shader compiler) + subsdk1 (movie) + sdk; 9,698 shaders from the bfsha archives, effects and layouts. -- **Window origin:** the only game so far that keeps NVN's default lower-left (OpenGL) window origin; the host flips its viewports and winding and the window image at present ([04](agents/04-graphics.md)). Before that, text the game renders into textures came out upside down. -- **Movies:** VP9 with a 5.1 Vorbis sound track; the host decodes the sound with stb_vorbis, which needed a local fix for type-2 residues with more than two channels ([README](../dm/third_party/stb_vorbis/README.md)). Before that, the opening and title movies sounded distorted and gurgling, with pops. -- **Lighting:** tiled forward lighting whose tile lists a compute shader builds each frame; it needs the dispatch size in NVN's driver segment `c0` ([04](agents/04-graphics.md)). Before the host supplied it, lights piled into the bottom tile row and the doorway step flickered with white tiles. -- **Sound:** SDK 8 numbers the audio renderer's commands differently (the output command is 0x10, not 0x13); the host recognises the output command by its size. Before that, music and sound effects were silent and only the movies had sound ([03](agents/03-bringup.md)). -- **Mac input:** the title wants L+R: `DM_KEYS="f6000:L+R~0.4,f6300:A,f6500:A,f6700:Down,f6800:A"` reaches a new file's name entry (a fresh save directory via `SV=`). -- **Frame rate:** gameplay runs at 60 like on the Switch; the game itself drops to 30 (present interval 2) for event cutscenes. -- **Depth of field:** outdoors the game blurs the top and bottom of the screen by depth. It sets NVN's near = 0 depth mode, which the shader translation now follows for every game (an entry point per mode, picked at run time); before that the whole screen was blurred ([04](agents/04-graphics.md)). -- **Sunlight:** outdoors the sun's shadow test failed everywhere, so the scene was dim and nothing cast a shadow. The world position its shaders pass between stages has no w written, and the translation filled it with 0 instead of the hardware's 1; every game's shaders now get the hardware's default ([04](agents/04-graphics.md)). -- **Physics:** at 30 fps the game steps physics twice per frame. Its completion task destroys itself after it runs, and on Apple devices that destructor could land after the next step had already rebuilt the task, so the physics workers crashed when a conversation or cutscene began. A per-build cut drops the destructor call ([03](agents/03-bringup.md)). - -**Super Mario Odyssey (`0100000000010000`)** -- **Engine:** Nintendo EPD's sead / agl with NintendoWare, SDK 7.3.2 (the oldest so far), update 1.4.1. Modules main + subsdk0 (movie) + sdk; 27,984 shaders (agl `sharcb` and `bfsha` in `ShaderData`, particles in `EffectData`); its three geometry shaders (a thunder effect) do not translate. -- **Start-up services:** it calls several system services no other game did, each answered by the host: the aborting `account::OpenPreselectedUser` and `GetLastOpenedUser`, news delivery (`bcat`: the delivery storage does not mount, which the game treats as "nothing delivered"), HOME-button blocking, the parental 3D-mode setting, `vi::SetLayerScalingMode`, `time::ToPosixTimeFromUtc`, the debug pad, and the SDK 7 names of two audio-device calls ([03](agents/03-bringup.md)). -- **Movies:** MP4 / H.264 (the videos in its tutorial hint boxes, the kingdom travel videos), 41 of 44 High or Main profile with B-frames. The host decodes them with VideoToolbox; the SDK's demuxer passes the parameter sets as the `avcc` record (the MP4 `avcC` box), and pictures leave in presentation order through a small queue. -- **Sticks:** the game reads all 16 entries of the controller history whatever count comes back; the host now fills every entry (before, only the first, and neither stick did anything in play while buttons and menus worked). -- **Motion:** it reads the six-axis history the same way (16 entries per sensor, the count ignored, `main+0x93fd84`). Its motion analyzer (`main+0x5d5590`) turns the newest acceleration, seen against the controller's pose, into a swing value per hand and sets the swing flags when it exceeds 0.2. A still controller gives 0, and the host's shake (±3 G side to side) gives up to 1.5, so a `Shake` sets the left, right and any-swing flags for its length. Before, the host reported no sensor and no motion move could happen ([03](agents/03-bringup.md)). -- **Mac input:** a fresh save wakes Mario in the Cap Kingdom with `DM_KEYS="f1000:A,f8200:RLeft~6,f9200:LUp~4"`: the "Look Around" hint waits for the right stick held about 6 s one way (left then right cancels out). Play starts at about frame 9,900, and the game saves there. From that save, `f1000:A,f1300:A` resumes into play at about frame 1,500, and `f1600:LUp~7` walks straight across the fog to the hills by the bridge, where Cappy flees from Mario. -- **HUD:** the coin counters are layout panes that take their alpha from single-channel render targets, as in Breath of the Wild; the profile lists the 26 layout shaders under `alpha_from_red` (before, they were solid white cards). Since the `nvnTextureCompare` fix the game's own capture mapping does that and the list no longer fires on the Mac ([04](agents/04-graphics.md)). -- **Ground:** a depth pre-pass followed by a less-or-equal G-buffer pass needs bit-identical positions from different vertex shaders; every shader is now compiled with `-fpreserve-invariance` (before, the Cap Kingdom ground showed stale 32×32 blocks on every fourth frame) ([04](agents/04-graphics.md)). -- **Fog:** the Cap Kingdom ground fog parts around Mario through a small fluid simulation fed by point-sprite splats. Their vertex shader clamps the point size to the driver segment's `c0 + 0x610`, which the host left at 0, so no splat was ever drawn and the fog never moved; the host now writes the largest point size there ([04](agents/04-graphics.md)). - -**Hades (`0100535012974000`)** -- **Engine:** Supergiant's engine on The Forge renderer, SDK 9.3.0, update 1.0.38233. It renders with **Vulkan**: `main` imports 100 `vk*` functions from subsdk0 (the Vulkan loader, over NVIDIA's driver in subsdk1). The host implements them on Metal itself (`dm/gfx/vk/hle_vk.m`), so only main and sdk are loaded. -- **Vulkan layer:** one Tegra X1-like device (Vulkan 1.1); device memory is a shared `MTLBuffer` made on first use, images are their own textures; command buffers are recorded and replayed into one Metal command buffer per submit on the layer's own queue; swapchain images are shown through the NVN layer's present path once their frame's GPU work is done. Names the game asks for through `vkGet*ProcAddr` are logged once each. -- **Shaders:** 74 SPIR-V files in `Content/EffectsForgeSwitch`. `dm/gfx/tools/vk_shaders.py [--ios ]` translates them with SPIRV-Cross into `/vk/`: one library, and a table of the Metal slot each (set, binding) moved to. The game hands the same words to `vkCreateShaderModule`, so the host finds each shader by the hash of its SPIR-V. -- **Notification event:** the game polls `oe::GetNotificationMessageEvent` every frame; the SDK's own event was never created (the host's `oe::Initialize` is a no-op), so the host now hands out an event of its own, signalled while a notification is queued. -- **Online:** its `EOS_*` imports (Epic Online Services, an NRO it would load through `nn::ro`) stay offline. -- **Mac input:** `DM_KEYS="f800:A,f1000:A,f1200:A,f1500:A,f1900:A"` goes through the title and the intro into Tartarus. -- **Packing:** for a Vulkan game the NSP script runs `vk_shaders.py` (Mac and iOS libraries in `_build/shader/vk` and `shader/ios/vk`) in place of the NVN shader split and chain build. - -**Hades II (`0100A00019DE0000`)** -- **Engine:** Supergiant Games' engine, SDK 19.3.9, base game v0 (the NACP's display version is 0.0.126424). Unlike Hades it renders with **NVN**, and its ExeFS holds only main and sdk. The NSP script's provisional profile booted it on the Mac with no host change: title, a new save, the opening and Erebus at 60 fps. It plays on an iPhone. -- **Shaders:** 88 BNSH files in `Shaders/NVN/bin` (`.vert` / `.frag`; the `.shader` files list each program's pair). They bind textures at fixed, sparse slots: 0, 1, 3, 6, 8, 11, 15, 21, 25, 26, 30, 31 and 34. Slot 34 (`SceneFade.frag`, c2 0x130) lies past the driver's 32 combined slots, inside the image table at 0x120; the game has no image bindings, and a bound texture there wins over an unbound image (`nvn_mtl.m`). -- **Online:** its `EOS_*` imports (Epic Online Services, `nro/EOSSDK-Switch-Shipping.nro`) stay offline, as in Hades: `ro::RegisterModuleInfo` fails (red line), so the game never loads the NRO. SDK 19's `nn::ro::LoadModule` takes one more bool (`…mib`); the host hooks that form and `ro::Finalize` too, so neither reaches the SDK's `ldr:ro` client. -- **Memory:** no system resource; the game asks for a 3.10 GB heap, which the 4 GB layout's heap region holds. main has an 82 MB bss, so the profile's `mem_used` is 108 MB. Resident memory levels off at about 3.4 GB in Erebus on the Mac. -- **Not a bug: two long lines through Melinoë.** While the staff's Omega attack charges, two thin white lines run from her to the screen edges, one forward and one back. They are that weapon's aim lines (`WeaponStaffSwing5`: `AimLineCountOverride = 2`, `AimLineAngleOffsetOverride = 180`, animation `AuraAimLineNoMovementStaff`). The sprite is a long arrow with white edges, a dark fill drawn emissive and its tip off-screen, and the game hides it while she moves. Traced with `DM_NVN_CBDUMP` vertex dumps and `DM_PROBE` on the aim-line update (`main+0x39ade0`); a scripted run that taps Y shows them often. -- **Mac input:** `DM_KEYS="f700:A,f900:A,f1100:A,f1300:A,f1500:A,f1800:A,f2100:A,f2400:A,f2700:A"` goes through Play, the first save slot and the opening into Erebus (control around frame 3000). - -**Hyrule Warriors: Age of Calamity (`01002B00111A2000`)** -- **Engine:** Koei Tecmo's KTGL on NVN, SDK 9.6.0, update 1.3.0 (v327680), four DLC packages (one is only a licence). Modules main + subsdk0 (movie) + sdk. -- **Files:** everything sits in RDB archives (`asset/*.rdb` index; `asset/*.rdb.bin*` and `asset/data/*.file` hold `IDRK` entries, zlib in 16 KB chunks) ([01](agents/01-extract.md)). -- **Shaders:** 40,726 GLSLC outputs: KTGL's G2S containers and one `_DOK` container in `system.rdb.bin*`, plus 117 in `main`. Many have 0x850-byte controls and the compute ones no code header ([04](agents/04-graphics.md)). The game builds 8,705 programs during its first frame (about 10 s on the Mac). -- **Heap:** its three modules take 66.6 MB, more than the 64 MB the host reported as used, so `SetHeapSize` failed and `nnMain` returned at once; the profile's `mem_used` reports 72 MB. -- **Sound:** the engine's 15 MB heap was too small on the host (renderer work buffer plus four players, three of them 5.1). The sound setup then gave up quietly, the first sound archive never loaded and the main thread waited forever; a per-build cut moves 4 MB from the 142 MB sound data heap to it ([03](agents/03-bringup.md)). The multi-stream hardware Opus decoder is the SDK's software one. -- **Graphics fixes it brought:** compute programs get their own binding in the replay (draws after a dispatch were dropped, so the intro movie stayed black), `nvnTexturePoolRegisterImage`, indirect draws (its GPU-driven effects are indirect quads, converted by a compute pass), and the movie planes (CPU-written, published with `nvnMemoryPoolFlushMappedRange`) through `tex_flush_upload`. -- **60 fps (30 on the Switch).** The game steps a fixed amount per update, from three 30 fps values: a KIDS task on `KIDSTaskScheduler` threads (main+0x166d980) waits until 1 / fps has passed since the last update and steps the game clock by a frame-rate object's 1/30 s (not by the time that passed); the battle steps 1 / the game's frame rate per update (a block at main+0x269e150, {30, interval 2, 30, 30.0}, read by the scene update's caller at main+0x8c3610 and its jobs); and the event scripts step by the KTGL device's frame rate, 59.94 / the present interval in its config (2). The first 60 fps cuts (interval 1, a 1/60 s wait) were checked only on cutscenes, which follow the device: `DM_PROBE` showed the game clock at 2.00 × real time, and battles ran twice as fast on an iPhone. Fixing the clock alone was not enough: in a 3 s stick hold Link still covered twice the ground of the 30 fps original, and the enemy waves came early, until a `sample` profile of a battle led to the frame-rate block. The game's updates are not held by the presents (the task kept 60 updates a second while the host held frames 3 vsyncs), so three frame-step stubs (`FRAME_STEP` in its `patches.py`) run the updates at the cap: the task's fps and the frame-rate block become the cap, the object's step is scaled by its fps × vsyncs per update / 60, the device frame rate becomes 59.94 / vsyncs, and nnMain's device config gets interval 1 above a 30 fps cap. Every step stays per update, as on the Switch, so an update rate below the cap slows them all together. The host shows each frame as soon as it is ready (profile `fps_hold off`); the updates keep their speed whatever is shown. Profile `fps_target 60`; `DM_FPS_TARGET=30` runs updates and presents at 30, and `DM_FPS_TARGET=0` is the Switch original (interval 2, the stubs' original paths). On the Mac the forced 30 and the original put Link on the same spot after the same stick hold, and the opening scenes at frame 2N at 60 match the original's frame N (same subtitle, same poses). -- **Battles: vertex conversion over the draw's vertices (`vconv_range`).** The soldiers' Half2 and Short2 attributes feed integer shader inputs, which the replay converts on the CPU (`vconv_bind` in `nvn_mtl.m`). KTGL binds whole mesh pools of up to ~590,000 vertices, so converting the bound ranges came to 4.4 million vertices per submission in the first battle: 24 of the 32 ms the replay took per frame on the Mac, 26 fps at a 30 or a 60 fps target. The profile's `vconv_range` converts only what each draw reads (219,000 vertices per submission, about twelve submissions a frame), and the half and 8 / 16-bit integer formats have conversion loops of their own. Keeping the conversions across frames did not pay: the soldiers' attributes change every frame. `DM_NVN_VCONV_CHECK=1` found no stale index range in 983,000 reuses. The visibility buffer ring was 4 deep, so the submit thread waited for the previous frame's GPU work; it now has as many buffers as command allocators (8). The battle runs at 58.6 fps on the Mac (M4 Pro; frame p50 16.9 ms, replay CPU 10 ms, GPU 11 ms). On an iPhone the GPU decides (14–16 ms per frame at p50 in the first battle, 40–60 ms once the phone reaches thermal state 2): the frame step holds 30 there. -- **Battles after the first cutscene: compute dispatches and texture views.** An iPhone log (2026-10-21, `fps_hold off`, thermal state 0) went from about 57 to 17 fps after the first battle's first cutscene: the same 1,000–1,400 draws a frame, but NVN calls from 82,000 to 287,000 a frame, GPU 15 → 51 ms and the replay 12 → 24 ms. The battle then runs more small compute dispatches (on the Mac about 280 → 900–1,500 a frame, each with a game barrier and about 17 texture binds), and two host costs grew with them: every dispatch had a compute encoder of its own with full queue barriers on both sides (about 10 µs of GPU each on the Mac), and KTGL registers a texture into the next ID of a ring before every bind, so the per-ID view was rebuilt for nearly every bind (half of the 8,000–28,000 registrations a frame made a new Metal texture view, 45% of the replay's time). Dispatches now share the copy encoder (about 1.3 µs each) and views are cached by texture serial ([04](agents/04-graphics.md)). Mac runs side by side, after the cutscene: GPU 18.0 → 8.1 ms and frame 35 → 22 ms with the dispatch change; the view cache took the replay before the cutscene from 14.3 to 10.8 ms (2,036 views made in 15 minutes). -- **Light and decal lists, vertex formats: two general fixes for its last red lines.** 16,438 shaders read KTGL's light and decal lists from buffer textures with the instruction Maxwell also uses for 1D textures. The translation read them as 2D and the host bound a dummy, so every list read 0. The lists are now read as rows of 16,384 texels from a linear 2D texture over the buffer ("row wrap"). The first battle is darker and more saturated than with the zeros. The CPU vertex conversion of the soldiers' Half2 / Short2 / RGBA8UI attributes was a quarter of the replay thread; it is replaced by shadow vertex inputs: 2,259 side libraries, 77 MB each for Mac and iOS. Mac battle at 60 fps against the CPU conversion: submit + present 10.0 → 5.7 ms, CPU 3.97 → 2.44 W ([04](agents/04-graphics.md), both general). A 2D texture bound where three vertex shaders sample 3D (slots 8 and 9, `texture type mismatch … declares slot 8 as Metal type 7`) is still bound as is; it was hidden behind the earlier red line. -- **Other games' saves:** it asks whether Breath of the Wild's (and `0100A2C01320E000`'s) save exists for bonus items; the host says no. -- **Mac input:** with a save whose first-boot settings were made, `DM_KEYS="f300:A,f420:A,f540:A,f660:A,f780:A,f900:A,f1300:Plus,f1350:Plus"` confirms the settings, skips the intro movie and plays the opening into the first battle, The Battle of Hyrule Field (about frame 4000). - -**Fire Emblem: Three Houses (`010055D009F78000`)** -- **Engine:** Koei Tecmo's older KTGL (the game is co-developed with Intelligent Systems), SDK 6.5.0 (the oldest so far), update 1.2.0. Modules main + subsdk0 (GLSLC, NVN's runtime shader compiler) + subsdk1 (movie) + sdk. RomFS: the LINKDATA archive (`DATA0.bin` is the index, `DATA1.bin` holds 6.9 GB), loose overrides in `patch1`–`patch4`, WebM movies and KTSL sound banks; compressed entries and `*.gz` files are Koei Tecmo's chunked zlib ([01](agents/01-extract.md)). -- **Shaders:** 1,364. `shader_split.py` reads the SPKG packages (G2S containers, as in Age of Calamity) in `DATA1.bin` and `patch*/nx/shader`, a `_SPK4100` post-effect package, and the BNSH and GLSLC outputs in `main`. SDK 6's GLSLC writes 0x848 / 0x850-byte control sections and compute code with no header, which the reader used to drop ([04](agents/04-graphics.md)). -- **Shaders compiled at run time:** KTGL compiles 9 built-in GLSL shaders at boot with the game's own GLSLC (2D primitives, text, the loading quill). No game file holds their output, so they are captured on the Mac. The game runs with `DM_GLSLC_CAPTURE=/glslc_capture` (`hle_glslc.c`), and `shader_split.py` reads that folder. The outputs are identical on every run. The NSP script does this by itself in its `GLSLC capture` stage, so the NSPs and prod.keys are enough. The stage builds the Mac host into `_build/tools/` and boots the game until the 9 files are in `_build/extract/v327680/glslc_capture`, which takes about 10 s in all. Without the folder the split tables would differ from the profile, and the script would stop at its table check. By hand, run the game for a few seconds with that variable (step 5 of the build by hand). -- **Host changes it needed** ([03](agents/03-bringup.md), [04](agents/04-graphics.md)): - - it submits on two NVN queues from two threads (submissions are serialized now); - - its command buffer records into one 16 MB ring chunk and frees the ring by `GetCommandMemoryUsed`, which now counts from the last `AddCommandMemory` as NVN does (frames stopped after about 15 s); - - its two texture pools and two sampler pools use the same IDs, now kept apart (menus showed plain bars or only the cursor); - - SDK 6's `account::OpenPreselectedUser` returns a Result (the save never mounted and the first loading screen never ended); - - SDK 6's audio command header has its own layout (no sound); - - its character shaders read Half2 UVs through integer-declared inputs, now converted to float32 bits at replay (black silhouettes, then flat tan characters); - - depth clamp is replayed. -- **Fixed:** the ground under close-up battle cameras showed as flat blue. The terrain's signed 16-bit heights were read zero-extended, so the vertices with negative heights went out of view ([04](agents/04-graphics.md), Three Houses). -- **Fixed:** the first loading screen took 78 s on the iPhone. The loader calls `nvnQueueFinish` after each resource it uploads, and that call waited for the display to take every queued frame; it now waits only for the GPU ([04](agents/04-graphics.md), Three Houses). -- **Light and decal lists read since row wrap** ([04](agents/04-graphics.md), general): 321 shaders read them through the instruction Maxwell also uses for 1D textures, and they used to read 0. Torches and burning houses now light the prologue battle map and the close-ups, and its Half2 / RGBA16I vertex attributes go through shadow vertex inputs instead of the CPU conversion. **To confirm:** 64-pixel rectangles in the ground where decals land, which skipping the decal pass removed; they did not appear on the Mac prologue route in either build. -- **Mac input to a first fight:** from a save right before the prologue battle, with `DM_AOC_DIR=/010055D009F78000_build/aoc` (or a "DLC deleted" dialog blocks loading), `DM_KEYS` with A every 90 frames from `f720` to `f2610`, then `f2700:B,f2800:B,f2900:A,f3000:Up,f3060:Up,f3120:Up,f3180:Up,f3240:A,f3340:A,f3440:A,f3540:A,f3640:A,f3740:A` closes the movement tutorial, moves Byleth up to the thief and attacks: the combat close-up runs at frames 3,400–3,700. -- **Memory:** about 3.5 GB resident and 2.8 GB of Metal allocations on the Mac; the game itself makes a 1.5 GB GPU-only memory pool. -- **60 fps:** the Switch version shows 30, but KTGL already updates the game 60 times a second on its main thread, each update a fixed 1/60 s step; the render thread draws the latest update and presents it with the interval of the render mode (2 for the docked 1080p and handheld 720p modes; the engine also has a 720p and a 1080p mode at interval 1 that nothing selects). Per-build cuts on 1.2.0 `main` set every mode's interval to 1, so every update is shown and the game runs at the same speed; the render thread's own per-frame steps (a fade counter, a scroll) read the same table. Two more cuts make the waits cheap. The frame limiter (`main+0x5be430`) yielded and polled the clock, which on the host spun 2 ms a frame on a performance core and ran at 59.5 updates a second; it now sleeps the time left. The render thread waits for the next update with a timeout of one frame given in milliseconds to a wait that takes microseconds, so it polled every 16 µs (about 50,000 wakeups a second at 60 fps); it now waits a frame. Prologue battle on the Mac: 59.9 fps, 55% of a core and 1.8 W of CPU energy (the original at 30 fps: 44% and 1.7 W). The render thread also forced interval 2 while either of two flags was set: one for the opening movie, one for scenes such as the character creation screen after it (the engine's own 60 fps modes dropped to 30 there to keep 1080p). A fourth cut ignores both (`main+0x50fb7c`), so those scenes run at 60 too; the movie still lasts 210.6 s (6,318 frames at 30 fps), as in the original. -- **Mac input:** a fresh save needs `DM_KBD=` for the name entry, and its confirm dialogs default to No. From a new game into the prologue battle (frame-based at 60 fps, about 7 minutes): `DM_KEYS="f720:A*185/90,f2400:Plus*35/438,f17400:Left~0.3*120/500,f17480:A*120/500,f17560:Left~0.3*120/500,f17640:A*120/500,f18020:Plus*100/562"`; the game offers a save right before the battle. The repeating Left, A, Left, A reaches Yes whether a confirm dialog is open or not (A, Left, A loops once it starts on an open dialog: A answers No, Left changes the selection, A reopens it). A presses during the opening movie pause it, so the scenes after it start a few seconds later from run to run. - -**Super Mario Galaxy (`010099C022B96000`)** -- **Engine:** the Wii game (codename Comet) on NERD's Hagi layer, SDK 18.3.5, update 1.3.1 (v327680). The PowerPC code is statically recompiled into `main` (`hagi.config`: `Cpu` `Jit false`, `Statifier true`), so there is no JIT; Hagi stands in for the Wii hardware: GX on Vulkan, the DSP with its ROMs in `data/dsp`, the disc from the RomFS, HD texture packs in `data/texture_replace` and `data/ondemand_texture_replace`. Modules main + subsdk0 (movie) + sdk; subsdk1 (GLSLC), subsdk2 (`nn::pcm`) and subsdk3 / subsdk4 (NVIDIA's OpenGL and Vulkan drivers) are not loaded. -- **Memory:** `main`'s bss is 2.9 GB (the emulated memory); the game never calls `SetHeapSize`, so the guest block is 1 GB. Footprint on the Mac 1.9 GB, 1.1 GB of it Metal. -- **Vulkan:** 99 `vk*` imports through Hades' layer (`hle_vk.m`), which Galaxy extended: push constants, descriptor arrays, dual-source blending from an output at location 1, copies inside a render pass, linear images over their memory, timestamp and occlusion queries, pipeline caches, `vkCmdDraw`, image copies and read-backs, the GX vertex formats (USCALED / SSCALED, three components), viewport and scissor kept across render passes, the swapchain's INHERIT transform and the vi layer crop ([04](agents/04-graphics.md)). -- **Shaders:** Hagi generates its GX shaders as SPIR-V at run time. While it shows "Initializing system..." it creates 3,089 modules (the 56 built into `main` among them) and 4,535 pipelines from `data/database/pip/pipelineDB.pip`, the same on every boot (about 15 s on the Mac). The RomFS has no SPIR-V file, so the NSP script's `Vulkan shader capture` stage, with the profile's key list `vk_capture.tsv`, boots the game in the Mac host with `DM_VK_CAPTURE=/vk_capture` until every listed key is there (under a minute), then `vk_shaders.py` translates the RomFS and the capture. Its two geometry shaders (GX points and lines, expanded to quads) are skipped: those draws come out as one-pixel points and lines. -- **Pointer:** docked, Galaxy's pointer is the Joy-Con gyro, which the host does not have; handheld, it is the touch screen. The profile's `op_mode handheld` starts it in handheld mode (720p). On the iPhone, touches no gamepad control takes go to the game's touch screen (in the 3D layout, outside the right third of the screen, which is the right stick; all of them while a controller is connected); on the Mac the mouse is a finger, and `DM_KEYS` taps with `Tx` (normalized picture coordinates). -- **CPU:** Hagi polls with `SleepThread(100 ns)`, which the host used to return from at once: one thread kept a core at 100%. Sleeps under 20 µs now count as yields and get the yield back-off ([03](agents/03-bringup.md)). -- **Not yet:** GX points and lines (the geometry shaders), sampler LOD bias (GX uses it; Metal samplers have none, red line). The base NSP in hand was damaged (147 MB zeroed near its end: the two WebM prologue movies, which the storybook prologue does not use); it was packed with `--allow-damaged`. -- **Mac input:** from a fresh save, `DM_KEYS="f1500:A,f1950:T0.373x0.70,f2300:A,f2700:A,f3200:A,f3600:LRight,f3800:A,f4200:T0.247x0.426,f4500:A,f4800:LRight,f5000:A,f5400:T0.659x0.366,f5800:A,f6200:LRight,f6400:A,f7000:A,f8000:A,f9000:A,f10000:A,f11000:A"` creates file 1 (Mario icon, Regular Mode) and plays the storybook prologue into the Star Festival (about frame 12,000). Screenshots of the handheld picture need `DM_NVN_SHOT_OUT=1280x720` (the raw swapchain image is 1920x1080 with the picture in its top-left 1280x720). - -**Super Mario Galaxy 2 (`0100FD8022DAA000`)** -- **Status:** brought up on the Mac (title, file select by touch, the opening and the first storybook stage at 59 fps) and playable on an iPhone. -- **Engine:** the same Hagi layer as Super Mario Galaxy, update 1.4.0 (v393216), with Galaxy's `sdk` and movie module builds (SDK 18.3.5); `main`'s bss is 2.6 GB. Everything Galaxy needed from the Vulkan layer and the host carries over; the profile is Galaxy's with its own memory layout and shader list. -- **Memory:** the movie player initializes its own StandardAllocator with virtual address memory, 384 MB (`main+0x62c280`), which aborted the game at boot: the guessed 256 MB alias region holds nothing after the SDK's 1 GB address-region node area. A rewriter cut for this `sdk` build (shared with Galaxy) shrinks the node area to 62 MB, and the alias region is 512 MB; the guest block is 1.25 GB, about 4 GB of address space with `main`. Footprint on the Mac 1.93 GB, flat over five minutes (1.0 GB of it Metal). -- **Shaders:** 3,797 SPIR-V modules, all created about 7 s after launch (Galaxy: 3,089), captured by the NSP script as for Galaxy (`vk_capture.tsv`); the same two geometry shaders (GX points and lines) are skipped. -- **Damaged NSP:** the base NSP in hand fails the hash check on `data/texture_replace/MarioGalaxy.hix` and `MarioGalaxy_Sub.har`; it was packed with `--allow-damaged`. -- **Mac input:** from a fresh save, `DM_KEYS="f1600:A,f2000:T0.268x0.579,f2400:T0.641x0.778,f2800:T0.128x0.426,f3200:T0.641x0.778,f3600:T0.659x0.366,f4000:T0.8125x0.889,f4500:A*6/300,f6000:LRight+A~1.2*200/100,f6050:Y*200/100"` creates file 1 (Mario icon, Regular Mode), plays the opening and walks the first storybook stage to the spin tutorial and on (about frame 11,000). DM_KEYS runs its steps one after another, so a hold blocks the steps after it: walking and jumping go in one step (`LRight+A`). - -**The Legend of Zelda: Tears of the Kingdom (`0100F2C0115B6000`)** -- **60 fps at normal speed:** the game picks a present interval each frame (2 = 30 fps, 3 under load) and steps its game time by 60 / fps. Stubs in its `patches.py` (`FRAME_STEP`; the sites come from masagrator's FPSLocker config for this build) replace that logic with the host's frame step in whole vsyncs, as in Breath of the Wild: int fps = 60 / step. Prerendered movies hold 30. On the Mac (M4 Pro) the opening cave holds 60 at 1.000 of real time; with 12 ms added to every frame it drops to 30 after three late frames and stays at 1.000. `DM_FPS_TARGET=30` saves power, `0` gives the game's own step back (every stub then runs the original instructions). -- **Before the host has decided a step** (the first frames): the stub steps by the measured frame time, capped at the target. Game time that ran ahead after a short frame is paid back by the next frames (the idea from OJZen/totk-mobile); frames longer than 1/15 s are not caught up. -- **Render scale:** `DM_RENDER_SCALE=0.5`–`1.0` multiplies the size of the 3D views (1600x896 docked) and fixes it there; the game's own dynamic resolution runs only at 1.0. The HUD keeps its resolution. -- **Address space (GitHub issue #70):** the game's resource heaps are address regions in the alias region (2.375 GB in the 5 GB guest layout, 64 GB on the Switch). With the SDK's ~1 GB node area at its start, the alias region was used up to +2.25 GB at the first descent from the Great Sky Island with 1.00 GB mapped, and the game then took a memory shortage: its frame check (main+0x1fd32ec) logs `MSF`, then `R` and `Rf` (reason bits of one resource heap's object), and plays the memory-shortage Blood Moon, which puts Link back at the start. A rewriter cut on the `sdk` build (the one Breath of the Wild 1.9.0 also uses) shrinks the node area to 62 MB, as Fire Emblem Engage's, Hollow Knight: Silksong's and other games' builds have it: the high-water mark is +1.75 GB there, and no shortage came in 3.7 minutes on the island (it came within a minute in 7 of 8 runs without it). Route from the issue's save on the Mac: `DM_KEYS="20:A*6/4,f2480:LUp~5"` (title, load, jump, steer into the pond). - -**Monster Hunter Rise (`0100B04011742000`)** -- **Engine:** Capcom's RE Engine, SDK 11.4.3, update 16.0.2 (v2228224, Sunbreak). The base game is an XCI (cartridge dump; nx_extract reads its secure partition), the update an NSP, and its 252 DLC NSPs are licences only. `main` has 125 MB of text: the rewriter puts its patch area right before the base (`BIG_TEXT`, a second pass with `DM_PATCH_FRONT`). -- **Files and shaders:** everything sits in KPKA PAK archives, later patch PAKs overriding earlier ones by path hash. The 63,111 shaders are in SDF files (`shader_split.py` `kpka_jobs`), with combined and separate texture tables at once (separate binds are texture slots 128 + n). -- **GPU heap:** one 64 GB sparse virtual pool built from 4 MB physical pools. The host aliases mapped pages (`vm_remap`) and keeps a 4 KB page table the replay reads textures through (streamed textures 32 GB in, mip tails at 4 KB offsets). The scene is GPU-driven (`MultiDrawElementsIndirectCount`). -- **Keyboard:** the hunter, Palamute and Palico are named through the inline keyboard (`DM_KBD` on the Mac, the input box on the iPhone); the name is masked through `ngc::ProfanityFilter`'s vtable, which the host fills. -- **Frame rate:** 60 fps (30 on the Switch). The frame loop (main+0x6b106c8) sleeps until `[obj+0x430] / [obj+0x434]` sixtieths of a second have passed and steps the game by the time that really passed; the game sets the limit at `[main+0x1295b3f0]+0x434` to 30 after boot and asks for present interval 2. The host writes the profile's `fps_target` there (`hle_fpslock.c`, FPSLocker's address for this build) and the profile's `present_interval display` ignores the interval; both are needed. `DM_FPS_TARGET=30` gives back 30, and the Mac `DM_KEYS` routes below count frames at 30, so run them with it. -- **Mac input:** a fresh save (with the first-boot notices accepted) reaches character creation with `DM_KEYS="f4100:A~0.3,f6300:A~0.3,f7000:A~0.3,f7600:A~0.3"`. With `DM_KBD=Hunter`, `DM_KEYS="f4100:A~0.3,f6300:A~0.3,f7000:A~0.3,f7600:A~0.3,f8000:A~0.3,f8150:Down~0.25*6/40,f8450:A~0.3,f8650:A~0.3,f8900:A~0.3,f9100:A~0.3,f9500:A~0.3,f9800:Down~0.25*9/40,f10250:A~0.3,f10450:A~0.3,f10700:A~0.3,f10950:A~0.3,f11300:A~0.3,f11800:Down~0.25*10/40,f12250:A~0.3,f12450:A~0.3,f12700:A~0.3,f12950:A~0.3,f13300:A~0.3,f16000:A~0.3,f16300:A~0.3"` goes on through creation (hunter, Palamute, Palico, each named) and saves to slot 1. Holds under 0.25 s are not seen, and 0.5 s repeats in lists. From a save made that way, `f4100:A~0.3,f6300:A~0.3,f6800:A~0.3,f7100:A~0.3,f7500:A~0.3` continues into the opening. - -**Ace Attorney Investigations Collection (`010005501E68C000`)** -- **Status:** brought up on the Mac (first-boot settings, title, Investigations 1's menus, Episode 1's opening and first investigation up to the first Logic prompt; 25 minutes with no red line) and playable on an iPhone. Much of the bring-up was worked out first by armyja in a fork (GitHub issues #17, #18, #19 and pull requests #15, #16). -- **Engine:** Unity 2022.3.17f1 IL2CPP, SDK 17.5.3, update 1.0.0.2 (v131072). The base game is an XCI; its DLC NSP is a licence only. Two IL2CPP thunks end in an indirect tail call through `x18` (`main+0x1d271bc`, `+0x204ebec`), which the rewriter now recognises ([03](agents/03-bringup.md)). -- **Shaders:** 4,951 (2,475 vertex / pixel pairs and one geometry shader, which Metal cannot express) in the player's serialized files and the Addressables bundles. Unity 2022 keeps each program's parameters as blob entries of their own and dropped the subprograms' local keywords; the player's files borrow the `Shader` type tree of the `unitybuiltinshaders` bundle ([04](agents/04-graphics.md)). Binding layout as Engage's. -- **Files:** the C# file API goes through the SDK's C library (`File.Exists` → `stat` → `open`), which checks every path with `nn::fs::IsPathMounted` and the length-limit queries before calling `nn::fs`. The host answers them now; before that Addressables found no `settings.json` and the game crashed on a null asset (`main+0x31100ec`). -- **Clock:** the C library's real-time clock reads `nn::time::GetStandardUserSystemClockCurrentTimespec`, now the host's clock; `std::chrono::system_clock::now()` called `std::terminate` right after the first dialog. -- **Memory:** the guest block is Engage's 5 GB with the same node-area cut on this `sdk` build, and a 1 GB application pool: StandardAllocator commits only 0.19 GB, and with Engage's 2.5 GB pool its reservation left Unity's own pools so little address space that the live regions reached the end of the alias region after 140 s (ten minutes: +3.91 GB of 4.375 with a 1.5 GB pool, +2.85 GB with 1 GB). On the Mac about 1.55 GB resident and 1.25 GB of Metal allocations, flat from the first investigation on (25 minutes; address space at +2.81 GB). -- **Frame rate:** 60 fps, as on the Switch (present interval 1). -- **Mac input:** a fresh save goes through the first-boot notices (the privacy prompt's second button is "Next"), the title and Investigations 1's Episode 1 with `DM_KEYS="f600:A~0.3,f1200:A~0.3,f1800:A~0.3,f2400:A~0.3,f3000:Down~0.3,f3200:A~0.3,f3800:A~0.3,f4400:A~0.3,f5000:A~0.3,f5600:A~0.3,f6200:A~0.3,f6800:A~0.3*270/120"` (the first investigation in Edgeworth's office at about frame 18,000; at about frame 30,000 the first Logic prompt waits for L). - -**Unicorn Overlord (`010069401ADB8000`)** -- **Status:** brought up on the Mac (logos, title, a new game and the opening story scene; no red line) and playable on an iPhone. -- **Engine:** Vanillaware's engine, as in 13 Sentinels, SDK 16.2.4, update 1.05 (v262144). Its ExeFS holds only main and sdk. All data is in 7 CRI CPK archives; audio is CRI ADX2 with Opus streams. Both DLC NSPs are licences only. -- **Shaders:** 2,188 GLSLC outputs compiled into main (1,725 distinct: 614 vertex, 1,111 pixel), single-stage programs combined per draw. Every binding decodes. -- **Service calls:** three host changes, each found by one boot ([03](agents/03-bringup.md)): - - the hardware multi-stream Opus decoder's C interface, forwarded to the SDK's software decoder; - - `nn::crypto::GenerateCryptographicallyRandomBytes`, which the sdk's own `getentropy` calls under `std::random_device`; - - the profile key `alloc_big_align` it shares with 13 Sentinels. Without it, a ring heap cut from a memory pool kept base 0 and the game wrote to `0x268` right after the title screen. -- **Memory:** no system resource; the game sets a 3.12 GB heap (pool − used), which the 4 GB layout's heap region holds. The rewritten modules are 92 MB (`mem_used`). On the Mac about 2.9 GB resident in the opening scene. On an iPhone (A20 Pro): a 2.88 GB footprint with at least 3.2 GB of headroom. -- **Frame rate:** 60 fps. The game sets present interval 1; on the iPhone the frame time is 16.7 ms at the median. -- **Mac input:** `DM_KEYS=f2400:A*20/300` goes from the title screen (about frame 2,500) through a new game into the opening story scene (about frame 4,900). - -**Octopath Traveler 0 (`01005270232F2000`)** -- **Engine:** Unreal Engine 5 (the paged global distance field shaders and the LowLevelTasks task system) on its mobile renderer, SDK 19.3.13, update 1.0.5 (v327680); the base game is an XCI. Everything is in one pak, `pakchunk0-Switch.pak` (AES-encrypted index; the key is found in `main` and never printed), with CRIWARE sound banks and a USM movie beside it. -- **Shaders:** 10,324, in the pak's two shader code archives, each one Oodle-compressed. The NSP script decodes them on the Mac with the game's own `OodleLZ_Decompress` (as for any game with Unreal paks: `dm/gfx/tools/uepak.py`, `DM_OODLE_JOB`), about 10 s for all of them. 61 vertex shaders (particles) pick a texture slot at run time; the profile key `tex_dyn_slots` binds them ([04](agents/04-graphics.md)). 2,084 shaders do atomics on buffer images, now bound to the image's memory. 16 compute shaders with 3D image atomics (Unreal's paged global distance field, which the mobile renderer does not dispatch) do not build. -- **Memory:** Unreal reserves one address region (62 GB on the Switch) and its allocator, FMallocBinned3, 37 GB of it. A cut skips the allocator's constructor and the host runs Unreal's heap on a malloc zone (`hle_uemalloc.c`); the region is capped at 2 GB (`game_args`) and the SDK's node area cut to 62 MB, so the guest block is 3.75 GB. 15 minutes into the story on the Mac: region mapped up to 1.26 GB of the alias region, Unreal heap 0.75 GB, 2.8 GB resident, 1.84 GB of GPU allocations. -- **Frame rate:** 60 fps (30 on the Switch). The game's config caps Unreal at 30: `t.MaxFPS=30` in `[ConsoleVariables]` of `DefaultEngine.ini` (and `FrameRateLimit=30` in `SwitchGameUserSettings.ini`, which sets it again), and `rhi.syncinterval=2` in `SwitchDeviceProfiles.ini`, the present interval 2 the game asks for. The shipping build reads no console variables from the command line (no `-ExecCmds`, `-DPCVars` or `-ini:`), so the host writes 60 and 1 into the two console variables (`hle_fpslock.c`, profile key `fps_target 60`), whenever the game sets them back; the game then asks for present interval 1 itself. Unreal steps the game by the time that really passed: on the Mac, `DM_NVN_REGLUM` traces of the same input script from a fresh save at 30 and 60 fps (frame time p50 33.4 → 16.7 ms) put the 1.5 s fade to white 70 s in at the same real time with the same shape, and both runs reach the same screens at the same times. `DM_FPS_TARGET=30` keeps the game's own 30. -- **Keyboard:** the character's name and favourite dish come from the system keyboard (`DM_KBD` on the Mac, the input box on the iPhone). The game keeps running behind it, so the guest clock does too (it used to freeze: the sound stuttered and the box lagged on the iPhone). -- **Mac input:** a fresh save reaches the story with `DM_KBD=Lyra DM_KEYS="20:Plus,24:A*27/4,150:Plus,154:A*62/3,340:Right*12/3,341.5:A*12/3,380:Plus*5/3,396:A*20/3,470:A,474:Up,476:A,480:A*140/3"` (initial settings, the prologue, character creation with three belongings, "Begin Lyra's journey?"). -- **Saves:** the first save (about 13 minutes in) needs `fs::GetSaveDataSizeMax`; four saves in a 26-minute run, and a later boot continues from them. -- **Status:** brought up on the Mac through character creation and the story opening into free play (26 minutes from a fresh save with no red line, 2.74 GB resident), and playable on an iPhone: 30 fps through character creation, name entry and the story opening, no audio underruns, three saves, a footprint of about 2.35 GB after 3.4 minutes; it plays at 60 fps on an iPhone too (`fps_target 60`). Battles are not tested yet. - -**Pokémon Legends: Z-A (`0100F43008C44000`)** -- **Status:** brought up on the Mac: language select, character creation, the opening and the first minutes of play in Lumiose City at a steady 30 fps (a 5-minute walk: GPU about 6 ms a frame, no crash; the red lines are the render condition the host does not evaluate, the placeholder cube and an occasional memory-pool retry). Playable on an iPhone: 30 fps, with a memory footprint of about 2 GB. -- **Engine:** Game Freak's Trinity engine on SDK 19.3.11 (NVN 55) with Havok 2022.2, update 2.0.2 (v393216). The SDK 19 calls the host had to answer are in [03](agents/03-bringup.md). -- **Files and shaders:** every file sits in the Trinity packs of `arc/data.trpfs` (FlatBuffers, Oodle Mermaid). The shader split decodes them with the game's own `OodleLZ_Decompress`, run in the Mac host's tool mode (`hle_oodle.c`; the NSP script sets it up for any game with Trinity packs), and finds 28,603 shaders ([01](agents/01-extract.md)). -- **Memory:** the 5 GB guest block with two cuts for 2.0.2: the StandardAllocator's address space 12 GB → 4 GB and the SDK's node area 1 GB → 62 MB. With `mem_map_limit` the host refuses a map past the application pool, as Horizon does, so the SDK gives back the 3.15 GB it keeps mapped after `nninitStartup`. -- **Graphics:** characters are skinned with transform feedback, the colour grading LUT is drawn as a layered pass, the deferred lighting reads stencil through a stencil-mode texture view, and the occlusion queries that decide how characters are animated need the draw's base instance in the driver segment ([04](agents/04-graphics.md)). -- **Fixed:** on that route, the Mac host crashed in the driver's compute encoder end at about frame 3390 (the transform feedback draw opened its encoder beside the open copy encoder), and from about frame 5300 a fragment shader's image atomics, sent to the 64 KB dummy buffer, ran about 1.4 MB past it: the game's heap was corrupted (the game or the replay crashed) or the GPU page-faulted, after which macOS ignores the command buffers and the picture freezes. An iPhone 17 freezing with `kIOGPUCommandBufferCallbackErrorPageFault` was reported in GitHub issue #101. The atomics now go to a scratch buffer as large as the image ([04](agents/04-graphics.md)); three runs of the whole route reached frame 12,300 with no GPU error. -- **Mac input:** a fresh save reaches play in Lumiose City with `DM_KBD=Taylor DM_KEYS="f450:A,f1200:LRight~1,f1600:A,f1900:Down,f2000:Down,f2100:A,f2400:A*80/90"`; a save made there continues with `DM_KEYS="f300:A*3/150"` (play from about frame 600), and `LUp~12` then walks along the platform. - -**Cuphead (`0100A5C00D162000`)** -- **Status:** brought up on the Mac with no host change: title, save slots, the opening storybook, Elder Kettle's house, Inkwell Isle One and Forest Follies at 60 fps with no red line; playable on an iPhone. -- **Fixed:** in about half the runs, sprites broke into dark slivers or went missing (a log in Forest Follies splintered for a whole attempt), and on the iPhone the picture flickered every fourth frame. The host reported a ZCull storage size of 0; Unity's NVN pool allocator, asked for a zero-size block per depth target, gave out the offset of a live block, lost that block from its map of allocations, and later freed it with a garbage size. Buffers allocated afterwards overlapped live ones, so copies read staging bytes another copy had overwritten (every fourth frame is the upload ring's period). The size is now never 0 (`hle_nvn.c` `zcull_size`). -- **Engine:** Unity 2017.4.9f1 IL2CPP, SDK 5.5.0 (the oldest so far, NVN API 53.313), update 1.3.7 (v655360); its DLC (The Delicious Last Course) is mounted as add-on content 1. Unity 2017.4's serialized and `Shader` formats (`unityfs.py`, `shader_split.py`) and the rewriter's jump-table form (`patch_nso.py`) were found by armyja on this game (GitHub pull requests #1, #2). -- **Shaders:** 3,118 (1,558 pixel, 1,560 vertex) from the player's serialized files and AssetBundles, plus 4 GLSLC outputs in `main`. Every binding decodes. -- **Memory:** no system resource, so no virtual address memory: Unity sets a 3.12 GB heap (application pool − used memory) in a 4 GB block. The rewritten modules take 80 MB, so the profile reports 84 MB of used memory (`mem_used`); with the default 64 MB, `SetHeapSize` fails and the game aborts at its first allocation. On the Mac about 3.2 GB resident and 2.7 GB of Metal allocations, flat over the house and the map. -- **Saves:** the game writes a slot when it reaches the map, not when the slot is picked. -- **Mac input:** a fresh save goes through the title (L+R), a new slot, the storybook and the house to the map with `DM_KEYS="20:L+R,26:A,32:A,38:A,44:A,50:A,56:A*20/3,120:LRight~4,125:A,127:LRight~3,131:A*10/2,152:LLeft~3,155.5:A"` (the map at about 160 s). From that save, `DM_KEYS="20:L+R,26:A,32:A,35:A,40:LRight~1.5,42:LDown~1.5,45:LRight~3,49:LUp~1,51:LRight~3,54.5:LLeft~0.5,55.2:LUp~0.25,55.6:LRight~2,58:LUp~0.2,58.4:LRight~0.25,58.8:LUp~0.2,59.2:LRight~0.25,59.6:LUp~0.2,60:LRight~0.25,60.4:LUp~0.2,61:A,63:A"` crosses the bridge (its entry is narrow between the posts) and enters Forest Follies (play from about 68 s; Y shoots). - -**FINAL FANTASY TACTICS - The Ivalice Chronicles (`010038B015560000`)** -- **Status:** brought up on the Mac: initial settings, the opening movie, the prologue and the Orbonne Monastery battle up to the player's first turn, at 60 fps with no red line (about 1.4 GB resident); playable on an iPhone. -- **Engine:** Square Enix's engine on SDK 19.3.9 (its `sdk` is byte-identical to Hades II's), NVN, update 1.4.0 (v393216). The OpenGL driver bundled as `subsdk0` is not loaded. CRI Mana movies and ADX audio run in the game's own code. -- **Shaders:** 306: 257 GLSLC outputs in the technique files (`resources_nx/release/master/shader/*.tec`), which `shader_split.py` now reads like `main`'s 49, and one geometry shader Metal cannot build. -- **Job system:** its pools wake one worker through a shared auto-clear event and relied on the workers sleeping in index order; a build cut makes a worker woken in another's place pass the wake on (its `patches.py`; the boot hung in 4 of 6 runs) ([03](agents/03-bringup.md)). -- **Graphics:** UI textures are written by the CPU into uncached pools after creation (`tex_flush_upload`, `tex_dirty_upload`), and the UI is composited into the window texture by fragment-shader image stores read back as `r32ui`, between game barriers inside one pass ([04](agents/04-graphics.md)). -- **Mac input:** a fresh save reaches the first battle turn at about frame 21,000 with `DM_KEYS="22:A*60/2,150:Plus,152:X~2.5,165:Plus,167:X~2.5,175:A*70/2"` (A through the settings and the birthday, then + and a held X skip each part of the opening). - -**Absolum (`0100A83022A22000`)** -- **Status:** brought up on the Mac: the logo movie, the licence agreement, the main menu, the campfire character select, the intro and the first level (the "Strike / Skill" tutorial and the cave after it), at 60 fps with real sound and saves; about 2.5 GB resident. Playable on an iPhone. -- **Engine:** FNA (XNA 4 in C#) compiled with .NET NativeAOT (its own GC, regions enabled), SDL3, FNA3D's NVN driver, Wwise, SDK 18.3.2, update 1.1.1 (v393216). NVIDIA's OpenGL and Vulkan drivers (`subsdk1`, `subsdk0`) are not loaded; `subsdk2` is the movie module (H.264 logos). Epic Online Services comes as an NRO the host does not load; the game plays offline without it. -- **Code:** `main` starts its text with 1.7 MB of data, which the rewriter now leaves alone (`dm/tools/nso_code.py`). -- **Shaders:** 162 GLSLC outputs in `shaders.nx.bin`. The picture is flipped by a viewport swizzle, which the host now applies ([04](agents/04-graphics.md)). -- **Memory:** everything is virtual address memory: malloc's StandardAllocator (40 GB of address space, cut to 2.5 GB on this build; 1.75 GB of it is FNA3D's GPU heap), the .NET GC's region range (`game_env DOTNET_GCRegionRange=60000000`: 1.5 GB instead of 3.75), and the SDK cuts for its node area and its 1 GB alignment of large regions. They take 4.13 GB of the 4.375 GB alias region in a 5 GB block ([03](agents/03-bringup.md)). -- **Mac input:** a fresh save reaches the first level at about frame 9,000 with `DM_KEYS="f1500:A,f1900:A,f2300:A,f2700:A,f3000:A,f3300:A,f3600:A,f4000:A,f4400:A,f4800:A,f5400:A*30/150"` (any button, accept the licence, New game, the character select, then A through the intro and the first dialogue); `LRight` walks, Y strikes and X uses the skill. - -**Captain Toad: Treasure Tracker (`01009BF0072D4000`)** -- **Status:** brought up on the Mac with no host change: - - the opening course, the cutscene that follows it and the title menu; - - the first save, the Episode 1 book and Plucky Pass Beginnings; - - the Special Episode's book (add-on content 1). - - It runs at 60 fps with no red line. Three minutes in a course: about 950 MB resident and 690 MB of Metal allocations, flat. Playable on an iPhone. -- **Engine:** Nintendo EPD Tokyo's engine (the `al` library over sead / agl, as Super Mario 3D World and Super Mario Odyssey) on SDK 8.2.0, update 1.4.2 (v393216). Its `sdk` is the build Link's Awakening 1.1.0 runs. `main` exports its symbols (48,804 defined), so `sdis.py --find` names most functions. -- **Shaders:** 7,258, all with Mario's binding layout. The fin fur geometry shader does not build, as in 3D World. `main` imports `glslcCompile` but no module defines it, so nothing is compiled at run time ([04](agents/04-graphics.md)). -- **Graphics:** agl's post-effect program samples its own attachment, as in 3D World, and here the 16x16 magenta blocks showed on the Mac too: the profile's `feedback_snap`. -- **Input:** the profile plays it handheld (`op_mode`): docked, the game's pointer (touch blocks, stunning enemies) is the Joy-Con gyro; handheld, it is the touch screen. The virtual gamepad starts in the 3D layout (right stick: camera). -- **Add-on content:** index 1 is the Special Episode; index 2 holds a 1-byte marker file. -- **Memory:** no system resource; the game sets a 3.00 GB heap (pool − 64 MB) in the 4 GB layout. -- **Mac input:** a fresh save starts in the opening course with control of Toad. `DM_KEYS="f900:LUp+LRight~2.0,f1030:LUp+LLeft~2.2,f1170:LDown+LLeft~2.8,f1345:LDown+LRight~0.4,f1375:LUp+LRight~2.0,f1500:LDown+LRight~1.5,f1595:LUp+LRight~1.4,f1685:LUp+LLeft~0.9"` walks him to the star: - - through the arch, up the back ramp, around the rock on the grass top and over the roof; - - the course clears at about frame 2,100 and the title menu comes at about 5,200, with the first save; - - A dismisses the title's notices. - - From that save, `DM_KEYS="f600:A,f1000:LRight~0.3,f1300:A"` starts Plucky Pass Beginnings (play from about frame 1,900). How the route was found (position probe, the stage map's guide rail) is in [05](agents/05-debugging.md). - -**Brain Memory Training Academy (`0100F5D01DA3A000`)** -- **Status:** brought up on the Mac: the title, the controls screen, the level select and the levels with their Level Complete / Level Failed screens, at 60 fps with real sound and saves, no red line; about 0.75 GB resident, flat over 20 minutes. Playable on an iPhone. -- **Engine:** Unity 2022.3.5f1 IL2CPP with FMOD on SDK 16.2.4 (its `sdk` is Unicorn Overlord 1.05's build), base game 1.0.0; `subsdk0` is the movie module. -- **Shaders:** 496 in the player's serialized files (`Data/*.assets`, `Data/Resources`) and `main`. There are no bundles, so no file carries a `Shader` type tree to borrow; `unityfs.py` has Unity 2022.3's layout written out (`KNOWN_TREES`, the hash Ace Attorney Investigations Collection's bundles carry). The built-in VR blit's geometry shader does not build ([04](agents/04-graphics.md)). -- **Memory:** virtual address memory as in Ace Attorney Investigations Collection: `nninitStartup` gives StandardAllocator pool − used, which needs the node area cut on this `sdk` build (`sdk_patches.py`; Unicorn Overlord, which has no system resource, runs the same with it). Ace Attorney's 1 GB pool and 5 GB block; the live regions end at about +2.1 GB of the 4.375 GB alias region ([03](agents/03-bringup.md)). -- **Input:** controller only (stick or D-pad, A, B, +); the game never reads the touch screen, so it stays docked at 1920x1080 and the virtual gamepad starts in the 2D layout. -- **Mac input:** `DM_KEYS="f400:A,f700:A,f1000:A,f1200:Right,f1300:A"` starts a new game, enters level 1 and clears it (Level Complete at about frame 1,350). - -**The Great Ace Attorney Chronicles (`010036E00FB20000`)** -- **Status:** brought up on the Mac: the first-boot notices (the DLC found, auto-save), the title and its Options, a new game of The Great Ace Attorney: Adventures, the opening movie, the first episode's opening scenes and its trial, at 30 fps with sound and saves; about 1.1 GB resident, flat over 14 minutes. Playable on an iPhone. -- **Engine:** Capcom's MT Framework on SDK 10.4.1, NVN, base game 1.0.0 (v0). Files are ARC archives of zlib entries under `nativeNXx64/` ([01](agents/01-extract.md)); `subsdk0` is the SDK's movie module, whose extractor runs natively while the host decodes the H.264 movies (VideoToolbox). -- **Shaders:** 460 (230 vertex, 230 pixel) in one MT Framework shader package (`sa/NX_64/root.arc`), which `shader_split.py` now reads ([04](agents/04-graphics.md)); all build, every binding decodes. -- **Host:** SDK 10 calls that reach a service, found from the import list (album notifications, store demo mode, `vi::ListDisplays`, `fs::RenameDirectory`, save thumbnails, shop and friend applets, the profanity filter called by name), and two NVN calls ([03](agents/03-bringup.md)). -- **Graphics:** the textures are copied into a CPU-cached pool by the CPU after creation, with no flush of any kind (`tex_flush_upload`, `tex_dirty_upload`; [04](agents/04-graphics.md)). -- **Add-on content:** index 1 (`DLC.dlcp`); the game reports it found and added to Special Contents. -- **Memory:** no system resource; the game sets a 3.14 GB heap (pool − 64 MB) in the 4 GB layout. -- **Mac input:** `DM_KEYS="f700:A*1000/24"` (A every 24 frames) goes through the notices, the title and a new game into the opening movie; the first episode's dialogue starts at about frame 6,000 and its trial at about 20,000. - -**STEINS;GATE RE:BOOT (`01008BC024FDE000`)** -- **Status:** the logos, the title menu (its background is a movie), a new game through the prologue movies into the story, the config pages, save and quick load, at 60 fps with music, voices and movie sound; no red line in 15 minutes of story. Playable on an iPhone. -- **Engine:** M2's ADV engine (`advengine`, m2lib, the E-mote player for the animated characters) on SDK 20.5.17, update 1.0.3 (v131072). `subsdk0` is the SDK's movie library: its `movie::Extractor` demuxes the 87 H.264 / AAC MP4 movies and the host decodes them (output 4, NV12 buffers). Everything else is in the game's own PSB archives (`*_info.psb.m` + `*_body.bin`), which it reads itself. -- **Shaders:** 487 in five BNSH in `main` (483 pixel, 4 vertex); RomFS holds none. -- **Host changes:** `nn::audio::SetAudioOutVolume` / `GetAudioOutVolume` (the movie sound's AudioOut, set every frame; [03](agents/03-bringup.md)); the storage-sharing texture table drops entries whose storage another texture takes over, which kept GPU memory near 1 GB where it used to grow with every picture shown ([04](agents/04-graphics.md)). -- **Graphics:** 1280x720 in a cropped 1920x1080 window in both modes, by the game's own choice; the backgrounds' scratches, dust and colour fringes look like the art's own film effect. -- **Input:** buttons only: it reads the touch screen in handheld mode but takes nothing from it, so the profile keeps the default docked mode and the 2D pad layout (ZL / ZR phone trigger; quick save on R3 needs the 3D layout). -- **Memory:** no system resource; the game sets a 3.05 GB heap in the 4 GB layout. About 1.0–1.1 GB resident in the story on the Mac. -- **Mac input:** A on the title, Right to CONTINUE, A, Right to QUICK LOAD, A, A on the save, Left to OK, A loads the quick save. The menu takes presses late while its background movie starts (100 to 500 frames after the title's A), so check a `DM_KEYS` route's frames with screenshots. - -**Big Brain Academy: Brain vs. Brain (`0100620012D6E000`)** -- **Status:** brought up on the Mac: the title, Solo, enrolling a new student (birthday, profession, appearance, online features, the creepy-crawly filter), the main menu (Practice, Ghost Clash, Test), the Practice categories and activities from each (Whack Match, Species Spotlight, Frame Filler, Fast Focus, Flash Memory, Cubegame, the photo quiz, Tick-Tock Turn, Shadow Shift) with their results, coins and unlocks, at 60 fps with sound, the H.264 activity previews and saves, by controller and by touch. Playable on an iPhone. -- **Engine:** NintendoWare Bezel Engine 1.56 on SDK 11.4.3, NVN, update 1.2.0 (v131072). Nearly all of RomFS is in Bezel Engine archives (`.bea`, zstd entries, [01](agents/01-extract.md)); `subsdk0` is GLSLC (imported, never called: `main` holds no GLSL source) and `subsdk1` the movie module. -- **Shaders:** 2,125 BNSH in the archives' `.bfsha` entries (most of them the engine's own, in `System.nx.bea`), the layouts and `main`; `shader_split.py` reads the archives (`bea_members`, [04](agents/04-graphics.md)). The 12 geometry shaders of Bezel's line renderer do not build: the main menu's chart is drawn with it every frame (a line strip with adjacency expanded into thick lines, then copied out of the frame), so the chart's outlines are missing. Ten pixel shaders pick their texture at run time from one of two places in the game's uniform buffers; they do not load and their draws are skipped. None of the activities above uses them; one 15-minute run of activities met one of them (`e10f24835bbaa5f6`, `03GamePerception.bnbshpk`) and the process aborted moments later, before the host caught SIGABRT, so its cause is not known; a second 15-minute run was clean. -- **Host:** `nninitStartup` chooses virtual address memory or a heap by `IsVirtualAddressMemoryEnabled`, which the host answered yes for every game; the profile key `sys_resource` reports this NPDM's none. SDK 11's Opus work-buffer call, the IR camera, cached Nintendo Account info, per-file data caches and SDK 11's socket overloads ([03](agents/03-bringup.md)). The activity previews loop with Flush and Start from four threads, which corrupted the movie decoder (a host fix, `hle_movie.c`). -- **Input:** the profile plays it handheld with Boost (`op_mode handheld`, `perf_mode boost`): handheld, the game reads the touch screen (every menu and activity, and Party's two-player touch battles); docked it does not. Handheld Normal draws 1280x720, Boost 1920x1080. The buttons work as well; the virtual gamepad starts in the 2D layout. -- **Memory:** no system resource; the game sets a 3.14 GB heap (pool − used) in the 4 GB layout. It creates 1.5 GB of NVN memory pools in its first 25 s (384 + 256 MB CPU-uncached, a 320 MB physical pool for sparse mappings, 192 + 64 MB of render targets), so about 1.3 GB is resident at the title and 1.5 GB in the first activity. Over 15 minutes of activities it rose to 1.86 GB in steps as each new activity loaded (pools 217 → 292, textures 444 → 1,173), flat in between. -- **Mac input:** from a fresh save, `DM_KEYS="f1900:A,f2400:A*9/150,f3800:Right*14/150,f3875:A*14/150,f6000:A*4/150,f6900:Down*5/60,f7250:A,f7500:A*17/150,f10050:A"` enrolls a student (birthday 01/01/2000, the default appearance, no online features) and starts Whack Match from Practice at about frame 10,050 (`/run/enroll.sh`). In handheld mode `Tx` taps work too: `f1900:T0.5x0.88` taps Start on the title. - -**Patrick's Parabox (`0100E44016C22000`)** -- **Status:** brought up on the Mac: the first-boot language menu (all ten languages drawn), the title, the hub, Intro 1 solved and Intro 2 with undo, reset and the menu, at 60 fps with music and saves (progress kept across a restart); no red line, about 0.42–0.48 GB resident, flat over four minutes. Playable on an iPhone. -- **Engine:** Unity 2020.3.30f1 IL2CPP with FMOD on SDK 13.3.7, update 1.0.1 (v65536); `subsdk0` is the movie module, which the game never uses. -- **Shaders:** 486, all Unity built-ins (`Data/Resources/unity default resources`, `unity_builtin_extra`) plus the player's 11 GLSLC outputs in `main`. There are no bundles, so `unityfs.py` has Unity 2020.3's `Shader` layout written out (`KNOWN_TREES`: the type tree of Engage's bundles, the same hash). The built-in VR blit's geometry shader does not build ([04](agents/04-graphics.md)). The whole game draws with three shader pairs. -- **Fonts:** its only font, Jost, falls back to the shared fonts for Japanese, Korean and Chinese text, which Unity's player opens with FreeType straight from `GetSharedFontAddress`; the host gives a game that imports no `ScalableFontEngineHelper::Decode` the TTF itself ([03](agents/03-bringup.md)). -- **Memory:** the NPDM declares 16 MB of system resource (`sys_resource`), so virtual address memory as in Ace Attorney Investigations Collection and Brain Memory Training Academy: `nninitStartup` gives StandardAllocator pool − used, which needs the node area cut on this `sdk` build (`sdk_patches.py`); their 1 GB pool and 5 GB block. The alias region is mapped up to +1.5 GB with 0.27 GB in use. -- **Input:** controller only (stick or D-pad to move, A redo, B undo, X reset, + menu): it stays docked at 1920x1080 with the 2D pad layout. -- **Mac input:** a fresh save solves Intro 1 with `DM_KEYS="f700:A,f1000:A,f1300:A,f1600:A,f2400:Right,f2450:Down,f2500:Down,f2550:Down,f2600:Down,f3000:Up*3/30,f3090:Right*3/30,f3180:Down*2/30,f3240:Up*3/30,f3330:Left*4/30"` (English, the title, into Intro 1 from the hub at about frame 2,700, solved and on to Intro 2 at about frame 3,500). - -**Celeste (`01002B30028F6000`)** -- **Status:** brought up on the Mac with no host change: the logos, the title, a new file and the prologue (walking, jumping, the camera following Madeline), at 60 fps with music and sound, no red line; about 1.6–1.7 GB resident, flat over five minutes. Playable on an iPhone. -- **Engine:** MonoGame for the Switch: MonoGame's and the game's C# compiled to C++ by Sickhead's BRUTE into `main` (78 MB with its bss), FMOD Studio (its banks in `Content/FMOD/Desktop`, sound through AudioOut), SDK 15.3.2, NVN, update 1.4.1.1 (v589824; the title shows v.1.4.1.0). Only `main` and `sdk`. -- **Shaders:** 26 (20 pixel, 6 vertex) in MonoGame effects: the 13 XNB files in `Content/Effects` and SpriteBatch's built-in effect in `main`, which `shader_split.py` now reads (`mgfx_effect`, [04](agents/04-graphics.md)). Every binding decodes. -- **Memory:** no system resource (`sys_resource 0`), so no virtual address memory: the game sets a 3.11 GB heap (pool − used memory) in the 4 GB layout. The rewritten modules need 100 MB of used memory (`mem_used`). -- **Input:** controller only; the game maps its buttons by position, so B and X jump. The virtual gamepad starts in the 2D layout. -- **Mac input:** a fresh save reaches the prologue with `DM_KEYS="f1850:A,f2200:A,f2600:A,f3000:A"` (the title, Climb, a new file, Begin; control of Madeline from about frame 5,000); then `f5100:LRight~1.0,f5400:LRight+X~0.3` walks to the first ledge and jumps onto it. - -**DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (`01006C300E9F0000`)** -- **Status:** brought up on the Mac through the opening movie, the title (name entry, system settings), the prologue movie (skipped with +) and into free play at Cobblestone Tor (the hero, Gemma, the villagers, the HUD and the minimap), at 30 fps with sound and saves; no red line, about 2.1 GB resident (1.1 GB of it Metal allocations), 1.5 GB mapped in the guest. Playable on an iPhone. -- **Engine:** Unreal Engine 4.18 on SDK 7.3.2, update 1.0.4 (v262144), with Square Enix's own sound driver (`SQEX::Sd`, "SEAD") on the audio renderer; `subsdk0` is the movie module, driven by Unreal's Switch movie player (VP9 with two 6-channel Vorbis tracks). -- **Files and shaders:** legacy version 4 paks whose blocks are LZ5-compressed, decoded on the Mac with the game's own `LZ5_decompress_safe` (`uepak.py`, `DM_OODLE_SERVE` after `nninitStartup`, since the decoder uses the game's malloc). The 38,892 shaders are inline in the cooked packages and in `GlobalShaderCache-GLSL_SWITCH.bin`, each a zlib stream of an `LSLG` record (`shader_split.py` `ue_legacy_jobs`). The colour-grading LUT's slice-writing geometry shader is folded into its vertex shader's layer entry points (`prog_fold_gs`); the other 22 geometry shaders do not build ([04](agents/04-graphics.md)). -- **Sound and movies:** SEAD refills its voices without waiting on the renderer event, so catch-up frames wait for queue space. The movie player registers its decoder events before filling them (read at signal time), plays only when every track holds a decoded output (audio decoders keep 32), and after a skip sends an end-of-stream packet for every input it gets (none is handed out after end-of-stream) ([03](agents/03-bringup.md)). Its 6-channel Vorbis tracks decode right only since stb_vorbis's fix for type-2 residues with more than two channels ([README](../dm/third_party/stb_vorbis/README.md)); before that, the movie sound came out distorted, with peaks far past full scale. -- **Memory:** Unreal's one address region is capped at 4064 MB (`game_args`, MB up to 4095); two cuts on the `sdk` build (node area 62 MB, 32 MB alignment for regions of 1 GB and more) put it 64 MB into Engage's 5 GB block. Its cube-map array (510 faces) needed NVN's layer-face count ([04](agents/04-graphics.md)). -- **Frame rate:** 60 fps (30 on the Switch). The profile's `fps_target` writes 60 into this build's `t.MaxFPS` and 1 into `nvn.SyncInterval` (`hle_fpslock.c`, both threads' copies; the game's config sets 30 and 2); `DM_FPS_TARGET=30` (`Documents/env.txt` on the iPhone) keeps the game's 30 and saves power. On the Mac (M4 Pro) the field holds 60 fps (frame time p95 17 ms, GPU 6.6 ms p50 / 9.7 ms p95) at normal speed, since Unreal steps the game by the time that really passed, with sound intact. The game's dynamic resolution budgets are for 30 fps (`r.DynamicResolution.*TimeBudget` 28–35 ms in `SwitchEngine.ini`), so it does not lower the resolution to reach 60: where a frame needs more than 16.7 ms, frames are dropped instead. -- **Mac input:** with the save of an earlier run (the name entered), `DM_KEYS="20:A*150/3,250:Plus*4/10,480:B,483:LUp~4"` skips the prologue movie at about 270 s and walks on Cobblestone Tor at about 480 s. - -**Monster Train 2 (`010051701FB46000`)** -- **Status:** brought up on the Mac: the title, the 90 s intro movie with its sound, the tutorial battle (deployment, three waves, enemies on two floors), the merchant and the route map, at 60 fps (30 on the Switch) with music and saves (the intro plays once per save); no red line, about 2.17 GB resident and 1.21 GB of Metal allocations, flat over twelve minutes. Playable on an iPhone. -- **Engine:** Unity 2022.3.54f1 IL2CPP with FMOD on SDK 18.3.5, update 1.1.1 (v131072); its `sdk` is Super Mario Galaxy's build, and `subsdk0` is the movie module, whose `movie::Extractor` demuxes the intro (`StreamingAssets/intro_fmv.mp4`, H.264 High and AAC). -- **Host:** three general fixes ([03](agents/03-bringup.md)): `svcGetInfo` type 24 (FreeThreadCount), which the host answered 0, so Unity never created the thread its main thread waits for at boot; 64 output slots for a movie's audio decoder (8 when the game was brought up, 32 since DRAGON QUEST XI S): with 8, Unity's movie player held every slot and the intro hung on a black screen, and it peaks at 30; `nvnSyncSetDebugLabel` (a no-op). -- **Shaders:** 15,937: 15,841 vertex, pixel and geometry shaders in the player's serialized files and bundles, the player's 11 GLSLC outputs in `main` (one of them compute), and 85 kernels of the post-processing stack's `ComputeShader` objects in `Data/sharedassets2.assets`, which the split now reads (`unity_compute`, [04](agents/04-graphics.md)); without them the colour grading LUT was never baked and the 3D battle scene stayed black behind the UI. No file carries a type tree for them, so `unityfs.py` has Unity 2022.3.54f1's `ComputeShader` layout (`KNOWN_TREES`); the `Shader` objects use the 2022.3 layout already there. The built-in VR blit's geometry shader does not build. -- **Memory:** the NPDM declares 16 MB of system resource (`sys_resource`), so virtual address memory with no heap, as in Ace Attorney Investigations Collection, Brain Memory Training Academy and Patrick\'s Parabox, on the node-area cut this `sdk` build already had (`sdk_patches.py`); their 1 GB pool and 5 GB block. With the hardware pool StandardAllocator's region alone reached +3.9 GB of the 4.38 GB alias region in `nninitStartup`. The alias region is mapped up to +3.25 GB within 90 s and stays there (+3.5 GB in a run to the first shop), at most 1.75 GB of it in use. -- **Input:** controller only: the game reads no touch, docked or handheld (taps on the title and on route map nodes in handheld mode did nothing). It stays docked at 1920x1080 with the 3D pad layout (`pad_layout full`), whose right stick scrolls the route map as the game\'s prompt shows; A selects and plays cards, B goes back, Y ends the turn. -- **Frame rate:** 60 fps (30 on the Switch). The game asks for present interval 2 (Unity's 30 fps setting) and nothing else holds it at 30: the profile's `present_interval display` presents a frame every display tick, and the iPhone's display link runs at 60 Hz. The GPU takes about 2 ms a frame in battle and on the route map (M4 Pro). Its animations follow the time that passed (Unity's `deltaTime`; the PC versions run at any refresh rate): from one save, screenshots every third of a second through the move to the merchant (its fade-in, the greeting) match at 30 and 60 fps, mean difference 3.9 against 11.0 for what frame counting would show. `DM_PRESENT_INTERVAL=game` keeps 30. At 60 fps a read into a texture pool lost a race with the dirty-page tracker's re-protection (two runs in three; [03](agents/03-bringup.md)). -- **Mac input:** from a fresh save, `DM_KEYS="14:A"` starts the game; the intro movie plays to its end (A, B and + do not skip it) and the tutorial battle loads at about frame 3,400; `DM_KEYS="14:A,110:A*90/2,111:Y*20/9"` plays through it to the merchant by about frame 8,000. A save past the intro starts at the title, and A there continues the run. - -**Raging Loop (`010020400E1C2000`)** -- **Status:** brought up on the Mac: the publisher logo, the opening movie with its sound, the title, options, a new game through the tutorial and the prologue (the date card, the opening pictures, choices, the in-game menu, scenario chart and log), at 60 fps as on the Switch (the game asks for present interval 1), with saves; no red line at run time, about 0.6 GB resident at the title and 0.77 GB after ten minutes of story (it grows with each new picture and slows down), 0.4 GB of Metal allocations. Playable on an iPhone. -- **Engine:** Unity 2018.3.10f1 IL2CPP with FMOD on SDK 7.3.2, base game 1.0.0 (v0); its `sdk` and `subsdk0` (the movie module, which plays `StreamingAssets/Movies/*.mp4`, H.264 Main and AAC) are DRAGON QUEST XI S's builds, so that build's cuts apply as they are. The story's pictures are loose PNG / JPG files in `StreamingAssets` that the game loads itself. No host change was needed. -- **Shaders:** 452: the 48 `Shader` objects of the player's serialized files (`Data/*.assets`, the Unity built-ins in `Data/Resources`; 450 subprograms) and the player's 4 GLSLC outputs in `main`. There are no bundles, so no file carries a `Shader` type tree to borrow; `unityfs.py` has Unity 2018.3's layout written out (`SHADER_2018_3` in `KNOWN_TREES`, from AssetRipper's TypeTreeDumps, [04](agents/04-graphics.md)). The two built-in `ComputeShader` objects stay unread (a red line in the split); the game dispatches no compute. -- **Memory:** the NPDM declares 16 MB of system resource (`sys_resource`), so virtual address memory with no heap, in Engage's 5 GB block with the hardware pool (3.2 GB). Unlike Unity 2020 and 2022, Unity 2018.3 reserves no address regions of its own: every page the game maps lies in StandardAllocator's region, which ends at +3.21 GB of the 4.38 GB alias region, so the pool can stay as on the Switch (377 MB mapped at the peak over the title, the tutorial and the opening, `DM_SVC` map log). -- **Input:** controller only: the game reads no touch, docked or handheld (a handheld run with taps on the title's READ logged no touch read). It stays docked at 1920x1080 with the 2D pad layout; menus answer the left stick as well as the D-pad, and no menu item is selected until a direction is pressed. A advances and selects, B goes back, X opens the menu, Y the scenario chart, L the log, R auto mode. -- **Mac input:** from a fresh save, `DM_KEYS="f700:A,f900:LLeft,f1000:LLeft,f1100:LLeft,f1200:A,f1500:A*14/120,f3300:LDown*5/60,f3640:A,f3900:A*35/120"` skips the opening movie, starts READ, reads through the tutorial, picks "I'm good (to main story)" and reads into the prologue (the motorbike picture at about frame 6,600). - -**Paper Mario: The Thousand-Year Door (`0100ECD018EBE000`)** -- **Status:** brought up on the Mac: the title, a new file, the storybook prologue, Peach in Rogueport, Mario's house, the letter and the boat into Rogueport, with Mario under control on the docks, with music and sound, at 60 fps (30 on the Switch) and at the game's normal speed; no crash in eight minutes. Playable on an iPhone since the spin waits (CPU, below); the first build ran Rogueport at 24 fps there. -- **Engine:** Intelligent Systems' engine on NintendoWare (G3d, Vfx, Atk) on SDK 15.3.2 (Celeste's `sdk` build), NVN, update 1.0.1 (v65536). Nearly every RomFS file is zstd, named `.zst`; the event scripts and data tables are ELF files the engine reads as data. `subsdk0` is the SDK's movie library: WebM movies (VP9 + Vorbis) through `movie::Extractor`, decoded by the host. -- **Shaders:** 11,858 BNSH in `shaders/*.bfsha.zst` and `effect/particle/*.ptcl.zst`, which `shader_split.py` found only once it decompressed zstd by magic rather than by the `.zs` name. 2,386 pixel shaders pick their texture between bound slots at run time (profile key `tex_dyn_slots`), and the reflection shaders index an array of handles in a uniform buffer, which the host now binds ([04](agents/04-graphics.md)). The 4 domain shaders of `tessellation.bfsha` have no Metal stage; nothing seen so far draws with them. -- **Host changes:** `nn::settings::GetAvailableLanguageCodes`; a rewriter rule for `sub sp, sp, xN`, which the game's script call bridges use with sizes that leave sp misaligned ([03](agents/03-bringup.md)); `nvnCommandBufferClearColori` / `ClearColorui`, a format-converting copy into a texture made without render-target use, and an unbound uniform slot reading zeros ([04](agents/04-graphics.md)). -- **Frame rate:** 60 fps (30 on the Switch). The game asks for present interval 2, fixed at startup, and steps its clocks in 60 Hz vsyncs: every frame `main+0x2e7750` measures the time since the last one (`oe::GetProgramTotalActiveTime`), rounds it to vsyncs and clamps it to at least 2, and the base clock (`main+0x2e753c`; the field and battle clocks below it, the scripts' frame counts, timers and fades follow it) advances by that step. Presented every vsync without a change, it ran at double speed (each fade took half its time). Two changes on 1.0.1's `main` (its `patches.py`): the floor 2 → 1, and a frame-step stub (`FRAME_STEP`) that gives the base clock the host's step, the vsyncs each frame stays on screen, so a frame that takes between one and two vsyncs does not round the game slow; the log's `frame step: game time … of real time` reads 0.994–1.003. With the profile's `fps_target 60` the fades take as long as at 30 fps (the same press script, `DM_NVN_REGLUM` brightness curves). `fps_hold off`: holding late frames made them slower (the frame waits for three render worker threads, idle during a hold), and the hold stuck at 20 fps on a loaded Mac. `DM_FPS_TARGET=30` keeps the game's 30. -- **CPU:** the first iPhone build ran Rogueport at 24 fps: frame p50 42 ms against 8 ms of GPU time, with the three `gfx::ParallelManager` workers (pinned to cores 0–2, priority 45) at 94% of a core each and the main thread waiting for them. `main+0xa8e680` is their loop. While a batch is pending, an idle worker polls the job cursor (`main+0xa8e784`) through a serial job, which only worker 1 runs, and through the end of a parallel-for, registering with it and backing out. The first worker to finish a parallel-for polls until the others are done (`main+0xa8e9a8`). None of these polls makes an SVC; they are what the general spin wait rule now handles (Spin loops, above). -- **Memory:** 8 MB of system resource (virtual address memory) and no heap: the game's malloc reserves 62 GB of address space, cut to 4 GB on `main`, with Absolum's node area and alignment cuts on this `sdk` build, in Engage's 5 GB block. It maps 1.5 GB at boot and 2.0 GB by Rogueport; about 2.4 GB resident on the Mac there. -- **Input:** buttons and sticks, docked (1920x1080). The D-pad opens the Gear and Journal menus, so the virtual gamepad starts in the 3D layout (`pad_layout full`). -- **Mac input:** a fresh save reaches Rogueport's docks with `DM_KEYS="35:A,45:A,55:A,65:A*195/2"` (the title, File 1, Start, then A every 2 s through the dialogue; Mario is under control at about 415 s, at 60 fps as at 30). - -**DARK SOULS: REMASTERED (`01004AB00A260000`)** -- **Status:** brought up on the Mac: the logos and the opening movie, the title (offline), the first-run Brightness, Options and Button settings, character creation with the name through the inline keyboard, the opening cutscene and the Undead Asylum cell (walking, the camera, pillaging the corpse), at 60 fps with music and sound; no red line but the conditional rendering note; about 1.6 GB resident, flat over three minutes. Playable on an iPhone at 60 fps and the game's normal speed. Once, loading a save on the iPhone stayed on the loading screen (no save write, no red line); the next launch loaded it, and it has not come back. -- **Engine:** FromSoftware's dantelion2 engine with Havok and FMOD (sound through one 6-channel AudioOut) on SDK 4.5.0, the oldest so far, NVN API 53.313, update 1.0.3 (v196608). `subsdk0` is the SDK's movie library: its `movie::Extractor` demuxes the H.264 MP4 movies (`movww/`, `movjp/`) and the host decodes them into the game's RGBA texture. Everything else is in the two dvdbnd archives (`.bdt` + `.bhd5`, DCX zlib entries, BND3 archives), which the game reads itself. -- **Shaders:** 3,099 GLSLC outputs in six BND3 archives of `dvdbnd0.bdt` and in `main`, read by `shader_split.py` (`bdt_shaders`); SDK 4's GLSLC outputs are read through their section headers (`glslc_section`, which also gave Three Houses 8 and Age of Calamity 3 shaders that used to be dropped) ([04](agents/04-graphics.md)). -- **Host changes:** SDK 4's movie decoder constructor and its buffer without a sync object, `friends::GetBlockedUserList` without a `Uid`, the UTF-16 profanity masking by name, and `Poll` / `Select` with nothing to wait on sleeping out their timeout (the network thread kept a core at 100%) ([03](agents/03-bringup.md)); handles read from uniform buffers take the base of the descriptor pool bound at replay, and the bound pools' bases live in each command buffer (the walls smeared sideways) ([04](agents/04-graphics.md)). -- **Frame rate:** 60 fps (30 on the Switch). The render thread ends each frame in the game's own CPU limiter, called with the constant 30 (`main+0x706fa4` → `main+0x707090`); a cut makes it 60 (its `patches.py`). The game does not step by the time that passed: its frame update takes a fixed 1/30 s (`main+0x667904`), so with the limiter alone it ran twice as fast on the iPhone; a second cut gives it 1/60, as the community's 60 fps cheat for this build does ([03](agents/03-bringup.md)). Climbing ladders still misplays at 60 (with that cheat too). The stalls it had once a second at either rate came from the host's real-time declaration for that thread, which now covers the thread's peak frames (`rt_comp_tick` in `hle_nvn.c`). -- **Input:** the game maps its buttons by position: B confirms, A goes back. The profile starts the virtual gamepad in the 3D layout (right stick for the camera, D-pad for spells and items). -- **Memory:** no system resource; the game sets a 3.10 GB heap (pool − used) in the 4 GB layout. The rewritten modules need 110 MB of used memory (`mem_used`). -- **Mac input:** from a fresh save, `DM_KBD=Ash DM_KEYS="20:A~0.2,25:Plus~0.2,40:A~0.2,50:A~0.2,60:A~0.2,70:B~0.2,80:Down~0.08,82:B~0.2,90:Up~0.08,92:B~0.2,100:Down~0.08,102:B~0.2,110:B~0.2,116:B~0.2,122:B~0.2*15/3,168:Left~0.1,170:B~0.2,185:Plus~0.2,190:B~0.2,200:Plus~0.2,210:B~0.2"` goes through the two network notices, New Game, the first-run settings, character creation (a Warrior with the default look) and the opening into the Asylum cell (control at about 245 s). The steps are in seconds, not frames: the loads take different frame counts from run to run. From a save made there, `DM_KEYS="20:A,25:Plus,40:A,50:A,60:A,70:B,76:B,82:B"` continues into the cell at about 90 s. - -**Hollow Knight: Silksong (`010013C00E930000`)** -- **Status:** brought up on the Mac: the logos, the title, a new file, the intro poem, the opening cinematic with its sound, and Hornet under control in the first two rooms of Moss Grotto (bind, jump, attack), with music and sound, at 60 fps as on the Switch (30 while a cinematic plays, as the game asks); memory flat in play. Playable on an iPhone. The base game alone (1.0.27776: another `main`, the same `sdk` and movie module) packs from a derived profile with its own 2,244 shaders and plays the same opening on the Mac with no red line. -- **Engine:** Unity 6000.0.50f1 IL2CPP with FMOD (one AudioOut) on SDK 20.5.8, update 1.0.30000 (v655360). Nearly everything is in 2,068 Addressables bundles (`StreamingAssets/aa/Switch`); `subsdk0` is the SDK's movie library, whose `movie::Extractor` demuxes the 27 MP4 cinematics (H.264 and AAC). While one plays the game draws 1280x720 with a window crop and present interval 2. -- **Shaders:** 2,450: 1,189 vertex, 1,193 pixel, 67 compute and one geometry shader (which does not build), 11 of them the player's own GLSLC outputs in `main`. The bundles carry their type trees (they report the engine version `0.0.0`), so `unityfs.py` read Unity 6's `Shader` objects with no layout written out ([04](agents/04-graphics.md)). -- **Host changes:** `nn::hid::GetDebugPadStates`, which Unity 6 polls every frame and the SDK's own aborts on (Result 0x2aca) ([03](agents/03-bringup.md)). -- **Memory:** 16 MB of system resource (virtual address memory) and no heap: Unity 6's `nninitStartup` sets up no StandardAllocator, and Unity reserves and commits its memory itself (`AllocateAddressRegion`, `AllocateMemoryPages`). Engage's 5 GB block with the hardware pool, and the node-area cut on this `sdk` build (`sdk_patches.py`): the alias region reaches +2.26 GB in the first room (+3.00 GB without the cut), 1.5 GB mapped, about 2.15 GB resident on the Mac. `profile_guess.py` now gives every game with a system resource the 5 GB block; it used to give heapless ones 256 MB of alias region. -- **Input:** A binds, B jumps, Y attacks (the game's own prompts name A and B). It stays docked at 1920x1080 with the 2D pad layout; whether it reads touch in handheld mode has not been tried. -- **Mac input:** from a fresh save, `DM_KEYS="f1600:A,f1900:A,f2200:A,f2500:A,f2800:A,f9400:A~1.5,f9700:LLeft~60,f9800:B~0.5*60/50,f9830:Y*60/50"` starts a new file in slot 1, plays the poem and the opening cinematic (control at about frame 9,200), binds, and walks left into the second room (its "B Jump" prompt), jumping and attacking as it goes. - -**Catherine: Full Body (`0100BF00112C0000`)** -- **Status:** brought up on the Mac: the ATLUS logo, the title, the puzzle type choice, the Golden Playhouse intro, the anime opening (CRI movies with sound) and the first Nightmare stage with Vincent under control, no red line in a 6-minute run. Plays on the iPhone. -- **Engine:** Atlus's GFD engine on SDK 8.2.1, NVN, update 1.0.1a (v131072); CRI middleware: everything in five CPK archives (`data/*.cpk`), ADX2 sound through one 6-channel AudioOut, Sofdec2 movies (`data/movie*/*.usm`) whose H.264 goes through the SDK's `movie::Decoder` (the host's VideoToolbox decoder). `subsdk0` is GLSLC, `subsdk1` the SDK's movie library. -- **Shaders:** 3,128 GLSLC outputs: 3,050 loose files in `data/shaders_binary`, 76 in `main` and 2 the game compiles at boot with its own GLSLC (captured on the Mac, as Three Houses'). This GLSLC (GPU code version 1.15) writes every vertex shader as a VertexA part (shader type 0, the position) and a VertexB part (the other outputs) in one code area; `shader_split.py` reads type 0 as a vertex shader (`sph_stage`) and `dmsh_build.py` merges the two parts as Ryujinx does (`vertex_ab`, [04](agents/04-graphics.md)). Without the merge every varying was lost and the screen was black. -- **Host changes:** the GLSLC GPU code version range follows the game's own GLSLC (it refused to compile against SDK 7's 1.5–1.14), `nn::settings::GetAvailableLanguageCodeCount`, `nv12-uv-stride` in the movie decoder's output format (CRI's player reads it unchecked), and a command-buffer commit right after buffer → texture copies, at the next pass boundary (the game reuses its staging memory about a frame after recording a copy; [03](agents/03-bringup.md), [04](agents/04-graphics.md)). -- **Memory:** no system resource; the game sets a 3.11 GB heap (pool − used) in the 4 GB layout. The rewritten modules need 96 MB of used memory (`mem_used`). On the Mac (M6) in the first stage: 0.79 GB resident, 1.9 GB of Metal allocations. -- **Frame rate:** 30 fps, as on the Switch (present interval 2). GPU p50 4.7 ms, CPU about a fifth of a core on the Mac. -- **Input:** + pauses a cutscene (B resumes, Y skips); the left stick moves Vincent, A grabs blocks. -- **Mac input:** from a fresh save, `DM_KEYS="5:A*25/2,60:Plus*14/4,61:Y*14/4,62:A*14/4"` skips the intro scenes into the first Nightmare stage (about frame 3,300); `120:LUp~0.4*40/1.5` then climbs. - -**Digimon Story Cyber Sleuth: Complete Edition (`010014E00DB56000`)** -- **Status:** brought up on the Mac: the logos, the title, the privacy policy, the DigiLab story choice, the EDEN chat opening (movies and real-time scenes), character creation and the login, into EDEN (the fly-in, then its entrance plaza in 3D with Taiga under control), with sound, at 60 fps in the menus and 30 from the story on (the game's own rates); no red line. About 0.95 GB resident (0.3 GB of Metal allocations in EDEN), flat over half an hour. Playable on an iPhone. -- **Engine:** Media.Vision's MVGL engine (the PC version's) on SDK 6.4.0, update 1.0.3 (v196608). It renders with **OpenGL**: NVIDIA's desktop GL 4.5 core profile through EGL, from `subsdk0`, which is not loaded. `main` gets every GL function from a glad loader (`eglGetProcAddress`, 2,956 names) and calls 79 of them; the host implements those on Metal (`dm/gfx/gl/hle_gl.m`). Its NVN device serves only a helper beside the movie player. CRI's middleware plays the sound (one 6-channel AudioOut) and the USM movies (H.264 through the SDK's movie library, `subsdk1`, into NV12 buffers the game uploads into GL textures). -- **Files:** everything but the movies is in nine MVGL archives (`DSDB*.nx64.mvgl`: `MDB1`, a name tree, doboz-compressed entries; `dm/gfx/tools/mvgl.py` lists and reads them); four of them are CRI AFS2 sound banks under the same extension. -- **GL layer:** one context; GL calls are encoded into Metal as they come, one command buffer per frame; GL's bottom-up orientation through the NVN layer's lower-left origin; textures made on first use from the levels given; buffers orphaned on `glBufferData`; the window surface (the size of the game's `vi::CreateLayer`, 1920x1080) presented through the NVN layer's present path, paced by the swap interval ([04](agents/04-graphics.md)). -- **Shaders:** GLSL ES 3.20 source compiled at run time: 1,682 `.shad` files in the archives (615 distinct) and 37 strings in `main`. `dm/gfx/tools/gl_shaders.py
[--ios ]` translates them with glslang and SPIRV-Cross into `/gl/` (676 shaders: built-in strings are translated as each stage that compiles); the host finds each by the hash of its text. `DM_GL_CAPTURE=` writes every source the game compiles, which `gl_shaders.py` reads like a RomFS; every one captured on the route so far is in the archives or `main`. -- **Host changes:** the add-on content queries run on the big host stack (`aoc_load` logs from a game thread with little stack left) ([03](agents/03-bringup.md)); `vi::CreateLayer(w, h)` keeps the layer's size for the window surface. -- **Memory:** no system resource; the game sets a 3.13 GB heap (pool − used) in the 4 GB layout. The rewritten modules need 74 MB of used memory (`mem_used`). -- **Packing:** for a GL game the NSP script runs `gl_shaders.py` (Mac and iOS libraries in `_build/shader/gl` and `shader/ios/gl`) in place of the NVN shader split and chain build; it needs glslang (`brew install glslang`). -- **Mac input:** a fresh save reaches EDEN with `DM_KBD=Taiga DM_KEYS="f2000:A*26/200,f7300:Right,f7400:A,f7600:A,f7800:Right,f7850:A,f8100:A*39/250,f9000:Plus*15/600,f18000:Down*3/1200,f18100:A*3/1200,f21700:Left*300/250,f21800:A*300/250"` (the title, the privacy policy, the DigiLab: Cyber Sleuth, no options, A through the opening chat; the login form comes at frame 18,500 to 19,700). The name is a field of that form: A opens the keyboard, and the game's name screen stays up about 500 frames after the text arrives, where another A opens it again, so Down and A come 1,200 frames apart. The login prompt defaults to No, hence Left before each A from there; EDEN at about frame 22,500. - -**Animal Crossing: New Horizons (`01006F8002326000`)** -- **Status:** brought up on the Mac: a new game through Tom Nook's counter (name entry on the game's own keyboard, birthday, look, hemisphere, island choice), the in-flight movie with its sound, the island's creation and the arrival (Timmy's welcome, the player at the dock under control), with the Happy Home Paradise add-on mounted; no red line but the ones below. Plays on the iPhone. A save is not reached on the Mac: the game first saves when the player sleeps at the end of the first day (the `−` menu says "You can't save right now." before that); until then it only probes for `main.dat`, `mainHeader.dat` and `Moved/personal.dat` in its device save data. -- **Engine:** Nintendo EPD's own (code name Park; sead / agl / gsys, NintendoWare G3d / Ui2d / Vfx2 / Atk / Font, Havok, Pia and NEX) on SDK 20.5.4, NVN, update 3.0.3 (v2228224); `subsdk0` is the SDK's movie library. RomFS: 97,997 zstd files (`.zs`), BWAV sound, a few Yaz0 SARC archives; the in-flight movie `Movie/OpeningMovie.webm` is VP9 1080p with Vorbis, demuxed by `movie::Extractor` and decoded on the host. -- **Shaders:** 9,766 BNSH programs: `Shader/Park_UBER_All` (3,385) and the other bfsha / sharcb in `Shader/`, agl's `System/Agl/agl_resource` (3,206), particle effects in `Effect/` (2,522), models' own bfsha in `Model/`, layouts in `Layout/`, the game's keyboard in `Swkbd/` and 40 in `main`. The keyboard's ocean background has 8 geometry shaders, which do not translate; the name entry draws without them. -- **Rewriter:** its 63 MB `main` was the first to put branches out of range of the patch area: halfword jump tables now get only as many landing pads as their largest entry (`half_count`, [03](agents/03-bringup.md)); the patch area went from 42 MB to 7.9 MB. -- **Host changes:** the game's own HIPC client for `olsc:u` (cloud backup settings) is answered by host sessions (`hle_sf.c`); device save data is the host save directory, and a second mount name over one save shares its transaction; the adjustable user clock, the region code and SDK 20's album and steady-clock calls; video movie decoders keep 16 output slots (its player holds every one it is offered); and a run of yields restarts when the yielding loop's x19–x28 change, so loops that yield between steps of work run at full speed (the island's creation took 77 s of black screen; [03](agents/03-bringup.md)). -- **Graphics:** `render_cond on`: the game skips clears of targets nothing drew into with conditional rendering, as Tears of the Kingdom does; without it every frame was black. -- **Memory:** no system resource; the game sets a 3.09 GB heap in the 4 GB layout. The rewritten modules need 122 MB of used memory (`mem_used`). On the Mac at the dock: 1.85 GB resident, 1.15 GB of Metal allocations. -- **Frame rate:** 30 fps, as on the Switch. -- **Prewarm list:** recorded from a fresh save through the counter, the movie and the arrival to the dock (965 lines). -- **Input:** the 3D gamepad layout (`pad_layout full`): the D-pad holds the tool ring and the right stick turns the camera. -- **Mac input:** from a fresh save, `DM_KEYS="40:A*51/2,145:B*12/0.4,151:Down,152:Right*4/0.4,154.5:A,155.5:Right*4/0.4,158:A,159:Down*2/0.4,160.5:Left*2/0.4,162:A,163.5:Plus,167:A*4/2,175:Plus,178:A*146/2,179:Plus*30/4"` names the player "tom" on the keyboard, takes the defaults, watches the movie and reaches the dock at about frame 14,100 (470 s). - -**DRAGON QUEST VII Reimagined (`0100A9D01C446000`)** -- **Status:** brought up on the Mac: the logos and opening movie with sound, the title, a new adventure log (name entry, settings), the opening cutscene (the well, the night scene with Maribel by the boat) into play at Pilchard Bay with the controls tutorial, and a saved log continued, at 30 fps with saves, and the opening cutscene into the first free play (the hero's house) again at 60 fps (`fps_target 60`); no red line at run time apart from the geometry shaders below; about 2.6 GB resident (1.1 GB of Metal allocations) in the cutscene. Playable on an iPhone at 30 fps; 60 fps on an iPhone is not tested yet. -- **Engine:** Unreal Engine 4.27 (HEXA DRIVE) on SDK 19.3.1, update 1.2.0 (v131072): IoStore containers beside one pak (`pakchunk0-Switch.pak`, version 11, its footer magic 0xcc bytes from the end), the desktop renderer with forward shading, PhysX 3.4, CRIWARE sound (a 6-channel AudioOut) and Sofdec movies (VP9 through the SDK's movie decoder, mode 1, NV12). `subsdk0` is the movie module. -- **Shaders:** 62,901 in the pak's two shader code archives, every one an LZ4 block (`uepak.py` `lz4_shaders`; the NSP script runs no Oodle host for them). The game has no PC version, so the facts table types nothing; a size query on an image handle now takes the image's type ([04](agents/04-graphics.md)). 62 of the 63 geometry shaders (one-pass cube shadow maps, six layers from one draw) do not build; their draws are skipped (point light shadows). -- **Host changes:** the audio renderer at 32 kHz (its upsampler exists only for DSP rendering, so the host resamples 3:2 itself), `GetAudioDeviceOutputVolume`, the shop and Nintendo Account calls of the title screen, `kpr::GetKeyCodeMap`, the movie decoder's `nv12-uv-stride` (grey movie in bands), stencil-mode texture builders (the hero dithered), and texture slots 32–39 reading the bound image (characters glowing white in cutscenes) ([03](agents/03-bringup.md), [04](agents/04-graphics.md)). -- **Memory:** Unreal's MallocBinned3 reserves 37 GB of pools in the engine region, so its creation is cut and an observation gate hands Unreal the host's malloc zone (its `patches.py`, `hle_uemalloc.c` layout UE4_27); the region is 2 GB (`game_args -SwitchVirtualAddressSpaceSize=2048`) with Octopath Traveler 0's node-area cut on this SDK build, in a 3.75 GB guest block. About 2.6 GB resident on the Mac in play (the Unreal heap about 1.25 GB, 0.9 GB of Metal allocations). -- **Frame rate:** 60 fps (30 on the Switch). The game's config caps Unreal at 30 twice: `FrameRateLimit=30` in `DefaultGameUserSettings.ini` and `SwitchGameUserSettings.ini`, which `UEngine::SetMaxFPS` (`main+0x134d2c0`) puts in `t.MaxFPS`, and `rhi.syncinterval=2` in `SwitchDeviceProfiles.ini`, the present interval 2 it asks for while `r.VSync=1` (`BaseSwitchEngine.ini`) locks presents to vsync. As for Octopath Traveler 0, the host writes 60 and 1 into the two console variables (`hle_fpslock.c`, profile key `fps_target 60`) whenever the game sets them back, and the game then asks for present interval 1 itself. StevensND's 60 fps mod for the base game's `main` (build `ab0602…`; GameBanana) lifts the same two limits with patches in code: it returns at once from `UEngine::SetMaxFPS`, so `t.MaxFPS` stays 0 (no limit), and it overwrites the first two characters of the UTF-16 name `r.VSync`, so the config's `r.VSync=1` finds no variable and presents are no longer locked to `rhi.SyncInterval`. Its "Uncap" variant also patches the `sdk` for present interval 0. Neither patch touches how the game steps time, and Unreal steps the game by the time that really passed. On the Mac, `DM_NVN_REGLUM` traces of `run/to_maribel.sh` from the same save at 30 and 60 fps (frame time p50 33.4 → 16.7 ms) match at time scale 1.00 with a constant offset (−1.75 ± 0.2 s over the last 90 s, correlation 0.998–1.000), the night scene comes out at the same resolution, and no new red line appears. `DM_FPS_TARGET=30` keeps the game's own 30. -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL` ([05](agents/05-debugging.md)); `run/to_maribel.sh ` goes from the title through a new adventure log into the opening cutscene's night scene by the boat (a save with an adventure log in the first slot). - -**FINAL FANTASY RESONANCE (`0100606026F12000`)** -- **Status:** brought up on the Mac: the first-boot brightness screen, the title, a new game (Normal difficulty), the opening cutscene, the first field (Battlefield) with its autosave, Continue from that save, and the first two battles, at 60 fps with no red line apart from one moment of the new game's load (below). Playable on an iPhone. -- **Engine:** Unreal Engine 5.6.1 (Square Enix; the desktop renderer, `SF_GLSL_SWITCH`), SDK 20.5.17 (the `sdk` and `subsdk0` builds of STEINS;GATE RE:BOOT), update 1.0.1 (v65536); CRIWARE sound. Everything is in one IoStore container (`FFRS-Switch.utoc` / `.ucas`, 6 GB) beside `FFRS-Switch.pak` (configs and the like). -- **Rewriter:** 94 MB of text, 95% of it without unwind entries (82 MB in one stretch, 6.65 MB before the first FDE). Two general changes: code before the first FDE is one function, and functions longer than 256 KB are analysed in pieces, without which the rewrite took 100 GB of memory ([03](agents/03-bringup.md)). -- **Shaders:** 73,496 unique (9,253 vertex, 61,305 pixel, 2,934 compute, 4 geometry) in the container's two IoStore shader libraries, two layers of Oodle, read through the game's own decoder in the Mac host's tool mode with no boot (`uepak.py`, [01](agents/01-extract.md)). 11 do not build: 3 geometry shaders and 8 compute shaders with 3D image atomics, none met so far. 2,077 particle vertex shaders pick their texture slot at run time (`tex_dyn_slots`); `CopyStencilToLightingChannelsPS` writes integers to an `R8Uint` target (`shader_facts_manual.tsv`: without it the lighting channels stayed empty and the sprites showed as dark silhouettes). -- **Memory:** Unreal's FMallocBinned3 runs on a host malloc zone (a 32-slot FMalloc vtable in UE 5.6, `hle_uemalloc.c` layout `UE5_6`). The engine's page allocator only ever grows its use of the engine region (2.03 GB of address space after seven minutes, about 1 GB of it mapped), and a 2 GB region aborted in the first battle; the region is 4064 MB (`game_args`) in Engage's 5 GB guest block ([03](agents/03-bringup.md)). About 2.3–2.6 GB resident and 1.2–1.6 GB of Metal allocations on the Mac in the field and the first battles, flat over seven minutes. -- **Frame rate:** 60 fps (30 on the Switch): the host writes `rhi.SyncInterval` 1 (`hle_fpslock.c`); the game steps by real time (same luminance traces at 30 and 60 fps). `DM_FPS_TARGET=30` keeps 30. -- **Known:** once in two runs, the frames that begin the new game's load bind 2D textures where a base pass shader declares 3D ones (red `texture type mismatch` lines, under a black screen). -- **Mac input:** the first boot shows the brightness screen; from a fresh save, `DM_KEYS="f600:A,f1500:A,f1700:A,f1850:Up,f1950:A,f2400:A*36/150,f7950:B,f8100:B,f8250:B,f8400:B"` takes New Game, Normal, "Yes", the opening and the tutorials into the first field (about frame 9,600 at 30 fps; B closes a tutorial on its last page, the stick turns its pages). From a save made there, `DM_KEYS="52:A,58:A,62:A"` continues into the field; `70:LUp~4` walks north to the first battle. The work folder's `run/save_battlefield` holds such a save. - -**Luigi's Mansion 3 (`0100DCA0064A6000`)** -- **Status:** brought up on the Mac: the boot notices, the title (the hotel in daylight), a new game and the opening cutscene (the bus ride, the arrival at the hotel, the lobby), then the lobby in play at 60 fps (30 on the Switch), about 3.7 GB phys_footprint (3.0 GB of Metal allocations). Playable on an iPhone at 60 fps. -- **Frame rate:** 60 fps (30 on the Switch). At the end of each frame the main thread calls its frame callbacks (`main+0xb7c308`) with the milliseconds since the frame began; the frame limiter among them (`main+0x7a3e70`) runs jobs or sleeps until 33.333 ms have passed, a float in read-only data (`main+0x13c94dc`, read only there), and the game steps by the time that really passed. The host writes the profile's `fps_target` there as a frame time (`hle_fpslock.c`: 16.167 ms at 60, half a millisecond under the vsync so the presents set the pace; at exactly 16.667 the work after the limiter made it 59.6 fps with a frame shown twice every 2.5 s), and `present_interval display` ignores the present interval 2 the game also asks for. On the Mac the lobby holds 60.0 fps (display interval p99 16.67 ms); the same 2 s walk and 1.5 s turn from the same save end at the same spot at 30 and 60 fps, and the title's animation matches at time scale 1.00 (`DM_NVN_REGLUM`). `DM_FPS_TARGET=30` keeps the game's 33.333 ms. The limiter also has a phase lock, a nudge of at most 0.05 ms a frame from the layer's frame history, which stays off: the host's `nn::vi::ListFrameHistory` returns no entries. -- **Engine:** Next Level Games' own on NVN, SDK 7.3.2 (the `sdk` build of Super Mario Odyssey), Havok physics and cloth, Wwise (a 6-channel AudioOut), NEX / Pia. `subsdk0` is a second copy of the SDK (`nnSdkEn`: `sdk`'s code byte for byte, another name and build ID), which comes first in the load order, so `main`'s imports bind to it; `subsdk1` is the movie module. No virtual address memory: `main` sets a 2.94 GB heap. -- **Files:** every RomFS file but the Wwise banks is an archive of the engine's own, `.dict` indexing zlib chunks in `.data`; update 1.4.0 ships `.patch` beside the unchanged base files, rebuilt into each section the way the game does it ([01](agents/01-extract.md)). `shader_split.py` (`nlg_shaders`) reads both. -- **Shaders:** 5,317 (5,149 in the base game), all in `init`: control sections back to back in one section, their code areas in the same order in another, one NVN program per stage (paired as single-stage programs). 586 pixel shaders read their shadow cascades from an array of the driver table's handles (`c2_array`), and 1,396 write the G-buffer, an R32Uint and an RGBA8Uint target, as integers (`fs_uint`) ([04](agents/04-graphics.md)). Not built: 10 geometry shaders (one expands a box from two corners) and the tessellation stages (15 hull, 18 domain shaders), whose patch draws are skipped. Neither is drawn much: from the title through a new game, the opening cutscene and the lobby, no draw used a geometry shader (the host now names one that does, `draw with geometry shader … which has no library`), and two tessellated programs drew twice a frame for about 30 frames, in the title scene behind the file select as it slides out before a new game. Metal 4, which the host draws with, has no tessellation stage: those draws would need a tessellator of the host's own. -- **Host changes:** the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from `GetCommandMemoryUsed`, so single texture binds are recorded with the handle there (`OP_BIND_TEX1`; without it the title showed a stretched icon and a new game a white screen); a depth texture drawn through a colour view of the same size (a shadow, copied back after the pass); `nvnBlendStateSetAdvancedMode` and `nvnCommandBufferSetDebugLabel` ([03](agents/03-bringup.md), [04](agents/04-graphics.md)). -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`; `DM_KEYS=14:A,18:A,26:A,31:A,41:Right,42:A,54:A` goes from the boot notices through a new game into the opening cutscene. - -**AI: THE SOMNIUM FILES (`010089B00D09C000`)** -- **Status:** brought up on the Mac: the Spike Chunsoft logo movie, the title (its background movies), a new game (language, auto save notice), the opening at the merry-go-round and the first investigation with its conversation tutorial, both sticks; loading the autosave from the title. 60 fps (present interval 1), about 2.27 GB resident, flat over 5 minutes; audio through FMOD's AudioOut. Playable on an iPhone. -- **Engine:** Unity 2017.4.17f1 IL2CPP with FMOD on SDK 6.4.2, update 1.0.1 (v65536). `subsdk0` is the movie module; the 168 movies (`StreamingAssets/Movie`, H.264 and AAC in MP4) play through Unity's video player, NV12 into CPU buffers copied to an R8 + RG8 texture pair each frame. -- **Shaders:** 13,057 (6,572 vertex, 6,482 pixel, 3 geometry) in the AssetBundles of `StreamingAssets` (Cuphead's Unity 2017.4 layout) and `main`. All build for Mac and iOS. The 3 geometry shaders (Unity's DX11 depth of field, a wireframe effect) use the general native-VS-capture / GS-compute path; GPU readback tests check their output, but none was drawn in the opening observed so far. See [geometry support and limits](geometry-metal-research.md). The ComputeShader objects of `Data/resources.assets` have no type tree (red line in the split, as in Cuphead); no dispatch has missed one so far. -- **Host changes:** an H.264 drain held up by a full set of output slots (the logo movie never ended, [03](agents/03-bringup.md)), and the per-stage composite programs restored after the precompile prediction ([04](agents/04-graphics.md)); both general. -- **Memory:** no system resource; Unity sets a 3.11 GB heap (pool − used) in the 4 GB layout, `mem_used` 104 MB for the rewritten modules. -- **Input:** controller only, docked (1280x720 in the cropped 1920x1080 window, by the game's choice), with the 3D pad layout: the investigation moves its cursor with the left stick and the view with the right one. -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`. From a fresh save: 15 s, A (New Game), Down, Down, A (English), Left, A (Yes), then A every 3 s into the investigation (about 3 minutes). With the autosave: `DM_KEYS="15:A,18:A,21:Left,22:A,30:A,40:A*45/3"` loads it and plays the opening again. - -**ANIMAL WELL (`010020D01AD24000`)** -- **Version and status:** update 1.0.10 (`v655360`), SDK 20.5.7. The Mac host reaches the opening rooms at 60 fps with movement, jumping and audio; the unsigned IPA is confirmed playable on the iPhone. Only this update is registered in the profile. Item pickup and pause menus also work; save/reload remains unverified because the test did not reach a save checkpoint. One repeat run recovered from an AudioUnit startup timeout through the existing audio restart path. -- **Files:** both the base and update Program NCAs have no data section. Assets live in `main`, whose read-only segment is about 32 MB. `nx_extract` now creates an empty RomFS directory (and a header-only survey manifest) when neither program has a data section. The profile's empty `romfs_marker` checks that directory. Base and update extraction were checked in both full and manifest modes. -- **Shaders:** 46 BNSH shaders embedded in `main` (1 vertex, 45 pixel), all translated for Mac and iOS by the existing splitter and translator. No shader overrides, rewriter cuts or host changes were needed. The prewarm list covers the opening rooms. -- **Memory and controls:** no NPDM system resource; the provisional 3.5 GB guest layout holds, with a 256 MB alias region and a 256 MB stack region. `main` has about 537 MB of BSS, counted by the existing big-module memory rule. During a four-minute Mac run, resident memory stayed near 855 MB and Metal allocations at 159 MB. The 2D virtual pad is selected; B jumps. - -**Mario Kart 8 Deluxe (`0100152000022000`)** -- **Status:** brought up on the Mac from update 4.0.0 (`v1441792`): boot, the title with its demo race, the Mii pick, the menus (their movie previews play), a 50cc Grand Prix race at Mario Kart Stadium to its results, and a second boot that loads the save. 60 fps (p50 16.7 ms), sound; about 2.4 GB resident and 1.26 GB of Metal allocations in the race, flat over minutes. Playable on an iPhone (A20 Pro): 60 fps docked with about 6 ms of GPU time a frame, a 2.4 GB footprint with 3.7 GB of headroom, no audio underruns. -- **Engine:** Nintendo's sead / agl / gsys with NintendoWare (G3d, Ui2d, Vfx, Atk) and Pia, on SDK 4.4.0, the oldest SDK so far; `subsdk0` is the movie module. No virtual address memory: `main` sets a 3.03 GB heap in the 4 GB layout. -- **Shaders:** 29,650 (12,529 vertex, 17,092 pixel, 28 compute, 1 geometry): BNSH in the .szs SARCs (courses, objects, karts, drivers), the effects, the layouts, agl's two sharcb and 37 in `main`. The race records conditional rendering as a course loads; the profile has `render_cond on`. -- **Miis, without the firmware:** the game builds Mii heads from the console's MiiModel system data, starting at boot for the title's demo race, and lists the six default Miis for its "Select a Mii". The host's Mii database has those six and no Mii of its own, and its MiiModel stand-in has every part empty, so Mii heads draw nothing while bodies and favourite colours show (`hle_mii.c`, [03](agents/03-bringup.md)). -- **Host changes, all general:** the Mii database and the MiiModel stand-in; the add-on content cache sized as Horizon's RomFS working memory (its licence is decrypted in a 0x3130-byte heap); device save data as a save of its own for a game that mounts account save data too (every boot started a new save); SDK 4's DeviceSink layout and the audio renderer's update lock read off each SDK's `Update` (it was SDK 16's for every SDK before 20); and the zero cube for a one-layer 2D array in a cube slot ([03](agents/03-bringup.md), [04](agents/04-graphics.md)). -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`; `run/race.sh` boots from a copy of a save past the Mii pick into a 50cc Grand Prix (Mario, Mushroom Cup) with frame-timed keys and holds accelerate (`DM_KEYS=f960:L+R~0.3,f1260:A,f1500:A,f1740:A,f2000:A,f2240:A,f2400:A,f2560:A,f2800:A,f3040:A,f4300:A~70`). From a fresh save, L+R at the title opens "Select a Mii" (Right, A takes the first default Mii). - -**Balatro (`0100CD801CE5E000`)** -- **Status:** brought up on the Mac from update 1.1.3 (`v851968`): the intro, the title, a new run by touch and the tutorial's first steps, at 60 fps with sound; about 0.65 GB resident and 0.2 GB of Metal allocations, flat over three minutes in a run. Plays on the iPhone (IPA about 82 MB). -- **Engine:** LÖVE 11.4 ("love-nx") with LuaJIT 2.1.0-beta3 in its interpreter and SDL, on SDK 18.3.2. The game is Lua source in the RomFS (`main.lua`, `game.lua`, `engine/`, `functions/`), its pictures PNG, its sound Ogg Vorbis through OpenAL Soft on the audio renderer. It renders with **OpenGL**: NVIDIA's desktop GL 4.5 through EGL (`subsdk0`, not loaded; `subsdk1` is NVIDIA's Vulkan driver, and `main` imports `vkGetInstanceProcAddr` only for SDL's Vulkan loader, which `profile_guess.py` now leaves out of its Vulkan count). LÖVE's glad loader asks for 2,133 GL names and its code calls about 150 of them (`loader_census.py`). -- **GL layer:** LÖVE takes its core-profile paths when `GL_CONTEXT_PROFILE_MASK` says core (the layer now answers it): its vertex streams are persistently mapped buffers (`glBufferStorage`, `glMapBufferRange`) fenced per frame (`glFenceSync`, `glClientWaitSync`); with a compatibility profile it would draw from client memory. Also new for it: attribute bindings (`glBindAttribLocation`: LÖVE fixes VertexPosition, VertexTexCoord, VertexColor and ConstantColor to 0–3) and current attribute values (`glVertexAttrib4f`: VertexColor and ConstantColor are disabled arrays), `glGetActiveUniform` and the uniform read-backs, every `glUniform*` form, texture storage with its levels, 3D and array textures (LÖVE makes a default of each), texture swizzles (its font glyphs are RG8 read as R, R, R, G), `glGenerateMipmap` (the game's images are mipmapped), instanced and base-vertex draws, separate read and draw framebuffers, blits and `glReadPixels` ([04](agents/04-graphics.md)). -- **Shaders:** LÖVE puts each shader's GLSL together at run time, in Lua, from its own header and built-in code and the game's 19 `resources/shaders/*.fs`: 35 sources (`#version 330 core`), all made by `Game:start_up` before the first frame. No game file holds them, so the NSP script captures them on the Mac (`gl_capture.tsv`) and `gl_shaders.py` translates them. A GLSL `bool` uniform is a `uint` in SPIR-V; `gl_shaders.py` now reports the declared type, since LÖVE's `Shader:send` takes only a boolean for a bool (the game stopped at its first card otherwise). -- **Sound:** the game opens the audio renderer without an event, and the host started its render thread only for the form with one, so only the game's own updates rendered, and nothing played them: silent. Both forms now start it (`hle_audren.c`). -- **Input and display:** `op_mode handheld`: the Switch build plays by touch there (SDL turns the touch screen into LÖVE touches and mouse presses), the iPhone's touches outside the gamepad controls, the Mac's mouse; docked it reads only the pads. Its window is the 1280x720 handheld display. `pad_layout touch`: the virtual gamepad starts in its new Tap layout (only the layout button; every touch goes to the game), since the 2D layout's corners cover Run Info, Options and the deck; the layout button brings back 2D and 3D. -- **When the game stops:** on the Switch, Balatro's error handler quits at once, and love-nx's video worker is then deleted twice (its thread releases the worker that the closing video module also deletes), which crashes in `main+0x339c00`. A Mac run that reaches this has hit a Lua error first: a copy of the RomFS with a `print` at the top of `love.errhand` in `main.lua` shows it (a capture run without the shader library stops at the first `Shader:send` so, after every shader is captured). -- **Mac input:** in handheld mode, `DM_KEYS="f1300:T0.35x0.86,f1700:T0.35x0.86,f2100:T0.5x0.68"` taps Play at the title, Play in the run setup and the tutorial's Next. - -**ROMANCE OF THE THREE KINGDOMS 8 REMAKE (`0100C2A01A730000`)** -- **Status:** brought up on the Mac from update 1.1.3 (`v851968`); playable on an iPhone (IPA 16.8 GB). On the Mac: the logo movie and intro, the title and its attract demo, the main menu, a new game through the recommended setup (Ruler, Cao Cao, 195), its opening story on the map, the council (espionage, personnel, the help panel), a saved and loaded game, a month's missions, the city view, a visit and a practice duel in 3D. 30 fps, as on the Switch (frame time p50 33.3 ms, p95 34.5 ms; GPU 5–7 ms a frame on the Mac in the duel), sound; about 2 GB resident in the council and 2.5 GB in the duel, flat on a screen left idle. The prewarm list covers that route. -- **Engine:** Koei Tecmo's KTGL on NVN, SDK 17.5.3 (Ace Attorney Investigations Collection's SDK); `subsdk0` is the movie module (WebM logo and story movies). No virtual address memory: `nninitStartup` sets a 3.13 GB heap in the 4 GB layout, `mem_used` 80 MB for the rewritten modules. -- **Files:** a newer RDB than Age of Calamity's: `0002/root.rdb` and `system.rdb` index 296 `.fdata` files (a `PDRK` header, then `IDRK` entries) and `0002/data/*.file`, every entry LZ4 or zlib in 16 KB chunks of a new form ([01](agents/01-extract.md)); scenarios, messages, movies and their subtitles sit in loose folders beside it. The update is a multi-program patch: it carries a whole second program (`0100C2A01A730001`) beside the game's patch, which `main` starts with `nn::oe::ExecuteProgram(1)`; `nx_extract` used to take that one and failed ([01](agents/01-extract.md)). The host runs program 0 only. -- **Shaders:** 59,363 GLSLC outputs (52,691 pixel, 5,359 vertex, 1,311 compute, 2 geometry): 59,214 in the G2S (`_S2G5000` / `6000` / `7000`) and `_DOK0000` containers of the RDB entries, read by `shader_split.py` (`idrk_data`), and 149 in `main`. The vertex shaders are single-stage programs, paired with a cover pixel shader. All build for the Mac (1 h 19 min on a 12-core M6). Three shaders get textures of another type than they declare on some slots (`texture type mismatch … bound as is`, once each: a 2D array where a pixel shader declares 3D, cube and 3D textures where two vertex shaders declare 2D); nothing in the scenes above looks wrong. -- **Sound:** SDK 17.5.3's `OpenAudioRenderer` with an event calls the form without one through the PLT, which the host's hook took for a second renderer; the inner call now goes straight to the SDK ([03](agents/03-bringup.md), general). -- **Display:** the game draws 1280x720 into its 1920x1080 window textures and crops (`WindowCrop`), docked as well; a screenshot without `DM_NVN_SHOT_OUT` shows the whole texture, the picture in its top-left corner. -- **Input:** controller only, with the 3D pad layout (the dialogue boxes' auto and log buttons are the right stick and R3). `main` initializes the touch screen and gestures but never reads them; it reads a USB mouse (`GetMouseStates`), which the host does not provide. Touches as a mouse would suit it, but OCTOPATH TRAVELER 0, Tears of the Kingdom and Unicorn Overlord also read a mouse and no touch screen, so such a rule needs testing on them first. -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`. From a fresh save: A (skips the intro), A at the title, A (New Game), A on Recommended, A ×3, the quiz's first answers (A, A, A), A on Ruler, X at Confirm Settings, A ×3 and X, then X at the story's Skip dialog: the council of 195 April, about 2 minutes. `run/save_ref` holds a manual save there: A, A, Down, A (Load), A, X loads it. - -**Taiko Risshiden V DX (`0100346017304000`)** -- **Status:** brought up on the Mac from update 1.1.1 (`v131072`); playable on an iPhone (IPA 3.3 GB). On the Mac: the notice, the logos and the opening movie, the title, a new game (1560, Kinoshita Tokichiro) through the opening story, the town, the council with a battle proposal, the territory map, a rest at home, a save and a load, and the field battle tutorial, docked with buttons and in handheld mode by touch. 60 fps (frame time p50 16.66 ms, p95 16.85 ms in the town), sound; about 0.7 GB resident and 1.6 GB of Metal allocations, flat over 15 minutes. No red line on that route. The prewarm list covers it (18 lines). -- **Engine:** Koei Tecmo's KTGL on NVN, SDK 12.3.3 (no other game's `sdk` build); `subsdk0` is the movie module (WebM opening and ending). No virtual address memory: `nninitStartup` sets a 3.14 GB heap in the 4 GB layout; the default 64 MB of used memory fits its small modules (`main` 15 MB). -- **Files:** loose files, 3.3 GB: KTGL G1T textures (some in MRLK packs), the game's own TR5 / TE5 / TS5 data, KTSS Opus streams (`.kno`, through `nn::codec`) and KTSL wave banks. Japanese, Simplified and Traditional Chinese; no English, so the NSP script packs Japanese (`DM_LANG` in `Documents/env.txt` switches). -- **Shaders:** 212 GLSLC outputs: 45 in `main` and 167 in the G2S container `eff/KTGLEffect.g1s` (`_S2G3000`, as Age of Calamity's), read by the existing splitter; the game is almost all 2D sprites (two draws a frame in the menus). -- **Host changes, all general:** `oe::SetCpuOverclockEnabled` (the first boot's crash, apm), and gestures for `hid::GetGestureStates`, recognized from the touch points ([03](agents/03-bringup.md)). -- **Frame rate:** KTGL's frame end sleeps in 2 ms rounds until 1/60 s has passed since the frame started, which ended at 18.2 ms (55 fps in the menus and the town); a build cut skips that wait and the presents set the pace (its `patches.py`, [03](agents/03-bringup.md)). -- **Input:** `op_mode handheld` and `pad_layout touch`: handheld, the Switch build plays by touch (tap the cards and buttons, drag or flick the map), which on the iPhone are all the touches in the Tap layout, on the Mac the mouse; the layout button brings back the gamepad (the map scrolls with the right stick in the 3D layout; L / R and ZL / ZR switch tabs and lists). It draws 1920x1080 in both modes. -- **The town's top-right panel runs off the screen on a council day** (its "council today" text is longer than the usual "n days left"): the panel starts where the manual's screenshot has it and widens for the longer text; on other days it fits. Not a host bug. -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`. From an empty save: A skips the notice and the movie, A at the title and on New Game, then A five times (the scenario, its story, Tokichiro, his abilities, the settings) and about 150 presses of A through the story to the town (on the council day: A, Right ×3, A enters the castle). `run/save_ref` holds a save in the town on 1560 May 1. Handheld (`DM_OPMODE=0`), the same by touch: five taps at the centre (`T0.5x0.5`) reach the load screen of Continue, then `T0.5x0.29` and `T0.5x0.47` load slot 1. - -**Super Smash Bros. Ultimate (`01006A800016E000`)** -- **Status:** plays on the iPhone (IPA 17.5 GB) from update 13.0.5 (`v2031616`) with its 99 DLC licences. On the Mac: the opening movie, the title, the first launch's DLC unlocks and Challenges board, the main menu, the Smash rulesets, stage select, fighter select, 2-player battles (Frigate Orpheon, Big Blue, Great Bay, Find Mii) at 60 fps with about 2.7 ms of GPU per frame and 2 GB resident (1.2 GB of Metal allocations), quitting a battle to its results and a second battle whose fighters' NROs the game loaded again. -- **Engine:** Bandai Namco / Sora's engine (`app`, `lib`, `phx`, `nu`) on SDK 8.2.1, the same `sdk` and movie (`subsdk0`) builds as Catherine: Full Body. Everything is in one `data.arc` (17.6 GB) beside 14 NRRs; `dm/gfx/tools/smash_arc.py` reads its current files ([01](agents/01-extract.md)). -- **Fighter code at run time:** the fighter, item and common scripts are 98 NRO modules in `data.arc` (193 MB) that the game loads with `nn::ro::LoadModule` as fighters appear (common and item at boot, the opening movie's fighters, each battle's) and unloads after; `main` imports their 1,045 `lua2cpp::create_agent_*` functions. They are collected, rewritten and loaded as described under "NRO modules" above ([03](agents/03-bringup.md)); 87 need no rewrite at all, 11 have one `x18` site each. On iOS they are packed into three sections of the executable (`gen_embed.py`), 157 MB of code. -- **Memory:** the engine reserves 62 GB of address space for all its memory at a static constructor; cuts on `main` make it 4 GB and on this `sdk` build drop the SDK's 1 GB node area and 1 GB alignment, so it starts 64 MB into the 4.375 GB alias region of the 5 GB block ([03](agents/03-bringup.md)). A battle commits up to 2.5 GB of it (`DM_ALIAS_WINDOWS`). -- **Shaders:** 35,012 (22,197 vertex, 12,761 pixel, 53 compute, 1 geometry): GLSLC outputs in the 41 SSBH shader databases (`.nushdb`, 7,764), BNSH in the effects (`EFFN`, 26,419) and UI layouts (SARC, 752), and 77 in `main`. The split reads `data.arc`'s current files (`shader_split.py` `smash_arc_jobs`). -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL` (`run/step.sh ` presses, waits on the frame counter and takes a shot). From `run/save_ref` (past the first launch): A skips the movie, A at the title reaches the main menu, A (Smash), A (a ruleset), A (Random stage), then at the fighter select (after its "Joined!" banner) `LUp~0.85`, `LLeft~0.85`, `LDown~0.3`, `LRight~0.4` lands on Mario, A, Plus. - -**Bayonetta 3 (`01004A4010FEA000`)** -- **Status:** plays on the iPhone (IPA 14.2 GB) from update 1.2.0 (`v131072`). On the Mac: the boot, the title, a new game (difficulty, Naive Angel Mode, brightness, the auto-save notice), the VP9 opening movie, the prologue's first real-time scene and a minute of play in it, with sound. 60 fps in play (frame time p50 16.67 ms, p95 17.5 ms; GPU about 5 ms a frame on an M6), 30 where the game asks for it (the title and loading screens); about 2 GB resident and 3.1 GB of Metal allocations, flat after the first scene. The title is up about 15 s after launch with a warm shader cache. The prewarm list covers that route (678 lines). Dotted patches on some surfaces (a grid every 4 pixels: the platforms breaking up in the prologue, a floor near the camera) look like the engine's dithered dissolve; not compared with the Switch yet. -- **Engine:** PlatinumGames' own engine on NVN, SDK 13.3.2 (no other game's `sdk` build); `subsdk0` is the movie module (WebM movies, VP9); Wwise sound through `AudioOut`. No virtual address memory: `nninitStartup` sets a 3.09 GB heap in the 4 GB layout, `mem_used` 116 MB for the rewritten modules. -- **Files:** 72 PKZ archives (14 GB) of DAT archives, compressed per entry with Oodle Kraken or stored, beside 478 loose WebM movies ([01](agents/01-extract.md)). `main` reads them only through Oodle's streaming decoder, so the Mac host's Oodle tool mode now finds and drives that decoder when a `main` has no `OodleLZ_Decompress` (`hle_oodle.c`); `profile_guess.py` turns it on for any game with Kraken PKZ archives (`oodle_packs`). -- **Shaders:** 6,424 distinct PlatinumGames shader objects (VSO / PSO / CSO / HSO / DSO) in `root_000.pkz` (`shader_split.py` `pkz_entries`, `pgdat_members`, `pg_shader`; single-stage programs). All but two domain shaders build ([04](agents/04-graphics.md)). -- **Host changes, all general:** the newest GPU code version the NVN layer reports now follows the SDK (1.16 from SDK 13): the engine refused its built-in shaders at 1.14 and `nnMain` returned at once ([03](agents/03-bringup.md)); `nvnWindowGetCrop` and `nvnSamplerBuilderGetLodClamp`. -- **Input:** the 3D pad layout (the camera is on the right stick). -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL` (`CTL=` picks another run's control file), `run/step.sh ` presses and takes a screenshot, `run/snapseq.sh` boots and takes one every few seconds, and `run/route.sh ` boots on a fresh save and drives the route above: A at the title, A through the logo, A (Standard), A, B (closes the Naive Angel Mode help), A (Off), A (brightness), A (the auto-save notice), then Plus, Down, A skips the opening movie. -**Bayonetta (`010076F0049A2000`)** -- **Status:** plays on the iPhone (IPA 8.8 GB) from update 1.1 (`v65536`). On the Mac from a fresh save: the boot, the opening movie (USM; Plus skips it), the title and its attract demo, a new game (PLAY, NEW GAME, the auto-save notice, Normal), the prologue on the falling clock tower, the graveyard cutscene (skipped: Plus, Down, A, then Left, A for Yes), the touch-controls tips, the practice tutorial (punch, kick, combos, evade, Witch Time), the graveyard fight of Verse 1 with Witch Time, its autosave and the next loading screen's practice, with sound and no red line. 60 fps in play (frame time p50 16.65 ms, p95 17.1–17.5 ms; GPU 2–4 ms a frame on an M6), 30 in its cutscenes, where the game sets present interval 2; about 2.45 GB resident and 1.54 GB of Metal allocations, flat over 15 minutes. -- **Engine:** PlatinumGames' own engine in its Wii U form, on NVN, SDK 3.6.0, as Bayonetta 2: the same `sdk` and `subsdk0` builds, and no host change. CRI middleware throughout: the file system (CPK), ADX2 sound (`se/` and `bgm/`, `.acf` / `.awb`) through one 6-channel `AudioOut`, and Sofdec movies (USM), all decoded by the game's own code. No virtual address memory: a 3.11 GB heap in the 4 GB layout, `mem_used` 102 MB. -- **Files:** nine CRI CPK archives in the RomFS root (`data00.cpk` … `data03.cpk`, `data10.cpk` … `data14.cpk`, 4.25 GB) of PlatinumGames DAT archives, beside the movies and the sound; `cpk.py` reads them as they are. -- **Shaders:** 458 BNSH files of one stage each (236 pixel, 222 vertex), the `.vso` / `.pso` members of `shader.dat` in `data00.cpk`; the split (`cpk_jobs`) takes about 1.7 minutes. All build. -- **Input:** the 3D pad layout (the camera is on the right stick). The touch controls are handheld only; the profile keeps it docked. -- **Mac input:** the work folder's `run/see.sh [secs]` boots on a fresh save (or `SV=`), presses `PRESS` once frame `AT` is logged and takes a shot every `EVERY` s; `run/ctl.sh`, `run/step.sh`, `run/watch.sh` and `run/fight.sh` as for Bayonetta 2; `run/tut.sh` works through the practice tutorial's pages (Minus is Next), and `run/witch.py` times the Witch Time page's countdown from shots and dodges at a sweep of offsets (the page wants a dodge right before the strike; it took 20–30 tries). `run/save_ref` is the save at the prologue's chapter screen, `run/save_ref2` the checkpoint after the tutorial (the tutorial comes back after a load), `run/save_ref3` the one after Verse 1. - -**Bayonetta 2 (`01007960049A0000`)** -- **Status:** plays on the iPhone (IPA 12.5 GB) from update 1.2 (`v131072`). On the Mac: the boot, the PlatinumGames logo movie (USM), the title, a new game (slot 1, the auto-save notice, Story, Normal), the opening narration (The Legend of Aesir, which cannot be skipped on a first play), the prologue's cutscene (skipped: Plus, Down, A, A), the tutorial (closed: Plus, Down ×4, A), Verse 1 on the jet, Rodin's cutscene and the next fight with an Umbran Climax and a torture attack, with sound. 60 fps in play (frame time p50 16.67 ms, p95 17.5 ms; GPU 2–3.5 ms a frame on an M6), 30 in its cutscenes as the game asks; about 1.6 GB resident and 1.75 GB of Metal allocations. The title is up about 20 s after launch. -- **Engine:** PlatinumGames' own engine in its Wii U form, on NVN, SDK 3.6.0: the oldest SDK so far, and no host change was needed for it (NVN API 53, as for the other SDKs up to 10). `subsdk0` is NVN's GLSLC; `main` imports `glslcCompile` but did not call it on the route above. Wwise sound through two 6-channel `AudioOut`s; CRI's file system and Sofdec movies (USM), both decoded by the game's own code. No virtual address memory: `nninitStartup` sets a 3.11 GB heap in the 4 GB layout, `mem_used` 102 MB for the rewritten modules. -- **Files:** six CRI CPK archives (9.5 GB, 6,091 files, 12 GB of them CRILAYLA-compressed) of PlatinumGames DAT archives, beside the USM movies and the Wwise sound ([01](agents/01-extract.md); `dm/gfx/tools/cpk.py`). -- **Shaders:** 1,856 BNSH files of one stage each (`.vso` / `.pso`) in `shader.dat`, `shader2.dat` and `shadereff.dat` of `data000.cpk`; the split decodes every CPK entry (`shader_split.py` `cpk_jobs`, about 2.5 minutes). All build ([04](agents/04-graphics.md)). -- **Host changes, all general:** a cleared attachment larger than the render pass is cleared in a pass of its own (the narration in a corner of a black screen, speckled opening scenes), and `Downsample` of a single-sample texture is a filtered scaling copy (its bloom chain) ([04](agents/04-graphics.md)). -- **Input:** the 3D pad layout (the camera is on the right stick). The game also offers touch controls in handheld mode; the profile keeps it docked. -- **Mac input:** the work folder's `run/route.sh [secs]` boots on a fresh save and presses A five times from the title (slot 1, the notice, Story, Normal), `run/ctl.sh` / `run/step.sh` press and take shots, `run/watch.sh` takes a series of shots, and `run/fight.sh` repeats a simple combo. `run/save_ref` holds the save after Verse 1, `run/save_ref2` the one after the second fight. - -**Advance Wars 1+2: Re-Boot Camp (`0100300012F2A000`)** -- **Status:** plays on the iPhone (IPA 7.1 GB) from the base game 1.0.0, the only version. On the Mac: the boot, the Bink opening movie, the title, Nell's introduction, a new casual campaign and the first training battle (Field Training, Troop Orders); 60 fps in the menus, 30 fps in battle by the game's own limit (present interval 1, the frame's work 33 ms with the CPU and GPU mostly idle), sound, about 2.5 GB resident and 1.6–1.9 GB of Metal allocations, level over 15 minutes with no red line. -- **Engine:** Unity 2020.2.1f3 IL2CPP on SDK 10.4.1 (The Great Ace Attorney Chronicles' `sdk` and movie module builds), NVN; FMOD sound through `AudioOut`; Bink 2 movies (`.bk2`) decoded by Bink's Unity plugin in `main`. The NPDM declares virtual address memory: `nninitStartup` hands pool − used to StandardAllocator and sets no heap, so the `sdk` build takes the node-area cut and the 1 GB alignment cut (`sdk_patches.py`), and the profile reports a 2 GB pool in Engage's 5 GB block: with the heap check's 1 GB the game's malloc ran dry at the first loading screen ([03](agents/03-bringup.md)). -- **Shaders:** 20,424: the Addressables bundles (`Data/StreamingAssets/aa/Switch`), Unity's 11 GLSLC outputs in `main`, and Bink's 32 (2 vertex, 22 pixel, 8 compute), whose control sections and code areas lie loose in `main` (`shader_split.py` `raw_shaders`, [04](agents/04-graphics.md)); without them every movie drew white. All build. -- **Input:** the 3D pad layout (the map cursor moves with the D-pad). -- **Mac input:** the work folder's `run/step.sh ` drives a run started with `DM_CTL`. About 22 s into a boot the opening movie plays: A skips it, `L+R~0.5` opens the main menu (the first time Nell's introduction, A about 25 times, then the file select); A (Campaign), A, A (Advance Wars, the file), A, A, A, A (the mission, Field Training, Troop Orders) start the first battle. - -**Pokémon Sword (`0100ABF008968000`)** -- **Status:** plays on the iPhone (IPA 12.4 GB) from update 1.3.2 (`v458752`). On the Mac: the first boot's language, photo and name choices, the opening (the Champion's broadcast, the stadium), Postwick (the house, the Bag, Hop's house), the walk to Wedgehurst and Leon's arrival at the station, the evening in Hop's yard, the starter choice (Scorbunny) and the first battle against Hop (Wooloo, then Grookey), the title screen and a save and load, at 30 fps as on the Switch (frame time p50 33.3 ms, p95 35–36 ms; GPU 4–9 ms a frame on an M6; CPU 42% of a core, 0.15 W), with sound (Wwise through a 6-channel `AudioOut`); resident memory 1.8 GB after the title, 2.4 GB after 17 minutes of new areas, 1.9 GB of Metal allocations. No red line on that route. The prewarm list covers it (3,273 lines). -- **Engine:** Game Freak's engine before Trinity, on NVN, SDK 7.3.2: the same `sdk` and movie module (`subsdk0`) builds as Super Mario Odyssey 1.4.1; `subsdk1` is GLSLC, which `main` imports but never calls. No virtual address memory: `nninitStartup` sets a 3.13 GB heap in the 4 GB layout, `mem_used` 76 MB for the rewritten modules. -- **Files:** 7,699 GFPAK archives (7.9 GB, every entry LZ4) under `bin/archive` beside loose message, battle and Wwise files ([01](agents/01-extract.md)). -- **Shaders:** 41,516: the BNSH entries of the GFPAK archives (`shader_split.py` `gfpak_members`), loose BNSH files, two in `main`, and Silicon Studio's YEBIS post effects, 3,155 shaders of a `.tfxo` file in a zlib stream in `main` (`tfxo_streams`, [04](agents/04-graphics.md)). -- **Host changes, all general:** the Poké Ball Plus (`hid::*Palma*`: none connected), `pdm::QueryApplicationPlayStatistics` (never played), `err::ShowNetworkServiceError`, `nvnDeviceGetWindowOriginMode` and `nvnDeviceGetDepthMode` ([03](agents/03-bringup.md)); a pixel shader entry point for G-buffers of float and integer targets, cube views counted in cubes, and textures whose shared edge pages are written but whose own bytes are not are no longer re-uploaded ([04](agents/04-graphics.md)). -- **Input:** the 3D pad layout (the camera is on the right stick). -- **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`, `run/step.sh [wait] ` presses and takes a screenshot (headless: a window that loses focus is throttled and its shots never come), and `run/route_out.sh ` boots a copy of `run/save_ref` (in the bedroom with the Bag) through the title and out of the front door. From a fresh save, A at the language (about frame 600), the photo and the name (`DM_KBD`) reach the opening; `run/save_hop` is a save in Hop's kitchen, before the walk to Wedgehurst: from it, down 2.5 s out of the house, left 3.5 s, up 0.5 s, left 5 s, right 0.6 s and up 8 s reach the hill above Wedgehurst, and A presses with the stick up through the station scene lead back to Hop's yard and the starters (the camera turns with each move: walk in short steps onto the court and press A facing the Pokémon). - -**Pikmin 4 (`0100B7C00933A000`)** -- **Status:** plays on the iPhone (IPA 10.8 GB) from update 1.1.1 (`v262144`). 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). -- **Engine:** Unreal Engine 4.26.2 (`Carrot4`) on SDK 10.4.8 (its own `sdk` build), 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 (`subsdk0`). IoStore containers (`Carrot4-Switch.utoc` / `.ucas`) beside one pak (`Carrot4-Switch.pak`, version 11, Zlib, index not encrypted) that holds the configs, the Wwise media and the two shader code archives. -- **Rewriter:** an exclusive window with an `adrp` inside and an observation gate on an epilogue, both new to the rewriter, which now handles them for every game ([03](agents/03-bringup.md)). -- **Memory:** Unreal's FMallocBinned3 is cut and runs on the host's malloc zone (UE 4.27's 21-slot layout, `hle_uemalloc.c`); the engine region is only reserved without `-SwitchVirtualAddressSpaceSize=` in this build, so its 62 GB default is cut to 4064 MB instead of a `game_args` cap, and the SDK build takes the node-area and 1 GB alignment cuts, in Engage's 5 GB block ([03](agents/03-bringup.md)). -- **Shaders:** 15,727 distinct (16,190 in the two archives, every one an LZ4 block, as DRAGON QUEST VII Reimagined's); all build. -- **Host change, general:** `nn::nifm` requests stay on hold for 50 ms after submission, as Horizon's asynchronous request does: answered at once, the game's BCAT thread could finish before its creator marked it started, and about one boot in six stopped at the black loading screen for good ([03](agents/03-bringup.md)). -- **Frame rate:** 30 fps. The game steps a fixed frame time (`bUseFixedFrameRate=True`, Unreal's default of 30) and asks for present interval 2 (`rhi.SyncInterval=2`), so a faster present rate would speed the game up. -- **Input:** the 3D pad layout (the camera is on the right stick); it plays with the controller, docked. -- **Mac input:** the work folder's `run/ctlrun.sh ` starts a run with `DM_CTL`, `run/see.sh ` 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. - -**Stardew Valley (`0100E65002BB8000`)** -- **Status:** plays on the iPhone (IPA 0.6 GB) from update 1.6.15.13 (`v1310720`). On the Mac: the title, a new farm (character creation with the software keyboard, Skip Intro), the farmhouse, the farm and its tools, the bus stop, Pelican Town by day and night (lamp light), Pierre's shop and a purchase, the game menu, the mail, a night's sleep and its save, Load from the title, and the Co-op menu (Online Communication goes back to the title with no network). 60 fps (frame time p50 16.67 ms, p95 16.8 ms; GPU about 0.6 ms a frame on an M6; CPU 8% of a core, 0.27 W), sound; 0.46 GB resident at the title and 0.93 GB in play, flat over the last 10 of 23 minutes, 0.3 GB of Metal allocations. No red line on that route. The title is up about 20 s after launch. About one frame in 800 takes 24–47 ms (nine in two idle minutes in the farmhouse): game CPU time, with no pipeline build, upload or present in it, while the main thread otherwise sleeps 95% of each frame in `SleepThread` (sampled), whose host sleep is exact; probably BRUTE's Boehm collector, not checked further. -- **Engine:** MonoGame for the Switch: MonoGame's and the game's C# compiled to C++ by Sickhead's BRUTE into `main` (124 MB of code, 230 MB with its data and bss) with a Boehm collector, as Celeste; XACT sound (`Content/XACT`, 0.48 GB of wave banks) through the audio renderer; SDK 20.5.27, NVN. Only `main` and `sdk`. It links Pia and NPLN for online co-op and LDN for local co-op. -- **Files:** 3,549 XNB files, 3,535 of them LZ4 and 8 LZX (MonoGame's compressed XNB). -- **Shaders:** 65 (23 pixel, 42 vertex): 64 in the MonoGame effects in `main` (SpriteBatch's and the game's) and one in `Content/Effects/ShadowRemove.xnb`, an LZ4 XNB that `shader_split.py` decompresses before reading it (`xnb_decompress`, [04](agents/04-graphics.md)). Nearly everything draws with SpriteBatch: the prewarm list is its two shaders and four pipelines, and with it nothing was built on demand over the route above. -- **Memory:** no system resource (`sys_resource 0`), so no virtual address memory: the game sets a 2.96 GB heap (pool − used memory) in the 4 GB layout; the rewritten modules need 218 MB of used memory (`mem_used`). -- **Host changes:** none in this profile's change. Two found on this game came before it (PR #120): compressed XNB shaders, and the Mac text box, which releases the buttons that opened it (character creation re-opened the keyboard after every OK) and stays up while the text is shorter than the minimum length. -- **Input:** the pad only (`main` imports no touch screen or mouse): the D-pad moves between menu items, the right stick moves the cursor, ZL / ZR change the toolbar slot (empty slots are skipped), X opens the menu, Y uses the tool, A checks. The 3D pad layout. The game drops presses while a menu fades in or out (about a second after it opens or closes). -- **Mac input:** the work folder's `run_v/boot.sh [secs] VAR=…` runs `build/valleyhost` (`SV=` for a copy of `run_v/save_ref`, the morning of day 2 in the farmhouse), `run_v/ctl.sh` drives a run started with `DM_CTL`, `run_v/step.sh [wait] ` presses and takes a screenshot. The title is up about 20 s into a boot: A (Load) and, a second later, A (the save). Menu presses held 0.3 s or more repeat, so hold 0.2 s; walking takes the left stick (`LLeft~1.3` is about five tiles). In character creation with `DM_KBD=`, A on each of the three text boxes fills it, and Skip Intro is the last item above OK. Out of the farmhouse (its door is below the gift box), the path east above the weeds, past the mailbox, leads to the bus stop, and east from there to Pelican Town. - -**BALL x PIT (`010086A022444000`)** -- **Status:** plays on the iPhone (IPA 1.1 GB) from update 1.293 (`v393216`). On the Mac: the boot, the title, a new game, the intro and the descent into the pit, the first dive and its tutorial (moving, aiming and shooting the balls, gems and XP, the enemy waves that overrun the player and end it), and the base-building tutorial (the sidebar, the build menu, placing a wheat field and a forest, the first harvest with ZR), with FMOD sound. 60 fps (frame time p50 16.66 ms, p95 about 16.8 ms; GPU 1–5 ms a frame), about 1.6 GB resident and 0.7 GB of Metal allocations, flat over 10 minutes; no red line on that route. The title is up within 25 s of launch. The prewarm list covers that route (297 lines). -- **Engine:** Unity 6000.0.62f1 IL2CPP with FMOD (one stereo AudioOut, banks in `StreamingAssets`) and Addressables (11 bundles in `StreamingAssets/aa/Switch`) on SDK 20.5.19, its own `sdk` build; `subsdk0` is the SDK's movie library. It draws 1280x720 into 1920x1080 window textures and crops the window to 1280x720 (`nvnWindowSetCrop`), which the present scales to the screen. -- **Shaders:** 38,099 (18,913 vertex, 18,917 pixel, 116 compute, 76 hull and domain, one geometry), 11 of them the player's own GLSLC outputs in `main`. The loose serialized files (`resources.assets`, `sharedassets1.assets`, the built-in resources) have no type trees; they borrow the `Shader` tree that 10 of the bundles carry (the same type hash), so `unityfs.py` needed no layout, as for Silksong. -- **Memory:** as Hollow Knight: Silksong: 16 MB of system resource, no heap, Unity 6 reserving and committing its memory itself, Engage's 5 GB block, and the node-area cut on this `sdk` build (`sdk_patches.py`; its AddressRegionManager is the SDK 20.5.22 build's at the same offsets). The alias region reaches +2.00 GB, 1.26 GB mapped. -- **Host changes:** none. -- **Input:** docked with the 3D pad layout (`pad_layout full`): in a dive the left stick moves, the right stick aims, ZR shoots and X toggles autofire; in the base the left stick moves the build cursor, A confirms, B goes back, X opens the sidebar, ZR sends a worker out to harvest and ZL + R pans. -- **Mac input:** the work folder's `run/ctlrun.sh ` starts a run with `DM_CTL`, `run/see.sh ` presses and takes a screenshot, `run/route.sh ` plays the route above from a fresh save (start the run with `SV=` set to an empty save folder). - -**Mario vs. Donkey Kong (`0100B99019412000`)** -- **Status:** plays on the iPhone (IPA 1.6 GB) from update 1.0.1 (`v65536`) over the cartridge base game. On the Mac: the opening movie with its sound, the title (Press L+R), file select, the play style choice, the first cutscene, level 1-1 (both colour switches, the spring, a time-out and its restart), the pause menu, leaving the level, level select and the save it shows in file select. 60 fps (frame interval p99 about 17 ms), sound, about 1 GB resident in the level and flat while it runs (0.95 GB of Metal allocations), CPU about 30% of a core; no red line on that route. -- **Engine:** Nintendo's sead / agl / gsys engine (`ainb` AI scripts, `bgyml` parameters) on SDK 15.3.0 with NVN. Its cutscenes are MP4 files (`Movie/*.mp4`, H.264 1080p and AAC stereo) played through the SDK's movie library (`subsdk0`: `movie::Extractor` and `movie::Decoder`), their sound through an AudioOut of the game's own. No virtual address memory: the game sets a 3.14 GB heap in the 4 GB layout. -- **Files:** 3,162 RomFS files (1.26 GB), nearly all zstd (`.zs`), as Wonder's. -- **Shaders:** 12,691 BNSH / BAHS programs (8,053 pixel, 4,587 vertex, 51 compute): the models' own shader archives in `Model/` (8,390), agl's in `Lib/` (3,520), effects, `Shader/`, layouts and 40 in `main`. All translate. -- **Host changes:** two in the movie decoder (`hle_movie.c`). The game's movie player keeps the outputs it holds in a ring of 32 per track and pushes with no bounds check, so the host's 64 audio outputs overran it into the heap a second into the opening movie: AAC decoders now offer 32 (Vorbis keeps 64). The player also sizes its AudioOut buffers by the audio decoder's `GetOutputBufferSize`, appends each whole and advances its clock by that size, so the host's 128 KB answer made every 4 KB AAC frame play as 0.68 s and the movie froze: an AAC decoder now reports one frame (1024 samples per channel, 2048 with SBR). And advanced blend modes: the level sets SRC_OVER on one blend state, and the Porter-Duff modes now blend with the fixed-function factors they amount to ([03](agents/03-bringup.md), [04](agents/04-graphics.md)). `DM_PROBE_POLL` takes a value filter (`=`) for a word that changes often before the bad store ([05](agents/05-debugging.md)). -- **Graphics:** `render_cond on`: the game skips clears under render conditions, as Tears of the Kingdom and Animal Crossing: New Horizons do; without it every level was black behind its HUD. -- **Prewarm list:** recorded from a fresh save over the route above (550 lines); with it nothing was built on demand on that route. -- **Input:** the pad only (`main` imports no touch screen); the default 2D gamepad layout. The title asks for L+R. -- **Mac input:** the work folder's `run/boot.sh [secs] VAR=…` runs `build/konghost`, `run/ctl.sh` / `run/step.sh` drive a run started with `DM_CTL`. The opening movie plays at every boot and Plus skips it (about 18 s in); then L+R on the title, A for file 1 (level select) and A for level 1-1, which starts about 15 s later. In level 1-1 `LRight~0.3`, `LRight+A~0.25` hits the blue switch and `LRight~0.55`, `LRight+A~0.15` the red one. +Each game's notes are in its profile folder, `dm/games//README.md`: what the profile needs and why, what was found on the game, Mac routes and open items. The [games table](../dm/games/README.md#games) lists them. ## Tools (`dm/tools`)